How to get width & height of drawn arc(s) in HTML5 Canvas? - html

Here I have drawn some arcs using Konvajs library, but I cannot get their width and height after the objects have been drawn, How can I do that?
for quick read of code:
function drawSurface(idnumber, radius, x, y, startAngleParam, endAngleParam) {
var borderbold = 5;
var surface;
if (typeof startAngleParam !== 'undefined') {
surface = new Konva.Shape({
x: x,
y: y,
fill: '#ccc',
stroke: "#ccc",
strokeWidth: 8,
id: idnumber,
opacity: 1,
drawFunc: function (context) {
var startAngle = startAngleParam * Math.PI;
var endAngle = (startAngleParam + 0.5 + endAngleParam) * Math.PI;
var counterClockwise = false;
context.beginPath();
context.arc(0, 0, radius, startAngle, endAngle, counterClockwise);
context.setAttr("lineWidth", borderbold);
context.stroke();
context.fillStrokeShape(this);
}
});
}
else {
surface = new Konva.Circle({
x: x,
y: y,
radius: radius,
fill: '#ccc',
strokeWidth: 3,
id: idnumber,
opacity: 1
});
}
return surface;
}
Please support your answer with a code example.

Find the bounding box of your arc and then calculate the width & height from the bounding box.
Geometrically, the only 5 possible bounding box corners are:
the arc centerpoint,
the point on the arc (if any) at 0 degrees (0 radians),
the point on the arc (if any) at 90 degrees (PI/2 radians),
the point on the arc (if any) at 180 degrees (PI radians),
the point on the arc (if any) at 270 degrees (PI*3/2 radians),
From these possible bounding box points, find the minimum X, minimum Y, maximum X & maximum Y. The [minX,minY] will be the top left corner of the bounding box. The [maxX,maxY] will be the bottom right corner of the bounding box.
Your arc width will be maxX-minX and height will be maxY-minY.
Here's example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var PI=Math.PI;
var cx=150;
var cy=150;
var radius=75;
var startAngle=-PI/4;
var endAngle=PI/3;
ctx.beginPath();
ctx.moveTo(cx,cy);
ctx.arc(cx,cy,radius,startAngle,endAngle);
ctx.closePath();
ctx.fillStyle='skyblue';
ctx.fill();
ctx.strokeStyle='lightgray';
ctx.lineWidth=3;
ctx.stroke();
var bb=arcBounds(cx,cy,radius,startAngle,endAngle);
ctx.strokeStyle='red';
ctx.lineWidth=1;
ctx.strokeRect(bb.x,bb.y,bb.width,bb.height);
function arcBounds(cx,cy,radius,startAngle,endAngle){
var minX=1000000;
var minY=1000000;
var maxX=-1000000;
var maxY=-1000000;
var possibleBoundingPoints=[]
// centerpoint
possibleBoundingPoints.push({x:cx,y:cy});
// starting angle
possibleBoundingPoints.push(arcpoint(cx,cy,radius,startAngle));
// ending angle
possibleBoundingPoints.push(arcpoint(cx,cy,radius,endAngle));
// 0 radians
if(0>=startAngle && 0<=endAngle){
possibleBoundingPoints.push(arcpoint(cx,cy,radius,0));
}
// PI/2 radians
var angle=PI/2;
if(angle>=startAngle && angle<=endAngle){
possibleBoundingPoints.push(arcpoint(cx,cy,radius,angle));
}
// PI radians
var angle=PI;
if(angle>=startAngle && angle<=endAngle){
possibleBoundingPoints.push(arcpoint(cx,cy,radius,angle));
}
// PI*3/2 radians
var angle=PI*3/2;
if(angle>=startAngle && angle<=endAngle){
possibleBoundingPoints.push(arcpoint(cx,cy,radius,angle));
}
for(var i=0;i<possibleBoundingPoints.length;i++){
var pt=possibleBoundingPoints[i];
if(pt.x<minX){minX=pt.x;}
if(pt.y<minY){minY=pt.y;}
if(pt.x>maxX){maxX=pt.x;}
if(pt.y>maxY){maxY=pt.y;}
}
return({ x:minX, y:minY, width:maxX-minX, height:maxY-minY });
}
function arcpoint(cx,cy,radius,angle){
var x=cx+radius*Math.cos(angle);
var y=cy+radius*Math.sin(angle);
return({x:x,y:y});
}
body{ background-color: ivory; }
#canvas{border:1px solid blue;}
<canvas id="canvas" width=300 height=300></canvas>

