I am getting a figure from an imageData that looks like this:
How could I get the edges of that figure? so that it looks more or less like this:
I have tried to validate for each pixel that the previous, next, top and bottom pixel is transparent in order to save that pixel and then draw on another canvas
var context = canvas.getContext("2d");
var pixelData = context.getImageData(0, 0, canvas.width, canvas.height);
var result = []
for (let index = 0; index < pixelData.data.length; index += 4) {
if (pixelData.data[index] != 0) {
if ((pixelData.data[index + canvas.width * 4]) == 0 || (pixelData.data[index - canvas.width * 4]) == 0 || pixelData.data[index - 4] == 0 || pixelData.data[index + 4] == 0) {
var x = index / 4 % canvas.width
var y = (index / 4 - x) / canvas.width;
result.push({ x: x, y: y })
}
}
}
context.clearRect(0, 0, canvas.width, canvas.height)
context.beginPath()
result.forEach(point => {
context.lineTo(point.x, point.y)
})
context.strokeStyle = "#000"
context.stroke()
But this does not seem to work very well.
How could I obtain the edge or, failing that, the polygon that is formed in that figure?
Expanding on my question in the comments ...
Your if statement is detecting sequentially, so drawing lines on the order it was detected will not have the desired effect.
Below is some sample code, Just drawing letter "W" and apply your algorithm to detect the edge, I added some transparency so you can see the output of your lines in action, it should look like:
var canvas = document.getElementById('canvas');
var context = canvas.getContext("2d");
context.font = "99px Arial";
context.fillStyle = "red"
context.fillText("W", 20, 70)
var pixelData = context.getImageData(0, 0, canvas.width, canvas.height);
var result = []
for (let index = 0; index < pixelData.data.length; index += 4) {
if (pixelData.data[index] != 0) {
if ((pixelData.data[index + canvas.width * 4]) == 0 || (pixelData.data[index - canvas.width * 4]) == 0 || pixelData.data[index - 4] == 0 || pixelData.data[index + 4] == 0) {
var x = index / 4 % canvas.width
var y = (index / 4 - x) / canvas.width;
result.push({ x, y })
}
}
}
context.strokeStyle = "blue"
context.globalAlpha = 0.5
context.beginPath()
result.forEach(point => {
context.lineTo(point.x, point.y)
})
context.stroke()
<canvas id="canvas" width=100 height=100></canvas>
...But if instead of lines we draw small dots, we get a very different picture.
var canvas = document.getElementById('canvas');
var context = canvas.getContext("2d");
context.font = "99px Arial";
context.fillStyle = "red"
context.fillText("W", 20, 70)
var pixelData = context.getImageData(0, 0, canvas.width, canvas.height);
var result = []
for (let index = 0; index < pixelData.data.length; index += 4) {
if (pixelData.data[index] != 0) {
if ((pixelData.data[index + canvas.width * 4]) == 0 || (pixelData.data[index - canvas.width * 4]) == 0 || pixelData.data[index - 4] == 0 || pixelData.data[index + 4] == 0) {
var x = index / 4 % canvas.width
var y = (index / 4 - x) / canvas.width;
result.push({ x, y })
}
}
}
context.strokeStyle = "blue"
context.globalAlpha = 0.5
result.forEach(point => {
context.beginPath()
context.arc(point.x, point.y, 1, 0, 2 * Math.PI)
context.stroke()
})
<canvas id="canvas" width=100 height=100></canvas>
You do have the edge in the var result but that is not a polygon, so you can not do lines between the points or you get something else like we see in the first code snippet. If you want to transform that into a polygon, you will have to sort that array by the points proximity to each other, but that is another question...
Related
I'm trying to make an animation inside a canvas: here, a numbered circle must be drawn and move from left to right one single time, disappearing as soon as it reaches the end of animation.
For now I managed to animate it in loop, but I need to animate at the same time (or with a set delay) multiple numbered circles, strating in different rows (changing y position) so they wont overlap.
Any idea how can I manage this? my JS code is the following:
// Single Animated Circle - Get Canvas element by Id
var canvas = document.getElementById("canvas");
// Set Canvas dimensions
canvas.width = 300;
canvas.height = 900;
// Get drawing context
var ctx = canvas.getContext("2d");
// Radius
var radius = 13;
// Starting Position
var x = radius;
var y = radius;
// Speed in x and y direction
var dx = 1;
var dy = 0;
// Generate random number
var randomNumber = Math.floor(Math.random() * 60) + 1;
if (randomNumber > 0 && randomNumber <= 10) {
ctx.strokeStyle = "#0b0bf1";
} else if (randomNumber > 10 && randomNumber <= 20) {
ctx.strokeStyle = "#f10b0b";
} else if (randomNumber > 20 && randomNumber <= 30) {
ctx.strokeStyle = "#0bf163";
} else if (randomNumber > 30 && randomNumber <= 40) {
ctx.strokeStyle = "#f1da0b";
} else if (randomNumber > 40 && randomNumber <= 50) {
ctx.strokeStyle = "#950bf1";
} else if (randomNumber > 50 && randomNumber <= 60) {
ctx.strokeStyle = "#0bf1e5";
}
function animate3() {
requestAnimationFrame(animate3);
ctx.clearRect(0, 0, 300, 900);
if (x + radius > 300 || x - radius < 0) {
x = radius;
}
x += dx;
ctx.beginPath();
ctx.arc(x, y, 12, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(randomNumber, x - 5, y + 3);
}
// Animate the Circle
animate3();
<canvas id="canvas"></canvas>
Here is a solution which doesn't use classes as such and separates the animation logic from the updating - which can be useful if you want more precise control over timing.
// Some helper functions
const clamp = (number, min, max) => Math.min(Math.max(number, min), max);
// Choose and remove random member of arr with equal probability
const takeRandom = arr => arr.splice(parseInt(Math.random() * arr.length), 1)[0]
// Call a function at an interval, passing the amount of time that has passed since the last call
function update(callBack, interval) {
let now = performance.now();
let last;
setInterval(function() {
last = now;
now = performance.now();
callBack((now - last) / 1000);
})
}
const settings = {
width: 300,
height: 150,
radius: 13,
gap: 5,
circles: 5,
maxSpeed: 100,
colors: ["#0b0bf1", "#f10b0b", "#0bf163", "#f1da0b", "#950bf1", "#0bf1e5"]
};
const canvas = document.getElementById("canvas");
canvas.width = settings.width;
canvas.height = settings.height;
const ctx = canvas.getContext("2d");
// Set circle properties
const circles = [...Array(settings.circles).keys()].map(i => ({
number: i + 1,
x: settings.radius,
y: settings.radius + (settings.radius * 2 + settings.gap) * i,
radius: settings.radius,
dx: settings.maxSpeed * Math.random(), // This is the speed in pixels per second
dy: 0,
color: takeRandom(settings.colors)
}));
function drawCircle(circle) {
ctx.strokeStyle = circle.color;
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(circle.number, circle.x - 5, circle.y + 3);
}
function updateCircle(circle, dt) {
// Update a circle's position after dt seconds
circle.x = clamp(circle.x + circle.dx * dt, circle.radius + 1, settings.width - circle.radius - 1);
circle.y = clamp(circle.y + circle.dy * dt, circle.radius + 1, settings.height - circle.radius - 1);
}
function animate() {
ctx.clearRect(0, 0, settings.width, settings.height);
circles.forEach(drawCircle);
requestAnimationFrame(animate);
}
update(dt => circles.forEach(circle => updateCircle(circle, dt)), 50);
animate();
<canvas id="canvas" style="border: solid 1px black"></canvas>
Here I transformed your sample code into a class ...
We pass all the data as a parameter, you can see that in the constructor, I simplified a lot of your code to keep it really short, but all the same drawing you did is there in the draw function
Then all we need to do is create instances of this class and call the draw function inside that animate3 loop you already have.
You had a hardcoded value on the radius:
ctx.arc(x, y, 12, 0, Math.PI * 2, false)
I assume that was a mistake and fix it on my code
var canvas = document.getElementById("canvas");
canvas.width = canvas.height = 300;
var ctx = canvas.getContext("2d");
class Circle {
constructor(data) {
this.data = data
}
draw() {
if (this.data.x + this.data.radius > 300 || this.data.x - this.data.radius < 0) {
this.data.x = this.data.radius;
}
this.data.x += this.data.dx;
ctx.beginPath();
ctx.arc(this.data.x, this.data.y, this.data.radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(this.data.number, this.data.x - 5, this.data.y + 3);
}
}
circles = []
circles.push(new Circle({radius:13, x: 10, y: 15, dx: 1, dy: 0, number: "1"}))
circles.push(new Circle({radius:10, x: 10, y: 50, dx: 2, dy: 0, number: "2"}))
function animate3() {
requestAnimationFrame(animate3);
ctx.clearRect(0, 0, canvas.width, canvas.height);
circles.forEach(item => item.draw());
}
animate3();
<canvas id="canvas"></canvas>
Code should be easy to follow let me know if you have any questions
The image is what I am looking forward to achieve using html canvas, without using blur or shadow.
Question :
Is there way to achieve right section of the image from left section of the image?
The fiddle for the problem is (basic drawing) here
var canvas = document.createElement('canvas'),
d = canvas.width = canvas.height = 400,
sq_size = d / 10,
ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
var color=["rgb(10,110,10)", "rgb(81,169,255)","rgb(81,239,255)",
"rgb(81,255,202)",
"rgb(81,255,132)","rgb(99,255,81)","rgb(169,255,81)",
"rgb(239,255,81)", "rgb(255,202,81)","rgb(255,132,81)"];
var x=0, len=color.length;
for(var i=0; i < d; i++){
while(x < d) {
var c = Math.floor(Math.random() * len);
ctx.fillStyle = color[c];
ctx.fillRect(x,i,sq_size,sq_size);
x = x + sq_size;
}
x = 0;
i = i+sq_size;
}
The closest you can get without implementing a blur.
You can use image smoothing ctx.imageSmoothingEnabled to blur an image that is very low resolution. Then mix the blurred and unblurred images using ctx.globalAlpha
Example
pixelSize controls the amount of blurring. A value of 1 is the max amount and gets less as this value gets bigger. Must be an integer value eg 1, 2, 3, 4, ...
Note results will vary depending on the device and browser / version used.
requestAnimationFrame(animationLoop);
const size = canvas.width;
const ctx = canvas.getContext('2d');
var mix = 123; // amount to transition
const transitionTime = 2; // seconds per transition
const swatches = [0,1,2];
const pixelSize = 2;
// eCurve p = 1 linear curve [default p = 2] is quadratic curve p = 3 cubic curve and so on.
const eCurve = (v, p = 2) => v < 0 ? 0 : v > 1 ? 1 : v ** p / (v ** p + (1 - v) ** p);
const cols = [
[10, 110, 10], [81, 169, 255], [81, 239, 255], [81, 255, 202], [81, 255, 132],
[99, 255, 81], [169, 255, 81], [239, 255, 81], [255,202, 81], [255,132,81]
];
const img = document.createElement("canvas");
img.height = img.width = swatches.length * pixelSize;
const imgCtx = img.getContext('2d');
function randomSwatch() {
swatches.forEach(y => { swatches.forEach(x => {
imgCtx.fillStyle = "rgb("+cols[Math.random() * cols.length | 0].join(",")+")";
imgCtx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
}); });
}
function animationLoop() {
mix = (mix >= 4 ? (randomSwatch(), 0) : mix) + 1 / (transitionTime * 60);
ctx.globalAlpha = 1;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(img, 0, 0, size, size);
ctx.imageSmoothingEnabled = true;
const a = mix % 2;
ctx.globalAlpha = eCurve(a > 1 ? 2 - a : a, 3);
ctx.drawImage(img, 0, 0, size, size);
requestAnimationFrame(animationLoop);
}
<canvas id="canvas" height="300" width="300"></canvas>
This solution works best for me. Complies with my large dataset.
Not O(1) and I cannot possibly think it can be, or even O(n) (Minor banter).
P.s: Code can be optimised further.
Fiddle here
var canvas = document.createElement('canvas'),
d = canvas.width = canvas.height = 250,
sq_size = d / 5,
ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
canvas.setAttribute('id','cav');
var color=["rgb(10,110,10)", "rgb(81,169,255)","rgb(81,239,255)", "rgb(81,255,202)", "rgb(81,255,132)","rgb(99,255,81)","rgb(169,255,81)", "rgb(239,255,81)", "rgb(255,202,81)","rgb(255,132,81)"];
var x=0, len=color.length;
var prevcolorh;
var colorobj = {};
for(var i=0; i < d;){
colorobj[i] = {};
while(x < d) {
var c = Math.floor(Math.random() * len);
colorobj[i][x] = color[c];
var gradient = ctx.createLinearGradient(x, 0, x+sq_size, 0);
var a = (prevcolorh !== undefined) ? prevcolorh : colorobj[i][x];
gradient.addColorStop(0, a);
gradient.addColorStop(1, colorobj[i][x]);
prevcolorh = colorobj[i][x];
ctx.fillStyle = gradient;
ctx.fillRect(x, i, d, d);
x = x + sq_size;
}
x = 0;
i = i+sq_size;
}
var rgbs = {};
for(var i=0; i<d; i+=sq_size) {
var imgd = ctx.getImageData(0, i+(sq_size/2), d, sq_size);
rgbs[i] = {};
var arr = [];
for (var j = 0, c = 0, n = imgd.data.length; j < n; j += 4, c++) {
if(j > 0 && j < d*4) {
arr.push([imgd.data[j],imgd.data[j+1],imgd.data[j+2],imgd.data[+3]]);
}
}
rgbs[i] = arr;
}
for(var k in rgbs) {
for(var i=0; i<rgbs[k].length; i++) {
if(rgbs[parseInt(k)+sq_size] !== undefined) {
var gradient2 = ctx.createLinearGradient(0, parseInt(k)+(sq_size/2), 0, parseInt(k)+(sq_size/2) + sq_size);
gradient2.addColorStop(0, 'rgba('+rgbs[k][i].join(',')+')');
gradient2.addColorStop(1, 'rgba('+rgbs[parseInt(k)+sq_size][i].join(',')+')');
ctx.fillStyle = gradient2;
ctx.fillRect(i, parseInt(k)+(3*sq_size/4), 1, sq_size/2);
}
}
}
I am trying to draw a box around multiple shapes in canvas to say that those shapes are related like a group.
Tried as below :
var ctx = c.getContext("2d"),
radius = 10,
rect = c.getBoundingClientRect(),
ctx.fillText("Draw something here..", 10, 10);
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(250, 300, radius, 0, 6.28);
ctx.fill();
ctx.fillStyle = "yellow";
ctx.beginPath();
ctx.arc(200, 100, radius, 0, 10.28);
ctx.fill();
ctx.fillStyle = "brown";
ctx.beginPath();
ctx.arc(350, 210, radius, 0, 10.28);
ctx.fill();
var x = (250+200+350)/3;
var y = (300+100+210)/3;
var radius = Math.sqrt((x1*x1)+(y1*y1));
var _minX = x - radius;
var _minY = y - radius;
var _maxX = x + radius;
var _maxY = y + radius;
ctx.rect(_minX,_minY,(_maxX-_minX+2),(_maxY-_minY+2));
ctx.stroke();
But it is not drawing properly.
How to get bounding box coordinates for canvas content? this link explains only for the path not for the existing shapes.
Below is the image how I want to draw:
Fabric <-- See if this library helps had used this for one of my project it is simple and quick.
This Code is not production ready, Or the best solution, but it works in "most cases".
I'm using the imageData, to check for non-white pixel. (If NOT 0 - RGBA Pixel ==> Object) and with this it narrows the possible Rectangle down. You would also need to tweak it, if you don't want the text to be in the Rectangle.
This code could / should be optimized.
EDIT: Now I am only checking if an Alpha Value is set. And some Random Object creation to test multiple Outcomes
Info: Objects that are clipped/cut happen, because they are out of the canvas size.
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
var colors = ["red", "blue", "green", "black"];
ctx.fillText("Draw something here..", 0, 10);
/** CREATEING SOME RANDOM OBJECTS (JUST FOR TEST) **/
createRandomObjects();
function createRandomIntMax(max){
return parseInt(Math.random() * 1000 * max) % max + 1;
}
function createRandomObjects(){
var objectsToDraw = createRandomIntMax(20);
for(var idx = 0; idx < objectsToDraw; idx++){
ctx.fillStyle = colors[createRandomIntMax(colors.length)];
ctx.beginPath();
ctx.arc(createRandomIntMax(c.width), createRandomIntMax(c.height), createRandomIntMax(30), 0, 2 * Math.PI);
ctx.fill();
}
}
/** GETTING IMAGE DATA **/
var myImageData = ctx.getImageData(0, 0, c.width, c.height);
/** FINDING BORDERS **/
findBorders(myImageData.data);
function findBorders(imageData) {
var result = {
left: c.width,
top: c.height,
right: -1,
bottom: -1
}
var idx = 0;
var lineLow = -1;
var lineHigh = -1;
var currentLine = 0;
var currentPoint, helper;
while (idx < imageData.length) {
currentPoint = imageData[idx + 3];
/** CHECKING FOR OBJECT **/
if (currentPoint != 0) {
helper = parseInt(idx % (c.width * 4)) / 4;
if (lineLow < 0) {
lineLow = helper;
lineHigh = helper;
} else {
lineHigh = helper;
}
}
if (idx !== 0 && (idx % (c.width * 4)) === 0) {
currentLine = idx / (c.width * 4);
// Updating the Border Points
if (lineLow > -1) {
result.left = Math.min(lineLow, result.left);
result.top = Math.min(currentLine, result.top);
result.right = Math.max(lineHigh, result.right);
result.bottom = Math.max(currentLine, result.bottom);
}
lineLow = -1;
lineHigh = -1;
}
idx += 4;
}
ctx.rect(result.left, result.top, result.right - result.left, result.bottom - result.top);
ctx.stroke()
}
<canvas id="canvas"></canvas>
USE getBoundingClientRect() to get the exact boundaries
How can I get the black points from a canvas stroke?
Here - https://jsfiddle.net/dsu3Lmfm/2/ - all points from the canvas are black.
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(20, 100);
ctx.lineTo(70, 100);
ctx.closePath();
ctx.stroke();
var imageData = ctx.getImageData(0, 0, c.width, c.height);
var pix = imageData.data;
// Loop over each pixel and get the black pixels
for (var i = 0, n = pix.length; i < n; i += 4) {
if (pix[i] === 0 && pix[i+1] === 0 && pix[i+2] === 0) {
var x = (i / 4) % c.width;
var y = (i / 4) / c.width;
console.log("x=" + parseInt(x, 10) + ", y=" + parseInt(y, 10));
}
}
only one rectangle is being drawn even tho the x and y value are constanly being changed and fillrect(); is in the loop.
var canvas=document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var n = 0;
a = [0,0,0.85,0.2,-0.15],
b = [0,0,0.04,-0.26,0.28],
c = [0,0,-0.04,0.23,0.26],
d = [0,0.16,0.85,0.22,0.24],
f = [0,1.6,1.6,1.6,0.44],
x = 1,
y = 1;
setInterval(function(){
ctx.translate(1400/2, 500/2);
i = Math.random();
if (i <= 0.02 ) n = 1;
else if (i > 0.02 && i < .89) n = 2;
else if(i > .89 && i < .96) n = 3;
else n = 4;
x = a[n] * x + b[n] * y;
y = c[n] * x + d[n] * y + f[n];
ctx.beginPath();
ctx.fillRect( x, y, 1, 1 );
ctx.stroke();
console.log(x, y);
}, 50);
http://jsfiddle.net/13huvske/
I think your problem is that you are setting ctx.translate in each cycle of your loop, which will add the origin offset for each cycle.
You could either set the ctx.translate once outside your loop, or you could (and i makes clearing the canvas easier) do ctx.save() before ctx.translate(), and ctx.restore() at the end of your loop.
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var n = 0,
a = [0, 0, 0.85, 0.2, -0.15],
b = [0, 0, 0.04, -0.26, 0.28],
c = [0, 0, -0.04, 0.23, 0.26],
d = [0, 0.16, 0.85, 0.22, 0.24],
f = [0, 1.6, 1.6, 1.6, 0.44],
x = 1,
y = 1;
setInterval(function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
i = Math.random();
if (i <= 0.02)
n = 1;
else if (i > 0.02 && i < .89)
n = 2;
else if (i > .89 && i < .96)
n = 3;
else
n = 4;
x = a[n] * x + b[n] * y;
y = c[n] * x + d[n] * y + f[n];
ctx.fillStyle = 'red';
ctx.fillRect(x, y, 10, 10);
ctx.restore();
}, 50);
or look at http://jsfiddle.net/72z0f01b/2/
Sorry, i made your rectangle 10x10 instead of 1x1 and red to make it easier to see.
I also added a ctx.clearRect() and removed the ctx.stroke() (which is not needed since you use fillRect()), and i took the width and height from the canvas object instead of hardcoding it (this is a preference thing, it will work without changing that, but now you know thats an option :))
Any more questions? :)
UPDATE: As GameAlchemist mentioned in his comment:
You could also do ctx.translate(-(canvas.width / 2), -(canvas.height / 2)); instead of save() and restore().
It would look like this:
setInterval(function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(canvas.width / 2, canvas.height / 2);
// Your render code
ctx.translate(-(canvas.width / 2), -(canvas.height / 2));
}, 50);
You can use the one you like the most, doing the latter version is probably faster to compute, however save() and restore() might be easier to read and understand.
What i would do is to use the one i think is the easiest to understand, and if your application runs into performance issues then i would start looking for things to improve.