slow script in the first 3 executions - javascript

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>

Related

How to rotate only part of canvas in javascript?

I've got a task to play a little with JavaScript canvas. I have to onclick rotate the blue block with the movie, and as you can see in the fiddle it works, but the problem is my name which is below the film - it shouldn't rotate but stay on its position at the bottom of canvas (before clicking it's where it should be).
This is the fiddle (without the movie, but it shows the problem):
https://jsfiddle.net/463h8se7/
And this is my JavaScript (it is also in the fiddle, but for clarity here too):
var video;
var canvas;
var ctx;
var click = 0;
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
printName();
ctx.save();
ctx.fillStyle = "#558899";
ctx.fillRect(canvas.width / 4 - 5, canvas.height / 4 - 5, canvas.width / 2 + 10, canvas.height / 2 + 10);
ctx.drawImage(video, canvas.width / 4, canvas.height / 4, canvas.width / 2, canvas.height / 2);
requestAnimationFrame(animate);
}
function rotate() {
if (click == 0) {
click = 1;
i = window.setInterval(function () {
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(2 * Math.PI / 180);
ctx.translate(-canvas.width / 2, -canvas.height / 2);
}, 20);
} else {
click = 0;
window.clearInterval(i);
}
}
function printName() {
ctx.fillStyle = "black";
ctx.font = "15px Arial";
ctx.fillText("aceimnors", canvas.width / 3, canvas.height - 1);
}
function load() {
video = document.getElementById("video");
canvas = document.getElementById("canvas");
ctx = canvas.getContext('2d');
animate();
}
<div id="div_canvas">
<canvas id="canvas" onclick="rotate()"></canvas>
<p>canvas</p>
</div>
I would just put it in animate, since you are already redrawing every requestAnimationFrame.
Keep an angle variable and remember to rotate back.
var video = document.body.appendChild(document.createElement("img"));
video.id = "video";
video.addEventListener("load", load);
video.style.display = "none";
video.src = "https://www.w3schools.com/tags/img_girl.jpg";
var canvas = document.body.appendChild(document.createElement("canvas"));
canvas.id = "canvas";
var ctx = canvas.getContext("2d");
var click = false;
var angle = 0;
var angleTarget = 0;
canvas.addEventListener("click", function () { return angleTarget += .1; });
function animate() {
//Compund Angle
if (angle < angleTarget) {
angle += 0.01;
}
if (angle > angleTarget) {
angle = angleTarget;
}
//Clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
//Text
printName();
//Rotate
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(angle * (Math.PI * 2));
ctx.translate(-canvas.width / 2, -canvas.height / 2);
//Draw Graphic
ctx.fillStyle = "#558899";
ctx.fillRect(canvas.width / 4 - 5, canvas.height / 4 - 5, canvas.width / 2 + 10, canvas.height / 2 + 10);
ctx.drawImage(video, canvas.width / 4, canvas.height / 4, canvas.width / 2, canvas.height / 2);
requestAnimationFrame(animate);
//Rotate back
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(-angle * (Math.PI * 2));
ctx.translate(-canvas.width / 2, -canvas.height / 2);
}
function printName() {
ctx.fillStyle = "black";
ctx.font = "15px Arial";
ctx.fillText("aceimnors", canvas.width / 3, canvas.height - 1);
}
function load() {
video = document.getElementById("video");
canvas = document.getElementById("canvas");
ctx = canvas.getContext('2d');
animate();
}
If you check out my DrawingBox it will give you an idea of how to rotate. You can start reading code at // magic under here:
//<![CDATA[
/* js/external.js */
let get, post,doc, htm, bod, nav, M, I, mobile, beacon, S, Q, hC, aC, rC, tC, inArray, shuffle, isNum, isInt, rand, DrawingBox;
addEventListener('load', ()=>{
get = (url, func, responseType = 'json', context = null)=>{
const x = new XMLHttpRequest;
const c = context || x;
x.open('GET', url); x.responseType = responseType;
x.onload = ()=>{
if(func)func.call(c, x.response);
}
x.onerror = e=>{
if(func)func.call(c, {xhrErrorEvent:e});
}
x.send();
return x;
}
post = function(url, send, func, responseType ='json', context = null){
const x = new XMLHttpRequest;
if(typeof send === 'object' && send && !(send instanceof Array)){
const c = context || x;
x.open('POST', url); x.responseType = responseType;
x.onload = ()=>{
if(func)func.call(c, x.response);
}
x.onerror = e=>{
if(func)func.call(c, {xhrErrorEvent:e});
}
let d;
if(send instanceof FormData){
d = send;
}
else{
let s;
d = new FormData;
for(let k in send){
s = send[k];
if(typeof s === 'object' && s)s = JSON.stringify(s);
d.append(k, s);
}
}
x.send(d);
}
else{
throw new Error('send argument must be an Object');
}
return x;
}
doc = document; htm = doc.documentElement; bod = doc.body; nav = navigator; M = tag=>doc.createElement(tag); I = id=>doc.getElementById(id);
mobile = nav.userAgent.match(/Mobi/i) ? true : false;
beacon = function(url, send){
let r = false;
if(typeof send === 'object' && send && !(send instanceof Array)){
let d;
if(send instanceof FormData){
d = send;
}
else{
let s;
d = new FormData;
for(let k in send){
s = send[k];
if(typeof s === 'object' && s)s = JSON.stringify(s);
d.append(k, s);
}
}
r = nav.sendBeacon(url, d);
}
else{
throw new Error('send argument must be an Object');
}
return r;
}
S = (selector, within)=>{
var w = within || doc;
return w.querySelector(selector);
}
Q = (selector, within)=>{
var w = within || doc;
return w.querySelectorAll(selector);
}
hC = function(node, className){
return node.classList.contains(className);
}
aC = function(){
const a = [...arguments];
a.shift().classList.add(...a);
return aC;
}
rC = function(){
const a = [...arguments];
a.shift().classList.remove(...a);
return rC;
}
tC = function(){
const a = [...arguments];
a.shift().classList.toggle(...a);
return tC;
}
inArray = (mixed, array)=>{
if(array.indexOf(mixed) === -1){
return false;
}
return true;
}
shuffle = array=>{
let a = array.slice(), i = a.length, n, h;
while(i){
n = Math.floor(Math.random()*i--); h = a[i]; a[i] = a[n]; a[n] = h;
}
return a;
}
isNum = mixed=>typeof mixed === 'number' && !isNaN(mixed); isInt = mixed=>Number.isInteger(mixed);
rand = (min, max)=>{
let mn = min, mx = max;
if(mx === undefined){
mx = mn; mn = 0;
}
return mn+Math.floor(Math.random()*(mx-mn+1));
}
DrawingBox = function(canvas, width = null, height = null){
this.canvas; this.ctx;
this.setCanvas = (canvas, width = null, height = null)=>{
canvas.width = width || innerWidth;
canvas.height = height || innerHeight; this.canvas = canvas;
this.ctx = canvas.getContext('2d');
return this;
}
this.point = (x, y)=>{
const c = this.ctx;
c.beginPath(); c.moveTo(x, y);
return this;
}
this.rotate = (x, y, degrees, drawFunc)=>{
const c = this.ctx;
c.save(); c.translate(x, y); c.rotate(degrees*Math.PI/180); c.translate(-x, -y);
drawFunc(c, x, y); c.restore();
return this;
}
this.setCanvas(canvas, width, height);
}
// magic under here
const box = new DrawingBox(I('can')), ctx = box.ctx;
ctx.fillStyle = '#c00'; ctx.fillRect(0, 0, 100, 75);
box.rotate(100, 75, 45, (c, x, y)=>{
c.fillStyle = '#00c'; c.fillRect(x, y, 100, 50);
});
ctx.strokeStyle = '#0c0'; ctx.strokeRect(100, 75, 35, 82);
}); // end load
/* css/external.css */
*{
box-sizing:border-box; color:#000; padding:0; margin:0; overflow:hidden;
}
html,body{
width:100%; height:100%;
}
#can{
background:#fff;
}
<!DOCTYPE html>
<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>
<head>
<meta charset='UTF-8' /><meta name='viewport' content='width=device-width, height=device-height, initial-scale:1, user-scalable=no' />
<title>Title Here</title>
<link type='text/css' rel='stylesheet' href='css/external.css' />
<script src='js/external.js'></script>
</head>
<body>
<canvas id='can'></canvas>
</body>
</html>
You'll notice the blue box is rotated. The DrawingBoxInstance.rotate method takes x,y origin and rotation degrees.
Not that I'm showing you this here, but I will tell you that the real secret of creating user generated animations or drawings (or games) is to push an Object, for every operation, onto an Array can be passed back into your drawing functions. Of course, you will be canvasContext.clearRect(0, 0, canvasWidth, canvasHeight)ing before redrawing previous states to create the illusion of movement.

