Check if image A exists in image B - html

I need to check if an image exists in another image using JavaScript, I need to know what are the best approaches (algorithm) and solutions (ex: librarie) to do this operations
I explained what I need to do in this image:

Using the GPU to help in image processing.
Using the 2D API and some simple tricks you can exploit the GPUs power to speed up Javascript.
Difference
To find an image you need to compare the pixels you are looking for (A) against the pixels in the image (B). If the difference between the Math.abs(A-B) === 0 then the pixels are the same.
A function to do this may look like the following
function findDif(imageDataSource, imageDataDest, xx,yy)
const ds = imageDataSource.data;
const dd = imageDataDest.data;
const w = imageDataSource.width;
const h = imageDataSource.height;
var x,y;
var dif = 0;
for(y = 0; y < h; y += 1){
for(x = 0; x < w; x += 1){
var indexS = (x + y * w) * 4;
var indexD = (x + xx + (y + yy) * imageDataDest.width) * 4;
dif += Math.abs(ds[indexS]-dd[indexD]);
dif += Math.abs(ds[indexS + 1]-dd[indexD + 1]);
dif += Math.abs(ds[indexS + 2]-dd[indexD + 2]);
}
}
return dif;
}
var source = sourceCanvas.getContext("2d").getImageData(0,0,sourceCanvas.width,sourceCanvas.height);
var dest = destinationCanvas.getContext("2d").getImageData(0,0,destinationCanvas.width,destinationCanvas.height);
if(findDif(source,dest,100,100)){ // is the image at 100,100?
// Yes image is very similar
}
Where the source is the image we are looking for and the dest is the image we want to find it in. We run the function for every location that the image may be and if the result is under a level then its a good chance we have found it.
But this is very very slow in JS. This is where the GPU can help.
Using the ctx.globalCompositeOperation = "difference"; operation we can speed up the process as it will do the difference calculation for us
When you render with the comp operation "difference" the resulting pixels are the difference between the pixels you are drawing and those that are already on the canvas. Thus if you draw on something that is the same the result is all pixels are black (no difference)
To find a similar image in the image you render the image you are testing for at each location on the canvas that you want to test for. Then you get the sum of all the pixels you just rendered on, if the result is under a threshold that you have set then the image under that area is very similar to the image you are testing for.
But we still need to count all the pixels one by one.
A GPU mean function
The comp op "difference" already does the pixel difference calculation for you, but to get the sum you can use the inbuilt image smoothing.
After you have rendered to find the difference you take that area and render it at a smaller scale with ctx.imageSmoothingEnabled = true the default setting. The GPU will do something similar to an average and can reduce the amount of work JS has to do by several orders of magnitude.
Now instead of 100s or 1000s of pixels you can reduce it down to as little at 4 or 16 depending on the accuracy you need.
An example.
Using these methods you can get a near realtime image in image search with just the basic numerical analysis.
Click to start a test. Results are shown plus the time it took. The image that is being searched for is in the top right.
//------------------------------------------------------------------------
// Some helper functions
var imageTools = (function () {
var tools = {
canvas(width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage : function (width, height) {
var i = this.canvas(width, height);
i.ctx = i.getContext("2d");
return i;
},
image2Canvas(img) {
var i = this.canvas(img.width, img.height);
i.ctx = i.getContext("2d");
i.ctx.drawImage(img, 0, 0);
return i;
},
copyImage(img){ // just a named stub
return this.image2Canvas(img);
},
};
return tools;
})();
const U = undefined;
const doFor = (count, callback) => {var i = 0; while (i < count && callback(i ++) !== true ); };
const setOf = (count, callback) => {var a = [],i = 0; while (i < count) { a.push(callback(i ++)) } return a };
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
const randA = (array) => array[(Math.random() * array.length) | 0];
const randG = (min, max = min + (min = 0)) => Math.random() * Math.random() * Math.random() * Math.random() * (max - min) + min;
// end of helper functions
//------------------------------------------------------------------------
function doit(){
document.body.innerHTML = ""; // clear the page;
var canvas = document.createElement("canvas");
document.body.appendChild(canvas);
var ctx = canvas.getContext("2d");
// a grid of 36 images
canvas.width = 6 * 64;
canvas.height = 6 * 64;
console.log("test");
// get a random character to look for
const digit = String.fromCharCode("A".charCodeAt(0) + randI(26));
// get some characters we dont want
const randomDigits = setOf(6,i=>{
return String.fromCharCode("A".charCodeAt(0) + randI(26));
})
randomDigits.push(digit); // add the image we are looking for
var w = canvas.width;
var h = canvas.height;
// create a canvas for the image we are looking for
const imageToFind = imageTools.createImage(64,64);
// and a larger one to cover pixels on the sides
const imageToFindExtend = imageTools.createImage(128,128);
// Draw the character onto the image with a white background and scaled to fit
imageToFindExtend.ctx.fillStyle = imageToFind.ctx.fillStyle = "White";
imageToFind.ctx.fillRect(0,0,64,64);
imageToFindExtend.ctx.fillRect(0,0,128,128);
ctx.font = imageToFind.ctx.font = "64px arial black";
ctx.textAlign = imageToFind.ctx.textAlign = "center";
ctx.textBaseline = imageToFind.ctx.textBaseline = "middle";
const digWidth = imageToFind.ctx.measureText(digit).width+8;
const scale = Math.min(1,64/digWidth);
imageToFind.ctx.fillStyle = "black";
imageToFind.ctx.setTransform(scale,0,0,scale,32,32);
imageToFind.ctx.fillText(digit,0,0);
imageToFind.ctx.setTransform(1,0,0,1,0,0);
imageToFindExtend.ctx.drawImage(imageToFind,32,32);
imageToFind.extendedImage = imageToFindExtend;
// Now fill the canvas with images of other characters
ctx.fillStyle = "white";
ctx.setTransform(1,0,0,1,0,0);
ctx.fillRect(0,0,w,h);
ctx.fillStyle = "black";
ctx.strokeStyle = "white";
ctx.lineJoin = "round";
ctx.lineWidth = 12;
// some characters will be rotated 90,180,-90 deg
const dirs = [
[1,0,0,1,0,0],
[0,1,-1,0,1,0],
[-1,0,0,-1,1,1],
[0,-1,1,0,0,1],
]
// draw random characters at random directions
doFor(h / 64, y => {
doFor(w / 64, x => {
const dir = randA(dirs)
ctx.setTransform(dir[0] * scale,dir[1] * scale,dir[2] * scale,dir[3] * scale,x * 64 + 32, y * 64 + 32);
const d = randA(randomDigits);
ctx.strokeText(d,0,0);
ctx.fillText(d,0,0);
});
});
ctx.setTransform(1,0,0,1,0,0);
// get a copy of the canvas
const saveCan = imageTools.copyImage(ctx.canvas);
// function that finds the images
// image is the image to find
// dir is the matrix direction to find
// smapleSize is the mean sampling size samller numbers are quicker
function checkFor(image,dir,sampleSize){
const can = imageTools.copyImage(saveCan);
const c = can.ctx;
const stepx = 64;
const stepy = 64;
// the image that will contain the reduced means of the differences
const results = imageTools.createImage(Math.ceil(w / stepx) * sampleSize,Math.ceil(h / stepy) * sampleSize);
const e = image.extendedImage;
// for each potencial image location
// set a clip area and draw the source image on it with
// comp mode "difference";
for(var y = 0 ; y < h; y += stepy ){
for(var x = 0 ; x < w; x += stepx ){
c.save();
c.beginPath();
c.rect(x,y,stepx,stepy);
c.clip();
c.globalCompositeOperation = "difference";
c.setTransform(dir[0],dir[1],dir[2],dir[3],x +32 ,y +32 );
c.drawImage(e,-64,-64);
c.restore();
}
}
// Apply the mean (reducing nnumber of pixels to check
results.ctx.drawImage(can,0,0,results.width,results.height);
// get the pixel data
var dat = new Uint32Array(results.ctx.getImageData(0,0,results.width,results.height).data.buffer);
// for each area get the sum of the difference
for(var y = 0; y < results.height; y += sampleSize){
for(var x = 0; x < results.width; x += sampleSize){
var val = 0;
for(var yy = 0; yy < sampleSize && y+yy < results.height; yy += 1){
var i = x + (y+yy)*results.width;
for(var xx = 0; xx < sampleSize && x + xx < results.width ; xx += 1){
val += dat[i++] & 0xFF;
}
}
// if the sum is under the threshold we have found an image
// and we mark it
if(val < sampleSize * sampleSize * 5){
ctx.strokeStyle = "red";
ctx.fillStyle = "rgba(255,0,0,0.5)";
ctx.lineWidth = 2;
ctx.strokeRect(x * (64/sampleSize),y * (64/sampleSize),64,64);
ctx.fillRect(x * (64/sampleSize),y * (64/sampleSize),64,64);
foundCount += 1;
}
}
}
}
var foundCount = 0;
// find the images at different orientations
var now = performance.now();
checkFor(imageToFind,dirs[0],4);
checkFor(imageToFind,dirs[1],6); // rotated images need larger sample size
checkFor(imageToFind,dirs[2],6);
checkFor(imageToFind,dirs[3],6);
var time = performance.now() - now;
var result = document.createElement("div");
result.textContent = "Found "+foundCount +" matching images in "+time.toFixed(3)+"ms. Click to try again.";
document.body.appendChild(result);
// show the image we are looking for
imageToFind.style.left = (64*6 + 16) + "px";
imageToFind.id = "lookingFor";
document.body.appendChild(imageToFind);
}
document.addEventListener("click",doit);
canvas {
border : 2px solid black;
position : absolute;
top : 28px;
left : 2px;
}
#lookingFor {
border : 4px solid red;
}
div {
border : 2px solid black;
position : absolute;
top : 2px;
left : 2px;
}
Click to start test.
Not perfect
The example is not perfect and will sometimes make mistakes. There is a huge amount of room for improving both the accuracy and the speed. This is just something I threw together as an example to show how to use the GPU via the 2D API. Some further maths will be needed to find the statistically good results.
This method can also work for different scales, and rotations, you can even use some of the other comp modes to remove colour and normalize contrast. I have used a very similar approch to stabilize webcam by tracking points from one frame to the next, and a veriaty of other image tracking uses.

