简体   繁体   中英

How to draw an irregular shaped polygon using the given angles

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 = (ex - canvasData.left) * (canvas.width / canvasData.width); mouse.y = (ey - 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.

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 = (ex - canvasData.left) * (canvas.width / canvasData.width); mouse.y = (ey - 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> 

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(px, py) } 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> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM