HTML5 Canvas stroke() changing color between draws - javascript

I've created a very simple JavaScript snippet to illustrate a weird HTML5 canvas behavior that I have been experiencing.
I keep drawing the same set of strokes, just in a different order, every 100ms. Why is it that the color of some of the strokes keeps changing? It only happens when I shuffle the draw order between calls, even though the lines are drawn in the same location and same color each frame.
const canvasWidth = 500;
const gapBetweenLines = 5;
const nbrLines = canvasWidth / gapBetweenLines;
const canvasHeight = 500;
const canvas = document.getElementById('map');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// create an array of line objects, each with a with random color
let lines = [];
for (let i = 0; i < nbrLines; i++) {
lines.push({
index: i,
x: i * gapBetweenLines,
color: '#' + Math.floor(Math.random() * 16777215).toString(16)
});
}
// function to shuffle the given array in place
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// draw lines on the canvas at specific intervals with the random colors
function drawLines() {
const shuffledLines = [...lines];
shuffle(shuffledLines);
let ctx = canvas.getContext('2d');
for (let i = 0; i < nbrLines; i++) {
const line = shuffledLines[i];
ctx.strokeStyle = line.color;
// ctx.save();
ctx.beginPath();
ctx.moveTo(line.x, 0);
ctx.lineTo(line.x, canvasHeight);
ctx.stroke();
// ctx.restore();
}
}
// call the drawLines function every 100ms
setInterval(drawLines, 100);
<!DOCTYPE html>
<html>
<body>
<h1>Flickering Lines</h1>
<canvas id="map"></canvas>
<div id="lineinfo"></div>
</body>
</html>
The past day had me spending an embarrassing amount of time trying to narrow down the cause of this. Is there something about HTML5 Canvas drawing that I simply misunderstand?
Saving and restoring the context between each stroke does not make any difference.

The problem is in your color generation.
color: '#' + Math.floor(Math.random() * 16777215).toString(16)
Number#toString(16) does not pad the returned string with zeroes:
console.log(12..toString(16)) // "c", not "0C"
This means that in your code, some of your lines may have their color property set to values that aren't a valid HEX format (e.g a five or two chars HEX is invalid).
To fix that, you can force your color generator to be always 6 length by padding as many zeroes as needed using the String#padStart() method.
const canvasWidth = 500;
const gapBetweenLines = 5;
const nbrLines = canvasWidth / gapBetweenLines;
const canvasHeight = 500;
const canvas = document.getElementById('map');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// create an array of line objects, each with a with random color
let lines = [];
for (let i = 0; i < nbrLines; i++) {
lines.push({
index: i,
x: i * gapBetweenLines,
color: '#' + Math.floor(Math.random() * 16777215).toString(16)
// force always 6 length
.padStart(6, "0")
});
}
// function to shuffle the given array in place
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// draw lines on the canvas at specific intervals with the random colors
function drawLines() {
const shuffledLines = [...lines];
shuffle(shuffledLines);
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < nbrLines; i++) {
const line = shuffledLines[i];
ctx.strokeStyle = line.color;
// ctx.save();
ctx.beginPath();
ctx.moveTo(line.x, 0);
ctx.lineTo(line.x, canvasHeight);
ctx.stroke();
// ctx.restore();
}
}
// call the drawLines function every 100ms
setInterval(drawLines, 100);
<!DOCTYPE html>
<html>
<body>
<h1>Flickering Lines</h1>
<canvas id="map"></canvas>
<div id="lineinfo"></div>
</body>
</html>

Related

Canvas Transparency creating perminant after-image

