콘텐츠로 이동

Vega 스토리 임베드

텍스트 파일로 스토리를 만들고 자신의 웹페이지에서 재생합니다. 먼저 대사 두 줄로 시작해서 배경, 소리, 모델을 더해 갑니다. 이 단계는 호스팅된 Vega 브라우저 모듈을 쓰므로 npm 패키지를 설치하지 않아도 됩니다.

완전한 원본 예제을 선택한 채로 고른 내용 불러오기, 이어서 재생를 누릅니다. 정원과 미라의 첫 대사가 보입니다. 다음 대사으로 진행하고, 일시 정지로 재생을 멈추며, 슬라이더로 스토리 안을 이동합니다. 플레이어 종료는 플레이어를 닫으므로, 그 뒤 다른 스토리를 불러올 수 있습니다.

1. 스토리 고르기

완전한 원본 파일로 시작합니다. 불러온 뒤 재생을 누르면 첫 줄을 읽을 수 있습니다.

완전한 JSON 열기 · JSON 내려받기

예제를 준비하는 중…

고급 매개변수와 완전한 입력(다시 불러오면 적용)
콘텐츠 언어
불러오기 기록과 오류 상세
이 예제 코드 보기

원본 예제는 이 완전한 story.json과 garden.svg를 씁니다. Our Notes: Treasure는 모델과 사운드 리소스를 포함한 공개 게임 스토리를 불러옵니다. 고급 매개변수의 설정은 다음 불러오기부터 적용됩니다. 언어 선택은 작성된 텍스트 중에서 고르는 것이며 단일 언어 스토리를 번역하지 않습니다.

my-story 폴더를 만듭니다. 텍스트 에디터에 아래의 완전한 JSON을 붙여 넣고 그 폴더에 UTF-8로 story.json이라고 저장합니다. 파일 이름이 story.json.txt가 아니라 story.json인지 확인하세요. JSON은 데이터입니다. 따옴표와 쉼표는 그대로 두고, const나 import 같은 JavaScript 단어는 쓰지 마세요.

{"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."}
]}

이 완전한 대화 JSON을 열어 story.json으로 저장해도 됩니다. commands는 스토리 동작을 순서대로 나열한 목록입니다. 여기서 2는 Talk, targetName은 화자, text는 화면에 표시되는 대사입니다. 먼저 대사 두 줄을 편집하고 다른 필드는 그대로 두세요.

완전한 시작용 HTML을 열어 index.html로 story.json 옆에 저장합니다. 시작 폴더 전체 ZIP을 내려받아 압축을 풀어도 이 디렉터리를 얻을 수 있습니다. 복사할 수 있는 완전한 소스는 아래에 있습니다.

my-story/
index.html
story.json

폴더를 HTTP로 제공합니다. Python이 없으면 Python.org에서 설치하세요. Python 3이 있으면 my-story에서 터미널을 열고 다음을 실행합니다.

Terminal window
python3 -m http.server 8000

Windows에서는 py -m http.server 8000을 씁니다. 브라우저에서 http://localhost:8000/을 엽니다. 에디터의 로컬 HTTP 미리보기도 됩니다. 파일을 더블 클릭하면 file:// 주소로 열리는데, JSON 요청에는 HTTP 주소가 필요합니다. 끝나면 Ctrl+C로 서버를 멈춥니다.

Load / retry를 누르고 Loaded를 기다린 뒤 Play를 누릅니다. 첫 대사가 나타나고, Next line으로 대사를 표시하거나 넘깁니다. 이 첫 스토리에는 이미지가 없어 배경이 단색입니다. 플레이어를 닫으면 리소스가 해제되며 다시 불러올 수 있습니다.

완전한 index.html: 전부 한 파일에 복사하기
<!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>

스크립트는 세 가지 동작을 따로 수행합니다.

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

이 조각은 완전한 HTML의 불러오기 부분을 설명합니다. 파일 이름, URL, 파싱된 객체는 서로 다른 입력입니다.

가지고 있는 것사용 방법
JavaScript로 이미 만든 스토리 객체mountStory(container, { document: storyData })
스토리 JSON 파일을 가리키는 HTTP 주소위처럼 가져와 파싱하거나 6단계의 httpDataSource를 쓴다
파일 입력으로 고른 JSON 파일fileDataSource를 쓰고 이미지와 오디오를 가져올 곳을 고른다
공개 Haneoka 게임 스토리 ID7단계의 haneokaStorySource를 쓴다

garden.svg를 같은 곳에 저장합니다. story.json을 배경이 있는 완전한 스토리로 바꾸거나, 다음 동작을 commands 배열의 맨 앞, 첫 Talk 앞에 넣습니다.

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

25는 Stage입니다. background.url이 이미지 파일을 가리킵니다. 이 동작과 다음 Talk 사이의 쉼표를 유지하세요. 폴더에는 index.html, story.json, garden.svg가 있습니다. 플레이어를 다시 불러오면 대사 뒤에 정원이 나타납니다. 시작용 파일의 assetsBase가 이 폴더를 가리키므로 garden.svg는 story.json 옆에서 해석됩니다.

5. 소리 하나를 더하고 여러 큐 대응시키기

섹션 제목: “5. 소리 하나를 더하고 여러 큐 대응시키기”

cue-tone.wav를 story.json 옆에 저장하고 JSON을 소리가 있는 완전한 스토리로 바꿉니다. 첫 대사와 함께 작은 원본 테스트 톤이 재생됩니다. 그 Talk 동작에 더해진 필드는 다음과 같습니다.

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

음성 대사를 쓰려면 WAV를 직접 녹음한 파일로 바꾸고 playableUrl을 그 정확한 파일 이름으로 변경합니다. soundId는 큐를 식별하고, playableUrl은 재생 가능한 오디오의 위치입니다. 브라우저가 소리를 허용하도록 Play는 클릭이나 탭으로 누르세요. SDK는 선언된 오디오를 공용 리소스 전송으로 읽고 재생 URL을 자동으로 준비합니다. 리소스 요청에는 설정한 바이트 예산과 기한이 적용되며, 해제하면 소유한 재생 URL이 풀립니다.

기존 스토리에 큐 ID가 있다면 주소를 JavaScript 맵 하나에 모으고 마운트 전에 음성 레코드를 채웁니다. 시작용 HTML에서는 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];
}
}

이는 샘플의 commands[].voices에 대한 호스트 측 준비이며, sounds는 mountStory 옵션이 아닙니다. BGM과 효과음 동작은 bgm.playableUrl과 se.playableUrl 레코드를 씁니다. 공개 Haneoka 스토리는 이미 사운드 레지스트리로 voiceRefs, bgmRef, seRef를 해석합니다. 다른 형식에는 타임라인이나 명령 그룹 안의 큐까지 포함해 큐 필드를 이 레코드로 바꾸는 어댑터가 필요합니다.

6. 다른 URL이나 선택한 파일 불러오기

섹션 제목: “6. 다른 URL이나 선택한 파일 불러오기”

소스 선택이 있는 완전한 HTML에는 네 가지 방법, 불러오기 오류 표시, 취소, 재생, 진행 슬라이더가 모두 들어 있습니다. 같은 JSON과 에셋 옆에 index.html로 저장합니다. Another JSON address를 고르고 주소를 붙여 넣은 뒤 Load를 누릅니다. 저장한 JSON을 고르려면 A JSON file on my computer를 선택합니다.

HTTP라면 더 짧은 SDK 경로로 시작용 파일의 fetch/JSON/mount 블록을 다음으로 바꿉니다.

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는 JSON을 내부에서 가져와 파싱합니다. 상대 리소스 경로는 JSON 주소의 디렉터리를 기준으로 하고, 절대 URL은 자기 출처를 유지합니다. assetsBase는 리소스가 다른 디렉터리에 있을 때만 설정하세요.

JSON 파일을 선택하면 그 파일에만 접근할 수 있습니다. 옆의 이미지, 모델, 오디오 접근 권한은 주어지지 않습니다. 완전한 HTML은 fileDataSource(file)을 쓰고 그 리소스를 HTML 폴더에서 제공합니다. 모두 로컬에서 고른 리소스를 쓰려면 로컬 파일과 fileResourceResolver를 참고하세요. 모델 부속 파일 경로도 그 리소스 맵으로 해석되어야 합니다.

7. 공개된 Our Notes 스토리를 진입점 하나로 불러오기

섹션 제목: “7. 공개된 Our Notes 스토리를 진입점 하나로 불러오기”

Haneoka 스토리 ID가 있으면 mountHaneokaStory를 씁니다. 릴리스 매니페스트 하나에서 맞는 플레이어 모듈, 테마, Cubism 런타임을 고릅니다. 지정할 것은 스토리 ID, 서버, 언어입니다.

완전한 단편 스토리 HTML을 열어 index.html로 저장하고 3단계처럼 제공합니다. Load를 누르고 Ready를 기다린 뒤 Play를 누릅니다. 이 예제는 afterlive_10109를 씁니다. 작성된 명령과 에셋 참조는 완전한 카탈로그 JSON에서 볼 수 있습니다.

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();

완전한 HTML은 요소, 버튼, 불러오기 단계, 취소, 오류 표시, 해제를 제공합니다. 네이티브 플레이어에는 장면 조작도 있습니다. 외부 버튼은 핸들 메서드를 보여 주는 것으로, 자신의 페이지에서는 레이아웃에 맞는 조작만 남길 수 있습니다.

공개 단편 스토리의 완전한 index.html
<!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>

페이지 안의 Our Notes: Treasure 프리셋은 더 긴 millsage_004_1_02 스토리를 씁니다. 완전한 공개 JSON에는 명령 728개가 있습니다. 두 프리셋 모두 같은 짧은 API를 씁니다. id는 스토리 카탈로그가 반환하는 ID로 바꾸세요.

