Skip to content

Vega story embed

Create a story in a text file and play it on your own web page. Start with two lines of dialogue, then add a background, sound and a model. These steps use the hosted Vega browser module; you can follow them without installing npm packages.

Keep Complete original example selected, click Load selected content, then Play. You should see a garden and Mira’s first line. Next line continues; Pause stops playback; the slider jumps within the story. Exit this player closes it before you load another story.

1. Choose a story

Start with the complete original file. Load it, then press Play to read the first line.

Open complete JSON · Download JSON

Preparing example…

Advanced parameters and complete input — reload to apply
Content language
Load log and error details
View this example's code

The original example uses this complete story.json and garden.svg. The Our Notes: Treasure option loads a published game story, including its model and sound resources. Settings in Advanced parameters take effect on the next load. Choosing a language selects available authored text; it does not translate a single-language story.

Make a folder called my-story. In a plain-text editor, paste the following complete JSON and save it there as story.json, using UTF-8. Check that its filename is story.json, not story.json.txt. JSON contains data: keep the quotation marks and commas, and leave out JavaScript words such as const and import.

{"localization":{"locales":["en"],"defaultLocale":"en"},"commands":[
{"command":2,"targetName":"Mira","text":"This is my first story."},
{"command":2,"targetName":"Mira","text":"Press Next line to continue."}
]}

You can also open this complete dialogue JSON and save it as story.json. commands is the ordered list of story actions. Here, 2 means Talk, targetName is the speaker, and text is the line shown on screen. Edit the two lines first; keep the other fields as shown.

Open the complete starter HTML, save it as index.html, and put it next to story.json. You can also download the complete starter folder ZIP and extract it to obtain this directory. The complete copyable source is below.

my-story/
index.html
story.json

Serve the folder over HTTP. If Python is missing, install it from Python.org. With Python 3 installed, open a terminal in my-story and run:

Terminal window
python3 -m http.server 8000

On Windows, use py -m http.server 8000. Open http://localhost:8000/ in your browser. An editor’s local HTTP preview also works. Double-clicking a file opens a file:// address; the JSON request needs an HTTP address. Stop the server with Ctrl+C when finished.

Click Load / retry, wait for Loaded, then Play. The first line appears; Next line reveals or advances the dialogue. This initial story has no image, so its background is plain. Closing the player frees its resources; you can load it again.

Complete index.html — copy everything into one file
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>My first story</title>
<style>
body { max-width: 60rem; margin: auto; padding: 1rem; font: 1rem/1.5 sans-serif; color: CanvasText; background: Canvas; }
#stage { height: 60vh; min-height: 320px; position: relative; }
button { font: inherit; min-height: 48px; margin: .25rem; }
</style>
<h1>My first story</h1>
<p id="status" role="status" aria-live="polite">Press Load to read story.json.</p>
<div id="stage"></div>
<button id="load">Load / retry</button>
<button id="play" disabled>Play</button>
<button id="pause" disabled>Pause</button>
<button id="next" disabled>Next line</button>
<button id="close">Close</button>
<script type="module">
const button = id => document.getElementById(id);
const status = document.getElementById("status");
let handle, controller, generation = 0;
function controls(enabled) {
for (const id of ["play", "pause", "next"]) button(id).disabled = !enabled;
}
async function close() {
generation++; controller?.abort(); controls(false);
const old = handle; handle = undefined;
await old?.dispose();
}
button("load").onclick = async () => {
button("load").disabled = true;
let current;
try {
await close(); current = ++generation;
controller = new AbortController(); const signal = controller.signal;
status.textContent = "Loading…";
const { mountStory } = await import("https://haneoka.org/embed/vega.js");
const response = await fetch("./story.json", { signal });
if (!response.ok) throw new Error(`story.json: HTTP ${response.status}`);
const storyData = await response.json();
signal.throwIfAborted(); if (current !== generation) return;
handle = mountStory(document.getElementById("stage"), {
document: storyData,
locale: storyData.localization?.defaultLocale ?? "en",
assetsBase: new URL("./", response.url).href,
signal,
onEvent(event) { if (event.type === "error" && current === generation) status.textContent = event.error.message; }
});
await handle.ready;
if (current !== generation) return;
controls(true); status.textContent = "Loaded. Press Play, then Next line.";
} catch (error) {
if (current === generation) {
const failed = handle; handle = undefined; await failed?.dispose().catch(() => {});
if (current === generation) status.textContent = `Loading failed: ${error.message}. Check the file, then retry.`;
}
} finally { button("load").disabled = false; }
};
button("play").onclick = () => handle?.play();
button("pause").onclick = () => handle?.pause();
button("next").onclick = () => handle?.next();
button("close").onclick = async () => { await close(); status.textContent = "Closed. Press Load to try again."; };
window.addEventListener("pagehide", () => { void close(); }, { once: true });
</script>
</html>