I am trying to achieve a tracing effect where the lines have a faded trail. The way I am trying to do it is simply by drawing the solid background once, and then on further frames draw a transparent background before drawing the new lines, so that you can still see a little of the image before it.
The issue is that I do want the lines to fade out completely after some time, but they seem to leave a permanent after image, even after drawing over them repeatedly.
I've tried setting different globalCompositeOperation(s) and it seemed like I was barking up the wrong tree there.
This code is called once
//initiate trace bg
traceBuffer.getContext("2d").fillStyle = "rgba(0, 30, 50, 1)";
traceBuffer.getContext("2d").fillRect(0, 0, traceBuffer.width, traceBuffer.height);
then inside the setInterval function it calls
//draw transparent background
ctx.fillStyle = "rgba(0, 30, 50, 0.04)";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//set drawing settings
ctx.strokeStyle = "#AAAAAA";
ctx.lineWidth = 4;
for (let i = 0; i < tracer.layer2.length; i++){
ctx.beginPath();
ctx.moveTo(newX, newY);
ctx.lineTo(oldX, oldY);
ctx.stroke();
}
Here's an example: https://i.imgur.com/QTkeIVf.png
On the left is what I am currently getting, and on the right is the edit of what I actually want to happen.
This is how I would do it. I would build a history of the particles moving on the track. The older the position the smaller the value of the alpha value for the fill. Also for a nicer effect I would reduce the size of the circle.
I hope this is what you need.
PS: I would have loved to have your curve. Since I don't have it I've drawn a different one.
const hypotrochoid = document.getElementById("hypotrochoid");
const ctx = hypotrochoid.getContext("2d");
const cw = (hypotrochoid.width = 300);
const ch = (hypotrochoid.height = 300);
const cx = cw / 2,
cy = ch / 2;
ctx.lineWidth = 1;
ctx.strokeStyle = "#d9d9d9";
// variables for the hypotrochoid
let a = 90;
let b = 15;
let h = 50;
// an array where to save the points used to draw the track
let track = [];
//add points to the track array. This will be used to draw the track for the particles
for (var t = 0; t < 2 * Math.PI; t += 0.01) {
let o = {};
o.x = cx + (a - b) * Math.cos(t) + h * Math.cos((a - b) / b * t);
o.y = cy + (a - b) * Math.sin(t) - h * Math.sin((a - b) / b * t);
track.push(o);
}
// a function to draw the track
function drawTrack(ry) {
ctx.beginPath();
ctx.moveTo(ry[0].x, ry[0].y);
for (let t = 1; t < ry.length; t++) {
ctx.lineTo(ry[t].x, ry[t].y);
}
ctx.closePath();
ctx.stroke();
}
// a class of points that are moving on the track
class Point {
constructor(pos) {
this.pos = pos;
this.r = 3;//the radius of the circle
this.history = [];
this.historyLength = 40;
}
update(newPos) {
let old_pos = {};
old_pos.x = this.pos.x;
old_pos.y = this.pos.y;
//save the old position in the history array
this.history.push(old_pos);
//if the length of the track is longer than the max length allowed remove the extra elements
if (this.history.length > this.historyLength) {
this.history.shift();
}
//gry the new position on the track
this.pos = newPos;
}
draw() {
for (let i = 0; i < this.history.length; i++) {
//calculate the alpha value for every element on the history array
let alp = i * 1 / this.history.length;
// set the fill style
ctx.fillStyle = `rgba(0,0,0,${alp})`;
//draw an arc
ctx.beginPath();
ctx.arc(
this.history[i].x,
this.history[i].y,
this.r * alp,
0,
2 * Math.PI
);
ctx.fill();
}
}
}
// 2 points on the track
let p = new Point(track[0]);
let p1 = new Point(track[~~(track.length / 2)]);
let frames = 0;
let n, n1;
function Draw() {
requestAnimationFrame(Draw);
ctx.clearRect(0, 0, cw, ch);
//indexes for the track position
n = frames % track.length;
n1 = (~~(track.length / 2) + frames) % track.length;
//draw the track
drawTrack(track);
// update and draw the first point
p.update(track[n]);
p.draw();
// update and draw the second point
p1.update(track[n1]);
p1.draw();
//increase the frames counter
frames++;
}
Draw();
canvas{border:1px solid}
<canvas id="hypotrochoid"></canvas>

Canvas code not updating and I'm not sure why

I'm trying to make a simple canvas program where the user clicks to create bouncing moving circles. It keeps freezing but still creates the circles without updating. I'm not sure whats going on, please help!
I'm adding each circle to an array of circles with the constructor
The setInterval loop seems to be freezing but the circles are still created even when this is happening
I'm having a hard time debugging this, any advice is greatly appreciated
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Background Test</title>
<style>
* { margin: 0; padding: 0; overflow: hidden; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
// Request animation frame -> Optimizes animation speed
const requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
const c = document.getElementById('canvas');
const ctx = c.getContext('2d');
// Fullscreen
c.width = window.innerWidth;
c.height = window.innerHeight;
ctx.fillStyle = 'red';
let fps = 60;
// FOR MOBILE DEVICES
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent))
fps = 29;
// Options
const background = '#333';
const circleMinSpeed = 3;
const circleMaxSpeed = 6;
const circleMinSize = 3;
const circleMaxSize = 10;
const circles = [];
let circlesCounter = 0;
const circlesTimeAlive = 20 * fps; // seconds
let i = 0;
const interval = 1000 / fps;
let now, delta;
let then = Date.now();
// Coordinate variables
let mouseX, mouseY, clickX, clickY;
// Tracks mouse movement
c.onmousemove = function(event)
{
mouseX = event.clientX;
mouseY = event.clientY;
};
// Tracks mouse click
c.onmousedown = function(event)
{
clickX = event.clientX;
clickY = event.clientY;
circle(clickX, clickY);
};
function draw()
{
// Loop
requestAnimationFrame(draw);
// Set NOW and DELTA
now = Date.now();
delta = now - then;
// New frame
if (delta > interval) {
// Update THEN
then = now - (delta % interval);
// Our animation
// Clear canvas then draw
ctx.clearRect(0, 0, c.width, c.height);
drawBackground();
drawCos();
drawCircles();
drawTest();
}
}
// Circle constructor
function circle(x, y)
{
// Pick random color
let r = Math.floor(Math.random() * 255);
let g = Math.floor(Math.random() * 255);
let b = Math.floor(Math.random() * 255);
self.color = 'rgb(' + r + ', ' + g + ', ' + b + ')';
self.xCo = x;
self.yCo = y;
// Pick random size within ranges
self.size = circleMinSize + Math.floor(Math.random() *
(circleMaxSize - circleMinSize));
// Pick random direction & speed (spdX spdY)
self.speed = circleMinSpeed + Math.floor(Math.random() *
(circleMaxSpeed - circleMinSpeed));
self.spdX = self.speed * (Math.random() * 2) - 1; // picks -1 to 1
self.spdY = self.speed * (Math.random() * 2) - 1;
self.draw = function()
{
ctx.beginPath();
ctx.arc(self.xCo, self.yCo, self.size, 0, 2*Math.PI);
ctx.fillStyle = self.color;
ctx.fill();
};
circles[circlesCounter++] = self;
}
// Draw the background
function drawBackground()
{
ctx.fillStyle = background;
ctx.fillRect(0, 0, c.width, c.height);
}
function drawCircles()
{
for (let i = 0; i < circles.length; i++)
circles[i].draw();
}
function drawTest()
{
ctx.fillStyle = 'red';
ctx.fillRect(i++, i, 5, 5);
}
function drawCos()
{
ctx.fillStyle = 'white';
ctx.fillText("X: " + mouseX + " Y:" + mouseY, 10, 10, 200);
}
// Main loop
setInterval(function()
{
// Loop through circles and move them
for (let i = 0; i < circles.length; i++)
{
if (circle[i])
{
// Check left and right bounce
if (circle[i].xCo <= 0 || circle[i].xCo >= c.width)
circle[i].spdX = -circle[i].spdX;
circle[i].xCo += circle[i].spdX;
// Check left and right bounce
if (circle[i].yCo <= 0 || circle[i].yCo >= c.height)
circle[i].spdY = -circle[i].spdY;
circle[i].yCo += circle[i].spdY;
}
}
// Draw Everything
draw();
}, interval);
</script>
</body>
</html>
This code:
self.draw = function()
{
ctx.beginPath();
ctx.arc(self.xCo, self.yCo, self.size, 0, 2*Math.PI);
ctx.fillStyle = self.color;
ctx.fill();
};
Is overriding this function:
function draw()
{
// Loop
requestAnimationFrame(draw);
// Set NOW and DELTA
now = Date.now();
delta = now - then;
// New frame
if (delta > interval) {
// Update THEN
then = now - (delta % interval);
// Our animation
// Clear canvas then draw
ctx.clearRect(0, 0, c.width, c.height);
drawBackground();
drawCos();
drawCircles();
drawTest();
}
}
You need to rethink how you want to draw your circles because you're re-drawing the black canvas every time a click event is triggered. I mean, when a click is triggered, you're applying new coordinates, color, Etc, and probably that's not what you want to do.
My suggestion is create canvas per circle and append them into a DIV.
Hope it helps!

JavaScript - Generate random pixels on HTML5 canvas

