From 89746277a519119bf3a58b1faf65f464c5258f36 Mon Sep 17 00:00:00 2001 From: han4wluc Date: Sat, 11 Apr 2026 13:36:09 +0800 Subject: [PATCH 1/2] Allow background transformId and add VT coverage --- package.json | 2 +- spec/projectDataSchema.test.js | 17 ++ spec/system/constructRenderState.spec.yaml | 16 +- .../renderState/addBackgroundOrCg.spec.yaml | 208 ++++++++++++++---- src/schemas/presentationActions.yaml | 3 + src/stores/constructRenderState.js | 59 ++++- .../background/transform-image-01.webp | 3 + .../background/transform-layout-01.webp | 3 + vt/specs/background/transform-image.yaml | 42 ++++ vt/specs/background/transform-layout.yaml | 79 +++++++ 10 files changed, 376 insertions(+), 56 deletions(-) create mode 100644 vt/reference/background/transform-image-01.webp create mode 100644 vt/reference/background/transform-layout-01.webp create mode 100644 vt/specs/background/transform-image.yaml create mode 100644 vt/specs/background/transform-layout.yaml diff --git a/package.json b/package.json index de0c9301..c88a67e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "route-engine-js", - "version": "1.0.0", + "version": "1.0.1", "description": "A lightweight Visual Novel engine built in JavaScript for creating interactive narrative games with branching storylines", "repository": { "type": "git", diff --git a/spec/projectDataSchema.test.js b/spec/projectDataSchema.test.js index 1509e2e2..3f98bc1e 100644 --- a/spec/projectDataSchema.test.js +++ b/spec/projectDataSchema.test.js @@ -11,6 +11,10 @@ const projectDataSchemaId = new URL( "projectData/projectData.yaml", schemaBaseUrl, ).href; +const presentationActionsSchemaId = new URL( + "presentationActions.yaml", + schemaBaseUrl, +).href; const systemActionsSchemaId = new URL("systemActions.yaml", schemaBaseUrl).href; const projectDataSchemaPaths = [ path.join(schemasRoot, "projectData"), @@ -110,6 +114,7 @@ const createValidator = (schemaId) => { }; const validateProjectData = createValidator(projectDataSchemaId); +const validatePresentationActions = createValidator(presentationActionsSchemaId); const validateSystemActions = createValidator(systemActionsSchemaId); const createMinimalProjectData = (overrides = {}) => ({ @@ -147,6 +152,18 @@ describe("projectData schema", () => { expect(validateProjectData.errors).toBeNull(); }); + it("accepts background transformId in presentation actions", () => { + expect( + validatePresentationActions({ + background: { + resourceId: "bg1", + transformId: "centerStage", + }, + }), + ).toBe(true); + expect(validatePresentationActions.errors).toBeNull(); + }); + it("parses all VT YAML specs", () => { const vtSpecsRoot = path.join(repoRoot, "vt", "specs"); const vtSpecPaths = collectYamlFiles(vtSpecsRoot); diff --git a/spec/system/constructRenderState.spec.yaml b/spec/system/constructRenderState.spec.yaml index 180dc2df..0ee9d4f1 100644 --- a/spec/system/constructRenderState.spec.yaml +++ b/spec/system/constructRenderState.spec.yaml @@ -82,13 +82,13 @@ out: - id: "bg-cg-bgImage" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "background.jpg" width: 1920 height: 1080 @@ -139,13 +139,13 @@ out: - id: "bg-cg-newBg" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "new-background.jpg" width: 1920 height: 1080 diff --git a/spec/system/renderState/addBackgroundOrCg.spec.yaml b/spec/system/renderState/addBackgroundOrCg.spec.yaml index f7e4f32f..858f4ef6 100644 --- a/spec/system/renderState/addBackgroundOrCg.spec.yaml +++ b/spec/system/renderState/addBackgroundOrCg.spec.yaml @@ -33,18 +33,88 @@ out: - id: "bg-cg-bg1" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "image1.png" width: 1920 height: 1080 animations: [] --- +case: add background image with transform to story container +in: + - elements: + - id: "story" + type: "container" + x: 0 + y: 0 + children: [] + animations: [] + - presentationState: + background: + resourceId: "bg1" + transformId: "bgTransform" + resources: + images: + bg1: + fileId: "image1.png" + width: 1920 + height: 1080 + transforms: + bgTransform: + x: 200 + y: 300 + anchorX: 0 + anchorY: 1 + scaleX: 1.25 +out: + elements: + - id: "story" + type: "container" + x: 0 + y: 0 + children: + - id: "bg-cg-bg1" + type: "sprite" + alpha: 1 + anchorX: 0 + anchorY: 1 + rotation: 0 + scaleX: 1.25 + scaleY: 1 + x: 200 + y: 300 + src: "image1.png" + width: 1920 + height: 1080 + animations: [] +--- +case: unknown background transformId should throw clear error +in: + - elements: + - id: "story" + type: "container" + x: 0 + y: 0 + children: [] + animations: [] + - presentationState: + background: + resourceId: "bg1" + transformId: "missing" + resources: + images: + bg1: + fileId: "image1.png" + width: 1920 + height: 1080 + transforms: {} +throws: 'Transform "missing" not found for background' +--- case: add background with single transition animation reference in: - elements: @@ -85,13 +155,13 @@ out: - id: "bg-cg-bg1" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "image1.png" width: 1920 height: 1080 @@ -179,13 +249,13 @@ out: - id: "bg-cg-bg1" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "image1.png" width: 1920 height: 1080 @@ -247,13 +317,13 @@ out: - id: "bg-cg-bg1" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "new.jpg" width: 1920 height: 1080 @@ -440,13 +510,13 @@ out: - id: "bg-cg-bg2" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "background.jpg" width: 1920 height: 1080 @@ -520,6 +590,64 @@ out: fill: "#FFFFFF" animations: [] --- +case: add background layout with transform +in: + - elements: + - id: "story" + type: "container" + x: 0 + y: 0 + children: [] + animations: [] + - presentationState: + background: + resourceId: "bgLayout" + transformId: "bgTransform" + resources: + layouts: + bgLayout: + elements: + - id: "bg-panel" + type: "rect" + width: 100 + height: 50 + colorId: "panel" + colors: + panel: + hex: "#000000" + transforms: + bgTransform: + x: 400 + y: 250 + anchorX: 0 + anchorY: 0 + rotation: 12 + scaleX: 0.75 + scaleY: 1.1 +out: + elements: + - id: "story" + type: "container" + x: 0 + y: 0 + children: + - id: "bg-cg-bgLayout" + type: "container" + x: 400 + y: 250 + anchorX: 0 + anchorY: 0 + rotation: 12 + scaleX: 0.75 + scaleY: 1.1 + children: + - id: "bg-panel" + type: "rect" + width: 100 + height: 50 + fill: "#000000" + animations: [] +--- case: resolves colorId in background layout elements in: - elements: @@ -642,13 +770,13 @@ out: - id: "bg-cg-bg1" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "image1.png" width: 1920 height: 1080 @@ -711,13 +839,13 @@ out: - id: "bg-cg-bg1" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "image1.png" width: 1920 height: 1080 @@ -789,13 +917,13 @@ out: - id: "bg-cg-bg1" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "image1.png" width: 1920 height: 1080 @@ -860,13 +988,13 @@ out: - id: "bg-cg-bg1" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "new.jpg" width: 1920 height: 1080 @@ -920,13 +1048,13 @@ out: - id: "bg-cg-bg1" type: "sprite" alpha: 1 - anchorX: 0 - anchorY: 0 + anchorX: 0.5 + anchorY: 0.5 rotation: 0 scaleX: 1 scaleY: 1 - x: 0 - y: 0 + x: 960 + y: 540 src: "image1.png" width: 1920 height: 1080 diff --git a/src/schemas/presentationActions.yaml b/src/schemas/presentationActions.yaml index 8510541b..68d6b784 100644 --- a/src/schemas/presentationActions.yaml +++ b/src/schemas/presentationActions.yaml @@ -34,6 +34,9 @@ properties: resourceId: type: string description: ID of the background resource to display + transformId: + type: string + description: Optional transform resource to position the background animations: $ref: "#/definitions/animationSelection" additionalProperties: false diff --git a/src/stores/constructRenderState.js b/src/stores/constructRenderState.js index d3c08529..4b05710e 100644 --- a/src/stores/constructRenderState.js +++ b/src/stores/constructRenderState.js @@ -393,6 +393,24 @@ const getRequiredVisualTransform = (resources, item) => { return transform; }; +const getBackgroundTransform = ( + resources, + background = {}, +) => { + if (!background?.transformId) { + return undefined; + } + + const transform = resources.transforms?.[background.transformId]; + if (!transform) { + throw new Error( + `Transform "${background.transformId}" not found for background`, + ); + } + + return transform; +}; + const getTextStyleResources = (resources = {}) => resources.textStyles || {}; const getImageResources = (resources = {}) => resources.images || {}; const getColorResources = (resources = {}) => resources.colors || {}; @@ -1328,6 +1346,7 @@ export const addBackgroundOrCg = ( presentationState, previousPresentationState, resources = {}, + screen = { width: 1920, height: 1080 }, isLineCompleted, skipTransitionsAndAnimations, variables, @@ -1346,6 +1365,10 @@ export const addBackgroundOrCg = ( if (!storyContainer) { return state; } + const authoredBackgroundTransform = getBackgroundTransform( + resources, + presentationState.background, + ); const previousBackgroundResourceId = previousPresentationState?.background?.resourceId; @@ -1362,20 +1385,30 @@ export const addBackgroundOrCg = ( videos[currentBackgroundResourceId]; if (background) { const isVideo = videos[currentBackgroundResourceId] !== undefined; + const backgroundTransform = { + x: (screen?.width ?? 1920) / 2, + y: (screen?.height ?? 1080) / 2, + anchorX: 0.5, + anchorY: 0.5, + rotation: 0, + scaleX: 1, + scaleY: 1, + ...authoredBackgroundTransform, + }; const element = { id: `bg-cg-${currentBackgroundResourceId}`, type: isVideo ? "video" : "sprite", - x: 0, - y: 0, + x: backgroundTransform.x, + y: backgroundTransform.y, src: background.fileId, width: background.width, height: background.height, alpha: 1, - anchorX: 0, - anchorY: 0, - rotation: 0, - scaleX: 1, - scaleY: 1, + anchorX: backgroundTransform.anchorX, + anchorY: backgroundTransform.anchorY, + rotation: backgroundTransform.rotation, + scaleX: backgroundTransform.scaleX, + scaleY: backgroundTransform.scaleY, }; if (isVideo) { @@ -1396,6 +1429,18 @@ export const addBackgroundOrCg = ( type: "container", children: layout.elements, }; + if (authoredBackgroundTransform) { + Object.assign(bgContainer, { + x: 0, + y: 0, + anchorX: 0, + anchorY: 0, + rotation: 0, + scaleX: 1, + scaleY: 1, + ...authoredBackgroundTransform, + }); + } const processedContainer = parseAndRender( bgContainer, createLayoutTemplateData({ diff --git a/vt/reference/background/transform-image-01.webp b/vt/reference/background/transform-image-01.webp new file mode 100644 index 00000000..e1b78923 --- /dev/null +++ b/vt/reference/background/transform-image-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83a82e9b27e73e149f72bd1ab265e05f4f13053136ec505ddb6841683c52d34d +size 3822 diff --git a/vt/reference/background/transform-layout-01.webp b/vt/reference/background/transform-layout-01.webp new file mode 100644 index 00000000..fac4e5c1 --- /dev/null +++ b/vt/reference/background/transform-layout-01.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:945a8dffe2f746cce5ab795b5ca548bde938fb86c6d469f106248b2219a5bf42 +size 1950 diff --git a/vt/specs/background/transform-image.yaml b/vt/specs/background/transform-image.yaml new file mode 100644 index 00000000..314b4856 --- /dev/null +++ b/vt/specs/background/transform-image.yaml @@ -0,0 +1,42 @@ +--- +title: Background Transform Image +description: | + Image backgrounds should honor an authored transformId instead of always + using the centered default placement. + + - the image background should render at the authored transform x/y + - the authored transform anchor should override the default 0.5/0.5 anchor +--- +screen: + width: 1920 + height: 1080 + backgroundColor: "#000000" +resources: + images: + bg-card: + fileId: dmni32 + width: 640 + height: 360 + transforms: + top-left-card: + x: 120 + y: 90 + anchorX: 0 + anchorY: 0 + rotation: 0 + scaleX: 1 + scaleY: 1 +story: + initialSceneId: backgroundScene + scenes: + backgroundScene: + name: Background Scene + initialSectionId: main + sections: + main: + lines: + - id: line1 + actions: + background: + resourceId: bg-card + transformId: top-left-card diff --git a/vt/specs/background/transform-layout.yaml b/vt/specs/background/transform-layout.yaml new file mode 100644 index 00000000..894e8fe1 --- /dev/null +++ b/vt/specs/background/transform-layout.yaml @@ -0,0 +1,79 @@ +--- +title: Background Transform Layout +description: | + Layout backgrounds should honor an authored transformId when one is provided. + + - the background layout container should render at the authored transform x/y + - the authored transform anchor should control the layout container position +--- +screen: + width: 1920 + height: 1080 + backgroundColor: "#000000" +resources: + layouts: + bg-layout: + elements: + - id: panel + type: rect + width: 340 + height: 180 + colorId: fg30 + - id: panel-title + type: text + x: 24 + y: 26 + content: Transform + textStyleId: titleStyle + - id: panel-subtitle + type: text + x: 24 + y: 88 + content: Background + textStyleId: bodyStyle + transforms: + lower-right-panel: + x: 1820 + y: 1020 + anchorX: 1 + anchorY: 1 + rotation: 0 + scaleX: 1 + scaleY: 1 + fonts: + fontDefault: + fileId: Arial + colors: + fg100: + hex: "#FFFFFF" + fg30: + hex: "#4D4D4D" + textStyles: + titleStyle: + fontId: fontDefault + colorId: fg100 + fontSize: 34 + fontWeight: "400" + fontStyle: normal + lineHeight: 1.2 + bodyStyle: + fontId: fontDefault + colorId: fg100 + fontSize: 24 + fontWeight: "400" + fontStyle: normal + lineHeight: 1.2 +story: + initialSceneId: backgroundScene + scenes: + backgroundScene: + name: Background Scene + initialSectionId: main + sections: + main: + lines: + - id: line1 + actions: + background: + resourceId: bg-layout + transformId: lower-right-panel From b6728267f4fa8b333d1841440012bb4582dfc841 Mon Sep 17 00:00:00 2001 From: han4wluc Date: Sat, 11 Apr 2026 13:36:30 +0800 Subject: [PATCH 2/2] Format constructRenderState --- src/stores/constructRenderState.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/stores/constructRenderState.js b/src/stores/constructRenderState.js index 4b05710e..c00b00aa 100644 --- a/src/stores/constructRenderState.js +++ b/src/stores/constructRenderState.js @@ -393,10 +393,7 @@ const getRequiredVisualTransform = (resources, item) => { return transform; }; -const getBackgroundTransform = ( - resources, - background = {}, -) => { +const getBackgroundTransform = (resources, background = {}) => { if (!background?.transformId) { return undefined; }