How can I do something like this?
Image one:
Image two:
And how can I do like this:
const image1 = document.querySelector("#imgone");
const image2 = document.querySelector("#imgtwo");
let ctx1 = image1.getContext("2d");
let ctx2 = image2.getContext("2d");
function locate(context1, context2) {
#??????
return "..."
}
function rectangle(context, corner1,corner2,corner3,corner4) {
#??????
return "..."
}
[corner1,corner2,corner3,corner4] = locate(ctx,ctx2);
rectangle(ctx2, corner1,corner2,corner3,corner4);
Basically a computer-vision task.
Is it possible? How can you do it?
it is me sld on a different account. I have figured it out, and would like to show you.
My solution is very inneficient, but I have this code.
function canvas(w = 300, h = 150) {
let r = document.createElement("canvas")
r.height = h
r.width = w
return r
}
function points(startx, starty, w, h) {
return [
[startx, starty],
[startx + w, starty],
[startx + w, starty + h],
[startx, starty + h]
]
}
function getPixelColor(canvas, x, y) {
return Array.from(canvas.getContext("2d").getImageData(x, y, 1, 1).data)
}
function gpc(canvas, x, y, sx, sy) {
return Array.from(canvas.getContext("2d").getImageData(x, y, sx, sy).data)
}
function gap(canvas) {
return gpc(canvas, 0, 0, canvas.width, canvas.height)
}
function gx(index, w = 300, h = 150, nx = 5, ny = 5) {
let fpd = []
for (let i = 0; i < w; i += nx) {
for (let ii = 0; ii < h; ii += ny) {
fpd.push([i, ii])
}
}
return fpd[index]
}
// function genPixels(canvas,nx=5,ny=5) {
// let ctx = canvas.getContext("2d")
// let fpd = []
// for (let i = 0; i < canvas.width; i+=nx) {
// for (let ii = 0; ii < canvas.height; ii+=ny) {
// fpd.push(Array.from(ctx.getImageData(i,ii,nx,ny).data))
// }
// }
// return fpd
// }
function chunk(n, c) {
return n.reduce((n, r, t) => {
const o = Math.floor(t / c);
return n[o] = [].concat(n[o] || [], r), n
}, [])
}
function findImg(parent, canvas, ih, iw) {
if (parent.tagName !== "canvas") {
console.log("parent argument must be canvas. use canvas.content.drawImage on a new canvas to convert img to canvas.")
}
if (canvas.tagName === "img") {
console.log("canvas argument must be type canvas. use canvas.context.drawImage on a new canvas to convert img to canvas.")
}
let allPixels = gap(parent)
let cp = gap(canvas)
let chunks = chunk(allPixels, ih * iw * 4)
for (let i = 0; i < chunks.length; i += 1) {
if (String(chunks[i]) === String(cp)) {
let start = gx(i, parent.width, parent.height, iw, ih)
return points(start[0], start[1], iw, ih)
}
}
return false
}
let pointsc = findImg(canvas(), canvas(5, 5), 5, 5);
let dim = pointsc[2].reverse()
this.strokeStyle = "black"
let yours = document.querySelector("#yours")
yours.getContext("2d").strokeRect(0, 0, dim[0], dim[1])
<canvas id="yours"></canvas>
Minified:
function canvas(t=300,n=150){let e=document.createElement("canvas");return e.height=n,e.width=t,e}function points(t,n,e,a){return[[t,n],[t+e,n],[t+e,n+a],[t,n+a]]}function getPixelColor(t,n,e){return Array.from(t.getContext("2d").getImageData(n,e,1,1).data)}function gpc(t,n,e,a,r){return Array.from(t.getContext("2d").getImageData(n,e,a,r).data)}function gap(t){return gpc(t,0,0,t.width,t.height)}function gx(t,n=300,e=150,a=5,r=5){let o=[];for(let t=0;t<n;t+=a)for(let n=0;n<e;n+=r)o.push([t,n]);return o[t]}function chunk(t,n){return t.reduce((t,e,a)=>{const r=Math.floor(a/n);return t[r]=[].concat(t[r]||[],e),t},[])}function findImg(t,n,e,a){"CANVAS"!==t.tagName&&console.log("parent argument must be canvas. use canvas.content.drawImage on a new canvas to convert img to canvas."),"IMG"===n.tagName&&console.log("canvas argument must be type canvas. use canvas.context.drawImage on a new canvas to convert img to canvas.");let r=gap(t),o=gap(n),c=chunk(r,e*a*4);for(let n=0;n<c.length;n+=1)if(String(c[n])===String(o)){let r=gx(n,t.width,t.height,a,e);return points(r[0],r[1],a,e)}return!1}
I will be doing an example with just two white images.
let pointsc = findImg(canvas(), canvas(5,5), 5, 5);
let dim = pointsc[2].reverse()
this.strokeStyle="black"
document.querySelector("#yours").getContext("2d").strokeRect(0,0,dim[0],dim[1])
This is extremely inefficient, and I am up for suggestions, for example how to not use as many loops.
Related
I don't know if this makes me lazy or what I just to use this kind method but it doesn't work well in react.
Here is the link of the code
HOVER
So basically my error here is the part of ctx = canvas.getContext('2d') I try to copy all the codes and do this something in my react
useEffect(() => {
const rippleSettings = {
maxSize: 100,
animationSpeed: 5,
strokeColor: [148, 217, 255],
};
const canvasSettings = {
blur: 8,
ratio: 1,
};
function Coords(x, y) {
this.x = x || null;
this.y = y || null;
}
const Ripple = function Ripple(x, y, circleSize, ctx) {
this.position = new Coords(x, y);
this.circleSize = circleSize;
this.maxSize = rippleSettings.maxSize;
this.opacity = 1;
this.ctx = ctx;
this.strokeColor = `rgba(${Math.floor(rippleSettings.strokeColor[0])},
${Math.floor(rippleSettings.strokeColor[1])},
${Math.floor(rippleSettings.strokeColor[2])},
${this.opacity})`;
this.animationSpeed = rippleSettings.animationSpeed;
this.opacityStep = (this.animationSpeed / (this.maxSize - circleSize)) / 2;
};
Ripple.prototype = {
update: function update() {
this.circleSize = this.circleSize + this.animationSpeed;
this.opacity = this.opacity - this.opacityStep;
this.strokeColor = `rgba(${Math.floor(rippleSettings.strokeColor[0])},
${Math.floor(rippleSettings.strokeColor[1])},
${Math.floor(rippleSettings.strokeColor[2])},
${this.opacity})`;
},
draw: function draw() {
this.ctx.beginPath();
this.ctx.strokeStyle = this.strokeColor;
this.ctx.arc(this.position.x, this.position.y, this.circleSize, 0,
2 * Math.PI);
this.ctx.stroke();
},
setStatus: function setStatus(status) {
this.status = status;
},
};
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
console.log(ctx)
const ripples = [];
const rippleStartStatus = 'start';
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
canvas.style.filter = `blur(${canvasSettings.blur}px)`;
canvas.width = width * canvasSettings.ratio;
canvas.height = height * canvasSettings.ratio;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
let animationFrame;
// Function which is executed on mouse hover on canvas
const canvasMouseOver = (e) => {
const x = e.clientX * canvasSettings.ratio;
const y = e.clientY * canvasSettings.ratio;
ripples.unshift(new Ripple(x, y, 2, ctx));
};
const animation = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const length = ripples.length;
for (let i = length - 1; i >= 0; i -= 1) {
const r = ripples[i];
r.update();
r.draw();
if (r.opacity <= 0) {
ripples[i] = null;
delete ripples[i];
ripples.pop();
}
}
animationFrame = window.requestAnimationFrame(animation);
};
animation();
canvas.addEventListener('mousemove', canvasMouseOver);
})
I removed the part of GUI based on the link because I don't need the settings. So what I want here is the getContext('2d') I don't understand why it is wrong. Can anyone figure out it for me? Should I be asking this question? I feel so unlucky right now hahha badly need help.
I have made a grid and need to check whether a cell has a or several bombs around it, but I'm a bit confused on how to do it now. I have tried this code;
function placeNumbers() {
for (let x = -ColumnRow; x < ColumnRow * 3; x += ColumnRow) {
for (let y = -ColumnRow; y < ColumnRow * 3; y += ColumnRow) {
if (cells[x].bomb == false) {
//do something
}
}
}
}
but then it says that I can't use .bomb. What can I do? I have made a class Cell which has a bomb feature and if a bomb is on the cell then the cell should have bomb = true. So then i have to check wether the neighbour of a cell does has the bomb true or false?
Does anyone have any tips or know what to do here?
Here's a sample of the code:
const canvas = document.getElementById("myCanvas")
const ctx = canvas.getContext("2d")
class Cell {
constructor(x, y, w) {
this.x = x
this.y = y
this.w = w
this.bomb = false
this.revealed = false
}
show() {
const cell = new Path2D();
cell.rect(this.x, this.y, this.w, this.w);
ctx.stroke(cell);
this.cell = cell;
}
}
const w = canvas.width
const h = canvas.height
const ColumnRow = w / 15
const cells = []
const bombs = 10
let checked = true
let bombPosition = []
function setup() {
for (let x = 0; x < w - 1; x += ColumnRow) {
for (let y = 0; y < h - 1; y += ColumnRow) {
cells.push(new Cell(x, y, ColumnRow))
}
}
}
function drawCells() {
for (let c of cells) {
c.show()
}
}
function numOfBombs() {
for (let i = 0; i < bombs; i++) {
randomX = Math.floor(Math.random() * w / ColumnRow) * ColumnRow
randomY = Math.floor(Math.random() * h / ColumnRow) * ColumnRow
bombPosition.push({ x: randomX, y: randomY });
}
}
function drawBomb() {
let img = new Image();
img.onload = function () {
for (let i = 0; i < bombPosition.length; i++) {
ctx.drawImage(img, bombPosition[i].x, bombPosition[i].y, ColumnRow, ColumnRow)
}
};
img.src = "https://raw.githubusercontent.com/americosp/Minesweeper/master/Minesweeper/images/mine.png";
}
function bombCollision(cell) {
for (let i = 0; i < bombPosition.length; i++) {
if (cell.x == bombPosition[i].x && cell.y == bombPosition[i].y) {
console.log("same position");
}
}
}
canvas.addEventListener('click', function (e) {
for (const cell of cells) {
if (ctx.isPointInPath(cell.cell, e.offsetX, e.offsetY)) {
ctx.clearRect(cell.x, cell.y, cell.w, cell.w);
checked = true
cell.revealed = true
bombCollision(cell)
} else {
/* ctx.clearRect(cell.x, cell.y, cell.w, cell.w); */
}
}
});
function update() {
drawCells()
if (checked) {
drawBomb()
// coverCell()
checked = false
}
requestAnimationFrame(update)
}
function update2() {
numOfBombs()
setup()
}
update2()
update()
<canvas id="myCanvas" width="600" height="600"></canvas>
My friend and I are working on a small project, but are struggling with bin-packing problem using canvas drawing, to let the customers imagine, what they're ordering.
It's basically a PCB order form. What we are struggling with is that the canvas is somehow drawing "stairs" in the middle of the image, when adding patterns.
I can find no good reason why this is happening.
/*-- CLASSES --*/
let GrowingPacker = function() { };
GrowingPacker.prototype = {
fit: function(blocks) {
var n, node, block, len = blocks.length;
var w = len > 0 ? blocks[0].w : 0;
var h = len > 0 ? blocks[0].h : 0;
this.root = { x: 0, y: 0, w: w, h: h };
for (n = 0; n < len ; n++) {
block = blocks[n];
if (node = this.findNode(this.root, block.w, block.h))
block.fit = this.splitNode(node, block.w, block.h);
else
block.fit = this.growNode(block.w, block.h);
}
},
findNode: function(root, w, h) {
if (root.used)
return this.findNode(root.right, w, h) || this.findNode(root.down, w, h);
else if ((w <= root.w) && (h <= root.h))
return root;
else
return null;
},
splitNode: function(node, w, h) {
node.used = true;
node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h };
node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h };
return node;
},
growNode: function(w, h) {
//var possibleGrowDown = (limit.area.max.height >= this.root.h + h);
var possibleGrowRight = (limit.area.max.width >= this.root.w + w);
var canGrowDown = (w <= this.root.w);
var canGrowRight = (h <= this.root.h);
var shouldGrowDown = possibleGrowRight && canGrowDown && (this.root.w >= (this.root.h + h)); // attempt to keep square-ish by growing down when width is much greater than height
var shouldGrowRight = possibleGrowRight && canGrowRight && (this.root.h >= (this.root.w + w)); // attempt to keep square-ish by growing right when height is much greater than width
if (shouldGrowDown)
return this.growDown(w, h);
else if (shouldGrowRight)
return this.growRight(w, h);
else if (canGrowDown)
return this.growDown(w, h);
else if (canGrowRight)
return this.growRight(w, h);
else
return null; // need to ensure sensible root starting size to avoid this happening
},
growRight: function(w, h) {
this.root = {
used: true,
x: 0,
y: 0,
w: this.root.w + w,
h: this.root.h,
down: this.root,
right: { x: this.root.w, y: 0, w: w, h: this.root.h }
};
var node;
if (node = this.findNode(this.root, w, h))
return this.splitNode(node, w, h);
else
return null;
},
growDown: function(w, h) {
this.root = {
used: true,
x: 0,
y: 0,
w: this.root.w,
h: this.root.h + h,
down: { x: 0, y: this.root.h, w: this.root.w, h: h },
right: this.root
};
var node;
if (node = this.findNode(this.root, w, h))
return this.splitNode(node, w, h);
else
return null;
}
}
class Line {
strokeStyle = '#ddd';
constructor(fX, fY, tX, tY) {
this.fX = fX;
this.fY = fY;
this.tX = tX;
this.tY = tY;
}
get length() {
const hL = Math.pow(this.tX - this.fX, 2);
const vL = Math.pow(this.tY - this.fY, 2);
const l = Math.sqrt(hL + vL);
return l;
}
draw(ctx) {
ctx.beginPath();
ctx.moveTo(this.fX, this.fY);
ctx.lineTo(this.tX, this.tY);
ctx.strokeStyle = this.strokeStyle;
ctx.stroke();
}
}
class Base {
x = 0;
y = 0;
w = 0;
h = 0;
fillStyle = '#e0ede0';
strokeStyle = '#0d0';
constructor() {}
resize(canvas, motifs) {
let x = canvas.width;
let y = canvas.height;
let w = 0;
let h = 0;
if(motifs && motifs.length > 0) {
motifs.forEach((motif) => {
if(motif.x < x) { x = motif.x; }
if(motif.y < y) { y = motif.y; }
if(motif.x + motif.w > x + w) { w = motif.x - x + motif.w; }
if(motif.y + motif.h > y + h) { h = motif.y - y + motif.h; }
});
}
else {
x = 0;
y = 0;
}
this.x = x - form.option.panelization[1].padding;
this.y = y - form.option.panelization[1].padding;
this.w = w + form.option.panelization[1].padding * 2;
this.h = h + form.option.panelization[1].padding * 2;
}
draw(ctx) {
ctx.beginPath();
ctx.rect(this.x, this.y, this.w, this.h);
ctx.fillStyle = this.fillStyle;
ctx.fill();
ctx.strokeStyle = this.strokeStyle;
ctx.stroke();
}
}
class Motif {
name = 'A';
subname = 1;
quantity = 1;
x = 0;
y = 0;
w = 0;
h = 0;
fillStyle = '#ede0e0';
strokeStyle = '#d00';
constructor(id, subname='1') {
this.id = id;
this.subname = subname;
}
get fullName() {
return this.name + this.subname;
}
get id() {
return this.number;
}
set id(id) {
this.number = id;
this.generateName();
}
get area() {
return this.w * this.h;
}
generateName() {
let num = this.number;
let s = '', t;
while(num > 0) {
t = (num - 1) % 26;
s = String.fromCharCode(65 + t) + s;
num = (num - t) / 26 | 0;
}
this.name = s;
}
rotate() {
const w = this.w;
this.w = this.h;
this.h = w;
}
draw(ctx) {
// Rectangle
ctx.beginPath();
ctx.rect(this.x, this.y, this.w, this.h);
ctx.fillStyle = this.fillStyle;
ctx.fill();
ctx.strokeStyle = this.strokeStyle;
ctx.stroke();
// Text
ctx.fillStyle = this.strokeStyle;
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.fullName, this.x + this.w / 2, this.y + this.h / 2);
}
}
class Canvas {
canvas;
ctx;
gridCellSize = 10;
gridLines = [];
base = new Base();
motifs = [];
millingLines = [];
constructor(canvas) {
this.canvas = canvas;
this.ctx = this.canvas.getContext('2d');
// Create grid lines
for(let i = 0; i < this.canvas.width / this.gridCellSize + 1; i++) {
this.gridLines.push(new Line(i * this.gridCellSize, 0, i * this.gridCellSize, this.canvas.height));
}
for(let i = 0; i < this.canvas.height / this.gridCellSize + 1; i++) {
this.gridLines.push(new Line(0, i * this.gridCellSize, this.canvas.width, i * this.gridCellSize));
}
this.update();
}
update(motifs) {
this.motifs = motifs;
this.arrangeMotifs();
this.base.resize(this.canvas, this.motifs);
this.draw();
}
arrangeMotifs() {
if(form.option.panelization[1].motifs.length > 0) {
// Copy motifs
let motifs = [];
form.option.panelization[1].motifs.forEach((motif) => {
if(motif.w > 0 && motif.h > 0) {
for(let i = 0; i < motif.quantity; i++) {
const motifCopy = JSON.parse(JSON.stringify(motif));
const motifCopyKeys = Object.keys(motifCopy);
const motifCopyValues = Object.values(motifCopy);
let newMotif = new Motif(motifCopy.id);
for(let i = 0; i < motifCopyKeys.length; i++) {
newMotif[motifCopyKeys[i]] = motifCopyValues[i];
}
newMotif.subname = i+1;
newMotif.x = 0;
newMotif.y = 0;
if(newMotif.w > newMotif.h) {
newMotif.rotate();
}
// Add milling padding
newMotif.w += limit.milling;
newMotif.h += limit.milling;
motifs.push(newMotif);
}
}
});
// Place motifs
const baseW = limit.area.max.width - limit.milling - form.option.panelization[1].padding * 2;
const baseH = limit.area.max.height - limit.milling - form.option.panelization[1].padding * 2;
this.motifs = this.placeMotifs(motifs, baseW, baseH);
}
}
placeMotifs(motifs, baseW, baseH) {
// Sort by area
motifs.sort((a, b) => b.area - a.area);
// Sort by max(width, height)
motifs.sort((a, b) => Math.max(b.w, b.h) - Math.max(a.w, a.h));
// Packing motifs
const packer = new GrowingPacker();
packer.fit(motifs);
const finalMotifs = [];
motifs.forEach((motif) => {
const motifCopy = JSON.parse(JSON.stringify(motif));
const motifCopyKeys = Object.keys(motifCopy);
const motifCopyValues = Object.values(motifCopy);
let newMotif = new Motif(motifCopy.id);
for(let i = 0; i < motifCopyKeys.length; i++) {
newMotif[motifCopyKeys[i]] = motifCopyValues[i];
}
// Fix x & y
if(motif.fit) {
newMotif.x = motif.fit.x;
newMotif.y = motif.fit.y;
}
// Add padding
newMotif.x += form.option.panelization[1].padding;
newMotif.y += form.option.panelization[1].padding;
// Milling
newMotif.w -= limit.milling;
newMotif.h -= limit.milling;
finalMotifs.push(newMotif);
});
return finalMotifs;
}
draw() {
// Clear
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Grid lines
if(this.gridLines && this.gridLines.length > 0) {
this.gridLines.forEach((line) => {
line.draw(this.ctx);
});
}
// Text
this.ctx.fillStyle = '#888';
this.ctx.font = '12px Arial';
this.ctx.textAlign = 'right';
this.ctx.textBaseline = 'baseline';
this.ctx.fillText(this.canvas.width + 'x' + this.canvas.height + 'mm', this.canvas.width - 10, this.canvas.height - 10);
// Base
this.base.draw(this.ctx);
// Motifs
if(this.motifs && this.motifs.length > 0) {
this.motifs.forEach((motif) => {
motif.draw(this.ctx);
});
}
// Milling lines
if(this.millingLines && this.millingLines.length > 0) {
this.millingLines.forEach((millingLine) => {
millingLine.draw(this.ctx);
});
}
}
}
Live preview:
http://jadvo.eu/
Panelized pattern -> Add pattern (Keep clicking it to add more patterns and you will see what's the problem)
I have been trying to learn filters in javascript, i have been following
https://www.html5rocks.com/en/tutorials/canvas/imagefilters/
this tutorial.
I came across some of code i don't get, can some body help me understanding these codes.
Filters.convolute = function(pixels, weights, opaque) {
var side = Math.round(Math.sqrt(weights.length));
var halfSide = Math.floor(side/2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
// pad output by the convolution matrix
var w = sw;
var h = sh;
var output = Filters.createImageData(w, h);
var dst = output.data;
// go through the destination image pixels
var alphaFac = opaque ? 1 : 0;
for (var y=0; y<h; y++) {
for (var x=0; x<w; x++) {
var sy = y;
var sx = x;
var dstOff = (y*w+x)*4;
// calculate the weighed sum of the source image pixels that
// fall under the convolution matrix
var r=0, g=0, b=0, a=0;
for (var cy=0; cy<side; cy++) {
for (var cx=0; cx<side; cx++) {
var scy = sy + cy - halfSide;
var scx = sx + cx - halfSide;
if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
var srcOff = (scy*sw+scx)*4;
var wt = weights[cy*side+cx];
r += src[srcOff] * wt;
g += src[srcOff+1] * wt;
b += src[srcOff+2] * wt;
a += src[srcOff+3] * wt;
}
}
}
dst[dstOff] = r;
dst[dstOff+1] = g;
dst[dstOff+2] = b;
dst[dstOff+3] = a + alphaFac*(255-a);
}
}
return output;
};
what is side and halfSide and why 4 for nested loop is used for. i am stuck here like many days.
I do, like you the same thing, I am trying to implement convolution filters using Javascript - TypeScript.
The reason why is 4 is because we have r, g, b, a
where r = red
where g = green
where b = blue
where a = alpha
this image data is inside an array of type Uint8ClampedArray
you get this information with this way:
const width = canvas.width;
const height = canvas.height;
const imageData = ctx.getImageData(0, 0, width, height);
and then to get the real image data:
const pixels = imageData.data;
The pixel data is a type of Uint8ClampedArray and can be represented like this:
[r, g, b, a, r, g, b, a, r, g, b, a ]
and every 4 elements in the array you get the pixel index and every 1,5 times you get the kernel Center but this depends on the kernel size 3x3 or 9x9
const image = imageData.data
The only code is working for me is this.
init() {
const img = new Image();
const img2 = new Image();
img.src = '../../../assets/graffiti.jpg';
img2.src = '../../../assets/graffiti.jpg';
const canvas: HTMLCanvasElement = this.canvas1.nativeElement;
const canvas2: HTMLCanvasElement = this.canvas2.nativeElement;
const ctx: CanvasRenderingContext2D = canvas.getContext('2d');
const ctx2: CanvasRenderingContext2D = canvas2.getContext('2d');
this.onImgLoad(img, ctx, canvas.width, canvas.height);
this.input(img2, ctx2, canvas2.width, canvas2.height);
}
input(img, ctx: CanvasRenderingContext2D, width, height) {
img.onload = () => {
ctx.drawImage(img, 0, 0);
};
}
onImgLoad(img, ctx: CanvasRenderingContext2D, width, height) {
img.onload = () => {
ctx.drawImage(img, 0, 0);
const kernelArr = new Kernel([
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
]);
const kernel = [
0, 1, 0,
0, 1, 0,
0, 1, 0
];
console.log(kernel);
const newImg = new Filter2D(ctx, width, height);
// const imgData = newImg.inverse(width, height); // applys inverse filter
const imgData = newImg.applyKernel(kernel);
ctx.putImageData(imgData, 0, 0);
};
}
class Filter2D {
width: number;
height: number;
ctx: CanvasRenderingContext2D;
imgData: ImageData;
constructor(ctx: CanvasRenderingContext2D, width: number, height: number) {
this.width = width;
this.height = height;
this.ctx = ctx;
this.imgData = ctx.getImageData(0, 0, width, height);
console.log(this.imgData);
}
grey(width: number, height: number): ImageData {
return this.imgData;
}
inverse(width: number, height: number): ImageData {
console.log('Width: ', width);
console.log('Height: ', height);
const pixels = this.imgData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = 255 - pixels[i]; // red
pixels[i + 1] = 255 - pixels[i + 1]; // green
pixels[i + 2] = 255 - pixels[i + 2]; // blue
}
return this.imgData;
}
applyKernel(kernel: any[]): ImageData {
const k1: number[] = [
1, 0, -1,
2, 0, -2,
1, 0, -1
];
const k2: number[] = [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
];
kernel = k2;
const dim = Math.sqrt(kernel.length);
const pad = Math.floor(dim / 2);
const pixels: Uint8ClampedArray = this.imgData.data;
const width: number = this.imgData.width;
const height: number = this.imgData.height;
console.log(this.imgData);
console.log('applyKernelMethod start');
console.log('Width: ', width);
console.log('Height: ', height);
console.log('kernel: ', kernel);
console.log('dim: ', dim); // 3
console.log('pad: ', pad); // 1
console.log('dim % 2: ', dim % 2); // 1
console.log('pixels: ', pixels);
if (dim % 2 !== 1) {
console.log('Invalid kernel dimension');
}
let pix, i, r, g, b;
const w = width;
const h = height;
const cw = w + pad * 2; // add padding
const ch = h + pad * 2;
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
r = 0;
g = 0;
b = 0;
for (let kx = -pad; kx <= pad; kx++) {
for (let ky = -pad; ky <= pad; ky++) {
i = (ky + pad) * dim + (kx + pad); // kernel index
pix = 4 * ((row + ky) * cw + (col + kx)); // image index
r += pixels[pix++] * kernel[i];
g += pixels[pix++] * kernel[i];
b += pixels[pix ] * kernel[i];
}
}
pix = 4 * ((row - pad) * w + (col - pad)); // destination index
pixels[pix++] = r;
pixels[pix++] = g;
pixels[pix++] = b;
pixels[pix ] = 255; // we want opaque image
}
}
console.log(pixels);
return this.imgData;
}
}
I'm developing a tool for modifying different geometrical shapes from an assortment of templates. The shapes are basic ones that could be found in rooms.
For example: L-shape, T-shape, Hexagon, Rectangle etc.
What I need to do is making the shape conform all necessary edges as to keep the shape's symmetry and bounding dimensions intact when the user modifies an edge.
A shape is simply implemented like this, with the first node starting in the upper left corner and going around the shape clockwise (I use TypeScript):
public class Shape {
private nodes: Array<Node>;
private scale: number; // Scale for calculating correct coordinate compared to given length
... // A whole lot of transformation methods
Which then is drawn as a graph, connecting each node to the next in the array. (See below)
If I, for example, would change the length of edge C to 3m from 3.5m, then I'd also want either edge E or G to change their length to keep the side to 12m and also push down E so that edge D is still fully horizontal.
If I instead would change side D to 2m, then B would have to change its length to 10m and so on.
(I do have shapes which have slanted angles as well, like a rectangle with one of its corners cut off)
The problem
I have the following code for modifying the specific edge:
public updateEdgeLength(start: Point, length: number): void {
let startNode: Node;
let endNode: Node;
let nodesSize = this.nodes.length;
// Find start node, and then select end node of selected edge.
for (let i = 0; i < nodesSize; i++) {
if (this.nodes[i].getX() === start.x && this.nodes[i].getY() === start.y) {
startNode = this.nodes[i];
endNode = this.nodes[(i + 1) % nodesSize];
break;
}
}
// Calculate linear transformation scalar and create a vector of the edge
let scaledLength = (length * this.scale);
let edge: Vector = Vector.create([endNode.getX() - startNode.getX(), endNode.getY() - startNode.getY()]);
let scalar = scaledLength / startNode.getDistance(endNode);
edge = edge.multiply(scalar);
// Translate the new vector to its correct position
edge = edge.add([startNode.getX(), startNode.getY()]);
// Calculate tranlation vector
edge = edge.subtract([endNode.getX(), endNode.getY()]);
endNode.translate({x: edge.e(1), y: edge.e(2)});
}
Now I need a more general case for finding the corresponding edges that will also need to be modified. I have begun implementing shape-specific algorithms as I know which nodes correspond to the edges of the shape, but this won't be very extensible for the future.
For example, the shape above could be implemented somewhat like this:
public updateSideLength(edge: Position): void {
// Get start node coordinates
let startX = edge.start.getX();
let startY = edge.start.getY();
// Find index of start node;
let index: num;
for (let i = 0; i < this.nodes.length; i++) {
let node: Node = this.nodes[i];
if(node.getX() === startX && node.getY() === startY) {
index = i;
break;
}
}
// Determine side
let side: number;
if (index === 0 || index === 2) {
side = this.TOP;
}
else if (index === 1 || index === 3 || index === 5) {
side = this.RIGHT;
}
else if (index === 4 || index === 6) {
side = this.BOTTOM;
}
else if (index === 7) {
side = this.LEFT;
}
adaptSideToBoundingBox(index, side); // adapts all other edges of the side except for the one that has been modified
}
public adaptSideToBoundingBox(exceptionEdge: number, side: number) {
// Modify all other edges
// Example: C and G will be modified
Move C.end Y-coord to D.start Y-coord;
Move G.start Y-coord to D.end Y-coord;
}
And so on.. But implementing this for each shape (5 atm.) and for future shapes would be very time consuming.
So what I'm wondering is if there is a more general approach to this problem?
Thanks!
Keep a list of point pairs and the key that constrains them and use that to overwrite coordinates on update.
This works with the example you gave:
var Point = (function () {
function Point(x, y, connectedTo) {
if (connectedTo === void 0) { connectedTo = []; }
this.x = x;
this.y = y;
this.connectedTo = connectedTo;
}
return Point;
}());
var Polygon = (function () {
function Polygon(points, constrains) {
if (constrains === void 0) { constrains = []; }
this.points = points;
this.constrains = constrains;
}
return Polygon;
}());
var Sketch = (function () {
function Sketch(polygons, canvas) {
if (polygons === void 0) { polygons = []; }
if (canvas === void 0) { canvas = document.body.appendChild(document.createElement("canvas")); }
this.polygons = polygons;
this.canvas = canvas;
this.canvas.width = 1000;
this.canvas.height = 1000;
this.ctx = this.canvas.getContext("2d");
this.ctx.fillStyle = "#0971CE";
this.ctx.strokeStyle = "white";
this.canvas.onmousedown = this.clickHandler.bind(this);
this.canvas.onmouseup = this.clickHandler.bind(this);
this.canvas.onmousemove = this.clickHandler.bind(this);
requestAnimationFrame(this.draw.bind(this));
}
Sketch.prototype.clickHandler = function (evt) {
if (evt.type == "mousedown") {
if (this.selectedPoint != void 0) {
this.selectedPoint = null;
}
else {
var score = null;
var best = null;
for (var p = 0; p < this.polygons.length; p++) {
var polygon = this.polygons[p];
for (var pi = 0; pi < polygon.points.length; pi++) {
var point = polygon.points[pi];
var dist = Math.abs(point.x - evt.offsetX) + Math.abs(point.y - evt.offsetY);
if (score == null ? true : dist < score) {
score = dist;
best = point;
}
}
}
this.selectedPoint = best;
}
}
if (evt.type == "mousemove" && this.selectedPoint != void 0) {
this.selectedPoint.x = Math.round(evt.offsetX / 5) * 5;
this.selectedPoint.y = Math.round(evt.offsetY / 5) * 5;
for (var pi = 0; pi < this.polygons.length; pi++) {
var polygon = this.polygons[pi];
if (polygon.points.indexOf(this.selectedPoint) < 0) {
continue;
}
for (var pa = 0; pa < polygon.constrains.length; pa++) {
var constrain = polygon.constrains[pa];
if (constrain.a == this.selectedPoint || constrain.b == this.selectedPoint) {
constrain.a[constrain.key] = this.selectedPoint[constrain.key];
constrain.b[constrain.key] = this.selectedPoint[constrain.key];
if (constrain.offset != void 0) {
if (constrain.a == this.selectedPoint) {
constrain.b[constrain.key] += constrain.offset;
}
else {
constrain.a[constrain.key] -= constrain.offset;
}
}
}
}
}
}
requestAnimationFrame(this.draw.bind(this));
};
Sketch.prototype.draw = function () {
var ctx = this.ctx;
//clear
ctx.fillStyle = "#0971CE";
ctx.fillRect(0, 0, 1000, 1000);
//grid
ctx.strokeStyle = "rgba(255,255,255,0.25)";
for (var x = 0; x <= this.canvas.width; x += 5) {
ctx.beginPath();
ctx.moveTo(x, -1);
ctx.lineTo(x, this.canvas.height);
ctx.stroke();
ctx.closePath();
}
for (var y = 0; y <= this.canvas.height; y += 5) {
ctx.beginPath();
ctx.moveTo(-1, y);
ctx.lineTo(this.canvas.width, y);
ctx.stroke();
ctx.closePath();
}
ctx.strokeStyle = "white";
ctx.fillStyle = "white";
//shapes
for (var i = 0; i < this.polygons.length; i++) {
var polygon = this.polygons[i];
for (var pa = 0; pa < polygon.points.length; pa++) {
var pointa = polygon.points[pa];
if (pointa == this.selectedPoint) {
ctx.beginPath();
ctx.fillRect(pointa.x - 2, pointa.y - 2, 4, 4);
ctx.closePath();
}
ctx.beginPath();
for (var pb = 0; pb < pointa.connectedTo.length; pb++) {
var pointb = pointa.connectedTo[pb];
if (polygon.points.indexOf(pointb) < pa) {
continue;
}
ctx.moveTo(pointa.x, pointa.y);
ctx.lineTo(pointb.x, pointb.y);
}
ctx.stroke();
ctx.closePath();
}
}
};
return Sketch;
}());
//==Test==
//Build polygon 1 (House)
var poly1 = new Polygon([
new Point(10, 10),
new Point(80, 10),
new Point(80, 45),
new Point(130, 45),
new Point(130, 95),
new Point(80, 95),
new Point(80, 135),
new Point(10, 135),
]);
//Connect dots
for (var x = 0; x < poly1.points.length; x++) {
var a = poly1.points[x];
var b = poly1.points[(x + 1) % poly1.points.length];
a.connectedTo.push(b);
b.connectedTo.push(a);
}
//Setup constrains
for (var x = 0; x < poly1.points.length; x++) {
var a = poly1.points[x];
var b = poly1.points[(x + 1) % poly1.points.length];
poly1.constrains.push({ a: a, b: b, key: x % 2 == 1 ? 'x' : 'y' });
}
poly1.constrains.push({ a: poly1.points[1], b: poly1.points[5], key: 'x' }, { a: poly1.points[2], b: poly1.points[5], key: 'x' }, { a: poly1.points[1], b: poly1.points[6], key: 'x' }, { a: poly1.points[2], b: poly1.points[6], key: 'x' });
//Build polygon 2 (Triangle)
var poly2 = new Polygon([
new Point(250, 250),
new Point(300, 300),
new Point(200, 300),
]);
//Connect dots
for (var x = 0; x < poly2.points.length; x++) {
var a = poly2.points[x];
var b = poly2.points[(x + 1) % poly2.points.length];
a.connectedTo.push(b);
b.connectedTo.push(a);
}
//Setup constrains
poly2.constrains.push({ a: poly2.points[0], b: poly2.points[1], key: 'x', offset: 50 }, { a: poly2.points[0], b: poly2.points[1], key: 'y', offset: 50 });
//Generate sketch
var s = new Sketch([poly1, poly2]);
<!-- TYPESCRIPT -->
<!--
class Point {
constructor(public x: number, public y: number, public connectedTo: Point[] = []) {
}
}
interface IConstrain {
a: Point,
b: Point,
key: string,
offset?: number
}
class Polygon {
constructor(public points: Point[], public constrains: IConstrain[] = []) {
}
}
class Sketch {
public ctx: CanvasRenderingContext2D;
constructor(public polygons: Polygon[] = [], public canvas = document.body.appendChild(document.createElement("canvas"))) {
this.canvas.width = 1000;
this.canvas.height = 1000;
this.ctx = this.canvas.getContext("2d");
this.ctx.fillStyle = "#0971CE";
this.ctx.strokeStyle = "white";
this.canvas.onmousedown = this.clickHandler.bind(this)
this.canvas.onmouseup = this.clickHandler.bind(this)
this.canvas.onmousemove = this.clickHandler.bind(this)
requestAnimationFrame(this.draw.bind(this))
}
public selectedPoint: Point
public clickHandler(evt: MouseEvent) {
if (evt.type == "mousedown") {
if (this.selectedPoint != void 0) {
this.selectedPoint = null;
} else {
let score = null;
let best = null;
for (let p = 0; p < this.polygons.length; p++) {
let polygon = this.polygons[p];
for (let pi = 0; pi < polygon.points.length; pi++) {
let point = polygon.points[pi];
let dist = Math.abs(point.x - evt.offsetX) + Math.abs(point.y - evt.offsetY)
if (score == null ? true : dist < score) {
score = dist;
best = point;
}
}
}
this.selectedPoint = best;
}
}
if (evt.type == "mousemove" && this.selectedPoint != void 0) {
this.selectedPoint.x = Math.round(evt.offsetX / 5) * 5;
this.selectedPoint.y = Math.round(evt.offsetY / 5) * 5;
for (let pi = 0; pi < this.polygons.length; pi++) {
let polygon = this.polygons[pi];
if (polygon.points.indexOf(this.selectedPoint) < 0) {
continue;
}
for (let pa = 0; pa < polygon.constrains.length; pa++) {
let constrain = polygon.constrains[pa];
if (constrain.a == this.selectedPoint || constrain.b == this.selectedPoint) {
constrain.a[constrain.key] = this.selectedPoint[constrain.key]
constrain.b[constrain.key] = this.selectedPoint[constrain.key]
if (constrain.offset != void 0) {
if (constrain.a == this.selectedPoint) {
constrain.b[constrain.key] += constrain.offset
} else {
constrain.a[constrain.key] -= constrain.offset
}
}
}
}
}
}
requestAnimationFrame(this.draw.bind(this))
}
public draw() {
var ctx = this.ctx;
//clear
ctx.fillStyle = "#0971CE";
ctx.fillRect(0, 0, 1000, 1000)
//grid
ctx.strokeStyle = "rgba(255,255,255,0.25)"
for (let x = 0; x <= this.canvas.width; x += 5) {
ctx.beginPath()
ctx.moveTo(x, -1)
ctx.lineTo(x, this.canvas.height)
ctx.stroke();
ctx.closePath()
}
for (let y = 0; y <= this.canvas.height; y += 5) {
ctx.beginPath()
ctx.moveTo(-1, y)
ctx.lineTo(this.canvas.width, y)
ctx.stroke();
ctx.closePath()
}
ctx.strokeStyle = "white"
ctx.fillStyle = "white";
//shapes
for (let i = 0; i < this.polygons.length; i++) {
let polygon = this.polygons[i];
for (let pa = 0; pa < polygon.points.length; pa++) {
let pointa = polygon.points[pa];
if (pointa == this.selectedPoint) {
ctx.beginPath();
ctx.fillRect(pointa.x - 2, pointa.y - 2, 4, 4)
ctx.closePath();
}
ctx.beginPath();
for (var pb = 0; pb < pointa.connectedTo.length; pb++) {
var pointb = pointa.connectedTo[pb];
if (polygon.points.indexOf(pointb) < pa) {
continue;
}
ctx.moveTo(pointa.x, pointa.y)
ctx.lineTo(pointb.x, pointb.y)
}
ctx.stroke();
ctx.closePath();
}
}
}
}
//==Test==
//Build polygon 1 (House)
var poly1 = new Polygon([
new Point(10, 10),
new Point(80, 10),
new Point(80, 45),
new Point(130, 45),
new Point(130, 95),
new Point(80, 95),
new Point(80, 135),
new Point(10, 135),
])
//Connect dots
for (let x = 0; x < poly1.points.length; x++) {
let a = poly1.points[x];
let b = poly1.points[(x + 1) % poly1.points.length]
a.connectedTo.push(b)
b.connectedTo.push(a)
}
//Setup constrains
for (let x = 0; x < poly1.points.length; x++) {
let a = poly1.points[x];
let b = poly1.points[(x + 1) % poly1.points.length]
poly1.constrains.push({ a: a, b: b, key: x % 2 == 1 ? 'x' : 'y' })
}
poly1.constrains.push(
{ a: poly1.points[1], b: poly1.points[5], key: 'x' },
{ a: poly1.points[2], b: poly1.points[5], key: 'x' },
{ a: poly1.points[1], b: poly1.points[6], key: 'x' },
{ a: poly1.points[2], b: poly1.points[6], key: 'x' }
)
//Build polygon 2 (Triangle)
var poly2 = new Polygon([
new Point(250, 250),
new Point(300, 300),
new Point(200, 300),
])
//Connect dots
for (let x = 0; x < poly2.points.length; x++) {
let a = poly2.points[x];
let b = poly2.points[(x + 1) % poly2.points.length]
a.connectedTo.push(b)
b.connectedTo.push(a)
}
//Setup constrains
poly2.constrains.push(
{ a: poly2.points[0], b: poly2.points[1], key: 'x', offset: 50 },
{ a: poly2.points[0], b: poly2.points[1], key: 'y', offset: 50 },
)
//Generate sketch
var s = new Sketch([poly1, poly2])
-->
UPDATE - Constrain offsets
Based on feedback in the comments i added a "offset" key in the constrains to handle uneven relationships.
The Triangles top-right-most edge (at least initially) is constrained with an offset.