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
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ live UI, with the chart's music and sound effects, auto-played at Perfect. The d
|---|---|
| `ournotes-player` | `ChartPlayer`, `ChartSession`, `AssetStore`, `loadLiveStore`, `defineOurnotesPlayer`, `OurnotesPlayerElement`, `LIVE_SPEEDS`, `LIVE_OPTIONS`, `LIVE_OPTION_GROUPS`, `LiveSettingsError`, `PLAYER_LANGUAGES`, `formatTime` |
| `ournotes-player/element` | the same exports; importing it defines `<ournotes-player>` |
| `ournotes-player/ui` (experimental) | `UILibrary`, `UISession`, `UIPlayer`, `OurnotesUIElement`, `defineOurnotesUI`, `cameraProjection`; [prefab preview API and limits](ui-preview.md) |
| `ournotes-player/ui` (experimental) | `UIPack`, `UI_FORMAT`, `UI_LIBRARY`, `UI_PRODUCT`, `UILibrary`, `UISession`, `UIPlayer`, `OurnotesUIElement`, `defineOurnotesUI`, `cameraProjection`; [prefab preview API and limits](ui-preview.md) |
| `ournotes-player/ui/element` (experimental) | the same UI exports; importing it defines `<ournotes-ui>` |
| `ournotes-player/replay/preset` | `parseJustJudgementTypes`, `applyAccuracyPreset`, `applySegmentPreset`: declared input plans that set the judgement grades of an `ournotes.replay/1` request in place |

Expand Down
42 changes: 20 additions & 22 deletions docs/ui-preview.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
# UI prefab preview (experimental)

The optional `ournotes-player/ui` module previews serialized UI assets exported by `nnnotes ui`. It uses Canvas 2D
The optional `ournotes-player/ui` module previews serialized UI assets exported by `nnnotes export ui`. It uses Canvas 2D
and is an inspector, not the game's GPU renderer or an implementation of every page Presenter. It does not change
the chart, story or Live2D entry points. The repository, example and npm package include no game assets.

## Load a library