The script performs three separate actions:

const response = await fetch("./story.json"); // request the saved file
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const storyData = await response.json(); // turn JSON text into a JavaScript object
const player = mountStory(document.getElementById("stage"), {
document: storyData, // pass that object to the player
locale: "en",
assetsBase: new URL("./", response.url).href,
});
await player.ready; // the page enables Play only when loading succeeds

This excerpt explains the load section in the full HTML. A filename, URL and parsed object are different inputs:

What you haveHow to use it
A story object already created in JavaScriptmountStory(container, { document: storyData })
An HTTP address pointing to a story JSON fileFetch and parse it as above, or use httpDataSource in step 6
A JSON file selected with a file inputUse fileDataSource; choose where its images and audio come from
A public Haneoka game story IDUse haneokaStorySource in step 7

Save garden.svg next to your files. Replace story.json with the complete background story, or insert this action at the start of its commands array, before the first Talk:

{"command":25,"background":{"url":"garden.svg"}}

25 means Stage. Its background.url names the image file. Keep a comma between this action and the following Talk. Your folder now contains index.html, story.json and garden.svg. Reload the player: the garden appears behind the dialogue. The starter’s assetsBase points to this folder, so garden.svg resolves beside story.json.

Save cue-tone.wav beside story.json and replace the JSON with the complete sound story. It plays a quiet, original test tone with the first line. This is the added field on that Talk action:

"voices": [{"soundId":10000001,"categoryName":"Voice","playableUrl":"cue-tone.wav","volume":0.5}]

To use spoken dialogue, replace the WAV with your recording and change playableUrl to its exact filename. soundId identifies the cue; playableUrl locates the playable audio. Use Play from a click or tap so the browser can allow sound. The SDK reads declared audio through the shared resource transport and prepares playback URLs automatically. Resource requests use the configured byte budget and deadline; disposal releases the owned playback URLs.

When your existing story already contains cue IDs, put their addresses in one JavaScript map and fill the voice records before mounting. In the starter HTML, add this immediately after const storyData = await response.json();:

const sounds = { 10000001: "./cue-tone.wav" };
for (const command of storyData.commands) {
for (const voice of command.voices ?? []) {
const key = voice.soundId ?? voice.id;
if (Object.hasOwn(sounds, key)) voice.playableUrl = sounds[key];
}
}

This is host-side preparation for the sample’s commands[].voices; sounds is not a mountStory option. BGM and sound-effect actions use their bgm.playableUrl and se.playableUrl records. Public Haneoka stories already resolve voiceRefs, bgmRef and seRef through their sound registry. Other formats need an adapter that translates their cue fields into these records, including cues inside timelines or command groups.

6. Load a different URL or a selected file

Section titled “6. Load a different URL or a selected file”

The complete source-selection HTML includes all four methods, visible loading errors, cancellation, playback and a progress slider. Save it as index.html beside the same JSON and assets. Choose Another JSON address, paste the address, then Load. Choose A JSON file on my computer to select the saved JSON instead.

For HTTP, the shorter SDK route replaces the starter’s fetch/JSON/mount block with:

const { mountStory } = await import("https://haneoka.org/embed/vega.js");
const { httpDataSource } = await import("https://haneoka.org/embed/core.js");
handle = mountStory(document.getElementById("stage"), {
source: httpDataSource({ url: new URL("./story.json", location.href).href }),
locale: "en",
signal,
});
await handle.ready;

