Skip to content

Commit bcef24c

Browse files
committed
Round corners of boardfill and generalize the rounding and offsetting of polygons
1 parent a34851e commit bcef24c

3 files changed

Lines changed: 119 additions & 31 deletions

File tree

src/boards/hexOfTri.ts

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { IPoint, IPolyPolygon } from "../grids";
22
import { RendererBase } from "../renderers/_base";
33
import { hexOfTri as hexOfTriGrid } from "../grids";
44
import { BoardReturn } from ".";
5+
import { offsetPolygon } from "../common/plotting";
56

67
export const hexOfTri = (ctx: RendererBase): BoardReturn => {
78
if ( (ctx.json === undefined) || (ctx.rootSvg === undefined) ) {
@@ -188,61 +189,53 @@ export const hexOfTri = (ctx: RendererBase): BoardReturn => {
188189
const widest = Math.max(...grid.map(row => row.length));
189190
const numWidest = grid.findIndex(row => row.length === widest);
190191
let boardFill: IPolyPolygon;
191-
const dist = cellsize / 1.5;
192192
// if numWidest is not the top or the bottom, then we have a full hex
193193
if (numWidest !== numTop && numWidest !== numBottom) {
194-
const xDist = dist / Math.sqrt(3);
195194
boardFill = {
196195
type: "poly",
197-
points: [
198-
{x: grid[numTop][0].x - xDist, y: grid[numTop][0].y - dist},
199-
{x: grid[numTop][grid[numTop].length - 1].x + xDist, y: grid[numTop][grid[numTop].length - 1].y - dist},
200-
{x: grid[numWidest][grid[numWidest].length - 1].x + (2 * xDist), y: grid[numWidest][grid[numWidest].length - 1].y},
201-
{x: grid[numBottom][grid[numBottom].length - 1].x + xDist, y: grid[numBottom][grid[numBottom].length - 1].y + dist},
202-
{x: grid[numBottom][0].x - xDist, y: grid[numBottom][0].y + dist},
203-
{x: grid[numWidest][0].x - (2 * xDist), y: grid[numWidest][0].y},
204-
],
196+
points: offsetPolygon([
197+
{x: grid[numTop][0].x, y: grid[numTop][0].y},
198+
{x: grid[numTop][grid[numTop].length - 1].x, y: grid[numTop][grid[numTop].length - 1].y},
199+
{x: grid[numWidest][grid[numWidest].length - 1].x, y: grid[numWidest][grid[numWidest].length - 1].y},
200+
{x: grid[numBottom][grid[numBottom].length - 1].x, y: grid[numBottom][grid[numBottom].length - 1].y},
201+
{x: grid[numBottom][0].x, y: grid[numBottom][0].y},
202+
{x: grid[numWidest][0].x, y: grid[numWidest][0].y},
203+
], cellsize / 1.5),
205204
};
206205
}
207206
// otherwise we just have a four-point poly
208207
else {
209-
const xDist = dist / Math.sqrt(3);
210208
const ptWideL = {
211-
x: grid[numWidest][0].x - (2 * xDist),
212-
y: grid[numWidest][0].y + dist * (half === "top" ? 1 : -1),
209+
x: grid[numWidest][0].x,
210+
y: grid[numWidest][0].y,
213211
};
214212
const ptWideR = {
215-
x: grid[numWidest][grid[numWidest].length - 1].x + (2 * xDist),
216-
y: grid[numWidest][grid[numWidest].length - 1].y + dist * (half === "top" ? 1 : -1),
213+
x: grid[numWidest][grid[numWidest].length - 1].x,
214+
y: grid[numWidest][grid[numWidest].length - 1].y,
217215
};
218216
let ptOtherL: IPoint; let ptOtherR: IPoint;
219217
if (half === "top") {
220218
ptOtherL = {
221-
x: grid[numTop][0].x - xDist,
222-
y: grid[numTop][0].y - dist
219+
x: grid[numTop][0].x,
220+
y: grid[numTop][0].y
223221
};
224222
ptOtherR = {
225-
x: grid[numTop][grid[numTop].length - 1].x + xDist,
226-
y: grid[numTop][grid[numTop].length - 1].y - dist
223+
x: grid[numTop][grid[numTop].length - 1].x,
224+
y: grid[numTop][grid[numTop].length - 1].y
227225
};
228226
} else {
229227
ptOtherL = {
230-
x: grid[numBottom][0].x - xDist,
231-
y: grid[numBottom][0].y + dist
228+
x: grid[numBottom][0].x,
229+
y: grid[numBottom][0].y
232230
};
233231
ptOtherR = {
234-
x: grid[numBottom][grid[numBottom].length - 1].x + xDist,
235-
y: grid[numBottom][grid[numBottom].length - 1].y + dist
232+
x: grid[numBottom][grid[numBottom].length - 1].x,
233+
y: grid[numBottom][grid[numBottom].length - 1].y
236234
};
237235
}
238236
boardFill = {
239237
type: "poly",
240-
points: [
241-
ptWideL,
242-
ptOtherL,
243-
ptOtherR,
244-
ptWideR,
245-
],
238+
points: offsetPolygon([ptWideL, ptOtherL, ptOtherR, ptWideR], cellsize / 1.5),
246239
};
247240
}
248241

src/common/plotting.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,97 @@ export const shortenLine = (x1: number, y1: number, x2: number, y2: number, val:
333333
const newy2 = y1 + (t1 * (y2 - y1));
334334
return [newx1, newy1, newx2, newy2];
335335
}
336+
337+
/**
338+
* Takes a closed polygon and generates a polygon of the exact same dimensions and angles
339+
* but larger or smaller by a fixed amount.
340+
*
341+
* @param points - The vertices of the polygon
342+
* @param distance - The amount to offset the polygon. Positive expands, negative contracts.
343+
* @returns The new set of vertices
344+
*/
345+
export const offsetPolygon = (points: IPoint[], distance: number): IPoint[] => {
346+
// Determine winding order (CW is positive area in SVG y-down coords)
347+
let area = 0;
348+
for (let i = 0; i < points.length; i++) {
349+
const j = (i + 1) % points.length;
350+
area += points[i].x * points[j].y;
351+
area -= points[j].x * points[i].y;
352+
}
353+
const isCW = area > 0;
354+
355+
const newPoints: IPoint[] = [];
356+
for (let i = 0; i < points.length; i++) {
357+
const prev = points[(i - 1 + points.length) % points.length];
358+
const curr = points[i];
359+
const next = points[(i + 1) % points.length];
360+
361+
const v1 = { x: curr.x - prev.x, y: curr.y - prev.y };
362+
const len1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
363+
let n1 = { x: -v1.y / len1, y: v1.x / len1 };
364+
365+
const v2 = { x: next.x - curr.x, y: next.y - curr.y };
366+
const len2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
367+
let n2 = { x: -v2.y / len2, y: v2.x / len2 };
368+
369+
// If CW, normals pointing "right" are pointing inside. Invert to point outside.
370+
if (isCW) {
371+
n1 = { x: -n1.x, y: -n1.y };
372+
n2 = { x: -n2.x, y: -n2.y };
373+
}
374+
375+
const p1 = { x: prev.x + n1.x * distance, y: prev.y + n1.y * distance };
376+
const C1 = n1.x * p1.x + n1.y * p1.y;
377+
378+
const p2 = { x: curr.x + n2.x * distance, y: curr.y + n2.y * distance };
379+
const C2 = n2.x * p2.x + n2.y * p2.y;
380+
381+
const det = n1.x * n2.y - n2.x * n1.y;
382+
383+
if (Math.abs(det) < 1e-6) {
384+
newPoints.push({ x: curr.x + n1.x * distance, y: curr.y + n1.y * distance });
385+
} else {
386+
const x = (n2.y * C1 - n1.y * C2) / det;
387+
const y = (n1.x * C2 - n2.x * C1) / det;
388+
newPoints.push({ x, y });
389+
}
390+
}
391+
return newPoints;
392+
}
393+
394+
export const roundPolygon = (points: IPoint[], radius: number): string => {
395+
let d = "";
396+
for (let i = 0; i < points.length; i++) {
397+
const curr = points[i];
398+
const prev = points[(i - 1 + points.length) % points.length];
399+
const next = points[(i + 1) % points.length];
400+
401+
const v1 = { x: curr.x - prev.x, y: curr.y - prev.y };
402+
const len1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
403+
const v2 = { x: next.x - curr.x, y: next.y - curr.y };
404+
const len2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
405+
406+
if (len1 < 0.001 || len2 < 0.001) {
407+
if (i === 0) {
408+
d += `M ${curr.x},${curr.y}`;
409+
} else {
410+
d += ` L ${curr.x},${curr.y}`;
411+
}
412+
continue;
413+
}
414+
415+
const r = Math.min(radius, len1 / 2, len2 / 2);
416+
417+
const p1 = { x: curr.x - (v1.x / len1) * r, y: curr.y - (v1.y / len1) * r };
418+
const p2 = { x: curr.x + (v2.x / len2) * r, y: curr.y + (v2.y / len2) * r };
419+
420+
if (i === 0) {
421+
d += `M ${p1.x},${p1.y}`;
422+
} else {
423+
d += ` L ${p1.x},${p1.y}`;
424+
}
425+
d += ` Q ${curr.x},${curr.y} ${p2.x},${p2.y}`;
426+
}
427+
d += " Z";
428+
return d;
429+
}

src/renderers/_base.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Hex } from "honeycomb-grid";
44
import { GridPoints, IPoint, type Poly, IPolyPolygon } from "../grids/_base";
55
import { AnnotationBasic, AnnotationSowing, APRenderRep, AreaButtonBar, AreaCompassRose, AreaKey, AreaPieces, AreaReserves, AreaScrollBar, ButtonBarButton, Colourfuncs, FunctionBestContrast, Glyph, Gradient, MarkerFence, MarkerFences, type Polymatrix } from "../schemas/schema";
66
import { sheets } from "../sheets";
7-
import { projectPoint, scale, rotate, usePieceAt, calcPyramidOffset, calcLazoOffset, projectPointEllipse, rotatePoint, calcBearing, smallestDegreeDiff, shortenLine } from "../common/plotting";
7+
import { projectPoint, scale, rotate, usePieceAt, calcPyramidOffset, calcLazoOffset, projectPointEllipse, rotatePoint, calcBearing, smallestDegreeDiff, shortenLine, roundPolygon } from "../common/plotting";
88
import { glyph2uid, x2uid } from "../common/glyph2uid";
99
import tinycolor from "tinycolor2";
1010
import { unionPolys } from "../common/polys";
@@ -3739,7 +3739,8 @@ export abstract class RendererBase {
37393739

37403740
}
37413741
else if (boardFill.type === "poly") {
3742-
const poly = this.rootSvg.polygon(boardFill.points.map((p) => `${p.x},${p.y}`).join(" ")).id("aprender-backfill").fill({color: bgcolour, opacity: bgopacity});
3742+
const path = roundPolygon(boardFill.points, 15);
3743+
const poly = this.rootSvg.path(path).id("aprender-backfill").fill({color: bgcolour, opacity: bgopacity}).stroke("none");
37433744
// `board` backfill can't just be pushed to the back but must be inside the `board` group
37443745
board.add(poly, 0);
37453746
}

0 commit comments

Comments
 (0)