HTML5 Canvas draw lines in a circle and move with music? - javascript

I am able to draw lines around a circle. I have the basic implementation of the AudioContext API setup.
The problem I am facing is when calling lineTo the line will only grow but not shrink. I am inspired by this https://www.kkhaydarov.com/audio-visualizer/. I am translating this code over into https://codesandbox.io/s/hungry-tereshkova-1pf0c?runonclick=1&file=/src/Visualizer.js:713-725 which is a React.js version.
If you run that code you will see the music play, then the bars will grow once, and then they stick. They refuse to shrink then grow to the beat.
I am not sure where I am going wrong or what I am missing in that code. It seems pretty similar to the other code in the example.
Here is the full code for the Visualizer component.
import React, { useEffect, useRef } from "react";
let frequencyArray = [];
let analyser;
const Visualizer = () => {
const canvasRef = useRef(null);
const requestRef = useRef(null);
useEffect(() => {
initAudio();
requestRef.current = requestAnimationFrame(drawCanvas);
return () => cancelAnimationFrame(requestRef.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const initAudio = () => {
const audio = new Audio();
audio.src =
"https://s3.us-west-2.amazonaws.com/storycreator.uploads/ck9kpb5ss0xf90132mgf8z893?client_id=d8976b195733c213f3ead34a2d95d1c1";
audio.crossOrigin = "anonymous";
audio.load();
const context = new (window.AudioContext || window.webkitAudioContext)();
analyser = context.createAnalyser();
const source = context.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(context.destination);
frequencyArray = new Uint8Array(analyser.frequencyBinCount);
audio.play();
};
// draw the whole thing
const drawCanvas = () => {
if (canvasRef.current) {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const radius = 200;
const bars = 100;
drawCircle(canvas, ctx, radius);
analyser.getByteFrequencyData(frequencyArray);
for (var i = 0; i < bars; i++) {
const height = frequencyArray[i] * 0.3;
drawLine(
{
i,
bars,
height,
radius
},
canvas,
ctx
);
}
requestRef.current = requestAnimationFrame(drawCanvas);
}
};
// draw the main circle
const drawCircle = (canvas, ctx, radius) => {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
ctx.save();
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = "white";
ctx.fill();
ctx.strokeStyle = "#dddddd";
ctx.lineWidth = 5;
ctx.stroke();
ctx.restore();
};
// dray lines around the circle
const drawLine = (opts, canvas, ctx) => {
const { i, radius, bars, height } = opts;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const lineWidth = 10;
const rads = (Math.PI * 2) / bars;
const x = centerX + Math.cos(rads * i) * (radius + lineWidth);
const y = centerY + Math.sin(rads * i) * (radius + lineWidth);
const endX = centerX + Math.cos(rads * i) * (radius + height);
const endY = centerY + Math.sin(rads * i) * (radius + height);
// draw the bar
ctx.strokeStyle = "#ddd";
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(endX, endY);
ctx.stroke();
};
return (
<canvas
ref={canvasRef}
style={{ background: "#f5f5f5" }}
width={window.innerWidth}
height={window.innerHeight}
/>
);
};
export default Visualizer;

You just missed a clearRect in your code...
Without that we see the lines grow only because any following shorter line does not overwrite the previous one, they are still getting drawn just we do not see it.
here is the working code:
https://codesandbox.io/s/dry-cdn-ghu4m?file=/src/Visualizer.js:1247-1276
I hardcoded a ctx.clearRect(0,0, 1000,1000) just to show you that it works, but you should use the canvas dimensions there, everything else looks good.
Only recommendation will be to somehow move:
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
somewhere global outside the drawCanvas function,
those do not change on every run, will be nice to set them just once.

Related

The act of deleting a certain line from a canvas without affecting arc 😑

I want to clear the canvas with condition on each iteration, may be performance. And, the condition is to only want to clear the line and not the arc.
I already tried chatGPT, save and restore method in JS for save the previous canvas, but it didn't work for me.
This line of code uses the clearRect method of the 2D rendering context to clear the canvas by specifying the x, y, width, and height. But, its clear the whole canvas which I don't want as I mention earlier.
For the better context of my question, you can see this image.
Any answer will be appreciated.
const canvas = document.getElementById('cvs')
const ctx = canvas.getContext('2d')
const CH = (canvas.height = 400)
const CW = (canvas.width = 400)
canvas.style.background = 'black'
const vs = () => {
let radius = 50;
let i = 0;
setInterval(() => {
let x = Math.cos(i) * radius + CH / 2;
let y = Math.sin(i) * radius + CW / 2;
if(i>2*Math.PI)return clearInterval();
/* if I add `ctx.clearRect(0, 0, CW, CH);` it's clear the whole canvas, I don't want that type of behavior */
ctx.beginPath();
ctx.arc(x,y,1,0,2*Math.PI)
ctx.closePath();
ctx.strokeStyle = "white";
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(x, y);
ctx.closePath();
ctx.strokeStyle = "red";
ctx.stroke();
i += 0.01;
}, 10)
}
vs();
body{
display:grid;
place-items:center;
min-height:100vh;
background:gray;
}
<canvas id="cvs"></canvas>
I already tried chatGPT, save and restore method in JS for save the previous canvas, but it didn't work for me.
Those store-restore the context state, color in use and the like. If you look into the box in the top-right, coincidentally you will see a link "Temporary policy: ChatGPT is banned" - that has a reason.
What you need is storing-restoring the bitmap data, getImageData() and putImageData() are the methods for that. If you're concerned about performance (though it feels a bit early), the arc() call can be skipped too, as with this step-size you won't end up with a dotted circle (of course the strokeRect() I'm using instead could be then replaced with direct pixel manipulation, but a cool thing is that it provides anti-aliasing):
const canvas = document.getElementById('cvs')
const ctx = canvas.getContext('2d')
const CH = (canvas.height = 400)
const CW = (canvas.width = 400)
canvas.style.background = 'black'
const vs = () => {
let radius = 50;
let i = 0;
let backup = false;
setInterval(() => {
let x = Math.cos(i) * radius + CH / 2;
let y = Math.sin(i) * radius + CW / 2;
if (backup) ctx.putImageData(backup, 0, 0);
if (i > 2 * Math.PI) return clearInterval();
ctx.strokeStyle = "white";
ctx.strokeRect(x, y, 2, 2);
/* ctx.beginPath();
ctx.arc(x,y,1,0,2*Math.PI)
ctx.closePath();
ctx.strokeStyle = "white";
ctx.stroke();*/
backup = ctx.getImageData(0, 0, x + 2, y + 2); // 0 or 1 will have some red pixels
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(x, y);
ctx.closePath();
ctx.strokeStyle = "red";
ctx.stroke();
i += 0.01;
}, 10)
}
vs();
body {
display: grid;
place-items: center;
min-height: 100vh;
background: gray;
}
<canvas id="cvs"></canvas>

Createjs: How to pixelate bitmaps?

I have a very specific project I'm working on and after 2 weeks the best option seems to be using a bitmap within an empty movie clip. It's perfect apart from one issue - I can't figure out how to pixelate the image.
Here is my code so far:
init_image = () => {
props.image = null
_.holder_grid.bitmap = null
props.image = new Image()
props.image.src = "images/myImage.jpg"
props.image.onload = function() {
stage.holder_grid.bitmap = new createjs.Bitmap(props.image)
stage.holder_grid.bitmap.scale = props.image_scale
stage.holder_grid.addChild(_.holder_grid.bitmap);
const coords = redraw.get_centre()
stage.holder_grid.bitmap.x = coords.x
stage.holder_grid.bitmap.y = coords.y
settings.update()
}
}
init_image()
Its all working as I want but I can't figure out how to pixelate the image.
I found one solution where I draw the image using canvas.context.drawImage() but due to the parameters of the project it's not adequate. That worked like so:
const canvas = document.getElementById("canvas2")
const base = document.getElementById("canvas")
const ctx = canvas.getContext("2d")
const image = new Image()
props.image = image
image.src = "images/myImage.jpg"
image.onload = function() {
const width = base.clientWidth
const height = base.clientHeight
canvas.width= width
canvas.height= height
const scale= props.image_scale
const x = (ctx.canvas.width - image.width * scale) / 2
const y = (ctx.canvas.height - image.height * scale) / 2
//draws tiny
const size = props.pixelate/1
const w = canvas.width * size
const h = canvas.height * size
ctx.drawImage(image, 0, 0, w, h);
// turn off image aliasing
ctx.msImageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
// enlarge the minimized image to full size
ctx.drawImage(canvas, 0, 0, w, h, x, y, image.width * scale, image.height * scale);
}
So basically you draw it small then use that small instance as the image source to draw it bigger and viola, it's pixelated.... but due to other issues I've encountered doing this I cannot use this method.
Can anyone help me with this issue please?
The pixelation in your example is accomplished by drawing the original image at a lower resolution to an html <canvas> element. With createJS however you don't have built-in support for manipulating the sources of it's own Bitmap object.
There's hope though. Besides URL's to images, the constructor to a Bitmap also accepts a <canvas> element. So the trick here is loading the image, preparing - thus pixelating it - using a temporary <canvas> and once it's done, pass this element to the Bitmap constructor.
For example:
let stage = new createjs.Stage("canvas");
function pixelate(src) {
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
let image = new Image();
image.crossOrigin = "anonymous";
image.onload = (e) => {
canvas.width = e.target.width;
canvas.height = e.target.height;
const width = e.target.width;
const height = e.target.height;
canvas.width = width;
canvas.height = height;
const scale = 1;
const x = (ctx.canvas.width - image.width * scale) / 2;
const y = (ctx.canvas.height - image.height * scale) / 2;
const size = 0.125 / 1;
const w = canvas.width * size;
const h = canvas.height * size;
ctx.drawImage(e.target, 0, 0, w, h);
ctx.msImageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(canvas, 0, 0, w, h, x, y, e.target.width * scale, e.target.height * scale);
let bitmap = new createjs.Bitmap(canvas);
stage.addChild(bitmap);
stage.update();
}
image.src = src;
}
pixelate("https://api.codetabs.com/v1/proxy?quest=https://picsum.photos/id/237/400/300");
<script src="https://code.createjs.com/1.0.0/createjs.min.js"></script>
<canvas id="canvas" width="400" height="300"></canvas>

Why do we have to rotate main canvas when trying rotate sprite image?

In every tutorial that I could find for how to rotate a sprite image on a canvas the canvas itself is rotated before applying sprite to it:
function drawSprite(sprite, x, y, deg)
{
const width = sprite.width / 2,
height = sprite.height / 2;
x = x + width;
y = y + height;
//clear main canvas
mainCtx.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
// move origin to the coordinates of the center where sprite will be drawn
mainCtx.translate(x, y);
// rotate canvas
mainCtx.rotate(deg);
// draw sprite
mainCtx.drawImage(sprite, -width, -height);
// restore previous rotation and origin
mainCtx.rotate(-deg);
mainCtx.translate(-x, -y);
}
//never mind the rest
const mainCtx = mainCanvas.getContext("2d"),
sprite = (() =>
{
const canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d"),
width = canvas.width = ctx.width = 100,
height = canvas.height = ctx.height = 50;
ctx.font = '20px arial';
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "lightgreen";
ctx.fillRect(0, 0, width, height);
ctx.strokeRect(0, 0, width, height);
ctx.strokeText("my sprite", width/2, height/2);
return canvas;
})();
let r = 0;
const d = Math.sqrt(sprite.width *sprite.width + sprite.height*sprite.height),
w = mainCanvas.width = mainCtx.width = 400,
h = mainCanvas.height = mainCtx.height = 200;
mainCtx.fillStyle = "pink";
setInterval(() =>
{
const deg = r++*Math.PI/180;
let x = ((w-d)/2) + (Math.sin(deg)*((w-d)/2)),
y = ((h-d)/1.2) + (Math.cos(deg)*((h-d)/2));
drawSprite(sprite, x, y, deg);
}, 10);
<canvas id="mainCanvas"></canvas>
To me this is counterintuitive, why can't we rotate sprite itself before drawing it on main canvas? Why doesn't this work?
function drawSprite(sprite, x, y, deg)
{
const spriteCtx = sprite.getContext("2d");
//clear main canvas
mainCtx.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
// rotate sprite
spriteCtx.rotate(deg);
// draw sprite
mainCtx.drawImage(sprite, x, y);
}
//never mind the rest
const mainCtx = mainCanvas.getContext("2d"),
sprite = (() =>
{
const canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d"),
width = canvas.width = ctx.width = 100,
height = canvas.height = ctx.height = 50;
ctx.font = '20px arial';
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "lightgreen";
ctx.fillRect(0, 0, width, height);
ctx.strokeRect(0, 0, width, height);
ctx.strokeText("my sprite", width/2, height/2);
return canvas;
})();
let r = 0;
const d = Math.sqrt(sprite.width *sprite.width + sprite.height*sprite.height),
w = mainCanvas.width = mainCtx.width = 400,
h = mainCanvas.height = mainCtx.height = 200;
mainCtx.fillStyle = "pink";
setInterval(() =>
{
const deg = r++*Math.PI/180;
let x = ((w-d)/2) + (Math.sin(deg)*((w-d)/2)),
y = ((h-d)/1.2) + (Math.cos(deg)*((h-d)/2));
drawSprite(sprite, x, y, deg);
}, 10);
<canvas id="mainCanvas"></canvas>
Wouldn't it be faster rotate a 100x100 sprite vs 10000x10000 main canvas?
Because the drawImage function takes only x(s),y(s) coordinate and width(s),/height(s).
I.e it only ever draws a straight rectangle, there is no way to make it draw anything skewed.
So you have to rotate the context's Current Transformation Matrix (CTM), which is not the canvas, so that the drawing is transformed.
Note that drawing a bitmap as a rectangle is a very basic model for drawing APIs.
As for the speed, once again you don't rotate the canvas, only the CTM and this only affects the future drawings and costs almost nothing anyway.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.font = "30px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.translate(150, 75);
const txtArr = [ "first", "second", "third", "fourth" ];
const colors = [ "red", "blue", "green", "orange" ];
for (let i = 0; i < txtArr.length; i++) {
ctx.fillStyle = colors[i];
ctx.fillText(txtArr[i], 0, 0);
// This doesn't rotate the previous drawings
ctx.rotate(Math.PI / txtArr.length);
}
<canvas></canvas>
So, yes, you could have your own drawImage(source, matrix) which would be something like
this.save();
this.setTransform(matrix);
this.drawImage(source, 0, 0);
this.restore();
But as you can see this means actually more operations per draw call, and thus performing two drawings on the same CTM would actually cost more than setting the CTM only once.

Javascript - rotating canvas causes stretching [duplicate]

This question already has answers here:
Canvas is stretched when using CSS but normal with "width" / "height" properties
(10 answers)
Closed 1 year ago.
For some reason, if you set the canvas aspect ratio to 2:1, everything appears normal; any other aspect ratio will make it stretch.
const canvas = document.getElementById('canvas');
canvas.style.width = '400px';
canvas.style.height = '400px';
canvas.style.border = '2px solid black';
const ctx = canvas.getContext('2d');
const w = canvas.width/4;
const h = canvas.height/4;
var relativeAngle = 0;
//(Target is the position I want it; in this case, right in the middle)
const targetX = canvas.width/2;
const targetY = canvas.height/2;
const targetDist = Math.sqrt(targetX ** 2 + targetY ** 2);
const targetAngle = Math.atan(targetY / targetX) * 180 / Math.PI;
ctx.fillStyle = 'red'
function draw(){
relativeAngle += 3;
let targetRelativeAngle = targetAngle - relativeAngle;
let targetRelativeX = targetDist * Math.cos(targetRelativeAngle * Math.PI / 180);
let targetRelativeY = targetDist * Math.sin(targetRelativeAngle * Math.PI / 180);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.save();
ctx.rotate(relativeAngle * Math.PI / 180);
ctx.translate(targetRelativeX,targetRelativeY);
ctx.fillRect(-w/2,-h/2,w,h);
ctx.restore();
window.requestAnimationFrame(draw);
}
draw();
What is the problem here and/or what is the better way to do this?
https://jsfiddle.net/1gx7bv3m/
My experience not a good idea to mix styles with canvas...
We can set the aspect ratio directly on the canvas like this:
const canvas = document.getElementById('canvas');
canvas.width = canvas.height = 400
const ctx = canvas.getContext('2d');
const w = canvas.width / 4;
const h = canvas.height / 4;
ctx.fillStyle = 'red'
function refresh() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(w, h)
ctx.rotate(0.02);
ctx.fillRect(-w / 2, -h / 2, w, h);
ctx.translate(-w, -h)
window.requestAnimationFrame(refresh);
}
refresh();
<canvas id="canvas"></canvas>
You can see that I also remove a lot of the calculations you had and I hope that I accomplish the same result you are looking for, a lot less code and much more readable.

Apply grayscale and sepia filters on mousemove - canvas

I am trying to apply grayscale and sepia filters on canvas at the time of mouseMove.
Am using CanvasRenderingContext2D.filter for applying the filters.
Here's the sample code
var radgrad = this.ctx.createRadialGradient(x, y, 50 / 8, x, y, 50 / 2);
radgrad.addColorStop(0, 'rgb(0, 0, 0)');
radgrad.addColorStop(1, 'rgb(0, 0, 0, 1)');
this.ctx.filter = "grayscale(100%) blur(5px) opacity(50%)";
this.ctx.fillStyle = radgrad;
this.ctx.beginPath();
this.ctx.arc(x, y, 50, 0, Math.PI * 2);
this.ctx.fill();
Problem is when I am trying to apply grayscale am not able to achieve it but the blur(5px) is getting applied.
Any solution how to apply grayscale or sepia filter in the above method.
Here's a sample fiddle
Any lead on the solution will be helpful. Thanks
I am not too clear as to what you want, so I'll assume you want something cumulative, as in moving over the same position twice will apply the filter twice.
To do this, the easiest is to create a CanvasPattern from your image. This way you'll be able to fill sub-path using that image as fillStyle, and in the mean time apply your filters on this new drawing:
const img = new Image();
img.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Sunset_2007-1.jpg/1024px-Sunset_2007-1.jpg";
img.onload = begin;
const canvas = document.getElementById( 'canvas' );
const ctx = canvas.getContext( '2d' );
const rad = 25;
function begin() {
canvas.width = img.width;
canvas.height = img.height;
// first draw the original image
ctx.drawImage( img, 0, 0 );
// create a CanvasPattern from it
const patt = ctx.createPattern(img, 'no-repeat');
// set the fillStyle to this pattern
ctx.fillStyle = patt;
// and the filter
ctx.filter = "grayscale(100%) blur(5px) opacity(50%)";
// now at each mousemove
document.onmousemove = e => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// we just draw a new arc
ctx.beginPath();
ctx.arc( x, y, rad, 0, Math.PI * 2 );
// this will use the filtered pattern
ctx.fill();
};
}
<canvas id="canvas"></canvas>
In case you didn't want it to be cumulative (like a scratch-card), then you could create a single big sub-path and redraw everything at every frame.
const img = new Image();
img.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Sunset_2007-1.jpg/1024px-Sunset_2007-1.jpg";
img.onload = begin;
const canvas = document.getElementById( 'canvas' );
const ctx = canvas.getContext( '2d' );
const rad = 25;
const points = [];
const filter = "grayscale(100%) blur(5px) opacity(50%)";
function begin() {
canvas.width = img.width;
canvas.height = img.height;
const patt = ctx.createPattern(img, 'no-repeat');
ctx.fillStyle = patt;
draw();
// now at each mousemove
document.onmousemove = e => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// store that point
points.push( { x, y } );
// and redraw
draw();
};
}
function draw() {
// remove the filter
ctx.filter = "none";
// so we can draw the background untouched
ctx.drawImage( img, 0, 0 );
// we'll now compose a big sub-path
ctx.beginPath();
points.forEach( ({ x, y }) => {
ctx.moveTo( x, y );
ctx.arc( x, y, rad, 0, Math.PI * 2 )
});
// with the filter
ctx.filter = filter;
ctx.fill();
}
<canvas id="canvas"></canvas>
Note that this code assumes you are on a modern browser which does throttle the mouse events to frame rate. If you are targetting older browsers, you may need to do it yourself.

Categories