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.
@@ -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(measuredWidth + sprite.paddingX * 2 + borderPad));
201
- const totalHeight = Math.max(1, Math.ceil(contentHeight + sprite.paddingY * 2 + borderPad));
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 startY = sprite.paddingY + sprite.borderWidth + lineHeightPx / 2;
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
- if (sprite.maxWidth <= 0) {
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 <= maxWidth) {
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, fontSize?: 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, fontSize = 16) {
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 = 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.8",
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",