feat: add ATL animation timelines and try/raise to the Ren'Py player
Python sandbox - try/except/else/finally, raise (incl. re-raise), assert, for/while ... else - Multi-generator and dict comprehensions - Builtin exception hierarchy; user exceptions subclass it - Inside try, missing keys/indexes, division by zero, attribute access on None and unknown names raise Python exceptions; lookups stay lenient elsewhere - int()/float() raise ValueError on non-numeric strings ATL - converter/atl.py: set/move (linear, ease, easein, easeout), pause, repeat, parallel, choice, block, on show/hide and named transforms become a timeline shipped as game.json "atl"; Move() becomes a linear slide - player/atl.ts: timeline runner wired into the renderer; `on hide` plays before the picture is removed - function, time, contains, matrixcolor and expressions still block the import Tests: exceptions/ATL fixtures and runtime tests, converter ATL tests; README and ARCHITECTURE updated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
962595c925
commit
ff82d882c0
18 files changed
+1050
-53
No files matched your search
@@ -1,6 +1,7 @@
|
||||
// DOM renderer for the image layer: one absolutely positioned element per shown tag.
|
||||
// The stage has the game's native resolution and is scaled with a CSS transform.
|
||||
|
||||
import { AtlRunner, type AtlBlock } from "./atl";
|
||||
import type { AtSpec, ImageDef, ShownImage, Transition } from "./types";
|
||||
|
||||
const POSITIONS: Record<string, Record<string, number>> = {
|
||||
@@ -27,11 +28,13 @@ export interface RendererHost {
|
||||
resolveRef(name: string): ImageDef | null;
|
||||
evalCond(e: any): boolean;
|
||||
transform(name: string): Record<string, any> | null;
|
||||
atl(name: string): AtlBlock | null;
|
||||
text(e: any): string;
|
||||
}
|
||||
|
||||
interface Entry extends ShownImage {
|
||||
el: HTMLElement;
|
||||
runners: { runner: AtlRunner; block: AtlBlock }[];
|
||||
}
|
||||
|
||||
export class Renderer {
|
||||
@@ -64,7 +67,10 @@ export class Renderer {
|
||||
// --- scene state ---------------------------------------------------------
|
||||
|
||||
scene() {
|
||||
for (const e of this.shown.values()) e.el.remove();
|
||||
for (const e of this.shown.values()) {
|
||||
e.runners.forEach((r) => r.runner.stop());
|
||||
e.el.remove();
|
||||
}
|
||||
this.shown.clear();
|
||||
}
|
||||
|
||||
@@ -73,8 +79,10 @@ export class Renderer {
|
||||
const atSpecs = at.length ? at : prev?.at ?? [];
|
||||
const el = this.build(img.def);
|
||||
el.classList.add("pg-image");
|
||||
this.place(el, img.def, atSpecs);
|
||||
const entry: Entry = { tag, name: img.name, at: atSpecs, zorder: opts.zorder ?? prev?.zorder ?? 0, el };
|
||||
const base = this.place(el, img.def, atSpecs);
|
||||
const entry: Entry = { tag, name: img.name, at: atSpecs, zorder: opts.zorder ?? prev?.zorder ?? 0, el,
|
||||
runners: [] };
|
||||
prev?.runners.forEach((r) => r.runner.stop());
|
||||
if (prev) {
|
||||
prev.el.replaceWith(el);
|
||||
} else {
|
||||
@@ -83,13 +91,39 @@ export class Renderer {
|
||||
this.master.insertBefore(el, behind ?? later ?? null);
|
||||
}
|
||||
this.shown.set(tag, entry);
|
||||
this.animate(entry, base, atSpecs);
|
||||
}
|
||||
|
||||
/** Start the ATL timelines of an image; each works on its own copy of the properties. */
|
||||
private animate(entry: Entry, base: Record<string, any>, specs: AtSpec[]) {
|
||||
if (this.fast) return;
|
||||
const live = { ...base };
|
||||
for (const spec of specs) {
|
||||
const block = typeof spec.atl === "string" ? this.host.atl(spec.atl) : spec.atl;
|
||||
if (!block) continue;
|
||||
const runner = new AtlRunner({
|
||||
block: (name) => this.host.atl(name),
|
||||
props: (name) => this.host.transform(name),
|
||||
apply: (p) => this.style(entry.el, { ...p }),
|
||||
}, live);
|
||||
entry.runners.push({ runner, block });
|
||||
void runner.start(block).catch((err) => console.warn("[pgvn] ATL", err));
|
||||
}
|
||||
}
|
||||
|
||||
hide(tag: string) {
|
||||
const e = this.shown.get(tag);
|
||||
if (!e) return;
|
||||
e.el.remove();
|
||||
this.shown.delete(tag);
|
||||
const exit = e.runners.find((r) => r.runner.hasHideHandler(r.block));
|
||||
if (!exit || this.fast) {
|
||||
e.runners.forEach((r) => r.runner.stop());
|
||||
e.el.remove();
|
||||
return;
|
||||
}
|
||||
// `on hide` animation: the picture leaves the scene state at once but stays visible until it ends
|
||||
e.runners.forEach((r) => r.runner !== exit.runner && r.runner.stop());
|
||||
void exit.runner.hide(exit.block).finally(() => e.el.remove());
|
||||
}
|
||||
|
||||
isShown(tag: string) {
|
||||
@@ -243,7 +277,8 @@ export class Renderer {
|
||||
|
||||
cameraAt: AtSpec[] = [];
|
||||
|
||||
private place(el: HTMLElement, def: ImageDef, at: AtSpec[]) {
|
||||
/** Sets the element's position and look; returns the properties it was built from. */
|
||||
private place(el: HTMLElement, def: ImageDef, at: AtSpec[]): Record<string, any> {
|
||||
const p: Record<string, any> = { ...POSITIONS.default, ...(def.props ?? {}) };
|
||||
for (const spec of [...(def.at ?? []), ...at]) {
|
||||
if (spec.effect === "shake") {
|
||||
@@ -256,6 +291,11 @@ export class Renderer {
|
||||
if (spec.name && !spec.props) Object.assign(p, this.host.transform(spec.name) ?? POSITIONS[spec.name] ?? {});
|
||||
if (spec.props) Object.assign(p, spec.props);
|
||||
}
|
||||
this.style(el, { ...p });
|
||||
return p;
|
||||
}
|
||||
|
||||
private style(el: HTMLElement, p: Record<string, any>) {
|
||||
normalize(p);
|
||||
const pos = (v: number, total: number) => (Number.isInteger(v) && Math.abs(v) > 1 ? `${v}px` : `${v * total}px`);
|
||||
const anchor = (v: number) => (Number.isInteger(v) && Math.abs(v) > 1 ? `${v}px` : `${v * 100}%`);
|
||||
|
||||
Reference in new issue
Block a user