Here is a different approach taken from this answer and ported to Javascript:
const PI = Math.PI;
const HALF_PI = Math.PI / 2;
const TWO_PI = Math.PI * 2;
const DEG_TO_RAD = Math.PI / 180;
const RAD_TO_DEG = 180 / Math.PI;
const getQuadrant = (_angle) => {
const angle = _angle % (TWO_PI);
if (angle > 0.0 && angle < HALF_PI) return 0;
if (angle >= HALF_PI && angle < PI) return 1;
if (angle >= PI && angle < PI + HALF_PI) return 2;
return 3;
};
// https://stackoverflow.com/a/35977476/461048
const getArcBoundingBox = (ini, end, radius, margin = 0) => {
const iniQuad = getQuadrant(ini);
const endQuad = getQuadrant(end);
const ix = Math.cos(ini) * radius;
const iy = Math.sin(ini) * radius;
const ex = Math.cos(end) * radius;
const ey = Math.sin(end) * radius;
const minX = Math.min(ix, ex);
const minY = Math.min(iy, ey);
const maxX = Math.max(ix, ex);
const maxY = Math.max(iy, ey);
const r = radius;
const xMax = [[maxX, r, r, r], [maxX, maxX, r, r], [maxX, maxX, maxX, r], [maxX, maxX, maxX, maxX]];
const yMax = [[maxY, maxY, maxY, maxY], [r, maxY, r, r], [r, maxY, maxY, r], [r, maxY, maxY, maxY]];
const xMin = [[minX, -r, minX, minX], [minX, minX, minX, minX], [-r, -r, minX, -r], [-r, -r, minX, minX]];
const yMin = [[minY, -r, -r, minY], [minY, minY, -r, minY], [minY, minY, minY, minY], [-r, -r, -r, minY]];
const x1 = xMin[endQuad][iniQuad];
const y1 = yMin[endQuad][iniQuad];
const x2 = xMax[endQuad][iniQuad];
const y2 = yMax[endQuad][iniQuad];
const x = x1 - margin;
const y = y1 - margin;
const w = x2 - x1 + margin * 2;
const h = y2 - y1 + margin * 2;
return { x, y, w, h };
};
jsfiddle: https://jsfiddle.net/brunoimbrizi/y3to5s6n/45/

Related

Clip + Arc leads to an unwanted closing of the path, while Clip + Rect shows the expected behavior

Question:
Why does CanvasRenderingContext2D.clip() closes an additional path when applying it to a collection of CanvasRenderingContext2D.arc() sampled along the path of a quadratic curve?
Background
I am trying to create a path of quadratic segments with a longitudinal color split. Based on a comment to the question "Square curve with lengthwise color division" I am trying to accomplish this goal by going through the following steps:
Draw the quadratic path
Sample point on the quadratic curve
Create a clipping region and draw a cycle at each sampled point
let region = new Path2D();
for (j = 0; j < pointsQBez.length; j++) {
region.arc(pointsQBez[j].x, pointsQBez[j].y, 4, 0, 2 * Math.PI );
}
ctx.clip(region)
Split the canvas into two segments based on the curve
Calculate the intersection of the start- and end-segment with the canvas border
Close the path (first clipping region)
Draw a rectangle over the whole canvas (second clipping region)
Fill in the two regions created in step four
Steps 3, 4, and 5 in pictures:
Issue
The pink part in the third image above should have the same thickness as the turquoise.
But for some strange reason, the whole inner part of the curve gets filled in.
Additional observations
This behaviour does not show when using CanvasRenderingContext2D.rect() instead of CanvasRenderingContext2D.arc():
When using CanvasRenderingContext2D.arc(), the inner part of the curve that is filled in is not consistent
Because rect does include a call to closePath() while arc doesn't.
Two ways of working around that:
You can call closePath() after each arc:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const pointsQBez = [];
const cx = 75;
const cy = 75;
const rad = 50;
for(let i = 0; i < 180; i++) {
const a = (Math.PI / 180) * i - Math.PI / 2;
const x = cx + Math.cos(a) * rad;
const y = cy + Math.sin(a) * rad;
pointsQBez.push({ x, y });
}
let region = new Path2D();
for (const {x, y} of pointsQBez) {
region.arc(x, y, 4, 0, 2 * Math.PI);
region.closePath();
}
ctx.clip(region);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);
<canvas></canvas>
Or you can moveTo() the entry point of your arc:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const pointsQBez = [];
const cx = 75;
const cy = 75;
const rad = 50;
for(let i = 0; i < 180; i++) {
const a = (Math.PI / 180) * i - Math.PI / 2;
const x = cx + Math.cos(a) * rad;
const y = cy + Math.sin(a) * rad;
pointsQBez.push({ x, y });
}
let region = new Path2D();
for (const {x, y} of pointsQBez) {
region.moveTo(x + 4, y); // x + arc radius
region.arc(x, y, 4, 0, 2 * Math.PI);
}
ctx.clip(region);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);
<canvas></canvas>

