Pixelate Image Data Algorithm very slow - javascript

I had this pixelate algorithm in my tools, but when I came to apply it today to my drawing app, it's performance is seriously bad. I was wondering if you could help me with this.
This is the algo I had:
//apply pixalate algorithm
for(var x = 1; x < w; x += aOptions.blockSize)
{
for(var y = 1; y < h; y += aOptions.blockSize)
{
var pixel = sctx.getImageData(x, y, 1, 1);
dctx.fillStyle = "rgb("+pixel.data[0]+","+pixel.data[1]+","+pixel.data[2]+")";
dctx.fillRect(x, y, x + aOptions.blockSize - 1, y + aOptions.blockSize - 1);
}
}
I was wondering if you could help me speed it up, I'm not sure whats causing this perf hit.
(yes i know the imageSmoothingEnabled techqnique however its not giving me perfect control over the block size)

Fast Hardware Pixilation
GPU can do it..
Why not just use the GPU to do it with the drawImage call, its much quicker. Draw the source canvas smaller at the number of pixels you want in the pixilation and then draw it to the source scaled back up. You just have to make sure that you turn off image smoothing before hand or the pixelation effect will not work.
No transparent pixels
For a canvas that has no transparent pixels it is a simplier solution the following function will do that. To pixelate the canvas where sctx is the source and dctx is the destination and pixelCount is the number of pixel blocks in the destination x axis. And you get filter option as a bonus because the GPU is doing the hard works for you.
Please Note that you should check vendor prefix for 2D context imageSmoothingEnabled and add them for the browsers you intend to support.
// pixelate where
// sctx is source context
// dctx is destination context
// pixelCount is the number of pixel blocks across in the destination
// filter boolean if true then pixel blocks as bilinear interpolation of all pixels involved
// if false then pixel blocks are nearest neighbour of pixel in center of block
function pixelate(sctx, dctx, pixelCount, filter){
var sw = sctx.canvas.width;
var sh = sctx.canvas.height;
var dw = dctx.canvas.width;
var dh = dctx.canvas.height;
var downScale = pixelCount / sw; // get the scale reduction
var pixH = Math.floor(sh * downScale); // get pixel y axis count
// clear destination
dctx.clearRect(0, 0, dw, dh);
// set the filter mode
dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = filter;
// scale image down;
dctx.drawImage(sctx.canvas, 0, 0, pixelCount, pixH);
// scale image back up
// IMPORTANT for this to work you must turn off smoothing
dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = false;
dctx.drawImage(dctx.canvas, 0, 0, pixelCount, pixH, 0, 0, dw, dh);
// restore smoothing assuming it was on.
dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = true;
//all done
}
Transparent pixels
If you have transparent pixels you will need a work canvas to hold the scaled down image. To do this add a workCanvas argument and return that same argument. It will create a canvas for you and resize it if needed but you should also keep a copy of it so you don't needlessly create a new one each time you pixilate
function pixelate(sctx, dctx, pixelCount, filter, workCanvas){
var sw = sctx.canvas.width;
var sh = sctx.canvas.height;
var dw = dctx.canvas.width;
var dh = dctx.canvas.height;
var downScale = pixelCount / sw; // get the scale reduction
var pixH = Math.floor(sh * downScale); // get pixel y axis count
// create a work canvas if needed
if(workCanvas === undefined){
workCanvas = document.createElement("canvas");
workCanvas.width = pixelCount ;
workCanvas.height = pixH;
workCanvas.ctx = workCanvas.getContext("2d");
}else // resize if needed
if(workCanvas.width !== pixelCount || workCanvas.height !== pixH){
workCanvas.width = pixelCount ;
workCanvas.height = pixH;
workCanvas.ctx = workCanvas.getContext("2d");
}
// clear the workcanvas
workCanvas.ctx.clearRect(0, 0, pixelCount, pixH);
// set the filter mode Note the prefix, and I have been told same goes for IE
workCanvas.ctx.mozImageSmoothingEnabled = workCanvas.ctx.imageSmoothingEnabled = filter;
// scale image down;
workCanvas.ctx.drawImage(sctx.canvas, 0, 0, pixelCount, pixH);
// clear the destination
dctx.clearRect(0,0,dw,dh);
// scale image back up
// IMPORTANT for this to work you must turn off smoothing
dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = false;
dctx.drawImage(workCanvas, 0, 0, pixelCount, pixH, 0, 0, dw, dh);
// restore smoothing assuming it was on.
dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = true;
//all done
return workCanvas; // Return the canvas reference so there is no need to recreate next time
}
To use the transparent version you need to hold the workCanvas referance or you will need to create a new one each time.
var pixelateWC; // create and leave undefined in the app global scope
// (does not have to be JS context global) just of your app
// so you dont loss it between renders.
Then in your main loop
pixelateWC = pixelate(sourceCtx,destContext,20,true, pixelateWC);
Thus the function will create it the first time, and then will use it over and over untill it needs to change its size or you delete it with
pixelateWC = undefined;
The demo
I have included a demo (as my original version had a bug) to make sure it all works. Shows the transparent version of the functio. Works well fullscreen 60fp I do the whole canvas not just the split part. Instructions in demo.
// adapted from QuickRunJS environment.
// simple mouse
var mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var mouse = {
x : 0, y : 0, buttonRaw : 0,
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
mouseEvents : "mousemove,mousedown,mouseup".split(",")
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];}
e.preventDefault();
}
mouse.start = function(element, blockContextMenu){
if(mouse.element !== undefined){ mouse.removeMouse();}
mouse.element = element;
mouse.mouseEvents.forEach(n => { element.addEventListener(n, mouseMove); } );
if(blockContextMenu === true){
element.addEventListener("contextmenu", preventDefault, false);
mouse.contextMenuBlocked = true;
}
}
mouse.remove = function(){
if(mouse.element !== undefined){
mouse.mouseEvents.forEach(n => { mouse.element.removeEventListener(n, mouseMove); } );
if(mouse.contextMenuBlocked === true){ mouse.element.removeEventListener("contextmenu", preventDefault);}
mouse.contextMenuBlocked = undefined;
mouse.element = undefined;
}
}
return mouse;
})();
// delete needed for my QuickRunJS environment
function removeCanvas(){
if(canvas !== undefined){
document.body.removeChild(canvas);
}
canvas = undefined;
canvasB = undefined;
canvasP = undefined;
}
// create onscreen, background, and pixelate canvas
function createCanvas(){
canvas = document.createElement("canvas");
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
canvas.style.zIndex = 1000;
document.body.appendChild(canvas);
canvasP = document.createElement("canvas");
canvasB = document.createElement("canvas");
}
function resizeCanvas(){
if(canvas === undefined){ createCanvas(); }
canvasB.width = canvasP.width = canvas.width = window.innerWidth;
canvasB.height = canvasP.height = canvas.height = window.innerHeight;
canvasB.ctx = canvasB.getContext("2d");
canvasP.ctx = canvasP.getContext("2d");
canvas.ctx = canvas.getContext("2d");
// lazy coder bug fix
joinPos = Math.floor(window.innerWidth/2);
}
function pixelate(sctx, dctx, pixelCount, filter, workCanvas){
var sw = sctx.canvas.width;
var sh = sctx.canvas.height;
var dw = dctx.canvas.width;
var dh = dctx.canvas.height;
var downScale = pixelCount / sw; // get the scale reduction
var pixH = Math.floor(sh * downScale); // get pixel y axis count
// create a work canvas if needed
if(workCanvas === undefined){
workCanvas = document.createElement("canvas");
workCanvas.width = pixelCount;
workCanvas.height = pixH;
workCanvas.ctx = workCanvas.getContext("2d");
}else // resize if needed
if(workCanvas.width !== pixelCount || workCanvas.height !== pixH){
workCanvas.width = pixelCount;
workCanvas.height = pixH;
workCanvas.ctx = workCanvas.getContext("2d");
}
// clear the workcanvas
workCanvas.ctx.clearRect(0, 0, pixelCount, pixH);
// set the filter mode
workCanvas.ctx.mozImageSmoothingEnabled = workCanvas.ctx.imageSmoothingEnabled = filter;
// scale image down;
workCanvas.ctx.drawImage(sctx.canvas, 0, 0, pixelCount, pixH);
// clear the destination
dctx.clearRect(0,0,dw,dh);
// scale image back up
// IMPORTANT for this to work you must turn off smoothing
dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = false;
dctx.drawImage(workCanvas, 0, 0, pixelCount, pixH, 0, 0, dw, dh);
// restore smoothing assuming it was on.
dctx.mozImageSmoothingEnabled = dctx.imageSmoothingEnabled = true;
//all done
return workCanvas; // Return the canvas reference so there is no need to recreate next time
}
var canvas,canvaP, canvasB;
// create and size canvas
resizeCanvas();
// start mouse listening to canvas
mouse.start(canvas,true); // flag that context needs to be blocked
// listen to resize
window.addEventListener("resize",resizeCanvas);
// get some colours
const NUM_COLOURS = 10;
var currentCol = 0;
var cols = (function(count){
for(var i = 0, a = []; i < count; i ++){
a.push("hsl("+Math.floor((i/count)*360)+",100%,50%)");
}
return a;
})(NUM_COLOURS);
var holdExit = 0; // To stop in QuickRunJS environment
var workCanvas;
var joinPos = Math.floor(canvas.width / 2);
var mouseOverJoin = false;
var dragJoin = false;
var drawing = false;
var filterChange = 0;
var filter = true;
ctx = canvas.ctx;
function update(time){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(canvasB, 0, 0, joinPos, canvas.height, 0 , 0, joinPos, canvas.height); // draw background
ctx.drawImage(canvasP,
joinPos, 0,
canvas.width - joinPos, canvas.height,
joinPos, 0,
canvas.width - joinPos, canvas.height
); // pixilation background
if(dragJoin){
if(!(mouse.buttonRaw & 1)){
dragJoin = false;
canvas.style.cursor = "default";
if(filterChange < 20){
filter = !filter;
}
}else{
joinPos = mouse.x;
}
filterChange += 1;
mouseOverJoin = true;
}else{
if(Math.abs(mouse.x - joinPos) < 4 && ! drawing){
if(mouse.buttonRaw & 1){
canvas.style.cursor = "none";
dragJoin = true;
filterChange = 0;
}else{
canvas.style.cursor = "e-resize";
}
mouseOverJoin = true;
}else{
canvas.style.cursor = "default";
mouseOverJoin = false;
if(mouse.buttonRaw & 1){
canvasB.ctx.fillStyle = cols[currentCol % NUM_COLOURS];
canvasB.ctx.beginPath();
canvasB.ctx.arc(mouse.x, mouse.y, 20, 0, Math.PI * 2);
canvasB.ctx.fill();
drawing = true
}else{
drawing = false;
}
}
ctx.fillStyle = cols[currentCol % NUM_COLOURS];
ctx.beginPath();
ctx.arc(mouse.x, mouse.y, 20, 0, Math.PI * 2);
ctx.fill();
}
if(mouse.buttonRaw & 4){ // setp cols on right button
currentCol += 1;
}
if(mouse.buttonRaw & 2){ // setp cols on right button
canvasB.ctx.clearRect(0, 0, canvas.width, canvas.height);
holdExit += 1;
}else{
holdExit = 0;
}
workCanvas = pixelate(canvasB.ctx, canvasP.ctx, 30, filter, workCanvas);
ctx.strokeStyle = "rgba(0,0,0,0.5)";
if(mouseOverJoin){
ctx.fillStyle = "rgba(0,255,0,0.5)";
ctx.fillRect(joinPos - 3, 0, 6, canvas.height);
ctx.fillRect(joinPos - 2, 0, 4, canvas.height);
ctx.fillRect(joinPos - 1, 0, 2, canvas.height);
}
ctx.strokeRect(joinPos - 3, 0, 6, canvas.height);
ctx.font = "18px arial";
ctx.textAlign = "left";
ctx.fillStyle = "black"
ctx.fillText("Normal canvas.", 10, 20)
ctx.textAlign = "right";
ctx.fillText("Pixelated canvas.", canvas.width - 10, 20);
ctx.textAlign = "center";
ctx.fillText("Click drag to move join.", joinPos, canvas.height - 62);
ctx.fillText("Click to change Filter : " + (filter ? "Bilinear" : "Nearest"), joinPos, canvas.height - 40);
ctx.fillText("Click drag to draw.", canvas.width / 2, 20);
ctx.fillText("Right click change colour.", canvas.width / 2, 42);
ctx.fillText("Middle click to clear.", canvas.width / 2, 64);
if(holdExit < 60){
requestAnimationFrame(update);
}else{
removeCanvas();
}
}
requestAnimationFrame(update);

