@kubohiroya/turbowarp-svg-text 0.1.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 +5 -0
- package/README.md +89 -0
- package/dist/extension-manifest.json +122 -0
- package/dist/svg-text.js +653 -0
- package/package.json +63 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# TurboWarp SVG Text
|
|
2
|
+
|
|
3
|
+
TurboWarpのstageサイズに追従する、名前付きスタイル対応のテキスト機能拡張です。同じスタイルをsay/think吹き出しと、スプライト自身をテキスト表示にするSVGスキンで共有できます。
|
|
4
|
+
|
|
5
|
+
利用方法と動作例は、[English guide](https://kubohiroya.github.io/turbowarp-svg-text/)または[日本語ガイド](https://kubohiroya.github.io/turbowarp-svg-text/ja/)を参照してください。TurboWarpの拡張パレットからも`docsURI`で英語ガイドを開けます。
|
|
6
|
+
|
|
7
|
+
## 機能
|
|
8
|
+
|
|
9
|
+
- 背景色・文字色・フォント・相対フォントサイズ・左/中央/右揃えを、任意のスタイル名で定義
|
|
10
|
+
- 吹き出しの表示方向を、上下左右と斜め4方向の計8方向から指定
|
|
11
|
+
- 名前付きスタイルでsay/think吹き出しを表示
|
|
12
|
+
- `set this sprite text`でスプライト自身のスキンを複数行SVGテキストへ置換
|
|
13
|
+
- 標準のsay/think/ask吹き出しには、再定義可能な`default`スタイルを適用
|
|
14
|
+
- 文字列中のリテラル`\n`、`\r\n`、`\r`を実際の改行へ変換
|
|
15
|
+
- 表示中のスタイル再定義やstageサイズ変更を、吹き出しとSVGテキストへ即時反映
|
|
16
|
+
- 旧サイズ指定say/think opcodeを、保存済みプロジェクトとの互換性のため非表示で維持
|
|
17
|
+
|
|
18
|
+
アニメーション機能は0.1.0の対象外です。
|
|
19
|
+
|
|
20
|
+
## ブロック
|
|
21
|
+
|
|
22
|
+
### `define text style [STYLE] background [BACKGROUND] text [TEXT_COLOR] font [FONT] size [SIZE] align [ALIGN] bubble direction [DIRECTION]`
|
|
23
|
+
|
|
24
|
+
名前付きスタイルを定義または再定義します。`ALIGN`は`left`、`center`、`right`、`DIRECTION`は`up`、`up-right`、`right`、`down-right`、`down`、`down-left`、`left`、`up-left`です。方向は吹き出しだけに適用されます。
|
|
25
|
+
|
|
26
|
+
同じ名前を再定義すると、そのスタイルを使用中の吹き出しとSVGテキストをすぐ再描画します。スタイル定義は実行時の状態なので、通常は緑の旗を押した直後に実行してください。空欄または未定義の名前は`default`へフォールバックします。
|
|
27
|
+
|
|
28
|
+
### `set this sprite text [TEXT] with style [STYLE]`
|
|
29
|
+
|
|
30
|
+
現在のスプライトのdrawableへ新しいSVGスキンを設定し、スプライト自身をテキスト表示にします。背景色・文字色・フォント・サイズ・文字揃えを指定したスタイルから取得します。`\n`を含む文字列は複数行の`<tspan>`として安全にエスケープして描画します。
|
|
31
|
+
|
|
32
|
+
### `say [MESSAGE] with style [STYLE]`
|
|
33
|
+
|
|
34
|
+
指定した名前のスタイルでsay吹き出しを表示します。
|
|
35
|
+
|
|
36
|
+
### `think [MESSAGE] with style [STYLE]`
|
|
37
|
+
|
|
38
|
+
指定した名前のスタイルでthink吹き出しを表示します。
|
|
39
|
+
|
|
40
|
+
## サイズ計算
|
|
41
|
+
|
|
42
|
+
480×360の標準stageにおけるTurboWarp既定の14pxをサイズ100とし、吹き出しとSVGテキストに同じ式を適用します。
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
stageScale = min(stageWidth / 480, stageHeight / 360)
|
|
46
|
+
fontSize = 14 × stageScale × styleSize / 100
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| stage | styleSize | fontSize |
|
|
50
|
+
| ------- | --------: | -------: |
|
|
51
|
+
| 480×360 | 100 | 14px |
|
|
52
|
+
| 960×720 | 100 | 28px |
|
|
53
|
+
| 960×720 | 150 | 42px |
|
|
54
|
+
|
|
55
|
+
サイズは1〜1000に制限され、空文字や有限でない値は100として扱われます。
|
|
56
|
+
|
|
57
|
+
## 利用方法
|
|
58
|
+
|
|
59
|
+
TurboWarpの「カスタム拡張を読み込む」で次のURLを読み込み、unsandboxed実行を許可します。
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-svg-text@0.1.0/dist/svg-text.js
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
define text style [narration] background [#fff4cc] text [#332200] font [Noto Sans JP] size [125] align [center] bubble direction [up]
|
|
67
|
+
say [むかし、むかし\nあるところに…] with style [narration]
|
|
68
|
+
set this sprite text [第一幕\n浦島太郎] with style [narration]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 開発
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
corepack enable
|
|
75
|
+
pnpm install --frozen-lockfile
|
|
76
|
+
pnpm check
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
TypeScriptソースは、`@kubohiroya/vite-plugin-turbowarp-extension`を通して単一の`dist/svg-text.js`へビルドされます。ビルド時には保存プロジェクトとのAPI互換性確認用に`dist/extension-manifest.json`も生成します。
|
|
80
|
+
|
|
81
|
+
## 互換性と制約
|
|
82
|
+
|
|
83
|
+
この拡張はTurboWarp VMとrendererへアクセスするためunsandboxedで実行します。吹き出しは`SAY`/`STAGE_SIZE_CHANGED`イベント、`Scratch.looks` custom state、drawable位置更新、および`scratch-render`の`TextBubbleSkin.setStyle()`をfeature detectionして利用します。SVGテキストは`createSVGSkin`、`updateDrawableSkinId`、`destroySkin`を使用します。
|
|
84
|
+
|
|
85
|
+
中央/右揃えの吹き出しでrenderer内部処理が互換でない場合は左揃えへ、位置更新APIがない場合はTurboWarp標準配置へフォールバックします。SVG skin APIがない環境では`setText`は明示的なエラーにします。通常のsay/think停止、位置追従、330文字制限、数値丸めは標準実装へ委譲します。
|
|
86
|
+
|
|
87
|
+
## ライセンス
|
|
88
|
+
|
|
89
|
+
MPL-2.0
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"formatVersion": 1,
|
|
3
|
+
"id": "kubohiroyasvgtext",
|
|
4
|
+
"blocks": [
|
|
5
|
+
{
|
|
6
|
+
"opcode": "defineStyle",
|
|
7
|
+
"blockType": "COMMAND",
|
|
8
|
+
"arguments": [
|
|
9
|
+
{
|
|
10
|
+
"id": "ALIGN",
|
|
11
|
+
"type": "STRING",
|
|
12
|
+
"menu": "alignment"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "BACKGROUND",
|
|
16
|
+
"type": "COLOR"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "DIRECTION",
|
|
20
|
+
"type": "STRING",
|
|
21
|
+
"menu": "direction"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "FONT",
|
|
25
|
+
"type": "STRING"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "SIZE",
|
|
29
|
+
"type": "NUMBER"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "STYLE",
|
|
33
|
+
"type": "STRING"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "TEXT_COLOR",
|
|
37
|
+
"type": "COLOR"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"opcode": "say",
|
|
43
|
+
"blockType": "COMMAND",
|
|
44
|
+
"arguments": [
|
|
45
|
+
{
|
|
46
|
+
"id": "MESSAGE",
|
|
47
|
+
"type": "STRING"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "SIZE",
|
|
51
|
+
"type": "NUMBER"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"opcode": "sayWithStyle",
|
|
57
|
+
"blockType": "COMMAND",
|
|
58
|
+
"arguments": [
|
|
59
|
+
{
|
|
60
|
+
"id": "MESSAGE",
|
|
61
|
+
"type": "STRING"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "STYLE",
|
|
65
|
+
"type": "STRING"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"opcode": "setText",
|
|
71
|
+
"blockType": "COMMAND",
|
|
72
|
+
"arguments": [
|
|
73
|
+
{
|
|
74
|
+
"id": "STYLE",
|
|
75
|
+
"type": "STRING"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "TEXT",
|
|
79
|
+
"type": "STRING"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"opcode": "think",
|
|
85
|
+
"blockType": "COMMAND",
|
|
86
|
+
"arguments": [
|
|
87
|
+
{
|
|
88
|
+
"id": "MESSAGE",
|
|
89
|
+
"type": "STRING"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": "SIZE",
|
|
93
|
+
"type": "NUMBER"
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"opcode": "thinkWithStyle",
|
|
99
|
+
"blockType": "COMMAND",
|
|
100
|
+
"arguments": [
|
|
101
|
+
{
|
|
102
|
+
"id": "MESSAGE",
|
|
103
|
+
"type": "STRING"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "STYLE",
|
|
107
|
+
"type": "STRING"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"menus": [
|
|
113
|
+
{
|
|
114
|
+
"id": "alignment",
|
|
115
|
+
"acceptReporters": true
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "direction",
|
|
119
|
+
"acceptReporters": true
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
package/dist/svg-text.js
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
// Name: SVG Text
|
|
2
|
+
// ID: kubohiroyasvgtext
|
|
3
|
+
// Description: Responsive named-style bubbles and SVG text actors for TurboWarp.
|
|
4
|
+
// By: Hiroya Kubo
|
|
5
|
+
// License: MPL-2.0
|
|
6
|
+
|
|
7
|
+
(function (Scratch) {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
//#region src/config.ts
|
|
11
|
+
var extensionConfig = {
|
|
12
|
+
id: "kubohiroyasvgtext",
|
|
13
|
+
slug: "svg-text",
|
|
14
|
+
name: "SVG Text",
|
|
15
|
+
description: "Responsive named-style bubbles and SVG text actors for TurboWarp.",
|
|
16
|
+
author: "Hiroya Kubo",
|
|
17
|
+
license: "MPL-2.0",
|
|
18
|
+
unsandboxed: true
|
|
19
|
+
};
|
|
20
|
+
var block_definitions_default = {
|
|
21
|
+
extensionName: "SVG Text",
|
|
22
|
+
blocks: [
|
|
23
|
+
{
|
|
24
|
+
"opcode": "defineStyle",
|
|
25
|
+
"blockType": "COMMAND",
|
|
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 shared by bubbles and SVG text actors.",
|
|
28
|
+
"arguments": {
|
|
29
|
+
"STYLE": {
|
|
30
|
+
"type": "STRING",
|
|
31
|
+
"defaultValue": "default"
|
|
32
|
+
},
|
|
33
|
+
"BACKGROUND": {
|
|
34
|
+
"type": "COLOR",
|
|
35
|
+
"defaultValue": "#ffffff"
|
|
36
|
+
},
|
|
37
|
+
"TEXT_COLOR": {
|
|
38
|
+
"type": "COLOR",
|
|
39
|
+
"defaultValue": "#575e75"
|
|
40
|
+
},
|
|
41
|
+
"FONT": {
|
|
42
|
+
"type": "STRING",
|
|
43
|
+
"defaultValue": "Helvetica"
|
|
44
|
+
},
|
|
45
|
+
"SIZE": {
|
|
46
|
+
"type": "NUMBER",
|
|
47
|
+
"defaultValue": 100
|
|
48
|
+
},
|
|
49
|
+
"ALIGN": {
|
|
50
|
+
"type": "STRING",
|
|
51
|
+
"defaultValue": "left",
|
|
52
|
+
"menu": "alignment"
|
|
53
|
+
},
|
|
54
|
+
"DIRECTION": {
|
|
55
|
+
"type": "STRING",
|
|
56
|
+
"defaultValue": "up-right",
|
|
57
|
+
"menu": "direction"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"opcode": "setText",
|
|
63
|
+
"blockType": "COMMAND",
|
|
64
|
+
"text": "set this sprite text [TEXT] with style [STYLE]",
|
|
65
|
+
"description": "Replaces this sprite's skin with responsive styled SVG text.",
|
|
66
|
+
"arguments": {
|
|
67
|
+
"TEXT": {
|
|
68
|
+
"type": "STRING",
|
|
69
|
+
"defaultValue": "Title\\nSubtitle"
|
|
70
|
+
},
|
|
71
|
+
"STYLE": {
|
|
72
|
+
"type": "STRING",
|
|
73
|
+
"defaultValue": "default"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"opcode": "sayWithStyle",
|
|
79
|
+
"blockType": "COMMAND",
|
|
80
|
+
"text": "say [MESSAGE] with style [STYLE]",
|
|
81
|
+
"description": "Shows a responsive say bubble using a named style.",
|
|
82
|
+
"arguments": {
|
|
83
|
+
"MESSAGE": {
|
|
84
|
+
"type": "STRING",
|
|
85
|
+
"defaultValue": "Hello!\\nHow are you?"
|
|
86
|
+
},
|
|
87
|
+
"STYLE": {
|
|
88
|
+
"type": "STRING",
|
|
89
|
+
"defaultValue": "default"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"opcode": "thinkWithStyle",
|
|
95
|
+
"blockType": "COMMAND",
|
|
96
|
+
"text": "think [MESSAGE] with style [STYLE]",
|
|
97
|
+
"description": "Shows a responsive think bubble using a named style.",
|
|
98
|
+
"arguments": {
|
|
99
|
+
"MESSAGE": {
|
|
100
|
+
"type": "STRING",
|
|
101
|
+
"defaultValue": "Hmm...\\nI wonder."
|
|
102
|
+
},
|
|
103
|
+
"STYLE": {
|
|
104
|
+
"type": "STRING",
|
|
105
|
+
"defaultValue": "default"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"opcode": "say",
|
|
111
|
+
"blockType": "COMMAND",
|
|
112
|
+
"text": "say [MESSAGE] with font size [SIZE]",
|
|
113
|
+
"description": "Legacy size-based say bubble kept for saved-project compatibility.",
|
|
114
|
+
"hideFromPalette": true,
|
|
115
|
+
"arguments": {
|
|
116
|
+
"MESSAGE": {
|
|
117
|
+
"type": "STRING",
|
|
118
|
+
"defaultValue": "Hello!\\nHow are you?"
|
|
119
|
+
},
|
|
120
|
+
"SIZE": {
|
|
121
|
+
"type": "NUMBER",
|
|
122
|
+
"defaultValue": 100
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"opcode": "think",
|
|
128
|
+
"blockType": "COMMAND",
|
|
129
|
+
"text": "think [MESSAGE] with font size [SIZE]",
|
|
130
|
+
"description": "Legacy size-based think bubble kept for saved-project compatibility.",
|
|
131
|
+
"hideFromPalette": true,
|
|
132
|
+
"arguments": {
|
|
133
|
+
"MESSAGE": {
|
|
134
|
+
"type": "STRING",
|
|
135
|
+
"defaultValue": "Hmm...\\nI wonder."
|
|
136
|
+
},
|
|
137
|
+
"SIZE": {
|
|
138
|
+
"type": "NUMBER",
|
|
139
|
+
"defaultValue": 100
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
menus: {
|
|
145
|
+
"alignment": {
|
|
146
|
+
"acceptReporters": true,
|
|
147
|
+
"items": [
|
|
148
|
+
"left",
|
|
149
|
+
"center",
|
|
150
|
+
"right"
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
"direction": {
|
|
154
|
+
"acceptReporters": true,
|
|
155
|
+
"items": [
|
|
156
|
+
"up",
|
|
157
|
+
"up-right",
|
|
158
|
+
"right",
|
|
159
|
+
"down-right",
|
|
160
|
+
"down",
|
|
161
|
+
"down-left",
|
|
162
|
+
"left",
|
|
163
|
+
"up-left"
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/extension.ts
|
|
170
|
+
var blockDefinitions = block_definitions_default.blocks;
|
|
171
|
+
var definitionMenus = block_definitions_default.menus;
|
|
172
|
+
var EXTENSION_DOCS_URI = "https://kubohiroya.github.io/turbowarp-svg-text/";
|
|
173
|
+
var bubbleStateKey = "Scratch.looks";
|
|
174
|
+
var defaultStyleName = "default";
|
|
175
|
+
var defaultFontPercent = 100;
|
|
176
|
+
var minimumFontPercent = 1;
|
|
177
|
+
var maximumFontPercent = 1e3;
|
|
178
|
+
var maximumFontNameLength = 128;
|
|
179
|
+
var baseStageWidth = 480;
|
|
180
|
+
var baseStageHeight = 360;
|
|
181
|
+
var baseStyle = {
|
|
182
|
+
maxLineWidth: 170,
|
|
183
|
+
minWidth: 50,
|
|
184
|
+
strokeWidth: 4,
|
|
185
|
+
padding: 10,
|
|
186
|
+
cornerRadius: 16,
|
|
187
|
+
tailHeight: 12,
|
|
188
|
+
fontSize: 14,
|
|
189
|
+
fontHeightRatio: .9,
|
|
190
|
+
lineHeight: 16
|
|
191
|
+
};
|
|
192
|
+
var actorStyle = {
|
|
193
|
+
minimumWidth: 1,
|
|
194
|
+
padding: 12,
|
|
195
|
+
cornerRadius: 8
|
|
196
|
+
};
|
|
197
|
+
var initialDefaultStyle = {
|
|
198
|
+
alignment: "left",
|
|
199
|
+
backgroundColor: "#ffffff",
|
|
200
|
+
direction: "up-right",
|
|
201
|
+
font: "Helvetica",
|
|
202
|
+
fontPercent: defaultFontPercent,
|
|
203
|
+
textColor: "#575e75"
|
|
204
|
+
};
|
|
205
|
+
var SvgTextExtension = class {
|
|
206
|
+
constructor(runtime = Scratch.vm?.runtime) {
|
|
207
|
+
this.styles = /* @__PURE__ */ new Map([[defaultStyleName, initialDefaultStyle]]);
|
|
208
|
+
this.activeStyles = /* @__PURE__ */ new WeakMap();
|
|
209
|
+
this.textActors = /* @__PURE__ */ new Map();
|
|
210
|
+
this.pendingStyles = /* @__PURE__ */ new WeakMap();
|
|
211
|
+
this.alignedSkins = /* @__PURE__ */ new WeakSet();
|
|
212
|
+
this.targetPositionHooks = /* @__PURE__ */ new WeakMap();
|
|
213
|
+
if (!runtime) throw new Error("SVG Text requires the TurboWarp VM.");
|
|
214
|
+
this.runtime = runtime;
|
|
215
|
+
this.handleSayOrThink = this.handleSayOrThink.bind(this);
|
|
216
|
+
this.handleStageSizeChanged = this.handleStageSizeChanged.bind(this);
|
|
217
|
+
this.runtime.on("SAY", this.handleSayOrThink);
|
|
218
|
+
this.runtime.on("STAGE_SIZE_CHANGED", this.handleStageSizeChanged);
|
|
219
|
+
}
|
|
220
|
+
getInfo() {
|
|
221
|
+
return {
|
|
222
|
+
id: extensionConfig.id,
|
|
223
|
+
name: Scratch.translate(block_definitions_default.extensionName),
|
|
224
|
+
docsURI: EXTENSION_DOCS_URI,
|
|
225
|
+
color1: "#9966ff",
|
|
226
|
+
blocks: blockDefinitions.map((block) => this.toScratchBlock(block)),
|
|
227
|
+
menus: definitionMenus
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
defineStyle(args) {
|
|
231
|
+
const styleName = this.normalizeStyleName(args.STYLE);
|
|
232
|
+
const definition = {
|
|
233
|
+
alignment: this.normalizeAlignment(args.ALIGN),
|
|
234
|
+
backgroundColor: this.normalizeColor(args.BACKGROUND, initialDefaultStyle.backgroundColor),
|
|
235
|
+
direction: this.normalizeDirection(args.DIRECTION),
|
|
236
|
+
font: this.normalizeFont(args.FONT),
|
|
237
|
+
fontPercent: this.normalizeFontPercent(args.SIZE),
|
|
238
|
+
textColor: this.normalizeColor(args.TEXT_COLOR, initialDefaultStyle.textColor)
|
|
239
|
+
};
|
|
240
|
+
this.styles.set(styleName, definition);
|
|
241
|
+
this.restyleVisibleBubbles(styleName, definition);
|
|
242
|
+
this.restyleTextActors(styleName);
|
|
243
|
+
}
|
|
244
|
+
setText(args, util) {
|
|
245
|
+
const selection = this.resolveStyle(args.STYLE);
|
|
246
|
+
this.applyTextActor(util.target, this.normalizeMessage(args.TEXT), selection);
|
|
247
|
+
}
|
|
248
|
+
sayWithStyle(args, util) {
|
|
249
|
+
this.showStyledBubble("say", args, util);
|
|
250
|
+
}
|
|
251
|
+
thinkWithStyle(args, util) {
|
|
252
|
+
this.showStyledBubble("think", args, util);
|
|
253
|
+
}
|
|
254
|
+
say(args, util) {
|
|
255
|
+
this.showLegacyBubble("say", args, util);
|
|
256
|
+
}
|
|
257
|
+
think(args, util) {
|
|
258
|
+
this.showLegacyBubble("think", args, util);
|
|
259
|
+
}
|
|
260
|
+
toScratchBlock(block) {
|
|
261
|
+
return {
|
|
262
|
+
opcode: block.opcode,
|
|
263
|
+
blockType: Scratch.BlockType[block.blockType],
|
|
264
|
+
text: Scratch.translate(block.text),
|
|
265
|
+
hideFromPalette: block.hideFromPalette ?? false,
|
|
266
|
+
arguments: Object.fromEntries(Object.entries(block.arguments).map(([name, argument]) => [name, {
|
|
267
|
+
type: Scratch.ArgumentType[argument.type],
|
|
268
|
+
defaultValue: argument.defaultValue,
|
|
269
|
+
...argument.menu === void 0 ? {} : { menu: argument.menu }
|
|
270
|
+
}]))
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
normalizeStyleName(value) {
|
|
274
|
+
return Scratch.Cast.toString(value).trim() || defaultStyleName;
|
|
275
|
+
}
|
|
276
|
+
normalizeFontPercent(value) {
|
|
277
|
+
if (typeof value === "string" && value.trim() === "") return defaultFontPercent;
|
|
278
|
+
const numericValue = Number(value);
|
|
279
|
+
if (!Number.isFinite(numericValue)) return defaultFontPercent;
|
|
280
|
+
return Math.min(maximumFontPercent, Math.max(minimumFontPercent, numericValue));
|
|
281
|
+
}
|
|
282
|
+
normalizeMessage(value) {
|
|
283
|
+
return Scratch.Cast.toString(value).replace(/\\r\\n|\\n|\\r/gu, "\n");
|
|
284
|
+
}
|
|
285
|
+
normalizeAlignment(value) {
|
|
286
|
+
const alignment = Scratch.Cast.toString(value).trim().toLowerCase();
|
|
287
|
+
if (alignment === "center" || alignment === "right") return alignment;
|
|
288
|
+
return "left";
|
|
289
|
+
}
|
|
290
|
+
normalizeDirection(value) {
|
|
291
|
+
const direction = Scratch.Cast.toString(value).trim().toLowerCase();
|
|
292
|
+
if (direction === "up" || direction === "up-right" || direction === "right" || direction === "down-right" || direction === "down" || direction === "down-left" || direction === "left" || direction === "up-left") return direction;
|
|
293
|
+
return initialDefaultStyle.direction;
|
|
294
|
+
}
|
|
295
|
+
normalizeColor(value, fallback) {
|
|
296
|
+
const color = Scratch.Cast.toString(value).trim();
|
|
297
|
+
if (color === "") return fallback;
|
|
298
|
+
if (/^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/iu.test(color)) return color;
|
|
299
|
+
if (globalThis.CSS?.supports?.("color", color)) return color;
|
|
300
|
+
return fallback;
|
|
301
|
+
}
|
|
302
|
+
normalizeFont(value) {
|
|
303
|
+
const font = Scratch.Cast.toString(value).trim();
|
|
304
|
+
const hasUnsafeCharacter = [...font].some((character) => {
|
|
305
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
306
|
+
return codePoint <= 31 || codePoint === 127 || ",;{}".includes(character);
|
|
307
|
+
});
|
|
308
|
+
if (font === "" || font.length > maximumFontNameLength || hasUnsafeCharacter) return initialDefaultStyle.font;
|
|
309
|
+
return font;
|
|
310
|
+
}
|
|
311
|
+
getStageScale() {
|
|
312
|
+
const nativeSize = this.runtime.renderer?.getNativeSize?.();
|
|
313
|
+
if (!Array.isArray(nativeSize) || nativeSize.length < 2) return 1;
|
|
314
|
+
const width = Number(nativeSize[0]);
|
|
315
|
+
const height = Number(nativeSize[1]);
|
|
316
|
+
if (!(width > 0) || !(height > 0)) return 1;
|
|
317
|
+
return Math.min(width / baseStageWidth, height / baseStageHeight);
|
|
318
|
+
}
|
|
319
|
+
createRenderStyle(definition) {
|
|
320
|
+
const stageScale = this.getStageScale();
|
|
321
|
+
const fontScale = stageScale * (definition.fontPercent / defaultFontPercent);
|
|
322
|
+
return {
|
|
323
|
+
maxLineWidth: baseStyle.maxLineWidth * stageScale,
|
|
324
|
+
minWidth: baseStyle.minWidth * stageScale,
|
|
325
|
+
strokeWidth: baseStyle.strokeWidth * stageScale,
|
|
326
|
+
padding: baseStyle.padding * stageScale,
|
|
327
|
+
cornerRadius: baseStyle.cornerRadius * stageScale,
|
|
328
|
+
tailHeight: baseStyle.tailHeight * stageScale,
|
|
329
|
+
font: definition.font,
|
|
330
|
+
fontSize: baseStyle.fontSize * fontScale,
|
|
331
|
+
fontHeightRatio: baseStyle.fontHeightRatio,
|
|
332
|
+
lineHeight: baseStyle.lineHeight * fontScale,
|
|
333
|
+
bubbleFill: definition.backgroundColor,
|
|
334
|
+
textFill: definition.textColor,
|
|
335
|
+
textAlign: definition.alignment
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
createTextActorSvg(text, definition) {
|
|
339
|
+
const stageScale = this.getStageScale();
|
|
340
|
+
const fontScale = stageScale * (definition.fontPercent / defaultFontPercent);
|
|
341
|
+
const fontSize = baseStyle.fontSize * fontScale;
|
|
342
|
+
const lineHeight = baseStyle.lineHeight * fontScale;
|
|
343
|
+
const padding = actorStyle.padding * stageScale;
|
|
344
|
+
const cornerRadius = actorStyle.cornerRadius * stageScale;
|
|
345
|
+
const lines = text.split("\n");
|
|
346
|
+
const contentWidth = Math.max(actorStyle.minimumWidth, ...lines.map((line) => this.measureTextWidth(line, fontSize)));
|
|
347
|
+
const width = Math.max(1, Math.ceil(contentWidth + padding * 2));
|
|
348
|
+
const height = Math.max(1, Math.ceil(lineHeight * lines.length + padding * 2));
|
|
349
|
+
const alignment = definition.alignment;
|
|
350
|
+
const textAnchor = alignment === "center" ? "middle" : alignment === "right" ? "end" : "start";
|
|
351
|
+
const x = alignment === "center" ? width / 2 : alignment === "right" ? width - padding : padding;
|
|
352
|
+
const title = this.escapeXml(text);
|
|
353
|
+
const tspans = lines.map((line, index) => {
|
|
354
|
+
const y = padding + fontSize + lineHeight * index;
|
|
355
|
+
return `<tspan x="${this.formatSvgNumber(x)}" y="${this.formatSvgNumber(y)}">${this.escapeXml(line)}</tspan>`;
|
|
356
|
+
}).join("");
|
|
357
|
+
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>`;
|
|
358
|
+
}
|
|
359
|
+
measureTextWidth(text, fontSize) {
|
|
360
|
+
let units = 0;
|
|
361
|
+
for (const character of text) {
|
|
362
|
+
if (/\p{Mark}/u.test(character)) continue;
|
|
363
|
+
if (/\s/u.test(character)) {
|
|
364
|
+
units += .35;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
368
|
+
units += codePoint <= 127 ? .62 : 1;
|
|
369
|
+
}
|
|
370
|
+
return units * fontSize;
|
|
371
|
+
}
|
|
372
|
+
escapeXml(value) {
|
|
373
|
+
return value.replace(/[&<>"']/gu, (character) => {
|
|
374
|
+
switch (character) {
|
|
375
|
+
case "&": return "&";
|
|
376
|
+
case "<": return "<";
|
|
377
|
+
case ">": return ">";
|
|
378
|
+
case "\"": return """;
|
|
379
|
+
default: return "'";
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
formatSvgNumber(value) {
|
|
384
|
+
return String(Math.round(value * 1e3) / 1e3);
|
|
385
|
+
}
|
|
386
|
+
applyTextActor(target, text, selection) {
|
|
387
|
+
const renderer = this.runtime.renderer;
|
|
388
|
+
if (typeof target.drawableID !== "number" || typeof renderer?.createSVGSkin !== "function" || typeof renderer.updateDrawableSkinId !== "function") throw new Error("SVG Text requires SVG skin APIs from TurboWarp.");
|
|
389
|
+
const skinId = renderer.createSVGSkin(this.createTextActorSvg(text, selection.definition));
|
|
390
|
+
if (typeof skinId !== "number") throw new Error("TurboWarp did not create an SVG text skin.");
|
|
391
|
+
try {
|
|
392
|
+
renderer.updateDrawableSkinId(target.drawableID, skinId);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
renderer.destroySkin?.(skinId);
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
const previous = this.textActors.get(target);
|
|
398
|
+
this.textActors.set(target, {
|
|
399
|
+
skinId,
|
|
400
|
+
styleName: selection.styleName ?? defaultStyleName,
|
|
401
|
+
text
|
|
402
|
+
});
|
|
403
|
+
if (previous && previous.skinId !== skinId) renderer.destroySkin?.(previous.skinId);
|
|
404
|
+
this.runtime.requestRedraw?.();
|
|
405
|
+
}
|
|
406
|
+
resolveStyle(value) {
|
|
407
|
+
const requestedName = this.normalizeStyleName(value);
|
|
408
|
+
const definition = this.styles.get(requestedName);
|
|
409
|
+
if (definition) return {
|
|
410
|
+
definition,
|
|
411
|
+
styleName: requestedName
|
|
412
|
+
};
|
|
413
|
+
return {
|
|
414
|
+
definition: this.styles.get(defaultStyleName) ?? initialDefaultStyle,
|
|
415
|
+
styleName: defaultStyleName
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
getBubbleState(target) {
|
|
419
|
+
const state = target.getCustomState?.(bubbleStateKey);
|
|
420
|
+
if (!state || typeof state !== "object") return null;
|
|
421
|
+
return state;
|
|
422
|
+
}
|
|
423
|
+
getTextBubbleSkin(skinId) {
|
|
424
|
+
const skins = this.runtime.renderer?._allSkins;
|
|
425
|
+
if (!skins) return null;
|
|
426
|
+
return (skins instanceof Map ? skins.get(skinId) : skins[skinId]) ?? null;
|
|
427
|
+
}
|
|
428
|
+
installAlignmentRenderer(skin) {
|
|
429
|
+
if (this.alignedSkins.has(skin)) return;
|
|
430
|
+
const originalRender = skin._renderTextBubble;
|
|
431
|
+
if (typeof originalRender !== "function") return;
|
|
432
|
+
skin._renderTextBubble = function renderAlignedTextBubble(scale) {
|
|
433
|
+
const style = this._style;
|
|
434
|
+
const alignment = style?.textAlign;
|
|
435
|
+
const textFill = style?.textFill;
|
|
436
|
+
if (!style || alignment !== "center" && alignment !== "right" || typeof textFill !== "string") {
|
|
437
|
+
originalRender.call(this, scale);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
style.textFill = "transparent";
|
|
441
|
+
try {
|
|
442
|
+
originalRender.call(this, scale);
|
|
443
|
+
} finally {
|
|
444
|
+
style.textFill = textFill;
|
|
445
|
+
}
|
|
446
|
+
const context = this._canvas?.getContext("2d");
|
|
447
|
+
const lines = this._lines;
|
|
448
|
+
const width = this._textAreaSize?.width;
|
|
449
|
+
const padding = style.padding;
|
|
450
|
+
const lineHeight = style.lineHeight;
|
|
451
|
+
const fontHeightRatio = style.fontHeightRatio;
|
|
452
|
+
const fontSize = style.fontSize;
|
|
453
|
+
if (!context || !lines || typeof width !== "number" || typeof padding !== "number" || typeof lineHeight !== "number" || typeof fontHeightRatio !== "number" || typeof fontSize !== "number") return;
|
|
454
|
+
context.save();
|
|
455
|
+
context.fillStyle = textFill;
|
|
456
|
+
context.textAlign = alignment;
|
|
457
|
+
const x = alignment === "center" ? width / 2 : width - padding;
|
|
458
|
+
for (let lineNumber = 0; lineNumber < lines.length; lineNumber += 1) {
|
|
459
|
+
const line = lines[lineNumber];
|
|
460
|
+
if (line === void 0) continue;
|
|
461
|
+
context.fillText(line, x, padding + lineHeight * lineNumber + fontHeightRatio * fontSize);
|
|
462
|
+
}
|
|
463
|
+
context.restore();
|
|
464
|
+
};
|
|
465
|
+
this.alignedSkins.add(skin);
|
|
466
|
+
}
|
|
467
|
+
installTargetPositionHook(target) {
|
|
468
|
+
const currentHook = target.onTargetVisualChange;
|
|
469
|
+
if (currentHook === this.targetPositionHooks.get(target)) return;
|
|
470
|
+
const originalHook = typeof currentHook === "function" ? currentHook : void 0;
|
|
471
|
+
const positionHook = (changedTarget) => {
|
|
472
|
+
originalHook?.(changedTarget);
|
|
473
|
+
this.positionBubble(target);
|
|
474
|
+
};
|
|
475
|
+
this.targetPositionHooks.set(target, positionHook);
|
|
476
|
+
target.onTargetVisualChange = positionHook;
|
|
477
|
+
}
|
|
478
|
+
positionBubble(target) {
|
|
479
|
+
if (target.visible === false) return;
|
|
480
|
+
const bubbleState = this.getBubbleState(target);
|
|
481
|
+
const renderer = this.runtime.renderer;
|
|
482
|
+
if (!bubbleState || typeof bubbleState.drawableId !== "number" || typeof renderer?.getCurrentSkinSize !== "function" || typeof renderer.updateDrawablePosition !== "function") return;
|
|
483
|
+
const size = renderer.getCurrentSkinSize(bubbleState.drawableId);
|
|
484
|
+
if (!Array.isArray(size) || size.length < 2) return;
|
|
485
|
+
const bubbleWidth = Number(size[0]);
|
|
486
|
+
const bubbleHeight = Number(size[1]);
|
|
487
|
+
if (!(bubbleWidth > 0) || !(bubbleHeight > 0)) return;
|
|
488
|
+
let targetBounds;
|
|
489
|
+
try {
|
|
490
|
+
targetBounds = target.getBoundsForBubble?.() ?? {
|
|
491
|
+
bottom: target.y ?? 0,
|
|
492
|
+
left: target.x ?? 0,
|
|
493
|
+
right: target.x ?? 0,
|
|
494
|
+
top: target.y ?? 0
|
|
495
|
+
};
|
|
496
|
+
} catch {
|
|
497
|
+
targetBounds = {
|
|
498
|
+
bottom: target.y ?? 0,
|
|
499
|
+
left: target.x ?? 0,
|
|
500
|
+
right: target.x ?? 0,
|
|
501
|
+
top: target.y ?? 0
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const nativeSize = renderer.getNativeSize?.();
|
|
505
|
+
if (!Array.isArray(nativeSize) || nativeSize.length < 2) return;
|
|
506
|
+
const stageWidth = Number(nativeSize[0]);
|
|
507
|
+
const stageHeight = Number(nativeSize[1]);
|
|
508
|
+
if (!(stageWidth > 0) || !(stageHeight > 0)) return;
|
|
509
|
+
const direction = this.activeStyles.get(target)?.definition.direction ?? initialDefaultStyle.direction;
|
|
510
|
+
const centerX = (targetBounds.left + targetBounds.right) / 2;
|
|
511
|
+
const centerY = (targetBounds.top + targetBounds.bottom) / 2;
|
|
512
|
+
const gap = baseStyle.tailHeight * this.getStageScale();
|
|
513
|
+
let x = centerX - bubbleWidth / 2;
|
|
514
|
+
let y = targetBounds.top + gap + bubbleHeight;
|
|
515
|
+
switch (direction) {
|
|
516
|
+
case "up-right":
|
|
517
|
+
x = targetBounds.right + gap;
|
|
518
|
+
break;
|
|
519
|
+
case "right":
|
|
520
|
+
x = targetBounds.right + gap;
|
|
521
|
+
y = centerY + bubbleHeight / 2;
|
|
522
|
+
break;
|
|
523
|
+
case "down-right":
|
|
524
|
+
x = targetBounds.right + gap;
|
|
525
|
+
y = targetBounds.bottom - gap;
|
|
526
|
+
break;
|
|
527
|
+
case "down":
|
|
528
|
+
y = targetBounds.bottom - gap;
|
|
529
|
+
break;
|
|
530
|
+
case "down-left":
|
|
531
|
+
x = targetBounds.left - gap - bubbleWidth;
|
|
532
|
+
y = targetBounds.bottom - gap;
|
|
533
|
+
break;
|
|
534
|
+
case "left":
|
|
535
|
+
x = targetBounds.left - gap - bubbleWidth;
|
|
536
|
+
y = centerY + bubbleHeight / 2;
|
|
537
|
+
break;
|
|
538
|
+
case "up-left": x = targetBounds.left - gap - bubbleWidth;
|
|
539
|
+
}
|
|
540
|
+
if (direction.endsWith("right") && !bubbleState.onSpriteRight) {
|
|
541
|
+
bubbleState.onSpriteRight = true;
|
|
542
|
+
this.updateTextSkin(bubbleState);
|
|
543
|
+
} else if (direction.endsWith("left") && bubbleState.onSpriteRight) {
|
|
544
|
+
bubbleState.onSpriteRight = false;
|
|
545
|
+
this.updateTextSkin(bubbleState);
|
|
546
|
+
}
|
|
547
|
+
const stageLeft = -stageWidth / 2;
|
|
548
|
+
const stageRight = stageWidth / 2;
|
|
549
|
+
const stageTop = stageHeight / 2;
|
|
550
|
+
const stageBottom = -stageHeight / 2;
|
|
551
|
+
x = this.clampPosition(x, stageLeft, stageRight - bubbleWidth);
|
|
552
|
+
y = this.clampPosition(y, stageBottom + bubbleHeight, stageTop);
|
|
553
|
+
renderer.updateDrawablePosition(bubbleState.drawableId, [x, y]);
|
|
554
|
+
}
|
|
555
|
+
updateTextSkin(bubbleState) {
|
|
556
|
+
if (typeof bubbleState.skinId !== "number") return;
|
|
557
|
+
this.runtime.renderer?.updateTextSkin?.(bubbleState.skinId, bubbleState.type, bubbleState.text, bubbleState.onSpriteRight, [0, 0]);
|
|
558
|
+
}
|
|
559
|
+
clampPosition(value, minimum, maximum) {
|
|
560
|
+
if (maximum < minimum) return minimum;
|
|
561
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
562
|
+
}
|
|
563
|
+
applyBubbleStyle(target, type, text, selection) {
|
|
564
|
+
const bubbleState = this.getBubbleState(target);
|
|
565
|
+
if (!bubbleState || typeof bubbleState.skinId !== "number") return;
|
|
566
|
+
const normalizedText = this.normalizeMessage(text);
|
|
567
|
+
if (bubbleState.text !== normalizedText) {
|
|
568
|
+
bubbleState.text = normalizedText;
|
|
569
|
+
this.runtime.renderer?.updateTextSkin?.(bubbleState.skinId, type, normalizedText, bubbleState.onSpriteRight, [0, 0]);
|
|
570
|
+
}
|
|
571
|
+
const skin = this.getTextBubbleSkin(bubbleState.skinId);
|
|
572
|
+
if (typeof skin?.setStyle === "function") {
|
|
573
|
+
this.installAlignmentRenderer(skin);
|
|
574
|
+
skin.setStyle(this.createRenderStyle(selection.definition));
|
|
575
|
+
}
|
|
576
|
+
this.installTargetPositionHook(target);
|
|
577
|
+
target.onTargetVisualChange?.(target);
|
|
578
|
+
this.runtime.requestRedraw?.();
|
|
579
|
+
}
|
|
580
|
+
handleSayOrThink(target, type, text) {
|
|
581
|
+
const selection = this.pendingStyles.get(target) ?? this.resolveStyle(defaultStyleName);
|
|
582
|
+
this.pendingStyles.delete(target);
|
|
583
|
+
const normalizedText = this.normalizeMessage(this.getBubbleState(target)?.text ?? text);
|
|
584
|
+
if (normalizedText === "") {
|
|
585
|
+
this.activeStyles.delete(target);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
this.activeStyles.set(target, selection);
|
|
589
|
+
this.applyBubbleStyle(target, type, normalizedText, selection);
|
|
590
|
+
}
|
|
591
|
+
handleStageSizeChanged() {
|
|
592
|
+
for (const target of this.runtime.targets ?? []) {
|
|
593
|
+
const bubbleState = this.getBubbleState(target);
|
|
594
|
+
if (!bubbleState?.text) continue;
|
|
595
|
+
const selection = this.activeStyles.get(target) ?? this.resolveStyle(defaultStyleName);
|
|
596
|
+
this.applyBubbleStyle(target, bubbleState.type, bubbleState.text, selection);
|
|
597
|
+
}
|
|
598
|
+
for (const [target, state] of [...this.textActors]) this.applyTextActor(target, state.text, this.resolveStyle(state.styleName));
|
|
599
|
+
}
|
|
600
|
+
restyleVisibleBubbles(styleName, definition) {
|
|
601
|
+
for (const target of this.runtime.targets ?? []) {
|
|
602
|
+
const selection = this.activeStyles.get(target);
|
|
603
|
+
const bubbleState = this.getBubbleState(target);
|
|
604
|
+
if (selection?.styleName !== styleName || !bubbleState?.text) continue;
|
|
605
|
+
const nextSelection = {
|
|
606
|
+
definition,
|
|
607
|
+
styleName
|
|
608
|
+
};
|
|
609
|
+
this.activeStyles.set(target, nextSelection);
|
|
610
|
+
this.applyBubbleStyle(target, bubbleState.type, bubbleState.text, nextSelection);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
restyleTextActors(styleName) {
|
|
614
|
+
const definition = this.styles.get(styleName);
|
|
615
|
+
if (!definition) return;
|
|
616
|
+
for (const [target, state] of [...this.textActors]) {
|
|
617
|
+
if (state.styleName !== styleName) continue;
|
|
618
|
+
this.applyTextActor(target, state.text, {
|
|
619
|
+
definition,
|
|
620
|
+
styleName
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
showStyledBubble(type, args, util) {
|
|
625
|
+
this.showBubble(type, args.MESSAGE, this.resolveStyle(args.STYLE), util);
|
|
626
|
+
}
|
|
627
|
+
showLegacyBubble(type, args, util) {
|
|
628
|
+
const defaultDefinition = this.styles.get(defaultStyleName) ?? initialDefaultStyle;
|
|
629
|
+
this.showBubble(type, args.MESSAGE, {
|
|
630
|
+
definition: {
|
|
631
|
+
...defaultDefinition,
|
|
632
|
+
fontPercent: this.normalizeFontPercent(args.SIZE)
|
|
633
|
+
},
|
|
634
|
+
styleName: null
|
|
635
|
+
}, util);
|
|
636
|
+
}
|
|
637
|
+
showBubble(type, messageValue, selection, util) {
|
|
638
|
+
const message = this.normalizeMessage(messageValue);
|
|
639
|
+
this.pendingStyles.set(util.target, selection);
|
|
640
|
+
try {
|
|
641
|
+
this.runtime.emit("SAY", util.target, type, message);
|
|
642
|
+
} finally {
|
|
643
|
+
this.pendingStyles.delete(util.target);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/index.ts
|
|
649
|
+
if (extensionConfig.unsandboxed && !Scratch.extensions.unsandboxed) throw new Error(`${extensionConfig.name} must run unsandboxed.`);
|
|
650
|
+
Scratch.extensions.register(new SvgTextExtension());
|
|
651
|
+
//#endregion
|
|
652
|
+
|
|
653
|
+
})(Scratch);
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kubohiroya/turbowarp-svg-text",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Responsive named-style bubbles and SVG text actors for TurboWarp.",
|
|
5
|
+
"author": "Hiroya Kubo <hiroya@cuc.ac.jp>",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MPL-2.0",
|
|
8
|
+
"homepage": "https://github.com/kubohiroya/turbowarp-svg-text#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/kubohiroya/turbowarp-svg-text.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/kubohiroya/turbowarp-svg-text/issues"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"main": "./dist/svg-text.js",
|
|
22
|
+
"unpkg": "./dist/svg-text.js",
|
|
23
|
+
"jsdelivr": "./dist/svg-text.js",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"registry": "https://registry.npmjs.org/"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@eslint/js": "^10.0.1",
|
|
30
|
+
"@kubohiroya/vite-plugin-turbowarp-extension": "0.1.1",
|
|
31
|
+
"@types/node": "^26.1.2",
|
|
32
|
+
"eslint": "^10.8.0",
|
|
33
|
+
"prettier": "^3.9.6",
|
|
34
|
+
"typescript": "^6.0.3",
|
|
35
|
+
"typescript-eslint": "^8.66.0",
|
|
36
|
+
"vite": "^8.2.0",
|
|
37
|
+
"vitest": "^4.1.10"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"turbowarp",
|
|
41
|
+
"scratch",
|
|
42
|
+
"extension",
|
|
43
|
+
"speech-bubble",
|
|
44
|
+
"svg-text",
|
|
45
|
+
"text-actor",
|
|
46
|
+
"named-styles",
|
|
47
|
+
"multiline"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
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
|
+
}
|