minimojs 1.0.0-alpha.6 → 1.0.0-alpha.7
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 +1 -0
- package/dist/internal/AssetSystem.d.ts +1 -0
- package/dist/internal/AssetSystem.js +64 -0
- package/dist/internal/BackgroundSystem.d.ts +1 -0
- package/dist/internal/BackgroundSystem.js +25 -0
- package/dist/internal/ExplosionSystem.js +6 -5
- package/dist/internal/InputSystem.js +5 -4
- package/dist/internal/PhysicsSystem.js +14 -10
- package/dist/internal/RenderSystem.js +239 -26
- package/dist/minimo.d.ts +333 -87
- package/dist/minimo.js +505 -86
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ Note: `drawText()` uses `"Press Start 2P", monospace`. Load the font in your HTM
|
|
|
70
70
|
- `examples/space-invader/`
|
|
71
71
|
- `examples/super-minimo-bros/`
|
|
72
72
|
- `examples/scale-shift/`
|
|
73
|
+
- `examples/background-desert/`
|
|
73
74
|
- `examples/animations/`
|
|
74
75
|
|
|
75
76
|
Run locally from the `minimojs` directory:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export class AssetSystem {
|
|
3
|
+
constructor() {
|
|
4
|
+
this._queuedImages = new Map();
|
|
5
|
+
this._loadedImages = new Map();
|
|
6
|
+
this._imageSources = new Map();
|
|
7
|
+
}
|
|
8
|
+
queueImage(key, src) {
|
|
9
|
+
const safeKey = key.trim();
|
|
10
|
+
const safeSrc = src.trim();
|
|
11
|
+
if (safeKey.length === 0) {
|
|
12
|
+
throw new Error("MinimoJS: Image keys must be non-empty strings.");
|
|
13
|
+
}
|
|
14
|
+
if (safeSrc.length === 0) {
|
|
15
|
+
throw new Error("MinimoJS: Image sources must be non-empty strings.");
|
|
16
|
+
}
|
|
17
|
+
const existingSrc = this._imageSources.get(safeKey);
|
|
18
|
+
if (existingSrc !== undefined && existingSrc !== safeSrc) {
|
|
19
|
+
throw new Error(`MinimoJS: Image key "${safeKey}" is already registered with a different source.`);
|
|
20
|
+
}
|
|
21
|
+
this._imageSources.set(safeKey, safeSrc);
|
|
22
|
+
this._queuedImages.set(safeKey, safeSrc);
|
|
23
|
+
}
|
|
24
|
+
getImage(key) {
|
|
25
|
+
return this._loadedImages.get(key);
|
|
26
|
+
}
|
|
27
|
+
hasImage(key) {
|
|
28
|
+
return this._loadedImages.has(key);
|
|
29
|
+
}
|
|
30
|
+
getQueuedImageCount() {
|
|
31
|
+
let total = 0;
|
|
32
|
+
for (const key of this._queuedImages.keys()) {
|
|
33
|
+
if (!this._loadedImages.has(key)) {
|
|
34
|
+
total++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return total;
|
|
38
|
+
}
|
|
39
|
+
async loadQueuedImages(onProgress) {
|
|
40
|
+
const entries = [...this._queuedImages.entries()].filter(([key]) => !this._loadedImages.has(key));
|
|
41
|
+
const total = entries.length;
|
|
42
|
+
let loaded = 0;
|
|
43
|
+
onProgress?.(0, total, null);
|
|
44
|
+
if (total === 0)
|
|
45
|
+
return;
|
|
46
|
+
await Promise.all(entries.map(async ([key, src]) => {
|
|
47
|
+
const image = await this.loadImage(src);
|
|
48
|
+
this._loadedImages.set(key, image);
|
|
49
|
+
loaded += 1;
|
|
50
|
+
onProgress?.(loaded, total, key);
|
|
51
|
+
}));
|
|
52
|
+
this._queuedImages.clear();
|
|
53
|
+
}
|
|
54
|
+
loadImage(src) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const image = new Image();
|
|
57
|
+
image.onload = () => resolve(image);
|
|
58
|
+
image.onerror = () => {
|
|
59
|
+
reject(new Error(`MinimoJS: Failed to load image "${src}".`));
|
|
60
|
+
};
|
|
61
|
+
image.src = src;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export class BackgroundSystem {
|
|
3
|
+
constructor() {
|
|
4
|
+
this._layers = [];
|
|
5
|
+
}
|
|
6
|
+
add(layer) {
|
|
7
|
+
this._layers.push(layer);
|
|
8
|
+
return layer;
|
|
9
|
+
}
|
|
10
|
+
destroyLayer(layer) {
|
|
11
|
+
const index = this._layers.indexOf(layer);
|
|
12
|
+
if (index !== -1) {
|
|
13
|
+
this._layers.splice(index, 1);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
getLayers() {
|
|
17
|
+
return [...this._layers];
|
|
18
|
+
}
|
|
19
|
+
getMutableLayers() {
|
|
20
|
+
return this._layers;
|
|
21
|
+
}
|
|
22
|
+
clearAll() {
|
|
23
|
+
this._layers = [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -32,16 +32,17 @@ export class ExplosionSystem {
|
|
|
32
32
|
const offsetX = this.getSafeNumber(renderData?.offsetX, 0);
|
|
33
33
|
const offsetY = this.getSafeNumber(renderData?.offsetY, 0);
|
|
34
34
|
const alphaMultiplier = this.getSafeNumber(renderData?.alphaMultiplier, 1);
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
35
|
+
const baseDisplayWidth = sprite.displayWidth;
|
|
36
|
+
const baseDisplayHeight = sprite.displayHeight;
|
|
37
|
+
const pivotOffsetX = (pivotX - 0.5) * baseDisplayWidth * (1 - deformScaleX);
|
|
38
|
+
const pivotOffsetY = (pivotY - 0.5) * baseDisplayHeight * (1 - deformScaleY);
|
|
38
39
|
const signedScaleX = this.getSafeScale(sprite.scale) * deformScaleX * (sprite.flipX ? -1 : 1);
|
|
39
40
|
const signedScaleY = this.getSafeScale(sprite.scale) * deformScaleY * (sprite.flipY ? -1 : 1);
|
|
40
41
|
const absScaleX = Math.abs(signedScaleX);
|
|
41
42
|
const absScaleY = Math.abs(signedScaleY);
|
|
42
43
|
const rotationRad = (sprite.rotation * Math.PI) / 180;
|
|
43
|
-
const baseX = sprite.
|
|
44
|
-
const baseY = sprite.
|
|
44
|
+
const baseX = sprite.renderX + pivotOffsetX + offsetX;
|
|
45
|
+
const baseY = sprite.renderY + pivotOffsetY + offsetY;
|
|
45
46
|
const baseAlpha = Math.max(0, Math.min(1, sprite.alpha * alphaMultiplier));
|
|
46
47
|
const pieces = [];
|
|
47
48
|
for (let row = 0; row < rows; row++) {
|
|
@@ -187,10 +187,11 @@ export class InputSystem {
|
|
|
187
187
|
}
|
|
188
188
|
isScreenPointOverSprite(x, y, sprite, radiusScale) {
|
|
189
189
|
const safeScale = Math.max(0, radiusScale);
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
190
|
+
const halfWidth = (sprite.displayWidth * safeScale) / 2;
|
|
191
|
+
const halfHeight = (sprite.displayHeight * safeScale) / 2;
|
|
192
|
+
const drawX = sprite.ignoreScroll ? sprite.renderX : sprite.renderX - this.game.scrollX;
|
|
193
|
+
const drawY = sprite.ignoreScroll ? sprite.renderY : sprite.renderY - this.game.scrollY;
|
|
194
|
+
return Math.abs(x - drawX) <= halfWidth && Math.abs(y - drawY) <= halfHeight;
|
|
194
195
|
}
|
|
195
196
|
collectPointers() {
|
|
196
197
|
const pointers = [];
|
|
@@ -37,10 +37,12 @@ export class PhysicsSystem {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
overlap(a, b) {
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
const halfWidthA = a.displayWidth / 2;
|
|
41
|
+
const halfHeightA = a.displayHeight / 2;
|
|
42
|
+
const halfWidthB = b.displayWidth / 2;
|
|
43
|
+
const halfHeightB = b.displayHeight / 2;
|
|
44
|
+
return (Math.abs(a.renderX - b.renderX) < halfWidthA + halfWidthB &&
|
|
45
|
+
Math.abs(a.renderY - b.renderY) < halfHeightA + halfHeightB);
|
|
44
46
|
}
|
|
45
47
|
overlapAny(listA, listB) {
|
|
46
48
|
for (const a of listA) {
|
|
@@ -98,12 +100,14 @@ export class PhysicsSystem {
|
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
getCollisionResolution(a, b) {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
103
|
+
const halfWidthA = a.displayWidth / 2;
|
|
104
|
+
const halfHeightA = a.displayHeight / 2;
|
|
105
|
+
const halfWidthB = b.displayWidth / 2;
|
|
106
|
+
const halfHeightB = b.displayHeight / 2;
|
|
107
|
+
const dx = b.renderX - a.renderX;
|
|
108
|
+
const dy = b.renderY - a.renderY;
|
|
109
|
+
const overlapX = halfWidthA + halfWidthB - Math.abs(dx);
|
|
110
|
+
const overlapY = halfHeightA + halfHeightB - Math.abs(dy);
|
|
107
111
|
if (overlapX <= 0 || overlapY <= 0) {
|
|
108
112
|
return null;
|
|
109
113
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/** @internal */
|
|
2
2
|
export class RenderSystem {
|
|
3
3
|
constructor() {
|
|
4
|
-
this.
|
|
4
|
+
this._surfaceCache = new Map();
|
|
5
5
|
this._lastAppliedPageBackground = undefined;
|
|
6
6
|
}
|
|
7
7
|
clearSpriteCache() {
|
|
8
|
-
this.
|
|
8
|
+
this._surfaceCache.clear();
|
|
9
9
|
}
|
|
10
10
|
getSpriteGlyphCanvasForEffects(sprite) {
|
|
11
|
-
return this.
|
|
11
|
+
return this.getRenderSurface(sprite);
|
|
12
12
|
}
|
|
13
13
|
getSpriteRenderSnapshot(sprite) {
|
|
14
|
+
const canvas = this.getRenderSurface(sprite);
|
|
14
15
|
const renderData = sprite._renderData;
|
|
15
16
|
const deformScaleX = this.getSafeRenderScale(renderData?.scaleX);
|
|
16
17
|
const deformScaleY = this.getSafeRenderScale(renderData?.scaleY);
|
|
@@ -22,14 +23,14 @@ export class RenderSystem {
|
|
|
22
23
|
const safeRenderScale = this.getSafeRenderScale(sprite.scale);
|
|
23
24
|
const finalScaleX = safeRenderScale * deformScaleX;
|
|
24
25
|
const finalScaleY = safeRenderScale * deformScaleY;
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
26
|
+
const baseDisplayWidth = sprite.displayWidth;
|
|
27
|
+
const baseDisplayHeight = sprite.displayHeight;
|
|
28
|
+
const pivotOffsetX = (pivotX - 0.5) * baseDisplayWidth * (1 - deformScaleX);
|
|
29
|
+
const pivotOffsetY = (pivotY - 0.5) * baseDisplayHeight * (1 - deformScaleY);
|
|
29
30
|
return {
|
|
30
31
|
canvas,
|
|
31
|
-
x: sprite.
|
|
32
|
-
y: sprite.
|
|
32
|
+
x: sprite.renderX + pivotOffsetX + offsetX,
|
|
33
|
+
y: sprite.renderY + pivotOffsetY + offsetY,
|
|
33
34
|
rotation: sprite.rotation,
|
|
34
35
|
alpha: Math.max(0, Math.min(1, sprite.alpha * alphaMultiplier)),
|
|
35
36
|
flipX: sprite.flipX,
|
|
@@ -45,17 +46,15 @@ export class RenderSystem {
|
|
|
45
46
|
const W = options.canvas.width;
|
|
46
47
|
const H = options.canvas.height;
|
|
47
48
|
this.applyPageBackground(options.pageBackground);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
ctx.fillStyle = options.background;
|
|
58
|
-
ctx.fillRect(0, 0, W, H);
|
|
49
|
+
this.paintCanvasBackground(ctx, W, H, options.background, options.backgroundGradient);
|
|
50
|
+
const backgrounds = [...options.backgroundLayers].sort((a, b) => a.layer - b.layer);
|
|
51
|
+
for (const layer of backgrounds) {
|
|
52
|
+
if (!layer.visible)
|
|
53
|
+
continue;
|
|
54
|
+
const image = options.resolveImage(layer.imageKey);
|
|
55
|
+
if (!image)
|
|
56
|
+
continue;
|
|
57
|
+
this.drawBackgroundLayer(ctx, options.canvas, layer, image);
|
|
59
58
|
}
|
|
60
59
|
const sorted = [...options.sprites].sort((a, b) => a.layer - b.layer);
|
|
61
60
|
const trailEntries = [...options.trails].sort((a, b) => a.layer - b.layer);
|
|
@@ -124,13 +123,49 @@ export class RenderSystem {
|
|
|
124
123
|
ctx.restore();
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
const
|
|
126
|
+
renderLoadingScreen(options) {
|
|
127
|
+
const ctx = options.context;
|
|
128
|
+
const W = options.canvas.width;
|
|
129
|
+
const H = options.canvas.height;
|
|
130
|
+
const safeTotal = Math.max(0, options.total);
|
|
131
|
+
const safeLoaded = Math.max(0, Math.min(options.loaded, safeTotal));
|
|
132
|
+
const progress = safeTotal === 0 ? 1 : safeLoaded / safeTotal;
|
|
133
|
+
const barWidth = Math.min(420, Math.max(220, Math.floor(W * 0.46)));
|
|
134
|
+
const barHeight = 18;
|
|
135
|
+
const barX = Math.round((W - barWidth) / 2);
|
|
136
|
+
const barY = Math.round(H / 2 + 24);
|
|
137
|
+
this.applyPageBackground(options.pageBackground);
|
|
138
|
+
this.paintCanvasBackground(ctx, W, H, options.background ?? "#101820", options.backgroundGradient);
|
|
139
|
+
ctx.save();
|
|
140
|
+
ctx.fillStyle = "rgba(8, 18, 28, 0.76)";
|
|
141
|
+
ctx.fillRect(0, 0, W, H);
|
|
142
|
+
ctx.font = `26px "Press Start 2P", monospace`;
|
|
143
|
+
ctx.textAlign = "center";
|
|
144
|
+
ctx.textBaseline = "middle";
|
|
145
|
+
ctx.fillStyle = "#ffffff";
|
|
146
|
+
ctx.fillText("Loading...", W / 2, H / 2 - 28);
|
|
147
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.16)";
|
|
148
|
+
ctx.fillRect(barX, barY, barWidth, barHeight);
|
|
149
|
+
ctx.fillStyle = "#f7fbff";
|
|
150
|
+
ctx.fillRect(barX, barY, Math.round(barWidth * progress), barHeight);
|
|
151
|
+
ctx.strokeStyle = "rgba(255, 255, 255, 0.35)";
|
|
152
|
+
ctx.strokeRect(barX, barY, barWidth, barHeight);
|
|
153
|
+
ctx.restore();
|
|
154
|
+
}
|
|
155
|
+
getRenderSurface(sprite) {
|
|
156
|
+
const cacheKey = sprite.getRenderCacheKey();
|
|
157
|
+
const cached = this._surfaceCache.get(cacheKey);
|
|
132
158
|
if (cached)
|
|
133
159
|
return cached;
|
|
160
|
+
const surface = this.isTextSprite(sprite)
|
|
161
|
+
? this.createTextSurface(sprite)
|
|
162
|
+
: this.createEmojiSurface(this.asEmojiSprite(sprite));
|
|
163
|
+
this._surfaceCache.set(cacheKey, surface);
|
|
164
|
+
return surface;
|
|
165
|
+
}
|
|
166
|
+
createEmojiSurface(sprite) {
|
|
167
|
+
const size = Math.max(1, Math.round(sprite.size));
|
|
168
|
+
const color = sprite.color;
|
|
134
169
|
const glyphCanvas = document.createElement("canvas");
|
|
135
170
|
const boxSize = Math.max(2, Math.ceil(size * 2));
|
|
136
171
|
glyphCanvas.width = boxSize;
|
|
@@ -149,9 +184,127 @@ export class RenderSystem {
|
|
|
149
184
|
glyphCtx.textAlign = "center";
|
|
150
185
|
glyphCtx.textBaseline = "middle";
|
|
151
186
|
glyphCtx.fillText(sprite.sprite, boxSize / 2, boxSize / 2);
|
|
152
|
-
this._spriteGlyphCache.set(cacheKey, glyphCanvas);
|
|
153
187
|
return glyphCanvas;
|
|
154
188
|
}
|
|
189
|
+
createTextSurface(sprite) {
|
|
190
|
+
const canvas = document.createElement("canvas");
|
|
191
|
+
const ctx = canvas.getContext("2d");
|
|
192
|
+
if (!ctx) {
|
|
193
|
+
throw new Error("MinimoJS: Could not acquire a text rendering context.");
|
|
194
|
+
}
|
|
195
|
+
const lines = this.layoutTextLines(ctx, sprite);
|
|
196
|
+
const measuredWidth = Math.max(1, Math.ceil(lines.reduce((maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width), 0)));
|
|
197
|
+
const lineHeightPx = Math.max(1, Math.ceil(sprite.fontSize * sprite.lineHeight));
|
|
198
|
+
const contentHeight = Math.max(1, lineHeightPx * Math.max(lines.length, 1));
|
|
199
|
+
const borderPad = Math.max(0, sprite.borderWidth) * 2;
|
|
200
|
+
const totalWidth = Math.max(1, Math.ceil(measuredWidth + sprite.paddingX * 2 + borderPad));
|
|
201
|
+
const totalHeight = Math.max(1, Math.ceil(contentHeight + sprite.paddingY * 2 + borderPad));
|
|
202
|
+
sprite._measuredWidth = totalWidth;
|
|
203
|
+
sprite._measuredHeight = totalHeight;
|
|
204
|
+
canvas.width = totalWidth;
|
|
205
|
+
canvas.height = totalHeight;
|
|
206
|
+
const drawCtx = canvas.getContext("2d");
|
|
207
|
+
if (!drawCtx) {
|
|
208
|
+
throw new Error("MinimoJS: Could not acquire a text rendering context.");
|
|
209
|
+
}
|
|
210
|
+
this.applyTextStyle(drawCtx, sprite);
|
|
211
|
+
if (sprite.backgroundColor !== null) {
|
|
212
|
+
drawCtx.fillStyle = sprite.backgroundColor;
|
|
213
|
+
this.fillRoundedRect(drawCtx, 0, 0, totalWidth, totalHeight, sprite.cornerRadius);
|
|
214
|
+
}
|
|
215
|
+
if (sprite.borderColor !== null && sprite.borderWidth > 0) {
|
|
216
|
+
drawCtx.strokeStyle = sprite.borderColor;
|
|
217
|
+
drawCtx.lineWidth = sprite.borderWidth;
|
|
218
|
+
this.strokeRoundedRect(drawCtx, sprite.borderWidth / 2, sprite.borderWidth / 2, totalWidth - sprite.borderWidth, totalHeight - sprite.borderWidth, sprite.cornerRadius);
|
|
219
|
+
}
|
|
220
|
+
drawCtx.fillStyle = sprite.color;
|
|
221
|
+
drawCtx.textAlign = sprite.textAlign;
|
|
222
|
+
drawCtx.textBaseline = "middle";
|
|
223
|
+
const contentLeft = sprite.paddingX + sprite.borderWidth;
|
|
224
|
+
const contentRight = totalWidth - sprite.paddingX - sprite.borderWidth;
|
|
225
|
+
const contentCenter = totalWidth / 2;
|
|
226
|
+
const startY = sprite.paddingY + sprite.borderWidth + lineHeightPx / 2;
|
|
227
|
+
const drawX = sprite.textAlign === "left"
|
|
228
|
+
? contentLeft
|
|
229
|
+
: sprite.textAlign === "right"
|
|
230
|
+
? contentRight
|
|
231
|
+
: contentCenter;
|
|
232
|
+
for (let i = 0; i < lines.length; i++) {
|
|
233
|
+
drawCtx.fillText(lines[i], drawX, startY + i * lineHeightPx);
|
|
234
|
+
}
|
|
235
|
+
return canvas;
|
|
236
|
+
}
|
|
237
|
+
isTextSprite(sprite) {
|
|
238
|
+
return "text" in sprite && "fontFamily" in sprite && "fontSize" in sprite;
|
|
239
|
+
}
|
|
240
|
+
asEmojiSprite(sprite) {
|
|
241
|
+
if ("sprite" in sprite && "size" in sprite) {
|
|
242
|
+
return sprite;
|
|
243
|
+
}
|
|
244
|
+
throw new Error("MinimoJS: Unsupported sprite type for emoji rendering.");
|
|
245
|
+
}
|
|
246
|
+
layoutTextLines(ctx, sprite) {
|
|
247
|
+
this.applyTextStyle(ctx, sprite);
|
|
248
|
+
const rawLines = sprite.text.split("\n");
|
|
249
|
+
if (sprite.maxWidth <= 0) {
|
|
250
|
+
return rawLines.length > 0 ? rawLines : [""];
|
|
251
|
+
}
|
|
252
|
+
const maxWidth = Math.max(1, sprite.maxWidth - sprite.paddingX * 2);
|
|
253
|
+
const wrappedLines = [];
|
|
254
|
+
for (const rawLine of rawLines) {
|
|
255
|
+
const words = rawLine.split(/\s+/).filter((word) => word.length > 0);
|
|
256
|
+
if (words.length === 0) {
|
|
257
|
+
wrappedLines.push("");
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
let currentLine = words[0];
|
|
261
|
+
for (let i = 1; i < words.length; i++) {
|
|
262
|
+
const candidate = `${currentLine} ${words[i]}`;
|
|
263
|
+
if (ctx.measureText(candidate).width <= maxWidth) {
|
|
264
|
+
currentLine = candidate;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
wrappedLines.push(currentLine);
|
|
268
|
+
currentLine = words[i];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
wrappedLines.push(currentLine);
|
|
272
|
+
}
|
|
273
|
+
return wrappedLines.length > 0 ? wrappedLines : [""];
|
|
274
|
+
}
|
|
275
|
+
applyTextStyle(ctx, sprite) {
|
|
276
|
+
ctx.font = `${sprite.fontWeight} ${sprite.fontSize}px ${sprite.fontFamily}`;
|
|
277
|
+
ctx.shadowColor = "transparent";
|
|
278
|
+
ctx.shadowBlur = 0;
|
|
279
|
+
ctx.shadowOffsetX = 0;
|
|
280
|
+
ctx.shadowOffsetY = 0;
|
|
281
|
+
}
|
|
282
|
+
fillRoundedRect(ctx, x, y, width, height, radius) {
|
|
283
|
+
this.buildRoundedRectPath(ctx, x, y, width, height, radius);
|
|
284
|
+
ctx.fill();
|
|
285
|
+
}
|
|
286
|
+
strokeRoundedRect(ctx, x, y, width, height, radius) {
|
|
287
|
+
this.buildRoundedRectPath(ctx, x, y, width, height, radius);
|
|
288
|
+
ctx.stroke();
|
|
289
|
+
}
|
|
290
|
+
buildRoundedRectPath(ctx, x, y, width, height, radius) {
|
|
291
|
+
const safeRadius = Math.max(0, Math.min(radius, width / 2, height / 2));
|
|
292
|
+
ctx.beginPath();
|
|
293
|
+
if (safeRadius <= 0) {
|
|
294
|
+
ctx.rect(x, y, width, height);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
ctx.moveTo(x + safeRadius, y);
|
|
298
|
+
ctx.lineTo(x + width - safeRadius, y);
|
|
299
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + safeRadius);
|
|
300
|
+
ctx.lineTo(x + width, y + height - safeRadius);
|
|
301
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - safeRadius, y + height);
|
|
302
|
+
ctx.lineTo(x + safeRadius, y + height);
|
|
303
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - safeRadius);
|
|
304
|
+
ctx.lineTo(x, y + safeRadius);
|
|
305
|
+
ctx.quadraticCurveTo(x, y, x + safeRadius, y);
|
|
306
|
+
ctx.closePath();
|
|
307
|
+
}
|
|
155
308
|
getSafeRenderScale(value) {
|
|
156
309
|
if (!Number.isFinite(value))
|
|
157
310
|
return 1;
|
|
@@ -169,6 +322,66 @@ export class RenderSystem {
|
|
|
169
322
|
return fallback;
|
|
170
323
|
return value;
|
|
171
324
|
}
|
|
325
|
+
paintCanvasBackground(ctx, width, height, background, backgroundGradient) {
|
|
326
|
+
ctx.clearRect(0, 0, width, height);
|
|
327
|
+
if (backgroundGradient !== null) {
|
|
328
|
+
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
329
|
+
gradient.addColorStop(0, backgroundGradient.from);
|
|
330
|
+
gradient.addColorStop(1, backgroundGradient.to);
|
|
331
|
+
ctx.fillStyle = gradient;
|
|
332
|
+
ctx.fillRect(0, 0, width, height);
|
|
333
|
+
}
|
|
334
|
+
else if (background !== null) {
|
|
335
|
+
ctx.fillStyle = background;
|
|
336
|
+
ctx.fillRect(0, 0, width, height);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
drawBackgroundLayer(ctx, canvas, layer, image) {
|
|
340
|
+
const destX = Math.round(layer.x);
|
|
341
|
+
const destY = Math.round(layer.y);
|
|
342
|
+
const destW = this.getPositiveLength(layer.width, canvas.width);
|
|
343
|
+
const destH = this.getPositiveLength(layer.height, canvas.height);
|
|
344
|
+
const alpha = Math.max(0, Math.min(1, layer.alpha));
|
|
345
|
+
ctx.save();
|
|
346
|
+
ctx.globalAlpha = alpha;
|
|
347
|
+
if (layer.fit === "none") {
|
|
348
|
+
ctx.drawImage(image, destX, destY);
|
|
349
|
+
ctx.restore();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (layer.fit === "stretch") {
|
|
353
|
+
ctx.drawImage(image, destX, destY, destW, destH);
|
|
354
|
+
ctx.restore();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const imageW = Math.max(1, image.naturalWidth || image.width);
|
|
358
|
+
const imageH = Math.max(1, image.naturalHeight || image.height);
|
|
359
|
+
const scale = this.getBackgroundScale(layer.fit, destW, destH, imageW, imageH);
|
|
360
|
+
const drawW = imageW * scale;
|
|
361
|
+
const drawH = imageH * scale;
|
|
362
|
+
const drawX = destX + (destW - drawW) / 2;
|
|
363
|
+
const drawY = destY + (destH - drawH) / 2;
|
|
364
|
+
if (layer.fit === "cover") {
|
|
365
|
+
ctx.beginPath();
|
|
366
|
+
ctx.rect(destX, destY, destW, destH);
|
|
367
|
+
ctx.clip();
|
|
368
|
+
}
|
|
369
|
+
ctx.drawImage(image, drawX, drawY, drawW, drawH);
|
|
370
|
+
ctx.restore();
|
|
371
|
+
}
|
|
372
|
+
getPositiveLength(value, fallback) {
|
|
373
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
374
|
+
return fallback;
|
|
375
|
+
return Math.round(value);
|
|
376
|
+
}
|
|
377
|
+
getBackgroundScale(fit, destW, destH, imageW, imageH) {
|
|
378
|
+
const scaleX = destW / imageW;
|
|
379
|
+
const scaleY = destH / imageH;
|
|
380
|
+
if (fit === "contain") {
|
|
381
|
+
return Math.min(scaleX, scaleY);
|
|
382
|
+
}
|
|
383
|
+
return Math.max(scaleX, scaleY);
|
|
384
|
+
}
|
|
172
385
|
applyPageBackground(pageBackground) {
|
|
173
386
|
if (!document.body)
|
|
174
387
|
return;
|