@cardenelabs/dragon 0.34.0 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -720
- package/dist/cli.cjs +4529 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +4523 -0
- package/dist/cli.js.map +1 -0
- package/dist/export.cjs +3912 -0
- package/dist/export.cjs.map +1 -0
- package/dist/export.d.cts +36 -0
- package/dist/export.d.ts +36 -0
- package/dist/export.js +3907 -0
- package/dist/export.js.map +1 -0
- package/dist/fonts.cjs +7 -0
- package/dist/fonts.cjs.map +1 -0
- package/dist/fonts.js +4 -0
- package/dist/fonts.js.map +1 -0
- package/dist/index.cjs +4105 -20356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -5522
- package/dist/index.d.ts +227 -5522
- package/dist/index.js +4091 -20283
- package/dist/index.js.map +1 -1
- package/dist/player.global.js +2 -0
- package/dist/schema-BBWfniwH.d.cts +112 -0
- package/dist/schema-BBWfniwH.d.ts +112 -0
- package/package.json +53 -27
- package/src/cli.ts +237 -0
- package/src/export/capture.ts +277 -0
- package/src/export/fonts.ts +68 -0
- package/src/export/html.ts +32 -0
- package/src/export/index.ts +146 -0
- package/src/field-messages.ts +104 -0
- package/src/figure-text.ts +42 -0
- package/src/index.ts +30 -283
- package/src/layout/architecture.ts +381 -0
- package/src/layout/measure.ts +21 -0
- package/src/layout/scene.ts +353 -0
- package/src/layout/sequence.ts +189 -0
- package/src/load-fonts.ts +8 -0
- package/src/look/fonts.ts +33 -0
- package/src/look/palettes.json +209 -0
- package/src/look/values.ts +155 -0
- package/src/markdown.ts +35 -0
- package/src/messages.ts +122 -0
- package/src/parser.ts +342 -310
- package/src/player-global.ts +4 -0
- package/src/player.ts +271 -0
- package/src/printer.ts +76 -0
- package/src/render/icons.ts +41 -0
- package/src/render/motion.ts +106 -0
- package/src/render/parts.ts +279 -0
- package/src/render/svg.ts +341 -0
- package/src/schema.ts +161 -16
- package/src/schemas/diagram.schema.json +150 -0
- package/src/schemas/validate-diagram.d.ts +12 -0
- package/src/schemas/validate-diagram.js +8 -0
- package/src/timeline.ts +166 -0
- package/src/tokenize.ts +20 -281
- package/src/version.ts +55 -0
- package/src/view.ts +16 -0
- package/src/words.ts +79 -0
- package/examples/quick-start.md +0 -142
- package/src/canvas-bounds.ts +0 -52
- package/src/color.ts +0 -197
- package/src/compile/actor-ids.ts +0 -297
- package/src/compile/actor-option-notice.ts +0 -379
- package/src/compile/actors.ts +0 -103
- package/src/compile/c4.ts +0 -74
- package/src/compile/chain.ts +0 -93
- package/src/compile/chart-fields.ts +0 -193
- package/src/compile/deep-rewrite.ts +0 -30
- package/src/compile/derived-source.ts +0 -18
- package/src/compile/direction.ts +0 -102
- package/src/compile/edge-option-notice.ts +0 -112
- package/src/compile/er-relation.ts +0 -200
- package/src/compile/flow.ts +0 -54
- package/src/compile/flowchart.ts +0 -344
- package/src/compile/funnel.ts +0 -87
- package/src/compile/gantt.ts +0 -337
- package/src/compile/generic.ts +0 -696
- package/src/compile/hierarchy.ts +0 -92
- package/src/compile/journey.ts +0 -93
- package/src/compile/kinds.ts +0 -139
- package/src/compile/lanes.ts +0 -138
- package/src/compile/metro.ts +0 -271
- package/src/compile/mind.ts +0 -393
- package/src/compile/node-title.ts +0 -24
- package/src/compile/notice.ts +0 -141
- package/src/compile/order.ts +0 -57
- package/src/compile/parts-lookup.ts +0 -17
- package/src/compile/parts.ts +0 -2709
- package/src/compile/placement-apply.ts +0 -689
- package/src/compile/placement.ts +0 -15
- package/src/compile/quadrant.ts +0 -265
- package/src/compile/record.ts +0 -75
- package/src/compile/row-marks.ts +0 -128
- package/src/compile/rows.ts +0 -40
- package/src/compile/sequence.ts +0 -305
- package/src/compile/slug.ts +0 -37
- package/src/compile/stages.ts +0 -247
- package/src/compile/subtitle.ts +0 -35
- package/src/compile/swimlane-marks.ts +0 -87
- package/src/compile/swimlane.ts +0 -82
- package/src/compile/timeline.ts +0 -213
- package/src/compile/topology.ts +0 -44
- package/src/compile/tree.ts +0 -69
- package/src/compile/value-chart.ts +0 -337
- package/src/compile/values.ts +0 -432
- package/src/compile/word-state.ts +0 -82
- package/src/compile.ts +0 -3168
- package/src/focus.ts +0 -46
- package/src/input-size.ts +0 -181
- package/src/json-line-map.ts +0 -30
- package/src/json-parser.ts +0 -4306
- package/src/keywords.ts +0 -417
- package/src/near-name.ts +0 -42
- package/src/notation-lint.ts +0 -342
- package/src/relative-pos.ts +0 -251
- package/src/schemas/diagram.json +0 -3088
- package/src/types.ts +0 -1009
- package/src/v05/index.ts +0 -9
- package/src/v05/input-table.generated.ts +0 -138
- package/src/v05/parser-types.ts +0 -32
- package/src/v05/parser.ts +0 -6668
- package/src/v05/readout-table.generated.ts +0 -1088
- package/src/v05/removed-syntax.ts +0 -115
- package/src/value-syntax.ts +0 -313
- package/src/write-position.ts +0 -270
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 区画、箱、線、線の名前、運ぶ物、段の番号、中身の帯を SVG に描く部品。見本帳の `engine.js` の部品を写す。
|
|
3
|
+
*
|
|
4
|
+
* どの部品も、置き場所は場面 (`layout/scene.ts`) から、いつ見えるかは時刻の表 (`timeline.ts`) から読む。
|
|
5
|
+
* 色は色の役の変数 (`--c` と `--ct`) だけを渡し、値は色味の表が持つ。
|
|
6
|
+
*/
|
|
7
|
+
import { hasWide, textWidth } from "../layout/measure";
|
|
8
|
+
import { along, type Point, pathLength, type SceneAction, type SceneBadge, type SceneBox, type SceneEdge, type SceneLabel, type SceneRegion, type SceneStrip } from "../layout/scene";
|
|
9
|
+
import { type ColorKey, EASE, LOOK, MOTION, TYPE } from "../look/values";
|
|
10
|
+
import type { CarrierTime, EdgeTime, LabelTime, Span, Timeline } from "../timeline";
|
|
11
|
+
import { ICONS } from "./icons";
|
|
12
|
+
import type { Frame, Motion } from "./motion";
|
|
13
|
+
|
|
14
|
+
export const esc = (s: string): string => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
15
|
+
const r1 = (v: number): number => Math.round(v * 100) / 100;
|
|
16
|
+
|
|
17
|
+
/** 色の役を部品へ渡す変数。 */
|
|
18
|
+
export const cs = (k: ColorKey): string => (k === "coin" ? "--c:var(--coin);--ct:var(--coint)" : `--c:var(--c-${k});--ct:var(--t-${k})`);
|
|
19
|
+
|
|
20
|
+
export function glyph(name: string | undefined, x: number, y: number, scale: number, cls = "dg-gl"): string {
|
|
21
|
+
const d = name === undefined ? undefined : ICONS[name];
|
|
22
|
+
return d ? `<path class="${cls}" transform="translate(${r1(x)} ${r1(y)}) scale(${scale})" d="${d}"/>` : "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const pathOf = (points: Point[]): string => points.map(([x, y], i) => `${i === 0 ? "M" : "L"} ${r1(x)} ${r1(y)}`).join(" ");
|
|
26
|
+
|
|
27
|
+
/** 矢じり。線の最後の 2 点の向きに開く。 */
|
|
28
|
+
export function arrowHead(points: Point[]): string {
|
|
29
|
+
const [ex, ey] = points[points.length - 1] as Point;
|
|
30
|
+
const [px, py] = points[points.length - 2] as Point;
|
|
31
|
+
const len = Math.hypot(ex - px, ey - py) || 1;
|
|
32
|
+
const dx = (ex - px) / len;
|
|
33
|
+
const dy = (ey - py) / len;
|
|
34
|
+
const { arrow: a, arrowHalf: h } = LOOK.edge;
|
|
35
|
+
return `M ${r1(ex - dx * a - dy * h)} ${r1(ey - dy * a + dx * h)} L ${r1(ex)} ${r1(ey)} L ${r1(ex - dx * a + dy * h)} ${r1(ey - dy * a - dx * h)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Ctx {
|
|
39
|
+
m: Motion;
|
|
40
|
+
t: Timeline;
|
|
41
|
+
/** 影の filter を付ける属性 */
|
|
42
|
+
sh: string;
|
|
43
|
+
/** 最後の段の終わり */
|
|
44
|
+
end: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 時刻の範囲 `[[a, b], ...]` の間だけ出る。 */
|
|
48
|
+
function rangeKf(m: Motion, ranges: [number, number][]): string {
|
|
49
|
+
const pts: Frame[] = [[0, { opacity: 0 }]];
|
|
50
|
+
for (const [a, b] of ranges) pts.push([a, { opacity: 0 }, EASE.out], [a + MOTION.fade, { opacity: 1 }], [b - 0.1, { opacity: 1 }, EASE.inOut], [b + 0.25, { opacity: 0 }]);
|
|
51
|
+
pts.push([m.total, { opacity: 0 }]);
|
|
52
|
+
return m.kf(pts);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 箱が動く段の並びを、続いた段は 1 つの範囲にまとめて出す。 */
|
|
56
|
+
export function activeKf(ctx: Ctx, on: number[]): string {
|
|
57
|
+
const ranges: [number, number][] = [];
|
|
58
|
+
for (let k = 0; k < on.length; ) {
|
|
59
|
+
let e = k;
|
|
60
|
+
while (e + 1 < on.length && on[e + 1] === (on[e] as number) + 1) e++;
|
|
61
|
+
ranges.push([(ctx.t.steps[on[k] as number] as Span).start, (ctx.t.steps[on[e] as number] as Span).end]);
|
|
62
|
+
k = e + 1;
|
|
63
|
+
}
|
|
64
|
+
return rangeKf(ctx.m, ranges);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function band(region: SceneRegion): string {
|
|
68
|
+
const { radius, icon, iconRadius, iconScale, nameY, line } = LOOK.region;
|
|
69
|
+
const { x, y } = region.header;
|
|
70
|
+
const lines = region.lines.map((t, k) => `<text class="dg-bz" x="${r1(x)}" y="${r1(region.y + nameY + k * line)}">${esc(t)}</text>`).join("");
|
|
71
|
+
const note = region.note ? `<text class="dg-bx" x="${r1(x)}" y="${r1(region.y + nameY + region.lines.length * line + 1)}">${esc(region.note)}</text>` : "";
|
|
72
|
+
const mark = region.icon && ICONS[region.icon] ? `<rect class="dg-bi" x="${r1(x)}" y="${r1(y)}" width="${icon}" height="${icon}" rx="${iconRadius}"/>${glyph(region.icon, x + icon / 2, y + icon / 2, iconScale)}` : "";
|
|
73
|
+
return `<g style="${cs(region.color)}"><rect class="dg-band" x="${r1(region.x + 0.5)}" y="${r1(region.y + 0.5)}" width="${r1(region.w - 1)}" height="${r1(region.h - 1)}" rx="${radius}"/>${mark}${lines}${note}</g>`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 箱の下の小さな字の移り変わり。`[時刻, 字, class]` の並びで、最初は元の字。 */
|
|
77
|
+
export type SubChange = [number, string, string];
|
|
78
|
+
|
|
79
|
+
/** 箱。動く段は縁に色を付け、外側に淡い光を足す。下の小さな字は起きたことに合わせて書き換わる。 */
|
|
80
|
+
export function card(box: SceneBox, ctx: Ctx, on: number[], changes: SubChange[]): string {
|
|
81
|
+
const { m, sh } = ctx;
|
|
82
|
+
const { radius, icon, iconRadius, iconScale, iconY, nameY, subY } = LOOK.box;
|
|
83
|
+
const seq: SubChange[] = [[0, box.sub ?? "", "dg-ss"], ...changes];
|
|
84
|
+
const subs = seq
|
|
85
|
+
.map(([t, text, cls], k) => {
|
|
86
|
+
const next = seq[k + 1]?.[0];
|
|
87
|
+
const anim = k === 0 ? (next === undefined ? "" : m.until(next - MOTION.untilFade)) : next === undefined ? m.rise(t) : m.rise(t, next - MOTION.untilFade);
|
|
88
|
+
if (text === "") return "";
|
|
89
|
+
return `<g${anim ? ` class="dg-a" style="${anim}"` : ""}><text class="${cls}" x="${r1(box.cx)}" y="${r1(box.cy + subY)}" text-anchor="middle">${esc(text)}</text></g>`;
|
|
90
|
+
})
|
|
91
|
+
.join("");
|
|
92
|
+
const act = on.length > 0 ? activeKf(ctx, on) : undefined;
|
|
93
|
+
const rect = (cls: string, extra = ""): string => `<rect class="${cls}" x="${r1(box.x)}" y="${r1(box.y)}" width="${r1(box.w)}" height="${box.h}" rx="${radius}"${extra}/>`;
|
|
94
|
+
const mark =
|
|
95
|
+
box.icon && ICONS[box.icon]
|
|
96
|
+
? `<rect class="dg-ic" x="${r1(box.cx - icon / 2)}" y="${r1(box.cy + iconY - icon / 2)}" width="${icon}" height="${icon}" rx="${iconRadius}"/>${glyph(box.icon, box.cx, box.cy + iconY, iconScale)}`
|
|
97
|
+
: "";
|
|
98
|
+
return (
|
|
99
|
+
`<g style="${cs(box.color)}">${act ? `<g class="dg-a" style="${act}">${rect("dg-halo")}</g>` : ""}${rect("dg-st", ` ${sh}`)}${act ? `<g class="dg-a" style="${act}">${rect("dg-on")}</g>` : ""}` +
|
|
100
|
+
`${mark}<text class="dg-sn" x="${r1(box.cx)}" y="${r1(box.cy + nameY)}" text-anchor="middle">${esc(box.name)}</text>${subs}</g>`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function lifeline(x: number, y1: number, y2: number): string {
|
|
105
|
+
return `<line class="dg-ll" x1="${r1(x)}" y1="${r1(y1)}" x2="${r1(x)}" y2="${r1(y2)}"/>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 線。点線で道を見せておき、その段で色の線を引いて、段の終わりまで流れる点で向きを見せる。
|
|
110
|
+
* 済んだ段は薄く残し、最後の段の後に通った道を全て濃くし直す。
|
|
111
|
+
*/
|
|
112
|
+
export function edge(e: SceneEdge, time: EdgeTime, ctx: Ctx): { base: string; lit: string } {
|
|
113
|
+
const { m } = ctx;
|
|
114
|
+
const d = pathOf(e.points);
|
|
115
|
+
const head = arrowHead(e.points);
|
|
116
|
+
const { start: a, drawn: b, end } = time;
|
|
117
|
+
const tail: Frame[] = [
|
|
118
|
+
[end, { opacity: 1 }, EASE.inOut],
|
|
119
|
+
[end + 0.4, { opacity: 0.45 }],
|
|
120
|
+
[ctx.end + 0.3, { opacity: 0.45 }, EASE.inOut],
|
|
121
|
+
[ctx.end + 1, { opacity: 0.95 }],
|
|
122
|
+
[m.last, { opacity: 0.95 }, EASE.inOut],
|
|
123
|
+
[m.total, { opacity: 0 }],
|
|
124
|
+
];
|
|
125
|
+
const line = m.kf([
|
|
126
|
+
[0, { opacity: 0, "stroke-dashoffset": 1 }],
|
|
127
|
+
[a, { opacity: 0, "stroke-dashoffset": 1 }, EASE.inOut],
|
|
128
|
+
[a + 0.05, { opacity: 1 }],
|
|
129
|
+
[b, { "stroke-dashoffset": 0 }],
|
|
130
|
+
[m.last, { "stroke-dashoffset": 0 }],
|
|
131
|
+
[m.total, { "stroke-dashoffset": 1 }],
|
|
132
|
+
...tail,
|
|
133
|
+
]);
|
|
134
|
+
const hk = m.kf([[0, { opacity: 0 }], [b - 0.05, { opacity: 0 }], [b, { opacity: 1 }], ...tail]);
|
|
135
|
+
const run = -(end - b) * 26;
|
|
136
|
+
const flow = m.kf([
|
|
137
|
+
[0, { opacity: 0, "stroke-dashoffset": 0 }],
|
|
138
|
+
[b, { opacity: 0, "stroke-dashoffset": 0 }],
|
|
139
|
+
[b + 0.25, { opacity: 0.9 }],
|
|
140
|
+
[end - 0.1, { opacity: 0.9 }, EASE.inOut],
|
|
141
|
+
[end + 0.2, { opacity: 0, "stroke-dashoffset": run }],
|
|
142
|
+
[m.total, { opacity: 0, "stroke-dashoffset": run }],
|
|
143
|
+
]);
|
|
144
|
+
return {
|
|
145
|
+
base: `<path class="dg-e" d="${d}"/><path class="dg-eh" d="${head}"/>`,
|
|
146
|
+
lit: `<g style="${cs(e.color)}"><path class="dg-e-on dg-a" pathLength="1" d="${d}" style="${line}"/><path class="dg-eh-on dg-a" d="${head}" style="${hk}"/><path class="dg-flow dg-a" d="${d}" style="${flow}"/></g>`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 線の名前の札。その段の間だけ出し、段の終わりまでに消え終わる。 */
|
|
151
|
+
export function label(l: SceneLabel, time: LabelTime, ctx: Ctx): string {
|
|
152
|
+
const ja = l.family === "sans" ? " dg-capj" : "";
|
|
153
|
+
const { radius } = LOOK.label;
|
|
154
|
+
return `<g class="dg-cap${ja} dg-a" style="${cs(l.color)};${ctx.m.rise(time.start, time.end, true)}"><rect x="${r1(l.x)}" y="${r1(l.y)}" width="${r1(l.w)}" height="${l.h}" rx="${radius}"/><text x="${r1(l.x + l.w / 2)}" y="${r1(l.y + l.h / 2 + LOOK.baseline.label)}" text-anchor="middle">${esc(l.text)}</text></g>`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 時系列図の箱の中の処理の札。その段の間だけ生存線の上に出す。 */
|
|
158
|
+
export function action(a: SceneAction, at: number, until: number, ctx: Ctx): string {
|
|
159
|
+
return `<g class="dg-act dg-a" style="${cs(a.color)};${ctx.m.rise(at, until, true)}"><rect x="${r1(a.x)}" y="${r1(a.y)}" width="${r1(a.w)}" height="${a.h}" rx="${LOOK.action.radius}"/><text x="${r1(a.x + a.w / 2)}" y="${r1(a.y + a.h / 2 + LOOK.baseline.action)}" text-anchor="middle">${esc(a.text)}</text></g>`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function carrierShape(e: SceneEdge, sh: string): string {
|
|
163
|
+
const C = LOOK.carrier;
|
|
164
|
+
if (e.carry.kind === "coin") return `<circle class="dg-coin" r="${C.coinR}"/><circle class="dg-coin-in" r="${C.coinIn}"/>`;
|
|
165
|
+
if (e.carry.kind === "tag") {
|
|
166
|
+
const text = e.carry.text;
|
|
167
|
+
const ja = hasWide(text);
|
|
168
|
+
const w = textWidth(text, ja ? TYPE.carrierJa : TYPE.carrier) + C.tagPadX * 2;
|
|
169
|
+
return `<rect class="dg-car" x="${r1(-w / 2)}" y="${-C.tagH / 2}" width="${r1(w)}" height="${C.tagH}" rx="${C.tagH / 2}" ${sh}/><text class="dg-car-t${ja ? " dg-car-tj" : ""}" y="${LOOK.baseline.carrier}" text-anchor="middle">${esc(text)}</text>`;
|
|
170
|
+
}
|
|
171
|
+
return `<rect class="dg-env" x="${-C.envW / 2}" y="${-C.envH / 2}" width="${C.envW}" height="${C.envH}" rx="3" ${sh}/><path class="dg-env-in" d="M -8 -4.5 L 0 1.5 L 8 -4.5"/>`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const easeInOut = (t: number): number => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
|
|
175
|
+
|
|
176
|
+
/** 運ぶ物。線の上を走り、着いた先 (`settle`) へ寄って、次の同じ主役が出発するまで留まる。 */
|
|
177
|
+
export function carrier(e: SceneEdge, time: CarrierTime, ctx: Ctx): string {
|
|
178
|
+
const { m } = ctx;
|
|
179
|
+
const n = LOOK.carrier.samples;
|
|
180
|
+
const length = pathLength(e.points);
|
|
181
|
+
const { depart: a, arrive: b, start: from, end: gone } = time;
|
|
182
|
+
const path: [number, number, number][] = [];
|
|
183
|
+
for (let k = 0; k <= n; k++) {
|
|
184
|
+
const [x, y] = along(e.points, easeInOut(k / n) * length);
|
|
185
|
+
path.push([a + ((b - a) * k) / n, x, y]);
|
|
186
|
+
}
|
|
187
|
+
const first = path[0] as [number, number, number];
|
|
188
|
+
const lastPoint = path[path.length - 1] as [number, number, number];
|
|
189
|
+
const rest: Point = e.settle ?? [lastPoint[1], lastPoint[2]];
|
|
190
|
+
const pts: [number, number, number][] = [[0, first[1], first[2]], ...path];
|
|
191
|
+
if (e.settle) pts.push([Math.min(b + 0.3, Math.max(gone, b + 0.01)), rest[0], rest[1]]);
|
|
192
|
+
pts.push([gone, rest[0], rest[1]], [m.total, rest[0], rest[1]]);
|
|
193
|
+
// 次の運ぶ物と入れ替わる時は、重なって 2 つに見えないよう素早く消す
|
|
194
|
+
const fade = gone < m.last ? 0.05 : 0.18;
|
|
195
|
+
return `<g class="dg-a" style="${m.move(pts, m.blink(from + 0.02, gone, fade))}">${carrierShape(e, ctx.sh)}</g>`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 段の番号。まだ来ていない段は出さず、今の段は塗り、済んだ段は縁取りに変えて番号を読めるまま残す。 */
|
|
199
|
+
export function badge(b: SceneBadge, ctx: Ctx): string {
|
|
200
|
+
const { m } = ctx;
|
|
201
|
+
const span = ctx.t.steps[b.step] as Span;
|
|
202
|
+
const a = span.start;
|
|
203
|
+
const e = span.end;
|
|
204
|
+
const { r, ring: rr } = LOOK.badge;
|
|
205
|
+
const fill = m.kf([
|
|
206
|
+
[0, { opacity: 0, transform: "scale(.5)" }],
|
|
207
|
+
[a, { opacity: 0, transform: "scale(.5)" }, EASE.pop],
|
|
208
|
+
[a + 0.35, { opacity: 1, transform: "scale(1)" }],
|
|
209
|
+
[e, { opacity: 1, transform: "scale(1)" }, EASE.inOut],
|
|
210
|
+
[e + 0.4, { opacity: 0, transform: "scale(1)" }],
|
|
211
|
+
[m.total, { opacity: 0, transform: "scale(.5)" }],
|
|
212
|
+
]);
|
|
213
|
+
const past = m.kf([[0, { opacity: 0 }], [e, { opacity: 0 }, EASE.inOut], [e + 0.4, { opacity: 1 }], [m.last, { opacity: 1 }, EASE.inOut], [m.total, { opacity: 0 }]]);
|
|
214
|
+
const ring = m.kf([
|
|
215
|
+
[0, { opacity: 0, transform: "scale(1)" }],
|
|
216
|
+
[a + 0.05, { opacity: 0, transform: "scale(1)" }],
|
|
217
|
+
[a + 0.1, { opacity: 0.7, transform: "scale(1)" }, EASE.out],
|
|
218
|
+
[a + 0.9, { opacity: 0, transform: "scale(2.3)" }],
|
|
219
|
+
[m.total, { opacity: 0, transform: "scale(1)" }],
|
|
220
|
+
]);
|
|
221
|
+
const x = r1(b.x);
|
|
222
|
+
const y = r1(b.y);
|
|
223
|
+
const ty = r1(b.y + LOOK.baseline.badge);
|
|
224
|
+
const no = b.step + 1;
|
|
225
|
+
return (
|
|
226
|
+
`<g class="dg-bd" data-step="${b.step}" style="${cs(b.color)}"><circle class="dg-bdr dg-popc dg-a" cx="${x}" cy="${y}" r="${rr}" style="${ring}"/>` +
|
|
227
|
+
`<g class="dg-a" style="${past}"><circle class="dg-bdp" cx="${x}" cy="${y}" r="${rr}"/><text class="dg-bdpt" x="${x}" y="${ty}" text-anchor="middle">${no}</text></g>` +
|
|
228
|
+
`<g class="dg-popc dg-a" style="${fill}"><circle class="dg-bdf" cx="${x}" cy="${y}" r="${r}"/><text class="dg-bdft" x="${x}" y="${ty}" text-anchor="middle">${no}</text></g></g>`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 中身の帯の 1 欄の移り変わり。値と札は、次の値か札が来るまで出る。 */
|
|
233
|
+
export interface FieldChange {
|
|
234
|
+
at: number;
|
|
235
|
+
value?: string;
|
|
236
|
+
tag?: string;
|
|
237
|
+
color: ColorKey;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function strip(s: SceneStrip, changes: Map<string, FieldChange[]>, ctx: Ctx): string {
|
|
241
|
+
const { m, sh } = ctx;
|
|
242
|
+
const S = LOOK.strip;
|
|
243
|
+
const my = s.y + S.h / 2;
|
|
244
|
+
const parts = [
|
|
245
|
+
`<rect class="dg-strip" x="${r1(s.x + 0.5)}" y="${r1(s.y + 0.5)}" width="${r1(s.w - 1)}" height="${r1(s.h - 1)}" rx="${LOOK.region.radius}" ${sh}/>`,
|
|
246
|
+
`<g style="${cs("slate")}"><rect class="dg-bi" x="${r1(s.x + S.pad)}" y="${r1(my - S.icon / 2)}" width="${S.icon}" height="${S.icon}" rx="${LOOK.box.iconRadius}"/>${glyph("env", s.x + S.pad + S.icon / 2, my, 0.78)}</g>`,
|
|
247
|
+
`<text class="dg-sh" x="${r1(s.x + S.titleX)}" y="${r1(my + LOOK.baseline.stripTitle)}">${esc(s.title)}</text>`,
|
|
248
|
+
];
|
|
249
|
+
s.fields.forEach((field, i) => {
|
|
250
|
+
const prev = s.fields[i - 1];
|
|
251
|
+
if (prev && prev.y === field.y) {
|
|
252
|
+
const sx = field.x - S.fieldGap / 2;
|
|
253
|
+
parts.push(`<line class="dg-sep" x1="${r1(sx)}" y1="${r1(field.y + S.sepInset)}" x2="${r1(sx)}" y2="${r1(field.y + S.h - S.sepInset)}"/>`);
|
|
254
|
+
}
|
|
255
|
+
parts.push(`<text class="dg-fl" x="${r1(field.x)}" y="${r1(field.y + S.nameY)}">${esc(field.name)}</text>`);
|
|
256
|
+
const list = changes.get(field.name) ?? [];
|
|
257
|
+
const values = list.filter((c) => c.value !== undefined);
|
|
258
|
+
const tags = list.filter((c) => c.tag !== undefined);
|
|
259
|
+
values.forEach((c, k) => {
|
|
260
|
+
const next = values[k + 1]?.at;
|
|
261
|
+
const value = c.value as string;
|
|
262
|
+
const anim = next === undefined ? m.rise(c.at) : m.rise(c.at, next - MOTION.untilFade);
|
|
263
|
+
parts.push(`<g class="dg-a" style="${anim}"><text class="${hasWide(value) ? "dg-fvj" : "dg-fv"}" x="${r1(field.x)}" y="${r1(field.y + S.valueY)}">${esc(value)}</text></g>`);
|
|
264
|
+
});
|
|
265
|
+
tags.forEach((c, k) => {
|
|
266
|
+
const next = tags[k + 1]?.at;
|
|
267
|
+
const tag = c.tag as string;
|
|
268
|
+
// 札は、その時に出ている値の右に置く
|
|
269
|
+
const shown = [...values].reverse().find((v) => v.at <= c.at + 1e-9)?.value;
|
|
270
|
+
const x = field.x + (shown ? textWidth(shown, hasWide(shown) ? TYPE.fieldValueJa : TYPE.fieldValue) + S.tagGap : 0);
|
|
271
|
+
const w = textWidth(tag, TYPE.tag) + S.tagPadX * 2;
|
|
272
|
+
const y = field.y + S.valueY - S.tagH / 2 - LOOK.baseline.tag;
|
|
273
|
+
parts.push(
|
|
274
|
+
`<g class="dg-tg dg-popc dg-a" style="${cs(c.color)};${next === undefined ? m.pop(c.at) : m.pop(c.at, next - MOTION.untilFade)}"><rect x="${r1(x)}" y="${r1(y)}" width="${r1(w)}" height="${S.tagH}" rx="${S.tagH / 2}"/><text x="${r1(x + w / 2)}" y="${r1(y + S.tagH / 2 + LOOK.baseline.tag)}" text-anchor="middle">${esc(tag)}</text></g>`,
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
return parts.join("");
|
|
279
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 原本の JSON から図を描く入口。置き場所と時刻の表を作り、部品を重なりの順に並べて SVG にする。
|
|
3
|
+
*
|
|
4
|
+
* 動きは全て SVG の中の CSS の keyframes が回す。1 周の長さは `--T`、止める時刻は `--at`、
|
|
5
|
+
* 止めるか流すかは `--play` の変数で外から変えられる (頁に置く `player.ts` がこれを使う)。
|
|
6
|
+
* 色と字は値の表から CSS の変数として 1 度だけ書き、部品はその変数だけを読む。
|
|
7
|
+
*/
|
|
8
|
+
import { FIGURE_TEXT } from "../figure-text";
|
|
9
|
+
import { layoutArchitecture } from "../layout/architecture";
|
|
10
|
+
import { textWidth } from "../layout/measure";
|
|
11
|
+
import type { Layout, Rect } from "../layout/scene";
|
|
12
|
+
import { layoutSequence } from "../layout/sequence";
|
|
13
|
+
import { FONTS } from "../look/fonts";
|
|
14
|
+
import { type ColorKey, EASE, FONT_VARS, LOOK, MOTION, type Palette, paletteVars, resolvePalette, type TextStyle, type Theme, TYPE } from "../look/values";
|
|
15
|
+
import { readDiagram } from "../version";
|
|
16
|
+
import type { Diagram, ViewMode } from "../schema";
|
|
17
|
+
import { buildTimeline, type Span, type Timeline } from "../timeline";
|
|
18
|
+
import type { NotationLang } from "../words";
|
|
19
|
+
import { action, badge, band, card, carrier, type Ctx, cs, edge, esc, type FieldChange, label, lifeline, strip, type SubChange } from "./parts";
|
|
20
|
+
import { Motion } from "./motion";
|
|
21
|
+
|
|
22
|
+
export interface RenderOptions {
|
|
23
|
+
/** 色味の名前。書かないか知らない名前なら紙と墨 */
|
|
24
|
+
palette?: string;
|
|
25
|
+
theme?: Theme;
|
|
26
|
+
/** 描く部品が出す字 (断りの文、全体図の札、読み上げの名前) の言葉。書かなければ日本語 (#3001) */
|
|
27
|
+
lang?: NotationLang;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Rendered {
|
|
31
|
+
svg: string;
|
|
32
|
+
palette: string;
|
|
33
|
+
theme: Theme;
|
|
34
|
+
/** 書き手が原本で決めた見せ方 */
|
|
35
|
+
view?: ViewMode;
|
|
36
|
+
fonts: { sans: string; mono: string };
|
|
37
|
+
layout: Layout;
|
|
38
|
+
timeline: Timeline;
|
|
39
|
+
diagram: Diagram;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const font = (t: TextStyle): string => `font:${t.weight} ${t.size}px var(--${t.family})`;
|
|
43
|
+
const mix = (pct: number, base = "var(--surf)"): string => `color-mix(in srgb,var(--c) ${pct}%,${base})`;
|
|
44
|
+
const S = LOOK.stroke;
|
|
45
|
+
const X = LOOK.mix;
|
|
46
|
+
|
|
47
|
+
/** 部品の見た目。どの値も値の表から読む。 */
|
|
48
|
+
const PART_CSS = [
|
|
49
|
+
".dg{display:block;width:100%;height:auto;overflow:visible}",
|
|
50
|
+
".dg .dg-a{animation-duration:var(--T);animation-timing-function:linear;animation-iteration-count:infinite;animation-fill-mode:both;animation-delay:calc(var(--at,0) * -1s);animation-play-state:var(--play,running)}",
|
|
51
|
+
".dg .dg-popc{transform-box:fill-box;transform-origin:center}",
|
|
52
|
+
".dg-bg{fill:var(--bg)}",
|
|
53
|
+
".dg-band{fill:color-mix(in srgb,var(--c) var(--tint),var(--bg));stroke:color-mix(in srgb,var(--c) var(--edge),transparent)}",
|
|
54
|
+
`.dg-bi{fill:${mix(X.regionIcon)};stroke:${mix(X.regionIconEdge, "transparent")}}`,
|
|
55
|
+
`.dg-bz{${font(TYPE.regionName)};fill:var(--ct)}`,
|
|
56
|
+
`.dg-bx{${font(TYPE.regionNote)};fill:var(--mute)}`,
|
|
57
|
+
".dg-st{fill:var(--surf);stroke:var(--hair)}",
|
|
58
|
+
`.dg-halo{fill:none;stroke:var(--c);stroke-width:${S.halo};stroke-opacity:.16}`,
|
|
59
|
+
`.dg-on{fill:none;stroke:var(--c);stroke-width:${S.on}}`,
|
|
60
|
+
`.dg-ic{fill:${mix(X.icon)}}`,
|
|
61
|
+
`.dg-gl{fill:none;stroke:var(--c);stroke-width:${S.glyph};stroke-linecap:round;stroke-linejoin:round}`,
|
|
62
|
+
`.dg-sn{${font(TYPE.boxName)};fill:var(--ink)}`,
|
|
63
|
+
`.dg-ss{${font(TYPE.boxSub)};fill:var(--mute)}`,
|
|
64
|
+
`.dg-ss-ok{${font(TYPE.boxSubStrong)};fill:var(--ct)}`,
|
|
65
|
+
`.dg-ss-act{${font(TYPE.boxSubStrong)};fill:var(--ink)}`,
|
|
66
|
+
`.dg-ll{stroke:var(--line);stroke-width:${S.lifeline};stroke-dasharray:3 4}`,
|
|
67
|
+
`.dg-e{fill:none;stroke:var(--line);stroke-width:${S.edge};stroke-linecap:round;stroke-dasharray:0 ${LOOK.edge.ghostDash}}`,
|
|
68
|
+
`.dg-eh{fill:none;stroke:var(--line);stroke-width:${S.arrow};stroke-linecap:round;stroke-linejoin:round}`,
|
|
69
|
+
`.dg-e-on{fill:none;stroke:var(--c);stroke-width:${S.edgeOn};stroke-linecap:round;stroke-dasharray:1 2}`,
|
|
70
|
+
`.dg-eh-on{fill:none;stroke:var(--c);stroke-width:${S.edgeOn};stroke-linecap:round;stroke-linejoin:round}`,
|
|
71
|
+
`.dg-flow{fill:none;stroke:var(--surf);stroke-width:${S.flow};stroke-linecap:round;stroke-dasharray:1.5 ${LOOK.edge.flowDash}}`,
|
|
72
|
+
".dg-bd{cursor:pointer}",
|
|
73
|
+
".dg-bdf{fill:var(--c)}",
|
|
74
|
+
`.dg-bdft{${font(TYPE.badge)};fill:var(--on-c)}`,
|
|
75
|
+
`.dg-bdr{fill:none;stroke:var(--c);stroke-width:${S.badgeRing}}`,
|
|
76
|
+
`.dg-bdp{fill:${mix(X.badgePast)};stroke:${mix(X.badgePastEdge, "transparent")};stroke-width:${S.hair}}`,
|
|
77
|
+
`.dg-bdpt{${font(TYPE.badge)};fill:var(--ct)}`,
|
|
78
|
+
`.dg-cap rect,.dg-act rect{fill:${mix(X.cap)};stroke:${mix(X.capEdge, "transparent")}}`,
|
|
79
|
+
`.dg-cap text{${font(TYPE.label)};fill:var(--ct)}`,
|
|
80
|
+
`.dg-capj text{${font(TYPE.labelJa)}}`,
|
|
81
|
+
`.dg-act text{${font(TYPE.action)};fill:var(--ct)}`,
|
|
82
|
+
".dg-car{fill:var(--carry,var(--ink))}",
|
|
83
|
+
`.dg-car-t{${font(TYPE.carrier)};fill:var(--bg)}`,
|
|
84
|
+
`.dg-car-tj{${font(TYPE.carrierJa)}}`,
|
|
85
|
+
`.dg-env{fill:var(--carry,var(--ink));stroke:var(--surf);stroke-width:${S.envelope}}`,
|
|
86
|
+
`.dg-env-in{fill:none;stroke:var(--surf);stroke-width:${S.flow};stroke-linejoin:round;stroke-linecap:round}`,
|
|
87
|
+
".dg-coin{fill:var(--coin);stroke:var(--surf);stroke-width:1.2}",
|
|
88
|
+
".dg-coin-in{fill:none;stroke:var(--coinin);stroke-width:1.1}",
|
|
89
|
+
".dg-strip{fill:var(--surf);stroke:var(--hair)}",
|
|
90
|
+
`.dg-sep{stroke:var(--hair);stroke-width:${S.hair}}`,
|
|
91
|
+
`.dg-sh{${font(TYPE.stripTitle)};fill:var(--ink)}`,
|
|
92
|
+
`.dg-fl{${font(TYPE.fieldName)};fill:var(--mute)}`,
|
|
93
|
+
`.dg-fv{${font(TYPE.fieldValue)};fill:var(--ink)}`,
|
|
94
|
+
`.dg-fvj{${font(TYPE.fieldValueJa)};fill:var(--ink)}`,
|
|
95
|
+
".dg-tg rect{fill:var(--c)}",
|
|
96
|
+
`.dg-tg text{${font(TYPE.tag)};fill:var(--on-c)}`,
|
|
97
|
+
".dg-vp-bg{fill:var(--bg)}",
|
|
98
|
+
".dg-vp-frame{fill:none;stroke:var(--hair)}",
|
|
99
|
+
".dg-mm-bg{fill:var(--surf);stroke:var(--hair)}",
|
|
100
|
+
`.dg-mm-t{${font(TYPE.miniTitle)};fill:var(--mute)}`,
|
|
101
|
+
".dg-mm-world{fill:var(--sunk);stroke:var(--hair)}",
|
|
102
|
+
`.dg-mm-band{fill:${mix(X.miniBand)};stroke:${mix(X.miniBandEdge, "transparent")};stroke-width:.6}`,
|
|
103
|
+
`.dg-mm-card{fill:${mix(X.mini)}}`,
|
|
104
|
+
`.dg-mm-vp{fill:color-mix(in srgb,var(--ink) ${X.view}%,transparent);stroke:var(--ink);stroke-width:${S.badgeRing};vector-effect:non-scaling-stroke}`,
|
|
105
|
+
].join("");
|
|
106
|
+
|
|
107
|
+
/** 同じ原本と同じ見た目からは同じ名前になるよう、中身から短い名前を作る (keyframes の名前が頁の中で混ざらないため)。 */
|
|
108
|
+
function nameOf(text: string): string {
|
|
109
|
+
let h = 0x811c9dc5;
|
|
110
|
+
for (let i = 0; i < text.length; i++) {
|
|
111
|
+
h ^= text.charCodeAt(i);
|
|
112
|
+
h = Math.imul(h, 0x01000193);
|
|
113
|
+
}
|
|
114
|
+
return (h >>> 0).toString(36);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 各箱が動く段。線の両端、処理、字の書き換えの箱。 */
|
|
118
|
+
function involved(diagram: Diagram): Map<string, number[]> {
|
|
119
|
+
const on = new Map<string, Set<number>>();
|
|
120
|
+
const add = (id: string, s: number): void => {
|
|
121
|
+
on.set(id, (on.get(id) ?? new Set()).add(s));
|
|
122
|
+
};
|
|
123
|
+
diagram.steps.forEach((step, s) => {
|
|
124
|
+
for (const event of step.events) {
|
|
125
|
+
if (event.type === "edge") {
|
|
126
|
+
add(event.from, s);
|
|
127
|
+
add(event.to, s);
|
|
128
|
+
} else if (event.type === "action" || event.type === "swap") add(event.box, s);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
return new Map([...on].map(([id, steps]) => [id, [...steps].sort((a, b) => a - b)]));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 箱の下の字の移り変わり。書き換えはそのまま残し、構成図の処理はその段の間だけ出して元へ戻す。 */
|
|
135
|
+
function subChanges(diagram: Diagram, timeline: Timeline, actionsInBox: boolean): Map<string, SubChange[]> {
|
|
136
|
+
type Raw = { t: number; order: number; box: string; set?: string; act?: string | null };
|
|
137
|
+
const raw: Raw[] = [];
|
|
138
|
+
let order = 0;
|
|
139
|
+
for (const time of timeline.events) {
|
|
140
|
+
const event = diagram.steps[time.step]?.events[time.event];
|
|
141
|
+
if (event?.type === "swap") raw.push({ t: time.at, order: order++, box: event.box, set: event.text });
|
|
142
|
+
if (actionsInBox && event?.type === "action") {
|
|
143
|
+
raw.push({ t: time.at, order: order++, box: event.box, act: event.label });
|
|
144
|
+
raw.push({ t: (timeline.steps[time.step] as Span).end, order: order++, box: event.box, act: null });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
raw.sort((a, b) => a.t - b.t || a.order - b.order);
|
|
148
|
+
const out = new Map<string, SubChange[]>();
|
|
149
|
+
for (const box of diagram.boxes) {
|
|
150
|
+
let base: [string, string] = [box.sub ?? "", "dg-ss"];
|
|
151
|
+
let acting: string | undefined;
|
|
152
|
+
let shown = base.join("|");
|
|
153
|
+
const list: SubChange[] = [];
|
|
154
|
+
for (const change of raw.filter((r) => r.box === box.id)) {
|
|
155
|
+
// 書き換えは、出ている処理の字も置き換える
|
|
156
|
+
if (change.set !== undefined) {
|
|
157
|
+
base = [change.set, "dg-ss-ok"];
|
|
158
|
+
acting = undefined;
|
|
159
|
+
}
|
|
160
|
+
if (change.act !== undefined) acting = change.act ?? undefined;
|
|
161
|
+
const now: [string, string] = acting !== undefined ? [acting, "dg-ss-act"] : base;
|
|
162
|
+
if (now.join("|") === shown) continue;
|
|
163
|
+
shown = now.join("|");
|
|
164
|
+
list.push([change.t, now[0], now[1]]);
|
|
165
|
+
}
|
|
166
|
+
// すぐ次の字に替わる字は読めないので出さない
|
|
167
|
+
const kept = list.filter((change, k) => {
|
|
168
|
+
const next = list[k + 1];
|
|
169
|
+
return next === undefined || next[0] - change[0] >= MOTION.minShow;
|
|
170
|
+
});
|
|
171
|
+
out.set(box.id, kept);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function fieldChanges(diagram: Diagram, timeline: Timeline, layout: Layout): Map<string, FieldChange[]> {
|
|
177
|
+
const out = new Map<string, FieldChange[]>();
|
|
178
|
+
for (const time of timeline.events) {
|
|
179
|
+
const event = diagram.steps[time.step]?.events[time.event];
|
|
180
|
+
if (event?.type !== "strip") continue;
|
|
181
|
+
const color: ColorKey = layout.badges[time.step]?.color ?? "slate";
|
|
182
|
+
const list = out.get(event.field) ?? [];
|
|
183
|
+
list.push({ at: time.at, color, ...(event.value !== undefined ? { value: event.value } : {}), ...(event.tag !== undefined ? { tag: event.tag } : {}) });
|
|
184
|
+
out.set(event.field, list);
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface FollowFrame {
|
|
190
|
+
/** 枠の幅 (px)。寄って追う窓はこの幅で、字を原寸で見せる */
|
|
191
|
+
width: number;
|
|
192
|
+
/** 隅の全体図の札の言葉。渡し忘れを型の検査で止めるため、省けない欄にする */
|
|
193
|
+
lang: NotationLang;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 図を描く。`follow` を渡すと、寄って追う窓と隅の全体図で描く。 */
|
|
197
|
+
export function drawSvg(diagram: Diagram, layout: Layout, timeline: Timeline, palette: Palette, theme: Theme, follow?: FollowFrame): string {
|
|
198
|
+
// 札の言葉で全体図の地の幅が変わりうるので、同じ頁に置いた別の言葉の図と名前を分ける
|
|
199
|
+
const id = nameOf(`${JSON.stringify(diagram)}|${palette.id}|${theme}|${follow ? `${follow.width}|${follow.lang}` : ""}`);
|
|
200
|
+
const m = new Motion(`dg${id}`, timeline.total, timeline.last);
|
|
201
|
+
const ctx: Ctx = { m, t: timeline, sh: `filter="url(#dg-sh-${id})"`, end: (timeline.steps[timeline.steps.length - 1] as Span).end };
|
|
202
|
+
const on = involved(diagram);
|
|
203
|
+
const subs = subChanges(diagram, timeline, layout.kind === "architecture");
|
|
204
|
+
|
|
205
|
+
const L = { band: [] as string[], base: [] as string[], lit: [] as string[], node: [] as string[], badge: [] as string[], top: [] as string[] };
|
|
206
|
+
for (const region of layout.regions) L.band.push(band(region));
|
|
207
|
+
if (layout.strip) L.band.push(strip(layout.strip, fieldChanges(diagram, timeline, layout), ctx));
|
|
208
|
+
for (const line of layout.lifelines) L.base.push(lifeline(line.x, line.y1, line.y2));
|
|
209
|
+
for (const e of layout.edges) {
|
|
210
|
+
const time = timeline.edges.find((t) => t.step === e.step && t.event === e.event);
|
|
211
|
+
if (!time) continue;
|
|
212
|
+
const drawn = edge(e, time, ctx);
|
|
213
|
+
L.base.push(drawn.base);
|
|
214
|
+
L.lit.push(drawn.lit);
|
|
215
|
+
}
|
|
216
|
+
for (const box of layout.boxes) L.node.push(card(box, ctx, on.get(box.id) ?? [], subs.get(box.id) ?? []));
|
|
217
|
+
for (const b of layout.badges) L.badge.push(badge(b, ctx));
|
|
218
|
+
for (const l of layout.labels) {
|
|
219
|
+
const time = timeline.labels.find((t) => t.step === l.step && t.event === l.event);
|
|
220
|
+
if (time) L.top.push(label(l, time, ctx));
|
|
221
|
+
}
|
|
222
|
+
for (const a of layout.actions) {
|
|
223
|
+
const time = timeline.events.find((t) => t.step === a.step && t.event === a.event);
|
|
224
|
+
if (time) L.top.push(action(a, time.at, (timeline.steps[a.step] as Span).end, ctx));
|
|
225
|
+
}
|
|
226
|
+
for (const time of timeline.carriers) {
|
|
227
|
+
const e = layout.edges.find((x) => x.step === time.step && x.event === time.event);
|
|
228
|
+
if (e) L.top.push(carrier(e, time, ctx));
|
|
229
|
+
}
|
|
230
|
+
const content = Object.values(L)
|
|
231
|
+
.map((parts) => parts.join(""))
|
|
232
|
+
.join("");
|
|
233
|
+
|
|
234
|
+
const { near, far, nearY, farY } = LOOK.shadow;
|
|
235
|
+
const shadow = `<filter id="dg-sh-${id}" x="-20%" y="-30%" width="140%" height="180%"><feDropShadow dx="0" dy="${nearY}" stdDeviation="${near}" style="flood-color:var(--sh);flood-opacity:var(--shA)"/><feDropShadow dx="0" dy="${farY}" stdDeviation="${far}" style="flood-color:var(--sh);flood-opacity:var(--shB)"/></filter>`;
|
|
236
|
+
const view = follow ? followView(layout, timeline, diagram, m, id, content, follow) : { body: `<rect class="dg-bg" width="${layout.width}" height="${layout.height}" rx="${LOOK.region.radius}"/>${content}`, w: layout.width, h: layout.height };
|
|
237
|
+
const css = `.dg-${id}{${FONT_VARS};--T:${timeline.total}s;${paletteVars(palette, theme)}}${PART_CSS}${m.frames.join("")}`;
|
|
238
|
+
return (
|
|
239
|
+
`<svg xmlns="http://www.w3.org/2000/svg" class="dg dg-${id}" viewBox="0 0 ${Math.ceil(view.w)} ${Math.ceil(view.h)}" width="${Math.ceil(view.w)}" height="${Math.ceil(view.h)}" role="img" aria-label="${esc(diagram.title)}" data-dragon="figure" data-view="${follow ? "follow" : "fixed"}">` +
|
|
240
|
+
`<style>${css}</style><defs>${shadow}</defs>${view.body}</svg>`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** 段で動く物が占める所。 */
|
|
245
|
+
function focusOf(layout: Layout, diagram: Diagram, step: number): Rect | undefined {
|
|
246
|
+
const rects: Rect[] = [];
|
|
247
|
+
for (const e of layout.edges.filter((x) => x.step === step)) {
|
|
248
|
+
const xs = e.points.map((p) => p[0]);
|
|
249
|
+
const ys = e.points.map((p) => p[1]);
|
|
250
|
+
rects.push({ x: Math.min(...xs), y: Math.min(...ys), w: Math.max(...xs) - Math.min(...xs), h: Math.max(...ys) - Math.min(...ys) });
|
|
251
|
+
}
|
|
252
|
+
rects.push(...layout.labels.filter((l) => l.step === step), ...layout.actions.filter((a) => a.step === step));
|
|
253
|
+
for (const event of diagram.steps[step]?.events ?? []) {
|
|
254
|
+
const ids = event.type === "edge" ? [event.from, event.to] : event.type === "action" || event.type === "swap" ? [event.box] : [];
|
|
255
|
+
for (const id of ids) {
|
|
256
|
+
const box = layout.boxes.find((b) => b.id === id);
|
|
257
|
+
// 時系列図の頭の箱は遠いので、線の行だけを見る
|
|
258
|
+
if (box && (layout.kind === "architecture" || event.type === "swap")) rects.push(box);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (rects.length === 0) return undefined;
|
|
262
|
+
const x = Math.min(...rects.map((r) => r.x));
|
|
263
|
+
const y = Math.min(...rects.map((r) => r.y));
|
|
264
|
+
return { x, y, w: Math.max(...rects.map((r) => r.x + r.w)) - x, h: Math.max(...rects.map((r) => r.y + r.h)) - y };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** 寄って追う窓。段ごとに動く所へ寄り、隅の全体図に今見ている所の枠を同じ動きで出す。 */
|
|
268
|
+
function followView(layout: Layout, timeline: Timeline, diagram: Diagram, m: Motion, id: string, content: string, follow: FollowFrame): { body: string; w: number; h: number } {
|
|
269
|
+
const F = LOOK.follow;
|
|
270
|
+
const M = LOOK.mini;
|
|
271
|
+
const vw = Math.min(layout.width, Math.max(1, follow.width));
|
|
272
|
+
const vh = Math.min(layout.height, Math.max(F.minH, vw * F.aspect));
|
|
273
|
+
const clampX = (x: number): number => Math.min(layout.width - vw / 2, Math.max(vw / 2, x));
|
|
274
|
+
const clampY = (y: number): number => Math.min(layout.height - vh / 2, Math.max(vh / 2, y));
|
|
275
|
+
let prev: [number, number] = [layout.width / 2, layout.height / 2];
|
|
276
|
+
const centers = timeline.steps.map((_, s) => {
|
|
277
|
+
const f = focusOf(layout, diagram, s);
|
|
278
|
+
prev = f ? [f.x + f.w / 2, f.y + f.h / 2] : prev;
|
|
279
|
+
return [clampX(prev[0]), clampY(prev[1])] as [number, number];
|
|
280
|
+
});
|
|
281
|
+
const c0 = centers[0] as [number, number];
|
|
282
|
+
const cam: [number, number, number, number?, string?][] = [
|
|
283
|
+
[0, c0[0], c0[1]],
|
|
284
|
+
[(timeline.steps[0] as Span).start, c0[0], c0[1]],
|
|
285
|
+
];
|
|
286
|
+
centers.forEach((c, s) => {
|
|
287
|
+
if (s === 0) return;
|
|
288
|
+
const before = centers[s - 1] as [number, number];
|
|
289
|
+
const start = (timeline.steps[s] as Span).start;
|
|
290
|
+
cam.push([start, before[0], before[1], 1, EASE.inOut], [start + F.ease, c[0], c[1]]);
|
|
291
|
+
});
|
|
292
|
+
const cl = centers[centers.length - 1] as [number, number];
|
|
293
|
+
cam.push([timeline.last, cl[0], cl[1], 1, EASE.inOut], [timeline.total, c0[0], c0[1]]);
|
|
294
|
+
const camKf = m.move(cam.map(([t, cx, cy, s, e]) => [t, vw / 2 - cx, vh / 2 - cy, s, e]));
|
|
295
|
+
|
|
296
|
+
const mw = Math.min(M.w, vw * 0.4, ((vh * 0.4) / layout.height) * layout.width);
|
|
297
|
+
const k = mw / layout.width;
|
|
298
|
+
const mh = layout.height * k;
|
|
299
|
+
// 地の幅は図の縦横の比で決まるので、縦に長い図では札より狭くなる。その時は地を札の幅まで広げ、図は広げた地の真ん中に置く
|
|
300
|
+
const title = FIGURE_TEXT[follow.lang].overview;
|
|
301
|
+
const bw = Math.max(mw, Math.ceil(textWidth(title, TYPE.miniTitle)));
|
|
302
|
+
const bx = vw - bw - M.inset - M.pad;
|
|
303
|
+
const mx = bx + (bw - mw) / 2;
|
|
304
|
+
const my = M.inset + M.pad + M.title;
|
|
305
|
+
const shapes = [
|
|
306
|
+
...layout.regions.map((r) => `<rect class="dg-mm-band" style="${cs(r.color)}" x="${mx + r.x * k}" y="${my + r.y * k}" width="${r.w * k}" height="${r.h * k}" rx="3"/>`),
|
|
307
|
+
...layout.boxes.map((b) => `<rect class="dg-mm-card" style="${cs(b.color)}" x="${mx + b.x * k}" y="${my + b.y * k}" width="${b.w * k}" height="${b.h * k}" rx="1.5"/>`),
|
|
308
|
+
].join("");
|
|
309
|
+
const vp = m.move(cam.map(([t, cx, cy, , e]) => [t, mx + (cx - vw / 2) * k, my + (cy - vh / 2) * k, 1, e]));
|
|
310
|
+
const mini =
|
|
311
|
+
`<rect class="dg-mm-bg" x="${bx - M.pad}" y="${my - M.pad - M.title}" width="${bw + M.pad * 2}" height="${mh + M.pad * 2 + M.title}" rx="${M.radius}"/>` +
|
|
312
|
+
`<text class="dg-mm-t" x="${bx}" y="${my - M.pad + 2}">${title}</text>` +
|
|
313
|
+
`<g clip-path="url(#dg-mm-${id})"><rect class="dg-mm-world" x="${mx}" y="${my}" width="${mw}" height="${mh}"/>${shapes}<rect class="dg-mm-vp dg-a" width="${vw * k}" height="${vh * k}" rx="2" style="${vp}"/></g>`;
|
|
314
|
+
const body =
|
|
315
|
+
`<defs><clipPath id="dg-vp-${id}"><rect width="${vw}" height="${vh}" rx="${LOOK.region.radius}"/></clipPath><clipPath id="dg-mm-${id}"><rect x="${mx}" y="${my}" width="${mw}" height="${mh}" rx="4"/></clipPath></defs>` +
|
|
316
|
+
`<g clip-path="url(#dg-vp-${id})"><rect class="dg-vp-bg" width="${vw}" height="${vh}"/><g class="dg-a" style="${camKf}">${content}</g></g>` +
|
|
317
|
+
`<rect class="dg-vp-frame" x="0.5" y="0.5" width="${vw - 1}" height="${vh - 1}" rx="${LOOK.region.radius}"/>${mini}`;
|
|
318
|
+
return { body, w: vw, h: vh };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 原本の JSON から図を描く。原本が形に合わない時は、誤りの場所を添えて断る。 */
|
|
322
|
+
export function renderDiagram(input: unknown, options: RenderOptions = {}): Rendered {
|
|
323
|
+
const lang = options.lang ?? "ja";
|
|
324
|
+
const read = readDiagram(input, { lang });
|
|
325
|
+
if (!read.ok) throw new Error(FIGURE_TEXT[lang].cannotDraw(read.errors.map((e) => `${e.pointer} ${e.message}`).join(" / ")));
|
|
326
|
+
const diagram = read.diagram;
|
|
327
|
+
const palette = resolvePalette(options.palette);
|
|
328
|
+
const theme: Theme = options.theme === "dark" ? "dark" : "light";
|
|
329
|
+
const layout = diagram.kind === "sequence" ? layoutSequence(diagram) : layoutArchitecture(diagram);
|
|
330
|
+
const timeline = buildTimeline(diagram);
|
|
331
|
+
return {
|
|
332
|
+
svg: drawSvg(diagram, layout, timeline, palette, theme),
|
|
333
|
+
palette: palette.id,
|
|
334
|
+
theme,
|
|
335
|
+
...(diagram.view ? { view: diagram.view } : {}),
|
|
336
|
+
fonts: { ...FONTS },
|
|
337
|
+
layout,
|
|
338
|
+
timeline,
|
|
339
|
+
diagram,
|
|
340
|
+
};
|
|
341
|
+
}
|