commit
3c5d3db810
|
|
@ -0,0 +1,59 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Physics</title>
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
inset: 0;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
folk-shape {
|
||||
position: absolute;
|
||||
background-color: rgba(100, 100, 100, 0.5);
|
||||
border: 2px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
folk-rope {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<folk-shape id="shape1" x="250" y="50" width="120" height="30" rotation="30"></folk-shape>
|
||||
<folk-shape id="shape2" x="450" y="350" width="40" height="120" rotation="45"></folk-shape>
|
||||
<folk-shape id="shape3" x="150" y="150" width="70" height="70" rotation="15"></folk-shape>
|
||||
<folk-shape id="shape4" x="350" y="300" width="90" height="45" rotation="-20"></folk-shape>
|
||||
<folk-shape id="shape5" x="550" y="200" width="35" height="85" rotation="60"></folk-shape>
|
||||
<folk-shape id="shape6" x="180" y="250" width="65" height="55" rotation="-40"></folk-shape>
|
||||
<folk-shape id="shape7" x="420" y="150" width="110" height="25" rotation="10"></folk-shape>
|
||||
<folk-shape id="shape8" x="280" y="380" width="75" height="95" rotation="-50"></folk-shape>
|
||||
<folk-rope id="rope1" source="#shape1" target="#shape2"></folk-rope>
|
||||
<folk-rope id="rope2" source="#shape1" target="#shape3"></folk-rope>
|
||||
<folk-rope id="rope3" source="#shape2" target="#shape3"></folk-rope>
|
||||
<folk-rope id="rope4" source="#shape3" target="#shape4"></folk-rope>
|
||||
<folk-rope id="rope5" source="#shape4" target="#shape8"></folk-rope>
|
||||
<folk-rope id="rope6" source="#shape5" target="#shape8"></folk-rope>
|
||||
<folk-graph
|
||||
sources="#shape1, #shape2, #shape3, #shape4, #shape5, #shape8, #rope1, #rope2, #rope3, #rope4, #rope5, #rope6"
|
||||
></folk-graph>
|
||||
<folk-physics sources="#shape1, #shape2, #shape3, #shape4, #shape5, #shape6, #shape7, #shape8"></folk-physics>
|
||||
|
||||
<script type="module">
|
||||
import '../src/standalone/folk-shape.ts';
|
||||
import '../src/standalone/folk-arrow.ts';
|
||||
import '../src/standalone/folk-rope.ts';
|
||||
import '../src/standalone/folk-graph.ts';
|
||||
import '../src/standalone/folk-physics.ts';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import type { FolkShape } from '../folk-shape';
|
||||
|
||||
/**
|
||||
* Coordinates effects between multiple systems, integrating their proposals into a single result.
|
||||
* Systems register, yield effects, and await integration when all systems are ready.
|
||||
*/
|
||||
export class EffectIntegrator<E extends Element, T> {
|
||||
private pending = new Map<E, T[] | T>();
|
||||
private systems = new Set<string>();
|
||||
private waiting = new Set<string>();
|
||||
private resolvers: ((value: Map<E, T>) => void)[] = [];
|
||||
|
||||
/** Register a system to participate in effect integration */
|
||||
register(id: string) {
|
||||
this.systems.add(id);
|
||||
return {
|
||||
yield: (element: E, effect: T) => {
|
||||
if (!this.pending.has(element)) {
|
||||
this.pending.set(element, []);
|
||||
}
|
||||
(this.pending.get(element)! as T[]).push(effect);
|
||||
},
|
||||
|
||||
/** Wait for all systems to submit effects, then receive integrated results */
|
||||
integrate: async (): Promise<Map<E, T>> => {
|
||||
this.waiting.add(id);
|
||||
|
||||
if (this.waiting.size === this.systems.size) {
|
||||
// Last system to call integrate - do integration
|
||||
for (const [element, effects] of this.pending) {
|
||||
this.pending.set(element, (this.constructor as typeof EffectIntegrator).integrate(element, effects as T[]));
|
||||
}
|
||||
const results = this.pending as Map<E, T>;
|
||||
|
||||
// Reset for next frame
|
||||
this.pending = new Map();
|
||||
this.waiting.clear();
|
||||
|
||||
// Resolve all waiting systems
|
||||
this.resolvers.forEach((resolve) => resolve(results));
|
||||
this.resolvers = [];
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Not all systems ready - wait for integration
|
||||
return new Promise((resolve) => {
|
||||
this.resolvers.push(resolve);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Integrate multiple effects into a single result. Must be implemented by derived classes. */
|
||||
protected static integrate(element: Element, effects: any[]): any {
|
||||
throw new Error('Derived class must implement static integrate');
|
||||
}
|
||||
}
|
||||
|
||||
// Transform-specific integrator
|
||||
interface TransformEffect {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export class TransformIntegrator extends EffectIntegrator<FolkShape, TransformEffect> {
|
||||
private static instance: TransformIntegrator;
|
||||
|
||||
static register(id: string) {
|
||||
if (!TransformIntegrator.instance) {
|
||||
TransformIntegrator.instance = new TransformIntegrator();
|
||||
}
|
||||
return TransformIntegrator.instance.register(id);
|
||||
}
|
||||
|
||||
/** If the element is focused, return the elements rect, otherwise average the effects */
|
||||
protected static override integrate(element: FolkShape, effects: TransformEffect[]): TransformEffect {
|
||||
if (element === document.activeElement) {
|
||||
// If the element is focused, we don't want to apply any effects and just return the current state
|
||||
const rect = element.getTransformDOMRect();
|
||||
return {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
rotation: rect.rotation,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
};
|
||||
}
|
||||
|
||||
// Accumulate all effects
|
||||
const result = effects.reduce(
|
||||
(acc, effect) => ({
|
||||
x: acc.x + effect.x,
|
||||
y: acc.y + effect.y,
|
||||
rotation: acc.rotation + effect.rotation,
|
||||
width: effect.width,
|
||||
height: effect.height,
|
||||
}),
|
||||
{ x: 0, y: 0, rotation: 0, width: effects[0].width, height: effects[0].height }
|
||||
);
|
||||
|
||||
// Average all effects
|
||||
const count = effects.length;
|
||||
result.x /= count;
|
||||
result.y /= count;
|
||||
result.rotation /= count;
|
||||
|
||||
// Apply averaged results to element
|
||||
element.x = result.x;
|
||||
element.y = result.y;
|
||||
element.rotation = result.rotation;
|
||||
|
||||
// Return averaged result
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { Layout } from 'webcola';
|
|||
import { FolkShape } from './folk-shape.ts';
|
||||
import { AnimationFrameController, AnimationFrameControllerHost } from './common/animation-frame-controller.ts';
|
||||
import { FolkBaseConnection } from './folk-base-connection';
|
||||
import { TransformIntegrator } from './common/EffectIntegrator.ts';
|
||||
|
||||
export class FolkGraph extends FolkBaseSet implements AnimationFrameControllerHost {
|
||||
static override tagName = 'folk-graph';
|
||||
|
|
@ -11,6 +12,7 @@ export class FolkGraph extends FolkBaseSet implements AnimationFrameControllerHo
|
|||
private graphSim = new Layout();
|
||||
private nodes = new Map<FolkShape, number>();
|
||||
private arrows = new Set<FolkBaseConnection>();
|
||||
private integrator = TransformIntegrator.register('graph');
|
||||
#rAF = new AnimationFrameController(this);
|
||||
|
||||
connectedCallback() {
|
||||
|
|
@ -34,21 +36,31 @@ export class FolkGraph extends FolkBaseSet implements AnimationFrameControllerHo
|
|||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
// TODO: figure out how to let cola continue running. I was never able to do that in the past...
|
||||
async tick() {
|
||||
this.graphSim.start(1, 0, 0, 0, true, false);
|
||||
|
||||
this.graphSim.nodes().forEach((node: any) => {
|
||||
// Yield graph layout effects
|
||||
for (const node of this.graphSim.nodes() as any[]) {
|
||||
const shape = node.id;
|
||||
if (shape === document.activeElement) {
|
||||
const rect = shape.getTransformDOMRect();
|
||||
node.x = rect.center.x;
|
||||
node.y = rect.center.y;
|
||||
} else {
|
||||
shape.x = node.x - shape.width / 2;
|
||||
shape.y = node.y - shape.height / 2;
|
||||
this.integrator.yield(shape, {
|
||||
x: node.x - shape.width / 2,
|
||||
y: node.y - shape.height / 2,
|
||||
rotation: shape.rotation,
|
||||
width: shape.width,
|
||||
height: shape.height,
|
||||
});
|
||||
}
|
||||
|
||||
// Get integrated results and update graph state
|
||||
const results = await this.integrator.integrate();
|
||||
for (const [shape, result] of results) {
|
||||
// TODO: this is a hack to get the node from the graph
|
||||
const node = this.graphSim.nodes().find((n: any) => n.id === shape);
|
||||
if (node) {
|
||||
node.x = result.x + shape.width / 2;
|
||||
node.y = result.y + shape.height / 2;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private createGraph() {
|
||||
|
|
@ -58,7 +70,9 @@ export class FolkGraph extends FolkBaseSet implements AnimationFrameControllerHo
|
|||
const colaNodes = this.createNodes();
|
||||
const colaLinks = this.createLinks();
|
||||
|
||||
this.graphSim.nodes(colaNodes).links(colaLinks).linkDistance(250).avoidOverlaps(true).handleDisconnected(true);
|
||||
console.log(colaNodes, colaLinks);
|
||||
|
||||
this.graphSim.nodes(colaNodes).links(colaLinks).linkDistance(150).avoidOverlaps(true).handleDisconnected(true);
|
||||
}
|
||||
|
||||
private createNodes() {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { FolkBaseSet } from './folk-base-set.ts';
|
|||
import { PropertyValues } from '@lit/reactive-element';
|
||||
import { FolkShape } from './folk-shape';
|
||||
import RAPIER, { init } from '@dimforge/rapier2d-compat';
|
||||
import { TransformIntegrator } from './common/EffectIntegrator.ts';
|
||||
await init();
|
||||
|
||||
export class FolkPhysics extends FolkBaseSet {
|
||||
|
|
@ -15,6 +16,7 @@ export class FolkPhysics extends FolkBaseSet {
|
|||
private elementToRect: Map<FolkShape, DOMRectTransform> = new Map();
|
||||
private animationFrameId?: number;
|
||||
private lastTimestamp?: number;
|
||||
private integrator = TransformIntegrator.register('physics');
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
|
@ -119,7 +121,7 @@ export class FolkPhysics extends FolkBaseSet {
|
|||
}
|
||||
|
||||
private startSimulation() {
|
||||
const step = (timestamp: number) => {
|
||||
const step = async (timestamp: number) => {
|
||||
if (!this.lastTimestamp) {
|
||||
this.lastTimestamp = timestamp;
|
||||
}
|
||||
|
|
@ -127,16 +129,33 @@ export class FolkPhysics extends FolkBaseSet {
|
|||
if (this.world) {
|
||||
this.world.step();
|
||||
|
||||
// Update visual elements based on physics
|
||||
this.bodies.forEach((body, shape) => {
|
||||
if (shape !== document.activeElement) {
|
||||
const position = body.translation();
|
||||
// Scale up the position when applying to visual elements
|
||||
shape.x = position.x / FolkPhysics.PHYSICS_SCALE - shape.width / 2;
|
||||
shape.y = position.y / FolkPhysics.PHYSICS_SCALE - shape.height / 2;
|
||||
shape.rotation = body.rotation();
|
||||
// Yield physics effects
|
||||
for (const [shape, body] of this.bodies) {
|
||||
const position = body.translation();
|
||||
this.integrator.yield(shape, {
|
||||
x: position.x / FolkPhysics.PHYSICS_SCALE - shape.width / 2,
|
||||
y: position.y / FolkPhysics.PHYSICS_SCALE - shape.height / 2,
|
||||
rotation: body.rotation(),
|
||||
width: shape.width,
|
||||
height: shape.height,
|
||||
});
|
||||
}
|
||||
|
||||
// Get integrated results and update physics state
|
||||
const results = await this.integrator.integrate();
|
||||
for (const [shape, result] of results) {
|
||||
const body = this.bodies.get(shape);
|
||||
if (body) {
|
||||
body.setTranslation(
|
||||
{
|
||||
x: (result.x + result.width / 2) * FolkPhysics.PHYSICS_SCALE,
|
||||
y: (result.y + result.height / 2) * FolkPhysics.PHYSICS_SCALE,
|
||||
},
|
||||
true
|
||||
);
|
||||
body.setRotation(result.rotation, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.animationFrameId = requestAnimationFrame(step);
|
||||
|
|
|
|||
Loading…
Reference in New Issue