Export data outside this repository with [nnnotes ui](https://github.com/MetaSekaiLab/nnnotes), then serve that data
Export data outside this repository with [nnnotes export ui](https://github.com/MetaSekaiLab/nnnotes), then serve that data
over HTTP. Cross-origin resources need CORS. The index and packs use the
`ournotes-ui-library` / `ournotes-ui-pack` format (schema 1). Legacy library indexes with
`dependency-controllers.json` are also accepted.
`nnnotes.ui-library/1` index and `nnnotes.product/1` manifests for `ui.pack`. Each product contains `ui.json`
(`nnnotes.ui-pack/1`) and the shared object files of its prefabs, controllers, clips, images and fonts.
See [ui-library schema](../schema/ui-library.schema.json) and [ui-pack schema](../schema/ui-pack.schema.json).

```js
import { UILibrary, UIPlayer } from 'ournotes-player/ui';

const library = await UILibrary.load('/ui-data/index.json');
const entry = library.entries.find(e => e.kind === 'prefab' && e.file);
const entry = library.entries.find(e => e.kind === 'prefab' && e.manifest);
const player = await UIPlayer.create(document.querySelector('#stage'), { library, entry });
await player.playState('Open', { animator: player.animators[0].index });
player.play();
```

`UIPlayer.create(host, {src: '/ui-data/packs/ID.json'})` loads a standalone pack. A standalone pack can use an
inline controller; separately exported controllers require a `UILibrary`. Importing `ournotes-player/ui` does not
`UIPlayer.create(host, {src: '/ui-data/uipacks/ID.json'})` loads one product, including its controllers and clips.
`UIPack.load(url)` loads the same native object data for a DOM-independent `UISession`. Importing `ournotes-player/ui` does not
register elements or require a DOM at import time. Constructing the player requires a browser Canvas 2D context.
The [example viewer](../examples/ui/index.html) takes a user-supplied library URL and provides selection and motion
controls; it has no bundled sample data.
Expand All @@ -48,9 +49,9 @@ an entry record. Ambiguous names throw; same-named controllers resolve by their

`UISession(pack, {bindings: true})` is DOM-independent. It clones the input and provides:

- `edit(node, component, field, value)` for a node index, stable nodeId or unique path/name. `component: null`
- `edit(node, component, field, value)` for a node index, stable object ID or unique path/name. `component: null`
edits a node field; dotted field paths are supported and prototype-related paths are rejected.
- `setController(node, {document, resources})`, `playState(nameOrIndex)`, `selectClip(nameOrIndex)` and
- `setController(node, pack.controller(nodeIndex))`, `playState(nameOrIndex)`, `selectClip(nameOrIndex)` and
`selectSequence(node)`.
- `setParameter(name, value)`, `seek(seconds)`, `update(deltaSeconds)`, `reset()` and `prepare()`.

Expand Down Expand Up @@ -128,7 +129,7 @@ fail explicitly. Runtime camera composition and Presenter-driven scene sizing re
`framing: 'content'` uses visible graphic bounds, including labels and graphics outside the root, and crops
only the output canvas's transparent margins; source geometry is unchanged. Its bounds are conservative
RectTransform/mask bounds, not an alpha-pixel scan. Default `framing: 'root'` retains the previous viewport.
Render results expose `regions[nodeId || path]`, four projected corners in top-left, top-right, bottom-right,
Render results expose `regions[id || path]`, four projected corners in top-left, top-right, bottom-right,
bottom-left order for visible nodes. Coordinates are logical root coordinates before canvas padding/scale;
convert a point to bitmap coordinates as `(point - bounds.min + padding) * scale`. Stable IDs avoid ambiguity
for duplicate instance paths. Regions describe the node rectangle; caller input handling and mask-aware hit
Expand All @@ -141,18 +142,15 @@ crossfade are preview approximations; long Animator advances are limited to 1,00
unbound images or active flags may be placeholders, not evidence that a component is obsolete. Use bindings,
edits or your own fixtures for its real use case.

The rendering review was based on international Android 1.0.1 (versionCode 25), not a full JP UI comparison.
The data reader accepts a separately exported JP library; differing schemas/fonts can remain unsupported.
Neither region is advertised as pixel-identical or runtime-complete.
## Data and verification

## Evidence and verification
Prefab nodes retain `pathId`, transforms and native `components[{type, script, fields}]`. A `UIPack` owns mutable
node copies while sharing immutable `AssetStore` data; `clone()` copies only the preview state. `UIController.document`
is the native controller, whose `clips` are references; `UIController.clip(index)` reads a clip by object ID.
Animation binding names come from the product's per-prefab table. Texture/font reads follow object references.

Game-specific state-binding rules were studied in the international binary's methods such as UIToggle.SwitchObjects
(`0x6ac4e74`), UIToggleButtonFrame (`0x647fcb8` / `0x64888e0`), DOTweenSequence.CreateSequence (`0x6aded88`) and
SimpleAnimationTrigger.PlayAnimation (`0x6b1f2f8`). Unity geometry/layout semantics are marked ENGINE in the helpers.
GetChildSizes' force-expand operation was checked in native instructions at `0xbd29fec..0xbd2a004`; its C dump
omitted that branch. These addresses are version-specific evidence, not runtime offsets or JP method identities.
`UIPack.browserFontFamily` optionally supplies a CSS font family for preview text. Otherwise embedded font files
are loaded per referenced font face, and numeric font glyphs use their native atlas pages and metrics.

Unit tests use only synthetic records/fonts and cover loading, cancellation, lifecycle, duplicate instances,
curves, state seek, sequence timing and layout invariants. Separate local visual review uses user-supplied data
outside the repository. A successful static render/curve audit is not pixel-fidelity verification.
Synthetic tests cover product loading, cancellation, lifecycle, duplicate instances, curves, state seek, sequence
timing and layout invariants.
2 changes: 1 addition & 1 deletion examples/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<main>
<aside><label for="search">Find a prefab</label><input id="search" type="search" placeholder="Name or key"><select id="assets" size="15" aria-label="Prefabs"></select><p id="count">No library loaded</p></aside>
<section class="viewer">
<div class="title"><div><h2 id="name">Choose a library</h2><p id="key">Export with nnnotes ui and serve the output over HTTP.</p></div><label><input id="hidden" type="checkbox">Hidden layers</label><label><input id="bounds" type="checkbox">Bounds</label></div>
<div class="title"><div><h2 id="name">Choose a library</h2><p id="key">Export with nnnotes export ui and serve the output over HTTP.</p></div><label><input id="hidden" type="checkbox">Hidden layers</label><label><input id="bounds" type="checkbox">Bounds</label></div>
<div class="motions"><select id="animator" aria-label="Animator" disabled><option value="">Animator</option></select><select id="state" aria-label="Animator state" disabled><option value="">State</option></select><select id="clip" aria-label="Animation clip" disabled><option value="">Clip</option></select><select id="sequence" aria-label="DOTween sequence" disabled><option value="">Sequence</option></select><button id="play" disabled>Play</button><button id="reset" disabled>Reset</button></div>
<div id="parameters"></div>
<div class="stage"><canvas id="canvas" aria-label="Prefab preview"></canvas><p id="empty">Your exported prefab will appear here.</p></div>
Expand Down
6 changes: 3 additions & 3 deletions examples/ui/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function list(){

async function select(id){
const revision=++generation,entry=library.find(id);player?.destroy();player=null;canvas.hidden=true;$('#empty').hidden=false;$('#empty').textContent='Loading prefab…';
$('#name').textContent=entry.name;$('#key').textContent=entry.key||entry.file;$('#parameters').replaceChildren();
$('#name').textContent=entry.name;$('#key').textContent=entry.key||entry.manifest;$('#parameters').replaceChildren();
fill($('#state'),'State',[]);fill($('#clip'),'Clip',[]);$('#play').disabled=$('#time').disabled=$('#reset').disabled=true;
const loaded=await UIPlayer.create(canvas,{library,entry,showHidden:$('#hidden').checked,bounds:$('#bounds').checked});
if(revision!==generation){loaded.destroy();return;}player=loaded;canvas.hidden=false;$('#empty').hidden=true;$('#reset').disabled=false;
Expand All @@ -35,13 +35,13 @@ async function select(id){

$('#open').onsubmit=async e=>{
e.preventDefault();const button=e.currentTarget.querySelector('button');button.disabled=true;
try{library=await UILibrary.load($('#source').value);entries=library.entries.filter(e=>['prefab','embedded'].includes(e.kind)&&e.file&&e.status!=='failed');list();if(!entries.length)throw Error('No exported prefabs in this library');$('#assets').value=entries[0].id;await select(entries[0].id);}catch(e){error(e);}finally{button.disabled=false;}
try{library=await UILibrary.load($('#source').value);entries=library.entries.filter(e=>e.kind==='prefab'&&e.manifest);list();if(!entries.length)throw Error('No exported prefabs in this library');$('#assets').value=entries[0].id;await select(entries[0].id);}catch(e){error(e);}finally{button.disabled=false;}
};
$('#search').oninput=list;$('#assets').onchange=run(()=>select($('#assets').value));
$('#animator').onchange=run(async()=>{
$('#parameters').replaceChildren();fill($('#state'),'State',[]);fill($('#clip'),'Clip',[]);await player.reset();$('#sequence').value='';if(!$('#animator').value)return;
const exported=await player.controller(Number($('#animator').value)),controller=exported.document,machine=controller.stateMachines?.[controller.layers?.[0]?.stateMachine||0];
fill($('#state'),'State',(machine?.states||[]).map((s,i)=>({name:s.name,value:i})));fill($('#clip'),'Clip',(controller.clips||[]).map((c,i)=>({name:c.clip,value:i})));
fill($('#state'),'State',(machine?.states||[]).map((s,i)=>({name:s.name,value:i})));fill($('#clip'),'Clip',(controller.clips||[]).map((_,i)=>({name:exported.clip(i).name,value:i})));
for(const parameter of controller.parameters||[]){
if(parameter.type===9){const button=document.createElement('button');button.textContent=parameter.name;button.onclick=run(()=>player.setParameter(parameter.name,true));$('#parameters').append(button);}
else{const label=document.createElement('label');label.textContent=parameter.name;const input=document.createElement('input');input.type=parameter.type===4?'checkbox':'number';input.step=parameter.type===3?'1':'any';input.value='0';input.onchange=run(()=>player.setParameter(parameter.name,input.type==='checkbox'?input.checked:Number(input.value)));label.append(input);$('#parameters').append(label);}
Expand Down
148 changes: 148 additions & 0 deletions schema/ui-library.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/empty-sekai/ournotes-player/main/schema/ui-library.schema.json",
"type": "object",
"required": [
"format",
"assets",
"embedded",
"controllers"
],
"properties": {
"format": {
"const": "nnnotes.ui-library/1"
},
"assets": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"name",
"manifest",
"key",
"kind",
"roots"
],
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"name": {
"type": "string"
},
"manifest": {
"type": "string",
"minLength": 1
},
"roots": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"key": {
"type": "string",
"minLength": 1
},
"kind": {
"type": "string"
},
"nodeCount": {
"type": "integer",
"minimum": 0
}
}
}
},
"embedded": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"name",
"manifest",
"roots",
"kind"
],
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"name": {
"type": "string"
},
"manifest": {
"type": "string",
"minLength": 1
},
"roots": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"key": {
"type": "string",
"minLength": 1
},
"kind": {
"type": "string"
},
"nodeCount": {
"type": "integer",
"minimum": 0
}
}
}
},
"controllers": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"name",
"manifest"
],
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"name": {
"type": "string"
},
"manifest": {
"type": "string",
"minLength": 1
},
"roots": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"key": {
"type": "string",
"minLength": 1
},
"kind": {
"type": "string"
},
"nodeCount": {
"type": "integer",
"minimum": 0
}
}
}
}
},
"title": "UI product library"
}
Loading
Loading