Related
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>
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'm tyring to rotate a diagram in canvas around its center while keeping the letters upright. I'm trying to use ctx.rotate(#) but it's rotating the entire diagram using what appears to be the left side of the canvas as the center.
The following link offers a visual: I want it to look like the green, not the red, as it currently does with my code. Visual Explanation
The following is the JSFiddle: http://jsfiddle.net/ddxarcag/143/
And my code is below:
<script>
$(document).ready(function () {
init();
function init() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
draw(ctx);
}
function draw(ctx) {
// layer1/Line
ctx.rotate(00);
ctx.beginPath();
ctx.moveTo(75.1, 7.7);
ctx.lineTo(162.9, 7.7);
ctx.stroke();
function WordSelector1() {
var word = ['A', 'B', 'C'];
var random = word[Math.floor(Math.random() * word.length)];
return random;
}
var x = WordSelector1();
// layer1/P
ctx.font = "12.0px 'Myriad Pro'";
ctx.rotate(0);
ctx.fillText(x, 60.0, 10.0);
}
});
</script>
Any help would be much appreciated. Thanks!
Drawing rotated graphs in canvas is somewhat easy once you know how to move the origin (0,0) to another point and then rotating the axes around it.
I added some comments in the code as not to repeat code and explanations.
I also moved the functions out of the $(document).ready and changed some numbers for more rounded values.
$(document).ready(function () {
init();
});
function init() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
draw(ctx);
}
function draw(ctx) {
ctx.font = "12.0px 'Myriad Pro'";
var angle = Math.random()*150; //Run more times to see other angles
//This translates the 0,0 to the center of the horizontal line
ctx.translate(100, 100);
//This draws the original straight line
ctx.beginPath();
ctx.moveTo(-50, 0); //The coordinates are relative to the new origin
ctx.lineTo(50, 0);
ctx.stroke();
//Draw the first letter
var x = WordSelector1();
ctx.fillText(x, -60, 0);
//This section draws the rotated line with the text straight
//Rotate the canvas axes by "angle"
ctx.rotate(angle * Math.PI/180);
ctx.beginPath();
ctx.moveTo(-50, 0); //The relative coordinates DO NOT change
ctx.lineTo(50, 0); //This shows that the axes rotate, not the drawing
ctx.stroke();
var x = WordSelector1();
ctx.translate(-60,0); //The origin must now move where the letter is to be placed
ctx.rotate(-angle * Math.PI/180); //Counter-rotate by "-angle"
ctx.fillText(x, 0, 0); //Draw the letter
}
function WordSelector1() {
var word = ['A', 'B', 'C'];
var random = word[Math.floor(Math.random() * word.length)];
return random;
}
canvas{
border: 1px solid;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" width="200" height="200"></canvas>
One warning: after everything is drawn the axes end parallel to the canvas borders because it was rotated by -angle, but the origin is where the last letter was placed. You may want to use ctx.save() and ctx.restore() to avoid having to revert translations and rotations.
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>
The final code that worked for me was:
<canvas id="bg-admin-canvas" width="500" height="500" style="margin:15px; background:#09F;"></canvas>
<script>
var postit = function(width,height,angle){
var canvas = document.getElementById("bg-admin-canvas");
var ctx = canvas.getContext("2d");
var radians = angle * Math.PI / 180;
var move = width*Math.sin(radians);
if(angle < 0 ){ ctx.translate(0,-move); }else{ ctx.translate(move,0); }
ctx.rotate(radians);
var gradient = ctx.createLinearGradient(0,height,width/2,height/2);
gradient.addColorStop(0.05,"rgba(0,0,0,0)");
gradient.addColorStop(0.5,"rgba(0,0,0,0.3)");
ctx.fillStyle = gradient;
ctx.fillRect(0,0,width,height);
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(width, 0);
ctx.lineTo(width,height);
ctx.lineTo(width-width*.8,height-height*.02);
ctx.quadraticCurveTo(0+width*.02,height-height*.02,0+width*.02,(height - height*.2));
ctx.closePath();
var gradient = ctx.createLinearGradient(0,height,width/2,height/2);
gradient.addColorStop(0,'#f7f8b9');
gradient.addColorStop(1,'#feffcf');
ctx.fillStyle = gradient;
ctx.fill();
ctx.beginPath();
ctx.moveTo(width-width*.8,height-height*.02);
ctx.quadraticCurveTo(0+width*.02,height-height*.02,0+width*.02,(height - height*.2));
ctx.quadraticCurveTo(width*.05,height-height*.05,width*.1,height-height*.1);
ctx.quadraticCurveTo(width*.1,height-height*.07,width-width*.8,height-height*.02);
ctx.closePath();
ctx.fillStyle = '#ffffff';
ctx.fill();
var gradient = ctx.createLinearGradient(0,height,width*.1,height-height*.1);
gradient.addColorStop(0,"rgba(222,222,163,0.8)");
gradient.addColorStop(1,'#feffcf');
ctx.fillStyle = gradient;
ctx.fill();
}
postit(300, 300, 10);
</script>
Hi,
I made a quick and dirty "post-it" note with html5's canvas and some js.
I want to be able to rotate them anyway I want so I tried to use the translate. The example below I have a translate of 0,250 just so you could see the whole thing.
Ideally, I know if my canvas was 300,300 then I would
ctx.translate(150,150);
ctx.rotate(-30);
ctx.translate(-150,-150);
Of course since I'm rotating a square it gets cut off.
How would I rotate the square and move it on the canvas so the whole thing is showing but at the very top left edge of the canvas?
I added an image with my thinking of just getting the height of a triangle and moving it that much, but when translated, it doesn't seem to work just right.
I'll paste my whole function so you can look at it, but if you have any ideas, I would appreciate it. This isn't important, just messing around today.
var postit = function(width,height,angle){
var canvas = jQuery("#bg-admin-canvas").get(0);
var ctx = canvas.getContext("2d");
/*var area = (width*width*Math.sin(angle))/2;
var h = (area*2) / width + 30;
ctx.translate(0,h);
*/
//ctx.translate(150,150);
ctx.translate(0,250);
ctx.rotate(angle*Math.PI / 180);
//ctx.translate(-150,-150);
var gradient = ctx.createLinearGradient(0,height,width/2,height/2);
gradient.addColorStop(0.05,"rgba(0,0,0,0)");
gradient.addColorStop(0.5,"rgba(0,0,0,0.3)");
ctx.fillStyle = gradient;
ctx.fillRect(0,0,width,height);
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(width, 0);
ctx.lineTo(width,height);
ctx.lineTo(width-width*.8,height-height*.02);
ctx.quadraticCurveTo(0+width*.02,height-height*.02,0+width*.02,(height - height*.2));
ctx.closePath();
var gradient = ctx.createLinearGradient(0,height,width/2,height/2);
gradient.addColorStop(0,'#f7f8b9');
gradient.addColorStop(1,'#feffcf');
ctx.fillStyle = gradient;
ctx.fill();
ctx.beginPath();
ctx.moveTo(width-width*.8,height-height*.02);
ctx.quadraticCurveTo(0+width*.02,height-height*.02,0+width*.02,(height - height*.2));
ctx.quadraticCurveTo(width*.05,height-height*.05,width*.1,height-height*.1);
ctx.quadraticCurveTo(width*.1,height-height*.07,width-width*.8,height-height*.02);
ctx.closePath();
ctx.fillStyle = '#ffffff';
ctx.fill();
var gradient = ctx.createLinearGradient(0,height,width*.1,height-height*.1);
gradient.addColorStop(0,"rgba(222,222,163,0.8)");
gradient.addColorStop(1,'#feffcf');
ctx.fillStyle = gradient;
ctx.fill();
}
postit(300, 300, -35);
MORE INFO
Phrog, I think you know what I'm trying to do. This image shows what I want to do:
Now, the only thing is, I want to be able to pass in any width and height and angle and make the adjustment on the fly.
As an example with the following code:
var canvas = document.getElementById("bg-admin-canvas");
var ctx = canvas.getContext("2d");
ctx.arc(0,0,3,0,360,true); ctx.fill();
ctx.translate(50, 50);
ctx.arc(0,0,3,0,360,true); ctx.fill();
ctx.translate(-25, -25);
ctx.arc(0,0,3,0,360,true); ctx.fill();
I get the following image:
Now, if I add a rotate in there like this:
var canvas = document.getElementById("bg-admin-canvas");
var ctx = canvas.getContext("2d");
ctx.arc(0,0,3,0,360,true); ctx.fill();
ctx.translate(50, 50);
ctx.arc(0,0,3,0,360,true); ctx.fill();
ctx.rotate(30*Math.PI/180);
ctx.translate(-25, -25);
ctx.arc(0,0,3,0,360,true); ctx.fill();
I now have a sloped coordinates as the result is:
As I found, this is because the coordinates are no longer horizontal and vertical.
So, with this rotated coordinate structure, I can't figure out how to move my square (which could be any size and rotated at any angle) back to the left and top (so it fits in as little space as possible)
Does that make sense?
In short:
Translate the context in the Y direction only to put the corner where it should be.
Rotate the context around this offset point.
Draw your object at 0,0.
Here is an interactive, working example, which you can see online here:
http://phrogz.net/tmp/canvas_rotate_square_in_corner.html
<!DOCTYPE HTML>
<html lang="en"><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>HTML5 Canvas Rotate Square in Corner</title>
<style type="text/css" media="screen">
body { background:#eee; margin:2em; text-align:center }
canvas { display:block; margin:auto; background:#fff; border:1px solid #ccc }
</style>
</head><body>
<canvas width="250" height="200"></canvas>
<script type="text/javascript" charset="utf-8">
var can = document.getElementsByTagName('canvas')[0];
var ctx = can.getContext('2d');
ctx.strokeStyle = '#600'; ctx.lineWidth = 2; ctx.lineJoin = 'round';
ctx.fillStyle = '#ff0'
document.body.onmousemove = function(evt){
var w=140, h=120;
var angle = evt ? (evt.pageX - can.offsetLeft)/100 : 0;
angle = Math.max(Math.min(Math.PI/2,angle),0);
ctx.clearRect(0,0,can.width,can.height); ctx.beginPath();
ctx.save();
ctx.translate(1,w*Math.sin(angle)+1);
ctx.rotate(-angle);
ctx.fillRect(0,0,w,h);
ctx.strokeRect(0,0,w,h);
ctx.restore();
};
document.body.onmousemove();
</script>
</body></html>
Analysis
In the above diagram, point A is the upper-left corner of our post-it note and point B is the upper-right corner. We have rotated the post-it note -a radians from the normal angle (clockwise rotations are positive, counter-clockwise are negative).
We can see that the point A stays on the y axis as the post-it rotates, so we only need to calculate how far down the y axis to move it. This distance is expressed in the diagram as BD. From trigonometry we know that
sin(a) = BD / AB
Rearranging this formula gives us
BD = AB * sin(a)
We know that AB is the width of our post-it note. A few details:
Because our angle will be expressed as a negative number, and the sin of a negative number yields a negative result, but because we want a positive result, we must either negate the result
BD = -AB * sin(-a)
or just 'cheat' and use a positive angle:
BD = AB * sin(a)
We need to remember to translate our context before we rotate it, so that we first move directly down the axis to establish our origin at the right spot.
Remember that rotations in HTML5 Canvas use radians (not degrees). If you want to rotate by 20 degrees, you need to convert that to radians by multiplying by Math.PI/180:
ctx.rotate( 20*Math.PI/180 );
This also applies to the arc command; you should be doing ctx.arc(x,y,r,0,Math.PI*2,false); for a full circle.
You should create you canvas element and then rotate it using CSS. It would keep your canvas intact and only rotate the element itself.
Here is some example css rules:
-webkit-transform: rotate(-30deg);
-moz-transform: rotate(-30deg);
Refer to http://snook.ca/archives/html_and_css/css-text-rotation