diff --git a/demo/hull.html b/demo/hull.html new file mode 100644 index 0000000..39d7ee3 --- /dev/null +++ b/demo/hull.html @@ -0,0 +1,49 @@ + + + + + + Shapes + + + + + + + + + + + + diff --git a/demo/shapes.html b/demo/shapes.html index c212694..5ed0c3b 100644 --- a/demo/shapes.html +++ b/demo/shapes.html @@ -17,13 +17,6 @@ fc-geometry { background: rgb(187, 178, 178); - box-shadow: rgba(0, 0, 0, 0.2) 1.95px 1.95px 2.6px; - transition: scale 100ms ease-out, box-shadow 100ms ease-out; - } - - fc-geometry:state(move) { - scale: 1.05; - box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; } diff --git a/src/arrows/abstract-arrow.ts b/src/arrows/abstract-arrow.ts index 98e0ab7..e9de015 100644 --- a/src/arrows/abstract-arrow.ts +++ b/src/arrows/abstract-arrow.ts @@ -1,22 +1,9 @@ import { FolkGeometry } from '../canvas/fc-geometry'; -import { Vertex } from './utils'; +import { parseVertex } from './utils'; import { ClientRectObserverEntry, ClientRectObserverManager } from '../client-rect-observer.ts'; const clientRectObserver = new ClientRectObserverManager(); -const vertexRegex = /(?-?([0-9]*[.])?[0-9]+),\s*(?-?([0-9]*[.])?[0-9]+)/; - -function parseVertex(str: string): Vertex | null { - const results = vertexRegex.exec(str); - - if (results === null) return null; - - return { - x: Number(results.groups?.x), - y: Number(results.groups?.y), - }; -} - function parseCSSSelector(selector: string): string[] { return selector.split('>>>').map((s) => s.trim()); } @@ -188,11 +175,6 @@ export class AbstractArrow extends HTMLElement { this.unobserveTarget(); } - // TODO: why reparse the vertex? - setSourceVertex(vertex: Vertex) { - this.target = `${vertex.x},${vertex.y}`; - } - observeSource() { this.unobserveSource(); diff --git a/src/arrows/utils.ts b/src/arrows/utils.ts index 1ebeea4..f760975 100644 --- a/src/arrows/utils.ts +++ b/src/arrows/utils.ts @@ -181,3 +181,16 @@ export function verticesToPolygon(vertices: Vertex[]): string { return `polygon(${vertices.map((vertex) => `${vertex.x}px ${vertex.y}px`).join(', ')})`; } + +const vertexRegex = /(?-?([0-9]*[.])?[0-9]+),\s*(?-?([0-9]*[.])?[0-9]+)/; + +export function parseVertex(str: string): Vertex | null { + const results = vertexRegex.exec(str); + + if (results === null) return null; + + return { + x: Number(results.groups?.x), + y: Number(results.groups?.y), + }; +} diff --git a/src/client-rect-observer.ts b/src/client-rect-observer.ts index ae21eff..99aafec 100644 --- a/src/client-rect-observer.ts +++ b/src/client-rect-observer.ts @@ -204,6 +204,16 @@ export class ClientRectObserver { export type ClientRectObserverEntryCallback = (entry: ClientRectObserverEntry) => void; export class ClientRectObserverManager { + static #instance: ClientRectObserverManager | null = null; + + // singleton so we only observe elements once + constructor() { + if (ClientRectObserverManager.#instance === null) { + ClientRectObserverManager.#instance = this; + } + return ClientRectObserverManager.#instance; + } + #elementMap = new WeakMap>(); #vo = new ClientRectObserver((entries) => { diff --git a/src/folk-hull.ts b/src/folk-hull.ts new file mode 100644 index 0000000..3e3a26e --- /dev/null +++ b/src/folk-hull.ts @@ -0,0 +1,100 @@ +import { FolkSet } from './folk-set'; +import { Vertex, verticesToPolygon } from './arrows/utils'; + +export class FolkHull extends FolkSet { + static tagName = 'folk-hull'; + + update() { + if (this.sourcesMap.size === 0) { + this.style.clipPath = ''; + return; + } + + const rects = Array.from(this.sourcesMap.values()); + const hull = makeHull(rects); + this.style.clipPath = verticesToPolygon(hull); + } +} + +/* This code has been modified from the original source, see the original source below. */ +/* + * Convex hull algorithm - Library (TypeScript) + * + * Copyright (c) 2021 Project Nayuki + * https://www.nayuki.io/page/convex-hull-algorithm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program (see COPYING.txt and COPYING.LESSER.txt). + * If not, see . + */ + +function comparePoints(a: Vertex, b: Vertex): number { + if (a.x < b.x) return -1; + if (a.x > b.x) return 1; + if (a.y < b.y) return -1; + if (a.y > b.y) return 1; + return 0; +} + +export function makeHull(rects: DOMRectReadOnly[]): Vertex[] { + const points: Vertex[] = rects + .flatMap((rect) => [ + { x: rect.left, y: rect.top }, + { x: rect.right, y: rect.top }, + { x: rect.left, y: rect.bottom }, + { x: rect.right, y: rect.bottom }, + ]) + .sort(comparePoints); + + if (points.length <= 1) return points; + + // Andrew's monotone chain algorithm. Positive y coordinates correspond to "up" + // as per the mathematical convention, instead of "down" as per the computer + // graphics convention. This doesn't affect the correctness of the result. + + const upperHull: Array = []; + for (let i = 0; i < points.length; i++) { + const p: Vertex = points[i]; + while (upperHull.length >= 2) { + const q: Vertex = upperHull[upperHull.length - 1]; + const r: Vertex = upperHull[upperHull.length - 2]; + if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) upperHull.pop(); + else break; + } + upperHull.push(p); + } + upperHull.pop(); + + const lowerHull: Array = []; + for (let i = points.length - 1; i >= 0; i--) { + const p: Vertex = points[i]; + while (lowerHull.length >= 2) { + const q: Vertex = lowerHull[lowerHull.length - 1]; + const r: Vertex = lowerHull[lowerHull.length - 2]; + if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) lowerHull.pop(); + else break; + } + lowerHull.push(p); + } + lowerHull.pop(); + + if ( + upperHull.length === 1 && + lowerHull.length === 1 && + upperHull[0].x === lowerHull[0].x && + upperHull[0].y === lowerHull[0].y + ) + return upperHull; + + return upperHull.concat(lowerHull); +} diff --git a/src/folk-set.ts b/src/folk-set.ts new file mode 100644 index 0000000..679f181 --- /dev/null +++ b/src/folk-set.ts @@ -0,0 +1,67 @@ +import { ClientRectObserverEntry, ClientRectObserverManager } from './client-rect-observer.ts'; + +const clientRectObserver = new ClientRectObserverManager(); + +export class FolkSet extends HTMLElement { + static tagName = 'folk-set'; + + static register() { + customElements.define(this.tagName, this); + } + + #sources = ''; + /** A CSS selector for the sources of the arrow. */ + get sources() { + return this.#sources; + } + + set sources(sources) { + this.#sources = sources; + this.observeSources(); + } + + #sourcesMap = new Map(); + get sourcesMap() { + return this.#sourcesMap; + } + + #sourcesCallback = (entry: ClientRectObserverEntry) => { + this.#sourcesMap.set(entry.target, entry.contentRect); + this.update(); + }; + + connectedCallback() { + this.sources = this.getAttribute('sources') || this.#sources; + } + + disconnectedCallback() { + this.unobserveSources(); + } + + observeSources() { + const sourceElements = new Set(document.querySelectorAll(this.sources)); + + const currentElements = new Set(this.#sourcesMap.keys()); + + const elementsToObserve = sourceElements.difference(currentElements); + + const elementsToUnobserve = currentElements.difference(sourceElements); + + this.unobserveSources(elementsToUnobserve); + + for (const el of elementsToObserve) { + clientRectObserver.observe(el, this.#sourcesCallback); + } + + this.update(); + } + + unobserveSources(elements: Iterable = this.#sourcesMap.keys()) { + for (const el of elements) { + clientRectObserver.unobserve(el, this.#sourcesCallback); + this.#sourcesMap.delete(el); + } + } + + update() {} +}