카탈로그 JSON에는 참조가 들어 있습니다. Haneoka 어댑터가 런타임, 모델 메타데이터, 미디어 레코드를 준비합니다. 직접 만든 완전한 Vega 스토리 JSON을 재생하려면 3~6단계의 fetch → response.json() → mountStory({ document }) 경로를 유지하세요. Haneoka 도우미가 리소스 출처를 제공하므로, 리소스 기준은 고급 플레이어 옵션으로 에셋을 의도적으로 바꿀 때만 설정합니다.

Cubism 제공자는 표준 *.model3.json과 구형 *.model.json을 받습니다. 표준 모델이라면 캐릭터 기술자가 매니페스트를 가리킬 수 있습니다.

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

내보낸 디렉터리 구조를 유지합니다.

models/Mira/
Mira.model3.json
Mira.moc3
Mira.2048/texture_00.png
motions/...
expressions/...
구성 요소필요한 경우
model3.json, 참조된 .moc3, 참조된 모든 텍스처기본 모델 렌더링. 이름과 경로는 매니페스트와 일치해야 한다
Cubism 플러그인, Web 런타임 모듈, 호환 Cubism Core모델 렌더링 실행. 기본 스토리 임베드는 모델 제공자를 자동으로 설치하지 않는다
물리, 포즈, 사용자 데이터 JSONFileReferences에 선언된 경우. 안 쓰는 선언을 지우거나 참조된 파일을 제공한다
모션과 표정 파일스토리가 쓰는 경우. 매니페스트 카탈로그와 파일 경로를 유지한다
추가 셰이더 JSON이나 셰이더 디렉터리이 런타임에는 외부 셰이더 설정이 필요 없다. 렌더링 셰이더가 포함되어 있다

Live2D의 내보낸 모델 파일 가이드와 model3.json 사양을 참고하세요. 제공자는 매니페스트로 부속 파일을 찾습니다.

완전한 모델 스토리 템플릿을 story.json으로 저장합니다. command: 0은 시작 동작, targets는 미라 식별, positionType: 5는 중앙 배치, characterModel은 모델 지정입니다. runtime.model을 실제 매니페스트 경로로 설정하고 모델 부속 파일 이름과 디렉터리는 그대로 두세요. 6단계의 완전한 HTML을 쓰고 Advanced: standard Live2D in my own story를 펼쳐 Cubism 제공자를 켠 뒤, 원본 JSON을 고르고 Load, Play를 누릅니다. 이 템플릿에는 직접 내보낸 모델 파일이 필요하며, 내려받기에 그림의 모델 디렉터리는 들어 있지 않습니다.

ZIP 입력은 현재 구현되어 있지 않습니다. 모델 압축을 풀고 경로를 유지한 채 파일을 제공하며, 기술자가 그 매니페스트를 가리키게 하세요. 기존 모델 내보내기는 ZIP 다운로드를 만들 뿐 ZIP을 불러오지 않습니다. 공개 옵션에는 shaders나 shadersDir도 없습니다. 사용자 정의 셰이더 팩 로더에는 별도의 런타임 확장이 필요하며, 이 키를 스토리에 더해도 효과가 없습니다.

보이는 것할 일
story.json: HTTP 404http://localhost:8000/story.json을 열어 파일 이름, 디렉터리, 대소문자를 확인한다
JSON 파싱 오류제공된 완전한 JSON을 쓰고 텍스트 값 하나만 편집한다. JSON은 큰따옴표를 쓰고 끝 쉼표가 없다
Failed to fetchJSON 주소를 직접 연다. HTTP 페이지를 쓰고 브라우저에서 실패한 요청 주소를 확인한다
다른 도메인의 CORS 오류파일을 페이지 출처에서 제공하거나, 리소스 서버 응답이 페이지 출처를 허용하게 설정한다. 클라이언트 요청 헤더로는 CORS 권한을 얻을 수 없다
대화는 되는데 이미지·오디오 파일이 실패한다문자 그대로의 경로와 assetsBase를 확인한다. JSON 하나를 골라도 부속 파일은 선택되지 않는다
모델 런타임이 실패한다런타임 모듈, Core, 매니페스트, 바이너리, 텍스처 요청을 따로 확인한다. JSON을 가져왔다는 것은 스토리 파일을 읽을 수 있다는 뜻일 뿐이다
불러오기가 취소되거나 플레이어가 닫혔다await player.dispose() 뒤에 새 핸들로 다시 시도한다. 완전한 예제는 이를 대신 처리한다

play()는 시작이나 재개, pause()는 일시 정지, next()는 대사 진행입니다. await player.seek(0.5)는 선택지를 포함해 현재 도달 가능한 스토리 경로의 중간에 있는 명령 경계로 이동하고, 복원된 명령 인덱스를 반환합니다. player.snapshot은 상태와 진행도를 제공합니다. 상태 이벤트는 약 100 ms마다 갱신되고 조작 시에는 즉시 갱신됩니다.

