So I have this project I have been working on and the goal of it is to randomly generate terrain on a 2D plane, and put rain in the background, and I chose to use the html5 canvas element to accomplish this goal. After creating it I am happy with the result but I am having performance issues and could use some advice on how to fix it. So far I have tried to only clear the bit of the canvas that is needed, which is above the rectangles I drew under the terrain to fill it in, but because of this I have to redraw the circles. The rn(rain number) has already been lowered by about 2 times and it still lags, any suggestions?
Note - The code in the snippet does not lag due to it's small size, but if I was to run it in full screen with the actual rain number(800), it would lag. I have shrunk the values to fit the snippet.
var canvas = document.getElementById('gamecanvas');
var c = canvas.getContext('2d');
var ma = Math.random;
var mo = Math.round;
var wind = 5;
var rn = 100;
var rp = [];
var tp = [];
var tn;
function setup() {
//fillstyle
c.fillStyle = 'black';
//canvas size
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
//rain setup
for (i = 0; i < rn; i++) {
let x = mo(ma() * canvas.width);
let y = mo(ma() * canvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rp[i] = { x, y, w, s };
}
//terrain setup
tn = (canvas.width) + 20;
tp[0] = { x: -2, y: canvas.height - 50 };
for (i = 1; i <= tn; i++) {
let x = tp[i - 1].x + 2;
let y = tp[i - 1].y + (ma() * 20) - 10;
if (y > canvas.height - 50) {
y = tp[i - 1].y -= 1;
}
if (y < canvas.height - 100) {
y = tp[i - 1].y += 1;
}
tp[i] = { x, y };
c.fillRect(x, y, 4, canvas.height - y);
}
}
function gameloop() {
//clearing canvas
for (i = 0; i < tn; i++) {
c.clearRect(tp[i].x - 2, 0, 2, tp[i].y);
}
for (i = 0; i < rn; i++) {
//rain looping
if (rp[i].y > canvas.height + 5) {
rp[i].y = -5;
}
if (rp[i].x > canvas.width + 5) {
rp[i].x = -5;
}
//rain movement
rp[i].y += rp[i].s;
rp[i].x += wind;
//rain drawing
c.fillRect(rp[i].x, rp[i].y, rp[i].w, 6);
}
for (i = 0; i < tn; i++) {
//terrain drawing
c.beginPath();
c.arc(tp[i].x, tp[i].y, 6, 0, 7);
c.fill();
}
}
setup();
setInterval(gameloop, 1000 / 60);
body {
background-color: white;
overflow: hidden;
margin: 0;
}
canvas {
background-color: white;
}
<html>
<head>
<link rel="stylesheet" href="index.css">
<title>A Snowy Night</title>
</head>
<body id="body"> <canvas id="gamecanvas"></canvas>
<script src="index.js"></script>
</body>
</html>
Superimposing canvas
Like I suggested in my comment, the use of a second canvas point is to only have to draw the terrain once, and hence it could enhance the performance of your animation by saving a redraw on each new frame. This can be done with CSS by positioning one on the other (like layers).
#canvasBase {
position: relative;
}
#canvasLayer1 {
position: absolute;
top: 0;
left: 0;
}
#canvasLayer2 {
position: absolute;
top: 0;
left: 0;
}
// etc...
Also I advise you to use requestAnimationFrame over setinterval (see why).
requestAnimationFrame
However, by using requestAnimationFrame, we don't control the refresh rate, it's tied to the client hardware. So we need to handle it and for that, we will use the DOMHighResTimeStamp which is passed as an argument to our callback method.
The idea is to let it run at native speed and manage the fps by updating the logic (our calculs) only at desired time. For exemple, if we need a fps = 60; that means we need to update our logic every 1000 / 60 = ~16,67 ms. So we check if the deltaTime with the time of the last frame is equal or superior than ~16,67ms. If not enough time elapsed, we call a new frame & we return (important, otherwise the control we just did is useless as the code keeps going whatever the outcome of it).
let fps = 60;
/* Check if we need to update the logic */
/* if not request a new frame & return */
if(deltaLastUpdate <= 1000 / fps){ // 1000 / 60 = ~16,67ms
requestAnimationFrame(animate);
return;
}
Clearing canvas
As you need to erase all the past rain drops, the simplest & cheapest in ressources in to clear the whole context in one swoop.
ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);
Path2D
As your drawing use the same color for the rain drops, you can as well group all these in one path:
rainPath = new Path2D();
...
So you will need only one instruction to draw them (same ressources saving type as the clearRect):
ctxRain.fill(rainPath);
Result
/* CANVAS "Terrain" */
const terrainCanvas = document.getElementById('gameTerrain');
const ctxTerrain = terrainCanvas.getContext('2d');
terrainCanvas.height = window.innerHeight;
terrainCanvas.width = window.innerWidth;
/* CANVAS "Rain" */
const rainCanvas = document.getElementById('gameRain');
const ctxRain = rainCanvas.getContext('2d');
rainCanvas.height = window.innerHeight;
rainCanvas.width = window.innerWidth;
/* Game Constants */
const wind = 5;
const rainMaxParticules = 100;
const rain = [];
let rainPath;
const terrainMaxParticules = terrainCanvas.width + 20;
const terrain = [];
let terrainPath;
/* Maths help */
const ma = Math.random;
const mo = Math.round;
/* Clear */
function clearTerrain(){
ctxTerrain.clearRect(0, 0, terrainCanvas.width, terrainCanvas.height);
}
function clearRain(){
ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);
}
/* Logic */
function initTerrain(){
terrain[0] = { x: -2, y: terrainCanvas.height - 50 };
for (let i = 1; i <= terrainMaxParticules; i++) {
let x = terrain[i - 1].x + 2;
let y = terrain[i - 1].y + (ma() * 20) - 10;
if (y > terrainCanvas.height - 50) {
y = terrain[i - 1].y -= 1;
}
if (y < terrainCanvas.height - 100) {
y = terrain[i - 1].y += 1;
}
terrain[i] = { x, y };
}
}
function initRain(){
for (let i = 0; i < rainMaxParticules; i++) {
let x = mo(ma() * rainCanvas.width);
let y = mo(ma() * rainCanvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rain[i] = { x, y, w, s };
}
}
function init(){
initTerrain();
initRain();
}
function updateTerrain(){
terrainPath = new Path2D();
for(let i = 0; i < terrain.length; i++){
terrainPath.arc(terrain[i].x, terrain[i].y, 6, Math.PI/2, 5*Math.PI/2);
}
terrainPath.lineTo(terrainCanvas.width, terrainCanvas.height);
terrainPath.lineTo(0, terrainCanvas.height);
}
function updateRain(){
rainPath = new Path2D();
for (let i = 0; i < rain.length; i++) {
// Rain looping
if (rain[i].y > rainCanvas.height + 5) {
rain[i].y = -5;
}
if (rain[i].x > rainCanvas.width + 5) {
rain[i].x = -5;
}
// Rain movement
rain[i].y += rain[i].s;
rain[i].x += wind;
// Path containing all the drops
rainPath.rect(rain[i].x, rain[i].y, rain[i].w, 6);
}
}
/* Drawing */
function drawTerrain(){
ctxTerrain.fillStyle = 'black';
ctxTerrain.fill(terrainPath);
}
function drawRain(){
ctxRain.fillStyle = 'black';
ctxRain.fill(rainPath);
}
/* Animation Constant */
const fps = 60;
let lastTimestampUpdate;
let terrainDrawn = false;
/* Game loop */
function animate(timestamp){
/* Initialize rain & terrain particules */
if(rain.length === 0 || terrain.length === 0){
init();
}
/* Define "lastTimestampUpdate" from the first call */
if (lastTimestampUpdate === undefined){
lastTimestampUpdate = timestamp;
}
/* Check if we need to update the logic & the drawing, if not, request a new frame & return */
if(timestamp - lastTimestampUpdate <= 1000 / fps){
requestAnimationFrame(animate);
return;
}
if(!terrainDrawn){
/* Terrain --------------------- */
/* Clear */
clearTerrain();
/* Logic */
updateTerrain();
/* Draw */
drawTerrain();
/* ----------------------------- */
terrainDrawn = true;
}
/* --- Rain -------------------- */
/* Clear */
clearRain();
/* Logic */
updateRain();
/* Draw */
drawRain();
/* ----------------------------- */
/* Request another frame */
lastTimestampUpdate = timestamp;
requestAnimationFrame(animate);
}
/* Start the animation */
requestAnimationFrame(animate);
body {
background-color: white;
overflow: hidden;
margin: 0;
}
#gameTerrain {
position: relative;
}
#gameRain {
position: absolute;
top: 0;
left: 0;
}
<body>
<canvas id="gameTerrain"></canvas>
<canvas id="gameRain"></canvas>
</body>
Aside
This won't affect performance, however I encourage you to use const & let over var (What's the difference between using “let” and “var”?).
Generally, having more paint instructions will be what costs the most, the complexity of these paint instructions only comes to play when it's really complex.
Here you are spamming the GPU with paint instructions:
(canvas.width) + 20 calls to clearRect(). clearRect() is a paint instruction, and not a cheap one. Use it sporadically, but actually, you should use it only to clear the whole context.
One fillRect() per rain drop.. They're all the same color, they can be merged in a single sub-path and drawn in a single draw call.
One fill per circle composing the terrain.
So instead of this huge number of draw calls, we could make it in only two draw calls:
One clearRect, one fill() of one big subpath containing both the drops and
the terrain.
However it's certainly more practical to keep the terrain and the rain separated, so let's make it three draw calls, by keeping the terrain in its own Path2D object, which is more friendly for the CPU:
var canvas = document.getElementById('gamecanvas');
var c = canvas.getContext('2d');
var ma = Math.random;
var mo = Math.round;
var wind = 5;
var rn = 100;
var rp = [];
// this will hold our Path2D object
// which will hold the full terrain drawing
// set a 'let' because we will set it again on resize
let terrain;
var tp = [];
var tn;
function setup() {
//fillstyle
c.fillStyle = 'black';
//canvas size
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
//rain setup
for (let i = 0; i < rn; i++) {
let x = mo(ma() * canvas.width);
let y = mo(ma() * canvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rp[i] = { x, y, w, s };
}
//terrain setup
tn = (canvas.width) + 20;
tp[0] = { x: -2, y: canvas.height - 50 };
terrain = new Path2D();
for (let i = 1; i <= tn; i++) {
let x = tp[i - 1].x + 2;
let y = tp[i - 1].y + (ma() * 20) - 10;
if (y > canvas.height - 50) {
y = tp[i - 1].y -= 1;
}
if (y < canvas.height - 100) {
y = tp[i - 1].y += 1;
}
tp[i] = { x, y };
terrain.rect(x, y, 4, canvas.height - y);
terrain.arc(x, y, 6, 0, Math.PI*2);
}
}
function gameloop() {
// clear the whole canvas
c.clearRect(0, 0, canvas.width, canvas.height);
// start a new sub-path for the rain
c.beginPath();
for (let i = 0; i < rn; i++) {
//rain looping
if (rp[i].y > canvas.height + 5) {
rp[i].y = -5;
}
if (rp[i].x > canvas.width + 5) {
rp[i].x = -5;
}
//rain movement
rp[i].y += rp[i].s;
rp[i].x += wind;
//rain tracing
c.rect(rp[i].x, rp[i].y, rp[i].w, 6);
}
// paint all the drops in a single op
c.fill();
// paint the whole terrain in a single op
c.fill(terrain);
// loop at screen refresh frequency
requestAnimationFrame(gameloop);
}
setup();
requestAnimationFrame(gameloop);
onresize = () => setup();
body {
background-color: white;
overflow: hidden;
margin: 0;
}
canvas {
background-color: white;
}
<canvas id="gamecanvas"></canvas>
Further possible improvements:
Instead of making our terrain path a set of rectangles, using only lineTo to trace the actual outline would probably help a bit, some more calculations at init, but it's done only once in a while.
If the terrain becomes more complex, with more details, or with various colors and shadows etc. then consider painting it only once, and then produce an ImageBitmap from the canvas. Then in gameLoop you'll just have to drawImage that ImageBitmap (drawing bitmaps is super fast, but storing it consumes memory, so remember to .close() the ImageBitmap when you don't need it anymore).
Related
I tried to create a canvas effect with fireworks, but the more you click, the faster it gets and it seems to accumulate on itself. When I listed the speed it was similar and did not correspond to what was happening there. I also tried to cancel the draw if it got out of the canvas but it didn´t help.
Here is link https://dybcmwd8icxxdxiym4xkaw-on.drv.tw/canvasTest.html
var fireAr = [];
var expAr = [];
function Firework(x, y, maxY, maxX, cn, s, w, en) {
this.x = x;
this.y = y;
this.maxY = maxY;
this.maxX = maxX;
this.cn = cn;
this.s = s;
this.w = w;
this.en = en;
this.i = 0;
this.explosion = function() {
for (; this.i < this.en; this.i++) {
var ey = this.maxY;
var ex = this.maxX;
var ecn = Math.floor(Math.random() * color.length);
var esX = (Math.random() - 0.5) * 3;
var esY = (Math.random() - 0.5) * 3;
var ew = Math.random() * 10;
var t = true;
expAr.push(new Exp(ew, esX, esY, ex, ey, ecn, t));
}
for (var e = 0; e < expAr.length; e++) {
expAr[e].draw();
}
}
this.draw = function() {
if (this.y < this.maxY) {
this.explosion();
} else {
this.track();
this.y -= this.s;
}
}
}
function Exp(ew, esX, esY, ex, ey, ecn, t) {
this.ew = ew;
this.esX = esX;
this.esY = esY;
this.ex = ex;
this.ey = ey;
this.ecn = ecn;
this.t = t;
this.draw = function() {
if (this.t == true) {
c.beginPath();
c.shadowBlur = 20;
c.shadowColor = color[this.ecn];
c.rect(this.ex, this.ey, this.ew, this.ew);
c.fillStyle = color[this.ecn];
c.fill();
c.closePath();
this.ex += this.esX;
this.ey += this.esY;
}
}
}
window.addEventListener('click', function(event) {
var x = event.clientX;
var y = canvas.height;
mouse.clickX = event.clientX;
mouse.clickY = event.clientY;
var maxY = event.clientY;
var maxX = event.clientX;
var cn = Math.floor(Math.random() * color.length);
var s = Math.random() * 5 + 5;
var w = Math.random() * 20 + 2;
var en = Math.random() * 50 + 5;
fireAr.push(new Firework(x, y, maxY, maxX, cn, s, w, en));
});
function ani() {
requestAnimationFrame(ani);
c.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < fireAr.length; i++) {
fireAr[i].draw();
}
}
ani();
I deleted some unnecessary parts in my opinion but if I'm wrong and I missed something I'll try to fix it
Here are a few simple ways you can improve performance:
Commenting out shadowBlur gives a noticeable boost. If you need shadows, see this answer which illustrates pre-rendering.
Try using fillRect and ctx.rotate() instead of drawing a path. Saving/rotating/restoring the canvas might be prohibitive, so you could use non-rotated rectangles.
Consider using a smaller canvas which is quicker to repaint than one that may fill the entire window.
Another issue is more subtle: Fireworks and Exps are being created (making objects is expensive!) and pushed onto arrays. But these arrays are never trimmed and objects are never reused after they've left the visible canvas. Eventually, the rendering loop gets bogged down by all of the computation for updating and rendering every object in the fireAr and expAr arrays.
A naive solution is to check for objects exiting the canvas and splice them from the expAr. Here's pseudocode:
for (let i = expAr.length - 1; i >= 0; i--) {
if (!inBounds(expAr[i], canvas)) {
expAr.splice(i, 1);
}
}
Iterate backwards since this mutates the array's length. inBounds is a function that checks an Exp object's x and y properties along with its size and the canvas width and height to determine if it has passed an edge. More pseudocode:
function inBounds(obj, canvas) {
return obj.x >= 0 && obj.x <= canvas.width &&
obj.y >= 0 && obj.y <= canvas.height;
}
This check isn't exactly correct since the rectangles are rotated. You could check each corner of the rectangle with a pointInRect function to ensure that at least one is inside the canvas.
Fireworks can be spliced out when they "explode".
splice is an expensive function that walks up to the entire array to shift items forward to fill in the vacated element. Splicing multiple items in a loop gives quadratic performance. This can be made linear by putting surviving fireworks in a new list and replacing the previous generation on each frame. Dead firework objects can be saved in a pool for reuse.
Beyond that, I strongly recommend using clear variable names.
this.cn = cn;
this.s = s;
this.w = w;
this.en = en;
this.i = 0;
These names have little or no meaning to an outside reader and are unlikely to mean much to you if you take a couple months away from the code. Use full words like "size", "width", etc.
Another side point is that it's a good idea to debounce your window resize listener.
Here's a quick proof of concept that illustrates the impact of shadowBlur and pruning dead elements.
const rnd = n => ~~(Math.random() * n);
const mouse = {pressed: false, x: 0, y: 0};
let fireworks = [];
let shouldSplice = false;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
document.body.style.margin = 0;
canvas.style.background = "#111";
canvas.width = document.body.scrollWidth;
canvas.height = document.body.clientHeight;
ctx.shadowBlur = 0;
const fireworksAmt = document.querySelector("#fireworks-amt");
document.querySelector("input[type=range]").addEventListener("change", e => {
ctx.shadowBlur = e.target.value;
document.querySelector("#shadow-amt").textContent = ctx.shadowBlur;
});
document.querySelector("input[type=checkbox]").addEventListener("change", e => {
shouldSplice = !shouldSplice;
});
const createFireworks = (x, y) => {
const color = `hsl(${rnd(360)}, 100%, 60%)`;
return Array(rnd(20) + 1).fill().map(_ => ({
x: x,
y: y,
vx: Math.random() * 6 - 3,
vy: Math.random() * 6 - 3,
size: rnd(4) + 2,
color: color
}));
}
(function render() {
if (mouse.pressed) {
fireworks.push(...createFireworks(mouse.x, mouse.y));
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const e of fireworks) {
e.x += e.vx;
e.y += e.vy;
e.vy += 0.03;
ctx.beginPath();
ctx.fillStyle = ctx.shadowColor = e.color;
ctx.arc(e.x, e.y, e.size, 0, Math.PI * 2);
ctx.fill();
if (shouldSplice) {
e.size -= 0.03;
if (e.size < 1) {
e.dead = true;
}
}
}
fireworks = fireworks.filter(e => !e.dead);
fireworksAmt.textContent = "fireworks: " + fireworks.length;
requestAnimationFrame(render);
})();
let debounce;
addEventListener("resize", e => {
clearTimeout(debounce);
debounce = setTimeout(() => {
canvas.width = document.body.scrollWidth;
canvas.height = document.body.clientHeight;
}, 100);
});
canvas.addEventListener("mousedown", e => {
mouse.pressed = true;
});
canvas.addEventListener("mouseup", e => {
mouse.pressed = false;
});
canvas.addEventListener("mousemove", e => {
mouse.x = e.offsetX;
mouse.y = e.offsetY;
});
* {
font-family: monospace;
user-select: none;
}
div > span, body > div {padding: 0.5em;}
canvas {display: block;}
<div>
<div id="fireworks-amt">fireworks: 0</div>
<div>
<label>splice? </label>
<input type="checkbox">
</div>
<div>
<label>shadowBlur (<span id="shadow-amt">0</span>): </label>
<input type="range" value=0>
</div>
</div>
I have some js code that i copied from a youtube tutorial and adapted for my own project to fill the header, the code works as intended and it works when the viewport is smaller than around 1200px, however when i put firefox into full screen the animation does not play & the image is being stretched, not retaining its aspect ratio. I do have a 10/15 year old gpu so i'm guessing thats half my issue. The script uses a png image file of 100x100 pixels, which it then converts into particle color values. Can this be optimized or made to run better. it seems that the wider the viewport the longer the animation takes to kick in, until it finally stops & doesn't work. full screen= [2550x1440]...
The original tutorial is here: Pure Javascript Particle Animations & to convert an image to base64 encoding is here: Image to base64.
HTML:
<html>
<body>
<canvas id="CanV"></canvas>
</body>
</html>
CSS:
#Canv{
position:absolute;
top:-1px;left:-2px;
z-index:67;
width:100vw !important;
max-height: 264px !important;
min-height: 245px !important;
filter:blur(2.27px);
}
Javascript:
window.addEventListener("DOMContentLoaded",(e)=>{
const canv = document.getElementById('Canv');
const ctx = canv.getContext('2d');
canv.width = window.innerWidth;
canv.height = window.innerHeight/ 3.85;
let particleArray = [];
let mouse = {
x: null,
y: null,
radius: 74
}
window.addEventListener('mousemove',(e)=>{
mouse.x = event.x + canv.clientLeft/2;
mouse.y = event.y + canv.clientTop/1.2;
});
function drawImage(){
let imageWidth = png.width; //These to values crop if / sum no.
let imageHeight = png.height;
const data = ctx.getImageData(0, 0, imageWidth, imageHeight); //Gets img data for particles
ctx.clearRect(0,0, canv.width, canv.height); // Clears the original img as its now being stored in the variable data.
class Particle {
constructor(x, y, color, size){
this.x = x + canv.width/2 - png.width * 174, //Chngd Ok:74
this.y = y + canv.height/2 - png.height * 32, //Ch<2 Ok:16
this.color = color,
this.size = 2.28, // Particle Size > Changed this value. from 2 i think!.
this.baseX = x + canv.width/1.8 - png.width * 3.1, //Chngd ok:5.1
this.baseY = y + canv.height/1.2 - png.height * 2.8,
this.density = (Math.random() * 14) + 2;
}
draw() {
ctx.beginPath(); // this creates the sort of force field around the mouse pointer.
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
update() {
ctx.fillStyle = this.color;
// Collision detection
let dx = mouse.x - this.x;
let dy = mouse.y - this.y;
let distance = Math.sqrt(dx * dx + dy * dy);
let forceDirectionX = dx / distance;
let forceDirectionY = dy / distance;
// Max distance, past that the force will be 0
const maxDistance = 144;
let force = (maxDistance - distance) / maxDistance;
if (force < 0) force = 0;
let directionX = (forceDirectionX * force * this.density * 0.6);
let directionY = (forceDirectionY * force * this.density * 8.7); //Ch.this
if (distance < mouse.radius + this.size) {
this.x -= directionX;
this.y -= directionY;
} else {
if (this.x !== this.baseX){
let dx = this.x - this.baseX;
this.x -= dx/54; // Speed Particles return to ori
} if (this.y !== this.baseY){
let dy = this.y - this.baseY;
this.y -= dy/17; // Speed Particles return to ori
}
}
this.draw();
}
}
function init(){
particleArray = [];
for(let y = 0, y2 = data.height; y<y2; y++){
for(let x =0, x2 = data.width; x<x2; x++){
if(data.data[(y * 4 * data.width) + (x*4) + 3] > 128){
let positionX = x + 25;
let positionY = y + 45; // Co-ords on Canv
let color = "rgb(" + data.data[(y * 4 * data.width) + (x * 4)] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 1] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 2] + ")";
particleArray.push(new Particle(positionX * 2, positionY * 2, color));
} /* These number effect png size but its to high */
}
}
}
function animate(){
requestAnimationFrame(animate);
ctx.fillStyle = 'rgba(0,0,0,.07)';
ctx.fillRect(0,0, innerWidth, innerHeight);
for(let i =0; i < particleArray.length; i++){
particleArray[i].update();
}
}
init();
animate();
}
const png = new Image();
png.src = "RemovedBase64StringToBig";
window.addEventListener('load',(e)=>{
console.log('page has loaded');
ctx.drawImage(png, 0, 0);
drawImage();
})
});
have managed to shorten it by about 100 characters by shortening all the variable names > PartArr, ImgWidth, DirX, DirY etc, but apart from minifying it is there any other ways to optimize this? and fix the full screen issue?
I tried to add it to a JSfiddle, So I could link to it here, but I don't think this is allowing the base64 string, its not loading anything anyway. The canvas loads, with the bg just no image or animation.
I've found out what part of the problem is with full screen, the cursor position is actually about 300px to the right of where the actual cursor is, but I still have no idea how to fix this or fix the major lagging performance issues. Guessing its alot to compute even just with 100x100.
One option I can think of to make this perform better would be to move it & its calculations, into its own dedicated web worker & convert the image to Webp but i'm still not very clued up about web workers or how to implement them properly.. Will play around & see what I can put together using All the suggestions in the comments & answers.
I'm adding these links only for future reference, when I come back to this later on:
MDN Canvas Optimizations
Html5Rocks Canvas Performance
Stack Question. Canv ~ Opti
Creating A blob From A Base 64 String in Js
Secondary bonus Question,
is there a maximum file size or max px dimensions,
that can be base64 encoded? only asking this as someone on facebook has recently sent me a question regarding another project with multiple base64 encoded images and I was unsure of the answer..
Shortening your code doesn't help much with performance. I'm using Firefox. To check what's taking your time up the most during browser runs in Firefox, you can read Performance from MDN.
The problem with your solution is your fps is dropping hard. This happens because you are painting each Particle every frame. Imagine how laggy it will be when there are thousands of Particles that you need to paint every frame. This paint call is called from your function Particle.draw (which calls the following: ctx.beginPath, ctx.arc, and ctx.closePath). This function, as said, will be called because of Particle.update for each frame. This is an extremely expensive operation. To improve your fps drastically, you can try to not draw each Particle individually, but rather gather all the Particles' ImageData wholly then placing it in the canvas only once in rAQ (thus only one paint happens). This ImageData is an object that contains the rgba for each pixel on canvas.
In my solution below, I did the following:
For each Particle that is dirty (has been updated), modify the ImageData that is to be put in the canvas
Then, after the whole ImageData has been constructed for one frame, only draw once to the canvas using putImageData. This saves a lot of the time needed to call your function Particle.update to draw each Particle individually.
One other obvious solution is to increase the size of Particles so that there are fewer Particles' pixels that are needed to be processed (to alter ImageData). I've also tweaked the code a little so that the image will always be at least 100px high; you can tweak the maths so that the image will always maintain your aspect ratio and respond to window size.
Here's a working example:
const canvas = document.querySelector('#canvas1')
const ctx = canvas.getContext('2d')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
let canvasWidth = canvas.width
let canvasHeight = canvas.height
let particleArray = []
let imageData = []
// mouse
let mouse = {
x: null,
y: null,
radius: 40
}
window.addEventListener('mousemove', e => {
mouse.x = event.x
mouse.y = event.y
})
function drawImage(width, height) {
let imageWidth = width
let imageHeight = height
const data = ctx.getImageData(0, 0, imageWidth, imageHeight)
class Particle {
constructor(x, y, color, size = 2) {
this.x = Math.round(x + canvas.width / 2 - imageWidth * 2)
this.y = Math.round(y + canvas.height / 2 - imageHeight * 2)
this.color = color
this.size = size
// Records base and previous positions to repaint the canvas to its original background color
this.baseX = Math.round(x + canvas.width / 2 - imageWidth * 2)
this.baseY = Math.round(y + canvas.height / 2 - imageHeight * 2)
this.previousX = null
this.previousY = null
this.density = (Math.random() * 100) + 2
}
stringifyColor() {
return `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, ${this.color.a}`
}
update() {
ctx.fillStyle = this.stringifyColor()
// collision detection
let dx = mouse.x - this.x
let dy = mouse.y - this.y
let distance = Math.sqrt(dx * dx + dy * dy)
let forceDirectionX = dx / distance
let forceDirectionY = dy / distance
// max distance, past that the force will be 0
const maxDistance = 100
let force = (maxDistance - distance) / maxDistance
if (force < 0) force = 0
let directionX = (forceDirectionX * force * this.density)
let directionY = (forceDirectionY * force * this.density)
this.previousX = this.x
this.previousY = this.y
if (distance < mouse.radius + this.size) {
this.x -= directionX
this.y -= directionY
} else {
// Rounded to one decimal number to as x and y cannot be the same (whole decimal-less integer)
// as baseX and baseY by decreasing using a random number / 20
if (Math.round(this.x) !== this.baseX) {
let dx = this.x - this.baseX
this.x -= dx / 20
}
if (Math.round(this.y) !== this.baseY) {
let dy = this.y - this.baseY
this.y -= dy / 20
}
}
}
}
function createParticle(x, y, size) {
if (data.data[(y * 4 * data.width) + (x * 4) + 3] > 128) {
let positionX = x
let positionY = y
let offset = (y * 4 * data.width) + (x * 4)
let color = {
r: data.data[offset],
g: data.data[offset + 1],
b: data.data[offset + 2],
a: data.data[offset + 3]
}
return new Particle(positionX * 4, positionY * 4, color, size)
}
}
// Instead of drawing each Particle one by one, construct an ImageData that can be
// painted into the canvas at once using putImageData()
function updateImageDataWith(particle) {
let x = particle.x
let y = particle.y
let prevX = particle.previousX
let prevY = particle.previousY
let size = particle.size
if (prevX || prevY) {
let prevMinY = Math.round(prevY - size)
let prevMaxY = Math.round(prevY + size)
let prevMinX = Math.round(prevX - size)
let prevMaxX = Math.round(prevX + size)
for (let y = prevMinY; y < prevMaxY; y++){
for (let x = prevMinX; x < prevMaxX; x++) {
if (y < 0 || y > canvasHeight) continue
else if (x < 0 || x > canvasWidth) continue
else {
let offset = y * 4 * canvasWidth + x * 4
imageData.data[offset] = 255
imageData.data[offset + 1] = 255
imageData.data[offset + 2] = 255
imageData.data[offset + 3] = 255
}
}
}
}
let minY = Math.round(y - size)
let maxY = Math.round(y + size)
let minX = Math.round(x - size)
let maxX = Math.round(x + size)
for (let y = minY; y < maxY; y++){
for (let x = minX; x < maxX; x++) {
if (y < 0 || y > canvasHeight) continue
else if (x < 0 || x > canvasWidth) continue
else {
let offset = y * 4 * canvasWidth + x * 4
imageData.data[offset] = particle.color.r
imageData.data[offset + 1] = particle.color.g
imageData.data[offset + 2] = particle.color.b
imageData.data[offset + 3] = particle.color.a
}
}
}
}
function init() {
particleArray = []
imageData = ctx.createImageData(canvasWidth, canvasHeight)
// Initializing imageData to a blank white "page"
for (let data = 1; data <= canvasWidth * canvasHeight * 4; data++) {
imageData.data[data - 1] = data % 4 === 0 ? 255 : 255
}
const size = 2 // Min size is 2
const step = Math.floor(size / 2)
for (let y = 0, y2 = data.height; y < y2; y += step) {
for (let x = 0, x2 = data.width; x < x2; x += step) {
// If particle's alpha value is too low, don't record it
if (data.data[(y * 4 * data.width) + (x * 4) + 3] > 128) {
let newParticle = createParticle(x, y, size)
particleArray.push(newParticle)
updateImageDataWith(newParticle)
}
}
}
}
function animate() {
requestAnimationFrame(animate)
for (let i = 0; i < particleArray.length; i++) {
let imageDataCanUpdateKey = `${Math.round(particleArray[i].x)}${Math.round(particleArray[i].y)}`
particleArray[i].update()
updateImageDataWith(particleArray[i])
}
ctx.putImageData(imageData, 0, 0)
}
init()
animate()
window.addEventListener('resize', e => {
canvas.width = innerWidth
canvas.height = innerHeight
canvasWidth = canvas.width
canvasHeight = canvas.height
init()
})
}
const png = new Image()
png.src = " data:image/png;base64,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"
window.addEventListener('load', e => {
// Ensuring height of image is always 100px
let pngWidth = png.width
let pngHeight = png.height
let divisor = pngHeight / 100
let finalWidth = pngWidth / divisor
let finalHeight = pngHeight / divisor
ctx.drawImage(png, 0, 0, finalWidth, finalHeight)
drawImage(finalWidth, finalHeight)
})
#canvas1 {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
<canvas id="canvas1"></canvas>
UPDATE 2: I have managed to optimize further. Now it can render FullHD image (1920x1080) without downgrading quality (on my PC it runs at about 20fps).
Take a look this code on JSFiddle (you can also tweak values).
Thanks also goes to #Richard (check out his answer) for idea to put all data in ImageData and make a single draw call. Code on JSFiddle is combination of his and mine optimizations (code below is my old code).
EDIT: Updated JSFiddle link, optimized more by spreading work of stationary particles across multiple frames (for given settings it improves performance for about 10%).
Regarding optimization, you won't achieve much by minifying code (in this case) because code that eats up CPU is runtime intensive (executes each frame). Minification is good for optimizing loading, not runtime execution.
Most of time is spent on drawing, and after some investigation I have found few performance optimizations but these are not enough to make big difference (eg. ctx.closePath() can be omitted and this saves some milliseconds).
What you can do is to either reduce resolution of image or skip some pixels in image in order to reduce work.
Additionally you could spread work across multiple frames to improve frame rate (but keep in mind if you spread it on more than few frames you might start seeing flickering).
Fullscreen issue can be solved by simply re-initializing everything on resize event.
Below is code with mentioned optimizations and fullscreen fix. Sample image is 375x375 pixels.
UPDATE: I played a little with code and I managed to improve further performance by optimizing calls (things I mentioned below code snippet). Code is updated with these changes.
var canv
var ctx
//performance critical parameters
const pixelStep = 2 //default 1; increase for images of higher resolution
const maxParticlesToProcessInOneFrame = 20000
//additional performance oriented paramteres
// Max distance, past that the force will be 0
const maxDistance = 144
const mouseRadius = 74
//customization parameters
const ctxFillStyle = 'rgba(0,0,0,.07)'
const speedOfActivatingParticle = 1
const speedOfRestoringParticle = 0.1
const png = new Image();
const mouse = {
x: null,
y: null
}
window.addEventListener('mousemove', (e) => {
mouse.x = event.x + canv.clientLeft;
mouse.y = event.y + canv.clientTop;
})
class Particle {
constructor(x, y, size) {
this.x = x
this.y = y
this.size = pixelStep
this.baseX = x
this.baseY = y
this.density = (Math.random() * 14) + 2
}
draw() {
//ctx.beginPath(); // this creates the sort of force field around the mouse pointer.
//ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.rect(this.x, this.y, this.size * 2, this.size * 2)
//ctx.closePath();
}
update() {
// Collision detection
let dx = mouse.x - this.x;
let dy = mouse.y - this.y;
let distance = Math.sqrt(dx * dx + dy * dy);
if (distance < mouseRadius + this.size) {
let forceDirectionX = dx / distance;
let forceDirectionY = dy / distance;
let force = (maxDistance - distance) / maxDistance;
if (force < 0)
force = 0;
const forceTimesDensity = force * this.density * speedOfActivatingParticle
let directionX = (forceDirectionX * forceTimesDensity);
let directionY = (forceDirectionY * forceTimesDensity); //Ch.this
this.x -= directionX;
this.y -= directionY;
} else {
if (this.x !== this.baseX) {
let dx = this.x - this.baseX;
this.x -= dx * speedOfRestoringParticle; // Speed Particles return to ori
}
if (this.y !== this.baseY) {
let dy = this.y - this.baseY;
this.y -= dy * speedOfRestoringParticle; // Speed Particles return to ori
}
}
this.draw();
}
}
window.addEventListener('resize', initializeCanvas)
window.addEventListener("load", initializeCanvas, {
once: true
})
let animationFrame = null
function initializeCanvas(e) {
cancelAnimationFrame(animationFrame)
canv = document.getElementById('Canv');
ctx = canv.getContext('2d');
canv.width = window.innerWidth;
canv.height = window.innerHeight;
let particles = {}
function drawImage() {
let imageWidth = png.width; //These to values crop if / sum no.
let imageHeight = png.height;
const data = ctx.getImageData(0, 0, imageWidth, imageHeight); //Gets img data for particles
ctx.clearRect(0, 0, canv.width, canv.height); // Clears the original img as its now being stored in the variable data.
function init() {
particles = {}
for (let y = 0, y2 = data.height; y < y2; y += pixelStep) {
for (let x = 0, x2 = data.width; x < x2; x += pixelStep) {
if (data.data[(y * 4 * data.width) + (x * 4) + 3] > 128) {
let positionX = x
let positionY = y
let color = "rgb(" + data.data[(y * 4 * data.width) + (x * 4)] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 1] + "," +
data.data[(y * 4 * data.width) + (x * 4) + 2] + ")";
let particlesArray = particles[color]
if (!particlesArray)
particlesArray = particles[color] = []
particlesArray.push(new Particle(positionX * 2, positionY * 2))
} /* These number effect png size but its to high */
}
}
}
let particlesProcessed = 0
let animateGenerator = animate()
function* animate() {
particlesProcessed = 0
ctx.fillStyle = ctxFillStyle;
ctx.fillRect(0, 0, innerWidth, innerHeight);
let colors = Object.keys(particles)
for (let j = 0; j < colors.length; j++) {
let color = colors[j]
ctx.fillStyle = color
let particlesArray = particles[color]
ctx.beginPath()
for (let i = 0; i < particlesArray.length; i++) {
particlesArray[i].update()
if (++particlesProcessed > maxParticlesToProcessInOneFrame) {
particlesProcessed = 0
ctx.fill()
yield
ctx.beginPath()
}
}
ctx.fill()
}
}
init();
function animateFrame() {
animationFrame = requestAnimationFrame(() => {
if (animateGenerator.next().done) {
animateGenerator = animate()
}
animateFrame()
})
}
animateFrame()
}
console.log('page has loaded');
ctx.drawImage(png, 0, 0, png.width, png.height);
drawImage();
}
png.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXcAAAF3CAMAAABpHvvMAAAC1lBMVEUAAAAAAAAAAADp6ekAAAAAAACcnJwAAAAAAACgoKAAAAAAAAAAAAAAAADq6uoAAAAAAADu7u6Ojo7d3d0AAABISEj5+fmYmJi2traampr6+vqXl5ebm5u3t7dNTU1HR0eRkZG8vLzd3d3f39+zs7NTU1Pw8PDk5OTu7u7////VuTP54C3r6uoAAADVuDDr6+vYvkL38db54Cv12y7r6+3avjL8/PzXuzPw1i/54Cr///3u7e3Wuzn99sP54TTWujcyMjLl3bf43y354TLk27H//vnZwEnWujXh4eH99b3VuTT875YlJSX//vv19fXXvD0GBgZHR0fawU39/Pfr6ufZv0X+/PT7+OnYvDL69uTdx1v8+e7cxVXYvUDy2S78+vH489tbW1vq6OPhzWz64zzu1C9DQ0PXvDvq59yxsbGFhYXfyWHm377cyW7gy2fbw1D65EP//fD+/Ory6b7x57jg0Ij54jcKCgr09PT59N/v5K/t4abezHfZwlTpzzD39/f+++T07Mfp2ZDbxmDr0S/65UkfHx/q6ODfzn/j0HXaxFr65VDgxTHfwzHn4snk2avq3Jnh1Jbiz3HixzH54S/o5NHz68Lh0pD874387oXk0Xn763VsbGzbx2dPT0/cwDL//vbp5dX17864uLjr3p3l1IL87H376m376GD651vnzDD5+fn++t7b29v++dT28dT28NHn4MTs36Hp25Tl033o48717cv+98f99bfw5bP98qjn2Iv76Wf65lQ8PDzdwjL++tju4qr98qHXuzoVFRXn5+fp5tn9867j2Kb88Zzn1ojm1YV9fX12dnbz8/P99LPi1qDlyzAQEBDX19f++M/Nzc3++MvExMTi1ZwrKysaGhrw8PDT09OsrKxWVlbkyTGXl5eQkJD54TO1tbXi16NkZGSnp6eKioqCgoLlyjGdnZ3j4+OkpKRKSkrkyjCpqank1pD353rMpemXAAAAKXRSTlMABAvSFCRoCA5mIh8RFtEnG95Wtxg39GuHavRsaYU1OVSCuLZyMd3D3S9YQ4oAABlBSURBVHja7NQBzoMgDIZh8UcpKzoEtyUmIxCnife/4F+9BAb63KBvvrRhjDHGGGOM1UoUq7knUYnmRsRFKdUWjQ68U3tBqDiA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";
body {
margin: 0;
padding: 0;
}
#Canv {
width: 100vw;
height: 100vh;
filter: blur(1.5px);
}
<canvas id="Canv"></canvas>
If you still need to optimize, you could do some optimization regarding ctx.beginPath(), ctx.fill() and ctx.rect() calls. For example, try to combine sibling pixels (pixels that are next to each other) and render them all in one call. Furthermore, you could merge similar colors in single color, but downside is that image will loose quality (depending on how much colors are merged).
Also (if this is option) you might want to set fixed canvas size rather than dynamically sized.
Disclosure: On my PC given code works nicely, but on others it might still have performance issues. For that reason try to play with pixelStep and maxParticlesToProcessInOneFrame variable values.
So I'm recreating the warping grid from Geometry Wars in a web page to further test my skills with JavaScript and I've hit another snag. I'm following a tutorial written in C# over on TutsPlus that I used a long time ago to recreate it while learning XNA Framework. The tutorial is straight forward, and most of the code is self-explanatory, but I think my lack of superior education in mathematics is letting me down once again.
I've successfully rendered the grid in a 300x300 canvas with no troubles, and even replicated all of the code in the tutorial, but since they're using the XNA Framework libraries, they have the advantage of not having to write the mathematical functions of the Vector3 type. I've implemented only what I need, but I believe I may have gotten my math incorrect or perhaps the implementation.
The initial grid (above) should look like this until I begin interacting with it, and it does, as long as I disable the Update function of my Grid. I've stepped through the code and the issue seems to be related to my calculation for the magnitude of my vectors. The XNA Framework libraries always called it Length and LengthSquared, but each Google search I performed was returning results for calculating magnitude as:
Now, this is incredibly simple to recreate in code, and my Vector3 class accounts for Magnitude and MagnitudeSquared since the tutorial calls for both. I've compared the results of my magnitude calculation to that of an online calculator and the results were the same:
V = (2, 3, 4)
|V| = 5.385164807134504
To top this off, the URL for this calculator says that I'm calculating the length of the vector. This is what leads me to believe that it may be my implementation here that is causing the whole thing to go crazy. I've included my snippet below, and it is unfortunately a bit long, but I assure you it has been trimmed as much as possible.
class Vector3 {
constructor(x, y, z) {
this.X = x;
this.Y = y;
this.Z = z;
}
Add(val) {
this.X += val.X;
this.Y += val.Y;
this.Z += val.Z;
}
Subtract(val) {
this.X -= val.X;
this.Y -= val.Y;
this.Z -= val.Z;
}
MultiplyByScalar(val) {
let result = new Vector3(0, 0, 0);
result.X = this.X * val;
result.Y = this.Y * val;
result.Z = this.Z * val;
return result;
}
DivideByScalar(val) {
let result = new Vector3(0, 0, 0);
result.X = this.X / val;
result.Y = this.Y / val;
result.Z = this.Z / val;
return result;
}
Magnitude() {
if (this.X == 0 && this.Y == 0 && this.Z == 0)
return 0;
return Math.sqrt(Math.pow(this.X, 2) +
Math.pow(this.Y, 2) +
Math.pow(this.Z, 2));
}
MagnitudeSquared() {
return Math.pow(this.Magnitude(), 2);
}
DistanceFrom(to) {
let x = Math.pow(this.X - to.X, 2);
let y = Math.pow(this.Y - to.Y, 2);
let z = Math.pow(this.Z - to.Z, 2);
return Math.sqrt(x + y + z);
}
}
class PointMass {
Acceleration = new Vector3(0, 0, 0);
Velocity = new Vector3(0, 0, 0);
Damping = 0.95;
constructor(position, inverseMass) {
this.Position = position;
this.InverseMass = inverseMass;
}
IncreaseDamping(factor) {
this.Damping *= factor;
}
ApplyForce(force) {
this.Acceleration.Add(force.MultiplyByScalar(this.InverseMass));
}
Update() {
this.Velocity.Add(this.Acceleration);
this.Position.Add(this.Velocity);
this.Acceleration = new Vector3(0, 0, 0);
if (this.Velocity.MagnitudeSquared() < 0.001 * 0.001)
Velocity = new Vector3(0, 0, 0);
this.Velocity.MultiplyByScalar(this.Damping);
this.Damping = 0.95;
}
}
class Spring {
constructor(startPoint, endPoint, stiffness, damping) {
this.StartPoint = startPoint;
this.EndPoint = endPoint;
this.Stiffness = stiffness;
this.Damping = damping;
this.TargetLength = startPoint.Position.DistanceFrom(endPoint.Position) * 0.95;
}
Update() {
let x = this.StartPoint.Position;
x.Subtract(this.EndPoint.Position);
let magnitude = x.Magnitude();
if (magnitude < this.TargetLength || magnitude == 0)
return;
x = x.DivideByScalar(magnitude).MultiplyByScalar(magnitude - this.TargetLength);
let dv = this.EndPoint.Velocity;
dv.Subtract(this.StartPoint.Velocity);
let force = x.MultiplyByScalar(this.Stiffness)
force.Subtract(dv.MultiplyByScalar(this.Damping));
this.StartPoint.ApplyForce(force);
this.EndPoint.ApplyForce(force);
}
}
class Grid {
Springs = [];
Points = [];
constructor(containerID, spacing) {
this.Container = document.getElementById(containerID);
this.Width = this.Container.width;
this.Height = this.Container.height;
this.ColumnCount = this.Width / spacing + 1;
this.RowCount = this.Height / spacing + 1;
let columns = [];
let fixedColumns = [];
let rows = [];
let fixedRows = [];
let fixedPoints = [];
for (let y = 0; y < this.Height; y += spacing) {
for (let x = 0; x < this.Width; x += spacing) {
columns.push(new PointMass(new Vector3(x, y, 0), 1));
fixedColumns.push(new PointMass(new Vector3(x, y, 0), 0));
}
rows.push(columns);
fixedRows.push(fixedColumns);
columns = [];
fixedColumns = [];
}
this.Points = rows;
for (let y = 0; y < rows.length; y++) {
for (let x = 0; x < rows[y].length; x++) {
if (x == 0 || y == 0 || x == rows.length - 1 || x == rows[y].length - 1)
this.Springs.push(new Spring(fixedRows[x][y], this.Points[x][y], 0.1, 0.1));
else if (x % 3 == 0 && y % 3 == 0)
this.Springs.push(new Spring(fixedRows[x][y], this.Points[x][y], 0.002, 0.002));
const stiffness = 0.28;
const damping = 0.06;
if (x > 0)
this.Springs.push(new Spring(this.Points[x - 1][y], this.Points[x][y], stiffness, damping));
if (y > 0)
this.Springs.push(new Spring(this.Points[x][y - 1], this.Points[x][y], stiffness, damping));
}
}
}
ApplyDirectedForce(force, position, radius) {
this.Points.forEach(function(row) {
row.forEach(function(point) {
if (point.Position.DistanceFrom(position) < Math.pow(radius, 2))
point.ApplyForce(force.MultiplyByScalar(10).DivideByScalar(10 + point.Position.DistanceFrom(position)));
});
});
}
ApplyImplosiveForce(force, position, radius) {
this.Points.forEach(function(point) {
let distance_squared = Math.pow(point.Position.DistanceFrom(position));
if (distance_squared < Math.pow(radius, 2)) {
point.ApplyForce(force.MultiplyByScalar(10).Multiply(position.Subtract(point.Position)).DivideByScalar(100 + distance_squared));
point.IncreaseDamping(0.6);
}
});
}
ApplyExplosiveForce(force, position, radius) {
this.Points.forEach(function(point) {
let distance_squared = Math.pow(point.Position.DistanceFrom(position));
if (distance_squared < Math.pow(radius, 2)) {
point.ApplyForce(force.MultiplyByScalar(100).Multiply(point.Position.Subtract(position)).DivideByScalar(10000 + distance_squared));
point.IncreaseDamping(0.6);
}
});
}
Update() {
this.Springs.forEach(function(spring) {
spring.Update();
});
this.Points.forEach(function(row) {
row.forEach(function(point) {
point.Update();
});
});
}
Draw() {
const context = this.Container.getContext('2d');
context.clearRect(0, 0, this.Width, this.Height);
context.strokeStyle = "#ffffff";
context.fillStyle = "#ffffff";
for (let y = 1; y < this.Points.length; y++) {
for (let x = 1; x < this.Points[y].length; x++) {
let left = new Vector3(0, 0, 0);
let up = new Vector3(0, 0, 0);
if (x > 1) {
left = this.Points[x - 1][y].Position;
context.beginPath();
context.moveTo(left.X, left.Y);
context.lineTo(this.Points[x][y].Position.X, this.Points[x][y].Position.Y);
context.stroke();
}
if (y > 1) {
up = this.Points[x][y - 1].Position;
context.beginPath();
context.moveTo(up.X, up.Y);
context.lineTo(this.Points[x][y].Position.X, this.Points[x][y].Position.Y);
context.stroke();
}
let radius = 3;
if (y % 3 == 1)
radius = 5;
context.beginPath();
context.arc(this.Points[x][y].Position.X, this.Points[x][y].Position.Y, radius, 0, 2 * Math.PI);
context.fill();
}
}
}
}
var grid = new Grid("grid", 40);
setInterval(function() {
grid.Update();
grid.Draw();
}, 5);
var mouseX = 0;
var mouseY = 0;
function updateMouseCoordinates(evt) {
var rect = grid.Container.getBoundingClientRect();
mouseX = evt.clientX - rect.left;
mouseY = evt.clientY - rect.top;
const context = grid.Container.getContext('2d');
context.clearRect(0, 0, this.Width, this.Height);
context.strokeStyle = "#ffffff";
context.fillStyle = "#ff3333";
context.beginPath();
context.arc(mouseX, mouseY, 15, 0, 2 * Math.PI);
context.fill();
grid.ApplyDirectedForce(new Vector3(0, 0, 5000), new Vector3(mouseX, mouseY, 0), 50);
}
html,
body {
margin: 0;
height: 100%;
background: #213;
background: linear-gradient(45deg, #213, #c13);
background: -webkit-linear-gradient(45deg, #213, #c13);
}
.container {
position: relative;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
<div class="container">
<canvas onmousemove="updateMouseCoordinates(event)" id="grid" class="grid" width="300" height="300"></canvas>
</div>
I believe the issue has something to do with the Update method in the Spring and PointMass classes as when I stepped through my code, I kept finding that the PointMass objects seemed to have acceleration when they shouldn't (as in, I haven't interacted with them yet). In all honesty, I think it's the implementation of my custom Vector3 functions in those update functions that are causing the issue but for the life of me, I can't figure out what I've done incorrectly here.
Perhaps I just need to take a break and come back to it, but I'm hoping someone here can help spot an incorrect implementation.
How do I prevent my grid from immediately dissipating due to forces that have not yet been exerted (as in they are just miscalculations)?
My advice is reduce the problem down. Have only a single point, slow the interval down, step through to see what's happening. The mouse doesn't appear to be doing anything. Commenting out the line grid.ApplyDirectedForce(new Vector3(0, 0, 5000), new Vector3(mouseX, mouseY, 0), 50); doesn't change the output. It goes wrong in grid.Update(), for some reason grid.Update() does something even if there's no force applied, maybe that means the spring code has a bug. The bottom right point doesn't move frame one maybe that means something. The debugger is your friend. Add a breakpoint to grid.Update() and see what the code is actually doing. I know this isn't a direct answer but I hope this guides you in the right direction.
I also want to point out that usually the whole point of Magnitude Squared is so that you can compare vectors or distances without having to do a square root operation. That is, in your Magnitude function you do a Square root operation and then in your Magnitude Squared function you square it. This is is the same as simply going x^2 + y^2 + z^2
frame 1:
frame 2:
Hi I want to make a blur effect particle like this:
Can I use shadowBlur and shadowOffsetX/shadowOffsetY to do this? The actual shine will glow and fade a little bit repeatedly, so if I have to write some kind of animation how can I achieve this?
I have tried this code (jsfiddle example) but it doesn't look like the effect. So I wonder how to blur and glow the particle at the same time?
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ra = window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.oRequestAnimationFrame
|| window.msRequestAnimationFrame
|| function(callback) {
window.setTimeout(callback, 1000 / 60);
};
class Particle {
constructor(options) {
this.ctx = options.context;
this.x = options.x;
this.y = options.y;
this.radius = options.radius;
this.lightSize = this.radius;
this.color = options.color;
this.lightDirection = true;
}
glow() {
const lightSpeed = 0.5;
this.lightSize += this.lightDirection ? lightSpeed : -lightSpeed;
if (this.lightSize > this.radius || this.lightSize < this.radius) {
this.lightDirection = !this.lightDirection;
}
}
render() {
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.glow();
this.ctx.globalAlpha = 0.5;
this.ctx.fillStyle = this.color;
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.lightSize,
0, Math.PI * 2
);
this.ctx.fill();
this.ctx.globalAlpha = 0.62;
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius * 0.7, 0, Math.PI * 2);
this.ctx.shadowColor = this.color;
this.ctx.shadowBlur = 6;
this.ctx.shadowOffsetX = 0;
this.ctx.shadowOffsetY = 0;
this.ctx.fill();
}
}
var particle = new Particle({
context: ctx,
x: 60,
y: 80,
radius: 12,
color: '#4d88ff'
});
function run() {
particle.render();
ra(run);
}
run();
<canvas id='canvas'></canvas>
There are several ways to do this. For a particle system my option is to pre render the blur using a blur filter. A common filter is the convolution filter. It uses a small array to determine the amount neighboring pixels contribute to each pixel of the image. You are best to look up convolution functions to understand it.
Wiki Convolution and Wiki Gaussian blur for more info.
I am not much of a fan of the standard Gaussian blur or the convolution filter used so in the demo snippet below you can find my version that I think creates a much better blur. The convolution blur filter is procedurally created and is in the imageTools object.
To use create a filter pass an object with properties size the blur amount in pixels and power is the strength. Lower powers is less spread on the blur.
// image must be loaded or created
var blurFilter = imageTools.createBlurConvolutionArray({size:17,power:1}); // size must be greater than 2 and must be odd eg 3,5,7,9...
// apply the convolution filter on the image. The returned image may be a new
//image if the input image does not have a ctx property pointing to a 2d canvas context
image = imageTools.applyConvolutionFilter(image,blurFilter);
In the demo I create a image, draw a circle on it, copy it and pad it so that there is room for the blur. Then create a blur filter and apply it to the image.
When I render the particles I first draw all the unblurred images, then draw the blurred copies with the ctx.globalCompositeOperation = "screen"; so that they have a shine. To vary the amount of shine I use the ctx.globalAlpha to vary the intensity of the rendered blurred image. To improve the FX I have drawn the blur image twice, once with oscillating scale and next at fixed scale and alpha.
The demo is simple, image tools can be found at the top. Then there is some stuff to setup the canvas and handle resize event. Then there is the code that creates the images, and apply the filters. Then starts the render adds some particles and renders everything.
Look in the function drawParticles for how I draw everything.
imageTools has all the image functions you will need. The imageTools.applyConvolutionFilter will apply any filter (sharpen, outline, and many more) you just need to create the appropriate filter. The apply uses the photon count colour model so gives a very high quality result especially for blurs type effects. (though for sharpen you may want to get in and change the squaring of the RGB values, I personally like it other do not)
The blur filter is not fast so if you apply it to larger images It would be best that you break it up in so you do not block the page execution.
A cheap way to get a blur is to copy the image to blur to a smaller version of itself, eg 1/4 then render it scaled back to normal size, the canvas will apply bilinear filtering on the image give a blur effect. Not the best quality but for most situations it is indistinguishable from the more sophisticated blur that I have presented.
UPDATE
Change the code so that the particles have a bit of a 3dFX to show that the blur can work up to larger scales. The blue particles are 32 by 32 image and the blur is 9 pixels with the blur image being 50by 50 pixels.
var imageTools = (function () {
var tools = {
canvas : function (width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage : function (width, height) {
var image = this.canvas(width, height);
image.ctx = image.getContext("2d");
return image;
},
image2Canvas : function (img) {
var image = this.canvas(img.width, img.height);
image.ctx = image.getContext("2d");
image.drawImage(img, 0, 0);
return image;
},
padImage : function(img,amount){
var image = this.canvas(img.width + amount * 2, img.height + amount * 2);
image.ctx = image.getContext("2d");
image.ctx.drawImage(img, amount, amount);
return image;
},
getImageData : function (image) {
return (image.ctx || (this.image2Canvas(image).ctx)).getImageData(0, 0, image.width, image.height);
},
putImageData : function (image, imgData){
(image.ctx || (this.image2Canvas(image).ctx)).putImageData(imgData,0, 0);
return image;
},
createBlurConvolutionArray : function(options){
var i, j, d; // misc vars
var filterArray = []; // the array to create
var size = options.size === undefined ? 3: options.size; // array size
var center = Math.floor(size / 2); // center of array
// the power ? needs descriptive UI options
var power = options.power === undefined ? 1: options.power;
// dist to corner
var maxDist = Math.sqrt(center * center + center * center);
var dist = 0; // distance sum
var sum = 0; // weight sum
var centerWeight; // center calculated weight
var totalDistance; // calculated total distance from center
// first pass get the total distance
for(i = 0; i < size; i++){
for(j = 0; j < size; j++){
d = (maxDist-Math.sqrt((center-i)*(center-i)+(center-j)*(center-j)));
d = Math.pow(d,power)
dist += d;
}
}
totalDistance = dist; // total distance to all points;
// second pass get the total weight of all but center
for(i = 0; i < size; i++){
for(j = 0; j < size; j++){
d = (maxDist-Math.sqrt((center-i)*(center-i)+(center-j)*(center-j)));
d = Math.pow(d,power)
d = d/totalDistance;
sum += d;
}
}
var scale = 1/sum;
sum = 0; // used to check
for(i = 0; i < size; i++){
for(j = 0; j < size; j++){
d = (maxDist-Math.sqrt((center-i)*(center-i)+(center-j)*(center-j)));
d = Math.pow(d,power)
d = d/totalDistance;
filterArray.push(d*scale);
}
}
return filterArray;
},
applyConvolutionFilter : function(image,filter){
imageData = this.getImageData(image);
imageDataResult = this.getImageData(image);
var w = imageData.width;
var h = imageData.height;
var data = imageData.data;
var data1 = imageDataResult.data;
var side = Math.round(Math.sqrt(filter.length));
var halfSide = Math.floor(side/2);
var r,g,b,a,c;
for(var y = 0; y < h; y++){
for(var x = 0; x < w; x++){
var ind = y*4*w+x*4;
r = 0;
g = 0;
b = 0;
a = 0;
for (var cy=0; cy<side; cy++) {
for (var cx=0; cx<side; cx++) {
var scy = y + cy - halfSide;
var scx = x + cx - halfSide;
if (scy >= 0 && scy < h && scx >= 0 && scx < w) {
var srcOff = (scy*w+scx)*4;
var wt = filter[cy*side+cx];
r += data[srcOff+0] * data[srcOff+0] * wt;
g += data[srcOff+1] * data[srcOff+1] * wt;
b += data[srcOff+2] * data[srcOff+2] * wt;
a += data[srcOff+3] * data[srcOff+3] * wt;
}
}
}
data1[ind+0] = Math.sqrt(Math.max(0,r));
data1[ind+1] = Math.sqrt(Math.max(0,g));
data1[ind+2] = Math.sqrt(Math.max(0,b));
data1[ind+3] = Math.sqrt(Math.max(0,a));
}
}
return this.putImageData(image,imageDataResult);
}
};
return tools;
})();
/** SimpleFullCanvasMouse.js begin **/
const CANVAS_ELEMENT_ID = "canv";
const U = undefined;
var w, h, cw, ch; // short cut vars
var canvas, ctx;
var globalTime = 0;
var createCanvas, resizeCanvas, setGlobals;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
c.id = CANVAS_ELEMENT_ID;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === U) { canvas = createCanvas(); }
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals(); }
}
setGlobals = function(){
cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2;
if(particles && particles.length > 0){
particles.length = 0;
}
}
resizeCanvas(); // create and size canvas
window.addEventListener("resize",resizeCanvas); // add resize event
const IMAGE_SIZE = 32;
const IMAGE_SIZE_HALF = 16;
const GRAV = 2001;
const NUM_PARTICLES = 90;
var background = imageTools.createImage(8,8);
var grad = ctx.createLinearGradient(0,0,0,8);
grad.addColorStop(0,"#000");
grad.addColorStop(1,"#048");
background.ctx.fillStyle = grad;
background.ctx.fillRect(0,0,8,8);
var circle = imageTools.createImage(IMAGE_SIZE,IMAGE_SIZE);
circle.ctx.fillStyle = "#5BF";
circle.ctx.arc(IMAGE_SIZE_HALF, IMAGE_SIZE_HALF, IMAGE_SIZE_HALF -2,0, Math.PI * 2);
circle.ctx.fill();
var blurFilter = imageTools.createBlurConvolutionArray({size:9,power:1}); // size must be greater than 2 and must be odd eg 3,5,7,9...
var blurCircle = imageTools.padImage(circle,9);
blurCircle = imageTools.applyConvolutionFilter(blurCircle,blurFilter)
var sun = imageTools.createImage(64,64);
grad = ctx.createRadialGradient(32,32,0,32,32,32);
grad.addColorStop(0,"#FF0");
grad.addColorStop(1,"#A40");
sun.ctx.fillStyle = grad;
sun.ctx.arc(32,32,32 -2,0, Math.PI * 2);
sun.ctx.fill();
var sunBlur = imageTools.padImage(sun,17);
blurFilter = imageTools.createBlurConvolutionArray({size:17,power:1}); // size must be greater than 2 and must be odd eg 3,5,7,9...
sunBlur = imageTools.applyConvolutionFilter(sunBlur,blurFilter);
var particles = [];
var createParticle = function(x,y,dx,dy){
var dir = Math.atan2(y-ch,x-cw);
var dist = Math.sqrt(Math.pow(y-ch,2)+Math.pow(x-cw,2));
var v = Math.sqrt(GRAV / dist); // get apporox orbital speed
return {
x : x,
y : y,
dx : dx + Math.cos(dir + Math.PI/2) * v, // set orbit speed at tangent
dy : dy + Math.sin(dir + Math.PI/2) * v,
s : (Math.random() + Math.random() + Math.random())/4 + 0.5, // scale
v : (Math.random() + Math.random() + Math.random()) / 3 + 2, // glow vary rate
};
}
var depthSort = function(a,b){
return b.y - a.y;
}
var updateParticles = function(){
var i,p,f,dist,dir;
for(i = 0; i < particles.length; i ++){
p = particles[i];
dist = Math.sqrt(Math.pow(cw-p.x,2)+Math.pow(ch-p.y,2));
dir = Math.atan2(ch-p.y,cw-p.x);
f = GRAV * 1 / (dist * dist);
p.dx += Math.cos(dir) * f;
p.dy += Math.sin(dir) * f;
p.x += p.dx;
p.y += p.dy;
p.rx = ((p.x - cw ) / (p.y + h)) * h + cw;
p.ry = ((p.y - ch ) / (p.y + h)) * h * -0.051+ ch;
//p.ry = ((h-p.y) - ch) * 0.1 + ch;
p.rs = (p.s / (p.y + h)) * h
}
particles.sort(depthSort)
}
var drawParticles = function(){
var i,j,p,f,dist,dir;
// draw behind the sun
for(i = 0; i < particles.length; i ++){
p = particles[i];
if(p.y - ch < 0){
break;
}
ctx.setTransform(p.rs,0,0,p.rs,p.rx,p.ry);
ctx.drawImage(circle,-IMAGE_SIZE_HALF,-IMAGE_SIZE_HALF);
}
// draw glow for behind the sun
ctx.globalCompositeOperation = "screen";
var iw = -blurCircle.width/2;
for(j = 0; j < i; j ++){
p = particles[j];
ctx.globalAlpha = ((Math.sin(globalTime / (50 * p.v)) + 1) / 2) * 0.6 + 0.4;
var scale = (1-(Math.sin(globalTime / (50 * p.v)) + 1) / 2) * 0.6 + 0.6;
ctx.setTransform(p.rs * 1.5 * scale,0,0,p.rs * 1.5* scale,p.rx,p.ry);
ctx.drawImage(blurCircle,iw,iw);
// second pass to intensify the glow
ctx.globalAlpha = 0.7;
ctx.setTransform(p.rs * 1.1,0,0,p.rs * 1.1,p.rx,p.ry);
ctx.drawImage(blurCircle,iw,iw);
}
// draw the sun
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,cw,ch);
ctx.drawImage(sun,-sun.width/2,-sun.height/2);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "screen";
ctx.setTransform(1,0,0,1,cw,ch);
ctx.drawImage(sunBlur,-sunBlur.width/2,-sunBlur.height/2);
var scale = Math.sin(globalTime / 100) *0.5 + 1;
ctx.globalAlpha = (Math.cos(globalTime / 100) + 1) * 0.2 + 0.4;;
ctx.setTransform(1 + scale,0,0,1 + scale,cw,ch);
ctx.drawImage(sunBlur,-sunBlur.width/2,-sunBlur.height/2);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
// draw in front the sun
for(j = i; j < particles.length; j ++){
p = particles[j];
if(p.y > -h){ // don't draw past the near view plane
ctx.setTransform(p.rs,0,0,p.rs,p.rx,p.ry);
ctx.drawImage(circle,-IMAGE_SIZE_HALF,-IMAGE_SIZE_HALF);
}
}
ctx.globalCompositeOperation = "screen";
var iw = -blurCircle.width/2;
for(j = i; j < particles.length; j ++){
p = particles[j];
if(p.y > -h){ // don't draw past the near view plane
ctx.globalAlpha = ((Math.sin(globalTime / (50 * p.v)) + 1) / 2) * 0.6 + 0.4;
var scale = (1-(Math.sin(globalTime / (50 * p.v)) + 1) / 2) * 0.6 + 0.6;
ctx.setTransform(p.rs * 1.5 * scale,0,0,p.rs * 1.5* scale,p.rx,p.ry);
ctx.drawImage(blurCircle,iw,iw);
// second pass to intensify the glow
ctx.globalAlpha = 0.7;
ctx.setTransform(p.rs * 1.1,0,0,p.rs * 1.1,p.rx,p.ry);
ctx.drawImage(blurCircle,iw,iw);
}
}
ctx.globalCompositeOperation = "source-over";
}
var addParticles = function(count){
var ww = (h-10)* 2;
var cx = cw - ww/2;
var cy = ch - ww/2;
for(var i = 0; i < count; i ++){
particles.push(createParticle(cx + Math.random() * ww,cy + Math.random() * ww, Math.random() - 0.5, Math.random() - 0.5));
}
}
function display(){ // put code in here
if(particles.length === 0){
addParticles(NUM_PARTICLES);
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.drawImage(background,0,0,w,h)
updateParticles();
drawParticles();
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
}
function update(timer){ // Main update loop
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
requestAnimationFrame(update);
/** SimpleFullCanvasMouse.js end **/
I have two questions, the first being how do I access the indexes within my array separately, because my console.log of [n][0] results in two values - x and y. Secondly, for the butterfly curve, https://en.wikipedia.org/wiki/Butterfly_curve_%28transcendental%29, how would I determine the values of t? and reiterate through a certain minimum and maximum. In need of logic support.
Here's my progress so far.
/*function drawButterFly(n){
c.beginPath();
console.log(n[2])
for (var i = 0; i < n.length; i++){
if (i === 0) {
c.moveTo();
} else {
c.lineTo();
}
c.stroke();
}
}*/
function butterFly() {
var r = 5;
var N = 3;
var value = [];
for (var a = 0.2; a < 2*Math.PI; a = a + 0.1){
value.push(a);
}
var t = value[Math.floor(Math.random()*value.length)];
var cos = r*Math.cos(t)*( (Math.exp(Math.cos(t))) - (2*Math.cos(4*t)) - (Math.sin(t/12)^5) );
var sin = r*Math.sin(t)*( (Math.exp(Math.cos(t))) - (2*Math.cos(4*t)) - (Math.sin(t/12)^5) );
var n = [];
for (var u = 0; u < N; u++){
var x = sin * -u;
var y = cos * -u;
n.push([x,y]);
}
drawButterFly(n);
}
Since you're pushing an array here: n.push([x,y]) you can access the x component of the first element with n[0][0] and the y component of the same element with n[0][1]
Example:
var n = [];
n.push( ["x", "y"] );
console.log( n[0][0] );
console.log( n[0][1] );
As for the useful values of t - in the image you've shown, you'll notice that the same butterfly is drawn several times at different sizes. To draw a complete butterfly, you need to use the range for t of [0..2pi]. If you want to draw two butterflies, you need to use the range [0..4pi]. That is it's cyclic over the same period that a circle is. Unlike a circle however, each cycle doesn't draw over the previous one.
Here's a quick and nasty example:
function byId(id) {
return document.getElementById(id);
}
window.addEventListener('load', onDocLoaded, false);
function onDocLoaded(evt) {
butterFly();
}
function butterFly() {
var pointArray = [];
var stepSize = 0.05; // ~125 steps for every 360°
var upperLimit = 4 * Math.PI;
var scale = 20;
for (var t = 0.0; t < upperLimit; t += stepSize) {
var xVal = Math.sin(t) * ((Math.exp(Math.cos(t))) - (2 * Math.cos(4 * t)) - (Math.pow(Math.sin(t / 12), 5)));
var yVal = Math.cos(t) * ((Math.exp(Math.cos(t))) - (2 * Math.cos(4 * t)) - (Math.pow(Math.sin(t / 12), 5)));
pointArray.push([scale * xVal, -scale * yVal]); // -1 value since screen-y direction is opposite direction to cartesian coords y
}
drawButterFly(pointArray);
}
function drawButterFly(pointArray) {
var can = byId('myCan');
var ctx = can.getContext('2d');
var originX, originY;
originX = can.width / 2;
originY = can.height / 2;
ctx.beginPath();
for (var i = 0; i < pointArray.length; i++) {
if (i === 0) {
ctx.moveTo(originX + pointArray[i][0], originX + pointArray[i][1]);
} else {
ctx.lineTo(originX + pointArray[i][0], originY + pointArray[i][1]);
}
}
ctx.closePath();
ctx.stroke();
}
canvas {
border: solid 1px red;
}
<canvas id='myCan' width='256' height='256' />
If I'm not mistaken, the Butterfly curve is given as a pair of parametric equations, meaning you increment t to get the next (x, y) points on your curve. In other words, your t is what you should be using in place of u in your code, and the range of values for t should be 0 .. 24*pi as that's the range in which sin(t / 12) has its unique values).
Here's a version that demonstrates the drawing of the curve to a canvas:
function getPoint(t, S, O) {
var cos_t = Math.cos(t);
var factor = Math.exp(cos_t) - 2 * Math.cos(4*t) - Math.pow(Math.sin(t/12), 5);
return {
x: S * Math.sin(t) * factor + O.x,
y: S * cos_t * factor + O.y
};
}
var canvas = document.getElementById("c");
canvas.width = 300;
canvas.height = 300;
var ctx = canvas.getContext("2d");
// First path
ctx.beginPath();
ctx.strokeStyle = 'blue';
var offset = {x:150, y:120};
var scale = 40;
var maxT = 24 * Math.PI;
var p = getPoint(0, scale, offset);
ctx.moveTo(p.x, canvas.height - p.y);
for (var t = 0.01; t <= maxT; t += 0.01) {
p = getPoint(t, scale, offset);
ctx.lineTo(p.x, canvas.height - p.y);
}
ctx.stroke();
#c {
border: solid 1px black;
}
<canvas id="c"></canvas>
One thing to note: canvases have y = 0 start at the top, so you need to inverse your y (i.e. canvas.height - y) to have your curve orient correctly.
UPDATE: Added animated version
As requested by royhowie, here's an animated version, using requestAnimationFrame:
function getPoint(t, S, O) {
var cos_t = Math.cos(t);
var factor = Math.exp(cos_t) - 2 * Math.cos(4*t) - Math.pow(Math.sin(t/12), 5);
return {
x: S * Math.sin(t) * factor + O.x,
y: S * cos_t * factor + O.y
};
}
var canvas = document.getElementById("c");
canvas.width = 300;
canvas.height = 300;
var ctx = canvas.getContext("2d");
var offset = {x:150, y:120};
var scale = 40;
var maxT = 24 * Math.PI;
var animationID;
var started = false;
var t = 0;
document.getElementById('start').addEventListener('click', function(e) {
e.preventDefault();
if (!started) {
animationID = requestAnimationFrame(animate);
started = true;
}
});
document.getElementById('pause').addEventListener('click', function(e) {
e.preventDefault();
if (started) {
cancelAnimationFrame(animationID);
started = false;
}
});
function animate() {
animationID = requestAnimationFrame(animate);
var p = getPoint(t, scale, offset);
if (t === 0) {
ctx.beginPath();
ctx.strokeStyle = 'blue';
ctx.moveTo(p.x, canvas.height - p.y);
t += 0.01;
} else if (t < maxT) {
ctx.lineTo(p.x, canvas.height - p.y);
ctx.stroke();
t += 0.01;
} else {
cancelAnimationFrame(animationID);
}
}
#c {
border: solid 1px black;
}
<div>
<button id="start">Start</button>
<button id="pause">Pause</button>
</div>
<canvas id="c"></canvas>
Question 1
For an arbitrary integer i, let n[i] = [xi, yi]. xi can then be accessed via n[i][0] and yi via n[i][1]
Question 2
For values of t, I am sure you're gonna want to use sub-integer values, so I recommend using a constant increment value representing the "resolution" of your graph.
Let's call it dt. Also, I'd advise changing your variable names from single letters to something more descriptive, like min_t and max_t, and instead of n I'm going to call your array points.
function drawButterFly(points){
for (var i = 0, n = points.count; i < n; ++i) {
var x = points[i][0];
var y = points[i][1];
...
}
}
function butterFly(min_t, max_t, dt, r) {
var points = [];
for (var t = min_t; t < max_t; t+=dt){
var x = r*Math.sin(t)*...
var y = r*Math.cos(t)*...
points.push([x,y]);
}
drawButterFly(points, dt);
}
I'm not sure what the other loop inside of that function was for, but if you need it, you can adapt from the pattern above.
Usage example: butterFly(0, 10, 0.01, 3) -> t goes from 0 to 10 with an increment of 0.01, and r=3
Regarding your first question it's a better option to replace the multidimensional array containing the x and y coordinates with an object. Then when iterating over the array you can check for the object values.
So instead of:
n.push([x,y]);
you should do:
m.push({
'xPos' : x,
'yPos' : y
})
Later you can access this by m.xPos or m.yPos
Then you can access the x and y values by object literal names.
Regarding the second question: for a good pseudo code implementation of butterfly curves you might check Paul Burke site: http://paulbourke.net/geometry/butterfly/. So t in your case is:
t = i * 24.0 * PI / N;
As you see t is a parametric value which got incremented on each step when iterating over the array.