diff --git a/demo/ink.html b/demo/ink.html index ff71a96..c642afd 100644 --- a/demo/ink.html +++ b/demo/ink.html @@ -13,7 +13,6 @@ min-height: 100%; position: relative; margin: 0; - padding: 0; } @@ -33,26 +32,24 @@ async function draw(e) { if (e.target === drawButton) return; + // Stop the default focus and pointer capture of geometry elements + e.preventDefault(); + e.stopPropagation(); + const ink = document.createElement('spatial-ink'); - ink.style.cursor = 'var(--tracing-cursor, crosshair)'; - ink.style.position = 'fixed'; - ink.style.inset = '0 0 0 0'; - ink.style.zIndex = 'calc(infinity)'; document.body.appendChild(ink); - const rect = await ink.draw(e); + await ink.draw(e); + const rect = ink.getPathBox(); + + ink.points = ink.points.map(([x, y, p]) => [x - rect.x, y - rect.y, p]); + const geometry = document.createElement('spatial-geometry'); geometry.x = rect.x; geometry.y = rect.y; geometry.height = rect.height; geometry.width = rect.width; - - ink.points = ink.points.map(([x, y, p]) => [x - rect.x, y - rect.y, p]); - ink.style.cursor = ''; - ink.style.position = ''; - ink.style.inset = ''; - ink.style.zIndex = ''; geometry.appendChild(ink); document.body.appendChild(geometry); @@ -65,10 +62,10 @@ isDrawing = !isDrawing; if (isDrawing) { - document.addEventListener('pointerdown', draw); + document.addEventListener('pointerdown', draw, { capture: true }); drawButton.textContent = 'Drawing'; } else { - document.removeEventListener('pointerdown', draw); + document.removeEventListener('pointerdown', draw, { capture: true }); drawButton.textContent = 'Draw'; } }); diff --git a/demo/shapes.html b/demo/shapes.html index b7643d3..a1a4f80 100644 --- a/demo/shapes.html +++ b/demo/shapes.html @@ -17,10 +17,11 @@ spatial-geometry { background: rgb(187, 178, 178); - box-shadow: rgba(0, 0, 0, 0.15) 1.95px 1.95px 2.6px; + box-shadow: rgba(0, 0, 0, 0.2) 1.95px 1.95px 2.6px; + transition: scale 100ms ease-out, box-shadow 100ms ease-out; } - spatial-geometry:state(moving) { + spatial-geometry:state(move) { scale: 1.05; box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; } diff --git a/src/canvas/spatial-geometry.ts b/src/canvas/spatial-geometry.ts index 1e574fc..3815a6d 100644 --- a/src/canvas/spatial-geometry.ts +++ b/src/canvas/spatial-geometry.ts @@ -51,7 +51,12 @@ styles.replaceSync(` outline: solid 2px hsl(214, 84%, 56%); } -:host(:state(moving)) { +:host(:state(move)), +:host(:state(rotate)), +:host(:state(resize-nw)), +:host(:state(resize-ne)), +:host(:state(resize-se)), +:host(:state(resize-sw)), { user-select: none; } @@ -139,9 +144,6 @@ export class SpatialGeometry extends HTMLElement { super(); this.addEventListener('pointerdown', this); - this.addEventListener('lostpointercapture', this); - // this.addEventListener('touchstart', this); - // this.addEventListener('dragstart', this); const shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true }); shadowRoot.adoptedStyleSheets.push(styles); @@ -255,8 +257,12 @@ export class SpatialGeometry extends HTMLElement { if (target !== this && !target.hasAttribute('part')) return; target.addEventListener('pointermove', this); + this.addEventListener('lostpointercapture', this); target.setPointerCapture(event.pointerId); - this.#internals.states.add('moving'); + + const interaction = target.getAttribute('part') || 'move'; + this.#internals.states.add(interaction); + this.focus(); return; } @@ -308,14 +314,11 @@ export class SpatialGeometry extends HTMLElement { return; } case 'lostpointercapture': { - this.#internals.states.delete('moving'); const target = event.composedPath()[0] as HTMLElement; + const interaction = target.getAttribute('part') || 'move'; + this.#internals.states.delete(interaction); target.removeEventListener('pointermove', this); - return; - } - case 'touchstart': - case 'dragstart': { - event.preventDefault(); + this.removeEventListener('lostpointercapture', this); return; } } diff --git a/src/canvas/spatial-ink.ts b/src/canvas/spatial-ink.ts index ee12616..f38c472 100644 --- a/src/canvas/spatial-ink.ts +++ b/src/canvas/spatial-ink.ts @@ -13,6 +13,13 @@ styles.replaceSync(` height: 100%; width: 100%; touch-action: none; + pointer-events: none; + } + + :host(:state(drawing)) { + position: fixed; + inset: 0 0 0 0; + cursor: var(--tracing-cursor, crosshair); } `); @@ -25,18 +32,14 @@ export class SpatialInk extends HTMLElement { #internals = this.attachInternals(); + #svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); #path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - #stroke: Stroke = []; - - #d = ''; - #size = Number(this.getAttribute('size') || 16); get size() { return this.#size; } - set size(size) { this.#size = size; this.#update(); @@ -47,7 +50,6 @@ export class SpatialInk extends HTMLElement { get thinning() { return this.#thinning; } - set thinning(thinning) { this.#thinning = thinning; this.#update(); @@ -58,7 +60,6 @@ export class SpatialInk extends HTMLElement { get smoothing() { return this.#smoothing; } - set smoothing(smoothing) { this.#smoothing = smoothing; this.#update(); @@ -69,7 +70,6 @@ export class SpatialInk extends HTMLElement { get streamline() { return this.#streamline; } - set streamline(streamline) { this.#streamline = streamline; this.#update(); @@ -80,7 +80,6 @@ export class SpatialInk extends HTMLElement { get simulatePressure() { return this.#simulatePressure; } - set simulatePressure(simulatePressure) { this.#simulatePressure = simulatePressure; this.#update(); @@ -91,7 +90,6 @@ export class SpatialInk extends HTMLElement { get points() { return this.#points; } - set points(points) { this.#points = points; this.#update(); @@ -102,27 +100,32 @@ export class SpatialInk extends HTMLElement { const shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true }); shadowRoot.adoptedStyleSheets.push(styles); - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.appendChild(this.#path); - shadowRoot.appendChild(svg); + this.#svg.appendChild(this.#path); + shadowRoot.appendChild(this.#svg); } connectedCallback() { this.#update(); } - #tracingPromise: PromiseWithResolvers | null = null; + getPathBox() { + return this.#path.getBBox(); + } + + setViewBox() { + this.#svg.viewBox; + } + + #tracingPromise: PromiseWithResolvers | null = null; // TODO: cancel trace? - async draw(event: PointerEvent): Promise { - if (event.button !== 0 || event.ctrlKey) return; - - this.points = []; - this.addPoint([event.pageX, event.pageY, event.pressure]); - this.addEventListener('lostpointercapture', this); - this.addEventListener('pointermove', this); - this.setPointerCapture(event.pointerId); - this.#tracingPromise = Promise.withResolvers(); + draw(event?: PointerEvent) { + if (event?.type === 'pointerdown') { + this.handleEvent(event); + } else { + this.addEventListener('pointerdown', this); + } + this.#tracingPromise = Promise.withResolvers(); return this.#tracingPromise.promise; } @@ -133,15 +136,27 @@ export class SpatialInk extends HTMLElement { handleEvent(event: PointerEvent) { switch (event.type) { + case 'pointerdown': { + if (event.button !== 0 || event.ctrlKey) return; + + this.points = []; + this.addPoint([event.offsetX, event.offsetY, event.pressure]); + this.addEventListener('lostpointercapture', this); + this.addEventListener('pointermove', this); + this.setPointerCapture(event.pointerId); + this.#internals.states.add('drawing'); + return; + } case 'pointermove': { - this.addPoint([event.pageX, event.pageY, event.pressure]); + this.addPoint([event.offsetX, event.offsetY, event.pressure]); return; } case 'lostpointercapture': { - this.removeEventListener('pointermove', this); this.removeEventListener('pointerdown', this); + this.removeEventListener('pointermove', this); this.removeEventListener('lostpointercapture', this); - this.#tracingPromise?.resolve(this.#path.getBoundingClientRect()); + this.#internals.states.delete('drawing'); + this.#tracingPromise?.resolve(); this.#tracingPromise = null; return; } @@ -168,10 +183,7 @@ export class SpatialInk extends HTMLElement { cap: true, }, }; - - this.#stroke = getStroke(this.#points, options); - this.#d = this.#getSvgPathFromStroke(this.#stroke); - this.#path.setAttribute('d', this.#d); + this.#path.setAttribute('d', this.#getSvgPathFromStroke(getStroke(this.#points, options))); } #getSvgPathFromStroke(stroke: Stroke): string {