A red rectangle, that I've drawn should smoothly disappear.
As you can see here, it works, but it does not completely disappear. Why?
(function init() {
var canvas = document.getElementById('canvas'), ctx;
if (!canvas.getContext) return;
ctx = canvas.getContext('2d');
ctx.fillStyle = "red";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.1)";
setInterval(function() {
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fill();
}, 100);
}());
It should also do the job with lots of different colors and alpha values at the same time.
Thank you :D
It's due to rounding errors in canvas. The value when multiplied with the alpha channel will have to cut the fraction to fit the integer nature of the bitmap.
In all cases here the value will never become full alpha.
The work-around is to track the current alpha level and at the last one clear manually.
Example here
var tracker = 0,
timer;
ctx.fillStyle = "rgba(255,255,255,0.1)";
timer = setInterval(function() {
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fill();
tracker++;
if (tracker > 43) {
clearTimeout(timer);
ctx.fillStyle = "rgb(255,255,255)";
ctx.fillRect(0,0,canvas.width,canvas.height);
}
}, 100);
Related
When creating an HTML canvas I was planning on making these cylinders and animating marbles moving inside them. However, when trying to do so it would just delete everything. After messing around with my code, I discovered the problem was due to the fillStyle which was a CanvasPattern from an image.
This snippet simulates exactly what I am experiencing. The rectangle draws perfectly fine, however, after 1 second, when the interval runs, it disappears and there is no arc or "marble" drawn. There are no errors in console either
With Interval (Not working):
let canv = document.getElementById("canvas");
let ctx = canv.getContext('2d');
let matte = new Image(canv.width, canv.height);
matte.onload = function() {
var pattern = ctx.createPattern(matte, 'repeat');
ctx.globalCompositeOperation = 'source-in';
ctx.rect(0, 0, canv.width, canv.height);
ctx.fillStyle = pattern;
ctx.fill();
};
matte.src = "https://www.muralswallpaper.com/app/uploads/classic-red-marble-textures-plain-820x532.jpg"; // An image src
ctx.lineWidth = "5";
ctx.fillRect(0, 0, 50, 50); // This dissapears when the setInterval runs???? Marble doesn't even draw
let x = 60,
y = 20;
var draw = setInterval(function() { // Drawing the marble
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
y += 1;
}, 1 * 1000);
<html>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
When I get rid of the interval it would work, but when the interval is there, nothing is drawn.
I have absolutely no idea why this is happening and I cannot find anything on the internet regarding this problem. Is there a way I can animate this marble while having the image continue to mask its fillStyle??
Without Interval (Working):
let canv = document.getElementById("canvas");
let ctx = canv.getContext('2d');
let matte = new Image(canv.width, canv.height);
matte.onload = function() {
var pattern = ctx.createPattern(matte, 'repeat');
ctx.globalCompositeOperation = 'source-in';
ctx.rect(0, 0, canv.width, canv.height);
ctx.fillStyle = pattern;
ctx.fill();
};
matte.src = "https://www.muralswallpaper.com/app/uploads/classic-red-marble-textures-plain-820x532.jpg"; // An image src
ctx.lineWidth = "5";
ctx.fillRect(0, 0, 50, 50); // This dissapears when the setInterval runs???? Marble doesn't even draw
let x = 60,
y = 20;
//var draw = setInterval(function() { // Drawing the marble
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
y += 1;
//}, 1 * 1000);
<html>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
Things I've tried:
Got rid of beginPath and closePath, doesn't make anything disappear but doesn't display arc
Recreating pattern inside the interval
Making the fillstyle a colour for everything (Works)
Making the fillstyle of the marble a colour (Doesnt work)
EDIT: After looking some more, I believe the problem is in the globalCompositeOperation. It's what deals with the pattern intersecting the drawing. When looking at all the types, source-in is the only one that satisfies my expected result, however, it's not working in this situation weirdly.
Thank you in advance
The problem is your ctx.globalCompositeOperation instruction. Using source-in, you're explicitly telling the canvas to make anything that's a different color from the new thing you're drawing (on a per pixel basis) transparent. Since every pixel is different, everything becomes transparent and you're left with what looks like an empty canvas (even if the ImageData will show RGBA data in which the RGB channels have meaningful content, but A is 0).
Remove the globalCompositeOperation rule and you're good to go, but you should probably take some time to rewrite the logic here, so that nothing happens until your image is loaded, because your code is pretty dependent on that pattern existing: wait for the image to load, the build the pattern, assign it to the context, and then start your draw loop.
const canv = document.getElementById("canvas");
const ctx = canv.getContext('2d');
let x = 60, y = 20;
function start() {
const matte = new Image(canv.width, canv.height);
matte.addEventListener(`load`, evt =>
startDrawing(ctx.createPattern(matte, 'repeat'))
);
matte.addEventListener(`load`, evt =>
console.error(`Could not load ${matte.src}...`);
);
matte.src = "https://www.muralswallpaper.com/app/uploads/classic-red-marble-textures-plain-820x532.jpg"; // An image src
}
function startDrawing(pattern) {
ctx.strokeStyle = `red`;
ctx.fillStyle = pattern;
setInterval(() => {
draw();
y += 10;
}, 1 * 1000);
}
function draw() {
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
// and kick everything off
start();
Although on another note, normally setInterval is not the best choice for animations: you usually want requestAnimationFrame instead, with a "wall time" check (e.g. doing things depending on the actual clock, instead of trusting intervals):
...
function startDrawing(pattern) {
ctx.strokeStyle = `red`;
ctx.fillStyle = pattern;
startAnimation();
}
let playing, lastTime;
function startAnimation() {
playing = true;
lastTime = Date.now();
requestAnimationFrame(nextFrame);
}
function stopAnimation() {
playing = false;
}
function nextFrame() {
let newTime = Date.now();
if (newTime - lastTime >= 1000) {
draw();
}
if (playing) {
lastTime = newTime;
requestAnimationFrame(nextFrame);
}
}
...
https://jsbin.com/wawecedeve/edit?js,output
There are thousands of moving particles on an HTML5 canvas, and my goal is to draw a short fading trail behind each one. A nice and fast way to do this is to not completely clear the canvas each frame, but overlay it with semi-transparent color. Here is an example with just one particle:
var canvas = document.getElementById('display');
var ctx = canvas.getContext('2d');
var displayHeight = canvas.height;
var backgroundColor = '#000000';
var overlayOpacity = 0.05;
var testParticle = {
pos: 0,
size: 3
};
function render(ctx, particle) {
ctx.globalAlpha = overlayOpacity;
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1.0;
ctx.fillStyle = '#FFF';
ctx.fillRect(particle.pos, displayHeight / 2, particle.size, particle.size);
}
function update(particle) {
particle.pos += 1;
}
// Fill with initial color
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
function mainLoop() {
update(testParticle);
render(ctx, testParticle);
requestAnimationFrame(mainLoop);
}
mainLoop();
<canvas id="display" width="320" height="240"></canvas>
There is an apparent problem: with low opacity values, the trail never fades away completely. You can see the horizontal line that (almost) does not fade in my single-particle example. I understand why this happens. ColorA overlayed by semi-transparent ColorB is basically a linear interpolation, and ColorA never fully converges to ColorB if we repeatedly do the following:
ColorA = lerp(ColorA, ColorB, opacityOfB)
My question is, what can I do to make it converge to the background color, so that trails don't remain there forever? Using WebGL or drawing trails manually are not valid options (because of compatibility and performance reasons respectively). One possibility is to loop over all canvas pixels and manually set pixels with low brightness to background color, although it may get expensive for large canvases. I wonder if there are better solutions.
As a workaround which could work in some cases is to set the overlayOpacity up to 0.1 (this value converges) but draw it only every x times and not in every render call.
So when drawn only every other time it keeps more or less the same trail length.
var renderCount = 0;
var overlayOpacity = 0.1;
function render(ctx, particle) {
if((renderCount++)%2 == 0) {
ctx.globalAlpha = overlayOpacity;
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.globalAlpha = 1.0;
ctx.fillStyle = '#FFF';
ctx.fillRect(particle.pos, displayHeight / 2, particle.size, particle.size);
}
Obviously the disadvantage is that it looks more jerked and perhaps this may not be acceptable in your case.
Best solution is to use the composite operation "destination-out" and fade to a transparent background. Works well for fade rates down to globalAlpha = 0.01 and event a little lower 0.006 but it can be troublesome below that. Then if you need even slower fade just doe the fade every 2nd or 3rd frame.
ctx.globalAlpha = 0.01; // fade rate
ctx.globalCompositeOperation = "destination-out" // fade out destination pixels
ctx.fillRect(0,0,w,h)
ctx.globalCompositeOperation = "source-over"
ctx.globalAlpha = 1; // reset alpha
If you want a coloured background you will need to render the animation on an offscreen canvas and render it over the onscreen canvas each frame. Or make the canvas background the colour you want.
If someone struggles with this, here is a workaround that worked for me:
// Do this instead of ctx.fillStyle some alpha value and ctx.fillRect
if(Math.random() > 0.8){
ctx.fillStyle = 'rgba(255, 255, 255, '+getRandomNumber(0.1,0.001)+')';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Define this helper function somewhere in your code
function getRandomNumber(minValue, maxValue) {
return Math.random() * (maxValue - minValue) + minValue;
}
It also works for different colored backgrounds. Adjust trail length by playing around with Math.random() > 0.8 and getRandomNumber(0.1,0.001).
I am trying to do a simple animation with html5. Please take a look at the link below, through a touch screen device.
https://dl.dropbox.com/u/41627/wipe.html
The problem is as follows : Every time the user touches the screen , a box gets drawn around his finger which animates from small to big. I want just the outer most boundary to be visible and not the rest. I do not want to clear the canvas as I want the state of the rest of the canvas to be preserved.
Images to illustrate the issue:
My code is as follows :
function init() {
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var img = document.createElement('IMG');
img.onload = function () {
ctx.beginPath();
ctx.drawImage(img, 0, 0);
ctx.closePath();
ctx.globalCompositeOperation = 'destination-out';
}
img.src = "https://dl.dropbox.com/u/41627/6.jpg";
function drawPoint(pointX,pointY){
var grd = ctx.createRadialGradient(pointX, pointY, 0, pointX, pointY, 30);
grd.addColorStop(0, "rgba(255,255,255,.6)");
grd.addColorStop(1, "transparent");
ctx.fillStyle = grd;
ctx.beginPath();
ctx.arc(pointX,pointY,50,0,Math.PI*2,true);
ctx.fill();
ctx.closePath();
}
var a = 0;
var b = 0;
function boxAround(pointX,pointY, a, b) {
ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle = "black";
ctx.strokeRect(pointX-a, pointY-b, (2*a), (2*b));
ctx.globalCompositeOperation = 'destination-out';
if(a < 100) {
setTimeout(function() {
boxAround(pointX,pointY, a+5, b+5);
}, 20);
}
}
canvas.addEventListener('touchstart',function(e){
drawPoint(e.touches[0].screenX,e.touches[0].screenY);
boxAround(e.touches[0].screenX,e.touches[0].screenY,0 , 0);
},false);
canvas.addEventListener('touchmove',function(e){
e.preventDefault();
drawPoint(e.touches[0].screenX,e.touches[0].screenY);
},false);
You can achieve this effect by either using a second canvas, or even just having the box be a plain <div> element that is positioned over the canvas. Otherwise, there is no way around redrawing your canvas.
I'm trying to make a website where an image is drawn on Canvas, then later the user is able to press a button to ctx.fill() certain parts of it with color. I'm running into issues where I can only ctx.fill() the most recently created shape which often isn't the shape I want.
Here's an example. In this code (live at http://build.rivingtondesignhouse.com/piol/test/) I'm trying to draw the first rectangle, then save() it on the stack, then draw the second rectangle (and don't save it), then when my fill() function is called I want to restore() the first rectangle and ctx.fill() it with a different pattern. It doesn't work!
In practice, I'm actually trying to fill the gray part of this complex shape with any color the user chooses AFTER the image has been drawn, but I think the technique is the same. (http://build.rivingtondesignhouse.com/piol/test/justTop.html)
Thanks in advance for any help!!!
Here's the code:
<script type="text/javascript">
var canvas;
var ctx;
function init() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
draw();
}
function draw() {
ctx.fillStyle = '#FA6900';
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 4;
ctx.shadowColor = 'rgba(204, 204, 204, 0.5)';
ctx.fillRect(0,0,15,150);
ctx.save();
ctx.fillStyle = '#E0E4CD';
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowBlur = 4;
ctx.shadowColor = 'rgba(204, 204, 204, 0.5)';
ctx.fillRect(30,0,30,150);
}
function fill(){
var image = new Image();
image.src = "http://www.html5canvastutorials.com/demos/assets/wood-pattern.png";
image.onload = drawPattern;
function drawPattern() {
ctx.restore();
ctx.fillStyle = ctx.createPattern(image, "repeat");
ctx.fill();
}
}
init();
There are a few misunderstands that we need to clear up before I can answer the question.
save() and restore() do not save and restore the canvas bitmap. Instead they save and restore all properties that are set on the canvas context and that's all!
For example
ctx.fillStyle = 'red';
ctx.save(); // save the fact that the fillstyle is red
ctx.fillStyle = 'blue'; // change the fillstyle
ctx.fillRect(0,0,5,5); // draws a blue rectangle
ctx.restore(); // restores the old context state, so the fillStyle is back to red
ctx.fillRect(10,0,5,5); // draws a red rectangle // draws a red rectangle
See that code live here.
So you aren't saving a rectangle (or anything drawn) by calling save(). The only way you you can save the bitmap is by drawing it (or part of it) to another canvas (using anotherCanvasContext.drawImage(canvasIWantToSave, 0, 0)) or by saving enough information that you can redraw the entire scene with the appropriate changes.
Here is an example of one way you could re-structure your code so that it does what you want: http://jsfiddle.net/xwqXb/
I'm looking to create a simple animation loop using html canvas. Each path(or triangle) is intended to fade-in in sequence and then fade-out in the same sequence. I've found a great deal of resources around animating with motion, but not with alpha and especially not animating paths in succession within canvas. Any ideas?
Disclaimer: This is my first foray into using canvas, and my javascript knowledge is shoddy. Since I'm using the same shape, I'm going to figure out how to replicate, rotate and translate the original as a separate learning.
Update 1: To track my progress, here is a link to my sandbox.
Update 2: I changed the structure of the script to give me more control over each path.
var elem = document.getElementById('loader');
if (elem && elem.getContext) {
var ctxYellow = elem.getContext('2d');
var ctxOrange = elem.getContext('2d');
var ctxRed = elem.getContext('2d');
var ctxViolet = elem.getContext('2d');
var ctxBlue = elem.getContext('2d');
// Yellow triangle
if (ctxYellow) {
ctxYellow.fillStyle = '#f5ab1c';
ctxYellow.beginPath();
ctxYellow.moveTo(150, 150);
ctxYellow.lineTo(20, 75);
ctxYellow.lineTo(150, 0);
ctxYellow.lineTo(150, 150);
ctxYellow.fill();
ctxYellow.closePath();
}
// Orange triangle
if (ctxOrange) {
ctxOrange.fillStyle = '#f26d23';
ctxOrange.beginPath();
ctxOrange.moveTo(150, 150);
ctxOrange.lineTo(150, 0);
ctxOrange.lineTo(280, 75);
ctxOrange.lineTo(150, 150);
ctxOrange.fill();
ctxOrange.closePath();
}
// Red triangle
if (ctxRed) {
ctxRed.fillStyle = '#cd1f44';
ctxRed.beginPath();
ctxRed.moveTo(150, 150);
ctxRed.lineTo(280, 75);
ctxRed.lineTo(280, 225);
ctxRed.lineTo(150, 150);
ctxRed.fill();
ctxRed.closePath();
}
// Violet triangle
if (ctxViolet) {
ctxViolet.fillStyle = '#851a54';
ctxViolet.beginPath();
ctxViolet.moveTo(150, 150);
ctxViolet.lineTo(280, 225);
ctxViolet.lineTo(150, 300);
ctxViolet.lineTo(150, 150);
ctxViolet.fill();
ctxViolet.closePath();
}
// Blue triangle
if (ctxBlue) {
ctxBlue.fillStyle = '#295a9c';
ctxBlue.beginPath();
ctxBlue.moveTo(150, 150);
ctxBlue.lineTo(150, 300);
ctxBlue.lineTo(20, 225);
ctxBlue.lineTo(150, 150);
ctxBlue.fill();
ctxBlue.closePath();
}
}
you're alomst there. first: you can NOT have multiple contexts at the same time for the same canvas node! either use multiple canvas-nodes stacked on top of each other or change the color after closePath() and before beginPath. That's actually what you are doing anyways, only that you used 5 variables for it where one is sufficient.
alpha can be animated with this:
var ctx = elem.getContext('2d');
ctx.globalAlpha = 0.4;
again, like the color this can be done during you animation though that will change the overall alpha value (not sure that's the right approach). I'd advice using rgba() values for fillStyle instead. basically you need to translate your color definition from hey to rgb values and then add a value from 0 to 1 for the alpha value. google hex to rgba for generators.
PS: check out the W3C spec for more info: http://dev.w3.org/html5/2dcontext/
OK, I kind of cheated. I ended up using jQuery. Here's what I came out with:
var elem = document.getElementById('yellow');
if (elem && elem.getContext) {
var ctx = elem.getContext('2d');
ctx.fillStyle = 'rgba(245,171,28,1)';
ctx.beginPath();
ctx.moveTo(150,150);
ctx.lineTo(20,75);
ctx.lineTo(150,0);
ctx.lineTo(150,150);
ctx.fill();
ctx.closePath();
}
var elem = document.getElementById('orange');
if (elem && elem.getContext) {
var ctx = elem.getContext('2d');
ctx.fillStyle = 'rgba(242,109,35,1)';
ctx.beginPath();
ctx.moveTo(150,150);
ctx.lineTo(150,0);
ctx.lineTo(280,75);
ctx.lineTo(150,150);
ctx.fill();
ctx.closePath();
}
var elem = document.getElementById('red');
if (elem && elem.getContext) {
var ctx = elem.getContext('2d');
ctx.fillStyle = 'rgba(205,31,68,1)';
ctx.beginPath();
ctx.moveTo(150,150);
ctx.lineTo(280,75);
ctx.lineTo(280,225);
ctx.lineTo(150,150);
ctx.fill();
ctx.closePath();
}
var elem = document.getElementById('violet');
if (elem && elem.getContext) {
var ctx = elem.getContext('2d');
ctx.fillStyle = 'rgba(133,26,84,1)';
ctx.beginPath();
ctx.moveTo(150,150);
ctx.lineTo(280,225);
ctx.lineTo(150,300);
ctx.lineTo(150,150);
ctx.fill();
ctx.closePath();
}
var elem = document.getElementById('blue');
if (elem && elem.getContext) {
var ctx = elem.getContext('2d');
ctx.fillStyle = 'rgba(41,90,156,1)';
ctx.beginPath();
ctx.moveTo(150, 150);
ctx.lineTo(150, 300);
ctx.lineTo(20, 225);
ctx.lineTo(150, 150);
ctx.fill();
ctx.closePath();
}
$(function() {
var timer = null;
var anim = function() {
$.each([yellow, orange, red, violet, blue], function(i, el) {
$('.color' + (i + 1)).delay(100 * i).fadeIn();
});
setTimeout(function() {
$.each([yellow, orange, red, violet, blue], function(i, el) {
$('.color' + (i + 1)).delay(100 * i).fadeOut();
});
}, 1500);
if (!timer) {
return timer = setInterval(anim, 3000);
} else {
return;
}
}
anim();
})
I made separate canvases for each path, absolutely positioned them, and animated them with jQuery. Thanks for the advice! It helped.
Update: My roommate walked me through some improvements to clean up the code and make it run smoother. I still can't get this to work in Firefox, though.