cleanup space morph
This commit is contained in:
parent
c2eacb93a6
commit
09b0fed6ec
|
|
@ -4,10 +4,11 @@ import { html } from '@lib/tags';
|
||||||
import { Point } from '@lib/types';
|
import { Point } from '@lib/types';
|
||||||
import { css } from '@lit/reactive-element';
|
import { css } from '@lit/reactive-element';
|
||||||
|
|
||||||
declare global {
|
interface TransformRect {
|
||||||
interface HTMLElementTagNameMap {
|
x: number;
|
||||||
'folk-space': FolkSpace;
|
y: number;
|
||||||
}
|
width: number;
|
||||||
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FolkSpace extends FolkElement {
|
export class FolkSpace extends FolkElement {
|
||||||
|
|
@ -35,19 +36,38 @@ export class FolkSpace extends FolkElement {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
transition: transform 0.6s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back {
|
|
||||||
transform: rotateX(90deg);
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
#frontMatrix = new DOMMatrix();
|
#perspective = 1000;
|
||||||
#backMatrix = new DOMMatrix().rotate(90, 0, 0);
|
|
||||||
#isRotated = false;
|
#isRotated = false;
|
||||||
#transitionProgress = 0;
|
#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() {
|
override createRenderRoot() {
|
||||||
const root = super.createRenderRoot() as ShadowRoot;
|
const root = super.createRenderRoot() as ShadowRoot;
|
||||||
|
|
||||||
|
|
@ -62,8 +82,6 @@ export class FolkSpace extends FolkElement {
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
this.transition();
|
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,27 +89,18 @@ export class FolkSpace extends FolkElement {
|
||||||
const spaceRect = this.getBoundingClientRect();
|
const spaceRect = this.getBoundingClientRect();
|
||||||
const centerX = spaceRect.width / 2;
|
const centerX = spaceRect.width / 2;
|
||||||
const centerY = spaceRect.height / 2;
|
const centerY = spaceRect.height / 2;
|
||||||
const perspective = 1000;
|
|
||||||
|
|
||||||
let rotation = 0;
|
// Use the same matrix we're using for CSS
|
||||||
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
|
|
||||||
const matrix = new DOMMatrix()
|
const matrix = new DOMMatrix()
|
||||||
.translate(centerX, centerY)
|
.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)
|
||||||
.translate(centerX, centerY)
|
.translate(centerX, centerY)
|
||||||
.rotate(rotation, 0, 0)
|
.multiply(face === 'front' ? this.#frontMatrix : this.#backMatrix)
|
||||||
.translate(-centerX, -centerY);
|
.translate(-centerX, -centerY);
|
||||||
|
|
||||||
const transformedPoint = matrix.transformPoint(new DOMPoint(point.x, point.y, 0, 1));
|
const transformedPoint = matrix.transformPoint(new DOMPoint(point.x, point.y, 0, 1));
|
||||||
|
|
||||||
// Perform perspective division
|
|
||||||
const w = transformedPoint.w || 1;
|
const w = transformedPoint.w || 1;
|
||||||
return {
|
return {
|
||||||
x: transformedPoint.x / w,
|
x: transformedPoint.x / w,
|
||||||
|
|
@ -101,33 +110,75 @@ export class FolkSpace extends FolkElement {
|
||||||
|
|
||||||
transition() {
|
transition() {
|
||||||
this.#isRotated = !this.#isRotated;
|
this.#isRotated = !this.#isRotated;
|
||||||
|
|
||||||
// Reset transition progress
|
|
||||||
this.#transitionProgress = 0;
|
this.#transitionProgress = 0;
|
||||||
|
|
||||||
// Track transition
|
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const duration = 600; // Match CSS transition duration (0.6s)
|
const duration = 600;
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
const elapsed = performance.now() - startTime;
|
const elapsed = performance.now() - startTime;
|
||||||
this.#transitionProgress = Math.min(elapsed / duration, 1);
|
this.#transitionProgress = Math.min(elapsed / duration, 1);
|
||||||
|
|
||||||
|
this.#updateTransforms();
|
||||||
|
|
||||||
if (this.#transitionProgress < 1) {
|
if (this.#transitionProgress < 1) {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
// Update DOM
|
/**
|
||||||
const frontFace = this.shadowRoot?.querySelector('.front');
|
* Transforms a rect from an element in either face to screen coordinates
|
||||||
const backFace = this.shadowRoot?.querySelector('.back');
|
*/
|
||||||
if (frontFace instanceof HTMLElement) {
|
transformRect(rect: TransformRect, face: 'front' | 'back'): TransformRect {
|
||||||
frontFace.style.transform = this.#isRotated ? 'rotateX(-90deg)' : 'rotateX(0deg)';
|
// Get center point
|
||||||
}
|
const center = {
|
||||||
if (backFace instanceof HTMLElement) {
|
x: rect.x + rect.width / 2,
|
||||||
backFace.style.transform = this.#isRotated ? 'rotateX(0deg)' : 'rotateX(90deg)';
|
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Point } from './types';
|
|
||||||
import { Matrix } from './Matrix';
|
import { Matrix } from './Matrix';
|
||||||
|
import { Point } from './types';
|
||||||
|
|
||||||
interface DOMRectTransformInit {
|
interface DOMRectTransformInit {
|
||||||
height?: number;
|
height?: number;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
<folk-rope id="rope"></folk-rope>
|
<folk-rope id="rope"></folk-rope>
|
||||||
<folk-space id="space">
|
<folk-space id="space">
|
||||||
<div slot="front">
|
<div slot="front">
|
||||||
<folk-shape id="source" x="100" y="100" width="50" height="50"></folk-shape>
|
<folk-shape id="source" x="250" y="100" width="50" height="50"></folk-shape>
|
||||||
<folk-shape x="200" y="200" width="75" height="75" rotation="90"></folk-shape>
|
<folk-shape x="200" y="200" width="75" height="75" rotation="90"></folk-shape>
|
||||||
<folk-shape x="50" y="250" width="25" height="25" rotation="180"></folk-shape>
|
<folk-shape x="50" y="250" width="25" height="25" rotation="180"></folk-shape>
|
||||||
<folk-shape x="350" y="50" width="100" height="100" rotation="270"></folk-shape>
|
<folk-shape x="350" y="50" width="100" height="100" rotation="270"></folk-shape>
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
<folk-shape x="700" y="700" width="250" height="250" rotation="540"></folk-shape>
|
<folk-shape x="700" y="700" width="250" height="250" rotation="540"></folk-shape>
|
||||||
</div>
|
</div>
|
||||||
<div slot="back">
|
<div slot="back">
|
||||||
<folk-shape id="target" x="150" y="150" width="50" height="50" rotation="45"></folk-shape>
|
<folk-shape id="target" x="550" y="150" width="50" height="50" rotation="45"></folk-shape>
|
||||||
<folk-shape x="300" y="400" width="150" height="50" rotation="45"></folk-shape>
|
<folk-shape x="300" y="400" width="150" height="50" rotation="45"></folk-shape>
|
||||||
<folk-shape x="250" y="350" width="100" height="100" rotation="135"></folk-shape>
|
<folk-shape x="250" y="350" width="100" height="100" rotation="135"></folk-shape>
|
||||||
<folk-shape x="400" y="200" width="75" height="75" rotation="225"></folk-shape>
|
<folk-shape x="400" y="200" width="75" height="75" rotation="225"></folk-shape>
|
||||||
|
|
@ -64,35 +64,18 @@
|
||||||
const source = document.getElementById('source');
|
const source = document.getElementById('source');
|
||||||
const target = document.getElementById('target');
|
const target = document.getElementById('target');
|
||||||
|
|
||||||
// Update rope connection points
|
|
||||||
function updateRopePoints() {
|
function updateRopePoints() {
|
||||||
if (!source || !target) return;
|
if (!source || !target) return;
|
||||||
|
|
||||||
// Get the shapes' transforms
|
const sourceRect = space.getElementScreenRect(source);
|
||||||
const sourceTransform = source.getTransformDOMRect();
|
const targetRect = space.getElementScreenRect(target);
|
||||||
const targetTransform = target.getTransformDOMRect();
|
|
||||||
|
|
||||||
// Get center points in local space
|
if (sourceRect && targetRect) {
|
||||||
const sourceCenter = {
|
rope.sourceRect = sourceRect;
|
||||||
x: sourceTransform.x + sourceTransform.width / 2,
|
rope.targetRect = targetRect;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update on animation frame
|
|
||||||
function animate() {
|
function animate() {
|
||||||
updateRopePoints();
|
updateRopePoints();
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue