Reputation: 3037
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 combine 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.com/a/44856925/3896501
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.com/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.com/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.
Upvotes: 4
Views: 2330
Reputation: 53725
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 compute 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 combination 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 complete, 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.com/haxeqamilo/edit?js,output
Of course, you can also go a completely 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.
Upvotes: 7
Reputation: 13225
With a mathematical expression at hand you can do a full scan of the bounding rectangle, and evaluate pixel-by-pixel if
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 composition 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.
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 components:
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>
Upvotes: 4