Model Norn and TeX as embedded Eve surfaces
This commit is contained in:
parent
1ad88187df
commit
f1caec28cd
3 changed files with 220 additions and 43 deletions
105
dist/eve.js
vendored
105
dist/eve.js
vendored
|
|
@ -37,6 +37,22 @@
|
|||
});
|
||||
}
|
||||
|
||||
function scenePlacement(input = {}) {
|
||||
return compact({
|
||||
space: input.space || "scene",
|
||||
anchor: input.anchor,
|
||||
slot: input.slot,
|
||||
mode: input.mode || input.fit || "overlay",
|
||||
quad: input.quad,
|
||||
transform: input.transform,
|
||||
zIndex: input.zIndex,
|
||||
opacity: input.opacity,
|
||||
chromaKey: input.chromaKey,
|
||||
occlusion: input.occlusion,
|
||||
lighting: input.lighting,
|
||||
});
|
||||
}
|
||||
|
||||
function styleTokens(input = {}) {
|
||||
return {
|
||||
themeId: input.themeId || "sai.vn.default",
|
||||
|
|
@ -81,7 +97,7 @@
|
|||
};
|
||||
}
|
||||
|
||||
function graphComponent(graph = {}) {
|
||||
function nornComponent(graph = {}) {
|
||||
if (!graph.nodes?.length) return null;
|
||||
const rawXs = graph.nodes.map((node) => node.viewX ?? node.x ?? 0);
|
||||
const rawYs = graph.nodes.map((node) => node.viewY ?? node.y ?? 0);
|
||||
|
|
@ -98,31 +114,76 @@
|
|||
|
||||
return component(
|
||||
"sai.graph",
|
||||
"graph",
|
||||
"embed.norn",
|
||||
{
|
||||
label: graph.label || "Ink knot graph",
|
||||
layout: graph.layout || "norn.2d",
|
||||
nodes: graph.nodes.map((node) =>
|
||||
compact({
|
||||
id: node.id,
|
||||
label: node.label || node.id,
|
||||
target: node.target || node.knot || node.id,
|
||||
x: normalizeX(node.viewX ?? node.x ?? 0),
|
||||
y: normalizeY(node.viewY ?? node.y ?? 0),
|
||||
rank: node.rank,
|
||||
order: node.order,
|
||||
current: Boolean(node.current),
|
||||
visited: Boolean(node.visited),
|
||||
available: Boolean(node.available),
|
||||
weight: node.weight,
|
||||
}),
|
||||
),
|
||||
edges: graph.edges || [],
|
||||
engine: {
|
||||
id: "norn",
|
||||
contract: "gamecult.norn.surface.v1",
|
||||
web: {
|
||||
wasm: graph.solver_wasm || graph.solverWasm,
|
||||
package: "@gamecult/norn-viewer",
|
||||
},
|
||||
native: {
|
||||
rustCrate: "norn-rs",
|
||||
direct2dAdapter: "fensalir.norn.direct2d",
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
mode: graph.layout || "norn.2d",
|
||||
iterations: graph.iterations,
|
||||
rankGap: graph.rank_gap || graph.rankGap,
|
||||
nodeGap: graph.node_gap || graph.nodeGap,
|
||||
edgeLength: graph.edge_length || graph.edgeLength,
|
||||
},
|
||||
graph: {
|
||||
nodes: graph.nodes.map((node) =>
|
||||
compact({
|
||||
id: node.id,
|
||||
label: node.label || node.id,
|
||||
target: node.target || node.knot || node.id,
|
||||
x: normalizeX(node.viewX ?? node.x ?? 0),
|
||||
y: normalizeY(node.viewY ?? node.y ?? 0),
|
||||
rank: node.rank,
|
||||
order: node.order,
|
||||
current: Boolean(node.current),
|
||||
visited: Boolean(node.visited),
|
||||
available: Boolean(node.available),
|
||||
weight: node.weight,
|
||||
}),
|
||||
),
|
||||
edges: graph.edges || [],
|
||||
},
|
||||
interaction: {
|
||||
nodeAction: "story.jump",
|
||||
focusFollowsStory: true,
|
||||
rendererMustUseNorn: true,
|
||||
},
|
||||
placement: scenePlacement(graph.placement || graph.diegetic),
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
function texComponents(items = []) {
|
||||
return items.map((item, index) =>
|
||||
component(`sai.tex.${item.id || index}`, "embed.tex", {
|
||||
label: item.label,
|
||||
source: item.source || item.tex || item.latex,
|
||||
sourceUri: item.sourceUri,
|
||||
format: item.format || "latex",
|
||||
display: item.display || "block",
|
||||
macros: item.macros,
|
||||
renderer: {
|
||||
web: item.webRenderer || "katex",
|
||||
native: item.nativeRenderer || "tex-renderer",
|
||||
direct2d: item.direct2dRenderer || "tex-to-directwrite",
|
||||
},
|
||||
placement: scenePlacement(item.placement || item.diegetic),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function spriteComponents(sprites = []) {
|
||||
return sprites.map((sprite, index) =>
|
||||
component(`sai.sprite.${sprite.actorName || index}`, "image.sprite", {
|
||||
|
|
@ -186,8 +247,9 @@
|
|||
const speaker = input.speaker || metadata.speaker || "Void";
|
||||
const line = input.line || "";
|
||||
const title = input.title || scene.label || "Sai Visual Novel";
|
||||
const graph = graphComponent(input.graph);
|
||||
const norn = nornComponent(input.graph || input.norn);
|
||||
const variables = variableComponents(input.variables || []);
|
||||
const tex = texComponents(input.tex || input.texSurfaces || []);
|
||||
|
||||
const stageChildren = [
|
||||
component("sai.background", "image.background", {
|
||||
|
|
@ -196,7 +258,8 @@
|
|||
fit: "cover",
|
||||
pixelArt: style.tokens.pixelArt,
|
||||
}),
|
||||
graph,
|
||||
norn,
|
||||
component("sai.embeds", "layer.embedded-surfaces", {}, tex),
|
||||
component("sai.sprites", "layer.sprites", {}, spriteComponents(input.sprites)),
|
||||
component("sai.cards", "layer.cards", {}, domCardComponents(input.domCards)),
|
||||
component(
|
||||
|
|
|
|||
53
docs/eve.md
53
docs/eve.md
|
|
@ -41,12 +41,31 @@ const surface = window.SaiEve.createVisualNovelSurface({
|
|||
graph: {
|
||||
label: "GameCult Ink Knot Graph",
|
||||
layout: "norn.2d",
|
||||
solver_wasm: "/static/interactive/norn/norn-rs.wasm",
|
||||
placement: {
|
||||
space: "scene",
|
||||
anchor: "whiteboard",
|
||||
mode: "keystone",
|
||||
quad: [[0.16, 0.18], [0.58, 0.13], [0.62, 0.50], [0.14, 0.56]]
|
||||
},
|
||||
nodes: [
|
||||
{ id: "hub", label: "Hub", target: "hub", x: 0, y: 0, current: true },
|
||||
{ id: "eve", label: "Eve", target: "eve", x: 1, y: 0 },
|
||||
],
|
||||
edges: [{ source: "hub", target: "eve" }],
|
||||
},
|
||||
tex: [
|
||||
{
|
||||
id: "bifrost-log-power",
|
||||
label: "Bifrost voting weight",
|
||||
source: "\\\\mathrm{votes}(p)=1+\\\\lfloor\\\\log_b(1+p)\\\\rfloor",
|
||||
placement: {
|
||||
space: "scene",
|
||||
anchor: "whiteboard",
|
||||
mode: "keystone"
|
||||
}
|
||||
}
|
||||
],
|
||||
choices: [
|
||||
{ text: "What is Eve trying to turn the web into?", targetPath: "eve" },
|
||||
],
|
||||
|
|
@ -73,13 +92,45 @@ without each renderer inventing its own theme protocol. A renderer may expose
|
|||
native color pickers, segmented controls, or accessibility settings, but the
|
||||
accepted style state belongs to the provider and comes back as surface tokens.
|
||||
|
||||
## Embedded Scene Surfaces
|
||||
|
||||
The point of the Eve module is not to flatten Sai into a dashboard. A VN scene
|
||||
can contain portable interactive surfaces that are diegetically placed inside
|
||||
the scene. The graph map is one of those surfaces.
|
||||
|
||||
`embed.norn` declares that a renderer must embed a Norn graph runtime, not draw
|
||||
a decorative graph lookalike. Web can use Norn WASM and `@gamecult/norn-viewer`;
|
||||
native targets can use `norn-rs` directly or through a platform adapter.
|
||||
|
||||
`embed.tex` declares TeX/LaTeX content as a portable rich math/layout surface.
|
||||
Web renderers may use KaTeX or another TeX path; native renderers should lower
|
||||
the same source into their local text/vector stack.
|
||||
|
||||
Both kinds accept `placement`:
|
||||
|
||||
```json
|
||||
{
|
||||
"space": "scene",
|
||||
"anchor": "whiteboard",
|
||||
"mode": "keystone",
|
||||
"quad": [[0.16, 0.18], [0.58, 0.13], [0.62, 0.50], [0.14, 0.56]],
|
||||
"chromaKey": { "color": "#00ff00", "tolerance": 0.12 },
|
||||
"occlusion": "sprite-mask:whiteboard-hand"
|
||||
}
|
||||
```
|
||||
|
||||
That is the hook for character-pointing-at-whiteboard shots, chromakey panels in
|
||||
sprite assets, and perspective/skew/keystone fitting onto scene props.
|
||||
|
||||
## Component Kinds
|
||||
|
||||
The first VN surface uses:
|
||||
|
||||
- `vn.stage`
|
||||
- `image.background`
|
||||
- `graph`
|
||||
- `embed.norn`
|
||||
- `embed.tex`
|
||||
- `layer.embedded-surfaces`
|
||||
- `layer.sprites`
|
||||
- `image.sprite`
|
||||
- `layer.cards`
|
||||
|
|
|
|||
105
src/eve.js
105
src/eve.js
|
|
@ -37,6 +37,22 @@
|
|||
});
|
||||
}
|
||||
|
||||
function scenePlacement(input = {}) {
|
||||
return compact({
|
||||
space: input.space || "scene",
|
||||
anchor: input.anchor,
|
||||
slot: input.slot,
|
||||
mode: input.mode || input.fit || "overlay",
|
||||
quad: input.quad,
|
||||
transform: input.transform,
|
||||
zIndex: input.zIndex,
|
||||
opacity: input.opacity,
|
||||
chromaKey: input.chromaKey,
|
||||
occlusion: input.occlusion,
|
||||
lighting: input.lighting,
|
||||
});
|
||||
}
|
||||
|
||||
function styleTokens(input = {}) {
|
||||
return {
|
||||
themeId: input.themeId || "sai.vn.default",
|
||||
|
|
@ -81,7 +97,7 @@
|
|||
};
|
||||
}
|
||||
|
||||
function graphComponent(graph = {}) {
|
||||
function nornComponent(graph = {}) {
|
||||
if (!graph.nodes?.length) return null;
|
||||
const rawXs = graph.nodes.map((node) => node.viewX ?? node.x ?? 0);
|
||||
const rawYs = graph.nodes.map((node) => node.viewY ?? node.y ?? 0);
|
||||
|
|
@ -98,31 +114,76 @@
|
|||
|
||||
return component(
|
||||
"sai.graph",
|
||||
"graph",
|
||||
"embed.norn",
|
||||
{
|
||||
label: graph.label || "Ink knot graph",
|
||||
layout: graph.layout || "norn.2d",
|
||||
nodes: graph.nodes.map((node) =>
|
||||
compact({
|
||||
id: node.id,
|
||||
label: node.label || node.id,
|
||||
target: node.target || node.knot || node.id,
|
||||
x: normalizeX(node.viewX ?? node.x ?? 0),
|
||||
y: normalizeY(node.viewY ?? node.y ?? 0),
|
||||
rank: node.rank,
|
||||
order: node.order,
|
||||
current: Boolean(node.current),
|
||||
visited: Boolean(node.visited),
|
||||
available: Boolean(node.available),
|
||||
weight: node.weight,
|
||||
}),
|
||||
),
|
||||
edges: graph.edges || [],
|
||||
engine: {
|
||||
id: "norn",
|
||||
contract: "gamecult.norn.surface.v1",
|
||||
web: {
|
||||
wasm: graph.solver_wasm || graph.solverWasm,
|
||||
package: "@gamecult/norn-viewer",
|
||||
},
|
||||
native: {
|
||||
rustCrate: "norn-rs",
|
||||
direct2dAdapter: "fensalir.norn.direct2d",
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
mode: graph.layout || "norn.2d",
|
||||
iterations: graph.iterations,
|
||||
rankGap: graph.rank_gap || graph.rankGap,
|
||||
nodeGap: graph.node_gap || graph.nodeGap,
|
||||
edgeLength: graph.edge_length || graph.edgeLength,
|
||||
},
|
||||
graph: {
|
||||
nodes: graph.nodes.map((node) =>
|
||||
compact({
|
||||
id: node.id,
|
||||
label: node.label || node.id,
|
||||
target: node.target || node.knot || node.id,
|
||||
x: normalizeX(node.viewX ?? node.x ?? 0),
|
||||
y: normalizeY(node.viewY ?? node.y ?? 0),
|
||||
rank: node.rank,
|
||||
order: node.order,
|
||||
current: Boolean(node.current),
|
||||
visited: Boolean(node.visited),
|
||||
available: Boolean(node.available),
|
||||
weight: node.weight,
|
||||
}),
|
||||
),
|
||||
edges: graph.edges || [],
|
||||
},
|
||||
interaction: {
|
||||
nodeAction: "story.jump",
|
||||
focusFollowsStory: true,
|
||||
rendererMustUseNorn: true,
|
||||
},
|
||||
placement: scenePlacement(graph.placement || graph.diegetic),
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
function texComponents(items = []) {
|
||||
return items.map((item, index) =>
|
||||
component(`sai.tex.${item.id || index}`, "embed.tex", {
|
||||
label: item.label,
|
||||
source: item.source || item.tex || item.latex,
|
||||
sourceUri: item.sourceUri,
|
||||
format: item.format || "latex",
|
||||
display: item.display || "block",
|
||||
macros: item.macros,
|
||||
renderer: {
|
||||
web: item.webRenderer || "katex",
|
||||
native: item.nativeRenderer || "tex-renderer",
|
||||
direct2d: item.direct2dRenderer || "tex-to-directwrite",
|
||||
},
|
||||
placement: scenePlacement(item.placement || item.diegetic),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function spriteComponents(sprites = []) {
|
||||
return sprites.map((sprite, index) =>
|
||||
component(`sai.sprite.${sprite.actorName || index}`, "image.sprite", {
|
||||
|
|
@ -186,8 +247,9 @@
|
|||
const speaker = input.speaker || metadata.speaker || "Void";
|
||||
const line = input.line || "";
|
||||
const title = input.title || scene.label || "Sai Visual Novel";
|
||||
const graph = graphComponent(input.graph);
|
||||
const norn = nornComponent(input.graph || input.norn);
|
||||
const variables = variableComponents(input.variables || []);
|
||||
const tex = texComponents(input.tex || input.texSurfaces || []);
|
||||
|
||||
const stageChildren = [
|
||||
component("sai.background", "image.background", {
|
||||
|
|
@ -196,7 +258,8 @@
|
|||
fit: "cover",
|
||||
pixelArt: style.tokens.pixelArt,
|
||||
}),
|
||||
graph,
|
||||
norn,
|
||||
component("sai.embeds", "layer.embedded-surfaces", {}, tex),
|
||||
component("sai.sprites", "layer.sprites", {}, spriteComponents(input.sprites)),
|
||||
component("sai.cards", "layer.cards", {}, domCardComponents(input.domCards)),
|
||||
component(
|
||||
|
|
|
|||
Loading…
Reference in a new issue