I am working with a javascript animation that shows ripples in water in html canvas.
This is the javascript code and the jfiddle link
(function(){
var canvas = document.getElementById('c'),
/** #type {CanvasRenderingContext2D} */
ctx = canvas.getContext('2d'),
width = 400,
height = 400,
half_width = width >> 1,
half_height = height >> 1,
size = width * (height + 2) * 2,
delay = 30,
oldind = width,
newind = width * (height + 3),
riprad = 3,
ripplemap = [],
last_map = [],
ripple,
texture,
line_width = 20,
step = line_width * 2,
count = height / line_width;
canvas.width = width;
canvas.height = height;
with (ctx) {
fillStyle = '#a2ddf8';
fillRect(0, 0, width, height);
fillStyle = '#07b';
save();
rotate(-0.785);
for (var i = 0; i < count; i++) {
fillRect(-width, i * step, width * 3, line_width);
}
restore();
}
texture = ctx.getImageData(0, 0, width, height);
ripple = ctx.getImageData(0, 0, width, height);
for (var i = 0; i < size; i++) {
last_map[i] = ripplemap[i] = 0;
}
/**
* Main loop
*/
function run() {
newframe();
ctx.putImageData(ripple, 0, 0);
}
/**
* Disturb water at specified point
*/
function disturb(dx, dy) {
dx <<= 0;
dy <<= 0;
for (var j = dy - riprad; j < dy + riprad; j++) {
for (var k = dx - riprad; k < dx + riprad; k++) {
ripplemap[oldind + (j * width) + k] += 128;
}
}
}
/**
* Generates new ripples
*/
function newframe() {
var a, b, data, cur_pixel, new_pixel, old_data;
var t = oldind; oldind = newind; newind = t;
var i = 0;
// create local copies of variables to decrease
// scope lookup time in Firefox
var _width = width,
_height = height,
_ripplemap = ripplemap,
_last_map = last_map,
_rd = ripple.data,
_td = texture.data,
_half_width = half_width,
_half_height = half_height;
for (var y = 0; y < _height; y++) {
for (var x = 0; x < _width; x++) {
var _newind = newind + i, _mapind = oldind + i;
data = (
_ripplemap[_mapind - _width] +
_ripplemap[_mapind + _width] +
_ripplemap[_mapind - 1] +
_ripplemap[_mapind + 1]) >> 1;
data -= _ripplemap[_newind];
data -= data >> 5;
_ripplemap[_newind] = data;
//where data=0 then still, where data>0 then wave
data = 1024 - data;
old_data = _last_map[i];
_last_map[i] = data;
if (old_data != data) {
//offsets
a = (((x - _half_width) * data / 1024) << 0) + _half_width;
b = (((y - _half_height) * data / 1024) << 0) + _half_height;
//bounds check
if (a >= _width) a = _width - 1;
if (a < 0) a = 0;
if (b >= _height) b = _height - 1;
if (b < 0) b = 0;
new_pixel = (a + (b * _width)) * 4;
cur_pixel = i * 4;
_rd[cur_pixel] = _td[new_pixel];
_rd[cur_pixel + 1] = _td[new_pixel + 1];
_rd[cur_pixel + 2] = _td[new_pixel + 2];
}
++i;
}
}
}
canvas.onmousemove = function(/* Event */ evt) {
disturb(evt.offsetX || evt.layerX, evt.offsetY || evt.layerY);
};
setInterval(run, delay);
// generate random ripples
var rnd = Math.random;
setInterval(function() {
disturb(rnd() * width, rnd() * height);
}, 700);
})();
The issue is the ripple spills over from one side of the canvas and animates on the opposite side, I want to keep them from moving into the opposite sides.
Thanks!
Stopping waves at edges
To stop the propagation of the waves across the edges you need to limit the function that adds the disturbance so that it does not write past the edges.
So change...
function disturb(dx, dy) {
dx <<= 0;
dy <<= 0;
for (var y = dy - riprad; y < dy + riprad; y++) {
for (var x = dx - riprad; x < dx + riprad; x++) {
ripplemap[oldind + (y * width) + x] += 128;
}
}
}
to...
function disturb(dx, dy) {
dx <<= 0;
dy <<= 0;
for (var y = dy - riprad; y < dy + riprad; y++) {
for (var x = dx - riprad; x < dx + riprad; x++) {
// don't go past the edges.
if(y >= 0 && y < height && x >= 0 && x < width){
ripplemap[oldind + (y * width) + x] += 128;
}
}
}
}
And you also need to change the wave propagation that is in the function newFrame. The propagation is controlled by the value in data. A value of zero means no wave and no propagation. So test if the pixel is on the edge. If the pixel is an edge pixel then just stop the wave by setting data to zero. To save CPU cycles I have added to vars h, w that are height - 1 and width - 1 so you don't have to perform the subtraction twice for every pixel.
Change the code in the function newFrame from...
for (var y = 0; y < _height; y++) {
for (var x = 0; x < _width; x++) {
var _newind = newind + i, _mapind = oldind + i;
data = (
_ripplemap[_mapind - _width] +
_ripplemap[_mapind + _width] +
_ripplemap[_mapind - 1] +
_ripplemap[_mapind + 1]) >> 1;
}
To...
var w = _width - 1; // to save having to subtract 1 for each pixel
var h = _height - 1; // dito
for (var y = 0; y < _height; y++) {
for (var x = 0; x < _width; x++) {
var _newind = newind + i, _mapind = oldind + i;
// is the pixel on the edge
if(y === 0 || x === 0 || y === h || x === w){
data = 0; // yes edge pixel so stop propagation.
}else{
// not on the edge so just do as befor.
data = (
_ripplemap[_mapind - _width] +
_ripplemap[_mapind + _width] +
_ripplemap[_mapind - 1] +
_ripplemap[_mapind + 1]) >> 1;
}
This is not perfect as it will dampen the waves bouncing from the sides but you don't have much CPU time so it is a good solution.
Some notes.
Use requestAnimationFrame rather than setInterval(run, delay) to time the animation as you will cause some devices to crash the page with the code you currently have, if the device can not handle the CPU load. requestAnimationFrame will stop this happening.
Change the run function to
function run() {
newframe();
ctx.putImageData(ripple, 0, 0);
requestAnimationFrame(run);
}
At bottom of code remove setInterval(run,delay); and add
requestAnimationFrame(run);
And change the second setInterval to
var drops = function() {
disturb(rnd() * width, rnd() * height);
setTimeout(drops,700)
};
drops();
NEVER use setInterval, especially for this type of CPU intensive code. You will cause many machines to crash the page. Us setTimeout or requestAnimationFrame instead.
Also remove the with statement where you create the texture.
Related
Im noob with p5j and i need help from someone please! :-(
On the website i must upload it , they not allow the external links.
and i need to change the "let url" to a png/jpg local link ( not external link ).
i dont know what the solution is ( maybe something similar with: "loadImage" or something.... please help me :-) )
Thank you so much and have a blessed week!
Here is the code:
//let url = "https://coolors.co/3a2e39-1e555c-f4d8cd-edb183-f15152";
let url = "https://blog.logrocket.com/wp-content/uploads/2022/03/Creating-animations-p5-js.png";
let palette;
let font;
function preload() {
font = loadFont("https://openprocessing.org/sketch/1359269/files/Happy Monsters.ttf");
}
function setup() {
createCanvas(1112, 834);
colorMode(HSB, 360, 100, 100, 100);
angleMode(DEGREES);
palette = createPalette(url);
background(10);
}
function draw() {
//background(0, 0, 90);
let offset = 100//width / 100;
let margin = 0; //offset / 5;
let cells = 1//int(random(2, 8));
let d = (width - offset * 2 - margin * (cells - 1)) / cells;
for (let j = 0; j < cells; j++) {
for (let i = 0; i < cells; i++) {
let x = offset + i * (d + margin) + d / 2;
let y = offset + j * (d + margin) + d / 2;
drawFancyShape(x, y, d, palette.concat());
}
}
frameRate(0.5);
noLoop();
}
function drawFancyShape(x, y, d, colors, char = String.fromCodePoint(65 + int(random(26)))) {
let g = createGraphics(d, d);
let g2 = createGraphics(d, d);
colors = shuffle(colors);
let c0 = colors[0];
colors.splice(0, 1);
let ratio = 0.2;
let xStep, yStep;
for (let y = 0; y < g.height; y += yStep) {
yStep = random(ratio, 1 - ratio) * g.height / 2;
if (y + yStep > g.height) yStep = g.height - y;
if (g.height - y - yStep < g.height / 100) yStep = g.height - y;
for (let x = 0; x < g.width; x += xStep) {
xStep = random(ratio, 1 - ratio) * g.width / 2;
if (x + xStep > g.width) xStep = g.width - x;
if (g.width - x - xStep < g.width / 100) xStep = g.width - x;
let r = [];
for (let i = 0; i < 4; i++) {
r.push(int(random(5)) * max(xStep, yStep) / 4);
}
g.rectMode(CENTER);
g.fill(random(colors));
g.noStroke();
g.rect(x + xStep / 2, y + yStep / 2, xStep - 2, yStep - 2, r[0], r[1], r[2], r[3]);
}
}
g2.textSize(g.width * 0.6);
g2.textAlign(CENTER, CENTER);
g2.textFont(font)
g2.textStyle(BOLD);
g2.fill(c0);
// g2.stroke(0);
g2.text(char, g.width / 2, g.height / 2 - g.height / 8);
let g_tmp = g.get();
let g2_tmp = g2.get();
g_tmp.mask(g2_tmp);
// g_tmp.mask(g2_tmp);
drawingContext.shadowColor = color(0, 0, 0, 33);
drawingContext.shadowBlur = d / 10;
push();
translate(x, y);
imageMode(CENTER);
// image(g, 0, 0);
let scl = 1.1;
image(g2, 0, 0, g2.width * scl, g2.height * scl);
image(g_tmp, 0, 0, g_tmp.width * scl, g_tmp.height * scl);
pop();
}
function createPalette(_url) {
let slash_index = _url.lastIndexOf('/');
let pallate_str = _url.slice(slash_index + 1);
let arr = pallate_str.split('-');
for (let i = 0; i < arr.length; i++) {
arr[i] = color('#' + arr[i]);
}
return arr;
}
// save jpg
let lapse = 0; // mouse timer
function mousePressed(){
// prevents mouse press from registering twice
if (millis() - lapse > 400){
save("img_" + month() + '-' + day() + '_' + hour() + '-' + minute() + '-' + second() + ".jpg");
lapse = millis();
}
}
I have been trying to write a simulator using the HTML5 Canvas. I have the following code to generate equipotential lines for electric fields.
function drawEqLines(charges) {
// don't bother if there aren't any charges
if (charges.length == 0) return;
// the Charge class contains the charge of the charge as well as its x and y coordinates
var fieldFilled = [];
for (var i = 0; i < 10; i++) {
fieldFilled.push([]);
for (var j = 0; j < 10; j++) {
fieldFilled[i].push(false);
}
}
var calculatedFields = [];
var maxForce = 0;
for (var i = 0; i < fieldFilled.length; i++) {
var direction = 1;
for (var jj=0; jj < fieldFilled[i].length; jj++) {
if (!fieldFilled[i][jj]) {
//create a path here
//Iterate at most 2 times in case the surface gets out of the area
for (var circleTimes = 0; circleTimes < 3; circleTimes+=2) {
//Define the center of the current block as a starting point of the surface
/*
horizontalBlock and verticalBlock are the width and height of the canvas divided into 10 different sections.
_BlockHalf is half of one of the blocks
*/
var curX = i * this.horizontalBlock + this.horizontalBlockHalf;
var curY = jj * this.verticalBlock + this.verticalBlockHalf;
// Point is a class that contains an x and y value
var curPt = new Point(curX, curY);
var direction = 1 - circleTimes;
var dots = [];
dots.push(curPt);
//Superposition the fields from all charges, and get the resulting force vector
var dirX = 0;
var dirY = 0;
var totalForce = 0;
for (var j = 0; j < charges.length; j++) {
var distX = curPt.x - charges[j].x;
var distY = curPt.y - charges[j].y;
var distanceSq = distX * distX + distY * distY;
var force = charges[j].charge / distanceSq;
var distanceFactor = force / Math.sqrt(distanceSq);
//Measure the initial force in order to match the equipotential surface points
totalForce += force;
dirX += distX * distanceFactor;
dirY += distY * distanceFactor;
}
//Maximum 2000 dots per surface line
var times = 2000;
while (times-- > 0) {
var dirTotal = Math.sqrt(dirX * dirX + dirY * dirY);
var stepX = dirX / dirTotal;
var stepY = dirY / dirTotal;
//The equipotential surface moves normal to the force vector
curPt.x = curPt.x + direction * 6 * stepY;
curPt.y = curPt.y - direction * 6 * stepX;
/*
*
* ***********************LOG LINE HERE***********************************
*
*/
// this prints out unique values
console.log(curPt.x + ", " + curPt.y);
//Correct the exact point a bit to match the initial force as near it can
var minForceIndex = -1;
var minForceDiff = 0;
var minDirX = 0;
var minDirY = 0;
var minCurX = 0;
var minCurY = 0;
curPt.x -= 3 * stepX;
curPt.y -= 3 * stepY;
for (var pointIndex = 0; pointIndex < 7; pointIndex++, curPt.x += stepX, curPt.y += stepY) {
dirX = 0;
dirY = 0;
var forceSum = 0;
for (var j = 0; j < charges.length; j++) {
var distX = curPt.x - charges[j].x;
var distY = curPt.y - charges[j].y;
var distanceSq = distX ** 2 + distY ** 2;
var force = charges[j].charge / distanceSq;
var distanceFactor = force / Math.sqrt(distanceSq);
//Measure the initial force in order to match the equipotential surface points
forceSum += force;
dirX += distX * distanceFactor;
dirY += distY * distanceFactor;
}
var forceDiff = Math.abs(forceSum - totalForce);
if (minForceIndex == -1 || forceDiff < minForceDiff) {
minForceIndex = pointIndex;
minForceDiff = forceDiff;
minDirX = dirX;
minDirY = dirY;
minCurX = curPt.x;
minCurY = curPt.y;
} else {
break;
}
}
//Set the corrected equipotential point
curPt.x = minCurX;
curPt.y = minCurY;
dirX = minDirX;
dirY = minDirY;
//Mark the containing block as filled with a surface line.
var indI = parseInt(curPt.x / this.horizontalBlock);
var indJ = parseInt(curPt.y / this.verticalBlock);
if (indI >= 0 && indI < fieldFilled.length) {
if (indJ >= 0 && indJ < fieldFilled[indI].length) {
fieldFilled[indI][indJ] = true;
}
}
//Add the dot to the line (was commented out when I added the other log)
dots.push(curPt);
if (dots.length > 5) {
//If got to the begining, a full circle has been made, terminate further iterations
if (indI == i && indJ == jj) {
distX = dots[0].x - curPt.x;
distY = dots[0].y - curPt.y;
if (distX * distX + distY * distY <= 49) {
dots.push(new Point(dots[0].x, dots[0].y));
times = 0;
circleTimes = 3;
}
}
//If got out of the area, terminate further iterations for this turn.
if (curPt.x < 0 || curPt.x > this.canvas.width || curPt.y < 0 || curPt.y > this.canvas.height) {
times = 0;
}
}
}
calculatedFields.push([totalForce, dots]);
maxForce = Math.max(maxForce, Math.abs(totalForce));
}
}
}
}
console.log(calculatedFields);
// draw the lines from the arrays of dots here...
}
The result of this code is an array that looks like this:
[
[
0.000007927962725256215,
[
// as you can see, these points are all the same
{x: 114.16365557137308, y: 402.6147544331975},
{x: 114.16365557137308, y: 402.6147544331975},
{x: 114.16365557137308, y: 402.6147544331975},
{x: 114.16365557137308, y: 402.6147544331975},
// etc...
]
],
[
0.000006140401131528559,
[
{x: -1.0201768243546043, y: 323.28955989370445},
{x: -1.0201768243546043, y: 323.28955989370445},
{x: -1.0201768243546043, y: 323.28955989370445},
{x: -1.0201768243546043, y: 323.28955989370445},
// etc...
]
]
]
These points should be unique, however, it seems to just generate the same point over and over again. For some reason, it does decide to stop after a reasonable number of iterations, as if it is doing the calculations correctly but just putting the same point into the array anyway.
Why is this happening? How can I fix it?
Update: Here are the classes that I am using:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Charge {
constructor(charge, x, y) {
this.charge = charge;
this.x = x;
this.y = y;
}
}
Also, the charges array is fine, because I use it in a similar method which does work correctly. An example for this would be:
[
{charge: 6, x: 50, y: 50},
{charge: -4, x: 70, y: 90.5},
// etc...
]
Update 2:
I have tried adding the log line indicated in the above code, which prints out only unique values. However, even if I try pushing the point to the array here it still results in the same issue.
Update 3:
Added this runnable snippet:
const height = 400; // for example
const width = 600; // also for example
const horizontalBlock = width / 10;
const verticalBlock = height / 10;
const horizontalBlockHalf = horizontalBlock / 2;
const verticalBlockHalf = verticalBlock / 2;
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
function drawEqLines(charges) {
// don't bother if there aren't any charges
if (charges.length == 0) return;
// the Charge class contains the charge of the charge as well as its x and y coordinates
var fieldFilled = [];
for (var i = 0; i < 10; i++) {
fieldFilled.push([]);
for (var j = 0; j < 10; j++) {
fieldFilled[i].push(false);
}
}
var calculatedFields = [];
var maxForce = 0;
for (var i = 0; i < fieldFilled.length; i++) {
var direction = 1;
for (var jj=0; jj < fieldFilled[i].length; jj++) {
if (!fieldFilled[i][jj]) {
//create a path here
//Iterate at most 2 times in case the surface gets out of the area
for (var circleTimes = 0; circleTimes < 3; circleTimes+=2) {
//Define the center of the current block as a starting point of the surface
/*
horizontalBlock and verticalBlock are the width and height of the canvas divided into 10 different sections.
_BlockHalf is half of one of the blocks
*/
var curX = i * horizontalBlock + horizontalBlockHalf;
var curY = jj * verticalBlock + verticalBlockHalf;
// Point is a class that contains an x and y value
var curPt = new Point(curX, curY);
var direction = 1 - circleTimes;
var dots = [];
dots.push(curPt);
//Superposition the fields from all charges, and get the resulting force vector
var dirX = 0;
var dirY = 0;
var totalForce = 0;
for (var j = 0; j < charges.length; j++) {
var distX = curPt.x - charges[j].x;
var distY = curPt.y - charges[j].y;
var distanceSq = distX * distX + distY * distY;
var force = charges[j].charge / distanceSq;
var distanceFactor = force / Math.sqrt(distanceSq);
//Measure the initial force in order to match the equipotential surface points
totalForce += force;
dirX += distX * distanceFactor;
dirY += distY * distanceFactor;
}
//Maximum 2000 dots per surface line
var times = 2000;
while (times-- > 0) {
var dirTotal = Math.sqrt(dirX * dirX + dirY * dirY);
var stepX = dirX / dirTotal;
var stepY = dirY / dirTotal;
//The equipotential surface moves normal to the force vector
curPt.x = curPt.x + direction * 6 * stepY;
curPt.y = curPt.y - direction * 6 * stepX;
/*
*
* ***********************LOG LINE HERE***********************************
*
*/
// this prints out unique values
console.log(curPt.x + ", " + curPt.y);
//Correct the exact point a bit to match the initial force as near it can
var minForceIndex = -1;
var minForceDiff = 0;
var minDirX = 0;
var minDirY = 0;
var minCurX = 0;
var minCurY = 0;
curPt.x -= 3 * stepX;
curPt.y -= 3 * stepY;
for (var pointIndex = 0; pointIndex < 7; pointIndex++, curPt.x += stepX, curPt.y += stepY) {
dirX = 0;
dirY = 0;
var forceSum = 0;
for (var j = 0; j < charges.length; j++) {
var distX = curPt.x - charges[j].x;
var distY = curPt.y - charges[j].y;
var distanceSq = distX ** 2 + distY ** 2;
var force = charges[j].charge / distanceSq;
var distanceFactor = force / Math.sqrt(distanceSq);
//Measure the initial force in order to match the equipotential surface points
forceSum += force;
dirX += distX * distanceFactor;
dirY += distY * distanceFactor;
}
var forceDiff = Math.abs(forceSum - totalForce);
if (minForceIndex == -1 || forceDiff < minForceDiff) {
minForceIndex = pointIndex;
minForceDiff = forceDiff;
minDirX = dirX;
minDirY = dirY;
minCurX = curPt.x;
minCurY = curPt.y;
} else {
break;
}
}
//Set the corrected equipotential point
curPt.x = minCurX;
curPt.y = minCurY;
dirX = minDirX;
dirY = minDirY;
//Mark the containing block as filled with a surface line.
var indI = parseInt(curPt.x / this.horizontalBlock);
var indJ = parseInt(curPt.y / this.verticalBlock);
if (indI >= 0 && indI < fieldFilled.length) {
if (indJ >= 0 && indJ < fieldFilled[indI].length) {
fieldFilled[indI][indJ] = true;
}
}
//Add the dot to the line (was commented out when I added the other log)
dots.push(curPt);
if (dots.length > 5) {
//If got to the begining, a full circle has been made, terminate further iterations
if (indI == i && indJ == jj) {
distX = dots[0].x - curPt.x;
distY = dots[0].y - curPt.y;
if (distX * distX + distY * distY <= 49) {
dots.push(new Point(dots[0].x, dots[0].y));
times = 0;
circleTimes = 3;
}
}
//If got out of the area, terminate further iterations for this turn.
if (curPt.x < 0 || curPt.x > width || curPt.y < 0 || curPt.y > height) {
times = 0;
}
}
}
calculatedFields.push([totalForce, dots]);
maxForce = Math.max(maxForce, Math.abs(totalForce));
}
}
}
}
console.log(calculatedFields);
// draw the lines from the arrays of dots here...
}
var charges = [
{charge: 6, x: 50, y: 75},
{charge: -5, x: 60, y: 254.5}
]
drawEqLines(charges);
You have a loop with code that says: dots.push(curPt)
In that loop, you reassign the x and y properties of the curPt object, but it's the same object that you keep pushing on each iteration.
You will have to create a new Point object inside the loop if you want to push different points into the array.
I create this animaiton using canvas and converting svg's to canvas shapes. Most times it runs it heats up my computer and the fan starts going.
Just wondering if there is something about the code, html5 canvas, canvas paths or the animation recursion that is so intensive?
View on codepen: https://codepen.io/benbyford-the-lessful/pen/ZjjVdR?editors=1010#
// check program is being run
console.log('bg animation running...');
// setup canvas
var canvas = document.getElementById('bgCanvas');
var ctx = canvas.getContext('2d')
// redo this - canvas size
//
var width = window.innerWidth,
height = window.innerHeight;
canvas.width = width * 2;
canvas.height = height * 2;
var gridSquareWidth = 20;
var gridWidth = (width * 2) / gridSquareWidth,
gridHeight = (height * 2) / gridSquareWidth;
var grid = [];
// create default grid array
for (var x = 0; x < gridWidth; x++) {
grid[x] = [];
for (var y = 0; y < gridHeight; y++) {
var rand = getRandomArbitrary(0,5);
var rand2 = getRandomArbitrary(0,2);
if(rand2 == 1 || x < (gridWidth / 4) || x > (gridWidth / 2) || y < (gridHeight / 4) || y > (gridHeight / 2)){
rand--;
}
if(rand > 2) grid[x][y] = 1;
}
}
//
// main update function
//
var animationSpeed = 0.1;
var animationSpeedCount = 0;
var running = true;
function update(dt) {
if(running){
animationSpeedCount += dt;
if(animationSpeedCount > animationSpeed){
moveGrid();
animationSpeedCount = 0;
}
draw();
}
}
var noOfFrames = 3;
var waveOffset = 15;
var increment = 0;
function moveGrid() {
var x = increment;
var x2 = increment - noOfFrames - waveOffset;
// add frmae wave
for (var i = 0; i < noOfFrames; i++) {
moveONeFrameForward(x, true);
x--;
}
// go back frmae wave
for (var i = 0; i < noOfFrames; i++) {
moveONeFrameForward(x2, false);
x2--;
}
// var x column, add of subtract by 1
function moveONeFrameForward(x, add){
if(x < 0){
x = Math.ceil(gridWidth + x);
}
if(x > 0 && x < gridWidth){
for (var y = 0; y < gridHeight; y++) {
if(grid[x][y] > 0){
if(add){
grid[x][y] = grid[x][y] + 1;
}else{
if(grid[x][y] > 1) grid[x][y] = grid[x][y] - 1;
}
}
}
}
}
// increment column
increment += 1;
if(increment > gridWidth){
increment = 0;
// stop running
// running = false;
}
}
var fills = ["#eeeeee","#efefef","#fefefe","#ffffff"];
function draw() {
// clear canvas to white
ctx.fillStyle = '#dddddd';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (var x = 0; x < gridWidth; x++) {
for (var y = 0; y < gridHeight; y++) {
var offsetX = x * gridSquareWidth;
var offsetY = y * gridSquareWidth;
var frame = 0;
switch (grid[x][y]) {
case 1:
frame = 1
break;
case 2:
frame = 2;
break;
case 3:
frame = 3;
break;
case 4:
frame = 4;
break;
default:
}
if(frame) drawframe(ctx, frame, offsetX, offsetY, fills);
}
}
}
// The main game loop
var lastTime = 0;
function gameLoop() {
var now = Date.now();
var dt = (now - lastTime) / 1000.0;
update(dt);
lastTime = now;
window.requestAnimationFrame(gameLoop);
};
// start game
gameLoop();
//
// UTILITIES
//
// cross browser requestAnimationFrame - https://gist.github.com/mrdoob/838785
if ( !window.requestAnimationFrame ) {
window.requestAnimationFrame = ( function() {
return window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(
/* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) {
window.setTimeout( callback, 1000 / 60 );
};
})();
}
function getRandomArbitrary(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
var frame1Center = 4.1;
var frame2Center = 2.1;
function drawframe(ctx, frame, x, y, fills) {
ctx.strokeStyle = 'rgba(0,0,0,0)';
ctx.lineCap = 'butt';
ctx.lineJoin = 'miter';
ctx.miterLimit = 4;
ctx.fillStyle = fills[frame-1];
switch (frame) {
case 1:
ctx.beginPath();
ctx.moveTo(3.1+x+frame1Center,0+y);
ctx.lineTo(0.6+x+frame1Center,0+y);
ctx.bezierCurveTo(0.3+x+frame1Center,0+y,0+x+frame1Center,0.3+y,0+x+frame1Center,0.6+y);
ctx.lineTo(0.3+x+frame1Center,12.1+y);
ctx.bezierCurveTo(0.3+x+frame1Center,12.4+y,0.6+x+frame1Center,12.7+y,0.8999999999999999+x+frame1Center,12.7+y);
ctx.lineTo(3.4+x+frame1Center,12.7+y);
ctx.bezierCurveTo(3.6999999999999997+x+frame1Center,12.7+y,4+x+frame1Center,12.399999999999999+y,4+x+frame1Center,12.1+y);
ctx.lineTo(4+x+frame1Center,0.6+y);
ctx.bezierCurveTo(4.1+x+frame1Center,0.3+y,3.7+x+frame1Center,0+y,3.1+x+frame1Center,0+y);
ctx.closePath();
ctx.fill();
ctx.stroke();
break;
case 2 || 6:
ctx.beginPath();
ctx.moveTo(4.4+x+frame2Center,0+y);
ctx.bezierCurveTo(3.1+x+frame2Center,0+y,0+x+frame2Center,0.8+y,0+x+frame2Center,2.1+y);
ctx.bezierCurveTo(0+x+frame2Center,3.4000000000000004+y,0.3+x+frame2Center,12.5+y,1.6+x+frame2Center,12.799999999999999+y);
ctx.bezierCurveTo(2.8+x+frame2Center,13+y,6+x+frame2Center,12+y,6+x+frame2Center,10.7+y);
ctx.bezierCurveTo(6+x+frame2Center,9.1+y,5.7+x+frame2Center,0+y,4.4+x+frame2Center,0+y);
ctx.closePath();
ctx.fill();
ctx.stroke();
break;
case 3 || 5:
ctx.beginPath();
ctx.moveTo(5.2+x,0 +y);
ctx.bezierCurveTo(7.5 +x,0+y,9.3+x,6.5+y,9.3 +x,8.7+y);
ctx.bezierCurveTo(9.3+x,10.899999999999999+y,6.300000000000001+x,12.799999999999999+y,4.1000000000000005+x,12.799999999999999+y);
ctx.bezierCurveTo(1.9000000000000004+x,12.799999999999999+y,0+x,6.3+y,0+x,4.1+y);
ctx.bezierCurveTo(0+x,1.8999999999999995+y,3+x,0+y,5.2+x,0+y);
ctx.closePath();
ctx.fill();
ctx.stroke();
break;
case 4:
ctx.beginPath();
ctx.arc(5.9+x,6.3+y,5.8,0,6.283185307179586,true);
ctx.closePath();
ctx.fill();
ctx.stroke();
break;
default:
}
};
It's a bit hard to tell exactly "why", but there are definitely some things that could be improved.
First, you are drawing twice as big as what is needed.
Set you canvas size to the rendered one, and you'll probably see a big improvement in performances.
Then, you are drawing a lot of sub-path at every draw (and setting a lot of times the context's properties for nothing).
You could try to merge all these sub-paths in bigger ones, grouped by fillStyle, so that the rasterizer works only four times per frame. This can also improve performances a bit.
But the approach I would personally take, is to pre-render all the 4 different states on 4 different canvases. Then, use only drawImage to draw the required strip.
In best case, you end up with only 4 calls to drawImage, in worth one, with 8 calls.
Here is a rough proof of concept:
// setup canvas
var canvas = document.getElementById('bgCanvas');
var ctx = canvas.getContext('2d')
// don't set it twice as big as needed
var width = window.innerWidth,
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
var gridSquareWidth = 10;
var gridWidth = (width) / gridSquareWidth,
gridHeight = (height) / gridSquareWidth;
var grid = [];
// create default grid array
for (var x = 0; x < gridWidth; x++) {
grid[x] = [];
for (var y = 0; y < gridHeight; y++) {
var rand = getRandomArbitrary(0, 5);
var rand2 = getRandomArbitrary(0, 2);
if (rand2 == 1 || x < (gridWidth / 4) || x > (gridWidth / 2) || y < (gridHeight / 4) || y > (gridHeight / 2)) {
rand--;
}
if (rand > 2) grid[x][y] = 1;
}
}
var fills = ["#eeeeee", "#efefef", "#fefefe", "#ffffff"];
var frame1Center = 4.1;
var frame2Center = 2.1;
// the 4 points drawers
var drawers = [draw0, draw1, draw2, draw3];
// initialise our four possible states
var states = [
initState(0),
initState(1),
initState(2),
initState(3)
];
//
// main update function
//
var running = true;
var speed = 2;
var waveWidth = 200;
var waveMargin = gridSquareWidth * 4;
var waveStart = 0;
var waveEnd = waveWidth;
// start game
update();
function initState(status) {
var c = canvas.cloneNode();
var ctx = c.getContext('2d');
ctx.scale(0.5, 0.5); // to circumvent values being set for scale(2)
ctx.beginPath(); // single path
ctx.fillStyle = fills[status];
for (var x = 0; x < gridWidth; x++) {
for (var y = 0; y < gridHeight; y++) {
if (grid[x][y]) {
drawers[status](ctx, x * gridSquareWidth * 2, y * gridSquareWidth * 2);
}
}
}
ctx.fill(); // single fill
return c;
}
function draw0(ctx, x, y) {
ctx.moveTo(3.1 + x + frame1Center, 0 + y);
ctx.lineTo(0.6 + x + frame1Center, 0 + y);
ctx.bezierCurveTo(0.3 + x + frame1Center, 0 + y, 0 + x + frame1Center, 0.3 + y, 0 + x + frame1Center, 0.6 + y);
ctx.lineTo(0.3 + x + frame1Center, 12.1 + y);
ctx.bezierCurveTo(0.3 + x + frame1Center, 12.4 + y, 0.6 + x + frame1Center, 12.7 + y, 0.8999999999999999 + x + frame1Center, 12.7 + y);
ctx.lineTo(3.4 + x + frame1Center, 12.7 + y);
ctx.bezierCurveTo(3.6999999999999997 + x + frame1Center, 12.7 + y, 4 + x + frame1Center, 12.399999999999999 + y, 4 + x + frame1Center, 12.1 + y);
ctx.lineTo(4 + x + frame1Center, 0.6 + y);
ctx.bezierCurveTo(4.1 + x + frame1Center, 0.3 + y, 3.7 + x + frame1Center, 0 + y, 3.1 + x + frame1Center, 0 + y);
ctx.closePath();
}
function draw1(ctx, x, y) {
ctx.moveTo(4.4 + x + frame2Center, 0 + y);
ctx.bezierCurveTo(3.1 + x + frame2Center, 0 + y, 0 + x + frame2Center, 0.8 + y, 0 + x + frame2Center, 2.1 + y);
ctx.bezierCurveTo(0 + x + frame2Center, 3.4000000000000004 + y, 0.3 + x + frame2Center, 12.5 + y, 1.6 + x + frame2Center, 12.799999999999999 + y);
ctx.bezierCurveTo(2.8 + x + frame2Center, 13 + y, 6 + x + frame2Center, 12 + y, 6 + x + frame2Center, 10.7 + y);
ctx.bezierCurveTo(6 + x + frame2Center, 9.1 + y, 5.7 + x + frame2Center, 0 + y, 4.4 + x + frame2Center, 0 + y);
ctx.closePath();
}
function draw2(ctx, x, y) {
ctx.moveTo(5.2 + x, 0 + y);
ctx.bezierCurveTo(7.5 + x, 0 + y, 9.3 + x, 6.5 + y, 9.3 + x, 8.7 + y);
ctx.bezierCurveTo(9.3 + x, 10.899999999999999 + y, 6.300000000000001 + x, 12.799999999999999 + y, 4.1000000000000005 + x, 12.799999999999999 + y);
ctx.bezierCurveTo(1.9000000000000004 + x, 12.799999999999999 + y, 0 + x, 6.3 + y, 0 + x, 4.1 + y);
ctx.bezierCurveTo(0 + x, 1.8999999999999995 + y, 3 + x, 0 + y, 5.2 + x, 0 + y);
ctx.closePath();
}
function draw3(ctx, x, y) {
ctx.moveTo(5.9 + x, 6.3 + y);
ctx.arc(5.9 + x, 6.3 + y, 5.8, 0, 2 * Math.PI);
}
function update(dt) {
if (running) {
draw();
moveGrid();
}
window.requestAnimationFrame(update);
}
function moveGrid() {
waveStart = (waveStart + speed) % canvas.width;
waveEnd = (waveStart + waveWidth) % canvas.width;
}
function draw() {
ctx.fillStyle = '#dddddd';
ctx.fillRect(0, 0, canvas.width, canvas.height);
var x = 0;
// the roll logic is a bit dirty... sorry.
if (waveEnd < waveStart) {
x = waveEnd - waveWidth;
drawStrip(1, x, waveMargin);
x = waveEnd - waveWidth + waveMargin;
drawStrip(3, x, (waveWidth - (waveMargin * 2)));
x = waveEnd - waveMargin;
drawStrip(2, x, waveMargin);
x = waveEnd;
}
drawStrip(0, x, waveStart - x);
drawStrip(1, waveStart, waveMargin);
drawStrip(3, waveStart + waveMargin, waveWidth - (waveMargin * 2));
drawStrip(2, waveStart + (waveWidth - waveMargin), waveMargin);
drawStrip(0, waveEnd, canvas.width - Math.max(waveEnd, waveStart));
}
function drawStrip(state, x, w) {
if(x < 0) w = w + x;
if (w <= 0) return;
x = Math.max(x, 0);
ctx.drawImage(states[state],
Math.max(x, 0), 0, w, canvas.height,
Math.max(x, 0), 0, w, canvas.height
);
}
function getRandomArbitrary(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
:root,body,canvas {margin: 0}
<canvas id="bgCanvas"></canvas>
How to achieve a similar effect on an image instead of raw canvas?
/**
* Water ripple effect.
* Original code (Java) by Neil Wallis
* #link http://www.neilwallis.com/java/water.html
*
* #author Sergey Chikuyonok (serge.che#gmail.com)
* #link http://chikuyonok.ru
*/
(function(){
var canvas = document.getElementById('c'),
/** #type {CanvasRenderingContext2D} */
ctx = canvas.getContext('2d'),
width = 400,
height = 400,
half_width = width >> 1,
half_height = height >> 1,
size = width * (height + 2) * 2,
delay = 30,
oldind = width,
newind = width * (height + 3),
riprad = 3,
mapind,
ripplemap = [],
last_map = [],
ripple,
texture,
line_width = 20,
step = line_width * 2,
count = height / line_width;
canvas.width = width;
canvas.height = height;
/*
* Water ripple demo can work with any bitmap image
* (see example here: http://media.chikuyonok.ru/ripple/)
* But I need to draw simple artwork to bypass 1k limitation
*/
with (ctx) {
fillStyle = '#a2ddf8';
fillRect(0, 0, width, height);
fillStyle = '#07b';
save();
rotate(-0.785);
for (var i = 0; i < count; i++) {
fillRect(-width, i * step, width * 3, line_width);
}
restore();
}
texture = ctx.getImageData(0, 0, width, height);
ripple = ctx.getImageData(0, 0, width, height);
for (var i = 0; i < size; i++) {
last_map[i] = ripplemap[i] = 0;
}
/**
* Main loop
*/
function run() {
newframe();
ctx.putImageData(ripple, 0, 0);
}
/**
* Disturb water at specified point
*/
function disturb(dx, dy) {
dx <<= 0;
dy <<= 0;
for (var j = dy - riprad; j < dy + riprad; j++) {
for (var k = dx - riprad; k < dx + riprad; k++) {
ripplemap[oldind + (j * width) + k] += 512;
}
}
}
/**
* Generates new ripples
*/
function newframe() {
var i, a, b, data, cur_pixel, new_pixel, old_data;
i = oldind;
oldind = newind;
newind = i;
i = 0;
mapind = oldind;
// create local copies of variables to decrease
// scope lookup time in Firefox
var _width = width,
_height = height,
_ripplemap = ripplemap,
_mapind = mapind,
_newind = newind,
_last_map = last_map,
_rd = ripple.data,
_td = texture.data,
_half_width = half_width,
_half_height = half_height;
for (var y = 0; y < _height; y++) {
for (var x = 0; x < _width; x++) {
data = (
_ripplemap[_mapind - _width] +
_ripplemap[_mapind + _width] +
_ripplemap[_mapind - 1] +
_ripplemap[_mapind + 1]) >> 1;
data -= _ripplemap[_newind + i];
data -= data >> 5;
_ripplemap[_newind + i] = data;
//where data=0 then still, where data>0 then wave
data = 1024 - data;
old_data = _last_map[i];
_last_map[i] = data;
if (old_data != data) {
//offsets
a = (((x - _half_width) * data / 1024) << 0) + _half_width;
b = (((y - _half_height) * data / 1024) << 0) + _half_height;
//bounds check
if (a >= _width) a = _width - 1;
if (a < 0) a = 0;
if (b >= _height) b = _height - 1;
if (b < 0) b = 0;
new_pixel = (a + (b * _width)) * 4;
cur_pixel = i * 4;
_rd[cur_pixel] = _td[new_pixel];
_rd[cur_pixel + 1] = _td[new_pixel + 1];
_rd[cur_pixel + 2] = _td[new_pixel + 2];
}
++_mapind;
++i;
}
}
mapind = _mapind;
}
canvas.onmousemove = function(/* Event */ evt) {
disturb(evt.offsetX || evt.layerX, evt.offsetY || evt.layerY);
};
setInterval(run, delay);
// generate random ripples
var rnd = Math.random;
setInterval(function() {
disturb(rnd() * width, rnd() * height);
}, 700);
})();
<canvas id="c"></canvas>
Just look at this majestic unicorn: codepen. Data URI used to avoid CORS problems, it'll work with any number of images (all images by default).
JS:
window.addEventListener('load',()=>{
document.querySelectorAll('img').forEach((img)=>{
var cont = document.createElement('div');
cont.style.position = 'relative'
cont.style.display = 'inline-block'
img.parentNode.insertBefore(cont,img);
img.style.verticalAlign = 'top';
cont.appendChild(img)
console.dir(img)
var c = document.createElement('canvas');
c.width = img.clientWidth
c.height = img.clientHeight
c.style.position = 'absolute'
c.style.top = '0px'
c.style.left = '0px'
cont.appendChild(c)
console.log(c)
makeRipple(c,img)
})
})
function makeRipple(el,img){
var canvas = el,
/** #type {CanvasRenderingContext2D} */
ctx = canvas.getContext('2d'),
width = img.clientWidth,
height = img.clientHeight,
half_width = width >> 1,
half_height = height >> 1,
size = width * (height + 2) * 2,
delay = 30,
oldind = width,
newind = width * (height + 3),
riprad = 3,
mapind,
ripplemap = [],
last_map = [],
ripple,
texture,
line_width = 20,
step = line_width * 2,
count = height / line_width;
canvas.width = width;
canvas.height = height;
/*
* Water ripple demo can work with any bitmap image
* (see example here: http://media.chikuyonok.ru/ripple/)
* But I need to draw simple artwork to bypass 1k limitation
*/
ctx.drawImage(img,0,0,img.clientWidth,img.clientHeight)
texture = ctx.getImageData(0, 0, width, height);
ripple = ctx.getImageData(0, 0, width, height);
for (var i = 0; i < size; i++) {
last_map[i] = ripplemap[i] = 0;
}
/**
* Main loop
*/
function run() {
console.log('bbb')
newframe();
ctx.putImageData(ripple, 0, 0);
}
/**
* Disturb water at specified point
*/
function disturb(dx, dy) {
dx <<= 0;
dy <<= 0;
for (var j = dy - riprad; j < dy + riprad; j++) {
for (var k = dx - riprad; k < dx + riprad; k++) {
ripplemap[oldind + (j * width) + k] += 512;
}
}
}
/**
* Generates new ripples
*/
function newframe() {
var i, a, b, data, cur_pixel, new_pixel, old_data;
i = oldind;
oldind = newind;
newind = i;
i = 0;
mapind = oldind;
// create local copies of variables to decrease
// scope lookup time in Firefox
var _width = width,
_height = height,
_ripplemap = ripplemap,
_mapind = mapind,
_newind = newind,
_last_map = last_map,
_rd = ripple.data,
_td = texture.data,
_half_width = half_width,
_half_height = half_height;
for (var y = 0; y < _height; y++) {
for (var x = 0; x < _width; x++) {
data = (
_ripplemap[_mapind - _width] +
_ripplemap[_mapind + _width] +
_ripplemap[_mapind - 1] +
_ripplemap[_mapind + 1]) >> 1;
data -= _ripplemap[_newind + i];
data -= data >> 5;
_ripplemap[_newind + i] = data;
//where data=0 then still, where data>0 then wave
data = 1024 - data;
old_data = _last_map[i];
_last_map[i] = data;
if (old_data != data) {
//offsets
a = (((x - _half_width) * data / 1024) << 0) + _half_width;
b = (((y - _half_height) * data / 1024) << 0) + _half_height;
//bounds check
if (a >= _width) a = _width - 1;
if (a < 0) a = 0;
if (b >= _height) b = _height - 1;
if (b < 0) b = 0;
new_pixel = (a + (b * _width)) * 4;
cur_pixel = i * 4;
_rd[cur_pixel] = _td[new_pixel];
_rd[cur_pixel + 1] = _td[new_pixel + 1];
_rd[cur_pixel + 2] = _td[new_pixel + 2];
}
++_mapind;
++i;
}
}
mapind = _mapind;
}
canvas.onmousemove = function(/* Event */ evt) {
console.log('XXXX',evt.offsetX)
disturb(evt.offsetX || evt.layerX, evt.offsetY || evt.layerY);
};
setInterval(run, delay);
// generate random ripples
var rnd = Math.random;
setInterval(function() {
console.log('aaa')
disturb(rnd() * width, rnd() * height);
}, 700);
};
I'm trying to draw a one pixel width line going form the canvas center and evolving with the canvas width/height ratio as it's drawn.
var x = 0;
var y = 0;
var dx = 0;
var dy = -1;
var width = 200;
var height = 40;
//var i = width * height;
var counter = 0;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
setInterval(function(){
//for (i = Math.pow(Math.max(width, height), 2); i>0; i--) {
if ((-width/2 < x <= width/2) && (-height/2 < y <= height/2)) {
console.log("[ " + x + " , " + y + " ]");
ctx.fillStyle = "#FF0000";
ctx.fillRect(width/2 + x, height/2 - y,1,1);
}
if (x === y || (x < 0 && x === -y) || (x > 0 && x === 1-y) || ( -width/2 > x > width/2 ) || ( -height/2 > y > height/2 ) ) {
// change direction
var tempdx = dx;
dx = -dy;
dy = tempdx;
}
counter += 1;
//alert (counter);
x += dx;
y += dy;
}, 1);
I want the spiral to evolve as such:
I'd like to be able to get the ratio between height and width on the equation, so I don't need to calculate the coordinates for points outside the canvas. Also, the purpose is for it to adjust the spiral drawing to the canvas proportions.
Any help would be appreciated.
A friend helped me handling a proper solution. I only have a 1 pixel offset to solve where I need to move all the drawing to the left by one pixel.
Here's the fiddle for the solution achieved: http://jsfiddle.net/hitbyatruck/c4Kd6/
And the Javascript code below:
var width = 150;
var height = 50;
var x = -(width - height)/2;
var y = 0;
var dx = 1;
var dy = 0;
var x_limit = (width - height)/2;
var y_limit = 0;
var counter = 0;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
setInterval(function(){
if ((-width/2 < x && x <= width/2) && (-height/2 < y && y <= height/2)) {
console.log("[ " + x + " , " + y + " ]");
ctx.fillStyle = "#FF0000";
ctx.fillRect(width/2 + x, height/2 - y,1,1);
}
if( dx > 0 ){//Dir right
if(x > x_limit){
dx = 0;
dy = 1;
}
}
else if( dy > 0 ){ //Dir up
if(y > y_limit){
dx = -1;
dy = 0;
}
}
else if(dx < 0){ //Dir left
if(x < (-1 * x_limit)){
dx = 0;
dy = -1;
}
}
else if(dy < 0) { //Dir down
if(y < (-1 * y_limit)){
dx = 1;
dy = 0;
x_limit += 1;
y_limit += 1;
}
}
counter += 1;
//alert (counter);
x += dx;
y += dy;
}, 1);
I nearly crashed my browser trying this. Here, have some code before I hurt myself!
It computes y=f(x) for the diagonal, and y2=f(x) for the antidiagonal, then checks if we're above or below the diagonals when needed.
var x = 0;
var y = 0;
var dx = 0;
var dy = -1;
var width = 200;
var height = 40;
//var i = width * height;
var counter = 0;
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
function diag1(x) {
return x*height/width;
}
function diag2(x) {
return -1/diag(x);
}
setInterval(function(){
//for (i = Math.pow(Math.max(width, height), 2); i>0; i--) {
if ((-width/2 < x && x <= width/2) && (-height/2 < y && y <= height/2)) {
console.log("[ " + x + " , " + y + " ]");
ctx.fillStyle = "#FF0000";
ctx.fillRect(width/2 + x, height/2 - y,1,1);
}
if (dx == 0) {
if (dy == 1) {
// moving up
if (y >= diag1(x)) {
// then move left
dy = 0;
dx = -1;
}
}
else {
// moving down
if (y <= diag2(x)) {
// then move right
dy = 0;
dx = 1;
}
}
}
else {
if (dx == 1) {
// moving right
if (y <= diag1(x)) {
// then move up
dy = 1;
dx = 0;
}
}
else {
// moving left
if (y <= diag2(x)) {
// then move down
dy = -1;
dx = 0;
}
}
}
counter += 1;
//alert (counter);
x += dx;
y += dy;
}, 1);