Introduction
Greetings to you, habravchane. In this topic, I would like to highlight the details of C # development for heterogeneous target platforms, primarily such as .NET and browser (JavaScript). As an example, anyone can explore the
gfranq.com photo processing web service, which implements client and server photo processing using filters, as well as collage functionality based on the material described in this article.
Since I do not know how to select pictures to attract attention, it will be on the topic:

Content
purposeImplementationCode examplesConclusion')
purpose
So, the task was to implement the processing of photos by filters and creating collages on the client, and, if possible, on the server. To begin, I will tell you how we have presented filters and collages.
Filter Description
The filter in our project is represented by a sequence of actions (actions), prepared in Photoshop, applied to a specific photo. The action can be for example one of:
- Brightness change
- Contrast change
- Saturation change
- Correction of color curves
- Mask overlay with different modes
- Overlay frames
- ...
In order to describe all these actions a certain format is needed. Of course, there are standard formats such as JSON and XML, but it was decided to develop a custom format for the following reasons:
- Code universality for all platforms (.NET, JavaScript, WinPhone, etc.).
- The filter format is simple, not having a hierarchical structure, which makes it easy to write a parser for it.
- XML and JSON weigh more (for this case).
For example, this is what the sequence of actions for the
XPro Film filter looks like:

In addition to photo processing with a filter, it was also necessary to implement cropping and rotation of the image. Yes, I knew that jQuery plug-ins exist to implement rotation and trimming, but, first, they were too overloaded, and, second, they did not fit into the universal architecture of the project.
Collage Description
The collage is presented in the form of several miniature photos combined into one with or without a mask. At the same time, it was necessary to provide the ability to drag and drop available photos onto the collage and change their position and scale.
Collage may look like this:

The collage also uses its own simple format that stores a set of rectangles in relative coordinates from 0 to 1, the addresses of photos, as well as their transformations. Relative coordinates are used because on the server the same client transformations are applied to large photos.
Implementation
So, it was necessary to choose a platform on which the functionality of filters and collages worked for users.
Choosing a platform for photo processing
There are the following RIA technologies:
- Adobe flash
- Microsoft Silverlight
- HTML 5 + JavaScript
- Native client
For obvious reasons, only Flash and HTML 5 currently deserve attention from the entire list, since all the others are not cross-platform. And Silverlight is also slowly dying off. Although the concept of
salt NaCl I really like, but, alas, it exists only on Chrome, and it is not known when it will and will be supported by other popular browsers.
So, fashionable and developing HTML 5 was chosen as a platform, which potentially works also on iOS, unlike Flash. Also, this choice is justified by the fact that there are many libraries that allow you to convert C # to JavaScript, including in Visual Studio. This, however, will be discussed further.
C # to JavaScript
In the previous section, a development platform was chosen: HTML 5 + JavaScript. But the question arose, is it possible to write a universal C # code that could compile both under .NET and under JavaScript?
Thus, several libraries were found to accomplish the task:
As a result, it was decided to use
Script # due to the fact that JSIL works directly with assemblies and generates less pure code (although it supports more C # features), and SharpKit is commercial. A detailed comparison of such tools can be seen in the
JSIL vs Script # vs SharpKit question on stackoverflow .
In summary, I want to highlight the following advantages and disadvantages of using ScriptSharp compared with writing JavaScript manually:
Advantages:- The ability to write a universal code for .NET and other platforms (WP, Mono).
- Development in a strongly typed C # language with OOP capabilities.
- IDE development support (autocompletion, refactoring).
- Identify many errors at compile time.
Disadvantages:- Redundancy and non-standard generated javascript (due to mscorlib).
- Supports only ISO-2 specifications (no function overload, type inference, extensions, generics, and more).
Structure
Compiling under .NET and JavaScript of the same code can be represented in the form of the following scheme:

