@cardenelabs/cdl 0.5.0
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/LICENSE +21 -0
- package/README.md +343 -0
- package/SPEC.md +374 -0
- package/dist/index.cjs +28085 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3415 -0
- package/dist/index.d.ts +3415 -0
- package/dist/index.js +27962 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +23043 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +2 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +23041 -0
- package/dist/react.js.map +1 -0
- package/dist/render-C78lIXeC.d.cts +2449 -0
- package/dist/render-C78lIXeC.d.ts +2449 -0
- package/examples/quick-start.md +88 -0
- package/package.json +79 -0
- package/src/anim/core/easing.ts +77 -0
- package/src/anim/core/timeline.ts +335 -0
- package/src/anim/core/types.ts +42 -0
- package/src/anim/index.ts +20 -0
- package/src/anim/react/index.ts +3 -0
- package/src/anim/react/useReducedMotion.ts +27 -0
- package/src/anim/react/useTimeline.ts +48 -0
- package/src/assert-never.ts +16 -0
- package/src/author-intent-verify.ts +587 -0
- package/src/builder.ts +3740 -0
- package/src/compile.ts +12 -0
- package/src/dom-verify-core.ts +121 -0
- package/src/dom-verify-types.ts +21 -0
- package/src/dom-verify.ts +948 -0
- package/src/event-handler/index.ts +184 -0
- package/src/formula/ast.ts +46 -0
- package/src/formula/evaluator.ts +163 -0
- package/src/formula/formula-computeds.ts +83 -0
- package/src/formula/index.ts +5 -0
- package/src/formula/parser.ts +399 -0
- package/src/index.ts +284 -0
- package/src/input-signals.ts +74 -0
- package/src/kinds/actor.tsx +79 -0
- package/src/kinds/card.tsx +61 -0
- package/src/kinds/chart-bar.tsx +121 -0
- package/src/kinds/chart-line.tsx +163 -0
- package/src/kinds/chart-pie.tsx +107 -0
- package/src/kinds/compact-title.ts +164 -0
- package/src/kinds/dyn-shape.tsx +394 -0
- package/src/kinds/event.tsx +70 -0
- package/src/kinds/function.tsx +53 -0
- package/src/kinds/funnel.tsx +122 -0
- package/src/kinds/gantt.tsx +193 -0
- package/src/kinds/generic.tsx +368 -0
- package/src/kinds/mind-map.tsx +367 -0
- package/src/kinds/mind-radial.tsx +209 -0
- package/src/kinds/node-tone.ts +18 -0
- package/src/kinds/quadrant.tsx +164 -0
- package/src/kinds/row-align.ts +179 -0
- package/src/kinds/shape-basement.tsx +683 -0
- package/src/kinds/shape-blockchain.tsx +750 -0
- package/src/kinds/shape-commerce.tsx +553 -0
- package/src/kinds/shape-finance.tsx +706 -0
- package/src/kinds/shape-hardware.tsx +862 -0
- package/src/kinds/shape-people.tsx +439 -0
- package/src/kinds/shape-region.tsx +383 -0
- package/src/kinds/shape-software.tsx +859 -0
- package/src/kinds/storage.tsx +180 -0
- package/src/kinds/text-width.ts +73 -0
- package/src/kinds/tree.tsx +275 -0
- package/src/kinds/user-journey.tsx +240 -0
- package/src/label-text.ts +71 -0
- package/src/layout/clearance-constants.ts +47 -0
- package/src/layout/collisions.ts +2078 -0
- package/src/layout/edges.ts +2498 -0
- package/src/layout/footer-shape.ts +97 -0
- package/src/layout/geometry.ts +135 -0
- package/src/layout/label-shift.ts +28 -0
- package/src/layout/lanes.ts +328 -0
- package/src/layout/nodes.ts +190 -0
- package/src/layout/predict-bbox.ts +76 -0
- package/src/layout/px-projection.ts +176 -0
- package/src/layout/spec.ts +872 -0
- package/src/layout/text-width.ts +133 -0
- package/src/layout/tokens.ts +181 -0
- package/src/layout/viewbox.ts +47 -0
- package/src/layout-with-validation.ts +175 -0
- package/src/layout.ts +381 -0
- package/src/presets.ts +2108 -0
- package/src/reactive/batch.ts +48 -0
- package/src/reactive/computed.ts +85 -0
- package/src/reactive/effect.ts +145 -0
- package/src/reactive/index.ts +6 -0
- package/src/reactive/internal.ts +109 -0
- package/src/reactive/signal.ts +113 -0
- package/src/render/edges.tsx +318 -0
- package/src/render/header.tsx +146 -0
- package/src/render/interactive-panel.tsx +9620 -0
- package/src/render/nodes.tsx +319 -0
- package/src/render/stage.tsx +377 -0
- package/src/render/tone.ts +64 -0
- package/src/render/utils.ts +238 -0
- package/src/render.tsx +221 -0
- package/src/scroll-trigger/index.ts +109 -0
- package/src/scroll-trigger/progress.ts +49 -0
- package/src/thumbnail.tsx +114 -0
- package/src/types.ts +2371 -0
- package/src/validate.ts +268 -0
- package/src/visual-validate.ts +4381 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import { shapeRegion, labelPlan, fitArt } from "./shape-region";
|
|
3
|
+
import type { LaidNode } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shape-driven finance 7 kinds (CAR-1111 Phase 2-D)。
|
|
7
|
+
*
|
|
8
|
+
* bank / trust-bank / payment-provider / credit-card / brokerage / exchange / atm。
|
|
9
|
+
*
|
|
10
|
+
* 設計原則。
|
|
11
|
+
* 1. shape 本体は node.cx, node.cy を中心に上下左右対称に配置する
|
|
12
|
+
* 2. 中央に 1 つの視覚シンボル (アイコン) を大きく置く
|
|
13
|
+
* 3. label は shape の下に固定 (下から 40px 上を title の baseline)
|
|
14
|
+
* 4. 詳細情報 (氏名 / 金額 / 番号) は削って抽象化、 一目でそれと分かる silhouette 優先
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type ShapeProps = {
|
|
18
|
+
node: LaidNode;
|
|
19
|
+
active: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const commonFrame = (active: boolean) => ({
|
|
23
|
+
fill: "var(--cdl-node-fill, #ffffff)",
|
|
24
|
+
stroke: active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)",
|
|
25
|
+
strokeWidth: active ? 3 : 1.5,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** 肩書の字間。 幅で縮める時はこれも同じ割合で詰める。 */
|
|
29
|
+
const LABEL_EYEBROW_SPACING = 2;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 1 字の幅は字の大きさの何倍か。 呼び出し側の書体
|
|
33
|
+
* (`"Inter Variable", Inter, -apple-system, "Segoe UI", sans-serif`) を太字 700 で実測した値。
|
|
34
|
+
*
|
|
35
|
+
* **一律の係数は使えない**。 半角の実測は最小 0.271 (`i` / `j` / `l`) から最大 1.038 (`W`) まで
|
|
36
|
+
* 4 倍近く開く。 平均の 0.630 で置くと `W` の並びで 3 分の 2 しか見ず、 その比で縮めても箱から
|
|
37
|
+
* 出たままになる (`W` 20 字が実測 390.4 に対し 0.7 の見積りで 224、 縮めた後も 86 出た)。
|
|
38
|
+
*
|
|
39
|
+
* | 階級 | 実測の最大 | 係数 |
|
|
40
|
+
* |---|---|---|
|
|
41
|
+
* | 細い (`i j l I . , : ; '` `!` と空白) | 0.365 | 0.37 |
|
|
42
|
+
* | 広い (`W @ % M m`) | 1.038 (`W`) | 1.04 |
|
|
43
|
+
* | その他の ASCII | 0.850 (`w`) | 0.86 |
|
|
44
|
+
* | ASCII の外 | 1.000 (和文) | 1.04 |
|
|
45
|
+
*
|
|
46
|
+
* 並 400 の最大は 0.985 なので太字の値で覆える。
|
|
47
|
+
*/
|
|
48
|
+
const 細字 = new Set([..."ijlI.,:;'! "]);
|
|
49
|
+
const 広字 = new Set([..."W@%Mm"]);
|
|
50
|
+
const 細字比 = 0.37;
|
|
51
|
+
const 広字比 = 1.04;
|
|
52
|
+
const 並字比 = 0.86;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 1 字が字の大きさの何倍か。
|
|
56
|
+
*
|
|
57
|
+
* **ASCII の外は分類しない**。 「どの字が全角か」 を手で列挙する限り抜けは出続け、 抜けは必ず
|
|
58
|
+
* **過小見積り** = 収まっていない物を収まっていると判定する向きに外れる。 実際に 3 回とも別の
|
|
59
|
+
* 抜け方をした (範囲の端点に見た目の同じ字が入る / 私用領域が混じる / 区画指定からカタカナ拡張と
|
|
60
|
+
* CJK 互換が落ちる)。
|
|
61
|
+
*
|
|
62
|
+
* ASCII の外は実測の最大 `1.04` で数える。 全角の実測は 1.000 なので 4% 広く見積もるだけで、
|
|
63
|
+
* 半角カナ (約 0.5) も外字も Latin-1 もギリシャ文字もまとめて安全側に倒れる。 代償は和文の名前が
|
|
64
|
+
* 4% 余分に縮むことだけで、 過小見積りが構造的に起きなくなる。
|
|
65
|
+
*/
|
|
66
|
+
function 字の比(c: string): number {
|
|
67
|
+
if (細字.has(c)) return 細字比;
|
|
68
|
+
if (広字.has(c)) return 広字比;
|
|
69
|
+
return (c.codePointAt(0) ?? 0) < 128 ? 並字比 : 広字比;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 字の並びの幅の見積り。 字ごとの比を足して、 字間を字数分足す。 */
|
|
73
|
+
function 字幅(文字列: string, 大きさ: number, 字間: number): number {
|
|
74
|
+
let 幅 = 0;
|
|
75
|
+
let 字数 = 0;
|
|
76
|
+
for (const c of 文字列) {
|
|
77
|
+
幅 += 大きさ * 字の比(c);
|
|
78
|
+
字数 += 1;
|
|
79
|
+
}
|
|
80
|
+
return 幅 + 字間 * 字数;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 1 行を箱の幅に収める字の大きさと字間。
|
|
85
|
+
*
|
|
86
|
+
* `labelPlan` は箱の **高さ** だけから字の大きさを決めていて、 幅も字数も見ていない。 その
|
|
87
|
+
* ままだと長い名前が箱の左右へ出る (幅 140 / 高さ 72 で `LongApplicationService` は 12px で
|
|
88
|
+
* 175.8 = 左右へ 17.9)。 絵と違って `fitArt` は名前を包まないので、 ここで行ごとに縮める。
|
|
89
|
+
*
|
|
90
|
+
* 字間も同じ割合で詰める = 大きさだけ縮めても字間が残ると幅が比例して縮まない。
|
|
91
|
+
*/
|
|
92
|
+
function 幅に収める(文字列: string, 大きさ: number, 字間: number, 箱幅: number) {
|
|
93
|
+
const 幅 = 字幅(文字列, 大きさ, 字間);
|
|
94
|
+
const 倍率 = 幅 > 0 && 箱幅 > 0 ? Math.min(1, 箱幅 / 幅) : 1;
|
|
95
|
+
return { size: 大きさ * 倍率, spacing: 字間 * 倍率 };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 名前の塊を描く。 縦の位置と大きさは `labelPlan` が決め (箱の下端から積み上げる、 #433)、
|
|
100
|
+
* 横は `幅に収める` が箱の幅に合わせて縮める。
|
|
101
|
+
*/
|
|
102
|
+
function bottomLabel(node: LaidNode, cx: number, plan: ReturnType<typeof labelPlan>) {
|
|
103
|
+
const 肩 = 幅に収める(node.eyebrow ?? "", plan.肩書?.size ?? 0, LABEL_EYEBROW_SPACING, node.w);
|
|
104
|
+
const 名 = 幅に収める(node.title ?? "", plan.名前.size, 0, node.w);
|
|
105
|
+
const 説 = 幅に収める(node.subtitle ?? "", plan.説明?.size ?? 0, 0, node.w);
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
{node.eyebrow && plan.肩書 && (
|
|
109
|
+
<text x={cx} y={plan.肩書.y} fontSize={肩.size} fontWeight={700} letterSpacing={肩.spacing} textAnchor="middle" fill="var(--cdl-tone-accent, #2d6a8f)">
|
|
110
|
+
{node.eyebrow}
|
|
111
|
+
</text>
|
|
112
|
+
)}
|
|
113
|
+
<text data-cdl-role="node-label" x={cx} y={plan.名前.y} fontSize={名.size} fontWeight={700} textAnchor="middle" fill="var(--cdl-text, #1a1f2a)">
|
|
114
|
+
{node.title}
|
|
115
|
+
</text>
|
|
116
|
+
{node.subtitle && plan.説明 && (
|
|
117
|
+
<text x={cx} y={plan.説明.y} fontSize={説.size} textAnchor="middle" fill="var(--cdl-text-dim, #5a6270)">
|
|
118
|
+
{node.subtitle}
|
|
119
|
+
</text>
|
|
120
|
+
)}
|
|
121
|
+
</>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* shape の描画 region を計算する helper。
|
|
127
|
+
* label 用に下部を除外、 残り領域を shape に割当。
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 神殿の絵 (`shape-bank` / `shape-trust-bank`) の固定部品 (#433)。
|
|
132
|
+
*
|
|
133
|
+
* 階段と桁と台は建物の外へ出る。 出る量は **箱の高さに依らない** 固定量なので、 建物を帯
|
|
134
|
+
* いっぱいに描くとその分がそのまま箱の外へ出る。 だから建物の寸法を決める前に、 **描ける帯を
|
|
135
|
+
* この分だけ先に狭める**。
|
|
136
|
+
*
|
|
137
|
+
* 広がりは描く側のこの定数から出す = 表に書き写すと絵を変えた時に片方だけ古くなる。
|
|
138
|
+
*/
|
|
139
|
+
const BANK_PEDIMENT_H = 44;
|
|
140
|
+
const BANK_ARCHITRAVE_H = 14;
|
|
141
|
+
const BANK_BASE_H = 16;
|
|
142
|
+
/** 破風の頂点が建物の上端より高い分。 */
|
|
143
|
+
const BANK_PEDIMENT_RISE = 4;
|
|
144
|
+
/** 桁と台が建物の左右へ出る量。 */
|
|
145
|
+
const BANK_BODY_OUT = 8;
|
|
146
|
+
/** 階段。 段ごとに横 24 / 縦 8 ずれ、 厚み 9、 枠 1。 一番外の段が絵の左右と下端を決める。 */
|
|
147
|
+
const BANK_STEP_DX = 24;
|
|
148
|
+
const BANK_STEP_DY = 8;
|
|
149
|
+
const BANK_STEP_H = 9;
|
|
150
|
+
const BANK_STEP_SW = 1;
|
|
151
|
+
/** 柱が潰れない高さ。 柱頭 6 + 胴 12 + 柱脚 6。 */
|
|
152
|
+
const BANK_COLUMN_MIN = 24;
|
|
153
|
+
/** 建物の幅。 下限は柱 4 本と隙間 5 本が潰れない幅。 */
|
|
154
|
+
const BANK_BLD_W_MIN = 120;
|
|
155
|
+
const BANK_BLD_W_MAX = 280;
|
|
156
|
+
|
|
157
|
+
/** shape-bank = 銀行 (Greek 神殿 pediment + $ + 4 columns + base、 中央配置) */
|
|
158
|
+
export function ShapeBankNode({ node, active }: ShapeProps): JSX.Element {
|
|
159
|
+
const frame = commonFrame(active);
|
|
160
|
+
const r = shapeRegion(node);
|
|
161
|
+
const 線 = frame.strokeWidth / 2;
|
|
162
|
+
const nSteps = 3;
|
|
163
|
+
// 建物の外へ出る量。 上は破風の頂点、 下は一番下の段、 左右は一番外の段と台のうち広い方
|
|
164
|
+
const 上出 = BANK_PEDIMENT_RISE + 線;
|
|
165
|
+
const 下出 = (nSteps - 1) * BANK_STEP_DY + BANK_STEP_H + BANK_STEP_SW / 2;
|
|
166
|
+
const 横出 = Math.max(((nSteps - 1) * BANK_STEP_DX) / 2 + BANK_STEP_SW / 2, BANK_BODY_OUT + 線);
|
|
167
|
+
// 帯を狭めてから建物の高さを決める。 部品が潰れる箱では下限で止め、 入り切らない分は
|
|
168
|
+
// `fitArt` が絵ごと縮める (縦横の比は保たれる)
|
|
169
|
+
const bldHMin = BANK_PEDIMENT_H + BANK_ARCHITRAVE_H + BANK_BASE_H + BANK_COLUMN_MIN;
|
|
170
|
+
const 縦 = Math.max(r.availH, bldHMin + 上出 + 下出);
|
|
171
|
+
const bldH = 縦 - 上出 - 下出;
|
|
172
|
+
const bldW = Math.min(Math.max(r.availW - 横出 * 2, BANK_BLD_W_MIN), BANK_BLD_W_MAX);
|
|
173
|
+
const x = r.cx - bldW / 2;
|
|
174
|
+
const y = r.cy - 縦 / 2 + 上出;
|
|
175
|
+
const 広がり = { 上: 縦 / 2, 下: 縦 / 2, 左: bldW / 2 + 横出, 右: bldW / 2 + 横出 };
|
|
176
|
+
const pedimentH = BANK_PEDIMENT_H;
|
|
177
|
+
const architraveH = BANK_ARCHITRAVE_H;
|
|
178
|
+
const baseH = BANK_BASE_H;
|
|
179
|
+
const columnAreaY = y + pedimentH + architraveH;
|
|
180
|
+
const columnAreaH = bldH - pedimentH - architraveH - baseH;
|
|
181
|
+
const nCols = 4;
|
|
182
|
+
const colGap = 14;
|
|
183
|
+
const areaW = bldW - 24;
|
|
184
|
+
const colW = (areaW - colGap * (nCols + 1)) / nCols;
|
|
185
|
+
const 絵 = (
|
|
186
|
+
<g>
|
|
187
|
+
{Array.from({ length: nSteps }, (_, i) => i).map((i) => {
|
|
188
|
+
const w = bldW + (nSteps - 1 - i) * BANK_STEP_DX;
|
|
189
|
+
return <rect key={i} x={r.cx - w / 2} y={y + bldH + i * BANK_STEP_DY} width={w} height={BANK_STEP_H} fill="var(--cdl-divider, #d4d4d8)" fillOpacity={0.5 + i * 0.15} stroke={frame.stroke} strokeWidth={BANK_STEP_SW} />;
|
|
190
|
+
})}
|
|
191
|
+
<path d={`M ${x - BANK_BODY_OUT} ${y + pedimentH} L ${r.cx} ${y - BANK_PEDIMENT_RISE} L ${x + bldW + BANK_BODY_OUT} ${y + pedimentH} Z`} fill="var(--cdl-node-fill, #ffffff)" stroke={frame.stroke} strokeWidth={frame.strokeWidth} strokeLinejoin="round" />
|
|
192
|
+
<circle cx={r.cx} cy={y + pedimentH / 2 + 4} r={14} fill="var(--cdl-tone-accent, #2d6a8f)" stroke={frame.stroke} strokeWidth={1.5} />
|
|
193
|
+
<text x={r.cx} y={y + pedimentH / 2 + 10} fontSize={18} fontWeight={700} textAnchor="middle" fill="#ffffff" fontFamily="Georgia, serif">
|
|
194
|
+
$
|
|
195
|
+
</text>
|
|
196
|
+
<rect x={x - 4} y={y + pedimentH} width={bldW + 8} height={architraveH} fill="var(--cdl-divider, #d4d4d8)" fillOpacity={0.4} stroke={frame.stroke} strokeWidth={frame.strokeWidth} />
|
|
197
|
+
{Array.from({ length: nCols }, (_, i) => i).map((i) => {
|
|
198
|
+
const colCx = x + 12 + colGap + i * (colW + colGap) + colW / 2;
|
|
199
|
+
return (
|
|
200
|
+
<g key={i}>
|
|
201
|
+
<rect x={colCx - colW / 2 - 4} y={columnAreaY} width={colW + 8} height={6} fill="var(--cdl-node-fill, #ffffff)" stroke={frame.stroke} strokeWidth={1.2} />
|
|
202
|
+
<rect x={colCx - colW / 2} y={columnAreaY + 6} width={colW} height={columnAreaH - 12} fill="var(--cdl-node-fill, #ffffff)" stroke={frame.stroke} strokeWidth={1.2} />
|
|
203
|
+
<line x1={colCx - colW * 0.3} y1={columnAreaY + 12} x2={colCx - colW * 0.3} y2={columnAreaY + columnAreaH - 12} stroke={frame.stroke} strokeWidth={0.8} opacity={0.5} />
|
|
204
|
+
<line x1={colCx + colW * 0.3} y1={columnAreaY + 12} x2={colCx + colW * 0.3} y2={columnAreaY + columnAreaH - 12} stroke={frame.stroke} strokeWidth={0.8} opacity={0.5} />
|
|
205
|
+
<rect x={colCx - colW / 2 - 4} y={columnAreaY + columnAreaH - 6} width={colW + 8} height={6} fill="var(--cdl-node-fill, #ffffff)" stroke={frame.stroke} strokeWidth={1.2} />
|
|
206
|
+
</g>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
<rect data-cdl-role="node-body" x={x - BANK_BODY_OUT} y={y + bldH - baseH} width={bldW + BANK_BODY_OUT * 2} height={baseH} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.4} stroke={frame.stroke} strokeWidth={frame.strokeWidth} />
|
|
210
|
+
</g>
|
|
211
|
+
);
|
|
212
|
+
return (
|
|
213
|
+
<g>
|
|
214
|
+
{fitArt(r, 広がり, 絵)}
|
|
215
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
216
|
+
</g>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 王冠 (`shape-trust-bank`) の固定部品。 建物の上に載るので絵の上端はここで決まる。
|
|
222
|
+
* 神殿の部分は `BANK_*` と同じ意味。
|
|
223
|
+
*/
|
|
224
|
+
const TRUST_CROWN_RISE = 4;
|
|
225
|
+
const TRUST_CROWN_TOP = 22;
|
|
226
|
+
const TRUST_CROWN_HALF = 32;
|
|
227
|
+
const TRUST_PEDIMENT_H = 40;
|
|
228
|
+
const TRUST_ARCHITRAVE_H = 14;
|
|
229
|
+
const TRUST_BASE_H = 14;
|
|
230
|
+
const TRUST_BODY_OUT = 8;
|
|
231
|
+
/** 建物の幅。 下限は柱 3 本と隙間 4 本が潰れない幅。 */
|
|
232
|
+
const TRUST_BLD_W_MIN = 140;
|
|
233
|
+
const TRUST_BLD_W_MAX = 280;
|
|
234
|
+
|
|
235
|
+
/** shape-trust-bank = 信託銀行 (王冠 + 神殿 3 columns、 中央柱に余計装飾なし) */
|
|
236
|
+
export function ShapeTrustBankNode({ node, active }: ShapeProps): JSX.Element {
|
|
237
|
+
const frame = commonFrame(active);
|
|
238
|
+
const r = shapeRegion(node);
|
|
239
|
+
const 線 = frame.strokeWidth / 2;
|
|
240
|
+
const nSteps = 2;
|
|
241
|
+
// 銀行と同じ考え方。 上は王冠の頂点、 下は一番下の段、 左右は段と台のうち広い方
|
|
242
|
+
const 上出 = TRUST_CROWN_RISE + TRUST_CROWN_TOP + 線;
|
|
243
|
+
const 下出 = (nSteps - 1) * BANK_STEP_DY + BANK_STEP_H + BANK_STEP_SW / 2;
|
|
244
|
+
const 横出 = Math.max(((nSteps - 1) * BANK_STEP_DX) / 2 + BANK_STEP_SW / 2, TRUST_BODY_OUT + 線);
|
|
245
|
+
const bldHMin = TRUST_PEDIMENT_H + TRUST_ARCHITRAVE_H + TRUST_BASE_H + BANK_COLUMN_MIN;
|
|
246
|
+
const 縦 = Math.max(r.availH, bldHMin + 上出 + 下出);
|
|
247
|
+
const bldH = 縦 - 上出 - 下出;
|
|
248
|
+
const bldW = Math.min(Math.max(r.availW - 横出 * 2, TRUST_BLD_W_MIN), TRUST_BLD_W_MAX);
|
|
249
|
+
const x = r.cx - bldW / 2;
|
|
250
|
+
const y = r.cy - 縦 / 2 + 上出;
|
|
251
|
+
// 王冠は半幅 32 で描かれるが、 建物の下限 140 の半分に段の出幅を足した 82.5 より内側に
|
|
252
|
+
// 収まるため、 左右を決めるのは常に建物側になる
|
|
253
|
+
const 広がり = { 上: 縦 / 2, 下: 縦 / 2, 左: bldW / 2 + 横出, 右: bldW / 2 + 横出 };
|
|
254
|
+
const pedimentH = TRUST_PEDIMENT_H;
|
|
255
|
+
const architraveH = TRUST_ARCHITRAVE_H;
|
|
256
|
+
const baseH = TRUST_BASE_H;
|
|
257
|
+
const columnAreaY = y + pedimentH + architraveH;
|
|
258
|
+
const columnAreaH = bldH - pedimentH - architraveH - baseH;
|
|
259
|
+
const nCols = 3;
|
|
260
|
+
const colGap = 22;
|
|
261
|
+
const areaW = bldW - 30;
|
|
262
|
+
const colW = (areaW - colGap * (nCols + 1)) / nCols;
|
|
263
|
+
const 絵 = (
|
|
264
|
+
<g>
|
|
265
|
+
<g transform={`translate(${r.cx}, ${y - TRUST_CROWN_RISE})`}>
|
|
266
|
+
<path d={`M ${-TRUST_CROWN_HALF} 0 L -22 -18 L -10 -6 L 0 ${-TRUST_CROWN_TOP} L 10 -6 L 22 -18 L ${TRUST_CROWN_HALF} 0 Z`} fill="#e5c07b" stroke="#a67c00" strokeWidth={frame.strokeWidth} strokeLinejoin="round" />
|
|
267
|
+
<rect x={-TRUST_CROWN_HALF} y={0} width={TRUST_CROWN_HALF * 2} height={5} fill="#c99820" stroke="#a67c00" strokeWidth={BANK_STEP_SW} />
|
|
268
|
+
<circle cx={-22} cy={-14} r={3} fill="#c9302c" stroke="#7a1a10" strokeWidth={0.8} />
|
|
269
|
+
<circle cx={0} cy={-18} r={4} fill="#3a80d1" stroke="#1a4a80" strokeWidth={0.8} />
|
|
270
|
+
<circle cx={22} cy={-14} r={3} fill="#c9302c" stroke="#7a1a10" strokeWidth={0.8} />
|
|
271
|
+
</g>
|
|
272
|
+
{Array.from({ length: nSteps }, (_, i) => i).map((i) => {
|
|
273
|
+
const w = bldW + (nSteps - 1 - i) * BANK_STEP_DX;
|
|
274
|
+
return <rect key={i} x={r.cx - w / 2} y={y + bldH + i * BANK_STEP_DY} width={w} height={BANK_STEP_H} fill="var(--cdl-divider, #d4d4d8)" fillOpacity={0.5 + i * 0.15} stroke={frame.stroke} strokeWidth={BANK_STEP_SW} />;
|
|
275
|
+
})}
|
|
276
|
+
<path d={`M ${x - 6} ${y + pedimentH} L ${r.cx} ${y + 2} L ${x + bldW + 6} ${y + pedimentH} Z`} fill="var(--cdl-node-fill, #ffffff)" stroke={frame.stroke} strokeWidth={frame.strokeWidth} strokeLinejoin="round" />
|
|
277
|
+
<text x={r.cx} y={y + pedimentH - 8} fontSize={15} fontWeight={700} letterSpacing={2} textAnchor="middle" fill="var(--cdl-tone-accent, #2d6a8f)" fontFamily="Georgia, serif">
|
|
278
|
+
TRUST
|
|
279
|
+
</text>
|
|
280
|
+
<rect x={x - 4} y={y + pedimentH} width={bldW + 8} height={architraveH} fill="var(--cdl-divider, #d4d4d8)" fillOpacity={0.4} stroke={frame.stroke} strokeWidth={frame.strokeWidth} />
|
|
281
|
+
{Array.from({ length: nCols }, (_, i) => i).map((i) => {
|
|
282
|
+
const colCx = x + 15 + colGap + i * (colW + colGap) + colW / 2;
|
|
283
|
+
return (
|
|
284
|
+
<g key={i}>
|
|
285
|
+
<rect x={colCx - colW / 2 - 4} y={columnAreaY} width={colW + 8} height={6} fill="var(--cdl-node-fill, #ffffff)" stroke={frame.stroke} strokeWidth={1.2} />
|
|
286
|
+
<rect x={colCx - colW / 2} y={columnAreaY + 6} width={colW} height={columnAreaH - 12} fill="var(--cdl-node-fill, #ffffff)" stroke={frame.stroke} strokeWidth={1.2} />
|
|
287
|
+
<line x1={colCx - colW * 0.3} y1={columnAreaY + 12} x2={colCx - colW * 0.3} y2={columnAreaY + columnAreaH - 12} stroke={frame.stroke} strokeWidth={0.8} opacity={0.5} />
|
|
288
|
+
<line x1={colCx + colW * 0.3} y1={columnAreaY + 12} x2={colCx + colW * 0.3} y2={columnAreaY + columnAreaH - 12} stroke={frame.stroke} strokeWidth={0.8} opacity={0.5} />
|
|
289
|
+
<rect x={colCx - colW / 2 - 4} y={columnAreaY + columnAreaH - 6} width={colW + 8} height={6} fill="var(--cdl-node-fill, #ffffff)" stroke={frame.stroke} strokeWidth={1.2} />
|
|
290
|
+
</g>
|
|
291
|
+
);
|
|
292
|
+
})}
|
|
293
|
+
<rect data-cdl-role="node-body" x={x - TRUST_BODY_OUT} y={y + bldH - baseH} width={bldW + TRUST_BODY_OUT * 2} height={baseH} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.4} stroke={frame.stroke} strokeWidth={frame.strokeWidth} />
|
|
294
|
+
</g>
|
|
295
|
+
);
|
|
296
|
+
return (
|
|
297
|
+
<g>
|
|
298
|
+
{fitArt(r, 広がり, 絵)}
|
|
299
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
300
|
+
</g>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** shape-payment-provider = P2-A base (中央 provider box + PAY header + processing spinner + PROCESSING label) */
|
|
305
|
+
export function ShapePaymentProviderNode({ node, active }: ShapeProps): JSX.Element {
|
|
306
|
+
const frame = commonFrame(active);
|
|
307
|
+
const r = shapeRegion(node);
|
|
308
|
+
const cx = r.cx;
|
|
309
|
+
const cy = r.cy + 4;
|
|
310
|
+
const boxW = Math.min(r.availW - 60, 140);
|
|
311
|
+
const boxH = Math.min(r.availH - 40, 200);
|
|
312
|
+
const x = cx - boxW / 2;
|
|
313
|
+
const y = cy - boxH / 2;
|
|
314
|
+
const headerH = 32;
|
|
315
|
+
const accentDark = "#1a4a68";
|
|
316
|
+
const accentDarker = "#0f3450";
|
|
317
|
+
return (
|
|
318
|
+
<g>
|
|
319
|
+
{/* drop shadow */}
|
|
320
|
+
<ellipse cx={cx} cy={y + boxH + 10} rx={boxW * 0.42} ry={6} fill={frame.stroke} opacity={0.2} stroke="none" />
|
|
321
|
+
{/* provider box */}
|
|
322
|
+
<rect data-cdl-role="node-body" x={x} y={y} width={boxW} height={boxH} rx={10} fill="#255a7a" stroke={accentDark} strokeWidth={frame.strokeWidth} />
|
|
323
|
+
{/* PAY header */}
|
|
324
|
+
<rect x={x} y={y} width={boxW} height={headerH} rx={10} fill={accentDarker} />
|
|
325
|
+
<rect x={x} y={y + headerH - 8} width={boxW} height={8} fill={accentDarker} />
|
|
326
|
+
<text x={cx} y={y + 22} fontSize={16} fontWeight={700} letterSpacing={4} textAnchor="middle" fill="#e5c07b">PAY</text>
|
|
327
|
+
{/* processing spinner (rotate anim) */}
|
|
328
|
+
<g>
|
|
329
|
+
<circle cx={cx} cy={cy + 4} r={22} fill="none" stroke="#e5c07b" strokeWidth={3.5} strokeDasharray="38 24" strokeLinecap="round">
|
|
330
|
+
<animateTransform attributeName="transform" type="rotate" from={`0 ${cx} ${cy + 4}`} to={`360 ${cx} ${cy + 4}`} dur="2s" repeatCount="indefinite" />
|
|
331
|
+
</circle>
|
|
332
|
+
<circle cx={cx} cy={cy + 4} r={10} fill="#e5c07b" stroke={accentDarker} strokeWidth={1.5} />
|
|
333
|
+
<text x={cx} y={cy + 9} fontSize={13} fontWeight={700} textAnchor="middle" fill={accentDarker} fontFamily="Georgia, serif">$</text>
|
|
334
|
+
</g>
|
|
335
|
+
{/* PROCESSING label */}
|
|
336
|
+
<text x={cx} y={y + boxH - 16} fontSize={9} fontWeight={700} letterSpacing={2} textAnchor="middle" fill="#a4b5f2">PROCESSING</text>
|
|
337
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
338
|
+
</g>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** shape-credit-card = クレジットカード (シンプル、 chip + NFC + 4 digit block + brand) */
|
|
343
|
+
export function ShapeCreditCardNode({ node, active }: ShapeProps): JSX.Element {
|
|
344
|
+
const frame = commonFrame(active);
|
|
345
|
+
const r = shapeRegion(node);
|
|
346
|
+
const cardW = Math.min(r.availW - 30, 260);
|
|
347
|
+
const cardH = Math.min(r.availH - 20, 160);
|
|
348
|
+
const x = r.cx - cardW / 2;
|
|
349
|
+
const y = r.cy - cardH / 2;
|
|
350
|
+
return (
|
|
351
|
+
<g>
|
|
352
|
+
<rect x={x + 4} y={y + 4} width={cardW} height={cardH} rx={16} fill={frame.stroke} opacity={0.25} stroke="none" />
|
|
353
|
+
<rect data-cdl-role="node-body" x={x} y={y} width={cardW} height={cardH} rx={16} fill="var(--cdl-tone-accent, #2d6a8f)" stroke="#1a4a68" strokeWidth={frame.strokeWidth} />
|
|
354
|
+
<path d={`M ${x} ${y} L ${x + cardW * 0.7} ${y} L ${x} ${y + cardH * 0.7} Z`} fill="#ffffff" opacity={0.1} stroke="none" />
|
|
355
|
+
{/* IC chip */}
|
|
356
|
+
<rect x={x + 26} y={y + cardH / 2 - 22} width={46} height={36} rx={5} fill="#e5c07b" stroke="#a67c00" strokeWidth={1.5} />
|
|
357
|
+
<rect x={x + 30} y={y + cardH / 2 - 18} width={38} height={28} rx={3} fill="none" stroke="#a67c00" strokeWidth={1.2} />
|
|
358
|
+
<line x1={x + 26} y1={y + cardH / 2 - 4} x2={x + 72} y2={y + cardH / 2 - 4} stroke="#a67c00" strokeWidth={0.8} />
|
|
359
|
+
<line x1={x + 49} y1={y + cardH / 2 - 22} x2={x + 49} y2={y + cardH / 2 + 14} stroke="#a67c00" strokeWidth={0.8} />
|
|
360
|
+
{/* NFC */}
|
|
361
|
+
<g transform={`translate(${x + 88}, ${y + cardH / 2 - 16})`} fill="none" stroke="#ffffff" strokeWidth={2.5} strokeLinecap="round">
|
|
362
|
+
<path d="M 0 16 Q 10 16, 10 6" />
|
|
363
|
+
<path d="M 8 24 Q 22 24, 22 6" />
|
|
364
|
+
</g>
|
|
365
|
+
{/* 4 digit block dots */}
|
|
366
|
+
{[0, 1, 2, 3].map((block) => (
|
|
367
|
+
<g key={block}>
|
|
368
|
+
{[0, 1, 2, 3].map((dot) => (
|
|
369
|
+
<circle key={dot} cx={x + 32 + block * (cardW - 64) / 4 + dot * 8} cy={y + cardH - 40} r={2.8} fill="#ffffff" stroke="none" />
|
|
370
|
+
))}
|
|
371
|
+
</g>
|
|
372
|
+
))}
|
|
373
|
+
{/* brand mark */}
|
|
374
|
+
<circle cx={x + cardW - 48} cy={y + cardH - 24} r={15} fill="#c9302c" opacity={0.9} stroke="none" />
|
|
375
|
+
<circle cx={x + cardW - 30} cy={y + cardH - 24} r={15} fill="#e5c07b" opacity={0.9} stroke="none" />
|
|
376
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
377
|
+
</g>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 本社ビル (`shape-brokerage`) の固定部品 (#433)。
|
|
383
|
+
*
|
|
384
|
+
* 電光掲示板 / ロゴ帯 / 基壇と回転扉はビルの上下に貼り付く固定の厚みで、 窓だけが残りを
|
|
385
|
+
* 埋める。 **窓の段数を残りの高さから出す** ので、 ビルを帯に合わせて伸び縮みさせても窓が
|
|
386
|
+
* 枠の外へ並ばない (変更前は段数 5 が固定で、 高さ 72 の箱では窓が下へ 136 出ていた)。
|
|
387
|
+
*
|
|
388
|
+
* 影と床帯はビルの外へ出る。 出る量は箱の高さに依らないので、 描ける帯を先に狭める。
|
|
389
|
+
*/
|
|
390
|
+
const BROKERAGE_TICK_H = 12;
|
|
391
|
+
const BROKERAGE_LOGO_H = 26;
|
|
392
|
+
/** 窓の格子。 上端はロゴ帯の下、 下端は基壇の上。 */
|
|
393
|
+
const BROKERAGE_GRID_TOP = 62;
|
|
394
|
+
const BROKERAGE_CELL_W = 14;
|
|
395
|
+
const BROKERAGE_CELL_H = 18;
|
|
396
|
+
const BROKERAGE_GAP_X = 8;
|
|
397
|
+
const BROKERAGE_GAP_Y = 8;
|
|
398
|
+
const BROKERAGE_COLS = 6;
|
|
399
|
+
/** 基壇と回転扉が使う、 ビル下端からの高さ。 */
|
|
400
|
+
const BROKERAGE_BASE_H = 26;
|
|
401
|
+
/** 影の楕円をビルの下端からどれだけ下に置くか、 その縦半径と横半径の比。 */
|
|
402
|
+
const BROKERAGE_SHADOW_DY = 12;
|
|
403
|
+
const BROKERAGE_SHADOW_RY = 6;
|
|
404
|
+
const BROKERAGE_SHADOW_RX_RATIO = 0.5;
|
|
405
|
+
/** 床帯がビルの下端と左右へ出る量、 とその厚み。 */
|
|
406
|
+
const BROKERAGE_FLOOR_DY = 2;
|
|
407
|
+
const BROKERAGE_FLOOR_H = 4;
|
|
408
|
+
const BROKERAGE_FLOOR_OUT = 10;
|
|
409
|
+
/** 左端の陰がビルの左へ出る量。 */
|
|
410
|
+
const BROKERAGE_SHADE_OUT = 2;
|
|
411
|
+
/** ビルの幅。 下限は窓 6 列が入る幅。 */
|
|
412
|
+
const BROKERAGE_W_MIN =
|
|
413
|
+
BROKERAGE_COLS * BROKERAGE_CELL_W + (BROKERAGE_COLS - 1) * BROKERAGE_GAP_X + 6;
|
|
414
|
+
const BROKERAGE_W_MAX = 140;
|
|
415
|
+
/** ビルの高さの下限。 電光 + ロゴ帯 + 基壇が重ならない高さ。 */
|
|
416
|
+
const BROKERAGE_H_MIN = BROKERAGE_TICK_H + BROKERAGE_LOGO_H + BROKERAGE_BASE_H;
|
|
417
|
+
|
|
418
|
+
/** shape-brokerage = B2-A 大和証券風 (縦長本社 + 上部電光 + $ SECURITIES ロゴ帯 + window grid + entrance 回転扉) */
|
|
419
|
+
export function ShapeBrokerageNode({ node, active }: ShapeProps): JSX.Element {
|
|
420
|
+
const frame = commonFrame(active);
|
|
421
|
+
const r = shapeRegion(node);
|
|
422
|
+
const cx = r.cx;
|
|
423
|
+
const 線 = frame.strokeWidth / 2;
|
|
424
|
+
const 上出 = 線;
|
|
425
|
+
const 下出 = Math.max(
|
|
426
|
+
BROKERAGE_SHADOW_DY + BROKERAGE_SHADOW_RY,
|
|
427
|
+
BROKERAGE_FLOOR_DY + BROKERAGE_FLOOR_H,
|
|
428
|
+
);
|
|
429
|
+
const 横出 = Math.max(BROKERAGE_FLOOR_OUT, BROKERAGE_SHADE_OUT, 線);
|
|
430
|
+
const 縦 = Math.max(r.availH, BROKERAGE_H_MIN + 上出 + 下出);
|
|
431
|
+
const h = 縦 - 上出 - 下出;
|
|
432
|
+
const w = Math.min(Math.max(r.availW - 横出 * 2, BROKERAGE_W_MIN), BROKERAGE_W_MAX);
|
|
433
|
+
const x = cx - w / 2;
|
|
434
|
+
const y = r.cy - 縦 / 2 + 上出;
|
|
435
|
+
// 影の横半径はビルの半幅ちょうどなので、 左右を決めるのは常に床帯の出幅の側になる
|
|
436
|
+
const 広がり = { 上: 縦 / 2, 下: 縦 / 2, 左: w / 2 + 横出, 右: w / 2 + 横出 };
|
|
437
|
+
const accentDarker = "#0f3450";
|
|
438
|
+
// window grid 位置計算。 段数は残りの高さから出す = ビルの外へ窓が並ばない
|
|
439
|
+
const gridStartY = y + BROKERAGE_GRID_TOP;
|
|
440
|
+
const cellW = BROKERAGE_CELL_W;
|
|
441
|
+
const cellH = BROKERAGE_CELL_H;
|
|
442
|
+
const gapX = BROKERAGE_GAP_X;
|
|
443
|
+
const gapY = BROKERAGE_GAP_Y;
|
|
444
|
+
const cols = BROKERAGE_COLS;
|
|
445
|
+
const rows = Math.max(
|
|
446
|
+
0,
|
|
447
|
+
Math.floor((h - BROKERAGE_GRID_TOP - BROKERAGE_BASE_H + gapY) / (cellH + gapY)),
|
|
448
|
+
);
|
|
449
|
+
const gridStartX = x + (w - (cols * cellW + (cols - 1) * gapX)) / 2;
|
|
450
|
+
const goldCells = new Set([`0,1`, `0,4`, `1,2`, `2,3`, `3,5`]); // ランダム点灯 window
|
|
451
|
+
const 絵 = (
|
|
452
|
+
<g>
|
|
453
|
+
{/* drop shadow */}
|
|
454
|
+
<ellipse cx={cx} cy={y + h + BROKERAGE_SHADOW_DY} rx={w * BROKERAGE_SHADOW_RX_RATIO} ry={BROKERAGE_SHADOW_RY} fill={frame.stroke} opacity={0.2} stroke="none" />
|
|
455
|
+
{/* 本社ビル body */}
|
|
456
|
+
<rect data-cdl-role="node-body" x={x} y={y} width={w} height={h} fill="#255a7a" stroke={accentDarker} strokeWidth={frame.strokeWidth} />
|
|
457
|
+
{/* 左端 shade (立体感) */}
|
|
458
|
+
<rect x={x - BROKERAGE_SHADE_OUT} y={y} width={BROKERAGE_SHADE_OUT * 2} height={h} fill={accentDarker} />
|
|
459
|
+
{/* 上部電光掲示板 tick */}
|
|
460
|
+
<rect x={x} y={y} width={w} height={BROKERAGE_TICK_H} fill="#0f1830" stroke="none" />
|
|
461
|
+
<rect x={x + 2} y={y + 2} width={w - 4} height={8} fill="#1a2340" stroke="none" />
|
|
462
|
+
<text x={cx} y={y + 9} fontSize={5.5} fontWeight={700} textAnchor="middle" fill="#4ea36a" fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">
|
|
463
|
+
▲NKY 38,240 ▲AAPL 194
|
|
464
|
+
</text>
|
|
465
|
+
{/* $ SECURITIES ロゴ帯 */}
|
|
466
|
+
<rect x={x} y={y + BROKERAGE_TICK_H} width={w} height={BROKERAGE_LOGO_H} fill="#e5c07b" stroke="none" />
|
|
467
|
+
<text x={cx} y={y + 30} fontSize={12} fontWeight={700} letterSpacing={2.5} textAnchor="middle" fill="#0f1830">
|
|
468
|
+
$ SECURITIES
|
|
469
|
+
</text>
|
|
470
|
+
{/* window grid (段数は高さ次第、 列は 6 固定) */}
|
|
471
|
+
{Array.from({ length: rows }).map((_, ri) =>
|
|
472
|
+
Array.from({ length: cols }).map((_, ci) => {
|
|
473
|
+
const wx = gridStartX + ci * (cellW + gapX);
|
|
474
|
+
const wy = gridStartY + ri * (cellH + gapY);
|
|
475
|
+
const isGold = goldCells.has(`${ri},${ci}`);
|
|
476
|
+
return <rect key={`w${ri}${ci}`} x={wx} y={wy} width={cellW} height={cellH} fill={isGold ? "#e5c07b" : accentDarker} />;
|
|
477
|
+
})
|
|
478
|
+
)}
|
|
479
|
+
{/* 底部 base line */}
|
|
480
|
+
<rect x={x} y={y + h - BROKERAGE_BASE_H} width={w} height={12} fill={accentDarker} />
|
|
481
|
+
{/* entrance 回転扉 (円 + 十字) */}
|
|
482
|
+
<circle cx={cx} cy={y + h - 8} r={12} fill="#a89d8f" stroke={accentDarker} strokeWidth={1.5} />
|
|
483
|
+
<circle cx={cx} cy={y + h - 8} r={9} fill="#8b949e" stroke={accentDarker} strokeWidth={1} />
|
|
484
|
+
<line x1={cx - 9} y1={y + h - 8} x2={cx + 9} y2={y + h - 8} stroke={accentDarker} strokeWidth={0.8} />
|
|
485
|
+
<line x1={cx} y1={y + h - 17} x2={cx} y2={y + h - 1} stroke={accentDarker} strokeWidth={0.8} />
|
|
486
|
+
{/* 底部床 (ATM のように狭め) */}
|
|
487
|
+
<rect x={x - BROKERAGE_FLOOR_OUT} y={y + h + BROKERAGE_FLOOR_DY} width={w + BROKERAGE_FLOOR_OUT * 2} height={BROKERAGE_FLOOR_H} fill="#5a6270" stroke="none" />
|
|
488
|
+
</g>
|
|
489
|
+
);
|
|
490
|
+
return (
|
|
491
|
+
<g>
|
|
492
|
+
{fitArt(r, 広がり, 絵)}
|
|
493
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
494
|
+
</g>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** shape-exchange = E2-A 中央 clearing house (hexagon EXCHANGE + 放射 8 node + 双方向 tx dots) */
|
|
499
|
+
export function ShapeExchangeNode({ node, active }: ShapeProps): JSX.Element {
|
|
500
|
+
const frame = commonFrame(active);
|
|
501
|
+
const r = shapeRegion(node);
|
|
502
|
+
const cx = r.cx;
|
|
503
|
+
const cy = r.cy + 4;
|
|
504
|
+
const region = Math.min(r.availW, r.availH) - 40;
|
|
505
|
+
const nodeR = region / 2 - 8;
|
|
506
|
+
const accentDarker = "#0f3450";
|
|
507
|
+
// 8 方向 node 座標 (中央から放射)
|
|
508
|
+
const angles = [-90, -45, 0, 45, 90, 135, 180, -135];
|
|
509
|
+
const nodes = angles.map((deg, i) => {
|
|
510
|
+
const rad = (deg * Math.PI) / 180;
|
|
511
|
+
return {
|
|
512
|
+
cx: cx + Math.cos(rad) * nodeR,
|
|
513
|
+
cy: cy + Math.sin(rad) * nodeR,
|
|
514
|
+
fill: i % 2 === 0 ? "#4ea36a" : "#c15a4a",
|
|
515
|
+
idx: i,
|
|
516
|
+
};
|
|
517
|
+
});
|
|
518
|
+
// 中央 hexagon = 縦向き正六角形 (上下に頂点、 30° 回転)
|
|
519
|
+
const hexR = 45;
|
|
520
|
+
const hexPoints = [-90, -30, 30, 90, 150, 210].map((deg) => {
|
|
521
|
+
const rad = (deg * Math.PI) / 180;
|
|
522
|
+
return { x: cx + Math.cos(rad) * hexR, y: cy + Math.sin(rad) * hexR };
|
|
523
|
+
});
|
|
524
|
+
// noUncheckedIndexedAccess 下では hexPoints[0] が possibly undefined になるため、
|
|
525
|
+
// index 参照せず「先頭は M、 以降は L」 を map 内で分岐して組み立てる。
|
|
526
|
+
const hexPath =
|
|
527
|
+
hexPoints.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ") + " Z";
|
|
528
|
+
/**
|
|
529
|
+
* 絵の広がり。 中心は `cy` (領域の中心より 4 下) なので上下で違う。
|
|
530
|
+
*
|
|
531
|
+
* 外へ届くのは 8 方向の節点 (`nodeR` + 半径 8 + 線の半分) と中央の六角形 (`hexR` + 線)、
|
|
532
|
+
* それに `clearing` の字 (`cy + 26` から下へ張り出す)。 大きい方を採る。
|
|
533
|
+
*/
|
|
534
|
+
const 節点 = Math.max(0, nodeR) + 8 + 1.5 / 2;
|
|
535
|
+
const 六角 = hexR + frame.strokeWidth / 2;
|
|
536
|
+
const 字下 = 26 + 8 * 0.25;
|
|
537
|
+
const 外 = Math.max(節点, 六角);
|
|
538
|
+
const ずれ = cy - r.cy;
|
|
539
|
+
const 広がり = {
|
|
540
|
+
上: 外 - ずれ,
|
|
541
|
+
下: Math.max(外, 字下) + ずれ,
|
|
542
|
+
左: 外,
|
|
543
|
+
右: 外,
|
|
544
|
+
};
|
|
545
|
+
const 絵 = (
|
|
546
|
+
<g>
|
|
547
|
+
{/* 8 dashed radial lines */}
|
|
548
|
+
{nodes.map((n, i) => (
|
|
549
|
+
<line key={`l${i}`} x1={cx} y1={cy} x2={n.cx} y2={n.cy} stroke={accentDarker} strokeWidth={1.5} strokeDasharray="3 2" />
|
|
550
|
+
))}
|
|
551
|
+
{/* animated tx dots (双方向) */}
|
|
552
|
+
{nodes.map((n, i) => {
|
|
553
|
+
const isGreen = i % 2 === 0;
|
|
554
|
+
return (
|
|
555
|
+
<circle key={`d${i}`} r={4} fill={isGreen ? "#4ea36a" : "#c15a4a"} stroke="none">
|
|
556
|
+
<animate attributeName="cx" values={isGreen ? `${n.cx};${cx};${n.cx}` : `${cx};${n.cx};${cx}`} dur={`${1.5 + (i % 3) * 0.3}s`} repeatCount="indefinite" begin={`${i * 0.15}s`} />
|
|
557
|
+
<animate attributeName="cy" values={isGreen ? `${n.cy};${cy};${n.cy}` : `${cy};${n.cy};${cy}`} dur={`${1.5 + (i % 3) * 0.3}s`} repeatCount="indefinite" begin={`${i * 0.15}s`} />
|
|
558
|
+
</circle>
|
|
559
|
+
);
|
|
560
|
+
})}
|
|
561
|
+
{/* 中央 hexagon EXCHANGE (単色 #255a7a、 縦向き正六角形) */}
|
|
562
|
+
<path d={hexPath} fill="#255a7a" stroke={accentDarker} strokeWidth={frame.strokeWidth} strokeLinejoin="round" />
|
|
563
|
+
<text x={cx} y={cy - 8} fontSize={10} fontWeight={700} letterSpacing={2} textAnchor="middle" fill="#e5c07b">EXCHANGE</text>
|
|
564
|
+
{/* swap 記号 */}
|
|
565
|
+
<text data-cdl-role="node-body" x={cx} y={cy + 12} fontSize={22} fontWeight={700} textAnchor="middle" fill="#e5c07b">⇄</text>
|
|
566
|
+
{/* clearing label */}
|
|
567
|
+
<text x={cx} y={cy + 26} fontSize={8} fontWeight={700} textAnchor="middle" fill="#a4b5f2">clearing</text>
|
|
568
|
+
{/* 8 外周 node (buyer/seller) */}
|
|
569
|
+
{nodes.map((n, i) => (
|
|
570
|
+
<circle key={`n${i}`} cx={n.cx} cy={n.cy} r={8} fill={n.fill} stroke={accentDarker} strokeWidth={1.5} />
|
|
571
|
+
))}
|
|
572
|
+
</g>
|
|
573
|
+
);
|
|
574
|
+
return (
|
|
575
|
+
<g>
|
|
576
|
+
{fitArt(r, 広がり, 絵)}
|
|
577
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
578
|
+
</g>
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* ATM (`shape-atm`) の固定部品 (#433)。
|
|
584
|
+
*
|
|
585
|
+
* 画面 / 10 キー / カード投入口は寸法が固定なので、 外枠だけを箱に合わせて縮めると中身が枠の
|
|
586
|
+
* 外へ出る (変更前は高さ 72 の箱で下へ 132)。 **操作面をひとまとまりにして縦横まとめて縮める**
|
|
587
|
+
* ので、 外枠がどんな大きさでも中身は枠の内側に収まる。
|
|
588
|
+
*
|
|
589
|
+
* 影と床帯は外枠の外へ出る。 出る量は箱の高さに依らないので、 描ける帯を先に狭める。
|
|
590
|
+
*/
|
|
591
|
+
const ATM_SIGN_H = 26;
|
|
592
|
+
const ATM_SIGN_SW = 1.5;
|
|
593
|
+
/** 天井看板と本体の間。 */
|
|
594
|
+
const ATM_SIGN_GAP = 24;
|
|
595
|
+
/** 影の楕円を本体の下端からどれだけ下に置くか、 その縦半径と横半径の比。 */
|
|
596
|
+
const ATM_SHADOW_DY = 8;
|
|
597
|
+
const ATM_SHADOW_RY = 5;
|
|
598
|
+
const ATM_SHADOW_RX_RATIO = 0.6;
|
|
599
|
+
/** 床帯の上端を全体の下端からどれだけ上に置くか、 その 2 枚の厚み、 本体から差し引く分。 */
|
|
600
|
+
const ATM_FLOOR_UP = 6;
|
|
601
|
+
const ATM_FLOOR_H = 10;
|
|
602
|
+
const ATM_FLOOR_RESERVE = 12;
|
|
603
|
+
/** 操作面の自然な大きさ。 これより本体が小さければ丸ごと縮める。 */
|
|
604
|
+
const ATM_PANEL_W = 140;
|
|
605
|
+
const ATM_PANEL_H = 146;
|
|
606
|
+
/** 全体の幅。 上限は今までと同じ 140。 */
|
|
607
|
+
const ATM_W_MIN = 100;
|
|
608
|
+
const ATM_W_MAX = 140;
|
|
609
|
+
/** 全体の高さの下限。 看板 + 間 + 本体 30 + 床帯。 */
|
|
610
|
+
const ATM_H_MIN = ATM_SIGN_H + ATM_SIGN_GAP + 30 + ATM_FLOOR_RESERVE;
|
|
611
|
+
|
|
612
|
+
/** shape-atm = A3-C1 (支店内 ATM 単体、 上部 ATM CORNER 天井看板 + 中央 ATM 大 + 直下床帯) */
|
|
613
|
+
export function ShapeAtmNode({ node, active }: ShapeProps): JSX.Element {
|
|
614
|
+
const frame = commonFrame(active);
|
|
615
|
+
const r = shapeRegion(node);
|
|
616
|
+
const cx = r.cx;
|
|
617
|
+
const 線 = ATM_SIGN_SW / 2;
|
|
618
|
+
// 影と床帯が本体の外へ出る。 出る量は箱の高さに依らないので、 描ける帯を先に狭める
|
|
619
|
+
const 上出 = 線;
|
|
620
|
+
const 下出 = Math.max(ATM_SHADOW_DY + ATM_SHADOW_RY, ATM_FLOOR_H - ATM_FLOOR_UP);
|
|
621
|
+
const 縦 = Math.max(r.availH, ATM_H_MIN + 上出 + 下出);
|
|
622
|
+
const totalH = 縦 - 上出 - 下出;
|
|
623
|
+
// 左右は影が一番外まで届く。 幅は影を含めて帯に収まる大きさから逆算する
|
|
624
|
+
const totalW = Math.min(
|
|
625
|
+
Math.max(r.availW / (ATM_SHADOW_RX_RATIO * 2), ATM_W_MIN),
|
|
626
|
+
ATM_W_MAX,
|
|
627
|
+
);
|
|
628
|
+
const centerX = cx;
|
|
629
|
+
const y = r.cy - 縦 / 2 + 上出;
|
|
630
|
+
const 広がり = {
|
|
631
|
+
上: 縦 / 2,
|
|
632
|
+
下: 縦 / 2,
|
|
633
|
+
左: totalW * ATM_SHADOW_RX_RATIO,
|
|
634
|
+
右: totalW * ATM_SHADOW_RX_RATIO,
|
|
635
|
+
};
|
|
636
|
+
// 天井看板 y=0-30
|
|
637
|
+
const signY = y;
|
|
638
|
+
const signH = ATM_SIGN_H;
|
|
639
|
+
const atmY = y + signH + ATM_SIGN_GAP;
|
|
640
|
+
const atmH = totalH - signH - ATM_SIGN_GAP - ATM_FLOOR_RESERVE;
|
|
641
|
+
const atmW = totalW;
|
|
642
|
+
const atmX = centerX - atmW / 2;
|
|
643
|
+
const floorY = y + totalH - ATM_FLOOR_UP;
|
|
644
|
+
const accentDarker = "#0f3450";
|
|
645
|
+
// 操作面は寸法が固定なので、 本体が縮んだら **縦横まとめて** 縮める。 縦だけ詰めると
|
|
646
|
+
// 画面と 10 キーが潰れて読めなくなる
|
|
647
|
+
const 面 = Math.min(1, atmW / ATM_PANEL_W, atmH / ATM_PANEL_H);
|
|
648
|
+
const 面左 = -ATM_PANEL_W / 2;
|
|
649
|
+
const 絵 = (
|
|
650
|
+
<g>
|
|
651
|
+
{/* drop shadow */}
|
|
652
|
+
<ellipse cx={cx} cy={y + totalH + ATM_SHADOW_DY} rx={totalW * ATM_SHADOW_RX_RATIO} ry={ATM_SHADOW_RY} fill={frame.stroke} opacity={0.15} stroke="none" />
|
|
653
|
+
{/* 天井 ATM CORNER 看板 */}
|
|
654
|
+
<rect x={centerX - totalW / 2} y={signY} width={totalW} height={signH} fill={accentDarker} stroke="#000" strokeWidth={ATM_SIGN_SW} />
|
|
655
|
+
<text x={centerX} y={signY + 17} fontSize={11} fontWeight={700} letterSpacing={2} textAnchor="middle" fill="#e5c07b">ATM CORNER</text>
|
|
656
|
+
{/* ATM body */}
|
|
657
|
+
<rect data-cdl-role="node-body" x={atmX} y={atmY} width={atmW} height={atmH} rx={6} fill="#255a7a" stroke={accentDarker} strokeWidth={frame.strokeWidth} />
|
|
658
|
+
<g transform={`translate(${centerX} ${atmY}) scale(${面})`}>
|
|
659
|
+
{/* screen */}
|
|
660
|
+
<rect x={面左 + 8} y={10} width={ATM_PANEL_W - 16} height={52} rx={3} fill="#0f1830" stroke="#000" strokeWidth={1.5} />
|
|
661
|
+
<text x={0} y={30} fontSize={10} fontWeight={700} letterSpacing={1} textAnchor="middle" fill="#e5c07b">お引出し</text>
|
|
662
|
+
<text x={0} y={44} fontSize={8} fontWeight={700} textAnchor="middle" fill="#a4b5f2">SELECT AMOUNT</text>
|
|
663
|
+
<text x={0} y={56} fontSize={10} fontWeight={700} textAnchor="middle" fill="#4ea36a">¥ 20,000</text>
|
|
664
|
+
{/* keypad 5x2 */}
|
|
665
|
+
<rect x={面左 + 8} y={68} width={ATM_PANEL_W - 16} height={42} rx={2} fill="#8b949e" stroke="#5a6270" strokeWidth={1} />
|
|
666
|
+
{(() => {
|
|
667
|
+
const keys = [
|
|
668
|
+
["1", "#5a6270"], ["2", "#5a6270"], ["3", "#5a6270"], ["C", "#5a6270"], ["X", "#c15a4a"],
|
|
669
|
+
["4", "#5a6270"], ["5", "#5a6270"], ["6", "#5a6270"], ["0", "#5a6270"], ["OK", "#4ea36a"],
|
|
670
|
+
];
|
|
671
|
+
const keyW = 18;
|
|
672
|
+
const keyH = 10;
|
|
673
|
+
const keyGap = 4;
|
|
674
|
+
const startX = 面左 + 14;
|
|
675
|
+
return keys.map((k, i) => {
|
|
676
|
+
const row = Math.floor(i / 5);
|
|
677
|
+
const col = i % 5;
|
|
678
|
+
const kx = startX + col * (keyW + keyGap);
|
|
679
|
+
const ky = 74 + row * (keyH + 2);
|
|
680
|
+
return (
|
|
681
|
+
<g key={`k${i}`}>
|
|
682
|
+
<rect x={kx} y={ky} width={keyW} height={keyH} fill={k[1]} stroke="#0f1830" strokeWidth={0.8} />
|
|
683
|
+
<text x={kx + keyW / 2} y={ky + 7} fontSize={k[0] === "OK" ? 6 : 7} fontWeight={700} textAnchor="middle" fill="#ffffff">{k[0]}</text>
|
|
684
|
+
</g>
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
})()}
|
|
688
|
+
{/* card slot */}
|
|
689
|
+
<rect x={面左 + 12} y={116} width={ATM_PANEL_W - 24} height={8} rx={1.5} fill="#0f1830" stroke={accentDarker} strokeWidth={1} />
|
|
690
|
+
<rect x={面左 + 15} y={118} width={ATM_PANEL_W - 30} height={4} rx={0.5} fill="#e5c07b" />
|
|
691
|
+
<text x={0} y={132} fontSize={6} fontWeight={700} letterSpacing={1} textAnchor="middle" fill="#e5c07b">CARD SLOT</text>
|
|
692
|
+
{/* cash slot */}
|
|
693
|
+
<rect x={面左 + 12} y={136} width={ATM_PANEL_W - 24} height={4} rx={1} fill="#0f1830" />
|
|
694
|
+
</g>
|
|
695
|
+
{/* 直下床帯 (ATM 直下のみ、 全幅 totalW) */}
|
|
696
|
+
<rect x={centerX - totalW / 2} y={floorY} width={totalW} height={6} fill="#8b949e" stroke="none" />
|
|
697
|
+
<rect x={centerX - totalW / 2} y={floorY + 6} width={totalW} height={ATM_FLOOR_H - 6} fill="#5a6270" stroke="none" />
|
|
698
|
+
</g>
|
|
699
|
+
);
|
|
700
|
+
return (
|
|
701
|
+
<g>
|
|
702
|
+
{fitArt(r, 広がり, 絵)}
|
|
703
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
704
|
+
</g>
|
|
705
|
+
);
|
|
706
|
+
}
|