I tried to draw animation with Blazor + canvas API

Introduction

I made a simple drawing web application that can draw animation for learning such as Blazor. I'm just talking about using Blazor, and I write TypeScript more often than C #.

Demo(Firebase) Source(GitHub)

blazorcanvas2.gif I wish I could draw an anime like a mysterious Ani-dan, but for me, blinking is the limit.

Currently it is only 8fps (so-called three-frame striking), but eventually we will be able to change the frame rate. It's easy to just change the rate, but since 24 images (24fps: general frame rate of Japanese animation) are not usually drawn for 1 second of animation playback, it is necessary to deal with the empty frame.

environment

Blazor WebAssembly 3.2.1 + .NET Standard 2.1 Microsoft.TypeScript.MSBuild 4.0.3 Firebase

Others, such as Bootstrap included in the Blazor template.

About implementation

While saying Blazor, all that Blazor does is control the UI.

How to draw a line

When I was looking for an implementation method to draw a line on Canvas, there were many examples of using the lineTo () method, but when I moved the pen quickly, it sometimes became a strange drawing method, so the pen's The circle is placed according to the orbit. However, if you try to draw quickly as it is, the line will be cut, so I try to fill the gap with a small amount of change as shown below. However, now that it is a linear complement, I would like to consider a more curved shape. I think I need to review math.

    private prevX :number;
    private prevY :number;
    public drawLineWithPen(x:number, y:number, isDrawing:boolean){
        let scaledX = x/this.scaleRate;
        let scaledY = y/this.scaleRate;
        if(!isDrawing) {
            this.context.beginPath();
            this.context.moveTo(scaledX,scaledY);
        } else {
            //Division number
            let div = 200;
            let dx = (scaledX - this.prevX) / div;
            let dy = (scaledY - this.prevY) / div;
            let r = this.context.lineWidth/2;
            for(let i = 0; i<=div; i++){
                let x = this.prevX + dx*i;
                let y = this.prevY + dy*i;
                this.context.beginPath();
                this.context.moveTo(x,y);
                this.context.arc(x,y, r, 0, 2 * Math.PI, false);
                this.context.stroke();
                this.context.fill();
                this.context.closePath();
            } 
        }
        this.prevX = scaledX;
        this.prevY = scaledY;
    }

Regarding the insertion and removal of the pen, I am thinking of changing the size and transparency of the circle using the time difference as a parameter.

How to display onion skins

Onion skin is a function to display the frames before and after the frame being edited in a specific color. This time, the rear frame is displayed in pink and the front frame is displayed in light blue.

I make a copy of the ImageData array that I have saved in advance, replace the colors, and set the transparency. After that, after creating the Bitmap data, it is written to the canvas for displaying the onion skin using the method of drawImage (). I wish I could write directly to the canvas with ʻImageData, but putImageData () `replaces everything in the canvas, so it looks like this.

private setOnionSkinsInternal(start:number, end:number, color:Color, frames: ImageData[], isPrev:boolean){
    let startNum = Math.max(start, 0);
    let endNum = Math.min(end,frames.length);
    for(let i= startNum; i< endNum; i++) {
        let imageData = new ImageData(frames[i].data.slice(),frames[i].width,frames[i].height);
        for(let j=0;j<imageData.data.length; j+=4) {
            imageData.data[j] = color.r;
            imageData.data[j+1] = color.g;
            imageData.data[j+2] = color.b;
            //Frames far from the current frame have stronger transparency and lighter display
            if(isPrev)
                imageData.data[j+3] = imageData.data[j+3] * (i+1) / (endNum+1);
            else
                imageData.data[j+3] = imageData.data[j+3] * (startNum+1) / (i+1);          
        }
        window.createImageBitmap(imageData).then(
            (img) => {
                // scale()The image is further reduced by the magnification set in, so divide by the magnification?
                this.context.drawImage(img,0,0,this.width/this.scaleRate,this.height/this.scaleRate);
            }
        ).catch(() => {
            console.log(`${i} Error`)});
    }
}

There are 921,600 or more loops per sheet, so if you try to increase the size and display about 10 sheets, it will be quite heavy. I want to think of a better way. I thought about adding canvas for the number of onion skins, but I haven't tried it.

Eraser and layer composition

https://hai3.net/blog/html5-canvas-eraser/ I referred to this method. Thank you very much.

CanvasRenderingContext2D.globalCompositeOperation = "destination-out"; When drawing with an eraser, it was OK if I set this.

https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation As far as I read MDN, I feel that layer composition and the watercolor-likeness of the pen tip can be achieved using this.

Currently, the only way to switch from an eraser to a pen is to switch colors or change the pen tip size, so I would like to create a radio button-like switching method.

undo/redo Just have ʻImageData []` for saving history and pop or push each time the undo / redo method is called. I thought about saving the history on the TypeScript side, but I want to control it on the UI side, so I leave the timing to call it to the Blazor side. Currently, ~~ is easy to implement ~~ It has a history for each frame, but it seems that the way of holding the history will change at the timing of implementing frame deletion.

public undo(){
    if(this.currentFrame.prevHistory.length > 0) {
        let prev = this.currentFrame.prevHistory.pop() ?? new ImageData(this.width,this.height) ;
        this.currentFrame.nextHistory.push(this.getImageData());
        this.putImageData(prev);      
    }
}