I want to create a random generated image (random colors), like this one. But, I want to do it in javascript, but for some reason I am getting black screen.
Here is my code:
var g=document . createElement( 'canvas').getContext('2d');
g.canvas.width=g.canvas.height = 800;
g.imgd = g.getImageData(0, 0, 800, 800);
g.data = g.imgd.data;
g.data.forEach((_, index) => (index & 3) < 3 && (g.data[index] = Math.random()));
g.putImageData(g.imgd, 0, 0);
document.body.appendChild(g.canvas);;;
And i am getting black screen, and on some websites it is white screen. So what is what not working in my script? My english is not very good, but can someone explain what is wrong, my code dont'esnt working.
I also tried different dimensions of canvas and I dont see any errors so what is wrong?
You are using Math.random() which generates floats from 0 to 1 without including 1. Since you're applying zeroes to the color components (the data from getImageData().data), you get the color black (rgb(0, 0, 0)).
Here's a more readable solution:
var canvas = document.createElement('canvas');
canvas.width = canvas.height = 800;
var ctx = canvas.getContext('2d');
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
for (var i = 0; i < imgData.data.length; i += 4) {
imgData.data[i] = randomInt(0, 255); // red
imgData.data[i+1] = randomInt(0, 255); // green
imgData.data[i+2] = randomInt(0, 255); // blue
imgData.data[i+3] = 255; // alpha
}
ctx.putImageData(imgData, 0, 0);
document.body.appendChild(canvas);
Math.random() returns a floating point number, not within the full range of 0-255. You can alternatively use .fillStyle() and set the color to a random hex color.
function pixels(width = 100, height = 100, size = 1, canvas) {
var canvas = canvas || document.createElement("canvas");
var ctx = canvas.getContext("2d");
var total = [];
canvas.width = width;
canvas.height = height;
function random() {
return "XXXXXX".replace(/X/g, function() {
var seed = "a0b1c2d3e4f56789";
return seed.charAt(Math.floor(Math.random() * seed.length))
})
};
for (var x = 0; x <= width; x += size) {
total.push(x)
};
total.forEach(function(value, index) {
for (var i = 0; i <= height; i++) {
ctx.fillStyle = "#" + random();
ctx.fillRect(value, total[i], size, size);
}
});
document.body.appendChild(canvas);
return ctx;
};
var c = pixels(window.innerWidth - 20, window.innerHeight - 20);

scrambling images on canvas producing whitespace

