How to calculate actual width and height of rotated object in canvas? - javascript

Am trying to calculate width and height of object i loaded into canvas. When object is not rotated i get correct left right top bottom values, but when i load rotated object in canvas then i not get correct values , so i wonder what will be the logic or math formula to do achieve it.
how am doing.
initially load image into canvas
get image data from canvas
loop through image data to get only colored pixels by using alpha check
from colored pixel array find min max xy values
var temp_ray = []; // pixel array
for (var y = 0; y < imgData.height; ++y) {
for (var x = 0; x < imgData.width; ++x) {
var index = (y * imgData.width + x) * 4;
if(imgData.data[index+3]){
var xc = (index / 4) % imgData.width;
var yc = Math.floor((index / 4) / imgData.width);
temp_ray.push([xc,yc]);
}
}
}
if(temp_ray.length > 0){
var Xind = MaxMin2darray(temp_ray,0);
var Yind = MaxMin2darray(temp_ray,1);
var W = parseFloat(Xind['max']) - parseFloat(Xind['min']);
var H = parseFloat(Yind['max']) - parseFloat(Yind['min']);
var center_x = Xind['min'] + (W/2);
var center_y = Yind['min'] + (H/2);
// find corners of object
// find *min x , min y
let top_left = temp_ray[Xind['imin']]; // min X priority , min Y // top left
// find max x , *min y
let top_right = temp_ray[Yind['imin']]; // max X, min Y priority , // top right
// find *max x , min y
let bot_right = temp_ray[Xind['imax']]; // max X priority , min Y // bottom right
// find max x , *max y
let bot_left = temp_ray[Yind['imax']]; // max X , max Y priority // bottom left
var dim = {'W':W,'H':H,'CenterX':center_x,'CenterY':center_y,'top_left':top_left,'top_right':top_right,'bot_right':bot_right,'bot_left':bot_left,'Xend':Xind['max'],'Yend':Yind['max'],'Xstart':Xind['min'],'Ystart':Yind['min'],'Xend':Xind['max'],'Yend':Yind['max']};
console.log(dim);
}
and then using min max xy value find corners of object which works with none rotated objects but not work with rotated/tilted objects.
so any idea how to solve this problem
openpnp project is achieving this through opencv, but i think in js we do not have opencv library nor am that pro of java :(.
https://github.com/openpnp/openpnp/blob/develop/src/main/java/org/openpnp/vision/pipeline/stages/DrawRotatedRects.java
jsfiddle: http://jsfiddle.net/4L13vtaj/

In some simple cases (like rectangular objects), you could try to rotate the image until you minimize the number of uncolored pixels.
So you start with your image, and for each of the possible 360°, you compute the ratio. This is not perfect, but "doable" simply in pure js.
Here's a pseudoCode that might help you:
for degree in [0,365]{
rotateOriginalImageBy(degree);
cost[degree] = NemptyPixels/NfilledPixels;
}
predictedDegree = Math.min(cost);
rotateOriginalImageBy(predictedDegree);
compute 2 dimensions;
width = largerDimension;
height = shorterDimension;
Begining of an implementation (I edited your jsfiddle):
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var rotatioDegree = 45;
var imageObject = new Image();
imageObject.onload = function() {
var canvasWidth = imageObject.width;
var canvasHeight = canvasWidth; // not useful since width==height
document.getElementById('canvas').width = canvasWidth;
document.getElementById('canvas').height = canvasWidth;
ctx.clearRect(0, 0, canvasWidth, canvasWidth);
// Move registration point to the center of the canvas
ctx.translate(canvasWidth/2, canvasWidth/2)
ctx.rotate(rotatioDegree*3.1415/180);
ctx.translate(-canvasWidth/2,-canvasWidth/2)
ctx.drawImage(imageObject,0,0);
ctx.translate(canvasWidth/2, canvasWidth/2)
ctx.rotate(-rotatioDegree*3.1415/180);
ctx.translate(-canvasWidth/2,-canvasWidth/2)
var imgData = ctx.getImageData(0, 0, canvasWidth, canvasWidth);
http://jsfiddle.net/4L13vtaj/17/
If this doesn't work, you could implement some image detection techniques (Mathematical morphology for example). But i think this is outside the scope of stackoverflow.

If you work with some approximation, you can have something like that; I hope at least it can provide to you some ideas:
// some pixels in this image are not transparent, so we add a tollerance
// you can try to remove the second condition.
const isNotEmpty = (color) => color && color < 0xffaaaaaa;
function getTop(buff, w, h) {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getRight(buff, w, h) {
for (let x = w; x >=0; x--) {
for (let y = 0; y < h; y++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getBottom(buff, w, h) {
for (let y = h; y >= 0; y--) {
for (let x = 0; x < w; x++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getLeft(buff, w, h) {
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
async function main(imageSource) {
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const imageObject = new Image();
imageObject.src = imageSource;
await new Promise(r => imageObject.onload = r);
const w = canvas.width = imageObject.width;
const h = canvas.height = imageObject.height;
ctx.clearRect(0, 0, w, h);
ctx.drawImage(imageObject, 0, 0);
const imgData = ctx.getImageData(0, 0, w, h);
const buff = new Uint32Array(imgData.data.buffer);
const points = [
getTop(buff, w, h),
getRight(buff, w, h),
getBottom(buff, w, h),
getLeft(buff, w, h)
];
ctx.strokeStyle = "#0000ff"
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
ctx.lineTo(points[2].x, points[2].y);
ctx.lineTo(points[3].x, points[3].y);
ctx.closePath();
ctx.stroke();
}
main(/* image's url*/);
Here the link for testing: https://codepen.io/zer0/pen/zLxyQV
There are several problem with this approach: as said, with irregular images, it's not precise, in fact you will see the pin are making the image's bounding box a little bit smaller.
But the thing can be worse: try in the link above to use the 2nd image, that is quite irregular, and you will see.
Of course we can compensate, using also a bit more complex algorithm instead this simple one, but the question is: what the expected result for something like the 2nd image? Depends by that you can decide how to proceed.

Related

Isometric grid coordinates

I'm trying to do an isometric projection in HTML5 and the closest I can get is by scaling and rotating the viewport
window.addEventListener('DOMContentLoaded', (event) => {
class Point {
constructor() {
this.x = 0;
this.y = 0;
}
}
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
function tick(time) {
ctx.save();
ctx.translate(250,0);
ctx.scale(1, 0.5);
ctx.rotate(45 * Math.PI /180);
let p = new Point();
p.x = 4*32;
p.y = 2*32;
ctx.fillStyle = 'green';
ctx.fillRect(p.x,p.y,32,32);
for (var x = 0; x < 10; ++x) {
for (var y = 0; y < 10; ++y) {
ctx.strokeStyle = 'black';
ctx.strokeRect(x*32, y*32, 32, 32);
}
}
ctx.restore();
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
});
And it does work..but this isn't a practical solution because then when I try to draw isometric sprites within that scaled and rotated view, it looks really distorted. So I'd rather not go that route unless I can get it to work without distorting my sprites.
I also tried this website https://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-primer-for-game-developers-updated--cms-28392
Which provided an equation for generating isometric grids. I used their cartesianToIsometric function to try and make an iso grid
window.addEventListener('DOMContentLoaded', (event) => {
class Point {
constructor() {
this.x = 0;
this.y = 0;
}
}
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
function cartesianToIsometric(cartPt) {
var tempPt = new Point();
tempPt.x = cartPt.x - cartPt.y;
tempPt.y = (cartPt.x + cartPt.y) / 2;
return (tempPt);
}
function tick(time) {
for (var x = 0; x < 50; ++x) {
for (var y = 0; y < 50; ++y) {
let p = new Point();
p.x = x * 32;
p.y = y * 32;
let iso = cartesianToIsometric(p);
ctx.strokeStyle = 'black';
ctx.strokeRect(iso.x, iso.y, 32, 32);
}
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
});
but it still looked wrong Unless I rotate or scale the viewport to correct it.
So my question is..how can I draw an isometric grid without scaling and rotating my viewport(if possible)
If I have to scale my viewport like the first example, how can I do it without distorting my sprites.... Sorry if this question is confusing to read my knowledge on this is iffy..
Here's my html file
index.html
<html>
<head>
<script src="index.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
Looks like you are so close to the solution but a little bit confused. Let me explain:
Normally, you don't have to rotate and scale your canvas viewport. This is something you do when you create your isometric sprites. You should already have some isometric sprites and all you have to do just to put them in correct isometric coordinates.
Your second approach will do exactly what I mean, calculating the isometric points by using cartesian coordinates. It should work, no need to draw rectangles just place the images on the isometric coordinates.
The only trick here you should place your isometric sprites from their bottom center points:
for (var x = 0; x < 50; ++x) {
for (var y = 0; y < 50; ++y) {
let p = new Point();
p.x = x * 32;
p.y = y * 32;
let iso = cartesianToIsometric(p);
// Apply offset to place each isometric image from its bottom center.
// The default pivot point (top left) won't do good
// because we need to stack them up according to their heights.
let offset = {x: floorImageWidth/2, y: floorImageHeight}
ctx.drawImage(floor, iso.x - offset.x, iso.y - offset.y);
}
}
I also suggest using a separate function to draw iso points (not the rectangles) so that you can debug your positions:
let debug = true;
function debugDraw(time) {
for (var x = 0; x < 50; ++x) {
for (var y = 0; y < 50; ++y) {
let p = new Point();
p.x = x * tileWidth;
p.y = y * tileWidth;
let iso = cartesianToIsometric(p);
// draw pivot point for each tile
ctx.fillStyle = 'yellow';
ctx.fillRect(iso.x - 5, iso.y - 5, 10, 10);
}
}
}
if(debug){
requestAnimationFrame(debugDraw)
}
Plus, here is a working demo for you to experiment further:
https://codepen.io/justintc/pen/eYpMabx
Hope it helps, cheers!

scrambling images on canvas producing whitespace

I'm starting with a canvas element. I'm making the left half red, and the right side blue. Every half second, setInterval calls a function, scramble, which splits both RHS and LHS into pieces, and shuffles them.
Here is a fiddle: https://jsfiddle.net/aeq1g3yb/
The code is below. The reason I'm using window.onload is because this thing is supposed to scramble pictures and I want the pictures to load first. I'm using colors here because of the cross-origin business that I don't know enough about, so this is my accommodation.
var n = 1;
var v = 1;
function scramble() {
//get the canvas and change its width
var c = document.getElementById("myCanvas");
c.width = 600;
var ctx = c.getContext("2d");
//drawing 2 different colors side by side
ctx.fillStyle = "red";
ctx.fillRect(0, 0, c.width/2, c.height);
ctx.fillStyle = "blue";
ctx.fillRect(c.width/2, 0, c.width/2, c.height);
//how big will each shuffled chunk be
var stepsA = (c.width/2) / n;
var stepsB = (c.width/2) / n;
var step = stepsA + stepsB;
var imgDataA = [];
var imgDataB = [];
for (var i = 0; i < n; i++) {
var imgDataElementA = ctx.getImageData(stepsA*i, 0, stepsA, c.height);
var imgDataElementB = ctx.getImageData(c.width/2+stepsB*i, 0, stepsB, c.height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
//clearing out the canvas before laying on the new stuff
ctx.fillStyle = "white";
ctx.fillRect(0, 0, c.width, c.height);
//put the images back
for (var i = 0; i < n; i++) {
ctx.putImageData(imgDataA[i], step*i, 0);
ctx.putImageData(imgDataB[i], step*i+stepsA, 0);
}
//gonna count the steps
var count = document.getElementById("count");
count.innerHTML = n;
n += v;
if (n >= 100 || n <= 1) {
v *= -1;
}
}; //closing function scramble
window.onload = function() { //gotta do this bc code executes before image loads
scramble();
};
window.setInterval(scramble, 500);
More or less, this thing works the way I want it to. But there is one problem: Sometimes there are vertical white lines.
My question is:
Why are there white lines? If you view the fiddle, you will see the degree to which this impairs the effect of the shuffle.
You can`t divide a Pixel
The problem can be solve but will introduce some other artifacts as you can not divide integer pixels into fractions.
Quick solution
The following solution for your existing code rounds down for the start of a section and up for the width.
for (var i = 0; i < n; i++) {
var imgDataElementA = ctx.getImageData(
Math.floor(stepsA * i), 0,
Math.ceil(stepsA + stepsA * i) - Math.floor(stepsA * i), c.height
);
var imgDataElementB = ctx.getImageData(
Math.floor(c.width / 2 + stepsB * i), 0,
Math.ceil(c.width / 2 + stepsB * i + stepsB) - Math.floor(c.width / 2 + stepsB * i), c.height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
Quicker options
But doing this via the pixel image data is about the slowest possible way you could find to do it. You can just use the 2D context.imageDraw function to do the movement for you. Or if you want the best in terms of performance a WebGL solution would be the best with the fragment shader doing the scrambling for you as a parallel solution.
There is no perfect solution
But in the end you can not cut a pixel in half, there are a wide range of ways to attempt to solve this but each method has its own artifacts. Ideally you should only slice an image if the rule image.width % slices === 0 in all other cases you will have one or more slices that will not fit on an integer number of pixels.
Example of 4 rounding methods.
The demo shows 4 different methods and with 2 colors. Mouse over to see a closer view. Each method is separated horizontally with a white line. Hold the mouse button to increase the slice counter.
The top is your original.
The next three are 3 different ways of dealing with the fractional pixel width.
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
const m = mouse;
if(m.element){
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
m.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : m.button;
}
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
const counterElement = document.getElementById("count");
// get constants for the demo
const c = document.getElementById("myCanvas");
mouse.element = c;
// The image with the blue and red
const img = document.createElement("canvas");
// the zoom image overlay
const zoom = document.createElement("canvas");
// the scrambled image
const scram = document.createElement("canvas");
// Set sizes and get context
const w = scram.width = zoom.width = img.width = c.width = 500;
const h = scram.height = zoom.height = img.height = c.height;
const dCtx = c.getContext("2d"); // display context
const iCtx = img.getContext("2d"); // source image context
const zCtx = zoom.getContext("2d"); // zoom context
const sCtx = scram.getContext("2d"); // scrambled context
// some constants
const zoomAmount = 4;
const zoomRadius = 60;
const framesToStep = 10;
function createTestPattern(ctx){
ctx.fillStyle = "red";
ctx.fillRect(0, 0, c.width/2, c.height/2);
ctx.fillStyle = "blue";
ctx.fillRect(c.width/2, 0, c.width/2, c.height/2);
ctx.fillStyle = "black";
ctx.fillRect(0, c.height/2, c.width/2, c.height/2);
ctx.fillStyle = "#CCC";
ctx.fillRect(c.width/2, c.height/2, c.width/2, c.height/2);
}
createTestPattern(iCtx);
sCtx.drawImage(iCtx.canvas, 0, 0);
// Shows a zoom area so that blind men like me can see what is going on.
function showMouseZoom(src,dest,zoom = zoomAmount,radius = zoomRadius){
dest.clearRect(0,0,w,h);
dest.imageSmoothingEnabled = false;
if(mouse.x >= 0 && mouse.y >= 0 && mouse.x < w && mouse.y < h){
dest.setTransform(zoom,0,0,zoom,mouse.x,mouse.y)
dest.drawImage(src.canvas, -mouse.x, -mouse.y);
dest.setTransform(1,0,0,1,0,0);
dest.globalCompositeOperation = "destination-in";
dest.beginPath();
dest.arc(mouse.x,mouse.y,radius,0,Math.PI * 2);
dest.fill();
dest.globalCompositeOperation = "source-over";
dest.lineWidth = 4;
dest.strokeStyle = "black";
dest.stroke();
}
}
function scramble(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w/2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
((i / 2) | 0) * steps + (i % 2) * (w / 2)- 0.5, y,
steps + 1, height,
i * steps - 0.5, y,
steps+ 1, height
);
}
}
function scrambleFloor(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w/2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
(((i / 2) | 0) * steps + (i % 2) * (w / 2)- 0.5) | 0, y,
steps + 1, height,
(i * steps - 0.5) | 0, y,
steps + 1, height
);
}
}
function scrambleNoOverlap(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
const steps = (w / 2) / slices;
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
for (var i = 0; i < slices * 2; i++) {
dest.drawImage(src.canvas,
((i / 2) | 0) * steps + (i % 2) * (w / 2), y,
steps, height,
i * steps - 0.5, y,
steps, height
);
}
}
function scrambleOriginal(src,dest,y,height) {
const w = src.canvas.width;
const h = src.canvas.height;
//how big will each shuffled chunk be
var stepsA = (w/2) / slices;
var stepsB = (w/2) / slices;
var step = stepsA + stepsB;
var imgDataA = [];
var imgDataB = [];
for (var i = 0; i < slices; i++) {
var imgDataElementA = src.getImageData(stepsA*i, y, stepsA, height);
var imgDataElementB = src.getImageData(w/2+stepsB*i, y, stepsB, height);
imgDataA.push(imgDataElementA);
imgDataB.push(imgDataElementB);
}
//clearing out the canvas before laying on the new stuff
dest.fillStyle = "white";
dest.fillRect(0, y, w, height);
//put the images back
for (var i = 0; i < slices; i++) {
dest.putImageData(imgDataA[i], step*i, y);
dest.putImageData(imgDataB[i], step*i+stepsA, y);
}
}; //closing function scramble
const scrambleMethods = [scrambleOriginal,scramble,scrambleFloor,scrambleNoOverlap];
var frameCount = 0;
var sliceStep = 1;
var slices = 1;
function mainLoop(){
if(mouse.button){
if(frameCount++ % framesToStep === framesToStep-1){ // every 30 Frames
slices += sliceStep;
if(slices > 150 || slices < 2){ sliceStep = -sliceStep }
counterElement.textContent = slices; // Prevent reflow by using textContent
sCtx.clearRect(0,0,w,h);
sCtx.imageSmoothingEnabled = true;
const len = scrambleMethods.length;
for(var i = 0; i < len; i ++){
scrambleMethods[i](iCtx,sCtx,(128/len) * i, 128/len-2);
scrambleMethods[i](iCtx,sCtx,(128/len) * i + 128, 128/len-2);
}
}
}
dCtx.fillStyle = "white";
dCtx.fillRect(0,0,w,h);
dCtx.drawImage(sCtx.canvas,0,0);
showMouseZoom(dCtx,zCtx);
dCtx.drawImage(zCtx.canvas,0,0);
requestAnimationFrame(mainLoop);
}
//scramble(iCtx,sCtx);
requestAnimationFrame(mainLoop);
canvas {
border: 1px solid black;
}
#count {
position : absolute;
top : 0px;
left : 10px;
font-family: monospace;
font-size: 20px;
}
<canvas id="myCanvas" height = "256" title="Hold mouse button to chance slice count"></canvas>
<p id="count"></p>

Canvas: draw lots of elements with a changing gradient (emulate angular gradient)

for this project http://biduleohm.free.fr/ledohm/ (sorry, the user interface is in french but the code is in english) I need an angular gradient but it doesn't exists in native so I've implemented it using a linear gradient on a line and I draw the lines more and more longer to form a triangle. The result is graphically OK but the speed isn't really good (1850 ms for 125 triangles). It's in the tab [Répartition], it redraws if there is a keyup event on one of the inputs, don't be afraid of the apparent slowness, I've limited to maximum one redraw every 2000 ms.
Before I used a simple linear gradient on the whole triangle (but this doesn't match the reality) and the speed was OK, it draws thousands of triangles in less than a second. This function was used :
drawFrontLightForColor : function(x, y, w, h, color) {
var x2 = x - w;
var x3 = x + w;
var gradient = Distri.frontCanvas.createLinearGradient(x2, y, x3, y);
gradient.addColorStop(0, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
gradient.addColorStop(0.5, 'rgba(' + color + ', ' + (color == Distri.lightColors.cw ? Distri.lightCenterAlphaCw : Distri.lightCenterAlphaOther) + ')');
gradient.addColorStop(1, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
Distri.frontCanvas.fillStyle = gradient;
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x, y);
Distri.frontCanvas.lineTo(x2, (y + h));
Distri.frontCanvas.lineTo(x3, (y + h));
Distri.frontCanvas.lineTo(x, y);
Distri.frontCanvas.fill();
Distri.frontCanvas.closePath();
},
Then I switched to this function :
drawFrontLightForColor : function(x, y, w, h, centerColor, edgeColor) {
var ratio = w / h;
var tmpY;
var tmpW;
var x2;
var x3;
var gradient;
Distri.frontCanvas.lineWidth = 1;
for (var tmpH = 0; tmpH < h; tmpH++) {
tmpY = y + tmpH;
tmpW = Math.round(tmpH * ratio);
x2 = x - tmpW;
x3 = x + tmpW;
gradient = Distri.frontCanvas.createLinearGradient(x2, tmpY, x3, tmpY);
gradient.addColorStop(0, edgeColor);
gradient.addColorStop(0.5, centerColor);
gradient.addColorStop(1, edgeColor);
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x2, tmpY);
Distri.frontCanvas.lineTo(x, tmpY);
Distri.frontCanvas.lineTo(x3, tmpY);
Distri.frontCanvas.strokeStyle = gradient;
Distri.frontCanvas.stroke();
Distri.frontCanvas.closePath();
}
},
You can find the whole source here
I can't put the beginPath, stroke, closePath out of the loop because of the gradient which is changing every iteration (I've tried but it used the last gradient for every line (which, ironically, is identical to the first function...) which is understandable but not what I want).
I accept any advice (including redo the whole function and modify his caller to outsource some code) to improve the speed let's say 5x (ideally more).
I think you took the wrong way from the start : when doing so much changes of color, you have better operate at the pixel level.
So yes that could be with a webgl pixel shader, but you'll have to fight just to get the boilerplate running ok on all platform (or get a lib to do that for you).
And anyway there's a solution perfect for your need, and fast enough (a few ms) : use raw pixel data, update them one by one with the relevant function, then draw the result.
The steps to do that are :
- create a buffer same size as the canvas.
- iterate through it's pixel, keeping track of the x,y of the point.
- normalize the coordinates so they match your 'space'.
- compute the value for the normalized (x,y) out of all the data that you have.
- write a color (in my example i choose greyscale) out of that value.
- draw the whole buffer to canvas.
I did a jsfiddle, and here's the result with 4 data points :
fiddle is here :
http://jsfiddle.net/gamealchemist/KsM9c/3/
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
var width = canvas.width,
height = canvas.height;
// builds an image for the target canvas
function buildImage(targetCanvas, valueForXY, someData) {
var width = targetCanvas.width;
var height = targetCanvas.height;
var tempImg = ctx.createImageData(width, height);
var buffer = tempImg.data;
var offset = 0;
var xy = [0, 0];
function normalizeXY(xy) {
xy[0] = xy[0] / width ;
xy[1] = xy[1] / height;
}
for (var y = 0; y < height; y++)
for (var x = 0; x < width; x++, offset += 4) {
xy[0] = x; xy[1]=y;
normalizeXY(xy);
var val = Math.floor(valueForXY(xy, someData) * 255);
buffer[offset] = val;
buffer[offset + 1] = val;
buffer[offset + 2] = val;
buffer[offset + 3] = 255;
}
ctx.putImageData(tempImg, 0, 0);
}
// return normalized (0->1) value for x,y and
// provided data.
// xy is a 2 elements array
function someValueForXY(xy, someData) {
var res = 0;
for (var i = 0; i < someData.length; i++) {
var thisData = someData[i];
var dist = Math.pow(sq(thisData[0] - xy[0]) + sq(thisData[1] - xy[1]), -0.55);
localRes = 0.04 * dist;
res += localRes;
}
if (res > 1) res = 1;
return res;
}
var someData = [
[0.6, 0.2],
[0.35, 0.8],
[0.2, 0.5],
[0.6, 0.75]
];
buildImage(canvas, someValueForXY, someData);
// ------------------------
function sq(x) {
return x * x
}
In fact the GameAlchemist's solution isn't fast or I do something really wrong. I've implemented this algo only for the top view because the front view is much more complex.
For 120 lights the top view take 100-105 ms with the old code and it take 1650-1700 ms with this code (and moreover it still lacks a few things in the new code like the color for example):
drawTopLightForColor_ : function(canvasW, canvasD, rampX, rampY, rampZ, ledsArrays, color) {
function sq(x) {
return x * x;
}
var tmpImg = Distri.topCanvasCtx.createImageData(canvasW, canvasD);
var rawData = tmpImg.data;
var ledsArray = ledsArrays[color];
var len = ledsArray.length;
var i = 0;
for (var y = 0; y < canvasD; y++) {
for (var x = 0; x < canvasW; x++, i += 4) {
var intensity = 0;
for (var j = 0; j < len; j++) {
intensity += 2 * Math.pow(
sq((rampX + ledsArray[j].x) - x) +
sq((rampZ + ledsArray[j].y) - y),
-0.5
);
}
if (intensity > 1) {
intensity = 1;
}
intensity = Math.round(intensity * 255);
rawData[i] = intensity;
rawData[i + 1] = intensity;
rawData[i + 2] = intensity;
rawData[i + 3] = 255;
}
}
Distri.topCanvasCtx.putImageData(tmpImg, 0, 0);
},
Am I doing something wrong?

Using midpoint circle algorithm to generate points of a filled circle

I need to generate and store the coordinates of each point of a filled circle of say, radius 10 in Javascript.
It seems like the best way to do this would be to use the midpoint circle algorithm, but I'm not sure how to adapt it to find every point in the circle. The coordinates are going to be stored as objects in an array.
Could someone help me with the implementation?
Personally I think it would probably be faster in this case to test all pixels in the bounding box for their distance to the center. If <= r then the point is in the circle and should be pushed onto your array.
function distance(p1, p2)
{
dx = p2.x - p1.x; dx *= dx;
dy = p2.y - p1.y; dy *= dy;
return Math.sqrt( dx + dy );
}
function getPoints(x, y, r)
{
var ret = [];
for (var j=x-r; j<=x+r; j++)
for (var k=y-r; k<=y+r; k++)
if (distance({x:j,y:k},{x:x,y:y}) <= r) ret.push({x:j,y:k});
return ret;
}
You loop through all the possible points and you run the Point-In-Circle check on them.
Something like the following would suffice...
var result = [];
var d = 10;
var r = d / 2;
var rr = r*r;
for(var y=0; y<d; y++)
for(var x=0; x<d; x++)
if((x-r)*(x-r)+(y-r)*(y-r) < rr)
result.push({"x": x, "y": y});
Modifying the above algorithm to handle other (more complex) shapes/path/polygons would be difficult. For a more generic solution you could use HTML5 CANVAS. You create a canvas, get the 2d context draw all of your shapes/paths/polygons in solid black then iterate through the pixel data and find the pixels with an alpha channel greater than 0 (or 127 if you want to alleviate false positives from anti-aliasing).
var r = 5; // radius of bounding circle
//
// set up a canvas element
//
var canvas = document.createElement("canvas");
canvas.width = r*2;
canvas.height = r*2;
canvas.style.width = (r*2) + "px";
canvas.style.height = (r*2) + "px";
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#000";
//
// draw your shapes/paths/polys here
//
ctx.beginPath();
ctx.arc(r, r, r, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
//
// process the pixel data
//
var imageData = ctx.getImageData(0,0,(r*2),(r*2));
var data = imageData.data;
var result = [];
var str = "";
for(var y = 0; y<(r*2); y++) {
for(var x = 0; x<(r*2); x++) {
var pixelOffset = (y * (r*2) + x) * 4;
if(data[pixelOffset+3] > 127) {
result.push({x: x, y: y});
str += "(" + x + ", " + y + ") "; // debug
}
}
}
//
// debug/test output
//
document.body.innerHTML += str;
document.body.appendChild(canvas);
alert(result.length);

Automatically Crop HTML5 canvas to contents

Let's say this is my canvas, with an evil-looking face drawn on it. I want to use toDataURL() to export my evil face as a PNG; however, the whole canvas is rasterised, including the 'whitespace' between the evil face and canvas edges.
+---------------+
| |
| |
| (.Y. ) |
| /_ |
| \____/ |
| |
| |
+---------------+
What is the best way to crop/trim/shrinkwrap my canvas to its contents, so my PNG is no larger than the face's 'bounding-box', like below? The best way seems to be scaling the canvas, but supposing the contents are dynamic...? I'm sure there should be a simple solution to this, but it's escaping me, with much Googling.
+------+
|(.Y. )|
| /_ |
|\____/|
+------+
Thanks!
Edited (see comments)
function cropImageFromCanvas(ctx) {
var canvas = ctx.canvas,
w = canvas.width, h = canvas.height,
pix = {x:[], y:[]},
imageData = ctx.getImageData(0,0,canvas.width,canvas.height),
x, y, index;
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
index = (y * w + x) * 4;
if (imageData.data[index+3] > 0) {
pix.x.push(x);
pix.y.push(y);
}
}
}
pix.x.sort(function(a,b){return a-b});
pix.y.sort(function(a,b){return a-b});
var n = pix.x.length-1;
w = 1 + pix.x[n] - pix.x[0];
h = 1 + pix.y[n] - pix.y[0];
var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);
canvas.width = w;
canvas.height = h;
ctx.putImageData(cut, 0, 0);
var image = canvas.toDataURL();
}
If I understood well you want to "trim" away all the surronding your image / drawing, and adjust the canvas to that size (like if you do a "trim" command in Photoshop).
Here is how I'll do it.
Run thru all the canvas pixels checking if their alpha component is > 0 (that means that something is drawn in that pixel). Alternativelly you could check for the r,g,b values, if your canvas background is fullfilled with a solid color, for instance.
Get te coordinates of the top most left pixel non-empty, and same for the bottom most right one. So you'll get the coordinates of an imaginay "rectangle" containing the canvas area that is not empty.
Store that region of pixeldata.
Resize your canvas to its new dimensions (the ones of the region we got at step 2.)
Paste the saved region back to the canvas.
Et, voilá :)
Accesing pixeldata is quite slow depending on the size of your canvas (if its huge it can take a while). There are some optimizations around to work with raw canvas pixeldata (I think there is an article about this topic at MDN), I suggest you to google about it.
I prepared a small sketch in jsFiddle that you can use as starting point for your code.
Working sample at jsFiddle
Hope I've helped you.
c:.
Here's my take. I felt like all the other solutions were overly complicated. Though, after creating it, I now see it's the same solution as one other's, expect they just shared a fiddle and not a function.
function trimCanvas(canvas){
const context = canvas.getContext('2d');
const topLeft = {
x: canvas.width,
y: canvas.height,
update(x,y){
this.x = Math.min(this.x,x);
this.y = Math.min(this.y,y);
}
};
const bottomRight = {
x: 0,
y: 0,
update(x,y){
this.x = Math.max(this.x,x);
this.y = Math.max(this.y,y);
}
};
const imageData = context.getImageData(0,0,canvas.width,canvas.height);
for(let x = 0; x < canvas.width; x++){
for(let y = 0; y < canvas.height; y++){
const alpha = imageData.data[((y * (canvas.width * 4)) + (x * 4)) + 3];
if(alpha !== 0){
topLeft.update(x,y);
bottomRight.update(x,y);
}
}
}
const width = bottomRight.x - topLeft.x;
const height = bottomRight.y - topLeft.y;
const croppedCanvas = context.getImageData(topLeft.x,topLeft.y,width,height);
canvas.width = width;
canvas.height = height;
context.putImageData(croppedCanvas,0,0);
return canvas;
}
Here's code in ES syntax, short, fast and concise:
/**
* Trim a canvas.
*
* #author Arjan Haverkamp (arjan at avoid dot org)
* #param {canvas} canvas A canvas element to trim. This element will be trimmed (reference)
* #param {int} threshold Alpha threshold. Allows for trimming semi-opaque pixels too. Range: 0 - 255
* #returns {Object} Width and height of trimmed canvcas and left-top coordinate of trimmed area. Example: {width:400, height:300, x:65, y:104}
*/
const trimCanvas = (canvas, threshold = 0) => {
const ctx = canvas.getContext('2d'),
w = canvas.width, h = canvas.height,
imageData = ctx.getImageData(0, 0, w, h),
tlCorner = { x:w+1, y:h+1 },
brCorner = { x:-1, y:-1 };
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (imageData.data[((y * w + x) * 4) + 3] > threshold) {
tlCorner.x = Math.min(x, tlCorner.x);
tlCorner.y = Math.min(y, tlCorner.y);
brCorner.x = Math.max(x, brCorner.x);
brCorner.y = Math.max(y, brCorner.y);
}
}
}
const cut = ctx.getImageData(tlCorner.x, tlCorner.y, brCorner.x - tlCorner.x, brCorner.y - tlCorner.y);
canvas.width = brCorner.x - tlCorner.x;
canvas.height = brCorner.y - tlCorner.y;
ctx.putImageData(cut, 0, 0);
return {width:canvas.width, height:canvas.height, x:tlCorner.x, y:tlCorner.y};
}
The top voted answer here, as well as the implementations i found online trim one extra pixel which was very apparent when trying to trim text out of canvas. I wrote my own that worked better for me:
var img = new Image;
img.onload = () => {
var canvas = document.getElementById('canvas');
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
document.getElementById('button').addEventListener('click', ()=>{
autoCropCanvas(canvas, ctx);
document.getElementById('button').remove();
});
};
img.src = 'data:image/png;base64,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';
function autoCropCanvas(canvas, ctx) {
var bounds = {
left: 0,
right: canvas.width,
top: 0,
bottom: canvas.height
};
var rows = [];
var cols = [];
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (var x = 0; x < canvas.width; x++) {
cols[x] = cols[x] || false;
for (var y = 0; y < canvas.height; y++) {
rows[y] = rows[y] || false;
const p = y * (canvas.width * 4) + x * 4;
const [r, g, b, a] = [imageData.data[p], imageData.data[p + 1], imageData.data[p + 2], imageData.data[p + 3]];
var isEmptyPixel = Math.max(r, g, b, a) === 0;
if (!isEmptyPixel) {
cols[x] = true;
rows[y] = true;
}
}
}
for (var i = 0; i < rows.length; i++) {
if (rows[i]) {
bounds.top = i ? i - 1 : i;
break;
}
}
for (var i = rows.length; i--; ) {
if (rows[i]) {
bounds.bottom = i < canvas.height ? i + 1 : i;
break;
}
}
for (var i = 0; i < cols.length; i++) {
if (cols[i]) {
bounds.left = i ? i - 1 : i;
break;
}
}
for (var i = cols.length; i--; ) {
if (cols[i]) {
bounds.right = i < canvas.width ? i + 1 : i;
break;
}
}
var newWidth = bounds.right - bounds.left;
var newHeight = bounds.bottom - bounds.top;
var cut = ctx.getImageData(bounds.left, bounds.top, newWidth, newHeight);
canvas.width = newWidth;
canvas.height = newHeight;
ctx.putImageData(cut, 0, 0);
}
<canvas id=canvas style='border: 1px solid pink'></canvas>
<button id=button>crop canvas</button>

Categories