From aa47012d4bcf1168d544fbd84dbba3d16cdc279d Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sun, 14 Feb 2021 21:27:20 -0500 Subject: [PATCH] Use arrows for attachment points instead of circles. (#3) Here we attempt to use arrowhead symbols in SVGs to allow artists to specify the orientation of attached symbols, rather than attempting to auto-detect it via projecting the circles onto the shape and using the normal as we did in #2. It seems like this logic is both much simpler and less error-prone, especially around corners. It also gives artists much more control over the placement of attachments. The one fragile aspect of the implementation is that we assume the first point of the arrow shape is its tip, and that the third point is its indented bottom. This appears to be consistent across all of Nina's SVGs, so far at least... --- lib/specs.ts | 212 ++++-------------- svg/antler specs.svg | 18 -- svg/arm arrows.svg | 23 ++ svg/arm specs.svg | 21 -- svg/bone arrows.svg | 22 ++ svg/church arrows.svg | 23 ++ svg/circle arrows.svg | 21 ++ svg/circle specs.svg | 19 -- svg/cloud arrows.svg | 21 ++ svg/cloud specs.svg | 19 -- svg/crown arrows.svg | 24 ++ svg/crown specs.svg | 21 -- svg/cup arrows.svg | 22 ++ svg/cup specs.svg | 19 -- svg/eye arrows.svg | 22 ++ svg/eye specs.svg | 19 -- svg/goat horn arrows.svg | 21 ++ svg/goat horn specs.svg | 19 -- svg/{hand specs.svg => hand arrows.svg} | 18 +- svg/heart arrows.svg | 21 ++ svg/heart specs.svg | 18 -- svg/leg arrows.svg | 21 ++ svg/leg hoof arrows.svg | 22 ++ svg/leg hoof specs.svg | 20 -- svg/leg specs.svg | 18 -- svg/lightning arrows.svg | 21 ++ ...le arm specs.svg => muscle arm arrows.svg} | 20 +- svg/skull arrows.svg | 21 ++ svg/sword arrows.svg | 26 +++ svg/tail arrows.svg | 21 ++ svg/tail specs.svg | 20 -- svg/teardrop specs.svg | 18 -- svg/wing arrows.svg | 21 ++ svg/wing specs.svg | 19 -- 34 files changed, 433 insertions(+), 458 deletions(-) delete mode 100644 svg/antler specs.svg create mode 100644 svg/arm arrows.svg delete mode 100644 svg/arm specs.svg create mode 100644 svg/bone arrows.svg create mode 100644 svg/church arrows.svg create mode 100644 svg/circle arrows.svg delete mode 100644 svg/circle specs.svg create mode 100644 svg/cloud arrows.svg delete mode 100644 svg/cloud specs.svg create mode 100644 svg/crown arrows.svg delete mode 100644 svg/crown specs.svg create mode 100644 svg/cup arrows.svg delete mode 100644 svg/cup specs.svg create mode 100644 svg/eye arrows.svg delete mode 100644 svg/eye specs.svg create mode 100644 svg/goat horn arrows.svg delete mode 100644 svg/goat horn specs.svg rename svg/{hand specs.svg => hand arrows.svg} (52%) create mode 100644 svg/heart arrows.svg delete mode 100644 svg/heart specs.svg create mode 100644 svg/leg arrows.svg create mode 100644 svg/leg hoof arrows.svg delete mode 100644 svg/leg hoof specs.svg delete mode 100644 svg/leg specs.svg create mode 100644 svg/lightning arrows.svg rename svg/{muscle arm specs.svg => muscle arm arrows.svg} (53%) create mode 100644 svg/skull arrows.svg create mode 100644 svg/sword arrows.svg create mode 100644 svg/tail arrows.svg delete mode 100644 svg/tail specs.svg delete mode 100644 svg/teardrop specs.svg create mode 100644 svg/wing arrows.svg delete mode 100644 svg/wing specs.svg diff --git a/lib/specs.ts b/lib/specs.ts index 10c2160..e7abe55 100644 --- a/lib/specs.ts +++ b/lib/specs.ts @@ -1,11 +1,10 @@ -import { Point, BBox, Bezier, Line } from "../vendor/bezier-js"; +import { Point, BBox } from "../vendor/bezier-js"; import { getBoundingBoxCenter, getBoundingBoxForBeziers } from "./bounding-box"; import * as colors from "./colors"; import { pathToShapes } from "./path"; -import { flatten } from "./util"; import type { SvgSymbolElement } from "./vocabulary"; -const SPEC_LAYER_ID_RE = /^specs.*/i; +const SPEC_LAYER_ID_RE = /^arrows.*/i; export type PointWithNormal = { point: Point; @@ -21,16 +20,42 @@ export type Specs = { nesting?: BBox[]; }; -function getPointsWithEmptyNormals(path: string): PointWithNormal[] { +const NUM_ARROW_POINTS = 4; +const ARROW_TOP_POINT_IDX = 0; +const ARROW_BOTTOM_POINT_IDX = 2; + +function subtractPoints(p1: Point, p2: Point): Point { + return { + x: p1.x - p2.x, + y: p1.y - p2.y, + }; +} + +function normalizePoint(p: Point): Point { + const len = Math.sqrt(Math.pow(p.x, 2) + Math.pow(p.y, 2)); + return { + x: p.x / len, + y: p.y / len, + }; +} + +function getArrowPoints(path: string): PointWithNormal[] { const shapes = pathToShapes(path); const points: PointWithNormal[] = []; for (let shape of shapes) { - const bbox = getBoundingBoxForBeziers(shape); - const point = getBoundingBoxCenter(bbox); + if (shape.length !== NUM_ARROW_POINTS) { + throw new Error( + `Expected arrow to have ${NUM_ARROW_POINTS} points, not ${shape.length}!` + ); + } + const point = shape[ARROW_BOTTOM_POINT_IDX].get(0.0); + const normal = normalizePoint( + subtractPoints(shape[ARROW_TOP_POINT_IDX].get(0.0), point) + ); points.push({ point, - normal: ORIGIN, + normal, }); } @@ -57,27 +82,27 @@ function updateSpecs(fill: string, path: string, specs: Specs): Specs { case colors.TAIL_ATTACHMENT_COLOR: return { ...specs, - tail: concat(specs.tail, getPointsWithEmptyNormals(path)), + tail: concat(specs.tail, getArrowPoints(path)), }; case colors.LEG_ATTACHMENT_COLOR: return { ...specs, - leg: concat(specs.leg, getPointsWithEmptyNormals(path)), + leg: concat(specs.leg, getArrowPoints(path)), }; case colors.ARM_ATTACHMENT_COLOR: return { ...specs, - arm: concat(specs.arm, getPointsWithEmptyNormals(path)), + arm: concat(specs.arm, getArrowPoints(path)), }; case colors.HORN_ATTACHMENT_COLOR: return { ...specs, - horn: concat(specs.horn, getPointsWithEmptyNormals(path)), + horn: concat(specs.horn, getArrowPoints(path)), }; case colors.CROWN_ATTACHMENT_COLOR: return { ...specs, - crown: concat(specs.crown, getPointsWithEmptyNormals(path)), + crown: concat(specs.crown, getArrowPoints(path)), }; case colors.NESTING_BOUNDING_BOX_COLOR: return { @@ -110,163 +135,8 @@ function getSpecs(layers: SvgSymbolElement[]): Specs { return specs; } -function filterElements( - elements: SvgSymbolElement[], - filter: (el: SvgSymbolElement) => boolean -): SvgSymbolElement[] { - const result: SvgSymbolElement[] = []; - - for (let el of elements) { - if (filter(el)) { - switch (el.tagName) { - case "g": - result.push({ - ...el, - children: filterElements(el.children, filter), - }); - break; - case "path": - result.push(el); - break; - } - } - } - - return result; -} - -function getAllShapes(layers: SvgSymbolElement[]): Bezier[][] { - const beziers: Bezier[][] = []; - - for (let layer of layers) { - switch (layer.tagName) { - case "g": - beziers.push(...getAllShapes(layer.children)); - break; - case "path": - if (!layer.props.d) { - throw new Error(` does not have a "d" attribute!`); - } - beziers.push(...pathToShapes(layer.props.d)); - break; - } - } - - return beziers; -} - -const ORIGIN: Point = { x: 0, y: 0 }; - -function invertVector(v: Point) { - v.x = -v.x; - v.y = -v.y; -} - -const TO_INFINITY_AMOUNT = 2000; - -/** - * Return whether the given point is inside the given shape, assuming - * the SVG "evenodd" fill rule: - * - * https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule#evenodd - */ -function isPointInsideShape(point: Point, beziers: Bezier[]): boolean { - let intersections = 0; - const line: Line = { - p1: point, - p2: { - x: point.x + TO_INFINITY_AMOUNT, - y: point.y + TO_INFINITY_AMOUNT, - }, - }; - - for (let bezier of beziers) { - const points = bezier.lineIntersects(line); - intersections += points.length; - } - - const isOdd = intersections % 2 === 1; - return isOdd; -} - -function addPoints(p1: Point, p2: Point): Point { - return { - x: p1.x + p2.x, - y: p1.y + p2.y, - }; -} - -function populateNormals(pwns: PointWithNormal[], shapes: Bezier[][]) { - if (shapes.length === 0) { - throw new Error(`Expected beizers to be non-empty!`); - } - - for (let pwn of pwns) { - let minDistance = Infinity; - let minDistanceNormal = ORIGIN; - let minDistancePoint = ORIGIN; - for (let shape of shapes) { - let minDistanceChanged = false; - for (let bezier of shape) { - const { t, d } = bezier.project(pwn.point); - if (d === undefined || t === undefined) { - throw new Error(`Expected bezier.project() to return t and d!`); - } - if (d < minDistance) { - minDistanceChanged = true; - minDistance = d; - minDistanceNormal = bezier.normal(t); - minDistancePoint = bezier.get(t); - } - } - if (minDistanceChanged) { - const pointToNormal = addPoints(minDistancePoint, minDistanceNormal); - if (isPointInsideShape(pointToNormal, shape)) { - invertVector(minDistanceNormal); - } - } - } - pwn.normal = minDistanceNormal; - } -} - -function filterFilledShapes(elements: SvgSymbolElement[]): SvgSymbolElement[] { - return filterElements(elements, (el) => { - if (el.tagName === "path") { - if (el.props.fill === "none") return false; - if (el.props.fillRule !== "evenodd") { - throw new Error( - `Expected to have fill-rule="evenodd" but it is "${el.props.fillRule}"!` - ); - } - } - return true; - }); -} - -function populateSpecNormals(specs: Specs, layers: SvgSymbolElement[]): void { - const shapes = getAllShapes(filterFilledShapes(layers)); - - if (specs.tail) { - populateNormals(specs.tail, shapes); - } - if (specs.leg) { - populateNormals(specs.leg, shapes); - } - if (specs.arm) { - populateNormals(specs.arm, shapes); - } - if (specs.horn) { - populateNormals(specs.horn, shapes); - } - if (specs.crown) { - populateNormals(specs.crown, shapes); - } -} - export function extractSpecs( - layers: SvgSymbolElement[], - populateNormals: boolean = true + layers: SvgSymbolElement[] ): [Specs | undefined, SvgSymbolElement[]] { const layersWithoutSpecs: SvgSymbolElement[] = []; let specs: Specs | undefined = undefined; @@ -287,7 +157,7 @@ export function extractSpecs( if (id && SPEC_LAYER_ID_RE.test(id)) { setSpecs(getSpecs(layer.children)); } else { - let [s, children] = extractSpecs(layer.children, false); + let [s, children] = extractSpecs(layer.children); setSpecs(s); layersWithoutSpecs.push({ ...layer, @@ -301,9 +171,5 @@ export function extractSpecs( } } - if (populateNormals && specs) { - populateSpecNormals(specs, layersWithoutSpecs); - } - return [specs, layersWithoutSpecs]; } diff --git a/svg/antler specs.svg b/svg/antler specs.svg deleted file mode 100644 index b86a32c..0000000 --- a/svg/antler specs.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/svg/arm arrows.svg b/svg/arm arrows.svg new file mode 100644 index 0000000..3d8243c --- /dev/null +++ b/svg/arm arrows.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/arm specs.svg b/svg/arm specs.svg deleted file mode 100644 index e48cfcd..0000000 --- a/svg/arm specs.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/svg/bone arrows.svg b/svg/bone arrows.svg new file mode 100644 index 0000000..b4af7ff --- /dev/null +++ b/svg/bone arrows.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/church arrows.svg b/svg/church arrows.svg new file mode 100644 index 0000000..d91f0eb --- /dev/null +++ b/svg/church arrows.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/circle arrows.svg b/svg/circle arrows.svg new file mode 100644 index 0000000..5dd4159 --- /dev/null +++ b/svg/circle arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/circle specs.svg b/svg/circle specs.svg deleted file mode 100644 index 4e68941..0000000 --- a/svg/circle specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/svg/cloud arrows.svg b/svg/cloud arrows.svg new file mode 100644 index 0000000..28aa62c --- /dev/null +++ b/svg/cloud arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/cloud specs.svg b/svg/cloud specs.svg deleted file mode 100644 index f312642..0000000 --- a/svg/cloud specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/svg/crown arrows.svg b/svg/crown arrows.svg new file mode 100644 index 0000000..bbfe0be --- /dev/null +++ b/svg/crown arrows.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/crown specs.svg b/svg/crown specs.svg deleted file mode 100644 index 28eddf0..0000000 --- a/svg/crown specs.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/svg/cup arrows.svg b/svg/cup arrows.svg new file mode 100644 index 0000000..dc5019c --- /dev/null +++ b/svg/cup arrows.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/cup specs.svg b/svg/cup specs.svg deleted file mode 100644 index 656c202..0000000 --- a/svg/cup specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/svg/eye arrows.svg b/svg/eye arrows.svg new file mode 100644 index 0000000..ef1dfcc --- /dev/null +++ b/svg/eye arrows.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/eye specs.svg b/svg/eye specs.svg deleted file mode 100644 index 1726b23..0000000 --- a/svg/eye specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/svg/goat horn arrows.svg b/svg/goat horn arrows.svg new file mode 100644 index 0000000..d1675d0 --- /dev/null +++ b/svg/goat horn arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/goat horn specs.svg b/svg/goat horn specs.svg deleted file mode 100644 index 505403b..0000000 --- a/svg/goat horn specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/svg/hand specs.svg b/svg/hand arrows.svg similarity index 52% rename from svg/hand specs.svg rename to svg/hand arrows.svg index be9a9a1..6982050 100644 --- a/svg/hand specs.svg +++ b/svg/hand arrows.svg @@ -6,14 +6,16 @@ - - - - - - - - + + + + + + + + + + diff --git a/svg/heart arrows.svg b/svg/heart arrows.svg new file mode 100644 index 0000000..f0e8885 --- /dev/null +++ b/svg/heart arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/heart specs.svg b/svg/heart specs.svg deleted file mode 100644 index d812f20..0000000 --- a/svg/heart specs.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/svg/leg arrows.svg b/svg/leg arrows.svg new file mode 100644 index 0000000..5ff6699 --- /dev/null +++ b/svg/leg arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/leg hoof arrows.svg b/svg/leg hoof arrows.svg new file mode 100644 index 0000000..b305b1c --- /dev/null +++ b/svg/leg hoof arrows.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/leg hoof specs.svg b/svg/leg hoof specs.svg deleted file mode 100644 index 4edd9c6..0000000 --- a/svg/leg hoof specs.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/svg/leg specs.svg b/svg/leg specs.svg deleted file mode 100644 index 71cde40..0000000 --- a/svg/leg specs.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/svg/lightning arrows.svg b/svg/lightning arrows.svg new file mode 100644 index 0000000..5ff159f --- /dev/null +++ b/svg/lightning arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/muscle arm specs.svg b/svg/muscle arm arrows.svg similarity index 53% rename from svg/muscle arm specs.svg rename to svg/muscle arm arrows.svg index f7b2a7c..76906cb 100644 --- a/svg/muscle arm specs.svg +++ b/svg/muscle arm arrows.svg @@ -4,17 +4,19 @@ - - + + - + - - - - - - + + + + + + + + diff --git a/svg/skull arrows.svg b/svg/skull arrows.svg new file mode 100644 index 0000000..b0aff48 --- /dev/null +++ b/svg/skull arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/sword arrows.svg b/svg/sword arrows.svg new file mode 100644 index 0000000..e42b473 --- /dev/null +++ b/svg/sword arrows.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/tail arrows.svg b/svg/tail arrows.svg new file mode 100644 index 0000000..fe47d4f --- /dev/null +++ b/svg/tail arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/tail specs.svg b/svg/tail specs.svg deleted file mode 100644 index d43d641..0000000 --- a/svg/tail specs.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/svg/teardrop specs.svg b/svg/teardrop specs.svg deleted file mode 100644 index efe9f15..0000000 --- a/svg/teardrop specs.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/svg/wing arrows.svg b/svg/wing arrows.svg new file mode 100644 index 0000000..b702f6c --- /dev/null +++ b/svg/wing arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/wing specs.svg b/svg/wing specs.svg deleted file mode 100644 index 2341366..0000000 --- a/svg/wing specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - -