DocDoeAI / src /video-engine /physics /continuousCanvasState.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
5.05 kB
import type { CanvasRegion, VisualAction, VisualBeatGraph } from "./contracts";
export type CanvasObjectState = {
objectId: string;
kind: "particle_field" | "slinky" | "wave_graph" | "formula" | "numerical" | "note" | "recap" | "text" | "label" | "arrow" | "measurement";
region: CanvasRegion;
text?: string;
role?: "title" | "explanation" | "definition" | "caption" | "recap";
equation?: string;
explanation?: string;
tokens?: string[];
answer?: string;
answerExplanation?: string;
label?: string;
dimmed?: boolean;
highlighted?: boolean;
appearedAtMs: number;
};
export type ContinuousCanvasState = {
objects: CanvasObjectState[];
activeBeatIndex: number;
activeBeatProgress: number;
};
function defaultTextRegion(action: Extract<VisualAction, { type: "write_text" }>): CanvasRegion {
if (action.role === "title") return "title";
if (action.role === "recap") return "centre";
return "right";
}
export function deriveContinuousCanvasState(graph: VisualBeatGraph, timeMs: number): ContinuousCanvasState {
const objects = new Map<string, CanvasObjectState>();
let activeBeatIndex = graph.beats.findIndex(
(beat) => timeMs >= beat.estimatedStartMs && timeMs < beat.estimatedEndMs,
);
if (activeBeatIndex < 0) activeBeatIndex = Math.max(0, graph.beats.length - 1);
for (const beat of graph.beats) {
if (beat.estimatedStartMs > timeMs) break;
for (const action of beat.actions) {
applyAction(objects, action, beat.estimatedStartMs);
}
}
const active = graph.beats[activeBeatIndex];
const activeBeatProgress = active
? Math.max(0, Math.min(1, (timeMs - active.estimatedStartMs) / (active.estimatedEndMs - active.estimatedStartMs)))
: 0;
return { objects: [...objects.values()], activeBeatIndex, activeBeatProgress };
}
function applyAction(objects: Map<string, CanvasObjectState>, action: VisualAction, appearedAtMs: number) {
switch (action.type) {
case "add_object":
objects.set(action.objectId, {
objectId: action.objectId,
kind: action.objectType,
region: action.region,
appearedAtMs,
});
return;
case "remove_object":
objects.delete(action.objectId);
return;
case "clear_region":
for (const [id, object] of objects) {
if (object.region === action.region) objects.delete(id);
}
return;
case "move_object": {
const object = objects.get(action.objectId);
if (object) objects.set(action.objectId, { ...object, region: action.region });
return;
}
case "write_text":
objects.set(action.objectId, {
objectId: action.objectId,
kind: "text",
region: defaultTextRegion(action),
text: action.text,
role: action.role,
appearedAtMs,
});
return;
case "write_equation": {
const current = objects.get(action.objectId);
objects.set(action.objectId, {
objectId: action.objectId,
kind: "formula",
region: current?.region ?? "formula",
equation: action.equation,
explanation: action.explanation,
tokens: action.tokens,
appearedAtMs,
});
return;
}
case "question": {
const current = objects.get(action.objectId);
objects.set(action.objectId, {
objectId: action.objectId,
kind: "numerical",
region: current?.region ?? "centre",
text: action.prompt,
appearedAtMs,
});
return;
}
case "answer_reveal": {
const current = objects.get(action.objectId);
if (current) {
objects.set(action.objectId, {
...current,
answer: action.answer,
answerExplanation: action.explanation,
});
}
return;
}
case "reveal_label":
objects.set(action.objectId, {
objectId: action.objectId,
kind: "label",
region: "centre",
text: action.text,
label: action.targetId,
appearedAtMs,
});
return;
case "draw_arrow":
objects.set(action.objectId, {
objectId: action.objectId,
kind: "arrow",
region: "centre",
label: action.label,
appearedAtMs,
});
return;
case "show_measurement":
objects.set(action.objectId, {
objectId: action.objectId,
kind: "measurement",
region: "centre",
label: action.label,
appearedAtMs,
});
return;
case "highlight": {
const object = objects.get(action.targetId);
if (object) objects.set(action.targetId, { ...object, highlighted: true, dimmed: false });
return;
}
case "dim": {
const object = objects.get(action.targetId);
if (object) objects.set(action.targetId, { ...object, dimmed: true });
return;
}
case "focus": {
const object = objects.get(action.targetId);
if (object) objects.set(action.targetId, { ...object, dimmed: false });
return;
}
default:
return;
}
}