Drawing in canvas with react doing weird things when rotating - html

I'm trying to draw in a canvas in a react component. The component draws a line and a number of squares depending on the length of an array passed to it as props inclining rotating all of them depending on another prop.
I have a loop that draws it perfectly until it reaches the 5th iteration, then something happens and it start to mess with the context restoration after the rotation. There is only one change of value in that loop ( initialX) Debugging the loop in the browser the rotate method is called the same times as the restore. I'm really confused by this behaviour, it is a very simple draw and I can't see where is my mistake.
This is what I'm getting
This is the same sketch without applying rotation
class Sketch extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
let canvas = document.getElementById("plano");
let detector = this.props.detector
let X, Y;
if (canvas && canvas.getContext && detector) {
inicializarCanvas(detector);
function inicializarCanvas(detector) {
let ctx = canvas.getContext("2d");
let s = getComputedStyle(canvas);
let w = s.width;
let h = s.height;
canvas.width = w.split("px")[0];
canvas.height = h.split("px")[0];
X = canvas.width / 2;
Y = canvas.height / 2;
//draw beam
ctx.moveTo( canvas.width / 3, canvas.height / 2);
ctx.lineTo(0, canvas.height / 2);
ctx.strokeStyle = "#f00";
ctx.stroke();
ctx.restore();
ctx.restore();
ctx.save();
drawBlades(ctx, canvas.width, canvas.height, detector)
}
function drawBlades(ctx, x, y, detector) {
let initialX = x / 3
let initialY = y / 4
let thick = 20
let margin = 5
let rotation = (90 - detector.angle) * Math.PI / 180
let i = 0
ctx.save();
let canvas = document.getElementById("plano");
let ctx2 = canvas.getContext("2d");
ctx2.save();
console.log("blade draw")
//This loop is messing up at the 5th iteration
for (; i < detector.blades.length; i++) {
ctx2.strokeStyle = "#000000";
ctx2.translate(initialX, initialY);
ctx2.rotate(rotation);
ctx2.strokeRect(0, 0, thick, y / 2);
ctx2.restore()
// this is the only variable in that changes of
// value in the loop
initialX = margin + thick + initialX
}
ctx2.save()
}
}
}
render() {
return (
<div className='sketch'>
<canvas width="400" height="150" id="plano">
Canvas not compatible with your browser
</canvas>
</div>
)
}
};

You're restoring your context in each iteration but you don't save it.
Try to add a ctx2.save() and it will work.
for (; i < detector.blades.length; i++) {
ctx2.save(); // save the context
ctx2.strokeStyle = "#000000";
ctx2.translate(initialX, initialY);
ctx2.rotate(rotation);
ctx2.strokeRect(0, 0, thick, y / 2);
ctx2.restore()
// this is the only variable in that changes of
// value in the loop
initialX = margin + thick + initialX
}

Related

Clip + Arc leads to an unwanted closing of the path, while Clip + Rect shows the expected behavior

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>

In p5.js how do I create a moving animation of an array group of lines?

