creating twinkling effect using javascript - javascript

var stars = function() {
this.x = Math.floor(Math.random()* 1000) ;
this.y = Math.floor(Math.random()* 900) ;
this.radius = 2 ;
this.starColour = "gold";
}
var starNum = 20;
var starry = new Array(starNum);
for(var s = 0 ; s < 100 ; s++){
starry[s] = new stars()
}
var starDraw = function() {
var starCanvas = document.getElementById("stars");
var starCtx = starCanvas.getContext("2d");
starCtx.clearRect(0, 0, 1000, 900);
for(i = 0; i < 100 ; i++){
var star = starry[i];
starCtx.fillStyle= "white";
starCtx.shadowBlur = 5;
starCtx.shadowColor = "white";
starCtx.beginPath();
// draw it
starCtx.arc(star.x, star.y, star.radius, Math.PI * 2, false);
starCtx.stroke();
starCtx.fill();
}
}
function starLoop(){
starDraw();
requestAnimationFrame(starLoop);
}
requestAnimationFrame(starLoop);
So I am trying to create a twinkling effect for the stars using only javascript and I can't figure out how to do it.
I have searched around and found no real answers up to now so I would appreciate if I could get an answer here. I am very new to coding so please take it easy on me.

A random star field. A little exaggerated, but easy to tone down (or up) if needed.
The important part is to avoid direct random values as most things in nature are not random but tend to fall close to a fixed point. This is call a gaussian distribution. There are several ways to generate such random values.
// gRandom is far more likely to be near 0.5 than 1 or zero
var gRandom = (Math.random()+Math.random()+Math.random()+Math.random()) / 4;
// or
// gRandom is more likely to be near zero than near 1
var gRandom = Math.random() * Math.random();
I use these method to set the sizes of stars (far more small stars than big) and create the colour and movement.
To try and get a more realistic effect I also move the stars by less than a pixel. This has the effect of changing the brightness but not look like movement.
Code has plenty of comments
const ctx = canvas.getContext("2d");
// function calls a callback count times. Saves typing out for loops all the time
const doFor = (count, callback) => {
var i = 0;
while (i < count) {
callback(i++)
}
};
// creates a random integer between min and max. If min only given the between 0 and the value
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
// same as above but as floats.
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
// creates a 2d point at x,y. If only x is a point than set to that point
const point = (x = 0, y) => {
if (x.x && y === undefined) {return { x: x.x,y: x.y} }
return {x,y: y === undefined ? 0 : y }
};
function ease (time, amount = 2) { return Math.pow(time % 1,amount) };
const clamp = (v, min = 1,max = min + (min = 0)) => v < min ? min : v > max ? max : v;
// stuff for stars
const skyColour = [10,30,50];
const density = 1000; // number of star per every density pixels
const colourChangeRate = 16; // Time in frames to change a colour
const stars = [];
const star = { // define a star
draw() {
this.count += 1; // integer counter used to triger color change every 16 frames
if (this.count % colourChangeRate === 0) { // change colour ?
// colour is a gaussian distrabution (NOT random) centered at #888
var c = (Math.random() + Math.random() + Math.random() + Math.random()) * 4;
var str = "#";
str += Math.floor(c * this.red).toString(16); // change color
str += Math.floor(c * this.green).toString(16); // change color
str += Math.floor(c * this.blue).toString(16); // change color
this.col = str;
}
ctx.fillStyle = this.col;
// move star around a pixel. Again its not random
// but a gaussian distrabution. The movement is sub pixel and will only
// make the stars brightness vary not look like its moving
var ox = (Math.random() + Math.random() + Math.random() + Math.random()) / 4;
var oy = (Math.random() + Math.random() + Math.random() + Math.random()) / 4;
ctx.fillRect(this.pos.x + ox, this.pos.y + oy, this.size, this.size);
}
}
// create a random star
// the size is caculated to produce many more smaller stars than big
function createStar(pos) {
stars.push(Object.assign({}, star, {
pos,
col: "#ccc",
count: randI(colourChangeRate),
size: rand(1) * rand(1) * 2 + 0.5,
red: 1-(rand(1) * rand(1) *rand(1)), // reduces colour channels
green: 1-(rand(1) * rand(1) *rand(1)), // but only by a very small amount
blue: 1-(rand(1) * rand(1) *rand(1)), // most of the time but occasional
// star will have a distinct colour
}));
}
var starCount;
var skyGrad;
// render the stars
function mainLoop(time) {
// resize canva if page size changes
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
canvas.width = innerWidth;
canvas.height = innerHeight;
// create a new set of stars
stars.length = 0;
// density is number of pixels one the canvas that has one star
starCount = Math.floor((canvas.width * canvas.height) / density);
// create the random stars;
doFor(starCount, () => createStar(point(randI(canvas.width), randI(canvas.height))));
skyGrad = ctx.createLinearGradient(0,0,0,canvas.height);
skyGrad.addColorStop(0,"black");
doFor(100,(i)=>{
var pos = clamp(i/100,0,1);
var col = ease(pos);
skyGrad.addColorStop(
pos,
"rgb(" +
Math.floor(skyColour[0] * col) + "," +
Math.floor(skyColour[1] * col) + "," +
Math.floor(skyColour[2] * col) + ")"
);
});
// floating point error can cause problems if we dont set the top
// at 1
skyGrad.addColorStop(1,"rgb("+skyColour[0]+","+skyColour[1]+","+skyColour[2]+")");
}
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
doFor(starCount, (i) => stars[i].draw());
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>

Related

P5.js curveVertex function is closing at a point

