feat: Kinetic Text Tags letters ({bt} {fi} {sc} {rotat} {glitch}) use the library's own formulas; Flatten; screen 3D
CI / frontend (push) Failing after 19s
CI / backend (push) Failing after 39s

Each letter is moved by one animation loop with the formulas of BounceText, FadeInText, ScareText and
GlitchText (fi follows the text speed, bt keeps its phase while a line is typed out, glitch cuts the text
into strips that jump sideways). Flatten and `at Flatten` isolate their children as one picture, screens
get the 3D turns, matrixtransform and blend of their widgets, and RotateMatrix/OffsetMatrix/ScaleMatrix
exist in Python.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-27 01:45:02 +03:00
1 parent 5ff43038ae
commit ea351e44b3
5 files changed
+189 -23

No files matched your search

+20 -1
View File
@@ -9,6 +9,7 @@ import { galleryClass } from "./gallery";
import { parseExpression } from "./pyparse";
import { movieDisplayable } from "./movie";
import { RenderHost, isDisplayableInstance } from "./render";
import { Matrix4, blendMode } from "./rendertransform";
import { strftime } from "./pymodules";
import { OBJECT, PyClass, PyDict, PyInstance, callableOf, nativeMethod, pyEq, pyStr, registerNativeClass, truthy, type Python } from "./python";
import type { AtSpec, Expr } from "./types";
@@ -1184,6 +1185,13 @@ export class ScreenManager {
const name = pyStr(a).replace(/\[([A-Za-z_][\w.]*)\]/g, (_m, source: string) => pyStr(this.host.py.eval(parseExpression(source))));
return this.displayable(name, depth + 1);
}
case "flatten": { // the children are drawn as one picture: opacity and filters then act on the whole
const group = document.createElement("div");
group.style.isolation = "isolate";
const child = this.displayable(a, depth + 1);
if (child) group.append(child);
return group;
}
case "animation": { // pictures that take turns; the last one stays when it has no time
const box = document.createElement("div");
const pictures: HTMLElement[] = [];
@@ -1591,7 +1599,14 @@ export class ScreenManager {
if (p.xoffset || p.yoffset) parts.push(`translate(${p.xoffset ?? 0}px, ${p.yoffset ?? 0}px)`);
if (xz !== 1 || yz !== 1) parts.push(`scale(${xz}, ${yz})`);
if (p.rotate) parts.push(`rotate(${p.rotate}deg)`);
// 3D turns about the widget's centre (the default matrixanchor) are Ren'Py's own matrices
if (p.xrotate || p.yrotate || p.zrotate || p.zpos) {
const turn = Matrix4.offset(0, 0, p.zpos ?? 0).mul(Matrix4.rotate(p.xrotate ?? 0, p.yrotate ?? 0, p.zrotate ?? 0));
parts.unshift(turn.toCss());
}
if (p.matrixtransform instanceof Matrix4) parts.unshift(p.matrixtransform.toCss());
el.style.transform = parts.join(" ");
el.style.mixBlendMode = blendMode(p);
if (p.alpha !== undefined) el.style.opacity = String(p.alpha);
if (p.rounded !== undefined) { // RoundedCorners(radius): one radius or four (the game's own shader)
const radius = Array.isArray(p.rounded) ? p.rounded.map((r: number) => `${r}px`).join(" ") : `${p.rounded}px`;
@@ -1600,6 +1615,7 @@ export class ScreenManager {
}
applyFilters(el, p);
};
if (specs.some((spec) => spec.name === "Flatten")) el.style.isolation = "isolate"; // `at Flatten`: the widget is one picture
for (const spec of specs) {
Object.assign(live, spec.props ?? {}, spec.name ? this.host.transform(spec.name) ?? {} : {});
}
@@ -2027,7 +2043,10 @@ export class ScreenManager {
out.Fixed = shape("composite");
out.Composite = shape("composite");
out.LiveComposite = shape("composite");
out.Flatten = (child: any) => child; // flattening only changes how the browser composites, not what is drawn
out.Flatten = shape("flatten");
out.RotateMatrix = (x: number, y: number, z: number) => Matrix4.rotate(x, y, z);
out.OffsetMatrix = (x: number, y: number, z: number) => Matrix4.offset(x, y, z);
out.ScaleMatrix = (x: number, y: number, z: number) => Matrix4.scale(x, y, z);
out.AlphaMask = shape("alphamask");
out.TrackCursor = shape("trackcursor");
out.PreviewSlowText = shape("text");