ready는 렌더러, 필요한 리소스, 글꼴, 탐색 색인을 기다립니다. cancel()은 불러오기를 멈추고, 준비된 뒤 주어진 신호를 중단하면 플레이어를 해제합니다. dispose()는 멱등입니다. 같은 요소에 다른 플레이어를 마운트하기 전에 기다리세요. 리소스 실패는 ready 거부와 오류 이벤트로 나타나고, 관찰자 오류는 onListenerError로 격리됩니다.

런타임은 선언된 URL을 해석하고 대사, ID, 소스 라벨은 그대로 둡니다. 로컬 리소스 맵은 유니코드를 포함한 문자 그대로의 경로를 씁니다. 코어 로더는 소유한 Blob URL을 해제하고, 모델 제공자와 렌더러는 디코딩된 리소스와 GPU 리소스를 해제합니다. 전송 기본값은 CORS, 자격 증명 생략, 응답당 64 MiB, 리소스 기한 30초입니다. 동적 런타임 모듈과 Core 스크립트에는 자체 브라우저 로딩 경로가 있습니다.

모든 플레이어에는 메인 사이트로 연결되는 반투명 Haneoka 아이콘이 처음에 왼쪽 위에 표시됩니다. brandingCorner: "top-right"로 옮길 수 있습니다. 오버레이는 장면 영역 전체를 유지합니다. 스토리와 에셋의 저작권과 라이선스는 각자의 것입니다.

번들링 애플리케이션의 패키지 이름은 @haneoka/embed-vega이며, 테마와 Haneoka 도우미에도 패키지 하위 진입점이 있습니다. 공개 npm 설치는 아직입니다. 이 튜토리얼에서는 호스팅된 브라우저 모듈을 쓰세요. 소스 저장소에서 빌드할 때는 워크스페이스 의존성을 설치하고 빌드한 뒤 패킹합니다. 테마의 글꼴과 스프라이트 에셋은 빌드된 모듈과 함께 두세요. 연동 전체는 아래의 매개변수 참고에 있습니다.

자체 파일·네이티브 리소스 포트를 제공하는 Engine 연동은 호스트 리소스에서 완전한 타입, 취소, 리스 소유를 참고하세요.

브라우저 진입점은 https://haneoka.org/embed/vega-haneoka.js이고, 번들용 패키지 하위 진입점은 @haneoka/embed-vega/hosted입니다. 호스팅된 진입점은 기본으로 자기 릴리스 매니페스트를 고릅니다. 패키지를 직접 가져오면 기본값은 https://haneoka.org/embed/manifest.json입니다. 컨테이너는 레이아웃 크기가 0이 아닌 브라우저 HTMLElement입니다. 브라우저 맥락이 없거나 ID가 비었거나 문자열이 아니면 TypeError, 해제 전 같은 컨테이너에 두 번째 호스팅 마운트를 하면 Error입니다. 핸들은 바로 반환되니 ready를 기다리는 동안 유지하세요.

옵션타입; 필수/기본값효과와 제약
id필수 문자열현재 카탈로그가 반환하는 스토리 ID 그대로. 비었거나 공백이거나 문자열이 아니면 동기적으로 거부하고, 쓸 수 없는 ID는 소스 오류로 ready를 거부한다.
server선택 문자열; intl에셋 서버. 텍스트 언어와 독립. 기존 카탈로그 슬러그 검증과 제공 여부를 쓴다.
locale선택 문자열; ja시작 콘텐츠와 UI 언어. 공개 스토리 배열은 ja/en/zh-TW/zh-CN/ko를 쓴다. 스토리에 작성된 언어 중 하나를 고른다.
signal선택 AbortSignal시작을 취소한다. ready 뒤의 중단은 플레이어를 해제한다. 이미 중단된 신호는 ready를 거부한다.
manifestUrl선택 절대 HTTP(S) URL; 호스팅 ESM은 진입점의 릴리스 매니페스트, 패키지 가져오기는 안정 루트호환 SDK 매니페스트를 고른다. 잘못된 URL·프로토콜은 예외, 잘못되거나 버전이 섞인 메타데이터는 ready를 거부한다. 완전한 릴리스 하나를 쓴다.
apiBase선택 절대 HTTP(S) 디렉터리; https://haneoka.org/api/v1/호환되는 대체 스토리 카탈로그. 코어 디렉터리 검증을 쓴다. 모델 경로는 그 카탈로그에서 얻는다.
runtime선택 CubismEmbedOptions; 릴리스 런타임 기술자고급 런타임 덮어쓰기. 필수 중첩 필드는 moduleUrl과 runtime.cubismCoreUrl, runtime.cubism2CoreUrl과 runtime.motionSyncCoreUrl은 선택. 라이선스를 받은 호환 모듈·Core URL을 쓴다. 팩토리 계약은 아래.
playerOptions선택 객체; 조립된 플레이어에 넘기는 일부 mountStory 옵션. 전체 필드와 제외 항목은 아래.
fetcher선택 (Request)=>Promise<Response>; 네이티브 fetch매니페스트, 런타임 기술자, 스토리, 리소스 전송. 기본 자사 요청은 no-referrer를 쓴다. 동적 모듈·Core 스크립트는 네이티브 브라우저 로딩을 유지한다.
headers선택 HeadersInit; 없음코어 전송 헤더. 스크립트·모듈 요청은 네이티브 경로를 쓴다.
credentials선택 omit/same-origin/include; omit리소스 전송 자격 증명. 공개 읽기는 omit. 자격 증명이 있는 교차 출처 응답에는 서버 허가가 필요하다.
maxBytes선택 양의 안전한 정수; 67108864시작과 하위 전송의 응답당 인코딩 바이트 제한. 잘못된 값은 RangeError, 초과하면 해당 읽기를 거부한다. GPU·디코딩 메모리 제한과 별개.
onEvent선택 (HaneokaStoryEvent)=>void생성 시부터 stage, load, state, seek, error 이벤트를 받는다.
onListenerError선택 (unknown)=>void관찰자 예외는 격리되어 여기로 전달된다. 리스너 실패는 재생 실패가 되지 않는다.

playerOptions는 theme, brandingCorner, shell, plugins, renderer, assetsBase, resolveResource, resourceTimeoutMs, resolveLocalizedText, onListenerError를 받습니다. 중첩 필드, 기본값, 범위, 오류는 아래 mountStory 표와 같습니다. 이 도우미는 theme 기본값을 haneoka로 하고, 테마·Cubism 플러그인을 설치하며, 주어진 플러그인 배열을 뒤에 더합니다. 하위 플레이어는 shell을 명시하지 않으면 Haneoka 게임 셸을 씁니다. 평소에는 카탈로그의 리소스 기준을 유지하고, assetsBase/resolveResource는 리소스를 의도적으로 바꿀 때만 덮어쓰세요. 하위 옵션 검증은 시작 중에 수행되어 ready를 거부합니다. 최상위 onListenerError는 호스팅 관찰자를 다루고, playerOptions.onListenerError는 하위 계약을 따릅니다.

playerOptions는 source, document, server, locale, signal, onEvent, fetcher, headers, credentials, maxBytes를 제외합니다. 공유 맥락 필드는 최상위에 주세요. 이 자사 도우미는 카탈로그 ID를 받습니다. 작성 문서, HTTP JSON URL, 선택한 File에는 3~6단계의 mountStory와 소스 팩토리를 쓰세요.

핸들 멤버타입·시그니처동작
readyPromise<void>고른 모듈, 런타임 기술자, 하위 데이터·모델·글꼴·탐색 준비를 기다린다. 원래 시작·불러오기 오류로 거부한다.
snapshotStoryEmbedSnapshotphase는 loading/ready/cancelled/error/disposed, playing/paused/finished/seeking, commandIndex/commandCount/progress. 래퍼 phase는 이 핸들을 나타낸다.
playerVegaPlayerHandle 또는 undefined고급 하위 접근. 수명은 호스팅 핸들이 소유한다.
stagemanifest/modules/runtime/data/resources/ready현재 준비 단계. 자체 불러오기 라벨에 쓸 수 있다.
release문자열 또는 undefined매니페스트를 읽은 뒤 고른 불변 SDK 릴리스.
play/pause/next()=>voidready가 필요하며 아니면 Error. 위임된 재생 오류는 하위 error 이벤트를 쓴다.
seek(ratio:number)=>Promise<number>유한한 0~1 명령 경로 진행도. 복원된 경계를 반환한다. 하위 범위·수명 주기 오류는 의미를 유지한다.
subscribe(listener)=>구독 해제이후의 HaneokaStoryEvent를 받는다. 해제 후 구독은 Error.
cancel()=>void불러오는 중인 시작을 끝낸다. ready 뒤에는 dispose를 쓴다.
dispose()=>Promise<void>멱등. 취소하고, 시작 정리를 기다리고, 하위와 로더를 해제하고, 컨테이너 등록을 푼다. 정리 실패는 AggregateError로 거부될 수 있다. 재사용 전에 기다린다.

stage 이벤트는 {type:"stage",stage,release?}입니다. load/state/seek/error 형태는 아래 mountStory와 같습니다. 공개 모듈 매니페스트와 런타임 기술자는 같은 릴리스에 속합니다. 잘못된 모듈 경로, 런타임 기술자 해시·길이 불일치, 배포 출처 밖의 런타임 경로, 가져오기 실패, 리소스 오류는 ready를 거부합니다. 시작이 실패하면 하위와 등록을 정리하므로 호스트가 새 핸들을 만들 수 있습니다. 자신의 수명 주기를 닫기 위해 실패한 핸들도 해제하세요.