I'm starting with a canvas element. I'm making the left half red, and the right side blue. Every half second, setInterval calls a function, scramble, which splits both RHS and LHS into pieces, and shuffles them.
Here is a fiddle: https://jsfiddle.net/aeq1g3yb/
The code is below. The reason I'm using window.onload is because this thing is supposed to scramble pictures and I want the pictures to load first. I'm using colors here because of the cross-origin business that I don't know enough about, so this is my accommodation.
var n = 1;
var v = 1;
function scramble() {
//get the canvas and change its width
var c = document.getElementById("myCanvas");
c.width = 600;
var ctx = c.getContext("2d");
//drawing 2 different colors side by side
ctx.fillStyle = "red";
ctx.fillRect(0, 0, c.width/2, c.height);
ctx.fillStyle = "blue";
ctx.fillRect(c.width/2, 0, c.width/2, c.height);
//how big will each shuffled chunk be
var stepsA = (c.width/2) / n;
var stepsB = (c.width/2) / n;
var step = stepsA + stepsB;
var imgDataA = [];
var imgDataB = [];
for (var i = 0; i < n; i++) {
var imgDataElementA = ctx.getImageData(stepsA*i, 0, stepsA, c.height);
var imgDataElementB = ctx.getImageData(c.width/2+stepsB*i, 0, stepsB, c.height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
//clearing out the canvas before laying on the new stuff
ctx.fillStyle = "white";
ctx.fillRect(0, 0, c.width, c.height);
//put the images back
for (var i = 0; i < n; i++) {
ctx.putImageData(imgDataA[i], step*i, 0);
ctx.putImageData(imgDataB[i], step*i+stepsA, 0);
}
//gonna count the steps
var count = document.getElementById("count");
count.innerHTML = n;
n += v;
if (n >= 100 || n <= 1) {
v *= -1;
}
}; //closing function scramble
window.onload = function() { //gotta do this bc code executes before image loads
scramble();
};
window.setInterval(scramble, 500);
More or less, this thing works the way I want it to. But there is one problem: Sometimes there are vertical white lines.
My question is:
Why are there white lines? If you view the fiddle, you will see the degree to which this impairs the effect of the shuffle.
You can`t divide a Pixel
The problem can be solve but will introduce some other artifacts as you can not divide integer pixels into fractions.
Quick solution
The following solution for your existing code rounds down for the start of a section and up for the width.
for (var i = 0; i < n; i++) {
var imgDataElementA = ctx.getImageData(
Math.floor(stepsA * i), 0,
Math.ceil(stepsA + stepsA * i) - Math.floor(stepsA * i), c.height
);
var imgDataElementB = ctx.getImageData(
Math.floor(c.width / 2 + stepsB * i), 0,
Math.ceil(c.width / 2 + stepsB * i + stepsB) - Math.floor(c.width / 2 + stepsB * i), c.height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
Quicker options
But doing this via the pixel image data is about the slowest possible way you could find to do it. You can just use the 2D context.imageDraw function to do the movement for you. Or if you want the best in terms of performance a WebGL solution would be the best with the fragment shader doing the scrambling for you as a parallel solution.
There is no perfect solution
But in the end you can not cut a pixel in half, there are a wide range of ways to attempt to solve this but each method has its own artifacts. Ideally you should only slice an image if the rule image.width % slices === 0 in all other cases you will have one or more slices that will not fit on an integer number of pixels.
Example of 4 rounding methods.
The demo shows 4 different methods and with 2 colors. Mouse over to see a closer view. Each method is separated horizontally with a white line. Hold the mouse button to increase the slice counter.
The top is your original.
The next three are 3 different ways of dealing with the fractional pixel width.
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
const m = mouse;
if(m.element){
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
m.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : m.button;
}
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
const counterElement = document.getElementById("count");
// get constants for the demo
const c = document.getElementById("myCanvas");
mouse.element = c;
// The image with the blue and red
const img = document.createElement("canvas");
// the zoom image overlay
const zoom = document.createElement("canvas");
// the scrambled image
const scram = document.createElement("canvas");
// Set sizes and get context
const w = scram.width = zoom.width = img.width = c.width = 500;
const h = scram.height = zoom.height = img.height = c.height;
const dCtx = c.getContext("2d"); // display context
const iCtx = img.getContext("2d"); // source image context
const zCtx = zoom.getContext("2d"); // zoom context
const sCtx = scram.getContext("2d"); // scrambled context
// some constants
const zoomAmount = 4;
const zoomRadius = 60;
const framesToStep = 10;
function createTestPattern(ctx){
ctx.fillStyle = "red";
ctx.fillRect(0, 0, c.width/2, c.height/2);
ctx.fillStyle = "blue";
ctx.fillRect(c.width/2, 0, c.width/2, c.height/2);
ctx.fillStyle = "black";
ctx.fillRect(0, c.height/2, c.width/2, c.height/2);
ctx.fillStyle = "#CCC";
ctx.fillRect(c.width/2, c.height/2, c.width/2, c.height/2);
}
createTestPattern(iCtx);
sCtx.drawImage(iCtx.canvas, 0, 0);
// Shows a zoom area so that blind men like me can see what is going on.
function showMouseZoom(src,dest,zoom = zoomAmount,radius = zoomRadius){
dest.clearRect(0,0,w,h);
dest.imageSmoothingEnabled = false;
if(mouse.x >= 0 && mouse.y >= 0 && mouse.x < w && mouse.y < h){
dest.setTransform(zoom,0,0,zoom,mouse.x,mouse.y)
dest.drawImage(src.canvas, -mouse.x, -mouse.y);
dest.setTransform(1,0,0,1,0,0);
dest.globalCompositeOperation = "destination-in";
dest.beginPath();
dest.arc(mouse.x,mouse.y,radius,0,Math.PI * 2);
dest.fill();
dest.globalCompositeOperation = "source-over";
dest.lineWidth = 4;
dest.strokeStyle = "black";
dest.stroke();
}
}
function scramble(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w/2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
((i / 2) | 0) * steps + (i % 2) * (w / 2)- 0.5, y,
steps + 1, height,
i * steps - 0.5, y,
steps+ 1, height
);
}
}
function scrambleFloor(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w/2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
(((i / 2) | 0) * steps + (i % 2) * (w / 2)- 0.5) | 0, y,
steps + 1, height,
(i * steps - 0.5) | 0, y,
steps + 1, height
);
}
}
function scrambleNoOverlap(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w / 2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
((i / 2) | 0) * steps + (i % 2) * (w / 2), y,
steps, height,
i * steps - 0.5, y,
steps, height
);
}
}
function scrambleOriginal(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
//how big will each shuffled chunk be
var stepsA = (w/2) / slices;
var stepsB = (w/2) / slices;
var step = stepsA + stepsB;
var imgDataA = [];
var imgDataB = [];
for (var i = 0; i < slices; i++) {
var imgDataElementA = src.getImageData(stepsA*i, y, stepsA, height);
var imgDataElementB = src.getImageData(w/2+stepsB*i, y, stepsB, height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
//clearing out the canvas before laying on the new stuff
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
//put the images back
for (var i = 0; i < slices; i++) {
dest.putImageData(imgDataA[i], step*i, y);
dest.putImageData(imgDataB[i], step*i+stepsA, y);
}
}; //closing function scramble
const scrambleMethods = [scrambleOriginal,scramble,scrambleFloor,scrambleNoOverlap];
var frameCount = 0;
var sliceStep = 1;
var slices = 1;
function mainLoop(){
if(mouse.button){
if(frameCount++ % framesToStep === framesToStep-1){ // every 30 Frames
slices += sliceStep;
if(slices > 150 || slices < 2){ sliceStep = -sliceStep }
counterElement.textContent = slices; // Prevent reflow by using textContent
sCtx.clearRect(0,0,w,h);
sCtx.imageSmoothingEnabled = true;
const len = scrambleMethods.length;
for(var i = 0; i < len; i ++){
scrambleMethods[i](iCtx,sCtx,(128/len) * i, 128/len-2);
scrambleMethods[i](iCtx,sCtx,(128/len) * i + 128, 128/len-2);
}
}
}
dCtx.fillStyle = "white";
dCtx.fillRect(0,0,w,h);
dCtx.drawImage(sCtx.canvas,0,0);
showMouseZoom(dCtx,zCtx);
dCtx.drawImage(zCtx.canvas,0,0);
requestAnimationFrame(mainLoop);
}
//scramble(iCtx,sCtx);
requestAnimationFrame(mainLoop);
canvas {
border: 1px solid black;
}
#count {
position : absolute;
top : 0px;
left : 10px;
font-family: monospace;
font-size: 20px;
}
<canvas id="myCanvas" height = "256" title="Hold mouse button to chance slice count"></canvas>
<p id="count"></p>

Animating canvas to look like tv noise

I have a function named generateNoise() which creates a canvas element and paints random RGBA values to it; which, gives the appearance of noise.
My Question
What would be the best way to infinitely animate the noise to give the appearance of movement. So that it may have more life?
JSFiddle
function generateNoise(opacity) {
if(!!!document.createElement('canvas').getContext) {
return false;
}
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
x,y,
r,g,b,
opacity = opacity || .2;
canvas.width = 55;
canvas.height = 55;
for (x = 0; x < canvas.width; x++){
for (y = 0; y < canvas.height; y++){
r = Math.floor(Math.random() * 255);
g = Math.floor(Math.random() * 255);
b = Math.floor(Math.random() * 255);
ctx.fillStyle = 'rgba(' + r + ',' + b + ',' + g + ',' + opacity + ')';
ctx.fillRect(x,y,1,1);
}
}
document.body.style.backgroundImage = "url(" + canvas.toDataURL("image/png") + ")";
}
generateNoise(.8);
Update 1/2017: I rewrote the entire answer as it started to become rather messy, and to address some of the issues pointed out in the comments. The original answer can be found here. The new answer has in essence the same code but improved, and with a couple of new techniques, one utilizes a new feature available since this answer was first posted.
For a "true" random look we would need to use pixel-level rendering. We can optimize this using 32-bit unsigned buffers instead of 8-bit, and we can also turn off the alpha-channel in more recent browsers which speeds up the entire process (for older browsers we can simply set a black opaque background for the canvas element).
We create a reusable ImageData object once outside the main loop so the main cost is only putImageData() and not both inside the loop.
var ctx = c.getContext("2d", {alpha: false}); // context without alpha channel.
var idata = ctx.createImageData(c.width, c.height); // create image data
var buffer32 = new Uint32Array(idata.data.buffer); // get 32-bit view
(function loop() {
noise(ctx);
requestAnimationFrame(loop)
})()
function noise(ctx) {
var len = buffer32.length - 1;
while(len--) buffer32[len] = Math.random() < 0.5 ? 0 : -1>>0;
ctx.putImageData(idata, 0, 0);
}
/* for browsers wo/2d alpha disable support */
#c {background:#000}
<canvas id=c width=640 height=320></canvas>
A very efficient way, at the cost of some memory but reduced cost on the CPU, is to pre-render a larger off-screen canvas with the noise once, then place that canvas into the main one using random integer offsets.
It require a few extra preparation steps but the loop can run entirely on the GPU.
var w = c.width;
var h = c.height;
var ocanvas = document.createElement("canvas"); // create off-screen canvas
ocanvas.width = w<<1; // set offscreen canvas x2 size
ocanvas.height = h<<1;
var octx = ocanvas.getContext("2d", {alpha: false});
var idata = octx.createImageData(ocanvas.width, ocanvas.height);
var buffer32 = new Uint32Array(idata.data.buffer); // get 32-bit view
// render noise once, to the offscreen-canvas
noise(octx);
// main loop draw the offscreen canvas to random offsets
var ctx = c.getContext("2d", {alpha: false});
(function loop() {
var x = (w * Math.random())|0; // force integer values for position
var y = (h * Math.random())|0;
ctx.drawImage(ocanvas, -x, -y); // draw static noise (pun intended)
requestAnimationFrame(loop)
})()
function noise(ctx) {
var len = buffer32.length - 1;
while(len--) buffer32[len] = Math.random() < 0.5 ? 0 : -1>>0;
ctx.putImageData(idata, 0, 0);
}
/* for browsers wo/2d alpha disable support */
#c {background:#000}
<canvas id=c width=640 height=320></canvas>
Do note though that with the latter technique you may risk getting "freezes" where the new random offset is similar to the previous one. To work around this problem, set criteria for the random position to disallow too close positions in a row.
I tried to make a similar function a while ago. I set each pixel random value, and in addition to that, I overlayed a sinusodial wave that traveled upwards with time just to make it look more realistic. You can play with the constants in the wave to get different effects.
var canvas = null;
var context = null;
var time = 0;
var intervalId = 0;
var makeNoise = function() {
var imgd = context.createImageData(canvas.width, canvas.height);
var pix = imgd.data;
for (var i = 0, n = pix.length; i < n; i += 4) {
var c = 7 + Math.sin(i/50000 + time/7); // A sine wave of the form sin(ax + bt)
pix[i] = pix[i+1] = pix[i+2] = 40 * Math.random() * c; // Set a random gray
pix[i+3] = 255; // 100% opaque
}
context.putImageData(imgd, 0, 0);
time = (time + 1) % canvas.height;
}
var setup = function() {
canvas = document.getElementById("tv");
context = canvas.getContext("2d");
}
setup();
intervalId = setInterval(makeNoise, 50);
<canvas id="tv" width="400" height="300"></canvas>
I used it as a preloader on a site. I also added a volume rocker as a loading bar, here's a screenshot:
I re-wrote your code so each step is separate so you can re-use things without having to create and re-create each time, reduced in-loop calls and hopefully made it clear enough to be able to follow by reading it.
function generateNoise(opacity, h, w) {
function makeCanvas(h, w) {
var canvas = document.createElement('canvas');
canvas.height = h;
canvas.width = w;
return canvas;
}
function randomise(data, opacity) { // see prev. revision for 8-bit
var i, x;
for (i = 0; i < data.length; ++i) {
x = Math.floor(Math.random() * 0xffffff); // random RGB
data[i] = x | opacity; // set all of RGBA for pixel in one go
}
}
function initialise(opacity, h, w) {
var canvas = makeCanvas(h, w),
context = canvas.getContext('2d'),
image = context.createImageData(h, w),
data = new Uint32Array(image.data.buffer);
opacity = Math.floor(opacity * 0x255) << 24; // make bitwise OR-able
return function () {
randomise(data, opacity); // could be in-place for less overhead
context.putImageData(image, 0, 0);
// you may want to consider other ways of setting the canvas
// as the background so you can take this out of the loop, too
document.body.style.backgroundImage = "url(" + canvas.toDataURL("image/png") + ")";
};
}
return initialise(opacity || 0.2, h || 55, w || 55);
}
Now you can create some interval or timeout loop which keeps re-invoking the generated function.
window.setInterval(
generateNoise(.8, 200, 200),
100
);
Or with requestAnimationFrame as in Ken's answer
var noise = generateNoise(.8, 200, 200);
(function loop() {
noise();
requestAnimationFrame(loop);
})();
DEMO
Ken's answer looked pretty good, but after looking at some videos of real TV static, I had some ideas and here's what I came up with (two versions):
http://jsfiddle.net/2bzqs/
http://jsfiddle.net/EnQKm/
Summary of changes:
Instead of every pixel being independently assigned a color, a run of multiple pixels will get a single color, so you get these short, variable-sized horizontal lines.
I apply a gamma curve (with the Math.pow) to bias the color toward black a little.
I don't apply the gamma in a "band" area to simulate the banding.
Here's the main part of the code:
var w = ctx.canvas.width,
h = ctx.canvas.height,
idata = ctx.createImageData(w, h),
buffer32 = new Uint32Array(idata.data.buffer),
len = buffer32.length,
run = 0,
color = 0,
m = Math.random() * 6 + 4,
band = Math.random() * 256 * 256,
p = 0,
i = 0;
for (; i < len;) {
if (run < 0) {
run = m * Math.random();
p = Math.pow(Math.random(), 0.4);
if (i > band && i < band + 48 * 256) {
p = Math.random();
}
color = (255 * p) << 24;
}
run -= 1;
buffer32[i++] = color;
}
I happen to have just written a script that does just this, by getting the pixels from a black canvas and just altering random alpha values and using putImageData
Result can be found at http://mouseroot.github.io/Video/index.html
var currentAnimationFunction = staticScreen
var screenObject = document.getElementById("screen").getContext("2d");
var pixels = screenObject.getImageData(0,0,500,500);
function staticScreen()
{
requestAnimationFrame(currentAnimationFunction);
//Generate static
for(var i=0;i < pixels.data.length;i+=4)
{
pixels.data[i] = 255;
pixels.data[i + 1] = 255;
pixels.data[i + 2] = 255;
pixels.data[i + 3] = Math.floor((254-155)*Math.random()) + 156;
}
screenObject.putImageData(pixels,0,0,0,0,500,500);
//Draw 'No video input'
screenObject.fillStyle = "black";
screenObject.font = "30pt consolas";
screenObject.fillText("No video input",100,250,500);
}
Mine doesn't look identical to real TV static, but it's similar nonetheless. I'm just looping through all the pixels on canvas, and changing the RGB colour components of each pixel at a random coordinate to a random colour. The demo can be found over at CodePen.
The code is as follows:
// Setting up the canvas - size, setting a background, and getting the image data(all of the pixels) of the canvas.
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
canvas.width = 400;
canvas.height = 400;
canvasData = ctx.createImageData(canvas.width, canvas.height);
//Event listeners that set the canvas size to that of the window when the page loads, and each time the user resizes the window
window.addEventListener("load", windowResize);
window.addEventListener("resize", windowResize);
function windowResize(){
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
}
//A function that manipulates the array of pixel colour data created above using createImageData()
function setPixel(x, y, r, g, b, a){
var index = (x + y * canvasData.width) * 4;
canvasData.data[index] = r;
canvasData.data[index + 1] = g;
canvasData.data[index + 2] = b;
canvasData.data[index + 3] = a;
}
window.requestAnimationFrame(mainLoop);
function mainLoop(){
// Looping through all the colour data and changing each pixel to a random colour at a random coordinate, using the setPixel function defined earlier
for(i = 0; i < canvasData.data.length / 4; i++){
var red = Math.floor(Math.random()*256);
var green = Math.floor(Math.random()*256);
var blue = Math.floor(Math.random()*256);
var randX = Math.floor(Math.random()*canvas.width);
var randY = Math.floor(Math.random()*canvas.height);
setPixel(randX, randY, red, green, blue, 255);
}
//Place the image data we created and manipulated onto the canvas
ctx.putImageData(canvasData, 0, 0);
//And then do it all again...
window.requestAnimationFrame(mainLoop);
}
You can do it like this:
window.setInterval('generateNoise(.8)',50);
The 2nd arg 50 is a delay in milliseconds. Increasing 50 will slow it down and decreasing visa versa.
though.. this is going to severely affect web page performance. If it were me, I'd do the rendering server-side and render a handful of frame iterations and output as an animated gif. Not quite the same as infinite randomness, but would be a huge performance boost and IMO most people won't even notice.

Categories