httpDataSource fetches and parses the JSON internally. Relative resource paths use the JSON address’s directory; an absolute URL keeps its own origin. Set assetsBase only when your resources live in a different directory.

Selecting a JSON file grants access to that file alone. It does not grant access to neighbouring images, models or audio. The complete HTML uses fileDataSource(file) and serves those resources from the HTML folder. For fully local selected resources, see local files and fileResourceResolver. Model sidecar paths also need to resolve through that resource map.

7. Load a published Our Notes story with one entry

Section titled “7. Load a published Our Notes story with one entry”

Use mountHaneokaStory when you have a Haneoka story ID. It selects matching player modules, the theme and Cubism runtime from one release manifest. You provide the story ID, server and language.

Open the complete short-story HTML, save it as index.html, and serve it as in step 3. Click Load, wait for Ready, then Play. This example uses afterlive_10109. Open its complete catalog JSON to inspect the authored commands and asset references.

import { mountHaneokaStory } from "https://haneoka.org/embed/vega-haneoka.js";
const player = mountHaneokaStory(document.getElementById("stage"), {
id: "afterlive_10109", server: "intl", locale: "en",
});
await player.ready;
// Call from a button click:
player.play();

The complete HTML supplies the element, buttons, loading stages, cancellation, error display and disposal. Its native player also provides scene controls. The external buttons demonstrate the handle methods; your own page can keep the control set that fits its layout.

Complete index.html for the published short story
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Haneoka story</title>
<style>
body{font:16px system-ui;margin:16px;background:Canvas;color:CanvasText}
#stage{height:min(70vh,600px);position:relative;width:100%}
button{font:inherit;margin:4px;min-height:48px;padding:8px 16px}
output{display:block;margin:8px}
</style>
<h1>Haneoka story</h1>
<button id="load" disabled>Load</button>
<button id="cancel" disabled>Cancel loading</button>
<button id="play" disabled>Play</button>
<button id="next" disabled>Next line</button>
<button id="exit" disabled>Dispose</button>
<output id="status" role="status">Preparing player</output>
<div id="stage"></div>
<script type="module">
import {mountHaneokaStory} from "https://haneoka.org/embed/vega-haneoka.js";
let player, generation=0;
const by=id=>document.getElementById(id);
const labels={manifest:"Preparing player",modules:"Preparing player",runtime:"Preparing player",data:"Loading story",resources:"Loading assets",ready:"Ready"};
const controls=ready=>{by("play").disabled=!ready;by("next").disabled=!ready;};
by("load").onclick=async()=>{
const current=++generation;
by("load").disabled=true;controls(false);
try{
await player?.dispose();
if(current!==generation)return;
player=mountHaneokaStory(by("stage"),{
id:"afterlive_10109",server:"intl",locale:"en",
onEvent:event=>{
if(current!==generation)return;
if(event.type==="stage")by("status").textContent=labels[event.stage];
if(event.type==="error"){controls(false);by("status").textContent=String(event.error?.message??event.error);}
},
});
window.storyPlayer=player;
by("exit").disabled=false;by("cancel").disabled=false;
await player.ready;
if(current===generation){controls(true);by("status").textContent="Ready";}
}catch(e){if(current===generation)by("status").textContent=player?.snapshot.phase==="cancelled"?"Cancelled":String(e.message??e);}
finally{if(current===generation){by("load").disabled=false;by("cancel").disabled=true;}}
};
by("cancel").onclick=()=>{player?.cancel();};
by("play").onclick=()=>{player.play();by("status").textContent="Playing";};
by("next").onclick=()=>{player.next();};
by("exit").onclick=async()=>{
++generation;controls(false);by("cancel").disabled=true;by("exit").disabled=true;
try{await player?.dispose();by("status").textContent="Disposed";}
catch(e){by("status").textContent=String(e.message??e);}
finally{by("load").disabled=false;}
};
by("load").disabled=false;by("status").textContent="Not loaded";
window.addEventListener("pagehide",()=>{void player?.dispose();},{once:true});
</script>
</html>