You fetch an ImageData object for each pixel, then construct a colour string which has to be parsed again by the canvas object and then use a canvas fill routine, which has to go through all the canvas settings (transformation matrix, composition etc.)
You also seem to paint rectangles that are bigger than they need to be: The third and fourth parameters to fillRect are the width and height, not the x and y coordinated of the lower right point. An why do you start at pixel 1, not at zero?
It is usually much faster to operate on raw data for pixel manipulations. Fetch the whole image as image data, manipulate it and finally put it on the destination canvas:
var idata = sctx.getImageData(0, 0, w, h);
var data = idata.data;
var wmax = ((w / blockSize) | 0) * blockSize;
var wrest = w - wmax;
var hmax = ((h / blockSize) | 0) * blockSize;
var hrest = h - hmax;
var hh = blockSize;
for (var y = 0; y < h; y += blockSize) {
var ww = blockSize;
if (y == hmax) hh = hrest;
for (var x = 0; x < w; x += blockSize) {
var n = 4 * (w * y + x);
var r = data[n];
var g = data[n + 1];
var b = data[n + 2];
var a = data[n + 3];
if (x == wmax) ww = wrest;
for (var j = 0; j < hh; j++) {
var m = n + 4 * (w * j);
for (var i = 0; i < ww; i++) {
data[m++] = r;
data[m++] = g;
data[m++] = b;
data[m++] = a;
}
}
}
}
dctx.putImageData(idata, 0, 0);
In my browser, that's faster even with the two additonal inner loops.

Related

How do I draw a box around multiple shapes in html5 canvas

