I'm trying to implement a parallax scrolling effect with vanilla JS/HTML/CSS; what I've implemented already works, but there seems to be a lag with the undermost background canvas (I've made three canvases, one to hold the player/other obstacles, one to hold a background, and one to hold a foreground). The lagging seems to be only happening with the background (not the foreground).
// Drawing the background
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Preventing browser(s) from smoothing out/blurring lines
this.ctx.mozImageSmoothingEnabled = false;
this.ctx.webkitImageSmoothingEnabled = false;
this.ctx.msImageSmoothingEnabled = false;
this.ctx.imageSmoothingEnabled = false;
this.ctx.drawImage(this.image, this.posX, 0, this.image.width, this.image.height, 0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(this.image, this.posX - this.image.width, 0, this.image.width, this.image.height, 0, 0, this.canvas.width, this.canvas.height);
if (this.image.width > this.canvas.width) {
this.ctx.drawImage(this.image, this.x - this.image.width * 2, 0);
}
if (this.posX >= this.image.width) {
this.posX = 0;
}
this.scrollImage();
}
// Scrolls an image
scrollImage() {
this.posX += this.speed;
}
I've tried using whole numbers for the speed to avoid calculating decimal pixel sizes and after looking at some other posts with this problem, it seems like workarounds are difficult to find. (Actually, I'm not quite sure if this is exactly my problem, but that's the only reason for the lag that I can think of). It is strange that the foreground is scrolling seamlessly though.
Here's everything that you'll need to reproduce the problem (I won't post the entire code files since it'll be too long, but I'm sure people can gather the remaining boiler plate form of the files):
// index.html
<body>
<canvas id="background-canvas"></canvas>
<canvas id="foreground-canvas" height="302" width="802"></canvas>
<div class="overlay-wrapper"></div>
<canvas id="game-canvas" tabindex="1"></canvas>
<p>Press tab to play</p>
</body>
// index.js
const Game = require('./game');
document.addEventListener('DOMContentLoaded', function () {
// Getting main game canvas
const gameCanvas = document.getElementById('game-canvas');
const gameCanvasCtx = gameCanvas.getContext('2d');
// Parallax scrolling effect
// Getting background canvas
const backgroundCanvas = document.getElementById('background-canvas');
const backgroundCanvasCtx = backgroundCanvas.getContext('2d');
// Getting foreground canvas
const foregroundCanvas = document.getElementById('foreground-canvas');
const foregroundCanvasCtx = foregroundCanvas.getContext('2d');
const game = new Game(
gameCanvasCtx,
gameCanvas,
backgroundCanvasCtx,
backgroundCanvas,
foregroundCanvasCtx,
foregroundCanvas
);
game.start();
});
// game.js
class Game {
// Constructor for game
constructor(gameCtx, gameCanvas, backgroundCtx, backgroundCanvas, foregroundCtx, foregroundCanvas) {
// Setting context and canvas
this.gameCtx = gameCtx;
this.gameCanvas = gameCanvas;
this.backgroundCtx = backgroundCtx;
this.backgroundCanvas = backgroundCanvas;
this.foregroundCtx = foregroundCtx;
this.foregroundCanvas = foregroundCanvas;
// Setting game state
this.gameOver = false;
this.paused = false;
// Binding class methods
this.draw = this.draw.bind(this);
// Creating background and foreground
this.createBackground(this.backgroundCtx, this.backgroundCanvas, this.foregroundCtx, this.foregroundCanvas);
}
// Creating the background and foreground
createBackground(backgroundCtx, backgroundCanvas, foregroundCtx, foregroundCanvas) {
// Background
const backgroundImage = new Image();
backgroundImage.src = '../dist/assets/images/background.png';
backgroundImage.alt = 'Background';
this.background = new Background(backgroundCtx, backgroundCanvas, backgroundImage, 1);
// Foreground
const foregroundImage = new Image();
foregroundImage.src = '../dist/assets/images/foreground.png';
this.foreground = new Background(foregroundCtx, foregroundCanvas, foregroundImage, 10);
}
// Drawing the game
draw() {
if (!this.gameOver) {
requestAnimationFrame(this.draw);
// Drawing background
this.background.draw();
this.foreground.draw();
}
}
// temp start function for game
start() {
this.draw();
}
}
// background.js
class Background {
// Constructor for background
constructor(ctx, canvas, image, speed) {
this.ctx = ctx;
this.canvas = canvas;
this.image = image;
this.speed = speed;
this.posX = 0;
}
// Drawing the background
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Preventing browser(s) from smoothing out/blurring lines
this.ctx.mozImageSmoothingEnabled = false;
this.ctx.webkitImageSmoothingEnabled = false;
this.ctx.msImageSmoothingEnabled = false;
this.ctx.imageSmoothingEnabled = false;
this.ctx.drawImage(this.image, this.posX, 0, this.image.width, this.image.height, 0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(this.image, this.posX - this.image.width, 0, this.image.width, this.image.height, 0, 0, this.canvas.width, this.canvas.height);
if (this.image.width > this.canvas.width) {
this.ctx.drawImage(this.image, this.x - this.image.width * 2, 0);
}
if (this.posX >= this.image.width) {
this.posX = 0;
}
this.scrollImage();
}
// Scrolls an image
scrollImage() {
this.posX += this.speed;
}
}
And in my css file, this is how I styled my canvases:
body {
width: 100vw;
height: 100vh;
overflow: hidden;
}
/* For rendering sprites without blurring */
canvas {
image-rendering: pixelated;
}
/* Canvas styling */
#game-canvas {
position: absolute;
top: 0;
left: 0;
tabindex: 1;
width: 100%;
height: 100%;
z-index: 3;
}
#background-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
#foreground-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.overlay-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.25);
z-index: 2;
}
Related
I would like to clip an animated canvas as background to a <h1 />. The requirements are:
Use an actual <h1 /> tag instead of rendered heading in canvas.
Use of images to be rendered with ctx.drawImage().
h1 {
color: transparant;
background-image: <some-canvas>;
background-clip: text;
}
There are several approaches I've tried so far with varying success:
Creating a regular canvas and setting it as background of the <h1 />-tag using -webkit-canvas and -moz-element. This approach ticked all of my requirements but unfortunatly -webkit-canvas was deprecated along with document.getCSSCanvasContext("2d") in Chromium. Safari is the only working browser.
Using the CSS Paint API (Houdini). Using a requestAnimationFrame() to update a css var I can keep ticking the animation and do the animation I would like to implement. However, in Chromium, passing in images has to be done using a workaround (instead of creating a property of type image, images have to be passed in using background-image, making me unable to use the background-image to tell CSS Paint to use my worklet as background. The spec is not entirely implemented.
Creating an inline svg as background-image: url("data:image/svg+xml;utf8,<svg><foreignObject><canvas id='..'/></foreignObject></svg>"); and trying to update that canvas using requestAnimationFrame. Does not work at all.
Are there any other methods I could try?
If it doesn't have to be a background-image, you could put the canvas element inside the h1 element, match the canvas width and height to that of the h1 element, give it a lower z-index than the text in the h1 element so that it appears to be behind the text, acting like a background.
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
/*
Set the dimensions of the canvas to
match the parent h1 element
----------------------------------------------------------*/
setCanvasSize();
// (and for demonstrative purposes, scale on window resize)
window.addEventListener('resize', setCanvasSize, false);
function setCanvasSize () {
var _w = canvas.parentNode.clientWidth;
var _h = canvas.parentNode.clientHeight;
canvas.width = _w;
canvas.height = _h;
canvas.style.width = "'" + _w + "px'";
canvas.style.height = "'" + _h + "px'";
}
/*--------------------------------------------------------*/
/*--------------------------------------------------------
All this code below is just a ball animation borrowed from MDN
to illustrate what I'm suggesting.
Source: MDN
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Advanced_animations
*/
var raf;
var running = false;
var ball = {
x: 100,
y: 100,
vx: 5,
vy: 1,
radius: 50,
color: 'blue',
draw: function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
};
function clear() {
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.fillRect(0,0,canvas.width,canvas.height);
}
function draw() {
clear();
ball.draw();
ball.x += ball.vx;
ball.y += ball.vy;
if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
ball.vy = -ball.vy;
}
if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
ball.vx = -ball.vx;
}
raf = window.requestAnimationFrame(draw);
}
if (!running) {
raf = window.requestAnimationFrame(draw);
running = true;
}
ball.draw();
/*--------------------------------------------------------*/
h1 {
/* Important for positioning the span element */
position: relative;
/* Cosmetic/demonstrative */
border: 2px solid red;
height: 100%;
text-align: center;
color: red;
font-size: 32px;
font-family: Roboto,sans-serif;
margin: 0 auto;
}
h1 span {
/* Vertically and horizontally center the text */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
/*
Text will appear above canvas as long as its
z-index > canvas' z-index
*/
z-index: 2;
}
h1 canvas {
/* this will make the canvas appear behind the text */
position: relative;
z-index: 1;
}
<h1>
<span>Lorem Ipsum etc.</span>
<canvas id="canvas"></canvas>
</h1>
Just a thought anyway, maybe you can try it out and expand on the idea to suit your needs. I'm not 100% sure what you're working on so apologies if it's not helpful.
I want to create a media control bar for a video player with the controls blurring the background. Currently I can get the glassy look but I can't get blurring to work.
I tried following this guide: https://medium.com/#AmJustSam/how-to-do-css-only-frosted-glass-effect-e2666bafab91 but it seems like it only works on static images.
Here's an example of what I want:
I also saw the webkit backdrop-filter which looks perfect but it's only stable on safari so I can't use that. Any advice for frosted glass on dynamic videos?
I was able to accomplish this by copying the bottom part of the video to a canvas every frame and blurring the canvas via CSS. It seems to work well in Chrome, but flashes sometimes in Firefox. Doing the blur programmatically in the canvas with something like Superfast Blur might be more performant, but that's an experiment for another day. Tomorrow, probably.
function initControls (player, blurRadius, controlHeight, videoWidth, videoHeight) {
// crop player to video size
let video = player.querySelector('video');
videoWidth = videoWidth || video.clientWidth;
videoHeight = videoHeight || video.clientHeight;
player.style.width = videoWidth + 'px';
player.style.height = videoHeight + 'px';
// crop control bar to video size
let controlBar = player.querySelector('.control-bar');
controlBar.style.width = videoWidth + 'px';
controlBar.style.height = controlHeight + 'px';
// canvas needs to be slightly taller than what gets displayed
// to blur cleanly
let canvas = player.querySelector('canvas');
canvas.width = videoWidth;
canvas.height = 2 * blurRadius + controlHeight;
canvas.style.filter = `blur(${blurRadius}px)`;
canvas.style.top = -2 * blurRadius + 'px';
// copy video to canvas
let ctx = canvas.getContext('2d');
let videoCropY = videoHeight - canvas.height;
function updateCanvas () {
ctx.drawImage(
video,
0, videoCropY, canvas.width, canvas.height,
0, 0, canvas.width, canvas.height
);
}
// update the canvas only when necessary
let hovering = false;
function renderLoop () {
updateCanvas();
if (hovering && !video.paused) {
window.requestAnimationFrame(renderLoop);
}
}
// no point in rendering to a canvas you can't see
player.addEventListener('mouseenter', () => {
hovering = true;
renderLoop();
});
player.addEventListener('mouseleave', () => { hovering = false; });
video.addEventListener('play', renderLoop);
}
document.addEventListener('DOMContentLoaded', event => {
// do the magic
initControls(document.querySelector('.player'), 4, 50, 320, 240);
// basic play button functionality
document.querySelector('.play-button').addEventListener('click', event => {
let v = event.target.closest('.player').querySelector('video');
if (v.ended) v.currentTime = 0;
if (v.paused) {
v.play();
} else {
v.pause();
}
});
});
/* styling required for blurred control background */
.player {
position: relative;
}
.player > video {
margin: 0;
display: block;
}
.control-bar > canvas {
margin: 0;
display: block;
left: 0;
position: absolute;
}
.control-bar {
margin: 0;
overflow: hidden;
position: absolute;
bottom: 0;
/* height of control bar is specified in javascript, sorry about that */
}
/* simple control-hiding mechanism; other methods also work */
/* javascript relies on mouseover and mouseout to decide whether to update canvas */
.player > .control-bar {
display: none;
}
.player:hover > .control-bar {
display: block;
}
/* styling actual controls; adjust to taste */
.play-button {
height: 30px;
line-height: 30px;
width: 100px;
color: white;
border: 1px solid white;
position: absolute;
top: 10px;
left: 50%;
margin-left: -50px;
text-align: center;
}
<div class="player">
<video src="https://archive.org/download/MedicalA1950/MedicalA1950_512kb.mp4"></video>
<div class="control-bar">
<canvas></canvas>
<div class="play-button">PLAY/PAUSE</div>
</div>
</div>
I have a scratch card simulator.
The user should be able to click and drag in order to reveal the text underneath.
I have 2 bugs with this implementaion:
a) Sometimes the scratch card clears itself as soon as the cursor enters the canvas from left or right. It should only clear itself when most of the card has been scratched off. Currently, it only works if the user moves their cursor in from the top border, going downwards.
b) Sometimes the scratch card will not work at all, or the scratching will be offset from the cursor, but only when the browser window is smaller than the document size (e.g., browser window is 300px wide but the body has a min-width of 900px or something and the user had to scroll the card into view).
(function () {
"use strict";
var container = document.getElementById('cbox-canvas'),
arrow = document.getElementById('cbox-arrow'),
textOne = document.getElementById('cbox-text-1'),
textTwo = document.getElementById('cbox-text-2'),
boxOne = document.getElementById('cbox-box-1'),
boxTwo = document.getElementById('cbox-box-2'),
cnv = container.getElementsByTagName('canvas'),
imageCover;
function createCanvas(parent, width, height) {
var canvas = {};
canvas.node = document.createElement('canvas');
canvas.context = canvas.node.getContext('2d');
canvas.node.width = width || 100;
canvas.node.height = height || 100;
parent.appendChild(canvas.node);
return canvas;
}
function init(container, width, height, fillColor) {
var canvas = createCanvas(container, width, height),
ctx = canvas.context;
// define a custom fillCircle method
ctx.fillCircle = function (x, y, radius, fillColor) {
//this.fillStyle = fillColor;
this.shadowBlur = 15;
this.shadowOffsetX = 0;
this.shadowOffsetY = 0;
this.shadowColor = fillColor;
this.beginPath();
this.moveTo(x, y);
this.arc(x, y, radius, 0, Math.PI * 2, false);
this.fill();
this.stroke();
};
ctx.clearTo = function (fillColor) {
var imageObj = new Image();
imageObj.onload = function () {
ctx.drawImage(imageObj, 0, 0);
};
imageObj.src = fillColor;
};
ctx.clearTo(fillColor || "#ddd");
// bind mouse events
canvas.node.onmousemove = function (e) {
var canvasRect = container.getBoundingClientRect(),
x = e.pageX - canvasRect.left,
y = e.pageY - canvasRect.top,
radius = 30,
calc = 0;
fillColor = '#ff0000';
ctx.globalCompositeOperation = 'destination-out';
ctx.fillCircle(x, y, radius, fillColor);
calc += x;
if (calc > 330 || calc < 6) {
container.removeChild(cnv[0]);
arrow.className += " slide-it";
textOne.className += " reveal-it";
textTwo.className += " fade-in";
boxOne.className += " fade-in-two";
boxTwo.className += " fade-in-one";
}
};
container.onmousemove = function (e) {
var canvasRect = container.getBoundingClientRect(),
mouseX = e.pageX || e.clientX,
mouseY = e.pageY || e.clientY,
relMouseX = mouseX - canvasRect.left,
relMouseY = mouseY - canvasRect.top,
leftLimit = 37,
topLimit = 37,
rightLimit = 25,
bottomLimit = 44,
x = e.pageX - canvasRect.left,
y = e.pageY - canvasRect.top,
radius = 25;
fillColor = '#ff0000';
if (relMouseX < leftLimit) {
relMouseX = leftLimit;
}
if (relMouseY < topLimit) {
relMouseY = topLimit;
}
if (relMouseX > width - rightLimit) {
relMouseX = width - rightLimit;
}
if (relMouseY > height - bottomLimit) {
relMouseY = height - bottomLimit;
}
if (!canvas.isDrawing) {
return;
}
ctx.globalCompositeOperation = 'destination-out';
ctx.fillCircle(x, y, radius, fillColor);
};
}
imageCover = "images/scratch.png";
init(container, 369, 371, imageCover);
}());
https://jsfiddle.net/p05kg0vq/
Problems
There are several issues here:
You are loading the clear image for each time which happens asynchronously and it may not appear in time.
The same method, clearTo(), also takes a fill-style color, but tries to load it as image
You are listening to mouse move events two places which is not needed
For improvement: You're listening to mouse move events on the canvas. This is not wrong, but it will be more fluid using the window object
You're using pageX/Y to calculate mouse position. These are relative to page not client. getBoundingClientRect() is relative to client.
Not sure how you intend to calculate coverage
There are additional room for refactoring.
Solutions
Load image once globally and use the object as argument instead of the URL.
Differentiate between an image and color string. To do this check if argument is a string, if so, set fillStyle and use fillRect() to clear. If not use drawImage().
Remove the event from the container. It's not needed and will conflict with the second listener.
Use window.onmousemove instead (not required, but a better option in this case as it will move the cursor completely outside canvas - optionally use a parent node that is wider - it's up to you...).
Calculate using clientX and clientY instead and always.
Extract the ImageData and count pixels with no alpha data (=0). Then divide this count on total number of pixels to get percentage of coverage. This is fast (I'll show how below).
Left to OP to improve :)
So, lets modify the structure a bit. This is not optimal, but meant to get you started. Load the image once and globally (or within the parent scope so the object is accessible).
// preload image once
var imageCover = "//i.imgur.com/b4m1M1n.png"; // needed cors for demo
var imageObj = new Image();
imageObj.onload = go;
imageObj.crossOrigin = ""; // for demo, for getImageData to work
imageObj.src = imageCover;
function go() {
/* ... inner code not shown ... */
init(container, 369, 371, imageObj);
};
Then rewrite clearTo() to accept both image and fill style. Notice this may break browser optimizations as there are two different types involved, but in this case it likely doesn't matter:
ctx.clearTo = function(fillColor) {
if (typeof fillColor === "string") { // is a string?
ctx.fillStyle = fillColor; // set as fill style
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
else { // assumes an image if not string
ctx.drawImage(fillColor, 0, 0);
}
};
ctx.clearTo(fillColor || "#ddd");
Then move onmousemove to window object and use clientX/clientY:
window.onmousemove = function(e) {
var canvasRect = container.getBoundingClientRect(),
x = e.clientX - canvasRect.left, // use clientX/Y (pageXY is unofficial)
y = e.clientY - canvasRect.top,
/* ... */
Within the same code block provide a function calculate real-time coverage of canvas:
// calc converage and clean if < 20%
if (calcCover(ctx) < 0.2) {
// end, reveal, etc.
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
console.log("DONE");
};
The function used here does:
function calcCover(ctx) {
var w = ctx.canvas.width, // just to cache width/height
h = ctx.canvas.height,
// convert Uint8ClampedArray to Uint32Array, no memory loss
// but faster
data32 = new Uint32Array(ctx.getImageData(0,0,w,h).data.buffer),
count = w * h; // total number of pixels
// iterate, check for alpha-channel (0xAABBGGRR, little-endian format)
// for the Uint32Array data.
for(var i = 0; i < data32.length; i++) if (!(data32[i] & 0xff000000)) count--;
// convert to a percentage (or rather normalize)
return count / (w*h);
};
Now we are good to go:
Modified fiddle
Looking at your code I could only suggest a full rewrite
;(function () {
"use strict";
// Generic functions and constants
const PI2 = Math.PI * 2;
function applyStyle (ctx, style) { Object.keys(style).forEach(key => ctx[key] = style[key] ) }
function ease (val, power) { return val < 0 ? 0 : val > 1 ? 1 : Math.pow(val, power) }
// General settings
const settings = {
width : 369,
height : 371,
coveragedMin : 0.2, // when to uncover all out of 1
coverColor : "#ddd", // colour to show on canvas while main image is loading. (not needed but to keep with you code)
mouseEvents : "mouseup,mousedown,mousemove".split(","), // list of mouse events to listen to
coverImage : loadImage("https://image.ibb.co/f8TNS5/scratch.png"), // the scratch image
container : document.getElementById('cbox-canvas'), // the container
drawStyle : { // the draw style of the revealing mouse moves. Note that this adds radius to the context but should not matter
radius : 20,
shadowBlur : 15,
shadowOffsetX : 0,
shadowOffsetY : 0,
shadowColor : "black",
fillStyle : "black",
globalCompositeOperation : "destination-out",
},
startAnim (){ // specific to this scratch reveal animations
document.getElementById("cbox-arrow").className = "cbox-arrow slide-it";
document.getElementById("cbox-text-1").className = "cbox-text-1 reveal-it";
document.getElementById("cbox-box-1").className = "cbox-box-1 fade-in-two";
document.getElementById("cbox-box-2").className = "cbox-box-2 fade-in-one";
document.getElementById("cbox-text-2").className = "cbox-text-2 fade-in";
},
coverageArray : (() => {const buf = new Uint8Array(64); buf.fill(1); return buf }) (), // array to is used to determine coverage
}
var update = true; // when true update canvas render
const mouse = { x : 0, y : 0, button : false}; // Mouse state
function mouseEvent (e) { // handles all mouse events
const canvasRect = settings.container.getBoundingClientRect();
mouse.x = e.pageX - canvasRect.left - scrollX;
mouse.y = e.pageY - canvasRect.top - scrollY;
if (e.type === "mousedown") { mouse.button = true }
else if (e.type === "mouseup") { mouse.button = false }
update = true; // flags that there needs to be a re render
}
function fillCircle (ctx, x, y, style) { // Draws a circle on context ctx, at location x,y using style
applyStyle(ctx, style);
ctx.beginPath();
ctx.arc(x, y, style.radius, 0, Math.PI * 2);
ctx.fill();
}
function setCoverage (array,x,y){ // Clears the coverage array, coordinates x,y are normalised 0-1
var i = array.length - 1; // and returns coverage as a value 0 no coverage to 1 full cover
const size = Math.sqrt(array.length) | 0;
array[(x * size) | 0 + ((y * size) | 0) * size] = 0;
var count = 0;
while(i-- > 0){ count += array[i] };
return count / array.length;
}
function loadImage (url) { // Loads an image and sets a property indicating if its has been rendered
const image = new Image();
image.src = url;
image.rendered = false;
return image;
}
function createCanvas (width, height) { // Creates a canvas of size width and height, set property ctx to the 2D context
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.ctx = canvas.getContext("2d");
return canvas;
}
(function (settings) { // Start the app
const canvas = createCanvas(settings.width, settings.height);
settings.container.appendChild(canvas);
const ctx = canvas.ctx;
settings.mouseEvents.forEach(eventName => addEventListener(eventName, mouseEvent)); // start the mouse
var reveal = false; // when true reveal the prize (yep suckers) ???
var fade = 1; // fades out the canvas
ctx.fillStyle = settings.coverColor; // cover while waiting for image to load
ctx.fillRect(0, 0, canvas.width, canvas.height); // image will not yet have loaded so cover image
(function mainLoop () { // main animation loop will play unt ill canvas faded out
if (settings.coverImage.complete && !settings.coverImage.rendered) { // wait till image has loaded
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(settings.coverImage, 0, 0, settings.width, settings.height);
settings.coverImage.rendered = true;
const swipeEl = document.getElementById("swipe-area");
swipeEl.className = "swipe-area loaded";
swipeEl.title = "Use your mouse to reveal your PRIZE :P";
}
if (update) { // only if needed render canvas
if (settings.coverImage.rendered) {
mouse.button && fillCircle(ctx, mouse.x, mouse.y, settings.drawStyle);
setCoverage(settings.coverageArray, mouse.x / settings.width, mouse.y / settings.height) < settings.coveragedMin && (reveal = true);
update = false;
}
if (reveal) {
fade -= 0.05;
canvas.style.opacity = ease(fade,2);
update = true; // need continuous update for animation
}
}
if (reveal && fade <= 0) { // scratching all done remove canvas, mouse events and start any animations. Do not call requestAnimationFrame as all done.
const swipeEl = document.getElementById("swipe-area");
swipeEl.style.cursor = "pointer";
swipeEl.title = "Click here to collect your $$$$";
settings.container.removeChild(canvas);
settings.mouseEvents.forEach(eventName => removeEventListener(eventName, mouseEvent));
settings.startAnim();
// All done. All objects should now have no references (important to remove mouse and requestAnimation frame) and any other functions
// that can hold a closure
} else {
requestAnimationFrame(mainLoop);
}
} () );
} (settings) );
} () );
/*SWIPE*/
.swipe-area {
position: absolute;
width: 369px;
height: 371px;
left: 10px;
top: 10px;
z-index: 15;
background-size: 100%;
}
.preload {
cursor : wait;
background: url('https://image.ibb.co/f8TNS5/scratch.png') no-repeat;
background-size: 100%;
}
.loaded {
cursor : url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAbCAYAAABm409WAAABkklEQVRIibXWPWtUQRjF8d/GbDRFomghZFNorREMiIUgpEgRSFJFuyAWNoovTQIqdvZp8lXCBb+ApZ9AApb2lmFS3Jns3XVm9ya7+8DDwMA9/3vOM/eFfC0W9qdSodHTFz46FkIQfv4S8GdaoCQY6AMaoIkgoapqodQJkgCTQgbE/4ecTOwkC+hDTi76qpAiYBAysHc6M0gIM3MxOPjLQkYCcpBLA4aPagHyNwGqasYurgRo6SIH6UzVRVX1OwKWcAMLmBsFOGvrYgiwhvtYxU3MTxRVxsVvbGEDD3Ab10qAzriomoCj4wsX+9jDM/RiXNmax+NxTsRvRwNwgI94GSNbUhj+Qszx7SgnCdCYwzd8xms8wS2FgSfAOr4348jF1BD/isMY1XrUyAK60d4aXuCH/NFMfYgvcf3UJqK56KKnHtge3uDfkPAHvI/rO7yKNzR2yKkWsYKHeI5NbGMHu3HdiXubeIpHUbz1b0+K62688F6he7iD5TZ3Plwd9dHt4nqhu+oHq/U7KQfpqOeT66LwOUobOkEQCJp1AAAAAElFTkSuQmCC') 9 20, pointer;
background: url('https://image.ibb.co/j4j7uk/sc_bg.png') no-repeat;
background-size: 100%;
}
.anim-container {
position: absolute;
width: 360px;
height: 366px;
right: 5px;
top: 5px;
z-index: -1;
background-size: 100%;
overflow: hidden;
}
.cbox-arrow {
position: absolute;
left: 56px;
top: 0;
z-index: -1;
width: 260px;
height: 264px;
background: url('https://image.ibb.co/fXKwn5/arrow.png') no-repeat;
background-size: 100%;
-webkit-animation: 10s slide;
}
.cbox-text-1 {
position: absolute;
left: 72px;
top: 100px;
z-index: -1;
width: 230px;
height: 65px;
background: url('https://image.ibb.co/d6YYZk/test1.png') no-repeat;
background-size: 100%;
opacity: 1;
}
.cbox-text-2 {
position: absolute;
left: 72px;
top: 100px;
z-index: -1;
width: 230px;
height: 65px;
background: url('https://image.ibb.co/bCaQfQ/test2.png') no-repeat;
background-size: 100%;
opacity: 0;
}
.cbox-box-1 {
position: absolute;
left: 55px;
top: 167px;
z-index: -1;
width: 257px;
height: 65px;
background: url('https://image.ibb.co/fG7hS5/box1.png') no-repeat;
background-size: 100%;
opacity: 0;
}
.cbox-box-2 {
position: absolute;
left: 135px;
top: 124px;
z-index: -1;
width: 99px;
height: 127px;
background: url('https://image.ibb.co/dOSSuk/box2.png') no-repeat;
background-size: 100%;
opacity: 0;
}
.hidden { display: none;}
/* unknowns */
.newslisting, #sidebar1Bottom { background: #ffffff !important; }
.tmx header { height: 269px;}
/*Animations, you can add agent prescripts, though we should never have to do that */
.fade-in { animation: 1.5s 2.5s fade;}
.fade-in-one { animation: 2.5s 5s fade;}
.fade-in-two { animation: 2.5s 7.5s fade-alt forwards;}
.fade-in-three { animation: 5s 15s fade;}
.reveal-it { animation: 2.5s reveal forwards;}
.slide-it { animation: 5s slide-in forwards;}
#keyframes fade-alt {0% { opacity: 0; } 10% {opacity : 1;} 100% { opacity: 1;} }
#keyframes fade { 0% { opacity: 0; } 10% {opacity: 1;} 90% {opacity: 1;} 100% {opacity: 0;} }
#keyframes reveal {0% { opacity: 1;} 80% { opacity: 1;} 100% { opacity: 0;} }
#keyframes slide-in {0% { top: 5px; } 80% { top: 5px; } 100% { top: -150px;} }
<div class="swipe-area preload" id="swipe-area" title="Just a moment as we asses your gullibility!">
<div id="cbox-canvas">
<div class="anim-container">
<div id="cbox-arrow" class="hidden"></div>
<div id="cbox-text-1" class="hidden"></div>
<div id="cbox-box-1" class="hidden"></div>
<div id="cbox-box-2" class="hidden"></div>
<div id="cbox-text-2" class="hidden"></div>
</div>
</div>
</div>
Why does html5 canvas become very slow when I draw 2000 images and more ?
How can I optimise it?
Here's a demo that I've made, disable the "safe mode" by left clicking on the canvas and start moving your mouse until you get ~2000 images drawn
var img = new Image()
img.src = "http://i.imgur.com/oVOibrL.png";
img.onload = Draw;
var canvas = $("canvas")[0];
var ctx = canvas.getContext("2d")
var cnv = $("canvas");
var draw = [];
$("canvas").mousemove(add)
function add(event) {
draw.push({x: event.clientX, y: event.clientY})
}
canvas.width = cnv.width();
canvas.height = cnv.height();
var safe = true;
cnv.contextmenu(function(e) { e.preventDefault() })
cnv.mousedown(function(event) {
if(event.which == 1) safe = !safe;
if(event.which == 3) draw = []
});
function Draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
requestAnimationFrame(Draw);
for(var i of draw) {
ctx.drawImage(img, i.x, i.y)
}
if(safe && draw.length > 300) draw = []
ctx.fillText("Images count: "+ draw.length,10, 50);
ctx.fillText("Left click to toggle the 300 images limit",10, 70);
ctx.fillText("Right click to clear canvas",10, 90);
}
Draw();
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
position: absolute;
}
canvas {
position: absolute;
width: 100%;
height: 100%;
z-index: 99999999999;
cursor: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas></canvas>
Codepen: http://codepen.io/anon/pen/PpeNme
The simple way with the actual code :
Don't redraw all your images at this rate.
Since in your example, the images are static, you actually don't need to redraw everything every frame : Just draw the latest ones.
Also, if you've got other drawings occurring (e.g your texts), you may want to use an offscreen canvas for only the images, that you'll redraw on the onscreen canvas + other drawings.
var img = new Image()
img.src = "http://i.imgur.com/oVOibrL.png";
img.onload = Draw;
var canvas = $("canvas")[0];
var ctx = canvas.getContext("2d")
var cnv = $("canvas");
var draw = [];
$("canvas").mousemove(add)
function add(event) {
draw.push({
x: event.clientX,
y: event.clientY
})
}
canvas.width = cnv.width();
canvas.height = cnv.height();
// create an offscreen clone of our canvas for the images
var imgCan = canvas.cloneNode();
var imgCtx = imgCan.getContext('2d');
var drawn = 0; // a counter to know how much image we've to draw
var safe = true;
cnv.contextmenu(function(e) {
e.preventDefault()
})
cnv.mousedown(function(event) {
if (event.which == 1) safe = !safe;
if (event.which == 3) draw = []
});
function Draw() {
// clear the visible canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
requestAnimationFrame(Draw);
if (draw.length) { // onmy if we've got some objects to draw
for (drawn; drawn < draw.length; drawn++) { // only the latest ones
let i = draw[drawn];
// draw it on the offscreen canvas
imgCtx.drawImage(img, i.x, i.y)
}
}
// should not be needed anymore but...
if (safe && draw.length > 300) {
draw = [];
drawn = 0; // reset our counter
// clear the offscren canvas
imgCtx.clearRect(0, 0, canvas.width, canvas.height);
}
// draw the offscreen canvas on the visible one
ctx.drawImage(imgCan, 0, 0);
// do the other drawings
ctx.fillText("Images count: " + draw.length, 10, 50);
ctx.fillText("Left click to toggle the 300 images limit", 10, 70);
ctx.fillText("Right click to clear canvas", 10, 90);
}
Draw();
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
position: absolute;
}
canvas {
position: absolute;
width: 100%;
height: 100%;
z-index: 99999999999;
cursor: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas></canvas>
Now, if you need these images to be dynamic (i.e move at every frame), you may consider using imageDatas.
You can see this original post by user #Loktar, which explains how to parse your drawn image imageData, and then redraw it pixel per pixel on the visible canvas' imageData. You can also see this follow-up Q/A, which provides a color implementation of Loktar's idea.
On small images, this actually improves drastically the performances, but it has the huge inconvenient to not support alpha channel multiplication. You will only have fully transparent and fully opaque pixels. An other cons is that it may be harder to implement, but this is just your problem ;-)
I’m trying to ‘merge’ some javascript code that changes pictures with a canvas that has also animation to it, without them cancelling eachother out. In this canvas there are some clouds that move and I want to throw kittens into it.
I understand this this might be a basic HTML5 canvas question but I’m terrible using canvas though. This is the first time I’m really working with it. Whenever I try to implement the code that applies to the kittens, the canvas screen just goes white and nothing shows.
I want to stick with most of the code that I have. I really want to keep the canvas the way it is and change nothing there but add those kittens in there too. Can someone puzzle out for me how to appropiately do this?
I'm guessing I would need to tweak the animation function somehow?
function animate(){
ctx.save();
ctx.clearRect(0, 0, cW, cH);
background.render();
foreground.render();
ctx.restore();
}
var animateInterval = setInterval(animate, 30);
}
The code is all in a FIDDLE, because it’s too much to show it on here.
EDIT: To be clear, I'm asking for the pictures of the kittens to be laid over the canvas, but I want the clouds in the canvas to overlay the kittens.
What you need to know is that the canvas element is transparent by default. So notice the minor change I made here :
body{
background:#667;
}
canvas {
width:50vw;
height:50vh;
margin-left: 20%;
}
#image {
border:#000 1px solid;
padding-left: 0;
padding-right: 0;
margin-left: auto;
margin-right: auto;
display: block;
width: 33%;
height: 100%;
position: relative;
top:140px;
z-index: -1;
}
#my_canvas{
border:#000 1px solid;
padding-left: 0;
padding-right: 0;
margin-left: auto;
margin-right: auto;
display: block;
width: 33%;
height: 100%;
}
<link href="css.css" rel="stylesheet" type="text/css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<img src="" id="image"/>
<script>
var bg = new Image();
bg.src = "http://proverum.ru/templates/oblaka/img/cloud.png";
var fg = new Image();
fg.src = "http://www.tourisme-fumel.com/img_interf/meteo/cloudy.png";
function initCanvas(){
var lastTick = 0;
var position = { x:0, y:0 };
var rain = document.getElementById('rain');
var ctx = document.getElementById('my_canvas').getContext('2d');
var canvas_container = document.getElementById('my_canvas2');
var cW = ctx.canvas.width, cH = ctx.canvas.height;
function Background(){
this.x = 0, this.y = 0, this.w = bg.width, this.h = bg.height;
this.render = function(){
ctx.drawImage(bg, this.x--, 0);
if(this.x <= -250){
this.x = 0;
}
}
}
var background = new Background();
var image1 = "http://www.catsofaustralia.com/images/three_kittens.jpg";
var image2 = "http://thecatpalace.com.au/wp-content/uploads/2015/05/kitten1.jpg";
var image3 = "http://www.keepingkittens.com/images/cute-little-kitten-minka-rose-the-real-cinderella-story-21652512.jpg";
$(function() {
$("#image").prop("src", image1);
setInterval(function() {
$("#image").prop("src", image2);
setTimeout(function() {
$("#image").prop("src", image2);
setTimeout(function() {
$("#image").prop("src", image3);
}, 50);
setTimeout(function() {
$("#image").prop("src", image1);
}, 500);
}, 10);
}, 5000);
});
function Foreground(){
this.x = 0, this.y = 0, this.w = fg.width, this.h = fg.height;
this.render = function(){
ctx.drawImage(fg, this.x--, 0);
if(this.x <= -499){
this.x = 0;
}
}
}
var foreground = new Foreground();
function animate(){
ctx.save();
ctx.clearRect(0, 0, cW, cH);
background.render();
foreground.render();
ctx.restore();
}
var animateInterval = setInterval(animate, 30);
}
window.addEventListener('load', function(event) {
initCanvas();
});
</script>
</head>
<body>
<canvas id="my_canvas" width="611" height="864"></canvas>
<h1 id="status"></h1>
</body>
</html>
I just removed the
background:#FFF;
from the canvas css and the canvas became transparent.
How you will line up the canvas over the kittens is up to you, I just quickly used position:relative and z-index to prove my point.