minimojs 1.0.0-alpha.8 → 1.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/internal/RenderSystem.js +29 -6
- package/dist/minimo.d.ts +28 -1
- package/dist/minimo.js +146 -2
- package/package.json +1 -1
|
@@ -197,8 +197,12 @@ export class RenderSystem {
|
|
|
197
197
|
const lineHeightPx = Math.max(1, Math.ceil(sprite.fontSize * sprite.lineHeight));
|
|
198
198
|
const contentHeight = Math.max(1, lineHeightPx * Math.max(lines.length, 1));
|
|
199
199
|
const borderPad = Math.max(0, sprite.borderWidth) * 2;
|
|
200
|
-
const totalWidth = Math.max(1, Math.ceil(
|
|
201
|
-
|
|
200
|
+
const totalWidth = Math.max(1, Math.ceil(sprite.fixedWidth > 0
|
|
201
|
+
? sprite.fixedWidth
|
|
202
|
+
: measuredWidth + sprite.paddingX * 2 + borderPad));
|
|
203
|
+
const totalHeight = Math.max(1, Math.ceil(sprite.fixedHeight > 0
|
|
204
|
+
? sprite.fixedHeight
|
|
205
|
+
: contentHeight + sprite.paddingY * 2 + borderPad));
|
|
202
206
|
sprite._measuredWidth = totalWidth;
|
|
203
207
|
sprite._measuredHeight = totalHeight;
|
|
204
208
|
canvas.width = totalWidth;
|
|
@@ -223,7 +227,12 @@ export class RenderSystem {
|
|
|
223
227
|
const contentLeft = sprite.paddingX + sprite.borderWidth;
|
|
224
228
|
const contentRight = totalWidth - sprite.paddingX - sprite.borderWidth;
|
|
225
229
|
const contentCenter = totalWidth / 2;
|
|
226
|
-
const
|
|
230
|
+
const availableContentHeight = Math.max(1, totalHeight - sprite.paddingY * 2 - borderPad);
|
|
231
|
+
const contentOffsetY = Math.max(0, Math.floor((availableContentHeight - contentHeight) / 2));
|
|
232
|
+
const startY = sprite.paddingY +
|
|
233
|
+
sprite.borderWidth +
|
|
234
|
+
contentOffsetY +
|
|
235
|
+
lineHeightPx / 2;
|
|
227
236
|
const drawX = sprite.textAlign === "left"
|
|
228
237
|
? contentLeft
|
|
229
238
|
: sprite.textAlign === "right"
|
|
@@ -246,10 +255,10 @@ export class RenderSystem {
|
|
|
246
255
|
layoutTextLines(ctx, sprite) {
|
|
247
256
|
this.applyTextStyle(ctx, sprite);
|
|
248
257
|
const rawLines = sprite.text.split("\n");
|
|
249
|
-
|
|
258
|
+
const availableTextWidth = this.getAvailableTextWidth(sprite);
|
|
259
|
+
if (availableTextWidth <= 0) {
|
|
250
260
|
return rawLines.length > 0 ? rawLines : [""];
|
|
251
261
|
}
|
|
252
|
-
const maxWidth = Math.max(1, sprite.maxWidth - sprite.paddingX * 2);
|
|
253
262
|
const wrappedLines = [];
|
|
254
263
|
for (const rawLine of rawLines) {
|
|
255
264
|
const words = rawLine.split(/\s+/).filter((word) => word.length > 0);
|
|
@@ -260,7 +269,7 @@ export class RenderSystem {
|
|
|
260
269
|
let currentLine = words[0];
|
|
261
270
|
for (let i = 1; i < words.length; i++) {
|
|
262
271
|
const candidate = `${currentLine} ${words[i]}`;
|
|
263
|
-
if (ctx.measureText(candidate).width <=
|
|
272
|
+
if (ctx.measureText(candidate).width <= availableTextWidth) {
|
|
264
273
|
currentLine = candidate;
|
|
265
274
|
}
|
|
266
275
|
else {
|
|
@@ -272,6 +281,20 @@ export class RenderSystem {
|
|
|
272
281
|
}
|
|
273
282
|
return wrappedLines.length > 0 ? wrappedLines : [""];
|
|
274
283
|
}
|
|
284
|
+
getAvailableTextWidth(sprite) {
|
|
285
|
+
const borderPad = Math.max(0, sprite.borderWidth) * 2;
|
|
286
|
+
const limits = [];
|
|
287
|
+
if (sprite.maxWidth > 0) {
|
|
288
|
+
limits.push(sprite.maxWidth - sprite.paddingX * 2 - borderPad);
|
|
289
|
+
}
|
|
290
|
+
if (sprite.fixedWidth > 0) {
|
|
291
|
+
limits.push(sprite.fixedWidth - sprite.paddingX * 2 - borderPad);
|
|
292
|
+
}
|
|
293
|
+
if (limits.length === 0) {
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
return Math.max(1, Math.floor(Math.min(...limits)));
|
|
297
|
+
}
|
|
275
298
|
applyTextStyle(ctx, sprite) {
|
|
276
299
|
ctx.font = `${sprite.fontWeight} ${sprite.fontSize}px ${sprite.fontFamily}`;
|
|
277
300
|
ctx.shadowColor = "transparent";
|
package/dist/minimo.d.ts
CHANGED
|
@@ -253,6 +253,8 @@ export declare class Sprite extends BaseSprite {
|
|
|
253
253
|
* pipeline as regular sprites.
|
|
254
254
|
*/
|
|
255
255
|
export declare class TextSprite extends BaseSprite {
|
|
256
|
+
private static _measurementCanvas;
|
|
257
|
+
private static _measurementContext;
|
|
256
258
|
anchorX: "left" | "center" | "right";
|
|
257
259
|
anchorY: "top" | "middle" | "bottom";
|
|
258
260
|
text: string;
|
|
@@ -261,6 +263,8 @@ export declare class TextSprite extends BaseSprite {
|
|
|
261
263
|
fontWeight: string;
|
|
262
264
|
lineHeight: number;
|
|
263
265
|
maxWidth: number;
|
|
266
|
+
fixedWidth: number;
|
|
267
|
+
fixedHeight: number;
|
|
264
268
|
paddingX: number;
|
|
265
269
|
paddingY: number;
|
|
266
270
|
backgroundColor: string | null;
|
|
@@ -270,13 +274,36 @@ export declare class TextSprite extends BaseSprite {
|
|
|
270
274
|
textAlign: CanvasTextAlign;
|
|
271
275
|
get width(): number;
|
|
272
276
|
get height(): number;
|
|
273
|
-
constructor(text: string, x?: number, y?: number,
|
|
277
|
+
constructor(text: string, x?: number, y?: number, fontSizeOrConfig?: number | TextSpriteConfig, config?: TextSpriteConfig);
|
|
274
278
|
getRenderCacheKey(): string;
|
|
275
279
|
get align(): CanvasTextAlign;
|
|
276
280
|
set align(value: CanvasTextAlign);
|
|
277
281
|
protected getAnchorOffsetX(): number;
|
|
278
282
|
protected getAnchorOffsetY(): number;
|
|
283
|
+
private applyConfig;
|
|
284
|
+
private ensureMeasured;
|
|
285
|
+
private layoutMeasuredLines;
|
|
286
|
+
private getAvailableTextWidth;
|
|
279
287
|
}
|
|
288
|
+
export type TextSpriteConfig = {
|
|
289
|
+
anchorX?: "left" | "center" | "right";
|
|
290
|
+
anchorY?: "top" | "middle" | "bottom";
|
|
291
|
+
fontFamily?: string;
|
|
292
|
+
fontSize?: number;
|
|
293
|
+
fontWeight?: string;
|
|
294
|
+
lineHeight?: number;
|
|
295
|
+
maxWidth?: number;
|
|
296
|
+
fixedWidth?: number;
|
|
297
|
+
fixedHeight?: number;
|
|
298
|
+
paddingX?: number;
|
|
299
|
+
paddingY?: number;
|
|
300
|
+
backgroundColor?: string | null;
|
|
301
|
+
borderColor?: string | null;
|
|
302
|
+
borderWidth?: number;
|
|
303
|
+
cornerRadius?: number;
|
|
304
|
+
textAlign?: CanvasTextAlign;
|
|
305
|
+
align?: CanvasTextAlign;
|
|
306
|
+
};
|
|
280
307
|
export type FontRequirementOptions = {
|
|
281
308
|
weight?: string;
|
|
282
309
|
style?: "normal" | "italic" | "oblique";
|
package/dist/minimo.js
CHANGED
|
@@ -237,12 +237,14 @@ export class Sprite extends BaseSprite {
|
|
|
237
237
|
*/
|
|
238
238
|
export class TextSprite extends BaseSprite {
|
|
239
239
|
get width() {
|
|
240
|
+
this.ensureMeasured();
|
|
240
241
|
return this._measuredWidth;
|
|
241
242
|
}
|
|
242
243
|
get height() {
|
|
244
|
+
this.ensureMeasured();
|
|
243
245
|
return this._measuredHeight;
|
|
244
246
|
}
|
|
245
|
-
constructor(text, x = 0, y = 0,
|
|
247
|
+
constructor(text, x = 0, y = 0, fontSizeOrConfig = 16, config = {}) {
|
|
246
248
|
super();
|
|
247
249
|
this.anchorX = "center";
|
|
248
250
|
this.anchorY = "middle";
|
|
@@ -250,6 +252,8 @@ export class TextSprite extends BaseSprite {
|
|
|
250
252
|
this.fontWeight = "400";
|
|
251
253
|
this.lineHeight = 1.2;
|
|
252
254
|
this.maxWidth = 0;
|
|
255
|
+
this.fixedWidth = 0;
|
|
256
|
+
this.fixedHeight = 0;
|
|
253
257
|
this.paddingX = 0;
|
|
254
258
|
this.paddingY = 0;
|
|
255
259
|
this.backgroundColor = null;
|
|
@@ -261,11 +265,20 @@ export class TextSprite extends BaseSprite {
|
|
|
261
265
|
this._measuredWidth = 1;
|
|
262
266
|
/** @internal */
|
|
263
267
|
this._measuredHeight = 1;
|
|
268
|
+
/** @internal */
|
|
269
|
+
this._measurementCacheKey = "";
|
|
264
270
|
this.text = text;
|
|
265
271
|
this.x = x;
|
|
266
272
|
this.y = y;
|
|
267
|
-
this.fontSize =
|
|
273
|
+
this.fontSize =
|
|
274
|
+
typeof fontSizeOrConfig === "number" ? fontSizeOrConfig : 16;
|
|
268
275
|
this.color = "#ffffff";
|
|
276
|
+
if (typeof fontSizeOrConfig === "number") {
|
|
277
|
+
this.applyConfig(config);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
this.applyConfig(fontSizeOrConfig);
|
|
281
|
+
}
|
|
269
282
|
}
|
|
270
283
|
getRenderCacheKey() {
|
|
271
284
|
return [
|
|
@@ -278,6 +291,8 @@ export class TextSprite extends BaseSprite {
|
|
|
278
291
|
this.fontWeight,
|
|
279
292
|
this.lineHeight,
|
|
280
293
|
this.maxWidth,
|
|
294
|
+
this.fixedWidth,
|
|
295
|
+
this.fixedHeight,
|
|
281
296
|
this.color,
|
|
282
297
|
this.textAlign,
|
|
283
298
|
this.paddingX,
|
|
@@ -312,7 +327,136 @@ export class TextSprite extends BaseSprite {
|
|
|
312
327
|
}
|
|
313
328
|
return 0;
|
|
314
329
|
}
|
|
330
|
+
applyConfig(config) {
|
|
331
|
+
if (config.anchorX !== undefined)
|
|
332
|
+
this.anchorX = config.anchorX;
|
|
333
|
+
if (config.anchorY !== undefined)
|
|
334
|
+
this.anchorY = config.anchorY;
|
|
335
|
+
if (config.fontFamily !== undefined)
|
|
336
|
+
this.fontFamily = config.fontFamily;
|
|
337
|
+
if (config.fontSize !== undefined)
|
|
338
|
+
this.fontSize = config.fontSize;
|
|
339
|
+
if (config.fontWeight !== undefined)
|
|
340
|
+
this.fontWeight = config.fontWeight;
|
|
341
|
+
if (config.lineHeight !== undefined)
|
|
342
|
+
this.lineHeight = config.lineHeight;
|
|
343
|
+
if (config.maxWidth !== undefined)
|
|
344
|
+
this.maxWidth = config.maxWidth;
|
|
345
|
+
if (config.fixedWidth !== undefined)
|
|
346
|
+
this.fixedWidth = config.fixedWidth;
|
|
347
|
+
if (config.fixedHeight !== undefined)
|
|
348
|
+
this.fixedHeight = config.fixedHeight;
|
|
349
|
+
if (config.paddingX !== undefined)
|
|
350
|
+
this.paddingX = config.paddingX;
|
|
351
|
+
if (config.paddingY !== undefined)
|
|
352
|
+
this.paddingY = config.paddingY;
|
|
353
|
+
if (config.backgroundColor !== undefined)
|
|
354
|
+
this.backgroundColor = config.backgroundColor;
|
|
355
|
+
if (config.borderColor !== undefined)
|
|
356
|
+
this.borderColor = config.borderColor;
|
|
357
|
+
if (config.borderWidth !== undefined)
|
|
358
|
+
this.borderWidth = config.borderWidth;
|
|
359
|
+
if (config.cornerRadius !== undefined)
|
|
360
|
+
this.cornerRadius = config.cornerRadius;
|
|
361
|
+
if (config.textAlign !== undefined)
|
|
362
|
+
this.textAlign = config.textAlign;
|
|
363
|
+
if (config.align !== undefined)
|
|
364
|
+
this.textAlign = config.align;
|
|
365
|
+
}
|
|
366
|
+
ensureMeasured() {
|
|
367
|
+
const nextKey = [
|
|
368
|
+
this.text,
|
|
369
|
+
this.fontFamily,
|
|
370
|
+
this.fontSize,
|
|
371
|
+
this.fontWeight,
|
|
372
|
+
this.lineHeight,
|
|
373
|
+
this.maxWidth,
|
|
374
|
+
this.fixedWidth,
|
|
375
|
+
this.fixedHeight,
|
|
376
|
+
this.paddingX,
|
|
377
|
+
this.paddingY,
|
|
378
|
+
this.borderWidth,
|
|
379
|
+
].join("|");
|
|
380
|
+
if (this._measurementCacheKey === nextKey) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (typeof document === "undefined") {
|
|
384
|
+
this._measuredWidth = 1;
|
|
385
|
+
this._measuredHeight = 1;
|
|
386
|
+
this._measurementCacheKey = nextKey;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (TextSprite._measurementCanvas === null) {
|
|
390
|
+
TextSprite._measurementCanvas = document.createElement("canvas");
|
|
391
|
+
TextSprite._measurementContext =
|
|
392
|
+
TextSprite._measurementCanvas.getContext("2d");
|
|
393
|
+
}
|
|
394
|
+
const ctx = TextSprite._measurementContext;
|
|
395
|
+
if (!ctx) {
|
|
396
|
+
this._measuredWidth = 1;
|
|
397
|
+
this._measuredHeight = 1;
|
|
398
|
+
this._measurementCacheKey = nextKey;
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
ctx.font = `${this.fontWeight} ${this.fontSize}px ${this.fontFamily}`;
|
|
402
|
+
const lines = this.layoutMeasuredLines(ctx);
|
|
403
|
+
const measuredWidth = Math.max(1, Math.ceil(lines.reduce((maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width), 0)));
|
|
404
|
+
const lineHeightPx = Math.max(1, Math.ceil(this.fontSize * this.lineHeight));
|
|
405
|
+
const contentHeight = Math.max(1, lineHeightPx * Math.max(lines.length, 1));
|
|
406
|
+
const borderPad = Math.max(0, this.borderWidth) * 2;
|
|
407
|
+
this._measuredWidth = Math.max(1, Math.ceil(this.fixedWidth > 0
|
|
408
|
+
? this.fixedWidth
|
|
409
|
+
: measuredWidth + this.paddingX * 2 + borderPad));
|
|
410
|
+
this._measuredHeight = Math.max(1, Math.ceil(this.fixedHeight > 0
|
|
411
|
+
? this.fixedHeight
|
|
412
|
+
: contentHeight + this.paddingY * 2 + borderPad));
|
|
413
|
+
this._measurementCacheKey = nextKey;
|
|
414
|
+
}
|
|
415
|
+
layoutMeasuredLines(ctx) {
|
|
416
|
+
const rawLines = this.text.split("\n");
|
|
417
|
+
const availableTextWidth = this.getAvailableTextWidth();
|
|
418
|
+
if (availableTextWidth <= 0) {
|
|
419
|
+
return rawLines.length > 0 ? rawLines : [""];
|
|
420
|
+
}
|
|
421
|
+
const wrappedLines = [];
|
|
422
|
+
for (const rawLine of rawLines) {
|
|
423
|
+
const words = rawLine.split(/\s+/).filter((word) => word.length > 0);
|
|
424
|
+
if (words.length === 0) {
|
|
425
|
+
wrappedLines.push("");
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
let currentLine = words[0];
|
|
429
|
+
for (let i = 1; i < words.length; i++) {
|
|
430
|
+
const candidate = `${currentLine} ${words[i]}`;
|
|
431
|
+
if (ctx.measureText(candidate).width <= availableTextWidth) {
|
|
432
|
+
currentLine = candidate;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
wrappedLines.push(currentLine);
|
|
436
|
+
currentLine = words[i];
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
wrappedLines.push(currentLine);
|
|
440
|
+
}
|
|
441
|
+
return wrappedLines.length > 0 ? wrappedLines : [""];
|
|
442
|
+
}
|
|
443
|
+
getAvailableTextWidth() {
|
|
444
|
+
const borderPad = Math.max(0, this.borderWidth) * 2;
|
|
445
|
+
const limits = [];
|
|
446
|
+
if (this.maxWidth > 0) {
|
|
447
|
+
limits.push(this.maxWidth - this.paddingX * 2 - borderPad);
|
|
448
|
+
}
|
|
449
|
+
if (this.fixedWidth > 0) {
|
|
450
|
+
limits.push(this.fixedWidth - this.paddingX * 2 - borderPad);
|
|
451
|
+
}
|
|
452
|
+
if (limits.length === 0) {
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
return Math.max(1, Math.floor(Math.min(...limits)));
|
|
456
|
+
}
|
|
315
457
|
}
|
|
458
|
+
TextSprite._measurementCanvas = null;
|
|
459
|
+
TextSprite._measurementContext = null;
|
|
316
460
|
/**
|
|
317
461
|
* A static image-based background layer rendered behind all sprites.
|
|
318
462
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimojs",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.9",
|
|
4
4
|
"description": "MinimoJS v1 — ultra-minimal, flat, deterministic 2D web game engine. Emoji-only sprites, rAF loop, TypeScript-first, LLM-friendly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/minimo.js",
|