最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - HTML5 Canvas How to draw squircle with gradient border? - Stack Overflow

programmeradmin1浏览0评论

After I googling a lot, I cannot find any tutorials which answering how to draw squircle shape in HTML5 canvas, please forgive me as I am very poor on math.

However I do find some similar / related answers, but I don't know how to bine these knowledges...

HTML5 Canvas alpha transparency doesn't work in firefox for curves when window is big

Continuous gradient along a HTML5 canvas path

The effect I try to achieve:

Thanks for any help!

UPDATE 1:

Code so far I created:

<body>
  <div class="con">
    <div class="ava"></div>
    <canvas id="canvas"></canvas>
  </div>
  <script>

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");

    var shadowPadding = 8;
    var strokeWidth = 2;
    canvas.width = canvas.height = (64 + shadowPadding * 2) * window.devicePixelRatio
    canvas.style.width = canvas.style.height = `${canvas.width / window.devicePixelRatio}px`

    function drawMultiRadiantCircle(xc, yc, r, radientColors) {
        var partLength = (2 * Math.PI) / radientColors.length;
        var start = 0;
        var gradient = null;
        var startColor = null,
            endColor = null;

        for (var i = 0; i < radientColors.length; i++) {
            startColor = radientColors[i];
            endColor = radientColors[(i + 1) % radientColors.length];

            // x start / end of the next arc to draw
            var xStart = xc + Math.cos(start) * r;
            var xEnd = xc + Math.cos(start + partLength) * r;
            // y start / end of the next arc to draw
            var yStart = yc + Math.sin(start) * r;
            var yEnd = yc + Math.sin(start + partLength) * r;

            ctx.beginPath();

            gradient = ctx.createLinearGradient(xStart, yStart, xEnd, yEnd);
            gradient.addColorStop(0, startColor);
            gradient.addColorStop(1, endColor);

            ctx.lineWidth = strokeWidth;
            ctx.strokeStyle = gradient;

            // squircle START
            // 
            // //Formula: (|x|)^3 + (|y|)^3 = radius^3
            // ctx.moveTo(-r, 0);
            // const radiusToPow = r ** 3;
            // const rad = r
            // for (let x = -rad ; x <= rad ; x++)
            //   ctx.lineTo(x + r, Math.cbrt(radiusToPow - Math.abs(x ** 3)) + r);
            // for (let x = rad ; x >= -rad ; x--)
            //   ctx.lineTo(x + r, -Math.cbrt(radiusToPow - Math.abs(x ** 3)) + r);
            // ctx.translate(r, r)
            // ctx.restore()
            // squircle END

            // circle START
            // 
            ctx.arc(xc, yc, r, start, start + partLength);
            // circle END
            if (i === 1) {
              break
            }
            ctx.stroke();
            ctx.closePath();

            start += partLength;
        }
    }

    var someColors = [];
    someColors.push('#0F0');
    someColors.push('#0FF');
    someColors.push('#F00');
    someColors.push('#FF0');
    someColors.push('#F0F');

    var mid = canvas.width / 2;
    var r = (canvas.width - (shadowPadding * 2)) / 2 + (strokeWidth / 2)
    drawMultiRadiantCircle(mid, mid, r, someColors);

  </script>
  <style>
  .con {
    align-items: center;
    justify-content: center;
    display: flex;
    height: 4rem;
    margin: 6rem;
    width: 4rem;
    position: relative;
  }
  .ava {
    background: #555 50% no-repeat;
    background-size: contain;
    border-radius: 24px;
    height: 100%;
    width: 100%;
  }
  canvas {
    height: 100%;
    width: 100%;
    position: absolute;
  }
  </style>
</body>

drawing portion of circle with gradient color:

drawing a squircle:

I don't know how to code a algorithm that draws a portion of squircle just like what context.arc does.

After I googling a lot, I cannot find any tutorials which answering how to draw squircle shape in HTML5 canvas, please forgive me as I am very poor on math.

However I do find some similar / related answers, but I don't know how to bine these knowledges...

HTML5 Canvas alpha transparency doesn't work in firefox for curves when window is big

Continuous gradient along a HTML5 canvas path

https://stackoverflow./a/44856925/3896501

The effect I try to achieve:

Thanks for any help!

UPDATE 1:

Code so far I created:

