Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "route-graphics",
"version": "1.7.2",
"version": "1.7.3",
"description": "A 2D graphics rendering interface that takes JSON input and renders pixels using PixiJS",
"main": "dist/RouteGraphics.js",
"type": "module",
Expand Down
5 changes: 3 additions & 2 deletions playground/pages/docs/nodes/text-revealing.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Try it in the [Playground](/playground/?template=text-revealing).
| `anchorY` | number | No | `0` | Anchor offset ratio. |
| `alpha` | number | No | `1` | Opacity `0..1`. |
| `textStyle` | object | No | text defaults | Base style for segments. |
| `speed` | number | No | `50` | Higher is faster (delay is inverse). |
| `speed` | number | No | `50` | Uses a curved `0..100` scale. `0..99` gets progressively faster with extra control in the upper range; `100` renders instantly. |
| `revealEffect` | `typewriter` \| `softWipe` \| `none` | No | `typewriter` | `softWipe` reveals pre-laid-out text with a soft left-to-right mask, one laid-out line at a time. `none` renders instantly. |
| `indicator` | object | No | - | Revealing/complete icon config + offset. |
| `complete` | object | No | - | Parsed and kept in computed node. |
Expand Down Expand Up @@ -55,7 +55,8 @@ Try it in the [Playground](/playground/?template=text-revealing).
## Behavior Notes

- Reveal runs chunk by chunk.
- `speed` affects per-character and per-chunk waits.
- `speed` uses an exponential/log-like mapping so `50..99` covers most of the fast reveal range with finer control than a linear scale.
- `speed: 100` skips animation entirely and paints the final text immediately, regardless of `revealEffect`.
- `softWipe` lays out the full text immediately and reveals it line by line with a moving soft mask.
- `revealEffect: none` skips animation and paints text immediately.
- Completion contributes to global `renderComplete` tracking.
Expand Down
9 changes: 8 additions & 1 deletion spec/animations/runReplaceAnimation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -753,11 +753,13 @@ describe("runReplaceAnimation", () => {
};

const destroyOrder = [];
const renderTextureConfigs = [];
let renderTextureIndex = 0;
let filterIndex = 0;
const renderTextureSpy = vi
.spyOn(RenderTexture, "create")
.mockImplementation(() => {
.mockImplementation((config) => {
renderTextureConfigs.push(config);
const id = renderTextureIndex++;
const texture = Object.create(Texture.EMPTY);
Object.defineProperty(texture, "source", {
Expand Down Expand Up @@ -865,6 +867,11 @@ describe("runReplaceAnimation", () => {
expect(maskFilterDestroyIndex).toBeLessThan(
destroyOrder.indexOf("renderTexture:2"),
);
expect(renderTextureConfigs).toEqual([
expect.objectContaining({ resolution: 1 }),
expect.objectContaining({ resolution: 1 }),
expect.objectContaining({ resolution: 1 }),
]);
} finally {
renderTextureSpy.mockRestore();
filterSpy.mockRestore();
Expand Down
41 changes: 41 additions & 0 deletions spec/elements/addTextRevealing.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ vi.mock(
"../../src/plugins/elements/text-revealing/textRevealingRuntime.js",
() => ({
runTextReveal: mocks.runTextReveal,
shouldRenderTextRevealImmediately: (element) =>
element?.revealEffect === "none" || (element?.speed ?? 50) >= 100,
}),
);

Expand Down Expand Up @@ -65,4 +67,43 @@ describe("addTextRevealing", () => {
}),
);
});

it("renders immediately at max speed without queueing deferred reveal work", async () => {
const parent = new Container();
const renderContext = createRenderContext({ suppressAnimations: true });

await addTextRevealing({
parent,
element: {
id: "line-1",
type: "text-revealing",
x: 0,
y: 0,
alpha: 1,
speed: 100,
revealEffect: "typewriter",
content: [],
},
animationBus: { dispatch: vi.fn() },
renderContext,
completionTracker: {
getVersion: () => 0,
track: vi.fn(),
complete: vi.fn(),
},
zIndex: 0,
signal: new AbortController().signal,
});

expect(mocks.runTextReveal).toHaveBeenCalledTimes(1);
expect(mocks.runTextReveal).toHaveBeenCalledWith(
expect.objectContaining({
playback: "autoplay",
}),
);

flushDeferredMountOperations(renderContext);

expect(mocks.runTextReveal).toHaveBeenCalledTimes(1);
});
});
99 changes: 99 additions & 0 deletions spec/elements/textRevealingRuntime.instant.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Container } from "pixi.js";
import { describe, expect, it, vi } from "vitest";

import { parseTextRevealing } from "../../src/plugins/elements/text-revealing/parseTextRevealing.js";
import { runTextReveal } from "../../src/plugins/elements/text-revealing/textRevealingRuntime.js";

const createCompletionTracker = () => ({
getVersion: () => 0,
track: vi.fn(),
complete: vi.fn(),
});

const getRenderedText = (container) => {
const textParts = [];
const visit = (node) => {
if (typeof node?.text === "string") {
textParts.push(node.text);
}

if (Array.isArray(node?.children)) {
node.children.forEach(visit);
}
};

visit(container);

return textParts.join("");
};

const createElement = (overrides = {}) =>
parseTextRevealing({
state: {
id: "line-1",
type: "text-revealing",
width: 500,
speed: 100,
content: [{ text: "Maximum speed should render immediately." }],
textStyle: {
fontSize: 20,
fontFamily: "Arial",
breakWords: false,
},
...overrides,
},
});

describe("runTextReveal instant speed", () => {
it("renders typewriter text immediately at max speed", async () => {
const container = new Container();
const completionTracker = createCompletionTracker();
const animationBus = { dispatch: vi.fn() };
const element = createElement({
revealEffect: "typewriter",
});

await runTextReveal({
container,
element,
completionTracker,
animationBus,
zIndex: 0,
signal: new AbortController().signal,
playback: "autoplay",
});

expect(getRenderedText(container)).toBe(
"Maximum speed should render immediately.",
);
expect(completionTracker.track).toHaveBeenCalledTimes(1);
expect(completionTracker.complete).toHaveBeenCalledTimes(1);
expect(animationBus.dispatch).not.toHaveBeenCalled();
});

it("renders softWipe text immediately at max speed without dispatching animation work", async () => {
const container = new Container();
const completionTracker = createCompletionTracker();
const animationBus = { dispatch: vi.fn() };
const element = createElement({
revealEffect: "softWipe",
});

await runTextReveal({
container,
element,
completionTracker,
animationBus,
zIndex: 0,
signal: new AbortController().signal,
playback: "autoplay",
});

expect(getRenderedText(container)).toBe(
"Maximum speed should render immediately.",
);
expect(completionTracker.track).toHaveBeenCalledTimes(1);
expect(completionTracker.complete).toHaveBeenCalledTimes(1);
expect(animationBus.dispatch).not.toHaveBeenCalled();
});
});
35 changes: 35 additions & 0 deletions spec/elements/updateTextRevealing.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ vi.mock(
"../../src/plugins/elements/text-revealing/textRevealingRuntime.js",
() => ({
runTextReveal: mocks.runTextReveal,
shouldRenderTextRevealImmediately: (element) =>
element?.revealEffect === "none" || (element?.speed ?? 50) >= 100,
}),
);

Expand Down Expand Up @@ -159,6 +161,39 @@ describe("updateTextRevealing", () => {
);
});

it("renders immediately at max speed without queueing deferred reveal work", async () => {
const parent = new Container();
const child = new Container();
child.label = "line-1";
parent.addChild(child);
const renderContext = createRenderContext({ suppressAnimations: true });

await updateTextRevealing({
parent,
prevElement: createElement(),
nextElement: createElement({
speed: 100,
}),
animations: [],
animationBus: { dispatch: vi.fn() },
renderContext,
completionTracker: createCompletionTracker(),
zIndex: 0,
signal: new AbortController().signal,
});

expect(mocks.runTextReveal).toHaveBeenCalledTimes(1);
expect(mocks.runTextReveal).toHaveBeenCalledWith(
expect.objectContaining({
playback: "autoplay",
}),
);

flushDeferredMountOperations(renderContext);

expect(mocks.runTextReveal).toHaveBeenCalledTimes(1);
});

it("resumes an unchanged in-flight reveal instead of leaving it frozen", async () => {
const parent = new Container();
const child = new Container();
Expand Down
31 changes: 19 additions & 12 deletions src/plugins/animations/replace/runReplaceAnimation.js
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,16 @@ const createMaskChannelWeights = (channel = "red") => {

const OUTPUT_MASK_CHANNEL_WEIGHTS = createMaskChannelWeights("red");

// These render textures are sampled as raw TextureSources in the custom
// replace shader, so they must stay at logical resolution rather than the
// renderer/device resolution.
const createShaderRenderTexture = (width, height) =>
RenderTexture.create({
width,
height,
resolution: 1,
});

const createMaskChannelFilter = (channelWeights, invert) => {
const maskChannelUniforms = new UniformGroup({
uMaskInvert: {
Expand Down Expand Up @@ -599,10 +609,7 @@ const renderMaskTextureToRenderTexture = ({
const maskContainer = new Container();
maskContainer.addChild(maskSprite);

const maskRenderTexture = RenderTexture.create({
width,
height,
});
const maskRenderTexture = createShaderRenderTexture(width, height);
const { filter: maskChannelFilter } = createMaskChannelFilter(
channelWeights,
invert,
Expand Down Expand Up @@ -986,14 +993,14 @@ const createMaskedOverlay = ({
nextRoot.addChild(nextSubject.wrapper);
}

const prevTexture = RenderTexture.create({
width: unionBounds.width,
height: unionBounds.height,
});
const nextTexture = RenderTexture.create({
width: unionBounds.width,
height: unionBounds.height,
});
const prevTexture = createShaderRenderTexture(
unionBounds.width,
unionBounds.height,
);
const nextTexture = createShaderRenderTexture(
unionBounds.width,
unionBounds.height,
);

const overlay = new Container();
overlay.zIndex = zIndex;
Expand Down
10 changes: 8 additions & 2 deletions src/plugins/elements/text-revealing/addTextRevealing.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Container } from "pixi.js";
import { queueDeferredTextRevealAutoplay } from "../renderContext.js";
import { runTextReveal } from "./textRevealingRuntime.js";
import {
runTextReveal,
shouldRenderTextRevealImmediately,
} from "./textRevealingRuntime.js";

/**
* Add text-revealing element to the stage
Expand All @@ -26,7 +29,10 @@ export const addTextRevealing = async ({
if (element.alpha !== undefined) container.alpha = element.alpha;
parent.addChild(container);

if (renderContext?.suppressAnimations && element.revealEffect !== "none") {
if (
renderContext?.suppressAnimations &&
!shouldRenderTextRevealImmediately(element)
) {
await runTextReveal({
container,
element,
Expand Down
Loading
Loading