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() {}
+}