Draw Line Arrowhead Without Rotating in Canvas

Most code to drawing arrowheads in html canvas involves rotating the canvas context and drawing the lines.
My use case is to draw them using trigonometry without rotating the canvas. or is that vector algorithm you call it? Help is appreciated.
This is what I have (forgot where I got most of the code). Draws 2 arrowheads on start and end based on the last 2 parameters arrowStart and arrowEnd which are boolean.
drawLineArrowhead: function(context, arrowStart, arrowEnd) {
// Place start end points here.
var x1 = 0;
var y1 = 0;
var x2 = 0;
var y2 = 0;
var distanceFromLine = 6;
var arrowLength = 9;
var dx = x2 - x1;
var dy = y2 - y1;
var angle = Math.atan2(dy, dx);
var length = Math.sqrt(dx * dx + dy * dy);
context.translate(x1, y1);
context.rotate(angle);
context.beginPath();
context.moveTo(0, 0);
context.lineTo(length, 0);
if (arrowStart) {
context.moveTo(arrowLength, -distanceFromLine);
context.lineTo(0, 0);
context.lineTo(arrowLength, distanceFromLine);
}
if (arrowEnd) {
context.moveTo(length - arrowLength, -distanceFromLine);
context.lineTo(length, 0);
context.lineTo(length - arrowLength, distanceFromLine);
}
context.stroke();
context.setTransform(1, 0, 0, 1, 0, 0);
},
See the code below, just a bit of trigonometry.
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
ctx.lineCap = "round";
ctx.lineWidth = 5;
function drawLineArrowhead(p1, p2, startSize, endSize) {
ctx.beginPath()
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
if (startSize > 0) {
lineAngle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
delta = Math.PI/6
for (i=0; i<2; i++) {
ctx.moveTo(p1.x, p1.y);
x = p1.x + startSize * Math.cos(lineAngle + delta)
y = p1.y + startSize * Math.sin(lineAngle + delta)
ctx.lineTo(x, y);
delta *= -1
}
}
if (endSize > 0) {
lineAngle = Math.atan2(p1.y - p2.y, p1.x - p2.x);
delta = Math.PI/6
for (i=0; i<2; i++) {
ctx.moveTo(p2.x, p2.y);
x = p2.x + endSize * Math.cos(lineAngle + delta)
y = p2.y + endSize * Math.sin(lineAngle + delta)
ctx.lineTo(x, y);
delta *= -1
}
}
ctx.stroke();
}
drawLineArrowhead({x:10, y:10}, {x:100, y:20}, 0, 30)
drawLineArrowhead({x:20, y:25}, {x:140, y:120}, 20, 20)
drawLineArrowhead({x:140, y:20}, {x:80, y:50} , 20, 0)
drawLineArrowhead({x:150, y:20}, {x:150, y:90}, 20, 5)
drawLineArrowhead({x:180, y:90}, {x:180, y:20}, 20, 5)
drawLineArrowhead({x:200, y:10}, {x:200, y:140}, 10, 10)
drawLineArrowhead({x:220, y:140}, {x:220, y:10}, 10, 20)
<canvas id="canvas">
If you run it you should see a few samples.
The drawLineArrowhead has 4 parameters (p1, p2, startSize, endSize)
the first two are the starting-point and end-point of the line, the last two are arrow size, just to give some control to the final user over how big are those arrows at the end, if we want to remove them we set to 0.

