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
3 changes: 2 additions & 1 deletion docs/Concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Static, read-only data that defines the visual novel content:
- Computed variables are derived read-only values declared under `resources.variables[*].computed`; their authored interface is documented in `docs/ComputedVariables.md`
- Voice audio is stored under `resources.voices[sceneId][voiceId]` and line actions reference the scene-local `voiceId`
- Layout text elements should reference shared styles with `textStyleId`
- Text-backed visual items should put text-specific data under `text` and reference shared styles with `text.textStyleId`
- `resources.colors[*].hex` should be opaque hex only; text fill and stroke transparency should be authored on `resources.textStyles` with `colorAlpha` / `strokeAlpha`, not inside `resources.colors`
- Layout sprite elements should reference images with `imageId` and optional `hoverImageId` / `clickImageId`
- Layout rect elements should reference shared colors with `colorId` and optional `hover.colorId` / `click.colorId` / `rightClick.colorId`
Expand Down Expand Up @@ -127,7 +128,7 @@ Presentation state includes:
- `colorId` references `resources.colors` for the persistent solid backing color behind the background resource; if omitted, the backing color falls back to `screen.backgroundColor`, then black
- `dialogue`: Speaker, layered speaker sprite, text content, mode (ADV/NVL)
- `character`: Character sprites and positions
- `visual`: Additional visual elements
- `visual`: Additional visual elements, including resource-backed visuals and text-backed visuals
- `bgm` / `sfx` / `voice`: Audio configuration
- `animation`: Active animations
- `layout`: UI layouts
Expand Down
73 changes: 71 additions & 2 deletions docs/RouteEngine.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ Actions that can be attached to lines to control presentation:
| `background` | `{ resourceId?, colorId?, transformId?, x?, y?, anchorX?, anchorY?, scaleX?, scaleY?, rotation?, originX?, originY?, opacity?, blur?, animations? }` | Set background/CG. Transform fields are renderer pixels/unitless multipliers/degrees; `blur: null` clears background blur |
| `dialogue` | `{ characterId?, character?, character.sprite?, persistCharacter?, content, append?, mode?, ui?, clear? }` | Display dialogue |
| `character` | `{ items }` | Display character sprites. Each item can set transform overrides, `opacity`, and `blur` |
| `visual` | `{ items }` | Display visual elements. Each item can set `layer`, transform overrides, `opacity`, and `blur` |
| `visual` | `{ items }` | Display visual elements. Each item can set `layer`, transform overrides, `opacity`, `blur`, and animations |
| `bgm` | `{ resourceId, volume?, loop?, startDelayMs? }` | Play background music |
| `sfx` | `{ items }` | Play sound effects. Each item can include `volume`, `loop`, and `startDelayMs` |
| `voice` | `{ resourceId, volume?, loop?, startDelayMs? }` | Play voice audio from `resources.voices[currentSceneId][resourceId]` |
Expand Down Expand Up @@ -712,6 +712,74 @@ screen transitions, overlay stack entries, and confirm dialogs. JavaScript
callers can use the exported `RENDER_LAYER`, `VISUAL_LAYER`, and
`DEFAULT_VISUAL_LAYER` constants when generating project data.

### Text Visuals

Visual items can be backed by text instead of an image, video, spritesheet, or
layout resource. A text-backed visual uses the same visual item placement,
layer, opacity, blur, and animation fields as every other visual item.

New visual items choose one render subject:

- `resourceId` for image, video, spritesheet, or layout resources
- `text` for direct RouteGraphics text

`resourceId` and `text` are mutually exclusive. The `text` object owns only
text-specific fields such as `content`, `textStyleId`, and optional text layout
fields like `width`. It must not contain visual item fields such as `x`,
`layer`, or `animations`.

```yaml
visual:
items:
- id: chapterTitle
text:
content: "Chapter 1"
textStyleId: title
width: 720
transformId: titleTop
layer: 70
opacity: 0.9
animations:
resourceId: titleFadeIn
```

`text.content` can be a plain string or the same rich content run shape used by
RouteGraphics text. Rich runs can override styles with `textStyleId`, and
furigana can use its own nested `textStyleId`.

```yaml
visual:
items:
- id: locationLabel
text:
content:
- text: "Kanji"
furigana:
text: "reading"
textStyleId: ruby
- text: " label"
textStyleId: emphasis
textStyleId: title
width: 640
transformId: titleTop
anchorX: 0.5
anchorY: 0.5
```