Hi I created a sparkler like shape in P5.js using array() and randomGaussian(). Here is what it looks like with the codes in p5.js:
let distribution = new Array(360);
let b = false;
let x, y;
function setup() {
var cny = createCanvas(windowWidth, windowHeight);
cnv.parent("sketchholder");
for (let i = 0; i < distribution.length; i++) {
distribution[i] = floor(randomGaussian(60, 50));
}
colorMode(HSB, 255);
// hue, saturation, brightness
x = width / 4;
у = height / 3;
}
function draw() {
background(21, 30, 10);
translate(width / 2, height / 2);
strokeWeight(3);
stroke(255, 70);
line(0, 0, -x, y);
if (b) {
for (let i = 0; i < distribution.length; i++) {
rotate(TWO_PI / distribution.length);
var colorH = random(0, 255);
var colorS = random(100, 200);
var colorB = random(0, 255);
var YY = random(1, 4);
stroke(colorH, colorS, colorB);
strokeWeight(YY);
strokeCap(ROUND);
let dist = abs(distribution[i]);
line(0, 0, dist, 0);
}
}
}
And I would like it to move slowly along the stick and disappear after like 20s before reaching the end. Can I achieve that in p5.js? Or can I create the same effect using the p5.js layer as a div in html and animate it in CSS?
Thanks.
Yes, you can definitely do this with p5.js. I think it would not make much sense to try to do an animation like this with CSS and that wouldn't work well with p5.js regardless (you would only be able to animate DOM elements). However, you do need to basically implement your own animation system with different parameter values for different times in the animation and then interpolating between them yourself. Here is a basic example:
let distribution = new Array(360);
let originX, originY;
let keyframes = [];
let keyframeTimes = [];
function setup() {
createCanvas(windowWidth, windowHeight);
for (let i = 0; i < distribution.length; i++) {
distribution[i] = floor(randomGaussian(60, 50));
}
colorMode(HSB, 255);
// hue, saturation, brightness
// Origin relative to the center of the sketch
originX = -width / 4;
originY = height / 3;
const lenX = width / 4;
const lenY = - height / 3;
// Add the desired parameters and times to keyframes and keyframeTimes
keyframes.push({
x: lenX,
y: lenY,
mag: 1
});
keyframeTimes.push(0);
keyframes.push({
x: 0.2 * lenX,
y: 0.2 * lenY,
mag: 0
});
keyframeTimes.push(20);
}
function draw() {
background(21, 30, 10);
translate(width / 2, height / 2);
translate(originX, originY);
strokeWeight(3);
stroke(255, 70);
// Find the next keyframe that we are animating towards.
let nextKeyframeIx = keyframeTimes.findIndex(t => t > millis() / 1000);
// Find the current/previous keyframe that we are starting from.
// Cases to consider: the next keyframe is the first one in the list, we're in
// between two keyframes, or there is no next keyframe
let keyframeIx =
nextKeyframeIx === 0 ?
// There is no previous keyframe (we're at the beginning)
0 :
// If we're beyond the end of the list, just use the parameters from the
// last keyframe
(nextKeyframeIx > 0 ? nextKeyframeIx - 1 : keyframes.length - 1);
let x, y, mag;
if (keyframeIx < nextKeyframeIx) {
// lerp between the current and next keyframe
let kf1 = keyframes[keyframeIx];
let kf2 = keyframes[nextKeyframeIx];
let t1 = keyframeTimes[keyframeIx];
let t2 = keyframeTimes[nextKeyframeIx];
let amt = (millis() / 1000 - t1) / (t2 - t1);
x = lerp(kf1.x, kf2.x, amt);
y = lerp(kf1.y, kf2.y, amt);
mag = lerp(kf1.mag, kf2.mag, amt);
} else {
// just use the current/previous keyframe
let kf = keyframes[keyframeIx];
x = kf.x;
y = kf.y;
mag = kf.mag;
}
// Draw a line from the origin (bottom left of the sparkler) to the current
// x,y position of the end of the sparkler
line(0, 0, x, y);
// Translate to the current end of the sparkler for drawing the colored lines
translate(x, y);
if (mag > 0) {
for (let i = 0; i < distribution.length; i++) {
rotate(TWO_PI / distribution.length);
const colorH = random(0, 255);
const colorS = random(100, 200);
const colorB = random(0, 255);
const YY = random(1, 4);
stroke(colorH, colorS, colorB);
strokeWeight(YY);
strokeCap(ROUND);
let dist = abs(distribution[i]) * mag;
line(0, 0, dist, 0);
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>

How to make HTML canvas' lines smoother for drawing equations

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/

CreateJS Radial gradient with matrix

I'm converting a Flash application to HTML5 Canvas. Most of the development is finished but for handling the colors there is a code like this in the flash application:
matrix = new Matrix ();
matrix.createGradientBox (600, ColorHeight * 1200, 0, 80, ColorHeight * -600);
Animation_gradient_mc.clear ();
Animation_gradient_mc.beginGradientFill (fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio);
The declaration for a radial gradient in CreateJS is the following:
beginRadialGradientFill(colors, ratios, x0, y0, r0, x1, y1, r1 )
Does anyone know a method to apply a Matrix to a gradient fill?
Any help would be appreciated.
Thanks in advance
Edit
Here are some examples of the gradient I'm trying to reproduce:
As you can see it starts off as a standard radial gradient.
However, it can also appear stretched, I think this is where the matrix helps.
I've attempted to create the same effect by creating a createjs.Graphics.Fill with a matrix but it doesn't seem to be doing anything:
var matrix = new VacpMatrix();
matrix.createGradientBox(
600,
discharge_gradient.color_height * 1200,
0,
80,
discharge_gradient.color_height * -600
);
// test_graphics.append(new createjs.Graphics.Fill('#0000ff', matrix));
console.log('matrix', matrix);
test_graphics.append(new createjs.Graphics.Fill('#ff0000', matrix).radialGradient(
discharge_gradient.colors,
discharge_gradient.ratios,
discharge_gradient.x0,
discharge_gradient.y0,
discharge_gradient.r0,
discharge_gradient.x1,
discharge_gradient.y1,
discharge_gradient.r1
));
var discharge_shape = new createjs.Shape(test_graphics);
I extended the Matrix2d class to add a createGradientBox method using code from the openfl project:
p.createGradientBox = function (width, height, rotation, tx, ty) {
if (_.isUndefined(rotation) || _.isNull(rotation)) {
rotation = 0;
}
if (_.isUndefined(tx) || _.isNull(tx)) {
tx = 0;
}
if (_.isUndefined(ty) || _.isNull(ty)) {
ty = 0;
}
var a = width / 1638.4,
d = height / 1638.4;
// Rotation is clockwise
if (rotation != 0) {
var cos = math.cos(rotation),
sin = math.sin(rotation);
this.b = sin * d;
this.c = -sin * a;
this.a = a * cos;
this.d = d * cos;
} else {
this.b = 0;
this.c = 0;
}
this.tx = tx + width / 2;
this.ty = ty + height / 2;
}
I hope the extra information is useful.
I don't know createJS enough, nor Flash Matrix object, but to make this kind of ovalGradient with the native Canvas2d API, you will need to transform the context's matrix.
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var horizontalScale = .3;
var verticalScale = 1;
var gradient = ctx.createRadialGradient(100/horizontalScale, 100/verticalScale, 100, 100/horizontalScale,100/verticalScale,0);
gradient.addColorStop(0,"green");
gradient.addColorStop(1,"red");
// shrink the context's matrix
ctx.scale(horizontalScale, verticalScale)
// draw your gradient
ctx.fillStyle = gradient;
// stretch the rectangle which contains the gradient accordingly
ctx.fillRect(0,0, 200/horizontalScale, 200/verticalScale);
// reset the context's matrix
ctx.setTransform(1,0,0,1,0,0);
canvas{ background-color: ivory;}
<canvas id="canvas" width="200" height="200"></canvas>
So if you are planning to write some kind of a function to reproduce it, have a look at ctx.scale(), ctx.transform() and ctx.setTransform().
EDIT
As you noticed, this will also shrink your drawn shapes, also, you will have to calculate how much you should "unshrink" those at the drawing, just like I did with the fillRect. (agreed, this one was an easy one)
Here is a function that could help you with more complicated shapes. I didn't really tested it (only with the given example), so it may fail somehow, but it can also give you an idea on how to deal with it :
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
function shrinkedRadial(ctx, shapeArray, xScale, yScale, gradientOpts) {
// scaling by 0 is like not drawing
if (!xScale || !yScale) return;
var gO = gradientOpts;
// apply our scale on the gradient options we passed
var gradient = ctx.createRadialGradient(gO.x0 / xScale, gO.y0 / yScale, gO.r0, gO.x1 / xScale, gO.y1 / yScale, gO.r1);
gradient.addColorStop(gO.c1_pos, gO.c1_fill);
gradient.addColorStop(gO.c2_pos, gO.c2_fill);
// shrink the context's matrix
ctx.scale(xScale, yScale);
ctx.fillStyle = gradient;
// execute the drawing operations' string
shapeArray.forEach(function(str) {
var val = str.split(' ');
var op = shapesRef[val[0]];
if (val[1]) {
var pos = val[1].split(',').map(function(v, i) {
// if even, it should be an y axis, otherwise an x one
return i % 2 ? v / yScale : v / xScale;
});
ctx[op].apply(ctx, pos);
} else {
// no parameters
ctx[op]();
}
});
// apply our gradient
ctx.fill();
// reset the transform matrix
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
// just for shortening our shape drawing operations
// notice how arc operations are omitted, it could be implemented but...
var shapesRef = {
b: 'beginPath',
fR: 'fillRect',
m: 'moveTo',
l: 'lineTo',
bC: 'bezierCurveTo',
qC: 'quadraticCurveTo',
r: 'rect',
c: 'closePath'
};
var gradientOpts = {
x0: 232,
y0: 55,
r0: 70,
x1: 232,
y1: 55,
r1: 0,
c1_fill: 'red',
c1_pos: 0,
c2_fill: 'green',
c2_pos: 1
}
var shapes = ['b', 'm 228,133', 'bC 209,121,154,76,183,43', 'bC 199,28,225,34,233,59', 'bC 239,34,270,29,280,39', 'bC 317,76,248,124,230,133']
// our shape is drawn at 150px from the right so we do move the context accordingly, but you won't have to.
ctx.translate(-150, 0);
shrinkedRadial(ctx, shapes, .3, 1, gradientOpts);
ctx.font = '15px sans-serif';
ctx.fillStyle = 'black';
ctx.fillText('shrinked radialGradient', 3, 20);
// how it looks like without scaling :
ctx.translate(50, 0)
var gO = gradientOpts;
var gradient = ctx.createRadialGradient(gO.x0, gO.y0, gO.r0, gO.x1, gO.y1, gO.r1);
gradient.addColorStop(gO.c1_pos, gO.c1_fill);
gradient.addColorStop(gO.c2_pos, gO.c2_fill);
ctx.fillStyle = gradient;
shapes.forEach(function(str) {
var val = str.split(' ');
var op = shapesRef[val[0]];
if (val[1]) {
var pos = val[1].split(',');
ctx[op].apply(ctx, pos);
} else {
ctx[op]();
}
});
ctx.fill();
ctx.font = '15px sans-serif';
ctx.fillStyle = 'black';
ctx.fillText('normal radialGradient', 160, 20);
<canvas id="canvas" width="400" height="150"></canvas>
A standard matrix would adjust inputs:
Width, angle Horizontal, angle Vertical, Height, pos X, pos Y in that order,
Here you are using gradientBox which is not the usual type of AS3 matrix. Expected input:Width, Height, Rotation, pos X, pos Y
I don't use createJS so I'm gunna guess this (you build on it)...
Your usual beginRadialGradientFill(colors, ratios, x0, y0, r0, x1, y1, r1 )
becomes like below (as though gradientBox matrix is involved):
beginRadialGradientFill(colors, ratios, posX, posY, Rotation, Width, Height, Rotation )

How do I keep object location from being increased exponentially after each call to draw function?

Simple animation that creates a firework-like effect on the canvas with each click. The issue is the animation is made with a setInterval(draw) and every time the canvas is redrawn the location of each particle is += particle.speed. But with each click the particles move faster and faster as it seems the speed of each particle is not reset.
As you can see with a couple clicks on the working example here: , with the first click the particles move very (correctly) slowly, but with each subsequent click the speed is increased.
JS used is pasted below as well, any help is greatly appreciated!
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
canvas.addEventListener("click", startdraw, false);
//Lets resize the canvas to occupy the full page
var W = window.innerWidth;
var H = window.innerHeight;
canvas.width = W;
canvas.height = H;
ctx.fillStyle = "black";
ctx.fillRect(0, 0, W, H);
//global variables
var radius;
radius = 10;
balls_amt = 20;
balls = [];
var locX = Math.round(Math.random()*W);
var locY = Math.round(Math.random()*H);
//ball constructor
function ball(positionx,positiony,speedX,speedY)
{
this.r = Math.round(Math.random()*255);
this.g = Math.round(Math.random()*255);
this.b = Math.round(Math.random()*255);
this.a = Math.random();
this.location = {
x: positionx,
y:positiony
}
this.speed = {
x: -2+Math.random()*4,
y: -2+Math.random()*4
};
}
function draw(){
ctx.globalCompositeOperation = "source-over";
//Lets reduce the opacity of the BG paint to give the final touch
ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
ctx.fillRect(0, 0, W, H);
//Lets blend the particle with the BG
//ctx.globalCompositeOperation = "lighter";
for(var i = 0; i < balls.length; i++)
{
var p = balls[i];
ctx.beginPath();
ctx.arc(p.location.x, p.location.y, radius, Math.PI*2, false);
ctx.fillStyle = "rgba("+p.r+","+p.g+","+p.b+", "+p.a+")";
ctx.fill();
var consolelogX = p.location.x;
var consolelogY = p.location.y;
p.location.x += p.speed.x;
p.location.y += p.speed.y;
}
}
function startdraw(e){
var posX = e.pageX; //find the x position of the mouse
var posY = e.pageY; //find the y position of the mouse
for(i=0;i<balls_amt;i++){
balls.push(new ball(posX,posY));
}
setInterval(draw,20);
//ball[1].speed.x;
}
After each click startdraw is called, which starts every time a new periodical call (setInterval) for the draw method. So after the 2nd click you have 2 parallel intervals, after the 3rd you have 3 parallel intervals.
It is not exponentially, only linearly increasing :)
A possible dirty fix:
Introduce an interval global variable, and replace this row:
setInterval(draw,20);
with this one:
if (!interval) interval = setInterval(draw,20);
Or a nicer solution is to start the interval at the onLoad event.
setInterval will repeat its call every 20th ms, and returns an ID.
You can stop the repetition by calling clearInterval(ID).
var id = setInterval("alert('yo!');", 500);
clearInterval(id);