필수 위치 인수 container는 ownerDocument/defaultView가 있고 레이아웃 크기가 0이 아닌 브라우저 HTMLElement입니다. 브라우저가 아닌 컨테이너는 TypeError, 해제 전 같은 컨테이너에 두 번째 스토리를 마운트하면 Error입니다. 핸들을 동기로 반환합니다. TypeScript에서는 document나 source 중 정확히 하나를 고르세요. 구현은 주어진 source를 우선하며, 명령이 없거나 잘못되면 ready를 거부합니다.

옵션타입; 필수/기본값효과, 제약, 실패
documentAdvStory; source 대신 필수메모리 엔진 문서. commands는 배열이어야 하며, 잘못된 문서는 TypeError로 ready를 거부한다. 작성 입력이며 5개 언어 카탈로그일 필요는 없다.
sourceDataSource<AdvStory>; document 대신 필수load(context)가 엔진 문서를 반환한다. memory/http/file/사용자 정의 팩토리를 지원한다. 소스 실패는 ready를 거부한다.
server선택 문자열; undefined, 공개 어댑터는 intlsource에 넘기는 에셋 레지스트리. locale과 독립.
locale선택 문자열; en작성·재생 언어. 시작 시 플레이어 로캘과 셸 UI 언어도 설정한다.
assetsBase선택 절대 HTTP(S) 디렉터리; 소스 기준쿼리·프래그먼트 없음. 소스 리소스 출처를 덮어쓴다. 잘못된 기준은 TypeError.
resolveResource선택 (key,DataContext)→string/URL/Blob 또는 Promise; 키 그대로선언된 미디어·모델·글꼴 경로를 대응시킨다. 이미 해석된 URL은 한 번 통과한다. 해석기 오류는 ready·리소스 사용을 거부한다.
fetcher선택 (Request)→Promise<Response>; fetch데이터와 리소스 바이트는 코어 전송을 쓴다. 모듈·Core 스크립트 로더는 별도 로딩 경로가 있다.
credentialsomit/same-origin/include; omit로더 요청. 필요하면 서비스가 자격 증명·출처를 허용해야 한다.
headers선택 HeadersInit; 없음코어 전송 헤더. 스크립트·모듈 요청 헤더가 아니다.
maxBytes양의 안전한 정수; 67108864문서·리소스당 디코딩 바이트. 잘못된 값은 RangeError. GPU·캐시 합산 예산이 아니다.
onListenerError선택 (unknown)→void관찰자 오류를 받는 곳. 재생 결과와 격리된다.
themeportable/haneoka; portableHaneoka에는 테마·셸 플러그인이 필요하다. 기여가 없으면 ready를 거부한다.
brandingCornertop-left/top-right; top-left전체 크기 장면 위에 항상 보이는 반투명 모서리 표기. 숨기는 옵션은 없다.
shellfalse 또는 VegaPlayerShellOptionsportable 기본값은 false, Haneoka 기본값은 {initialScreen:"game"}. 전체 필드는 아래.
pluginsreadonly VegaPlugin[]; []richtext, portable UI, Three, 리소스 어댑터 뒤에 더해지는 제공자·명령. 실제 플러그인 setup/manifest/contributions가 필요하다. setup·의존성 실패는 ready를 거부한다.
rendererThreeRendererPluginOptions; {}렌더러와 포스트 텍스처 전체 설정은 아래.
resourceTimeoutMs양의 안전한 정수; 30000어댑터 리소스 읽기당 총 기한. 잘못된 값은 RangeError, 초과하면 TimeoutError로 거부한다. 스토리 전체 기한이 아니다.
signal선택 AbortSignal시작을 취소한다. ready 뒤의 중단은 이 플레이어를 해제한다. 리소스별 시간 제한은 별개.
onEvent선택 (StoryEmbedEvent)→void시작 전에 등록된다. subscribe와 같은 이벤트 형태.
resolveLocalizedText선택 (value:unknown)→{text:string,lang?:string}사용자 정의 텍스트 해석. 기본은 Vega 현지화. 해석기 형태를 만들어야 하며, 자체 예외는 진행 중인 작업을 실패시킬 수 있다.

공통 로더 옵션 9개는 자세한 코어 전송·리소스 계약을 따릅니다. 마운트 자동 재생이나 전체 loadTimeoutMs 옵션은 없습니다. 전체 불러오기에는 외부 신호·기한을 쓰고, 재생은 사용자 동작에서 시작하세요.