The in-page Our Notes: Treasure preset keeps the longer millsage_004_1_02 story. Its complete public JSON contains 728 commands. Both presets use the same short API. Replace id with an ID returned by the story catalog.

Catalog JSON contains references. The Haneoka adapter prepares its runtime, model metadata and media records. To play your own complete Vega story JSON, keep the fetch → response.json() → mountStory({ document }) path in steps 3–6. The Haneoka helper supplies its resource origin; you only set a resource base when intentionally overriding its assets through advanced player options.

The Cubism provider accepts a standard *.model3.json and legacy *.model.json. For a standard model, its character descriptor can point to the manifest:

{"runtime":{"format":"cubism3","model":"models/Mira/Mira.model3.json"}}

Keep the exported directory structure:

models/Mira/
Mira.model3.json
Mira.moc3
Mira.2048/texture_00.png
motions/...
expressions/...
ComponentWhen it is needed
model3.json, its referenced .moc3, and every referenced textureBasic model rendering; names and paths must match the manifest
Cubism plugin, Web runtime module and compatible Cubism CoreExecuting model rendering; the main story embed does not install a model provider automatically
Physics, pose and user-data JSONWhen declared in FileReferences; remove unused declarations or provide the referenced files
Motion and expression filesWhen used by the story; preserve the manifest catalog and file paths
Extra shader JSON or a shader directoryNo external shader configuration is required by this runtime; its rendering shaders are bundled

See Live2D’s exported model file guide and model3.json specification. The provider uses the manifest to locate sidecars.

Save the complete model story template as story.json. Its command: 0 is the entry action, targets identifies Mira, positionType: 5 places her in the centre, and characterModel points to the model. Set runtime.model to your actual manifest path; keep the model sidecar filenames and directories intact. Use the complete HTML from step 6, expand Advanced: standard Live2D in my own story, enable the Cubism provider, choose the original JSON, Load, then Play. This template requires your own exported model files; its download does not contain the illustrated model directory.

ZIP input is not currently implemented. Extract the model archive, preserve its paths, serve the extracted files, and point the descriptor to its manifest. The existing model export creates ZIP downloads; it does not load ZIPs. The public options also do not include shaders or shadersDir. A custom shader-pack loader would require an additional runtime extension; adding those keys to a story has no such effect.

What you seeWhat to do
story.json: HTTP 404Open http://localhost:8000/story.json; check the filename, directory and letter case
JSON parse errorUse the complete provided JSON, then edit only one text value; JSON uses double quotes and has no trailing commas
Failed to fetchOpen the JSON address directly; use an HTTP page and check the browser’s failed request address
CORS error for another domainServe the files on your page’s origin, or configure that resource server’s response to allow your page’s origin; client request headers cannot grant CORS permission
Dialogue works but an image/audio file failsCheck its literal path and assetsBase; selecting one JSON does not select its sidecar files
Model runtime failsCheck the runtime module, Core, manifest, binary and texture requests separately; a successful JSON fetch proves only that the story file is readable
Load cancelled or player closedRetry with a new handle after await player.dispose(); the complete examples do this for you

Playback, cleanup and advanced integration

Section titled “Playback, cleanup and advanced integration”

play() starts or resumes; pause() pauses; next() advances dialogue. await player.seek(0.5) moves to a command boundary halfway along the current reachable story path, including choices. It returns the restored command index. player.snapshot supplies state and progress; state events update about every 100 ms, with immediate updates for controls.

ready waits for the renderer, required resources, fonts and seek index. cancel() stops loading; aborting the supplied signal after readiness disposes the player. dispose() is idempotent. Await it before mounting another player in the same element. Resource failures appear through ready rejection and error events; observer errors are isolated through onListenerError.

The runtime resolves declared URLs and leaves dialogue, IDs and source labels intact. Local resource maps use literal paths, including Unicode. The core loader releases owned Blob URLs; model providers and the renderer release their decoded and GPU resources. Transport defaults are CORS, omitted credentials, 64 MiB per response and a 30-second resource deadline. Dynamic runtime modules/Core scripts have their own browser loading paths.

Every player shows a translucent Haneoka icon linking to the main site, initially in the top-left corner. brandingCorner: "top-right" moves it. The overlay keeps the full scene area. Your story and assets retain their own authorship and licenses.