Canvas window.addEvent error when running on gh-pages

I have a HTML5 canvas project which allows for the uploading of an image to the canvas. Then it is variously drawn on.
I can run this project locally with
$ http-server
Running this locally, I don't get any errors.
The project is on github
The issue is that in gh-pages I get the error,
Uncaught TypeError: window.addEvent is not a function
at (index):22
Line 22 is,
window.addEvent('load', function() {
This is also a problem when trying to run this with the snippet tool.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" crossorigin="anonymous">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://rawgit.com/eligrey/FileSaver.js/master/FileSaver.js"></script>
<script src="https://rawgit.com/eligrey/canvas-toBlob.js/master/canvas-toBlob.js"></script>
<script type="text/javascript">
var Pts = [];
var dist;
let inputValue;
var ratio;
var angle;
window.addEvent('load', function() {
var imageLoader = document.getElementById('imageLoader');
imageLoader.addEventListener('change', handleImage, false);
var c = document.getElementById('canvas');
var ctx = c.getContext('2d');
function handleImage(e) {
var reader = new FileReader();
reader.onload = function(event) {
var img = new Image();
img.onload = function() {
c.width = img.width;
c.height = img.height;
ctx.drawImage(img, 0, 0);
}
img.src = event.target.result;
}
reader.readAsDataURL(e.target.files[0]);
}
$("#canvas").click(function(e) {
getPosition(e);
});
});
var pointSize = 3;
// Event will be a click event which can be retrieved as first parameter in the addEventListener(function(event){}); or in jQuery with $("selector").click(function(event){});
function getPosition(event) {
var rect = canvas.getBoundingClientRect();
var x = event.clientX - rect.left; // x == the location of the click in the document - the location (relative to the left) of the canvas in the document
var y = event.clientY - rect.top; // y == the location of the click in the document - the location (relative to the top) of the canvas in the document
Pts.push({
x: x,
y: y
});
if (Pts.length == 2) {
dist = getDistance();
addInput(Pts[1].x, Pts[1].y);
}
drawCoordinates(x, y);
if (Pts.length % 2 == 0) {
drawLine(Pts[Pts.length - 2].x, Pts[Pts.length - 2].y, Pts[Pts.length - 1].x, Pts[Pts.length - 1].y);
};
}
function decimalAdjust(type, value, exp) {
// If the exp is undefined or zero...
if (typeof exp === 'undefined' || +exp === 0) {
return Math[type](value);
}
value = +value;
exp = +exp;
// If the value is not a number or the exp is not an integer...
if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) {
return NaN;
}
// If the value is negative...
if (value < 0) {
return -decimalAdjust(type, -value, exp);
}
// Shift
value = value.toString().split('e');
value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp)));
// Shift back
value = value.toString().split('e');
return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp));
}
// Decimal round
if (!Math.round10) {
Math.round10 = function(value, exp) {
return decimalAdjust('round', value, exp);
};
}
function getDistance() {
dist = Math.sqrt(Math.pow(Math.abs(Pts[Pts.length - 2].x - Pts[Pts.length - 1].x), 2) + Math.pow(Math.abs(Pts[Pts.length - 2].y - Pts[Pts.length - 1].y), 2));
return dist.toFixed(2);
}
function drawLine(x1, y1, x2, y2) {
var ctx = document.getElementById("canvas").getContext("2d");
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
if (Pts.length < 3) {
ctx.strokeStyle = 'blue';
} else {
ctx.strokeStyle = 'black';
}
ctx.stroke();
dist = getDistance();
txt = dist * ratio;
if (Pts.length > 2) {
drawText(txt, x1, y1, x2, y2);
}
}
function addInput(x, y) {
var input = document.createElement('input');
input.type = 'text';
input.style.position = 'fixed';
input.style.left = (x + 4) + 'px';
input.style.top = (y + 4) + 'px';
input.onkeydown = handleEnter;
document.body.appendChild(input);
input.focus();
hasInput = true;
}
function handleEnter(e) {
var keyCode = e.keyCode;
if (keyCode === 13) {
inputValue = this.value;
document.body.removeChild(this);
hasInput = false;
ratio = inputValue / dist;
if (Pts.length == 2) {
drawText("reference line = " + dist * ratio, Pts[Pts.length - 2].x, Pts[Pts.length - 2].y, Pts[Pts.length - 1].x, Pts[Pts.length - 1].y);
}
}
}
function drawCoordinates(x, y) {
var pointSize = 3; // Change according to the size of the point.
var ctx = document.getElementById("canvas").getContext("2d");
if (Pts.length < 3) {
ctx.fillStyle = "blue"; // Red color
} else {
ctx.fillStyle = "red"; // Red color
}
ctx.beginPath(); //Start path
ctx.arc(x, y, pointSize, 0, Math.PI * 2, true); // Draw a point using the arc function of the canvas with a point structure.
ctx.fill(); // Close the path and fill.
}
function drawText(txt, x1, y1, x2, y2) {
// (x,y) coordinate of text mid way between both points
x = ((x2 + x1) / 2) + 5;
y = ((y2 + y1) / 2) + 5;
var ctx = document.getElementById("canvas").getContext("2d");
ctx.save();
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
// ctx.font = font;
angle = Math.atan((Math.abs(y2 - y1)) / (Math.abs(x2 - x1)));
console.log(angle);
ctx.translate(x, y)
// ctx.rotate(-1 * angle);
ctx.fillText(txt, 0, 0);
ctx.restore();
}
function download_image() {
// Dump the canvas contents to a file.
var canvas = document.getElementById("canvas");
var today = new Date();
var date = today.getFullYear() + "" + (today.getMonth() + 1) + "" + "" + today.getDate() + "" + (today.getHours() - 2) + "" + today.getMinutes() + "" + today.getSeconds();
today.getDate();
canvas.toBlob(function(blob) {
saveAs(blob, date + "Canvas.png");
}, "image/png");
};
</script>
</head>
<style media="screen">
upload_form {
background-color: red;
width: 100%;
padding: 20px;
}
</style>
<body>
<div class="container">
<div class="row">
<div class="col-md-12 upload_form">
<label>Image File:</label><br/>
<input type="file" id="imageLoader" name="imageLoader" />
<button type="button" onclick="download_image()" name="button">Save Canvas</button>
</div>
</div>
<div class=row>
<div class="col-md-12">
<canvas id="canvas"></canvas>
</div>
</div>
</div>
</body>
</html>
Any help would be greatly appreciated,
Thanks
I'm not sure why it works with http-server, but you should probably be using window.addEventListener instead of window.addEvent