Related

shadowBlur (html canvas) not working in js loop

I was trying to improve my stars in the sky animation which I created using JS. This is when i discovered, I could use the shadowBlur property to change the size of the shadow created around my stars, to make it look like they are flickering. The issue now is that the shadowBlur goes up but doesn't go back to black. Here is the code I have used. Any help with this will be greatly appreciated :).
Have a great day!
// ---- Vars for star animation
let randomStars = [];
let starCollection = [];
let numberofStars = 50;
let flickeringStars = 50;
class Star{
constructor(x,y,color,radius,shadowBlur){
this._canvas = document.querySelector('canvas');
this._canvas.width = window.innerWidth;
this._canvas.height = window.innerHeight;
this._c = this._canvas.getContext('2d');
this._radius = radius;
this._x = x;
this._y = y;
this._color = color;
this._shadowBlur = 10;
this._shadowColor = 'white';
}
//drawing individual stars
draw(){
this._c.beginPath();
this._c.arc(this._x,this._y,this._radius,0,Math.PI * 2,false);
this._c.fillStyle = this._color;
this._c.strokeStyle = 'black';
this._c.shadowColor = this._shadowColor;
this._c.shadowBlur = this._shadowBlur;
this._c.stroke();
this._c.fill();
this._c.closePath();
}
//Fade in and out for stars
flicker(){
setTimeout(()=>{this._shadowBlur=10;},200);
setTimeout(()=>{this._shadowBlur=8;},400);
setTimeout(()=>{this._shadowBlur=6;},600);
setTimeout(()=>{this._shadowBlur=4;},800);
setTimeout(()=>{this._shadowBlur=2;},1000);
setTimeout(()=>{this._shadowBlur=0;},1200);
setTimeout(()=>{this._shadowBlur=2;},1400);
setTimeout(()=>{this._shadowBlur=4;},1600);
setTimeout(()=>{this._shadowBlur=6;},1800);
setTimeout(()=>{this._shadowBlur=8;},2000);
setTimeout(()=>{this._shadowBlur=10;},2200);
setTimeout(()=>{this.draw();},200);
setTimeout(()=>{this.draw();},400);
setTimeout(()=>{this.draw();},600);
setTimeout(()=>{this.draw();},800);
setTimeout(()=>{this.draw();},1000);
setTimeout(()=>{this.draw();},1200);
setTimeout(()=>{this.draw();},1400);
setTimeout(()=>{this.draw();},1600);
setTimeout(()=>{this.draw();},1800);
setTimeout(()=>{this.draw();},2000);
setTimeout(()=>{this.draw();},2200);
}
//Clears the canvas
clearstars(){
this._c.clearRect(0,0,window.innerWidth, window.innerHeight);
}
}
// ---- Functions ----
//Typing animation
const typingAnimation = ()=>{
if(textProgress < text.length){
setTimeout(()=>{requestAnimationFrame(typingAnimation)}, speed);
if(text.charAt(textProgress) === '\n')document.getElementById('animation-text').innerHTML += '</br>';
document.getElementById('animation-text').innerHTML += text.charAt(textProgress);
textProgress ++;
}else{
let event = new CustomEvent('showStars');
dispatchEvent(event);
}
}
//Store stars
const storeStars = ()=>{
starCollection = [];
for(let i=0;i<numberofStars;i++){
let x = Math.floor(Math.random()*window.innerWidth);
let y = Math.floor(Math.random()*window.innerHeight);
starCollection.push(new Star(x,y,"white",(Math.random()+1)-0.7));
}
}
//Show stars to the screen
const showStars = ()=>{
for(let i=0;i<starCollection.length;i++){
starCollection[i].draw();
}
}
//Store random stars
const generateRandomStars = ()=>{
randomStars = [];
for(let i=0;i<flickeringStars;i++){
let x = Math.floor(Math.random()*window.innerWidth);
let y = Math.floor(Math.random()*window.innerHeight);
randomStars.push(new Star(x,y,"white",(Math.random()+1)-0.7));
}
}
//Show randoms stars after clearing previous set of flickering stars
const showRandomStars = ()=>{
let id = window.setTimeout(function () { }, 0);
while (id--) {
window.clearTimeout(id);
}
let starHandler = new Star(0,0,"white",0);
starHandler.clearstars();
showStars();
flickerStars();
}
//Flickers stars and changes set of stars randomly
const flickerStars = ()=>{
for(let i=0;i<flickeringStars;i++){
setInterval(()=>{
randomStars[i].flicker();
},2200);
setInterval(()=>{
console.log("changing stars pattern");
generateRandomStars();
showRandomStars();
},12200);
}
}
// ---- Event Listeners ----
//Typing animation on load
window.addEventListener("load", ()=>{
storeStars();
generateRandomStars();
showStars();
flickerStars();
});
//Handles star animation scaling on window resize
window.addEventListener("resize", ()=>{
let id = window.setTimeout(function () { }, 0);
while (id--) {
window.clearTimeout(id);
}
let starHandler = new Star(0,0,"white",0);
starHandler.clearstars();
generateRandomStars();
storeStars();
showStars();
flickerStars();
});
body{
background-color:black;
}
<html>
<body><canvas></canvas></body>
</html>
I can not work out what FX you are trying to get. Below is a simple flickering star animation that uses small rectangles that change size to simulate flicker.
The frame rate is reduced so that the flicker is more pronounced.
It is very efficient and does not require complex state changes, or slow render methods (like blur)
var W = 0, H = 0; // hold canvas size. Set to zero so first frame sizes canvas
var count = 500; // number of stars
var frame = 0; // current frame number
const frameRate = 5; // render stars ever 5 frames
const sizeRange = 1.5; // max size of star us minSize + sizeRange + flickerSize
const minSize = 1; // minimum size of star
const flickerSize = 1; // amount of change random change to make a star brighter
const flickerRate = 0.1; // odds per rendered frame that a star flickers
const stars = [];
// Create a random set of numbers for star positions.
// Values must be larger than largest canvas you are going to use.
// This will work up to 8K display
while (count--) { stars.push(Math.random() * 1024 * 8) }
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
function mainLoop() {
var len = stars.length, i = 0, x = stars[i++], starSize;
// if the window has resized change the canvas to fit
if (W !== innerWidth || H !== innerHeight) {
W = canvas.width = innerWidth;
H = canvas.height = innerHeight;
ctx.fillStyle = "#FFF";
frame = 0;
}
if (frame++ % frameRate === 0) { // only render every frameRate frames
ctx.clearRect(0, 0, W, H);
ctx.beginPath(); // draw all stars with one path aas it is several orders
// of magnitude quicker than creating a path for each
while (i < len) {
// draws small stars to large
const starScale = (i / len); // set scale from 0 to 1
starSize = sizeRange; // set the range of sizes
if (Math.random() < flickerRate) { // on random odds of flicker
starSize += flickerSize * Math.random(); // add random flicker size
}
starSize *= starScale; // scale the star
starSize += minSize; // add min size of star
halfSize = starSize / 2; // offset to top left of star
const y = stars[i++]; // get next random number as star y pos
// add rect to path fitted to canvas width and height (W, H)
ctx.rect((x % W) - halfSize , (y % H) - halfSize , starSize , starSize );
x = y; // Use y for the next x star coordinate
}
ctx.fill(); // fill in all the stars
}
requestAnimationFrame(mainLoop);
}
canvas {
position: absolute;
top: 0px;
left: 0px;
background: #000;
}
<canvas id="canvas"></canvas>

