📜 ⬆️ ⬇️

Universal C # code under .NET and JavaScript

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


purpose
Implementation
Code examples
Conclusion
')

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:


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:


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:

image

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:

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:

Disadvantages:


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:


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:


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:

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:
Plain codeOptimized code
 //   . for (int i = 0; i < 256; i++) { r[i] = <actionFunc1R>(i); g[i] = <actionFunc1G>(i); b[i] = <actionFunc1B>(i); } … //    . 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]]; } … //   . for (int i = 0; i < 256; i++) { r[i] = <actionFunc2R>(i); g[i] = <actionFunc2G>(i); b[i] = <actionFunc2B>(i); } … //   . 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]]; } … 

 //   . for (int i = 0; i < 256; i++) { r[i] = <actionFunc1R>(i); g[i] = <actionFunc1G>(i); b[i] = <actionFunc1B>(i); } … //   . tr = r.Clone(); tg = g.Clone(); tb = b.Clone(); for (int i = 0; i < 256; i++) { r[i] = tr[<actionFunc2R>(i)]; g[i] = tg[<actionFunc2G>(i)]; b[i] = tb[<actionFunc2B>(i)]; } … //   . 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]]; } … 


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 respectively
Please 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 images
Please 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 browser
This 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 drawing
Used 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 animation
To 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 , . :


PS gfranq.com oneuser VladaOrlova .

UPDATE

JavaScript, Script# :
imgProcLib

mscorlib ( ):
mscorlib

, , , mscorlib.

UPDATE 2

Source: https://habr.com/ru/post/164439/


All Articles