I am trying to draw a box around multiple shapes in canvas to say that those shapes are related like a group.
Tried as below :
var ctx = c.getContext("2d"),
radius = 10,
rect = c.getBoundingClientRect(),
ctx.fillText("Draw something here..", 10, 10);
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(250, 300, radius, 0, 6.28);
ctx.fill();
ctx.fillStyle = "yellow";
ctx.beginPath();
ctx.arc(200, 100, radius, 0, 10.28);
ctx.fill();
ctx.fillStyle = "brown";
ctx.beginPath();
ctx.arc(350, 210, radius, 0, 10.28);
ctx.fill();
var x = (250+200+350)/3;
var y = (300+100+210)/3;
var radius = Math.sqrt((x1*x1)+(y1*y1));
var _minX = x - radius;
var _minY = y - radius;
var _maxX = x + radius;
var _maxY = y + radius;
ctx.rect(_minX,_minY,(_maxX-_minX+2),(_maxY-_minY+2));
ctx.stroke();
But it is not drawing properly.
How to get bounding box coordinates for canvas content? this link explains only for the path not for the existing shapes.
Below is the image how I want to draw:
Fabric <-- See if this library helps had used this for one of my project it is simple and quick.
This Code is not production ready, Or the best solution, but it works in "most cases".
I'm using the imageData, to check for non-white pixel. (If NOT 0 - RGBA Pixel ==> Object) and with this it narrows the possible Rectangle down. You would also need to tweak it, if you don't want the text to be in the Rectangle.
This code could / should be optimized.
EDIT: Now I am only checking if an Alpha Value is set. And some Random Object creation to test multiple Outcomes
Info: Objects that are clipped/cut happen, because they are out of the canvas size.
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
var colors = ["red", "blue", "green", "black"];
ctx.fillText("Draw something here..", 0, 10);
/** CREATEING SOME RANDOM OBJECTS (JUST FOR TEST) **/
createRandomObjects();
function createRandomIntMax(max){
return parseInt(Math.random() * 1000 * max) % max + 1;
}
function createRandomObjects(){
var objectsToDraw = createRandomIntMax(20);
for(var idx = 0; idx < objectsToDraw; idx++){
ctx.fillStyle = colors[createRandomIntMax(colors.length)];
ctx.beginPath();
ctx.arc(createRandomIntMax(c.width), createRandomIntMax(c.height), createRandomIntMax(30), 0, 2 * Math.PI);
ctx.fill();
}
}
/** GETTING IMAGE DATA **/
var myImageData = ctx.getImageData(0, 0, c.width, c.height);
/** FINDING BORDERS **/
findBorders(myImageData.data);
function findBorders(imageData) {
var result = {
left: c.width,
top: c.height,
right: -1,
bottom: -1
}
var idx = 0;
var lineLow = -1;
var lineHigh = -1;
var currentLine = 0;
var currentPoint, helper;
while (idx < imageData.length) {
currentPoint = imageData[idx + 3];
/** CHECKING FOR OBJECT **/
if (currentPoint != 0) {
helper = parseInt(idx % (c.width * 4)) / 4;
if (lineLow < 0) {
lineLow = helper;
lineHigh = helper;
} else {
lineHigh = helper;
}
}
if (idx !== 0 && (idx % (c.width * 4)) === 0) {
currentLine = idx / (c.width * 4);
// Updating the Border Points
if (lineLow > -1) {
result.left = Math.min(lineLow, result.left);
result.top = Math.min(currentLine, result.top);
result.right = Math.max(lineHigh, result.right);
result.bottom = Math.max(currentLine, result.bottom);
}
lineLow = -1;
lineHigh = -1;
}
idx += 4;
}
ctx.rect(result.left, result.top, result.right - result.left, result.bottom - result.top);
ctx.stroke()
}
<canvas id="canvas"></canvas>
USE getBoundingClientRect() to get the exact boundaries