Maximum size of cards inside a canvas

I am now working with a Web Application, which needs to draw some cards on a fixed rectangle canvas. Below is the criteria:
The canvas size is fixed with width "w" and height "h" when the Web Application starts.
There are "n" no. of cards which won't be changed after started.
All cards must in the same size, which has a fixed ratio with width "cw" and height "ch", the cards are able to re-size within the canvas.
I would like to calculate the maximum width and height of each card in such cases. Can anybody help?
Your question is lacking a lot of information. please read the comments in the code. I hope my answer may help you.
// initiate the canvas
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let cw = canvas.width = 300,
cx = cw / 2;
let ch = canvas.height = 300,
cy = ch / 2;
// number of cards
let n = 12;
// the horizontal and vertical ratio
let ratio = {x:.2,y:.3}
// the width and the height of a card
let w = cw*ratio.x;
let h = ch*ratio.y;
// a counter
let i = 0;
//a double for loop to draw the cards
for(let y = 0; y<ch; y+=h){
for(let x = 0; x<cw; x+=w){
if(i < n)
{drawCard(x,y);
i++}
}
}
function drawCard(x,y){
ctx.beginPath();
ctx.strokeRect(x,y,w,h);
}
canvas {
border:1px solid #d9d9d9;
}
<canvas id="canvas"></canvas>
Finally, I found the solution myself.
This performance may not is the optimal one, but the solution gives me the new card width, new card height, cards per rows and cards per column. So that the cards can almost cover the canvas.
function crwh(col, row, n, cw, ch, w, h, exactN) {
// Methods
this.fn_init = function(col, row) {
this.col = col;
this.row = row;
if (this.col > 0 && this.row > 0) {
// Calculate new card width & card height base on col
this.cw_new = (this.w / this.col);
this.ch_new = (this.cw_new / this.cw * this.ch);
if (this.fn_valid() == false) {
// Calculate new card height & card width base on row
this.ch_new = (this.h / this.row);
this.cw_new = (this.ch_new / this.ch * cw);
}
}
}
this.fn_area = function() {
// Get the size the rectangle
return this.cw_new * this.ch_new;
}
this.fn_valid = function() {
var valid = true;
// True if col * row must equal to no. of cards, False allow col * row greater than no. of cards
valid = valid && ((this.exactN == true && (this.col * this.row) == this.n) || (this.exactN == false && (this.col * this.row) >= this.n));
// col * card width (new) must be shorter than canvas width
valid = valid && ((this.col * this.cw_new) <= this.w);
// row * card height (new) must be shorter than canvas height
valid = valid && ((this.row * this.ch_new) <= this.h);
return valid;
}
// Properties
this.n = n;
this.cw = cw;
this.ch = ch;
this.w = w;
this.h = h;
this.exactN = exactN;
this.col = 0;
this.row = 0;
this.cw_new = 0;
this.ch_new = 0;
this.fn_init(col, row);
}
function fn_getCardDimensions(n, cw, ch, w, h, exactN) {
var crwh_max = new crwh(0, 0);
// Loop thru 1 to n for col & row to see which combination allow a maximum card size
for (var col = 1; col <= n; col++) {
for (var row = 1; row <= n; row++) {
if ((col * row) >= n) {
var crwh_cur = new crwh(col, row, n, cw, ch, w, h, exactN);
if (crwh_cur.fn_valid()) {
if (crwh_cur.fn_area() > crwh_max.fn_area()) {
crwh_max = crwh_cur;
}
}
}
}
}
return [crwh_max.col, crwh_max.row, crwh_max.cw_new, crwh_max.ch_new];
}
var n = 80; // No. of Cards
var cw = 344; // Card Width (orig)
var ch = 512; // Card Height (orig)
var w = 0; // Canvas Width
var h = 0; // Canvas Height
var exactN = true; // True if col * row must equal to no. of cards
function fn_drawCards() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
w = canvas.width;
h = canvas.height;
[col, row, cw_new, ch_new] = fn_getCardDimensions(n, cw, ch, w, h, exactN);
for (var i = 0; i < col; i++) {
for (var j = 0; j < row; j++) {
ctx.beginPath();
ctx.strokeRect(i * cw_new, j * ch_new, cw_new, ch_new);
}
}
}
fn_drawCards();
canvas {
border:1px solid #00FF00;
}
<canvas id="canvas"></canvas>

