@formicoidea/labre-framework-wardley 0.23.0 → 0.23.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/dist/consts.d.ts +72 -0
- package/{src/consts.ts → dist/consts.js} +63 -72
- package/dist/descriptor.d.ts +7 -0
- package/{src/descriptor.ts → dist/descriptor.js} +1 -1
- package/dist/effects.d.ts +10 -0
- package/dist/effects.js +7 -0
- package/dist/element-renderer.d.ts +15 -0
- package/dist/element-renderer.js +160 -0
- package/dist/element-view.d.ts +21 -0
- package/dist/element-view.js +122 -0
- package/dist/gradient.d.ts +18 -0
- package/dist/gradient.js +112 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/label-layout.d.ts +21 -0
- package/dist/label-layout.js +73 -0
- package/dist/legend.d.ts +12 -0
- package/dist/legend.js +333 -0
- package/dist/node/consts.d.ts +107 -0
- package/{src/node/consts.ts → dist/node/consts.js} +12 -20
- package/dist/node/label-editor.d.ts +28 -0
- package/dist/node/label-editor.js +216 -0
- package/dist/node/node-renderer.d.ts +17 -0
- package/dist/node/node-renderer.js +106 -0
- package/{src/node/node-view.ts → dist/node/node-view.d.ts} +3 -3
- package/dist/node/node-view.js +10 -0
- package/dist/templates/index.d.ts +3 -0
- package/dist/templates/index.js +172 -0
- package/dist/templates/maps.d.ts +3 -0
- package/dist/templates/maps.js +247 -0
- package/dist/toolbar/config.d.ts +75 -0
- package/dist/toolbar/config.js +206 -0
- package/dist/toolbar/icons.d.ts +31 -0
- package/{src/toolbar/icons.ts → dist/toolbar/icons.js} +51 -66
- package/dist/toolbar/node-config.d.ts +2 -0
- package/{src/toolbar/node-config.ts → dist/toolbar/node-config.js} +7 -14
- package/dist/toolbar/senior-tool.d.ts +2 -0
- package/{src/toolbar/senior-tool.ts → dist/toolbar/senior-tool.js} +5 -5
- package/dist/toolbar/wardley-menu.d.ts +53 -0
- package/dist/toolbar/wardley-menu.js +408 -0
- package/dist/toolbar/wardley-senior-button.d.ts +18 -0
- package/dist/toolbar/wardley-senior-button.js +146 -0
- package/dist/toolbar/wardley-tool-button.d.ts +10 -0
- package/dist/toolbar/wardley-tool-button.js +123 -0
- package/dist/view.d.ts +7 -0
- package/dist/view.js +36 -0
- package/package.json +15 -6
- package/src/effects.ts +0 -17
- package/src/element-renderer.ts +0 -242
- package/src/element-view.ts +0 -143
- package/src/gradient.ts +0 -137
- package/src/index.ts +0 -1
- package/src/label-layout.ts +0 -126
- package/src/legend.ts +0 -438
- package/src/node/node-renderer.ts +0 -142
- package/src/templates/index.ts +0 -236
- package/src/templates/maps.ts +0 -283
- package/src/toolbar/config.ts +0 -280
- package/src/toolbar/wardley-menu.ts +0 -552
- package/src/toolbar/wardley-senior-button.ts +0 -154
- package/src/view.ts +0 -39
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ElementRenderer,
|
|
3
|
-
ElementRendererExtension,
|
|
4
|
-
} from '@formicoidea/labre-core/blocks/surface';
|
|
5
|
-
import { shape as shapeRenderer } from '@formicoidea/labre-core/gfx/shape';
|
|
6
|
-
import { type WardleyNodeElementModel, DefaultTheme } from '@formicoidea/labre-core/model';
|
|
7
|
-
|
|
8
|
-
import { ANCHOR, ECOSYSTEM, METHOD, NODE_FILL } from './consts';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Renderer for a Wardley node. The circle is drawn by REUSING the native shape
|
|
12
|
-
* renderer (so stroke width, colors and theme behave exactly like a native
|
|
13
|
-
* ellipse). On top of it, a glyph is drawn for two kinds:
|
|
14
|
-
* - `anchor` → an inscribed person (head + shoulders), clipped to the circle.
|
|
15
|
-
* - `ecosystem` → a double border at the rim + diagonal hatching confined to the
|
|
16
|
-
* inner donut + a hollow central circle.
|
|
17
|
-
* All glyph strokes use the model's (editable) stroke color; the white band and
|
|
18
|
-
* the hollow center come from the base fill, so colors stay editable.
|
|
19
|
-
*/
|
|
20
|
-
export const wardleyNode: ElementRenderer<WardleyNodeElementModel> = (
|
|
21
|
-
model,
|
|
22
|
-
ctx,
|
|
23
|
-
matrix,
|
|
24
|
-
renderer,
|
|
25
|
-
rc,
|
|
26
|
-
bound
|
|
27
|
-
) => {
|
|
28
|
-
const [, , w, h] = model.deserializedXYWH;
|
|
29
|
-
const cx = w / 2;
|
|
30
|
-
const cy = h / 2;
|
|
31
|
-
|
|
32
|
-
// Capture the element-local transform BEFORE the shape renderer mutates the
|
|
33
|
-
// matrix, so the glyph can be drawn in the same space afterwards.
|
|
34
|
-
const glyphMatrix = DOMMatrix.fromMatrix(matrix)
|
|
35
|
-
.translateSelf(cx, cy)
|
|
36
|
-
.rotateSelf(model.rotate)
|
|
37
|
-
.translateSelf(-cx, -cy);
|
|
38
|
-
|
|
39
|
-
// Native ellipse (fill / stroke / theme handled natively).
|
|
40
|
-
shapeRenderer(model, ctx, matrix, renderer, rc, bound);
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
model.kind !== 'anchor' &&
|
|
44
|
-
model.kind !== 'ecosystem' &&
|
|
45
|
-
model.kind !== 'method'
|
|
46
|
-
)
|
|
47
|
-
return;
|
|
48
|
-
|
|
49
|
-
const strokeWidth = model.strokeWidth || 1;
|
|
50
|
-
const R = Math.min(w, h) / 2 - strokeWidth / 2;
|
|
51
|
-
const color = renderer.getColorValue(
|
|
52
|
-
model.strokeColor,
|
|
53
|
-
DefaultTheme.shapeStrokeColor,
|
|
54
|
-
true
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
ctx.setTransform(glyphMatrix);
|
|
58
|
-
|
|
59
|
-
// ── Anchor: person glyph (clipped to the circle) ────────────────────
|
|
60
|
-
if (model.kind === 'anchor') {
|
|
61
|
-
ctx.save();
|
|
62
|
-
ctx.beginPath();
|
|
63
|
-
ctx.arc(cx, cy, R - strokeWidth / 2, 0, Math.PI * 2);
|
|
64
|
-
ctx.clip();
|
|
65
|
-
|
|
66
|
-
ctx.lineWidth = strokeWidth;
|
|
67
|
-
ctx.strokeStyle = color;
|
|
68
|
-
ctx.lineCap = 'round';
|
|
69
|
-
ctx.lineJoin = 'round';
|
|
70
|
-
|
|
71
|
-
// head
|
|
72
|
-
ctx.beginPath();
|
|
73
|
-
ctx.arc(cx, cy + ANCHOR.headCY * R, ANCHOR.headR * R, 0, Math.PI * 2);
|
|
74
|
-
ctx.stroke();
|
|
75
|
-
|
|
76
|
-
// rounded shoulders (extremities sit on the circle border)
|
|
77
|
-
const sx = ANCHOR.shoulderEndX * R;
|
|
78
|
-
const sy = ANCHOR.shoulderEndY * R;
|
|
79
|
-
const kx = ANCHOR.shoulderCtrlX * R;
|
|
80
|
-
const ky = ANCHOR.shoulderCtrlY * R;
|
|
81
|
-
ctx.beginPath();
|
|
82
|
-
ctx.moveTo(cx - sx, cy + sy);
|
|
83
|
-
ctx.bezierCurveTo(cx - kx, cy + ky, cx + kx, cy + ky, cx + sx, cy + sy);
|
|
84
|
-
ctx.stroke();
|
|
85
|
-
|
|
86
|
-
ctx.restore();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ── Method: a white component inscribed in the colored outer circle ──
|
|
91
|
-
if (model.kind === 'method') {
|
|
92
|
-
const rInner = R * METHOD.centerRatio;
|
|
93
|
-
ctx.fillStyle = NODE_FILL;
|
|
94
|
-
ctx.lineWidth = strokeWidth;
|
|
95
|
-
ctx.strokeStyle = color;
|
|
96
|
-
ctx.beginPath();
|
|
97
|
-
ctx.arc(cx, cy, rInner, 0, Math.PI * 2);
|
|
98
|
-
ctx.fill();
|
|
99
|
-
ctx.stroke();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── Ecosystem: double border + hatched inner donut + hollow center ──
|
|
104
|
-
const rBorder2 = R * ECOSYSTEM.secondBorderRatio;
|
|
105
|
-
const rCenter = R * ECOSYSTEM.centerRatio;
|
|
106
|
-
const rHatch = R * ECOSYSTEM.hatchOuterRatio;
|
|
107
|
-
|
|
108
|
-
// Hatch confined to the donut [rCenter, rHatch] (even-odd clip = annulus).
|
|
109
|
-
ctx.save();
|
|
110
|
-
ctx.beginPath();
|
|
111
|
-
ctx.arc(cx, cy, rHatch, 0, Math.PI * 2);
|
|
112
|
-
ctx.moveTo(cx + rCenter, cy);
|
|
113
|
-
ctx.arc(cx, cy, rCenter, 0, Math.PI * 2);
|
|
114
|
-
ctx.clip('evenodd');
|
|
115
|
-
|
|
116
|
-
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.6);
|
|
117
|
-
ctx.strokeStyle = color;
|
|
118
|
-
const step = R * ECOSYSTEM.hatchSpacingRatio;
|
|
119
|
-
for (let d = -2 * R; d <= 2 * R; d += step) {
|
|
120
|
-
ctx.beginPath();
|
|
121
|
-
ctx.moveTo(cx - R, cy - R + d);
|
|
122
|
-
ctx.lineTo(cx + R, cy + R + d);
|
|
123
|
-
ctx.stroke();
|
|
124
|
-
}
|
|
125
|
-
ctx.restore();
|
|
126
|
-
|
|
127
|
-
// Double border (2nd inscribed circle) + central hole border. No fill: the
|
|
128
|
-
// white band and hollow center come from the base ellipse fill.
|
|
129
|
-
ctx.lineWidth = strokeWidth;
|
|
130
|
-
ctx.strokeStyle = color;
|
|
131
|
-
ctx.beginPath();
|
|
132
|
-
ctx.arc(cx, cy, rBorder2, 0, Math.PI * 2);
|
|
133
|
-
ctx.stroke();
|
|
134
|
-
ctx.beginPath();
|
|
135
|
-
ctx.arc(cx, cy, rCenter, 0, Math.PI * 2);
|
|
136
|
-
ctx.stroke();
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
export const WardleyNodeRendererExtension = ElementRendererExtension(
|
|
140
|
-
'wardleyNode',
|
|
141
|
-
wardleyNode
|
|
142
|
-
);
|
package/src/templates/index.ts
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
makeTemplateSnapshot,
|
|
3
|
-
type SurfaceElementsJSON,
|
|
4
|
-
surfaceText,
|
|
5
|
-
type Template,
|
|
6
|
-
type TemplateCategory,
|
|
7
|
-
} from '@formicoidea/labre-core/gfx/template';
|
|
8
|
-
import {
|
|
9
|
-
ConnectorMode,
|
|
10
|
-
FontFamily,
|
|
11
|
-
PointStyle,
|
|
12
|
-
ShapeStyle,
|
|
13
|
-
StrokeStyle,
|
|
14
|
-
TextAlign,
|
|
15
|
-
type WardleyBgVariant,
|
|
16
|
-
} from '@formicoidea/labre-core/model';
|
|
17
|
-
|
|
18
|
-
import { REF_WIDTH } from '../consts';
|
|
19
|
-
import { wardleyMaps } from './maps';
|
|
20
|
-
import {
|
|
21
|
-
ECOSYSTEM_SIZE,
|
|
22
|
-
HANDLE_SIZE,
|
|
23
|
-
INERTIA_COLOR,
|
|
24
|
-
INERTIA_SIZE,
|
|
25
|
-
LABEL_FONT_SIZE,
|
|
26
|
-
LINK_GREY,
|
|
27
|
-
LINK_STROKE_WIDTH,
|
|
28
|
-
MARKET_DOT_RING,
|
|
29
|
-
MARKET_DOT_SIZE,
|
|
30
|
-
MARKET_DOT_STROKE_WIDTH,
|
|
31
|
-
MARKET_LINK_COLOR,
|
|
32
|
-
MARKET_LINK_WIDTH,
|
|
33
|
-
MARKET_SIZE,
|
|
34
|
-
METHOD_FILL,
|
|
35
|
-
METHOD_SIZE,
|
|
36
|
-
NODE_FILL,
|
|
37
|
-
NODE_SIZE,
|
|
38
|
-
NODE_STROKE,
|
|
39
|
-
NODE_STROKE_WIDTH,
|
|
40
|
-
PIPELINE_FILL,
|
|
41
|
-
PIPELINE_HEIGHT,
|
|
42
|
-
PIPELINE_WIDTH,
|
|
43
|
-
WARDLEY_RED,
|
|
44
|
-
} from '../node/consts';
|
|
45
|
-
|
|
46
|
-
const VARIANT_DEFAULTS: Record<WardleyBgVariant, Record<string, unknown>> = {
|
|
47
|
-
classic: {},
|
|
48
|
-
opportunity: {
|
|
49
|
-
yAxisTitle: 'Opportunity',
|
|
50
|
-
showVisibilityLabels: false,
|
|
51
|
-
showCornerLabels: false,
|
|
52
|
-
},
|
|
53
|
-
benefit: {
|
|
54
|
-
yAxisTitle: '',
|
|
55
|
-
visibilityHigh: 'Benefit',
|
|
56
|
-
visibilityLow: 'Investment',
|
|
57
|
-
showCornerLabels: false,
|
|
58
|
-
},
|
|
59
|
-
'evolution-gradient': {},
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const bg = (variant: WardleyBgVariant, w = REF_WIDTH) => {
|
|
63
|
-
const h = Math.round((w * 9) / 16);
|
|
64
|
-
return { type: 'wardley', variant, ...VARIANT_DEFAULTS[variant], xywh: `[0,0,${w},${h}]` };
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/** A wardley node ellipse positioned by top-left. */
|
|
68
|
-
function node(
|
|
69
|
-
kind: string,
|
|
70
|
-
x: number,
|
|
71
|
-
y: number,
|
|
72
|
-
d = NODE_SIZE,
|
|
73
|
-
fill = NODE_FILL,
|
|
74
|
-
strokeWidth = NODE_STROKE_WIDTH
|
|
75
|
-
) {
|
|
76
|
-
return {
|
|
77
|
-
type: 'wardleyNode',
|
|
78
|
-
kind,
|
|
79
|
-
shapeType: 'ellipse',
|
|
80
|
-
filled: true,
|
|
81
|
-
fillColor: fill,
|
|
82
|
-
strokeColor: NODE_STROKE,
|
|
83
|
-
strokeWidth,
|
|
84
|
-
shapeStyle: ShapeStyle.General,
|
|
85
|
-
roughness: 0,
|
|
86
|
-
xywh: `[${x},${y},${d},${d}]`,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function label(x: number, y: number, str: string, align: 'left' | 'center' = 'left') {
|
|
91
|
-
return {
|
|
92
|
-
type: 'text',
|
|
93
|
-
text: surfaceText(str),
|
|
94
|
-
color: NODE_STROKE,
|
|
95
|
-
fontFamily: FontFamily.Inter,
|
|
96
|
-
fontSize: LABEL_FONT_SIZE,
|
|
97
|
-
textAlign: align === 'center' ? TextAlign.Center : TextAlign.Left,
|
|
98
|
-
xywh: `[${x},${y},140,26]`,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function connect(
|
|
103
|
-
source: Record<string, unknown>,
|
|
104
|
-
target: Record<string, unknown>,
|
|
105
|
-
opts: { red?: boolean } = {}
|
|
106
|
-
) {
|
|
107
|
-
return {
|
|
108
|
-
type: 'connector',
|
|
109
|
-
mode: ConnectorMode.Straight,
|
|
110
|
-
stroke: opts.red ? WARDLEY_RED : LINK_GREY,
|
|
111
|
-
strokeStyle: opts.red ? StrokeStyle.Dash : StrokeStyle.Solid,
|
|
112
|
-
strokeWidth: LINK_STROKE_WIDTH,
|
|
113
|
-
frontEndpointStyle: PointStyle.None,
|
|
114
|
-
rearEndpointStyle: opts.red ? PointStyle.Triangle : PointStyle.None,
|
|
115
|
-
source,
|
|
116
|
-
target,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const inertia = (x = 0, y = 0) => ({
|
|
121
|
-
type: 'shape',
|
|
122
|
-
shapeType: 'rect',
|
|
123
|
-
filled: true,
|
|
124
|
-
fillColor: INERTIA_COLOR,
|
|
125
|
-
strokeColor: INERTIA_COLOR,
|
|
126
|
-
strokeWidth: 0,
|
|
127
|
-
shapeStyle: ShapeStyle.General,
|
|
128
|
-
roughness: 0,
|
|
129
|
-
radius: 0,
|
|
130
|
-
xywh: `[${x},${y},${INERTIA_SIZE.w},${INERTIA_SIZE.h}]`,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
/** Pipeline composite: body rect + handle square (straddling top) + label. */
|
|
134
|
-
function pipeline(): SurfaceElementsJSON {
|
|
135
|
-
return {
|
|
136
|
-
label: label(0, 0, 'Pipeline', 'center'),
|
|
137
|
-
body: {
|
|
138
|
-
type: 'wardleyNode',
|
|
139
|
-
kind: 'pipeline',
|
|
140
|
-
shapeType: 'rect',
|
|
141
|
-
filled: true,
|
|
142
|
-
fillColor: PIPELINE_FILL,
|
|
143
|
-
strokeColor: NODE_STROKE,
|
|
144
|
-
strokeWidth: NODE_STROKE_WIDTH,
|
|
145
|
-
shapeStyle: ShapeStyle.General,
|
|
146
|
-
roughness: 0,
|
|
147
|
-
radius: 0,
|
|
148
|
-
xywh: `[0,39,${PIPELINE_WIDTH},${PIPELINE_HEIGHT}]`,
|
|
149
|
-
},
|
|
150
|
-
handle: {
|
|
151
|
-
type: 'wardleyNode',
|
|
152
|
-
kind: 'handle',
|
|
153
|
-
shapeType: 'rect',
|
|
154
|
-
filled: true,
|
|
155
|
-
fillColor: NODE_FILL,
|
|
156
|
-
strokeColor: NODE_STROKE,
|
|
157
|
-
strokeWidth: NODE_STROKE_WIDTH,
|
|
158
|
-
shapeStyle: ShapeStyle.General,
|
|
159
|
-
roughness: 0,
|
|
160
|
-
radius: 0,
|
|
161
|
-
xywh: `[${PIPELINE_WIDTH / 2 - HANDLE_SIZE / 2},${39 - HANDLE_SIZE / 2},${HANDLE_SIZE},${HANDLE_SIZE}]`,
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Market composite: outer circle + 3 inner dots wired in a triangle + label. */
|
|
167
|
-
function market(): SurfaceElementsJSON {
|
|
168
|
-
const R = MARKET_SIZE / 2;
|
|
169
|
-
const c = R; // center within the [0,0,MARKET_SIZE,MARKET_SIZE] box
|
|
170
|
-
const rho = MARKET_DOT_RING;
|
|
171
|
-
const sin60 = Math.sqrt(3) / 2;
|
|
172
|
-
const verts = [
|
|
173
|
-
[0, -rho],
|
|
174
|
-
[rho * sin60, rho / 2],
|
|
175
|
-
[-rho * sin60, rho / 2],
|
|
176
|
-
];
|
|
177
|
-
const dotAt = (vx: number, vy: number) =>
|
|
178
|
-
node('component', c + vx - MARKET_DOT_SIZE / 2, c + vy - MARKET_DOT_SIZE / 2, MARKET_DOT_SIZE, NODE_FILL, MARKET_DOT_STROKE_WIDTH);
|
|
179
|
-
const tri = (a: string, b: string) => ({
|
|
180
|
-
type: 'connector',
|
|
181
|
-
mode: ConnectorMode.Straight,
|
|
182
|
-
stroke: MARKET_LINK_COLOR,
|
|
183
|
-
strokeStyle: StrokeStyle.Solid,
|
|
184
|
-
strokeWidth: MARKET_LINK_WIDTH,
|
|
185
|
-
frontEndpointStyle: PointStyle.None,
|
|
186
|
-
rearEndpointStyle: PointStyle.None,
|
|
187
|
-
source: { id: a },
|
|
188
|
-
target: { id: b },
|
|
189
|
-
});
|
|
190
|
-
return {
|
|
191
|
-
circle: node('market', 0, 0, MARKET_SIZE, NODE_FILL),
|
|
192
|
-
d0: dotAt(verts[0][0], verts[0][1]),
|
|
193
|
-
d1: dotAt(verts[1][0], verts[1][1]),
|
|
194
|
-
d2: dotAt(verts[2][0], verts[2][1]),
|
|
195
|
-
t0: tri('d0', 'd1'),
|
|
196
|
-
t1: tri('d1', 'd2'),
|
|
197
|
-
t2: tri('d2', 'd0'),
|
|
198
|
-
label: label(MARKET_SIZE + 8, 2, 'Market'),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const single = (el: Record<string, unknown>): SurfaceElementsJSON => ({ a: el });
|
|
203
|
-
const nodeWithLabel = (kind: string, d: number, fill: string, name: string): SurfaceElementsJSON => ({
|
|
204
|
-
n: node(kind, 0, 0, d, fill),
|
|
205
|
-
l: label(d + 8, d / 2 - 13, name),
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
|
|
209
|
-
const bgPreview = (extra = '') =>
|
|
210
|
-
`<svg ${ATTRS} fill="none"><path d="M22 12 V64 H120" stroke="#3b3d42" stroke-width="2"/><path d="M44 12 V64 M68 12 V64 M94 12 V64" stroke="#9aa0a6" stroke-width="0.8"/>${extra}</svg>`;
|
|
211
|
-
const dotPreview = (fill: string, sw = 2) =>
|
|
212
|
-
`<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="13" fill="${fill}" stroke="#1f2328" stroke-width="${sw}"/></svg>`;
|
|
213
|
-
|
|
214
|
-
function tpl(name: string, preview: string, elements: SurfaceElementsJSON): Template {
|
|
215
|
-
return { name, type: 'template', preview, content: makeTemplateSnapshot(elements, name) };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export const wardleyTemplateCategory: TemplateCategory = {
|
|
219
|
-
name: 'Wardley',
|
|
220
|
-
templates: [
|
|
221
|
-
...wardleyMaps,
|
|
222
|
-
tpl('Map background', bgPreview(), { bg: bg('classic') }),
|
|
223
|
-
tpl('Opportunity gradient', bgPreview('<rect x="22" y="12" width="98" height="52" fill="#eef4fb" opacity="0.6"/>'), { bg: bg('opportunity') }),
|
|
224
|
-
tpl('Benefit gradient', bgPreview('<rect x="22" y="12" width="98" height="26" fill="#e6eef8" opacity="0.6"/>'), { bg: bg('benefit') }),
|
|
225
|
-
tpl('Evolution gradient', bgPreview('<rect x="22" y="12" width="98" height="52" fill="#e3e2e4" opacity="0.5"/>'), { bg: bg('evolution-gradient') }),
|
|
226
|
-
tpl('Component', dotPreview('#ffffff', 1.5), nodeWithLabel('component', NODE_SIZE, NODE_FILL, 'Component')),
|
|
227
|
-
tpl('Anchor', `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="13" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="36" r="3.5" fill="#1f2328"/><path d="M59 48 q8 -9 16 0" stroke="#1f2328" stroke-width="1.5" fill="none"/></svg>`, nodeWithLabel('anchor', NODE_SIZE, NODE_FILL, 'Anchor')),
|
|
228
|
-
tpl('Ecosystem', `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="15" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="40" r="11" fill="none" stroke="#1f2328"/><circle cx="67" cy="40" r="5" fill="#fff" stroke="#1f2328"/></svg>`, nodeWithLabel('ecosystem', ECOSYSTEM_SIZE, NODE_FILL, 'Ecosystem')),
|
|
229
|
-
tpl('Method', `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="15" fill="#d9d9d9" stroke="#1f2328" stroke-width="1.5"/><circle cx="67" cy="40" r="7" fill="#fff" stroke="#1f2328"/></svg>`, nodeWithLabel('method', METHOD_SIZE, METHOD_FILL, 'Component')),
|
|
230
|
-
tpl('Pipeline', `<svg ${ATTRS} fill="none"><rect x="34" y="40" width="66" height="14" fill="#fff" stroke="#1f2328"/><rect x="60" y="33" width="14" height="14" fill="#fff" stroke="#1f2328"/></svg>`, pipeline()),
|
|
231
|
-
tpl('Market', `<svg ${ATTRS} fill="none"><circle cx="67" cy="40" r="16" fill="#fff" stroke="#1f2328"/><circle cx="67" cy="30" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="75" cy="46" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><circle cx="59" cy="46" r="3.5" fill="#fff" stroke="#1f2328" stroke-width="1.5"/><path d="M67 30 L75 46 L59 46 Z" stroke="#1f2328" stroke-width="0.8" fill="none"/></svg>`, market()),
|
|
232
|
-
tpl('Inertia', `<svg ${ATTRS} fill="none"><rect x="63" y="22" width="8" height="36" fill="#1f2328"/></svg>`, single(inertia())),
|
|
233
|
-
tpl('Link', `<svg ${ATTRS} fill="none"><path d="M24 40 H110" stroke="#666" stroke-width="2.4"/></svg>`, single(connect({ position: [0, 0] }, { position: [160, 0] }))),
|
|
234
|
-
tpl('Evolution arrow', `<svg ${ATTRS} fill="none"><path d="M24 40 H100" stroke="#d6455d" stroke-width="2.4" stroke-dasharray="6 4"/><path d="M98 33 L112 40 L98 47 Z" fill="#d6455d"/></svg>`, single(connect({ position: [0, 0] }, { position: [160, 0] }, { red: true }))),
|
|
235
|
-
],
|
|
236
|
-
};
|
package/src/templates/maps.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
makeTemplateSnapshot,
|
|
3
|
-
type SurfaceElementsJSON,
|
|
4
|
-
surfaceText,
|
|
5
|
-
type Template,
|
|
6
|
-
} from '@formicoidea/labre-core/gfx/template';
|
|
7
|
-
import {
|
|
8
|
-
ConnectorMode,
|
|
9
|
-
FontFamily,
|
|
10
|
-
PointStyle,
|
|
11
|
-
ShapeStyle,
|
|
12
|
-
StrokeStyle,
|
|
13
|
-
TextAlign,
|
|
14
|
-
} from '@formicoidea/labre-core/model';
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
INERTIA_COLOR,
|
|
18
|
-
LABEL_FONT_SIZE,
|
|
19
|
-
LINK_GREY,
|
|
20
|
-
LINK_STROKE_WIDTH,
|
|
21
|
-
NODE_FILL,
|
|
22
|
-
NODE_SIZE,
|
|
23
|
-
NODE_STROKE,
|
|
24
|
-
WARDLEY_RED,
|
|
25
|
-
} from '../node/consts';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Authoring kit for canonical Wardley maps. Positions are given as
|
|
29
|
-
* (evolution 0..1, value 0..1) and mapped into the plot interior of a fixed
|
|
30
|
-
* 1600x900 background. Per the composition principle, every glyph reuses an
|
|
31
|
-
* existing shape: stakeholders/users are anchor (person) nodes, "needs" are
|
|
32
|
-
* thick-stroked component nodes, capabilities are component nodes, notes are
|
|
33
|
-
* native rects + text, inertia is the inertia bar, and the future / evolution
|
|
34
|
-
* arrow is the red dashed connector. Legends are produced by the editor's
|
|
35
|
-
* auto-legend action rather than baked into the template.
|
|
36
|
-
*/
|
|
37
|
-
const W = 1600;
|
|
38
|
-
const H = 900;
|
|
39
|
-
const PL = { x: 70, y: 56, w: 1470, h: 786 };
|
|
40
|
-
const ex = (e: number) => PL.x + e * PL.w;
|
|
41
|
-
const vy = (v: number) => PL.y + (1 - v) * PL.h;
|
|
42
|
-
const D = NODE_SIZE; // 18
|
|
43
|
-
|
|
44
|
-
const bg = (variant = 'classic') => ({ type: 'wardley', variant, xywh: `[0,0,${W},${H}]` });
|
|
45
|
-
|
|
46
|
-
function dot(e: number, v: number, sw: number, stroke = NODE_STROKE, fill = NODE_FILL) {
|
|
47
|
-
const cx = ex(e);
|
|
48
|
-
const cy = vy(v);
|
|
49
|
-
return {
|
|
50
|
-
type: 'wardleyNode',
|
|
51
|
-
kind: 'component',
|
|
52
|
-
shapeType: 'ellipse',
|
|
53
|
-
filled: true,
|
|
54
|
-
fillColor: fill,
|
|
55
|
-
strokeColor: stroke,
|
|
56
|
-
strokeWidth: sw,
|
|
57
|
-
shapeStyle: ShapeStyle.General,
|
|
58
|
-
roughness: 0,
|
|
59
|
-
xywh: `[${cx - D / 2},${cy - D / 2},${D},${D}]`,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
const comp = (e: number, v: number) => dot(e, v, 1);
|
|
63
|
-
const future = (e: number, v: number) => dot(e, v, 2, WARDLEY_RED);
|
|
64
|
-
function stake(e: number, v: number) {
|
|
65
|
-
const cx = ex(e);
|
|
66
|
-
const cy = vy(v);
|
|
67
|
-
const d = 24;
|
|
68
|
-
return {
|
|
69
|
-
type: 'wardleyNode',
|
|
70
|
-
kind: 'anchor',
|
|
71
|
-
shapeType: 'ellipse',
|
|
72
|
-
filled: true,
|
|
73
|
-
fillColor: NODE_FILL,
|
|
74
|
-
strokeColor: NODE_STROKE,
|
|
75
|
-
strokeWidth: 1,
|
|
76
|
-
shapeStyle: ShapeStyle.General,
|
|
77
|
-
roughness: 0,
|
|
78
|
-
xywh: `[${cx - d / 2},${cy - d / 2},${d},${d}]`,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
type LblOpts = { dx?: number; dy?: number; align?: 'left' | 'right' | 'center'; color?: string; size?: number; w?: number };
|
|
83
|
-
function lbl(e: number, v: number, text: string, o: LblOpts = {}) {
|
|
84
|
-
const cx = ex(e);
|
|
85
|
-
const cy = vy(v);
|
|
86
|
-
const w = o.w ?? 200;
|
|
87
|
-
const dx = o.dx ?? 12;
|
|
88
|
-
const dy = o.dy ?? -10;
|
|
89
|
-
const align = o.align ?? 'left';
|
|
90
|
-
const x = align === 'right' ? cx - w - dx : align === 'center' ? cx - w / 2 : cx + dx;
|
|
91
|
-
return {
|
|
92
|
-
type: 'text',
|
|
93
|
-
text: surfaceText(text),
|
|
94
|
-
color: o.color ?? NODE_STROKE,
|
|
95
|
-
fontFamily: FontFamily.Inter,
|
|
96
|
-
fontSize: o.size ?? LABEL_FONT_SIZE,
|
|
97
|
-
textAlign: align === 'right' ? TextAlign.Right : align === 'center' ? TextAlign.Center : TextAlign.Left,
|
|
98
|
-
xywh: `[${x},${cy + dy},${w},26]`,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function link(a: string, b: string, o: { red?: boolean; arrow?: boolean } = {}) {
|
|
103
|
-
return {
|
|
104
|
-
type: 'connector',
|
|
105
|
-
mode: ConnectorMode.Straight,
|
|
106
|
-
stroke: o.red ? WARDLEY_RED : LINK_GREY,
|
|
107
|
-
strokeStyle: o.arrow ? StrokeStyle.Dash : StrokeStyle.Solid,
|
|
108
|
-
strokeWidth: LINK_STROKE_WIDTH,
|
|
109
|
-
frontEndpointStyle: PointStyle.None,
|
|
110
|
-
rearEndpointStyle: o.arrow ? PointStyle.Triangle : PointStyle.None,
|
|
111
|
-
source: { id: a },
|
|
112
|
-
target: { id: b },
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function inertia(e: number, v: number) {
|
|
117
|
-
const cx = ex(e);
|
|
118
|
-
const cy = vy(v);
|
|
119
|
-
return {
|
|
120
|
-
type: 'shape',
|
|
121
|
-
shapeType: 'rect',
|
|
122
|
-
filled: true,
|
|
123
|
-
fillColor: INERTIA_COLOR,
|
|
124
|
-
strokeColor: INERTIA_COLOR,
|
|
125
|
-
strokeWidth: 0,
|
|
126
|
-
shapeStyle: ShapeStyle.General,
|
|
127
|
-
roughness: 0,
|
|
128
|
-
radius: 0,
|
|
129
|
-
xywh: `[${cx - 4},${cy - 22},8,44]`,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function panel(x: number, y: number, w: number, h: number) {
|
|
134
|
-
return {
|
|
135
|
-
type: 'shape',
|
|
136
|
-
shapeType: 'rect',
|
|
137
|
-
filled: true,
|
|
138
|
-
fillColor: '#ffffff',
|
|
139
|
-
strokeColor: NODE_STROKE,
|
|
140
|
-
strokeWidth: 1.2,
|
|
141
|
-
shapeStyle: ShapeStyle.General,
|
|
142
|
-
roughness: 0,
|
|
143
|
-
radius: 0,
|
|
144
|
-
xywh: `[${x},${y},${w},${h}]`,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
function freeText(x: number, y: number, w: number, str: string, size = 16, color = NODE_STROKE) {
|
|
148
|
-
return {
|
|
149
|
-
type: 'text',
|
|
150
|
-
text: surfaceText(str),
|
|
151
|
-
color,
|
|
152
|
-
fontFamily: FontFamily.Inter,
|
|
153
|
-
fontSize: size,
|
|
154
|
-
textAlign: TextAlign.Left,
|
|
155
|
-
xywh: `[${x},${y},${w},26]`,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
/** Centred, enlarged map title spanning the plot width. */
|
|
159
|
-
function title(str: string) {
|
|
160
|
-
return {
|
|
161
|
-
type: 'text',
|
|
162
|
-
text: surfaceText(str),
|
|
163
|
-
color: NODE_STROKE,
|
|
164
|
-
fontFamily: FontFamily.Inter,
|
|
165
|
-
fontSize: 28,
|
|
166
|
-
textAlign: TextAlign.Center,
|
|
167
|
-
xywh: `[${W / 2 - 500},12,1000,40]`,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
|
|
172
|
-
const mapPreview = (extra: string) =>
|
|
173
|
-
`<svg ${ATTRS} fill="none"><path d="M22 12 V64 H120" stroke="#3b3d42" stroke-width="2"/>${extra}</svg>`;
|
|
174
|
-
|
|
175
|
-
function tpl(name: string, preview: string, elements: SurfaceElementsJSON): Template {
|
|
176
|
-
return { name, type: 'template', preview, content: makeTemplateSnapshot(elements, name) };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function ann(e: number, v: number) {
|
|
180
|
-
const cx = ex(e);
|
|
181
|
-
const cy = vy(v);
|
|
182
|
-
return {
|
|
183
|
-
type: 'shape',
|
|
184
|
-
shapeType: 'ellipse',
|
|
185
|
-
filled: true,
|
|
186
|
-
fillColor: '#ffffff',
|
|
187
|
-
strokeColor: NODE_STROKE,
|
|
188
|
-
strokeWidth: 1.5,
|
|
189
|
-
shapeStyle: ShapeStyle.General,
|
|
190
|
-
roughness: 0,
|
|
191
|
-
xywh: `[${cx - 14},${cy - 14},28,28]`,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
function annTxt(e: number, v: number, n: string) {
|
|
195
|
-
const cx = ex(e);
|
|
196
|
-
const cy = vy(v);
|
|
197
|
-
return {
|
|
198
|
-
type: 'text',
|
|
199
|
-
text: surfaceText(n),
|
|
200
|
-
color: NODE_STROKE,
|
|
201
|
-
fontFamily: FontFamily.Inter,
|
|
202
|
-
fontSize: 14,
|
|
203
|
-
textAlign: TextAlign.Center,
|
|
204
|
-
xywh: `[${cx - 14},${cy - 9},28,20]`,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ── Tea Shop (the canonical map) ──────────────────────────────────────
|
|
209
|
-
function teaShop(): SurfaceElementsJSON {
|
|
210
|
-
return {
|
|
211
|
-
bg: bg(),
|
|
212
|
-
title: title('Tea Shop'),
|
|
213
|
-
annBox: panel(120, 200, 420, 64),
|
|
214
|
-
annText: freeText(132, 208, 400, 'Annotations:\n1. Standardising power lets kettles evolve faster\n2. Hot water is obvious and well known', 13),
|
|
215
|
-
business: stake(0.62, 0.93),
|
|
216
|
-
businessL: lbl(0.62, 0.93, 'Business', { align: 'center', dy: -28, w: 120 }),
|
|
217
|
-
public: stake(0.78, 0.93),
|
|
218
|
-
publicL: lbl(0.78, 0.93, 'Public', { align: 'center', dy: -28, w: 120 }),
|
|
219
|
-
cupOfTea: comp(0.62, 0.74),
|
|
220
|
-
cupOfTeaL: lbl(0.62, 0.74, 'Cup of Tea', { align: 'right' }),
|
|
221
|
-
cup: comp(0.8, 0.7),
|
|
222
|
-
cupL: lbl(0.8, 0.7, 'Cup'),
|
|
223
|
-
tea: comp(0.83, 0.6),
|
|
224
|
-
teaL: lbl(0.83, 0.6, 'Tea'),
|
|
225
|
-
hotWater: comp(0.8, 0.47),
|
|
226
|
-
hotWaterL: lbl(0.8, 0.47, 'Hot Water'),
|
|
227
|
-
water: comp(0.81, 0.34),
|
|
228
|
-
waterL: lbl(0.81, 0.34, 'Water'),
|
|
229
|
-
kettle: comp(0.36, 0.38),
|
|
230
|
-
kettleL: lbl(0.36, 0.38, 'Kettle', { align: 'right', dy: 6 }),
|
|
231
|
-
electric: future(0.56, 0.38),
|
|
232
|
-
electricL: lbl(0.56, 0.38, 'Electric Kettle'),
|
|
233
|
-
power: comp(0.7, 0.1),
|
|
234
|
-
powerL: lbl(0.7, 0.1, 'Power', { align: 'right', dy: 6 }),
|
|
235
|
-
powerFut: future(0.88, 0.1),
|
|
236
|
-
powerFutL: lbl(0.88, 0.1, 'Power'),
|
|
237
|
-
limitedBy: lbl(0.56, 0.43, 'limited by', { align: 'center', w: 120, size: 13 }),
|
|
238
|
-
ann1a: ann(0.5, 0.385), ann1t: annTxt(0.5, 0.385, '1'),
|
|
239
|
-
ann2a: ann(0.84, 0.45), ann2t: annTxt(0.84, 0.45, '2'),
|
|
240
|
-
l1: link('business', 'cupOfTea'),
|
|
241
|
-
l2: link('public', 'cupOfTea'),
|
|
242
|
-
l3: link('cupOfTea', 'cup'),
|
|
243
|
-
l4: link('cupOfTea', 'tea'),
|
|
244
|
-
l5: link('cupOfTea', 'hotWater'),
|
|
245
|
-
l6: link('hotWater', 'water'),
|
|
246
|
-
l7: link('hotWater', 'kettle'),
|
|
247
|
-
l8: link('kettle', 'power'),
|
|
248
|
-
a1: link('kettle', 'electric', { red: true, arrow: true }),
|
|
249
|
-
a2: link('power', 'powerFut', { red: true, arrow: true }),
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ── Kodak inertia (2005) ──────────────────────────────────────────────
|
|
254
|
-
function kodak(): SurfaceElementsJSON {
|
|
255
|
-
return {
|
|
256
|
-
bg: bg(),
|
|
257
|
-
title: title("Wardley map of Kodak's 2005 inertia to digital"),
|
|
258
|
-
user: stake(0.54, 0.92),
|
|
259
|
-
userL: lbl(0.54, 0.92, 'User'),
|
|
260
|
-
capture: dot(0.53, 0.8, 3),
|
|
261
|
-
captureL: lbl(0.53, 0.8, 'Capture a moment'),
|
|
262
|
-
film: comp(0.52, 0.62),
|
|
263
|
-
filmL: lbl(0.52, 0.62, 'Film camera', { align: 'right' }),
|
|
264
|
-
digital: future(0.74, 0.62),
|
|
265
|
-
digitalL: lbl(0.74, 0.62, 'Digital camera', { color: WARDLEY_RED }),
|
|
266
|
-
roll: comp(0.52, 0.4),
|
|
267
|
-
rollL: lbl(0.52, 0.4, 'Photographic film', { align: 'right' }),
|
|
268
|
-
storage: future(0.84, 0.4),
|
|
269
|
-
storageL: lbl(0.84, 0.4, 'Digital storage', { color: WARDLEY_RED }),
|
|
270
|
-
inertiaBar: inertia(0.78, 0.4),
|
|
271
|
-
l1: link('user', 'capture'),
|
|
272
|
-
l2: link('capture', 'film'),
|
|
273
|
-
l3: link('film', 'roll'),
|
|
274
|
-
r1: link('capture', 'storage', { red: true }),
|
|
275
|
-
a1: link('film', 'digital', { red: true, arrow: true }),
|
|
276
|
-
a2: link('roll', 'storage', { red: true, arrow: true }),
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export const wardleyMaps: Template[] = [
|
|
281
|
-
tpl('Tea Shop', mapPreview('<circle cx="78" cy="24" r="3" fill="#fff" stroke="#1f2328"/><circle cx="50" cy="44" r="3" fill="#fff" stroke="#1f2328"/><circle cx="86" cy="40" r="3" fill="#fff" stroke="#1f2328"/><circle cx="92" cy="58" r="3" fill="#fff" stroke="#1f2328"/><path d="M78 24 L50 44 M78 24 L86 40 L92 58" stroke="#666"/><path d="M50 44 h22" stroke="#d6455d" stroke-dasharray="3 2"/>'), teaShop()),
|
|
282
|
-
tpl('Kodak inertia', mapPreview('<circle cx="56" cy="22" r="3" fill="#fff" stroke="#1f2328"/><circle cx="54" cy="40" r="3" fill="#fff" stroke="#1f2328"/><circle cx="86" cy="40" r="3" fill="#fff" stroke="#d6455d"/><rect x="76" y="35" width="2.5" height="11" fill="#1f2328"/><path d="M57 40 h17" stroke="#d6455d" stroke-dasharray="3 2"/>'), kodak()),
|
|
283
|
-
];
|