How to perform per pixel collision test for transparent images? [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 6 years ago.
Improve this question
If I have two partially transparent images (GIF, PNG, SVG etc.), how do I check if the non-transparent areas of the images intersect?
I'm fine with using canvas if it's necessary. The solution needs to work with all image formats that support transparency. No jQuery please.
Touching
Not Touching
Fast GPU assisted Pixel / Pixel collisions using 2D API.
By using the 2D context globalCompositeOperation you can greatly increase the speed of pixel pixel overlap test.
destination-in
The comp operation "destination-in" will only leave pixels that are visible on the canvas and the image you draw on top of it. Thus you create a canvas, draw one image, then set the comp operation to "destination-in" then draw the second image. If any pixels are overlapping then they will have a non zero alpha. All you do then is read the pixels and if any of them are not zero you know there is an overlap.
More speed
Testing all the pixels in the overlapping area will be slow. You can get the GPU to do some math for you and scale the composite image down. There is some loss as pixels are only 8bit values. This can be overcome by reducing the image in steps and rendering the results several times. Each reduction is like calculating a mean. I scale down by 8 effectively getting the mean of 64 pixels. To stop pixels at the bottom of the range disappearing due to rounding I draw the image several times. I do it 32 time which has the effect of multiplying the alpha channel by 32.
Extending
This method can easily be modified to allow both images to be scaled, skewed and rotated without any major performance hit. You can also use it to test many images with it returning true if all images have pixels overlapping.
Pixels are small so you can get extra speed if you reduce the image size before creating the test canvas in the function. This can give a significant performance boost.
There is a flag reuseCanvas that allows you to reuse the working canvases. If you use the test function a lot (many times a second) then set the flag to true. If you only need the test every now and then then set it to false.
Limits
This method is good for large images that need occasional tests; it is not good for small images and many tests per frame (such as in games where you may need to test 100's of images). For fast (almost perfect pixel) collision tests see Radial Perimeter Test.
The test as a function.
// Use the options to set quality of result
// Slow but perfect
var slowButPerfect = false;
// if reuseCanvas is true then the canvases are resused saving some time
const reuseCanvas = true;
// hold canvas references.
var pixCanvas;
var pixCanvas1;
// returns true if any pixels are overlapping
// img1,img2 the two images to test
// x,y location of img1
// x1,y1 location of img2
function isPixelOverlap(img1,x,y,img2,x1,y1){
var ax,aw,ay,ah,ctx,canvas,ctx1,canvas1,i,w,w1,h,h1;
w = img1.width;
h = img1.height;
w1 = img2.width;
h1 = img2.height;
// function to check if any pixels are visible
function checkPixels(context,w,h){
var imageData = new Uint32Array(context.getImageData(0,0,w,h).data.buffer);
var i = 0;
// if any pixel is not zero then there must be an overlap
while(i < imageData.length){
if(imageData[i++] !== 0){
return true;
}
}
return false;
}
// check if they overlap
if(x > x1 + w1 || x + w < x1 || y > y1 + h1 || y + h < y1){
return false; // no overlap
}
// size of overlapping area
// find left edge
ax = x < x1 ? x1 : x;
// find right edge calculate width
aw = x + w < x1 + w1 ? (x + w) - ax : (x1 + w1) - ax
// do the same for top and bottom
ay = y < y1 ? y1 : y;
ah = y + h < y1 + h1 ? (y + h) - ay : (y1 + h1) - ay
// Create a canvas to do the masking on
if(!reuseCanvas || pixCanvas === undefined){
pixCanvas = document.createElement("canvas");
}
pixCanvas.width = aw;
pixCanvas.height = ah;
ctx = pixCanvas.getContext("2d");
// draw the first image relative to the overlap area
ctx.drawImage(img1,x - ax, y - ay);
// set the composite operation to destination-in
ctx.globalCompositeOperation = "destination-in"; // this means only pixels
// will remain if both images
// are not transparent
ctx.drawImage(img2,x1 - ax, y1 - ay);
ctx.globalCompositeOperation = "source-over";
// are we using slow method???
if(slowButPerfect){
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // no then release referance
}
return checkPixels(ctx,aw,ah);
}
// now draw over its self to amplify any pixels that have low alpha
for(var i = 0; i < 32; i++){
ctx.drawImage(pixCanvas,0,0);
}
// create a second canvas 1/8th the size but not smaller than 1 by 1
if(!reuseCanvas || pixCanvas1 === undefined){
pixCanvas1 = document.createElement("canvas");
}
ctx1 = pixCanvas1.getContext("2d");
// reduced size rw, rh
rw = pixCanvas1.width = Math.max(1,Math.floor(aw/8));
rh = pixCanvas1.height = Math.max(1,Math.floor(ah/8));
// repeat the following untill the canvas is just 64 pixels
while(rw > 8 && rh > 8){
// draw the mask image several times
for(i = 0; i < 32; i++){
ctx1.drawImage(
pixCanvas,
0,0,aw,ah,
Math.random(),
Math.random(),
rw,rh
);
}
// clear original
ctx.clearRect(0,0,aw,ah);
// set the new size
aw = rw;
ah = rh;
// draw the small copy onto original
ctx.drawImage(pixCanvas1,0,0);
// clear reduction canvas
ctx1.clearRect(0,0,pixCanvas1.width,pixCanvas1.height);
// get next size down
rw = Math.max(1,Math.floor(rw / 8));
rh = Math.max(1,Math.floor(rh / 8));
}
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // release ref
pixCanvas1 = undefined;
}
// check for overlap
return checkPixels(ctx,aw,ah);
}
The demo (Use full page)
The demo lets you compare the two methods. The mean time for each test is displayed. (will display NaN if no tests done)
For the best results view the demo full page.
Use left or right mouse buttons to test for overlap. Move the splat image over the other to see overlap result. On my machine I am getting about 11ms for the slow test and 0.03ms for the quick test (using Chrome, much faster on Firefox).
I have not spent much time testing how fast I can get it to work but there is plenty of room to increase the speed by reducing the number of time the images are drawn over each other. At some point faint pixels will be lost.
// Use the options to set quality of result
// Slow but perfect
var slowButPerfect = false;
const reuseCanvas = true;
var pixCanvas;
var pixCanvas1;
// returns true if any pixels are overlapping
function isPixelOverlap(img1,x,y,w,h,img2,x1,y1,w1,h1){
var ax,aw,ay,ah,ctx,canvas,ctx1,canvas1,i;
// function to check if any pixels are visible
function checkPixels(context,w,h){
var imageData = new Uint32Array(context.getImageData(0,0,w,h).data.buffer);
var i = 0;
// if any pixel is not zero then there must be an overlap
while(i < imageData.length){
if(imageData[i++] !== 0){
return true;
}
}
return false;
}
// check if they overlap
if(x > x1 + w1 || x + w < x1 || y > y1 + h1 || y + h < y1){
return false; // no overlap
}
// size of overlapping area
// find left edge
ax = x < x1 ? x1 : x;
// find right edge calculate width
aw = x + w < x1 + w1 ? (x + w) - ax : (x1 + w1) - ax
// do the same for top and bottom
ay = y < y1 ? y1 : y;
ah = y + h < y1 + h1 ? (y + h) - ay : (y1 + h1) - ay
// Create a canvas to do the masking on
if(!reuseCanvas || pixCanvas === undefined){
pixCanvas = document.createElement("canvas");
}
pixCanvas.width = aw;
pixCanvas.height = ah;
ctx = pixCanvas.getContext("2d");
// draw the first image relative to the overlap area
ctx.drawImage(img1,x - ax, y - ay);
// set the composite operation to destination-in
ctx.globalCompositeOperation = "destination-in"; // this means only pixels
// will remain if both images
// are not transparent
ctx.drawImage(img2,x1 - ax, y1 - ay);
ctx.globalCompositeOperation = "source-over";
// are we using slow method???
if(slowButPerfect){
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // no then release reference
}
return checkPixels(ctx,aw,ah);
}
// now draw over its self to amplify any pixels that have low alpha
for(var i = 0; i < 32; i++){
ctx.drawImage(pixCanvas,0,0);
}
// create a second canvas 1/8th the size but not smaller than 1 by 1
if(!reuseCanvas || pixCanvas1 === undefined){
pixCanvas1 = document.createElement("canvas");
}
ctx1 = pixCanvas1.getContext("2d");
// reduced size rw, rh
rw = pixCanvas1.width = Math.max(1,Math.floor(aw/8));
rh = pixCanvas1.height = Math.max(1,Math.floor(ah/8));
// repeat the following untill the canvas is just 64 pixels
while(rw > 8 && rh > 8){
// draw the mask image several times
for(i = 0; i < 32; i++){
ctx1.drawImage(
pixCanvas,
0,0,aw,ah,
Math.random(),
Math.random(),
rw,rh
);
}
// clear original
ctx.clearRect(0,0,aw,ah);
// set the new size
aw = rw;
ah = rh;
// draw the small copy onto original
ctx.drawImage(pixCanvas1,0,0);
// clear reduction canvas
ctx1.clearRect(0,0,pixCanvas1.width,pixCanvas1.height);
// get next size down
rw = Math.max(1,Math.floor(rw / 8));
rh = Math.max(1,Math.floor(rh / 8));
}
if(!reuseCanvas){ // are we keeping the canvas
pixCanvas = undefined; // release ref
pixCanvas1 = undefined;
}
// check for overlap
return checkPixels(ctx,aw,ah);
}
function rand(min,max){
if(max === undefined){
max = min;
min = 0;
}
var r = Math.random() + Math.random() + Math.random() + Math.random() + Math.random();
r += Math.random() + Math.random() + Math.random() + Math.random() + Math.random();
r /= 10;
return (max-min) * r + min;
}
function createImage(w,h){
var c = document.createElement("canvas");
c.width = w;
c.height = h;
c.ctx = c.getContext("2d");
return c;
}
function createCSSColor(h,s,l,a) {
var col = "hsla(";
col += (Math.floor(h)%360) + ",";
col += Math.floor(s) + "%,";
col += Math.floor(l) + "%,";
col += a + ")";
return col;
}
function createSplat(w,h,hue, hue2){
w = Math.floor(w);
h = Math.floor(h);
var c = createImage(w,h);
if(hue2 !== undefined) {
c.highlight = createImage(w,h);
}
var maxSize = Math.min(w,h)/6;
var pow = 5;
while(maxSize > 4 && pow > 0){
var count = Math.min(100,Math.pow(w * h,1/pow) / 2);
while(count-- > 0){
const rhue = rand(360);
const s = rand(25,75);
const l = rand(25,75);
const a = (Math.random()*0.8+0.2).toFixed(3);
const size = rand(4,maxSize);
const x = rand(size,w - size);
const y = rand(size,h - size);
c.ctx.fillStyle = createCSSColor(rhue + hue, s, l, a);
c.ctx.beginPath();
c.ctx.arc(x,y,size,0,Math.PI * 2);
c.ctx.fill();
if (hue2 !== undefined) {
c.highlight.ctx.fillStyle = createCSSColor(rhue + hue2, s, l, a);
c.highlight.ctx.beginPath();
c.highlight.ctx.arc(x,y,size,0,Math.PI * 2);
c.highlight.ctx.fill();
}
}
pow -= 1;
maxSize /= 2;
}
return c;
}
var splat1,splat2;
var slowTime = 0;
var slowCount = 0;
var notSlowTime = 0;
var notSlowCount = 0;
var onResize = function(){
ctx.font = "14px arial";
ctx.textAlign = "center";
splat1 = createSplat(rand(w/2, w), rand(h/2, h), 0, 100);
splat2 = createSplat(rand(w/2, w), rand(h/2, h), 100);
}
function display(){
ctx.clearRect(0,0,w,h)
ctx.setTransform(1.8,0,0,1.8,w/2,0);
ctx.fillText("Fast GPU assisted Pixel collision test using 2D API",0, 14)
ctx.setTransform(1,0,0,1,0,0);
ctx.fillText("Hold left mouse for Traditional collision test. Time : " + (slowTime / slowCount).toFixed(3) + "ms",w /2 , 28 + 14)
ctx.fillText("Hold right (or CTRL left) mouse for GPU assisted collision. Time: "+ (notSlowTime / notSlowCount).toFixed(3) + "ms",w /2 , 28 + 28)
if((mouse.buttonRaw & 0b101) === 0) {
ctx.drawImage(splat1, w / 2 - splat1.width / 2, h / 2 - splat1.height / 2)
ctx.drawImage(splat2, mouse.x - splat2.width / 2, mouse.y - splat2.height / 2);
} else if(mouse.buttonRaw & 0b101){
if((mouse.buttonRaw & 1) && !mouse.ctrl){
slowButPerfect = true;
}else{
slowButPerfect = false;
}
var now = performance.now();
var res = isPixelOverlap(
splat1,
w / 2 - splat1.width / 2, h / 2 - splat1.height / 2,
splat1.width, splat1.height,
splat2,
mouse.x - splat2.width / 2, mouse.y - splat2.height / 2,
splat2.width,splat2.height
)
var time = performance.now() - now;
ctx.drawImage(res ? splat1.highlight: splat1, w / 2 - splat1.width / 2, h / 2 - splat1.height / 2)
ctx.drawImage(splat2, mouse.x - splat2.width / 2, mouse.y - splat2.height / 2);
if(slowButPerfect){
slowTime += time;
slowCount += 1;
}else{
notSlowTime = time;
notSlowCount += 1;
}
if(res){
ctx.setTransform(2,0,0,2,mouse.x,mouse.y);
ctx.fillText("Overlap detected",0,0)
ctx.setTransform(1,0,0,1,0,0);
}
//mouse.buttonRaw = 0;
}
}
// Boilerplate code below
const RESIZE_DEBOUNCE_TIME = 100;
var w, h, cw, ch, canvas, ctx, mouse, createCanvas, resizeCanvas, setGlobals, globalTime = 0, resizeCount = 0;
var firstRun = true;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
}
e.preventDefault();
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element = undefined;
m.active = false;
}
}
return mouse;
})();
resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);
function update1(timer) { // Main update loop
if(ctx === undefined){
return;
}
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update1);
}
requestAnimationFrame(update1);

Drag and drop on a canvas HTML5

I am trying to implement a drag and drop on a canvas representing 3 disks.
I would like to change with mouse the position of each mass. My main problem is that I am constrained by the length of axe for each of these 3 spheres.
For the moment, I have implemented the following function when mouse is moving inside the canvas (value of indexMass indicates which mass is moved: 1, 2 or 3 and t1, t2, t3 represents respectively the angle of mass 1, 2, 3):
// Happens when the mouse is moving inside the canvas
function myMove(event) {
if (isDrag) {
var x = event.offsetX;
var y = event.offsetY;
if (indexMass == 1)
{ // Update theta1 value
t1 = t1 + 0.1*Math.atan(y/x);
}
else if (indexMass == 2)
{ // Update theta2 value
t2 = t2 + 0.1*Math.atan(y/x);
}
else if (indexMass == 3)
{ // Update theta3 value
t3 = t3 + 0.1*Math.atan(y/x);
}
// Update drawing
DrawPend(canvas);
}
}
As you can see, I did for each angle:
t = t + 0.1*Math.atan(y/x);
with:
var x = event.offsetX;
var y = event.offsetY;
But this effect is not very nice. Once the sphere is selected with mouse (on mouse click), I would like the cursor to be stuck with this sphere or the sphere to follow the "delta" of the mouse coordinates when I am not on sphere any more.
Update 1
#Blindman67: thanks for your help, your code snippet is pretty complex for me, I didn't understand it all. But I am on the right way.
I am starting by the first issue: make rotate the selected disk with mouse staying very closed to it or over it, when dragging.
For the moment, I have modified my function myMove (which is called when I have clicked down and move the mouse for dragging) like:
// Happens when the mouse is moving inside the canvas
function myMove(event) {
// If dragging
if (isDrag) {
// Compute dx and dy before calling DrawPend
var lastX = parseInt(event.offsetX - mx);
var lastY = parseInt(event.offsetY - my);
var dx = lastX - window['x'+indexMass];
var dy = lastY - window['y'+indexMass];
// Change angle when dragging
window['t'+indexMass] = Math.atan2(dy, dx);
// Update drawing
DrawPend(canvas);
// Highlight dragging disk
fillDisk(indexMass, 'pink');
}
}
where indexMass is the index of dragged disk and window['x'+indexMass] , window['y'+indexMass] are the current coordinates of the selected disk center.
After, I compute the dx, dy respectively from coordinates mouse clicked when starting drag (mx, my returned by getMousePos function) and mouse coordinates with moving.
Finally, I change the angle of disk by set, for global variable (theta of selected disk), i.e window['t'+indexMass]:
// Change angle when dragging
window['t'+indexMass] = Math.atan2(dy, dx);
I have took your part of code with Math.atan2.
But the result of this function doesn't make a good animation with mouse dragging, I would like to know where this could come from.
Right now, I would like to implement only the dragging without modifying the length of axis, I will see more later for this functionality.
Update 2
I keep going on to find a solution about the dragging of a selected mass with mouse.
For trying a synthesis of what I have done previously, I believe the following method is good but this dragging method is not working very well: the selected disk doesn't follow correctly the mouse and I don't know why.
In myMove function (function called when I start dragging), I decided to:
Compute the dx, dy between the mouse coordinates and the selected disk coordinates, i.e:
var dx = parseInt(event.offsetX - window['x'+indexMass]);
var dy = parseInt(event.offsetY - window['y'+indexMass]);
indexMass represents the index of the selected disk.
Increment the position of selected disk (stored in temporary variables tmpX, tmpY) by dx, dy.
Compute the new angle theta (identified in code by global variable window['t'+indexMass]
Compute the new positions of selected disk with this new value of theta, i.e for example with disk1 (indexMass=1 and theta = t1):
x1= x0 +l1 * sin(t1)
y1= y0 +l1 * sin(t1)
I want to draw readers' attention to the fact that I want dragging with mouse not to modify the lengths of axes with mouse, this is a constraint.
Here's the entire myMove function (called when drag is starting) :
// Happens when the mouse is moving inside the canvas
function myMove(event) {
// If dragging
if (isDrag) {
console.log('offsetX', event.offsetX);
console.log('offsetY', event.offsetY);
var dx = parseInt(event.offsetX - window['x'+indexMass]);
var dy = parseInt(event.offsetY - window['y'+indexMass]);
console.log('dx', dx);
console.log('dy', dy);
// Temp variables
var tmpX = window['x'+indexMass];
var tmpY = window['y'+indexMass];
// Increment temp positions
tmpX += dx;
tmpY += dy;
// Compute new angle for indexMass
window['t'+indexMass] = Math.atan2(tmpX, tmpY);
console.log('printf', window['t'+indexMass]);
// Compute new positions of disks
dragComputePositions();
// Update drawing
DrawPend(canvas);
// Highlight dragging disk
fillDisk(indexMass, 'pink');
}
}
You can not move the OS mouse position. You can hide the mouse canvas.style.cursor = "none"; and then draw a mouse on the canvas your self but it will lag behind by one frame because when you get the mouse coordinates the OS has already placed the mouse at that position, and if you use requestAnimationFrame (RAF) the next presentation of the canvas will be at the next display refresh interval. If you don't use RAF you may or may not present the canvas on the current display refresh, but you will get occasional flicker and shearing.
To solve the problem (which is subjective) draw a line from the rotation point through the ball to the mouse position this will at least give the user some feedback as to what is happening.
I would also add some handles to the balls so you could change the mass (volume of sphere * density) and the length of axis.. The resize cursors are a problem as the will not match the direction of required movement when the angles have changes. You would need to find one closest to the correct angle or render a cursor to a canvas and use that.
Example code shows what I mean. (does not include sim) Move mouse over balls to move, when over you will also see two circles appear to change distance and radius (mass)
/*-------------------------------------------------------------------------------------
answer code
---------------------------------------------------------------------------------------*/
var balls = [];
var startX,startY;
var mouseOverBallIndex = -1;
var mouseOverDist = false;
var mouseOverMass = false;
const DRAG_CURSOR = "move";
const MASS_CURSOR = "ew-resize";
const DIST_CURSOR = "ns-resize";
var dragging = false;
var dragStartX = 0;
var dragStartY = 0;
function addBall(dist,radius){
balls.push({
dist : dist,
radius : Math.max(10,radius),
angle : -Math.PI / 2,
x : 0,
y : 0,
mass : (4/3) * radius * radius * radius * Math.PI,
});
}
function drawBalls(){
var i = 0;
var len = balls.length;
var x,y,dist,b,minDist,index,cursor;
ctx.lineWidth = 2;
ctx.strokeStyle = "black";
ctx.fillStyle = "blue"
ctx.beginPath();
x = startX;
y = startY;
ctx.moveTo(x, y)
for(; i < len; i += 1){
b = balls[i];
x += Math.cos(b.angle) * b.dist;
y += Math.sin(b.angle) * b.dist;
ctx.lineTo(x, y);
b.x = x;
b.y = y;
}
ctx.stroke();
minDist = Infinity;
index = -1;
for(i = 0; i < len; i += 1){
b = balls[i];
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
if(!dragging){
x = b.x - mouse.x;
y = b.y - mouse.y;
dist = Math.sqrt(x * x + y * y);
if(dist < b.radius + 5 && dist < minDist){
minDist = dist;
index = i;
}
}
}
if(!dragging){
mouseOverBallIndex = index;
if(index !== -1){
cursor = DRAG_CURSOR;
b = balls[index];
ctx.fillStyle = "Red"
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
dx = b.x - Math.cos(b.angle) * b.radius;
dy = b.y - Math.sin(b.angle) * b.radius;
x = dx - mouse.x;
y = dy - mouse.y;
dist = Math.sqrt(x * x + y * y);
ctx.beginPath();
if(dist < 6){
ctx.strokeStyle = "Yellow"
mouseOverDist = true;
ctx.arc(dx, dy, 12, 0, Math.PI * 2);
cursor = DIST_CURSOR;
}else{
ctx.strokeStyle = "black"
mouseOverDist = false;
ctx.arc(dx, dy, 5, 0, Math.PI * 2);
}
ctx.stroke();MASS_CURSOR
dx = b.x - Math.cos(b.angle + Math.PI/2) * b.radius;
dy = b.y - Math.sin(b.angle + Math.PI/2) * b.radius;
x = dx - mouse.x;
y = dy - mouse.y;
dist = Math.sqrt(x * x + y * y);
ctx.beginPath();
if(dist < 6){
ctx.strokeStyle = "Yellow"
mouseOverMass = true;
ctx.arc(dx, dy, 12, 0, Math.PI * 2);
cursor = MASS_CURSOR;
}else{
ctx.strokeStyle = "black"
mouseOverMass = false;
ctx.arc(dx, dy, 5, 0, Math.PI * 2);
}
ctx.stroke();
canvas.style.cursor = cursor;
}else{
canvas.style.cursor = "default";
}
}else{
b = balls[mouseOverBallIndex];
ctx.fillStyle = "Yellow"
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
}
}
function display(){ // put code in here
var x,y,b
if(balls.length === 0){
startX = canvas.width/2;
startY = canvas.height/2;
addBall((startY * 0.8) * (1/4), startY * 0.04);
addBall((startY * 0.8) * (1/3), startY * 0.04);
addBall((startY * 0.8) * (1/2), startY * 0.04);
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
if((mouse.buttonRaw & 1) && mouseOverBallIndex > -1){
b = balls[mouseOverBallIndex];
if(dragging === false){
dragging = true;
dragStartX = balls[mouseOverBallIndex].x;
dragStartY = balls[mouseOverBallIndex].y;
}else{
b = balls[mouseOverBallIndex];
if(mouseOverBallIndex === 0){
x = startX;
y = startY;
}else{
x = balls[mouseOverBallIndex-1].x
y = balls[mouseOverBallIndex-1].y
}
if(mouseOverDist){
var dist = Math.sqrt(Math.pow(x-mouse.x,2)+Math.pow(y-mouse.y,2));
b.dist = dist + b.radius;
}else
if(mouseOverMass){
var dist = Math.sqrt(Math.pow(dragStartX-mouse.x,2)+Math.pow(dragStartY-mouse.y,2));
b.radius = Math.max(10,dist);
b.mass = dist * dist * dist * (4/3) * Math.PI;
}else{
b.angle = Math.atan2(mouse.y - y, mouse.x - x);
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "grey";
ctx.moveTo(x,y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
}
}
}else if(dragging){
dragging = false;
}
drawBalls();
}
/*-------------------------------------------------------------------------------------
answer code END
---------------------------------------------------------------------------------------*/
/** SimpleFullCanvasMouse.js begin **/
const CANVAS_ELEMENT_ID = "canv";
const U = undefined;
var w, h, cw, ch; // short cut vars
var canvas, ctx, mouse;
var globalTime = 0;
var createCanvas, resizeCanvas, setGlobals;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
c.id = CANVAS_ELEMENT_ID;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === U) { canvas = createCanvas(); }
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals(); }
}
setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; balls.length = 0; }
mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === U) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }
else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
else if (t === "mouseover") { m.over = true; }
else if (t === "mousewheel") { m.w = e.wheelDelta; }
else if (t === "DOMMouseScroll") { m.w = -e.detail; }
if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === U) { m.callbacks = [callback]; }
else { m.callbacks.push(callback); }
} else { throw new TypeError("mouse.addCallback argument must be a function"); }
}
m.start = function (element, blockContextMenu) {
if (m.element !== U) { m.removeMouse(); }
m.element = element === U ? document : element;
m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
}
m.remove = function () {
if (m.element !== U) {
m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
m.element = m.callbacks = m.contextMenuBlocked = U;
}
}
return mouse;
})();
var done = function(){
window.removeEventListener("resize",resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = U;
L("All done!")
}
resizeCanvas(); // create and size canvas
mouse.start(canvas,true); // start mouse on canvas and block context menu
window.addEventListener("resize",resizeCanvas); // add resize event
function update(timer){ // Main update loop
globalTime = timer;
display(); // call demo code
// continue until mouse right down
if (!(mouse.buttonRaw & 2)) { requestAnimationFrame(update); } else { done(); }
}
requestAnimationFrame(update);
/** SimpleFullCanvasMouse.js end **/
(Posted a solution from the question author to move it to the answer space).
Problem solved! I forgot to take into account the position of "indexMass-1" disk to compute the new angle with Math.atan2 function.

Resizing Image Without Affecting Look of Original Canvas?

I have a canvas where a user draws. After tapping a button, I do a few things in a second canvas such as trimming away the white space and re-centering the drawing (so as to not affect the original canvas).
I also create a third canvas so I can resize the output to a certain size. My problem is that I don't want the original canvas where users draw to be affected. Right now everything works and my image is resized, but so is the original canvas. How to I leave the original canvas unaffected?
Here's my function:
//Get Canvas
c = document.getElementById('simple_sketch');
//Define Context
var ctx = c.getContext('2d');
//Create Copy of Canvas
var copyOfContext = document.createElement('canvas').getContext('2d');
//Get Pixels
var pixels = ctx.getImageData(0, 0, c.width, c.height);
//Get Length of Pixels
var lengthOfPixels = pixels.data.length;
//Define Placeholder Variables
var i;
var x;
var y;
var bound = {
top: null,
left: null,
right: null,
bottom: null
};
//Loop Through Pixels
for (i = 0; i < lengthOfPixels; i += 4) {
if (pixels.data[i+3] !== 0) {
x = (i / 4) % c.width;
y = ~~((i / 4) / c.width);
if (bound.top === null) {
bound.top = y;
}
if (bound.left === null) {
bound.left = x;
} else if (x < bound.left) {
bound.left = x;
}
if (bound.right === null) {
bound.right = x;
} else if (bound.right < x) {
bound.right = x;
}
if (bound.bottom === null) {
bound.bottom = y;
} else if (bound.bottom < y) {
bound.bottom = y;
}
}
}
//Calculate Trimmed Dimensions
var padding = 1;
var trimmedHeight = bound.bottom + padding - bound.top;
var trimmedWidth = bound.right + padding - bound.left;
//Get Longest Dimension (We Need a Square Image That Fits the Drawing)
var longestDimension = Math.max(trimmedHeight, trimmedWidth);
//Define Rect
var trimmedRect = ctx.getImageData(bound.left, bound.top, trimmedWidth, trimmedHeight);
//Define New Canvas Parameters
copyOfContext.canvas.width = longestDimension;
copyOfContext.canvas.height = longestDimension;
copyOfContext.putImageData(trimmedRect, (longestDimension - trimmedWidth)/2, (longestDimension - trimmedHeight)/2);
copyOfContext.globalCompositeOperation = "source-out";
copyOfContext.fillStyle = "#fff";
copyOfContext.fillRect(0, 0, longestDimension, longestDimension);
//Define Resized Context
var resizedContext = c.getContext('2d');
resizedContext.canvas.width = 32;
resizedContext.canvas.height = 32;
resizedContext.drawImage(copyOfContext.canvas, 0, 0, 32, 32);
//Get Cropped Image URL
var croppedImageURL = resizedContext.canvas.toDataURL("image/jpeg");
//Open Image in New Window
window.open(croppedImageURL, '_blank');
How to make a "spare" copy of an html5 canvas:
var theCopy=copyCanvas(originalCanvas);
function copyCanvas(originalCanvas){
var c=originalCanvas.cloneNode();
c.getContext('2d').drawImage(originalCanvas,0,0);
return(c);
}
Make a spare copy of the original canvas you don't want affected. Then after you've altered the original, but want the original contents back ...
// optionally clear the canvas before restoring the original content
originalCanvasContext.drawImage(theCopy,0,0);

How to create a camera view in canvas that will follow a players rotation and rotation?

I'm trying to create a game in canvas with javascript where you control a spaceship and have it so that the canvas will translate and rotate to make it appear like the spaceship is staying stationary and not rotating.
Any help would be greatly appreciated.
window.addEventListener("load",eventWindowLoaded, false);
function eventWindowLoaded() {
canvasApp();
}
function canvasSupport() {
return Modernizr.canvas;
}
function canvasApp() {
if (!canvasSupport()) {
return;
}
var theCanvas = document.getElementById("myCanvas");
var height = theCanvas.height; //get the heigth of the canvas
var width = theCanvas.width; //get the width of the canvas
var context = theCanvas.getContext("2d"); //get the context
var then = Date.now();
var bgImage = new Image();
var stars = new Array;
bgImage.onload = function() {
context.translate(width/2,height/2);
main();
}
var rocket = {
xLoc: 0,
yLoc: 0,
score : 0,
damage : 0,
speed : 20,
angle : 0,
rotSpeed : 1,
rotChange: 0,
pointX: 0,
pointY: 0,
setScore : function(newScore){
this.score = newScore;
}
}
function Star(){
var dLoc = 100;
this.xLoc = rocket.pointX+ dLoc - Math.random()*2*dLoc;
this.yLoc = rocket.pointY + dLoc - Math.random()*2*dLoc;
//console.log(rocket.xLoc+" "+rocket.yLoc);
this.draw = function(){
drawStar(this.xLoc,this.yLoc,20,5,.5);
}
}
//var stars = new Array;
var drawStars = function(){
context.fillStyle = "yellow";
if (typeof stars !== 'undefined'){
//console.log("working");
for(var i=0;i< stars.length ;i++){
stars[i].draw();
}
}
}
var getDistance = function(x1,y1,x2,y2){
var distance = Math.sqrt(Math.pow((x2-x1),2)+Math.pow((y2-y1),2));
return distance;
}
var updateStars = function(){
var numStars = 10;
while(stars.length<numStars){
stars[stars.length] = new Star();
}
for(var i=0; i<stars.length; i++){
var tempDist = getDistance(rocket.pointX,rocket.pointY,stars[i].xLoc,stars[i].yLoc);
if(i == 0){
//console.log(tempDist);
}
if(tempDist > 100){
stars[i] = new Star();
}
}
}
function drawRocket(xLoc,yLoc, rWidth, rHeight){
var angle = rocket.angle;
var xVals = [xLoc,xLoc+(rWidth/2),xLoc+(rWidth/2),xLoc-(rWidth/2),xLoc-(rWidth/2),xLoc];
var yVals = [yLoc,yLoc+(rHeight/3),yLoc+rHeight,yLoc+rHeight,yLoc+(rHeight/3),yLoc];
for(var i = 0; i < xVals.length; i++){
xVals[i] -= xLoc;
yVals[i] -= yLoc+rHeight;
if(i == 0){
console.log(yVals[i]);
}
var tempXVal = xVals[i]*Math.cos(angle) - yVals[i]*Math.sin(angle);
var tempYVal = xVals[i]*Math.sin(angle) + yVals[i]*Math.cos(angle);
xVals[i] = tempXVal + xLoc;
yVals[i] = tempYVal+(yLoc+rHeight);
}
rocket.pointX = xVals[0];
rocket.pointY = yVals[0];
//rocket.yLoc = yVals[0];
//next rotate
context.beginPath();
context.moveTo(xVals[0],yVals[0])
for(var i = 1; i < xVals.length; i++){
context.lineTo(xVals[i],yVals[i]);
}
context.closePath();
context.lineWidth = 5;
context.strokeStyle = 'blue';
context.stroke();
}
var world = {
//pixels per second
startTime: Date.now(),
speed: 50,
startX:width/2,
startY:height/2,
originX: 0,
originY: 0,
xDist: 0,
yDist: 0,
rotationSpeed: 20,
angle: 0,
distance: 0,
calcOrigins : function(){
world.originX = -world.distance*Math.sin(world.angle*Math.PI/180);
world.originY = -world.distance*Math.cos(world.angle*Math.PI/180);
}
};
var keysDown = {};
addEventListener("keydown", function (e) {
keysDown[e.keyCode] = true;
}, false);
addEventListener("keyup", function (e) {
delete keysDown[e.keyCode];
}, false);
var update = function(modifier) {
if (37 in keysDown) { // Player holding left
rocket.angle -= rocket.rotSpeed* modifier;
rocket.rotChange = - rocket.rotSpeed* modifier;
//console.log("left");
}
if (39 in keysDown) { // Player holding right
rocket.angle += rocket.rotSpeed* modifier;
rocket.rotChange = rocket.rotSpeed* modifier;
//console.log("right");
}
};
var render = function (modifier) {
context.clearRect(-width*10,-height*10,width*20,height*20);
var dX = (rocket.speed*modifier)*Math.sin(rocket.angle);
var dY = (rocket.speed*modifier)*Math.cos(rocket.angle);
rocket.xLoc += dX;
rocket.yLoc -= dY;
updateStars();
drawStars();
context.translate(-dX,dY);
context.save();
context.translate(-rocket.pointX,-rocket.pointY);
context.translate(rocket.pointX,rocket.pointY);
drawRocket(rocket.xLoc,rocket.yLoc,50,200);
context.fillStyle = "red";
context.fillRect(rocket.pointX,rocket.pointY,15,5);
//context.restore(); // restores the coordinate system back to (0,0)
context.fillStyle = "green";
context.fillRect(0,0,10,10);
context.rotate(rocket.angle);
context.restore();
};
function drawStar(x, y, r, p, m)
{
context.save();
context.beginPath();
context.translate(x, y);
context.moveTo(0,0-r);
for (var i = 0; i < p; i++)
{
context.rotate(Math.PI / p);
context.lineTo(0, 0 - (r*m));
context.rotate(Math.PI / p);
context.lineTo(0, 0 - r);
}
context.fill();
context.restore();
}
// the game loop
function main(){
requestAnimationFrame(main);
var now = Date.now();
var delta = now - then;
update(delta / 1000);
//now = Date.now();
//delta = now - then;
render(delta / 1000);
then = now;
// Request to do this again ASAP
}
var w = window;
var requestAnimationFrame = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.msRequestAnimationFrame || w.mozRequestAnimationFrame;
//start the game loop
//gameLoop();
//event listenters
bgImage.src = "images/background.jpg";
} //canvasApp()
Origin
When you need to rotate something in canvas it will always rotate around origin, or center for the grid if you like where the x and y axis crosses.
You may find my answer here useful as well
By default the origin is in the top left corner at (0, 0) in the bitmap.
So in order to rotate content around a (x,y) point the origin must first be translated to that point, then rotated and finally (and usually) translated back. Now things can be drawn in the normal order and they will all be drawn rotated relative to that rotation point:
ctx.translate(rotateCenterX, rotateCenterY);
ctx.rotate(angleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
Absolute angles and positions
Sometimes it's easier to keep track if an absolute angle is used rather than using an angle that you accumulate over time.
translate(), transform(), rotate() etc. are accumulative methods; they add to the previous transform. We can set absolute transforms using setTransform() (the last two arguments are for translation):
ctx.setTransform(1, 0, 0, 1, rotateCenterX, rotateCenterY); // absolute
ctx.rotate(absoluteAngleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
The rotateCenterX/Y will represent the position of the ship which is drawn untransformed. Also here absolute transforms can be a better choice as you can do the rotation using absolute angles, draw background, reset transformations and then draw in the ship at rotateCenterX/Y:
ctx.setTransform(1, 0, 0, 1, rotateCenterX, rotateCenterY);
ctx.rotate(absoluteAngleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
// update scene/background etc.
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transforms
ctx.drawImage(ship, rotateCenterX, rotateCenterY);
(Depending on orders of things you could replace the first line here with just translate() as the transforms are reset later, see demo for example).
This allows you to move the ship around without worrying about current transforms, when a rotation is needed use the ship's current position as center for translation and rotation.
And a final note: the angle you would use for rotation would of course be the counter-angle that should be represented (ie. ctx.rotate(-angle);).
Space demo ("random" movements and rotations)
The red "meteors" are dropping in one direction (from top), but as the ship "navigates" around they will change direction relative to our top view angle. Camera will be fixed on the ship's position.
(ignore the messy part - it's just for the demo setup, and I hate scrollbars... focus on the center part :) )
var img = new Image();
img.onload = function() {
var ctx = document.querySelector("canvas").getContext("2d"),
w = 600, h = 400, meteors = [], count = 35, i = 0, x = w * 0.5, y, a = 0, a2 = 0;
ctx.canvas.width = w; ctx.canvas.height = h; ctx.fillStyle = "#555";
while(i++ < count) meteors.push(new Meteor());
(function loop() {
ctx.clearRect(0, 0, w, h);
y = h * 0.5 + 30 + Math.sin((a+=0.01) % Math.PI*2) * 60; // ship's y and origin's y
// translate to center of ship, rotate, translate back, render bg, reset, draw ship
ctx.translate(x, y); // translate to origin
ctx.rotate(Math.sin((a2+=0.005) % Math.PI) - Math.PI*0.25); // rotate some angle
ctx.translate(-x, -y); // translate back
ctx.beginPath(); // render some moving meteors for the demo
for(var i = 0; i < count; i++) meteors[i].update(ctx); ctx.fill();
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transforms
ctx.drawImage(img, x - 32, y); // draw ship as normal
requestAnimationFrame(loop); // loop animation
})();
};
function Meteor() { // just some moving object..
var size = 5 + 35 * Math.random(), x = Math.random() * 600, y = -200;
this.update = function(ctx) {
ctx.moveTo(x + size, y); ctx.arc(x, y, size, 0, 6.28);
y += size * 0.5; if (y > 600) y = -200;
};
}
img.src = "http://i.imgur.com/67KQykW.png?1";
body {background:#333} canvas {background:#000}
<canvas></canvas>

Categories

Resources