<body>
  <div class="con">
    <div class="ava"></div>
    <canvas id="canvas"></canvas>
  </div>
  <script>

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");

    var shadowPadding = 8;
    var strokeWidth = 2;
    canvas.width = canvas.height = (64 + shadowPadding * 2) * window.devicePixelRatio
    canvas.style.width = canvas.style.height = `${canvas.width / window.devicePixelRatio}px`

    function drawMultiRadiantCircle(xc, yc, r, radientColors) {
        var partLength = (2 * Math.PI) / radientColors.length;
        var start = 0;
        var gradient = null;
        var startColor = null,
            endColor = null;

        for (var i = 0; i < radientColors.length; i++) {
            startColor = radientColors[i];
            endColor = radientColors[(i + 1) % radientColors.length];

            // x start / end of the next arc to draw
            var xStart = xc + Math.cos(start) * r;
            var xEnd = xc + Math.cos(start + partLength) * r;
            // y start / end of the next arc to draw
            var yStart = yc + Math.sin(start) * r;
            var yEnd = yc + Math.sin(start + partLength) * r;

            ctx.beginPath();

            gradient = ctx.createLinearGradient(xStart, yStart, xEnd, yEnd);
            gradient.addColorStop(0, startColor);
            gradient.addColorStop(1, endColor);

            ctx.lineWidth = strokeWidth;
            ctx.strokeStyle = gradient;

            // squircle START
            // https://stackoverflow./questions/50206406/drawing-a-squircle-shape-on-canvas-android
            // //Formula: (|x|)^3 + (|y|)^3 = radius^3
            // ctx.moveTo(-r, 0);
            // const radiusToPow = r ** 3;
            // const rad = r
            // for (let x = -rad ; x <= rad ; x++)
            //   ctx.lineTo(x + r, Math.cbrt(radiusToPow - Math.abs(x ** 3)) + r);
            // for (let x = rad ; x >= -rad ; x--)
            //   ctx.lineTo(x + r, -Math.cbrt(radiusToPow - Math.abs(x ** 3)) + r);
            // ctx.translate(r, r)
            // ctx.restore()
            // squircle END

            // circle START
            // https://stackoverflow./a/22231473/3896501
            ctx.arc(xc, yc, r, start, start + partLength);
            // circle END
            if (i === 1) {
              break
            }
            ctx.stroke();
            ctx.closePath();

            start += partLength;
        }
    }

    var someColors = [];
    someColors.push('#0F0');
    someColors.push('#0FF');
    someColors.push('#F00');
    someColors.push('#FF0');
    someColors.push('#F0F');

    var mid = canvas.width / 2;
    var r = (canvas.width - (shadowPadding * 2)) / 2 + (strokeWidth / 2)
    drawMultiRadiantCircle(mid, mid, r, someColors);

  </script>
  <style>
  .con {
    align-items: center;
    justify-content: center;
    display: flex;
    height: 4rem;
    margin: 6rem;
    width: 4rem;
    position: relative;
  }
  .ava {
    background: #555 50% no-repeat;
    background-size: contain;
    border-radius: 24px;
    height: 100%;
    width: 100%;
  }
  canvas {
    height: 100%;
    width: 100%;
    position: absolute;
  }
  </style>
</body>

drawing portion of circle with gradient color:

drawing a squircle:

I don't know how to code a algorithm that draws a portion of squircle just like what context.arc does.

Share Improve this question edited Jul 21, 2019 at 3:46 user128511 asked Jun 19, 2019 at 21:21 user3896501user3896501 3,0371 gold badge23 silver badges25 bronze badges 6
  • 3 Greatest word in English..."squircle" – Jack Bashford Commented Jun 19, 2019 at 21:25
  • 2 so, you read at least 3 things about this, and yet you haven't written single line of code? – Jaromanda X Commented Jun 19, 2019 at 21:26
  • 1 @JaromandaX I just created a canvas which overlapping on a squircle created by CSS, since drawing the lines require math skills which I am not understand sorry.. – user3896501 Commented Jun 19, 2019 at 22:03
  • 1 ok, wasn't thinking about the maths skills required to draw an arc – Jaromanda X Commented Jun 19, 2019 at 23:35
  • 1 I updated the code to what I so far achieved, now stuck on converting ctx.arc to some algorithm that draws a part of squircle. – user3896501 Commented Jun 20, 2019 at 6:10
 |  Show 1 more ment

2 Answers 2

Reset to default 7

If we read the wikipedia article on squircles, we see that this is just the unweighted ellipse function using powers of 2 or higher, which means we can pretty easily pute the "y" values given "x" values and draw things that way, but doing so will give us extremely uneven segments: small changes in x will lead to HUGE changes in y at the start and end points, and tiny changes in y at the midpoint.

Instead, let's model the squircle as a parametric function, so we vary one control value and get reasonably evenly spaced intervals to work with. We can find this explained in the wikipedia article on the superellipse function:

x = |cos(t)^(2/n)| * sign(cos(t))
y = |sin(t)^(2/n)| * sign(sin(t))

for t from 0 to 2π, and the radii fixed to 1 (so they disappear from multiplications).

If we implement that, then we can add the rainbow coloring almost as an afterthought, drawing each path segment separately, with a strokeStyle coloring that uses HSL colors where the hue values shifts based on our t value:

