I have this easy animation but I have issue. Everytime I update something on page (for example r of arc) animation become faster and faster after each update or after I saved changes in code.
Here is my code:
import React, { useEffect, useRef } from "react";
const CanvasPage = () => {
const canvasRef = useRef(null);
const x = useRef(0);
const r = 50
const v = useRef(1);
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
//animation function
const render = () => {
window.requestAnimationFrame(render);
//bouncing left and right
x.current = x.current + v.current;
if(x.current > canvas.width){
v.current = -1;
}
if(x.current <= 0){
v.current = 1;
}
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.arc(x.current, 75, r, 0, 2 * Math.PI);
context.stroke();
};
render();
}, []);
return (
<div className="Canvas">
<h1>Canvas page</h1>
<canvas id="canvas" ref={canvasRef} height="500px" width="500px" />
</div>
)
}
export default CanvasPage;
I have tried all topics regarding with this issue sutch a cancelAnimationFrame() but it did not work.
Can some one help me please?
Thank you very much.
It is most likely because the render function is being triggered on each load. And since requestAnimationFrame call is not being cancelled in your code, previous animation function will keep being triggered.
Below fix may solve your issue:
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
const timerIdHolder = {timerId: null};
//animation function
const render = () => {
// let's store the last timerId in our ref object
timerIdHolder.timerId = window.requestAnimationFrame(render);
//bouncing left and right
x.current = x.current + v.current;
if(x.current > canvas.width){
v.current = -1;
}
if(x.current <= 0){
v.current = 1;
}
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.arc(x.current, 75, r, 0, 2 * Math.PI);
context.stroke();
};
render();
// let's return a unmount callback
return () => cancelAnimationFrame(timerIdHolder.timerId);
}, []);
Related
i've been trying working on Canvas inside React but once i'm reloading the page its not showing output but if i'm changing anything in the code with hot-reload it is showing the output,
const [screenWidth, setScreenWidth] = useState(1);
const [screenHeight, setScreenHeight] = useState(2527);
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
function windowResizeOnLoad() {
setScreenWidth((window.innerWidth-2));
};
function windowResizeOnResize() {
setScreenWidth((window.innerWidth - 2));
};
windowResizeOnLoad();
window.addEventListener('resize', windowResizeOnResize);
ctx.beginPath();
ctx.strokeStyle = '#D4AF38';
ctx.moveTo(100, 10);
ctx.bezierCurveTo(100, 90, 1100, 320, 100, 600);
ctx.stroke();
}, [])
<canvas width={screenWidth} height={screenHeight} ref={canvasRef}></canvas>
the question may seem simple, but I can not understand what is my mistake.
I draw images on my canvas, everything is fine, but at the moment when I add clearRect, the elements are completely removed from the canvas without restoring them. Although the cleanup cycle should happen before the rendering
what's my mistake? everything seems to be simple, but I'm stuck with it for a very long time
App main game class
Game class - responsible for rendering to the canvas and clearing it
player - responsible for the logic of the player's behavior
animation - requestanimationframe is called in this function, on the function passed to it
class App {
constructor() {
this.player = new Player("Player 1", 0, 102);
this.game = new Game();
this.game.CreateCanvas();
window.addEventListener('keyup', () => this.player.UpKey());
window.addEventListener('keydown', (event)=> this.player.HandlerKeyPress(event));
new Animation(this.Display.bind(this));
}
Display() {
this.game.ClearCanvas();
this.SetLevel();
}
SetLevel() {
this.game.LoadSprite(assets.mario, [...player_assets.mario.small.standing_right, ...this.player.GetPosition(), 16, 16]);
this.StaticObject();
}
StaticObject () {
for(let i = 0; i < 32; i++){
this.game.LoadSprite(assets.block, [...lvl_1.block.ground.size, i* 16, 134, 16,16]);
this.game.LoadSprite(assets.block, [...lvl_1.block.ground.size, i* 16, 118, 16,16]);
}
}
}
new App();
export default class Game {
CreateCanvas () {
this.canvas = document.createElement("canvas");
this.ctx = this.canvas.getContext("2d");
document.body.appendChild(this.canvas);
}
ClearCanvas() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
SetBackgroundColor (color) {
this.ctx.fillStyle = color;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
LoadSprite (src, position) {
const _image = new Image();
_image.src = src;
_image.onload = () => {
this.ctx.drawImage(_image, ...position, true);
}
}
}
export default class LoopAnimation {
constructor(display) {
this.display = display;
this.Animation = this.Animation.bind(this);
this.Animation();
}
Animation() {
requestAnimationFrame(this.Animation);
this.display();
}
}
My suspicion is it's related to cache and event onload not firing. image.onload event and browser cache
suggests to set the onload property before the src.
var img = new Image();
img.onload = function () {
alert("image is loaded");
}
img.src = "img.jpg";
If you want to reuse loaded images, then store them in an array. Then reuse the image from there. See for example:
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.width = 450;
canvas.height = 250;
const ctx = canvas.getContext("2d");
const assets = [
"https://picsum.photos/id/237/150",
"https://picsum.photos/id/235/150",
"https://picsum.photos/id/232/150",
];
const assetsLoaded = assets.map(url =>
new Promise(resolve => {
const img = new Image();
img.onload = e => resolve(img);
img.src = url;
})
);
Promise
.all(assetsLoaded)
.then(images => {
(function gameLoop() {
requestAnimationFrame(gameLoop);
ctx.clearRect(0, 0, canvas.width, canvas.height);
images.forEach((e, i) =>
ctx.drawImage(
e,
i * 150 + Math.cos(Date.now() * 0.003 + i) * 20, // x
Math.sin(Date.now() * 0.005 + i) * 50 + 50 // y
)
);
})();
})
.catch(err => console.error(err))
;
// from https://stackoverflow.com/a/61337279/3807365
Specifically for your question, you can declare a global object var cache={} where its keys are the src of images to load. so, for example:
var cache = {};
LoadSprite(src, position) {
if (cache[src]) {
this.ctx.drawImage(cache[src], ...position, true);
return;
}
const _image = new Image();
_image.src = src;
_image.onload = () => {
cache[src] = _image;
this.ctx.drawImage(_image, ...position, true);
}
}
I try to build mini paint in browser with using typescript + styled-components + react, however Canvas calculates position of mouse wrongly when I try to draw something. Thing is that it happens only when I use canvas which is located inside CanvasContainer, If I just write tag <canvas ... /> inside ImageEditor then position is counted correctly. Does anyone has any idea why does it happen and how to fix it? As I don't really understand this behaviour.
export default function ImageEditorContainer() {
const CANVAS_REF = useRef<any>(null);
const isDrawing = useRef(false);
function startDrawing(event: MouseEvent) {
isDrawing.current = true;
const canvas = CANVAS_REF.current;
const context = canvas.getContext('2d');
console.log('STARt');
context.beginPath();
context.moveTo(event.clientX - canvas.offsetLeft, event.clientY - canvas.offsetTop);
event.preventDefault();
}
function draw(event: MouseEvent) {
if (isDrawing.current) {
const canvas = CANVAS_REF.current;
const context = canvas.getContext('2d');
console.log('Draw');
context.lineTo(event.clientX - canvas.offsetLeft, event.clientY - canvas.offsetTop);
context.strokeStyle = 'black';
context.lineWidth = '3px';
context.lineCap = 'round';
context.lineJoin = 'round';
context.stroke();
}
event.preventDefault();
}
function stopDrawing(event: MouseEvent) {
if (isDrawing.current) {
const canvas = CANVAS_REF.current;
const context = canvas.getContext('2d');
console.log('STOP');
context.stroke();
context.closePath();
isDrawing.current = false;
}
event.preventDefault();
}
useEffect(() => {
const canvas = CANVAS_REF.current;
if (canvas) {
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
}
}, []);
return (
<>
....
<CanvasContainer CANVAS_REF={CANVAS_REF} ... />
</>
);
}
export default function CanvasContainer({ ...restProps }: TCanvasContent) {
return (
<Canvas.Container>
<Canvas.Content {...restProps} />
</Canvas.Container>
);
}
export default function Canvas({ children }: TCanvas) {
return { children };
}
Canvas.Content = function CanvasContent({ CANVAS_REF, ...restProps }: TCanvasContent) {
return <Content ref={CANVAS_REF} {...restProps} />;
};
https://codesandbox.io/s/dank-dust-vo4lh?file=/src/CanvasContainer.js
Got it worked.
https://codesandbox.io/s/peaceful-jepsen-wh913?file=/src/App.js
The "Style" of the canvas was wrong.
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.
In a React component how can I render an image created using canvas?
componentWillMount () {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.width = 256
canvas.height = 256
// omitted canvas painting code
const image = context.getImageData(0, 0, canvas.width, canvas.height)
this.setState({image})
}
render() {
return ?
}
I'm not sure you can render it directly in the render method. But you can draw it if you use componentDidMount/componentDidUpdate hooks if you are setting it with state.
class Hi extends React.Component {
state = {};
componentWillMount () {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.width = 256
canvas.height = 256
context.rect(20,20,150,100);
context.stroke();
const image = context.getImageData(0, 0, canvas.width, canvas.height)
this.setState({image})
}
componentDidMount() {
const context = this.refs.canvas.getContext('2d');
context.putImageData(this.state.image, 0, 0);
}
render() {
return <canvas ref="canvas" />
}
}
ReactDOM.render(<Hi />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
Please convert your canvas content to data URI using canvas.toDataURL, then you can render the image with an img tag passing the data URI as src attribute:
<img src={this.state.dataURI} />
class Example extends React.Component {
state = {}
componentWillMount () {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.width = 256
canvas.height = 256
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = 70;
context.beginPath();
context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
context.fillStyle = 'green';
context.fill();
context.lineWidth = 5;
context.strokeStyle = '#003300';
context.stroke();
const dataURI = canvas.toDataURL()
this.setState({dataURI})
}
render() {
return <img src={this.state.dataURI} />
}
}
ReactDOM.render(<Example />, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>