How to zoom canvas with animation - javascript

I am designing a paint bucket app. My code is working fine. Just need a little help. On zoom in and zoom out buttons, I am changing the height and width of canvas. How can I apply animation to it? This is my complete code. The zoom part is at the end. You can simply copy paste it inside a file. It will work.
<!DOCTYPE html>
<html>
<head>
<title>Painitng</title>
<style>
body {
width: 100%;
height: auto;
text-align: center;
}
.colorpick {
widh: 100%;
height: atuo;
}
.pick {
display: inline-block;
width: 30px;
height: 30px;
margin: 5px;
cursor: pointer;
}
canvas {
border: 2px solid silver;
}
</style>
</head>
<body>
<button id="zoomin">Zoom In</button>
<button id="zoomout">Zoom Out</button>
<button id = "undo-button" onclick="history('undo')">Undo</button>
<button id = "redo-button" onclick="history('redo')">Redo</button>
<div id="canvasDiv"></div>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script>
<script type="text/javascript">
var colorYellow = {
r: 255,
g: 207,
b: 51
};
var context;
var canvasWidth = 500;
var canvasHeight = 500;
var myColor = colorYellow;
var curColor = myColor;
var outlineImage = new Image();
var backgroundImage = new Image();
var drawingAreaX = 0;
var drawingAreaY = 0;
var drawingAreaWidth = 500;
var drawingAreaHeight = 500;
var colorLayerData;
var outlineLayerData;
var totalLoadResources = 2;
var curLoadResNum = 0;
var undoarr = new Array();
var redoarr = new Array();
function history(command){ // handles undo/redo button events.
var data;
if(command === "redo"){
data = historyManager.redo(); // get data for redo
}else
if(command === "undo"){
data = historyManager.undo(); // get data for undo
}
if(data !== undefined){ // if data has been found
setColorLayer(data); // set the data
}
}
// sets colour layer and creates copy into colorLayerData
function setColorLayer(data){
context.putImageData(data, 0, 0);
colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);
context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
}
// Clears the canvas.
function clearCanvas() {
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
}
// Draw the elements on the canvas
function redraw() {
uc = 0;
rc = 0;
var locX,
locY;
// Make sure required resources are loaded before redrawing
if (curLoadResNum < totalLoadResources) {
return; // To check if images are loaded successfully or not.
}
clearCanvas();
// Draw the current state of the color layer to the canvas
context.putImageData(colorLayerData, 0, 0);
historyManager.push(context.getImageData(0, 0, canvasWidth, canvasHeight));
redoarr = new Array();
// Draw the background
context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight);
// Draw the outline image on top of everything. We could move this to a separate
// canvas so we did not have to redraw this everyime.
context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight);
}
;
function matchOutlineColor(r, g, b, a) {
return (r + g + b < 100 && a === 255);
}
;
function matchStartColor(pixelPos, startR, startG, startB) {
var r = outlineLayerData.data[pixelPos],
g = outlineLayerData.data[pixelPos + 1],
b = outlineLayerData.data[pixelPos + 2],
a = outlineLayerData.data[pixelPos + 3];
// If current pixel of the outline image is black
if (matchOutlineColor(r, g, b, a)) {
return false;
}
r = colorLayerData.data[pixelPos];
g = colorLayerData.data[pixelPos + 1];
b = colorLayerData.data[pixelPos + 2];
// If the current pixel matches the clicked color
if (r === startR && g === startG && b === startB) {
return true;
}
// If current pixel matches the new color
if (r === curColor.r && g === curColor.g && b === curColor.b) {
return false;
}
return true;
}
;
function colorPixel(pixelPos, r, g, b, a) {
colorLayerData.data[pixelPos] = r;
colorLayerData.data[pixelPos + 1] = g;
colorLayerData.data[pixelPos + 2] = b;
colorLayerData.data[pixelPos + 3] = a !== undefined ? a : 255;
}
;
function floodFill(startX, startY, startR, startG, startB) {
var newPos,
x,
y,
pixelPos,
reachLeft,
reachRight,
drawingBoundLeft = drawingAreaX,
drawingBoundTop = drawingAreaY,
drawingBoundRight = drawingAreaX + drawingAreaWidth - 1,
drawingBoundBottom = drawingAreaY + drawingAreaHeight - 1,
pixelStack = [[startX, startY]];
while (pixelStack.length) {
newPos = pixelStack.pop();
x = newPos[0];
y = newPos[1];
// Get current pixel position
pixelPos = (y * canvasWidth + x) * 4;
// Go up as long as the color matches and are inside the canvas
while (y >= drawingBoundTop && matchStartColor(pixelPos, startR, startG, startB)) {
y -= 1;
pixelPos -= canvasWidth * 4;
}
pixelPos += canvasWidth * 4;
y += 1;
reachLeft = false;
reachRight = false;
// Go down as long as the color matches and in inside the canvas
while (y <= drawingBoundBottom && matchStartColor(pixelPos, startR, startG, startB)) {
y += 1;
colorPixel(pixelPos, curColor.r, curColor.g, curColor.b);
if (x > drawingBoundLeft) {
if (matchStartColor(pixelPos - 4, startR, startG, startB)) {
if (!reachLeft) {
// Add pixel to stack
pixelStack.push([x - 1, y]);
reachLeft = true;
}
} else if (reachLeft) {
reachLeft = false;
}
}
if (x < drawingBoundRight) {
if (matchStartColor(pixelPos + 4, startR, startG, startB)) {
if (!reachRight) {
// Add pixel to stack
pixelStack.push([x + 1, y]);
reachRight = true;
}
} else if (reachRight) {
reachRight = false;
}
}
pixelPos += canvasWidth * 4;
}
}
}
;
// Start painting with paint bucket tool starting from pixel specified by startX and startY
function paintAt(startX, startY) {
var pixelPos = (startY * canvasWidth + startX) * 4,
r = colorLayerData.data[pixelPos],
g = colorLayerData.data[pixelPos + 1],
b = colorLayerData.data[pixelPos + 2],
a = colorLayerData.data[pixelPos + 3];
if (r === curColor.r && g === curColor.g && b === curColor.b) {
// Return because trying to fill with the same color
return;
}
if (matchOutlineColor(r, g, b, a)) {
// Return because clicked outline
return;
}
floodFill(startX, startY, r, g, b);
redraw();
}
;
// Add mouse event listeners to the canvas
function createMouseEvents() {
$('#canvas').mousedown(function (e) {
// Mouse down location
var mouseX = e.pageX - this.offsetLeft,
mouseY = e.pageY - this.offsetTop;
// assuming that the mouseX and mouseY are the mouse coords.
if(this.style.width){ // make sure there is a width in the style
// (assumes if width is there then height will be too
var w = Number(this.style.width.replace("px","")); // warning this will not work if size is not in pixels
var h = Number(this.style.height.replace("px","")); // convert the height to a number
var pixelW = this.width; // get the canvas resolution
var pixelH = this.height;
mouseX = Math.floor((mouseX / w) * pixelW); // convert the mouse coords to pixel coords
mouseY = Math.floor((mouseY / h) * pixelH);
}
if ((mouseY > drawingAreaY && mouseY < drawingAreaY + drawingAreaHeight) && (mouseX <= drawingAreaX + drawingAreaWidth)) {
paintAt(mouseX, mouseY);
}
});
}
;
resourceLoaded = function () {
curLoadResNum += 1;
//if (curLoadResNum === totalLoadResources) {
createMouseEvents();
redraw();
//}
};
var historyManager = (function (){ // Anon for private (closure) scope
var uBuffer = []; // this is undo buff
var rBuffer = []; // this is redo buff
var currentState = undefined; // this holds the current history state
var undoElement = undefined;
var redoElement = undefined;
var manager = {
UI : { // UI interface just for disable and enabling redo undo buttons
assignUndoButton : function(element){
undoElement = element;
this.update();
},
assignRedoButton : function(element){
redoElement = element;
this.update();
},
update : function(){
if(redoElement !== undefined){
redoElement.disabled = (rBuffer.length === 0);
}
if(undoElement !== undefined){
undoElement.disabled = (uBuffer.length === 0);
}
}
},
reset : function(){
uBuffer.length = 0;
rBuffer.length = 0;
currentState = undefined;
this.UI.update();
},
push : function(data){
if(currentState !== undefined){
var same=true
for(i=0;i<data.data.length;i++){
if(data.data[i] !== currentState.data[i]){
same = false;break;
}
} if(same){
return;
}
}
if(currentState !== undefined){
uBuffer.push(currentState);
}
currentState = data;
rBuffer.length = 0;
this.UI.update();
},
undo : function(){
if(uBuffer.length > 0){
if(currentState !== undefined){
rBuffer.push(currentState);
}
currentState = uBuffer.pop();
}
this.UI.update();
return currentState; // return data or unfefined
},
redo : function(){
if(rBuffer.length > 0){
if(currentState !== undefined){
uBuffer.push(currentState);
}
currentState = rBuffer.pop();
}
this.UI.update();
return currentState;
},
}
return manager;
})();
function start() {
var canvas = document.createElement('canvas');
canvas.setAttribute('width', canvasWidth);
canvas.setAttribute('height', canvasHeight);
canvas.setAttribute('id', 'canvas');
document.getElementById('canvasDiv').appendChild(canvas);
if (typeof G_vmlCanvasManager !== "undefined") {
canvas = G_vmlCanvasManager.initElement(canvas);
}
context = canvas.getContext("2d");
backgroundImage.onload = resourceLoaded();
backgroundImage.src = "images/t1.png";
outlineImage.onload = function () {
context.drawImage(outlineImage, drawingAreaX, drawingAreaY, drawingAreaWidth, drawingAreaHeight);
try {
outlineLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
} catch (ex) {
window.alert("Application cannot be run locally. Please run on a server.");
return;
}
clearCanvas();
colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight);
resourceLoaded();
};
outlineImage.src = "images/d.png";
}
;
if(historyManager !== undefined){
// only for visual feedback and not required for the history manager to function.
historyManager.UI.assignUndoButton(document.querySelector("#undo-button"));
historyManager.UI.assignRedoButton(document.querySelector("#redo-button"));
}
getColor = function () {
};
</script>
<script type="text/javascript"> $(document).ready(function () {
start();
});</script>
<script language="javascript">
$('#zoomin').click(function () {
if ($("#canvas").width()==500){
$("#canvas").width(750);
$("#canvas").height(750);
var ctx = canvas.getContext("2d");
ctx.drawImage(backgroundImage, 0, 0, 749, 749);
ctx.drawImage(outlineImage, 0, 0, 749, 749);
redraw();
} else if ($("#canvas").width()==750){
$("#canvas").width(1000);
$("#canvas").height(1000);
var ctx = canvas.getContext("2d");
ctx.drawImage(backgroundImage, 0, 0, 999, 999);
ctx.drawImage(outlineImage, 0, 0, 999, 999);
redraw();
}
});
$('#zoomout').click(function () {
if ($("#canvas").width() == 1000) {
$("#canvas").width(750);
$("#canvas").height(750);
var ctx = canvas.getContext("2d");
ctx.drawImage(backgroundImage, 0, 0, 749, 749);
ctx.drawImage(outlineImage, 0, 0, 749, 749);
redraw();
} else if ($("#canvas").width() == 750) {
$("#canvas").width(500);
$("#canvas").height(500);
var ctx = canvas.getContext("2d");
ctx.drawImage(backgroundImage, 0, 0, 499, 499);
ctx.drawImage(outlineImage, 0, 0, 499, 499);
redraw();
}
});
</script>
<div class="colorpick">
<div class="pick" style="background-color:rgb(150, 0, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 0, 152);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 151, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 5);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 255, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 255, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 150, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 150);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 255, 150);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(150, 0, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 150, 255);" onclick="hello(this.style.backgroundColor);"></div>
</div>
<script>
function hello(e) {
var rgb = e.replace(/^(rgb|rgba)\(/, '').replace(/\)$/, '').replace(/\s/g, '').split(',');
myColor.r = parseInt(rgb[0]);
myColor.g = parseInt(rgb[1]);
myColor.b = parseInt(rgb[2]);
curColor = myColor;
}
</script>
</body>
</html>

