Skip to content

Commit

Permalink
'projector' test
Browse files Browse the repository at this point in the history
  • Loading branch information
OrionReed committed Dec 11, 2024
1 parent c9afe5e commit d056566
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 0 deletions.
92 changes: 92 additions & 0 deletions demo/projector.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Projector</title>
<style>
html {
height: 100%;
}

body {
min-height: 100%;
position: relative;
margin: 0;
overscroll-behavior: none;
}

folk-shape {
box-sizing: border-box;
background-color: rgb(187, 178, 178);
border-radius: 2px;
border: 2px solid rgba(0, 0, 0, 0.5);
}

folk-projector {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}

::view-transition-group(*) {
overflow: clip;
}

::view-transition-old(*),
::view-transition-new(*) {
height: 100%;
width: auto;
object-fit: cover;
}
</style>
</head>
<body>
<folk-projector>
<folk-shape x="100" y="100" width="50" height="50"></folk-shape>
<folk-shape x="100" y="200" width="50" height="50"></folk-shape>
<folk-shape x="100" y="300" width="50" height="50" rotation="45"></folk-shape>
<folk-shape x="300" y="150" width="80" height="40" rotation="45"></folk-shape>
<folk-shape x="400" y="250" width="60" height="90" rotation="45"></folk-shape>
</folk-projector>
<folk-shape x="500" y="290" width="80" height="40" rotation="45"></folk-shape>
<folk-shape x="300" y="350" width="60" height="90" rotation="45"></folk-shape>

<script type="module">
import '../src/standalone/folk-shape.ts';
import '../src/standalone/folk-projector.ts';

const projector = document.querySelector('folk-projector');

function createSpreadsheetEditor(shape) {
const editor = document.createElement('div');
const xInput = document.createElement('input');
const yInput = document.createElement('input');

[xInput, yInput].forEach((input) => (input.type = 'number'));
xInput.value = shape.x;
yInput.value = shape.y;

xInput.addEventListener('input', (e) => (shape.x = Number(e.target.value)));
yInput.addEventListener('input', (e) => (shape.y = Number(e.target.value)));

editor.appendChild(xInput);
editor.appendChild(yInput);
return editor;
}

const shapes = document.querySelectorAll('folk-shape');
shapes.forEach((shape) => {
projector.mapping(shape, createSpreadsheetEditor);
console.log(shape);
});

document.addEventListener('keydown', (event) => {
if (event.key === 'v') {
projector.project();
}
});
</script>
</body>
</html>
151 changes: 151 additions & 0 deletions src/folk-projector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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<FolkShape, HTMLElement>();
#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;
}
div input {
width: 50%;
min-width: 0;
font-size: 12px;
box-sizing: border-box;
}
`;
this.appendChild(style);
}

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 = 'all';

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);

// Ensure elements are painted before transition
await new Promise(requestAnimationFrame);

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`;
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 = '';
mappedElements[i].style.viewTransitionName = '';
});
});
}

this.#isProjecting = !this.#isProjecting;
}
}
5 changes: 5 additions & 0 deletions src/standalone/folk-projector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { FolkProjector } from '../folk-projector';

FolkProjector.define();

export { FolkProjector };

0 comments on commit d056566

Please sign in to comment.