import { DOMRectTransform } from './common/DOMRectTransform'; import { FolkShape } from './folk-shape'; declare global { interface HTMLElementTagNameMap { 'folk-projector': FolkProjector; } } export class FolkProjector extends HTMLElement { static tagName = 'folk-projector'; static define() { if (customElements.get(this.tagName)) return; customElements.define(this.tagName, this); } #isProjecting = false; #mappedElements = new Map(); #isTransitioning = false; constructor() { super(); const style = document.createElement('style'); style.textContent = ` :host { display: block; position: relative; } /* Add styles for mapped elements and their children */ div { opacity: 1; box-sizing: border-box; display: flex; flex-wrap: wrap; position: absolute; border-radius: 4px; overflow: hidden; } div input:first-of-type { border-right: 2px solid lightgray; } div input { background-color: #36454f; color: white; width: 50%; min-width: 0; font-size: 12px; box-sizing: border-box; border: none; text-align: center; } `; this.appendChild(style); } // TODO: avoid this kind of hack #maintainProjectedPositions() { if (!this.#isProjecting) return; const shapes = Array.from(this.children).filter((el): el is FolkShape => el instanceof FolkShape); const mappedElements = shapes .map((shape) => this.#mappedElements.get(shape)) .filter((el): el is HTMLElement => el !== null); shapes.forEach((shape, i) => { const mappedEl = mappedElements[i]; // Restore the projected position before paint shape.style.transform = mappedEl.style.transform; shape.style.width = mappedEl.style.width; shape.style.height = mappedEl.style.height; }); requestAnimationFrame(() => this.#maintainProjectedPositions()); } mapping(shape: FolkShape, mappingFn: (element: FolkShape) => HTMLElement) { const mappedEl = mappingFn(shape); const rect = shape.getTransformDOMRect(); mappedEl.style.position = 'absolute'; mappedEl.style.width = rect.width + 'px'; mappedEl.style.height = rect.height + 'px'; mappedEl.style.transform = shape.style.transform; mappedEl.style.transformOrigin = '0 0'; mappedEl.style.opacity = '0'; mappedEl.style.pointerEvents = 'none'; this.appendChild(mappedEl); this.#mappedElements.set(shape, mappedEl); } async project(spacing = 20) { if (this.#isTransitioning) return; this.#isTransitioning = true; const shapes = Array.from(this.children).filter((el): el is FolkShape => el instanceof FolkShape); const mappedElements = shapes .map((shape) => this.#mappedElements.get(shape)) .filter((el): el is HTMLElement => el !== null); const CELL_WIDTH = 100; const CELL_HEIGHT = 50; const X_OFFSET = 20; let yOffset = 0; const positions = shapes.map((shape) => { if (this.#isProjecting) { return shape.getTransformDOMRect(); } else { const newRect = new DOMRectTransform({ x: X_OFFSET, y: yOffset, width: CELL_WIDTH, height: CELL_HEIGHT, rotation: 0, }); yOffset += CELL_HEIGHT + spacing; return newRect; } }); if (!document.startViewTransition) { shapes.forEach((shape, i) => { const newTransform = positions[i].toCssString(); const newRect = positions[i]; shape.style.transform = newTransform; shape.style.width = `${newRect.width}px`; shape.style.height = `${newRect.height}px`; const mappedEl = mappedElements[i]; if (mappedEl) { mappedEl.style.transform = newTransform; mappedEl.style.width = `${newRect.width}px`; mappedEl.style.height = `${newRect.height}px`; mappedEl.style.opacity = this.#isProjecting ? '1' : '0'; } }); } else { shapes.forEach((shape, i) => { shape.style.viewTransitionName = `shape-${i}`; mappedElements[i].style.viewTransitionName = `mapped-${i}`; }); await new Promise(requestAnimationFrame); const transition = document.startViewTransition(() => { shapes.forEach((shape, i) => { const newTransform = positions[i].toCssString(); const newRect = positions[i]; shape.style.transform = newTransform; shape.style.width = `${newRect.width}px`; shape.style.height = `${newRect.height}px`; if (this.#isProjecting) { shape.blur(); shape.style.zIndex = '0'; } const mappedEl = mappedElements[i]; if (mappedEl) { mappedEl.style.transform = newTransform; mappedEl.style.width = `${newRect.width}px`; mappedEl.style.height = `${newRect.height}px`; mappedEl.style.opacity = this.#isProjecting ? '1' : '0'; } }); }); transition.finished.finally(() => { this.#isTransitioning = false; shapes.forEach((shape, i) => { shape.style.viewTransitionName = ''; shape.style.pointerEvents = this.#isProjecting ? 'none' : 'all'; mappedElements[i].style.viewTransitionName = ''; mappedElements[i].style.pointerEvents = this.#isProjecting ? 'all' : 'none'; }); }); } this.#isProjecting = !this.#isProjecting; if (this.#isProjecting) { this.#maintainProjectedPositions(); } } }