// alias some math functions so we don't need that "Math." all the time
const abs=Math.abs, sign=Math.sign, sin=Math.sin, cos=Math.cos, pow=Math.pow;

// N=2 YIELDS A CIRCLE, N>2 YIELDS A SQUIRCLE
const n = 4;

function coord(t) {
  let power = 2/n;
  let c = cos(t), x = pow(abs(c), power) * sign(c);
  let s = sin(t), y = pow(abs(s), power) * sign(s);
  return { x, y };
}

function drawSegmentTo(t) {
  let c = coord(t);
  let cx = dim + r * c.x;     // Here, dim is our canvas "radius",
  let cy = dim + r * c.y;     // and r is our circle radius, with
  ctx.lineTo(cx, cy);         // ctx being our canvas context.

  // stroke segment in rainbow colours
  let h = (360 * t)/TAU;
  ctx.strokeStyle = `hsl(${h}, 100%, 50%)`;
  ctx.stroke();  

  // start a new segment at the end point
  ctx.beginPath();
  ctx.moveTo(cx, cy);
}

We can then use this in bination with some standard Canvas2D API code:

const PI = Math.PI,
      TAU = PI * 2,
      edge = 200, // SIZE OF THE CANVAS, IN PIXELS
      dim = edge/2,
      r = dim * 0.9,
      cvs = document.getElementById('draw');

// set up our canvas
cvs.height = cvs.width = edge;
ctx = cvs.getContext('2d');
ctx.lineWidth = 2;
ctx.fillStyle = '#004';
ctx.strokeStyle = 'black';
ctx.fillRect(0, 0, edge, edge);

And with all that setup plete, the draw code is really straight-forward:

// THIS DETERMINES HOW SMOOTH OF A CURVE GETS DRAWN
const segments = 32;

// Peg our starting point, which we know is (r,0) away from the center.
ctx.beginPath();
ctx.moveTo(dim + r, dim)

// Then we generate all the line segments on the path
for (let step=TAU/segments, t=step; t<=TAU; t+=step) drawSegmentTo(t);

// And because IEEE floats are imprecise, the last segment may not
// actually reach our starting point. As such, make sure to draw it!
ctx.lineTo(dim + r, dim);
ctx.stroke();

Running this will yield the following squircle:

With a jsbin so you can play with the numbers: https://jsbin./haxeqamilo/edit?js,output

Of course, you can also go a pletely other way: Create an SVG element (since SVG is part of HTML5) with a <path> element and appropriately set width, height, and viewbox, and then generate a d attribute and gradient-color that, but that's definitely way more finnicky.

With a mathematical expression at hand you can do a full scan of the bounding rectangle, and evaluate pixel-by-pixel if

  • it lies outside
  • it is part of the border
  • it lies inside

For the gradient thing I would apply some continuous function(s) to the angle. Like some sin/cos thing:

let ctx=cnv.getContext("2d");

function gradient(angle){
  return "rgb("+
    (128+127*Math.sin(angle*8))+","+
    (128+127*Math.cos(angle*6))+","+
    (128+127*Math.sin(angle*16))+")";
}

for(let x=0;x<360;x++){
  ctx.fillStyle=gradient(x*Math.PI/180);
  ctx.fillRect(250-180+x,0,1,10);
}

let mx=250,my=90,rx=70,ry=70;

let start=Date.now();

for(let x=-rx;x<=rx;x++)
  for(let y=-ry;y<=ry;y++){
    let r4=Math.pow(x/rx,4)+Math.pow(y/ry,4);
    if(r4<0.8){
      ctx.fillStyle="gray";
      ctx.fillRect(mx+x,my+y,1,1);
    }else if(r4<1){
      ctx.fillStyle=gradient(Math.atan2(x,y));
      ctx.fillRect(mx+x,my+y,1,1);
    }
  }

console.log(Date.now()-start);
<canvas id="cnv" width="500" height="170"></canvas>

For real-life use this approach may perform better with off-screen position into ImageData and perhaps pre-calculating the gradient too:

let ctx=cnv.getContext("2d");

let gradient=new Uint8Array(360*3);

for(let x=0;x<360;x++){
  let r=gradient[x*3]=128+127*Math.sin(x*Math.PI/180*8);
  let g=gradient[x*3+1]=128+127*Math.cos(x*Math.PI/180*6);
  let b=gradient[x*3+2]=128+127*Math.sin(x*Math.PI/180*16);
  ctx.fillStyle="rgb("+r+","+g+","+b+")";
  ctx.fillRect(250-180+x,0,1,10);
}

let mx=250,my=90,rx=70,ry=70;

let start=Date.now();

let imgdata=ctx.createImageData(rx*2+1,ry*2+1);
let data=imgdata.data;