After a text visual exists, later lines can patch it by `id` without restating
the whole text config:

```yaml
visual:
items:
- id: chapterTitle
text:
content: "Chapter 2"
```

For a new text visual, `text.content` and `text.textStyleId` are both required.
For a patch to an existing text visual, either field can be supplied alone.

### Item Transform Overrides

`resources.transforms` can define `x`, `y`, `anchorX`, `anchorY`, `scaleX`,
Expand Down Expand Up @@ -806,7 +874,8 @@ shape as `background.blur` and `screen.blur`; `blur: null` clears the item blur.

Character item appearance applies to the whole character container, so every
sprite part is faded or blurred together. Visual item appearance applies to the
single visual item container, sprite, video, animated sprite, or layout.
single visual item container, sprite, video, animated sprite, layout, or text
element.

```yaml
character:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "route-engine-js",
"version": "1.22.0",
"version": "1.23.0",
"description": "A lightweight Visual Novel engine built in JavaScript for creating interactive narrative games with branching storylines",
"repository": {
"type": "git",
Expand Down
181 changes: 181 additions & 0 deletions spec/constructPresentationState.backgroundTransform.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { describe, expect, it } from "vitest";
import { constructPresentationState } from "../src/stores/constructPresentationState.js";
import { constructRenderState } from "../src/stores/constructRenderState.js";

const createResources = () => ({
images: {
bg: {
fileId: "bg.png",
width: 1920,
height: 1080,
},
},
videos: {},
layouts: {},
animations: {},
transforms: {
preset: {
x: 100,
y: 200,
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1,
rotation: 0,
},
},
characters: {},
controls: {},
colors: {},
fonts: {},
sectionTransitions: {},
sounds: {},
sprites: {},
spritesheets: {},
textStyles: {},
variables: {},
});

const findBackgroundSprite = (elements = []) => {
const pending = [...elements];
while (pending.length > 0) {
const element = pending.shift();
if (element?.id === "bg-cg-background-sprite") {
return element;
}
pending.push(...(element?.children ?? []));
}
return undefined;
};

describe("constructPresentationState background transforms", () => {
it("does not inherit previous inline background transform when transformId is selected", () => {
const presentationState = constructPresentationState([
{
background: {
resourceId: "bg",
x: 900,
y: 800,
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2,
rotation: 0,
},
},
{
background: {
resourceId: "bg",
transformId: "preset",
},
},
]);

expect(presentationState.background).toEqual({
resourceId: "bg",
transformId: "preset",
});

const renderState = constructRenderState({
presentationState,
resources: createResources(),
screen: {
width: 1920,
height: 1080,
},
});

expect(findBackgroundSprite(renderState.elements)).toMatchObject({
x: 100,
y: 200,
scaleX: 1,
scaleY: 1,
});
});

it("keeps explicit inline overrides supplied with a transformId", () => {
const presentationState = constructPresentationState([
{
background: {
resourceId: "bg",
x: 900,
y: 800,
},
},
{
background: {
resourceId: "bg",
transformId: "preset",
x: 300,
},
},
]);

expect(presentationState.background).toEqual({
resourceId: "bg",
transformId: "preset",
x: 300,
});

const renderState = constructRenderState({
presentationState,
resources: createResources(),
screen: {
width: 1920,
height: 1080,
},
});

expect(findBackgroundSprite(renderState.elements)).toMatchObject({
x: 300,
y: 200,
});
});

it("keeps previous inline transform overrides on non-transform partial updates", () => {
const presentationState = constructPresentationState([
{
background: {
resourceId: "bg",
transformId: "preset",
x: 300,
y: 400,
scaleX: 1.3,
scaleY: 1.3,
},
},
{
background: {
opacity: 0.5,
},
},
]);

expect(presentationState.background).toEqual({
resourceId: "bg",
transformId: "preset",
x: 300,
y: 400,
scaleX: 1.3,
scaleY: 1.3,
opacity: 0.5,
});

const renderState = constructRenderState({
presentationState,
resources: createResources(),
screen: {
width: 1920,
height: 1080,
},
});

expect(findBackgroundSprite(renderState.elements)).toMatchObject({
x: 300,
y: 400,
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.5,
});
});
});
Loading
Loading