diff --git a/package.json b/package.json index 7960a341..c24b82dd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/playground/pages/docs/nodes/text-revealing.md b/playground/pages/docs/nodes/text-revealing.md index 13862aaa..4be17ac9 100644 --- a/playground/pages/docs/nodes/text-revealing.md +++ b/playground/pages/docs/nodes/text-revealing.md @@ -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. | @@ -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. diff --git a/spec/animations/runReplaceAnimation.spec.js b/spec/animations/runReplaceAnimation.spec.js index da51084a..f5745ca0 100644 --- a/spec/animations/runReplaceAnimation.spec.js +++ b/spec/animations/runReplaceAnimation.spec.js @@ -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", { @@ -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(); diff --git a/spec/elements/addTextRevealing.spec.js b/spec/elements/addTextRevealing.spec.js index 3bab2592..183a671e 100644 --- a/spec/elements/addTextRevealing.spec.js +++ b/spec/elements/addTextRevealing.spec.js @@ -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, }), ); @@ -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); + }); }); diff --git a/spec/elements/textRevealingRuntime.instant.spec.js b/spec/elements/textRevealingRuntime.instant.spec.js new file mode 100644 index 00000000..a8e5957f --- /dev/null +++ b/spec/elements/textRevealingRuntime.instant.spec.js @@ -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(); + }); +}); diff --git a/spec/elements/updateTextRevealing.spec.js b/spec/elements/updateTextRevealing.spec.js index 8027dd7a..abfe95da 100644 --- a/spec/elements/updateTextRevealing.spec.js +++ b/spec/elements/updateTextRevealing.spec.js @@ -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, }), ); @@ -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(); diff --git a/src/plugins/animations/replace/runReplaceAnimation.js b/src/plugins/animations/replace/runReplaceAnimation.js index e67f9555..53667e59 100644 --- a/src/plugins/animations/replace/runReplaceAnimation.js +++ b/src/plugins/animations/replace/runReplaceAnimation.js @@ -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: { @@ -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, @@ -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; diff --git a/src/plugins/elements/text-revealing/addTextRevealing.js b/src/plugins/elements/text-revealing/addTextRevealing.js index 650c9fd0..e7d20f46 100644 --- a/src/plugins/elements/text-revealing/addTextRevealing.js +++ b/src/plugins/elements/text-revealing/addTextRevealing.js @@ -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 @@ -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, diff --git a/src/plugins/elements/text-revealing/textRevealingRuntime.js b/src/plugins/elements/text-revealing/textRevealingRuntime.js index 10154405..5eef424c 100644 --- a/src/plugins/elements/text-revealing/textRevealingRuntime.js +++ b/src/plugins/elements/text-revealing/textRevealingRuntime.js @@ -15,9 +15,51 @@ const TEXT_REVEAL_SNAPSHOT = Symbol("textRevealSnapshot"); const MIN_SOFT_WIPE_EDGE = 18; const MAX_SOFT_WIPE_EDGE = 64; const SOFT_WIPE_EDGE_MULTIPLIER = 1.25; +const DEFAULT_TEXT_REVEAL_SPEED = 50; +const MIN_TEXT_REVEAL_SPEED = 0; +const MAX_TEXT_REVEAL_SPEED = 100; +const MAX_ANIMATED_TEXT_REVEAL_SPEED = MAX_TEXT_REVEAL_SPEED - 1; +const MIN_TEXT_REVEAL_RATE = 10; +const MAX_TEXT_REVEAL_RATE = 120; +const TEXT_REVEAL_RATE_CURVE = 0.9; + +const clampTextRevealSpeed = (speed = DEFAULT_TEXT_REVEAL_SPEED) => { + if (typeof speed !== "number" || !Number.isFinite(speed)) { + return DEFAULT_TEXT_REVEAL_SPEED; + } + + return Math.max( + MIN_TEXT_REVEAL_SPEED, + Math.min(MAX_TEXT_REVEAL_SPEED, speed), + ); +}; + +export const isInstantTextRevealSpeed = (speed) => + clampTextRevealSpeed(speed) >= MAX_TEXT_REVEAL_SPEED; + +const getEffectiveSpeed = (speed) => { + const clampedSpeed = Math.min( + clampTextRevealSpeed(speed), + MAX_ANIMATED_TEXT_REVEAL_SPEED, + ); + const normalizedSpeed = + MAX_ANIMATED_TEXT_REVEAL_SPEED > 0 + ? clampedSpeed / MAX_ANIMATED_TEXT_REVEAL_SPEED + : 0; + const curvedSpeed = normalizedSpeed ** TEXT_REVEAL_RATE_CURVE; + + return ( + MIN_TEXT_REVEAL_RATE * + (MAX_TEXT_REVEAL_RATE / MIN_TEXT_REVEAL_RATE) ** curvedSpeed + ); +}; + +const getTextRevealSnapshotMode = (element) => + element?.revealEffect === "softWipe" ? "softWipe" : "typewriter"; -const getEffectiveSpeed = (speed) => - typeof speed === "number" && speed > 0 ? speed : 1; +export const shouldRenderTextRevealImmediately = (element) => + element?.revealEffect === "none" || + isInstantTextRevealSpeed(element?.speed ?? DEFAULT_TEXT_REVEAL_SPEED); const createIndicatorSprite = (element) => { let indicatorSprite = new Sprite(Texture.EMPTY); @@ -676,6 +718,7 @@ export const runTextReveal = async ({ return; } + const renderImmediately = shouldRenderTextRevealImmediately(element); const resumableSnapshot = playback === "resume" ? getResumableTypewriterSnapshot(container) : null; @@ -694,15 +737,19 @@ export const runTextReveal = async ({ try { if (playback === "paused-initial") { - if (element.revealEffect !== "none") { + if (!renderImmediately) { setTextRevealSnapshot(container, { - mode: "typewriter", + mode: getTextRevealSnapshotMode(element), revealedCharacters: 0, completed: false, }); } - if (element.revealEffect === "none") { + if (renderImmediately) { + setTextRevealSnapshot(container, { + mode: "none", + completed: true, + }); runNoneReveal({ contentContainer, indicatorSprite, element }); } else { runPausedInitialReveal({ indicatorSprite, element }); @@ -710,7 +757,18 @@ export const runTextReveal = async ({ return; } - if (element.revealEffect === "softWipe") { + const stateVersion = completionTracker.getVersion(); + let completed = false; + + if (renderImmediately) { + completionTracker.track(stateVersion); + setTextRevealSnapshot(container, { + mode: "none", + completed: true, + }); + runNoneReveal({ contentContainer, indicatorSprite, element }); + completed = true; + } else if (element.revealEffect === "softWipe") { setTextRevealSnapshot(container, { mode: "softWipe", completed: false, @@ -726,28 +784,13 @@ export const runTextReveal = async ({ }); if (!dispatched && !signal?.aborted && !container.destroyed) { - const stateVersion = completionTracker.getVersion(); - completionTracker.track(stateVersion); - completionTracker.complete(stateVersion); + completed = true; + } else { + return; } - - return; - } - - const stateVersion = completionTracker.getVersion(); - let completed = false; - - completionTracker.track(stateVersion); - - if (element.revealEffect === "none") { - setTextRevealSnapshot(container, { - mode: "none", - completed: true, - }); - runNoneReveal({ contentContainer, indicatorSprite, element }); - completed = true; } else { + completionTracker.track(stateVersion); const nextSnapshot = setTextRevealSnapshot(container, { mode: "typewriter", revealedCharacters: resumableSnapshot?.revealedCharacters ?? 0, diff --git a/src/plugins/elements/text-revealing/updateTextRevealing.js b/src/plugins/elements/text-revealing/updateTextRevealing.js index 9f56e80e..d9903d4f 100644 --- a/src/plugins/elements/text-revealing/updateTextRevealing.js +++ b/src/plugins/elements/text-revealing/updateTextRevealing.js @@ -1,6 +1,9 @@ import { dispatchLiveAnimations } from "../../animations/planAnimations.js"; import { queueDeferredTextRevealAutoplay } from "../renderContext.js"; -import { runTextReveal } from "./textRevealingRuntime.js"; +import { + runTextReveal, + shouldRenderTextRevealImmediately, +} from "./textRevealingRuntime.js"; const getRevealIdentity = (element = {}) => JSON.stringify({ @@ -49,7 +52,7 @@ export const updateTextRevealing = async ({ if (!shouldRestartReveal(prevElement, element)) { if ( renderContext?.suppressAnimations !== true && - element.revealEffect !== "none" + !shouldRenderTextRevealImmediately(element) ) { await runTextReveal({ container: textRevealingElement, @@ -67,7 +70,7 @@ export const updateTextRevealing = async ({ if ( renderContext?.suppressAnimations === true && - element.revealEffect !== "none" + !shouldRenderTextRevealImmediately(element) ) { await runTextReveal({ container: textRevealingElement, diff --git a/src/schemas/elements/text-revealing.computed.yaml b/src/schemas/elements/text-revealing.computed.yaml index 79c650bf..a9d76b7c 100644 --- a/src/schemas/elements/text-revealing.computed.yaml +++ b/src/schemas/elements/text-revealing.computed.yaml @@ -151,7 +151,7 @@ properties: "$ref": "#/$def/textStyle" speed: type: number - description: Animation speed for text revealing + description: Animation speed on a curved 0-100 scale; 100 renders instantly default: 50 revealEffect: type: string diff --git a/src/schemas/elements/text-revealing.element.yaml b/src/schemas/elements/text-revealing.element.yaml index db834c17..45a1ffa8 100644 --- a/src/schemas/elements/text-revealing.element.yaml +++ b/src/schemas/elements/text-revealing.element.yaml @@ -125,7 +125,7 @@ properties: default: 12 speed: type: number - description: Animation speed for text revealing + description: Animation speed on a curved 0-100 scale; 100 renders instantly default: 50 revealEffect: type: string diff --git a/src/types.js b/src/types.js index dcffb875..cf82fe19 100644 --- a/src/types.js +++ b/src/types.js @@ -444,7 +444,7 @@ * @property {number} [width] - Width constraint for text wrapping * @property {number} alpha - Opacity/transparency (0-1) * @property {Object} textStyle - Default text style - * @property {number} [speed=50] - Animation speed (default: 50) + * @property {number} [speed=50] - Animation speed on a curved 0-100 scale; 100 renders instantly * @property {Object} complete - Complete event * @property {Object} [indicator] - Settings for the text continuation indicator * @property {Object} [indicator.revealing] - Settings for the revealing state indicator diff --git a/vt/reference/rendercompleteevent/render-complete-text-revealing-speed-100-01.webp b/vt/reference/rendercompleteevent/render-complete-text-revealing-speed-100-01.webp new file mode 100644 index 00000000..389f193a --- /dev/null +++ b/vt/reference/rendercompleteevent/render-complete-text-revealing-speed-100-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5072b947aff5fddb768449530a71935eb72b1c0d1778d8d64061e799e952c2c4 +size 1754 diff --git a/vt/reference/rendercompleteevent/render-complete-text-revealing-speed-100-02.webp b/vt/reference/rendercompleteevent/render-complete-text-revealing-speed-100-02.webp new file mode 100644 index 00000000..6d41f05d --- /dev/null +++ b/vt/reference/rendercompleteevent/render-complete-text-revealing-speed-100-02.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3167433125cd131bf395455dfe1d2d9798419a0763da34f3a3e85d9d0404466c +size 20786 diff --git a/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-01.webp b/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-01.webp new file mode 100644 index 00000000..113f181b --- /dev/null +++ b/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e42a3e8889072b30aa879d48b2ada64e3b50794d165160b6ccfc282ab85b5517 +size 1700 diff --git a/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-02.webp b/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-02.webp new file mode 100644 index 00000000..113f181b --- /dev/null +++ b/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-02.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e42a3e8889072b30aa879d48b2ada64e3b50794d165160b6ccfc282ab85b5517 +size 1700 diff --git a/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-03.webp b/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-03.webp new file mode 100644 index 00000000..b6322ddd --- /dev/null +++ b/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-03.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc698bea504effb021e3ee335fdad7c7d91c6ff1c099621ce91a90fa2a390d0b +size 24694 diff --git a/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-04.webp b/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-04.webp new file mode 100644 index 00000000..8f1f9999 --- /dev/null +++ b/vt/reference/textrevealing/text-revealing-soft-wipe-speed-scale-04.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6795d6cae4713cb13af87f200c1647d0ecdc1a3f8eb8e01e21c1f80fcd3ad7c +size 34358 diff --git a/vt/reference/textrevealing/text-revealing-speed-scale-01.webp b/vt/reference/textrevealing/text-revealing-speed-scale-01.webp new file mode 100644 index 00000000..113f181b --- /dev/null +++ b/vt/reference/textrevealing/text-revealing-speed-scale-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e42a3e8889072b30aa879d48b2ada64e3b50794d165160b6ccfc282ab85b5517 +size 1700 diff --git a/vt/reference/textrevealing/text-revealing-speed-scale-02.webp b/vt/reference/textrevealing/text-revealing-speed-scale-02.webp new file mode 100644 index 00000000..113f181b --- /dev/null +++ b/vt/reference/textrevealing/text-revealing-speed-scale-02.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e42a3e8889072b30aa879d48b2ada64e3b50794d165160b6ccfc282ab85b5517 +size 1700 diff --git a/vt/reference/textrevealing/text-revealing-speed-scale-03.webp b/vt/reference/textrevealing/text-revealing-speed-scale-03.webp new file mode 100644 index 00000000..762ef666 --- /dev/null +++ b/vt/reference/textrevealing/text-revealing-speed-scale-03.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb0a878dd0aa2899fb610c23a068111de77ea69dddeb9a5450111bae815dc1e4 +size 27356 diff --git a/vt/reference/textrevealing/text-revealing-speed-scale-04.webp b/vt/reference/textrevealing/text-revealing-speed-scale-04.webp new file mode 100644 index 00000000..da34377f --- /dev/null +++ b/vt/reference/textrevealing/text-revealing-speed-scale-04.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:380acd93901af5d597e144055fda3db44922dd939af07d42e6f673311b8a97bd +size 41194 diff --git a/vt/specs/rendercompleteevent/render-complete-text-revealing-speed-100.yaml b/vt/specs/rendercompleteevent/render-complete-text-revealing-speed-100.yaml new file mode 100644 index 00000000..ae522b1c --- /dev/null +++ b/vt/specs/rendercompleteevent/render-complete-text-revealing-speed-100.yaml @@ -0,0 +1,119 @@ +--- +title: Render Complete Event - Text Revealing Speed 100 +description: Asserts renderComplete fires once for max-speed text-revealing, even when revealEffect stays animated. +specs: + - renderComplete should fire once with aborted=false for immediate max-speed text reveals + - typewriter speed 100 should render fully without waiting for per-character animation + - softWipe speed 100 should render fully without dispatching wipe animation work +steps: + - action: customEvent + name: clearEvents + - action: keypress + key: "n" + - action: wait + ms: 100 + - action: screenshot + - action: assert + type: js + fn: vtAssert.payloadDeepEquals + args: + - renderComplete + - all + value: + - id: text-revealing-speed-100 + aborted: false +--- +states: + - id: baseline + elements: + - id: baseline-rect + type: rect + x: 80 + y: 80 + width: 220 + height: 120 + fill: "#4D4D4D" + - id: text-revealing-speed-100 + elements: + - id: speed-100-bg + type: rect + x: 32 + y: 32 + width: 1216 + height: 640 + fill: "#171717" + - id: speed-100-title + type: text + x: 60 + y: 48 + content: "speed 100 should render immediately" + textStyle: + fontSize: 24 + fill: "#FFFFFF" + fontFamily: Arial + - id: speed-100-typewriter-label + type: text + x: 60 + y: 112 + content: "typewriter" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: speed-100-typewriter + type: text-revealing + x: 220 + y: 108 + width: 920 + speed: 100 + revealEffect: typewriter + indicator: + revealing: + src: circle-red + width: 12 + height: 12 + complete: + src: circle-green + width: 12 + height: 12 + offset: 12 + content: + - text: "Maximum speed should skip the typewriter effect and draw the full sentence at once." + textStyle: + fontSize: 26 + fill: "#FFFFFF" + fontFamily: Arial + lineHeight: 1.3 + - id: speed-100-softwipe-label + type: text + x: 60 + y: 240 + content: "softWipe" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: speed-100-softwipe + type: text-revealing + x: 220 + y: 236 + width: 920 + speed: 100 + revealEffect: softWipe + indicator: + revealing: + src: circle-red + width: 12 + height: 12 + complete: + src: circle-green + width: 12 + height: 12 + offset: 12 + content: + - text: "Maximum speed should also bypass the soft wipe and present the final layout immediately." + textStyle: + fontSize: 26 + fill: "#D9D9D9" + fontFamily: Arial + lineHeight: 1.3 diff --git a/vt/specs/textrevealing/text-revealing-soft-wipe-speed-scale.yaml b/vt/specs/textrevealing/text-revealing-soft-wipe-speed-scale.yaml new file mode 100644 index 00000000..eaf34c6a --- /dev/null +++ b/vt/specs/textrevealing/text-revealing-soft-wipe-speed-scale.yaml @@ -0,0 +1,190 @@ +--- +title: Text Revealing - Soft Wipe Speed Scale +description: Compares softWipe reveal progress across low, mid, high, and instant speed settings on the shared text-reveal speed scale. +specs: + - lower speeds should leave more of the line masked after the same animation time + - upper-range speeds should expose much more of the line than the lower half of the scale + - speed 100 should bypass the wipe and render the full line immediately +steps: + - action: screenshot + - action: keypress + key: "n" + - action: customEvent + name: "snapShotKeyFrame" + detail: + deltaMS: 450 + - action: screenshot + - action: customEvent + name: "snapShotKeyFrame" + detail: + deltaMS: 800 + - action: screenshot +--- +states: + - id: text-revealing-soft-wipe-speed-scale-blank + elements: [] + - id: text-revealing-soft-wipe-speed-scale-grid + elements: + - id: softwipe-scale-bg + type: rect + x: 24 + y: 24 + width: 1232 + height: 648 + fill: "#171717" + - id: softwipe-scale-title + type: text + x: 52 + y: 44 + content: "Soft wipe speed scale" + textStyle: + fontSize: 24 + fill: "#FFFFFF" + fontFamily: Arial + - id: softwipe-scale-caption + type: text + x: 52 + y: 78 + content: "All rows start together; only speed changes." + textStyle: + fontSize: 14 + fill: "#A6A6A6" + fontFamily: Arial + - id: softwipe-speed-label-0 + type: text + x: 52 + y: 132 + content: "speed 0" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: softwipe-speed-line-0 + type: text-revealing + x: 180 + y: 128 + width: 980 + speed: 0 + revealEffect: softWipe + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25 + - id: softwipe-speed-label-25 + type: text + x: 52 + y: 204 + content: "speed 25" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: softwipe-speed-line-25 + type: text-revealing + x: 180 + y: 200 + width: 980 + speed: 25 + revealEffect: softWipe + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25 + - id: softwipe-speed-label-50 + type: text + x: 52 + y: 276 + content: "speed 50" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: softwipe-speed-line-50 + type: text-revealing + x: 180 + y: 272 + width: 980 + speed: 50 + revealEffect: softWipe + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25 + - id: softwipe-speed-label-75 + type: text + x: 52 + y: 348 + content: "speed 75" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: softwipe-speed-line-75 + type: text-revealing + x: 180 + y: 344 + width: 980 + speed: 75 + revealEffect: softWipe + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25 + - id: softwipe-speed-label-99 + type: text + x: 52 + y: 420 + content: "speed 99" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: softwipe-speed-line-99 + type: text-revealing + x: 180 + y: 416 + width: 980 + speed: 99 + revealEffect: softWipe + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25 + - id: softwipe-speed-label-100 + type: text + x: 52 + y: 492 + content: "speed 100" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: softwipe-speed-line-100 + type: text-revealing + x: 180 + y: 488 + width: 980 + speed: 100 + revealEffect: softWipe + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25 diff --git a/vt/specs/textrevealing/text-revealing-speed-scale.yaml b/vt/specs/textrevealing/text-revealing-speed-scale.yaml new file mode 100644 index 00000000..a446f4be --- /dev/null +++ b/vt/specs/textrevealing/text-revealing-speed-scale.yaml @@ -0,0 +1,194 @@ +--- +title: Text Revealing - Speed Scale +description: Compares typewriter reveal progress across low, mid, high, and instant speed settings on the curved text-reveal scale. +specs: + - lower speeds should reveal fewer characters after the same elapsed time + - upper-range speeds should progress much faster than the low half of the scale + - speed 100 should render complete text immediately without any reveal animation +steps: + - action: screenshot + - action: keypress + key: "n" + - action: wait + ms: 450 + - action: customEvent + name: "snapShotKeyFrame" + detail: + deltaMS: 16 + - action: screenshot + - action: wait + ms: 1150 + - action: customEvent + name: "snapShotKeyFrame" + detail: + deltaMS: 16 + - action: screenshot +--- +states: + - id: text-revealing-speed-scale-blank + elements: [] + - id: text-revealing-speed-scale-grid + elements: + - id: scale-bg + type: rect + x: 24 + y: 24 + width: 1232 + height: 648 + fill: "#171717" + - id: scale-title + type: text + x: 52 + y: 44 + content: "Curved text reveal speed scale" + textStyle: + fontSize: 24 + fill: "#FFFFFF" + fontFamily: Arial + - id: scale-caption + type: text + x: 52 + y: 78 + content: "All rows start together; only speed changes." + textStyle: + fontSize: 14 + fill: "#A6A6A6" + fontFamily: Arial + - id: speed-label-0 + type: text + x: 52 + y: 132 + content: "speed 0" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: speed-line-0 + type: text-revealing + x: 180 + y: 128 + width: 980 + speed: 0 + revealEffect: typewriter + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25 + - id: speed-label-25 + type: text + x: 52 + y: 204 + content: "speed 25" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: speed-line-25 + type: text-revealing + x: 180 + y: 200 + width: 980 + speed: 25 + revealEffect: typewriter + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25 + - id: speed-label-50 + type: text + x: 52 + y: 276 + content: "speed 50" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: speed-line-50 + type: text-revealing + x: 180 + y: 272 + width: 980 + speed: 50 + revealEffect: typewriter + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25 + - id: speed-label-75 + type: text + x: 52 + y: 348 + content: "speed 75" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: speed-line-75 + type: text-revealing + x: 180 + y: 344 + width: 980 + speed: 75 + revealEffect: typewriter + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25 + - id: speed-label-99 + type: text + x: 52 + y: 420 + content: "speed 99" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: speed-line-99 + type: text-revealing + x: 180 + y: 416 + width: 980 + speed: 99 + revealEffect: typewriter + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25 + - id: speed-label-100 + type: text + x: 52 + y: 492 + content: "speed 100" + textStyle: + fontSize: 18 + fill: "#8FD3FF" + fontFamily: Arial + - id: speed-line-100 + type: text-revealing + x: 180 + y: 488 + width: 980 + speed: 100 + revealEffect: typewriter + content: + - text: "Curved speed mapping keeps the upper range responsive without losing control, so the reveal stays readable for dialogue and captions while higher settings still feel clearly faster in motion." + textStyle: + fontSize: 18 + fill: "#F2F2F2" + fontFamily: Arial + lineHeight: 1.25