I've created a noise function that pairs with a circle function to create a random noise circle thing that looks pretty cool. My problem is the curveVertex function in P5.js works correctly except for the connection of the first and last vertex. My code is:
let start = Array(50).fill(0); // dont change
let amount = 1; // amount of shapes
let gap = 30; // between shapes
let amplify = 50; // 0 -->
let colorSpeed = 1; // 1 - 9
let colorSeparation = 3; // 0 - 80 recomended 0 - 10
function setup() {
createCanvas(windowWidth, windowHeight);
for(let i = 0 ; i < start.length; i++){
start[i] = random(i);
}
}
function draw() {
background(0);
for(let dnc = (amount + 1) * gap; dnc > gap; dnc -= gap){
drawNoiseCircle(dnc, getNoise(start.length));
}
start = start.map( c => c + 0.01 );
}
function getNoise(amount){
let lengths = [];
for(let i = 1; i < amount + 1; i++){
let n1 = noise(start[i - 1]);
let noise1 = map(n1, 0, 1, -amplify, amplify);
lengths.push(abs(-noise1));
}
return lengths;
}
function drawNoiseCircle(radius, lengths){
colorMode(HSB);
fill(((frameCount + radius) * colorSeparation)/-map(colorSpeed, 1, 10, -10, -1) % 360, 100, 50);
noStroke()
let x;
let y;
beginShape();
for(let l = 0; l < lengths.length; l++){
x = Math.cos(radians(l * 360 / lengths.length)) * (radius + lengths[l]) + width/2;
y = Math.sin(radians(l * 360 / lengths.length)) * (radius + lengths[l]) + height/2;
curveVertex(x, y);
}
endShape(CLOSE);
stroke("black");
line(width/2, height/2, width, height/2);
line(width/2, height/2 + 9, width, height/2 + 9);
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.4.1/lib/p5.js"></script>
I understand the endShape(CLOSED) closes the shape with a straight line, but I'm not sure any other way to close the shape.
You can see the pointed edge on the right side, directly in the middle of the shape.
!EDIT!
I've added lines to the shape to show the line segment that isn't affected by the curve vertex. Also, I understand it may not be a very significant problem, but if the amount of vertexes shrink, it becomes a much bigger problem (eg. a square or a triangle).
Unfortunately I won't have time to dive deep and debug the actual issue with curveVertex (or it's math) at the time, but it seems there's something interesting with curveVertex() in particular.
#Ouoborus point makes sense and the function "should" behave that way (and it was with vertex(), but not curveVertex()). For some reason curveVertex() requires looping over the not just the first point again, but the second and third.
Here's basic example:
function setup() {
createCanvas(300, 300);
background(220);
let numPoints = 6;
let angleIncrement = TWO_PI / numPoints;
let radius = 120;
beginShape();
for(let i = 0 ; i < numPoints + 3; i++){
let angle = angleIncrement * i;
let x = 150 + cos(angle) * radius;
let y = 150 + sin(angle) * radius;
curveVertex(x, y);
}
endShape();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.min.js"></script>
Try decreasing numPoints + 3 to numPoints + 2 or notice the behaviour you're describing.
(I could speculate it might have something to do with how curveVertex() (Catmull-Rom splines) are implemented in p5 and how many coordinates/points it requires, but this isn't accurate without reading the source code and debugging a bit)
Here's a version of your code using the above notes:
let start = Array(30).fill(0);
let colorSpeed = 1; // 1 - 9
let colorSeparation = 3; // 0 - 80 recomended 0 - 10
function setup() {
createCanvas(600, 600);
colorMode(HSB);
noStroke();
// init noise seeds
for(let i = 0 ; i < start.length; i++){
start[i] = random(i);
}
}
function getNoise(seeds, amplify = 50){
let amount = seeds.length;
let lengths = [];
for(let i = 1; i < amount + 1; i++){
let n1 = noise(seeds[i - 1]);
let noise1 = map(n1, 0, 1, -amplify, amplify);
lengths.push(abs(-noise1));
}
return lengths;
}
function drawNoiseCircle(radius, lengths){
let sides = lengths.length;
let ai = TWO_PI / sides;
let cx = width * 0.5;
let cy = height * 0.5;
fill(((frameCount + radius) * colorSeparation)/-map(colorSpeed, 1, 10, -10, -1) % 360, 100, 50);
beginShape();
for(let i = 0 ; i < sides + 3; i++){
let noiseRadius = radius + lengths[i % sides];
let a = ai * i;
let x = cx + cos(a) * noiseRadius;
let y = cy + sin(a) * noiseRadius;
curveVertex(x, y);
}
endShape();
}
function draw() {
background(0);
// draw with updated perlin noise values
drawNoiseCircle(120, getNoise(start));
// increment noise seed
start = start.map( c => c + 0.01 );
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.min.js"></script>

Canvas - floodfill leaves white pixels at edges for PNG images with transparent

Now, I tried to perform flood fill algorithm to fill up the transparent PNG images using flood fill algorithm from the article How can I avoid exceeding the max call stack size during a flood fill algorithm? which use non recursive method along with Uint32Array to handle color stack with work quite well.
However, this flood fill algorithm has left the white (actually the light grey edge or anti-alias edges) which remain unfilled. Here is my code:
var BrushColorString = '#F3CDA6'; // skin color
canvas.addEventListener('mousedown', function(e) {
const rect = canvas.getBoundingClientRect()
CanvasMouseX = e.clientX - rect.left;
CanvasMouseY = e.clientY - rect.top;
if (mode === 'flood-fill')
{
// test flood fill algorithm
paintAt(context, CanvasMouseX,CanvasMouseY,hexToRgb(BrushColorString));
}
});
function paintAt(ContextOutput,startX, startY,curColor) {
//function paintAt(ctx,startX, startY,curColor) {
// read the pixels in the canvas
const width = ContextOutput.canvas.width,
height = ContextOutput.canvas.height,pixels = width*height;
const imageData = ContextOutput.getImageData(0, 0, width, height);
var data1 = imageData.data;
const p32 = new Uint32Array(data1.buffer);
const stack = [startX + (startY * width)]; // add starting pos to stack
const targetColor = p32[stack[0]];
var SpanLeft = true, SpanRight = true; // logic for spanding left right
var leftEdge = false, rightEdge = false;
// proper conversion of color to Uint32Array
const newColor = new Uint32Array((new Uint8ClampedArray([curColor.r,curColor.g, curColor.b, curColor.a])).buffer)[0];
// need proper comparison of target color and new Color
if (targetColor === newColor || targetColor === undefined) { return } // avoid endless loop
while (stack.length){
let idx = stack.pop();
while(idx >= width && p32[idx - width] === targetColor) { idx -= width }; // move to top edge
SpanLeft = SpanRight = false; // not going left right yet
leftEdge = (idx % width) === 0;
rightEdge = ((idx +1) % width) === 0;
while (p32[idx] === targetColor) {
p32[idx] = newColor;
if(!leftEdge) {
if (p32[idx - 1] === targetColor) { // check left
if (!SpanLeft) {
stack.push(idx - 1); // found new column to left
SpanLeft = true; //
} else if (SpanLeft) {
SpanLeft = false;
}
}
}
if(!rightEdge) {
if (p32[idx + 1] === targetColor) {
if (!SpanRight) {
stack.push(idx + 1); // new column to right
SpanRight = true;
}else if (SpanRight) {
SpanRight = false;
}
}
}
idx += width;
}
}
clearCanvas(ContextOutput);
ContextOutput.putImageData(imageData,0, 0);
};
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
a: 255
} : null;
};
So far, I have tried using the following suggestion:
using matchOutlineColor function using RGBA value mentioned in Canvas - floodfill leaves white pixels at edges
When I tried to implemented "Restrict fill area based on intensity gradient changes instead of simple threshold " mentioned in Canvas - floodfill leaves white pixels at edges which is considered as the most promising algorithm, I still have no clue how to implement this algorithm with minimum change on existing algorithm to handle the anti-alias edge issue for the cases of images with transparent.
When I take a look at the example on how to apply a tolerance and a toleranceFade mentioned in Canvas flood fill not filling to edge, I still have no clue how implement such a tolerance and a toleranceFade in my case.
Color Difference method (colorDiff function) within mentioned tolerance in Canvas Javascript FloodFill algorithm left white pixels without color and so far still not working. Similar thing can be said to colorsMatch function to be within Range Square (rangeSq) mentioned in How can I perform flood fill with HTML Canvas? which still unable to solve the anti-alias edge problem.
If you have any idea on how to deal with anti-alias edge problems of the flood-fill algorithm, please response as soon as possible.
Updated:
Here is the revised code on paintAt fucntion from the suggestion that takes tolerance into account:
<div id="container"><canvas id="control" >Does Not Support Canvas Element</canvas></div>
<div><label for="tolerance">Tolerance</label>
<input id="tolerance" type="range" min="0" max="255" value="32" step="1" oninput="this.nextElementSibling.value = this.value"><output>32</output></div>
var canvas = document.getElementById("control");
var context = canvas.getContext('2d');
var CanvasMouseX = -1; var CanvasMouseY = -1;
var BrushColorString = '#F3CDA6'; // skin color
canvas.addEventListener('mousedown', function(e) {
const rect = canvas.getBoundingClientRect()
CanvasMouseX = e.clientX - rect.left;
CanvasMouseY = e.clientY - rect.top;
// testing
if (mode === 'flood-fill')
{
// test flood fill algorithm
paintAt(context,CanvasMouseX,CanvasMouseY,
hexToRgb(BrushColorString),tolerance.value);
}
});
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
a: 255
} : null;
};
function clearCanvas(ctx) {
ctx.clearRect(0, 0,ctx.canvas.width,ctx.canvas.height);
};
function colorDistance(index, R00,G00,B00,A00, data0)
{
var index1 = index << 2; // multiplyed by 4
const R = R00 - data0[index1 + 0];
const G = G00 - data0[index1 + 1];
const B = B00 - data0[index1 + 2];
const A = A00 - data0[index1 + 3];
return Math.sqrt((R * R) + (B * B) + (G * G) + (A * A));
}
function paintAt(ContextOutput,startX, startY,curColor,tolerance) {
// read the pixels in the canvas
const width = ContextOutput.canvas.width,
height = ContextOutput.canvas.height, pixels = width*height;
const rightEdgeNum = width - 1, bottomEdgeNum = height - 1;
const imageData = ContextOutput.getImageData(0, 0, width, height);
var data1 = imageData.data;
const p32 = new Uint32Array(data1.buffer);
const stack = [startX + (startY * width)]; // add starting pos to stack
const targetColor = p32[stack[0]];
var SpanLeft = true, SpanRight = true; // logic for spanning left right
var leftEdge = false, rightEdge = false, IsBlend = false;
const DistancesArray = new Uint16Array(pixels); // array distance value
var R=-1,G=-1,B=-1,A = -1,idx =0,Distance=0;
var R0 = data1[(4*(startX + (startY * width)))+0],
G0 = data1[(4*(startX + (startY * width)))+1],
B0 = data1[(4*(startX + (startY * width)))+2],
A0 = data1[(4*(startX + (startY * width)))+3];
var CalculatedTolerance = Math.sqrt(tolerance * tolerance * 4);
const BlendR = curColor.r |0, BlendG = curColor.g |0,
BlendB = curColor.b |0, BlendA = curColor.a|0;
// color variable for blending
const newColor = new Uint32Array((new Uint8ClampedArray([BlendR,BlendG,BlendB,BlendA])).buffer)[0];
if (targetColor === newColor || targetColor === undefined) { return }
// avoid endless loop
while (stack.length){
idx = stack.pop();
while (idx >= width &&
colorDistance(idx - width,R0,G0,B0,A0,data1) <= CalculatedTolerance) { idx -= width }; // move to top edge
SpanLeft = SpanRight = false; // not going left right yet
leftEdge = (idx % width) === 0;
rightEdge = ((idx +1) % width) === 0;
while ((Distance = colorDistance(idx,R0,G0,B0,A0,data1)) <= CalculatedTolerance) {
DistancesArray[idx] = (Distance / CalculatedTolerance) * 255 | 0x8000;
p32[idx] = newColor;
if(!leftEdge) {
if (colorDistance(idx - 1,R0,G0,B0,A0,data1) <= CalculatedTolerance) { // check left
if (!SpanLeft) {
stack.push(idx - 1); // found new column to left
SpanLeft = true; //
} else if (SpanLeft) {
SpanLeft = false;
}
}
}
if(!rightEdge) {
if (colorDistance(idx + 1,R0,G0,B0,A0,data1) <= CalculatedTolerance) {
if (!SpanRight) {
stack.push(idx + 1); // new column to right
SpanRight = true;
}else if (SpanRight) {
SpanRight = false;
}
}
}
idx += width;
}
}
idx = 0;
while (idx <= pixels-1) {
Distance = DistancesArray[idx];
if (Distance !== 0) {
if (Distance === 0x8000) {
p32[idx] = newColor;
} else {
IsBlend = false;
const x = idx % width;
const y = idx / width | 0;
if (x >= 1 && DistancesArray[idx - 1] === 0) { IsBlend = true }
else if (x <= rightEdgeNum -1 && DistancesArray[idx + 1] === 0) { IsBlend = true }
else if (y >=1 && DistancesArray[idx - width] === 0) { IsBlend = true }
else if (y <=bottomEdgeNum-1 && DistancesArray[idx + width] === 0) { IsBlend = true }
if (IsBlend) {
// blending at the edge
Distance &= 0xFF;
Distance = Distance / 255;
const invDist = 1 - Distance;
const idx1 = idx << 2;
data1[idx1 + 0] = data1[idx1 + 0] * Distance + BlendR * invDist;
data1[idx1 + 1] = data1[idx1 + 1] * Distance + BlendG * invDist;
data1[idx1 + 2] = data1[idx1 + 2] * Distance + BlendB * invDist;
data1[idx1 + 3] = data1[idx1 + 3] * Distance + BlendA * invDist;
} else {
p32[idx] = newColor;
}
}
}
idx++;
}
// this recursive algorithm works but still not working well due to the issue stack overflow!
clearCanvas(ContextOutput);
ContextOutput.putImageData(imageData,0, 0);
// way to deal with memory leak at the array.
DistancesArray = [];
newColor = [];
p32 = [];
};
However, the results of flood fill have been found wanting as shown in the transition tolerance as shown here:'
How can I deal with this kind problem when tolerance has become too much. Any alternative algorithm would be appreciated.
Double pass Flood fill in the 4th dimension
I am the Author of the accepted answers for How can I avoid exceeding the max call stack size during a flood fill algorithm? and Canvas flood fill not filling to edge
Unfortunately there is no perfect solution.
The following method has problems.
Setting the tolerance so that it gets all edge aliasing will often fill unwanted areas.
Setting the tolerance too low can make the edges look even worse than the standard fill.
Repeated fills will result in harder edge aliasing.
Uses a simple blend function. The correct blend function can be found at W3C Compositing and Blending Level "blending normal" Sorry I am out of time to complete this answer.
Does not easily convert to gradients or pattern fills.
There is a much better solution but it is 1000+ lines long and the code alone would not fit in the 32K answer limit.
This answer is a walk through of how to change your function to reduce edge aliasing using a tolerance and simple edge blending.
Note
The various snippets in the answer may have typos or wrong names. For the correct working code see example at bottom.
Tolerance
The simplest method to detect edges is to use a tolerance and fill pixels that are within the tolerance of the pixel color at the fill origin.
This lets the fill overlap the aliased edges that can then be detected and blended to reduce the artifacts due to anti-aliasing.
The problem is that to get a good coverage of the aliasing requires a large tolerance and this ends up filling areas you intuitively would not want colored.
Calculating color distance
A color can be represented by the 3 values red, green, blue. If one substitutes the names with x, y, z it is easy to see how each color has a unique position in 3D space.
Event better is that the distance between any two colors in this 3D space directly correlates to perceived difference in color. We can thus us simple math to calculate the difference (Pythagoras).
As we need to also consider the alpha channel we need to step up one dimension. Each color and its alpha part have a unique point in 4D space. The distance between any of these 4D colors directly correlates to perceived difference in color and transparency.
Lucky we do not need to imagine 4D space, all we do is extend the math (Pythagoras works in all euclidean dimensions).
Thus we get the function and prep code you can add to your flood fill function.
var idx = stack[0] << 2; // remove let first line inside while (stack.length){
const r = data1[idx] ;
const g = data1[idx + 1] ;
const b = data1[idx + 2];
const a = data1[idx + 3]
function colorDist(idx) { // returns the spacial distance from the target color of pixel at idx
idx <<= 2;
const R = r - data1[i];
const G = g - data1[i + 1];
const B = b - data1[i + 2];
const A = a - data1[i + 3];
return (R * R + B * B + G * G + A * A) ** 0.5;
}
To the function declaration we add an argument tolerance specified as a value 0 to 255
The function declaration changes from
function paintAt(contextOutput, startX, startY, curColor) {
To
function paintAt(contextOutput, startX, startY, curColor, tolerance = 0) {
With tolerance as an optional argument.
A tolerance of 0 only fills the targetColor
A tolerance of 255 should fill all pixels
We need to convert the tolerance from a channel value to a 4D distance value so that the 255 covers the greatest distance between two colors in the 4D color space.
Add the following line to the top of the function paintAt
tolerance = (tolerance * tolerance * 4) ** 0.5; // normalize to 4D RGBA space
We now need to change the pixel match statements to use the tolerance. Anywhere you have
p32[idx] === targetColor or similar needs to be replaced with colorDist(idx) <= tolerance. The exception is the inner while loop as we need to use the 4D color distance
while (checkPixel(ind)) {
becomes
// declare variable dist at top of function
while ((dist = colorDist(idx)) <= tolerance) {
Double pass solution
To combat the aliasing we need to blend the fill color by an amount proportional to the color distance.
Doing this for all pixels means that pixels away from the edge of the fill will get the wrong color if the color distance is not 0 and less than tolerance.
We only want to blend pixels if they are at the edge of the fill, excluding those at the edge of the canvas. For many of the pixels there is no way of knowing if a pixel is at the edge of the fill as we come across them. We can only know when we have found all filled pixels.
First pass the flood fill
Thus we must keep an array that holds the color distance for all pixels filled
At the top of the function create a buffer to hold pixel color distances.
const distances = new Uint16Array(width*height);
Then in the inner loop along with setting the pixel color set the matching locations distance.
while ((dist = colorDist(idx)) <= tolerance) {
//Must not fill color here do in second pass p32[idx] = newColor;
distances[idx] = (dist / tolerance) * 255 | 0x8000;
To track which pixels are filled we set the top bit of the distance value. That means that distances will hold a non zero value for all pixels to fill and zero for pixels to ignore. This is done with the | 0x8000
The main part of the fill is no done. We let the fill do its thing before we start on the next pass.
Second pass edge detect and blend
After the outer loop has exited we step over each pixel one at a time. Check if it needs to be filled.
If it needs filling we extract the color distance. If zero set that pixels color in the p32 array. If the distance is not zero we then check the 4 pixels around it. If any of the 4 neighboring pixels is marked as do not fill distances[idx] === 0 and that pixel is not outside the canvas bounds we know it is an edge and needs to be blended.
// declare at top of function
var blend, dist, rr, gg, bb, aa;
// need fill color's channels for quickest possible access.
const fr = curColor.r | 0;
const fg = curColor.g | 0;
const fb = curColor.b | 0;
const fa = curColor.a | 0;
// after main fill loop.
idx = 0;
const rightEdge = width - 1, bottomEdge = height - 1;
while (idx < width * height){
dist = distances[idx];
if (dist !== 0) {
if (dist === 0x8000) {
p32[idx] = newColor;
} else {
blend = false;
const x = idx % width;
const y = idx / width | 0;
if (x > 0 && distances[idx - 1] === 0) { blend = true }
else if (x < rightEdge && distances[idx + 1] === 0) { blend = true }
else if (y > 0 && distances[idx - width] === 0) { blend = true }
else if (y < bottomEdge && distances[idx + width] === 0) { blend = true }
if (blend) { // pixels is at fill edge an needs to blend
dist &= 0xFF; // remove fill bit
dist = dist / 255; // normalize to range 0-1
const invDist = 1 - dist; // invert distance
// get index in byte array
const idx1 = idx << 2; // same as idx * 4
// simple blend function (not the same as used by 2D API)
data[idx1] = data[idx1 ] * dist + fr * invDist;
data[idx1 + 1] = data[idx1 + 1] * dist + fg * invDist;
data[idx1 + 2] = data[idx1 + 2] * dist + fb * invDist;
data[idx1 + 3] = data[idx1 + 3] * dist + fa * invDist;
} else {
p32[idx] = newColor;
}
}
}
idx++;
}
And now just put the new pixel array onto the canvas.
Example
This example is a bare bones wrapper around a modified version of your code. It is there to make sure I did not make any algorithmic mistakes and to highlight the quality or lack of quality when using this method.
Click first button to add random circle.
Use slider to set tolerance 0 - 255
Click clear to clear canvas.
Click canvas to fill random color at mouse pos.
Canvas has been scaled by 2 to make artifacts more visible.
The function floodFill replaces your paintAt and is too big and should be broken into two parts, one for the fill pass, and another for edge detect and blend.
const ctx = canvas.getContext("2d");
var circle = true;
test();
canvas.addEventListener("click", e => {circle = false; test(e)});
toggleFill.addEventListener("click",e => {circle = true; test(e)});
clear.addEventListener("click",()=>ctx.clearRect(0,0,500,500));
function randomCircle() {
ctx.beginPath();
ctx.strokeStyle = "black";
ctx.lineWidth = 4;
const x = Math.random() * 100 | 0;
const y = Math.random() * 100 | 0;
ctx.arc(x, y, Math.random() * 25 + 25, 0 , Math.PI * 2);
ctx.stroke();
return {x,y};
}
function test(e) {
if (circle) {
toggleFill.textContent = "Click canvas to fill";
randomCircle();
} else {
toggleFill.textContent = "Click button add random circle";
const col = {
r: Math.random() * 255 | 0,
g: Math.random() * 255 | 0,
b: Math.random() * 255 | 0,
a: Math.random() * 255 | 0,
};
floodFill(ctx, (event.offsetX - 1) / 2 | 0, (event.offsetY -1) / 2| 0, col, tolerance.value);
}
}
// Original function from SO question https://stackoverflow.com/q/65359146/3877726
function floodFill(ctx, startX, startY, curColor, tolerance = 0) {
var idx, blend, dist, rr, gg, bb, aa, spanLeft = true, spanRight = true, leftEdge = false, rightEdge = false;
const width = ctx.canvas.width, height = ctx.canvas.height, pixels = width*height;
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const p32 = new Uint32Array(data.buffer);
const stack = [startX + (startY * width)];
const targetColor = p32[stack[0]];
const fr = curColor.r | 0;
const fg = curColor.g | 0;
const fb = curColor.b | 0;
const fa = curColor.a | 0;
const newColor = (fa << 24) + (fb << 16) + (fg << 8) + fr;
if (targetColor === newColor || targetColor === undefined) { return }
idx = stack[0] << 2;
const rightE = width - 1, bottomE = height - 1;
const distances = new Uint16Array(width*height);
tolerance = (tolerance * tolerance * 4) ** 0.5;
const r = data[idx] ;
const g = data[idx + 1] ;
const b = data[idx + 2];
const a = data[idx + 3]
function colorDist(idx) {
if (distances[idx]) { return Infinity }
idx <<= 2;
const R = r - data[idx];
const G = g - data[idx + 1];
const B = b - data[idx + 2];
const A = a - data[idx + 3];
return (R * R + B * B + G * G + A * A) ** 0.5;
}
while (stack.length) {
idx = stack.pop();
while (idx >= width && colorDist(idx - width) <= tolerance) { idx -= width }; // move to top edge
spanLeft = spanRight = false; // not going left right yet
leftEdge = (idx % width) === 0;
rightEdge = ((idx + 1) % width) === 0;
while ((dist = colorDist(idx)) <= tolerance) {
distances[idx] = (dist / tolerance) * 255 | 0x8000;
if (!leftEdge) {
if (colorDist(idx - 1) <= tolerance) {
if (!spanLeft) {
stack.push(idx - 1);
spanLeft = true;
} else if (spanLeft) {
spanLeft = false;
}
}
}
if (!rightEdge) {
if (colorDist(idx + 1) <= tolerance) {
if (!spanRight) {
stack.push(idx + 1);
spanRight = true;
}else if (spanRight) {
spanRight = false;
}
}
}
idx += width;
}
}
idx = 0;
while (idx < pixels) {
dist = distances[idx];
if (dist !== 0) {
if (dist === 0x8000) {
p32[idx] = newColor;
} else {
blend = false;
const x = idx % width;
const y = idx / width | 0;
if (x > 0 && distances[idx - 1] === 0) { blend = true }
else if (x < rightE && distances[idx + 1] === 0) { blend = true }
else if (y > 0 && distances[idx - width] === 0) { blend = true }
else if (y < bottomE && distances[idx + width] === 0) { blend = true }
if (blend) {
dist &= 0xFF;
dist = dist / 255;
const invDist = 1 - dist;
const idx1 = idx << 2;
data[idx1] = data[idx1 ] * dist + fr * invDist;
data[idx1 + 1] = data[idx1 + 1] * dist + fg * invDist;
data[idx1 + 2] = data[idx1 + 2] * dist + fb * invDist;
data[idx1 + 3] = data[idx1 + 3] * dist + fa * invDist;
} else {
p32[idx] = newColor;
}
}
}
idx++;
}
ctx.putImageData(imageData,0, 0);
}
canvas {
width: 200px;
height: 200px;
border: 1px solid black;
}
<label for="tolerance">Tolerance</label>
<input id="tolerance" type="range" min="0" max="255" value="32" step="1"></input>
<button id ="toggleFill" >Click add random circle</button>
<button id ="clear" >Clear</button><br>
<canvas id="canvas" width="100" height="100"></canvas>

p5.js - Low FPS for some basic animations

I'm having really bad performance on a project i wrote in Javascript (with the p5.js library)
Here is the code:
const fps = 60;
const _width = 400;
const _height = 300;
const firePixelChance = 1;
const coolingRate = 1;
const heatSourceSize = 10;
const noiseIncrement = 0.02;
const fireColor = [255, 100, 0, 255];
const bufferWidth = _width;
const bufferHeight = _height;
let buffer1;
let buffer2;
let coolingBuffer;
let ystart = 0.0;
function setup() {
createCanvas(_width, _height);
frameRate(fps);
buffer1 = createGraphics(bufferWidth, bufferHeight);
buffer2 = createGraphics(bufferWidth, bufferHeight);
coolingBuffer = createGraphics(bufferWidth, bufferHeight);
}
// Draw a line at the bottom
function heatSource(buffer, rows, _color) {
const start = bufferHeight - rows;
for (let x = 0; x < bufferWidth; x++) {
for (let y = start; y < bufferHeight; y++) {
if(Math.random() >= firePixelChance)
continue;
buffer.pixels[(x + (y * bufferWidth)) * 4] = _color[0]; // Red
buffer.pixels[(x + (y * bufferWidth)) * 4 +1] = _color[1]; // Green
buffer.pixels[(x + (y * bufferWidth)) * 4 +2] = _color[2]; // Blue
buffer.pixels[(x + (y * bufferWidth)) * 4 +3] = 255; // Alpha
}
}
}
// Produces the 'smoke'
function coolingMap(buffer){
let xoff = 0.0;
for(x = 0; x < bufferWidth; x++){
xoff += noiseIncrement;
yoff = ystart;
for(y = 0; y < bufferHeight; y++){
yoff += noiseIncrement;
n = noise(xoff, yoff);
bright = pow(n, 3) * 20;
buffer.pixels[(x + (y * bufferWidth)) * 4] = bright;
buffer.pixels[(x + (y * bufferWidth)) * 4 +1] = bright;
buffer.pixels[(x + (y * bufferWidth)) * 4 +2] = bright;
buffer.pixels[(x + (y * bufferWidth)) * 4 +3] = bright;
}
}
ystart += noiseIncrement;
}
// Change color of a pixel so it looks like its smooth
function smoothing(buffer, _buffer2, _coolingBuffer) {
for (let x = 0; x < bufferWidth; x++) {
for (let y = 0; y < bufferHeight; y++) {
// Get all 4 neighbouring pixels
const left = getColorFromPixelPosition(x+1,y,buffer.pixels);
const right = getColorFromPixelPosition(x-1,y,buffer.pixels);
const bottom = getColorFromPixelPosition(x,y+1,buffer.pixels);
const top = getColorFromPixelPosition(x,y-1,buffer.pixels);
// Set this pixel to the average of those neighbours
let sumRed = left[0] + right[0] + bottom[0] + top[0];
let sumGreen = left[1] + right[1] + bottom[1] + top[1];
let sumBlue = left[2] + right[2] + bottom[2] + top[2];
let sumAlpha = left[3] + right[3] + bottom[3] + top[3];
// "Cool down" color
const coolingMapColor = getColorFromPixelPosition(x,y,_coolingBuffer.pixels)
sumRed = (sumRed / 4) - (Math.random() * coolingRate) - coolingMapColor[0];
sumGreen = (sumGreen / 4) - (Math.random() * coolingRate) - coolingMapColor[1];
sumBlue = (sumBlue / 4) - (Math.random() * coolingRate) - coolingMapColor[2];
sumAlpha = (sumAlpha / 4) - (Math.random() * coolingRate) - coolingMapColor[3];
// Make sure we dont get negative numbers
sumRed = sumRed > 0 ? sumRed : 0;
sumGreen = sumGreen > 0 ? sumGreen : 0;
sumBlue = sumBlue > 0 ? sumBlue : 0;
sumAlpha = sumAlpha > 0 ? sumAlpha : 0;
// Update this pixel
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4] = sumRed; // Red
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4 +1] = sumGreen; // Green
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4 +2] = sumBlue; // Blue
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4 +3] = sumAlpha; // Alpha
}
}
}
function draw() {
background(0);
text("FPS: "+Math.floor(frameRate()), 10, 20);
fill(0,255,0,255);
buffer1.loadPixels();
buffer2.loadPixels();
coolingBuffer.loadPixels();
heatSource(buffer1, heatSourceSize, fireColor);
coolingMap(coolingBuffer);
smoothing(buffer1, buffer2, coolingBuffer);
buffer1.updatePixels();
buffer2.updatePixels();
coolingBuffer.updatePixels();
let temp = buffer1;
buffer1 = buffer2;
buffer2 = temp;
image(buffer2, 0, 0); // Draw buffer to screen
// image(coolingBuffer, 0, bufferHeight); // Draw buffer to screen
}
function mousePressed() {
buffer1.fill(fireColor);
buffer1.noStroke();
buffer1.ellipse(mouseX, mouseY, 100, 100);
}
function getColorFromPixelPosition(x, y, pixels) {
let _color = [];
for (let i = 0; i < 4; i++)
_color[i] = pixels[(x + (y * bufferWidth)) * 4 + i];
return _color;
}
function getRandomColorValue() {
return Math.floor(Math.random() * 255);
}
I'm getting ~12 FPS on chrome and ~1 FPS on any other browser and i cant figure out why..
Resizing my canvas to make it bigger also impacts the fps negatively...
In the devtools performance tab i noticed that both my smoothing and coolingMap functions are the things slowing it down, but i cant figure out what part of them are so heavy..
You've pretty much answered this for yourself already:
i'm starting to think this is normal and i should work on caching stuff and maybe use pixel groups instead of single pixels
Like you're discovering, doing some calculation for every single pixel is pretty slow. Computers only have finite resources, and there's going to be a limit to what you can throw at them.
In your case, you might consider drawing the whole thing to a canvas once at startup, and then moving the canvas up over the life of the program.

Canvas problems. Not able to reproduce design

I need to build canvas animation like design requires. I spend almost 3 days but I'm not able to do anything like in design. Here a REQUESTED design!. And here - what I've got for now: current implementation which definitely not what requested from design .I need only animation of planet from particles at background (also whole process of animation changes in time, it starts from few particles but then amount growing and movings directions of particles changes)
here my current code:
export class CanvasComponent implements OnInit {
sphereRad = 280;
radius_sp = 1;
distance = 600;
particle_size = 0.7;
constructor() { }
ngOnInit() {
this.canvasApp();
}
canvasApp () {
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let displayWidth;
let displayHeight;
let wait;
let count;
let numToAddEachFrame;
let particleList;
let recycleBin;
let particleAlpha;
let r, g, b;
let fLen;
let m;
let projCenterX;
let projCenterY;
let zMax;
let turnAngle;
let turnSpeed;
let sphereCenterX, sphereCenterY, sphereCenterZ;
let particleRad;
let zeroAlphaDepth;
let randAccelX, randAccelY, randAccelZ;
let gravity;
let rgbString;
// we are defining a lot of letiables used in the screen update functions globally so that they don't have to be redefined every frame.
let p;
let outsideTest;
let nextParticle;
let sinAngle;
let cosAngle;
let rotX, rotZ;
let depthAlphaFactor;
let i;
let theta, phi;
let x0, y0, z0;
// INITIALLI
const init = () => {
wait = 1;
count = wait - 1;
numToAddEachFrame = 30;
// particle color
r = 255;
g = 255;
b = 255;
rgbString = 'rgba(' + r + ',' + g + ',' + b + ','; // partial string for color which will be completed by appending alpha value.
particleAlpha = 1; // maximum alpha
displayWidth = canvas.width;
displayHeight = canvas.height;
fLen = this.distance; // represents the distance from the viewer to z=0 depth.
// projection center coordinates sets location of origin
projCenterX = displayWidth / 2;
projCenterY = displayHeight / 2;
// we will not draw coordinates if they have too large of a z-coordinate (which means they are very close to the observer).
zMax = fLen - 2;
particleList = {};
recycleBin = {};
// random acceleration factors - causes some random motion
randAccelX = 0.1;
randAccelY = 0.1;
randAccelZ = 0.1;
gravity = -0; // try changing to a positive number (not too large, for example 0.3), or negative for floating upwards.
particleRad = this.particle_size;
sphereCenterX = 0;
sphereCenterY = 0;
sphereCenterZ = -3 - this.sphereRad;
// alpha values will lessen as particles move further back, causing depth-based darkening:
zeroAlphaDepth = 0;
turnSpeed = 2 * Math.PI / 1200; // the sphere will rotate at this speed (one complete rotation every 1600 frames).
turnAngle = 0; // initial angle
// timer = setInterval(onTimer, 10 / 24);
onTimer();
}
const onTimer = () => {
// if enough time has elapsed, we will add new particles.
count++;
if (count >= wait) {
count = 0;
for (i = 0; i < numToAddEachFrame; i++) {
theta = Math.random() * 2 * Math.PI;
phi = Math.acos(Math.random() * 2 - 1);
x0 = this.sphereRad * Math.sin(phi) * Math.cos(theta);
y0 = this.sphereRad * Math.sin(phi) * Math.sin(theta);
z0 = this.sphereRad * Math.cos(phi);
// We use the addParticle function to add a new particle. The parameters set the position and velocity components.
// Note that the velocity parameters will cause the particle to initially fly outwards away from the sphere center (after
// it becomes unstuck).
const p = addParticle(x0, sphereCenterY + y0, sphereCenterZ + z0, 0.002 * x0, 0.002 * y0, 0.002 * z0);
// we set some 'envelope' parameters which will control the evolving alpha of the particles.
p.attack = 50;
p.hold = 50;
p.decay = 100;
p.initValue = 0;
p.holdValue = particleAlpha;
p.lastValue = 0;
// the particle will be stuck in one place until this time has elapsed:
p.stuckTime = 90 + Math.random() * 20;
p.accelX = 0;
p.accelY = gravity;
p.accelZ = 0;
}
}
// update viewing angle
turnAngle = (turnAngle + turnSpeed) % (2 * Math.PI);
sinAngle = Math.sin(turnAngle);
cosAngle = Math.cos(turnAngle);
// background fill
context.fillStyle = '#000000';
context.fillRect(0, 0, displayWidth, displayHeight);
// update and draw particles
p = particleList.first;
while (p != null) {
// before list is altered record next particle
nextParticle = p.next;
// update age
p.age++;
// if the particle is past its 'stuck' time, it will begin to move.
if (p.age > p.stuckTime) {
p.velX += p.accelX + randAccelX * (Math.random() * 2 - 1);
p.velY += p.accelY + randAccelY * (Math.random() * 2 - 1);
p.velZ += p.accelZ + randAccelZ * (Math.random() * 2 - 1);
p.x += p.velX;
p.y += p.velY;
p.z += p.velZ;
}
/*
We are doing two things here to calculate display coordinates.
The whole display is being rotated around a vertical axis, so we first calculate rotated coordinates for
x and z (but the y coordinate will not change).
Then, we take the new coordinates (rotX, y, rotZ), and project these onto the 2D view plane.
*/
rotX = cosAngle * p.x + sinAngle * (p.z - sphereCenterZ);
rotZ = -sinAngle * p.x + cosAngle * (p.z - sphereCenterZ) + sphereCenterZ;
// m = this.radius_sp * fLen / (fLen - rotZ);
m = this.radius_sp;
p.projX = rotX * m + projCenterX;
p.projY = p.y * m + projCenterY;
p.projZ = rotZ * m + projCenterX;
// update alpha according to envelope parameters.
if (p.age < p.attack + p.hold + p.decay) {
if (p.age < p.attack) {
p.alpha = (p.holdValue - p.initValue) / p.attack * p.age + p.initValue;
} else if (p.age < p.attack + p.hold) {
p.alpha = p.holdValue;
} else if (p.age < p.attack + p.hold + p.decay) {
p.alpha = (p.lastValue - p.holdValue) / p.decay * (p.age - p.attack - p.hold) + p.holdValue;
}
} else {
p.dead = true;
}
// see if the particle is still within the viewable range.
if ((p.projX > displayWidth) || (p.projX < 0) || (p.projY < 0) || (p.projY > displayHeight) || (rotZ > zMax)) {
outsideTest = true;
} else {
outsideTest = false;
}
if (outsideTest || p.dead ||
(p.projX > displayWidth / (2 + (1 - Math.random())) && p.projZ + displayWidth * 0.1 > displayWidth / 2) ||
(p.projX < displayWidth / (2 - (1 - Math.random())) && p.projZ + displayWidth * 0.25 < displayWidth / 2)
) {
recycle(p);
} else {
// depth-dependent darkening
// console.log(turnAngle, rotZ)
depthAlphaFactor = 1;
// depthAlphaFactor = (1 - (1.5 + rotZ / 100));
depthAlphaFactor = (depthAlphaFactor > 1) ? 1 : ((depthAlphaFactor < 0) ? 0 : depthAlphaFactor);
context.fillStyle = rgbString + depthAlphaFactor * p.alpha + ')';
// draw
context.beginPath();
context.arc(p.projX, p.projY, m * particleRad, 0, 2 * Math.PI, false);
context.closePath();
context.fill();
}
p = nextParticle;
}
window.requestAnimationFrame(onTimer);
}
const addParticle = (x0, y0, z0, vx0, vy0, vz0) => {
let newParticle;
// const color;
// check recycle bin for available drop:
if (recycleBin.first != null) {
newParticle = recycleBin.first;
// remove from bin
if (newParticle.next != null) {
recycleBin.first = newParticle.next;
newParticle.next.prev = null;
} else {
recycleBin.first = null;
}
} else {
newParticle = {};
}
// if the recycle bin is empty, create a new particle (a new empty object):
// add to beginning of particle list
if (particleList.first == null) {
particleList.first = newParticle;
newParticle.prev = null;
newParticle.next = null;
} else {
newParticle.next = particleList.first;
particleList.first.prev = newParticle;
particleList.first = newParticle;
newParticle.prev = null;
}
// initialize
newParticle.x = x0;
newParticle.y = y0;
newParticle.z = z0;
newParticle.velX = vx0;
newParticle.velY = vy0;
newParticle.velZ = vz0;
newParticle.age = 0;
newParticle.dead = false;
if (Math.random() < 0.5) {
newParticle.right = true;
} else {
newParticle.right = false;
}
return newParticle;
}
const recycle = (p) => {
// remove from particleList
if (particleList.first === p) {
if (p.next != null) {
p.next.prev = null;
particleList.first = p.next;
} else {
particleList.first = null;
}
} else {
if (p.next == null) {
p.prev.next = null;
} else {
p.prev.next = p.next;
p.next.prev = p.prev;
}
}
// add to recycle bin
if (recycleBin.first == null) {
recycleBin.first = p;
p.prev = null;
p.next = null;
} else {
p.next = recycleBin.first;
recycleBin.first.prev = p;
recycleBin.first = p;
p.prev = null;
}
};
init();
}
}
So I will be happy with any help also REWARD(for full implementation) is possible (ETH, BTC any currency you wish).

Translating pixels in canvas on sine wave

I am trying to create an image distortion effect on my canvas, but nothing appears to be happening. Here is my code:
self.drawScreen = function (abilityAnimator, elapsed) {
if (!self.initialized) {
self.initialized = true;
self.rawData = abilityAnimator.context.getImageData(self.targetX, self.targetY, self.width, self.height);
self.initialImgData = self.rawData.data;
}
abilityAnimator.drawBackground();
self.rawData = abilityAnimator.context.getImageData(self.targetX, self.targetY, self.width, self.height);
var imgData = self.rawData.data, rootIndex, translationIndex, newX;
for (var y = 0; y < self.height; y++) {
for (var x = 0; x < self.width; x++) {
rootIndex = (y * self.height + x) * 4;
newX = Math.ceil(self.amplitude * Math.sin(self.frequency * (y + elapsed)));
translationIndex = (y * self.width + newX) * 4;
imgData[translationIndex + 0] = self.initialImgData[rootIndex + 0];
imgData[translationIndex + 1] = self.initialImgData[rootIndex + 1];
imgData[translationIndex + 2] = self.initialImgData[rootIndex + 2];
imgData[translationIndex + 3] = self.initialImgData[rootIndex + 3];
}
}
abilityAnimator.context.putImageData(self.rawData, self.targetX, self.targetY);
};
abilityAnimator is a wrapper for my canvas object:
abilityAnimator.context = //canvas.context
abilityAnimator.drawBackground = function(){
this.canvas.width = this.canvas.width;
}
elapsed is simply the number of milliseconds since the animation began (elapsed is always <= 2000)
My member variables have the following values:
self.width = 125;
self.height = 125;
self.frequency = 0.5;
self.amplitude = self.width / 4;
self.targetX = //arbitrary value within canvas
self.targetY = //arbitrary value within canvas
I can translate the image to the right very easily so long as there is no sine function, however, introducing these lines:
newX = Math.ceil(self.amplitude * Math.sin(self.frequency * (y + elapsed)));
translationIndex = (y * self.width + newX) * 4;
Causes nothing to render at all. The translation indexes don't appear to be very strange, and the nature of the sinusoidal function should guarantee that the offset is no greater than 125 / 4 pixels.
Your formula using sin is wrong, the frequency will be so high it will be seen as noise.
The typical formula to build a sinusoid is :
res = sin ( 2 * PI * frequency * time ) ;
where frequency is in Hz and time in s.
So in js that would translate to :
res = Math.sin ( 2 * Math.PI * f * time_ms * 1e-3 ) ;
you can obviously compute just once the constant factor :
self.frequency = 0.5 * ( 2 * Math.PI * 1e-3 );
// then use
res = Math.sin ( self.frequency * time_ms ) ;
So you see you were 1000 times too fast.
Second issue :
Now that you have your time frequency ok, let's fix your spatial frequency : when multiplying time frequency by y, you're quite adding apples and cats.
To build the formula, think that you want to cross n time 2*PI during the height of the canvas.
So :
spatialFrequency = ( n ) * 2 * Math.PI / canvasHeight ;
and your formula becomes :
res = Math.sin ( self.frequency * time_ms + spatialFrequency * y ) ;
You can play with various values with this jsbin i made so you can visualize the effect :
http://jsbin.com/ludizubo/1/edit?js,output

Categories

Resources