feat(tokens): animated paths for scene and NPC tokens
Add path editor window, session playback on control/presentation, and RMB controls. Fix live pose clock so motion no longer freezes after ~250ms. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { TokenPath } from '../../../shared/types/tokenPath';
|
||||
import { tokenPathPolyline } from '../../../shared/types/tokenPath';
|
||||
import type { SceneNpcToken, SceneToken } from '../../../shared/types';
|
||||
import { tokenPathKey, type TokenPathSessionState } from '../../../shared/types/tokenPathSession';
|
||||
|
||||
import styles from './TokenPathsOverlay.module.css';
|
||||
|
||||
type Viewport = { x: number; y: number; w: number; h: number };
|
||||
|
||||
type PathItem = {
|
||||
key: string;
|
||||
path: TokenPath;
|
||||
emphasized?: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
tokens: readonly SceneToken[];
|
||||
npcTokens?: readonly SceneNpcToken[];
|
||||
viewport: Viewport | null;
|
||||
/** control/editor: always; presentation: only presentationVisible */
|
||||
mode: 'always' | 'presentation';
|
||||
pathSession?: TokenPathSessionState | null;
|
||||
/** Highlight path currently edited */
|
||||
emphasizeKey?: string | null;
|
||||
};
|
||||
|
||||
function toSvgPoints(path: TokenPath, viewport: Viewport): string {
|
||||
const pts = tokenPathPolyline(path);
|
||||
return pts
|
||||
.map((p) => {
|
||||
const x = viewport.x + p.nx * viewport.w;
|
||||
const y = viewport.y + p.ny * viewport.h;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
export function TokenPathsOverlay({
|
||||
tokens,
|
||||
npcTokens = [],
|
||||
viewport,
|
||||
mode,
|
||||
pathSession = null,
|
||||
emphasizeKey = null,
|
||||
}: Props) {
|
||||
if (!viewport) return null;
|
||||
|
||||
const items: PathItem[] = [];
|
||||
for (const t of tokens) {
|
||||
if (!t.path || t.path.points.length < 2) continue;
|
||||
const key = tokenPathKey('token', String(t.id));
|
||||
if (mode === 'presentation' && !pathSession?.presentationVisible[key]) continue;
|
||||
items.push({ key, path: t.path, emphasized: emphasizeKey === key });
|
||||
}
|
||||
for (const t of npcTokens) {
|
||||
if (!t.path || t.path.points.length < 2) continue;
|
||||
const key = tokenPathKey('npcToken', String(t.id));
|
||||
if (mode === 'presentation' && !pathSession?.presentationVisible[key]) continue;
|
||||
items.push({ key, path: t.path, emphasized: emphasizeKey === key });
|
||||
}
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<svg className={styles.layer} width="100%" height="100%" aria-hidden>
|
||||
{items.map((item) => {
|
||||
const pts = toSvgPoints(item.path, viewport);
|
||||
if (!pts) return null;
|
||||
const first = item.path.points[0]!;
|
||||
const fx = viewport.x + first.nx * viewport.w;
|
||||
const fy = viewport.y + first.ny * viewport.h;
|
||||
return (
|
||||
<g key={item.key} className={item.emphasized ? styles.emphasized : undefined}>
|
||||
<polyline className={styles.lineGlow} points={pts} fill="none" />
|
||||
<polyline className={styles.line} points={pts} fill="none" />
|
||||
<circle className={styles.start} cx={fx} cy={fy} r={4} />
|
||||
{item.path.closed ? (
|
||||
<circle
|
||||
className={styles.closed}
|
||||
cx={fx}
|
||||
cy={fy}
|
||||
r={7}
|
||||
fill="none"
|
||||
/>
|
||||
) : null}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user