add set and hull elements
This commit is contained in:
parent
f3e916ef1c
commit
4e5496839b
|
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Shapes</title>
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fc-geometry {
|
||||
background: rgb(187, 178, 178);
|
||||
}
|
||||
|
||||
folk-hull {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
background-color: #b4d8f669;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<fc-geometry x="50" y="100" width="50" height="50"></fc-geometry>
|
||||
<fc-geometry x="200" y="200" width="50" height="50"></fc-geometry>
|
||||
<fc-geometry x="100" y="300" width="50" height="50"></fc-geometry>
|
||||
|
||||
<folk-hull sources="fc-geometry"></folk-hull>
|
||||
|
||||
<script type="module">
|
||||
import { FolkGeometry } from '../src/canvas/fc-geometry.ts';
|
||||
import { FolkHull } from '../src/folk-hull.ts';
|
||||
|
||||
FolkGeometry.register();
|
||||
FolkHull.register();
|
||||
</script>
|
||||
</body>
|
||||
</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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -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 = /(?<x>-?([0-9]*[.])?[0-9]+),\s*(?<y>-?([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();
|
||||
|
||||
|
|
|
|||
|
|
@ -181,3 +181,16 @@ export function verticesToPolygon(vertices: Vertex[]): string {
|
|||
|
||||
return `polygon(${vertices.map((vertex) => `${vertex.x}px ${vertex.y}px`).join(', ')})`;
|
||||
}
|
||||
|
||||
const vertexRegex = /(?<x>-?([0-9]*[.])?[0-9]+),\s*(?<y>-?([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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Element, Set<ClientRectObserverEntryCallback>>();
|
||||
|
||||
#vo = new ClientRectObserver((entries) => {
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Vertex> = [];
|
||||
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<Vertex> = [];
|
||||
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);
|
||||
}
|
||||
|
|
@ -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<Element, DOMRectReadOnly>();
|
||||
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<Element> = this.#sourcesMap.keys()) {
|
||||
for (const el of elements) {
|
||||
clientRectObserver.unobserve(el, this.#sourcesCallback);
|
||||
this.#sourcesMap.delete(el);
|
||||
}
|
||||
}
|
||||
|
||||
update() {}
|
||||
}
|
||||
Loading…
Reference in New Issue