For a bundled application, the package name is @haneoka/embed-vega; theme and Haneoka helpers also have package subentries. Public npm installation is pending. Use the hosted browser modules for this tutorial. When building from the source repository, install its workspace dependencies and build before packing. Keep the theme’s font and sprite assets with its built module. The parameter reference below covers the complete integration surface.

For Engine integrations that supply their own file/native resource port, see host resources: complete types, cancellation and lease ownership.

The browser entry is https://haneoka.org/embed/vega-haneoka.js; the bundled package subentry is @haneoka/embed-vega/hosted. The hosted entry selects its own release manifest by default. A direct package import defaults to https://haneoka.org/embed/manifest.json. The container is a browser HTMLElement with a nonzero layout size. Missing browser context or a blank/non-string ID throws TypeError; a second hosted mount in the same container before disposal throws Error. The handle returns immediately; keep it while awaiting ready.

OptionType; required/defaultEffect and constraints
idrequired stringExact story ID returned by the current catalog. Empty/blank/non-string rejects synchronously; an unavailable ID rejects ready with the source error.
serveroptional string; intlAsset server, independent of text language. Uses the existing catalog slug validation and availability.
localeoptional string; jaStartup content and UI language. Public story arrays use ja/en/zh-TW/zh-CN/ko; select one of the story’s authored languages.
signaloptional AbortSignalCancels bootstrap; abort after ready disposes the player. A supplied already-aborted signal rejects ready.
manifestUrloptional absolute HTTP(S) URL; entry-local release manifest for hosted ESM, stable root for package importSelect a compatible SDK manifest. Invalid URL/protocol throws; malformed/mixed-version metadata rejects ready. Use one complete release.
apiBaseoptional absolute HTTP(S) directory; https://haneoka.org/api/v1/Alternate compatible story catalog. Uses core directory validation; model paths still come from that catalog.
runtimeoptional CubismEmbedOptions; release runtime descriptorAdvanced runtime override. Required nested fields are moduleUrl and runtime.cubismCoreUrl; runtime.cubism2CoreUrl and runtime.motionSyncCoreUrl are optional. Use compatible licensed module/Core URLs. Factory contract is below.
playerOptionsoptional object; Selected mountStory options passed to the assembled player; complete fields and exclusions below.
fetcheroptional (Request)=>Promise<Response>; native fetchManifest, runtime descriptor, story and resource transport. The default first-party requests use no-referrer. Dynamic modules/Core scripts retain native browser loading.
headersoptional HeadersInit; noneCore transport headers; script/module requests use their native paths.
credentialsoptional omit/same-origin/include; omitResource transport credentials. Public reads use omit; credentialed cross-origin responses need appropriate server permission.
maxBytesoptional positive safe integer; 67108864Per-response encoded-byte limit for bootstrap and child transport. Invalid values throw RangeError; exceeding the limit rejects the relevant read. It is separate from GPU/decoded-memory limits.
onEventoptional (HaneokaStoryEvent)=>voidReceives stage, load, state, seek and error events from construction onward.
onListenerErroroptional (unknown)=>voidObserver exceptions are isolated and forwarded here; listener failure does not become playback failure.

playerOptions accepts theme, brandingCorner, shell, plugins, renderer, assetsBase, resolveResource, resourceTimeoutMs, resolveLocalizedText, onListenerError. Their nested fields, defaults, ranges and errors are the same as the mountStory tables below. This helper defaults theme to haneoka, installs theme/Cubism plugins, and appends the supplied plugin array. Its child uses the Haneoka game shell unless shell is explicitly configured. Keep the catalog’s resource base for normal use; override assetsBase/resolveResource only when intentionally replacing resources. Child-option validation runs during bootstrap and rejects ready; top-level onListenerError handles hosted observers, while playerOptions.onListenerError follows the child contract.

playerOptions excludes source, document, server, locale, signal, onEvent, fetcher, headers, credentials and maxBytes. Supply the shared context fields at the top level. This first-party helper accepts a catalog ID. For an authored document, HTTP JSON URL or selected File, use mountStory and the source factories in steps 3–6.