Centering on a canvas object within an HTML5 canvas

I have an Html5 canvas which i am drawing squares to.
The canvas itself is roughly the size of the window.
When i detect a click on a square i would like to translate the canvas so that the square is roughly in the center of the window. Any insights, hints, or straight-forward replies are welcome.
Here is what i tried so far:
If a square is at point (1000, 1000) I would simply translate the canvas (-1000, -1000). I know i need to add an offset so that it is centered in the window. However, the canvas always ends up off of the visible window (too far in the upper-left corner somewhere).
A more complex scenario:
Ultimately i would like to be able to center on a clicked object on a canvas that is transformed (rotated & skewed). I'm going for an isometric effect which seems to work really well. I'm wondering if this transformation affects the centering logic/math at all?
Transforming from screen to world and back
When working with non standard axis (or projections) such as isometrix it is always best to use a transformation matrix. It will cover every possible 2D projection with the same simple functions.
The coordinates of the iso world are called world coordinates. All you objects are stored as world coordinates. When you render them you project those coordinates to the screen coordinates using a transformation matrix.
The matrix, not a movie.
The matrix represents the direction and size in screen coordinates of the world
x and y axis and the screen location of the world origin (0,0)
For iso that is
x axis across 1 down 0.5
y axis across -1 down 0.5
z axis up 1 (-1 as up is the reverse of down) but this example does not use z
So the matrix as an array
const isoMat = [1,0.5,-1,0.5,0,0]; // ISO (pixel art) dimorphic projection
The first two are the x axis, the next two the y axis and the last two values are the screen coordinates of the origin.
Use the matrix to transform points
You apply a matrix to a point, this transforms the point from one coordinate system to another. You can also convert back via a inverse transform.
World to screen
You will need to convert from world coordinates to screen coordinates.
function worldToScreen(pos,retPos){
retPos.x = pos.x * isoMat[0] + pos.y * isoMat[2] + isoMat[4];
retPos.y = pos.x * isoMat[1] + pos.y * isoMat[3] + isoMat[5];
}
In the demo I ignore the origin as I set that at the center of the canvas at all times. Thus remove the origin from that function
function worldToScreen(pos,retPos){
retPos.x = pos.x * isoMat[0] + pos.y * isoMat[2];
retPos.y = pos.x * isoMat[1] + pos.y * isoMat[3];
}
Screen to world.
You will also need to convert from the screen coordinates to the world. For this you need to use the inverse transform. It's a bit like the inverse of multiply a * 2 = b is the inverse of b / 2 = a
There is a standard method for calculating the inverse matrix as follows
const invMatrix = []; // inverse matrix
// I call the next line cross, most call it the determinant which I
// think is stupid as it is effectively a cross product and is used
// like you would use a cross product. Anyways I digress
const cross = isoMat[0] * isoMat[3] - isoMat[1] * isoMat[2];
invMatrix[0] = isoMat[3] / cross;
invMatrix[1] = -isoMat[1] / cross;
invMatrix[2] = -isoMat[2] / cross;
invMatrix[3] = isoMat[0] / cross;
Then we have a function that converts from the screen x,y to the world position
function screenToWorld(pos,retPos){
const x = pos.x - isoMat[4];
const y = pos.y - isoMat[5];
retPos.x = x * invMatrix[0] + y * invMatrix[2];
retPos.y = x * invMatrix[1] + y * invMatrix[3];
}
So you get the mouse coords as screen pixels, use the above function to convert to world coords. Then you can use the world coords to find the object you are looking for.
To move a world object to the screen center you convert its coords to screen coords, add the position on the screen (the canvas center) and set the transform matrix origin to that location.
The demo
The demo creates a set of boxes in world coordinates. It sets the 2D context transform to the isoMat (isometric projection) via ctx.setTransform(
Every frame I convert the mouse screen coords to world coords then use that to check which box the mouse is over.
If the mouse button is down I then convert that box from world coords to screen and add the screen center. To smooth the step the new screen center is chased (smoothed)..
Well you should be able to work it out in the code, any problems ask in the comments.
const ctx = canvas.getContext("2d");
const moveSpeed = 0.4;
const boxMin = 20;
const boxMax = 50;
const boxCount = 100;
const boxArea = 2000;
// some canvas vals
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime;
const U = undefined;
// Helper function
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
const setOf = (count, cb) => {var a = [],i = 0; while (i < count) { a.push(cb(i ++)) } return a };
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
// mouse function and object
const mouse = {x : 0, y : 0, button : false, world : {x : 0, y : 0}}
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));
// boxes in world coordinates.
const boxes = [];
function draw(){
if(this.dead){
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(this.x,this.y,this.w,this.h);
}
ctx.strokeStyle = this.col;
ctx.globalAlpha = 1;
ctx.strokeRect(this.x,this.y,this.w,this.h);
// the rest is just overkill
if(this.col === "red"){
this.mr = 10;
}else{
this.mr = 1;
}
this.mc += (this.mr-this.m) * 0.45;
this.mc *= 0.05;
this.m += this.mc;
for(var i = 0; i < this.m; i ++){
const m = this.m * (i + 1);
ctx.globalAlpha = 1-(m / 100);
ctx.strokeRect(this.x-m,this.y-m,this.w,this.h);
}
}
// make random boxes.
function createBoxes(){
boxes.length = 0;
boxes.push(...setOf(boxCount,()=>{
return {
x : randI(cw- boxArea/ 2, cw + boxArea/2),
y : randI(ch- boxArea/ 2, ch + boxArea/2),
w : randI(boxMin,boxMax),
h : randI(boxMin,boxMax),
m : 5,
mc : 0,
mr : 5,
col : "black",
dead : false,
draw : draw,
isOver : isOver,
}
}));
}
// use mouse world coordinates to find box under mouse
function isOver(x,y){
return x > this.x && x < this.x + this.w && y > this.y && y < this.y + this.h;
}
var overBox;
function findBox(x,y){
if(overBox){
overBox.col = "black";
}
overBox = undefined;
eachOf(boxes,box=>{
if(box.isOver(x,y)){
overBox = box;
box.col = "red";
return true;
}
})
}
function drawBoxes(){
boxes.forEach(box=>box.draw());
}
// next 3 values control the movement of the origin
// rather than move instantly the currentPos chases the new pos.
const currentPos = {x :0, y : 0};
const newPos = {x :0, y : 0};
const chasePos = {x :0, y : 0};
// this function does the chasing
function updatePos(){
chasePos.x += (newPos.x - currentPos.x) * moveSpeed;
chasePos.y += (newPos.y - currentPos.y) * moveSpeed;
chasePos.x *= moveSpeed;
chasePos.y *= moveSpeed;
currentPos.x += chasePos.x;
currentPos.y += chasePos.y;
}
// ISO matrix and inverse matrix plus 2world and 2 screen
const isoMat = [1,0.5,-1,0.5,0,0];
const invMatrix = [];
const cross = isoMat[0] * isoMat[3] - isoMat[1] * isoMat[2];
invMatrix[0] = isoMat[3] / cross;
invMatrix[1] = -isoMat[1] / cross;
invMatrix[2] = -isoMat[2] / cross;
invMatrix[3] = isoMat[0] / cross;
function screenToWorld(pos,retPos){
const x = pos.x - isoMat[4];
const y = pos.y - isoMat[5];
retPos.x = x * invMatrix[0] + y * invMatrix[2];
retPos.y = x * invMatrix[1] + y * invMatrix[3];
}
function worldToScreen(pos,retPos){
retPos.x = pos.x * isoMat[0] + pos.y * isoMat[2];// + isoMat[4];
retPos.y = pos.x * isoMat[1] + pos.y * isoMat[3];// + isoMat[5];
}
// main update function
function update(timer){
// standard frame setup
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;
createBoxes();
}else{
ctx.clearRect(0,0,w,h);
}
ctx.fillStyle = "black";
ctx.font = "28px arial";
ctx.textAlign = "center";
ctx.fillText("Click on a box to center it.",cw,28);
// update position
updatePos();
isoMat[4] = currentPos.x;
isoMat[5] = currentPos.y;
// set the screen transform to the iso matrix
// all drawing can now be done in world coordinates.
ctx.setTransform(isoMat[0], isoMat[1], isoMat[2], isoMat[3], isoMat[4], isoMat[5]);
// convert the mouse to world coordinates
screenToWorld(mouse,mouse.world);
// find box under mouse
findBox(mouse.world.x, mouse.world.y);
// if mouse down and over a box
if(mouse.button && overBox){
mouse.button = false;
overBox.dead = true; // make it gray
// get the screen coordinates of the box
worldToScreen({
x:-(overBox.x + overBox.w/2),
y:-(overBox.y + overBox.h/2),
},newPos
);
// move it to the screen center
newPos.x += cw;
newPos.y += ch;
}
// forget what the following function does, think it does something like draw boxes, but I am guessing.. :P
drawBoxes();
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>

