I have designed a Duval's Triangle ( a diagnostic tool ), on SVG,
cosisting of Segments (closed Paths) of different colors.
The diagnostic result will be a coordinate.
Need to detect the resultant coordinate is in which Closed Path ?
Like in the following case the diagnostic result is a RED DOT.
Need to detect the close path i.e in this case : D2
You have several options:
SVG 2 has the isPointInFill() method. Which you can call on each shape to see if a point is within the fill of a path. However I believe only Chrome and Opera have implemented this new feature of SVG2.
https://developer.mozilla.org/en-US/docs/Web/API/SVGGeometryElement/isPointInFill
You can draw your SVG to a Canvas and use the isPointInPath() method.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
You could draw the SVG to a Canvas and use the getImageData() method to get the colour of that particular pixel.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData
And of course you can also do it numerically by calculating it yourself in some way.
This is using Paul LeBeau's answer.
1.
First a demo where you check if the point is in a certain path, the #c path in this case.
Please read the comments in my code.
// creating a new SVG point
let point = svg.createSVGPoint()
point.x = 300;
point.y = 300;
//checking if the point is in the path c
console.log(c.isPointInFill(point));
svg{border:1px solid}
<svg id="svg" viewBox="0 0 606.731 526.504" width="200" >
<polygon id="a" fill="#D9D1E0" stroke="#020202" stroke-miterlimit="10" points="300.862,1.001 0.862,526.001 605.862,526.001 "/>
<polygon id="b" fill="#926BB5" stroke="#020202" stroke-miterlimit="10" points="289.576,19.681 442.34,283.546 411.092,343.437
515.705,526.001 428.453,525.716 337.314,365.138 385.054,280.945 262.668,66.555 "/>
<polygon id="c" fill="#8ED5ED" stroke="#020202" stroke-miterlimit="10" points="334.4,193.005 384.186,280.946 337.315,364.272
428.453,525.716 142.019,525.716 "/>
<circle cx="300" cy="300" r="5" fill="red" />
</svg>
2.
A second demo where I'm drawing the polygons on a canvas and using the method isPointInPath() of the context:
Please read the comments in my code.
let ctx = canv.getContext("2d");
canv.width = 606.731;
canv.height = 526.504;
// the main triangle
let duval = [300.862, 1.001, 0.862, 526.001, 605.862, 526.001];
// the arrays of points for the 2 polygons inside the main triangle
let rys = [
[
289.576,
19.681,
442.34,
283.546,
411.092,
343.437,
515.705,
526.001,
428.453,
525.716,
337.314,
365.138,
385.054,
280.945,
262.668,
66.555
],
[
334.4,
193.005,
384.186,
280.946,
337.315,
364.272,
428.453,
525.716,
142.019,
525.716
]
];
// drawing the polygons
drawPoly(duval, "#D9D1E0");
drawPoly(rys[0], "#926BB5");
drawPoly(rys[1], "#8ED5ED");
// the point to check
let p = { x: 300, y: 300 };
drawPoint(p);
// looping through the array of shapes to check if the point is in path
for (let i = 0; i < rys.length; i++) {
// draw again the polygon without stroking or filling
drawPoly(rys[i]);
//chect if the point is in path
if (ctx.isPointInPath(p.x, p.y)) {
// do something
console.log(i);
// if found break the loop
break;
}
}
// a function to draw a polygon from an array
function drawPoly(ry, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(ry[0], ry[1]);
for (let i = 2; i < ry.length; i += 2) {
ctx.lineTo(ry[i], ry[i + 1]);
}
ctx.closePath();
if (color) {
ctx.fill();
ctx.stroke();
}
}
function drawPoint(p) {
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
ctx.fill();
}
canvas{border:1px solid}
<canvas id="canv"></canvas>
3
This time using the getImageData() method to get the colour of the pixel at the point.
The code is similar to the one in the previous example
let ctx = canv.getContext("2d");
canv.width = 606.731;
canv.height = 526.504;
let duval = [300.862, 1.001, 0.862, 526.001, 605.862, 526.001];
let rys = [
[
289.576,
19.681,
442.34,
283.546,
411.092,
343.437,
515.705,
526.001,
428.453,
525.716,
337.314,
365.138,
385.054,
280.945,
262.668,
66.555
],
[
334.4,
193.005,
384.186,
280.946,
337.315,
364.272,
428.453,
525.716,
142.019,
525.716
]
];
drawPoly(duval, "#D9D1E0");
drawPoly(rys[0], "#926BB5");
drawPoly(rys[1], "#8ED5ED");
function drawPoly(ry, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(ry[0], ry[1]);
for (let i = 2; i < ry.length; i += 2) {
ctx.lineTo(ry[i], ry[i + 1]);
}
ctx.closePath();
if (color) {
ctx.fill();
ctx.stroke();
}
}
// HERE BEGINS THE IMPORTANT PART
let imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
let p = { x: 300, y: 300 };
// mark the point with an empty circle
ctx.beginPath();
ctx.arc(p.x,p.y,5,0,2*Math.PI);
ctx.stroke();
// the index of the point p in the imgData.data array
let index = (p.y*imgData.width + p.x)*4;
//the red,green and blue components of the color of the pixel at the index
let r = imgData.data[index];
let g = imgData.data[index + 1];
let b = imgData.data[index + 2];
//test the color
test.style.background = `rgb(${r},${g},${b})`;
canvas{border:1px solid}
#test{width:50px; height:50px; border:1px solid;}
<canvas id="canv"></canvas>
<div id="test"></div>
Related
I'm playing around with the <canvas> element, drawing lines and such.
I've noticed that my diagonal lines are antialiased. I'd prefer the jaggy look for what I'm doing - is there any way of turning this feature off?
For images there's now context.imageSmoothingEnabled= false.
However, there's nothing that explicitly controls line drawing. You may need to draw your own lines (the hard way) using getImageData and putImageData.
Draw your 1-pixel lines on coordinates like ctx.lineTo(10.5, 10.5). Drawing a one-pixel line over the point (10, 10) means, that this 1 pixel at that position reaches from 9.5 to 10.5 which results in two lines that get drawn on the canvas.
A nice trick to not always need to add the 0.5 to the actual coordinate you want to draw over if you've got a lot of one-pixel lines, is to ctx.translate(0.5, 0.5) your whole canvas at the beginning.
It can be done in Mozilla Firefox. Add this to your code:
contextXYZ.mozImageSmoothingEnabled = false;
In Opera it's currently a feature request, but hopefully it will be added soon.
It must antialias vector graphics
Antialiasing is required for correct plotting of vector graphics that involves non-integer coordinates (0.4, 0.4), which all but very few clients do.
When given non-integer coordinates, the canvas has two options:
Antialias - paint the pixels around the coordinate based on how far the integer coordinate is from non-integer one (ie, the rounding error).
Round - apply some rounding function to the non-integer coordinate (so 1.4 will become 1, for example).
The later strategy will work for static graphics, although for small graphics (a circle with radius of 2) curves will show clear steps rather than a smooth curve.
The real problem is when the graphics is translated (moved) - the jumps between one pixel and another (1.6 => 2, 1.4 => 1), mean that the origin of the shape may jump with relation to the parent container (constantly shifting 1 pixel up/down and left/right).
Some tips
Tip #1: You can soften (or harden) antialiasing by scaling the canvas (say by x) then apply the reciprocal scale (1/x) to the geometries yourself (not using the canvas).
Compare (no scaling):
with (canvas scale: 0.75; manual scale: 1.33):
and (canvas scale: 1.33; manual scale: 0.75):
Tip #2: If a jaggy look is really what you're after, try to draw each shape a few times (without erasing). With each draw, the antialiasing pixels get darker.
Compare. After drawing once:
After drawing thrice:
Try something like canvas { image-rendering: pixelated; }.
This might not work if you're trying to only make one line not antialiased.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillRect(4, 4, 2, 2);
canvas {
image-rendering: pixelated;
width: 100px;
height: 100px; /* Scale 10x */
}
<html>
<head></head>
<body>
<canvas width="10" height="10">Canvas unsupported</canvas>
</body>
</html>
I haven't tested this on many browsers though.
I would draw everything using a custom line algorithm such as Bresenham's line algorithm. Check out this javascript implementation:
http://members.chello.at/easyfilter/canvas.html
I think this will definitely solve your problems.
Adding this:
image-rendering: pixelated; image-rendering: crisp-edges;
to the style attribute of the canvas element helped to draw crisp pixels on the canvas. Discovered via this great article:
https://developer.mozilla.org/en-US/docs/Games/Techniques/Crisp_pixel_art_look
I discovered a better way to disable antialiasing on path / shape rendering using the context's filter property:
The magic / TL;DR:
ctx = canvas.getContext('2d');
// make canvas context render without antialiasing
ctx.filter = "url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9ImZpbHRlciIgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVDb21wb25lbnRUcmFuc2Zlcj48ZmVGdW5jUiB0eXBlPSJpZGVudGl0eSIvPjxmZUZ1bmNHIHR5cGU9ImlkZW50aXR5Ii8+PGZlRnVuY0IgdHlwZT0iaWRlbnRpdHkiLz48ZmVGdW5jQSB0eXBlPSJkaXNjcmV0ZSIgdGFibGVWYWx1ZXM9IjAgMSIvPjwvZmVDb21wb25lbnRUcmFuc2Zlcj48L2ZpbHRlcj48L3N2Zz4=#filter)";
Demystified:
The data url is a reference to an SVG containing a single filter:
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="filter" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
<feComponentTransfer>
<feFuncR type="identity"/>
<feFuncG type="identity"/>
<feFuncB type="identity"/>
<feFuncA type="discrete" tableValues="0 1"/>
</feComponentTransfer>
</filter>
</svg>
Then at the very end of the url is an id reference to that #filter:
"url(data:image/svg+...Zz4=#filter)";
The SVG filter uses a discrete transform on the alpha channel, selecting only completely transparent or completely opaque on a 50% boundary when rendering. This can be tweaked to add some anti-aliasing back in if needed, e.g.:
...
<feFuncA type="discrete" tableValues="0 0 0.25 0.75 1"/>
...
Cons / Notes / Gotchas
Note, I didn't test this method with images, but I can presume it would affect semi-transparent parts of images. I can also guess that it probably would not prevent antialiasing on images at differing color boundaries. It isn't a 'nearest color' solution but rather a binary transparency solution. It seems to work best with path / shape rendering since alpha is the only channel antialiased with paths.
Also, using a minimum lineWidth of 1 is safe. Thinner lines become sparse or may often disappear completely.
Edit:
I've discovered that, in Firefox, setting filter to a dataurl does not work immediately / synchronously: the dataurl has to 'load' first.
e.g. The following will not work in Firefox:
ctx.filter = "url(data:image/svg+xml;base64,...#filter)";
ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(20,20);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.filter = "none";
But waiting till the next JS frame works fine:
ctx.filter = "url(data:image/svg+xml;base64,...#filter)";
setTimeout(() => {
ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(20,20);
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.filter = "none";
}, 0);
I want to add that I had trouble when downsizing an image and drawing on canvas, it was still using smoothing, even though it wasn't using when upscaling.
I solved using this:
function setpixelated(context){
context['imageSmoothingEnabled'] = false; /* standard */
context['mozImageSmoothingEnabled'] = false; /* Firefox */
context['oImageSmoothingEnabled'] = false; /* Opera */
context['webkitImageSmoothingEnabled'] = false; /* Safari */
context['msImageSmoothingEnabled'] = false; /* IE */
}
You can use this function like this:
var canvas = document.getElementById('mycanvas')
setpixelated(canvas.getContext('2d'))
Maybe this is useful for someone.
ctx.translate(0.5, 0.5);
ctx.lineWidth = .5;
With this combo I can draw nice 1px thin lines.
While we still don't have proper shapeSmoothingEnabled or shapeSmoothingQuality options on the 2D context (I'll advocate for this and hope it makes its way in the near future), we now have ways to approximate a "no-antialiasing" behavior, thanks to SVGFilters, which can be applied to the context through its .filter property.
So, to be clear, it won't deactivate antialiasing per se, but provides a cheap way both in term of implementation and of performances (?, it should be hardware accelerated, which should be better than a home-made Bresenham on the CPU) in order to remove all semi-transparent pixels while drawing, but it may also create some blobs of pixels, and may not preserve the original input color.
For this we can use a <feComponentTransfer> node to grab only fully opaque pixels.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ABEDBE";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "black";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
// first without filter
ctx.fillText("no filter", 60, 20);
drawArc();
drawTriangle();
// then with filter
ctx.setTransform(1, 0, 0, 1, 120, 0);
ctx.filter = "url(#remove-alpha)";
// and do the same ops
ctx.fillText("no alpha", 60, 20);
drawArc();
drawTriangle();
// to remove the filter
ctx.filter = "none";
function drawArc() {
ctx.beginPath();
ctx.arc(60, 80, 50, 0, Math.PI * 2);
ctx.stroke();
}
function drawTriangle() {
ctx.beginPath();
ctx.moveTo(60, 150);
ctx.lineTo(110, 230);
ctx.lineTo(10, 230);
ctx.closePath();
ctx.stroke();
}
// unrelated
// simply to show a zoomed-in version
const zoomed = document.getElementById("zoomed");
const zCtx = zoomed.getContext("2d");
zCtx.imageSmoothingEnabled = false;
canvas.onmousemove = function drawToZoommed(e) {
const
x = e.pageX - this.offsetLeft,
y = e.pageY - this.offsetTop,
w = this.width,
h = this.height;
zCtx.clearRect(0,0,w,h);
zCtx.drawImage(this, x-w/6,y-h/6,w, h, 0,0,w*3, h*3);
}
<svg width="0" height="0" style="position:absolute;z-index:-1;">
<defs>
<filter id="remove-alpha" x="0" y="0" width="100%" height="100%">
<feComponentTransfer>
<feFuncA type="discrete" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</defs>
</svg>
<canvas id="canvas" width="250" height="250" ></canvas>
<canvas id="zoomed" width="250" height="250" ></canvas>
For the ones that don't like to append an <svg> element in their DOM, and who live in the near future (or with experimental flags on), the CanvasFilter interface we're working on should allow to do this without a DOM (so from Worker too):
if (!("CanvasFilter" in globalThis)) {
throw new Error("Not Supported", "Please enable experimental web platform features, or wait a bit");
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ABEDBE";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "black";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
// first without filter
ctx.fillText("no filter", 60, 20);
drawArc();
drawTriangle();
// then with filter
ctx.setTransform(1, 0, 0, 1, 120, 0);
ctx.filter = new CanvasFilter([
{
filter: "componentTransfer",
funcA: {
type: "discrete",
tableValues: [ 0, 1 ]
}
}
]);
// and do the same ops
ctx.fillText("no alpha", 60, 20);
drawArc();
drawTriangle();
// to remove the filter
ctx.filter = "none";
function drawArc() {
ctx.beginPath();
ctx.arc(60, 80, 50, 0, Math.PI * 2);
ctx.stroke();
}
function drawTriangle() {
ctx.beginPath();
ctx.moveTo(60, 150);
ctx.lineTo(110, 230);
ctx.lineTo(10, 230);
ctx.closePath();
ctx.stroke();
}
// unrelated
// simply to show a zoomed-in version
const zoomed = document.getElementById("zoomed");
const zCtx = zoomed.getContext("2d");
zCtx.imageSmoothingEnabled = false;
canvas.onmousemove = function drawToZoommed(e) {
const
x = e.pageX - this.offsetLeft,
y = e.pageY - this.offsetTop,
w = this.width,
h = this.height;
zCtx.clearRect(0,0,w,h);
zCtx.drawImage(this, x-w/6,y-h/6,w, h, 0,0,w*3, h*3);
};
<canvas id="canvas" width="250" height="250" ></canvas>
<canvas id="zoomed" width="250" height="250" ></canvas>
Or you can also save the SVG as an external file and set the filter property to path/to/svg_file.svg#remove-alpha.
Notice a very limited trick. If you want to create a 2 colors image, you may draw any shape you want with color #010101 on a background with color #000000. Once this is done, you may test each pixel in the imageData.data[] and set to 0xFF whatever value is not 0x00 :
imageData = context2d.getImageData (0, 0, g.width, g.height);
for (i = 0; i != imageData.data.length; i ++) {
if (imageData.data[i] != 0x00)
imageData.data[i] = 0xFF;
}
context2d.putImageData (imageData, 0, 0);
The result will be a non-antialiased black & white picture. This will not be perfect, since some antialiasing will take place, but this antialiasing will be very limited, the color of the shape being very much like the color of the background.
Here is a basic implementation of Bresenham's algorithm in JavaScript. It's based on the integer-arithmetic version described in this wikipedia article: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
function range(f=0, l) {
var list = [];
const lower = Math.min(f, l);
const higher = Math.max(f, l);
for (var i = lower; i <= higher; i++) {
list.push(i);
}
return list;
}
//Don't ask me.
//https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
function bresenhamLinePoints(start, end) {
let points = [];
if(start.x === end.x) {
return range(f=start.y, l=end.y)
.map(yIdx => {
return {x: start.x, y: yIdx};
});
} else if (start.y === end.y) {
return range(f=start.x, l=end.x)
.map(xIdx => {
return {x: xIdx, y: start.y};
});
}
let dx = Math.abs(end.x - start.x);
let sx = start.x < end.x ? 1 : -1;
let dy = -1*Math.abs(end.y - start.y);
let sy = start.y < end.y ? 1 : - 1;
let err = dx + dy;
let currX = start.x;
let currY = start.y;
while(true) {
points.push({x: currX, y: currY});
if(currX === end.x && currY === end.y) break;
let e2 = 2*err;
if (e2 >= dy) {
err += dy;
currX += sx;
}
if(e2 <= dx) {
err += dx;
currY += sy;
}
}
return points;
}
For those who still looking for answers. here is my solution.
Assumming image is 1 channel gray. I just thresholded after ctx.stroke().
ctx.beginPath();
ctx.moveTo(some_x, some_y);
ctx.lineTo(some_x, some_y);
...
ctx.closePath();
ctx.fill();
ctx.stroke();
let image = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)
for(let x=0; x < ctx.canvas.width; x++) {
for(let y=0; y < ctx.canvas.height; y++) {
if(image.data[x*image.height + y] < 128) {
image.data[x*image.height + y] = 0;
} else {
image.data[x*image.height + y] = 255;
}
}
}
if your image channel is 3 or 4. you need to modify the array index like
x*image.height*number_channel + y*number_channel + channel
Just two notes on StashOfCode's answer:
It only works for a grayscale, opaque canvas (fillRect with white then draw with black, or viceversa)
It may fail when lines are thin (~1px line width)
It's better to do this instead:
Stroke and fill with #FFFFFF, then do this:
imageData.data[i] = (imageData.data[i] >> 7) * 0xFF
That solves it for lines with 1px width.
Other than that, StashOfCode's solution is perfect because it doesn't require to write your own rasterization functions (think not only lines but beziers, circular arcs, filled polygons with holes, etc...)
According to MDN docs, Scaling for high resolution displays, "You may find that canvas items appear blurry on higher-resolution displays. While many solutions may exist, a simple first step is to scale the canvas size up and down simultaneously, using its attributes, styling, and its context's scale."
Ignoring the apparent paradox in their statement, this worked in my case, sharpening edges which had previously been unacceptably fuzzy.
// Get the DPR and size of the canvas
const dpr = window.devicePixelRatio;
const rect = canvas.getBoundingClientRect();
// Set the "actual" size of the canvas
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
// Scale the context to ensure correct drawing operations
ctx.scale(dpr, dpr);
// Set the "drawn" size of the canvas
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
I have designed a Duval's Triangle ( a diagnostic tool ), on SVG,
cosisting of Segments (closed Paths) of different colors.
The diagnostic result will be a coordinate.
Need to detect the resultant coordinate is in which Closed Path ?
Like in the following case the diagnostic result is a RED DOT.
Need to detect the close path i.e in this case : D2
You have several options:
SVG 2 has the isPointInFill() method. Which you can call on each shape to see if a point is within the fill of a path. However I believe only Chrome and Opera have implemented this new feature of SVG2.
https://developer.mozilla.org/en-US/docs/Web/API/SVGGeometryElement/isPointInFill
You can draw your SVG to a Canvas and use the isPointInPath() method.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
You could draw the SVG to a Canvas and use the getImageData() method to get the colour of that particular pixel.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData
And of course you can also do it numerically by calculating it yourself in some way.
This is using Paul LeBeau's answer.
1.
First a demo where you check if the point is in a certain path, the #c path in this case.
Please read the comments in my code.
// creating a new SVG point
let point = svg.createSVGPoint()
point.x = 300;
point.y = 300;
//checking if the point is in the path c
console.log(c.isPointInFill(point));
svg{border:1px solid}
<svg id="svg" viewBox="0 0 606.731 526.504" width="200" >
<polygon id="a" fill="#D9D1E0" stroke="#020202" stroke-miterlimit="10" points="300.862,1.001 0.862,526.001 605.862,526.001 "/>
<polygon id="b" fill="#926BB5" stroke="#020202" stroke-miterlimit="10" points="289.576,19.681 442.34,283.546 411.092,343.437
515.705,526.001 428.453,525.716 337.314,365.138 385.054,280.945 262.668,66.555 "/>
<polygon id="c" fill="#8ED5ED" stroke="#020202" stroke-miterlimit="10" points="334.4,193.005 384.186,280.946 337.315,364.272
428.453,525.716 142.019,525.716 "/>
<circle cx="300" cy="300" r="5" fill="red" />
</svg>
2.
A second demo where I'm drawing the polygons on a canvas and using the method isPointInPath() of the context:
Please read the comments in my code.
let ctx = canv.getContext("2d");
canv.width = 606.731;
canv.height = 526.504;
// the main triangle
let duval = [300.862, 1.001, 0.862, 526.001, 605.862, 526.001];
// the arrays of points for the 2 polygons inside the main triangle
let rys = [
[
289.576,
19.681,
442.34,
283.546,
411.092,
343.437,
515.705,
526.001,
428.453,
525.716,
337.314,
365.138,
385.054,
280.945,
262.668,
66.555
],
[
334.4,
193.005,
384.186,
280.946,
337.315,
364.272,
428.453,
525.716,
142.019,
525.716
]
];
// drawing the polygons
drawPoly(duval, "#D9D1E0");
drawPoly(rys[0], "#926BB5");
drawPoly(rys[1], "#8ED5ED");
// the point to check
let p = { x: 300, y: 300 };
drawPoint(p);
// looping through the array of shapes to check if the point is in path
for (let i = 0; i < rys.length; i++) {
// draw again the polygon without stroking or filling
drawPoly(rys[i]);
//chect if the point is in path
if (ctx.isPointInPath(p.x, p.y)) {
// do something
console.log(i);
// if found break the loop
break;
}
}
// a function to draw a polygon from an array
function drawPoly(ry, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(ry[0], ry[1]);
for (let i = 2; i < ry.length; i += 2) {
ctx.lineTo(ry[i], ry[i + 1]);
}
ctx.closePath();
if (color) {
ctx.fill();
ctx.stroke();
}
}
function drawPoint(p) {
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
ctx.fill();
}
canvas{border:1px solid}
<canvas id="canv"></canvas>
3
This time using the getImageData() method to get the colour of the pixel at the point.
The code is similar to the one in the previous example
let ctx = canv.getContext("2d");
canv.width = 606.731;
canv.height = 526.504;
let duval = [300.862, 1.001, 0.862, 526.001, 605.862, 526.001];
let rys = [
[
289.576,
19.681,
442.34,
283.546,
411.092,
343.437,
515.705,
526.001,
428.453,
525.716,
337.314,
365.138,
385.054,
280.945,
262.668,
66.555
],
[
334.4,
193.005,
384.186,
280.946,
337.315,
364.272,
428.453,
525.716,
142.019,
525.716
]
];
drawPoly(duval, "#D9D1E0");
drawPoly(rys[0], "#926BB5");
drawPoly(rys[1], "#8ED5ED");
function drawPoly(ry, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(ry[0], ry[1]);
for (let i = 2; i < ry.length; i += 2) {
ctx.lineTo(ry[i], ry[i + 1]);
}
ctx.closePath();
if (color) {
ctx.fill();
ctx.stroke();
}
}
// HERE BEGINS THE IMPORTANT PART
let imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
let p = { x: 300, y: 300 };
// mark the point with an empty circle
ctx.beginPath();
ctx.arc(p.x,p.y,5,0,2*Math.PI);
ctx.stroke();
// the index of the point p in the imgData.data array
let index = (p.y*imgData.width + p.x)*4;
//the red,green and blue components of the color of the pixel at the index
let r = imgData.data[index];
let g = imgData.data[index + 1];
let b = imgData.data[index + 2];
//test the color
test.style.background = `rgb(${r},${g},${b})`;
canvas{border:1px solid}
#test{width:50px; height:50px; border:1px solid;}
<canvas id="canv"></canvas>
<div id="test"></div>
Here I have some code that make svg path:
http://jsbin.com/gazecasagi/1/edit?html,output
<html>
<head>
<script>
function draw() {
var polygons = [[{"X":22,"Y":59.45},{"X":136,"Y":66},{"X":170,"Y":99},{"X":171,"Y":114},{"X":183,"Y":125},{"X":218,"Y":144},{"X":218,"Y":165},{"X":226,"Y":193},{"X":254,"Y":195},{"X":283,"Y":195},{"X":292,"Y":202},{"X":325,"Y":213},{"X":341,"Y":134},{"X":397,"Y":245},{"X":417,"Y":548}]];
var scale = 1000;
reverse_copy(polygons);
polygons = scaleup(polygons, scale);
var cpr = new ClipperLib.Clipper();
var delta = 20;
var joinType = ClipperLib.JoinType.jtRound;
var miterLimit = 2;
var AutoFix = true;
var svg, offsetted_polygon,
cont = document.getElementById('svgcontainer');
offsetted_polygon = cpr.OffsetPolygons(polygons, delta * scale, joinType, miterLimit, AutoFix);
//console.log(JSON.stringify(offsetted_polygon));
// Draw red offset polygon
svg = '<svg style="margin-top:10px;margin-right:10px;margin-bottom:10px;background-color:#dddddd" width="800" height="600">';
svg += '<path stroke="red" fill="red" stroke-width="2" stroke-opacity="0.6" fill-opacity="0.2" d="' + polys2path(offsetted_polygon, scale) + '"/>';
//Draw blue polyline
svg += '<path stroke="blue" stroke-width="1" d="' + polys2path(polygons, scale) + '"/>';
svg += '</svg>';
cont.innerHTML += svg;
}
// helper function to scale up polygon coordinates
function scaleup(poly, scale) {
var i, j;
if (!scale) scale = 1;
for(i = 0; i < poly.length; i++) {
for(j = 0; j < poly[i].length; j++) {
poly[i][j].X *= scale;
poly[i][j].Y *= scale;
}
}
return poly;
}
// converts polygons to SVG path string
function polys2path (poly, scale) {
var path = "", i, j;
if (!scale) scale = 1;
for(i = 0; i < poly.length; i++) {
for(j = 0; j < poly[i].length; j++){
if (!j) path += "M";
else path += "L";
path += (poly[i][j].X / scale) + ", " + (poly[i][j].Y / scale);
}
path += "Z";
}
return path;
}
function reverse_copy(poly) {
// Make reverse copy of polygons = convert polyline to a 'flat' polygon ...
var k, klen = poly.length, len, j;
for (k = 0; k < klen; k++) {
len = poly[k].length;
poly[k].length = len * 2 - 2;
for (j = 1; j <= len - 2; j++) {
poly[k][len - 1 + j] = {
X: poly[k][len - 1 - j].X,
Y: poly[k][len - 1 - j].Y
}
}
}
}
</script>
</head>
<body onload="draw()">
<div id="svgcontainer"></div>
</body>
</html>
Is there a simple way to tranform SVG path to Canvas. I need this becouse I need to show this example on mobile devices and Canvas have a better performance than canvas on mobile devices.
This code I need to transform to CANVAS:
// Draw red offset polygon
svg = '<svg style="margin-top:10px;margin-right:10px;margin-bottom:10px;background-color:#dddddd" width="800" height="600">';
svg += '<path stroke="red" fill="red" stroke-width="2" stroke-opacity="0.6" fill-opacity="0.2" d="' + polys2path(offsetted_polygon, scale) + '"/>';
//Draw blue polyline
svg += '<path stroke="blue" stroke-width="1" d="' + polys2path(polygons, scale) + '"/>';
svg += '</svg>';
How I can transform SVG path to simple CANVAS path?
You can use canvg library to convert svg to canvas.
You should include all its necessary js files to your page and then use it something like this:
canvg(document.getElementById('canvasElement'), '<svg>...</svg>')
Of course, the fastest way to present your complex polyline is to convert it into an image.
A fully optimized canvas version of your complex polyline would involve a canvas path:
Create a closed path of your red outline using lines with Bezier curves for joins. You can use context.lineTo and context.quadraticCurveTo + context.bezierCurveTo to define the path. The resulting path is commonly called a spline.
Stroke the path with red.
Fill the path with pink.
Draw the blue line.
This is not hard to do but does involve some trigonometry (mainly finding points tangent to the vectors of your polyline).
Here's an alternative that uses shadowing to mimic your complex SVG polyline:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var pts = [{x:22,y:59.45},{x:136,y:66},{x:170,y:99},{x:171,y:114},{x:183,y:125},{x:218,y:144},{x:218,y:165},{x:226,y:193},{x:254,y:195},{x:283,y:195},{x:292,y:202},{x:325,y:213},{x:341,y:134},{x:397,y:245},{x:417,y:548}];
mimicSvg(pts);
function mimicSvg(pts){
// make caps & joins round
ctx.lineCap='round';
ctx.lineJoin='round';
// draw the outside line with red shadow
ctx.shadowColor='red';
ctx.shadowBlur='2';
ctx.lineWidth=25;
// draw multiple times to darken shadow
drawPolyline(pts);
drawPolyline(pts);
drawPolyline(pts);
// stop shadowing
ctx.shadowColor='transparent';
// refill the outside line with pink
ctx.strokeStyle='pink';
drawPolyline(pts);
// draw the inside line
ctx.lineWidth=2;
ctx.strokeStyle='blue';
drawPolyline(pts);
}
function drawPolyline(pts){
ctx.beginPath();
ctx.moveTo(pts[0].x,pts[0].y);
for(var i=1;i<pts.length;i++){
ctx.lineTo(pts[i].x,pts[i].y);
}
ctx.stroke();
}
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=500 height=600></canvas>
I'm working on concept maps application, which has a set of nodes and links. I have connected the links to nodes using the center of the node as reference. Since I have nodes with different size and shapes, it is not advisable to draw arrow-head for the link by specifying height or width of the shape. My approach is to draw a link, starting from one node, pixel by pixel till the next node is reached(here the nodes are of different color from that of the background), then by accessing the pixel value, I want to be able to decide the point of intersection of link and the node, which is actually the co-ordinate for drawing the arrow-head.
It would be great, if I could get some help with this.
Sample Code:
http://jsfiddle.net/9tUQP/4/
Here the green squares are nodes and the line starting from left square and entering into the right square is the link. I want the arrow-head to be drawn at the point of intersection of link and the right square.
I've created an example that does this. I use Bresenham's Line Algorithm to walk the line of whole canvas pixels and check the alpha at each point; whenever it crosses a 'threshold' point I record that as a candidate. I then use the first and last such points to draw an arrow (with properly-rotated arrowhead).
Here's the example: http://phrogz.net/tmp/canvas_shape_edge_arrows.html
Refresh the example to see a new random test case. It 'fails' if you have another 'shape' already overlapping one of the end points. One way to solve this would be to draw your shapes first to a blank canvas and then copy the result (drawImage) to the final canvas.
For Stack Overflow posterity (in case my site is down) here's the relevant code:
<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<title>HTML5 Canvas Shape Edge Detection (for Arrow)</title>
<style type="text/css">
body { background:#eee; margin:2em 4em; text-align:center; }
canvas { background:#fff; border:1px solid #666 }
</style>
</head><body>
<canvas width="800" height="600"></canvas>
<script type="text/javascript">
var ctx = document.querySelector('canvas').getContext('2d');
for (var i=0;i<20;++i) randomCircle(ctx,'#999');
var start = randomDiamond(ctx,'#060');
var end = randomDiamond(ctx,'#600');
ctx.lineWidth = 2;
ctx.fillStyle = ctx.strokeStyle = '#099';
arrow(ctx,start,end,10);
function arrow(ctx,p1,p2,size){
ctx.save();
var points = edges(ctx,p1,p2);
if (points.length < 2) return
p1 = points[0], p2=points[points.length-1];
// Rotate the context to point along the path
var dx = p2.x-p1.x, dy=p2.y-p1.y, len=Math.sqrt(dx*dx+dy*dy);
ctx.translate(p2.x,p2.y);
ctx.rotate(Math.atan2(dy,dx));
// line
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(-len,0);
ctx.closePath();
ctx.stroke();
// arrowhead
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(-size,-size);
ctx.lineTo(-size, size);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// Find all transparent/opaque transitions between two points
// Uses http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
function edges(ctx,p1,p2,cutoff){
if (!cutoff) cutoff = 220; // alpha threshold
var dx = Math.abs(p2.x - p1.x), dy = Math.abs(p2.y - p1.y),
sx = p2.x > p1.x ? 1 : -1, sy = p2.y > p1.y ? 1 : -1;
var x0 = Math.min(p1.x,p2.x), y0=Math.min(p1.y,p2.y);
var pixels = ctx.getImageData(x0,y0,dx+1,dy+1).data;
var hits=[], over=null;
for (x=p1.x,y=p1.y,e=dx-dy; x!=p2.x||y!=p2.y;){
var alpha = pixels[((y-y0)*(dx+1)+x-x0)*4 + 3];
if (over!=null && (over ? alpha<cutoff : alpha>=cutoff)){
hits.push({x:x,y:y});
}
var e2 = 2*e;
if (e2 > -dy){ e-=dy; x+=sx }
if (e2 < dx){ e+=dx; y+=sy }
over = alpha>=cutoff;
}
return hits;
}
function randomDiamond(ctx,color){
var x = Math.round(Math.random()*(ctx.canvas.width - 100) + 50),
y = Math.round(Math.random()*(ctx.canvas.height - 100) + 50);
ctx.save();
ctx.fillStyle = color;
ctx.translate(x,y);
ctx.rotate(Math.random() * Math.PI);
var scale = Math.random()*0.8 + 0.4;
ctx.scale(scale,scale);
ctx.lineWidth = 5/scale;
ctx.fillRect(-50,-50,100,100);
ctx.strokeRect(-50,-50,100,100);
ctx.restore();
return {x:x,y:y};
}
function randomCircle(ctx,color){
ctx.save();
ctx.beginPath();
ctx.arc(
Math.round(Math.random()*(ctx.canvas.width - 100) + 50),
Math.round(Math.random()*(ctx.canvas.height - 100) + 50),
Math.random()*20 + 10,
0, Math.PI * 2, false
);
ctx.fillStyle = color;
ctx.fill();
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
</script>
</body></html>
I have a college project that I've chose to present in HTML, the user would input the three sides of a triangle and the shape would be rendered on the screen. I've made a JavaScript that get these values and create the x and y coordinates drawing the triangle inside the <canvas> tag:
<script type="application/javascript">
function init() {
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
var a = *user input*;
var b = *user input*;
var c = *user input*;
var ox = 450-(a/2); // 450px since the canvas size is 900px,
var oy = 450+(y3/2); // this aligns the figure to the center
var x3 = ((b*b)+(a*a)-(c*c))/(2*a);
var y3 = Math.ceil(Math.sqrt((b*b)-(x3*2)));
var img = new Image();
img.src = 'grad.png';
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 10;
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
var ptrn = ctx.createPattern(img,'repeat');
ctx.fillStyle = ptrn;
ctx.beginPath();
ctx.moveTo(ox,oy);
ctx.lineTo(a+ox,oy);
ctx.lineTo(ox+x3,oy-y3);
ctx.lineTo(ox,oy);
ctx.fill();
ctx.stroke();
ctx.closePath();
}
}
</script>
<body onLoad="init();">
<canvas id="canvas" width="900" height="900"></canvas><br>
</body>
I'm trying to compose a simple scale animation once the page is loaded making the triangle and other shapes to "grow" on the screen. If I use CSS, the entire canvas will scale. Also, I don't know how to make this animation possible since the values are not fixed and using canvas, I would have to animate this frame-by-frame.
Now if I use CSS and SVG, I could use a simple ease-in and scale effect for each element, the problem is that I would have to generate the triangle in a SVG using the values inputted by user. How can I do this?
A triangle is a polygon with 3 points. Look at SVG Polygon documentation.
In JavaScript you can create a polygon like so:
var svgns = "http://www.w3.org/2000/svg";
function makeTriangle() {
shape = svgDocument.createElementNS(svgns, "polygon");
shape.setAttributeNS(null, "points", "5,5 45,45 5,45");
shape.setAttributeNS(null, "fill", "none");
shape.setAttributeNS(null, "stroke", "green");
svgDocument.documentElement.appendChild(shape);
}
If you're always going to have a triangle (or polygon) on the screen, I would create the basic framework with SVG/CSS and set the attribute wuth CSS:
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="900">
<defs>
<filter id="dropshadow" height="130%">
<feGaussianBlur in="SourceAlpha" stdDeviation="10"/>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<polygon id="triangle" filter="url(#dropshadow)" />
</svg>
<style>
#triangle {
fill: url(grad.png);
stroke-width: 3px;
stroke: white;
}
</style>
You could then use much of the same code to set the polygon points:
var points = [
[ox, oy].join(','),
[a + ox, oy].join(','),
[ox + x3, oy - y3].join(',')
].join(' ');
document.getElementById('triangle').setAttribute('points', points);
You can see an example here: http://fiddle.jshell.net/fTPdy/