public redo(){
    if(this.currentFrame.nextHistory.length > 0){
        let next = this.currentFrame.nextHistory.pop() ?? new ImageData(this.width,this.height);
        this.currentFrame.prevHistory.push(this.getImageData());
        this.putImageData(next);
    }
}

public saveHistory(){
    this.currentFrame.prevHistory.push(this.getImageData());
    //When a new history is added, the forward history is deleted.
    this.currentFrame.nextHistory = [];
}

Canvas element priority

<canvas id="1"></canvas>
<canvas id="2"></canvas>
<canvas id="3"></canvas>

If the event is picked up by ʻid = "1" `, you must bring it to the bottom to pick up the event.

TypeScript + dotnet CLI The .ts file was also compiled with the C # code when I did a dotnet run with the dotnet add package Microsoft.TypeScript.MSBuild command and the json file configuration, which was very easy. I thought about writing in F # using Fable, but I gave up because it seems to be strict due to the specification of JavaScript interoperability where the method name must be specified from the method name generation method.

Features to be added in the future

I wrote in the README on GitHub that it was a mess, but since the problem of drawing comfort is big, I want to solve the problem that the lines are stiff or the entry and exit are all-out. Also, if you use the Web Storage API, it seems that you can save the setting values of color and pen size, so I would like to support that as well.

Support for tablets and smartphones will be a long time later.

about me

Neat seems to have been involved in system production using Windows Form applications. I have only a little understanding of C # and C, and I have no experience with TypeScript.

Intention to make

I can't easily draw and move animations with certain drawing software, it's difficult to use even if I make animations, there is no screen effect, and I think that there seems to be no drawing (Web) application that unexpectedly turned to this direction. From. Also, there is no database operation or login function, but apart from issues such as opportunity and quality, it is for showing to someone somewhere.

Originally, I was trying the Canvas API and didn't think about the animation function, but as I was making it, I wanted to add the function, and now I am.

in conclusion

There are still many features I want to implement and points I want to improve, so I'd like to continue as long as my motivation continues. TypeScript was developed by Microsoft, and it was a lot easier because I could see some errors on the TypeScript side at compile time because there was a part similar to C # and there was a combination with dotnet CLI. Blazor can also touch the API of the browser by itself, but since it can only be done via IJS Runtime, it seems to be strict to use only Blazor (C #).

Recommended Posts

I tried to draw animation with Blazor + canvas API
I tried to interact with Java
I tried to link chat with Minecraft server with Discord API
I tried to get started with WebAssembly
I tried to summarize the Stream API
I tried to implement ModanShogi with Kinx
I tried to make a Web API that connects to DB with Quarkus
I tried to verify AdoptOpenJDK 11 (11.0.2) with Docker image
I tried to make Basic authentication with Java
I tried to manage login information with JMX
I tried to break a block with java (1)
I tried what I wanted to try with Stream softly.
I tried to implement file upload with Spring MVC
I tried to read and output CSV with Outsystems
I tried to implement TCP / IP + BIO with JAVA
[Java 11] I tried to execute Java without compiling with javac
I tried to get started with Spring Data JPA
I tried to implement Stalin sort with Java Collector
[Java] I tried to implement Yahoo API product search
roman numerals (I tried to simplify it with hash)
I tried to check the operation of http request (Put) with Talented API Tester
I tried DI with Ruby
I tried UPSERT with PostgreSQL.
I tried BIND with Docker
I tried to verify yum-cron
I tried to make an introduction to PHP + MySQL with Docker
I tried to create a java8 development environment with Chocolatey
I tried to modernize a Java EE application with OpenShift.
I tried to increase the processing speed with spiritual engineering
[Rails] I tried to create a mini app with FullCalendar
I tried to make Venn diagram an easy-to-understand GIF animation
[Rails] I tried to implement batch processing with Rake task
What I was addicted to with the Redmine REST API
I tried to automate LibreOffice Calc with Ruby + PyCall.rb (Ubuntu 18.04)
I tried to create a padrino development environment with Docker
I tried to get started with Swagger using Spring Boot
I tried upgrading from CentOS 6.5 to CentOS 7 with the upgrade tool
I tried to be able to pass multiple objects with Ractor
I tried to build an API server with Go (Echo) x MySQL x Docker x Clean Architecture
I tried to chew C # (indexer)
I tried using JOOQ with Gradle
I tried to build the environment of PlantUML Server with Docker
I tried connecting to MySQL using JDBC Template with Spring MVC
I tried to summarize iOS 14 support
I tried to implement the image preview function with Rails / jQuery
I tried to build an http2 development environment with Eclipse + Tomcat
I tried to implement flexible OR mapping with MyBatis Dynamic SQL
I tried UDP communication with Java
I tried to explain the method
I tried to reimplement Ruby Float (arg, exception: true) with builtin
I tried using Java8 Stream API
I tried to make an Android application with MVC now (Java)
I tried to check the operation of gRPC server with grpcurl
I tried GraphQL with Spring Boot
I tried to summarize Java learning (1)
I tried to understand nil guard
I tried Flyway with Spring Boot
I tried to summarize Java 8 now
I tried to chew C # (polymorphism: polymorphism)
I tried customizing slim with Scaffold
I tried to explain Active Hash