I have the following SVG element which was created using JS: https://akzhy.com/blog/create-animated-donut-chart-using-svg-and-javascript
<div class="doughnut">
<svg width="100%" height="100%" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="30" stroke="#80e080" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="141.372" transform='rotate(-90 50 50)'/>
<circle cx="50" cy="50" r="30" stroke="#4fc3f7" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="103.6728" transform='rotate(0 50 50)'/>
<circle cx="50" cy="50" r="30" stroke="#9575cd" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="169.6464" transform='rotate(162 50 50)'/>
<circle cx="50" cy="50" r="30" stroke="#f06292" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="150.7968" transform='rotate(198 50 50)'/>
</svg>
</div>
Is it possible to to get a path from the svg node?
Conversion via graphic app
Open you svg in an application like Illustrator/inkscape etc.
You could use path operations like "stroke-to-path" to convert stroke based chart segments (the visual colored segments are just dashed strokes applied to a full circle).
Use a pie/donut generator script returning solid paths
Based on this answer by #ray hatfield Pie chart using circle element you can calculate d properties based on the arc command.
Example json based pie chart generator
let pies = document.querySelectorAll('.pie-generate');
generatePies(pies);
function generatePies(pies) {
if (pies.length) {
pies.forEach(function(pie, i) {
let data = pie.getAttribute('data-pie');
if (data) {
data = JSON.parse(data);
w = data['width'];
h = data['height'];
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
svg.setAttribute('width', w);
svg.setAttribute('height', h);
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
pie.appendChild(svg);
addSegments(svg, data);
}
})
}
}
function addSegments(svg, data) {
let segments = data["segments"];
let strokeWidth = data["strokeWidth"];
let centerX = data["centerX"];
let centerY = data["centerY"];
let radius = data["radius"];
let startingAngle = (data["startingAngle"] || data["startingAngle"] == 0) ? data["startingAngle"] : -90;
let gap = data["gap"];
let decimals = data["decimals"];
let offset = 0;
let output = "";
// calculate auto percentages
let total = 0;
let calc = data["calc"] ? true : false;
if (calc) {
segments.forEach(function(segment, i) {
total += segment[0];
});
}
// prevent too large gaps
let circumference = Math.PI * radius * 2;
let circumferencePerc = (circumference / 100);
let gapPercentOuter = 100 / circumference * gap;
if (gapPercentOuter > circumferencePerc) {
gap = gap / (gapPercentOuter / circumferencePerc)
}
segments.forEach(function(segment, i) {
let percent = segment[0];
// calc percentages
let percentCalc = percent.toString().indexOf('/') != -1 ? segment[0].split('/') : [];
percent = percentCalc.length ? percentCalc[0] / percentCalc[1] * 100 : +percent
// calculate auto percentages to get 100% in total
if (total) {
percent = 100 / total * percent;
}
let percentRound = percent.toFixed(decimals);
// auto fill color
let segOptions = segment[1] ? segment[1] : '';
let fill = segOptions ? 'fill="' + segOptions['color'] + '"' : "";
if (!fill) {
let hueCut = 0;
let hueShift = 0;
let hue = Math.abs((360 - hueCut) / 100 * (offset + percent)) + hueShift;
let autoColor = hslToHex(hue.toFixed(0) * 1, 60, 50);
fill = 'fill="' + autoColor + '"';
}
let className = segOptions['class'] ? segOptions['class'] : "";
let classPercent = percentRound.toString().replaceAll('.', '_');
let id = segOptions['id'] ? 'id="' + segOptions['id'] + '" ' : '';
let d = getArcD(centerX, centerY, strokeWidth, offset, percent, radius, gap, decimals, startingAngle);
output +=
`\n<path d="${d}" ${fill} class="segment segment-${classPercent} segment-${(i+1)} ${className}" ${id} data-percent="${percentRound}"/>`;
offset += percent;
});
svg.innerHTML = output;
}
function getArcD(centerX, centerY, strokeWidth, percentStart, percent, radiusOuter, gap, decimals = 3, startingAngle = -90) {
let radiusInner = radiusOuter - strokeWidth;
let circumference = Math.PI * radiusOuter * 2;
let isPieChart = false;
// if pie chart – stroke equals radius
if (strokeWidth + gap >= radiusOuter) {
isPieChart = true;
}
let circumferenceInner = Math.PI * radiusInner * 2;
let gapPercentOuter = ((100 / circumference) * gap) / 2;
let gapPercentInner = ((100 / circumferenceInner) * gap) / 2;
//add offset from previous segments
percentStart = percentStart;
let percentEnd = percent + percentStart;
// outer coordinates
let [x1, y1] = getPosOnCircle(centerX, centerY, (percentStart + gapPercentOuter), radiusOuter, decimals, startingAngle);
let [x2, y2] = getPosOnCircle(centerX, centerY, percentEnd - gapPercentOuter, radiusOuter, decimals, startingAngle);
// switch arc output between long or short arc segment according to percentage
let longArc = percent >= 50 ? 1 : 0;
let rotation = 0;
let clockwise = 1;
let counterclockwise = 0;
let d = '';
// if donut chart
if (!isPieChart) {
//inner coordinates
let [x3, y3] = getPosOnCircle(centerX, centerY, percentEnd - gapPercentInner, radiusInner, decimals, startingAngle);
let [x4, y4] = getPosOnCircle(centerX, centerY, percentStart + gapPercentInner, radiusInner, decimals, startingAngle);
d = [
"M", x1, y1,
"A", radiusOuter, radiusOuter, rotation, longArc, clockwise, x2, y2,
"L", x3, y3,
"A", radiusInner, radiusInner, rotation, longArc, counterclockwise, x4, y4,
"z"
];
}
// if pie chart – stroke equals radius: drop inner radius arc
else {
// find opposite coordinates
let [x1o, y1o] = getPosOnCircle(centerX, centerY, (percentStart - gapPercentOuter) - 50, radiusOuter, decimals, startingAngle);
let [x2o, y2o] = getPosOnCircle(centerX, centerY, (percentEnd + gapPercentOuter) - 50, radiusOuter, decimals, startingAngle);
let extrapolatedIntersection = getLinesIntersection(
[x1, y1, x1o, y1o], [x2, y2, x2o, y2o],
decimals);
d = [
"M", x1, y1,
"A", radiusOuter, radiusOuter, rotation, longArc, clockwise, x2, y2,
"L", extrapolatedIntersection.join(" "),
"z"
];
}
return d.join(" ");
}
// helper: get x/y coordinates according to angle percentage
function getPosOnCircle(centerX, centerY, percent, radius, decimals = 3, angleOffset = -90) {
let angle = 360 / (100 / percent) + angleOffset;
let x = +(centerX + Math.cos((angle * Math.PI) / 180) * radius).toFixed(
decimals
);
let y = +(centerY + Math.sin((angle * Math.PI) / 180) * radius).toFixed(
decimals
);
return [x, y];
}
// helper: get intersection coordinates
function getLinesIntersection(l1, l2, decimals = 3) {
let intersection = [];
let c2x = l2[0] - l2[2];
let c3x = l1[0] - l1[2];
let c2y = l2[1] - l2[3];
let c3y = l1[1] - l1[3];
// down part of intersection point formula
let d = c3x * c2y - c3y * c2x;
if (d != 0) {
// upper part of intersection point formula
let u1 = l1[0] * l1[3] - l1[1] * l1[2];
let u4 = l2[0] * l2[3] - l2[1] * l2[2];
// intersection point formula
let px = +((u1 * c2x - c3x * u4) / d).toFixed(decimals);
let py = +((u1 * c2y - c3y * u4) / d).toFixed(decimals);
intersection = [px, py];
}
return intersection;
}
function hslToHex(h, s, l) {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
};
return `#${f(0)}${f(8)}${f(4)}`;
}
<div class="pie-generate" data-pie='{
"width": 100,
"height": 100,
"radius": 50,
"centerX": 50,
"centerY": 50,
"strokeWidth": 20,
"gap": 0,
"decimals": 3,
"segments": [
["25", {"color":"#80e080", "id":"seg01", "class":"segCustom"}],
["45", {"color":"#4fc3f7", "id":"seg02", "class":"segCustom"}],
["10", {"color":"#9575cd", "id":"seg03", "class":"segCustom"}],
["20", {"color":"#f06292", "id":"seg04", "class":"segCustom"}]
]
}'>
</div>
You can tweak different segment percentages by changing the JSOn data-attribute.
<div class="pie-generate" data-pie='{
"width": 100,
"height": 100,
"radius": 50,
"centerX": 50,
"centerY": 50,
"strokeWidth": 20,
"gap": 0,
"decimals": 3,
"segments": [
["25", {"color":"#80e080", "id":"seg01", "class":"segCustom"}],
["45", {"color":"#4fc3f7", "id":"seg02", "class":"segCustom"}],
["10", {"color":"#9575cd", "id":"seg03", "class":"segCustom"}],
["20", {"color":"#f06292", "id":"seg04", "class":"segCustom"}]
]
}'>
</div>
Segment output:
<path d="M 50 0 A 50 50 0 0 1 100 50 L 80 50 A 30 30 0 0 0 50 20 z" fill="#80e080" />
Related
I am creating a random SVG "Blob" generator, but can't figure out how to connect the last bézier to the "M" point correctly. In the example you can see a little spike there.
function generate() {
const points = [
{ x: 55.380049480163834, y: 8.141661255952418 },
{ x: 61.89338428790346, y: 59.21935310168805 },
{ x: 6.637386502817552, y: 65.10477483405401 },
{ x: 15.309460889587692, y: 11.231848017862793 }
]
let d = `M ${points[0].x / 2} ${points[0].y}`
d += `Q ${points[0].x} ${points[0].y} ${(points[0].x + points[1].x) * 0.5} ${(points[0].y + points[1].y) * 0.5}`
d += `Q ${points[1].x} ${points[1].y} ${(points[1].x + points[2].x) * 0.5} ${(points[1].y + points[2].y) * 0.5}`
d += `Q ${points[2].x} ${points[2].y} ${(points[2].x + points[3].x) * 0.5} ${(points[2].y + points[3].y) * 0.5}`
d += `Q ${points[3].x} ${points[3].y} ${(points[3].x + points[0].x) * 0.5} ${(points[3].y + points[0].y) * 0.5} Z`
return d
}
document.getElementById('blob').setAttribute('d', generate())
<svg>
<path viewBox="0 0 70 70" id="blob"></path>
</svg>
This is how I would do it: first you need to find the first midpoint (between the last and the first point ) and move to it. next you calculate the midpoint between every 2 points in the points array. Finally you draw the curve through the last point, back to the first midpoint.
const points = [
{ x: 55.380049480163834, y: 8.141661255952418 },
{ x: 61.89338428790346, y: 59.21935310168805 },
{ x: 6.637386502817552, y: 65.10477483405401 },
{ x: 15.309460889587692, y: 11.231848017862793 }
]
function drawCurve(points) {
//find the first midpoint and move to it
var p = {};
p.x = (points[points.length - 1].x + points[0].x) / 2;
p.y = (points[points.length - 1].y + points[0].y) / 2;
let d = `M${p.x}, ${p.y}`;
//curve through the rest, stopping at each midpoint
for (var i = 0; i < points.length - 1; i++) {
var mp = {}
mp.x = (points[i].x + points[i + 1].x) / 2;
mp.y = (points[i].y + points[i + 1].y) / 2;
d += `Q${points[i].x},${points[i].y},${mp.x},${mp.y}`
}
//curve through the last point, back to the first midpoint
d+= `Q${points[points.length - 1].x},${points[points.length - 1].y},${p.x},${p.y}`
blob.setAttributeNS(null,"d",d)
}
drawCurve(points);
svg{border:1px solid; fill:none; stroke:black; width:300px;}
<svg viewBox="0 0 70 70" >
<path id="blob"></path>
</svg>
I am making a drawing application. I have created a class Polygon. Its constructor will receive three arguments and these will be its properties:
points(Number): Number of points the polygon will have.
rotation(Number): The angle the whole polygon will be rotated.
angles(Array Of number): The angles between two lines of the polygon.
I have been trying for the whole day, but I couldn't figure out the correct solution.
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');
let isMouseDown = false;
let tool = 'polygon';
let savedImageData;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
const mouse = {x:null,y:null}
let mousedown = {x:null,y:null}
const toDegree = val => val * 180 / Math.PI
class Polygon {
constructor(points, rotation, angles){
this.points = points;
this.rotation = rotation;
//if angles are given then convert them to radian
if(angles){
this.angles = angles.map(x => x * Math.PI/ 180);
}
//if angles array is not given
else{
/*get the angle for a regular polygon for given points.
3-points => 60
4-points => 90
5-points => 108
*/
let angle = (this.points - 2) * Math.PI/ this.points;
//fill the angles array with the same angle
this.angles = Array(points).fill(angle)
}
let sum = 0;
this.angles = this.angles.map(x => {
sum += x;
return sum;
})
}
draw(startx, starty, endx, endy){
c.beginPath();
let rx = (endx - startx) / 2;
let ry = (endy - starty) / 2;
let r = Math.max(rx, ry)
c.font = '35px cursive'
let cx = startx + r;
let cy = starty + r;
c.fillRect(cx - 2, cy - 2, 4, 4); //marking the center
c.moveTo(cx + r, cy);
c.strokeText(0, cx + r, cy);
for(let i = 1; i < this.points; i++){
//console.log(this.angles[i])
let dx = cx + r * Math.cos(this.angles[i] + this.rotation);
let dy = cy + r * Math.sin(this.angles[i] + this.rotation);
c.strokeStyle = 'red';
c.strokeText(i, dx, dy, 100);
c.strokeStyle ='black';
c.lineTo(dx, dy);
}
c.closePath();
c.stroke();
}
}
//update();
c.beginPath();
c.lineWidth = 1;
document.addEventListener('mousemove', function(e){
//Getting the mouse coords according to canvas
const canvasData = canvas.getBoundingClientRect();
mouse.x = (e.x - canvasData.left) * (canvas.width / canvasData.width);
mouse.y = (e.y - canvasData.top) * (canvas.height / canvasData.height);
if(tool === 'polygon' && isMouseDown){
drawImageData();
let pol = new Polygon(5, 0);
pol.draw(mousedown.x, mousedown.y, mouse.x, mouse.y);
}
})
function saveImageData(){
savedImageData = c.getImageData(0, 0, canvas.width, canvas.height);
}
function drawImageData(){
c.putImageData(savedImageData, 0, 0)
}
document.addEventListener('mousedown', () => {
isMouseDown = true;
mousedown = {...mouse};
if(tool === 'polygon'){
saveImageData();
}
});
document.addEventListener('mouseup', () => isMouseDown = false);
<canvas></canvas>
In the above code I am trying to make a pentagon but it doesn't work.
Unit polygon
The following snippet contains a function polygonFromSidesOrAngles that returns the set of points defining a unit polygon as defined by the input arguments. sides, or angles
Both arguments are optional but must have one argument
If only sides given then angles are calculated to make the complete polygon with all side lengths equal
If only angles given then the number of sides is assumed to be the number of angles. Angles are in degrees 0-360
If the arguments can not define a polygon then there are several exceptions throw.
The return is a set of points on a unit circle that define the points of the polygon. The first point is at coordinate {x : 1, y: 0} from the origin.
The returned points are not rotated as that is assumed to be a function of the rendering function.
All points on the polygon are 1 unit distance from the origin (0,0)
Points are in the form of an object containing x and y properties as defined by the function point and polarPoint
Method used
I did not lookup an algorithm, rather I worked it out from the assumption that a line from (1,0) on the unit circle at the desired angle will intercept the circle at the correct distance from (1,0). The intercept point is used to calculate the angle in radians from the origin. That angle is then used to calculate the ratio of the total angles that angle represents.
The function that does this is calcRatioOfAngle(angle, sides) returning the angle as a ratio (0-1) of Math.PI * 2
It is a rather long handed method and likely can be significantly reduced
As it is unclear in your question what should be done with invalid arguments the function will throw a range error if it can not proceed.
Polygon function
Math.PI2 = Math.PI * 2;
Math.TAU = Math.PI2;
Math.deg2Rad = Math.PI / 180;
const point = (x, y) => ({x, y});
const polarPoint = (ang, dist) => ({x: Math.cos(ang) * dist, y: Math.sin(ang) * dist});
function polygonFromSidesOrAngles(sides, angles) {
function calcRatioOfAngle(ang, sides) {
const v1 = point(Math.cos(ang) - 1, Math.sin(ang));
const len2 = v1.x * v1.x + v1.y * v1.y;
const u = -v1.x / len2;
const v2 = point(v1.x * u + 1, v1.y * u);
const d = (1 - (v2.y * v2.y + v2.x * v2.x)) ** 0.5 / (len2 ** 0.5);
return Math.atan2(v2.y + v1.y * d, v2.x + 1 + v1.x * d) / (Math.PI * (sides - 2) / 2);
}
const vetAngles = angles => angles.reduce((sum, ang) => sum += ang, 0) === (angles.length - 2) * 180;
var ratios = [];
if(angles === undefined) {
if (sides < 3) { throw new RangeError("Polygon must have more than 2 side") }
const rat = 1 / sides;
while (sides--) { ratios.push(rat) }
} else {
if (sides === undefined) { sides = angles.length }
else if (sides !== angles.length) { throw new RangeError("Numbers of sides does not match number of angles") }
if (sides < 3) { throw new RangeError("Polygon must have more than 2 side") }
if (!vetAngles(angles)) { throw new RangeError("Set of angles can not create a "+sides+" sided polygon") }
ratios = angles.map(ang => calcRatioOfAngle(ang * Math.deg2Rad, sides));
ratios.unshift(ratios.pop()); // rotate right to get first angle at start
}
var ang = 0;
const points = [];
for (const rat of ratios) {
ang += rat;
points.push(polarPoint(ang * Math.TAU, 1));
}
return points;
}
Render function
Function to render the polygon. It includes the rotation so you don't need to create a separate set of points for each angle you want to render the polygon at.
The radius is the distance from the center point x,y to any of the polygons vertices.
function drawPolygon(ctx, poly, x, y, radius, rotate) {
ctx.setTransform(radius, 0, 0, radius, x, y);
ctx.rotate(rotate);
ctx.beginPath();
for(const p of poly.points) { ctx.lineTo(p.x, p.y) }
ctx.closePath();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
Example
The following renders a set of test polygons to ensure that the code is working as expected.
Polygons are rotated to start at the top and then rendered clock wise.
The example has had the vetting of input arguments removed.
const ctx = can.getContext("2d");
can.height = can.width = 512;
Math.PI2 = Math.PI * 2;
Math.TAU = Math.PI2;
Math.deg2Rad = Math.PI / 180;
const point = (x, y) => ({x, y});
const polarPoint = (ang, dist) => ({x: Math.cos(ang) * dist, y: Math.sin(ang) * dist});
function polygonFromAngles(sides, angles) {
function calcRatioOfAngle(ang, sides) {
const x = Math.cos(ang) - 1, y = Math.sin(ang);
const len2 = x * x + y * y;
const u = -x / len2;
const x1 = x * u + 1, y1 = y * u;
const d = (1 - (y1 * y1 + x1 * x1)) ** 0.5 / (len2 ** 0.5);
return Math.atan2(y1 + y * d, x1 + 1 + x * d) / (Math.PI * (sides - 2) / 2);
}
var ratios = [];
if (angles === undefined) {
const rat = 1 / sides;
while (sides--) { ratios.push(rat) }
} else {
ratios = angles.map(ang => calcRatioOfAngle(ang * Math.deg2Rad, angles.length));
ratios.unshift(ratios.pop());
}
var ang = 0;
const points = [];
for(const rat of ratios) {
ang += rat;
points.push(polarPoint(ang * Math.TAU, 1));
}
return points;
}
function drawPolygon(poly, x, y, radius, rot) {
const xdx = Math.cos(rot) * radius;
const xdy = Math.sin(rot) * radius;
ctx.setTransform(xdx, xdy, -xdy, xdx, x, y);
ctx.beginPath();
for (const p of poly) { ctx.lineTo(p.x, p.y) }
ctx.closePath();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
const segs = 4;
const tests = [
[3], [, [45, 90, 45]], [, [90, 10, 80]], [, [60, 50, 70]], [, [40, 90, 50]],
[4], [, [90, 90, 90, 90]], [, [90, 60, 90, 120]],
[5], [, [108, 108, 108, 108, 108]], [, [58, 100, 166, 100, 116]],
[6], [, [120, 120, 120, 120, 120, 120]], [, [140, 100, 180, 100, 100, 100]],
[7], [8],
];
var angOffset = -Math.PI / 2; // rotation of poly
const w = ctx.canvas.width;
const h = ctx.canvas.height;
const wStep = w / segs;
const hStep = h / segs;
const radius = Math.min(w / segs, h / segs) / 2.2;
var x,y, idx = 0;
for (y = 0; y < segs && idx < tests.length; y ++) {
for (x = 0; x < segs && idx < tests.length; x ++) {
drawPolygon(polygonFromAngles(...tests[idx++]), (x + 0.5) * wStep , (y + 0.5) * hStep, radius, angOffset);
}
}
canvas {
border: 1px solid black;
}
<canvas id="can"></canvas>
I do just a few modification.
Constructor take angles on degree
When map angles to radian complement 180 because canvas use angles like counterclockwise. We wan to be clockwise
First point start using the passed rotation
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');
let isMouseDown = false;
let tool = 'polygon';
let savedImageData;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
const mouse = {x:null,y:null}
let mousedown = {x:null,y:null}
const toDegree = val => val * 180 / Math.PI;
const toRadian = val => val * Math.PI / 180;
class Polygon {
constructor(points, rotation, angles){
this.points = points;
this.rotation = toRadian(rotation);
//if angles array is not given
if(!angles){
/*get the angle for a regular polygon for given points.
3-points => 60
4-points => 90
5-points => 108
*/
let angle = (this.points - 2) * 180 / this.points;
//fill the angles array with the same angle
angles = Array(points).fill(angle);
}
this.angles = angles;
let sum = 0;
console.clear();
// To radians
this.angles = this.angles.map(x => {
x = 180 - x;
x = toRadian(x);
return x;
})
}
draw(startx, starty, endx, endy){
c.beginPath();
let rx = (endx - startx) / 2;
let ry = (endy - starty) / 2;
let r = Math.max(rx, ry)
c.font = '35px cursive'
let cx = startx + r;
let cy = starty + r;
c.fillRect(cx - 2, cy - 2, 4, 4); //marking the center
c.moveTo(cx + r, cy);
let sumAngle = 0;
let dx = cx + r * Math.cos(this.rotation);
let dy = cy + r * Math.sin(this.rotation);
c.moveTo(dx, dy);
for(let i = 0; i < this.points; i++){
sumAngle += this.angles[i];
dx = dx + r * Math.cos((sumAngle + this.rotation));
dy = dy + r * Math.sin((sumAngle + this.rotation));
c.strokeStyle = 'red';
c.strokeText(i, dx, dy, 100);
c.strokeStyle ='black';
c.lineTo(dx, dy);
}
c.closePath();
c.stroke();
}
}
//update();
c.beginPath();
c.lineWidth = 1;
document.addEventListener('mousemove', function(e){
//Getting the mouse coords according to canvas
const canvasData = canvas.getBoundingClientRect();
mouse.x = (e.x - canvasData.left) * (canvas.width / canvasData.width);
mouse.y = (e.y - canvasData.top) * (canvas.height / canvasData.height);
if(tool === 'polygon' && isMouseDown){
drawImageData();
let elRotation = document.getElementById("elRotation").value;
let rotation = elRotation.length == 0 ? 0 : parseInt(elRotation);
let elPoints = document.getElementById("elPoints").value;
let points = elPoints.length == 0 ? 3 : parseInt(elPoints);
let elAngles = document.getElementById("elAngles").value;
let angles = elAngles.length == 0 ? null : JSON.parse(elAngles);
let pol = new Polygon(points, rotation, angles);
pol.draw(mousedown.x, mousedown.y, mouse.x, mouse.y);
}
})
function saveImageData(){
savedImageData = c.getImageData(0, 0, canvas.width, canvas.height);
}
function drawImageData(){
c.putImageData(savedImageData, 0, 0)
}
document.addEventListener('mousedown', () => {
isMouseDown = true;
mousedown = {...mouse};
if(tool === 'polygon'){
saveImageData();
}
});
document.addEventListener('mouseup', () => isMouseDown = false);
<!DOCTYPE html>
<html lang="en">
<body>
Points: <input id="elPoints" style="width:30px" type="text" value="3" />
Rotation: <input id="elRotation" style="width:30px" type="text" value="0" />
Angles: <input id="elAngles" style="width:100px" type="text" value="[45, 45, 90]" />
<canvas></canvas>
</body>
</html>
I write a converter for my Company from Metafile to SVG (TCanvas->arc).
I already finished to convert rectangle or some other elements but i dont get it how i can convert the arc.
I write my Code in JavaScript. :)
I have a file and i read it in buffer and get the values but that is uninteresting for you.
So we currently have all the values I can get:
Point1,Point2,Start,End
These 4 points are given and from this I should draw an arc now
dc->Arc (Point1.x + offset->x,
Point1.y + offset->y,
Point2.x + offset->x,
Point2.y + offset->y,
Start.x + offset->x,
Start.y + offset->y,
Ende.x + offset->x,
Ende.y + offset->y);
They are currently drawing the arc with this command. You can not pay attention to the offset here.
How can i get all Informations from my given points to draw in Arc in SVG.
for Example real values:
Point1: -50, -6
Point2: -10, 34
Start: -10, 34
End: -10, -6
or
Point1: 1, 18
Point2: 41, 58
Start: 1, 18
End: 1, 58
How do I get to the: large-arc-flag, sweep-flag and rotation and what values do I have to use or calculate that it is drawn correctly.
I tried to draw it and looked at a lot of documentation and tried to create it in writing.
I've whipped up something that seems to work. It's based on the documentation here.
I haven't tested it exhaustively.
I've made the assumption that, in a TCanvas, (0,0) is at the top. If it isn't, you'll need to reverse the logic of the sweep and large arc flags.
var svg = document.querySelector("svg");
var debug = svg.getElementById("debug");
function arc(x1, y1, x2, y2, x3, y3, x4, y4)
{
let xRadius = Math.abs(x2 - x1) / 2;
let yRadius = Math.abs(y2 - y1) / 2;
let xCentre = Math.min(x1, x2) + xRadius;
let yCentre = Math.min(y1, y2) + yRadius;
// get intercepts relative to ellipse centre
let startpt = interceptEllipseAndLine(xRadius, yRadius, x3 - xCentre, y3 - yCentre);
let endpt = interceptEllipseAndLine(xRadius, yRadius, x4 - xCentre, y4 - yCentre);
let largeArcFlag = isLargeArc(startpt, endpt) ? 1 : 0;
return ['M', xCentre + startpt.x, yCentre + startpt.y,
'A', xRadius, yRadius, 0, largeArcFlag, 0, xCentre + endpt.x, yCentre + endpt.y].join(' ');
}
// Finds the intercept of an ellipse and a line from centre to x0,y0
function interceptEllipseAndLine(xRadius, yRadius, x0,y0)
{
let den = Math.sqrt(xRadius * xRadius * y0 * y0 + yRadius * yRadius * x0 * x0);
let mult = xRadius * yRadius / den;
return {x: mult * x0, y: mult * y0};
}
// Returns true if the angle between the two intercept lines is >= 180deg
function isLargeArc(start, end)
{
let angle = Math.atan2(start.x * end.y - start.y * end.x, start.x * end.x + start.y * end.y);
return angle > 0;
}
let path1 = svg.getElementById("path1");
path1.setAttribute("d", arc(1, 18, 41, 58, 1, 18, 1, 58) );
let path2 = svg.getElementById("path2");
path2.setAttribute("d", arc(-50, -6, -10, 34, -10, 34, -10, -6) );
svg {
width: 400px;
}
path {
fill: none;
stroke: red;
stroke-width: 1px;
}
<svg viewBox="-100 -100 200 200">
<path id="path1"/>
<path id="path2"/>
</svg>
And here's a version that adds some extra shapes for debugging purposes...
var svg = document.querySelector("svg");
var debug = svg.getElementById("debug");
function arc(x1, y1, x2, y2, x3, y3, x4, y4)
{
let xRadius = Math.abs(x2 - x1) / 2;
let yRadius = Math.abs(y2 - y1) / 2;
let xCentre = Math.min(x1, x2) + xRadius;
let yCentre = Math.min(y1, y2) + yRadius;
{
let rect = document.createElementNS(svg.namespaceURI, "rect");
rect.setAttribute("x", x1);
rect.setAttribute("y", y1);
rect.setAttribute("width", x2-x1);
rect.setAttribute("height", y2-y1);
debug.append(rect);
let ellipse = document.createElementNS(svg.namespaceURI, "ellipse");
ellipse.setAttribute("cx", xCentre);
ellipse.setAttribute("cy", yCentre);
ellipse.setAttribute("rx", xRadius);
ellipse.setAttribute("ry", yRadius);
debug.append(ellipse);
let start = document.createElementNS(svg.namespaceURI, "line");
start.setAttribute("x1", xCentre);
start.setAttribute("y1", yCentre);
start.setAttribute("x2", x3);
start.setAttribute("y2", y3);
debug.append(start);
let end = document.createElementNS(svg.namespaceURI, "line");
end.setAttribute("x1", xCentre);
end.setAttribute("y1", yCentre);
end.setAttribute("x2", x4);
end.setAttribute("y2", y4);
debug.append(end);
}
// get intercepts relative to ellipse centre
let startpt = interceptEllipseAndLine(xRadius, yRadius, x3 - xCentre, y3 - yCentre);
let endpt = interceptEllipseAndLine(xRadius, yRadius, x4 - xCentre, y4 - yCentre);
let largeArcFlag = isLargeArc(startpt, endpt) ? 1 : 0;
{
let circ = document.createElementNS(svg.namespaceURI, "circle");
circ.setAttribute("cx", xCentre + startpt.x);
circ.setAttribute("cy", yCentre + startpt.y);
circ.setAttribute("r", 1);
debug.append(circ);
}
return ['M', xCentre + startpt.x, yCentre + startpt.y,
'A', xRadius, yRadius, 0, largeArcFlag, 0, xCentre + endpt.x, yCentre + endpt.y].join(' ');
}
// Finds the intercept of an ellipse and a line from centre to x0,y0
function interceptEllipseAndLine(xRadius, yRadius, x0,y0)
{
let den = Math.sqrt(xRadius * xRadius * y0 * y0 + yRadius * yRadius * x0 * x0);
let mult = xRadius * yRadius / den;
return {x: mult * x0, y: mult * y0};
}
// Returns true if the angle between the two intercept lines is >= 180deg
function isLargeArc(start, end)
{
let angle = Math.atan2(start.x * end.y - start.y * end.x, start.x * end.x + start.y * end.y);
return angle > 0;
}
let path1 = svg.getElementById("path1");
path1.setAttribute("d", arc(1, 18, 41, 58, 1, 18, 1, 58) );
let path2 = svg.getElementById("path2");
path2.setAttribute("d", arc(-50, -6, -10, 34, -10, 34, -10, -6) );
svg {
width: 400px;
}
ellipse, rect, line {
fill: none;
stroke: lightgrey;
stroke-width: 0.5px;
}
path {
fill: none;
stroke: red;
stroke-width: 1px;
}
<svg viewBox="-100 -100 200 200">
<g id="debug"></g>
<path id="path1"/>
<path id="path2"/>
</svg>
Update: Pie
For the Pie function, it should be almost identical to arc() but it will return a slightly different path.
function pie(x1, y1, x2, y2, x3, y3, x4, y4)
{
// ... rest of function is the same as arc() ...
return ['M', xCentre, yCentre,
'L', xCentre + startpt.x, yCentre + startpt.y,
'A', xRadius, yRadius, 0, largeArcFlag, 0, xCentre + endpt.x, yCentre + endpt.y,
'Z'].join(' ');
}
I am trying to create a hexagon using an svg polygon.
I want to create the x and why coordinates but my code is not working.
I thought I could use the trig functions by transforming each point by 60 degrees.
It is clearly not working.
const radius = 25;
const points = [0, 1, 2, 3, 4, 5, 6].map((n) => {
const current = n * 60;
return [radius * Math.cos(current), -radius * Math.sin(current)];
}).map((p) => p.join(','))
.join(' ');
document.querySelector('polygon')
.setAttribute("points", "100,0 50,0 100,100");
<svg width="200px" height="200px" viewBox="0 0 200 200">
<polygon points="" style="fill: #ccffcc; stroke: red;stroke-width: 3;"
/>
</svg>
According to this article, it can be converted to javascript like:
const radius = 50;
const height = 200;
const width = 200;
const points = [0, 1, 2, 3, 4, 5, 6].map((n, i) => {
var angle_deg = 60 * i - 30;
var angle_rad = Math.PI / 180 * angle_deg;
return [width/2 + radius * Math.cos(angle_rad), height/2 + radius * Math.sin(angle_rad)];
}).map((p) => p.join(','))
.join(' ');
document.querySelector('polygon')
.setAttribute("points", points);
<svg width="200px" height="200px" viewBox="0 0 200 200">
<polygon points="" style="fill: #ccffcc; stroke: red;stroke-width: 3;"
/>
</svg>
You have one too many indexes in the example above and your actually adding commas in your first join when you don't need to be. Here is a cleaned-up version.
const generateHexPoints = (radius, height, width) => {
const hexPoints = new Array(6)
for (let i = 0; i < hexPoints.length; i++) {
const angleDeg = 60 * i - 30
const angleRad = (Math.PI / 180) * angleDeg;
hexPoints[i] = [
width/2 + radius * Math.cos(angleRad),
height/2 + radius * Math.sin(angleRad)
];
}
return hexPoints.map((p) => p.join(' ')).join(' ')
}
I have to move the small rectangle on the path. The rectangle moves after a click inside the canvas.
I am not able to animate it as the object just jumps to the required point.
Please find the code on Fiddle.
HTML
<canvas id="myCanvas" width=578 height=200></canvas>
CSS
#myCanvas {
width:578px;
height:200px;
border:2px thin;
}
JavaScript
var myRectangle = {
x: 100,
y: 20,
width: 25,
height: 10,
borderWidth: 1
};
$(document).ready(function () {
$('#myCanvas').css("border", "2px solid black");
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var cntxt = canvas.getContext('2d');
drawPath(context);
drawRect(myRectangle, cntxt);
$('#myCanvas').click(function () {
function animate(myRectangle, canvas, cntxt, startTime) {
var time = (new Date()).getTime() - startTime;
var linearSpeed = 10;
var newX = Math.round(Math.sqrt((100 * 100) + (160 * 160)));
if (newX < canvas.width - myRectangle.width - myRectangle.borderWidth / 2) {
myRectangle.x = newX;
}
context.clearRect(0, 0, canvas.width, canvas.height);
drawPath(context);
drawRect(myRectangle, cntxt);
// request new frame
requestAnimFrame(function () {
animate(myRectangle, canvas, cntxt, startTime);
});
}
drawRect(myRectangle, cntxt);
myRectangle.x = 100;
myRectangle.y = 121;
setTimeout(function () {
var startTime = (new Date()).getTime();
animate(myRectangle, canvas, cntxt, startTime);
}, 1000);
});
});
$(document).keypress(function (e) {
if (e.which == 13) {
$('#myCanvas').click();
}
});
function drawRect(myRectangle, cntxt) {
cntxt.beginPath();
cntxt.rect(myRectangle.x, myRectangle.y, myRectangle.width, myRectangle.height);
cntxt.fillStyle = 'cyan';
cntxt.fill();
cntxt.strokeStyle = 'black';
cntxt.stroke();
};
function drawPath(context) {
context.beginPath();
context.moveTo(100, 20);
// line 1
context.lineTo(200, 160);
// quadratic curve
context.quadraticCurveTo(230, 200, 250, 120);
// bezier curve
context.bezierCurveTo(290, -40, 300, 200, 400, 150);
// line 2
context.lineTo(500, 90);
context.lineWidth = 5;
context.strokeStyle = 'blue';
context.stroke();
};
Here is how to move an object along a particular path
Animation involves movement over time. So for each “frame” of your animation you need to know the XY coordinate where to draw your moving object (rectangle).
This code takes in a percent-complete (0.00 to 1.00) and returns the XY coordinate which is that percentage along the path segment. For example:
0.00 will return the XY at the beginning of the line (or curve).
0.50 will return the XY at the middle of the line (or curve).
1.00 will return the XY at the end of the line (or curve).
Here is the code to get the XY at the specified percentage along a line:
// line: percent is 0-1
function getLineXYatPercent(startPt,endPt,percent) {
var dx = endPt.x-startPt.x;
var dy = endPt.y-startPt.y;
var X = startPt.x + dx*percent;
var Y = startPt.y + dy*percent;
return( {x:X,y:Y} );
}
Here is the code to get the XY at the specified percentage along a quadratic bezier curve:
// quadratic bezier: percent is 0-1
function getQuadraticBezierXYatPercent(startPt,controlPt,endPt,percent) {
var x = Math.pow(1-percent,2) * startPt.x + 2 * (1-percent) * percent * controlPt.x + Math.pow(percent,2) * endPt.x;
var y = Math.pow(1-percent,2) * startPt.y + 2 * (1-percent) * percent * controlPt.y + Math.pow(percent,2) * endPt.y;
return( {x:x,y:y} );
}
Here is the code to get the XY at the specified percentage along a cubic bezier curve:
// cubic bezier percent is 0-1
function getCubicBezierXYatPercent(startPt,controlPt1,controlPt2,endPt,percent){
var x=CubicN(percent,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
var y=CubicN(percent,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
return({x:x,y:y});
}
// cubic helper formula at percent distance
function CubicN(pct, a,b,c,d) {
var t2 = pct * pct;
var t3 = t2 * pct;
return a + (-a * 3 + pct * (3 * a - a * pct)) * pct
+ (3 * b + pct * (-6 * b + b * 3 * pct)) * pct
+ (c * 3 - c * 3 * pct) * t2
+ d * t3;
}
And here is how you put it all together to animate the various segments of your path
// calculate the XY where the tracking will be drawn
if(pathPercent<25){
var line1percent=pathPercent/24;
xy=getLineXYatPercent({x:100,y:20},{x:200,y:160},line1percent);
}
else if(pathPercent<50){
var quadPercent=(pathPercent-25)/24
xy=getQuadraticBezierXYatPercent({x:200,y:160},{x:230,y:200},{x:250,y:120},quadPercent);
}
else if(pathPercent<75){
var cubicPercent=(pathPercent-50)/24
xy=getCubicBezierXYatPercent({x:250,y:120},{x:290,y:-40},{x:300,y:200},{x:400,y:150},cubicPercent);
}
else {
var line2percent=(pathPercent-75)/25
xy=getLineXYatPercent({x:400,y:150},{x:500,y:90},line2percent);
}
// draw the tracking rectangle
drawRect(xy);
Here is working code and a Fiddle: http://jsfiddle.net/m1erickson/LumMX/
<!doctype html>
<html lang="en">
<head>
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>
<script src="http://code.jquery.com/ui/1.10.3/jquery-ui.js"></script>
<script>
$(function() {
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// set starting values
var fps = 60;
var percent=0
var direction=1;
// start the animation
animate();
function animate() {
// set the animation position (0-100)
percent+=direction;
if(percent<0){ percent=0; direction=1; };
if(percent>100){ percent=100; direction=-1; };
draw(percent);
// request another frame
setTimeout(function() {
requestAnimationFrame(animate);
}, 1000 / fps);
}
// draw the current frame based on sliderValue
function draw(sliderValue){
// redraw path
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(100, 20);
ctx.lineTo(200, 160);
ctx.strokeStyle = 'red';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(200, 160);
ctx.quadraticCurveTo(230, 200, 250, 120);
ctx.strokeStyle = 'green';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(250,120);
ctx.bezierCurveTo(290, -40, 300, 200, 400, 150);
ctx.strokeStyle = 'blue';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(400, 150);
ctx.lineTo(500, 90);
ctx.strokeStyle = 'gold';
ctx.stroke();
// draw the tracking rectangle
var xy;
if(sliderValue<25){
var percent=sliderValue/24;
xy=getLineXYatPercent({x:100,y:20},{x:200,y:160},percent);
}
else if(sliderValue<50){
var percent=(sliderValue-25)/24
xy=getQuadraticBezierXYatPercent({x:200,y:160},{x:230,y:200},{x:250,y:120},percent);
}
else if(sliderValue<75){
var percent=(sliderValue-50)/24
xy=getCubicBezierXYatPercent({x:250,y:120},{x:290,y:-40},{x:300,y:200},{x:400,y:150},percent);
}
else {
var percent=(sliderValue-75)/25
xy=getLineXYatPercent({x:400,y:150},{x:500,y:90},percent);
}
drawRect(xy,"red");
}
// draw tracking rect at xy
function drawRect(point,color){
ctx.fillStyle="cyan";
ctx.strokeStyle="gray";
ctx.lineWidth=3;
ctx.beginPath();
ctx.rect(point.x-13,point.y-8,25,15);
ctx.fill();
ctx.stroke();
}
// draw tracking dot at xy
function drawDot(point,color){
ctx.fillStyle=color;
ctx.strokeStyle="black";
ctx.lineWidth=3;
ctx.beginPath();
ctx.arc(point.x,point.y,8,0,Math.PI*2,false);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
// line: percent is 0-1
function getLineXYatPercent(startPt,endPt,percent) {
var dx = endPt.x-startPt.x;
var dy = endPt.y-startPt.y;
var X = startPt.x + dx*percent;
var Y = startPt.y + dy*percent;
return( {x:X,y:Y} );
}
// quadratic bezier: percent is 0-1
function getQuadraticBezierXYatPercent(startPt,controlPt,endPt,percent) {
var x = Math.pow(1-percent,2) * startPt.x + 2 * (1-percent) * percent * controlPt.x + Math.pow(percent,2) * endPt.x;
var y = Math.pow(1-percent,2) * startPt.y + 2 * (1-percent) * percent * controlPt.y + Math.pow(percent,2) * endPt.y;
return( {x:x,y:y} );
}
// cubic bezier percent is 0-1
function getCubicBezierXYatPercent(startPt,controlPt1,controlPt2,endPt,percent){
var x=CubicN(percent,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
var y=CubicN(percent,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
return({x:x,y:y});
}
// cubic helper formula at percent distance
function CubicN(pct, a,b,c,d) {
var t2 = pct * pct;
var t3 = t2 * pct;
return a + (-a * 3 + pct * (3 * a - a * pct)) * pct
+ (3 * b + pct * (-6 * b + b * 3 * pct)) * pct
+ (c * 3 - c * 3 * pct) * t2
+ d * t3;
}
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="canvas" width=600 height=300></canvas>
</body>
</html>
If you're gonna use the built-in Bezier curves of the canvas, you would still need to do the math yourself.
You can use this implementation of a cardinal spline and have all the points returned for you pre-calculated.
An example of usage is this little sausage-mobile moving along the slope (generated with the above cardinal spline):
Full demo here (cut-and-copy as you please).
The main things you need is when you have the point array is to find two points you want to use for the object. This will give us the angle of the object:
cPoints = quantX(pointsFromCardinalSpline); //see below
//get points from array (dx = current array position)
x1 = cPoints[dx];
y1 = cPoints[dx + 1];
//get end-points from array (dlt=length, must be an even number)
x2 = cPoints[dx + dlt];
y2 = cPoints[dx + dlt + 1];
To avoid stretching in steeper slopes we recalculate the length based on angle. To get an approximate angle we use the original end-point to get an angle, then we calculate a new length of the line based on wanted length and this angle:
var dg = getLineAngle(x1, y1, x2, y2);
var l = ((((lineToAngle(x1, y2, dlt, dg).x - x1) / 2) |0) * 2);
x2 = cPoints[dx + l];
y2 = cPoints[dx + l + 1];
Now we can plot the "car" along the slope by subtracting it's vertical height from the y positions.
What you will notice doing just this is that the "car" moves at variable speed. This is due to the interpolation of the cardinal spline.
We can smooth it out so the speed look more even by quantize the x axis. It will still not be perfect as in steep slopes the y-distance between to points will be greater than on a flat surface - we would really need a quadratic quantization, but for this purpose we do only the x-axis.
This gives us a new array with new points for each x-position:
function quantX(pts) {
var min = 99999999,
max = -99999999,
x, y, i, p = pts.length,
res = [];
//find min and max of x axis
for (i = 0; i < pts.length - 1; i += 2) {
if (pts[i] > max) max = pts[i];
if (pts[i] < min) min = pts[i];
}
max = max - min;
//this will quantize non-existng points
function _getY(x) {
var t = p,
ptX1, ptX2, ptY1, ptY2, f, y;
for (; t >= 0; t -= 2) {
ptX1 = pts[t];
ptY1 = pts[t + 1];
if (x >= ptX1) {
//p = t + 2;
ptX2 = pts[t + 2];
ptY2 = pts[t + 3];
f = (ptY2 - ptY1) / (ptX2 - ptX1);
y = (ptX1 - x) * f;
return ptY1 - y;
}
}
}
//generate new array per-pixel on the x-axis
//note: will not work if curve suddenly goes backwards
for (i = 0; i < max; i++) {
res.push(i);
res.push(_getY(i));
}
return res;
}
The other two functions we need is the one calculating the angle for a line, and the one calculating end-points based on angle and length:
function getLineAngle(x1, y1, x2, y2) {
var dx = x2 - x1,
dy = y2 - y1,
th = Math.atan2(dy, dx);
return th * 180 / Math.PI;
}
function lineToAngle(x1, y1, length, angle) {
angle *= Math.PI / 180;
var x2 = x1 + length * Math.cos(angle),
y2 = y1 + length * Math.sin(angle);
return {x: x2, y: y2};
}