Despite the fact that .NET and HTML5 are completely different technologies, they have similar features. This also applies to working with graphics. For example, in .NET there is a
Bitmap , and in JavaScript, the equivalent is
canvas . Also with
Graphics and
Context , and pixel arrays. In order to combine all this in one code, it was decided to develop the following architecture:

Of course, it is not limited to two platforms. In the future we plan to add support for WP, and then, perhaps, Android and iOS.
It should be noted that there are two types of graphic operations:
- Using API functions (DrawImage, Arc, MoveTo, LineTo). The advantage is high speed and possible hardware acceleration. The disadvantage is that they can be implemented differently on different platforms.
- Pixel by pixel. The advantage is the ability to implement any effects and unified work on all platforms. The disadvantage is low speed. However, disadvantages can be leveled by parallelizing, using shaders and using pre-calculated tables (which will be discussed later in the section on optimization).
As you can see, in the abstract
Graphics class all methods for working with graphics are described, and in derived classes they are implemented for various platforms. In order to abstract from such classes as Bitmap and Canvas, the following
aliases were written. Also in the WP version being developed, the
adapter pattern is also used.
Use alias
#if SCRIPTSHARP using System.Html; using System.Html.Media.Graphics; using System.Runtime.CompilerServices; using Bitmap = System.Html.CanvasElement; using Graphics = System.Html.Media.Graphics.CanvasContext2D; using ImageData = System.Html.Media.Graphics.ImageData; using Image = System.Html.ImageElement; #elif DOTNET using System.Drawing; using System.Drawing.Imaging; using System.Drawing.Drawing2D; using Bitmap = System.Drawing.Bitmap; using Graphics = System.Drawing.Graphics; using ImageData = System.Drawing.Imaging.BitmapData; using Image = System.Drawing.Bitmap; #endif
However, in C #, unfortunately, it is impossible to make aliases for unsafe types and arrays, i.e. so (
Alias ​​to pointer (byte *) in C # ):
using PixelArray = byte*, using PixelArray = byte[]
And in order to be able to use unmanaged code in C # for fast pixel processing, while simultaneously compiling into Script #, the following scheme was introduced using directives:
#if SCRIPTSHARP PixelArray data = context.GetPixelArray(); #elif DOTNET byte* data = context.GetPixelArray(); #endif
Further, the data array is used for various pixel-by-pixel operations (such as masking, fish-eye, saturation, and others), parallelized and not.
File Links
For each platform, a separate project is added to the solution, but, of course, Mono, Script # and even Silverlight projects cannot refer to a regular .NET assembly. Fortunately, there is an add mechanism in Visual Studio
links to files, which allows you to reuse the same code in different projects.
An indication of compilation directives (DOTNET, SCRIPTSHARP, etc.) is added to the project properties in Conditional Compilation Symbols.
Notes about .NET implementation
Thanks to the above abstractions and aliases, C # code with a low level of redundancy was written. However, further I want to draw attention to the problems of the .NET and JavaScript platforms that we had to face when developing, but which were successfully solved.
Using Dispose
I would like to draw attention to the fact that for any instance of the C # class that implements the IDisposable interface, you should always call
Dispose after using it or using the using pattern. In this project, such classes were Bitmap and Context. This is not only my words and simple theory, but also practice: On an ASP.NET Developer Server x86 server, the processing of a large number of large photos (up to 2400 * 2400) led to the exception associated with memory. After placing Dispose in the right places, the problem disappeared. This and many other image processing tips are also written in article
20 Image Resizing Pitfalls and
.NET Memory Leak: To dispose or to dispose, that's the 1 GB question .
Use lock
In JavaScript, there is a separation between the already loaded picture with the
img tag, which can be set the source and the loading event, and between the canvas with the canvas tag, on which you can draw something. However, in .NET everything is represented by a single Bitmap class. Thus, the aliases Bitmap and Image in .NET indicate the same System.Drawing.Bitmap class as can be seen above.
Nevertheless, this separation in JavaScript on img and canvas helped a lot in the .NET version in the future. The fact is that pre-loaded masks are used for filters, which are used by different threads, and therefore you need to use the
lock pattern to avoid synchronization exclusion (the image is copied from lock, and then the result is used without blocking):
internal static Bitmap CloneImage(Image image) { #if SCRIPTSHARP Bitmap result = (Bitmap)Document.CreateElement("canvas"); result.Width = image.Width; result.Height = image.Height; Graphics context = (Graphics)result.GetContext(Rendering.Render2D); context.DrawImage(image, 0, 0); return result; #else Bitmap result; lock (image) result = new Bitmap(image); return result; #endif }
Do not forget that lock should also be used when accessing properties of a synchronized object (because any properties are in essence methods).
Storing masks in memory
When the server starts, all potentially used filter masks are loaded into memory to speed up processing. Do not forget that no matter what format the mask has, the loaded Bitmap on the server takes 4 * 2400 * 2400 ~ 24 MB (the maximum image size is 2400 * 2400, number of bytes per pixel is 4), which means all the masks for filters ( ~ 30 pieces) and collages (40 pieces) will occupy ~ 1.5 GB in memory, which in principle is not much for the server, but with an increase in the number of masks, this number can increase significantly. So in the future, it may be necessary to use compression of the masks in memory (in the form of .jpg, .png formats) and then unpacking them during use, especially considering that the size can be reduced by about 300 times. An additional advantage of this approach is that a compressed image will copy faster than a large one, which means that the
lock operation will take less time and the threads will be less blocked.
JavaScript implementation notes
Minification
I did not specifically use the word “obfuscation” in the title of this section, since this term is not very applicable to a language with all the time open source, which in this case is JavaScript. However, the logic and readability of the code can be confused by “depersonalizing” various identifiers (we will not talk now about the perverted methods shown
in one of the topics ). Well and most importantly, this technique will significantly reduce the size of the script (now in compressed form it weighs ~ 80 Kb).
There are two ways to minify JavaScript in our case:
- Manually . At the generation stage, using ScriptSharp.
- Automatically . After the generation stage. To do this, use external tools, such as Google Closure Compiler, Yui.
Manual minification
In order to shorten the names of methods, classes and attributes, such a syntactic construction was used immediately before the declarations of these entities. Naturally, for methods that are called from external scripts and classes (public), this of course is not necessary.
#if SCRIPTSHARP && !DEBUG [ScriptName("a0")] #endif
However, local variables still cannot be minimized. Also a disadvantage is the clogging of the code with such constructions and, as a result, deterioration of the code readability. However, this technique can significantly reduce and confuse the generated JavaScript code.
Another disadvantage is that such short names need to be monitored if they rename the names of methods (especially overloaded abstract in descendants) and fields, because in this case Script # will not swear at duplicate names. However, it will not allow duplicate classes.
By the way, in the developing version of Script # it would seem that the minification of private and internal methods and fields has already been added.
Minification automatically
There are many utilities for JavaScript minification, but I used Google Closure Compiler because of the brand, good compression quality. However, Google’s minifier’s disadvantage is that it cannot compress CSS files, but
YUI, for example, can. In fact, Script # also minifies scripts, but it makes it significantly worse than GCC.
It is worth noting that Google minifier has several levels of compression: Whitespace, Simple and Advanced. The Simple level was chosen in the project, because although with Advanced you can achieve maximum compression quality, you need to write code in a special way so that the methods and classes are accessible from the outside. Well, partly this minification was done manually with Script #. If you are interested in Google Closure minifiers, I recommend viewing
this list of Russian-language articles.
Debug & Release Modes
The connection of debug and release versions of libraries to ASP.NET pages was done as follows:
<% if (Gfranq.JavaScriptFilters.HtmlHelper.IsDebug) { %> <script src="Scripts/mscorlib.debug.js" ></script> <script src="Scripts/imgProcLib.debug.js" ></script> <% } else { %> <script src="Scripts/mscorlib.js" ></script> <script src="Scripts/imgProcLib.js" ></script> <% } %>
By the way, in our project not only scripts were minified, but also filter description files, too.
CrossOrigin property
In order to be able to access the pixels of an image, it must first be converted to canvas. However, this may cause a cross-domain access error (CORS). In our case, this problem was resolved as follows:
- Putting crossOrigin = '' on the client.
- Adding a special header to the http package on the server.
But since ScriptSharp does not support this property for img elements, the following code was written:
[Imported] internal class AdvImage { [IntrinsicProperty] internal string CrossOrigin { get { return string.Empty; } set { } } }
And then use it like this:
((AdvImage)(object)result).CrossOrigin = "";
It should be noted that this technique allows you to add any property to an object without a compilation error. In particular, the wheelDelta
property (at least in version 0.7.5), which displays the amount of rotation of the wheel (used in collages), has
not yet
been implemented in ScriptSharp. Therefore, it was implemented in a similar way. In fact, such a dirty hack with properties is bad, but for good you need to make forks into the project. But I honestly have not quite figured out how to compile ScriptSharp from source.
On the server for such images you need to return such headers (in Global.asax):
Response.AppendHeader("Access-Control-Allow-Origin", "*");
You can read more about cross-domain access to resources
here .
Optimization
Use of precomputed (tabular) values
For some operations, such as changing the brightness, contrast and color curves, optimization was applied, consisting in a preliminary calculation of the resulting color components (r, g, b) for all possible values, and then using the resulting arrays to directly change the colors of the pixels. However, it is worth noting that such optimization is only suitable for operations in which the neighboring pixels do not affect the color of the resulting pixel.
Calculation of color components for all possible values:
for (int i = 0; i < 256; i++) { r[i] = <actionFuncR>(i); g[i] = <actionFuncG>(i); b[i] = <actionFuncB>(i); }
Using pre-computed color components:
for (int i = 0; i < data.Length; i += 4) { data[i] = r[data[i]]; data[i + 1] = g[data[i + 1]]; data[i + 2] = b[data[i + 2]]; }
It is worth noting that if such table operations go in a row, then intermediate images can not be calculated at all, but only the arrays of the color components can be transferred. But due to the fact that the code worked on the client and the server quite quickly, so far it was decided not to implement such optimization. In addition, there were some other problems because of it. However, the listing of this optimization, I still give:
However, even this is not all. If you look at the right table, you will notice that new arrays are created there using Clone. In fact, the array can not be copied, but simply change the pointers to the old and new arrays (here we recall the analogy with
double buffering ).
Convert an image to an array of pixels
The JavaScript profiler in Google Chrome has revealed that the GetImageData function (which is used to convert canvas to an array of pixels) takes a long time, which, however, can be read in various articles on optimizing Canvas in JavaScript.
However, the number of calls to this function can also be minimized. Namely, to use the same array of pixels for pixel-by-pixel operations, by analogy with the previous optimization.
Code examples
Here I will describe examples of code that seemed interesting and useful to me. To prevent the article from getting too long, I enclosed them in spoilers.
Are common
Determining whether a string is a number internal static bool IsNumeric(string n) { #if !SCRIPTSHARP return ((Number)int.Parse(n)).ToString() != "NaN"; #else double number; return double.TryParse(n, out number); #endif }
Integer division internal static int Div(int n, int k) { int result = n / k; #if SCRIPTSHARP result = Math.Floor(n / k); #endif return result; }
Rotate and reverse images on canvas and bitmap respectivelyPlease note that in html5 canvas there are no functions to rotate the image by 90, 180 degrees, except using matrices, but in .NET there are. So the corresponding exact pixel function was written.
It is also worth noting that in the .NET version, turning 90 degrees in any direction can lead to incorrect results. Therefore, after using the RotateFlip function in these cases, you need to create a new Bitmap.
public static Bitmap RotateFlip(Bitmap bitmap, RotFlipType rotFlipType) { #if SCRIPTSHARP int t, i4, j4, w, h, c; if (rotFlipType == RotFlipType.RotateNoneFlipNone) return bitmap; GraphicsContext context; PixelArray data; if (rotFlipType == RotFlipType.RotateNoneFlipX) { context = GraphicsContext.GetContext(bitmap); data = context.GetPixelArray(); w = bitmap.Width; h = bitmap.Height; for (int i = 0; i < h; i++) { c = (i + 1) * w * 4 - 4; for (int j = 0; j < w / 2; j++) { i4 = (i * w + j) * 4; j4 = j * 4; t = (int)data[i4]; data[i4] = data[c - j4]; data[c - j4] = t; t = (int)data[i4 + 1]; data[i4 + 1] = data[c - j4 + 1]; data[c - j4 + 1] = t; t = (int)data[i4 + 2]; data[i4 + 2] = data[c - j4 + 2]; data[c - j4 + 2] = t; t = (int)data[i4 + 3]; data[i4 + 3] = data[c - j4 + 3]; data[c - j4 + 3] = t; } } context.PutImageData(); } else if (rotFlipType == RotFlipType.Rotate180FlipNone || rotFlipType == RotFlipType.Rotate180FlipX) { context = GraphicsContext.GetContext(bitmap); data = context.GetPixelArray(); w = bitmap.Width; h = bitmap.Height; c = w * 4 - 4; int dlength4 = data.Length - 4; for (int i = 0; i < data.Length / 4 / 2; i++) { i4 = i * 4; if (rotFlipType == RotFlipType.Rotate180FlipNone) j4 = i4; else j4 = (Math.Truncate((double)i / w) * w + (w - i % w)) * 4; t = (int)data[j4]; data[j4] = data[dlength4 - i4]; data[dlength4 - i4] = t; t = (int)data[j4 + 1]; data[j4 + 1] = data[dlength4 - i4 + 1]; data[dlength4 - i4 + 1] = t; t = (int)data[j4 + 2]; data[j4 + 2] = data[dlength4 - i4 + 2]; data[dlength4 - i4 + 2] = t; t = (int)data[j4 + 3]; data[j4 + 3] = data[dlength4 - i4 + 3]; data[dlength4 - i4 + 3] = t; } context.PutImageData(); } else { Bitmap tempBitmap = PrivateUtils.CreateCloneBitmap(bitmap); GraphicsContext tempContext = GraphicsContext.GetContext(tempBitmap); PixelArray temp = tempContext.GetPixelArray(); t = bitmap.Width; bitmap.Width = bitmap.Height; bitmap.Height = t; context = GraphicsContext.GetContext(bitmap); data = context.GetPixelArray(); w = tempBitmap.Width; h = tempBitmap.Height; if (rotFlipType == RotFlipType.Rotate90FlipNone || rotFlipType == RotFlipType.Rotate90FlipX) { c = w * h - w; for (int i = 0; i < temp.Length / 4; i++) { t = Math.Truncate((double)i / h); if (rotFlipType == RotFlipType.Rotate90FlipNone) i4 = i * 4; else i4 = (t * h + (h - i % h)) * 4; j4 = (c - w * (i % h) + t) * 4; //j4 = (w * (h - 1 - i4 % h) + i4 / h) * 4; data[i4] = temp[j4]; data[i4 + 1] = temp[j4 + 1]; data[i4 + 2] = temp[j4 + 2]; data[i4 + 3] = temp[j4 + 3]; } } else if (rotFlipType == RotFlipType.Rotate270FlipNone || rotFlipType == RotFlipType.Rotate270FlipX) { c = w - 1; for (int i = 0; i < temp.Length / 4; i++) { t = Math.Truncate((double)i / h); if (rotFlipType == RotFlipType.Rotate270FlipNone) i4 = i * 4; else i4 = (t * h + (h - i % h)) * 4; j4 = (c + w * (i % h) - t) * 4; // j4 = w * (1 + i4 % h) - i4 / h - 1; data[i4] = temp[j4]; data[i4 + 1] = temp[j4 + 1]; data[i4 + 2] = temp[j4 + 2]; data[i4 + 3] = temp[j4 + 3]; } } context.PutImageData(); } return bitmap; #elif DOTNET Bitmap result = null; switch (rotFlipType) { case RotFlipType.RotateNoneFlipNone: result = bitmap; break; case RotFlipType.Rotate90FlipNone: bitmap.RotateFlip(RotateFlipType.Rotate90FlipNone); result = new Image(bitmap); bitmap.Dispose(); break; case RotFlipType.Rotate270FlipNone: bitmap.RotateFlip(RotateFlipType.Rotate270FlipNone); result = new Image(bitmap); bitmap.Dispose(); break; case RotFlipType.Rotate180FlipNone: bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone); result = bitmap; break; case RotFlipType.RotateNoneFlipX: bitmap.RotateFlip(RotateFlipType.RotateNoneFlipX); result = bitmap; break; case RotFlipType.Rotate90FlipX: bitmap.RotateFlip(RotateFlipType.Rotate90FlipX); result = new Image(bitmap); bitmap.Dispose(); break; case RotFlipType.Rotate180FlipX: bitmap.RotateFlip(RotateFlipType.Rotate180FlipX); result = bitmap; break; case RotFlipType.Rotate270FlipX: bitmap.RotateFlip(RotateFlipType.Rotate270FlipX); result = new Image(bitmap); bitmap.Dispose(); break; } return result; #endif }
Asynchronous and synchronous loading imagesPlease note that in the ScriptSharp version, another CollageImageLoad function is specified, which will be called after the image is loaded, while in the .NET version everything happens synchronously (from the file system or the Internet).
public CollageData(string smallMaskPath, string bigMaskPath, List<CollageDataPart> dataParts) { SmallMaskImagePath = smallMaskPath; BigMaskImagePath = bigMaskPath; #if SCRIPTSHARP CurrentMask = PrivateUtils.CreateEmptyImage(); CurrentMask.AddEventListener("load", CollageImageLoad, false); CurrentMask.Src = CurrentMaskImagePath; #else CurrentMask = PrivateUtils.LoadBitmap(CurrentMaskImagePath); if (!CurrentMaskImagePath.Contains("http://") && !CurrentMaskImagePath.Contains("https://")) CurrentMask = Bitmap(CurrentMaskImagePath); else { var request = WebRequest.Create(CurrentMaskImagePath); using (var response = request.GetResponse()) using (var stream = response.GetResponseStream()) CurrentMask = (Bitmap)Bitmap.FromStream(stream); } #endif DataParts = dataParts; }
Only Script #
Determining the type and version of the browserThis function is used, for example, to determine the drag & drop capabilities in various browsers (I tried to use
modernizr , but it returned that Safari (in my case for Win) and IE9 implement it. However, in practice, it turned out that these browsers do not completely implement drag & drop right).
internal static string BrowserVersion { get { DetectBrowserTypeAndVersion(); return _browserVersion; } } private static void DetectBrowserTypeAndVersion() { if (!_browserDetected) { string userAgent = Window.Navigator.UserAgent.ToLowerCase(); if (userAgent.IndexOf("opera") != -1) _browser = BrowserType.Opera; else if (userAgent.IndexOf("chrome") != -1) _browser = BrowserType.Chrome; else if (userAgent.IndexOf("safari") != -1) _browser = BrowserType.Safari; else if (userAgent.IndexOf("firefox") != -1) _browser = BrowserType.Firefox; else if (userAgent.IndexOf("msie") != -1) { int numberIndex = userAgent.IndexOf("msie") + 5; _browser = BrowserType.IE; _browserVersion = userAgent.Substring(numberIndex, userAgent.IndexOf(';', numberIndex)); } else _browser = BrowserType.Unknown; _browserDetected = true; } }
Dashed line drawingUsed for a rectangle when cropping images. For ideas thanks to everyone who answered in this
question on SO .
internal static void DrawDahsedLine(GraphicsContext context, double x1, double y1, double x2, double y2, int[] dashArray) { if (dashArray == null) dashArray = new int[2] { 10, 5 }; int dashCount = dashArray.Length; double dx = x2 - x1; double dy = y2 - y1; bool xSlope = Math.Abs(dx) > Math.Abs(dy); double slope = xSlope ? dy / dx : dx / dy; context.MoveTo(x1, y1); double distRemaining = Math.Sqrt(dx * dx + dy * dy); int dashIndex = 0; while (distRemaining >= 0.1) { int dashLength = (int)Math.Min(distRemaining, dashArray[dashIndex % dashCount]); double step = Math.Sqrt(dashLength * dashLength / (1 + slope * slope)); if (xSlope) { if (dx < 0) step = -step; x1 += step; y1 += slope * step; } else { if (dy < 0) step = -step; x1 += slope * step; y1 += step; } if (dashIndex % 2 == 0) context.LineTo(x1, y1); else context.MoveTo(x1, y1); distRemaining -= dashLength; dashIndex++; } }
Image rotation animationTo animate the rotation of the image, use the setInterval function. Notice that the result result is rendered during the animation so that there are no small lags at the end of the animation.
public void Rotate(bool cw) { if (!_rotating && !_flipping) { _rotating = true; _cw = cw; RotFlipType oldRotFlipType = _curRotFlipType; _curRotFlipType = RotateRotFlipValue(_curRotFlipType, _cw); int currentStep = 0; int stepCount = (int)(RotateFlipTimeSeconds * 1000 / StepTimeTicks); Bitmap result = null; _interval = Window.SetInterval(delegate() { if (currentStep < stepCount) { double absAngle = GetAngle(oldRotFlipType) + currentStep / stepCount * Math.PI / 2 * (_cw ? -1 : 1); DrawRotated(absAngle); currentStep++; } else { Window.ClearInterval(_interval); if (result != null) Draw(result); _rotating = false; } }, StepTimeTicks); result = GetCurrentTransformResult(); if (!_rotating) Draw(result); } } private void DrawRotated(double rotAngle) { _resultContext.FillColor = FillColor; _resultContext.FillRect(0, 0, _result.Width, _result.Height); _resultContext.Save(); _resultContext._graphics.Translate(_result.Width / 2, _result.Height / 2); _resultContext._graphics.Rotate(-rotAngle); _resultContext._graphics.Translate(-_origin.Width / 2, -_origin.Height / 2); _resultContext._graphics.DrawImage(_origin, 0, 0); _resultContext.Restore(); } private void Draw(Bitmap bitmap) { _resultContext.FillColor = FillColor; _resultContext.FillRect(0, 0, _result.Width, _result.Height); _resultContext.Draw2(bitmap, (int)((_result.Width - bitmap.Width) / 2), (int)((_result.Height - bitmap.Height) / 2)); }
Conclusion
This article showed how large cross-platform C # can have, combining unmanaged code on one side and compiling for JavaScript on the other. Despite the fact that the main emphasis was placed on .NET and JavaScript, compilation for Android, iOS (using Mono) and Windows Phone is also possible based on the described approach, naturally with its pitfalls. , , , .
, , . .
, , google ASP.NET MS SQL , . :
- MS SQL ( TSQL, C#, ).
- .
- Google maps geocoding api ( , ).
PS
gfranq.com oneuser VladaOrlova .
UPDATEJavaScript, Script# :
imgProcLibmscorlib ( ):
mscorlib, , , mscorlib.
UPDATE 2