Draw a water polo with canvas

Keywords: Front-end github angular Webpack

The sample code is hosted in: http://www.github.com/dashnowords/blogs

Blog Garden Address: The original bibliography of Dashi Living in the Big Front End

Huawei Cloud Community Address: [Guidelines for Upgrading Your Front-end Crackers]

[TOC]

I. Mission Statement

Using the native canvas API to draw a water polo map will be an interesting challenge. Water globe is a common loading animation, which belongs to extended graphics. When used in echarts, you need to download the extended library (also included text cloud plug-in and map plug-in, the project address is as follows: https://github.com/ecomfe/echarts-liquidfill).

II. Key Tips

There are several difficulties in drawing a water polo chart:

  1. Drawing of Water Wave

    In fact, the drawing of water waves is simulated by the simple harmonic oscillation formula, which is x = A*(wt + phi). Amplitude A determines the ripple height of water waves, angular frequency w determines the speed of water waves, phase Phi determines the initial displacement difference, plus some y-axis displacement deviation and color difference, different water waves can be simulated. Then, only changing Phi constantly in frame animation is needed. By redrawing the curve, we can simulate the effect of the water wave.

  2. Spherical clipping area

    The scope of the water wave can not flow out of the spherical outline. The method here is to use context.clip() method to control the visible drawing area of the water wave in the water ball before drawing the water wave. If there are other figures outside the water ball that need to be drawn, remember to call context.restore() after drawing the water wave in each frame to cancel the previous clipping.

  3. Drawing of Text

    It's easier to just draw words floating on a water polo chart, but it's not so easy to achieve more detailed effects. What we expect to achieve is that when the text is not immersed in the water waves, it will show the blue of the water marks, and the part that is immersed in the water will show white, which will make it more vivid. But it is not easy to draw. If the text is painted blue, the submerged part will disappear in the watermarks. If the watermarks are painted white, the text will be completely invisible when the height of the watermarks is small. So how to achieve such rendering text?

3. Sample code

let options = {
    value:0,
    a:20,//amplitude
    pos:[300,300],//Location of water polo chart
    r:160,//Radius of spherical chart
    color:['#2E5199','#1567c8','#1593E7','#42B8F9'//Watermark color
};

start(options);

/**
 * Draw a water polo chart
 */
function start(options) {
    //Move the drawing coordinates to the left boundary point of the water polo chart
    context.translate(options.pos[0],options.pos[1]);
    context.font = 'bold 60px Arial';
    context.textAlign='center';
    context.textBaseLine = 'baseline';
    //Calculating the Drawing Data of Water Ball Chart
    createParams(options);
    //Open frame animation
    requestAnimationFrame(startAnim);
}

//Generated water wave drawing parameters, position coordinate formula is y = A* (wt + phi)
function createParams(options) {
    options.w = [];//Angular velocity of stored water wave
    options.theta = [];//Storage of displacement of each water wave
    for(let i = 0; i < 4; i++){
      options.w.push(Math.PI /(100 + 20*Math.random()));
      options.theta.push(20*Math.random());
    }
}

//Drawing water wave lines
function drawWaterLines(options) {
   let offset;
   let A = options.a;//Sinusoidal Curve Amplitude
   let y,x,w,theta;
   let r = options.r;
   //Traverse each water texture
   for(let line = 0; line < 4; line++){ 
     context.save();
     //The offset distance of water wave in each drawing
     theta = Math.random();
     offset = r + A / 2  -  (r*19/8 + A) * (options.value / 100 ) + line * r/12;
     //Obtaining the Calculating Parameters of Sinusoidal Curve
     w = options.w[line];
     theta = options.theta[line];
     context.fillStyle = options.color[line];
     context.moveTo(0,0);
     context.beginPath(); 
     //Drawing Sinusoidal Curve with 0.1 Step Sinusoidal Curve
     for(x = 0; x <= 2*r; x+=0.1){
        y = A * Math.sin(w * x + theta) + offset;
        //Drawing Points
        context.lineTo(x,y);
     }
      //Draw a closed figure beyond the sphere of water
      context.lineTo(x,r);
      context.lineTo(x - 2 * r,r);
      context.lineTo(0, A * Math.sin(theta) - options.height);
      context.closePath();
      //Fill in the closed figure to get a water wave
      context.fill();
      //Intercept the water wave range and draw text (explained later here)
      context.clip();
      context.fillStyle = 'white';
      context.fillText(parseInt(options.value,10) + '%',options.r + 10,10);
      context.restore();
   }
}

//Drawing the lowest text
function drawText1(options) {
    context.fillStyle = options.color[0];
    context.fillText(parseInt(options.value,10) + '%',options.r + 10,10);
}

//Frame animation loop
function startAnim() {
    //Simulating Water Wave by Displacement Change
    options.theta = options.theta.map(item=>item-0.03);
    //Calculating the Height of Water Wave with Percentage Progress
    options.value += options.value > 100 ? 0:0.1;
    context.save();
    resetClip(options);//Shear plotting area
    drawText1(options);//Draw blue text
    drawWaterLines(options);//Drawing water wave lines
    context.restore();
    requestAnimationFrame(startAnim);
}

/**Set the sphere to the clipping area
*(In this case, there is no part other than water polo that needs to be drawn. In fact, there is no need to add a frame animation loop here, just need to set it at the beginning once. )
*/
function resetClip(options) {
   let r = options.r;
   context.strokeStyle = '#2E5199';
   context.fillStyle = 'white';
   context.lineWidth = 10;
   context.beginPath();
   context.arc(r, 0, r + 10, 0, 2*Math.PI, false);
   context.closePath();
   context.fill();
   context.stroke();
   context.beginPath();
   context.arc(r, 0, r, 0, 2*Math.PI, true);
   context.clip();
}

Viewable effects in browsers:

IV. Realization of the Effect of Writing Flooding

In fact, the drawing of text flooding effect is carried out according to the following ideas:

  1. Firstly, the text is drawn with the same color as the top watermarking, so that the text can be displayed in visible color before it is submerged.
  2. In the process of drawing water waves, context.clip() method is used to cut the drawing area into all the immersed parts after the connection is completed. At this time, the fill color is set to white, and then the text is rendered in the same place, so that the rendered white text does not exceed the range of the water marks, so the blue part of the text outside the water marks is saved on the canvas.
  3. In order to avoid truncating the white part of the text when it is drawn by the next layer of water marks, we need to repeat step 2 after each layer of water marks is drawn. Set all ranges of the layer of water marks to the bottom of the water ball as the cutting area, and then draw the white part of the text within the layer of water marks, so that when several layers of water marks are drawn, the waterlogged part of the text will be dyed white.
  4. In such rendering method, the final effect of text is equivalent to splicing the fragments drawn layer by layer. In each rendering process, the last part can be saved, only the part intersecting with the current layer's watermarks.

If we modify the rendering color of each layer of text, it will be easier to understand the rendering process.

V. About canvas Anti-aliasing

If you look closely at the outer circle of the water polo above, you will find that the outside of the water polo chart is not very flat, and it looks like there will be a lot of jagged teeth. Most of the methods found on the Internet are to adjust canvas size (canvas.height,canvas.width) to 3-4 times of the element size (the size of canvas elements set in CSS). We hope to use scaling to achieve the anti-aliasing effect, but the measured results have not been significantly improved. Using canvas size to zoom in to solve the problem of blurring image and filling is better, but in the anti-aliasing aspect. The effect seems to be related to the size of the line itself, not an absolutely effective solution. Another effective scheme is to add 2px-4px dark shadow when drawing the outer circle, which can weaken the sense of sawtooth visually.

//Add the following code before drawing the outer circle   
   context.shadowColor = '#2E5199';
   context.shadowBlur = 2;
   context.shadowOffsetX = 0;
   context.shadowOffsetY = 2;

VI. Summary

So far, we have completed the original API drawing of all the basic charts in this series. Some relatively advanced charts are not necessarily very complicated. For example, rectangular tree charts are actually rectangular squares, but they can help us to observe data in a more intuitive and expressive way, such as visualizing the packaged results of webpack. The basic task of data visualization is to make data visible, which requires us to choose the right way of expression for the data we want to observe. This is not only achieved by technology, but also requires some artistic cells and imagination. But in any case, this is an interesting direction worth studying.

Posted by kevinfwb on Wed, 19 Jun 2019 12:53:04 -0700