Getting pixel data on setInterval with canvas

I want to build an animated alphabet, made up of particles. Basically, the particles transform from one letter shape to another.
My idea is to fill the letters as text on canvas real quickly (like for a frame), get the pixel data and put the particles to the correct location on setInterval. I have this code for scanning the screen right now:
var ctx = canvas.getContext('2d'),
width = ctx.canvas.width,
height = ctx.canvas.height,
particles = [],
gridX = 8,
gridY = 8;
function Particle(x, y) {
this.x = x;
this.y = y;
}
// fill some text
ctx.font = 'bold 80px sans-serif';
ctx.fillStyle = '#ff0';
ctx.fillText("STACKOVERFLOW", 5, 120);
// now parse bitmap based on grid
var idata = ctx.getImageData(0, 0, width, height);
// use a 32-bit buffer as we are only checking if a pixel is set or not
var buffer32 = new Uint32Array(idata.data.buffer);
// using two loops here, single loop with index-to-x/y is also an option
for(var y = 0; y < height; y += gridY) {
for(var x = 0; x < width; x += gridX) {
//buffer32[] will have a value > 0 (true) if set, if not 0=false
if (buffer32[y * width + x]) {
particles.push(new Particle(x, y));
}
}
}
// render particles
ctx.clearRect(0, 0, width, height);
particles.forEach(function(p) {
ctx.fillRect(p.x - 2, p.y - 2, 4, 4); // just squares here
})
But this way I am only showing one word, without any changes throughout the time. Also, I want to set up initially like 200 particles and reorganise them based on the pixel data, not create them on each scan.. How would you rewrite the code, so on every 1500ms I can pass a different letter and render it with particles?
Hopefully the different parts of this code should be clear enough : There are particles, that can draw and update, fillParticle will spawn particles out of a text string, and spawnChars will get a new part of the text rendered on a regular basis.
It is working quite well, play with the parameters if you wish, they are all at the start of the fiddle.
You might want to make this code cleaner, by avoiding globals and creating classes.
http://jsbin.com/jecarupiri/1/edit?js,output
// --------------------
// parameters
var text = 'STACKOVERFLOW';
var fontHeight = 80;
var gridX = 4,
gridY = 4;
var partSize = 2;
var charDelay = 400; // time between two chars, in ms
var spead = 80; // max distance from start point to final point
var partSpeed = 0.012;
// --------------------
var canvas = document.getElementById('cv'),
ctx = canvas.getContext('2d'),
width = ctx.canvas.width,
height = ctx.canvas.height,
particles = [];
ctx.translate(0.5,0.5);
// --------------------
// Particle class
function Particle(startX, startY, finalX, finalY) {
this.speed = partSpeed*(1+Math.random()*0.5);
this.x = startX;
this.y = startY;
this.startX = startX;
this.startY = startY;
this.finalX =finalX;
this.finalY =finalY;
this.parameter = 0;
this.draw = function() {
ctx.fillRect(this.x - partSize*0.5, this.y - partSize*0.5, partSize, partSize);
};
this.update = function(p) {
if (this.parameter>=1) return;
this.parameter += partSpeed;
if (this.parameter>=1) this.parameter=1;
var par = this.parameter;
this.x = par*this.finalX + (1-par)*this.startX;
this.y = par*this.finalY + (1-par)*this.startY;
};
}
// --------------------
// Text spawner
function fillParticle(text, offx, offy, spread) {
// fill some text
tmpCtx.clearRect(0,0,tmpCtx.canvas.width, tmpCtx.canvas.height);
tmpCtx.font = 'bold ' + fontHeight +'px sans-serif';
tmpCtx.fillStyle = '#A40';
tmpCtx.textBaseline ='top';
tmpCtx.textAlign='left';
tmpCtx.fillText(text, 0, 0);
//
var txtWidth = Math.floor(tmpCtx.measureText(text).width);
// now parse bitmap based on grid
var idata = tmpCtx.getImageData(0, 0, txtWidth, fontHeight);
// use a 32-bit buffer as we are only checking if a pixel is set or not
var buffer32 = new Uint32Array(idata.data.buffer);
// using two loops here, single loop with index-to-x/y is also an option
for(var y = 0; y < fontHeight; y += gridY) {
for(var x = 0; x < txtWidth; x += gridX) {
//buffer32[] will have a value > 0 (true) if set, if not 0=false
if (buffer32[y * txtWidth + x]) {
particles.push(new Particle(offx + x+Math.random()*spread - 0.5*spread,
offy + y+Math.random()*spread - 0.5*spread, offx+x, offy+y));
}
}
}
return txtWidth;
}
var tmpCv = document.createElement('canvas');
// uncomment for debug
//document.body.appendChild(tmpCv);
var tmpCtx = tmpCv.getContext('2d');
// --------------------------------
// spawn the chars of the text one by one
var charIndex = 0;
var lastSpawnDate = -1;
var offX = 30;
var offY = 30;
function spawnChars() {
if (charIndex>= text.length) return;
if (Date.now()-lastSpawnDate < charDelay) return;
offX += fillParticle(text[charIndex], offX, offY, spead);
lastSpawnDate = Date.now();
charIndex++;
}
// --------------------------------
function render() {
// render particles
particles.forEach(function(p) { p.draw();
});
}
function update() {
particles.forEach(function(p) { p.update(); } );
}
// --------------------------------
// animation
function animate(){
requestAnimationFrame(animate);
ctx.clearRect(0, 0, width, height);
render();
update();
//
spawnChars();
}
// launch :
animate();

