diff --git a/labs/folk-space.ts b/labs/folk-space.ts index a5f140a..e6453c5 100644 --- a/labs/folk-space.ts +++ b/labs/folk-space.ts @@ -4,10 +4,11 @@ import { html } from '@lib/tags'; import { Point } from '@lib/types'; import { css } from '@lit/reactive-element'; -declare global { - interface HTMLElementTagNameMap { - 'folk-space': FolkSpace; - } +interface TransformRect { + x: number; + y: number; + width: number; + height: number; } export class FolkSpace extends FolkElement { @@ -35,19 +36,38 @@ export class FolkSpace extends FolkElement { width: 100%; height: 100%; backface-visibility: hidden; - transition: transform 0.6s linear; - } - - .back { - transform: rotateX(90deg); } `; - #frontMatrix = new DOMMatrix(); - #backMatrix = new DOMMatrix().rotate(90, 0, 0); + #perspective = 1000; #isRotated = false; #transitionProgress = 0; + // Create base matrices + #frontMatrix = new DOMMatrix(); + #backMatrix = new DOMMatrix().rotateAxisAngle(1, 0, 0, 90); + // Update matrices and DOM + #updateTransforms() { + const rotation = this.#isRotated ? -90 * this.#transitionProgress : -90 * (1 - this.#transitionProgress); + + const backRotation = this.#isRotated ? 90 * (1 - this.#transitionProgress) : 90 * this.#transitionProgress; + + // Update matrices + this.#frontMatrix = new DOMMatrix().rotateAxisAngle(1, 0, 0, rotation); + this.#backMatrix = new DOMMatrix().rotateAxisAngle(1, 0, 0, backRotation); + + // Update DOM + const frontFace = this.shadowRoot?.querySelector('.front'); + const backFace = this.shadowRoot?.querySelector('.back'); + + if (frontFace instanceof HTMLElement) { + frontFace.style.transform = this.#frontMatrix.toString(); + } + if (backFace instanceof HTMLElement) { + backFace.style.transform = this.#backMatrix.toString(); + } + } + override createRenderRoot() { const root = super.createRenderRoot() as ShadowRoot; @@ -62,8 +82,6 @@ export class FolkSpace extends FolkElement { `); - this.transition(); - return root; } @@ -71,27 +89,18 @@ export class FolkSpace extends FolkElement { const spaceRect = this.getBoundingClientRect(); const centerX = spaceRect.width / 2; const centerY = spaceRect.height / 2; - const perspective = 1000; - let rotation = 0; - if (face === 'front') { - rotation = this.#isRotated ? -90 * this.#transitionProgress : -90 * (1 - this.#transitionProgress); - } else { - rotation = this.#isRotated ? 90 * (1 - this.#transitionProgress) : 90 * this.#transitionProgress; - } - - // Create perspective matrix + // Use the same matrix we're using for CSS const matrix = new DOMMatrix() .translate(centerX, centerY) - .multiply(new DOMMatrix([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -1 / perspective, 0, 0, 0, 1])) + .multiply(new DOMMatrix([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -1 / this.#perspective, 0, 0, 0, 1])) .translate(-centerX, -centerY) .translate(centerX, centerY) - .rotate(rotation, 0, 0) + .multiply(face === 'front' ? this.#frontMatrix : this.#backMatrix) .translate(-centerX, -centerY); const transformedPoint = matrix.transformPoint(new DOMPoint(point.x, point.y, 0, 1)); - // Perform perspective division const w = transformedPoint.w || 1; return { x: transformedPoint.x / w, @@ -101,33 +110,75 @@ export class FolkSpace extends FolkElement { transition() { this.#isRotated = !this.#isRotated; - - // Reset transition progress this.#transitionProgress = 0; - // Track transition const startTime = performance.now(); - const duration = 600; // Match CSS transition duration (0.6s) + const duration = 600; const animate = () => { const elapsed = performance.now() - startTime; this.#transitionProgress = Math.min(elapsed / duration, 1); + this.#updateTransforms(); + if (this.#transitionProgress < 1) { requestAnimationFrame(animate); } }; requestAnimationFrame(animate); + } - // Update DOM - const frontFace = this.shadowRoot?.querySelector('.front'); - const backFace = this.shadowRoot?.querySelector('.back'); - if (frontFace instanceof HTMLElement) { - frontFace.style.transform = this.#isRotated ? 'rotateX(-90deg)' : 'rotateX(0deg)'; - } - if (backFace instanceof HTMLElement) { - backFace.style.transform = this.#isRotated ? 'rotateX(0deg)' : 'rotateX(90deg)'; + /** + * Transforms a rect from an element in either face to screen coordinates + */ + transformRect(rect: TransformRect, face: 'front' | 'back'): TransformRect { + // Get center point + const center = { + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + }; + + // Transform center point + const transformedCenter = this.localToScreen(center, face); + + return { + x: transformedCenter.x - rect.width / 2, + y: transformedCenter.y - rect.height / 2, + width: rect.width, + height: rect.height, + }; + } + + /** + * Gets the screen coordinates for any element slotted into either face + */ + getElementScreenRect(element: Element): TransformRect | null { + // Find which slot the element belongs to + const slot = element.closest('[slot]'); + if (!slot) return null; + + const face = slot.getAttribute('slot') as 'front' | 'back'; + if (face !== 'front' && face !== 'back') return null; + + // Get the element's transform + if ('getTransformDOMRect' in element) { + const rect = (element as any).getTransformDOMRect(); + return this.transformRect(rect, face); } + + // Fallback to getBoundingClientRect + const rect = element.getBoundingClientRect(); + const spaceRect = this.getBoundingClientRect(); + + return this.transformRect( + { + x: rect.x - spaceRect.x, + y: rect.y - spaceRect.y, + width: rect.width, + height: rect.height, + }, + face, + ); } } diff --git a/lib/DOMRectTransform.ts b/lib/DOMRectTransform.ts index 17366f5..17466c9 100644 --- a/lib/DOMRectTransform.ts +++ b/lib/DOMRectTransform.ts @@ -1,5 +1,5 @@ -import { Point } from './types'; import { Matrix } from './Matrix'; +import { Point } from './types'; interface DOMRectTransformInit { height?: number; diff --git a/website/canvas/space-morph.html b/website/canvas/space-morph.html index d1f876a..773e80c 100644 --- a/website/canvas/space-morph.html +++ b/website/canvas/space-morph.html @@ -35,7 +35,7 @@
- + @@ -44,7 +44,7 @@
- + @@ -64,35 +64,18 @@ const source = document.getElementById('source'); const target = document.getElementById('target'); - // Update rope connection points function updateRopePoints() { if (!source || !target) return; - // Get the shapes' transforms - const sourceTransform = source.getTransformDOMRect(); - const targetTransform = target.getTransformDOMRect(); + const sourceRect = space.getElementScreenRect(source); + const targetRect = space.getElementScreenRect(target); - // Get center points in local space - const sourceCenter = { - x: sourceTransform.x + sourceTransform.width / 2, - y: sourceTransform.y + sourceTransform.height / 2, - }; - - const targetCenter = { - x: targetTransform.x + targetTransform.width / 2, - y: targetTransform.y + targetTransform.height / 2, - }; - - // Convert to screen space - const sourcePoint = space.localToScreen(sourceCenter, 'front'); - const targetPoint = space.localToScreen(targetCenter, 'back'); - - // Update rope - rope.sourceRect = { x: sourcePoint.x, y: sourcePoint.y, width: 0, height: 0 }; - rope.targetRect = { x: targetPoint.x, y: targetPoint.y, width: 0, height: 0 }; + if (sourceRect && targetRect) { + rope.sourceRect = sourceRect; + rope.targetRect = targetRect; + } } - // Update on animation frame function animate() { updateRopePoints(); requestAnimationFrame(animate);