How to shift pixel value to the next mousemove position in canvas?

I am creating a smudging tool with HTML5 canvas. Now I have to shift the pixel color at the point of mouse pointer to the next position where mouse pointer moves. Is it possible to do with javascript?
<canvas id="canvas"><canvas>
var canvas = document.getElementById("canvas");
var context = canvas.getContext('2d');
var url = 'download.jpg';
var imgObj = new Image();
imgObj.src = url;
imgObj.onload = function(e) {
context.drawImage(imgObj, 0, 0);
}
function findPos(obj) {
var curleft = 0,
curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return {
x: curleft,
y: curtop
};
}
return undefined;
}
function rgbToHex(r, g, b) {
if (r > 255 || g > 255 || b > 255)
throw "Invalid color component";
return ((r << 16) | (g << 8) | b).toString(16);
}
$('#canvas').mousemove(function(e) {
var pos = findPos(this);
var x = e.pageX - pos.x;
var y = e.pageY - pos.y;
console.log(x, y);
var c = this.getContext('2d');
var p = c.getImageData(x, y, 1, 1).data;
var hex = "#" + ("000000" + rgbToHex(p[0], p[1], p[2])).slice(-6);
console.log(hex)
});
I am very short on time ATM so code only.
Uses an offscreen canvas brush to get a copy of the background canvas background where the mouse was last frame. Then use a radial gradient to feather the brush using ctx.globalCompositeOperation = "destination-in". Then draw the updated brush at the next mouse position.
The main canvas is use just to display, the canvas being smeared is called background You can put whatever content you want on that canvas (eg image) and it can be any size, and you can zoom, pan, rotate the background though you will have to convert the mouse coordinates to match the background coordinates
Click drag mouse to smear colours.
const ctx = canvas.getContext("2d");
const background = createCanvas(canvas.width,canvas.height);
const brushSize = 64;
const bs = brushSize;
const bsh = bs / 2;
const smudgeAmount = 0.25; // values from 0 none to 1 full
// helpers
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
// simple mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
// brush gradient for feather
const grad = ctx.createRadialGradient(bsh,bsh,0,bsh,bsh,bsh);
grad.addColorStop(0,"black");
grad.addColorStop(1,"rgba(0,0,0,0)");
const brush = createCanvas(brushSize)
// creates an offscreen canvas
function createCanvas(w,h = w){
var c = document.createElement("canvas");
c.width = w;
c.height = h;
c.ctx = c.getContext("2d");
return c;
}
// get the brush from source ctx at x,y
function brushFrom(ctx,x,y){
brush.ctx.globalCompositeOperation = "source-over";
brush.ctx.globalAlpha = 1;
brush.ctx.drawImage(ctx.canvas,-(x - bsh),-(y - bsh));
brush.ctx.globalCompositeOperation = "destination-in";
brush.ctx.globalAlpha = 1;
brush.ctx.fillStyle = grad;
brush.ctx.fillRect(0,0,bs,bs);
}
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime;
var lastX;
var lastY;
// update background is size changed
function createBackground(){
background.width = w;
background.height = h;
background.ctx.fillStyle = "white";
background.ctx.fillRect(0,0,w,h);
doFor(64,()=>{
background.ctx.fillStyle = `rgb(${randI(255)},${randI(255)},${randI(255)}`;
background.ctx.fillRect(randI(w),randI(h),randI(10,100),randI(10,100));
});
}
// main update function
function update(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
createBackground();
}else{
ctx.clearRect(0,0,w,h);
}
ctx.drawImage(background,0,0);
// if mouse down then do the smudge for all pixels between last mouse and mouse now
if(mouse.button){
brush.ctx.globalAlpha = smudgeAmount;
var dx = mouse.x - lastX;
var dy = mouse.y - lastY;
var dist = Math.sqrt(dx*dx+dy*dy);
for(var i = 0;i < dist; i += 1){
var ni = i / dist;
brushFrom(background.ctx,lastX + dx * ni,lastY + dy * ni);
ni = (i+1) / dist;
background.ctx.drawImage(brush,lastX + dx * ni - bsh,lastY + dy * ni - bsh);
}
}else{
brush.ctx.clearRect(0,0,bs,bs); /// clear brush if not used
}
lastX = mouse.x;
lastY = mouse.y;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>

How to zoom canvas with animation

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.

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

Categories