for(let y=-ry,idx=0;y<=ry;y++)
  for(let x=-rx;x<=rx;x++){
    let r4=Math.pow(x/rx,4)+Math.pow(y/ry,4);
    if(r4<0.8){
      data[idx++]=128;
      data[idx++]=128;
      data[idx++]=128;
      data[idx++]=255;
    }else if(r4<1){
      gidx=Math.floor(180+Math.atan2(x,y)*180/Math.PI)%360*3;
      data[idx++]=gradient[gidx++];
      data[idx++]=gradient[gidx++];
      data[idx++]=gradient[gidx++];
      data[idx++]=255;
    }else idx+=4;
  }

ctx.putImageData(imgdata,mx-rx,my-ry);

console.log(Date.now()-start);
<canvas id="cnv" width="500" height="170"></canvas>

On my machine this latter variant is slower for the first run (some 40 ms vs 35 ms), but gets significantly faster for subsequent ones (14 ms vs 31 ms, so the other one does not really speed up). But I have not checked if it is a result of ImageData, gradient[], or both.


EDIT 06-07-2019 applying suggestions, though not together...

Uint32Array makes it shorter, simpler and faster:

let ctx=cnv.getContext("2d");

let gradient=new Uint32Array(360);

for(let x=0;x<360;x++){
  let r=128+127*Math.sin(x*Math.PI/180*8);
  let g=128+127*Math.cos(x*Math.PI/180*6);
  let b=128+127*Math.sin(x*Math.PI/180*16);
  gradient[x]=0xFF000000+(b<<16)+(g<<8)+r;
  ctx.fillStyle="rgb("+r+","+g+","+b+")";
  ctx.fillRect(250-180+x,0,1,10);
}

let mx=250,my=90,rx=70,ry=70;

let start=Date.now();

let imgdata=ctx.createImageData(rx*2+1,ry*2+1);
let data=new Uint32Array(imgdata.data.buffer);

for(let y=-ry,idx=0;y<=ry;y++)
  for(let x=-rx;x<=rx;x++,idx++){
    let r4=Math.pow(x/rx,4)+Math.pow(y/ry,4);
    if(r4<0.8){
      data[idx]=0xFF808080;
    }else if(r4<1){
      gidx=Math.floor(180+Math.atan2(x,y)*180/Math.PI)%360;
      data[idx]=gradient[gidx];
    }
  }

ctx.putImageData(imgdata,mx-rx,my-ry);

console.log(Date.now()-start);
<canvas id="cnv" width="500" height="170"></canvas>

However anti-aliasing is not very trivial with 32-bit numbers, so this one reverts to separate ponents:

let ctx=cnv.getContext("2d");

let gradient=new Uint8Array(360*3);

for(let x=0;x<360;x++){
  let r=gradient[x*3]=128+127*Math.sin(x*Math.PI/180*8);
  let g=gradient[x*3+1]=128+127*Math.cos(x*Math.PI/180*6);
  let b=gradient[x*3+2]=128+127*Math.sin(x*Math.PI/180*16);
  ctx.fillStyle="rgb("+r+","+g+","+b+")";
  ctx.fillRect(250-180+x,0,1,10);
}

let mx=250,my=90,r=70,rr=65;

let start=Date.now();

let imgdata=ctx.createImageData(r*2+1,r*2+1);
let data=imgdata.data;

function mix(a,b,w){
  return b+(a-b)*w;
}

for(let y=-r,idx=0;y<=r;y++)
  for(let x=-r;x<=r;x++){
    let d=Math.pow(Math.pow(x,4)+Math.pow(y,4),0.25);
    if(d<=rr){
      data[idx++]=128;
      data[idx++]=128;
      data[idx++]=128;
      data[idx++]=255;
    }else if(d>=r){
      idx+=4;
    }else{
      let gidx=Math.floor(180+Math.atan2(x,y)*180/Math.PI)%360*3;
      if(d<rr+1){
        let w=d-rr;
        data[idx++]=mix(gradient[gidx++],128,w);
        data[idx++]=mix(gradient[gidx++],128,w);
        data[idx++]=mix(gradient[gidx++],128,w);
        data[idx++]=255;
      }else if(d>r-1){
        let w=r-d;
        data[idx++]=mix(gradient[gidx++],255,w);
        data[idx++]=mix(gradient[gidx++],255,w);
        data[idx++]=mix(gradient[gidx++],255,w);
        data[idx++]=255;
      }else{
        data[idx++]=gradient[gidx++];
        data[idx++]=gradient[gidx++];
        data[idx++]=gradient[gidx++];
        data[idx++]=255;
      }
    }
  }

ctx.putImageData(imgdata,mx-r,my-r);

console.log(Date.now()-start);
<canvas id="cnv" width="500" height="170"></canvas>

发布评论

评论列表(0)

  1. 暂无评论