shell 필드타입/기본값의미
storage선택 VegaSaveStorage; 메모리read(key)→Promise<string|null>, write(key,value)→Promise<void>, remove(key)→Promise<void>, list(prefix)→Promise<readonly string[]>. 영속화는 호출자 백엔드가 맡는다. 잘못된 저장 데이터는 해당 저장·불러오기 작업을 실패시킨다.
projectId선택 문자열다듬은 명시 ID, 다음은 story.vegaProject.id/story.id, 다음은 vega-story. 저장 네임스페이스 식별.
settingsId선택 문자열; projectId별도 환경설정 네임스페이스. 저장된 설정이 initialSettings보다 우선한다.
initialSettings선택 Partial<VegaNarrativeSettings>; 표의 기본값셸 생성 시 적용한다. 이후 마운트가 바깥 locale을 플레이어·UI에 적용하므로 시작 언어에는 바깥 locale을 쓴다.
title선택 문자열다듬은 명시 제목, 다음은 현지화된 프로젝트·스토리 제목·이름, 다음은 projectId.
initialScreentitle/game일반 셸 기본값은 title, Haneoka 임베드의 암묵 셸은 game으로 시작한다.
onExitRequest선택 ()=>void/Promise<void>셸이 일시 정지·타이틀로 돌아간 뒤 Exit를 받는다. 이동·닫기는 호스트가 고른다. 콜백 실패는 그 셸 작업을 거부한다.
initialSettings 키타입/기본값범위와 동작
textSpeed숫자/1.1~5로 제한.
autoDelay숫자/1.5초, 0~30으로 제한.
masterVolume숫자/10~1로 제한.
bgmVolume숫자/.80~1로 제한.
voiceVolume숫자/10~1로 제한.
seVolume숫자/.90~1로 제한.
language문자열/auto스토리 언어 또는 auto. 저장 문자열은 최대 64자.
uiLanguage문자열/autoUI 언어. 서버와 별개.
reducedMotion불리언/false지원되는 UI 움직임을 줄인다.
highContrast불리언/false지원되는 UI 대비 설정.
instantText불리언/false읽을 텍스트를 바로 완전히 표시한다.
webText불리언/falseHaneoka 읽기 UI의 브라우저 텍스트 모드. 버전 참고는 아래.
textSize숫자/1.5~2로 제한.
subtitlesEnabled불리언/true있는 자막을 표시한다.
bgmEnabled불리언/trueBGM 사용 설정.

유한하지 않은 숫자 설정은 대체값을 쓰고, 불리언 설정은 정규화됩니다. 알 수 없는 셸 설정 키는 설정할 때 RangeError입니다. webText는 공개 소스 리비전 engine1916c57/theme1d5b20e/shell436ade8에 있습니다. 맞는 빌드나 그 리비전을 포함한 이후 CDN 기록을 쓰세요. 이전 r22 플레이어를 명시적으로 고정한다면 그 리비전이 지원하는 설정을 씁니다. 맞는 셸에서는 생성 시 shell.initialSettings.webText를 설정하거나 ready 뒤에 story.player.shell.setSetting("webText", true)를 호출합니다.

필드타입/기본값의미와 조합
rendererIdvega-three-webgl2/haneoka-three-webgl2; vega-three-webgl2장면 제공자 경로. 보통 임베드의 백엔드를 그대로 둔다.
profilevega/unity-adv; vega제공자 장면 프로필.
staticPortraitFallback불리언/true모델 제공자가 없을 때 정적 초상화 대체를 허용한다.
colorGradingModeldr/hdr; ldr포스트 색상 파이프라인.
contributionId문자열/haneoka.renderer-three설치되는 렌더러 기여 식별.
contributionName문자열/Vega Three WebGL2사람이 읽는 기여 이름.
backendvega-three-webgl2/haneoka-three-webgl2; vega-three-webgl2기여 경로. 임베드는 vega-three-webgl2를 고른다. 호환되지 않는 등록은 플레이어 생성을 거부한다.
postTextureResolver선택 ThreePostTextureResolverpostTextureAssets와 함께 쓸 수 없다(TypeError). 해석기가 제공한 텍스처를 소유하며, 선택 dispose는 플러그인 수명에 묶인다.
postTextureAssets선택 Readonly<Record<string,string>> 또는 ReadonlyMap<string,string>안정적인 렌더러 텍스처 키에 대응한 허가된 URL. 단독으로 주면 해석기를 만든다.

사용자 정의 postTextureResolver에는 필수 resolve(reference:unknown,usage)→Three.Texture|null, 선택 preload(requests,signal?)→Promise<void>, 선택 dispose()가 있습니다. 각 요청은 {reference,usage}이고 usage에는 film-grain, color-lookup, lens-dirt가 포함됩니다. null을 반환하면 기존 파이프라인 대체를 허용합니다. 고른 Three 인스턴스와 호환되는 텍스처를 반환하고, 정리는 제공자가 맡습니다. 매개변수 충돌은 플러그인 생성 중 동기로 예외가 되고, 불러오기·렌더링·제공자 오류는 ready와 이벤트로 도착합니다.

