@kubohiroya/turbowarp-svg-text 0.2.0 → 0.3.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/README.md +29 -3
- package/dist/composition.js +918 -0
- package/dist/svg-text.js +25 -14
- package/dist/types/composition.d.ts +39 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/extension.d.ts +80 -0
- package/package.json +33 -17
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ See the [English guide](https://kubohiroya.github.io/turbowarp-svg-text/) or [Ja
|
|
|
15
15
|
- Immediately update visible bubbles and SVG text when a style is redefined or the stage size changes.
|
|
16
16
|
- Keep legacy size-based say/think opcodes hidden but executable for saved-project compatibility.
|
|
17
17
|
|
|
18
|
-
Animation is not included in
|
|
18
|
+
Animation is not included in this release.
|
|
19
19
|
|
|
20
20
|
## Blocks
|
|
21
21
|
|
|
@@ -42,7 +42,7 @@ Defines or replaces a named style. `ALIGN` accepts `left`, `center`, or `right`.
|
|
|
42
42
|
| `up-left` | `northwest` |
|
|
43
43
|
| `up-up-left` | `north-northwest` |
|
|
44
44
|
|
|
45
|
-
Direction input is trimmed and case-insensitive.
|
|
45
|
+
Direction input is trimmed and case-insensitive. Direction is measured from the actor center to the bubble body center. Changing the bubble width or height does not change the requested angle, although stage-edge clamping can move the bubble when necessary.
|
|
46
46
|
|
|
47
47
|
`DIRECTION` also accepts any number from `0` through `360` and uses Scratch sprite direction semantics: `0` is up, `90` is right, `180` is down, `270` is left, and `360` is up again. Values between the named directions are placed continuously without rounding to the nearest of the sixteen menu positions. Values outside this range fall back to the initial `up-right` direction.
|
|
48
48
|
|
|
@@ -82,7 +82,7 @@ Style sizes are clamped to 1–1000. A blank or non-finite value uses 100.
|
|
|
82
82
|
Load the following URL as a TurboWarp custom extension and allow it to run unsandboxed:
|
|
83
83
|
|
|
84
84
|
```text
|
|
85
|
-
https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-svg-text@0.
|
|
85
|
+
https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-svg-text@0.3.0/dist/svg-text.js
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
```text
|
|
@@ -91,6 +91,32 @@ say [Once upon a time...\nIn a village by the sea...] with style [narration]
|
|
|
91
91
|
set this sprite text [Act One\nUrashima Taro] with style [narration]
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
### Composition API
|
|
95
|
+
|
|
96
|
+
Composite runtimes can import `@kubohiroya/turbowarp-svg-text/composition` without registering the
|
|
97
|
+
Standalone extension or adding blocks. The caller supplies one TurboWarp runtime and explicit
|
|
98
|
+
targets.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import { createSvgTextComposition } from "@kubohiroya/turbowarp-svg-text/composition";
|
|
102
|
+
|
|
103
|
+
const svgText = createSvgTextComposition({ runtime: Scratch.vm.runtime });
|
|
104
|
+
svgText.defineStyle({
|
|
105
|
+
name: "title",
|
|
106
|
+
alignment: "center",
|
|
107
|
+
backgroundColor: "#112233",
|
|
108
|
+
direction: "up",
|
|
109
|
+
font: "Noto Sans JP",
|
|
110
|
+
fontPercent: 150,
|
|
111
|
+
textColor: "#ffffff",
|
|
112
|
+
});
|
|
113
|
+
svgText.setText({ target, text: "The End", styleName: "title" });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Each composition owns its styles and the SVG skins it applies. Use `releaseTarget(target)` when a
|
|
117
|
+
target leaves the composition, or call the idempotent finalizer `releaseAll()` to destroy every
|
|
118
|
+
owned skin. Mutating methods reject calls after `releaseAll()`.
|
|
119
|
+
|
|
94
120
|
## Development
|
|
95
121
|
|
|
96
122
|
```bash
|
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
var block_definitions_default = {
|
|
2
|
+
extensionName: "SVG Text",
|
|
3
|
+
blocks: [
|
|
4
|
+
{
|
|
5
|
+
"opcode": "defineStyle",
|
|
6
|
+
"blockType": "COMMAND",
|
|
7
|
+
"text": "define text style [STYLE] background [BACKGROUND] text [TEXT_COLOR] font [FONT] size [SIZE] align [ALIGN] bubble direction [DIRECTION]",
|
|
8
|
+
"description": "Defines or replaces a named responsive text style. Bubble direction is measured from the actor center to the bubble body center and accepts names, compass aliases, or Scratch-style degrees from 0 to 360.",
|
|
9
|
+
"arguments": {
|
|
10
|
+
"STYLE": {
|
|
11
|
+
"type": "STRING",
|
|
12
|
+
"defaultValue": "default"
|
|
13
|
+
},
|
|
14
|
+
"BACKGROUND": {
|
|
15
|
+
"type": "COLOR",
|
|
16
|
+
"defaultValue": "#ffffff"
|
|
17
|
+
},
|
|
18
|
+
"TEXT_COLOR": {
|
|
19
|
+
"type": "COLOR",
|
|
20
|
+
"defaultValue": "#575e75"
|
|
21
|
+
},
|
|
22
|
+
"FONT": {
|
|
23
|
+
"type": "STRING",
|
|
24
|
+
"defaultValue": "Helvetica"
|
|
25
|
+
},
|
|
26
|
+
"SIZE": {
|
|
27
|
+
"type": "NUMBER",
|
|
28
|
+
"defaultValue": 100
|
|
29
|
+
},
|
|
30
|
+
"ALIGN": {
|
|
31
|
+
"type": "STRING",
|
|
32
|
+
"defaultValue": "left",
|
|
33
|
+
"menu": "alignment"
|
|
34
|
+
},
|
|
35
|
+
"DIRECTION": {
|
|
36
|
+
"type": "STRING",
|
|
37
|
+
"defaultValue": "up-right",
|
|
38
|
+
"menu": "direction"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"opcode": "setText",
|
|
44
|
+
"blockType": "COMMAND",
|
|
45
|
+
"text": "set this sprite text [TEXT] with style [STYLE]",
|
|
46
|
+
"description": "Replaces this sprite's skin with responsive styled SVG text.",
|
|
47
|
+
"arguments": {
|
|
48
|
+
"TEXT": {
|
|
49
|
+
"type": "STRING",
|
|
50
|
+
"defaultValue": "Title\\nSubtitle"
|
|
51
|
+
},
|
|
52
|
+
"STYLE": {
|
|
53
|
+
"type": "STRING",
|
|
54
|
+
"defaultValue": "default"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"opcode": "sayWithStyle",
|
|
60
|
+
"blockType": "COMMAND",
|
|
61
|
+
"text": "say [MESSAGE] with style [STYLE]",
|
|
62
|
+
"description": "Shows a responsive say bubble using a named style.",
|
|
63
|
+
"arguments": {
|
|
64
|
+
"MESSAGE": {
|
|
65
|
+
"type": "STRING",
|
|
66
|
+
"defaultValue": "Hello!\\nHow are you?"
|
|
67
|
+
},
|
|
68
|
+
"STYLE": {
|
|
69
|
+
"type": "STRING",
|
|
70
|
+
"defaultValue": "default"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"opcode": "thinkWithStyle",
|
|
76
|
+
"blockType": "COMMAND",
|
|
77
|
+
"text": "think [MESSAGE] with style [STYLE]",
|
|
78
|
+
"description": "Shows a responsive think bubble using a named style.",
|
|
79
|
+
"arguments": {
|
|
80
|
+
"MESSAGE": {
|
|
81
|
+
"type": "STRING",
|
|
82
|
+
"defaultValue": "Hmm...\\nI wonder."
|
|
83
|
+
},
|
|
84
|
+
"STYLE": {
|
|
85
|
+
"type": "STRING",
|
|
86
|
+
"defaultValue": "default"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"opcode": "say",
|
|
92
|
+
"blockType": "COMMAND",
|
|
93
|
+
"text": "say [MESSAGE] with font size [SIZE]",
|
|
94
|
+
"description": "Legacy size-based say bubble kept for saved-project compatibility.",
|
|
95
|
+
"hideFromPalette": true,
|
|
96
|
+
"arguments": {
|
|
97
|
+
"MESSAGE": {
|
|
98
|
+
"type": "STRING",
|
|
99
|
+
"defaultValue": "Hello!\\nHow are you?"
|
|
100
|
+
},
|
|
101
|
+
"SIZE": {
|
|
102
|
+
"type": "NUMBER",
|
|
103
|
+
"defaultValue": 100
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"opcode": "think",
|
|
109
|
+
"blockType": "COMMAND",
|
|
110
|
+
"text": "think [MESSAGE] with font size [SIZE]",
|
|
111
|
+
"description": "Legacy size-based think bubble kept for saved-project compatibility.",
|
|
112
|
+
"hideFromPalette": true,
|
|
113
|
+
"arguments": {
|
|
114
|
+
"MESSAGE": {
|
|
115
|
+
"type": "STRING",
|
|
116
|
+
"defaultValue": "Hmm...\\nI wonder."
|
|
117
|
+
},
|
|
118
|
+
"SIZE": {
|
|
119
|
+
"type": "NUMBER",
|
|
120
|
+
"defaultValue": 100
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
menus: {
|
|
126
|
+
"alignment": {
|
|
127
|
+
"acceptReporters": true,
|
|
128
|
+
"items": [
|
|
129
|
+
"left",
|
|
130
|
+
"center",
|
|
131
|
+
"right"
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
"direction": {
|
|
135
|
+
"acceptReporters": true,
|
|
136
|
+
"items": [
|
|
137
|
+
"up",
|
|
138
|
+
"up-up-right",
|
|
139
|
+
"up-right",
|
|
140
|
+
"right-up-right",
|
|
141
|
+
"right",
|
|
142
|
+
"right-down-right",
|
|
143
|
+
"down-right",
|
|
144
|
+
"down-down-right",
|
|
145
|
+
"down",
|
|
146
|
+
"down-down-left",
|
|
147
|
+
"down-left",
|
|
148
|
+
"left-down-left",
|
|
149
|
+
"left",
|
|
150
|
+
"left-up-left",
|
|
151
|
+
"up-left",
|
|
152
|
+
"up-up-left"
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/config.ts
|
|
159
|
+
var extensionConfig = {
|
|
160
|
+
id: "kubohiroyasvgtext",
|
|
161
|
+
slug: "svg-text",
|
|
162
|
+
name: "SVG Text",
|
|
163
|
+
description: "Responsive named-style bubbles and SVG text actors for TurboWarp.",
|
|
164
|
+
author: "Hiroya Kubo",
|
|
165
|
+
license: "MPL-2.0",
|
|
166
|
+
unsandboxed: true
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/extension.ts
|
|
170
|
+
var bubbleDirections = [
|
|
171
|
+
"up",
|
|
172
|
+
"up-up-right",
|
|
173
|
+
"up-right",
|
|
174
|
+
"right-up-right",
|
|
175
|
+
"right",
|
|
176
|
+
"right-down-right",
|
|
177
|
+
"down-right",
|
|
178
|
+
"down-down-right",
|
|
179
|
+
"down",
|
|
180
|
+
"down-down-left",
|
|
181
|
+
"down-left",
|
|
182
|
+
"left-down-left",
|
|
183
|
+
"left",
|
|
184
|
+
"left-up-left",
|
|
185
|
+
"up-left",
|
|
186
|
+
"up-up-left"
|
|
187
|
+
];
|
|
188
|
+
var bubbleDirectionAliases = /* @__PURE__ */ new Map([
|
|
189
|
+
["east", "right"],
|
|
190
|
+
["east-northeast", "right-up-right"],
|
|
191
|
+
["east-southeast", "right-down-right"],
|
|
192
|
+
["north", "up"],
|
|
193
|
+
["northeast", "up-right"],
|
|
194
|
+
["north-northeast", "up-up-right"],
|
|
195
|
+
["northwest", "up-left"],
|
|
196
|
+
["north-northwest", "up-up-left"],
|
|
197
|
+
["south", "down"],
|
|
198
|
+
["southeast", "down-right"],
|
|
199
|
+
["south-southeast", "down-down-right"],
|
|
200
|
+
["southwest", "down-left"],
|
|
201
|
+
["south-southwest", "down-down-left"],
|
|
202
|
+
["west", "left"],
|
|
203
|
+
["west-northwest", "left-up-left"],
|
|
204
|
+
["west-southwest", "left-down-left"]
|
|
205
|
+
]);
|
|
206
|
+
var intermediateDirectionOffset = Math.SQRT2 - 1;
|
|
207
|
+
var bubbleDirectionVectors = {
|
|
208
|
+
down: {
|
|
209
|
+
x: 0,
|
|
210
|
+
y: -1
|
|
211
|
+
},
|
|
212
|
+
"down-down-left": {
|
|
213
|
+
x: -intermediateDirectionOffset,
|
|
214
|
+
y: -1
|
|
215
|
+
},
|
|
216
|
+
"down-down-right": {
|
|
217
|
+
x: intermediateDirectionOffset,
|
|
218
|
+
y: -1
|
|
219
|
+
},
|
|
220
|
+
"down-left": {
|
|
221
|
+
x: -1,
|
|
222
|
+
y: -1
|
|
223
|
+
},
|
|
224
|
+
"down-right": {
|
|
225
|
+
x: 1,
|
|
226
|
+
y: -1
|
|
227
|
+
},
|
|
228
|
+
left: {
|
|
229
|
+
x: -1,
|
|
230
|
+
y: 0
|
|
231
|
+
},
|
|
232
|
+
"left-down-left": {
|
|
233
|
+
x: -1,
|
|
234
|
+
y: -intermediateDirectionOffset
|
|
235
|
+
},
|
|
236
|
+
"left-up-left": {
|
|
237
|
+
x: -1,
|
|
238
|
+
y: intermediateDirectionOffset
|
|
239
|
+
},
|
|
240
|
+
right: {
|
|
241
|
+
x: 1,
|
|
242
|
+
y: 0
|
|
243
|
+
},
|
|
244
|
+
"right-down-right": {
|
|
245
|
+
x: 1,
|
|
246
|
+
y: -intermediateDirectionOffset
|
|
247
|
+
},
|
|
248
|
+
"right-up-right": {
|
|
249
|
+
x: 1,
|
|
250
|
+
y: intermediateDirectionOffset
|
|
251
|
+
},
|
|
252
|
+
up: {
|
|
253
|
+
x: 0,
|
|
254
|
+
y: 1
|
|
255
|
+
},
|
|
256
|
+
"up-left": {
|
|
257
|
+
x: -1,
|
|
258
|
+
y: 1
|
|
259
|
+
},
|
|
260
|
+
"up-right": {
|
|
261
|
+
x: 1,
|
|
262
|
+
y: 1
|
|
263
|
+
},
|
|
264
|
+
"up-up-left": {
|
|
265
|
+
x: -intermediateDirectionOffset,
|
|
266
|
+
y: 1
|
|
267
|
+
},
|
|
268
|
+
"up-up-right": {
|
|
269
|
+
x: intermediateDirectionOffset,
|
|
270
|
+
y: 1
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
function normalizeVectorComponent(value) {
|
|
274
|
+
if (Math.abs(value) < 1e-12) return 0;
|
|
275
|
+
if (Math.abs(1 - Math.abs(value)) < 1e-12) return Math.sign(value);
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
278
|
+
function directionVector(direction) {
|
|
279
|
+
if (typeof direction === "string") return bubbleDirectionVectors[direction];
|
|
280
|
+
const radians = direction * Math.PI / 180;
|
|
281
|
+
const rawX = Math.sin(radians);
|
|
282
|
+
const rawY = Math.cos(radians);
|
|
283
|
+
return {
|
|
284
|
+
x: normalizeVectorComponent(rawX),
|
|
285
|
+
y: normalizeVectorComponent(rawY)
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
var blockDefinitions = block_definitions_default.blocks;
|
|
289
|
+
var definitionMenus = block_definitions_default.menus;
|
|
290
|
+
var EXTENSION_DOCS_URI = "https://kubohiroya.github.io/turbowarp-svg-text/";
|
|
291
|
+
var bubbleStateKey = "Scratch.looks";
|
|
292
|
+
var defaultStyleName$1 = "default";
|
|
293
|
+
var defaultFontPercent = 100;
|
|
294
|
+
var minimumFontPercent = 1;
|
|
295
|
+
var maximumFontPercent = 1e3;
|
|
296
|
+
var maximumFontNameLength = 128;
|
|
297
|
+
var baseStageWidth = 480;
|
|
298
|
+
var baseStageHeight = 360;
|
|
299
|
+
var baseStyle = {
|
|
300
|
+
maxLineWidth: 170,
|
|
301
|
+
minWidth: 50,
|
|
302
|
+
strokeWidth: 4,
|
|
303
|
+
padding: 10,
|
|
304
|
+
cornerRadius: 16,
|
|
305
|
+
tailHeight: 12,
|
|
306
|
+
fontSize: 14,
|
|
307
|
+
fontHeightRatio: .9,
|
|
308
|
+
lineHeight: 16
|
|
309
|
+
};
|
|
310
|
+
var actorStyle = {
|
|
311
|
+
minimumWidth: 1,
|
|
312
|
+
padding: 12,
|
|
313
|
+
cornerRadius: 8
|
|
314
|
+
};
|
|
315
|
+
var initialDefaultStyle = {
|
|
316
|
+
alignment: "left",
|
|
317
|
+
backgroundColor: "#ffffff",
|
|
318
|
+
direction: "up-right",
|
|
319
|
+
font: "Helvetica",
|
|
320
|
+
fontPercent: defaultFontPercent,
|
|
321
|
+
textColor: "#575e75"
|
|
322
|
+
};
|
|
323
|
+
var SvgTextExtension = class {
|
|
324
|
+
constructor(runtime = Scratch.vm?.runtime, options = {}) {
|
|
325
|
+
this.styles = /* @__PURE__ */ new Map([[defaultStyleName$1, initialDefaultStyle]]);
|
|
326
|
+
this.activeStyles = /* @__PURE__ */ new WeakMap();
|
|
327
|
+
this.textActors = /* @__PURE__ */ new Map();
|
|
328
|
+
this.pendingStyles = /* @__PURE__ */ new WeakMap();
|
|
329
|
+
this.alignedSkins = /* @__PURE__ */ new WeakSet();
|
|
330
|
+
this.targetPositionHooks = /* @__PURE__ */ new WeakMap();
|
|
331
|
+
if (!runtime) throw new Error("SVG Text requires the TurboWarp VM.");
|
|
332
|
+
this.runtime = runtime;
|
|
333
|
+
this.castToString = options.castToString ?? Scratch.Cast.toString;
|
|
334
|
+
this.handleSayOrThink = this.handleSayOrThink.bind(this);
|
|
335
|
+
this.handleStageSizeChanged = this.handleStageSizeChanged.bind(this);
|
|
336
|
+
if (options.listenForRuntimeEvents ?? true) {
|
|
337
|
+
this.runtime.on("SAY", this.handleSayOrThink);
|
|
338
|
+
this.runtime.on("STAGE_SIZE_CHANGED", this.handleStageSizeChanged);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
getInfo() {
|
|
342
|
+
return {
|
|
343
|
+
id: extensionConfig.id,
|
|
344
|
+
name: Scratch.translate(block_definitions_default.extensionName),
|
|
345
|
+
docsURI: EXTENSION_DOCS_URI,
|
|
346
|
+
color1: "#9966ff",
|
|
347
|
+
blocks: blockDefinitions.map((block) => this.toScratchBlock(block)),
|
|
348
|
+
menus: definitionMenus
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
defineStyle(args) {
|
|
352
|
+
const styleName = this.normalizeStyleName(args.STYLE);
|
|
353
|
+
const definition = {
|
|
354
|
+
alignment: this.normalizeAlignment(args.ALIGN),
|
|
355
|
+
backgroundColor: this.normalizeColor(args.BACKGROUND, initialDefaultStyle.backgroundColor),
|
|
356
|
+
direction: this.normalizeDirection(args.DIRECTION),
|
|
357
|
+
font: this.normalizeFont(args.FONT),
|
|
358
|
+
fontPercent: this.normalizeFontPercent(args.SIZE),
|
|
359
|
+
textColor: this.normalizeColor(args.TEXT_COLOR, initialDefaultStyle.textColor)
|
|
360
|
+
};
|
|
361
|
+
this.styles.set(styleName, definition);
|
|
362
|
+
this.restyleVisibleBubbles(styleName, definition);
|
|
363
|
+
this.restyleTextActors(styleName);
|
|
364
|
+
}
|
|
365
|
+
setText(args, util) {
|
|
366
|
+
const selection = this.resolveStyle(args.STYLE);
|
|
367
|
+
this.applyTextActor(util.target, this.normalizeMessage(args.TEXT), selection);
|
|
368
|
+
}
|
|
369
|
+
releaseTextActor(target) {
|
|
370
|
+
const state = this.textActors.get(target);
|
|
371
|
+
if (!state) return false;
|
|
372
|
+
this.textActors.delete(target);
|
|
373
|
+
this.runtime.renderer?.destroySkin?.(state.skinId);
|
|
374
|
+
this.runtime.requestRedraw?.();
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
sayWithStyle(args, util) {
|
|
378
|
+
this.showStyledBubble("say", args, util);
|
|
379
|
+
}
|
|
380
|
+
thinkWithStyle(args, util) {
|
|
381
|
+
this.showStyledBubble("think", args, util);
|
|
382
|
+
}
|
|
383
|
+
say(args, util) {
|
|
384
|
+
this.showLegacyBubble("say", args, util);
|
|
385
|
+
}
|
|
386
|
+
think(args, util) {
|
|
387
|
+
this.showLegacyBubble("think", args, util);
|
|
388
|
+
}
|
|
389
|
+
toScratchBlock(block) {
|
|
390
|
+
return {
|
|
391
|
+
opcode: block.opcode,
|
|
392
|
+
blockType: Scratch.BlockType[block.blockType],
|
|
393
|
+
text: Scratch.translate(block.text),
|
|
394
|
+
hideFromPalette: block.hideFromPalette ?? false,
|
|
395
|
+
arguments: Object.fromEntries(Object.entries(block.arguments).map(([name, argument]) => [name, {
|
|
396
|
+
type: Scratch.ArgumentType[argument.type],
|
|
397
|
+
defaultValue: argument.defaultValue,
|
|
398
|
+
...argument.menu === void 0 ? {} : { menu: argument.menu }
|
|
399
|
+
}]))
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
normalizeStyleName(value) {
|
|
403
|
+
return this.castToString(value).trim() || defaultStyleName$1;
|
|
404
|
+
}
|
|
405
|
+
normalizeFontPercent(value) {
|
|
406
|
+
if (typeof value === "string" && value.trim() === "") return defaultFontPercent;
|
|
407
|
+
const numericValue = Number(value);
|
|
408
|
+
if (!Number.isFinite(numericValue)) return defaultFontPercent;
|
|
409
|
+
return Math.min(maximumFontPercent, Math.max(minimumFontPercent, numericValue));
|
|
410
|
+
}
|
|
411
|
+
normalizeMessage(value) {
|
|
412
|
+
return this.castToString(value).replace(/\\r\\n|\\n|\\r/gu, "\n");
|
|
413
|
+
}
|
|
414
|
+
normalizeAlignment(value) {
|
|
415
|
+
const alignment = this.castToString(value).trim().toLowerCase();
|
|
416
|
+
if (alignment === "center" || alignment === "right") return alignment;
|
|
417
|
+
return "left";
|
|
418
|
+
}
|
|
419
|
+
normalizeDirection(value) {
|
|
420
|
+
const direction = this.castToString(value).trim().toLowerCase();
|
|
421
|
+
if (bubbleDirections.includes(direction)) return direction;
|
|
422
|
+
const alias = bubbleDirectionAliases.get(direction);
|
|
423
|
+
if (alias) return alias;
|
|
424
|
+
if (direction !== "") {
|
|
425
|
+
const degrees = Number(direction);
|
|
426
|
+
if (Number.isFinite(degrees) && degrees >= 0 && degrees <= 360) return degrees === 360 ? 0 : degrees;
|
|
427
|
+
}
|
|
428
|
+
return initialDefaultStyle.direction;
|
|
429
|
+
}
|
|
430
|
+
normalizeColor(value, fallback) {
|
|
431
|
+
const color = this.castToString(value).trim();
|
|
432
|
+
if (color === "") return fallback;
|
|
433
|
+
if (/^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/iu.test(color)) return color;
|
|
434
|
+
if (globalThis.CSS?.supports?.("color", color)) return color;
|
|
435
|
+
return fallback;
|
|
436
|
+
}
|
|
437
|
+
normalizeFont(value) {
|
|
438
|
+
const font = this.castToString(value).trim();
|
|
439
|
+
const hasUnsafeCharacter = [...font].some((character) => {
|
|
440
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
441
|
+
return codePoint <= 31 || codePoint === 127 || ",;{}".includes(character);
|
|
442
|
+
});
|
|
443
|
+
if (font === "" || font.length > maximumFontNameLength || hasUnsafeCharacter) return initialDefaultStyle.font;
|
|
444
|
+
return font;
|
|
445
|
+
}
|
|
446
|
+
getStageScale() {
|
|
447
|
+
const nativeSize = this.runtime.renderer?.getNativeSize?.();
|
|
448
|
+
if (!Array.isArray(nativeSize) || nativeSize.length < 2) return 1;
|
|
449
|
+
const width = Number(nativeSize[0]);
|
|
450
|
+
const height = Number(nativeSize[1]);
|
|
451
|
+
if (!(width > 0) || !(height > 0)) return 1;
|
|
452
|
+
return Math.min(width / baseStageWidth, height / baseStageHeight);
|
|
453
|
+
}
|
|
454
|
+
createRenderStyle(definition) {
|
|
455
|
+
const stageScale = this.getStageScale();
|
|
456
|
+
const fontScale = stageScale * (definition.fontPercent / defaultFontPercent);
|
|
457
|
+
return {
|
|
458
|
+
maxLineWidth: baseStyle.maxLineWidth * stageScale,
|
|
459
|
+
minWidth: baseStyle.minWidth * stageScale,
|
|
460
|
+
strokeWidth: baseStyle.strokeWidth * stageScale,
|
|
461
|
+
padding: baseStyle.padding * stageScale,
|
|
462
|
+
cornerRadius: baseStyle.cornerRadius * stageScale,
|
|
463
|
+
tailHeight: baseStyle.tailHeight * stageScale,
|
|
464
|
+
font: definition.font,
|
|
465
|
+
fontSize: baseStyle.fontSize * fontScale,
|
|
466
|
+
fontHeightRatio: baseStyle.fontHeightRatio,
|
|
467
|
+
lineHeight: baseStyle.lineHeight * fontScale,
|
|
468
|
+
bubbleFill: definition.backgroundColor,
|
|
469
|
+
textFill: definition.textColor,
|
|
470
|
+
textAlign: definition.alignment
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
createTextActorSvg(text, definition) {
|
|
474
|
+
const stageScale = this.getStageScale();
|
|
475
|
+
const fontScale = stageScale * (definition.fontPercent / defaultFontPercent);
|
|
476
|
+
const fontSize = baseStyle.fontSize * fontScale;
|
|
477
|
+
const lineHeight = baseStyle.lineHeight * fontScale;
|
|
478
|
+
const padding = actorStyle.padding * stageScale;
|
|
479
|
+
const cornerRadius = actorStyle.cornerRadius * stageScale;
|
|
480
|
+
const lines = text.split("\n");
|
|
481
|
+
const contentWidth = Math.max(actorStyle.minimumWidth, ...lines.map((line) => this.measureTextWidth(line, fontSize)));
|
|
482
|
+
const width = Math.max(1, Math.ceil(contentWidth + padding * 2));
|
|
483
|
+
const height = Math.max(1, Math.ceil(lineHeight * lines.length + padding * 2));
|
|
484
|
+
const alignment = definition.alignment;
|
|
485
|
+
const textAnchor = alignment === "center" ? "middle" : alignment === "right" ? "end" : "start";
|
|
486
|
+
const x = alignment === "center" ? width / 2 : alignment === "right" ? width - padding : padding;
|
|
487
|
+
const title = this.escapeXml(text);
|
|
488
|
+
const tspans = lines.map((line, index) => {
|
|
489
|
+
const y = padding + fontSize + lineHeight * index;
|
|
490
|
+
return `<tspan x="${this.formatSvgNumber(x)}" y="${this.formatSvgNumber(y)}">${this.escapeXml(line)}</tspan>`;
|
|
491
|
+
}).join("");
|
|
492
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img"><title>${title}</title><rect width="${width}" height="${height}" rx="${this.formatSvgNumber(cornerRadius)}" fill="${this.escapeXml(definition.backgroundColor)}"/><text xml:space="preserve" fill="${this.escapeXml(definition.textColor)}" font-family="${this.escapeXml(definition.font)}" font-size="${this.formatSvgNumber(fontSize)}" text-anchor="${textAnchor}">${tspans}</text></svg>`;
|
|
493
|
+
}
|
|
494
|
+
measureTextWidth(text, fontSize) {
|
|
495
|
+
let units = 0;
|
|
496
|
+
for (const character of text) {
|
|
497
|
+
if (/\p{Mark}/u.test(character)) continue;
|
|
498
|
+
if (/\s/u.test(character)) {
|
|
499
|
+
units += .35;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
503
|
+
units += codePoint <= 127 ? .62 : 1;
|
|
504
|
+
}
|
|
505
|
+
return units * fontSize;
|
|
506
|
+
}
|
|
507
|
+
escapeXml(value) {
|
|
508
|
+
return value.replace(/[&<>"']/gu, (character) => {
|
|
509
|
+
switch (character) {
|
|
510
|
+
case "&": return "&";
|
|
511
|
+
case "<": return "<";
|
|
512
|
+
case ">": return ">";
|
|
513
|
+
case "\"": return """;
|
|
514
|
+
default: return "'";
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
formatSvgNumber(value) {
|
|
519
|
+
return String(Math.round(value * 1e3) / 1e3);
|
|
520
|
+
}
|
|
521
|
+
applyTextActor(target, text, selection) {
|
|
522
|
+
const renderer = this.runtime.renderer;
|
|
523
|
+
if (typeof target.drawableID !== "number" || typeof renderer?.createSVGSkin !== "function" || typeof renderer.updateDrawableSkinId !== "function") throw new Error("SVG Text requires SVG skin APIs from TurboWarp.");
|
|
524
|
+
const skinId = renderer.createSVGSkin(this.createTextActorSvg(text, selection.definition));
|
|
525
|
+
if (!Number.isInteger(skinId) || skinId < 0) throw new Error("TurboWarp did not create an SVG text skin.");
|
|
526
|
+
try {
|
|
527
|
+
renderer.updateDrawableSkinId(target.drawableID, skinId);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
renderer.destroySkin?.(skinId);
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
const previous = this.textActors.get(target);
|
|
533
|
+
this.textActors.set(target, {
|
|
534
|
+
skinId,
|
|
535
|
+
styleName: selection.styleName ?? defaultStyleName$1,
|
|
536
|
+
text
|
|
537
|
+
});
|
|
538
|
+
if (previous && previous.skinId !== skinId) renderer.destroySkin?.(previous.skinId);
|
|
539
|
+
this.runtime.requestRedraw?.();
|
|
540
|
+
}
|
|
541
|
+
resolveStyle(value) {
|
|
542
|
+
const requestedName = this.normalizeStyleName(value);
|
|
543
|
+
const definition = this.styles.get(requestedName);
|
|
544
|
+
if (definition) return {
|
|
545
|
+
definition,
|
|
546
|
+
styleName: requestedName
|
|
547
|
+
};
|
|
548
|
+
return {
|
|
549
|
+
definition: this.styles.get(defaultStyleName$1) ?? initialDefaultStyle,
|
|
550
|
+
styleName: defaultStyleName$1
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
getBubbleState(target) {
|
|
554
|
+
const state = target.getCustomState?.(bubbleStateKey);
|
|
555
|
+
if (!state || typeof state !== "object") return null;
|
|
556
|
+
return state;
|
|
557
|
+
}
|
|
558
|
+
getTextBubbleSkin(skinId) {
|
|
559
|
+
const skins = this.runtime.renderer?._allSkins;
|
|
560
|
+
if (!skins) return null;
|
|
561
|
+
return (skins instanceof Map ? skins.get(skinId) : skins[skinId]) ?? null;
|
|
562
|
+
}
|
|
563
|
+
installAlignmentRenderer(skin) {
|
|
564
|
+
if (this.alignedSkins.has(skin)) return;
|
|
565
|
+
const originalRender = skin._renderTextBubble;
|
|
566
|
+
if (typeof originalRender !== "function") return;
|
|
567
|
+
skin._renderTextBubble = function renderAlignedTextBubble(scale) {
|
|
568
|
+
const style = this._style;
|
|
569
|
+
const alignment = style?.textAlign;
|
|
570
|
+
const textFill = style?.textFill;
|
|
571
|
+
if (!style || alignment !== "center" && alignment !== "right" || typeof textFill !== "string") {
|
|
572
|
+
originalRender.call(this, scale);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
style.textFill = "transparent";
|
|
576
|
+
try {
|
|
577
|
+
originalRender.call(this, scale);
|
|
578
|
+
} finally {
|
|
579
|
+
style.textFill = textFill;
|
|
580
|
+
}
|
|
581
|
+
const context = this._canvas?.getContext("2d");
|
|
582
|
+
const lines = this._lines;
|
|
583
|
+
const width = this._textAreaSize?.width;
|
|
584
|
+
const padding = style.padding;
|
|
585
|
+
const lineHeight = style.lineHeight;
|
|
586
|
+
const fontHeightRatio = style.fontHeightRatio;
|
|
587
|
+
const fontSize = style.fontSize;
|
|
588
|
+
if (!context || !lines || typeof width !== "number" || typeof padding !== "number" || typeof lineHeight !== "number" || typeof fontHeightRatio !== "number" || typeof fontSize !== "number") return;
|
|
589
|
+
context.save();
|
|
590
|
+
context.fillStyle = textFill;
|
|
591
|
+
context.textAlign = alignment;
|
|
592
|
+
const x = alignment === "center" ? width / 2 : width - padding;
|
|
593
|
+
for (let lineNumber = 0; lineNumber < lines.length; lineNumber += 1) {
|
|
594
|
+
const line = lines[lineNumber];
|
|
595
|
+
if (line === void 0) continue;
|
|
596
|
+
context.fillText(line, x, padding + lineHeight * lineNumber + fontHeightRatio * fontSize);
|
|
597
|
+
}
|
|
598
|
+
context.restore();
|
|
599
|
+
};
|
|
600
|
+
this.alignedSkins.add(skin);
|
|
601
|
+
}
|
|
602
|
+
installTargetPositionHook(target) {
|
|
603
|
+
const currentHook = target.onTargetVisualChange;
|
|
604
|
+
if (currentHook === this.targetPositionHooks.get(target)) return;
|
|
605
|
+
const originalHook = typeof currentHook === "function" ? currentHook : void 0;
|
|
606
|
+
const positionHook = (changedTarget) => {
|
|
607
|
+
originalHook?.(changedTarget);
|
|
608
|
+
this.positionBubble(target);
|
|
609
|
+
};
|
|
610
|
+
this.targetPositionHooks.set(target, positionHook);
|
|
611
|
+
target.onTargetVisualChange = positionHook;
|
|
612
|
+
}
|
|
613
|
+
positionBubble(target) {
|
|
614
|
+
if (target.visible === false) return;
|
|
615
|
+
const bubbleState = this.getBubbleState(target);
|
|
616
|
+
const renderer = this.runtime.renderer;
|
|
617
|
+
if (!bubbleState || typeof bubbleState.drawableId !== "number" || typeof renderer?.getCurrentSkinSize !== "function" || typeof renderer.updateDrawablePosition !== "function") return;
|
|
618
|
+
const size = renderer.getCurrentSkinSize(bubbleState.drawableId);
|
|
619
|
+
if (!Array.isArray(size) || size.length < 2) return;
|
|
620
|
+
const bubbleWidth = Number(size[0]);
|
|
621
|
+
const bubbleHeight = Number(size[1]);
|
|
622
|
+
if (!(bubbleWidth > 0) || !(bubbleHeight > 0)) return;
|
|
623
|
+
let targetBounds;
|
|
624
|
+
try {
|
|
625
|
+
targetBounds = target.getBoundsForBubble?.() ?? {
|
|
626
|
+
bottom: target.y ?? 0,
|
|
627
|
+
left: target.x ?? 0,
|
|
628
|
+
right: target.x ?? 0,
|
|
629
|
+
top: target.y ?? 0
|
|
630
|
+
};
|
|
631
|
+
} catch {
|
|
632
|
+
targetBounds = {
|
|
633
|
+
bottom: target.y ?? 0,
|
|
634
|
+
left: target.x ?? 0,
|
|
635
|
+
right: target.x ?? 0,
|
|
636
|
+
top: target.y ?? 0
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
const nativeSize = renderer.getNativeSize?.();
|
|
640
|
+
if (!Array.isArray(nativeSize) || nativeSize.length < 2) return;
|
|
641
|
+
const stageWidth = Number(nativeSize[0]);
|
|
642
|
+
const stageHeight = Number(nativeSize[1]);
|
|
643
|
+
if (!(stageWidth > 0) || !(stageHeight > 0)) return;
|
|
644
|
+
const direction = this.activeStyles.get(target)?.definition.direction ?? initialDefaultStyle.direction;
|
|
645
|
+
const centerX = (targetBounds.left + targetBounds.right) / 2;
|
|
646
|
+
const centerY = (targetBounds.top + targetBounds.bottom) / 2;
|
|
647
|
+
const gap = baseStyle.tailHeight * this.getStageScale();
|
|
648
|
+
const centeredBubbleX = centerX - bubbleWidth / 2;
|
|
649
|
+
const centeredBubbleY = centerY + bubbleHeight / 2;
|
|
650
|
+
const leftBubbleX = targetBounds.left - gap - bubbleWidth;
|
|
651
|
+
const rightBubbleX = targetBounds.right + gap;
|
|
652
|
+
const upperBubbleY = targetBounds.top + gap + bubbleHeight;
|
|
653
|
+
const lowerBubbleY = targetBounds.bottom - gap;
|
|
654
|
+
const vector = directionVector(direction);
|
|
655
|
+
const horizontalDistance = vector.x < 0 ? centeredBubbleX - leftBubbleX : rightBubbleX - centeredBubbleX;
|
|
656
|
+
const verticalDistance = vector.y < 0 ? centeredBubbleY - lowerBubbleY : upperBubbleY - centeredBubbleY;
|
|
657
|
+
const placementScale = Math.min(vector.x === 0 ? Number.POSITIVE_INFINITY : horizontalDistance / Math.abs(vector.x), vector.y === 0 ? Number.POSITIVE_INFINITY : verticalDistance / Math.abs(vector.y));
|
|
658
|
+
let x = centeredBubbleX + vector.x * placementScale;
|
|
659
|
+
let y = centeredBubbleY + vector.y * placementScale;
|
|
660
|
+
if (vector.x > 0 && !bubbleState.onSpriteRight) {
|
|
661
|
+
bubbleState.onSpriteRight = true;
|
|
662
|
+
this.updateTextSkin(bubbleState);
|
|
663
|
+
} else if (vector.x < 0 && bubbleState.onSpriteRight) {
|
|
664
|
+
bubbleState.onSpriteRight = false;
|
|
665
|
+
this.updateTextSkin(bubbleState);
|
|
666
|
+
}
|
|
667
|
+
const stageLeft = -stageWidth / 2;
|
|
668
|
+
const stageRight = stageWidth / 2;
|
|
669
|
+
const stageTop = stageHeight / 2;
|
|
670
|
+
const stageBottom = -stageHeight / 2;
|
|
671
|
+
x = this.clampPosition(x, stageLeft, stageRight - bubbleWidth);
|
|
672
|
+
y = this.clampPosition(y, stageBottom + bubbleHeight, stageTop);
|
|
673
|
+
renderer.updateDrawablePosition(bubbleState.drawableId, [x, y]);
|
|
674
|
+
}
|
|
675
|
+
updateTextSkin(bubbleState) {
|
|
676
|
+
if (typeof bubbleState.skinId !== "number") return;
|
|
677
|
+
this.runtime.renderer?.updateTextSkin?.(bubbleState.skinId, bubbleState.type, bubbleState.text, bubbleState.onSpriteRight, [0, 0]);
|
|
678
|
+
}
|
|
679
|
+
clampPosition(value, minimum, maximum) {
|
|
680
|
+
if (maximum < minimum) return minimum;
|
|
681
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
682
|
+
}
|
|
683
|
+
applyBubbleStyle(target, type, text, selection) {
|
|
684
|
+
const bubbleState = this.getBubbleState(target);
|
|
685
|
+
if (!bubbleState || typeof bubbleState.skinId !== "number") return;
|
|
686
|
+
const normalizedText = this.normalizeMessage(text);
|
|
687
|
+
if (bubbleState.text !== normalizedText) {
|
|
688
|
+
bubbleState.text = normalizedText;
|
|
689
|
+
this.runtime.renderer?.updateTextSkin?.(bubbleState.skinId, type, normalizedText, bubbleState.onSpriteRight, [0, 0]);
|
|
690
|
+
}
|
|
691
|
+
const skin = this.getTextBubbleSkin(bubbleState.skinId);
|
|
692
|
+
if (typeof skin?.setStyle === "function") {
|
|
693
|
+
this.installAlignmentRenderer(skin);
|
|
694
|
+
skin.setStyle(this.createRenderStyle(selection.definition));
|
|
695
|
+
}
|
|
696
|
+
this.installTargetPositionHook(target);
|
|
697
|
+
target.onTargetVisualChange?.(target);
|
|
698
|
+
this.runtime.requestRedraw?.();
|
|
699
|
+
}
|
|
700
|
+
handleSayOrThink(target, type, text) {
|
|
701
|
+
const selection = this.pendingStyles.get(target) ?? this.resolveStyle(defaultStyleName$1);
|
|
702
|
+
this.pendingStyles.delete(target);
|
|
703
|
+
const normalizedText = this.normalizeMessage(this.getBubbleState(target)?.text ?? text);
|
|
704
|
+
if (normalizedText === "") {
|
|
705
|
+
this.activeStyles.delete(target);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
this.activeStyles.set(target, selection);
|
|
709
|
+
this.applyBubbleStyle(target, type, normalizedText, selection);
|
|
710
|
+
}
|
|
711
|
+
handleStageSizeChanged() {
|
|
712
|
+
for (const target of this.runtime.targets ?? []) {
|
|
713
|
+
const bubbleState = this.getBubbleState(target);
|
|
714
|
+
if (!bubbleState?.text) continue;
|
|
715
|
+
const selection = this.activeStyles.get(target) ?? this.resolveStyle(defaultStyleName$1);
|
|
716
|
+
this.applyBubbleStyle(target, bubbleState.type, bubbleState.text, selection);
|
|
717
|
+
}
|
|
718
|
+
for (const [target, state] of [...this.textActors]) this.applyTextActor(target, state.text, this.resolveStyle(state.styleName));
|
|
719
|
+
}
|
|
720
|
+
restyleVisibleBubbles(styleName, definition) {
|
|
721
|
+
for (const target of this.runtime.targets ?? []) {
|
|
722
|
+
const selection = this.activeStyles.get(target);
|
|
723
|
+
const bubbleState = this.getBubbleState(target);
|
|
724
|
+
if (selection?.styleName !== styleName || !bubbleState?.text) continue;
|
|
725
|
+
const nextSelection = {
|
|
726
|
+
definition,
|
|
727
|
+
styleName
|
|
728
|
+
};
|
|
729
|
+
this.activeStyles.set(target, nextSelection);
|
|
730
|
+
this.applyBubbleStyle(target, bubbleState.type, bubbleState.text, nextSelection);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
restyleTextActors(styleName) {
|
|
734
|
+
const definition = this.styles.get(styleName);
|
|
735
|
+
if (!definition) return;
|
|
736
|
+
for (const [target, state] of [...this.textActors]) {
|
|
737
|
+
if (state.styleName !== styleName) continue;
|
|
738
|
+
this.applyTextActor(target, state.text, {
|
|
739
|
+
definition,
|
|
740
|
+
styleName
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
showStyledBubble(type, args, util) {
|
|
745
|
+
this.showBubble(type, args.MESSAGE, this.resolveStyle(args.STYLE), util);
|
|
746
|
+
}
|
|
747
|
+
showLegacyBubble(type, args, util) {
|
|
748
|
+
const defaultDefinition = this.styles.get(defaultStyleName$1) ?? initialDefaultStyle;
|
|
749
|
+
this.showBubble(type, args.MESSAGE, {
|
|
750
|
+
definition: {
|
|
751
|
+
...defaultDefinition,
|
|
752
|
+
fontPercent: this.normalizeFontPercent(args.SIZE)
|
|
753
|
+
},
|
|
754
|
+
styleName: null
|
|
755
|
+
}, util);
|
|
756
|
+
}
|
|
757
|
+
showBubble(type, messageValue, selection, util) {
|
|
758
|
+
const message = this.normalizeMessage(messageValue);
|
|
759
|
+
this.pendingStyles.set(util.target, selection);
|
|
760
|
+
try {
|
|
761
|
+
this.runtime.emit("SAY", util.target, type, message);
|
|
762
|
+
} finally {
|
|
763
|
+
this.pendingStyles.delete(util.target);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region src/composition.ts
|
|
769
|
+
var defaultStyleName = "default";
|
|
770
|
+
var directions = /* @__PURE__ */ new Set([
|
|
771
|
+
"up",
|
|
772
|
+
"up-up-right",
|
|
773
|
+
"up-right",
|
|
774
|
+
"right-up-right",
|
|
775
|
+
"right",
|
|
776
|
+
"right-down-right",
|
|
777
|
+
"down-right",
|
|
778
|
+
"down-down-right",
|
|
779
|
+
"down",
|
|
780
|
+
"down-down-left",
|
|
781
|
+
"down-left",
|
|
782
|
+
"left-down-left",
|
|
783
|
+
"left",
|
|
784
|
+
"left-up-left",
|
|
785
|
+
"up-left",
|
|
786
|
+
"up-up-left"
|
|
787
|
+
]);
|
|
788
|
+
function isRecord(value) {
|
|
789
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
790
|
+
}
|
|
791
|
+
function compositionError(code, message) {
|
|
792
|
+
const error = new Error(message);
|
|
793
|
+
Object.defineProperty(error, "code", { value: code });
|
|
794
|
+
return error;
|
|
795
|
+
}
|
|
796
|
+
function requireExactKeys(value, required, optional, label) {
|
|
797
|
+
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
798
|
+
if (required.some((key) => !Object.prototype.hasOwnProperty.call(value, key)) || Object.keys(value).some((key) => !allowed.has(key))) throw compositionError("SVG-TEXT-COMPOSITION-001", `${label} has missing or unknown properties.`);
|
|
799
|
+
}
|
|
800
|
+
function requireName(value, label) {
|
|
801
|
+
if (typeof value !== "string" || value.trim().length === 0) throw compositionError("SVG-TEXT-COMPOSITION-001", `${label} must be a non-empty string.`);
|
|
802
|
+
return value.trim();
|
|
803
|
+
}
|
|
804
|
+
function validateRuntime(value) {
|
|
805
|
+
if (!isRecord(value) || !isRecord(value.renderer)) throw new TypeError("SVG Text composition runtime must provide a renderer.");
|
|
806
|
+
const renderer = value.renderer;
|
|
807
|
+
const methods = [
|
|
808
|
+
"createSVGSkin",
|
|
809
|
+
"destroySkin",
|
|
810
|
+
"updateDrawableSkinId"
|
|
811
|
+
];
|
|
812
|
+
if (methods.some((method) => typeof renderer[method] !== "function")) throw new TypeError(`SVG Text composition renderer must provide ${methods.join(", ")}.`);
|
|
813
|
+
if (value.requestRedraw !== void 0 && typeof value.requestRedraw !== "function") throw new TypeError("SVG Text composition requestRedraw must be a function.");
|
|
814
|
+
return value;
|
|
815
|
+
}
|
|
816
|
+
function validateTarget(value) {
|
|
817
|
+
if (!isRecord(value) || typeof value.drawableID !== "number" || !Number.isInteger(value.drawableID) || value.drawableID < 0) throw compositionError("SVG-TEXT-COMPOSITION-002", "SVG Text target must provide a non-negative integer drawableID.");
|
|
818
|
+
return value;
|
|
819
|
+
}
|
|
820
|
+
function validateOptionalString(value, label) {
|
|
821
|
+
if (value !== void 0 && (typeof value !== "string" || value.length === 0)) throw compositionError("SVG-TEXT-COMPOSITION-001", `${label} must be a non-empty string when provided.`);
|
|
822
|
+
}
|
|
823
|
+
function validateStyle(value) {
|
|
824
|
+
if (!isRecord(value)) throw compositionError("SVG-TEXT-COMPOSITION-001", "SVG Text style must be an object.");
|
|
825
|
+
requireExactKeys(value, ["name"], [
|
|
826
|
+
"alignment",
|
|
827
|
+
"backgroundColor",
|
|
828
|
+
"direction",
|
|
829
|
+
"font",
|
|
830
|
+
"fontPercent",
|
|
831
|
+
"textColor"
|
|
832
|
+
], "SVG Text style");
|
|
833
|
+
const name = requireName(value.name, "SVG Text style name");
|
|
834
|
+
if (value.alignment !== void 0 && value.alignment !== "left" && value.alignment !== "center" && value.alignment !== "right") throw compositionError("SVG-TEXT-COMPOSITION-001", "SVG Text alignment is invalid.");
|
|
835
|
+
validateOptionalString(value.backgroundColor, "SVG Text backgroundColor");
|
|
836
|
+
validateOptionalString(value.font, "SVG Text font");
|
|
837
|
+
validateOptionalString(value.textColor, "SVG Text textColor");
|
|
838
|
+
if (value.fontPercent !== void 0 && (typeof value.fontPercent !== "number" || !Number.isFinite(value.fontPercent) || value.fontPercent < 1 || value.fontPercent > 1e3)) throw compositionError("SVG-TEXT-COMPOSITION-001", "SVG Text fontPercent must be a finite number from 1 to 1000.");
|
|
839
|
+
if (value.direction !== void 0) {
|
|
840
|
+
if (!(typeof value.direction === "string" && directions.has(value.direction) || typeof value.direction === "number" && Number.isFinite(value.direction) && value.direction >= 0 && value.direction <= 360)) throw compositionError("SVG-TEXT-COMPOSITION-001", "SVG Text direction is invalid.");
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
...value,
|
|
844
|
+
name
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function createSvgTextComposition(options) {
|
|
848
|
+
if (!isRecord(options)) throw new TypeError("SVG Text composition options must be an object.");
|
|
849
|
+
const extension = new SvgTextExtension(validateRuntime(options.runtime), {
|
|
850
|
+
castToString: (value) => String(value),
|
|
851
|
+
listenForRuntimeEvents: false
|
|
852
|
+
});
|
|
853
|
+
const styles = /* @__PURE__ */ new Set([defaultStyleName]);
|
|
854
|
+
const targets = /* @__PURE__ */ new Set();
|
|
855
|
+
let disposed = false;
|
|
856
|
+
function ensureActive() {
|
|
857
|
+
if (disposed) throw compositionError("SVG-TEXT-COMPOSITION-004", "SVG Text composition has been released.");
|
|
858
|
+
}
|
|
859
|
+
return Object.freeze({
|
|
860
|
+
defineStyle(input) {
|
|
861
|
+
ensureActive();
|
|
862
|
+
const style = validateStyle(input);
|
|
863
|
+
extension.defineStyle({
|
|
864
|
+
ALIGN: style.alignment ?? "",
|
|
865
|
+
BACKGROUND: style.backgroundColor ?? "",
|
|
866
|
+
DIRECTION: style.direction ?? "",
|
|
867
|
+
FONT: style.font ?? "",
|
|
868
|
+
SIZE: style.fontPercent ?? "",
|
|
869
|
+
STYLE: style.name,
|
|
870
|
+
TEXT_COLOR: style.textColor ?? ""
|
|
871
|
+
});
|
|
872
|
+
styles.add(style.name);
|
|
873
|
+
},
|
|
874
|
+
setText(input) {
|
|
875
|
+
ensureActive();
|
|
876
|
+
if (!isRecord(input)) throw compositionError("SVG-TEXT-COMPOSITION-001", "SVG Text actor input is invalid.");
|
|
877
|
+
requireExactKeys(input, [
|
|
878
|
+
"styleName",
|
|
879
|
+
"target",
|
|
880
|
+
"text"
|
|
881
|
+
], [], "SVG Text actor input");
|
|
882
|
+
const target = validateTarget(input.target);
|
|
883
|
+
const styleName = requireName(input.styleName, "SVG Text styleName");
|
|
884
|
+
if (!styles.has(styleName)) throw compositionError("SVG-TEXT-COMPOSITION-003", `SVG Text style is not defined: ${styleName}`);
|
|
885
|
+
if (typeof input.text !== "string") throw compositionError("SVG-TEXT-COMPOSITION-001", "SVG Text text must be a string.");
|
|
886
|
+
extension.setText({
|
|
887
|
+
STYLE: styleName,
|
|
888
|
+
TEXT: input.text
|
|
889
|
+
}, { target });
|
|
890
|
+
targets.add(target);
|
|
891
|
+
},
|
|
892
|
+
releaseTarget(value) {
|
|
893
|
+
ensureActive();
|
|
894
|
+
const target = validateTarget(value);
|
|
895
|
+
if (!targets.delete(target)) throw compositionError("SVG-TEXT-COMPOSITION-005", "SVG Text target is not owned by this composition.");
|
|
896
|
+
if (!extension.releaseTextActor(target)) throw compositionError("SVG-TEXT-COMPOSITION-005", "SVG Text target ownership is inconsistent.");
|
|
897
|
+
},
|
|
898
|
+
releaseAll() {
|
|
899
|
+
if (disposed) return;
|
|
900
|
+
disposed = true;
|
|
901
|
+
const errors = [];
|
|
902
|
+
for (const target of targets) try {
|
|
903
|
+
extension.releaseTextActor(target);
|
|
904
|
+
} catch (error) {
|
|
905
|
+
errors.push(error);
|
|
906
|
+
}
|
|
907
|
+
targets.clear();
|
|
908
|
+
styles.clear();
|
|
909
|
+
if (errors.length > 0) {
|
|
910
|
+
const error = compositionError("SVG-TEXT-COMPOSITION-006", "SVG Text composition failed to release one or more skins.");
|
|
911
|
+
Object.defineProperty(error, "errors", { value: Object.freeze([...errors]) });
|
|
912
|
+
throw error;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
//#endregion
|
|
918
|
+
export { createSvgTextComposition };
|
package/dist/svg-text.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"opcode": "defineStyle",
|
|
25
25
|
"blockType": "COMMAND",
|
|
26
26
|
"text": "define text style [STYLE] background [BACKGROUND] text [TEXT_COLOR] font [FONT] size [SIZE] align [ALIGN] bubble direction [DIRECTION]",
|
|
27
|
-
"description": "Defines or replaces a named responsive text style. Bubble direction accepts names, compass aliases, or Scratch-style degrees from 0 to 360.",
|
|
27
|
+
"description": "Defines or replaces a named responsive text style. Bubble direction is measured from the actor center to the bubble body center and accepts names, compass aliases, or Scratch-style degrees from 0 to 360.",
|
|
28
28
|
"arguments": {
|
|
29
29
|
"STYLE": {
|
|
30
30
|
"type": "STRING",
|
|
@@ -329,7 +329,7 @@
|
|
|
329
329
|
textColor: "#575e75"
|
|
330
330
|
};
|
|
331
331
|
var SvgTextExtension = class {
|
|
332
|
-
constructor(runtime = Scratch.vm?.runtime) {
|
|
332
|
+
constructor(runtime = Scratch.vm?.runtime, options = {}) {
|
|
333
333
|
this.styles = /* @__PURE__ */ new Map([[defaultStyleName, initialDefaultStyle]]);
|
|
334
334
|
this.activeStyles = /* @__PURE__ */ new WeakMap();
|
|
335
335
|
this.textActors = /* @__PURE__ */ new Map();
|
|
@@ -338,10 +338,13 @@
|
|
|
338
338
|
this.targetPositionHooks = /* @__PURE__ */ new WeakMap();
|
|
339
339
|
if (!runtime) throw new Error("SVG Text requires the TurboWarp VM.");
|
|
340
340
|
this.runtime = runtime;
|
|
341
|
+
this.castToString = options.castToString ?? Scratch.Cast.toString;
|
|
341
342
|
this.handleSayOrThink = this.handleSayOrThink.bind(this);
|
|
342
343
|
this.handleStageSizeChanged = this.handleStageSizeChanged.bind(this);
|
|
343
|
-
|
|
344
|
-
|
|
344
|
+
if (options.listenForRuntimeEvents ?? true) {
|
|
345
|
+
this.runtime.on("SAY", this.handleSayOrThink);
|
|
346
|
+
this.runtime.on("STAGE_SIZE_CHANGED", this.handleStageSizeChanged);
|
|
347
|
+
}
|
|
345
348
|
}
|
|
346
349
|
getInfo() {
|
|
347
350
|
return {
|
|
@@ -371,6 +374,14 @@
|
|
|
371
374
|
const selection = this.resolveStyle(args.STYLE);
|
|
372
375
|
this.applyTextActor(util.target, this.normalizeMessage(args.TEXT), selection);
|
|
373
376
|
}
|
|
377
|
+
releaseTextActor(target) {
|
|
378
|
+
const state = this.textActors.get(target);
|
|
379
|
+
if (!state) return false;
|
|
380
|
+
this.textActors.delete(target);
|
|
381
|
+
this.runtime.renderer?.destroySkin?.(state.skinId);
|
|
382
|
+
this.runtime.requestRedraw?.();
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
374
385
|
sayWithStyle(args, util) {
|
|
375
386
|
this.showStyledBubble("say", args, util);
|
|
376
387
|
}
|
|
@@ -397,7 +408,7 @@
|
|
|
397
408
|
};
|
|
398
409
|
}
|
|
399
410
|
normalizeStyleName(value) {
|
|
400
|
-
return
|
|
411
|
+
return this.castToString(value).trim() || defaultStyleName;
|
|
401
412
|
}
|
|
402
413
|
normalizeFontPercent(value) {
|
|
403
414
|
if (typeof value === "string" && value.trim() === "") return defaultFontPercent;
|
|
@@ -406,15 +417,15 @@
|
|
|
406
417
|
return Math.min(maximumFontPercent, Math.max(minimumFontPercent, numericValue));
|
|
407
418
|
}
|
|
408
419
|
normalizeMessage(value) {
|
|
409
|
-
return
|
|
420
|
+
return this.castToString(value).replace(/\\r\\n|\\n|\\r/gu, "\n");
|
|
410
421
|
}
|
|
411
422
|
normalizeAlignment(value) {
|
|
412
|
-
const alignment =
|
|
423
|
+
const alignment = this.castToString(value).trim().toLowerCase();
|
|
413
424
|
if (alignment === "center" || alignment === "right") return alignment;
|
|
414
425
|
return "left";
|
|
415
426
|
}
|
|
416
427
|
normalizeDirection(value) {
|
|
417
|
-
const direction =
|
|
428
|
+
const direction = this.castToString(value).trim().toLowerCase();
|
|
418
429
|
if (bubbleDirections.includes(direction)) return direction;
|
|
419
430
|
const alias = bubbleDirectionAliases.get(direction);
|
|
420
431
|
if (alias) return alias;
|
|
@@ -425,14 +436,14 @@
|
|
|
425
436
|
return initialDefaultStyle.direction;
|
|
426
437
|
}
|
|
427
438
|
normalizeColor(value, fallback) {
|
|
428
|
-
const color =
|
|
439
|
+
const color = this.castToString(value).trim();
|
|
429
440
|
if (color === "") return fallback;
|
|
430
441
|
if (/^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/iu.test(color)) return color;
|
|
431
442
|
if (globalThis.CSS?.supports?.("color", color)) return color;
|
|
432
443
|
return fallback;
|
|
433
444
|
}
|
|
434
445
|
normalizeFont(value) {
|
|
435
|
-
const font =
|
|
446
|
+
const font = this.castToString(value).trim();
|
|
436
447
|
const hasUnsafeCharacter = [...font].some((character) => {
|
|
437
448
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
438
449
|
return codePoint <= 31 || codePoint === 127 || ",;{}".includes(character);
|
|
@@ -519,7 +530,7 @@
|
|
|
519
530
|
const renderer = this.runtime.renderer;
|
|
520
531
|
if (typeof target.drawableID !== "number" || typeof renderer?.createSVGSkin !== "function" || typeof renderer.updateDrawableSkinId !== "function") throw new Error("SVG Text requires SVG skin APIs from TurboWarp.");
|
|
521
532
|
const skinId = renderer.createSVGSkin(this.createTextActorSvg(text, selection.definition));
|
|
522
|
-
if (
|
|
533
|
+
if (!Number.isInteger(skinId) || skinId < 0) throw new Error("TurboWarp did not create an SVG text skin.");
|
|
523
534
|
try {
|
|
524
535
|
renderer.updateDrawableSkinId(target.drawableID, skinId);
|
|
525
536
|
} catch (error) {
|
|
@@ -651,9 +662,9 @@
|
|
|
651
662
|
const vector = directionVector(direction);
|
|
652
663
|
const horizontalDistance = vector.x < 0 ? centeredBubbleX - leftBubbleX : rightBubbleX - centeredBubbleX;
|
|
653
664
|
const verticalDistance = vector.y < 0 ? centeredBubbleY - lowerBubbleY : upperBubbleY - centeredBubbleY;
|
|
654
|
-
const
|
|
655
|
-
let x = centeredBubbleX + vector.x *
|
|
656
|
-
let y = centeredBubbleY + vector.y *
|
|
665
|
+
const placementScale = Math.min(vector.x === 0 ? Number.POSITIVE_INFINITY : horizontalDistance / Math.abs(vector.x), vector.y === 0 ? Number.POSITIVE_INFINITY : verticalDistance / Math.abs(vector.y));
|
|
666
|
+
let x = centeredBubbleX + vector.x * placementScale;
|
|
667
|
+
let y = centeredBubbleY + vector.y * placementScale;
|
|
657
668
|
if (vector.x > 0 && !bubbleState.onSpriteRight) {
|
|
658
669
|
bubbleState.onSpriteRight = true;
|
|
659
670
|
this.updateTextSkin(bubbleState);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type SvgTextAlignment = "center" | "left" | "right";
|
|
2
|
+
export type SvgTextDirection = "up" | "up-up-right" | "up-right" | "right-up-right" | "right" | "right-down-right" | "down-right" | "down-down-right" | "down" | "down-down-left" | "down-left" | "left-down-left" | "left" | "left-up-left" | "up-left" | "up-up-left" | number;
|
|
3
|
+
export interface SvgTextStyleInput {
|
|
4
|
+
name: string;
|
|
5
|
+
alignment?: SvgTextAlignment;
|
|
6
|
+
backgroundColor?: string;
|
|
7
|
+
direction?: SvgTextDirection;
|
|
8
|
+
font?: string;
|
|
9
|
+
fontPercent?: number;
|
|
10
|
+
textColor?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SvgTextTarget {
|
|
13
|
+
drawableID: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SvgTextActorInput {
|
|
16
|
+
styleName: string;
|
|
17
|
+
target: SvgTextTarget;
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SvgTextCompositionRenderer {
|
|
21
|
+
createSVGSkin(svg: string): number;
|
|
22
|
+
destroySkin(skinId: number): void;
|
|
23
|
+
getNativeSize?(): unknown;
|
|
24
|
+
updateDrawableSkinId(drawableId: number, skinId: number): void;
|
|
25
|
+
}
|
|
26
|
+
export interface SvgTextCompositionRuntime {
|
|
27
|
+
renderer: SvgTextCompositionRenderer;
|
|
28
|
+
requestRedraw?(): void;
|
|
29
|
+
}
|
|
30
|
+
export interface SvgTextComposition {
|
|
31
|
+
defineStyle(input: SvgTextStyleInput): void;
|
|
32
|
+
releaseAll(): void;
|
|
33
|
+
releaseTarget(target: SvgTextTarget): void;
|
|
34
|
+
setText(input: SvgTextActorInput): void;
|
|
35
|
+
}
|
|
36
|
+
export interface SvgTextCompositionOptions {
|
|
37
|
+
runtime: SvgTextCompositionRuntime;
|
|
38
|
+
}
|
|
39
|
+
export declare function createSvgTextComposition(options: SvgTextCompositionOptions): SvgTextComposition;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const extensionConfig: {
|
|
2
|
+
readonly id: "kubohiroyasvgtext";
|
|
3
|
+
readonly slug: "svg-text";
|
|
4
|
+
readonly name: "SVG Text";
|
|
5
|
+
readonly description: "Responsive named-style bubbles and SVG text actors for TurboWarp.";
|
|
6
|
+
readonly author: "Hiroya Kubo";
|
|
7
|
+
readonly license: "MPL-2.0";
|
|
8
|
+
readonly unsandboxed: true;
|
|
9
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
interface LegacyBubbleArguments {
|
|
2
|
+
MESSAGE: unknown;
|
|
3
|
+
SIZE: unknown;
|
|
4
|
+
}
|
|
5
|
+
interface StyledBubbleArguments {
|
|
6
|
+
MESSAGE: unknown;
|
|
7
|
+
STYLE: unknown;
|
|
8
|
+
}
|
|
9
|
+
interface TextActorArguments {
|
|
10
|
+
STYLE: unknown;
|
|
11
|
+
TEXT: unknown;
|
|
12
|
+
}
|
|
13
|
+
interface DefineStyleArguments {
|
|
14
|
+
ALIGN: unknown;
|
|
15
|
+
BACKGROUND: unknown;
|
|
16
|
+
DIRECTION: unknown;
|
|
17
|
+
FONT: unknown;
|
|
18
|
+
SIZE: unknown;
|
|
19
|
+
STYLE: unknown;
|
|
20
|
+
TEXT_COLOR: unknown;
|
|
21
|
+
}
|
|
22
|
+
interface BlockUtility {
|
|
23
|
+
target: TurboWarpTarget;
|
|
24
|
+
}
|
|
25
|
+
interface SvgTextExtensionOptions {
|
|
26
|
+
castToString?: (value: unknown) => string;
|
|
27
|
+
listenForRuntimeEvents?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare const EXTENSION_DOCS_URI = "https://kubohiroya.github.io/turbowarp-svg-text/";
|
|
30
|
+
export declare class SvgTextExtension implements TurboWarpExtension {
|
|
31
|
+
private readonly runtime;
|
|
32
|
+
private readonly castToString;
|
|
33
|
+
private readonly styles;
|
|
34
|
+
private readonly activeStyles;
|
|
35
|
+
private readonly textActors;
|
|
36
|
+
private readonly pendingStyles;
|
|
37
|
+
private readonly alignedSkins;
|
|
38
|
+
private readonly targetPositionHooks;
|
|
39
|
+
constructor(runtime?: any, options?: SvgTextExtensionOptions);
|
|
40
|
+
getInfo(): Record<string, unknown>;
|
|
41
|
+
defineStyle(args: DefineStyleArguments): void;
|
|
42
|
+
setText(args: TextActorArguments, util: BlockUtility): void;
|
|
43
|
+
releaseTextActor(target: TurboWarpTarget): boolean;
|
|
44
|
+
sayWithStyle(args: StyledBubbleArguments, util: BlockUtility): void;
|
|
45
|
+
thinkWithStyle(args: StyledBubbleArguments, util: BlockUtility): void;
|
|
46
|
+
say(args: LegacyBubbleArguments, util: BlockUtility): void;
|
|
47
|
+
think(args: LegacyBubbleArguments, util: BlockUtility): void;
|
|
48
|
+
private toScratchBlock;
|
|
49
|
+
private normalizeStyleName;
|
|
50
|
+
private normalizeFontPercent;
|
|
51
|
+
private normalizeMessage;
|
|
52
|
+
private normalizeAlignment;
|
|
53
|
+
private normalizeDirection;
|
|
54
|
+
private normalizeColor;
|
|
55
|
+
private normalizeFont;
|
|
56
|
+
private getStageScale;
|
|
57
|
+
private createRenderStyle;
|
|
58
|
+
private createTextActorSvg;
|
|
59
|
+
private measureTextWidth;
|
|
60
|
+
private escapeXml;
|
|
61
|
+
private formatSvgNumber;
|
|
62
|
+
private applyTextActor;
|
|
63
|
+
private resolveStyle;
|
|
64
|
+
private getBubbleState;
|
|
65
|
+
private getTextBubbleSkin;
|
|
66
|
+
private installAlignmentRenderer;
|
|
67
|
+
private installTargetPositionHook;
|
|
68
|
+
private positionBubble;
|
|
69
|
+
private updateTextSkin;
|
|
70
|
+
private clampPosition;
|
|
71
|
+
private applyBubbleStyle;
|
|
72
|
+
private handleSayOrThink;
|
|
73
|
+
private handleStageSizeChanged;
|
|
74
|
+
private restyleVisibleBubbles;
|
|
75
|
+
private restyleTextActors;
|
|
76
|
+
private showStyledBubble;
|
|
77
|
+
private showLegacyBubble;
|
|
78
|
+
private showBubble;
|
|
79
|
+
}
|
|
80
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubohiroya/turbowarp-svg-text",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Responsive named-style bubbles and SVG text actors for TurboWarp.",
|
|
5
5
|
"author": "Hiroya Kubo <hiroya@cuc.ac.jp>",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/kubohiroya/turbowarp-svg-text/issues"
|
|
15
15
|
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./dist/svg-text.js",
|
|
18
|
+
"./composition": {
|
|
19
|
+
"types": "./dist/types/composition.d.ts",
|
|
20
|
+
"import": "./dist/composition.js"
|
|
21
|
+
},
|
|
22
|
+
"./dist/svg-text.js": "./dist/svg-text.js",
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
16
25
|
"files": [
|
|
17
26
|
"dist/",
|
|
18
27
|
"README.md",
|
|
@@ -25,6 +34,27 @@
|
|
|
25
34
|
"access": "public",
|
|
26
35
|
"registry": "https://registry.npmjs.org/"
|
|
27
36
|
},
|
|
37
|
+
"packageManager": "pnpm@11.18.0",
|
|
38
|
+
"scripts": {
|
|
39
|
+
"dev": "vite build --watch",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"build": "pnpm build:extension && pnpm build:composition && pnpm build:types",
|
|
42
|
+
"build:extension": "vite build",
|
|
43
|
+
"build:composition": "vite build --config vite.composition.config.ts",
|
|
44
|
+
"build:types": "tsc --project tsconfig.build.json",
|
|
45
|
+
"composition:dist:check": "node scripts/check-composition-dist.mjs",
|
|
46
|
+
"types:composition:check": "tsc --project tsconfig.composition-consumer.json",
|
|
47
|
+
"lint": "eslint src tests scripts vite.config.ts eslint.config.mjs",
|
|
48
|
+
"format": "prettier --check .",
|
|
49
|
+
"format:write": "prettier --write .",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"docs:site:check": "node scripts/check-site.mjs",
|
|
52
|
+
"check:dist": "pnpm build && node scripts/check-dist.mjs",
|
|
53
|
+
"pack:check": "npm pack --dry-run --ignore-scripts",
|
|
54
|
+
"release:check": "pnpm publish --dry-run --access public",
|
|
55
|
+
"check": "pnpm typecheck && pnpm lint && pnpm format && pnpm test && pnpm docs:site:check && pnpm check:dist && pnpm composition:dist:check && pnpm types:composition:check && pnpm pack:check",
|
|
56
|
+
"prepack": "pnpm check"
|
|
57
|
+
},
|
|
28
58
|
"devDependencies": {
|
|
29
59
|
"@eslint/js": "^10.0.1",
|
|
30
60
|
"@kubohiroya/vite-plugin-turbowarp-extension": "0.1.1",
|
|
@@ -45,19 +75,5 @@
|
|
|
45
75
|
"text-actor",
|
|
46
76
|
"named-styles",
|
|
47
77
|
"multiline"
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
"dev": "vite build --watch",
|
|
51
|
-
"typecheck": "tsc --noEmit",
|
|
52
|
-
"build": "vite build",
|
|
53
|
-
"lint": "eslint src tests scripts vite.config.ts eslint.config.mjs",
|
|
54
|
-
"format": "prettier --check .",
|
|
55
|
-
"format:write": "prettier --write .",
|
|
56
|
-
"test": "vitest run",
|
|
57
|
-
"docs:site:check": "node scripts/check-site.mjs",
|
|
58
|
-
"check:dist": "pnpm build && node scripts/check-dist.mjs",
|
|
59
|
-
"pack:check": "npm pack --dry-run --ignore-scripts",
|
|
60
|
-
"release:check": "pnpm publish --dry-run --access public",
|
|
61
|
-
"check": "pnpm typecheck && pnpm lint && pnpm format && pnpm test && pnpm docs:site:check && pnpm check:dist && pnpm pack:check"
|
|
62
|
-
}
|
|
63
|
-
}
|
|
78
|
+
]
|
|
79
|
+
}
|