Handle memberType/signatureBehavior
readyPromise<void>Waits for selected modules, runtime descriptor and child data/model/font/seek preparation. Rejects the original bootstrap/load error.
snapshotStoryEmbedSnapshotphase loading/ready/cancelled/error/disposed, playing/paused/finished/seeking, commandIndex/commandCount/progress. Wrapper phase describes this handle.
playerVegaPlayerHandle or undefinedAdvanced child access; its lifetime is owned by the hosted handle.
stagemanifest/modules/runtime/data/resources/readyCurrent preparation stage, suitable for your own user-facing loading label.
releasestring or undefinedSelected immutable SDK release after the manifest is read.
play/pause/next()=>voidRequire ready; otherwise throw Error. Delegated playback errors use the child error event.
seek(ratio:number)=>Promise<number>Finite 0–1 command-path progress; returns the restored boundary. Child range/lifecycle errors retain their meaning.
subscribe(listener)=>unsubscribeReceives future HaneokaStoryEvent values; subscribing after disposed throws Error.
cancel()=>voidRetires loading bootstrap. After ready use dispose.
dispose()=>Promise<void>Idempotent; cancels, awaits bootstrap cleanup, disposes the child and loader, releases its container registration. Cleanup failures can reject AggregateError. Await before reuse.

Stage events are {type:"stage",stage,release?}. Load/state/seek/error shapes match mountStory below. The public module manifest and runtime descriptor belong to the same release; invalid module paths, runtime descriptor hash/length mismatch, runtime paths outside the distribution origin, import failures and resource errors reject ready. Failed bootstrap cleans up its child and registration so the host can create a fresh handle. Continue to dispose the failed handle to close your own lifecycle.

Required positional container is a browser HTMLElement with an ownerDocument/defaultView and nonzero layout dimensions. A nonbrowser container throws TypeError; mounting a second story in the same container before disposal throws Error. It returns the handle synchronously. In TypeScript choose exactly one document or source; the implementation prefers a supplied source, while missing/invalid commands reject ready.

OptionType; required/defaultEffect, constraints and failures
documentAdvStory; required alternative to sourceIn-memory engine document. commands must be an array; malformed document rejects ready with TypeError. Objects are authored input, not a required five-language catalog.
sourceDataSource<AdvStory>; required alternative to documentload(context) returns the engine document; memory/http/file/custom factories are supported. Source failures reject ready.
serveroptional string; undefined, public adapter intlAsset registry passed to source, independent from locale.
localeoptional string; enAuthor/playback language; startup also sets the player’s locale and shell UI language.
assetsBaseoptional absolute HTTP(S) directory; source baseNo query/fragment; overrides source resource origin. Invalid base TypeError.
resolveResourceoptional (key,DataContext)→string/URL/Blob or Promise; key unchangedMaps declared media/model/font paths; already-resolved URLs pass through once. Resolver errors reject ready/resource use.
fetcheroptional (Request)→Promise<Response>; fetchData and resource bytes use core transport. Modules/Core script loaders have their separate loading path.
credentialsomit/same-origin/include; omitLoader requests; service must allow credentials/origin when applicable.
headersoptional HeadersInit; noneCore transport headers, not script/module request headers.
maxBytespositive safe integer;67108864Per document/resource decoded bytes; illegal value RangeError. Not a combined GPU/cache budget.
onListenerErroroptional (unknown)→voidObserver-error sink, isolated from playback results.
themeportable/haneoka; portableHaneoka requires its theme/shell plugins. Missing contributions reject ready.
brandingCornertop-left/top-right; top-leftAlways-visible translucent corner attribution over the full-size scene; no hide option.
shellfalse or VegaPlayerShellOptionsPortable defaults false; Haneoka defaults {initialScreen:"game"}. Full fields below.
pluginsreadonly VegaPlugin[];[]Appended providers/commands after richtext, portable UI, Three and resource adapter. Real plugin setup/manifest/contributions are required; setup/dependency failures reject ready.
rendererThreeRendererPluginOptions;{}Full renderer and post-texture settings below.
resourceTimeoutMspositive safe integer;30000Total deadline for each adapter resource read. Illegal value RangeError; elapsed resource request rejects with TimeoutError. It is not a whole-story deadline.
signaloptional AbortSignalCancels boot; abort after ready disposes this player. The per-resource timeout remains separate.
onEventoptional (StoryEmbedEvent)→voidAttached before boot; same event shapes as subscribe.
resolveLocalizedTextoptional (value:unknown)→{text:string,lang?:string}Custom text resolution; default Vega localization. Must produce the resolver shape; own exceptions can fail the active operation.

