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 need to draw a Pentagon with html 5 canvas in Javascript. Not much else to write here. I have tried looking it up, but a lot of the examples don't work correctly.
The pentagon starts at the 3oclock pos use rotation to change the start position in radians. ie To start at the top rotation is -90deg = -Math.PI / 2
function pentagon(x, y, radius, rotation){
for(var i = 0; i < 5; i ++){
const ang = (i / 5) * Math.PI * 2 + rotation;
ctx.lineTo(
Math.cos(ang) * radius + x,
Math.sin(ang) * radius + y
);
}
ctx.closePath();
}
ctx.beginPath();
pentagon(100,100,50,-Math.PI / 2);
ctx.fill();
ctx.stroke();
I'm trying to draw a curve in canvas with a linear gradient stoke style along the curve, as in this image. On that page there is a linked svg file that gives instructions on how to accomplish the effect in svg. Maybe a similar method would be possible in canvas?
A Demo: http://jsfiddle.net/m1erickson/4fX5D/
It's fairly easy to create a gradient that changes along the path:
It's more difficult to create a gradient that changes across the path:
To create a gradient across the path you draw many gradient lines tangent to the path:
If you draw enough tangent lines then the eye sees the curve as a gradient across the path.
Note: Jaggies can occur on the outsides of the path-gradient. That's because the gradient is really made up of hundreds of tangent lines. But you can smooth out the jaggies by drawing a line on either side of the gradient using the appropriate colors (here the anti-jaggy lines are red on the top side and purple on the bottom side).
Here are the steps to creating a gradient across the path:
Plot hundreds of points along the path.
Calculate the angle of the path at those points.
At each point, create a linear gradient and draw a gradient stroked line across the tangent of that point. Yes, you will have to create a new gradient for each point because the linear gradient must match the angle of the line tangent to that point.
To reduce the jaggy effect caused by drawing many individual lines, you can draw a smooth path along the top and bottom side of the gradient path to overwrite the jaggies.
Here is annotated code:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
#canvas{border:1px solid red;}
</style>
<script>
$(function(){
// canvas related variables
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// variables defining a cubic bezier curve
var PI2=Math.PI*2;
var s={x:20,y:30};
var c1={x:200,y:40};
var c2={x:40,y:200};
var e={x:270,y:220};
// an array of points plotted along the bezier curve
var points=[];
// we use PI often so put it in a variable
var PI=Math.PI;
// plot 400 points along the curve
// and also calculate the angle of the curve at that point
for(var t=0;t<=100;t+=0.25){
var T=t/100;
// plot a point on the curve
var pos=getCubicBezierXYatT(s,c1,c2,e,T);
// calculate the tangent angle of the curve at that point
var tx = bezierTangent(s.x,c1.x,c2.x,e.x,T);
var ty = bezierTangent(s.y,c1.y,c2.y,e.y,T);
var a = Math.atan2(ty, tx)-PI/2;
// save the x/y position of the point and the tangent angle
// in the points array
points.push({
x:pos.x,
y:pos.y,
angle:a
});
}
// Note: increase the lineWidth if
// the gradient has noticable gaps
ctx.lineWidth=2;
// draw a gradient-stroked line tangent to each point on the curve
for(var i=0;i<points.length;i++){
// calc the topside and bottomside points of the tangent line
var offX1=points[i].x+20*Math.cos(points[i].angle);
var offY1=points[i].y+20*Math.sin(points[i].angle);
var offX2=points[i].x+20*Math.cos(points[i].angle-PI);
var offY2=points[i].y+20*Math.sin(points[i].angle-PI);
// create a gradient stretching between
// the calculated top & bottom points
var gradient=ctx.createLinearGradient(offX1,offY1,offX2,offY2);
gradient.addColorStop(0.00, 'red');
gradient.addColorStop(1/6, 'orange');
gradient.addColorStop(2/6, 'yellow');
gradient.addColorStop(3/6, 'green')
gradient.addColorStop(4/6, 'aqua');
gradient.addColorStop(5/6, 'blue');
gradient.addColorStop(1.00, 'purple');
// draw the gradient-stroked line at this point
ctx.strokeStyle=gradient;
ctx.beginPath();
ctx.moveTo(offX1,offY1);
ctx.lineTo(offX2,offY2);
ctx.stroke();
}
// draw a top stroke to cover jaggies
// on the top of the gradient curve
var offX1=points[0].x+20*Math.cos(points[0].angle);
var offY1=points[0].y+20*Math.sin(points[0].angle);
ctx.strokeStyle="red";
// Note: increase the lineWidth if this outside of the
// gradient still has jaggies
ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(offX1,offY1);
for(var i=1;i<points.length;i++){
var offX1=points[i].x+20*Math.cos(points[i].angle);
var offY1=points[i].y+20*Math.sin(points[i].angle);
ctx.lineTo(offX1,offY1);
}
ctx.stroke();
// draw a bottom stroke to cover jaggies
// on the bottom of the gradient
var offX2=points[0].x+20*Math.cos(points[0].angle+PI);
var offY2=points[0].y+20*Math.sin(points[0].angle+PI);
ctx.strokeStyle="purple";
// Note: increase the lineWidth if this outside of the
// gradient still has jaggies
ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(offX2,offY2);
for(var i=0;i<points.length;i++){
var offX2=points[i].x+20*Math.cos(points[i].angle+PI);
var offY2=points[i].y+20*Math.sin(points[i].angle+PI);
ctx.lineTo(offX2,offY2);
}
ctx.stroke();
//////////////////////////////////////////
// helper functions
//////////////////////////////////////////
// calculate one XY point along Cubic Bezier at interval T
// (where T==0.00 at the start of the curve and T==1.00 at the end)
function getCubicBezierXYatT(startPt,controlPt1,controlPt2,endPt,T){
var x=CubicN(T,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
var y=CubicN(T,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
return({x:x,y:y});
}
// cubic helper formula at T distance
function CubicN(T, a,b,c,d) {
var t2 = T * T;
var t3 = t2 * T;
return a + (-a * 3 + T * (3 * a - a * T)) * T
+ (3 * b + T * (-6 * b + b * 3 * T)) * T
+ (c * 3 - c * 3 * T) * t2
+ d * t3;
}
// calculate the tangent angle at interval T on the curve
function bezierTangent(a, b, c, d, t) {
return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
};
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="canvas" width=300 height=300></canvas>
</body>
</html>
I am working on doing something very similar, and I just wanted to add a couple things. markE's answer is great, but what he calls tangent lines to the curve, are actually lines normal or perpendicular to the curve. (Tangent lines are parallel, normal lines are perpendicular)
For my particular application, I am using a gradient across a line with transparency. In this case, it is important to get near pixel perfect gradient regions, as overlapping transparency will get drawn twice, changing the desired color. So instead of drawing a bunch of lines perpendicular to the curve, I divided the curve up into quadrilaterals and applied a linear gradient to each. Additionally, using these quadrilateral regions reduces the number of calls to draw you have to make, which can make it more efficient. You don't need a ton of regions to get a pretty smooth effect, and the fewer regions you use, the faster it will be able to render.
I adapted markE's code, so credit to him for that great answer. Here is the fiddle: https://jsfiddle.net/hvyt58dz/
Here is the adapted code I used:
// canvas related variables
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// variables defining a cubic bezier curve
var PI2 = Math.PI * 2;
var s = {
x: 20,
y: 30
};
var c1 = {
x: 200,
y: 40
};
var c2 = {
x: 40,
y: 200
};
var e = {
x: 270,
y: 220
};
// an array of points plotted along the bezier curve
var points = [];
// we use PI often so put it in a variable
var PI = Math.PI;
// plot 400 points along the curve
// and also calculate the angle of the curve at that point
var step_size = 100/18;
for (var t = 0; t <= 100 + 0.1; t += step_size) {
var T = t / 100;
// plot a point on the curve
var pos = getCubicBezierXYatT(s, c1, c2, e, T);
// calculate the tangent angle of the curve at that point
var tx = bezierTangent(s.x, c1.x, c2.x, e.x, T);
var ty = bezierTangent(s.y, c1.y, c2.y, e.y, T);
var a = Math.atan2(ty, tx) - PI / 2;
// save the x/y position of the point and the tangent angle
// in the points array
points.push({
x: pos.x,
y: pos.y,
angle: a
});
}
// Note: increase the lineWidth if
// the gradient has noticable gaps
ctx.lineWidth = 2;
var overlap = 0.2;
var outside_color = 'rgba(255,0,0,0.0)';
var inside_color = 'rgba(255,0,0,0.7)';
// draw a gradient-stroked line tangent to each point on the curve
var line_width = 40;
var half_width = line_width/2;
for (var i = 0; i < points.length - 1; i++) {
var x1 = points[i].x, y1 = points[i].y;
var x2 = points[i+1].x, y2 = points[i+1].y;
var angle1 = points[i].angle, angle2 = points[i+1].angle;
var midangle = (angle1 + angle2)/ 2;
// calc the topside and bottomside points of the tangent line
var gradientOffsetX1 = x1 + half_width * Math.cos(midangle);
var gradientOffsetY1 = y1 + half_width * Math.sin(midangle);
var gradientOffsetX2 = x1 + half_width * Math.cos(midangle - PI);
var gradientOffsetY2 = y1 + half_width * Math.sin(midangle - PI);
var offX1 = x1 + half_width * Math.cos(angle1);
var offY1 = y1 + half_width * Math.sin(angle1);
var offX2 = x1 + half_width * Math.cos(angle1 - PI);
var offY2 = y1 + half_width * Math.sin(angle1 - PI);
var offX3 = x2 + half_width * Math.cos(angle2)
- overlap * Math.cos(angle2-PI/2);
var offY3 = y2 + half_width * Math.sin(angle2)
- overlap * Math.sin(angle2-PI/2);
var offX4 = x2 + half_width * Math.cos(angle2 - PI)
+ overlap * Math.cos(angle2-3*PI/2);
var offY4 = y2 + half_width * Math.sin(angle2 - PI)
+ overlap * Math.sin(angle2-3*PI/2);
// create a gradient stretching between
// the calculated top & bottom points
var gradient = ctx.createLinearGradient(gradientOffsetX1, gradientOffsetY1, gradientOffsetX2, gradientOffsetY2);
gradient.addColorStop(0.0, outside_color);
gradient.addColorStop(0.25, inside_color);
gradient.addColorStop(0.75, inside_color);
gradient.addColorStop(1.0, outside_color);
//gradient.addColorStop(1 / 6, 'orange');
//gradient.addColorStop(2 / 6, 'yellow');
//gradient.addColorStop(3 / 6, 'green')
//gradient.addColorStop(4 / 6, 'aqua');
//gradient.addColorStop(5 / 6, 'blue');
//gradient.addColorStop(1.00, 'purple');
// line cap
if(i == 0){
var x = x1 - overlap * Math.cos(angle1-PI/2);
var y = y1 - overlap * Math.sin(angle1-PI/2);
var cap_gradient = ctx.createRadialGradient(x, y, 0, x, y, half_width);
ctx.beginPath();
ctx.arc(x, y, half_width, angle1 - PI, angle1);
cap_gradient.addColorStop(0.5, inside_color);
cap_gradient.addColorStop(1.0, outside_color);
ctx.fillStyle = cap_gradient;
ctx.fill();
}
if(i == points.length - 2){
var x = x2 + overlap * Math.cos(angle2-PI/2);
var y = y2 + overlap * Math.sin(angle2-PI/2);
var cap_gradient = ctx.createRadialGradient(x, y, 0, x, y, half_width);
ctx.beginPath();
ctx.arc(x, y, half_width, angle2, angle2 + PI);
cap_gradient.addColorStop(0.5, inside_color);
cap_gradient.addColorStop(1.0, outside_color);
ctx.fillStyle = cap_gradient;
ctx.fill();
console.log(x,y);
}
// draw the gradient-stroked line at this point
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.moveTo(offX1, offY1);
ctx.lineTo(offX2, offY2);
ctx.lineTo(offX4, offY4);
ctx.lineTo(offX3, offY3);
ctx.fill();
}
//////////////////////////////////////////
// helper functions
//////////////////////////////////////////
// calculate one XY point along Cubic Bezier at interval T
// (where T==0.00 at the start of the curve and T==1.00 at the end)
function getCubicBezierXYatT(startPt, controlPt1, controlPt2, endPt, T) {
var x = CubicN(T, startPt.x, controlPt1.x, controlPt2.x, endPt.x);
var y = CubicN(T, startPt.y, controlPt1.y, controlPt2.y, endPt.y);
return ({
x: x,
y: y
});
}
// cubic helper formula at T distance
function CubicN(T, a, b, c, d) {
var t2 = T * T;
var t3 = t2 * T;
return a + (-a * 3 + T * (3 * a - a * T)) * T + (3 * b + T * (-6 * b + b * 3 * T)) * T + (c * 3 - c * 3 * T) * t2 + d * t3;
}
// calculate the tangent angle at interval T on the curve
function bezierTangent(a, b, c, d, t) {
return (3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b));
};
How to compute the starting co-ordinates to draw a square inside a cirle?
Function Draws the circular spectrum .
Now help me to find the starting coordinates to draw the rectangle inside the circle
Gradient.prototype.renderSpectrum = function() {
var radius = this.width / 2;
var toRad = (2 * Math.PI) / 360;
var step = 1 / radius;
this.ctx.clearRect(0, 0, this.width, this.height);
for(var i = 0; i < 360; i += step) {
var rad = i * toRad;
this.ctx.strokeStyle = 'hsl(' + i + ', 100%, 50%)';
this.ctx.beginPath();
this.ctx.moveTo(radius, radius);
this.ctx.lineTo(radius + radius * Math.cos(rad), radius + radius * Math.sin(rad));
this.ctx.stroke();
}
this.ctx.fillStyle = 'rgb(255, 255, 255)';
this.ctx.beginPath();
this.ctx.arc(radius, radius, radius * 0.8, 0, Math.PI * 2, true);
this.ctx.closePath();
return this.ctx.fill();
}
Function to draw the square
Gradient.prototype.renderGradient = function() {
var color, colors, gradient, index, xy, _i, _len, _ref, _ref1;
xy = arguments[0], colors = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
gradient = (_ref = this.ctx).createLinearGradient.apply(_ref, [0, 0].concat(__slice.call(xy)));
gradient.addColorStop(0, (_ref1 = colors.shift()) != null ? _ref1.toString() : void 0);
for (index = _i = 0, _len = colors.length; _i < _len; index = ++_i) {
color = colors[index];
gradient.addColorStop(index + 1 / colors.length, color.toString());
}
this.ctx.fillStyle = gradient;
this.renderSpectrum();
return this.ctx.fillRect(?, ?, this.width * 0.8, this.height * 0.8);
};
To fit a square inside a circle you can use something like this (adopt as needed):
Live example
/**
* ctx - context
* cx/cy - center of circle
* radius - radius of circle
*/
function squareInCircle(ctx, cx, cy, radius) {
var side = Math.sqrt(radius * radius * 2), // calc side length of square
half = side * 0.5; // position offset
ctx.strokeRect(cx - half, cy - half, side, side);
}
Just replace strokeRect() with fillRect().
Which will result in this (circle added for reference):
Adopting it for general usage:
function getSquareInCircle(cx, cy, radius) {
var side = Math.sqrt(radius * radius * 2), // calc side length of square
half = side * 0.5; // position offset
return {
x: cx - half,
y: cy - half,
w: side,
h: side
}
}
Then in your method:
Gradient.prototype.renderGradient = function() {
var color, colors, gradient, index, xy, _i, _len, _ref, _ref1;
xy = arguments[0], colors = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
gradient = (_ref = this.ctx).createLinearGradient.apply(_ref, [0, 0].concat(__slice.call(xy)));
gradient.addColorStop(0, (_ref1 = colors.shift()) != null ? _ref1.toString() : void 0);
for (index = _i = 0, _len = colors.length; _i < _len; index = ++_i) {
color = colors[index];
gradient.addColorStop(index + 1 / colors.length, color.toString());
}
this.ctx.fillStyle = gradient;
this.renderSpectrum();
// supply the proper position/radius here:
var square = getSquareInCircle(centerX, centerY, radius);
return this.ctx.fillRect(square.x, square.y, square.w, square.h);
};
Here is how I computed the co ordinates to draw square inside a circle
1)Get the coordinates of a inner circle at 135 degree
using the formula
x = rad + rad * Math.cos(135 * ( 2 Math.PI / 360);
y = rad - rad * Math.sin(135 * ( 2 Math.PI / 360);
2) then pyhthogoram therom to find the width if the square
width = Math.sqrt(rad * rad / 2);
For a flash game project I am working on I want to create a camera which should be able to detect a player through ray-casting. The camera should be able to have different view angles (eg. 45 degrees or 90 degrees), and according to these view angles the ray-casting should vary. Creating the actual view angles using ray-casting doesn't cause a problem, however, for some reason that is beyond me the angles in which these view angles are positioned are wrong.
This is the code I use:
for (var i:Number = 0; i < this._viewAngle; i++)
{
var pointX:Number = (this._viewRange) * Math.cos(this._viewAngle + (i * Math.PI / 180));
var pointY:Number = (this._viewRange) * Math.sin(this._viewAngle + (i * Math.PI / 180));
this._rayHolder.graphics.lineTo(pointX, pointY);
this._rayHolder.graphics.moveTo(0, 0);
}
And this is the result:
(90 degrees)
(45 degrees)
I hope what I wrote was understandable enough, because I don't know how to explain it more clearlyIf anybody could shed some light on what I am doing wrong here, that would be awesome.
EDIT:
Changing Math.cos(this._viewAngle + (i * Math.PI / 180)); to Math.cos((this._viewAngle + i) * Math.PI / 180); results in this:
(90 degrees)
(45 degrees)
I think you're close with your update. As Sunil D points out, you presumably just want to align the vision cone so that it's centred at 90˚ from the vertical. In other words, starting from -_viewAngle * 0.5:
for (var i:Number = 0; i < this._viewAngle; i++)
{
var pointX:Number = (this._viewRange) * Math.cos((-this._viewAngle * 0.5 + i) * Math.PI / 180);
var pointY:Number = (this._viewRange) * Math.sin((-this._viewAngle * 0.5 + i) * Math.PI / 180);
this._rayHolder.graphics.lineTo(pointX, pointY);
this._rayHolder.graphics.moveTo(0, 0);
}
Edit: While these equations work for this application, the following reflect Flash's odd coordinate systems more accurately. Specifically, with positive x to the right, positive y downwards:
for (var i:Number = 0; i < this._viewAngle; i++)
{
var pointX:Number = (this._viewRange) * Math.sin((90 - this._viewAngle * 0.5 + i) * Math.PI / 180);
var pointY:Number = (this._viewRange) * -Math.cos((90 - this._viewAngle * 0.5 + i) * Math.PI / 180);
graphics.lineTo(pointX + 100, pointY + 100);
graphics.moveTo(100, 100);
}