rope transforms
This commit is contained in:
parent
023f667be3
commit
e7bf3522be
|
|
@ -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)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue