From 929f3384f945f4b4086ceb5617d589b037c1d9df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cchrisshank=E2=80=9D?= Date: Sun, 18 Aug 2024 12:49:01 -0700 Subject: [PATCH] render resize handlers --- demo/shapes.html | 6 +- src/elements/main.css | 11 --- src/elements/spatial-geometry.ts | 161 +++++++++++++++++++++++++++++-- 3 files changed, 155 insertions(+), 23 deletions(-) delete mode 100644 src/elements/main.css diff --git a/demo/shapes.html b/demo/shapes.html index acbb7d0..84c1c73 100644 --- a/demo/shapes.html +++ b/demo/shapes.html @@ -5,8 +5,6 @@ Shapes diff --git a/src/elements/main.css b/src/elements/main.css deleted file mode 100644 index faddafb..0000000 --- a/src/elements/main.css +++ /dev/null @@ -1,11 +0,0 @@ -spatial-canvas { - display: block; - position: relative; -} - -spatial-geometry { - display: block; - position: absolute; - cursor: pointer; - content-visibility: auto; -} diff --git a/src/elements/spatial-geometry.ts b/src/elements/spatial-geometry.ts index 223566f..0854480 100644 --- a/src/elements/spatial-geometry.ts +++ b/src/elements/spatial-geometry.ts @@ -3,15 +3,125 @@ export type Shape = 'rectangle' | 'circle' | 'triangle'; // Can we make adding new shapes extensible via a static property? const shapes = new Set(['rectangle', 'circle', 'triangle']); -export type Vector = { x: number; y: number; movementX: number; movementY: number }; +export type MoveEventDetail = { x: number; y: number; movementX: number; movementY: number }; // Should the move event bubble? -export class MoveEvent extends CustomEvent { - constructor(vector: Vector) { +export class MoveEvent extends CustomEvent { + constructor(vector: MoveEventDetail) { super('move', { detail: vector, cancelable: true, bubbles: true }); } } +const styles = new CSSStyleSheet(); +styles.replaceSync(` +:host { + display: block; + position: absolute; + cursor: pointer; +} + +:host(:not([selected])) [resize-handler] { + display: none; +} + +:host([selected]) [resize-handler] { + display: block; + position: absolute; + box-sizing: border-box; + padding: 0; + background: hsl(210, 20%, 98%); + + &[resize-handler="top-left"], + &[resize-handler="top-right"], + &[resize-handler="bottom-right"], + &[resize-handler="bottom-left"] { + width: 13px; + aspect-ratio: 1; + transform: translate(-50%, -50%); + border: 1.5px solid hsl(214, 84%, 56%); + border-radius: 2px; + z-index: 2; + } + + &[resize-handler="top-left"] { + top: 0; + left: 0; + } + + &[resize-handler="top-right"] { + top: 0; + left: 100%; + } + + &[resize-handler="bottom-right"] { + top: 100%; + left: 100%; + } + + &[resize-handler="bottom-left"] { + top: 100%; + left: 0; + } + + &[resize-handler="top-left"], &[resize-handler="bottom-right"] { + cursor: url("data:image/svg+xml,") 16 16, pointer; + } + + &[resize-handler="top-right"], &[resize-handler="bottom-left"] { + cursor: url("data:image/svg+xml,") 16 16, pointer; + } + + &[resize-handler="top"], + &[resize-handler="right"], + &[resize-handler="bottom"], + &[resize-handler="left"] { + background-color: hsl(214, 84%, 56%); + background-clip: content-box; + border: unset; + z-index: 1; + } + + &[resize-handler="top"] { + top: 0; + left: 0; + right: 0; + transform: translate(0, -50%); + } + + &[resize-handler="right"] { + top: 0; + bottom: 0; + right: 0; + transform: translate(50%, 0); + } + + &[resize-handler="bottom"] { + bottom:0; + left: 0; + right: 0; + transform: translate(0, 50%); + } + + &[resize-handler="left"] { + top: 0; + bottom: 0; + left: 0; + transform: translate(-50%, 0); + } + + &[resize-handler="top"], &[resize-handler="bottom"] { + height: 6px; + padding: 2px 0; + cursor: url("data:image/svg+xml,") 16 16, pointer; + } + + &[resize-handler="right"], &[resize-handler="left"] { + width: 6px; + padding: 0 2px; + cursor: url("data:image/svg+xml,") 16 16, pointer; + } +}`); + // TODO: add z coordinate? export class SpatialGeometry extends HTMLElement { static tagName = 'spatial-geometry'; @@ -29,6 +139,9 @@ export class SpatialGeometry extends HTMLElement { this.addEventListener('lostpointercapture', this); this.addEventListener('touchstart', this); this.addEventListener('dragstart', this); + + const shadowRoot = this.attachShadow({ mode: 'open' }); + shadowRoot.adoptedStyleSheets.push(styles); } #type: Shape = 'rectangle'; @@ -82,29 +195,42 @@ export class SpatialGeometry extends HTMLElement { } // Similar to `Element.getClientBoundingRect()`, but returns an SVG path that precisely outlines the shape. - // We might also want some kind of utility function that maps a path into an approximate set of vertices. getBoundingPath(): string { return ''; } + // We might also want some kind of utility function that maps a path into an approximate set of vertices. + getBoundingVertices() { + return []; + } + handleEvent(event: PointerEvent) { switch (event.type) { case 'pointerdown': { if (event.button !== 0 || event.ctrlKey) return; - this.addEventListener('pointermove', this); - this.setPointerCapture(event.pointerId); + const target = event.composedPath()[0] as HTMLElement; + + target.addEventListener('pointermove', this); + target.setPointerCapture(event.pointerId); + this.setAttribute('selected', ''); + this.#createResizeHandlers(); this.style.userSelect = 'none'; return; } case 'pointermove': { - this.x += event.movementX; - this.y += event.movementY; + if (event.target === this) { + this.x += event.movementX; + this.y += event.movementY; + } else if ((event.target as HTMLElement).matches('[resize-handler]')) { + console.log('resizing'); + } return; } case 'lostpointercapture': { this.style.userSelect = ''; - this.removeEventListener('pointermove', this); + const target = event.composedPath()[0] as HTMLElement; + target.removeEventListener('pointermove', this); return; } case 'touchstart': @@ -169,4 +295,21 @@ export class SpatialGeometry extends HTMLElement { } } } + + #firstSelection = true; + #createResizeHandlers() { + // lazily create resize handlers on first selection + if (this.#firstSelection && this.shadowRoot !== null) { + this.shadowRoot.innerHTML = ` + + + + + + + + `; + this.#firstSelection = false; + } + } }