Question:
Why does CanvasRenderingContext2D.clip() closes an additional path when applying it to a collection of CanvasRenderingContext2D.arc() sampled along the path of a quadratic curve?
Background
I am trying to create a path of quadratic segments with a longitudinal color split. Based on a comment to the question "Square curve with lengthwise color division" I am trying to accomplish this goal by going through the following steps:
Draw the quadratic path
Sample point on the quadratic curve
Create a clipping region and draw a cycle at each sampled point
let region = new Path2D();
for (j = 0; j < pointsQBez.length; j++) {
region.arc(pointsQBez[j].x, pointsQBez[j].y, 4, 0, 2 * Math.PI );
}
ctx.clip(region)
Split the canvas into two segments based on the curve
Calculate the intersection of the start- and end-segment with the canvas border
Close the path (first clipping region)
Draw a rectangle over the whole canvas (second clipping region)
Fill in the two regions created in step four
Steps 3, 4, and 5 in pictures:
Issue
The pink part in the third image above should have the same thickness as the turquoise.
But for some strange reason, the whole inner part of the curve gets filled in.
Additional observations
This behaviour does not show when using CanvasRenderingContext2D.rect() instead of CanvasRenderingContext2D.arc():
When using CanvasRenderingContext2D.arc(), the inner part of the curve that is filled in is not consistent
Because rect does include a call to closePath() while arc doesn't.
Two ways of working around that:
You can call closePath() after each arc:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const pointsQBez = [];
const cx = 75;
const cy = 75;
const rad = 50;
for(let i = 0; i < 180; i++) {
const a = (Math.PI / 180) * i - Math.PI / 2;
const x = cx + Math.cos(a) * rad;
const y = cy + Math.sin(a) * rad;
pointsQBez.push({ x, y });
}
let region = new Path2D();
for (const {x, y} of pointsQBez) {
region.arc(x, y, 4, 0, 2 * Math.PI);
region.closePath();
}
ctx.clip(region);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);
<canvas></canvas>
Or you can moveTo() the entry point of your arc:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const pointsQBez = [];
const cx = 75;
const cy = 75;
const rad = 50;
for(let i = 0; i < 180; i++) {
const a = (Math.PI / 180) * i - Math.PI / 2;
const x = cx + Math.cos(a) * rad;
const y = cy + Math.sin(a) * rad;
pointsQBez.push({ x, y });
}
let region = new Path2D();
for (const {x, y} of pointsQBez) {
region.moveTo(x + 4, y); // x + arc radius
region.arc(x, y, 4, 0, 2 * Math.PI);
}
ctx.clip(region);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);
<canvas></canvas>
I am trying to implement an animation of drawing an arcTo line on Canvas. For a straight line for example, the animation would be as follows
c = canvas.getContext("2d");
width = window.innerWidth;
height = window.innerHeight;
complete = false
var percent = 1
function drawEdge(x1, y1, x2, y2, color){
c.beginPath();
c.lineWidth = 10;
c.strokeStyle = color;
c.moveTo(x1, y1);
c.lineTo(x2, y2);
c.stroke();
c.closePath();
}
function getPosition(x1, y1, x2, y2, percentageBetweenPoints){
let xPosition = x1 + (x2 - x1) * (percentageBetweenPoints / 100);
let yPosition = y1 + (y2 - y1) * (percentageBetweenPoints / 100);
const position = {
x: xPosition,
y: yPosition,
}
return position
}
function drawLine(){
if (!complete){
requestAnimationFrame(drawLine);
}
if (percent >= 100){
complete = true;
percent = 100;
} else{
percent = percent + 1;
}
position = getPosition(300,300,1000,300,percent);
c.clearRect(0, 0 , width, height);
drawEdge(300,300,position.x,position.y, "black");
}
drawLine()
This creates an animation of a line being drawn across the screen. However, I am having trouble doing the same thing for arcTo lines. Is there any way to implement this?
You are looking for something like this?
let ctx = canvas.getContext('2d');
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = 'bold 18px Arial';
requestAnimationFrame(draw);
function draw(t) {
t = t % 5e3 / 5e3;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(canvas.width/2, canvas.height/2, 50, 0, t * 2 * Math.PI);
ctx.stroke();
ctx.fillText((t*100).toFixed(0), canvas.width/2, canvas.height/2);
requestAnimationFrame(draw);
}
<canvas id=canvas></canvas>
To Hack or not to Hack?
There are two ways to do this
Calculate the start, end, and length of each line segment, the start, end angle, direction (CW or CCW), and center of each arc segment. Basically repeating all the maths and logic (around 50 lines of code) that makes arcTo such a useful render function.
You can get details on how to approach the full solution from html5 canvas triangle with rounded corners
Use ctx.lineDash with a long dash and a long space. Move the dash over time with ctx.lineDashOffset giving the appearance of a line growing in length (see demo). The dash offset value is reversed, starting at max length and ending when zero.
NOTE there is one problem with this method. You don't know the length of the line, and thus you don`t know how long it will take for the line to be completed. You can make an estimation. To know the length of the line you must do all the calculations (well there abouts)
The Hack
As the second method is the easiest to implement and covers most needs I will demo that method.
Not much to say about it, it animates a path created by ctx.arcTo
Side benefit is it will animated any path rendered using ctx.stroke
requestAnimationFrame(mainLoop);
// Line is defined in unit space.
// Origin is at center of canvas, -1,-1 top left, 1, 1 bottom right
// Unit box is square and will be scaled to fit the canvas size.
// Note I did not use ctx.setTransform to better highlight what is scaled and what is not.
const ctx = canvas.getContext("2d");
var w, h, w2, h2; // canvas size and half size
var linePos; // current dash offset
var scale; // canvas scale
const LINE_WIDTH = 0.05; // in units
const LINE_STYLE = "#000"; // black
const LINE_SPEED = 1; // in units per second
const MAX_LINE_LENGTH = 9; // in units approx
const RADIUS = 0.08; //Arc radius in units
const SHAPE = [[0.4, 0.2], [0.8, 0.2], [0.5, 0.5], [0.95, 0.95], [0.0, 0.5], [-0.95, 0.95], [-0.5, 0.5], [-0.8, 0.2], [-0.2, 0.2], [-0.2, -0.2], [-0.8, -0.2], [-0.5, -0.5], [-0.95, -0.95], [0.0, -0.5], [0.95,-0.95], [0.5, -0.5], [0.8, -0.2], [0.2, -0.2], [0.2, 0.2], [0.6, 0.2], [0.8, 0.2]];
function sizeCanvas() {
w2 = (w = canvas.width = innerWidth) / 2;
h2 = (h = canvas.height = innerHeight) / 2;
scale = Math.min(w2, h2);
resetLine();
}
function addToPath(shape) {
var p1, p2;
for (p2 of shape) {
!p2.length ?
ctx.closePath() :
(p1 ? ctx.arcTo(p1[0] * scale + w2, p1[1] * scale + h2, p2[0] * scale + w2, p2[1] * scale + h2, RADIUS * scale) :
ctx.lineTo(p2[0] * scale + w2, p2[1] * scale + h2)
);
p1 = p2;
}
}
function resetLine() {
ctx.setLineDash([MAX_LINE_LENGTH * scale, MAX_LINE_LENGTH * scale]);
linePos = MAX_LINE_LENGTH * scale;
ctx.lineWidth = LINE_WIDTH * scale;
ctx.lineJoin = ctx.lineCap = "round";
}
function mainLoop() {
if (w !== innerWidth || h !== innerHeight) { sizeCanvas() }
else { ctx.clearRect(0, 0, w, h) }
ctx.beginPath();
addToPath(SHAPE);
ctx.lineDashOffset = (linePos -= LINE_SPEED * scale * (1 / 60));
ctx.stroke();
if (linePos <= 0) { resetLine() }
requestAnimationFrame(mainLoop);
}
body {
padding: 0px,
margin: 0px;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
Trying to make a graphing utility. I am trying to make the lines smoother. I don't think the problem is with how I draw the lines on the canvas, but rather with how I calculate the x and y coordinates.
HTML
<canvas></canvas>
JS
const canvas = document.querySelector('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let c = canvas.getContext('2d');
// set the graph origin to middle of the canvas
const originX = window.innerWidth / 2;
const originY = window.innerHeight / 2;
c.strokeStyle = `rgba(240, 40, 40, 0.9)`;
c.beginPath();
c.moveTo(originX, originY);
// calculate x and y values for the equation "x^3"
for (let x = -60; x < 60; x = x + 0.1) {
let y = x**3;
draw(x, y);
}
function draw(x, y) {
// Calculated the canvas specific coordinates
let calculatedX = originX + x * 30;
let calculatedY = originY + -y * 30;
c.lineCap = "round";
c.lineWidth = 1;
// draw the line
c.lineTo(calculatedX, calculatedY);
c.stroke();
}
I tried the solutions from responds to other line-smoothing question, but they didn't work. So I think the problem is with the the for loop or the draw function.
live site: https://etasbasi.github.io/simple-grapher/dist/
I'm using the following JS code to draw a 16-sided polygon:
context.beginPath();
var x_offset = 350;
var y_offset = 350;
var sides = 16;
var r = 300;
context.strokeStyle = "#000000";
for (i = 0; i < (sides); i++) {
x = x_offset + (r * Math.cos(2 * Math.PI * i / sides));
y = y_offset + (r * Math.sin(2 * Math.PI * i / sides));
context.moveTo(x, y);
x = x_offset + (r * Math.cos(2 * Math.PI * (i+1) / sides));
y = y_offset + (r * Math.sin(2 * Math.PI * (i+1) / sides));
context.lineTo(x, y);
}
context.stroke();
This is working fine, except that I want the polygon to have a 'flat' sides (top, bottom, left, right). In the picture below you see 2 polygons: the polygon with the red background color is what I want to achieve, the transparent polygon is the one generated by the code above.
Question: how can I generate a polygon with flat sides like the red one? Do I need to do a context.rotate()? I would prefer not too, to avoid all the translate() stuff.
You can just rotate by half of the stepping angle
context.beginPath();
for (var i = 0; i < sides; i++) {
var angle = 2 * Math.PI * (i + 0.5) / sides;
var x = x_offset + (r * Math.cos(angle));
var y = y_offset + (r * Math.sin(angle));
if (i == 0) context.moveTo(x, y);
else context.lineTo(x, y);
}
context.closePath();
I made some minor changes like using local variables and factoring out the angle computation (the added 0.5 is what is needed to have a vertical side).
Note that the polygon will have also horizontal sides only if the number of sides is a multiple of 4.
Here is my pseudo code:
var percentage = 0.781743; // no specific length
var degrees = percentage * 360.0;
var radians = degrees * Math.PI / 180.0;
var x = 50;
var y = 50;
var r = 30;
var s = 1.5 * Math.PI;
var context = canvas.getContext('2d');
context.beginPath();
context.lineWidth = 5;
context.arc(x, y, r, s, radians, false);
context.closePath();
context.stroke();
I'm using the KineticJS library to control the shapes I make and redraw them as necessary. My problem is that the above code does not work at all. I assume I have the math incorrect, because if I change radians to something like 4.0 * Math.PI is draws the entire circle.
I've been using HTML5 Canvas Arc Tutorial for reference.
Your code works just fine, but you have a starting angle that ought to be zero to get what you expect. Here's working code:
http://jsfiddle.net/HETvZ/
I think your confusion is from what starting angle does. It does not mean that it starts at that point and then adds endAngle radians to it. It means that the angle starts at that point and ends at the endAngle radians point absolutely.
So if you startAngle was 1.0 and endAngle was 1.3, you'd only see an arc of 0.3 radians.
If you want it to work the way you're thinking, you're going to have add the startAngle to your endAngle:
context.arc(x, y, r, s, radians+s, false);
Like in this example: http://jsfiddle.net/HETvZ/5/
Your code is just fine. you need to have:
s=0 i.e. starting point must be zero.
and if you want circle to start drawing at top you can use:
context.rotate(-90 * Math.PI / 180);
but after rotating you will have to check arc()'s x,y arguments. i used it like:
context.rotate(-90 * Math.PI / 180);
context.arc(-200, 200, 150, startPoint, radian, false);
context.lineWidth = 20;
context.strokeStyle = '#b3e5fc';
context.stroke();
context.closePath();
after this i needed to display percentage in text form so i did:
context.rotate(90 * Math.PI / 180);
context.fillStyle = '#1aa8ff';
context.textAlign = 'center';
context.font = "bold 76px Verdana";;
context.textBaseline = "middle";
context.fillText("50%", 200, 200);