The nine common loader options use the detailed core transport/resource contract. There is no mount autoplay or general loadTimeoutMs option. Use an external signal/deadline for whole loading; start playback from a user gesture.

shell fieldType/defaultMeaning
storageoptional VegaSaveStorage; memoryread(key)→Promise<string|null>, write(key,value)→Promise<void>, remove(key)→Promise<void>, list(prefix)→Promise<readonly string[]>. Caller backend owns persistence; malformed saves fail the relevant save/load operation.
projectIdoptional stringTrimmed explicit ID, then story.vegaProject.id/story.id, then vega-story. Save namespace identity.
settingsIdoptional string; projectIdSeparate preferences namespace; persisted settings take precedence over initialSettings.
initialSettingsoptional Partial<VegaNarrativeSettings>; table defaultsApplied during shell creation. Mount then applies outer locale to player/UI; use outer locale for startup language.
titleoptional stringTrimmed explicit title, then localized project/story title/name, then projectId.
initialScreentitle/gameGeneric shell defaults title; Haneoka embed’s implicit shell starts game.
onExitRequestoptional ()=>void/Promise<void>Receives shell Exit after it pauses/returns to title. Host chooses navigation/close; callback failure rejects that shell operation.
initialSettings keyType/defaultRange and behavior
textSpeednumber/1Clamped .1–5.
autoDelaynumber/1.5Seconds, clamped0–30.
masterVolumenumber/1Clamped0–1.
bgmVolumenumber/.8Clamped0–1.
voiceVolumenumber/1Clamped0–1.
seVolumenumber/.9Clamped0–1.
languagestring/autoNarrative language or auto; stored string up to64 characters.
uiLanguagestring/autoUI language, separate from server.
reducedMotionboolean/falseReduce supported UI motion.
highContrastboolean/falseSupported UI contrast preference.
instantTextboolean/falseComplete readable text immediately.
webTextboolean/falseBrowser text mode for Haneoka Reading UI; version note below.
textSizenumber/1Clamped .5–2.
subtitlesEnabledboolean/trueDisplay available subtitles.
bgmEnabledboolean/trueBGM enabled preference.

Nonfinite numeric settings use their fallback; boolean settings are normalized. Unknown shell setting keys throw RangeError when set. webText is present in the published source revisions engine1916c57/theme1d5b20e/shell436ade8; use a matching build or a later CDN receipt containing those revisions. If you explicitly pin the old r22 player, use the settings supported by that revision. On a matching shell, set shell.initialSettings.webText at construction or story.player.shell.setSetting("webText", true) after ready.

FieldType/defaultMeaning and combinations
rendererIdvega-three-webgl2/haneoka-three-webgl2; vega-three-webgl2Scene provider route; normally leave the embed’s backend intact.
profilevega/unity-adv;vegaProvider scene profile.
staticPortraitFallbackboolean/truePermit static portrait fallback when a model provider is missing.
colorGradingModeldr/hdr;ldrPost color pipeline.
contributionIdstring/haneoka.renderer-threeInstalled renderer contribution identity.
contributionNamestring/Vega Three WebGL2Human-readable contribution name.
backendvega-three-webgl2/haneoka-three-webgl2;vega-three-webgl2Contribution route. Embed selects vega-three-webgl2; incompatible registration rejects player creation.
postTextureResolveroptional ThreePostTextureResolverCannot combine with postTextureAssets (TypeError). resolver owns supplied textures and optional dispose is tied to plugin lifetime.
postTextureAssetsoptional Readonly<Record<string,string>> or ReadonlyMap<string,string>Authorized URLs keyed by stable renderer texture key. Builds a resolver if supplied alone.