haneokaStorySource({id,apiBase?})에는 공개 스토리 ID가 필요합니다. apiBase 기본값은 https://haneoka.org/api/v1/이고 코어 디렉터리 검증을 따릅니다. 고른 스토리, story-runtime, 선언된 Live2D 메타데이터만 마운트의 server/locale로 불러옵니다. 스토리·모델이 없거나 카탈로그 레코드가 잘못되면 불러오기를 거부합니다. 릴리스나 계정 인수는 필요 없습니다.

cubismStoryPlugin({moduleUrl,runtime})에는 별도 라이선스를 받은 절대 모듈 URL과 runtime.cubismCoreUrl이 필요합니다. runtime.cubism2CoreUrl?과 runtime.motionSyncCoreUrl?은 선택적인 버전별 스크립트 URL입니다. 모듈은 createCubismWebRuntimeAdapter를 내보내야 하며, 내보내기나 제공자 기능이 없으면 모델 준비를 거부합니다. 이 플러그인은 모델이 필요할 때 어댑터를 지연 로드하고, 제공자 수명 주기를 유지하며, Core를 포함하지 않습니다. 호환되는 어댑터와 Core 버전을 함께 유지하세요.

haneokaStoryPlugins에는 옵션이 없으며 테마+기본 셸 플러그인 목록입니다. createHaneokaThemeAssetsPlugin(provider)는 선택 assets(), resolveChatImage(value,kind:icon|stamp), resolveSourceAsset(path) 콜백을 받습니다. assets는 선택 URL 필드 arrow,auto,chatWindow,chatBackground,chatBackgroundFallback,chatLock,chatTextBox,chatComposerPlus,chatComposerPhoto,chatComposerPicture,chatComposerSmile,chatComposerMicrophone,chatIcons,fast,frame,fullscreen,subtitles,skip을 반환합니다. 덮어쓰지 않으면 기존 테마 에셋을 씁니다. HANEOKA_POST_TEXTURE_ASSETS는 고정된 허가 렌더러 에셋 맵이며, 필요하면 renderer.postTextureAssets로 주세요.

AdvStory에는 commands(이 마운트에서 필수), 선택 runtime과 localization, 소스가 정의한 필드가 있습니다. localization에는 선택 locales:string, defaultLocale:string, arrayOrder:string가 들어 있으며, 작성자가 정의하는 것으로 5개 언어 고정 요구가 아닙니다. runtime은 Vega의 Partial<AdvRuntimeConfig>이며 엔진을 통해 유지됩니다. 개별 명령 opcode와 리소스 구조는 고른 명령·제공자 스키마를 따릅니다. 예제는 공개 프로토콜의 Stage25와 Talk2를 씁니다.

핸들 멤버시그니처동작·오류
readyPromise<void>렌더러·리소스·글꼴·탐색 색인 시작. onEvent가 error를 관찰해도 거부는 남는다.
snapshot읽기 전용 객체phase,playing,paused,finished,seeking,commandIndex,commandCount,progress. phase는 loading/ready/cancelled/error/disposed.
playerVegaPlayerHandle 또는 undefined준비된 고급 엔진 핸들: id,player,lifetime,root,slots,shell?,dispose(). 해제는 임베드에 맡긴다.
play / pause / next()=>voidready 필요(아니면 Error). Play는 재개한다. 비동기 인터프리터 실패는 error 이벤트가 된다. Next는 읽기 진행을 요청한다.
seek(ratio:number)=>Promise<number>유한한 0~1, 아니면 RangeError. 실제 명령 인덱스를 반환한다. 재생·일시 정지 의도를 유지한다.
subscribe(listener)=>구독 해제이후 이벤트. 해제 후 구독은 Error.
cancel()=>void불러오는 중인 시작만. ready 뒤에는 아무 동작도 하지 않는다. 다시 시도하기 전에 해제한다.
dispose()=>Promise<void>공유·멱등 Promise. 로더·URL보다 먼저 엔진을 해제하고, 호스트 DOM·리스너를 없애고, 시작 정리를 기다린다. 정리 실패는 AggregateError.

이벤트는 {type:"load",event:EmbedEvent}, {type:"state",snapshot}, {type:"seek",commandIndex}, {type:"error",error}입니다. load 이벤트 필드는 코어를 따릅니다. state는 바뀐 스냅샷을 100ms마다, 그리고 조작 시 공개합니다. 리스너 예외는 격리됩니다. 해제는 phase disposed로 끝나며, 그 state/load 이벤트는 생성 시 리스너가 관찰할 수 있습니다. 리소스·소스·플러그인 실패는 실제 오류를 유지하고, 취소는 signal.reason으로 거부합니다. 이 임베드는 두 번째 오류 코드 체계를 더하지 않습니다.

이 페이지는 안정 별칭을 씁니다. 이전 불변 r22 플레이어는 전용 줄 표기 레이아웃을 유지합니다. 리비전을 명시적으로 고정할 때는 엔진, 테마, 셸, 런타임 버전의 호환성을 유지하세요.