You could continuously redraw the canvas content as you're resizing, but there is a more efficient way to do it.
Alternatively...
Continuously redrawing the canvas content is resource intensive. A more efficient way to handle the zoom-animation is to use CSS to do it. When the animation completes, only then resize the canvas and redraw the content at its final zoomed scale. It's true that while animating the transition the canvas content may temporarily appear stretched or squished, but it all happens so quickly that it will not be noticeable.
Save the unzoomed canvas to another in-memory canvas: var memCanvas = canvas.cloneNode() & drawImage the visible canvas to the memCanvas.
Use CSS animations to resize the canvas element to a zoom size. Here's an example on a previous SO Q&A. The animation completes so quickly it probably won't be noticeable if the content resizes disproportionally.
When the animation is complete (the canvas is at its full-zoomed size) you can scale the saved drawing to the new size with drawImage( savedCanvas, 0,0, originalWidth*zoomFactor, originalHeight*zoomFactor) and reset the CSS width and height to the original size.

Related

slow script in the first 3 executions

i made a simple script for color a pixel and all the near pixel with the same color
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="http://code.jquery.com/jquery-latest.js"></script>
<title>
Click foto
</title>
<style type="text/css">
/*<![CDATA[*/
html, body{
height: 100%;
}
/*]]>*/
</style>
</head>
<body>
<div id="canvasDiv">
</div>
</body>
<script>
var canvasDiv = document.getElementById('canvasDiv');
canvas = document.createElement('canvas');
canvas.setAttribute('width', 500);
canvas.setAttribute('height', 500);
canvas.setAttribute('id', 'canvas');
$(canvasDiv).prepend(canvas);
if(typeof G_vmlCanvasManager != 'undefined') {
canvas = G_vmlCanvasManager.initElement(canvas);
}
var context = canvas.getContext('2d');
var imageObj = new Image();
imageObj.onload = function() {
$(canvas).attr({width : this.width, height: this.height});
context.drawImage(imageObj,0,0);
};
imageObj.src = 'cartina_italia.png';
$('#canvas').click(function(e){
console.time('click');
mouseX = e.pageX - this.offsetLeft;
mouseY = e.pageY - this.offsetTop;
c = this.getContext('2d');
p = c.getImageData(mouseX, mouseY, 1, 1).data;
hex = ("000000" + rgbToHex(p[0], p[1], p[2])).slice(-6);
console.timeEnd('click');
console.time('selectArea');
selectArea(mouseX,mouseY,c,hex);
console.timeEnd('selectArea');
});
function selectArea(x,y,c,color){
if (x>=0 && y>=0){
p = c.getImageData(x, y, 1, 1).data;
hex =("000000" + rgbToHex(p[0], p[1], p[2])).slice(-6);
if (color==hex){
c.fillStyle = "rgba(255,0,0,0.1)";
c.fillRect( x, y, 1, 1 );
selectArea(x+1,y,c,color);
selectArea(x-1,y,c,color);
selectArea(x,y+1,c,color);
selectArea(x,y-1,c,color);
}
}
}
function rgbToHex(r, g, b) {
if (r > 255 || g > 255 || b > 255)
throw "Invalid color component";
return ((r << 16) | (g << 8) | b).toString(16);
}
</script>
</html>
i'm using for test this file: http://mappa.italiachecambia.org/assets/homemap/cartina_italia.png the first 3 times i click on a region for color it i have a slow response(1000-5000ms), after the first 3 times the function end in 50ms
i can't use jsfiddle for show the problem bc i get cross-origin error
the code is simple recursive function that change the color of the clicked pixel and launched on the near pixel untill the pixel color is different from the first
but i don't understand why the first 3 times have this slow response and after the 4th is with 0 lag....
You should cache the imageData of the hole image and work with that data instead of invoking getImageData on each selectArea call. Also you could think of implementing it iterative, to prevent maximum call stack errors.
Here's an example:
var ExtendedCanvas = (function() {
var context, data, canvas;
function ExtendedCanvas(selector, imageSrc) {
var wrapper = document.querySelector(selector);
this.element = canvas = document.createElement('canvas');
context = this.element.getContext('2d');
loadImage.call(this, imageSrc, function(image) {
canvas.setAttribute('width', image.width);
canvas.setAttribute('height', image.height);
context.drawImage(image,0,0);
data = context.getImageData(0,0,canvas.width, canvas.height);
});
wrapper.appendChild(this.element);
}
function loadImage(src, cb) {
var image = new Image();
var canvas = this.element;
image.onload = function() {
cb(this);
}
image.crossOrigin = 'Anonymous';
image.src = src;
}
ExtendedCanvas.prototype.getPixelIndex = function(x, y) {
return (Math.floor(y) * canvas.width + Math.floor(x)) * 4;
}
ExtendedCanvas.prototype.getPixelColor = function(x, y) {
var index = this.getPixelIndex(x, y);
var d = data.data;
var r = d[index];
var g = d[index + 1];
var b = d[index + 2];
var a = d[index + 3];
return [r, g, b, a];
}
ExtendedCanvas.prototype.setPixelColor = function(x, y, color) {
var index = this.getPixelIndex(x, y);
var d = data.data;
d[index] = color[0];
d[index + 1] = color[1];
d[index + 2] = color[2];
d[index + 3] = color[3];
}
ExtendedCanvas.prototype.fill = function(x, y, fillColor) {
if(x < 0 || y < 0 || x > canvas.width || y > canvas.height) {
return;
}
fillColor = fillColor || [0,0,0,255];
var stack = [];
var color = this.getPixelColor(x, y).join();
if(color === fillColor) {
return;
}
stack.push([x, y]);
context.fillStyle = fillColor;
if(color === fillColor.join()) {
return;
}
while(stack.length > 0) {
var position = stack.pop();
var posX = position[0];
var posY = position[1];
var posColor = this.getPixelColor(posX, posY).join();
if(posColor === color) {
this.setPixelColor(posX, posY, fillColor);
stack.push([posX, posY + 1]);
stack.push([posX, posY - 1]);
stack.push([posX + 1, posY]);
stack.push([posX - 1, posY]);
}
}
context.putImageData(data, 0, 0);
}
return ExtendedCanvas;
})();
document.addEventListener('DOMContentLoaded', function() {
var c = new ExtendedCanvas('#canvasWrapper', 'https://i.imgur.com/QWaKVGO.png');
c.element.addEventListener('click', function(e) {
var x = e.pageX - this.offsetLeft;
var y = e.pageY - this.offsetTop;
c.fill(x, y);
});
});
<div id="canvasWrapper"></div>

Zoom/scale from different point of image in canvas

I have made a canvas in html5. I'm using it to show parts of a bigger image that you can move and I want to make it possible to zoom both in and out. But I do not know how to make the image scale from a certain point. When I increase the size of the image the part which is shown by the canvas is moved, I want the part in the center of the canvas to be the focus point when the scaling is complete, but no matter how I try it gets distorted some how. It seems like depending on which part of the image is shown, when the scaling happens that part of the picture is going to get moved to different coordinates. I do not know what type of algorithm I would have to use so calculate the movement of the image.
This is a link to a visual example of the movement i mean, https://imgur.com/a/aZiVM, the two images are scaled the same but depending on which part of the image that is visible in the canvas, the amount the image needs to be moved tor the zoom differs.
This is my code, but it isn't really working that well.
<!DOCTYPE HTML>
<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.js"></script>
<style>
body {
padding: 0px;
margin: 0px;
}
</style>
</head>
<body>
<div id="map" style="position:absolute;top:10px;left:50px;">
<canvas id="canvas" width="800" height="600" style="float:left;border:1px solid #000000;">Your browser doesn't support canvas</canvas>
<div id="floorDown" onMouseDown="zoomIn()" style="width:200px;height:50px;float:left;">Zoom in</div><br>
<div id="floorDown" onMouseDown="zoomOut()" style="width:200px;height:50px;float:left;">Zoom out</div>
</div>
<script>
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var canvasOffset=$("#canvas").offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
var startX;
var startY;
var isDown=false;
//start position
var imageX=0;
var imageY=0;
var imageWidth,imageHeight,imageRight,imageBottom;
var draggingImage=false;
var startX;
var startY;
var img=new Image();
img.onload=function(){
imageWidth=img.width;
imageHeight=img.height;
draw();
}
img.src='http://orig00.deviantart.net/35cb/f/2013/030/f/0/tripolar_by_zy0rg-d5t9tqh.png';
function draw(){
// clear the canvas
ctx.clearRect(0,0,canvas.width,canvas.height);
//Disable anti-aliasing
ctx.imageSmoothingEnabled=false;
// draw the image
ctx.drawImage(img,0,0,img.width,img.height,imageX,imageY,imageWidth,imageHeight);
}
function handleMouseDown(e){
startX=parseInt(e.clientX-offsetX);
startY=parseInt(e.clientY-offsetY);
draggingImage= true;
}
function handleMouseUp(e){
draggingImage=false;
draw();
}
function handleMouseOut(e){
handleMouseUp(e);
}
function handleMouseMove(e){
if(draggingImage){
imageClick=false;
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// move the image by the amount of the latest drag
var dx=mouseX-startX;
var dy=mouseY-startY;
imageX+=dx;
imageY+=dy;
// reset the startXY for next time
startX=mouseX;
startY=mouseY;
// redraw the image with border
draw();
}
}
// TEST zoom in/out functions
function zoomIn() {
imageX=imageX*2;
imageY=imageY*2;
imageWidth=imageWidth*2;
imageHeight=imageHeight*2;
draw();
}
function zoomOut() {
imageX=imageX/2;
imageY=imageY/2;
imageWidth=imageWidth/2;
imageHeight=imageHeight/2;
draw();
}
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUp(e);});
$("#canvas").mouseout(function(e){handleMouseOut(e);});
</script>
</body>
Given the origin (pos) and scale to zoom at a point
var pos = {x : 0, y : 0};
var scale = 1;
function zoomAt(x,y,_scale)
scale *= _scale
pos.x = x - (x - pos.x) * scale;
pos.y = y - (y - pos.y) * scale;
}
You can then create the transform with
ctx.setTransform(scale, 0, 0, scale, pos.x, pos.y);
So to zoom at the center of screen
zoomAt(canvas.width / 2, canvas.height / 2, 1.1); // zoom in
zoomAt(canvas.width / 2, canvas.height / 2, 1 / 1.1); // zoom out
Put all together
// the following globals are available
// w, h, cw, ch, width height centerWidth centerHeight of canvas
// canvas, ctx, mouse, globalTime
const image = new Image;
image.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/KTZ_2TE10U_Aynabulak.jpg/800px-KTZ_2TE10U_Aynabulak.jpg";
const font = {
font : "28px Arial",
textAlign : "center",
textBaseline : "middle",
}
function setStyle(ctx, style){
Object.keys(style).forEach(key => ctx[key] = style[key]);
}
// Handle all key input
const keys = { // key input object
ArrowLeft : false, // only add key names you want to listen to
ArrowRight : false,
keyEvent (event) {
if (keys[event.code] !== undefined) { // are we interested in this key
keys[event.code] = event.type === "keydown";
}
}
}
// add key listeners
document.addEventListener("keydown", keys.keyEvent);
document.addEventListener("keyup", keys.keyEvent);
const view = (()=>{
const matrix = [1,0,0,1,0,0]; // current view transform
const invMatrix = [1,0,0,1,0,0]; // current inverse view transform
var m = matrix; // alias
var im = invMatrix; // alias
var rotate = 0; // current x axis direction in radians
var scale = 1; // current scale
const pos = { // current position of origin
x : 0,
y : 0,
}
var dirty = true;
return {
apply(ctx){
if(dirty){ this.update() }
var m = matrix;
ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
},
update(){ // call to update transforms
var xdx = Math.cos(rotate) * scale;
var xdy = Math.sin(rotate) * scale;
m[0] = xdx;
m[1] = xdy;
m[2] = -xdy;
m[3] = xdx;
m[4] = pos.x;
m[5] = pos.y;
// calculate the inverse transformation
cross = m[0] * m[3] - m[1] * m[2];
im[0] = m[3] / cross;
im[1] = -m[1] / cross;
im[2] = -m[2] / cross;
im[3] = m[0] / cross;
dirty = false;
},
toWorld(x,y,point = {}){ // convert screen to world coords
var xx, yy;
if(dirty){ this.update() }
xx = x - matrix[4];
yy = y - matrix[5];
point.x = xx * im[0] + yy * im[2];
point.y = xx * im[1] + yy * im[3];
return point;
},
toScreen(x,y,point = {}){ // convert world coords to coords
if(dirty){ this.update() }
point.x = x * m[0] + y * m[2] + m[4];
point.y = x * m[1] + y * m[3] + m[5];
return point;
},
movePos(x,y){
pos.x += x;
pos.y += y;
dirty = true;
},
setPos(x,y){
pos.x = x;
pos.y = y;
dirty = true;
},
setScale(sc){
scale = sc;
dirty = true;
},
scaleScale(sc){
scale *= sc;
dirty = true;
},
scaleAt(x,y,sc){
if(dirty){ this.update() }
scale *= sc;
pos.x = x - (x - pos.x) * sc;
pos.y = y - (y - pos.y) * sc;
dirty = true;
}
};
})();
function onResize(){
setStyle(ctx,font);
}
const drag = {
dragging : false,
lastX : 0,
lastY : 0,
update(){
var dx,dy;
if(mouse.w){
if(mouse.w < 0){
mouse.w += 10;
view.scaleAt(mouse.x,mouse.y,1/1.02);
if(mouse.w > 0){
mouse.w = 0;
}
} else if(mouse.w > 0){
mouse.w -= 10;
view.scaleAt(mouse.x,mouse.y,1.02);
if(mouse.w < 0){
mouse.w = 0;
}
}
}
if(mouse.buttonRaw){
if(!this.dragging){
this.dragging = true;
this.lastX = mouse.x;
this.lastY = mouse.y;
}else{
if(mouse.buttonRaw & 1){
dx = mouse.x-this.lastX;
dy = mouse.y-this.lastY;
this.lastX = mouse.x;
this.lastY = mouse.y;
view.movePos(dx,dy);
}
}
}else{
if(this.dragging){
this.dragging = false;
}
}
}
}
function display() { // call once per frame
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
if(keys.ArrowLeft ){ mouse.w += 10 }
if(keys.ArrowRight){ mouse.w -= 10 }
drag.update();
if(image.complete){
view.apply(ctx);
ctx.drawImage(image,0,0);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillText("Click drag to pan. Wheel to zoom or left/right arrow.",cw,20)
}else{
ctx.fillText("Loading Image...",cw,ch)
}
}
/******************************************************************************
The code from here down is generic full page mouse and canvas boiler plate
code. As I do many examples which all require the same mouse and canvas
functionality I have created this code to keep a consistent interface. The
Code may or may not be part of the answer.
This code may or may not have ES6 only sections so will require a transpiler
such as babel.js to run on legacy browsers.
*****************************************************************************/
// V2.0 ES6 version for Stackoverflow and Groover QuickRun
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0;
// You can declare onResize (Note the capital R) as a callback that is also
// called once at start up. Warning on first call canvas may not be at full
// size.
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var resizeTimeoutHandle;
var firstRun = true;
function createCanvas () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 10;
document.body.appendChild(c);
return c;
}
function resizeCanvas () {
if (canvas === undefined) { canvas = createCanvas() }
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals() }
if (typeof onResize === "function") {
clearTimeout(resizeTimeoutHandle);
if (firstRun) { onResize() }
else { resizeTimeoutHandle = setTimeout(onResize, RESIZE_DEBOUNCE_TIME) }
firstRun = false;
}
}
function setGlobals () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) { e.preventDefault() }
var m; // alias for mouse
var mouse = {
x : 0, y : 0, w : 0, // mouse position and wheel
alt : false, shift : false, ctrl : false, // mouse modifiers
buttonRaw : 0,
over : false, // true if mouse over the element
buttonOnMasks : [0b1, 0b10, 0b100], // mouse button on masks
buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
active : false,
bounds : null,
eventNames : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(","),
event(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
else if (t === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
else if (t === "mouseout") { m.over = false }
else if (t === "mouseover") { m.over = true }
else if (t === "mousewheel") {
m.w = e.wheelDelta
e.preventDefault();
}
else if (t === "DOMMouseScroll") {
m.w = -e.detail
e.preventDefault();
}
},
start(element) {
m.element = element === undefined ? document : element;
m.eventNames.forEach(name => document.addEventListener(name, mouse.event) );
document.addEventListener("contextmenu", preventDefault, false);
m.active = true;
},
}
m = mouse;
return mouse;
})();
function update(timer) { // Main update loop
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update)
}
setTimeout(function(){
canvas = createCanvas();
mouse.start(canvas, true);
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
/** SimpleFullCanvasMouse.js end **/
#imageCC {
font-family : arial;
font-size : 10px;
position : absolute;
z-index : 100;
bottom : 3px;
right : 10px;
background : rgba(255,255,255,0.7);
}
<div id=imageCC>Image rights.
Kabelleger / David Gubler (http://www.bahnbilder.ch), KTZ 2TE10U Aynabulak, CC BY-SA 3.0
</div>

Fade canvas video from greyscale to color

I have 2 elements - video and canvas. On video play event, a functions draws the same video on canvas only greyscale. Then I have a button which is supposed to fade canvas video from greyscale back to color. So far I've managed to get back colors on button click, but I need it to fade - from greyscale to color, not just instantly show color.
Any ideas on how could I accomplish that? Or.. is it even possible?
Here's the code:
function grey() {
if (!stop) {
bgContext.drawImage(video, 0, 0, w, h);
var pixelData = bgContext.getImageData(0, 0, w, h);
for (var i = 0; i < pixelData.data.length; i += 4 ) {
var r = pixelData.data[i];
var g = pixelData.data[i+1];
var b = pixelData.data[i+2];
var averageColour = (r + g + b) / 3;
pixelData.data[i] = averageColour;
pixelData.data[i+1] = averageColour;
pixelData.data[i+2] = averageColour;
}
context.putImageData(pixelData, 0, 0);
}
}
function color() {
bgContext.drawImage(video, 0, 0, w, h);
var pixelData = bgContext.getImageData(0, 0, w, h);
for (var i = 0; i < pixelData.data.length; i += 4 ) {
var r = pixelData.data[i];
var g = pixelData.data[i+1];
var b = pixelData.data[i+2];
pixelData.data[i] = r;
pixelData.data[i+1] = g;
pixelData.data[i+2] = b;
}
context.putImageData(pixelData, 0, 0);
}
video.addEventListener('play', function() {
setInterval("grey()", 0);
}, false);
button.addEventListener('click', function() {
stop = true;
setInterval("color()", 0);
}, false);
Canvas Filters for real-time Video/Animation.
Black & White filter
To do a black and white filter is easy.
// mixAmount is a value from 0 - 1 0 = no mix 1 = full FX
// video is the video
ctx.drawImage(video,0,0); // draw the video
// set up filter
ctx.fillStyle = "#888"; // gray colour
ctx.globalAlpha = mixAmount; // amount of FX
ctx.globalCompositeOperation = "color"; // The comp setting to do BLACK/WHITE
ctx.fillRect(0,0,video.width,video.height); // Draw gray over the video
ctx.globalAlpha = 1; // reset alpha
ctx.globalCompositeOperation = "source-over"; // reset comp
Or you can render the video over itself to get other FX, the demo shows the Black and White filter and several more by just using the above code and a few extra layers.
More info
For more on displaying a video see Display video inside canvas
Demo
The demo show how to do Black and white and some other FX while I am at it.
How to use.
See video title for attribution. FX on left from top to bottom "Lighten", "Black & white", "Sepia", "Saturate", and "Negative".
The Demo has the following FX Lighter, Darken, Black/White, Negative, Saturate, Sepia, B&W negative, and more.
The Javascript
The code relating to the question is all at the top and marked. The rest is UI loading etc..
Each FX is a function that will call either addMix or addOverlay to apply the filter as shown in the snippet above. The addMix function is slightly different as it draws the video over the video to get the FX rather than a fill.
Instructions are on the demo.
Please note not all browser support all comp modes (WHY?? who knows!! :( ) Nor is there a way to be 100% sure if a browser supports a mode or not. The safe bet is Firefox, Chrome, and Edge for all other browsers best of luck..
//==========================================================================
// All the mix function are in this section
var FXMix = 1;
var addOverlay = function(type, repeat = 1){
if(FXMix > 0){
ctx.globalCompositeOperation = type;
ctx.globalAlpha = FXMix;
while (repeat-- > 0) {
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
}
}
var addMix = function(type,video, repeat = 1){
if(FXMix > 0){
ctx.globalCompositeOperation = type;
ctx.globalAlpha = FXMix;
while (repeat-- > 0) {
ctx.drawImage(video,0, 0, canvas.width, canvas.height);
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
}
}
var fill = function(style){
ctx.globalAlpha = FXMix;
ctx.fillStyle = style;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1;
}
var FX = {
}
var FXList = [];
var currentFX = "";
var addFX = function(name,func){
FXList.push(name);
FX[name] = func;
currentFX = name;
}
// multiply,screen,overlay,color-dodge,color-burn,hard-light,soft-light,difference,exclusion,hue,saturation,color,luminosity
addFX("Ligher",(vid)=>{ addMix("lighter",vid);} );
addFX("BlackWhite",(vid)=>{ ctx.fillStyle = "#888"; addOverlay("color");} );
addFX("Negative",(vid)=>{ ctx.fillStyle = "#FFF"; addOverlay("difference");} );
addFX("Sepia",(vid)=>{ fill("#F94"); addMix("luminosity",vid); ;} );
addFX("B&W Negative",(vid)=>{ ctx.fillStyle = "#FFF"; addOverlay("difference");ctx.fillStyle = "#888"; addOverlay("color");} );
addFX("Ligher+",(vid)=>{ addMix("lighter",vid);addMix("lighter",vid);addMix("lighter",vid);} );
addFX("B&W Lighten",(vid)=>{ addMix("lighter",vid);ctx.fillStyle = "#888"; addOverlay("color");} );
addFX("Darken+",(vid)=>{ addMix("multiply",vid);addMix("multiply",vid);addMix("multiply",vid);} );
addFX("Darken",(vid)=>{ addMix("multiply",vid);} );
addFX("Saturate",()=>{ ctx.fillStyle = "#F00";addOverlay("saturation");});
addFX("Movement",(vid) => {
const keepMix = FXMix;
FXMix = 1;
addMix("difference",can1);
addMix("lighter",ctx.canvas,2);
addMix("multiply",vid,1);
FXMix = keepMix * 0.95;
addMix("screen",can2,1);
can2.ctx.drawImage(ctx.canvas,0,0,canvas.width, canvas.height);
FXMix = 1;
addMix("lighter",ctx.canvas,1);
FXMix = keepMix;
var scale = videoContainer.scale;
var vidH = vid.videoHeight;
var vidW = vid.videoWidth;
var top = canvas.height / 2 - (vidH /2 ) * scale;
var left = canvas.width / 2 - (vidW /2 ) * scale;
if(can1.counting === undefined) { can1.counting = 0 }
else { can1.counting ++ }
if(can1.counting % 2 === 0) {
can1.ctx.drawImage(vid, left, top, vidW * scale, vidH * scale);
}
});
addFX("None",()=>{});
// end of FX mixing
//==========================================================================
var mediaSource = "http://video.webmfiles.org/big-buck-bunny_trailer.webm";
var mediaSource = "http://upload.wikimedia.org/wikipedia/commons/7/79/Big_Buck_Bunny_small.ogv";
var muted = true;
var canvas = document.getElementById("myCanvas"); // get the canvas from the page
var ctx = canvas.getContext("2d");
const can1 = document.createElement("canvas");
can1.width = canvas.width;
can1.height = canvas.height;
can1.ctx = can1.getContext("2d");
const can2 = document.createElement("canvas");
can2.width = canvas.width;
can2.height = canvas.height;
can2.ctx = can2.getContext("2d");
var videoContainer; // object to hold video and associated info
var video = document.createElement("video"); // create a video element
video.src = mediaSource;
// the video will now begin to load.
// As some additional info is needed we will place the video in a
// containing object for convenience
video.autoPlay = false; // ensure that the video does not auto play
video.loop = true; // set the video to loop.
video.muted = muted;
videoContainer = { // we will add properties as needed
video : video,
ready : false,
};
// To handle errors. This is not part of the example at the moment. Just fixing for Edge that did not like the ogv format video
video.onerror = function(e){
document.body.removeChild(canvas);
document.body.innerHTML += "<h2>There is a problem loading the video</h2><br>";
document.body.innerHTML += "Users of IE9+ , the browser does not support WebM videos used by this demo";
document.body.innerHTML += "<br><a href='https://tools.google.com/dlpage/webmmf/'> Download IE9+ WebM support</a> from tools.google.com<br> this includes Edge and Windows 10";
}
video.oncanplay = readyToPlayVideo; // set the event to the play function that
// can be found below
function readyToPlayVideo(event){ // this is a referance to the video
// the video may not match the canvas size so find a scale to fit
videoContainer.scale = Math.min(
canvas.width / this.videoWidth,
canvas.height / this.videoHeight);
videoContainer.ready = true;
// the video can be played so hand it off to the display function
requestAnimationFrame(updateCanvas);
// add instruction
document.getElementById("playPause").textContent = "Click video to play/pause.";
document.querySelector(".mute").textContent = "Mute";
}
var playClick = false;
function updateCanvas(){
ctx.clearRect(0,0,canvas.width,canvas.height);
// only draw if loaded and ready
if(videoContainer !== undefined && videoContainer.ready){
// find the top left of the video on the canvas
video.muted = muted;
var scale = videoContainer.scale;
var vidH = videoContainer.video.videoHeight;
var vidW = videoContainer.video.videoWidth;
var top = canvas.height / 2 - (vidH /2 ) * scale;
var left = canvas.width / 2 - (vidW /2 ) * scale;
// now just draw the video the correct size
ctx.drawImage(videoContainer.video, left, top, vidW * scale, vidH * scale);
FX[currentFX](videoContainer.video);
if(videoContainer.video.paused){ // if not playing show the paused screen
drawPayIcon();
}
overUI = false;
cursor = "default";
drawSlider();
drawList();
if(mouse.over){
if(!overUI){
if((mouse.button&1)===1){ // bit field
playClick = true;
}
if((mouse.button&1)===0 && playClick){ // bit field
playClick = false;
playPauseClick();
}
cursor = "pointer";
}
}
if(showFXName > 0){
showFXName = Math.max(0,showFXName - 0.05);
ctx.globalAlpha = Math.min(1,showFXName);
ctx.font = "32px Arial";
ctx.textAlign = "center";
ctx.textbaseLine = "middle";
ctx.fillStyle = "white";
ctx.strokeStyle = "black";
ctx.lineJoin = "round"
ctx.strokeText(currentFX,canvas.width/2,canvas.height/2);
ctx.fillText(currentFX,canvas.width/2,canvas.height/2);
ctx.globalAlpha = 1;
}
canvas.style.cursor = cursor;
}
// all done for display
// request the next frame in 1/60th of a second
requestAnimationFrame(updateCanvas);
}
var showFXName = 0;
var cursor = "default";
var overUI = false;
var sliderAlpha = 1;
var listAlpha = 1;
var dragging = false;
var listWidth = null;
function getMaxListWidth(){
ctx.font = "12px arial";
FXList.forEach(text => {listWidth = Math.max(listWidth,ctx.measureText(text).width)})
}
function drawList(){
if(listWidth === null){
getMaxListWidth();
listWidth += 10;
}
if(!overUI && mouse.over && mouse.x > canvas.width - listWidth){
listAlpha = 1;
overUI = true;
}else{
listAlpha = Math.max(0,listAlpha - 0.05);
}
if(listAlpha > 0){
ctx.font = "12px arial";
var textH = 14;
var border = 10;
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.globalAlpha = listAlpha;
ctx.fillStyle = "black";
ctx.strokeStyle = "white";
var len = FXList.length;
var h = len * textH;
var y = canvas.height / 2 - h/2;
var x = canvas.width - border * 2;
ctx.fillRect(x - listWidth,y - border, listWidth+border,h + border );
ctx.strokeRect(x - listWidth,y - border, listWidth + border,h + border );
ctx.fillStyle = "white"
for(var i = 0; i < len; i ++){
var yy = y + i * textH;
if(FXList[i] === currentFX){
ctx.fillStyle = "#0FF";
ctx.fillText(FXList[i],x,yy);
ctx.fillStyle = "white"
}else
if(mouse.x > canvas.width - listWidth && mouse.y > yy - textH/2 && mouse.y < yy + textH /2){
ctx.fillStyle = "#0F0";
ctx.fillText(FXList[i],x,yy);
ctx.fillStyle = "white"
cursor = "pointer";
if((mouse.button & 1) === 1){
currentFX =FXList[i];
showFXName = 4;
}
}else{
ctx.fillText(FXList[i],x,yy);
}
}
ctx.globalAlpha = 1;
}
}
function drawSlider(){
if(currentFX === "None"){
sliderAlpha = 0;
return;
}
var cw = canvas.width;
var ch = canvas.height;
var handle = 5;
var inset = 10
var x = inset;
var w = cw - inset*2;
var h = 20;
var y = ch - inset - h;
var pos = FXMix * w + x;;
if(mouse.y > y - h* 2){
cursor = "e-resize";
overUI = true;
if((mouse.button&1) && !dragging){ // bit field
dragging = true;
}
}else{
cursor = "pointer";
}
if(dragging){
overUI = true;
cursor = "e-resize";
sliderAlpha = 1;
pos = mouse.x - x;
FXMix = Math.min(1,Math.max(0,pos / w));
if( (mouse.button&1) === 0 ){ //bit field
dragging = false;
}
}else{
}
if(!dragging && mouse.y > y-h*2 && mouse.over){
sliderAlpha = 1;
}else{
if(sliderAlpha > 0){
sliderAlpha = Math.max(0,sliderAlpha- 0.05);
}
}
if(sliderAlpha === 0){
return;
}
ctx.globalAlpha = sliderAlpha;
ctx.font = "18px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
var amount = FXMix;
ctx.fillStyle = "black";
ctx.strokeStyle = "white";
ctx.fillRect(x,y,w,h);
ctx.strokeRect(x,y,w,h);
ctx.fillStyle = "white";
ctx.fillText(currentFX + " "+ (FXMix * 100).toFixed(0)+"%",w/2,y + h / 2);
pos = amount * w + x;
ctx.fillStyle = "white";
ctx.strokeStyle = "black";
ctx.fillRect(pos-handle*2,y-handle,handle* 4,h + handle * 2);
ctx.strokeRect(pos-handle*2,y-handle,handle* 4,h + handle * 2);
ctx.strokeRect(pos-1,y-handle * 0.5,2,h + handle);
ctx.globalAlpha = 1;
}
function drawPayIcon(){
// ctx.fillStyle = "black"; // darken display
// ctx.globalAlpha = 0.5;
// ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "#DDD"; // colour of play icon
ctx.globalAlpha = 0.75; // partly transparent
ctx.beginPath(); // create the path for the icon
var size = (canvas.height / 2) * 0.5; // the size of the icon
ctx.moveTo(canvas.width/2 + size/2, canvas.height / 2); // start at the pointy end
ctx.lineTo(canvas.width/2 - size/2, canvas.height / 2 + size);
ctx.lineTo(canvas.width/2 - size/2, canvas.height / 2 - size);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = 1; // restore alpha
}
mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0,
button : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
border : {top : 10, left : 10},
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,contextmenu".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.clientX - m.bounds.left - m.border.left;
m.y = e.clientY - m.bounds.top - m.border.top;
if (t === "mousedown") {
m.button |= m.bm[e.which-1];
} else if (t === "mouseup") {
m.button &= m.bm[e.which + 2];
}else if (t === "mouseout") {
m.button = 0;
m.over = false;
}else if (t === "mouseover") {
m.over = true;
}
e.preventDefault();
}
m.start = function (element) {
m.element = element;
m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
m.active = true;
//m.border.top = Number(element.style.borderTopWidth.replace(/[a-zA-Z]/g,""));
//m.border.left = Number(element.style.borderLeftWidth.replace(/[a-zA-Z]/g,""));
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
m.active = false;
m.element = undefined;
}
}
return mouse;
})();
function playPauseClick(){
if(videoContainer !== undefined && videoContainer.ready){
if(videoContainer.video.paused){
videoContainer.video.play();
}else{
videoContainer.video.pause();
}
}
}
function videoMute(){
muted = !muted;
if(muted){
document.querySelector(".mute").textContent = "Mute";
}else{
document.querySelector(".mute").textContent= "Sound on";
}
}
// register the event
//canvas.addEventListener("click",playPauseClick);
document.querySelector(".mute").addEventListener("click",videoMute)
setTimeout(()=>{mouse.start(canvas)},100);
body {
font :14px arial;
text-align : center;
background : #36A;
}
h2 {
color : white;
}
canvas {
border : 10px white solid;
cursor : pointer;
}
a {
color : #F93;
}
.mute {
cursor : pointer;
display: initial;
}
<h2>Simple video FX via canvas "globalCompositeOperation"</h2>
<p>This example show how to use the 2d context "globalCompositeOperation" property to create a variety of FX. Video may take a few moment to load.
</p>
<p>Play pause video with click. Move to bottom of video to see FX mix slider (Not available if filter None). Move to right to get filter selection and select the filter example. Happy filtering</p>
<canvas id="myCanvas" width = "532" height ="300" ></canvas><br>
<h3><div id = "playPause">Loading content.</div></h3>
<div class="mute"></div><br>
Update 30 Sep 2019
Add filter "Movement" that highlight the change per frame (movement). Slider changes the persistence of the highlighted changes.
The simplest is to set a greyscale css filter on the canvas.
var video = document.getElementById("myCanvas");
var button = document.getElementById("myButton");
function grey() {
video.className += " greyscale";
}
function color() {
classGreyscale = video.className.indexOf("greyscale");
if (classGreyscale > 0)
video.className = video.className.substring(0, video.className.length - 10)
}
button.addEventListener('click', function() {
color();
});
grey();
.greyscale {
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.transition {
transition: all 1s;
-webkit-transition: all 1s;
}
<div>
<img src="https://upload.wikimedia.org/wikipedia/en/2/24/Lenna.png" id="myCanvas" class="transition" />
<br/>
<button id="myButton">to Colour</button>
</div>
So what we do here is add a transition class for our canvas, so it can animate the changes. Then when we add a grayscale class to it and this changes a css filter along with the transition so it fades in. When we want to make it colorful again we remove the greyscale class.
I made the example with an image but it will work with everything.
This way you fade in the class with 1s transition on all parameters (greyscale here). It is better to use this, because you don't have to count every pixel in grayscale, it is cleaner.
Note that you could use jQuery addClass removeClass for simpler and cleaner soultion.
Also note that you should average r,g,b with weights:
https://en.wikipedia.org/wiki/Grayscale#Colorimetric_.28luminance-preserving.29_conversion_to_grayscale
Saturation version with transition and -webkit-transition: -webkit-filter 10s;
This is only for Safari and Chrome.
The code is for hovering.
I guess very similar to #godzsa
The other method I can think of is to create a div with a higher index on top of your video with a play on white saturation.
For working Youtube video,
https://jsfiddle.net/yd215t9p/
And for image,
div {
-webkit-filter:saturate(0.0);
-webkit-transition: -webkit-filter 10s; /* Safari */
transition: -webkit-filter 10s;
}
div:hover {
-webkit-filter:saturate(1.0);
}
<div>
<img src="https://upload.wikimedia.org/wikipedia/en/2/24/Lenna.png" id="myCanvas" class="transition" />
</div>

Canvas flood fill stroke color

I'm building a small application using canvas. My application will have an option to fill a black and white image.
I downloaded a code and is working fine, but It only works when the image stroke is black. All images that I am going to use have grey stroke.
So, I would like to know what do I need to change to put the code working with grey strokes, instead of black strokes.
Here the code:
https://jsfiddle.net/mx0fmdh3/
HTML:
<canvas id="canvas" width=250 height=243></canvas>
JavaScript
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var $canvas = $("#canvas");
var canvasOffset = $canvas.offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var canvasWidth = canvas.width;
var canvasHeight = canvas.height;
var strokeColor = {
r: 152,
g: 152,
b: 152
};
var fillColor = {
r: 101,
g: 155,
b: 65
};
var fillData;
var strokeData;
// load image
var img = new Image();
img.onload = function () {
start();
}
img.crossOrigin = "anonymous";
img.src = "http://i.imgur.com/kjY1kiE.png";
function matchstrokeColor(r, g, b, a) {
// never recolor the initial black divider strokes
// must check for near black because of anti-aliasing
return (r + g + b < 100 && a === 155);
}
function matchStartColor(pixelPos, startR, startG, startB) {
// get the color to be matched
var r = strokeData.data[pixelPos],
g = strokeData.data[pixelPos + 1],
b = strokeData.data[pixelPos + 2],
a = strokeData.data[pixelPos + 3];
// If current pixel of the outline image is black-ish
if (matchstrokeColor(r, g, b, a)) {
return false;
}
// get the potential replacement color
r = fillData.data[pixelPos];
g = fillData.data[pixelPos + 1];
b = fillData.data[pixelPos + 2];
// If the current pixel matches the clicked color
if (r === startR && g === startG && b === startB) {
return true;
}
// If current pixel matches the new color
if (r === fillColor.r && g === fillColor.g && b === fillColor.b) {
return false;
}
return true;
}
// Thank you William Malone!
function floodFill(startX, startY, startR, startG, startB) {
var newPos;
var x;
var y;
var pixelPos;
var neighborLeft;
var neighborRight;
var pixelStack = [
[startX, startY]
];
while (pixelStack.length) {
newPos = pixelStack.pop();
x = newPos[0];
y = newPos[1];
// Get current pixel position
pixelPos = (y * canvasWidth + x) * 4;
// Go up as long as the color matches and are inside the canvas
while (y >= 0 && matchStartColor(pixelPos, startR, startG, startB)) {
y -= 1;
pixelPos -= canvasWidth * 4;
}
pixelPos += canvasWidth * 4;
y += 1;
neighborLeft = false;
neighborRight = false;
// Go down as long as the color matches and in inside the canvas
while (y <= (canvasHeight - 1) && matchStartColor(pixelPos, startR, startG, startB)) {
y += 1;
fillData.data[pixelPos] = fillColor.r;
fillData.data[pixelPos + 1] = fillColor.g;
fillData.data[pixelPos + 2] = fillColor.b;
fillData.data[pixelPos + 3] = 255;
if (x > 0) {
if (matchStartColor(pixelPos - 4, startR, startG, startB)) {
if (!neighborLeft) {
// Add pixel to stack
pixelStack.push([x - 1, y]);
neighborLeft = true;
}
} else if (neighborLeft) {
neighborLeft = false;
}
}
if (x < (canvasWidth - 1)) {
if (matchStartColor(pixelPos + 4, startR, startG, startB)) {
if (!neighborRight) {
// Add pixel to stack
pixelStack.push([x + 1, y]);
neighborRight = true;
}
} else if (neighborRight) {
neighborRight = false;
}
}
pixelPos += canvasWidth * 4;
}
}
}
// Start a floodfill
// 1. Get the color under the mouseclick
// 2. Replace all of that color with the new color
// 3. But respect bounding areas! Replace only contiguous color.
function paintAt(startX, startY) {
// get the clicked pixel's [r,g,b,a] color data
var pixelPos = (startY * canvasWidth + startX) * 4,
r = fillData.data[pixelPos],
g = fillData.data[pixelPos + 1],
b = fillData.data[pixelPos + 2],
a = fillData.data[pixelPos + 3];
// this pixel's already filled
if (r === fillColor.r && g === fillColor.g && b === fillColor.b) {
return;
}
// this pixel is part of the original black image--don't fill
if (matchstrokeColor(r, g, b, a)) {
return;
}
// execute the floodfill
floodFill(startX, startY, r, g, b);
// put the colorized data back on the canvas
context.clearRect(0, 0, canvasWidth, canvasHeight);
context.putImageData(fillData, 0, 0);
context.drawImage(img, 0, 0);
}
// create a random color object {red,green,blue}
function randomColorRGB() {
var hex = Math.floor(Math.random() * 16777215).toString(16);
//var r = parseInt(hex.substring(0, 2), 16);
var r = 155;
var g = 155;
var b = 255;
//var g = parseInt(hex.substring(2, 4), 16);
//var b = parseInt(hex.substring(4, 6), 16);
return ({
r: r,
g: g,
b: b
});
}
// draw the image to the canvas and get its pixel array
// listen for mouse clicks and do floodfill when clicked
function start() {
context.drawImage(img, 0, 0);
strokeData = context.getImageData(0, 0, canvasWidth, canvasHeight);
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
fillData = context.getImageData(0, 0, canvasWidth, canvasHeight);
context.drawImage(img, 0, 0);
$('#canvas').mousedown(function (e) {
// Mouse down location
var mouseX = parseInt(e.clientX - offsetX);
var mouseY = parseInt(e.clientY - offsetY);
// set a new random fillColor
fillColor = randomColorRGB();
// floodfill
paintAt(mouseX, mouseY);
});
Thank you.
The match stroke function:
function matchstrokeColor(r, g, b, a) {
// never recolor the initial black divider strokes
// must check for near black because of anti-aliasing
return (r + g + b < 100 && a === 155);
}
is only check whether rgb is a small number and is a painted stroke, as you directly paint your image on that canvas, rgb should now become something else, and alpha is now 255(or unpredictable if your image has alpha).
Try change it to something that is aware of the storke's color, like sqrt distance:
// A small threshold would make it fill closer to stroke.
var strokeThreshold = 1;
function matchstrokeColor(r, g, b, a) {
// Use sqrt difference to decide its storke or not.
var diffr = r - strokeColor.r;
var diffg = g - strokeColor.g;
var diffb= b - strokeColor.b;
var diff = Math.sqrt(diffr * diffr + diffg * diffg + diffb * diffb) / 3;
return (diff < strokeThreshold);
}
See Example jsfiddle

Simple memory game doesn't update its state. Bad game loop?

I'm trying to make a simple (or so I thought) memory game. Unfortunately it does not update state of cards when user clicks on them. I'm running out of ideas, probably because it's my first javascript game. I suppose there is a problem with game loop. Could anyone at least point me in the right direction and help me understand what needs to be changed/rewritten?
//HTML5 Memory Game implementation
//main variables
var cards = [1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8];
var exposed = [makeArray("false",16)];
var first_card = 0;
var second_card = 0;
var moves = 0;
var WIDTH = 800;
var HEIGHT = 100;
var state = 0;
var mouseX = 0;
var mouseY = 0;
//creating canvas
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = WIDTH;
canvas.height = HEIGHT;
document.getElementById("game").appendChild(canvas);
//filling empty array with number,character,object
function makeArray(value, length) {
var newArray = [];
var i = 0;
while (i<length) {
newArray[i] = value;
i++;
}
return newArray;
}
//shuffling algorithm
function shuffle(array) {
var copy = [];
var n = array.length;
var i;
while (n) {
i = Math.floor(Math.random() * n--);
copy.push(array.splice(i, 1)[0]);
}
return copy;
}
//where user clicks
function getClickPosition(event) {
var X = event.pageX - canvas.offsetLeft;
var Y = event.pageY - canvas.offsetTop;
return mouse = [X, Y];
}
//read click position
function readPos(event) {
mousePos = getClickPosition(event);
mouseX = mousePos[0];
mouseY = mousePos[1];
}
//initializing
function init() {
state = 0;
moves = 0;
exposed = [makeArray("false",16)];
cards = shuffle(cards);
}
//drawing cards
function draw() {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
for (var i in cards) {
if (exposed[i] === true) {
ctx.fillStyle = "rgb(250, 250, 250)";
ctx.font = "50px Courier New";
ctx.fillText(cards[i], (i*50+12), 65);
} else {
ctx.strokeStyle = "rgb(250, 0, 0)";
ctx.fillStyle = "rgb(0, 0, 250)";
ctx.fillRect(i*50, 0, 50, 100);
ctx.strokeRect(i*50, 0, 50, 100);
}
}
};
//update cards
function update() {
if (exposed[parseInt(mouseX / 50)] === false) {
if (state == 0) {
state = 1;
first_card = parseInt(mouseX / 50);
exposed[parseInt(mouseX / 50)] = true;
} else if (state == 1) {
state = 2;
second_card = parseInt(mouseX / 50);
exposed[parseInt(mouseX / 50)] = true;
} else {
if (cards[first_card] != cards[second_card]) {
exposed[first_card] = false;
exposed[second_card] = false;
}
state = 1;
first_card = parseInt(mouseX / 50);
exposed[parseInt(mouseX / 50)] = true;
}
}
}
addEventListener('click', readPos, false);
setInterval(function() {
update();
draw();
}, 16);
I would check your addEventListener method: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
I also recommend you look into using jQuery.
After copy and pasting your code I found a couple of things:
You didn't add an event listener to anything, you should add it to something so I added it to document.
You initialize the exposed array with values "false" and later check if they are false. These are not the same, the string "false" isn't the Boolean false.
You initializes the exposed array as a multi dimensional array [[false,false,false ...]] this should be a single dimension array because later you check exposed[1] (1 depending on the mouse x position.
No need to call draw and update every 16 milliseconds, you can call it after someone clicked.
Wrapped the whole thing up in a function so there are no global variables created.
Here is the code after changing these obvious errors. There might be room for optimization but for now I've gotten the problems out.
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<div id="game"></div>
<script type="text/javascript">
(function(){
//HTML5 Memory Game implementation
//main variables
var cards = [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8];
var exposed = makeArray(false, 16);
var first_card = 0;
var second_card = 0;
var moves = 0;
var WIDTH = 800;
var HEIGHT = 100;
var state = 0;
var mouseX = 0;
var mouseY = 0;
//creating canvas
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = WIDTH;
canvas.height = HEIGHT;
document.getElementById("game").appendChild(canvas);
//filling empty array with number,character,object
function makeArray(value, length) {
var newArray = [];
var i = 0;
while (i < length) {
newArray.push(value);
i++;
}
return newArray;
}
//shuffling algorithm
function shuffle(array) {
var copy = [];
var n = array.length;
var i;
while (n) {
i = Math.floor(Math.random() * n--);
copy.push(array.splice(i, 1)[0]);
}
return copy;
}
//where user clicks
function getClickPosition(event) {
var X = event.pageX - canvas.offsetLeft;
var Y = event.pageY - canvas.offsetTop;
return mouse = [X, Y];
}
//read click position
function readPos(event) {
mousePos = getClickPosition(event);
mouseX = mousePos[0];
mouseY = mousePos[1];
update();
draw();
}
//initializing
function init() {
state = 0;
moves = 0;
exposed = makeArray(false, 16);
cards = shuffle(cards);
}
//drawing cards
function draw() {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
for (var i in cards) {
if (exposed[i] === true) {
ctx.fillStyle = "rgb(150, 150, 150)";
ctx.font = "50px Courier New";
ctx.fillText(cards[i], (i * 50 + 12), 65);
} else {
ctx.strokeStyle = "rgb(250, 0, 0)";
ctx.fillStyle = "rgb(0, 0, 250)";
ctx.fillRect(i * 50, 0, 50, 100);
ctx.strokeRect(i * 50, 0, 50, 100);
}
}
};
//update cards
function update() {
if (exposed[parseInt(mouseX / 50)] === false) {
if (state == 0) {
state = 1;
first_card = parseInt(mouseX / 50);
exposed[parseInt(mouseX / 50)] = true;
} else if (state == 1) {
state = 2;
second_card = parseInt(mouseX / 50);
exposed[parseInt(mouseX / 50)] = true;
} else {
if (cards[first_card] != cards[second_card]) {
exposed[first_card] = false;
exposed[second_card] = false;
}
state = 1;
first_card = parseInt(mouseX / 50);
exposed[parseInt(mouseX / 50)] = true;
}
}
}
document.body.addEventListener('click', readPos, false);
init();
draw();
})();
</script>
</body>
</html>
Your overall logic was good.
The point that was 'bad' was the way you handle the event :
the event handler should store some valuable information that
the update will later process and clear.
Here you mix your update with event handling, which cannot work
especially since the event will not fire on every update.
So i did a little fiddle to show you, the main change is
the click event handler, which update the var last_clicked_card :
http://jsfiddle.net/wpymH/
//read click position
function readPos(event) {
last_clicked_card = -1;
mousePos = getClickPosition(event);
mouseX = mousePos[0];
mouseY = mousePos[1];
// on canvas ?
if ((mouseY>100)||(mouseX<0)||(mouseX>WIDTH)) return;
// now yes : which card clicked ?
last_clicked_card = Math.floor(mouseX/50);
}
and then update is the processing of this information :
//update cards
function update() {
// return if no new card clicked
if (last_clicked_card == -1) return;
// read and clear last card clicked
var newCard = last_clicked_card;
last_clicked_card=-1;
// flip, store it as first card and return
// if there was no card flipped
if (state==0) { exposed[newCard] = true;
first_card = newCard;
state = 1 ;
return; }
// just unflip card if card was flipped
if ((state = 1) && exposed[newCard]) {
exposed[newCard]=false ;
state=0;
return;
}
// we have a second card now
second_card = newCard;
exposed[second_card] = true;
draw();
// ... i don't know what you want to do ...
if (cards[first_card] == cards[second_card]) {
alert('win'); }
else {
alert('loose'); }
exposed[first_card]=false;
exposed[second_card]=false;
state=0;
}

Categories