Custom postTextureResolver has required resolve(reference:unknown,usage)→Three.Texture|null, optional preload(requests,signal?)→Promise<void>, optional dispose(). Each request is {reference,usage}; usages include film-grain, color-lookup and lens-dirt. A null result allows the existing pipeline fallback. Return textures compatible with the selected Three instance; the provider owns their cleanup. Parameter conflicts throw synchronously during plugin creation; load/render/provider errors arrive through ready/events.

haneokaStorySource({id,apiBase?}) requires a public story ID. apiBase defaults https://haneoka.org/api/v1/ and follows core directory validation. It loads the selected story, story-runtime and only declared Live2D metadata, with server/locale from the mount. A missing story/model or malformed catalog record rejects load; no release/account argument is needed.

cubismStoryPlugin({moduleUrl,runtime}) requires a separately licensed absolute module URL and runtime.cubismCoreUrl. runtime.cubism2CoreUrl? and runtime.motionSyncCoreUrl? are optional version-specific script URLs. The module must export createCubismWebRuntimeAdapter; a missing export/provider capability rejects model preparation. This plugin lazily loads the adapter when a model needs it, preserves the provider lifecycle, and does not bundle Core. Keep compatible adapter/Core versions together.

haneokaStoryPlugins has no options: it is the theme+default-shell plugin list. createHaneokaThemeAssetsPlugin(provider) accepts optional assets(), resolveChatImage(value,kind:icon|stamp), and resolveSourceAsset(path) callbacks. assets returns optional URL fields arrow,auto,chatWindow,chatBackground,chatBackgroundFallback,chatLock,chatTextBox,chatComposerPlus,chatComposerPhoto,chatComposerPicture,chatComposerSmile,chatComposerMicrophone,chatIcons,fast,frame,fullscreen,subtitles,skip. Missing overrides use existing theme assets. HANEOKA_POST_TEXTURE_ASSETS is the fixed authorized renderer asset map; supply it as renderer.postTextureAssets when needed.

AdvStory has commands (required by this mount), optional runtime and localization, plus source-defined fields. localization contains optional locales:string, defaultLocale:string, arrayOrder:string; these are author-defined, not a fixed five-language requirement. Runtime is Vega’s Partial<AdvRuntimeConfig>, retained through the engine. Individual command opcodes/resource structures follow the selected command/provider schema; the example uses Stage25 and Talk2 from the public protocol.

Handle memberSignatureBehavior/errors
readyPromise<void>Renderer/resources/fonts/seek-index boot. Rejection remains available even when onEvent observes error.
snapshotreadonly objectphase,playing,paused,finished,seeking,commandIndex,commandCount,progress. phase:loading/ready/cancelled/error/disposed.
playerVegaPlayerHandle or undefinedAdvanced ready engine handle: id,player,lifetime,root,slots,shell?,dispose(). Let the embed own disposal.
play / pause / next()=>voidRequire ready (otherwise Error). Play resumes; async interpreter failure becomes error event. Next requests readable advancement.
seek(ratio:number)=>Promise<number>Finite0–1, otherwise RangeError. Returns actual command index; preserves play/pause intent.
subscribe(listener)=>unsubscribeFuture events; subscribing after disposed throws Error.
cancel()=>voidOnly loading boot; after ready it is a no-op. Dispose before retry.
dispose()=>Promise<void>Shared/idempotent Promise; release engine before loader/URLs, remove host DOM/listeners, await boot cleanup. Cleanup failures AggregateError.

Events are {type:"load",event:EmbedEvent}, {type:"state",snapshot}, {type:"seek",commandIndex}, {type:"error",error}. Load event fields follow core; state publishes changed snapshots at100ms and on controls. Listener exceptions are isolated. A disposal completes with phase disposed; its state/load events can be observed by construction listeners. Resource/source/plugin failures keep their actual errors; cancellation rejects signal.reason. This embed does not add a second error-code taxonomy.

This page uses the stable aliases. The older immutable r22 player retains its reserved-row attribution layout. When explicitly pinning a revision, keep engine, theme, shell and runtime versions compatible.