@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,750 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import type { LaidNode } from "../types";
|
|
3
|
+
import { fitArt, labelPlan, shapeRegion, textWidth, type ArtExtent } from "./shape-region";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shape-driven blockchain / web3 10 kinds (CAR-1111 Phase 2-D)。
|
|
7
|
+
*
|
|
8
|
+
* smart-contract / blockchain-block / rpc-node / wallet / nft / token /
|
|
9
|
+
* blockchain / bitcoin-chain / ethereum-chain / blockchain-node。
|
|
10
|
+
*
|
|
11
|
+
* ETH icon は Ethereum 公式 Diamond ロゴ (上半分 4 面クリスタル + 下半分 2 面三角錐)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type ShapeProps = {
|
|
15
|
+
node: LaidNode;
|
|
16
|
+
active: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const FRAME_SW_ACTIVE = 3;
|
|
20
|
+
const FRAME_SW_INACTIVE = 1.5;
|
|
21
|
+
|
|
22
|
+
const SMART_CONTRACT_W = 220;
|
|
23
|
+
const SMART_CONTRACT_H = 220;
|
|
24
|
+
|
|
25
|
+
const BLOCK_W = 240;
|
|
26
|
+
const BLOCK_H = 220;
|
|
27
|
+
const BLOCK_SHADOW = 6;
|
|
28
|
+
|
|
29
|
+
const NFT_W = 220;
|
|
30
|
+
const NFT_H = 240;
|
|
31
|
+
const NFT_SHADOW = 4;
|
|
32
|
+
|
|
33
|
+
const CHAIN_BLOCK_COUNT = 5;
|
|
34
|
+
const CHAIN_BLOCK_W = 50;
|
|
35
|
+
const CHAIN_BLOCK_H = 66;
|
|
36
|
+
const CHAIN_LINK_W = 12;
|
|
37
|
+
const CHAIN_SHADOW = 3;
|
|
38
|
+
const CHAIN_BLOCK_SW = 2.5;
|
|
39
|
+
|
|
40
|
+
const NODE_HEX_R = 44;
|
|
41
|
+
const NODE_PEER_R = 12;
|
|
42
|
+
const NODE_PEER_GAP = 22;
|
|
43
|
+
const NODE_PEER_DIST = NODE_HEX_R + NODE_PEER_R + NODE_PEER_GAP;
|
|
44
|
+
|
|
45
|
+
const commonFrame = (active: boolean) => ({
|
|
46
|
+
fill: "var(--cdl-node-fill, #ffffff)",
|
|
47
|
+
stroke: active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)",
|
|
48
|
+
strokeWidth: active ? FRAME_SW_ACTIVE : FRAME_SW_INACTIVE,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function smartContractExtent(strokeWidth: number): ArtExtent {
|
|
52
|
+
const 線 = strokeWidth / 2;
|
|
53
|
+
return {
|
|
54
|
+
上: SMART_CONTRACT_H / 2 + 線,
|
|
55
|
+
下: SMART_CONTRACT_H / 2 + 線,
|
|
56
|
+
左: SMART_CONTRACT_W / 2 + 線,
|
|
57
|
+
右: SMART_CONTRACT_W / 2 + 線,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function blockchainBlockExtent(strokeWidth: number): ArtExtent {
|
|
62
|
+
const 線 = strokeWidth / 2;
|
|
63
|
+
return {
|
|
64
|
+
上: BLOCK_H / 2 + 線,
|
|
65
|
+
下: BLOCK_H / 2 + Math.max(BLOCK_SHADOW, 線),
|
|
66
|
+
左: BLOCK_W / 2 + 線,
|
|
67
|
+
右: BLOCK_W / 2 + Math.max(BLOCK_SHADOW, 線),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function nftExtent(strokeWidth: number): ArtExtent {
|
|
72
|
+
const 線 = strokeWidth / 2;
|
|
73
|
+
return {
|
|
74
|
+
上: NFT_H / 2 + 線,
|
|
75
|
+
下: NFT_H / 2 + Math.max(NFT_SHADOW, 線),
|
|
76
|
+
左: NFT_W / 2 + 線,
|
|
77
|
+
右: NFT_W / 2 + Math.max(NFT_SHADOW, 線),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function chainExtent(): ArtExtent {
|
|
82
|
+
const 線 = CHAIN_BLOCK_SW / 2;
|
|
83
|
+
const 全幅 = CHAIN_BLOCK_COUNT * CHAIN_BLOCK_W + (CHAIN_BLOCK_COUNT - 1) * CHAIN_LINK_W;
|
|
84
|
+
return {
|
|
85
|
+
上: CHAIN_BLOCK_H / 2 + 線,
|
|
86
|
+
下: CHAIN_BLOCK_H / 2 + Math.max(CHAIN_SHADOW, 線),
|
|
87
|
+
左: 全幅 / 2 + 線,
|
|
88
|
+
右: 全幅 / 2 + Math.max(CHAIN_SHADOW, 線),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function blockchainNodeExtent(strokeWidth: number): ArtExtent {
|
|
93
|
+
const 線 = strokeWidth / 2;
|
|
94
|
+
const 縦 = NODE_PEER_DIST + NODE_PEER_R + 線;
|
|
95
|
+
const 横 = (NODE_PEER_DIST + NODE_PEER_R) * Math.cos(Math.PI / 6) + 線;
|
|
96
|
+
return { 上: 縦, 下: 縦, 左: 横, 右: 横 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 名前の行を箱の幅に収める倍率。
|
|
101
|
+
*
|
|
102
|
+
* 字間は字の大きさに追随しないため、 大きさだけを縮めても幅は `字間 x 字数` より狭く
|
|
103
|
+
* ならない。 大きさと字間を同じ倍率で縮めると、 両項が揃って縮むので幅が倍率に比例する。
|
|
104
|
+
*
|
|
105
|
+
* 収まっている間は 1 を返す = 箱が広い時の見え方を変えない。
|
|
106
|
+
*/
|
|
107
|
+
function 幅の倍率(text: string, size: number, letterSpacing: number, 幅: number): number {
|
|
108
|
+
if (!(幅 > 0) || !(size > 0)) return 1;
|
|
109
|
+
// 階級表を二重に持つと、 片方を直した時にもう片方だけ古くなる。
|
|
110
|
+
const 実幅 = textWidth(text, size, letterSpacing);
|
|
111
|
+
return 実幅 > 幅 ? 幅 / 実幅 : 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 名前の塊を描く。 位置と大きさは `labelPlan` が決める (箱の下端から積み上げる、 #433)。
|
|
116
|
+
*/
|
|
117
|
+
function bottomLabel(node: LaidNode, cx: number, plan: ReturnType<typeof labelPlan>) {
|
|
118
|
+
const 肩書倍率 = node.eyebrow && plan.肩書
|
|
119
|
+
? 幅の倍率(node.eyebrow, plan.肩書.size, 2, node.w)
|
|
120
|
+
: 1;
|
|
121
|
+
const 名前倍率 = 幅の倍率(node.title, plan.名前.size, 0, node.w);
|
|
122
|
+
const 説明倍率 = node.subtitle && plan.説明
|
|
123
|
+
? 幅の倍率(node.subtitle, plan.説明.size, 0, node.w)
|
|
124
|
+
: 1;
|
|
125
|
+
return (
|
|
126
|
+
<>
|
|
127
|
+
{node.eyebrow && plan.肩書 && (
|
|
128
|
+
<text x={cx} y={plan.肩書.y} fontSize={plan.肩書.size * 肩書倍率} fontWeight={700} letterSpacing={2 * 肩書倍率} textAnchor="middle" fill="var(--cdl-tone-accent, #2d6a8f)">
|
|
129
|
+
{node.eyebrow}
|
|
130
|
+
</text>
|
|
131
|
+
)}
|
|
132
|
+
<text data-cdl-role="node-label" x={cx} y={plan.名前.y} fontSize={plan.名前.size * 名前倍率} fontWeight={700} textAnchor="middle" fill="var(--cdl-text, #1a1f2a)">
|
|
133
|
+
{node.title}
|
|
134
|
+
</text>
|
|
135
|
+
{node.subtitle && plan.説明 && (
|
|
136
|
+
<text x={cx} y={plan.説明.y} fontSize={plan.説明.size * 説明倍率} textAnchor="middle" fill="var(--cdl-text-dim, #5a6270)">
|
|
137
|
+
{node.subtitle}
|
|
138
|
+
</text>
|
|
139
|
+
)}
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Ethereum 公式 Diamond ロゴを描画する helper。
|
|
147
|
+
* cx, cy 中心、 h = ロゴ全高の半分 (上 = cy - h、 下 = cy + h)。
|
|
148
|
+
* 上半分 = 4 面クリスタル (右 / 左に濃淡差)、 下半分 = 2 面三角錐。
|
|
149
|
+
* white = 上半分明色 / accent 左右暗色 / stroke = ロゴ輪郭。
|
|
150
|
+
*/
|
|
151
|
+
function EthereumDiamond({ cx, cy, h, colors }: { cx: number; cy: number; h: number; colors?: { light: string; medium: string; dark: string; darker: string; stroke: string } }) {
|
|
152
|
+
const c = colors ?? { light: "#8a92b2", medium: "#62688f", dark: "#454a75", darker: "#3c3c3b", stroke: "#3c3c3b" };
|
|
153
|
+
const w = h * 0.62;
|
|
154
|
+
const midY = cy - h * 0.1; // 中央線 (上下切替位置)
|
|
155
|
+
const upperTopY = cy - h;
|
|
156
|
+
const lowerBotY = cy + h;
|
|
157
|
+
return (
|
|
158
|
+
<g>
|
|
159
|
+
{/* 上半分左面 (medium) */}
|
|
160
|
+
<path d={`M ${cx} ${upperTopY} L ${cx - w} ${cy + h * 0.05} L ${cx} ${midY} Z`} fill={c.medium} stroke={c.stroke} strokeWidth={0.6} strokeLinejoin="round" />
|
|
161
|
+
{/* 上半分右面 (light) */}
|
|
162
|
+
<path d={`M ${cx} ${upperTopY} L ${cx} ${midY} L ${cx + w} ${cy + h * 0.05} Z`} fill={c.light} stroke={c.stroke} strokeWidth={0.6} strokeLinejoin="round" />
|
|
163
|
+
{/* 下半分左面 (darker) */}
|
|
164
|
+
<path d={`M ${cx} ${midY} L ${cx - w} ${cy + h * 0.05} L ${cx} ${lowerBotY} Z`} fill={c.darker} stroke={c.stroke} strokeWidth={0.6} strokeLinejoin="round" />
|
|
165
|
+
{/* 下半分右面 (dark) */}
|
|
166
|
+
<path d={`M ${cx} ${midY} L ${cx + w} ${cy + h * 0.05} L ${cx} ${lowerBotY} Z`} fill={c.dark} stroke={c.stroke} strokeWidth={0.6} strokeLinejoin="round" />
|
|
167
|
+
</g>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** shape-smart-contract = 契約書 (前バージョン: document + 契約書テキスト + function 表示 + 底に回転 gear) */
|
|
172
|
+
export function ShapeSmartContractNode({ node, active }: ShapeProps): JSX.Element {
|
|
173
|
+
const frame = commonFrame(active);
|
|
174
|
+
const r = shapeRegion(node);
|
|
175
|
+
const docW = SMART_CONTRACT_W;
|
|
176
|
+
const docH = SMART_CONTRACT_H;
|
|
177
|
+
const cx = r.cx;
|
|
178
|
+
const cy = r.cy;
|
|
179
|
+
const x = cx - docW / 2;
|
|
180
|
+
const y = cy - docH / 2;
|
|
181
|
+
const fold = 28;
|
|
182
|
+
const path = `M ${x + 10} ${y} L ${x + docW - fold} ${y} L ${x + docW} ${y + fold} L ${x + docW} ${y + docH - 10} Q ${x + docW} ${y + docH}, ${x + docW - 10} ${y + docH} L ${x + 10} ${y + docH} Q ${x} ${y + docH}, ${x} ${y + docH - 10} L ${x} ${y + 10} Q ${x} ${y}, ${x + 10} ${y} Z`;
|
|
183
|
+
const foldPath = `M ${x + docW - fold} ${y} L ${x + docW - fold} ${y + fold} L ${x + docW} ${y + fold}`;
|
|
184
|
+
// gear at bottom-right
|
|
185
|
+
const gearCx = x + docW - 32;
|
|
186
|
+
const gearCy = y + docH - 32;
|
|
187
|
+
const gearBodyR = 14;
|
|
188
|
+
const gearTipR = gearBodyR + 6;
|
|
189
|
+
const teeth = 10;
|
|
190
|
+
const anglePerTooth = (Math.PI * 2) / teeth;
|
|
191
|
+
const halfBase = anglePerTooth * 0.35;
|
|
192
|
+
const halfTip = anglePerTooth * 0.2;
|
|
193
|
+
const gearPts: string[] = [];
|
|
194
|
+
for (let i = 0; i < teeth; i++) {
|
|
195
|
+
const center = i * anglePerTooth;
|
|
196
|
+
const a1 = center - halfBase;
|
|
197
|
+
const a2 = center - halfTip;
|
|
198
|
+
const a3 = center + halfTip;
|
|
199
|
+
const a4 = center + halfBase;
|
|
200
|
+
gearPts.push(`${gearCx + Math.cos(a1) * gearBodyR},${gearCy + Math.sin(a1) * gearBodyR}`);
|
|
201
|
+
gearPts.push(`${gearCx + Math.cos(a2) * gearTipR},${gearCy + Math.sin(a2) * gearTipR}`);
|
|
202
|
+
gearPts.push(`${gearCx + Math.cos(a3) * gearTipR},${gearCy + Math.sin(a3) * gearTipR}`);
|
|
203
|
+
gearPts.push(`${gearCx + Math.cos(a4) * gearBodyR},${gearCy + Math.sin(a4) * gearBodyR}`);
|
|
204
|
+
}
|
|
205
|
+
const art = (
|
|
206
|
+
<g>
|
|
207
|
+
<path data-cdl-role="node-body" d={path} {...frame} strokeLinejoin="round" />
|
|
208
|
+
<path d={foldPath} fill="none" stroke={frame.stroke} strokeWidth={frame.strokeWidth} strokeLinejoin="round" />
|
|
209
|
+
{/* 契約書テキスト 4 行 */}
|
|
210
|
+
{[y + 40, y + 58, y + 76, y + 94].map((ly, i) => (
|
|
211
|
+
<rect key={i} x={x + 16} y={ly - 3} width={docW - 32 - i * 22} height={5} rx={1} fill="var(--cdl-divider, #d4d4d8)" fillOpacity={0.7} stroke="none" />
|
|
212
|
+
))}
|
|
213
|
+
{/* function signature */}
|
|
214
|
+
<text x={x + 16} y={y + docH - 46} fontSize={11} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fill="var(--cdl-tone-accent, #2d6a8f)">
|
|
215
|
+
{"function execute()"}
|
|
216
|
+
</text>
|
|
217
|
+
{/* gear (auto-execution 記号) */}
|
|
218
|
+
<polygon
|
|
219
|
+
points={gearPts.join(" ")}
|
|
220
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
221
|
+
stroke={frame.stroke}
|
|
222
|
+
strokeWidth={1.2}
|
|
223
|
+
strokeLinejoin="round"
|
|
224
|
+
>
|
|
225
|
+
<animateTransform attributeName="transform" type="rotate" from={`0 ${gearCx} ${gearCy}`} to={`360 ${gearCx} ${gearCy}`} dur="8s" repeatCount="indefinite" />
|
|
226
|
+
</polygon>
|
|
227
|
+
<circle cx={gearCx} cy={gearCy} r={gearBodyR * 0.4} fill="var(--cdl-node-fill, #ffffff)" stroke="none" />
|
|
228
|
+
</g>
|
|
229
|
+
);
|
|
230
|
+
return (
|
|
231
|
+
<g>
|
|
232
|
+
{fitArt(r, smartContractExtent(frame.strokeWidth), art)}
|
|
233
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
234
|
+
</g>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** shape-blockchain-block = 単一 block 詳細 (header + 5 fields: hash/prev/merkle/nonce/tx + chain link icon) */
|
|
239
|
+
export function ShapeBlockchainBlockNode({ node, active }: ShapeProps): JSX.Element {
|
|
240
|
+
const frame = commonFrame(active);
|
|
241
|
+
const r = shapeRegion(node);
|
|
242
|
+
const bw = BLOCK_W;
|
|
243
|
+
const bh = BLOCK_H;
|
|
244
|
+
const cx = r.cx;
|
|
245
|
+
const cy = r.cy;
|
|
246
|
+
const x = cx - bw / 2;
|
|
247
|
+
const y = cy - bh / 2;
|
|
248
|
+
const art = (
|
|
249
|
+
<g>
|
|
250
|
+
{/* 3D shadow */}
|
|
251
|
+
<rect x={x + 6} y={y + 6} width={bw} height={bh} rx={8} fill={frame.stroke} opacity={0.2} stroke="none" />
|
|
252
|
+
<rect data-cdl-role="node-body" x={x} y={y} width={bw} height={bh} rx={8} {...frame} />
|
|
253
|
+
{/* header bar */}
|
|
254
|
+
<path d={`M ${x} ${y + 32} L ${x} ${y + 8} Q ${x} ${y}, ${x + 8} ${y} L ${x + bw - 8} ${y} Q ${x + bw} ${y}, ${x + bw} ${y + 8} L ${x + bw} ${y + 32} Z`} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.9} stroke="none" />
|
|
255
|
+
<text x={x + 14} y={y + 22} fontSize={13} fontWeight={700} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fill="#ffffff">
|
|
256
|
+
Block #421,873
|
|
257
|
+
</text>
|
|
258
|
+
<circle cx={x + bw - 16} cy={y + 16} r={5.5} fill="#7cd88f" stroke="#ffffff" strokeWidth={1.5} />
|
|
259
|
+
{/* fields */}
|
|
260
|
+
{[
|
|
261
|
+
{ label: "hash", value: "0xaf31c9d2..." },
|
|
262
|
+
{ label: "prev", value: "0x7b3f92c1..." },
|
|
263
|
+
{ label: "merkle", value: "0xc1892f4a..." },
|
|
264
|
+
{ label: "nonce", value: "82,914" },
|
|
265
|
+
{ label: "tx", value: "142 tx" },
|
|
266
|
+
].map((f, i) => (
|
|
267
|
+
<g key={i}>
|
|
268
|
+
<text x={x + 14} y={y + 54 + i * 22} fontSize={10.5} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fill="var(--cdl-text-dim, #5a6270)">
|
|
269
|
+
{f.label}
|
|
270
|
+
</text>
|
|
271
|
+
<text x={x + bw - 14} y={y + 54 + i * 22} fontSize={11} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fontWeight={600} textAnchor="end" fill="var(--cdl-text, #1a1f2a)">
|
|
272
|
+
{f.value}
|
|
273
|
+
</text>
|
|
274
|
+
<line x1={x + 14} y1={y + 60 + i * 22} x2={x + bw - 14} y2={y + 60 + i * 22} stroke="var(--cdl-divider, #d4d4d8)" strokeWidth={0.5} opacity={0.5} />
|
|
275
|
+
</g>
|
|
276
|
+
))}
|
|
277
|
+
{/* chain link icon (bottom center) */}
|
|
278
|
+
<g transform={`translate(${cx - 32}, ${y + bh - 26})`}>
|
|
279
|
+
<rect x={0} y={0} width={22} height={18} rx={3} fill="none" stroke={frame.stroke} strokeWidth={1.5} />
|
|
280
|
+
<line x1={22} y1={9} x2={42} y2={9} stroke={frame.stroke} strokeWidth={1.5} />
|
|
281
|
+
<rect x={42} y={0} width={22} height={18} rx={3} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.5} stroke={frame.stroke} strokeWidth={1.5} />
|
|
282
|
+
</g>
|
|
283
|
+
</g>
|
|
284
|
+
);
|
|
285
|
+
return <g>{fitArt(r, blockchainBlockExtent(frame.strokeWidth), art)}</g>;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** shape-blockchain = 汎用 5 block chain (シンプル hash pointer) */
|
|
289
|
+
export function ShapeBlockchainNode({ node, active }: ShapeProps): JSX.Element {
|
|
290
|
+
const frame = commonFrame(active);
|
|
291
|
+
const r = shapeRegion(node);
|
|
292
|
+
const cx = r.cx;
|
|
293
|
+
const cy = r.cy;
|
|
294
|
+
const blockCount = CHAIN_BLOCK_COUNT;
|
|
295
|
+
const blockW = CHAIN_BLOCK_W;
|
|
296
|
+
const blockH = CHAIN_BLOCK_H;
|
|
297
|
+
const linkW = CHAIN_LINK_W;
|
|
298
|
+
const totalW = blockCount * blockW + (blockCount - 1) * linkW;
|
|
299
|
+
const startX = cx - totalW / 2;
|
|
300
|
+
const art = (
|
|
301
|
+
<g>
|
|
302
|
+
{Array.from({ length: blockCount }, (_, i) => i).map((i) => {
|
|
303
|
+
const bx = startX + i * (blockW + linkW);
|
|
304
|
+
const isCurrent = i === blockCount - 1;
|
|
305
|
+
return (
|
|
306
|
+
<g key={i}>
|
|
307
|
+
{i > 0 && (
|
|
308
|
+
<g>
|
|
309
|
+
<line x1={bx - linkW} y1={cy} x2={bx} y2={cy} stroke={frame.stroke} strokeWidth={3} strokeLinecap="round" />
|
|
310
|
+
<circle cx={bx - linkW / 2} cy={cy} r={2.5} fill="var(--cdl-tone-accent, #2d6a8f)" stroke="none">
|
|
311
|
+
<animate attributeName="opacity" values="0.3;1;0.3" dur={`${1 + i * 0.2}s`} repeatCount="indefinite" />
|
|
312
|
+
</circle>
|
|
313
|
+
</g>
|
|
314
|
+
)}
|
|
315
|
+
<rect x={bx + 3} y={cy - blockH / 2 + 3} width={blockW} height={blockH} rx={4} fill={frame.stroke} opacity={0.18} stroke="none" />
|
|
316
|
+
<rect
|
|
317
|
+
data-cdl-role={i === 0 ? "node-body" : undefined}
|
|
318
|
+
x={bx}
|
|
319
|
+
y={cy - blockH / 2}
|
|
320
|
+
width={blockW}
|
|
321
|
+
height={blockH}
|
|
322
|
+
rx={4}
|
|
323
|
+
fill={isCurrent ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-node-fill, #ffffff)"}
|
|
324
|
+
stroke={frame.stroke}
|
|
325
|
+
strokeWidth={isCurrent ? 2.5 : 1.5}
|
|
326
|
+
/>
|
|
327
|
+
<rect x={bx} y={cy - blockH / 2} width={blockW} height={16} rx={4} fill={isCurrent ? "#1a4a68" : "var(--cdl-divider, #d4d4d8)"} fillOpacity={isCurrent ? 1 : 0.4} stroke="none" />
|
|
328
|
+
<text x={bx + blockW / 2} y={cy - blockH / 2 + 12} fontSize={10} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fontWeight={700} textAnchor="middle" fill={isCurrent ? "#ffffff" : "var(--cdl-text-dim, #5a6270)"}>
|
|
329
|
+
#{420 + i}
|
|
330
|
+
</text>
|
|
331
|
+
<text x={bx + blockW / 2} y={cy + 8} fontSize={14} fontWeight={700} textAnchor="middle" fill={isCurrent ? "#ffffff" : "var(--cdl-tone-accent, #2d6a8f)"} fontFamily="Georgia, serif">
|
|
332
|
+
#
|
|
333
|
+
</text>
|
|
334
|
+
<text x={bx + blockW / 2} y={cy + 22} fontSize={7} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" textAnchor="middle" fill={isCurrent ? "#ffffff" : "var(--cdl-text-dim, #5a6270)"} opacity={0.9}>
|
|
335
|
+
0x…
|
|
336
|
+
</text>
|
|
337
|
+
</g>
|
|
338
|
+
);
|
|
339
|
+
})}
|
|
340
|
+
</g>
|
|
341
|
+
);
|
|
342
|
+
return (
|
|
343
|
+
<g>
|
|
344
|
+
{fitArt(r, chainExtent(), art)}
|
|
345
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
346
|
+
</g>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** shape-bitcoin-chain = Bitcoin (橙 accent + ₿ symbol) */
|
|
351
|
+
export function ShapeBitcoinChainNode({ node, active }: ShapeProps): JSX.Element {
|
|
352
|
+
const frame = commonFrame(active);
|
|
353
|
+
const r = shapeRegion(node);
|
|
354
|
+
const cx = r.cx;
|
|
355
|
+
const cy = r.cy;
|
|
356
|
+
const btcOrange = "#f7931a";
|
|
357
|
+
const blockCount = CHAIN_BLOCK_COUNT;
|
|
358
|
+
const blockW = CHAIN_BLOCK_W;
|
|
359
|
+
const blockH = CHAIN_BLOCK_H;
|
|
360
|
+
const linkW = CHAIN_LINK_W;
|
|
361
|
+
const totalW = blockCount * blockW + (blockCount - 1) * linkW;
|
|
362
|
+
const startX = cx - totalW / 2;
|
|
363
|
+
const art = (
|
|
364
|
+
<g>
|
|
365
|
+
{Array.from({ length: blockCount }, (_, i) => i).map((i) => {
|
|
366
|
+
const bx = startX + i * (blockW + linkW);
|
|
367
|
+
const isCurrent = i === blockCount - 1;
|
|
368
|
+
return (
|
|
369
|
+
<g key={i}>
|
|
370
|
+
{i > 0 && <line x1={bx - linkW} y1={cy} x2={bx} y2={cy} stroke={btcOrange} strokeWidth={3.5} strokeLinecap="round" />}
|
|
371
|
+
<rect x={bx + 3} y={cy - blockH / 2 + 3} width={blockW} height={blockH} rx={4} fill={frame.stroke} opacity={0.18} stroke="none" />
|
|
372
|
+
<rect
|
|
373
|
+
data-cdl-role={i === 0 ? "node-body" : undefined}
|
|
374
|
+
x={bx}
|
|
375
|
+
y={cy - blockH / 2}
|
|
376
|
+
width={blockW}
|
|
377
|
+
height={blockH}
|
|
378
|
+
rx={4}
|
|
379
|
+
fill={isCurrent ? btcOrange : "var(--cdl-node-fill, #ffffff)"}
|
|
380
|
+
stroke={btcOrange}
|
|
381
|
+
strokeWidth={isCurrent ? 2.5 : 1.5}
|
|
382
|
+
/>
|
|
383
|
+
<text x={bx + blockW / 2} y={cy - 4} fontSize={30} fontWeight={700} textAnchor="middle" fill={isCurrent ? "#ffffff" : btcOrange} fontFamily="Georgia, serif">
|
|
384
|
+
₿
|
|
385
|
+
</text>
|
|
386
|
+
<text x={bx + blockW / 2} y={cy + 16} fontSize={9} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fontWeight={700} textAnchor="middle" fill={isCurrent ? "#ffffff" : "var(--cdl-text-dim, #5a6270)"}>
|
|
387
|
+
#{820 + i}
|
|
388
|
+
</text>
|
|
389
|
+
</g>
|
|
390
|
+
);
|
|
391
|
+
})}
|
|
392
|
+
</g>
|
|
393
|
+
);
|
|
394
|
+
return (
|
|
395
|
+
<g>
|
|
396
|
+
{fitArt(r, chainExtent(), art)}
|
|
397
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
398
|
+
</g>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** shape-ethereum-chain = Ethereum (紫 accent + 本物 Ethereum Diamond ロゴ) */
|
|
403
|
+
export function ShapeEthereumChainNode({ node, active }: ShapeProps): JSX.Element {
|
|
404
|
+
const frame = commonFrame(active);
|
|
405
|
+
const r = shapeRegion(node);
|
|
406
|
+
const cx = r.cx;
|
|
407
|
+
const cy = r.cy;
|
|
408
|
+
const ethPurple = "#627eea";
|
|
409
|
+
const blockCount = CHAIN_BLOCK_COUNT;
|
|
410
|
+
const blockW = CHAIN_BLOCK_W;
|
|
411
|
+
const blockH = CHAIN_BLOCK_H;
|
|
412
|
+
const linkW = CHAIN_LINK_W;
|
|
413
|
+
const totalW = blockCount * blockW + (blockCount - 1) * linkW;
|
|
414
|
+
const startX = cx - totalW / 2;
|
|
415
|
+
const art = (
|
|
416
|
+
<g>
|
|
417
|
+
{Array.from({ length: blockCount }, (_, i) => i).map((i) => {
|
|
418
|
+
const bx = startX + i * (blockW + linkW);
|
|
419
|
+
const isCurrent = i === blockCount - 1;
|
|
420
|
+
const iconColors = isCurrent
|
|
421
|
+
? { light: "#ffffff", medium: "#e5e5f5", dark: "#c9c9e0", darker: "#a4a4c9", stroke: "#ffffff" }
|
|
422
|
+
: { light: "#8a92b2", medium: "#62688f", dark: "#454a75", darker: "#3c3c3b", stroke: ethPurple };
|
|
423
|
+
return (
|
|
424
|
+
<g key={i}>
|
|
425
|
+
{i > 0 && <line x1={bx - linkW} y1={cy} x2={bx} y2={cy} stroke={ethPurple} strokeWidth={3.5} strokeLinecap="round" />}
|
|
426
|
+
<rect x={bx + 3} y={cy - blockH / 2 + 3} width={blockW} height={blockH} rx={4} fill={frame.stroke} opacity={0.18} stroke="none" />
|
|
427
|
+
<rect
|
|
428
|
+
data-cdl-role={i === 0 ? "node-body" : undefined}
|
|
429
|
+
x={bx}
|
|
430
|
+
y={cy - blockH / 2}
|
|
431
|
+
width={blockW}
|
|
432
|
+
height={blockH}
|
|
433
|
+
rx={4}
|
|
434
|
+
fill={isCurrent ? ethPurple : "var(--cdl-node-fill, #ffffff)"}
|
|
435
|
+
stroke={ethPurple}
|
|
436
|
+
strokeWidth={isCurrent ? 2.5 : 1.5}
|
|
437
|
+
/>
|
|
438
|
+
{/* Ethereum Diamond ロゴ */}
|
|
439
|
+
<EthereumDiamond cx={bx + blockW / 2} cy={cy - 6} h={16} colors={iconColors} />
|
|
440
|
+
<text x={bx + blockW / 2} y={cy + 22} fontSize={9} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fontWeight={700} textAnchor="middle" fill={isCurrent ? "#ffffff" : "var(--cdl-text-dim, #5a6270)"}>
|
|
441
|
+
#{21_400 + i}
|
|
442
|
+
</text>
|
|
443
|
+
</g>
|
|
444
|
+
);
|
|
445
|
+
})}
|
|
446
|
+
</g>
|
|
447
|
+
);
|
|
448
|
+
return (
|
|
449
|
+
<g>
|
|
450
|
+
{fitArt(r, chainExtent(), art)}
|
|
451
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
452
|
+
</g>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** shape-blockchain-node = P2P full node (中央 hex + 6 peer 外側配置) */
|
|
457
|
+
export function ShapeBlockchainNodeNode({ node, active }: ShapeProps): JSX.Element {
|
|
458
|
+
const frame = commonFrame(active);
|
|
459
|
+
const r = shapeRegion(node);
|
|
460
|
+
const cx = r.cx;
|
|
461
|
+
const cy = r.cy;
|
|
462
|
+
const hexR = NODE_HEX_R;
|
|
463
|
+
const peerR = NODE_PEER_R;
|
|
464
|
+
const peerDist = NODE_PEER_DIST;
|
|
465
|
+
const art = (
|
|
466
|
+
<g>
|
|
467
|
+
{Array.from({ length: 6 }, (_, i) => i).map((i) => {
|
|
468
|
+
const a = (i / 6) * Math.PI * 2 - Math.PI / 2;
|
|
469
|
+
const px = cx + Math.cos(a) * peerDist;
|
|
470
|
+
const py = cy + Math.sin(a) * peerDist;
|
|
471
|
+
return (
|
|
472
|
+
<g key={i}>
|
|
473
|
+
<line x1={cx + Math.cos(a) * hexR} y1={cy + Math.sin(a) * hexR} x2={px} y2={py} stroke={frame.stroke} strokeWidth={1.5} strokeDasharray="4 3" opacity={0.7} />
|
|
474
|
+
<polygon
|
|
475
|
+
points={Array.from({ length: 6 }, (_, k) => {
|
|
476
|
+
const ka = (k / 6) * Math.PI * 2 - Math.PI / 2;
|
|
477
|
+
return `${px + Math.cos(ka) * peerR},${py + Math.sin(ka) * peerR}`;
|
|
478
|
+
}).join(" ")}
|
|
479
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
480
|
+
stroke={frame.stroke}
|
|
481
|
+
strokeWidth={1.5}
|
|
482
|
+
strokeLinejoin="round"
|
|
483
|
+
/>
|
|
484
|
+
<circle cx={px} cy={py} r={4} fill="#27c93f" stroke="none">
|
|
485
|
+
<animate attributeName="opacity" values="0.4;1;0.4" dur={`${1.5 + i * 0.2}s`} repeatCount="indefinite" />
|
|
486
|
+
</circle>
|
|
487
|
+
</g>
|
|
488
|
+
);
|
|
489
|
+
})}
|
|
490
|
+
<polygon
|
|
491
|
+
data-cdl-role="node-body"
|
|
492
|
+
points={Array.from({ length: 6 }, (_, i) => {
|
|
493
|
+
const a = (i / 6) * Math.PI * 2 - Math.PI / 2;
|
|
494
|
+
return `${cx + Math.cos(a) * hexR},${cy + Math.sin(a) * hexR}`;
|
|
495
|
+
}).join(" ")}
|
|
496
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
497
|
+
fillOpacity={0.18}
|
|
498
|
+
stroke={frame.stroke}
|
|
499
|
+
strokeWidth={frame.strokeWidth}
|
|
500
|
+
strokeLinejoin="round"
|
|
501
|
+
/>
|
|
502
|
+
<g transform={`translate(${cx - 16}, ${cy - 16})`}>
|
|
503
|
+
<rect x={0} y={0} width={32} height={8} rx={2} fill="var(--cdl-tone-accent, #2d6a8f)" stroke="none" />
|
|
504
|
+
<rect x={0} y={12} width={32} height={8} rx={2} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.6} stroke="none" />
|
|
505
|
+
<rect x={0} y={24} width={32} height={8} rx={2} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.3} stroke="none" />
|
|
506
|
+
</g>
|
|
507
|
+
</g>
|
|
508
|
+
);
|
|
509
|
+
return (
|
|
510
|
+
<g>
|
|
511
|
+
{fitArt(r, blockchainNodeExtent(frame.strokeWidth), art)}
|
|
512
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
513
|
+
</g>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** shape-rpc-node = JSON-RPC provider */
|
|
518
|
+
export function ShapeRpcNodeNode({ node, active }: ShapeProps): JSX.Element {
|
|
519
|
+
const frame = commonFrame(active);
|
|
520
|
+
const r = shapeRegion(node);
|
|
521
|
+
const cx = r.cx;
|
|
522
|
+
const cy = r.cy;
|
|
523
|
+
const bw = Math.min(r.availW - 40, 240);
|
|
524
|
+
const bh = Math.min(r.availH - 20, 180);
|
|
525
|
+
const x = cx - bw / 2;
|
|
526
|
+
const y = cy - bh / 2;
|
|
527
|
+
return (
|
|
528
|
+
<g>
|
|
529
|
+
<rect data-cdl-role="node-body" x={x} y={y} width={bw} height={bh} rx={10} fill="#1a2332" stroke={frame.stroke} strokeWidth={frame.strokeWidth} />
|
|
530
|
+
<rect x={x} y={y} width={bw} height={30} rx={10} fill="#2d3a4f" stroke="none" />
|
|
531
|
+
<circle cx={x + 14} cy={y + 15} r={5} fill="#27c93f" stroke="none">
|
|
532
|
+
<animate attributeName="opacity" values="0.4;1;0.4" dur="1.5s" repeatCount="indefinite" />
|
|
533
|
+
</circle>
|
|
534
|
+
<text x={x + 26} y={y + 20} fontSize={12} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fontWeight={700} fill="#7cd88f">
|
|
535
|
+
RPC ONLINE
|
|
536
|
+
</text>
|
|
537
|
+
<text x={cx} y={cy + 6} fontSize={30} fontWeight={700} textAnchor="middle" fill="#e6edf3" fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">
|
|
538
|
+
{"{...}"}
|
|
539
|
+
</text>
|
|
540
|
+
<text x={cx} y={cy + 26} fontSize={11} textAnchor="middle" fill="#7a8290" fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">
|
|
541
|
+
JSON-RPC 2.0
|
|
542
|
+
</text>
|
|
543
|
+
<rect x={x + 16} y={y + bh - 24} width={bw - 32} height={8} rx={4} fill="#0f1419" stroke="none" />
|
|
544
|
+
<rect x={x + 16} y={y + bh - 24} width={(bw - 32) * 0.92} height={8} rx={4} fill="#27c93f" stroke="none">
|
|
545
|
+
<animate attributeName="width" values={`${(bw - 32) * 0.86};${(bw - 32) * 0.96};${(bw - 32) * 0.86}`} dur="3s" repeatCount="indefinite" />
|
|
546
|
+
</rect>
|
|
547
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
548
|
+
</g>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** shape-wallet = FC-S5 忠実移植 (二つ折り札入れ横長 + fold line 中央 + bar clasp + engraved Ξ text + コイン peek 3) */
|
|
553
|
+
export function ShapeWalletNode({ node, active }: ShapeProps): JSX.Element {
|
|
554
|
+
const frame = commonFrame(active);
|
|
555
|
+
const r = shapeRegion(node);
|
|
556
|
+
const cx = r.cx;
|
|
557
|
+
const cy = r.cy + 4;
|
|
558
|
+
// FC-S5 元 SVG = body 220x140 (横長 rounded rect)
|
|
559
|
+
const w = Math.min(r.availW - 60, 220);
|
|
560
|
+
const h = Math.min(r.availH - 60, 140);
|
|
561
|
+
const x = cx - w / 2;
|
|
562
|
+
const y = cy - h / 2;
|
|
563
|
+
const accent = "var(--cdl-tone-accent, #2d6a8f)";
|
|
564
|
+
const accentDarker = "#0f3450";
|
|
565
|
+
// fold line = body 真ん中 y
|
|
566
|
+
const foldY = y + h / 2;
|
|
567
|
+
// bar clasp = FC-S5 元 SVG の比率 = w*0.255 (56/220)、 高さ 12 (fold line 上に重なる)
|
|
568
|
+
const barW = w * 0.255;
|
|
569
|
+
const barH = 12;
|
|
570
|
+
const barX = cx - barW / 2;
|
|
571
|
+
const barY = foldY - barH / 2 - 3; // fold line が bar 中央 y=148 を通るように配置
|
|
572
|
+
// コイン peek 3 枚 = FC-S5 元 SVG 座標を w=220 相対比率に変換
|
|
573
|
+
// 元 = body x=40, y=80、 coin: (98,62 r=14) (150,52 r=16) (200,64 r=13) → 相対 (x + w*0.264, y-18, r*14/220 * w) 等
|
|
574
|
+
const coins = [
|
|
575
|
+
{ cx: x + w * 0.264, cy: y - 18, r: 14 }, // 左 (元 cx=98, cy=62, r=14)
|
|
576
|
+
{ cx: cx, cy: y - 28, r: 16 }, // 中央 (元 cx=150, cy=52, r=16)
|
|
577
|
+
{ cx: x + w * 0.727, cy: y - 16, r: 13 }, // 右 (元 cx=200, cy=64, r=13)
|
|
578
|
+
];
|
|
579
|
+
return (
|
|
580
|
+
<g>
|
|
581
|
+
{/* ==== drop shadow (浮遊感、 FC-S5 元) ==== */}
|
|
582
|
+
<ellipse cx={cx} cy={y + h + 25} rx={w * 0.45} ry={10} fill="#000" opacity={0.3} stroke="none" />
|
|
583
|
+
<ellipse cx={cx} cy={y + h + 27} rx={w * 0.4} ry={6} fill="#000" opacity={0.2} stroke="none" />
|
|
584
|
+
{/* ==== 全体を -3° 回転 (立体感、 FC-S5 元 rotate(-3 150 145)) ==== */}
|
|
585
|
+
<g transform={`rotate(-3 ${cx} ${cy})`}>
|
|
586
|
+
{/* ==== コイン peek 3 枚 (body 上端 y より上に配置、 body の背後に半分隠れる) ==== */}
|
|
587
|
+
{coins.map((coin, i) => (
|
|
588
|
+
<g key={i}>
|
|
589
|
+
{/* 底の厚み (FC-S5 元 = ellipse rx=r ry=4) */}
|
|
590
|
+
<ellipse cx={coin.cx} cy={coin.cy + 4} rx={coin.r} ry={4} fill="#8b6914" />
|
|
591
|
+
{/* 前面 circle */}
|
|
592
|
+
<circle cx={coin.cx} cy={coin.cy} r={coin.r} fill="#e5c07b" stroke="#a67c00" strokeWidth={1.8} />
|
|
593
|
+
{/* highlight ellipse (FC-S5 元では中央 coin のみだが全 coin に統一) */}
|
|
594
|
+
{i !== 1 ? null : (
|
|
595
|
+
<ellipse cx={coin.cx - 4} cy={coin.cy - 5} rx={5} ry={4} fill="#f5dea0" opacity={0.7} />
|
|
596
|
+
)}
|
|
597
|
+
{/* Ξ 3D shading (Ethereum Diamond) */}
|
|
598
|
+
<EthereumDiamond
|
|
599
|
+
cx={coin.cx}
|
|
600
|
+
cy={coin.cy}
|
|
601
|
+
h={coin.r * 0.5}
|
|
602
|
+
colors={{
|
|
603
|
+
light: "#3a2a08",
|
|
604
|
+
medium: "#5a4a1a",
|
|
605
|
+
dark: "#2a1a04",
|
|
606
|
+
darker: "#3a2a08",
|
|
607
|
+
stroke: "#3a2a08",
|
|
608
|
+
}}
|
|
609
|
+
/>
|
|
610
|
+
</g>
|
|
611
|
+
))}
|
|
612
|
+
{/* ==== body 横長 rounded rect ==== */}
|
|
613
|
+
<rect
|
|
614
|
+
data-cdl-role="node-body"
|
|
615
|
+
x={x}
|
|
616
|
+
y={y}
|
|
617
|
+
width={w}
|
|
618
|
+
height={h}
|
|
619
|
+
rx={14}
|
|
620
|
+
fill={accent}
|
|
621
|
+
stroke={accentDarker}
|
|
622
|
+
strokeWidth={frame.strokeWidth}
|
|
623
|
+
/>
|
|
624
|
+
{/* ==== fold line (body 中央 y、 1 本線) ==== */}
|
|
625
|
+
<line x1={x} y1={foldY} x2={x + w} y2={foldY} stroke={accentDarker} strokeWidth={2.5} />
|
|
626
|
+
<path d={`M ${x + 2} ${foldY - 2} L ${x + w - 2} ${foldY - 2}`} fill="none" stroke="#ffffff" strokeWidth={0.8} opacity={0.3} />
|
|
627
|
+
{/* ==== bar clasp (fold line 中央に配置、 fold 隠す minimal 形) ==== */}
|
|
628
|
+
{/* side edge (下 3px 厚み) */}
|
|
629
|
+
<rect x={barX} y={barY + 3} width={barW} height={barH} fill="#8b6914" />
|
|
630
|
+
{/* bar body */}
|
|
631
|
+
<rect x={barX} y={barY} width={barW} height={barH} rx={2} fill="#e5c07b" stroke="#a67c00" strokeWidth={1.5} />
|
|
632
|
+
{/* 上部 highlight */}
|
|
633
|
+
<rect x={barX + 2} y={barY + 2} width={barW - 4} height={3} rx={1} fill="#f5dea0" opacity={0.7} />
|
|
634
|
+
{/* 両端 rivet 3D (FC-S5 元 = cx=128/172, cy=148, r=2.5) */}
|
|
635
|
+
<circle cx={barX + 6} cy={foldY} r={2.5} fill="#5a3a10" stroke="#3a2a08" strokeWidth={0.6} />
|
|
636
|
+
<circle cx={barX + 5} cy={foldY - 1} r={0.8} fill="#8b6914" />
|
|
637
|
+
<circle cx={barX + barW - 6} cy={foldY} r={2.5} fill="#5a3a10" stroke="#3a2a08" strokeWidth={0.6} />
|
|
638
|
+
<circle cx={barX + barW - 7} cy={foldY - 1} r={0.8} fill="#8b6914" />
|
|
639
|
+
{/* engraved Ξ text (FC-S5 元 = SVG text font-size=9、 EthereumDiamond ではない) */}
|
|
640
|
+
<text x={cx} y={foldY + 3} fontSize={9} fontWeight={700} fill="#5a3a10" textAnchor="middle" opacity={0.8}>Ξ</text>
|
|
641
|
+
{/* body 上端 highlight (FC-S5 元 = M 42 86 L 258 86) */}
|
|
642
|
+
<path d={`M ${x + 2} ${y + 6} L ${x + w - 2} ${y + 6}`} fill="none" stroke="#ffffff" strokeWidth={1} opacity={0.4} />
|
|
643
|
+
</g>
|
|
644
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
645
|
+
</g>
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** shape-nft = NFT card (白 body + accent art frame + 巨大 NFT 文字、 統一デザイン) */
|
|
650
|
+
export function ShapeNftNode({ node, active }: ShapeProps): JSX.Element {
|
|
651
|
+
const frame = commonFrame(active);
|
|
652
|
+
const r = shapeRegion(node);
|
|
653
|
+
const cx = r.cx;
|
|
654
|
+
const cy = r.cy;
|
|
655
|
+
const cardW = NFT_W;
|
|
656
|
+
const cardH = NFT_H;
|
|
657
|
+
const x = cx - cardW / 2;
|
|
658
|
+
const y = cy - cardH / 2;
|
|
659
|
+
const artH = cardH * 0.72;
|
|
660
|
+
const artCx = x + cardW / 2;
|
|
661
|
+
const artCy = y + artH / 2;
|
|
662
|
+
const art = (
|
|
663
|
+
<g>
|
|
664
|
+
{/* shadow */}
|
|
665
|
+
<rect x={x + 4} y={y + 4} width={cardW} height={cardH} rx={14} fill={frame.stroke} opacity={0.2} stroke="none" />
|
|
666
|
+
{/* card body (白背景 + accent stroke) */}
|
|
667
|
+
<rect data-cdl-role="node-body" x={x} y={y} width={cardW} height={cardH} rx={14} {...frame} />
|
|
668
|
+
{/* 上部 art area (accent 色 fill light、 他 shape の accent 色帯 pattern) */}
|
|
669
|
+
<path
|
|
670
|
+
d={`M ${x} ${y + artH} L ${x} ${y + 14} Q ${x} ${y}, ${x + 14} ${y} L ${x + cardW - 14} ${y} Q ${x + cardW} ${y}, ${x + cardW} ${y + 14} L ${x + cardW} ${y + artH} Z`}
|
|
671
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
672
|
+
fillOpacity={0.15}
|
|
673
|
+
stroke="none"
|
|
674
|
+
/>
|
|
675
|
+
{/* 巨大 NFT 文字 (art 中央、 accent 色) */}
|
|
676
|
+
<text
|
|
677
|
+
x={artCx}
|
|
678
|
+
y={artCy + 24}
|
|
679
|
+
fontSize={68}
|
|
680
|
+
fontWeight={900}
|
|
681
|
+
letterSpacing={4}
|
|
682
|
+
textAnchor="middle"
|
|
683
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
684
|
+
fontFamily="Georgia, serif"
|
|
685
|
+
>
|
|
686
|
+
NFT
|
|
687
|
+
</text>
|
|
688
|
+
{/* 下部 info bar */}
|
|
689
|
+
<line x1={x} y1={y + artH} x2={x + cardW} y2={y + artH} stroke={frame.stroke} strokeWidth={1} opacity={0.5} />
|
|
690
|
+
{/* token id (左) */}
|
|
691
|
+
<text x={x + 16} y={y + artH + 30} fontSize={15} fontWeight={700} fill="var(--cdl-text, #1a1f2a)" fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">
|
|
692
|
+
#1024
|
|
693
|
+
</text>
|
|
694
|
+
{/* verified check (右) */}
|
|
695
|
+
<g transform={`translate(${x + cardW - 20}, ${y + artH + 22})`}>
|
|
696
|
+
<circle cx={0} cy={0} r={10} fill="#27c93f" stroke="#ffffff" strokeWidth={2} />
|
|
697
|
+
<path d="M -4 0 L -1 3 L 4 -3" fill="none" stroke="#ffffff" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
|
|
698
|
+
</g>
|
|
699
|
+
</g>
|
|
700
|
+
);
|
|
701
|
+
return (
|
|
702
|
+
<g>
|
|
703
|
+
{fitArt(r, nftExtent(frame.strokeWidth), art)}
|
|
704
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
705
|
+
</g>
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** shape-token = coin (白 body + accent 色 rim + accent 色 Diamond ロゴ、 統一デザイン) */
|
|
710
|
+
export function ShapeTokenNode({ node, active }: ShapeProps): JSX.Element {
|
|
711
|
+
const frame = commonFrame(active);
|
|
712
|
+
const r = shapeRegion(node);
|
|
713
|
+
const cx = r.cx;
|
|
714
|
+
const cy = r.cy;
|
|
715
|
+
const rr = Math.min(r.availW, r.availH) * 0.32;
|
|
716
|
+
return (
|
|
717
|
+
<g>
|
|
718
|
+
{/* shadow */}
|
|
719
|
+
<circle cx={cx + 4} cy={cy + 4} r={rr} fill={frame.stroke} opacity={0.2} stroke="none" />
|
|
720
|
+
{/* 硬貨 body (白 + accent stroke) */}
|
|
721
|
+
<circle
|
|
722
|
+
data-cdl-role="node-body"
|
|
723
|
+
cx={cx}
|
|
724
|
+
cy={cy}
|
|
725
|
+
r={rr}
|
|
726
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
727
|
+
stroke={frame.stroke}
|
|
728
|
+
strokeWidth={frame.strokeWidth}
|
|
729
|
+
/>
|
|
730
|
+
{/* accent 色 rim (少し内側) */}
|
|
731
|
+
<circle cx={cx} cy={cy} r={rr - 6} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.12} stroke="var(--cdl-tone-accent, #2d6a8f)" strokeWidth={1.5} />
|
|
732
|
+
{/* inner engraved dashed ring */}
|
|
733
|
+
<circle cx={cx} cy={cy} r={rr - 14} fill="none" stroke="var(--cdl-tone-accent, #2d6a8f)" strokeWidth={1} strokeDasharray="3 3" opacity={0.5} />
|
|
734
|
+
{/* Ethereum Diamond ロゴ (硬貨表面 中央、 accent 色) */}
|
|
735
|
+
<EthereumDiamond
|
|
736
|
+
cx={cx}
|
|
737
|
+
cy={cy}
|
|
738
|
+
h={rr * 0.55}
|
|
739
|
+
colors={{
|
|
740
|
+
light: "var(--cdl-tone-accent, #2d6a8f)",
|
|
741
|
+
medium: "var(--cdl-tone-accent, #2d6a8f)",
|
|
742
|
+
dark: "var(--cdl-tone-accent, #2d6a8f)",
|
|
743
|
+
darker: "#1a4a68",
|
|
744
|
+
stroke: "#1a4a68",
|
|
745
|
+
}}
|
|
746
|
+
/>
|
|
747
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
748
|
+
</g>
|
|
749
|
+
);
|
|
750
|
+
}
|