rope transforms

This commit is contained in:
Orion Reed 2024-12-19 03:39:55 -05:00
parent 023f667be3
commit e7bf3522be
3 changed files with 186 additions and 31 deletions

View File

@ -1,5 +1,7 @@
import { FolkElement } from '@lib'; import { FolkElement } from '@lib';
import { DOMTransform } from '@lib/DOMTransform';
import { html } from '@lib/tags'; import { html } from '@lib/tags';
import { Point } from '@lib/types';
import { css } from '@lit/reactive-element'; import { css } from '@lit/reactive-element';
declare global { declare global {
@ -14,7 +16,7 @@ export class FolkSpace extends FolkElement {
static styles = css` static styles = css`
:host { :host {
display: block; display: block;
perspective: 1000px; // perspective: 1000px;
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -26,7 +28,6 @@ export class FolkSpace extends FolkElement {
height: 100%; height: 100%;
transform-style: preserve-3d; transform-style: preserve-3d;
transform-origin: center; transform-origin: center;
transition: transform 0.6s;
} }
.space.rotate { .space.rotate {
@ -38,6 +39,7 @@ export class FolkSpace extends FolkElement {
width: 100%; width: 100%;
height: 100%; height: 100%;
backface-visibility: hidden; backface-visibility: hidden;
transition: transform 0.6s linear;
} }
.front { .front {
@ -49,15 +51,20 @@ export class FolkSpace extends FolkElement {
} }
`; `;
#frontMatrix = new DOMMatrix();
#backMatrix = new DOMMatrix().rotate(90, 0, 0);
#isRotated = false;
#transitionProgress = 0;
override createRenderRoot() { override createRenderRoot() {
const root = super.createRenderRoot() as ShadowRoot; const root = super.createRenderRoot() as ShadowRoot;
root.setHTMLUnsafe(html` root.setHTMLUnsafe(html`
<div class="space"> <div class="space">
<div class="face front"> <div class="face front" style="transform: ${this.#frontMatrix}">
<slot name="front"></slot> <slot name="front"></slot>
</div> </div>
<div class="face back"> <div class="face back" style="transform: ${this.#backMatrix}">
<slot name="back"></slot> <slot name="back"></slot>
</div> </div>
</div> </div>
@ -66,8 +73,61 @@ export class FolkSpace extends FolkElement {
return root; return root;
} }
localToScreen(point: Point, face: 'front' | 'back'): Point {
const spaceRect = this.getBoundingClientRect();
const centerY = spaceRect.height / 2;
// Calculate transition rotation
let rotation = 0;
if (face === 'front') {
// When rotating to back, go from 0 to -90
// When rotating to front, go from -90 to 0
rotation = this.#isRotated ? -90 * this.#transitionProgress : -90 * (1 - this.#transitionProgress);
} else {
// When rotating to back, go from 90 to 0
// When rotating to front, go from 0 to 90
rotation = this.#isRotated ? 90 * (1 - this.#transitionProgress) : 90 * this.#transitionProgress;
}
const matrix = new DOMMatrix().translate(0, centerY).rotate(rotation, 0, 0).translate(0, -centerY);
const transformedPoint = matrix.transformPoint(new DOMPoint(point.x, point.y));
return {
x: transformedPoint.x,
y: transformedPoint.y,
};
}
transition() { transition() {
const space = this.shadowRoot?.querySelector('.space'); this.#isRotated = !this.#isRotated;
space?.classList.toggle('rotate');
// Reset transition progress
this.#transitionProgress = 0;
// Track transition
const startTime = performance.now();
const duration = 600; // Match CSS transition duration (0.6s)
const animate = () => {
const elapsed = performance.now() - startTime;
this.#transitionProgress = Math.min(elapsed / duration, 1);
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)';
}
} }
} }

View File

@ -1,10 +1,11 @@
import { Matrix } from './Matrix';
import { Point } from './types'; import { Point } from './types';
interface DOMTransformInit { interface DOMTransformInit {
x?: number; x?: number;
y?: number; y?: number;
rotation?: number; rotationX?: number;
rotationY?: number;
rotationZ?: number;
} }
/** /**
@ -15,20 +16,24 @@ export class DOMTransform {
// Private properties for position and rotation // Private properties for position and rotation
#x: number; #x: number;
#y: number; #y: number;
#rotation: number; #rotationX: number;
#rotationY: number;
#rotationZ: number;
// Internal transformation matrices // Internal transformation matrices
#transformMatrix: Matrix; #transformMatrix: DOMMatrix;
#inverseMatrix: Matrix; #inverseMatrix: DOMMatrix;
constructor(init: DOMTransformInit = {}) { constructor(init: DOMTransformInit = {}) {
this.#x = init.x ?? 0; this.#x = init.x ?? 0;
this.#y = init.y ?? 0; this.#y = init.y ?? 0;
this.#rotation = init.rotation ?? 0; this.#rotationX = init.rotationX ?? 0;
this.#rotationY = init.rotationY ?? 0;
this.#rotationZ = init.rotationZ ?? 0;
// Initialize transformation matrices // Initialize with identity matrices
this.#transformMatrix = Matrix.Identity(); this.#transformMatrix = new DOMMatrix();
this.#inverseMatrix = Matrix.Identity(); this.#inverseMatrix = new DOMMatrix();
this.#updateMatrices(); this.#updateMatrices();
} }
@ -49,41 +54,74 @@ export class DOMTransform {
this.#updateMatrices(); this.#updateMatrices();
} }
get rotationX(): number {
return this.#rotationX;
}
set rotationX(value: number) {
this.#rotationX = value;
this.#updateMatrices();
}
get rotationY(): number {
return this.#rotationY;
}
set rotationY(value: number) {
this.#rotationY = value;
this.#updateMatrices();
}
get rotationZ(): number {
return this.#rotationZ;
}
set rotationZ(value: number) {
this.#rotationZ = value;
this.#updateMatrices();
}
get rotation(): number { get rotation(): number {
return this.#rotation; return this.#rotationZ;
} }
set rotation(value: number) { set rotation(value: number) {
this.#rotation = value; this.#rotationZ = value;
this.#updateMatrices(); this.#updateMatrices();
} }
// Matrix accessors // Matrix accessors
get matrix(): Matrix { get matrix(): DOMMatrix {
return this.#transformMatrix; return this.#transformMatrix;
} }
get inverse(): Matrix { get inverse(): DOMMatrix {
return this.#inverseMatrix; return this.#inverseMatrix;
} }
/** /**
* Converts a point from **parent space** to **local space**. * Converts a point from parent space to local space.
*/ */
toPoint(point: Point): Point { toPoint(point: Point): Point {
return this.#inverseMatrix.applyToPoint(point); // Transform using DOMMatrix directly without DOMPoint
const { a, b, c, d, e, f } = this.#inverseMatrix;
return {
x: point.x * a + point.y * c + e,
y: point.x * b + point.y * d + f,
};
} }
/** /**
* Converts a point from **local space** to **parent space**. * Converts a point from local space to parent space.
*/ */
toInversePoint(point: Point): Point { toInversePoint(point: Point): Point {
return this.#transformMatrix.applyToPoint(point); const { a, b, c, d, e, f } = this.#transformMatrix;
return {
x: point.x * a + point.y * c + e,
y: point.x * b + point.y * d + f,
};
} }
/** /**
* Generates a CSS transform string representing the transformation. * Generates a CSS transform string representing the transformation.
*/ */
toCssString(): string { toCssString(): string {
return this.#transformMatrix.toCssString(); return this.#transformMatrix.toString();
} }
/** /**
@ -93,7 +131,9 @@ export class DOMTransform {
return { return {
x: this.x, x: this.x,
y: this.y, y: this.y,
rotation: this.rotation, rotationX: this.rotationX,
rotationY: this.rotationY,
rotationZ: this.rotationZ,
}; };
} }
@ -101,8 +141,14 @@ export class DOMTransform {
* Updates the transformation matrices based on the current position and rotation. * Updates the transformation matrices based on the current position and rotation.
*/ */
#updateMatrices() { #updateMatrices() {
this.#transformMatrix.identity().translate(this.#x, this.#y).rotate(this.#rotation); // Create a fresh identity matrix
this.#transformMatrix = new DOMMatrix()
.translate(this.#x, this.#y)
.rotate(0, 0, this.#rotationZ)
.rotate(0, this.#rotationY, 0)
.rotate(this.#rotationX, 0, 0);
this.#inverseMatrix = this.#transformMatrix.clone().invert(); // DOMMatrix has built-in inverse calculation
this.#inverseMatrix = this.#transformMatrix.inverse();
} }
} }

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@ -23,12 +23,19 @@
[slot='front'] folk-shape { [slot='front'] folk-shape {
background: rgb(187, 178, 178); background: rgb(187, 178, 178);
} }
folk-rope {
position: absolute;
inset: 0;
pointer-events: none;
}
</style> </style>
</head> </head>
<body> <body>
<folk-rope id="rope"></folk-rope>
<folk-space id="space"> <folk-space id="space">
<div slot="front"> <div slot="front">
<folk-shape x="100" y="100" width="50" height="50"></folk-shape> <folk-shape id="source" x="100" 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>
@ -37,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 x="150" y="150" width="50" height="50" rotation="45"></folk-shape> <folk-shape id="target" x="150" 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>
@ -50,8 +57,50 @@
<script type="module"> <script type="module">
import '@labs/standalone/folk-space.ts'; import '@labs/standalone/folk-space.ts';
import '@labs/standalone/folk-shape.ts'; import '@labs/standalone/folk-shape.ts';
import '@labs/standalone/folk-rope.ts';
document.addEventListener('click', () => window.space.transition()); const space = document.getElementById('space');
const rope = document.getElementById('rope');
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();
// 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 };
}
// Update on animation frame
function animate() {
updateRopePoints();
requestAnimationFrame(animate);
}
animate();
// Handle transition
document.addEventListener('click', () => space.transition());
</script> </script>
</body> </body>
</html> </html>