From 9913751bc6eca3f4d8564c32a0200f2657a0076e Mon Sep 17 00:00:00 2001 From: han4wluc Date: Mon, 13 Apr 2026 10:53:52 +0800 Subject: [PATCH 1/2] Keep background render targets stable across swaps --- ...rState.backgroundTransitionTargets.test.js | 146 ++++++++++++++++++ spec/system/constructRenderState.spec.yaml | 8 +- .../renderState/addBackgroundOrCg.spec.yaml | 46 +++--- src/stores/constructRenderState.js | 64 +++++++- 4 files changed, 229 insertions(+), 35 deletions(-) create mode 100644 spec/constructRenderState.backgroundTransitionTargets.test.js diff --git a/spec/constructRenderState.backgroundTransitionTargets.test.js b/spec/constructRenderState.backgroundTransitionTargets.test.js new file mode 100644 index 00000000..9740a058 --- /dev/null +++ b/spec/constructRenderState.backgroundTransitionTargets.test.js @@ -0,0 +1,146 @@ +import { describe, expect, it } from "vitest"; +import { constructPresentationState } from "../src/stores/constructPresentationState.js"; +import { constructRenderState } from "../src/stores/constructRenderState.js"; +import { normalizePersistentPresentationState } from "../src/util.js"; + +const createResources = () => ({ + images: { + bg1: { + fileId: "bg-old.png", + width: 1920, + height: 1080, + }, + bg2: { + fileId: "bg-new.png", + width: 1920, + height: 1080, + }, + }, + videos: {}, + layouts: { + bgLayout: { + elements: [], + }, + }, + animations: { + maskTransition: { + type: "transition", + prev: { + tween: { + alpha: { + initialValue: 1, + keyframes: [{ duration: 1000, value: 0 }], + }, + }, + }, + next: { + tween: { + alpha: { + initialValue: 0, + keyframes: [{ duration: 1000, value: 1 }], + }, + }, + }, + }, + }, + transforms: {}, + characters: {}, + controls: {}, + colors: {}, + fonts: {}, + sectionTransitions: {}, + sounds: {}, + sprites: {}, + spritesheets: {}, + textStyles: {}, + variables: {}, +}); + +const createPresentationPair = (nextBackground) => { + const previousPresentationState = normalizePersistentPresentationState( + constructPresentationState([ + { + background: { + resourceId: "bg1", + }, + }, + ]), + ); + + const presentationState = constructPresentationState([ + previousPresentationState, + { + background: nextBackground, + }, + ]); + + return { + previousPresentationState, + presentationState, + }; +}; + +describe("constructRenderState background transition targets", () => { + it("keeps a stable background target id for same-type background swaps", () => { + const resources = createResources(); + const { previousPresentationState, presentationState } = + createPresentationPair({ + resourceId: "bg2", + animations: { + resourceId: "maskTransition", + }, + }); + + const renderState = constructRenderState({ + presentationState, + previousPresentationState, + resources, + }); + + const story = renderState.elements.find((element) => element.id === "story"); + expect(story.children).toContainEqual( + expect.objectContaining({ + id: "bg-cg-background-sprite", + type: "sprite", + src: "bg-new.png", + }), + ); + expect(renderState.animations).toEqual([ + expect.objectContaining({ + id: "bg-cg-animation-transition", + type: "transition", + targetId: "bg-cg-background-sprite", + }), + ]); + }); + + it("falls back to separate transition targets when background render kinds change", () => { + const resources = createResources(); + const { previousPresentationState, presentationState } = + createPresentationPair({ + resourceId: "bgLayout", + animations: { + resourceId: "maskTransition", + }, + }); + + const renderState = constructRenderState({ + presentationState, + previousPresentationState, + resources, + }); + + expect(renderState.animations).toEqual([ + expect.objectContaining({ + id: "bg-cg-animation-out", + type: "transition", + targetId: "bg-cg-background-sprite", + }), + expect.objectContaining({ + id: "bg-cg-animation-in", + type: "transition", + targetId: "bg-cg-background-container", + }), + ]); + }); +}); diff --git a/spec/system/constructRenderState.spec.yaml b/spec/system/constructRenderState.spec.yaml index 0ee9d4f1..6d0705bd 100644 --- a/spec/system/constructRenderState.spec.yaml +++ b/spec/system/constructRenderState.spec.yaml @@ -23,7 +23,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-mainBackground" + - id: "bg-cg-background-container" type: "container" children: - type: "container" @@ -79,7 +79,7 @@ out: y: 0 width: 100 height: 100 - - id: "bg-cg-bgImage" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 @@ -131,12 +131,12 @@ out: x: 0 y: 0 children: - - id: "bg-cg-oldBg" + - id: "bg-cg-background-container" type: "container" children: - type: "container" id: "old-container" - - id: "bg-cg-newBg" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 diff --git a/spec/system/renderState/addBackgroundOrCg.spec.yaml b/spec/system/renderState/addBackgroundOrCg.spec.yaml index 858f4ef6..90af41c5 100644 --- a/spec/system/renderState/addBackgroundOrCg.spec.yaml +++ b/spec/system/renderState/addBackgroundOrCg.spec.yaml @@ -30,7 +30,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-bg1" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 @@ -78,7 +78,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-bg1" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0 @@ -152,7 +152,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-bg1" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 @@ -168,7 +168,7 @@ out: animations: - id: "bg-cg-animation-in" type: "transition" - targetId: "bg-cg-bg1" + targetId: "bg-cg-background-sprite" next: tween: alpha: @@ -246,7 +246,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-bg1" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 @@ -314,7 +314,7 @@ out: src: "old.jpg" width: 1920 height: 1080 - - id: "bg-cg-bg1" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 @@ -330,7 +330,7 @@ out: animations: - id: "bg-cg-animation-update" type: "update" - targetId: "bg-cg-bg1" + targetId: "bg-cg-background-sprite" tween: alpha: initialValue: 1 @@ -507,7 +507,7 @@ out: - id: "existingChild" type: "text" content: "Hello" - - id: "bg-cg-bg2" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 @@ -525,7 +525,7 @@ out: type: "existing" - id: "bg-cg-animation-in" type: "transition" - targetId: "bg-cg-bg2" + targetId: "bg-cg-background-sprite" next: tween: x: @@ -575,7 +575,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-bgLayout" + - id: "bg-cg-background-container" type: "container" children: - id: "bg-label" @@ -631,7 +631,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-bgLayout" + - id: "bg-cg-background-container" type: "container" x: 400 y: 250 @@ -679,7 +679,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-coloredLayout" + - id: "bg-cg-background-container" type: "container" children: - id: "bg-panel" @@ -720,7 +720,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-imageLayout" + - id: "bg-cg-background-container" type: "container" children: - id: "bg-image" @@ -767,7 +767,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-bg1" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 @@ -783,7 +783,7 @@ out: animations: - id: "bg-cg-animation-in" type: "transition" - targetId: "bg-cg-bg1" + targetId: "bg-cg-background-sprite" next: tween: alpha: @@ -836,7 +836,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-bg1" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 @@ -852,7 +852,7 @@ out: animations: - id: "bg-cg-animation-in" type: "transition" - targetId: "bg-cg-bg1" + targetId: "bg-cg-background-sprite" prev: tween: alpha: @@ -914,7 +914,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-bg1" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 @@ -930,7 +930,7 @@ out: animations: - id: "bg-cg-animation-transition" type: "transition" - targetId: "bg-cg-bg1" + targetId: "bg-cg-background-sprite" prev: tween: alpha: @@ -985,7 +985,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-bg1" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 @@ -1001,7 +1001,7 @@ out: animations: - id: "bg-cg-animation-update" type: "update" - targetId: "bg-cg-bg1" + targetId: "bg-cg-background-sprite" tween: alpha: initialValue: 0.2 @@ -1045,7 +1045,7 @@ out: x: 0 y: 0 children: - - id: "bg-cg-bg1" + - id: "bg-cg-background-sprite" type: "sprite" alpha: 1 anchorX: 0.5 @@ -1061,7 +1061,7 @@ out: animations: - id: "bg-cg-animation-update" type: "update" - targetId: "bg-cg-bg1" + targetId: "bg-cg-background-sprite" tween: alpha: initialValue: 0 diff --git a/src/stores/constructRenderState.js b/src/stores/constructRenderState.js index c00b00aa..59c5ed30 100644 --- a/src/stores/constructRenderState.js +++ b/src/stores/constructRenderState.js @@ -408,6 +408,38 @@ const getBackgroundTransform = (resources, background = {}) => { return transform; }; +const resolveBackgroundKind = (resources = {}, resourceId) => { + if (!resourceId) { + return undefined; + } + + if (resources.images?.[resourceId]) { + return "sprite"; + } + + if (resources.videos?.[resourceId]) { + return "video"; + } + + if (resources.layouts?.[resourceId]) { + return "container"; + } + + return undefined; +}; + +const resolveBackgroundTargetId = ({ resourceId, kind }) => { + if (!resourceId) { + return undefined; + } + + if (kind) { + return `bg-cg-background-${kind}`; + } + + return `bg-cg-${resourceId}`; +}; + const getTextStyleResources = (resources = {}) => resources.textStyles || {}; const getImageResources = (resources = {}) => resources.images || {}; const getColorResources = (resources = {}) => resources.colors || {}; @@ -1374,6 +1406,14 @@ export const addBackgroundOrCg = ( (presentationState.background.animations ? previousBackgroundResourceId : undefined); + const previousBackgroundKind = resolveBackgroundKind( + resources, + previousBackgroundResourceId, + ); + const currentBackgroundKind = resolveBackgroundKind( + resources, + currentBackgroundResourceId, + ); if (currentBackgroundResourceId) { const { images = {}, videos = {} } = resources; @@ -1393,7 +1433,10 @@ export const addBackgroundOrCg = ( ...authoredBackgroundTransform, }; const element = { - id: `bg-cg-${currentBackgroundResourceId}`, + id: resolveBackgroundTargetId({ + resourceId: currentBackgroundResourceId, + kind: isVideo ? "video" : "sprite", + }), type: isVideo ? "video" : "sprite", x: backgroundTransform.x, y: backgroundTransform.y, @@ -1422,7 +1465,10 @@ export const addBackgroundOrCg = ( const layout = layouts[currentBackgroundResourceId]; if (layout) { const bgContainer = { - id: `bg-cg-${currentBackgroundResourceId}`, + id: resolveBackgroundTargetId({ + resourceId: currentBackgroundResourceId, + kind: "container", + }), type: "container", children: layout.elements, }; @@ -1477,12 +1523,14 @@ export const addBackgroundOrCg = ( resources, previousResourceId: previousBackgroundResourceId, currentResourceId: currentBackgroundResourceId, - previousTargetId: previousBackgroundResourceId - ? `bg-cg-${previousBackgroundResourceId}` - : undefined, - currentTargetId: currentBackgroundResourceId - ? `bg-cg-${currentBackgroundResourceId}` - : undefined, + previousTargetId: resolveBackgroundTargetId({ + resourceId: previousBackgroundResourceId, + kind: previousBackgroundKind, + }), + currentTargetId: resolveBackgroundTargetId({ + resourceId: currentBackgroundResourceId, + kind: currentBackgroundKind, + }), animationPath: "background.animations", idPrefix: "bg-cg", allowIncomingUpdateFallback: true, From 2032228751e581f236839c2c6af92056fd328aa5 Mon Sep 17 00:00:00 2001 From: han4wluc Date: Mon, 13 Apr 2026 10:58:58 +0800 Subject: [PATCH 2/2] Bump version to 1.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c88a67e2..49f62f5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "route-engine-js", - "version": "1.0.1", + "version": "1.0.2", "description": "A lightweight Visual Novel engine built in JavaScript for creating interactive narrative games with branching storylines", "repository": { "type": "git",