feat: ATL pictures/contains/function/time, colour matrices and more Python for Ren'Py
ATL
- Image blocks become timelines: picture steps with dissolve, parallel, contains,
finite repeat, time, function f(trans, st, at), calls of other transforms with
arguments, on hover/idle events for buttons. Animations stop when their element
leaves the page
- Pictures defined by an expression (get_face_path("base")) are drawn at show time
- Purely graphical hints (shader, mesh, gl_*, u_*) are accepted and dropped
Colour
- matrixcolor and im.matrix.* as 5x5 matrices combined with `*`, applied through SVG
filters; im.MatrixColor/Scale/Crop/Blur/Composite and Text/Image as values
Python
- match statement, re/copy/time/datetime/defaultdict/functools.partial, more
str/dict methods, Character(...) created by code
- narrator("..."), Character calls, renpy.say and renpy.show/hide with expressions in
Python blocks become script nodes
- Audit follows reachability: functions and classes nothing refers to (custom
statement lexers, unused tools) no longer block an import
Docs: README and ARCHITECTURE now describe a multi-engine library (GameMaker and
QSP planned). Tests for the new runtime pieces.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
62ece25902
commit
ea03cebf6a
21 files changed
+1264
-93
No files matched your search
@@ -2,6 +2,7 @@
|
||||
// The stage has the game's native resolution and is scaled with a CSS transform.
|
||||
|
||||
import { AtlRunner, bindArguments, type AtlBlock } from "./atl";
|
||||
import { ColorMatrix, matrixFilter } from "./colormatrix";
|
||||
import type { AtSpec, ImageDef, ShownImage, Transition } from "./types";
|
||||
|
||||
const POSITIONS: Record<string, Record<string, number>> = {
|
||||
@@ -93,25 +94,18 @@ export class Renderer {
|
||||
this.master.insertBefore(el, behind ?? later ?? null);
|
||||
}
|
||||
this.shown.set(tag, entry);
|
||||
this.animate(entry, base, [...(img.def.at ?? []), ...atSpecs]);
|
||||
this.animate(entry, base, [...(img.def.at ?? []), ...atSpecs], img.def.timeline);
|
||||
}
|
||||
|
||||
/** 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[]) {
|
||||
private animate(entry: Entry, base: Record<string, any>, specs: AtSpec[], timeline?: AtlBlock) {
|
||||
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 }),
|
||||
eval: (expr, frame) => this.host.evalExpr(expr, frame),
|
||||
}, live, bindArguments(block, spec, (e) => this.host.evalExpr(e)));
|
||||
entry.runners.push({ runner, block });
|
||||
void runner.start(block).catch((err) => console.warn("[pgvn] ATL", err));
|
||||
if (block) this.startTimeline(entry.el, block, live, bindArguments(block, spec, (e) => this.host.evalExpr(e)), entry.runners);
|
||||
}
|
||||
if (timeline) this.startTimeline(entry.el, timeline, live, new Map(), entry.runners);
|
||||
}
|
||||
|
||||
hide(tag: string) {
|
||||
@@ -149,6 +143,16 @@ export class Renderer {
|
||||
const ref = this.host.resolveRef(def.ref);
|
||||
if (ref) return this.build({ ...ref, props: { ...ref.props, ...def.props } }, depth + 1);
|
||||
}
|
||||
if (def.expr) {
|
||||
// an image defined by a Python expression: its value names a picture, evaluated when shown
|
||||
const value = this.host.evalExpr(def.expr);
|
||||
if (typeof value === "string") {
|
||||
const ref = this.host.resolveRef(value);
|
||||
const color = /^#[0-9a-f]{3,8}$/i.test(value) ? { color: value } : null;
|
||||
return this.build(ref ?? color ?? { file: value }, depth + 1);
|
||||
}
|
||||
return document.createElement("div");
|
||||
}
|
||||
if (def.switch) {
|
||||
const hit = def.switch.find(([c]) => this.host.evalCond(c));
|
||||
if (hit) return this.build(hit[1], depth + 1);
|
||||
@@ -192,6 +196,7 @@ export class Renderer {
|
||||
d.textContent = this.host.text(def.textExpr ?? { k: "const", v: def.text });
|
||||
return d;
|
||||
}
|
||||
if (def.timeline) return this.timelineBox(def, depth);
|
||||
if (def.frames?.length) return this.animated(def, depth);
|
||||
if (def.layered) return this.layered(def, depth);
|
||||
if (def.file) {
|
||||
@@ -210,6 +215,61 @@ export class Renderer {
|
||||
return document.createElement("div");
|
||||
}
|
||||
|
||||
/** Container of an ATL timeline that switches pictures; the first one is shown at once. */
|
||||
private timelineBox(def: ImageDef, depth: number): HTMLElement {
|
||||
const box = document.createElement("div");
|
||||
box.style.display = "grid";
|
||||
box.dataset.timeline = "1";
|
||||
const first = firstPicture(def.timeline!);
|
||||
if (first) {
|
||||
const child = this.build(first, depth + 1);
|
||||
child.style.gridArea = "1 / 1";
|
||||
box.append(child);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
/** Replace the picture shown in an animated element, optionally with a cross-fade. */
|
||||
private swap(box: HTMLElement, def: ImageDef, transition: Transition | null | undefined) {
|
||||
const child = this.build(def);
|
||||
child.style.gridArea = "1 / 1";
|
||||
const old = [...box.children];
|
||||
if (!transition || this.fast) {
|
||||
box.replaceChildren(child);
|
||||
return;
|
||||
}
|
||||
child.style.opacity = "0";
|
||||
box.append(child);
|
||||
child.animate([{ opacity: 0 }, { opacity: 1 }], { duration: (transition.d ?? 0.5) * 1000, fill: "forwards" })
|
||||
.onfinish = () => {
|
||||
child.style.opacity = "";
|
||||
old.forEach((e) => e.remove());
|
||||
};
|
||||
}
|
||||
|
||||
/** Run a timeline on `el`; its properties are applied with the same styling as any shown image. */
|
||||
private startTimeline(el: HTMLElement, block: AtlBlock, base: Record<string, any>,
|
||||
vars: Map<string, any>, runners: Entry["runners"]) {
|
||||
const live = base;
|
||||
const runner = new AtlRunner({
|
||||
block: (name) => this.host.atl(name),
|
||||
props: (name) => this.host.transform(name),
|
||||
apply: (p) => this.style(el, { ...p }),
|
||||
eval: (expr, frame) => this.host.evalExpr(expr, frame),
|
||||
image: (img, transition) => this.swap(el, img, transition),
|
||||
contains: (img) => {
|
||||
const child = this.build(img);
|
||||
child.style.position = "absolute";
|
||||
child.style.left = child.style.top = "0";
|
||||
el.append(child);
|
||||
if (img.timeline) this.startTimeline(child, img.timeline, {}, new Map(), runners);
|
||||
},
|
||||
}, live, vars);
|
||||
runner.watch = el;
|
||||
runners.push({ runner, block });
|
||||
void runner.start(block).catch((err) => console.warn("[pgvn] ATL", err));
|
||||
}
|
||||
|
||||
/** An element for an image definition, for UI built outside the scene (screens). */
|
||||
displayable(def: ImageDef): HTMLElement {
|
||||
return this.build(def);
|
||||
@@ -322,6 +382,7 @@ export class Renderer {
|
||||
const filters: string[] = [];
|
||||
if (p.filter) filters.push(`${p.filter}(1)`);
|
||||
if (typeof p.blur === "number" && p.blur > 0) filters.push(`blur(${p.blur}px)`);
|
||||
if (p.matrixcolor instanceof ColorMatrix) filters.push(matrixFilter(p.matrixcolor));
|
||||
if (filters.length) el.style.filter = filters.join(" ");
|
||||
}
|
||||
|
||||
@@ -384,6 +445,16 @@ export class Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/** The first picture an ATL timeline shows (what is on screen before it starts). */
|
||||
function firstPicture(block: AtlBlock): ImageDef | null {
|
||||
for (const step of block.steps) {
|
||||
if (step.op === "image") return step.img;
|
||||
if (step.op === "parallel") for (const b of step.blocks) { const f = firstPicture(b); if (f) return f; }
|
||||
if (step.op === "block") { const f = firstPicture(step.block); if (f) return f; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalize(p: Record<string, any>) {
|
||||
if (p.align) [p.xalign, p.yalign] = p.align;
|
||||
if (p.pos) [p.xpos, p.ypos] = p.pos;
|
||||
|
||||
Reference in new issue
Block a user