Properties lost when copying HTML canvas or converting toDataURL

I'm using a native HTML 5 canvas to create a text effect then attempting to create an image from the canvas or copy the canvas to a fabric.js object, but the output doesn't reflect the changes done to the text.
Fiddle: https://jsfiddle.net/rf8kdxq1/2/
Something simple I hope?
var ctx = demo.getContext('2d'),
font = '64px impact',
w = demo.width,
h = demo.height,
curve,
offsetY,
bottom,
textHeight,
isTri = false,
dltY,
angleSteps = 180 / w,
i = w,
y,
os = document.createElement('canvas'),
octx = os.getContext('2d');
os.width = w;
os.height = h;
octx.font = font;
octx.textBaseline = 'top';
octx.textAlign = 'center';
curve = parseInt(110, 10);
offsetY = parseInt(4, 10);
textHeight = parseInt(64, 10);
bottom = parseInt(200, 10);
octx.fillText('BRIDGE', w * 0.5, 0);
/// slide and dice
i = w;
dltY = curve / textHeight;
y = 0;
while (i--) {
y = bottom - curve * Math.sin(i * angleSteps * Math.PI / 180);
ctx.drawImage(os, i, 0, 1, textHeight,
i, h * 0.5 - offsetY / textHeight * y, 1, y);
}
// ctx.drawImage(os, i, 0);
dataURL = os.toDataURL();
img = new Image();
img.src = dataURL;
img.onload = function() {
console.log(img);
};
var newCanv = new fabric.Canvas('canvasd');
function xfer() {
var image = new fabric.Image(os);
setTimeout(function() {
newCanv.add(image);
},500);
}
xfer();

find coordinates to draw square inside circle