How can I implement Lanczos resampling after every canvas transform without having to make a new canvas?

UPDATE: Once I got this demo working... holy smokes, it's SLOW, like 12-16 seconds for only a level 2 render (when image is around 1000x2000 pixels). This is not even worth bothering with.
I found this really awesome and hopeful looking code in the top answer here: Resizing an image in an HTML5 canvas
//returns a function that calculates lanczos weight
function lanczosCreate(lobes){
return function(x){
if (x > lobes)
return 0;
x *= Math.PI;
if (Math.abs(x) < 1e-16)
return 1
var xx = x / lobes;
return Math.sin(x) * Math.sin(xx) / x / xx;
}
}
//elem: canvas element, img: image element, sx: scaled width, lobes: kernel radius
function thumbnailer(elem, img, sx, lobes){
this.canvas = elem;
elem.width = img.width;
elem.height = img.height;
elem.style.display = "none";
this.ctx = elem.getContext("2d");
this.ctx.drawImage(img, 0, 0);
this.img = img;
this.src = this.ctx.getImageData(0, 0, img.width, img.height);
this.dest = {
width: sx,
height: Math.round(img.height * sx / img.width),
};
this.dest.data = new Array(this.dest.width * this.dest.height * 3);
this.lanczos = lanczosCreate(lobes);
this.ratio = img.width / sx;
this.rcp_ratio = 2 / this.ratio;
this.range2 = Math.ceil(this.ratio * lobes / 2);
this.cacheLanc = {};
this.center = {};
this.icenter = {};
setTimeout(this.process1, 0, this, 0);
}
thumbnailer.prototype.process1 = function(self, u){
self.center.x = (u + 0.5) * self.ratio;
self.icenter.x = Math.floor(self.center.x);
for (var v = 0; v < self.dest.height; v++) {
self.center.y = (v + 0.5) * self.ratio;
self.icenter.y = Math.floor(self.center.y);
var a, r, g, b;
a = r = g = b = 0;
for (var i = self.icenter.x - self.range2; i <= self.icenter.x + self.range2; i++) {
if (i < 0 || i >= self.src.width)
continue;
var f_x = Math.floor(1000 * Math.abs(i - self.center.x));
if (!self.cacheLanc[f_x])
self.cacheLanc[f_x] = {};
for (var j = self.icenter.y - self.range2; j <= self.icenter.y + self.range2; j++) {
if (j < 0 || j >= self.src.height)
continue;
var f_y = Math.floor(1000 * Math.abs(j - self.center.y));
if (self.cacheLanc[f_x][f_y] == undefined)
self.cacheLanc[f_x][f_y] = self.lanczos(Math.sqrt(Math.pow(f_x * self.rcp_ratio, 2) + Math.pow(f_y * self.rcp_ratio, 2)) / 1000);
weight = self.cacheLanc[f_x][f_y];
if (weight > 0) {
var idx = (j * self.src.width + i) * 4;
a += weight;
r += weight * self.src.data[idx];
g += weight * self.src.data[idx + 1];
b += weight * self.src.data[idx + 2];
}
}
}
var idx = (v * self.dest.width + u) * 3;
self.dest.data[idx] = r / a;
self.dest.data[idx + 1] = g / a;
self.dest.data[idx + 2] = b / a;
}
if (++u < self.dest.width)
setTimeout(self.process1, 0, self, u);
else
setTimeout(self.process2, 0, self);
};
thumbnailer.prototype.process2 = function(self){
self.canvas.width = self.dest.width;
self.canvas.height = self.dest.height;
self.ctx.drawImage(self.img, 0, 0);
self.src = self.ctx.getImageData(0, 0, self.dest.width, self.dest.height);
var idx, idx2;
for (var i = 0; i < self.dest.width; i++) {
for (var j = 0; j < self.dest.height; j++) {
idx = (j * self.dest.width + i) * 3;
idx2 = (j * self.dest.width + i) * 4;
self.src.data[idx2] = self.dest.data[idx];
self.src.data[idx2 + 1] = self.dest.data[idx + 1];
self.src.data[idx2 + 2] = self.dest.data[idx + 2];
}
}
self.ctx.putImageData(self.src, 0, 0);
self.canvas.style.display = "block";
}
...
img.onload = function() {
var canvas = document.createElement("canvas");
new thumbnailer(canvas, img, 188, 3); //this produces lanczos3
//but feel free to raise it up to 8. Your client will appreciate
//that the program makes full use of his machine.
document.body.appendChild(canvas);
}
However, this implementation loads an image and renders it, end of story.
I have been trying to re-implement this code so that it does the filtering every time an existing canvas is scaled (think, zooming in and out of an image or document) without having to load a new image or create a new canvas.
How can I adapt it to work this way? Or is that even possible?
What you want to do is something like a singleton to reuse your canvas object. This will let you save the cost of create a new canvas object each time and you will reuse the same object
function getCanvas(){
var canvas;
if (typeof canvas === "undefined"){ canvas = document.createElement("canvas");}
return canvas;
}
img.onload = function() {
var canvas = getCanvas("canvas");
.... THE REST OF YOUR CODE .......
}
.
However this is not what slows your code, image scaling Algorithms are really heavy algorithms with intensive cpu use "usually make use of gpu acceleration at a really low level", and use advanced techniques like multiple bufferr and so others. here is a interesting tutorial in java.net on how image scaling works, it is in java but you can interpolate to any language.
Javascript is not ready for this techniques, so I recommend you to use the transformations available in the canvas api, as in the tutorial you read the efficient way is using the canvas2Dcontext.
var ctx = canvas.getContext("2d");
ctx.scale(2,2);