How to compute the starting co-ordinates to draw a square inside a cirle?
Function Draws the circular spectrum .
Now help me to find the starting coordinates to draw the rectangle inside the circle
Gradient.prototype.renderSpectrum = function() {
var radius = this.width / 2;
var toRad = (2 * Math.PI) / 360;
var step = 1 / radius;
this.ctx.clearRect(0, 0, this.width, this.height);
for(var i = 0; i < 360; i += step) {
var rad = i * toRad;
this.ctx.strokeStyle = 'hsl(' + i + ', 100%, 50%)';
this.ctx.beginPath();
this.ctx.moveTo(radius, radius);
this.ctx.lineTo(radius + radius * Math.cos(rad), radius + radius * Math.sin(rad));
this.ctx.stroke();
}
this.ctx.fillStyle = 'rgb(255, 255, 255)';
this.ctx.beginPath();
this.ctx.arc(radius, radius, radius * 0.8, 0, Math.PI * 2, true);
this.ctx.closePath();
return this.ctx.fill();
}
Function to draw the square
Gradient.prototype.renderGradient = function() {
var color, colors, gradient, index, xy, _i, _len, _ref, _ref1;
xy = arguments[0], colors = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
gradient = (_ref = this.ctx).createLinearGradient.apply(_ref, [0, 0].concat(__slice.call(xy)));
gradient.addColorStop(0, (_ref1 = colors.shift()) != null ? _ref1.toString() : void 0);
for (index = _i = 0, _len = colors.length; _i < _len; index = ++_i) {
color = colors[index];
gradient.addColorStop(index + 1 / colors.length, color.toString());
}
this.ctx.fillStyle = gradient;
this.renderSpectrum();
return this.ctx.fillRect(?, ?, this.width * 0.8, this.height * 0.8);
};
To fit a square inside a circle you can use something like this (adopt as needed):
Live example
/**
* ctx - context
* cx/cy - center of circle
* radius - radius of circle
*/
function squareInCircle(ctx, cx, cy, radius) {
var side = Math.sqrt(radius * radius * 2), // calc side length of square
half = side * 0.5; // position offset
ctx.strokeRect(cx - half, cy - half, side, side);
}
Just replace strokeRect() with fillRect().
Which will result in this (circle added for reference):
Adopting it for general usage:
function getSquareInCircle(cx, cy, radius) {
var side = Math.sqrt(radius * radius * 2), // calc side length of square
half = side * 0.5; // position offset
return {
x: cx - half,
y: cy - half,
w: side,
h: side
}
}
Then in your method:
Gradient.prototype.renderGradient = function() {
var color, colors, gradient, index, xy, _i, _len, _ref, _ref1;
xy = arguments[0], colors = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
gradient = (_ref = this.ctx).createLinearGradient.apply(_ref, [0, 0].concat(__slice.call(xy)));
gradient.addColorStop(0, (_ref1 = colors.shift()) != null ? _ref1.toString() : void 0);
for (index = _i = 0, _len = colors.length; _i < _len; index = ++_i) {
color = colors[index];
gradient.addColorStop(index + 1 / colors.length, color.toString());
}
this.ctx.fillStyle = gradient;
this.renderSpectrum();
// supply the proper position/radius here:
var square = getSquareInCircle(centerX, centerY, radius);
return this.ctx.fillRect(square.x, square.y, square.w, square.h);
};
Here is how I computed the co ordinates to draw square inside a circle
1)Get the coordinates of a inner circle at 135 degree
using the formula
x = rad + rad * Math.cos(135 * ( 2 Math.PI / 360);
y = rad - rad * Math.sin(135 * ( 2 Math.PI / 360);
2) then pyhthogoram therom to find the width if the square
width = Math.sqrt(rad * rad / 2);

Chart Label text rotation

I am using very similar code to create a pie chart using canvas as per this article:
http://wickedlysmart.com/how-to-make-a-pie-chart-with-html5s-canvas/
As you can see from this image, there are cases where the labels are upside down:
Here is the code that writes the labels to the graph:
var drawSegmentLabel = function(canvas, context, i) {
context.save();
var x = Math.floor(canvas.width / 2);
var y = Math.floor(canvas.height / 2);
var degrees = sumTo(data, i);
var angle = degreesToRadians(degrees);
context.translate(x, y);
context.rotate(angle);
context.textAlign = 'right';
var fontSize = Math.floor(canvas.height / 32);
context.font = fontSize + 'pt Helvetica';
var dx = Math.floor(canvas.width * 0.3) - 20;
var dy = Math.floor(canvas.height * 0.05);
context.fillText(labels[i], dx, dy);
context.restore();
};
I am trying to rectify this so the text is always readable and not upside down but cant work out how to do it!
Here's my solution! (A little kludgey but seems to work on the basic example, I haven't tested in on edge cases...)
var drawSegmentLabel = function(canvas, context, i) {
context.save();
var x = Math.floor(canvas.width / 2);
var y = Math.floor(canvas.height / 2);
var angle;
var angleD = sumTo(data, i);
var flip = (angleD < 90 || angleD > 270) ? false : true;
context.translate(x, y);
if (flip) {
angleD = angleD-180;
context.textAlign = "left";
angle = degreesToRadians(angleD);
context.rotate(angle);
context.translate(-(x + (canvas.width * 0.5))+15, -(canvas.height * 0.05)-10);
}
else {
context.textAlign = "right";
angle = degreesToRadians(angleD);
context.rotate(angle);
}
var fontSize = Math.floor(canvas.height / 25);
context.font = fontSize + "pt Helvetica";
var dx = Math.floor(canvas.width * 0.5) - 10;
var dy = Math.floor(canvas.height * 0.05);
context.fillText(labels[i], dx, dy);
context.restore();
};
To display the text in the correct way you have to check if the rotation angle is between 90 and 270 degree. If it is then you know the text will be display upside down.
To switch it correctly you then have to rotate you canvas of planed rotation - 180 degree and then to align it in left not right :
var drawSegmentLabel = function(canvas, context, i) {
context.save();
var x = Math.floor(canvas.width / 2);
var y = Math.floor(canvas.height / 2);
var degrees = sumTo(data, i);
var angle = 0;
if (degree > 90 && degree < 270)
angle = degreesToRadians(degrees - 180);
else
angle = degreesToRadians(degrees);
context.translate(x, y);
context.rotate(angle);
context.textAlign = 'right';
var fontSize = Math.floor(canvas.height / 32);
context.font = fontSize + 'pt Helvetica';
var dx = Math.floor(canvas.width * 0.3) - 20;
if (degree > 90 && degree < 270)
dx = 20;
var dy = Math.floor(canvas.height * 0.05);
context.fillText(labels[i], dx, dy);
context.restore();
};