js-game-zero 0.3.0 → 0.4.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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * js-game-zero 0.3.0
2
+ * js-game-zero 0.4.0
3
3
  * https://github.com/Leaf-Learning/js-game-zero
4
4
  *
5
5
  * MIT License
@@ -3548,6 +3548,14 @@ var shortForms = {
3548
3548
  lists: 'screen.rect takes x, y, width, height as four numbers, like screen.rect(40, 40, 120, 60, "orange"). For a list or a Rect, use screen.draw.filledRect([x, y, width, height], color).',
3549
3549
  extra: "screen.rect always fills, so it has no outline width. For an outline, use screen.draw.rect([x, y, width, height], color, width)."
3550
3550
  },
3551
+ roundedRect: {
3552
+ call: "screen.roundedRect(x, y, width, height, radius, color)",
3553
+ numbers: ["x", "y", "width", "height", "radius"],
3554
+ arity: 6,
3555
+ lists: 'screen.roundedRect takes x, y, width, height and radius as five numbers, like screen.roundedRect(40, 40, 120, 60, 12, "orange"). For a list or a Rect, use screen.draw.filledRoundedRect([x, y, width, height], radius, color).',
3556
+ extra: "screen.roundedRect always fills, so it has no outline width. For an outline, use screen.draw.roundedRect([x, y, width, height], radius, color, width).",
3557
+ colorTooSoon: `screen.roundedRect takes the corners' radius before the color, like screen.roundedRect(40, 40, 120, 60, 12, "orange").`
3558
+ },
3551
3559
  triangle: {
3552
3560
  call: "screen.triangle(x1, y1, x2, y2, x3, y3, color)",
3553
3561
  numbers: ["x1", "y1", "x2", "y2", "x3", "y3"],
@@ -3563,6 +3571,10 @@ var shortForms = {
3563
3571
  extra: "screen.line takes at most six values: screen.line(x1, y1, x2, y2, color, width)."
3564
3572
  }
3565
3573
  };
3574
+ var roundedRectCalls = {
3575
+ outline: "screen.draw.roundedRect(rect, radius, color, width)",
3576
+ filled: "screen.draw.filledRoundedRect(rect, radius, color)"
3577
+ };
3566
3578
  var currentScreen = null;
3567
3579
  var screenSizes = new WeakMap;
3568
3580
  var densityAttribute = "data-jgz-density";
@@ -3671,6 +3683,13 @@ class Screen {
3671
3683
  filledPolygon: (points, color) => this.drawPolygon(points, color, 0, true),
3672
3684
  rect: (rect, color, width = 1) => this.drawRect(rect, color, width),
3673
3685
  filledRect: (rect, color) => this.drawRect(rect, color, 0, true),
3686
+ roundedRect: (rect, radius, color, width = 1) => this.drawRoundedRect(roundedRectCalls.outline, rect, radius, color, width),
3687
+ filledRoundedRect: (rect, radius, color, ...extra) => {
3688
+ if (extra.length > 0) {
3689
+ throw new TypeError(`screen.draw.filledRoundedRect always fills, so it has no outline width. For an outline, use ${roundedRectCalls.outline}.`);
3690
+ }
3691
+ this.drawRoundedRect(roundedRectCalls.filled, rect, radius, color, 0, true);
3692
+ },
3674
3693
  text: (text, posOrOptions, options) => this.drawText(text, posOrOptions, options),
3675
3694
  textbox: (text, rect, options = {}) => this.drawTextbox(text, rect, options),
3676
3695
  image: (image, pos) => this.blit(image, pos)
@@ -3755,6 +3774,10 @@ class Screen {
3755
3774
  requireShortForm(shortForms.rect, arguments);
3756
3775
  this.drawRect([x, y, width, height], color, 0, true);
3757
3776
  }
3777
+ roundedRect(x, y, width, height, radius, color = "white") {
3778
+ requireShortForm(shortForms.roundedRect, arguments);
3779
+ this.drawRoundedRect(shortForms.roundedRect.call, [x, y, width, height], radius, color, 0, true);
3780
+ }
3758
3781
  triangle(x1, y1, x2, y2, x3, y3, color = "white") {
3759
3782
  requireShortForm(shortForms.triangle, arguments);
3760
3783
  this.drawPolygon([
@@ -3771,6 +3794,16 @@ class Screen {
3771
3794
  requireTextShortForm(arguments);
3772
3795
  this.drawText(String(text), [x, y], { color, fontsize: size });
3773
3796
  }
3797
+ measureText(text, posOrOptions, maybeOptions) {
3798
+ requireMeasureText(arguments);
3799
+ const { pos, options } = textArguments("screen.measureText", 'measureText("Hi", { size: 20 })', posOrOptions, maybeOptions);
3800
+ const layout = layoutText(this.context, String(text), pos, options);
3801
+ return {
3802
+ width: layout.textWidth,
3803
+ height: layout.boxHeight,
3804
+ lines: [...layout.lines]
3805
+ };
3806
+ }
3774
3807
  drawSurface(surface, pos) {
3775
3808
  const target = rectFromBlitTarget(pos);
3776
3809
  const source = surface.source;
@@ -3878,20 +3911,33 @@ class Screen {
3878
3911
  }
3879
3912
  });
3880
3913
  }
3881
- drawText(text, posOrOptions, maybeOptions) {
3882
- let pos;
3883
- let given;
3884
- if (isPlainOptions(posOrOptions)) {
3885
- if (maybeOptions !== undefined) {
3886
- throw new TypeError('screen.draw.text takes its options once: either second, as in text("Hi", { center: [x, y] }), or third, after a position.');
3914
+ drawRoundedRect(call, rectLike, radius, color, width, filled = false) {
3915
+ const rect = new Rect(rectLike);
3916
+ const parsedRadius = requireRadius(call, radius);
3917
+ recordDrawn(this, {
3918
+ kind: "rect",
3919
+ x: rect.left,
3920
+ y: rect.top,
3921
+ width: rect.width,
3922
+ height: rect.height,
3923
+ color: colorOf(color),
3924
+ filled,
3925
+ radius: parsedRadius
3926
+ });
3927
+ withSavedCanvasState(this.context, () => {
3928
+ traceRoundedRect(this.context, rect, parsedRadius);
3929
+ if (filled) {
3930
+ this.context.fillStyle = normalizeColor(color);
3931
+ this.context.fill();
3932
+ } else {
3933
+ this.context.strokeStyle = normalizeColor(color);
3934
+ this.context.lineWidth = requirePositiveNumber(width, "width");
3935
+ this.context.stroke();
3887
3936
  }
3888
- given = posOrOptions;
3889
- } else {
3890
- pos = posOrOptions;
3891
- given = maybeOptions ?? {};
3892
- }
3893
- rejectUnknownOptions(given, textOptionKeys, "screen.draw.text");
3894
- const options = canonicalTextOptions(given, "screen.draw.text");
3937
+ });
3938
+ }
3939
+ drawText(text, posOrOptions, maybeOptions) {
3940
+ const { pos, options } = textArguments("screen.draw.text", 'text("Hi", { center: [x, y] })', posOrOptions, maybeOptions);
3895
3941
  const layout = layoutText(this.context, text, pos, options);
3896
3942
  recordDrawn(this, {
3897
3943
  kind: "text",
@@ -3945,6 +3991,21 @@ function isPlainOptions(value) {
3945
3991
  const prototype = Object.getPrototypeOf(value);
3946
3992
  return prototype === Object.prototype || prototype === null;
3947
3993
  }
3994
+ function textArguments(label, example, posOrOptions, maybeOptions) {
3995
+ let pos;
3996
+ let given;
3997
+ if (isPlainOptions(posOrOptions)) {
3998
+ if (maybeOptions !== undefined) {
3999
+ throw new TypeError(`${label} takes its options once: either second, as in ${example}, or third, after a position.`);
4000
+ }
4001
+ given = posOrOptions;
4002
+ } else {
4003
+ pos = posOrOptions;
4004
+ given = maybeOptions ?? {};
4005
+ }
4006
+ rejectUnknownOptions(given, textOptionKeys, label);
4007
+ return { pos, options: canonicalTextOptions(given, label) };
4008
+ }
3948
4009
  function canonicalTextOptions(options, label) {
3949
4010
  let result = null;
3950
4011
  for (const [alias, canonical] of Object.entries(textOptionAliases)) {
@@ -4358,6 +4419,20 @@ function withTemporaryFont(context, font, callback) {
4358
4419
  context.font = previousFont;
4359
4420
  }
4360
4421
  }
4422
+ function traceRoundedRect(context, rect, radius) {
4423
+ const left = Math.min(rect.left, rect.left + rect.width);
4424
+ const top = Math.min(rect.top, rect.top + rect.height);
4425
+ const right = left + Math.abs(rect.width);
4426
+ const bottom = top + Math.abs(rect.height);
4427
+ const r = Math.min(radius, (right - left) / 2, (bottom - top) / 2);
4428
+ context.beginPath();
4429
+ context.moveTo(left + r, top);
4430
+ context.arc(right - r, top + r, r, -Math.PI / 2, 0);
4431
+ context.arc(right - r, bottom - r, r, 0, Math.PI / 2);
4432
+ context.arc(left + r, bottom - r, r, Math.PI / 2, Math.PI);
4433
+ context.arc(left + r, top + r, r, Math.PI, Math.PI * 1.5);
4434
+ context.closePath();
4435
+ }
4361
4436
  function withSavedCanvasState(context, callback) {
4362
4437
  context.save();
4363
4438
  try {
@@ -4370,9 +4445,13 @@ function requireShortForm(form, args) {
4370
4445
  const values = Array.from(args);
4371
4446
  const name = form.call.slice(0, form.call.indexOf("("));
4372
4447
  const numbers = values.slice(0, form.numbers.length);
4373
- if (numbers.some((value) => typeof value === "object" && value !== null)) {
4374
- throw new TypeError(form.lists);
4448
+ const isList = (value) => typeof value === "object" && value !== null;
4449
+ const last = values[form.numbers.length - 1];
4450
+ if (form.colorTooSoon !== undefined && (typeof last === "string" || Array.isArray(last)) && !numbers.slice(0, -1).some(isList)) {
4451
+ throw new TypeError(form.colorTooSoon);
4375
4452
  }
4453
+ if (numbers.some(isList))
4454
+ throw new TypeError(form.lists);
4376
4455
  form.numbers.forEach((label, index) => {
4377
4456
  const value = values[index];
4378
4457
  if (value === undefined) {
@@ -4413,6 +4492,38 @@ function requireTextShortForm(args) {
4413
4492
  throw new TypeError(`screen.text takes at most five values: ${textCall}. For more options, use screen.draw.text.`);
4414
4493
  }
4415
4494
  }
4495
+ var measureCall = "screen.measureText(text, options)";
4496
+ function requireMeasureText(args) {
4497
+ const [text, second] = Array.from(args);
4498
+ if (text === undefined) {
4499
+ throw new TypeError(`screen.measureText is missing text: ${measureCall}.`);
4500
+ }
4501
+ if (typeof text !== "string" && !(typeof text === "number" && Number.isFinite(text))) {
4502
+ throw new TypeError(`screen.measureText's text must be words in quotes or a number, but got ${describeValue(text)}.`);
4503
+ }
4504
+ if (typeof second === "number") {
4505
+ throw new TypeError('screen.measureText takes its size in its options, as screen.draw.text does: screen.measureText("Score: 3", { size: 32 }) measures what screen.text("Score: 3", x, y, color, 32) draws.');
4506
+ }
4507
+ if (args.length > 3) {
4508
+ throw new TypeError(`screen.measureText takes the text and its options: ${measureCall}. It also takes a position before the options, as screen.draw.text does, but nothing more.`);
4509
+ }
4510
+ }
4511
+ function requireRadius(call, radius) {
4512
+ const name = call.slice(0, call.indexOf("("));
4513
+ if (radius === undefined) {
4514
+ throw new TypeError(`${name} is missing radius: ${call}.`);
4515
+ }
4516
+ if (typeof radius === "string" || Array.isArray(radius)) {
4517
+ throw new TypeError(`${name} takes the corners' radius before the color: ${call}.`);
4518
+ }
4519
+ if (typeof radius !== "number" || !Number.isFinite(radius)) {
4520
+ throw new TypeError(`${name}'s radius must be a number, but got ${describeValue(radius)}.`);
4521
+ }
4522
+ if (radius < 0) {
4523
+ throw new TypeError(`${name}'s radius must be 0 or more, but got ${radius}.`);
4524
+ }
4525
+ return radius;
4526
+ }
4416
4527
  function requirePositiveNumber(value, label) {
4417
4528
  if (typeof value === "number" && Number.isFinite(value) && value > 0) {
4418
4529
  return value;
@@ -6570,6 +6681,8 @@ var drawMethods = [
6570
6681
  "filledPolygon",
6571
6682
  "rect",
6572
6683
  "filledRect",
6684
+ "roundedRect",
6685
+ "filledRoundedRect",
6573
6686
  "text",
6574
6687
  "textbox",
6575
6688
  "image"
@@ -6590,7 +6703,19 @@ var screenHints = Object.fromEntries([
6590
6703
  ["fillrectangle", "screen.draw.filledRect"],
6591
6704
  ["drawrectangle", "screen.draw.rect"],
6592
6705
  ["triangle", "screen.triangle"],
6593
- ["filledtriangle", "screen.triangle"]
6706
+ ["filledtriangle", "screen.triangle"],
6707
+ ["fillroundrect", "screen.draw.filledRoundedRect"],
6708
+ ["roundedrectangle", "screen.draw.roundedRect"],
6709
+ ["filledroundedrectangle", "screen.draw.filledRoundedRect"],
6710
+ ...[
6711
+ "measuretext",
6712
+ "measure",
6713
+ "textsize",
6714
+ "textwidth",
6715
+ "textheight",
6716
+ "gettextsize",
6717
+ "gettextwidth"
6718
+ ].map((name) => [name, "screen.measureText"])
6594
6719
  ]);
6595
6720
  function pointToService(cls, className, serviceName, service) {
6596
6721
  return new Proxy(cls, {
@@ -6616,6 +6741,7 @@ var screenInternals = [
6616
6741
  "drawCircle",
6617
6742
  "drawPolygon",
6618
6743
  "drawRect",
6744
+ "drawRoundedRect",
6619
6745
  "drawText",
6620
6746
  "drawTextbox"
6621
6747
  ];
package/dist/host.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * js-game-zero 0.3.0
2
+ * js-game-zero 0.4.0
3
3
  * https://github.com/Leaf-Learning/js-game-zero
4
4
  *
5
5
  * MIT License
@@ -69,7 +69,7 @@ import {
69
69
  parseToneFrequency,
70
70
  setScreen,
71
71
  validateAssetName
72
- } from "./chunk-brk0rjcy.js";
72
+ } from "./chunk-q9m20c2h.js";
73
73
 
74
74
  // packages/js-game-zero/src/inspection.ts
75
75
  var MAX_DEPTH = 6;
package/dist/index.d.ts CHANGED
@@ -238,7 +238,7 @@ export declare const Screen: typeof renderingModule.Screen;
238
238
  export type Screen = renderingModule.Screen;
239
239
  export declare const screen: renderingModule.Screen;
240
240
  export declare const rgb: typeof colorModule.rgb;
241
- export type { CanvasLike, Color, HorizontalAnchor, ScreenOptions, TextAlignOption, TextAnchor, TextOptions, TextboxOptions, VerticalAnchor, } from "./rendering.js";
241
+ export type { CanvasLike, Color, HorizontalAnchor, ScreenOptions, TextAlignOption, TextAnchor, TextOptions, TextSize, TextboxOptions, VerticalAnchor, } from "./rendering.js";
242
242
  export declare const defineGame: typeof runtimeModule.defineGame;
243
243
  export declare const exit: typeof runtimeModule.exit;
244
244
  export declare const run: typeof runtimeModule.run;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * js-game-zero 0.3.0
2
+ * js-game-zero 0.4.0
3
3
  * https://github.com/Leaf-Learning/js-game-zero
4
4
  *
5
5
  * MIT License
@@ -101,7 +101,7 @@ import {
101
101
  tweenNames,
102
102
  tweens,
103
103
  validateAssetName
104
- } from "./chunk-brk0rjcy.js";
104
+ } from "./chunk-q9m20c2h.js";
105
105
 
106
106
  // packages/js-game-zero/src/index.ts
107
107
  var Actor2 = Actor;
package/dist/jgz.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * js-game-zero 0.3.0
2
+ * js-game-zero 0.4.0
3
3
  * https://github.com/Leaf-Learning/js-game-zero
4
4
  *
5
5
  * MIT License
@@ -3666,6 +3666,14 @@
3666
3666
  lists: 'screen.rect takes x, y, width, height as four numbers, like screen.rect(40, 40, 120, 60, "orange"). For a list or a Rect, use screen.draw.filledRect([x, y, width, height], color).',
3667
3667
  extra: "screen.rect always fills, so it has no outline width. For an outline, use screen.draw.rect([x, y, width, height], color, width)."
3668
3668
  },
3669
+ roundedRect: {
3670
+ call: "screen.roundedRect(x, y, width, height, radius, color)",
3671
+ numbers: ["x", "y", "width", "height", "radius"],
3672
+ arity: 6,
3673
+ lists: 'screen.roundedRect takes x, y, width, height and radius as five numbers, like screen.roundedRect(40, 40, 120, 60, 12, "orange"). For a list or a Rect, use screen.draw.filledRoundedRect([x, y, width, height], radius, color).',
3674
+ extra: "screen.roundedRect always fills, so it has no outline width. For an outline, use screen.draw.roundedRect([x, y, width, height], radius, color, width).",
3675
+ colorTooSoon: `screen.roundedRect takes the corners' radius before the color, like screen.roundedRect(40, 40, 120, 60, 12, "orange").`
3676
+ },
3669
3677
  triangle: {
3670
3678
  call: "screen.triangle(x1, y1, x2, y2, x3, y3, color)",
3671
3679
  numbers: ["x1", "y1", "x2", "y2", "x3", "y3"],
@@ -3681,6 +3689,10 @@
3681
3689
  extra: "screen.line takes at most six values: screen.line(x1, y1, x2, y2, color, width)."
3682
3690
  }
3683
3691
  };
3692
+ var roundedRectCalls = {
3693
+ outline: "screen.draw.roundedRect(rect, radius, color, width)",
3694
+ filled: "screen.draw.filledRoundedRect(rect, radius, color)"
3695
+ };
3684
3696
  var currentScreen = null;
3685
3697
  var screenSizes = new WeakMap;
3686
3698
  var densityAttribute = "data-jgz-density";
@@ -3789,6 +3801,13 @@
3789
3801
  filledPolygon: (points, color) => this.drawPolygon(points, color, 0, true),
3790
3802
  rect: (rect, color, width = 1) => this.drawRect(rect, color, width),
3791
3803
  filledRect: (rect, color) => this.drawRect(rect, color, 0, true),
3804
+ roundedRect: (rect, radius, color, width = 1) => this.drawRoundedRect(roundedRectCalls.outline, rect, radius, color, width),
3805
+ filledRoundedRect: (rect, radius, color, ...extra) => {
3806
+ if (extra.length > 0) {
3807
+ throw new TypeError(`screen.draw.filledRoundedRect always fills, so it has no outline width. For an outline, use ${roundedRectCalls.outline}.`);
3808
+ }
3809
+ this.drawRoundedRect(roundedRectCalls.filled, rect, radius, color, 0, true);
3810
+ },
3792
3811
  text: (text, posOrOptions, options) => this.drawText(text, posOrOptions, options),
3793
3812
  textbox: (text, rect, options = {}) => this.drawTextbox(text, rect, options),
3794
3813
  image: (image, pos) => this.blit(image, pos)
@@ -3873,6 +3892,10 @@
3873
3892
  requireShortForm(shortForms.rect, arguments);
3874
3893
  this.drawRect([x, y, width, height], color, 0, true);
3875
3894
  }
3895
+ roundedRect(x, y, width, height, radius, color = "white") {
3896
+ requireShortForm(shortForms.roundedRect, arguments);
3897
+ this.drawRoundedRect(shortForms.roundedRect.call, [x, y, width, height], radius, color, 0, true);
3898
+ }
3876
3899
  triangle(x1, y1, x2, y2, x3, y3, color = "white") {
3877
3900
  requireShortForm(shortForms.triangle, arguments);
3878
3901
  this.drawPolygon([
@@ -3889,6 +3912,16 @@
3889
3912
  requireTextShortForm(arguments);
3890
3913
  this.drawText(String(text), [x, y], { color, fontsize: size });
3891
3914
  }
3915
+ measureText(text, posOrOptions, maybeOptions) {
3916
+ requireMeasureText(arguments);
3917
+ const { pos, options } = textArguments("screen.measureText", 'measureText("Hi", { size: 20 })', posOrOptions, maybeOptions);
3918
+ const layout = layoutText(this.context, String(text), pos, options);
3919
+ return {
3920
+ width: layout.textWidth,
3921
+ height: layout.boxHeight,
3922
+ lines: [...layout.lines]
3923
+ };
3924
+ }
3892
3925
  drawSurface(surface, pos) {
3893
3926
  const target = rectFromBlitTarget(pos);
3894
3927
  const source = surface.source;
@@ -3996,20 +4029,33 @@
3996
4029
  }
3997
4030
  });
3998
4031
  }
3999
- drawText(text, posOrOptions, maybeOptions) {
4000
- let pos;
4001
- let given;
4002
- if (isPlainOptions(posOrOptions)) {
4003
- if (maybeOptions !== undefined) {
4004
- throw new TypeError('screen.draw.text takes its options once: either second, as in text("Hi", { center: [x, y] }), or third, after a position.');
4032
+ drawRoundedRect(call, rectLike, radius, color, width, filled = false) {
4033
+ const rect = new Rect(rectLike);
4034
+ const parsedRadius = requireRadius(call, radius);
4035
+ recordDrawn(this, {
4036
+ kind: "rect",
4037
+ x: rect.left,
4038
+ y: rect.top,
4039
+ width: rect.width,
4040
+ height: rect.height,
4041
+ color: colorOf(color),
4042
+ filled,
4043
+ radius: parsedRadius
4044
+ });
4045
+ withSavedCanvasState(this.context, () => {
4046
+ traceRoundedRect(this.context, rect, parsedRadius);
4047
+ if (filled) {
4048
+ this.context.fillStyle = normalizeColor(color);
4049
+ this.context.fill();
4050
+ } else {
4051
+ this.context.strokeStyle = normalizeColor(color);
4052
+ this.context.lineWidth = requirePositiveNumber(width, "width");
4053
+ this.context.stroke();
4005
4054
  }
4006
- given = posOrOptions;
4007
- } else {
4008
- pos = posOrOptions;
4009
- given = maybeOptions ?? {};
4010
- }
4011
- rejectUnknownOptions(given, textOptionKeys, "screen.draw.text");
4012
- const options = canonicalTextOptions(given, "screen.draw.text");
4055
+ });
4056
+ }
4057
+ drawText(text, posOrOptions, maybeOptions) {
4058
+ const { pos, options } = textArguments("screen.draw.text", 'text("Hi", { center: [x, y] })', posOrOptions, maybeOptions);
4013
4059
  const layout = layoutText(this.context, text, pos, options);
4014
4060
  recordDrawn(this, {
4015
4061
  kind: "text",
@@ -4063,6 +4109,21 @@
4063
4109
  const prototype = Object.getPrototypeOf(value);
4064
4110
  return prototype === Object.prototype || prototype === null;
4065
4111
  }
4112
+ function textArguments(label, example, posOrOptions, maybeOptions) {
4113
+ let pos;
4114
+ let given;
4115
+ if (isPlainOptions(posOrOptions)) {
4116
+ if (maybeOptions !== undefined) {
4117
+ throw new TypeError(`${label} takes its options once: either second, as in ${example}, or third, after a position.`);
4118
+ }
4119
+ given = posOrOptions;
4120
+ } else {
4121
+ pos = posOrOptions;
4122
+ given = maybeOptions ?? {};
4123
+ }
4124
+ rejectUnknownOptions(given, textOptionKeys, label);
4125
+ return { pos, options: canonicalTextOptions(given, label) };
4126
+ }
4066
4127
  function canonicalTextOptions(options, label) {
4067
4128
  let result = null;
4068
4129
  for (const [alias, canonical] of Object.entries(textOptionAliases)) {
@@ -4476,6 +4537,20 @@
4476
4537
  context.font = previousFont;
4477
4538
  }
4478
4539
  }
4540
+ function traceRoundedRect(context, rect, radius) {
4541
+ const left = Math.min(rect.left, rect.left + rect.width);
4542
+ const top = Math.min(rect.top, rect.top + rect.height);
4543
+ const right = left + Math.abs(rect.width);
4544
+ const bottom = top + Math.abs(rect.height);
4545
+ const r = Math.min(radius, (right - left) / 2, (bottom - top) / 2);
4546
+ context.beginPath();
4547
+ context.moveTo(left + r, top);
4548
+ context.arc(right - r, top + r, r, -Math.PI / 2, 0);
4549
+ context.arc(right - r, bottom - r, r, 0, Math.PI / 2);
4550
+ context.arc(left + r, bottom - r, r, Math.PI / 2, Math.PI);
4551
+ context.arc(left + r, top + r, r, Math.PI, Math.PI * 1.5);
4552
+ context.closePath();
4553
+ }
4479
4554
  function withSavedCanvasState(context, callback) {
4480
4555
  context.save();
4481
4556
  try {
@@ -4488,9 +4563,13 @@
4488
4563
  const values = Array.from(args);
4489
4564
  const name = form.call.slice(0, form.call.indexOf("("));
4490
4565
  const numbers = values.slice(0, form.numbers.length);
4491
- if (numbers.some((value) => typeof value === "object" && value !== null)) {
4492
- throw new TypeError(form.lists);
4566
+ const isList = (value) => typeof value === "object" && value !== null;
4567
+ const last = values[form.numbers.length - 1];
4568
+ if (form.colorTooSoon !== undefined && (typeof last === "string" || Array.isArray(last)) && !numbers.slice(0, -1).some(isList)) {
4569
+ throw new TypeError(form.colorTooSoon);
4493
4570
  }
4571
+ if (numbers.some(isList))
4572
+ throw new TypeError(form.lists);
4494
4573
  form.numbers.forEach((label, index) => {
4495
4574
  const value = values[index];
4496
4575
  if (value === undefined) {
@@ -4531,6 +4610,38 @@
4531
4610
  throw new TypeError(`screen.text takes at most five values: ${textCall}. For more options, use screen.draw.text.`);
4532
4611
  }
4533
4612
  }
4613
+ var measureCall = "screen.measureText(text, options)";
4614
+ function requireMeasureText(args) {
4615
+ const [text, second] = Array.from(args);
4616
+ if (text === undefined) {
4617
+ throw new TypeError(`screen.measureText is missing text: ${measureCall}.`);
4618
+ }
4619
+ if (typeof text !== "string" && !(typeof text === "number" && Number.isFinite(text))) {
4620
+ throw new TypeError(`screen.measureText's text must be words in quotes or a number, but got ${describeValue(text)}.`);
4621
+ }
4622
+ if (typeof second === "number") {
4623
+ throw new TypeError('screen.measureText takes its size in its options, as screen.draw.text does: screen.measureText("Score: 3", { size: 32 }) measures what screen.text("Score: 3", x, y, color, 32) draws.');
4624
+ }
4625
+ if (args.length > 3) {
4626
+ throw new TypeError(`screen.measureText takes the text and its options: ${measureCall}. It also takes a position before the options, as screen.draw.text does, but nothing more.`);
4627
+ }
4628
+ }
4629
+ function requireRadius(call, radius) {
4630
+ const name = call.slice(0, call.indexOf("("));
4631
+ if (radius === undefined) {
4632
+ throw new TypeError(`${name} is missing radius: ${call}.`);
4633
+ }
4634
+ if (typeof radius === "string" || Array.isArray(radius)) {
4635
+ throw new TypeError(`${name} takes the corners' radius before the color: ${call}.`);
4636
+ }
4637
+ if (typeof radius !== "number" || !Number.isFinite(radius)) {
4638
+ throw new TypeError(`${name}'s radius must be a number, but got ${describeValue(radius)}.`);
4639
+ }
4640
+ if (radius < 0) {
4641
+ throw new TypeError(`${name}'s radius must be 0 or more, but got ${radius}.`);
4642
+ }
4643
+ return radius;
4644
+ }
4534
4645
  function requirePositiveNumber(value, label) {
4535
4646
  if (typeof value === "number" && Number.isFinite(value) && value > 0) {
4536
4647
  return value;
@@ -6688,6 +6799,8 @@
6688
6799
  "filledPolygon",
6689
6800
  "rect",
6690
6801
  "filledRect",
6802
+ "roundedRect",
6803
+ "filledRoundedRect",
6691
6804
  "text",
6692
6805
  "textbox",
6693
6806
  "image"
@@ -6708,7 +6821,19 @@
6708
6821
  ["fillrectangle", "screen.draw.filledRect"],
6709
6822
  ["drawrectangle", "screen.draw.rect"],
6710
6823
  ["triangle", "screen.triangle"],
6711
- ["filledtriangle", "screen.triangle"]
6824
+ ["filledtriangle", "screen.triangle"],
6825
+ ["fillroundrect", "screen.draw.filledRoundedRect"],
6826
+ ["roundedrectangle", "screen.draw.roundedRect"],
6827
+ ["filledroundedrectangle", "screen.draw.filledRoundedRect"],
6828
+ ...[
6829
+ "measuretext",
6830
+ "measure",
6831
+ "textsize",
6832
+ "textwidth",
6833
+ "textheight",
6834
+ "gettextsize",
6835
+ "gettextwidth"
6836
+ ].map((name) => [name, "screen.measureText"])
6712
6837
  ]);
6713
6838
  function pointToService(cls, className, serviceName, service) {
6714
6839
  return new Proxy(cls, {
@@ -6734,6 +6859,7 @@
6734
6859
  "drawCircle",
6735
6860
  "drawPolygon",
6736
6861
  "drawRect",
6862
+ "drawRoundedRect",
6737
6863
  "drawText",
6738
6864
  "drawTextbox"
6739
6865
  ];
package/dist/node.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * js-game-zero 0.3.0
2
+ * js-game-zero 0.4.0
3
3
  * https://github.com/Leaf-Learning/js-game-zero
4
4
  *
5
5
  * MIT License
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * js-game-zero 0.3.0
2
+ * js-game-zero 0.4.0
3
3
  * https://github.com/Leaf-Learning/js-game-zero
4
4
  *
5
5
  * MIT License
@@ -33,7 +33,7 @@ export const reference = Object.freeze({
33
33
  "hooks.md": "# Hooks And Input Constants\n\nYour game is a set of top-level functions and settings. The runtime calls the\nfunctions it recognizes, in zero-boilerplate mode as plain `function`\ndeclarations and in import mode as properties on the object you pass to\n`run()`. This page documents the settings, every hook, then the `keys`,\n`keymods`, and `mouse` constant tables used by the input hooks.\n\nHook names are camelCase (`onKeyDown`, `onMouseDown`, ...), and work the\nsame way in both modes. (A game made with `defineGame()` is the exception:\nits input hooks receive one event object — see [`runtime.md`](runtime.md).)\nA hook spelled another way, such as Pygame Zero's `on_key_down`, is never\ncalled; the runtime warns about it with a `hook-typo` diagnostic that names\nthe camelCase hook to rename it to.\n\nA hook's arguments are matched **by parameter name** first. The runtime reads\nyour function's declared parameter names and passes the value with that name,\nin whatever order you wrote them. That means\n`function onKeyDown(mod, key) {}` receives `mod` and `key` correctly even\nthough the \"natural\" order is `key, mod`.\n\nA parameter with any other name receives the next value in the hook's\ndocumented order that isn't already taken by name, just like an ordinary\nJavaScript function: `function onKeyDown(k) {}` gets the key, and\n`function onMouseDown(position) {}` gets `pos`. The runtime adds a\n`hook-parameter` warning saying which value such a parameter receives, since\n`onMouseDown(btn)` gets the position, not the button. Name your parameters\nas shown below to avoid the warning.\n\nOne name is different: a hook whose only parameter is called `event`\nreceives all of its values in one object, as a `defineGame()` hook does, so\n`function onKeyDown(event) {}` reads `event.key` and `event.mod`.\n\n## Settings\n\nSet these at the top of a game, like `WIDTH = 480;`.\n\n| Setting | What it does | Default |\n| ---------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------ |\n| `WIDTH` | The game's width in pixels, rounded down to a whole number. `screen` and the mouse use it. | `800` |\n| `HEIGHT` | The game's height in pixels, rounded down to a whole number. | `600` |\n| `TITLE` | The page's title, shown on its tab. | The page's own title |\n| `ICON` | The name of an image in `images/`, as `Actor` takes it (`ICON = \"ship\";`), shown as the page's icon. | The page's own icon |\n| `PIXEL_DENSITY` | How many canvas pixels each game pixel gets. See [`screen.md`](screen.md#pixel_density-and-screendensity). | The device's pixel ratio |\n| `TOUCH_CONTROLS` | On-screen buttons for touch screens. See [Touch controls](#touch-controls). | None |\n\nA `WIDTH` or `HEIGHT` that isn't a positive number is ignored, with an\n`invalid-config` warning, and so is an `ICON` that isn't an image name. In\nimport mode, put the settings on the game module or in `run()`'s options\n(see [`runtime.md`](runtime.md#runoptions)).\n\n## draw()\n\n```ts\nfunction draw(): void;\n```\n\nCalled once per animation frame after `update` and after any pending assets\nhave loaded. `draw` must not declare any parameters — `run()` throws a\n`TypeError` at startup if it does, since a `draw(dt)` typo (mixing it up with\n`update`) is a common beginner mistake worth catching immediately.\n\n```js\nfunction draw() {\n screen.fill(\"#0f172a\");\n screen.draw.filledCircle([160, 90], 40, \"gold\");\n}\n```\n\n## update() / update(dt)\n\n```ts\nfunction update(): void;\nfunction update(dt: number): void;\n```\n\nCalled once per frame, before `draw`. Declare zero parameters if you don't\nneed frame timing, or one parameter (conventionally named `dt`) to receive the\nseconds elapsed since the previous frame — use it to make movement\nframe-rate-independent. When the game runs on its own, `dt` is clamped to at\nmost `0.25` seconds so a dropped frame (e.g. the tab was backgrounded) can't\ncause a huge jump. A host stepping the game itself, as `handle.step(dt)` and\nthe CLI's `step` do, passes its `dt` through unchanged.\n\n```js\nfunction update(dt) {\n if (keyboard.right) player.x += 200 * dt; // 200 px/sec, any frame rate\n}\n```\n\n## onKeyDown(key, mod, unicode, repeat) / onKeyUp(key, mod, unicode, repeat)\n\n```ts\nfunction onKeyDown(\n key: number,\n mod: number,\n unicode: string,\n repeat: boolean,\n): void;\nfunction onKeyUp(\n key: number,\n mod: number,\n unicode: string,\n repeat: boolean,\n): void;\n```\n\nFired on `keydown`/`keyup`.\n\n- `key` — **a numeric key constant from `keys`**, not a string. This is the\n single most common mistake when porting from other frameworks: compare it\n with `key === keys.LEFT`, never `key === \"left\"` or `key === \"ArrowLeft\"`.\n- `mod` — a bitmask of currently-held modifier keys from `keymods` (see\n below). Test a specific modifier with `mod & keymods.SHIFT`.\n- `unicode` — the typed character as a one-character string (`\"\"` for\n non-printable keys like arrows or function keys).\n- `repeat` — `true` if this event is an OS-generated key-repeat while the key\n is held down, rather than the initial press. It's always `false` for\n `onKeyUp`.\n\n```js\nfunction onKeyDown(key, mod) {\n if (key === keys.SPACE) jump();\n if (key === keys.R && mod & keymods.CTRL) restartLevel();\n}\n```\n\n`keyboard.left`, `keyboard.a`, and similar boolean properties (see below) are\nusually a better fit for continuous movement inside `update`; use\n`onKeyDown`/`onKeyUp` for one-shot actions like jumping or firing.\n\n## Touch controls\n\nSet `TOUCH_CONTROLS` near `WIDTH` and `HEIGHT` to add controls below your\ngame on phones, tablets, and computers with touch input. The runtime fits\nthe canvas into the space above the controls, preserving its aspect ratio\nand logical `WIDTH`/`HEIGHT`. Keyboard code works with these buttons too:\n\n```js\nTOUCH_CONTROLS = {\n left: keys.LEFT,\n right: keys.RIGHT,\n a: keys.SPACE,\n start: keys.RETURN,\n};\n```\n\nIn portrait mode, the dock places the D-pad on the left and the action\nbuttons on the right, with Start/Select beneath them; a game with no A or B\nputs Start/Select on the right instead. A sits above and to the right of B\nin a Game Boy-style diagonal arrangement. Three or four directions join into\none cross; left and right alone (or up and down) stay two separate keys. The\ndock stays below the canvas in both portrait and landscape layouts, near the\nbottom of the game's viewport. In portrait, the canvas is centered\nvertically with at least 48px above the control dock. On shorter screens it\nmoves upward as needed to keep the game and controls visible. Landscape\nkeeps a 16px gap. Bottom padding keeps buttons clear of the device's home\nindicator.\n\nOnly the buttons you list appear. Supported names are `up`, `down`, `left`,\n`right`, `a`, `b`, `start`, and `select`. Each value must be a numeric\nconstant from `keys`; the button's label is independent of its mapped key.\nFor example, `a: keys.SPACE` makes A trigger a game's existing Space action.\nOmitting `TOUCH_CONTROLS`, or setting it to `false`, hides the controls.\n\n`TOUCH_CONTROLS = true` enables this full preset:\n\n| Button | Mapped key |\n| ------------------------ | ---------------------------------------------------- |\n| Up / Down / Left / Right | `keys.UP` / `keys.DOWN` / `keys.LEFT` / `keys.RIGHT` |\n| A | `keys.SPACE` |\n| B | `keys.X` |\n| Start | `keys.RETURN` |\n| Select | `keys.ESCAPE` |\n\nThe default `show: \"auto\"` displays controls when the browser reports touch\nsupport or a coarse pointing device. Add `show: \"always\"` to a mapping to\npreview them on desktop. These are native buttons, so mouse clicks and\nTab followed by Space or Enter work as well.\n\nHolding a button updates `keyboard` and fires `onKeyDown` once; releasing\nit fires `onKeyUp`. Virtual presses have `mod: keymods.NONE`,\n`unicode: \"\"`, and `repeat: false`; use `update(dt)` for repeated actions.\nMultiple fingers can hold directions and action buttons together. Slide a\nfinger across the D-pad to change direction. Each mapped key stays held\nuntil every touch button and physical key holding it has\nbeen released. Cancellation, loss of pointer capture, leaving the window,\nand hiding the page clear held inputs. Stopping or reloading a game removes\nits controls and listeners.\n\nA page that hosts a game can restyle the dock by setting these CSS custom\nproperties on any element around the canvas, such as `:root`:\n\n| Property | What it sets | Default |\n| -------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------- |\n| `--jgz-key-size` | A key's size; the cross is three keys across | `clamp(40px, 10vw, 52px)` |\n| `--jgz-key-fill`, `--jgz-key-ink` | A key's fill, and its label and arrow color | `#263244`, `#fff` |\n| `--jgz-key-line`, `--jgz-key-line-width` | A key's outline | `#718096`, `2px` |\n| `--jgz-key-pressed-fill`, `--jgz-key-pressed-line` | A held key's fill and outline | `#4267a0`, `#b9d5ff` |\n| `--jgz-key-shadow-x`, `-y`, `-color` | A hard shadow under each key and the cross; a held key moves onto it | `0px`, `0px`, `transparent` |\n| `--jgz-key-radius` | Separate keys' corners (any `border-radius`) | `12px` |\n| `--jgz-dpad-radius` | The cross's outer corners | `10px` |\n| `--jgz-dpad-dot` | The dot in the middle of the cross | `rgba(255, 255, 255, 0.16)` |\n| `--jgz-key-font`, `--jgz-action-font` | Start/Select labels, and A/B labels (a `font` shorthand) | `bold 13px` and `bold 18px` `system-ui` |\n| `--jgz-action-line-width` | A and B's outline | `--jgz-key-line-width` |\n| `--jgz-system-height`, `--jgz-system-radius` | Start and Select's height and corners | `40px`, `12px` |\n| `--jgz-focus-ring` | The keyboard focus outline | `#facc15` |\n| `--jgz-dock-gap` | The least space between the canvas and the dock | `48px` portrait, `16px` landscape |\n\nIn import mode, put `TOUCH_CONTROLS` on the module returned by `defineGame`,\non a raw game module, or in the options passed to `run`. TypeScript exports\n`TouchButton`, `TouchControlsConfig`, and `TouchControlsOptions`.\n\n### Tapping and dragging the canvas\n\nCanvas touch and pen input work even without a control dock. A primary\ntouch triggers `onMouseDown`, dragging triggers `onMouseMove` with\n`mouse.LEFT` in `buttons`, and releasing or cancelling triggers\n`onMouseUp`. Positions and movement are converted to logical game\ncoordinates when the canvas is scaled. The pointer remains captured when\nit leaves the canvas; extra canvas touches are ignored until the first is\nreleased. Touch buttons do not trigger canvas mouse hooks.\n\nThe canvas and control buttons disable native touch panning during play;\nthe surrounding page retains its normal gestures. Games can therefore use\ntap-to-flap or drag-to-steer without adding separate touch hooks.\n\nFor embedding, give each canvas a parent with a defined height. While the\ndock is visible, the runtime groups the canvas and controls into a column\nthat fills the available space, anchors controls at the bottom, and centers\nthe portrait canvas when space permits. It accounts for visible siblings,\npadding, and row gaps, and restores the original canvas placement when the\ndock is hidden or the game stops.\nThe sandbox preview handles this automatically.\n\n## onMouseDown(pos, button) / onMouseUp(pos, button)\n\n```ts\nfunction onMouseDown(pos: [number, number], button: number): void;\nfunction onMouseUp(pos: [number, number], button: number): void;\n```\n\nFired on `mousedown`/`mouseup`, and also for **wheel scroll**: a wheel event\nis reported as `onMouseDown` with `button` set to `mouse.WHEEL_UP` or\n`mouse.WHEEL_DOWN` (matching pygame's convention of treating the scroll wheel\nas buttons 4 and 5, rather than a separate event). There is no\n`onMouseWheel` hook — check `button` inside `onMouseDown` instead.\n\n```js\nfunction onMouseDown(pos, button) {\n if (button === mouse.LEFT) fireAt(pos);\n if (button === mouse.WHEEL_UP) zoom += 1;\n if (button === mouse.WHEEL_DOWN) zoom -= 1;\n}\n```\n\n## onMouseMove(pos, rel, buttons)\n\n```ts\nfunction onMouseMove(\n pos: [number, number],\n rel: [number, number],\n buttons: Set<number>,\n): void;\n```\n\nFired on `mousemove`. `rel` is the movement since the previous event (from\nthe browser's `movementX`/`movementY` when available). `buttons` is a `Set`\nof currently held mouse buttons (`mouse.LEFT`, `mouse.MIDDLE`, `mouse.RIGHT`)\n— check membership with `buttons.has(mouse.LEFT)`.\n\n## onMusicEnd()\n\n```ts\nfunction onMusicEnd(): void;\n```\n\nFired once the active `music` track (and any queued tracks) finish playing\nwith nothing left to play.\n\n## `keyboard`\n\n`keyboard` is a live object of boolean properties reflecting which keys are\ncurrently held down, one for each key in `keys` under the same name in any\ncapitalization (`keyboard.left`, `keyboard.space`, `keyboard.a`, and\n`keyboard.K_1` for the digit keys). Read it inside `update()` for continuous\nmovement:\n\n```js\nfunction update(dt) {\n if (keyboard.left) player.x -= 150 * dt;\n if (keyboard.right) player.x += 150 * dt;\n}\n```\n\nAccessing an attribute that isn't a real key name throws an error (`The key\n\"...\" does not exist`) instead of silently returning `false` — this catches\ntypos like `keyboard.lfet` immediately rather than letting your character\nquietly fail to move. A misspelling says which key it's closest to:\n`keyboard.lfet doesn't exist. Did you mean keyboard.left?`. `keyboard.pressed()` returns a sorted array of every\ncurrently-held key constant, for debugging.\n\n## `keys`\n\nNumeric key constants, one property per key. The full set includes:\n\n| Category | Names |\n| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Letters | `keys.A` … `keys.Z` |\n| Digits | `keys.K_0` … `keys.K_9` |\n| Arrows | `keys.UP`, `keys.DOWN`, `keys.LEFT`, `keys.RIGHT` |\n| Editing | `keys.BACKSPACE`, `keys.TAB`, `keys.RETURN` (alias `keys.ENTER`), `keys.DELETE`, `keys.ESCAPE`, `keys.SPACE` |\n| Function keys | `keys.F1` … `keys.F15` |\n| Navigation | `keys.HOME`, `keys.END`, `keys.PAGEUP`, `keys.PAGEDOWN`, `keys.INSERT` |\n| Modifiers (as key presses) | `keys.LSHIFT`, `keys.RSHIFT`, `keys.LCTRL`, `keys.RCTRL`, `keys.LALT`, `keys.RALT`, `keys.LMETA`, `keys.RMETA` |\n| Numpad | `keys.KP0` … `keys.KP9`, `keys.KP_PERIOD`, `keys.KP_DIVIDE`, `keys.KP_MULTIPLY`, `keys.KP_MINUS`, `keys.KP_PLUS`, `keys.KP_ENTER`, `keys.KP_EQUALS` |\n| Punctuation | `keys.MINUS`, `keys.EQUALS`, `keys.LEFTBRACKET`, `keys.RIGHTBRACKET`, `keys.BACKSLASH`, `keys.SEMICOLON`, `keys.QUOTE`, `keys.BACKQUOTE`, `keys.COMMA`, `keys.PERIOD`, `keys.SLASH` |\n| Locks | `keys.CAPSLOCK`, `keys.NUMLOCK`, `keys.SCROLLOCK` |\n\nThe values are arbitrary integers (matching SDL/pygame's numbering) — never\nhardcode them, always compare against the named constant. Constants are\nupper case. In a game file, any other spelling such as `keys.space` or\n`keys.SPCE` throws with a suggestion (`Did you mean keys.SPACE?`) instead of\nquietly comparing against `undefined`. The same goes for `keymods` and\n`mouse`.\n\n## `keymods`\n\nA bitmask of held modifier keys, combined with `|` and tested with `&`:\n\n| Constant | Meaning |\n| ----------------------------------- | ----------------------------------------- |\n| `keymods.NONE` | No modifier held |\n| `keymods.SHIFT` | Either Shift key (`LSHIFT \\| RSHIFT`) |\n| `keymods.CTRL` | Either Control key (`LCTRL \\| RCTRL`) |\n| `keymods.ALT` | Either Alt/Option key (`LALT \\| RALT`) |\n| `keymods.META` | Either Cmd/Windows key (`LMETA \\| RMETA`) |\n| `keymods.LSHIFT` / `keymods.RSHIFT` | Left/right Shift specifically |\n| `keymods.LCTRL` / `keymods.RCTRL` | Left/right Control specifically |\n| `keymods.LALT` / `keymods.RALT` | Left/right Alt specifically |\n| `keymods.LMETA` / `keymods.RMETA` | Left/right Cmd/Windows specifically |\n| `keymods.CAPS` | Caps Lock is toggled on |\n| `keymods.NUM` | Num Lock is toggled on |\n| `keymods.MODE` | AltGr / alternate-graphics modifier |\n\n```js\nfunction onKeyDown(key, mod) {\n if (mod & keymods.SHIFT) console.log(\"shift-modified key\");\n}\n```\n\n## `mouse`\n\nIn a game file, `mouse` also says where the mouse is right now, for code in\n`update()` that follows or aims at it — the way `keyboard` does for keys:\n\n| Property | Meaning |\n| --------------- | ----------------------------------------------------------------------------- |\n| `mouse.pos` | `[x, y]` of the mouse (or a finger) over the game. The center until it moves. |\n| `mouse.x` | The same `x`. |\n| `mouse.y` | The same `y`. |\n| `mouse.buttons` | A `Set` of buttons held now: `mouse.buttons.has(mouse.LEFT)`. |\n| `mouse.down` | `true` while any button, or a finger, is down. |\n\n```js\nfunction update(dt) {\n ship.angle = ship.angleTo(mouse.pos); // aim at the mouse\n}\n```\n\nButton constants:\n\n| Constant | Value | Meaning |\n| ------------------ | ----- | ---------------------------------------------------- |\n| `mouse.LEFT` | 1 | Left mouse button |\n| `mouse.MIDDLE` | 2 | Middle mouse button (wheel click) |\n| `mouse.RIGHT` | 3 | Right mouse button |\n| `mouse.WHEEL_UP` | 4 | Scroll wheel moved up (reported via `onMouseDown`) |\n| `mouse.WHEEL_DOWN` | 5 | Scroll wheel moved down (reported via `onMouseDown`) |\n",
34
34
  "loaders.md": "# Loading Assets\n\nGames load four kinds of asset, each from its own conventional directory:\n\n| Kind | Directory | Extensions |\n| ------ | --------- | ------------------------------------------------- |\n| Images | `images/` | `png`, `gif`, `jpg`, `jpeg`, `bmp`, `webp`, `svg` |\n| Sounds | `sounds/` | `wav`, `ogg`, `mp3`, `m4a` |\n| Music | `music/` | `wav`, `ogg`, `mp3`, `m4a` |\n| Fonts | `fonts/` | `ttf`, `otf`, `woff`, `woff2` |\n\nIn the sandbox, these are the Images, Sounds, Music and Fonts panels; for examples and\nhand-written projects, they're plain directories next to your game's code.\n\n## Asset name grammar\n\nAn asset name is a lowercase, forward-slash path, resolved relative to its\nkind's directory — `\"player.svg\"` means `images/player.svg`,\n`\"levels/forest_bg.png\"` means `images/levels/forest_bg.png`. Rules:\n\n- Each path segment must start with a lowercase letter and contain only\n lowercase letters, digits, and underscores (no hyphens, no uppercase, no\n spaces) — `\"player.svg\"` is valid, `\"Player.svg\"` and `\"player-1.svg\"` are\n not.\n- No leading `/`, no `\\`, no empty segments, no `.`/`..` segments.\n- At most one extension per segment, and it must be one of the kind's\n supported list above, in lowercase.\n- The extension is optional when you reference the asset — `images.player`\n and `new Actor(\"player\")` both work by trying every supported extension\n for that kind in turn until one loads.\n\nAn invalid name throws `InvalidAssetNameError`; a name that doesn't match\nany file throws `MissingAssetError` naming the asset kind and the name you\nasked for — both are meant to be immediately readable, so check the browser\nconsole for the exact reason before assuming a typo is something else.\n\n## Using assets\n\n### Images\n\nMost games load images through `Actor` (see\n[`docs/reference/actor.md`](actor.md)) or `screen.blit()`. To use an image\ndirectly — for its dimensions, say — the zero-boilerplate `images` global\n(or `import { images } from \"js-game-zero\"`) exposes property-style\nlazy access:\n\n```js\nlet ship = images.player; // starts loading images/player.svg\n\nfunction draw() {\n let [width, height] = ship.getSize();\n}\n```\n\nThe game's first frame waits for every image its top level named, so their\nsizes are there in `update()` and `draw()`. At the top level an image is\nusually still loading, and reading its size there throws an error that says\nso, unless the host page handed the images over already decoded\n(`loadImages`, which `jgz` and the sandbox use).\n\nAn image has `getWidth()`, `getHeight()`, `getSize()` and `getRect()`. Give\n`getRect()` a point to place it by, like an Actor's\n(`images.player.getRect({ center: [160, 90] })`), for a `Rect` of the image's\nsize there; without one it sits at `[0, 0]`.\n\nIn an import-mode game, or any `async` function, load explicitly and `await`\nthe result:\n\n```js\nconst sprite = await images.load(\"player.svg\");\nsprite.getWidth();\nsprite.getHeight();\n```\n\n### Sounds and music\n\n`sounds.<name>` and `music.play(name)` resolve assets the same way — see\n[`docs/reference/audio.md`](audio.md) for the playback API.\n\n### Fonts\n\nFonts aren't injected as a zero-boilerplate global; use `fontname` in\n`draw.text`/`draw.textbox` (see [`docs/reference/screen.md`](screen.md)) and\nthe loader starts fetching the font in the background the first time it's\nreferenced:\n\n```js\nscreen.draw.text(\"Retro!\", [20, 20], { fontname: \"pixel\", fontsize: 32 });\n```\n\n`fontname` is the file's stem (`fonts/pixel.ttf` → `\"pixel\"`), registered\nwith the browser under that family name once loaded. If you need to `await`\na font being ready (e.g. to measure text before your first draw), import\n`fonts` and call `await fonts.load(\"pixel\")` directly.\n\n## Assets without a server\n\nBy default a run loads each asset from a URL under `assetBaseUrl`\n(`/assets/<projectId>/images/cat.svg`), so something has to serve them. A\nhost page that already has the files can hand them over instead, with\n`assetUrls`: a map from each asset's path, as it sits in the project, to a\nURL the browser can load, such as a data URL or a blob URL.\n\n```js\nrun(source, {\n assetUrls: {\n \"images/cat.svg\": \"data:image/svg+xml;base64,PHN2ZyB4bWxucz0i…\",\n \"sounds/pop.wav\": \"data:audio/wav;base64,UklGRi…\",\n },\n});\n```\n\nGames name assets as usual: `new Actor(\"cat\")`, `images.cat` and\n`sounds.pop` find `images/cat.svg` and `sounds/pop.wav` in the map, trying\neach supported extension as they would on a server. With a map, assets come\nonly from it: one it doesn't list is a `MissingAssetError`, and nothing is\nrequested from `assetBaseUrl`.\n\n`run()` starts a game's code straight away, and an image the run loads\nitself is still decoding while the top level runs, so it has no size yet:\n`player.width` reads 0 there, and an Actor placed by `midBottom` and then\nmoved lands as if it were 0×0. A page can decode the images first with\n`loadImages` from `js-game-zero/host` (`JGZ.loadImages` in the\nscript-tag build), as `jgz` and the sandbox do. The game then finds them\nready from its first line, as a Pygame Zero game would:\n\n```js\nconst images = await loadImages(assetUrls);\nrun(source, { assetUrls, preloadedAssets: { images } });\n```\n\n`loadImages` decodes every `images/` entry of the map, by its name under\n`images/`. One that fails is left out, so the run loads it itself and\nreports the failure only if the game uses it. Sounds, music and fonts given as data URLs\nare decoded in the page rather than fetched, so a page whose content\nsecurity policy allows no connections (no `connect-src`) still plays them;\nimages need `img-src data:` (or `blob:`), and sounds `media-src` to match.\nAn `ICON` the map doesn't list leaves the page's favicon as it is.\n\n## Loader methods (advanced)\n\nEvery asset kind is backed by a loader with the same shape. Games normally use\nthe `images`, `sounds`, and `fonts` services from `js-game-zero`; music\nplayback uses the `music` player. Hosts that need to construct or override\nloaders import `createAssetLoaders()` or the individual factories from\n`js-game-zero/host`:\n\n- `load(name): Promise<Asset>` — fetches and caches the asset.\n- `assetUrls` — the asset map the loader reads instead of `assetBaseUrl`,\n or `undefined` when it has none.\n- `get(name): Asset | undefined` — synchronous; only returns something if\n already cached.\n- `has(name): boolean`\n- `define(name, asset)` — registers an asset object directly without going\n through the network, useful for tests or preloading fixtures.\n- `unload(name): boolean` — evicts one cached asset.\n- `unloadAll()` — clears the whole cache.\n- `cacheKeys(): string[]` — every currently-cached asset name, sorted.\n\nWhy a frame never shows a half-loaded sprite: each runtime tracks its own image\nand font loads started while the script's top level runs (and any started\nafterward) and holds frames until they settle. Audio loads remain recoverable\nand do not gate frames — see the\n\"preload gate\" section of [`docs/reference/runtime.md`](runtime.md).\n",
35
35
  "rect.md": "# Rect And ZRect\n\n`Rect` is a mutable rectangle with pygame/pgzero-style geometry helpers:\nposition properties that move the rectangle without resizing it, collision\ntests, and set operations. `ZRect` is a subclass of `Rect` that adds\nnothing, kept for code ported from pgzero that references `ZRect` directly —\nthe two behave the same, and a `ZRect` is also a `Rect`.\n\n## Constructing a Rect\n\n```ts\nnew Rect(left: number, top: number, width: number, height: number)\nnew Rect(topLeft: [number, number], size: [number, number])\nnew Rect(other: Rect)\nnew Rect([left, top, width, height])\nnew Rect({ left, top, width, height })\nnew Rect({ x, y, w, h })\nnew Rect(objectWithADotRectPropertyOrMethod)\n```\n\nThe last form is duck-typed: if you pass any object with a `.rect` property\n(or a `.rect()` method), the `Rect` constructor follows it — recursively, up\nto a sensible depth — until it finds real rectangle data. This is what lets\nyou write `new Rect(myActor)` directly, since `Actor` exposes a `.rect()`\nmethod, and it means most `Rect`/`Actor` collision methods below accept an\n`Actor` anywhere they say \"rect-like\" without you needing to call `.rect()`\nyourself.\n\n```js\nlet hitbox = new Rect(10, 20, 40, 30);\nlet same = new Rect(hitbox); // copy via any accepted shape\nlet fromActor = new Rect(player); // works because Actor has .rect()\n```\n\n## Properties\n\nAll of these read or write the rectangle in place; setting a position\nproperty moves the rectangle without changing its size, and setting `width`,\n`height`, `w`, `h`, or `size` resizes it.\n\n| Property | Meaning |\n| -------------------------------------------------- | --------------------------------- |\n| `left`, `top`, `width`, `height` | The four base fields. |\n| `x`, `y`, `w`, `h` | Aliases for the same. |\n| `right`, `bottom` | `left + width`, `top + height`. |\n| `centerX`, `centerY` | Center point, one axis at a time. |\n| `topLeft`, `topRight`, `bottomLeft`, `bottomRight` | Corners, as `[x, y]`. |\n| `midTop`, `midLeft`, `midRight`, `midBottom` | Edge midpoints, as `[x, y]`. |\n| `center` | `[centerX, centerY]`. |\n| `size` | `[width, height]`. |\n\n`Rect` also supports index access (`rect[0]` through `rect[3]` for\n`left`/`top`/`width`/`height`) and spreading/iteration\n(`[...rect]` → `[left, top, width, height]`, `Array.from(rect)`), so it can\nbe dropped into APIs that expect a plain tuple.\n\n```js\nhitbox.center = [100, 80]; // moves it; width/height unchanged\nhitbox.size = [64, 64]; // resizes it in place\nlet [left, top, width, height] = hitbox;\n```\n\n## Copying and moving\n\n- `copy(): Rect`\n- `move(x, y)` / `move([x, y])` → new `Rect`; `moveIp(...)` moves in place.\n- `inflate(x, y)` / `inflate([x, y])` → new `Rect`, grown by `x`/`y` around\n its center (negative shrinks); `inflateIp(...)` mutates in place.\n- `normalize()` — flips a negative `width`/`height` so the rectangle has a\n well-defined top-left corner (mutates in place).\n\n## Set operations\n\nEach but `fit` has a mutating `…Ip` counterpart that applies the result back\nonto the rectangle itself instead of returning a new one:\n\n- `clamp(other)` / `clampIp(other)` — moves (without resizing) to fit\n entirely inside `other`, centering on whichever axis doesn't fit.\n- `clip(other)` / `clipIp(other)` — the overlapping area of this rect and\n `other` (zero-sized if they don't overlap).\n- `union(other)` / `unionIp(other)` — the smallest rect containing both.\n- `unionAll(others)` / `unionAllIp(others)` — union across an array of\n rect-likes.\n- `fit(other)` — scales (preserving aspect ratio) and centers this rect to\n fit inside `other`.\n\n```js\nlet hitbox = new Rect(300, 60, 40, 40);\nhitbox.clampIp(screen.bounds()); // now fully on screen\n```\n\n`Actor` has its own `clampIp`, which moves the actor itself:\n`player.clampIp(screen.bounds())` keeps the player on screen. (Calling\n`player.rect().clampIp(...)` would only move a copy of the player's\nrectangle, not the player.)\n\n## Collision tests\n\nCollision uses **half-open bounds**: the left and top edges are inclusive,\nthe right and bottom edges are exclusive — a point exactly on the right or\nbottom edge does not count as colliding. This matches pygame's `Rect`.\n\n- `touches(target): boolean` — the one test most games need: `target` is an\n `[x, y]` point (does the rect contain it?) or any rect-like value, such as\n another `Rect` or an `Actor` (do they overlap?).\n- `contains(other): boolean`\n- `collidePoint(point)` / `collidePoint(x, y): boolean`\n- `collideRect(other): boolean`\n- `collideList(others): number` — index of the first colliding rect-like in\n the array, or `-1`.\n- `collideListAll(others): number[]` — indexes of every colliding entry.\n- `collideDict(dict, useValues = true)` — `dict` is a `Map` or plain object;\n returns the first `[key, value]` entry whose value (or key, if\n `useValues` is `false`) collides with this rect, or `undefined`.\n- `collideDictAll(dict, useValues = true)` — same, but returns every\n matching `[key, value]` entry as an array.\n\n```js\nlet enemies = { goblin: goblinRect, bat: batRect };\nlet hit = player.rect().collideDict(enemies);\nif (hit) console.log(`hit the ${hit[0]}`);\n```\n\n## Standalone helpers\n\n`angleTo(from, to)` and `distanceTo(from, to)` are exported functions (used\ninternally by `Actor.angleTo`/`distanceTo`) if you need the same math on\nplain points without an `Actor`. They aren't globals in a game file; import\nthem from `js-game-zero`.\n",
36
- "runtime.md": "# The `run()` Module API\n\nMost learners never call `run()` directly — the sandbox and zero-boilerplate\nexamples call it for you, and you just write top-level `WIDTH`/`draw`/\n`update`/hook definitions. This page is for import-mode code and for anyone\nembedding the framework in a host page (a custom sandbox, a course platform,\nan automated test).\n\n```ts\nfunction run(\n moduleOrSource: GameModule | DefinedGame | string,\n options?: RunOptions,\n): GameHandle;\n```\n\n## Three entry styles\n\n**Scoped import mode (recommended for applications)** — `defineGame()` gives\nthe game a typed set of services owned by that one run. Actors, assets,\nstorage, animation, input, and audio therefore cannot accidentally bind to a\ndifferent game on the same page:\n\n```ts\nimport { defineGame, run } from \"js-game-zero\";\n\nrun(\n defineGame(({ Actor, screen, keyboard }) => {\n const player = new Actor(\"player.svg\");\n return {\n WIDTH: 320,\n HEIGHT: 180,\n update(dt) {\n if (keyboard.right) player.x += 120 * dt;\n },\n draw() {\n screen.clear();\n player.draw();\n },\n onKeyDown(event) {\n console.log(event.key);\n },\n };\n }),\n);\n```\n\nThe input hooks in a defined game receive one typed event object.\nZero-boilerplate and raw import hooks keep their Pygame Zero-style\nparameter-name dispatch.\n\n| Event | Fields |\n| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `GameKeyEvent` (`onKeyDown`, `onKeyUp`) | `key`, `mod`, `unicode` and `repeat`, as [`hooks.md`](hooks.md#onkeydownkey-mod-unicode-repeat--onkeyupkey-mod-unicode-repeat) describes them. |\n| `GameMouseEvent` (`onMouseDown`, `onMouseUp`, `onMouseMove`) | `pos`, `button` (the button pressed or released, or `null` for one with no `mouse` constant; `onMouseMove` reads `buttons` instead), `buttons` (a `Set` of held buttons) and `rel` (movement since the last event). |\n\nThe function given to `defineGame()` receives the run's own services:\n`screen`, `clock`, `keyboard`, `mouse`, `images`, `sounds`, `music`, `fonts`,\n`storage`, `tone`, `animate` and `exit`, the constants `keys` and `keymods`,\nand the classes `Actor`, `Animation`, `Rect`, `ZRect`, `Screen` and\n`Storage`. An `Actor` or `Animation` made from them belongs to that run.\n\nPackage-level `screen`, `clock`, `keyboard`, `images`, `sounds`, `music`, and\n`storage` remain available for learner compatibility. They are convenience\nproxies for a single active runtime; applications that can run more than one\ngame should prefer the services passed to `defineGame()`.\n\n**Import mode** — pass a plain object of hooks and config:\n\n```js\nimport { run, screen, keyboard } from \"js-game-zero\";\n\nrun({\n WIDTH: 320,\n HEIGHT: 180,\n draw() {\n screen.fill(\"#10243d\");\n },\n update(dt) {\n /* ... */\n },\n onKeyDown(key) {\n /* hooks receive values by parameter name */\n },\n});\n```\n\n**Global mode** — pass a source-code string, which runs with the framework's\nglobals (`Actor`, `screen`, `keyboard`, `WIDTH`, `draw`, ...) already in\nscope, exactly like a zero-boilerplate sandbox project. This is how the\nsandbox's preview pane runs the code from your editor.\n\n```js\nrun(`\n WIDTH = 320;\n HEIGHT = 180;\n function draw() { screen.fill(\"navy\"); }\n`);\n```\n\n`handle.mode` tells you which one was used (`\"import\"` or `\"global\"`).\n\nA global-mode game runs in its own scope rather than the page's. Reading a\nname nothing defines throws a `ReferenceError` with a suggestion\n(`scren is not defined. Did you mean screen?`); `typeof name` stays safe.\nBrowser APIs a game can't reach say what to use instead — for example,\n`setTimeout` points at `clock.schedule(callback, seconds)`, and\n`localStorage` at `storage`. Standard JavaScript built-ins such as `Math`,\n`JSON`, `isNaN`, `Map` and `console` are available as usual.\n\nEvery library name has one spelling, in camelCase: `screen.draw.filledCircle`,\n`actor.angleTo`, `rect.collidePoint`, `clock.scheduleInterval`, `onKeyDown`.\nConstants stay upper case (`WIDTH`, `keys.SPACE`, `mouse.LEFT`). Another\nspelling of a member, such as Pygame Zero's `actor.angle_to` or\n`rect.colliderect`, throws where it's read, naming the real one\n(`actor.angle_to doesn't exist. Did you mean actor.angleTo?`). In a\nglobal-mode game, `screen`, `clock`, `music`, `tone`, `keys`, `keymods` and\n`mouse` also throw with a suggestion for any member they don't have\n(`screen.draw.cirlce doesn't exist. Did you mean screen.draw.circle?`), and\nan undefined global suggests the real one\n(`random_int is not defined. Did you mean randomInt?`).\nThe classes behind them (`Screen`, `Clock`, `Storage`) are globals too, so a\ncapital letter by mistake, as in `Screen.fill(\"navy\")`, points at the service\ninstead (`Screen.fill doesn't exist. Did you mean screen.fill?`).\n\n## `RunOptions`\n\n| Option | Purpose |\n| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `canvas` | Draw into an existing `<canvas>` instead of creating one. |\n| `container` | Where to mount a created canvas (default the page's `<body>`). Called from a script in `<head>`, before there's a `<body>`, the game starts at once and its canvas joins the `<body>` when the page has been read. |\n| `autostart` | Default `true`. Set `false` to skip the automatic frame loop and drive frames yourself via `handle.step(dt)` — useful for deterministic tests. |\n| `projectId` | Scopes storage keys and asset URLs, so multiple games on one page don't collide. |\n| `assetBaseUrl` | Where asset URLs are rooted (default `/assets`). |\n| `assetUrls` | The game's assets by path, each with its URL: `{ \"images/cat.svg\": \"data:image/svg+xml;base64,…\" }`. When set, assets load only from here, never from `assetBaseUrl`. See [assets without a server](loaders.md#assets-without-a-server). |\n| `preload` | An array of asset paths (e.g. `[\"images/player.svg\"]`) to start loading immediately, before any is referenced by code. |\n| `preloadedAssets` | Pre-built `{ images, sounds, music, fonts }` asset maps to seed the cache synchronously — used by hosts that already have the bytes. `loadImages(assetUrls)` makes the `images` map; see [assets without a server](loaders.md#assets-without-a-server). |\n| `storage` | A `Storage` instance or a raw `StorageAdapter` (`{ path, load(), save(text) }`) to use instead of the default `localStorage`-backed one. |\n| `loaders` | Override one or more of the `images`/`sounds`/`music`/`fonts` loader instances. |\n| `keyboardTarget` | The `EventTarget` that receives keyboard events (default `window`). |\n| `mouseReleaseTarget` | The `EventTarget` where mouse releases (`mouseup`) are heard (default `window`, so a drag released outside the canvas still ends). A host driving a game itself passes the canvas, so its releases reach no other game. |\n| `seed` | A string or number that makes the game's randomness repeatable: `Math.random` as a zero-boilerplate game sees it, and `randomInt` and `randomChoice`, follow one sequence for this seed, the sequence `jgz test --seed` uses. See [seeded runs](#seeded-runs). |\n| `TOUCH_CONTROLS` | `true` for the full touch-control preset, `false` to disable it, or a button mapping with optional `show: \"auto\"` / `\"always\"`. See [touch controls](hooks.md#touch-controls). |\n| `PIXEL_DENSITY` | Canvas pixels per logical pixel. Defaults to the device's pixel ratio, from 1 to 3. See [pixel density](#pixel-density). |\n| `scheduler` | Override `requestAnimationFrame`/`cancelAnimationFrame` (for tests). |\n| `diagnostics` | `(diagnostic) => void` — called for every diagnostic (see below). Without it, they go to the browser console. |\n\nAny hook or `WIDTH`/`HEIGHT`/`PIXEL_DENSITY`/`TITLE`/`ICON`/`TOUCH_CONTROLS`\nconfig can also be passed directly on `RunOptions`; if the same one is also present on the module\nobject, the option wins and a diagnostic warns about the duplicate.\n\n## `GameHandle`\n\n`run()` returns a handle for controlling the running game:\n\n| Member | Purpose |\n| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |\n| `canvas`, `screen`, `clock`, `keyboard`, `loaders`, `storage` | The live instances this run is using. |\n| `module` | The final, merged hook/config object. |\n| `diagnostics` | The diagnostics collected so far. |\n| `state`, `failure` | Lifecycle state and the first fatal runtime failure, if one occurred. |\n| `ready: Promise<void>` | Resolves once the preload gate is satisfied (see below). |\n| `done: Promise<void>` | Resolves after final storage flushing and resource cleanup complete. |\n| `pause()` / `resume()` | Stop/restart the frame loop without tearing anything down. |\n| `step(dt)` | Manually advance one frame by `dt` seconds — only meaningful with `autostart: false`. |\n| `stop()` | Detaches input listeners, flushes any pending storage save, and cancels the frame loop. |\n| `dispose()` | Awaited form of cleanup; resolves only after pending persistence work is finished. |\n| `restart(nextSource?)` | Awaits cleanup, then starts a fresh run with the same options. Resolves to the new run's handle; this one stays stopped. |\n| `reload(nextSource?)` | Compatibility alias that starts synchronously and returns the new run's handle; prefer `restart()` when persistence ordering matters. |\n\n```js\nconst handle = run(gameModule, { canvas });\n// later\nhandle.stop();\n```\n\n## `exit()`\n\n```js\nimport { exit } from \"js-game-zero\";\nexit(); // equivalent to calling .stop() on the currently active run() handle\n```\n\nAlso injected as a zero-boilerplate global, so game code can call `exit()`\ndirectly to end itself.\n\n## Preload gate\n\nEach run tracks its own required image and font loads (Actor constructions,\n`images.player` touches, and `preload` entries) and holds frames, with no\n`update` and no `draw`, until those loads and storage initialization settle.\nAn image or font first asked for later, in a hook (a new `actor.image`, a first\n`screen.blit`), holds the frames after it the same way until it has loaded,\nand `handle.step()` does nothing meanwhile. So a host that steps a game in a\nsynchronous loop should hand it its images decoded, with\n[`loadImages`](loaders.md#assets-without-a-server). A required image/font\nfailure rejects `handle.ready`, fails the handle, and performs cleanup. Sound and music loads\nare recoverable: they report a warning but never freeze another run or stop\nrendering. `handle.ready` lets hosts await startup explicitly instead of\npolling.\n\n## Pixel density\n\nThe canvas holds `WIDTH × HEIGHT × density` pixels, so a game is as sharp\nas the screen it's on. Density is the device's `devicePixelRatio`, from 1\nto 3, unless the game file or `RunOptions` sets `PIXEL_DENSITY`. A\n`PIXEL_DENSITY` that isn't a positive number warns and uses the device's\nratio.\n\nGame code works in logical pixels at any density; only reading the\ncanvas itself sees its real pixels:\n\n| Logical pixels | Canvas pixels (logical × density) |\n| -------------------------------------------------------- | --------------------------------- |\n| `screen.width`, `screen.height`, `screen.bounds()` | `canvas.width`, `canvas.height` |\n| Every drawing call, text, images and Actors | `getImageData()`, `toDataURL()` |\n| Mouse and touch positions, Actor positions and hit tests | |\n\n`screen.density` reports the density in use, so a host reading pixels back\nmultiplies logical coordinates by it.\n\n**CSS size.** A denser canvas would show at its pixel size, so the runtime\ngives it a CSS width of `WIDTH` px through a `:where(canvas[data-jgz-density])`\nrule; its height follows the aspect ratio. The rule has no specificity, so\nCSS the page sets on the canvas wins. A page that sizes the canvas should\nset its width, or both width and height: a height alone keeps the rule's\nwidth. The touch-control dock and the sandbox preview fit the canvas by its\nlogical size. At density 1 the canvas gets no rule and shows at its own\nsize.\n\n**Changes.** Before each frame the runtime checks the canvas against the\nscreen:\n\n- With no `PIXEL_DENSITY`, density follows `devicePixelRatio`, which\n changes when the page is zoomed or moved to another display. The canvas\n keeps what it shows, rescaled, for games that don't redraw everything.\n- A host that sets `canvas.width` or `canvas.height` sets the logical size,\n and the runtime multiplies it by the density.\n- Setting a canvas's size resets its context, so the runtime restores the\n density scale.\n\n**Limit.** iOS Safari leaves a canvas over 16,777,216 pixels (4096²)\nblank, so density comes down until the canvas fits, though never below 1\nunless the game asks for less. A `PIXEL_DENSITY` that doesn't fit warns\nand uses the most that does.\n\nRaster images (PNG, JPEG) can't hold more detail than their own pixels. In\nChromium, SVG images draw from their vectors at the canvas's density, so an\nSVG Actor stays sharp at any size.\n\n## Package boundaries\n\nGames import learner-facing values from `js-game-zero`. Embedders should\nimport loader factories, browser adapters, scheduler/diagnostic types, and\nother integration APIs from `js-game-zero/host`; Node filesystem storage\nlives at `js-game-zero/node`. Browser-safe advanced root imports remain\nas deprecated compatibility aliases through 0.2.\n\nHost tooling can also import `getGameInspector(handle)` from\n`js-game-zero/host`. It returns a live, read-only binding inspector for a\nrunning zero-boilerplate source game, or `null` for import-mode and disposed\nhandles (a failed game keeps its inspector, so a host can read the state at the\ncrash). This is the inspection seam the browser-backed `jgz` verification CLI\nuses; it does not add globals or learner-facing runtime methods.\n\n| Member | Purpose |\n| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| `bindings()` | The captured non-function top-level identifiers, in source order. |\n| `functions()` | The top-level functions. |\n| `read(path)` | The live value at a binding or a [state path](#state-paths), as it is. |\n| `inspect(path?)` | The value at a path as JSON, exactly as `jgz`'s `inspect` reports it (`serializeGameValue`); with no path, every binding's value by name. |\n\nThe same module exports `serializeGameValue(value)`, `parseStatePath(source)`,\n`createSeededRandom(seed)` and `loadImages(assetUrls)` (see\n[assets without a server](loaders.md#assets-without-a-server)).\n\nThe rest of `js-game-zero/host` is for building a host:\n\n| Export | What it's for |\n| ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |\n| `createImageLoader`, `createSoundLoader`, `createMusicLoader`, `createFontLoader` and `createAssetLoaders` | Loaders for `RunOptions.loaders`, each taking a `projectId` and an `assetBaseUrl` or `assetUrls`. |\n| `createStorage({ adapter?, projectId?, path? })` and `createLocalStorageAdapter({ projectId?, path? })` | A `Storage`, or the `localStorage` adapter behind the default one, for `RunOptions.storage`. |\n| `collectRuntimeDiagnostics(game, options?)` | The static [diagnostics](#diagnostics) `run()` would report for a game's source or module, without running it. |\n| `layoutText(context, text, pos, options?)` and `fitTextboxFontSize(context, text, rect, options?)` | Where `screen.draw.text` and `screen.draw.textbox` would put text, and at what size. |\n\nIts other exports (the input dispatcher, the `map…Event` functions,\n`dispatchHook`, the audio and tone classes, `getScreen`/`setScreen`) are the\nruntime's own parts, exported for tools and tests, and may change.\n`js-game-zero/node` exports `createSandboxFileStorageAdapter({\nprojectRoot, path? })`, a `StorageAdapter` that keeps a project's storage in\na JSON file under `projectRoot` (`storage.json` by default).\n\nThis reference ships with `js-game-zero`, for a host that hands it to a\nlearner, a tool or a model. `js-game-zero/reference` exports `reference`,\nevery page's Markdown by its file name (`reference[\"actor.md\"]`), and each\npage is also a file at `js-game-zero/reference/<page>.md`.\n\n### State paths\n\nA path names a top-level binding, then `.name` for a property and `[n]` for an\nindex: `score`, `player.x`, `fish[0].y`. `parseStatePath(\"fish[0].y\")` returns\n`{ source: \"fish[0].y\", binding: \"fish\", segments: [0, \"y\"] }`, and throws a\n`TypeError` for anything else, including `__proto__`, `prototype` and\n`constructor`. `jgz`'s `inspect` takes the same paths.\n\n### What a frame drew\n\nA path that starts with `$drawn` reads what the game's last `draw()` drew,\nin order, rather than one of its bindings, through `read`, `inspect` and\n`jgz`'s `inspect` alike. Each thing drawn has its `kind` and what it is and\nwhere, as the game gave them:\n\n| `kind` | Fields | From |\n| --------- | ----------------------------------------------------------------------------- | --------------------------------------------------------- |\n| `fill` | `color`, and `to` for a gradient | `screen.fill` |\n| `clear` | | `screen.clear` |\n| `image` | `image` (its name), `x`, `y` (its top-left) | `screen.blit`, `screen.draw.image` |\n| `actor` | `image`, `x`, `y` (its `pos`: its center, unless its `anchor` says otherwise) | an Actor's `draw()` |\n| `circle` | `x`, `y`, `radius`, `color`, `filled` | `screen.circle`, `screen.draw.circle`, `filledCircle` |\n| `rect` | `x`, `y`, `width`, `height`, `color`, `filled` | `screen.rect`, `screen.draw.rect`, `filledRect` |\n| `polygon` | `x`, `y` (its first point), `points` (how many), `color`, `filled` | `screen.triangle`, `screen.draw.polygon`, `filledPolygon` |\n| `line` | `x`, `y`, `x2`, `y2`, `color` | `screen.line`, `screen.draw.line` |\n| `text` | `text`, `x`, `y`, `width`, `height` (its box), and `color` when it has one | `screen.text`, `screen.draw.text`, `screen.draw.textbox` |\n\n`$drawn[0].kind`, `$drawn[2].image` and the like read single things. A frame\nkeeps its first 200 things; before the first frame `$drawn` is empty. So a\ncheck that something is on the screen reads what the game drew rather than\nits pixels: `{ kind: \"actor\", image: \"robot.svg\" }` is in `$drawn`.\n\n### Inspected values\n\n`serializeGameValue` turns a value into JSON: numbers, strings, booleans,\narrays and plain objects as they are (object keys sorted); anything else tagged\nwith `$type`:\n\n```js\nserializeGameValue(new Actor(\"cat\", [40, 60]));\n// { $type: \"Actor\", image: \"cat\", x: 40, y: 60, width, height, angle, …, properties: {} }\nserializeGameValue(new Rect(1, 2, 3, 4));\n// { $type: \"Rect\", left: 1, top: 2, right: 4, bottom: 6, width: 3, height: 4, center: [2.5, 4] }\nserializeGameValue({ missing: undefined, big: Infinity });\n// { big: { $type: \"Number\", value: \"Infinity\" }, missing: { $type: \"Undefined\" } }\n```\n\nAn Actor's `properties` are the fields the game gave it (`cat.speed = 40` is\n`properties.speed`); a `read` gives the Actor itself, so `cat.speed` there.\nSets, maps, dates and errors are tagged too; values deeper than six levels,\nlists past 100 items and circular references come back as `Truncated` or\n`Circular`.\n\n### Seeded runs\n\n`run(source, { seed })` gives the game a sequence of its own. A\nzero-boilerplate game sees a `Math` whose `random` follows the seed (every\nother member is the real `Math`'s), and `randomInt` and `randomChoice` draw\nfrom it while the game runs, in its hooks and its top-level code alike. An\nimport-mode game's `randomInt` and `randomChoice` follow the seed in its hooks;\nits `Math.random` is the page's. Nothing else changes: the page's own\n`Math.random`, and any other game on the page, keep their randomness, and a\n`restart()` starts the sequence again.\n\nUp to nine plain digits are the number they spell (`42` and `\"42\"` are the same\nseed); any other string is hashed (FNV-1a), and the sequence is Mulberry32 over\nthat 32-bit state. `jgz test --seed` passes its seed to the same option, so one\nseed gives the same values in the CLI and in a host page;\n`createSeededRandom(seed)` returns the sequence itself, for a host or a test to\npredict. A seed that isn't a string or a finite number fails at startup.\n\n### Driving a game from a host page\n\nThe script-tag build is `dist/jgz.js` in `js-game-zero` (`js-game-zero/iife`),\nwhich jsDelivr also serves, at\n`https://cdn.jsdelivr.net/npm/js-game-zero@0.3/dist/jgz.js`.\nIts `window.JGZ` carries `loadImages`, `getGameInspector`,\n`serializeGameValue`, `parseStatePath` and `createSeededRandom` beside the\nlearner-facing API, so a page can run a game it drives itself, the way the CLI\ndoes, without a bundler:\n\n```js\nconst canvas = document.createElement(\"canvas\"); // never added to the page\nconst handle = JGZ.run(source, {\n canvas,\n autostart: false,\n seed: \"check-1\",\n keyboardTarget: canvas,\n mouseReleaseTarget: canvas,\n PIXEL_DENSITY: 1,\n});\nawait handle.ready;\nhandle.step(0); // draws the first screen, as the CLI does\ncanvas.dispatchEvent(\n new KeyboardEvent(\"keydown\", { key: \"ArrowRight\", code: \"ArrowRight\" }),\n);\nfor (let frame = 0; frame < 30; frame += 1) handle.step(1 / 60);\ncanvas.dispatchEvent(\n new KeyboardEvent(\"keyup\", { key: \"ArrowRight\", code: \"ArrowRight\" }),\n);\ncanvas.dispatchEvent(\n new MouseEvent(\"mousedown\", {\n clientX: 25,\n clientY: 30,\n button: 0,\n buttons: 1,\n }),\n);\ncanvas.dispatchEvent(\n new MouseEvent(\"mouseup\", { clientX: 25, clientY: 30, button: 0 }),\n);\nJGZ.getGameInspector(handle).inspect(\"player.x\");\n```\n\n- **Frames.** With `autostart: false`, nothing runs until the page calls\n `handle.step(dt)`; each call is one `update(dt)` and one `draw()`. The CLI\n steps once with `dt` 0 after `ready`, then one sixtieth of a second per frame,\n so a page that does the same sees what `jgz test play` sees.\n- **Keys.** Keyboard events are heard on `keyboardTarget` (default `window`):\n `keydown` and `keyup`, read by `code` first (`ArrowRight`, `Space`, `KeyA`)\n and then `key`. Passing the canvas keeps them from reaching other games.\n- **Mouse and taps.** `mousedown`, `mousemove` and `wheel` are heard on the\n canvas, `mouseup` on `mouseReleaseTarget` (default `window`). A position is\n mapped from the canvas's displayed box onto the logical screen:\n `clientX = box.left + x / screen.width * box.width`, and the same for `y`\n (`handle.canvas.getBoundingClientRect()`, `handle.screen`). A canvas that\n isn't on the page has an empty box, so its client coordinates are the\n logical ones, as above. Touch arrives as `pointerdown`, `pointermove` and\n `pointerup` with `pointerType: \"touch\"`; the CLI uses the mouse events.\n\n## Diagnostics\n\n`run()` performs lightweight static checks and reports them through the\n`diagnostics` option (and collects them on `handle.diagnostics`) rather than\nfailing outright. A few mistakes stop the game before it starts instead:\n`run()` reports a `runtime-error` in the `startup` phase and throws. The\nmain one is that **`draw()` must not declare any parameters** (a\n`TypeError`, since mixing it up with `update(dt)` is a very common typo).\nThe others are a `TOUCH_CONTROLS` that isn't a valid mapping, a `seed` that\nisn't a string or a finite number, an invalid `projectId` or `assetUrls`, and\na top-level statement that throws.\n\nEverything else is a warning:\n\n| `code` | Meaning |\n| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |\n| `hook-typo` | A top-level function is named nearly like a hook (e.g. `onKeyDwon`), or is a hook spelled another way (`on_key_down`), which is never called. |\n| `constant-typo` | A top-level assignment is named nearly like a setting, such as `WIDTH` or `TITLE`. |\n| `duplicate-config` | The same setting was given both on the module and in the run options. |\n| `hook-arity` | `update` declares more than one parameter. |\n| `hook-parameter` | An input hook parameter isn't one of its documented names; says what it receives. |\n| `invalid-config` | `WIDTH`/`HEIGHT`/`PIXEL_DENSITY`/`ICON` isn't usable. |\n| `asset-name` | A quoted string that looks like an asset filename doesn't match the naming grammar. |\n| `runtime-error` | An uncaught error, or a failure loading/saving storage. |\n\nThe two typo checks only look at names the script _defines_ at top level\n(`function drow()`, `const udpate = () => {}`, `WIDHT = 800`), never at names\nit merely uses (`row`, `col`, `rows[y]`), and they stay quiet when the real\nname is also defined. Short names get one edit of slack and longer ones two.\n\nA `runtime-error` raised by the game's own code (a hook that threw, or a\ntop-level statement at startup) also carries `line`: the 1-based line in the\ngame source where the error was thrown, when the engine's stack trace names it.\nA `hook-typo` or `constant-typo` warning carries the line that declares the\nmisspelled name, and `draw()` declaring parameters fails on the line that\ndeclares it.\nA syntax error carries one too: where the engine gives no position (V8 gives\nnone for compiled game code), it's the first line whose part of the source\nfails the same way, so a missing `)` is placed on the line it's missing from,\nand a block left open on the line it opens on. A name the game never defined\nis placed the same way where the engine gives no position (JavaScriptCore): on\nthe one line that reads it, when only one does. `handle.failure.line` is the\nsame number. Errors thrown inside the library have no `line`.\n\nThe sandbox shows these under Messages, and marks a problem's line in the editor; import-mode hosts can pass\na `diagnostics` callback to log or display them however they like. Without\none, a run reports them to the browser console: a warning with\n`console.warn` and an error with `console.error`, each starting\n`js-game-zero:` and ending with its line, if it has one. A mistake that\nmakes `run()` itself throw isn't logged as well. A game that fails while\nrunning removes a canvas it made, so on a page with no callback, the\nconsole is where to find out why.\n",
37
- "screen.md": "# Screen And Drawing\n\n`screen` is the global canvas surface. Everything you draw happens inside\n`draw()`, in screen pixels: `[0, 0]` is the top-left corner, x grows right,\ny grows down, and `[WIDTH, HEIGHT]` is the bottom-right corner.\n\n## Short forms\n\nFive short calls draw the most common shapes and text, with each position\ngiven as separate numbers rather than an `[x, y]` list:\n\n| Call | Draws |\n| ------------------------------------------------ | -------------------------------------------------- |\n| `screen.circle(x, y, radius, color)` | a filled circle centered on `x, y` |\n| `screen.rect(x, y, width, height, color)` | a filled rectangle whose top-left corner is `x, y` |\n| `screen.triangle(x1, y1, x2, y2, x3, y3, color)` | a filled triangle with those three corners |\n| `screen.line(x1, y1, x2, y2, color, width = 1)` | a line from `x1, y1` to `x2, y2`, `width` px thick |\n| `screen.text(text, x, y, color, size = 24)` | text whose top-left corner is `x, y`, `size` tall |\n\n`color` is optional and defaults to `\"white\"`; it takes any of the\n[colors](#colors) below.\n\n```js\nfunction draw() {\n screen.clear();\n screen.rect(0, 220, 400, 80, \"green\");\n screen.circle(200, 150, 25, \"gold\");\n screen.triangle(160, 40, 120, 120, 200, 120);\n screen.line(20, 20, 200, 120, \"white\", 3);\n screen.text(\"Score: \" + score, 20, 30);\n}\n```\n\n`screen.text` takes words in quotes or a number. To place text by its\ncenter, wrap it or give it an outline, use\n[`screen.draw.text`](#screendrawtexttext-pos-options--screendrawtexttext-options).\n\nThe short forms fill their shapes. The outline versions, and the versions\nthat take `[x, y]` lists, stay under `screen.draw` below. Mind the names:\n**`screen.circle` fills, but `screen.draw.circle` draws only an outline**,\nand the same goes for `screen.rect` and `screen.draw.rect`.\n\nA wrong call throws a `TypeError` that says what to change. A list, as in\n`screen.circle([200, 150], 25)`, points to the `screen.draw` method that\ntakes lists (here `screen.draw.filledCircle`). A missing or non-number\nargument is named, and an extra one, such as an outline width, points to\nthe outline method.\n\n## Colors\n\nEvery drawing method accepts a `Color`, which is either a CSS color string\n(`\"white\"`, `\"#0f172a\"`, `\"rgb(20, 30, 40)\"`) or an RGB/RGBA tuple of\nintegers `0`–`255`: `[249, 115, 22]` or `[249, 115, 22, 128]` (the fourth\nvalue is alpha out of 255, not a `0`–`1` fraction). Passing anything else\nthrows a `TypeError` that says what was wrong:\n\n- A misspelled color name suggests the right one: `\"lightblu\"` fails with\n `Did you mean \"lightblue\"?`. (A canvas would otherwise ignore the color\n and keep drawing in the previous one.)\n- A hex code needs 3 or 6 hex digits (or 4 or 8, with alpha).\n- A fractional alpha such as `[255, 0, 0, 0.5]` fails with the 0–255 value\n to use instead (`128`).\n\nDrawing options are checked the same way: an option a method doesn't have,\nsuch as `{ colour: \"red\" }`, throws and names the closest real option.\n\n### `rgb(red, green, blue, alpha?)`\n\nMixes a color from red, green and blue light, each `0`–`255`, with an\noptional alpha out of 255. It returns the matching list, so\n`rgb(255, 140, 105)` is `[255, 140, 105]` and works anywhere a color does.\nIt's ready to use in a game file; in import mode, import it from\n`js-game-zero`.\n\n```js\nlet coral = rgb(255, 140, 105);\nlet glass = rgb(120, 200, 255, 128); // half see-through\nscreen.circle(200, 150, 25, coral);\n```\n\nIts numbers are checked like a list's, with the same hints (a fractional\nalpha such as `0.5` names `128` instead), and a missing one is named:\n`rgb(255, 140)` fails with `rgb is missing blue`.\n\n## `screen.clear()`\n\nResets the whole canvas to opaque black.\n\n```js\nfunction draw() {\n screen.clear();\n}\n```\n\n## `screen.fill(color, bottomColor?)`\n\nFills the canvas with a solid color, or a top-to-bottom gradient when a\nsecond color is given: `color` at the top fading to `bottomColor` at the\nbottom.\n\n```js\nscreen.fill(\"#0f172a\"); // solid\nscreen.fill(\"#0f172a\", \"#164e63\"); // gradient, dark at top to teal at bottom\n```\n\n## `screen.draw.image(image, pos)` / `screen.blit(image, pos)`\n\n`screen.draw.image` and `screen.blit` (Pygame Zero's name) do the same\nthing. Draws an already-loaded image (by asset name, or a `SurfaceLike` you already\nhold) at a position. `pos` is a `[x, y]` point for the image's top-left\ncorner, or a rect-like value (its top-left corner is used; blit does not\nscale the image to the rect's size).\n\n```js\nscreen.draw.image(\"player.svg\", [40, 40]);\n```\n\nIf the named image hasn't finished loading yet, nothing is drawn: it returns\na `Promise` that resolves once the image has loaded, and the runtime holds\nthe frames that follow until then, so the image shows from the next one.\nIf it can't be loaded, the promise rejects and the run reports the missing\nimage by name, so `draw()` needn't handle the promise itself.\nImages touched while the script starts up load before the first frame (see\n[`docs/reference/runtime.md`](runtime.md#preload-gate)).\nPrefer `Actor` for anything that moves or needs collision — drawing an image\ndirectly is for background art and tiles.\n\n## `screen.bounds()`\n\nReturns a `ZRect` covering the whole canvas (`[0, 0, WIDTH, HEIGHT]`), handy\nfor clamping or collision against the screen edge.\n\n```js\nif (!screen.bounds().contains(player.rect()))\n player.center = [WIDTH / 2, HEIGHT / 2];\n```\n\n## `PIXEL_DENSITY` and `screen.density`\n\nOn a phone, one screen pixel covers several of the device's own pixels.\nThe canvas has a pixel for each of them, so text, outlines and SVG pictures\nstay sharp. `screen.density` says how many canvas pixels make up one screen\npixel along each side: between 2 and 3 on most phones and 1 on a computer\nmonitor. Your game's numbers don't change with it: a game 360 pixels wide\nis 360 wide at any density.\n\nSet `PIXEL_DENSITY` next to `WIDTH` and `HEIGHT` to choose the density\nyourself. Without it, the game uses the device's own, up to 3.\n\n```js\nWIDTH = 360;\nHEIGHT = 640;\nPIXEL_DENSITY = 1; // one canvas pixel per screen pixel, even on a phone\n```\n\nSee [pixel density](runtime.md#pixel-density) for how hosts size and read\nthe canvas.\n\n## `screen.draw.line(start, end, color, width = 1)`\n\n```js\nscreen.draw.line([20, 20], [200, 120], \"white\", 3);\n```\n\n## `screen.draw.circle(pos, radius, color, width = 1)` / `filledCircle(pos, radius, color)`\n\n`screen.draw.circle` draws an outline; `filledCircle` and the short form\n[`screen.circle`](#short-forms) fill.\n\n```js\nscreen.draw.circle([160, 90], 40, \"#38bdf8\", 2); // outline\nscreen.draw.filledCircle([160, 90], 40, \"#38bdf8\"); // filled\n```\n\n## `screen.draw.rect(rect, color, width = 1)` / `filledRect(rect, color)`\n\n`rect` accepts anything geometry accepts: a `Rect`, an `Actor` (via its\n`.rect()` duck type — see [`docs/reference/rect.md`](rect.md)), or a plain\n`[left, top, width, height]` tuple.\n\n```js\nscreen.draw.filledRect([40, 40, 120, 60], \"#f97316\");\nscreen.draw.rect(player.rect(), \"yellow\", 2); // highlight a hitbox\n```\n\n## `screen.draw.polygon(points, color, width = 1)` / `filledPolygon(points, color)`\n\nNeeds at least three points.\n\n```js\nscreen.draw.filledPolygon(\n [\n [238, 40],\n [292, 64],\n [270, 126],\n [214, 118],\n ],\n \"#f8fafc\",\n);\n```\n\n## `screen.draw.text(text, pos?, options?)` / `screen.draw.text(text, options)`\n\nDraws text with a large set of beginner-friendly options — this is a\nJavaScript-native reimplementation of pgzero's `ptext` module, including its\nword-wrapping algorithm and gradient/outline/shadow effects.\n\n```js\nscreen.draw.text(\"Score: 12\", [16, 16], {\n size: 24,\n color: \"white\",\n});\n```\n\nWhen the options say where the text goes (`center`, `topRight`, ...), pass\nthem second, in place of the position:\n\n```js\nscreen.draw.text(\"Game Over\", { center: [WIDTH / 2, HEIGHT / 2], size: 40 });\n```\n\nSeveral options have two names: a readable one, and the shorter one Pygame\nZero uses. Either works, but not both at once:\n\n| Readable name | Pygame Zero name |\n| --------------- | ---------------- |\n| `size` | `fontsize` |\n| `outline` | `owidth` |\n| `outlineColor` | `ocolor` |\n| `shadowColor` | `scolor` |\n| `gradientColor` | `gcolor` |\n\n### Position options\n\nGive **exactly one** of the following ways to position the text (mixing\nthem throws a `TypeError`):\n\n- `pos` (or the second positional argument) — a `[x, y]` point. Combined\n with `anchor` (default `\"topLeft\"`) to decide which part of the text box\n sits at that point.\n- A named anchor point directly: `topLeft`, `topRight`, `bottomLeft`,\n `bottomRight`, `midTop`, `midLeft`, `midRight`, `midBottom`, `center` —\n each takes a `[x, y]` point for that corner/edge/center of the text.\n- Scalar axis keywords, combined per-axis: `left`/`centerX`/`right` for the\n horizontal position, `top`/`centerY`/`bottom` for the vertical position.\n You can mix one horizontal and one vertical scalar (`{ right: 300, top: 10\n}`), or use just one axis and fall back to `pos` for the other.\n\n```js\nscreen.draw.text(\"Game Over\", {\n center: [WIDTH / 2, HEIGHT / 2],\n size: 40,\n});\nscreen.draw.text(\"HUD\", { right: WIDTH - 10, top: 10 }); // top-right corner, 10px margin\n```\n\n### Font options\n\n- `size` / `fontsize` (default `24`)\n- `fontname` — the stem of a font asset under `fonts/` (e.g. `\"pixel\"` for\n `fonts/pixel.ttf`); loads asynchronously the first time it's used.\n- `sysfontname` — a literal CSS font family name instead (e.g. `\"Georgia\"`).\n Mutually exclusive with `fontname`.\n- `bold`, `italic`, `underline` — booleans.\n- `color` (default `\"white\"`), `background` (fills a box behind the text).\n- `gradientColor` / `gcolor` — a second color for a top-to-bottom gradient\n across the text.\n\n### Layout options\n\n- `width` — wraps text to fit this many pixels; without it, text is only\n broken at explicit newlines (`\\n`). Wrapping is greedy word-wrap: a single\n word longer than `width` is placed on its own (overflowing) line rather\n than being cut.\n- `widthem` — like `width`, but expressed as a multiple of `size`.\n- `lineheight` — pixels between lines (default `size * 1.2`).\n- `align` — `\"left\"` (default), `\"center\"`, or `\"right\"`, for multi-line text.\n A [`textbox`](#screendrawtextboxtext-rect-options) follows its `anchor`\n instead, so it centers by default.\n- `strip` — trims trailing spaces from each wrapped line (default `true`).\n- `anchor` — overrides which part of the text's bounding box sits at the\n resolved position; either one of the named anchor strings above, or a\n `[horizontal, vertical]` pair where each side is `\"left\"`/`\"center\"`/`\"right\"`,\n `\"top\"`/`\"center\"`/`\"bottom\"`, or a literal pixel-offset number.\n- `angle` — rotates the text counterclockwise around the anchor point, in\n degrees.\n\n### Effects\n\n- `outline` / `owidth` + `outlineColor` / `ocolor` — an outline stroke\n around the glyphs, `outline` pixels thick.\n- `shadow` (a number or `[x, y]` offset) + `shadowColor` / `scolor`\n (default `\"black\"`) — a drop shadow.\n- `alpha` — overall opacity, `0`–`1` (default `1`).\n\n## `screen.draw.textbox(text, rect, options?)`\n\nLike `draw.text`, but instead of a fixed `size` you give a rectangle and\n`textbox` picks the **largest font size that fits inside it** (shrinking\ndown to `minSize`, default `4`, if necessary; `minFontsize` is another name\nfor it, but not both at once). Text is centered in the rect by default.\nPass `anchor` (e.g. `\"topLeft\"`) to place it elsewhere in the rect; its\nlines line up the same way (on the left for `\"topLeft\"`) unless you give\n`align`. The rect places the text, so `textbox` takes none of\n`draw.text`'s position options (`pos`, `center`, `topLeft`, `left`, ...)\nand no `width`; giving one throws a `TypeError` that says so.\n\n```js\nscreen.draw.textbox(\"You win!\", [40, 60, 240, 80], {\n color: \"white\",\n size: 48, // upper bound; shrinks to fit if \"You win!\" would overflow\n});\n```\n",
36
+ "runtime.md": "# The `run()` Module API\n\nMost learners never call `run()` directly — the sandbox and zero-boilerplate\nexamples call it for you, and you just write top-level `WIDTH`/`draw`/\n`update`/hook definitions. This page is for import-mode code and for anyone\nembedding the framework in a host page (a custom sandbox, a course platform,\nan automated test).\n\n```ts\nfunction run(\n moduleOrSource: GameModule | DefinedGame | string,\n options?: RunOptions,\n): GameHandle;\n```\n\n## Three entry styles\n\n**Scoped import mode (recommended for applications)** — `defineGame()` gives\nthe game a typed set of services owned by that one run. Actors, assets,\nstorage, animation, input, and audio therefore cannot accidentally bind to a\ndifferent game on the same page:\n\n```ts\nimport { defineGame, run } from \"js-game-zero\";\n\nrun(\n defineGame(({ Actor, screen, keyboard }) => {\n const player = new Actor(\"player.svg\");\n return {\n WIDTH: 320,\n HEIGHT: 180,\n update(dt) {\n if (keyboard.right) player.x += 120 * dt;\n },\n draw() {\n screen.clear();\n player.draw();\n },\n onKeyDown(event) {\n console.log(event.key);\n },\n };\n }),\n);\n```\n\nThe input hooks in a defined game receive one typed event object.\nZero-boilerplate and raw import hooks keep their Pygame Zero-style\nparameter-name dispatch.\n\n| Event | Fields |\n| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `GameKeyEvent` (`onKeyDown`, `onKeyUp`) | `key`, `mod`, `unicode` and `repeat`, as [`hooks.md`](hooks.md#onkeydownkey-mod-unicode-repeat--onkeyupkey-mod-unicode-repeat) describes them. |\n| `GameMouseEvent` (`onMouseDown`, `onMouseUp`, `onMouseMove`) | `pos`, `button` (the button pressed or released, or `null` for one with no `mouse` constant; `onMouseMove` reads `buttons` instead), `buttons` (a `Set` of held buttons) and `rel` (movement since the last event). |\n\nThe function given to `defineGame()` receives the run's own services:\n`screen`, `clock`, `keyboard`, `mouse`, `images`, `sounds`, `music`, `fonts`,\n`storage`, `tone`, `animate` and `exit`, the constants `keys` and `keymods`,\nand the classes `Actor`, `Animation`, `Rect`, `ZRect`, `Screen` and\n`Storage`. An `Actor` or `Animation` made from them belongs to that run.\n\nPackage-level `screen`, `clock`, `keyboard`, `images`, `sounds`, `music`, and\n`storage` remain available for learner compatibility. They are convenience\nproxies for a single active runtime; applications that can run more than one\ngame should prefer the services passed to `defineGame()`.\n\n**Import mode** — pass a plain object of hooks and config:\n\n```js\nimport { run, screen, keyboard } from \"js-game-zero\";\n\nrun({\n WIDTH: 320,\n HEIGHT: 180,\n draw() {\n screen.fill(\"#10243d\");\n },\n update(dt) {\n /* ... */\n },\n onKeyDown(key) {\n /* hooks receive values by parameter name */\n },\n});\n```\n\n**Global mode** — pass a source-code string, which runs with the framework's\nglobals (`Actor`, `screen`, `keyboard`, `WIDTH`, `draw`, ...) already in\nscope, exactly like a zero-boilerplate sandbox project. This is how the\nsandbox's preview pane runs the code from your editor.\n\n```js\nrun(`\n WIDTH = 320;\n HEIGHT = 180;\n function draw() { screen.fill(\"navy\"); }\n`);\n```\n\n`handle.mode` tells you which one was used (`\"import\"` or `\"global\"`).\n\nA global-mode game runs in its own scope rather than the page's. Reading a\nname nothing defines throws a `ReferenceError` with a suggestion\n(`scren is not defined. Did you mean screen?`); `typeof name` stays safe.\nBrowser APIs a game can't reach say what to use instead — for example,\n`setTimeout` points at `clock.schedule(callback, seconds)`, and\n`localStorage` at `storage`. Standard JavaScript built-ins such as `Math`,\n`JSON`, `isNaN`, `Map` and `console` are available as usual.\n\nEvery library name has one spelling, in camelCase: `screen.draw.filledCircle`,\n`actor.angleTo`, `rect.collidePoint`, `clock.scheduleInterval`, `onKeyDown`.\nConstants stay upper case (`WIDTH`, `keys.SPACE`, `mouse.LEFT`). Another\nspelling of a member, such as Pygame Zero's `actor.angle_to` or\n`rect.colliderect`, throws where it's read, naming the real one\n(`actor.angle_to doesn't exist. Did you mean actor.angleTo?`). In a\nglobal-mode game, `screen`, `clock`, `music`, `tone`, `keys`, `keymods` and\n`mouse` also throw with a suggestion for any member they don't have\n(`screen.draw.cirlce doesn't exist. Did you mean screen.draw.circle?`), and\nan undefined global suggests the real one\n(`random_int is not defined. Did you mean randomInt?`).\nThe classes behind them (`Screen`, `Clock`, `Storage`) are globals too, so a\ncapital letter by mistake, as in `Screen.fill(\"navy\")`, points at the service\ninstead (`Screen.fill doesn't exist. Did you mean screen.fill?`).\n\n## `RunOptions`\n\n| Option | Purpose |\n| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `canvas` | Draw into an existing `<canvas>` instead of creating one. |\n| `container` | Where to mount a created canvas (default the page's `<body>`). Called from a script in `<head>`, before there's a `<body>`, the game starts at once and its canvas joins the `<body>` when the page has been read. |\n| `autostart` | Default `true`. Set `false` to skip the automatic frame loop and drive frames yourself via `handle.step(dt)` — useful for deterministic tests. |\n| `projectId` | Scopes storage keys and asset URLs, so multiple games on one page don't collide. |\n| `assetBaseUrl` | Where asset URLs are rooted (default `/assets`). |\n| `assetUrls` | The game's assets by path, each with its URL: `{ \"images/cat.svg\": \"data:image/svg+xml;base64,…\" }`. When set, assets load only from here, never from `assetBaseUrl`. See [assets without a server](loaders.md#assets-without-a-server). |\n| `preload` | An array of asset paths (e.g. `[\"images/player.svg\"]`) to start loading immediately, before any is referenced by code. |\n| `preloadedAssets` | Pre-built `{ images, sounds, music, fonts }` asset maps to seed the cache synchronously — used by hosts that already have the bytes. `loadImages(assetUrls)` makes the `images` map; see [assets without a server](loaders.md#assets-without-a-server). |\n| `storage` | A `Storage` instance or a raw `StorageAdapter` (`{ path, load(), save(text) }`) to use instead of the default `localStorage`-backed one. |\n| `loaders` | Override one or more of the `images`/`sounds`/`music`/`fonts` loader instances. |\n| `keyboardTarget` | The `EventTarget` that receives keyboard events (default `window`). |\n| `mouseReleaseTarget` | The `EventTarget` where mouse releases (`mouseup`) are heard (default `window`, so a drag released outside the canvas still ends). A host driving a game itself passes the canvas, so its releases reach no other game. |\n| `seed` | A string or number that makes the game's randomness repeatable: `Math.random` as a zero-boilerplate game sees it, and `randomInt` and `randomChoice`, follow one sequence for this seed, the sequence `jgz test --seed` uses. See [seeded runs](#seeded-runs). |\n| `TOUCH_CONTROLS` | `true` for the full touch-control preset, `false` to disable it, or a button mapping with optional `show: \"auto\"` / `\"always\"`. See [touch controls](hooks.md#touch-controls). |\n| `PIXEL_DENSITY` | Canvas pixels per logical pixel. Defaults to the device's pixel ratio, from 1 to 3. See [pixel density](#pixel-density). |\n| `scheduler` | Override `requestAnimationFrame`/`cancelAnimationFrame` (for tests). |\n| `diagnostics` | `(diagnostic) => void` — called for every diagnostic (see below). Without it, they go to the browser console. |\n\nAny hook or `WIDTH`/`HEIGHT`/`PIXEL_DENSITY`/`TITLE`/`ICON`/`TOUCH_CONTROLS`\nconfig can also be passed directly on `RunOptions`; if the same one is also present on the module\nobject, the option wins and a diagnostic warns about the duplicate.\n\n## `GameHandle`\n\n`run()` returns a handle for controlling the running game:\n\n| Member | Purpose |\n| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |\n| `canvas`, `screen`, `clock`, `keyboard`, `loaders`, `storage` | The live instances this run is using. |\n| `module` | The final, merged hook/config object. |\n| `diagnostics` | The diagnostics collected so far. |\n| `state`, `failure` | Lifecycle state and the first fatal runtime failure, if one occurred. |\n| `ready: Promise<void>` | Resolves once the preload gate is satisfied (see below). |\n| `done: Promise<void>` | Resolves after final storage flushing and resource cleanup complete. |\n| `pause()` / `resume()` | Stop/restart the frame loop without tearing anything down. |\n| `step(dt)` | Manually advance one frame by `dt` seconds — only meaningful with `autostart: false`. |\n| `stop()` | Detaches input listeners, flushes any pending storage save, and cancels the frame loop. |\n| `dispose()` | Awaited form of cleanup; resolves only after pending persistence work is finished. |\n| `restart(nextSource?)` | Awaits cleanup, then starts a fresh run with the same options. Resolves to the new run's handle; this one stays stopped. |\n| `reload(nextSource?)` | Compatibility alias that starts synchronously and returns the new run's handle; prefer `restart()` when persistence ordering matters. |\n\n```js\nconst handle = run(gameModule, { canvas });\n// later\nhandle.stop();\n```\n\n## `exit()`\n\n```js\nimport { exit } from \"js-game-zero\";\nexit(); // equivalent to calling .stop() on the currently active run() handle\n```\n\nAlso injected as a zero-boilerplate global, so game code can call `exit()`\ndirectly to end itself.\n\n## Preload gate\n\nEach run tracks its own required image and font loads (Actor constructions,\n`images.player` touches, and `preload` entries) and holds frames, with no\n`update` and no `draw`, until those loads and storage initialization settle.\nAn image or font first asked for later, in a hook (a new `actor.image`, a first\n`screen.blit`), holds the frames after it the same way until it has loaded,\nand `handle.step()` does nothing meanwhile. So a host that steps a game in a\nsynchronous loop should hand it its images decoded, with\n[`loadImages`](loaders.md#assets-without-a-server). A required image/font\nfailure rejects `handle.ready`, fails the handle, and performs cleanup. Sound and music loads\nare recoverable: they report a warning but never freeze another run or stop\nrendering. `handle.ready` lets hosts await startup explicitly instead of\npolling.\n\n## Pixel density\n\nThe canvas holds `WIDTH × HEIGHT × density` pixels, so a game is as sharp\nas the screen it's on. Density is the device's `devicePixelRatio`, from 1\nto 3, unless the game file or `RunOptions` sets `PIXEL_DENSITY`. A\n`PIXEL_DENSITY` that isn't a positive number warns and uses the device's\nratio.\n\nGame code works in logical pixels at any density; only reading the\ncanvas itself sees its real pixels:\n\n| Logical pixels | Canvas pixels (logical × density) |\n| -------------------------------------------------------- | --------------------------------- |\n| `screen.width`, `screen.height`, `screen.bounds()` | `canvas.width`, `canvas.height` |\n| Every drawing call, text, images and Actors | `getImageData()`, `toDataURL()` |\n| Mouse and touch positions, Actor positions and hit tests | |\n\n`screen.density` reports the density in use, so a host reading pixels back\nmultiplies logical coordinates by it.\n\n**CSS size.** A denser canvas would show at its pixel size, so the runtime\ngives it a CSS width of `WIDTH` px through a `:where(canvas[data-jgz-density])`\nrule; its height follows the aspect ratio. The rule has no specificity, so\nCSS the page sets on the canvas wins. A page that sizes the canvas should\nset its width, or both width and height: a height alone keeps the rule's\nwidth. The touch-control dock and the sandbox preview fit the canvas by its\nlogical size. At density 1 the canvas gets no rule and shows at its own\nsize.\n\n**Changes.** Before each frame the runtime checks the canvas against the\nscreen:\n\n- With no `PIXEL_DENSITY`, density follows `devicePixelRatio`, which\n changes when the page is zoomed or moved to another display. The canvas\n keeps what it shows, rescaled, for games that don't redraw everything.\n- A host that sets `canvas.width` or `canvas.height` sets the logical size,\n and the runtime multiplies it by the density.\n- Setting a canvas's size resets its context, so the runtime restores the\n density scale.\n\n**Limit.** iOS Safari leaves a canvas over 16,777,216 pixels (4096²)\nblank, so density comes down until the canvas fits, though never below 1\nunless the game asks for less. A `PIXEL_DENSITY` that doesn't fit warns\nand uses the most that does.\n\nRaster images (PNG, JPEG) can't hold more detail than their own pixels. In\nChromium, SVG images draw from their vectors at the canvas's density, so an\nSVG Actor stays sharp at any size.\n\n## Package boundaries\n\nGames import learner-facing values from `js-game-zero`. Embedders should\nimport loader factories, browser adapters, scheduler/diagnostic types, and\nother integration APIs from `js-game-zero/host`; Node filesystem storage\nlives at `js-game-zero/node`. Browser-safe advanced root imports remain\nas deprecated compatibility aliases through 0.2.\n\nHost tooling can also import `getGameInspector(handle)` from\n`js-game-zero/host`. It returns a live, read-only binding inspector for a\nrunning zero-boilerplate source game, or `null` for import-mode and disposed\nhandles (a failed game keeps its inspector, so a host can read the state at the\ncrash). This is the inspection seam the browser-backed `jgz` verification CLI\nuses; it does not add globals or learner-facing runtime methods.\n\n| Member | Purpose |\n| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| `bindings()` | The captured non-function top-level identifiers, in source order. |\n| `functions()` | The top-level functions. |\n| `read(path)` | The live value at a binding or a [state path](#state-paths), as it is. |\n| `inspect(path?)` | The value at a path as JSON, exactly as `jgz`'s `inspect` reports it (`serializeGameValue`); with no path, every binding's value by name. |\n\nThe same module exports `serializeGameValue(value)`, `parseStatePath(source)`,\n`createSeededRandom(seed)` and `loadImages(assetUrls)` (see\n[assets without a server](loaders.md#assets-without-a-server)).\n\nThe rest of `js-game-zero/host` is for building a host:\n\n| Export | What it's for |\n| ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |\n| `createImageLoader`, `createSoundLoader`, `createMusicLoader`, `createFontLoader` and `createAssetLoaders` | Loaders for `RunOptions.loaders`, each taking a `projectId` and an `assetBaseUrl` or `assetUrls`. |\n| `createStorage({ adapter?, projectId?, path? })` and `createLocalStorageAdapter({ projectId?, path? })` | A `Storage`, or the `localStorage` adapter behind the default one, for `RunOptions.storage`. |\n| `collectRuntimeDiagnostics(game, options?)` | The static [diagnostics](#diagnostics) `run()` would report for a game's source or module, without running it. |\n| `layoutText(context, text, pos, options?)` and `fitTextboxFontSize(context, text, rect, options?)` | Where `screen.draw.text` and `screen.draw.textbox` would put text, and at what size. |\n\nIts other exports (the input dispatcher, the `map…Event` functions,\n`dispatchHook`, the audio and tone classes, `getScreen`/`setScreen`) are the\nruntime's own parts, exported for tools and tests, and may change.\n`js-game-zero/node` exports `createSandboxFileStorageAdapter({\nprojectRoot, path? })`, a `StorageAdapter` that keeps a project's storage in\na JSON file under `projectRoot` (`storage.json` by default).\n\nThis reference ships with `js-game-zero`, for a host that hands it to a\nlearner, a tool or a model. `js-game-zero/reference` exports `reference`,\nevery page's Markdown by its file name (`reference[\"actor.md\"]`), and each\npage is also a file at `js-game-zero/reference/<page>.md`.\n\n### State paths\n\nA path names a top-level binding, then `.name` for a property and `[n]` for an\nindex: `score`, `player.x`, `fish[0].y`. `parseStatePath(\"fish[0].y\")` returns\n`{ source: \"fish[0].y\", binding: \"fish\", segments: [0, \"y\"] }`, and throws a\n`TypeError` for anything else, including `__proto__`, `prototype` and\n`constructor`. `jgz`'s `inspect` takes the same paths.\n\n### What a frame drew\n\nA path that starts with `$drawn` reads what the game's last `draw()` drew,\nin order, rather than one of its bindings, through `read`, `inspect` and\n`jgz`'s `inspect` alike. Each thing drawn has its `kind` and what it is and\nwhere, as the game gave them:\n\n| `kind` | Fields | From |\n| --------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |\n| `fill` | `color`, and `to` for a gradient | `screen.fill` |\n| `clear` | | `screen.clear` |\n| `image` | `image` (its name), `x`, `y` (its top-left) | `screen.blit`, `screen.draw.image` |\n| `actor` | `image`, `x`, `y` (its `pos`: its center, unless its `anchor` says otherwise) | an Actor's `draw()` |\n| `circle` | `x`, `y`, `radius`, `color`, `filled` | `screen.circle`, `screen.draw.circle`, `filledCircle` |\n| `rect` | `x`, `y`, `width`, `height`, `color`, `filled`, and `radius` when rounded | `screen.rect`, `screen.draw.rect`, `filledRect`, and the `roundedRect` forms |\n| `polygon` | `x`, `y` (its first point), `points` (how many), `color`, `filled` | `screen.triangle`, `screen.draw.polygon`, `filledPolygon` |\n| `line` | `x`, `y`, `x2`, `y2`, `color` | `screen.line`, `screen.draw.line` |\n| `text` | `text`, `x`, `y`, `width`, `height` (its box), and `color` when it has one | `screen.text`, `screen.draw.text`, `screen.draw.textbox` |\n\n`$drawn[0].kind`, `$drawn[2].image` and the like read single things. A frame\nkeeps its first 200 things; before the first frame `$drawn` is empty. So a\ncheck that something is on the screen reads what the game drew rather than\nits pixels: `{ kind: \"actor\", image: \"robot.svg\" }` is in `$drawn`.\n\n### Inspected values\n\n`serializeGameValue` turns a value into JSON: numbers, strings, booleans,\narrays and plain objects as they are (object keys sorted); anything else tagged\nwith `$type`:\n\n```js\nserializeGameValue(new Actor(\"cat\", [40, 60]));\n// { $type: \"Actor\", image: \"cat\", x: 40, y: 60, width, height, angle, …, properties: {} }\nserializeGameValue(new Rect(1, 2, 3, 4));\n// { $type: \"Rect\", left: 1, top: 2, right: 4, bottom: 6, width: 3, height: 4, center: [2.5, 4] }\nserializeGameValue({ missing: undefined, big: Infinity });\n// { big: { $type: \"Number\", value: \"Infinity\" }, missing: { $type: \"Undefined\" } }\n```\n\nAn Actor's `properties` are the fields the game gave it (`cat.speed = 40` is\n`properties.speed`); a `read` gives the Actor itself, so `cat.speed` there.\nSets, maps, dates and errors are tagged too; values deeper than six levels,\nlists past 100 items and circular references come back as `Truncated` or\n`Circular`.\n\n### Seeded runs\n\n`run(source, { seed })` gives the game a sequence of its own. A\nzero-boilerplate game sees a `Math` whose `random` follows the seed (every\nother member is the real `Math`'s), and `randomInt` and `randomChoice` draw\nfrom it while the game runs, in its hooks and its top-level code alike. An\nimport-mode game's `randomInt` and `randomChoice` follow the seed in its hooks;\nits `Math.random` is the page's. Nothing else changes: the page's own\n`Math.random`, and any other game on the page, keep their randomness, and a\n`restart()` starts the sequence again.\n\nUp to nine plain digits are the number they spell (`42` and `\"42\"` are the same\nseed); any other string is hashed (FNV-1a), and the sequence is Mulberry32 over\nthat 32-bit state. `jgz test --seed` passes its seed to the same option, so one\nseed gives the same values in the CLI and in a host page;\n`createSeededRandom(seed)` returns the sequence itself, for a host or a test to\npredict. A seed that isn't a string or a finite number fails at startup.\n\n### Driving a game from a host page\n\nThe script-tag build is `dist/jgz.js` in `js-game-zero` (`js-game-zero/iife`),\nwhich jsDelivr also serves, at\n`https://cdn.jsdelivr.net/npm/js-game-zero@0.3/dist/jgz.js`.\nIts `window.JGZ` carries `loadImages`, `getGameInspector`,\n`serializeGameValue`, `parseStatePath` and `createSeededRandom` beside the\nlearner-facing API, so a page can run a game it drives itself, the way the CLI\ndoes, without a bundler:\n\n```js\nconst canvas = document.createElement(\"canvas\"); // never added to the page\nconst handle = JGZ.run(source, {\n canvas,\n autostart: false,\n seed: \"check-1\",\n keyboardTarget: canvas,\n mouseReleaseTarget: canvas,\n PIXEL_DENSITY: 1,\n});\nawait handle.ready;\nhandle.step(0); // draws the first screen, as the CLI does\ncanvas.dispatchEvent(\n new KeyboardEvent(\"keydown\", { key: \"ArrowRight\", code: \"ArrowRight\" }),\n);\nfor (let frame = 0; frame < 30; frame += 1) handle.step(1 / 60);\ncanvas.dispatchEvent(\n new KeyboardEvent(\"keyup\", { key: \"ArrowRight\", code: \"ArrowRight\" }),\n);\ncanvas.dispatchEvent(\n new MouseEvent(\"mousedown\", {\n clientX: 25,\n clientY: 30,\n button: 0,\n buttons: 1,\n }),\n);\ncanvas.dispatchEvent(\n new MouseEvent(\"mouseup\", { clientX: 25, clientY: 30, button: 0 }),\n);\nJGZ.getGameInspector(handle).inspect(\"player.x\");\n```\n\n- **Frames.** With `autostart: false`, nothing runs until the page calls\n `handle.step(dt)`; each call is one `update(dt)` and one `draw()`. The CLI\n steps once with `dt` 0 after `ready`, then one sixtieth of a second per frame,\n so a page that does the same sees what `jgz test play` sees.\n- **Keys.** Keyboard events are heard on `keyboardTarget` (default `window`):\n `keydown` and `keyup`, read by `code` first (`ArrowRight`, `Space`, `KeyA`)\n and then `key`. Passing the canvas keeps them from reaching other games.\n- **Mouse and taps.** `mousedown`, `mousemove` and `wheel` are heard on the\n canvas, `mouseup` on `mouseReleaseTarget` (default `window`). A position is\n mapped from the canvas's displayed box onto the logical screen:\n `clientX = box.left + x / screen.width * box.width`, and the same for `y`\n (`handle.canvas.getBoundingClientRect()`, `handle.screen`). A canvas that\n isn't on the page has an empty box, so its client coordinates are the\n logical ones, as above. Touch arrives as `pointerdown`, `pointermove` and\n `pointerup` with `pointerType: \"touch\"`; the CLI uses the mouse events.\n\n## Diagnostics\n\n`run()` performs lightweight static checks and reports them through the\n`diagnostics` option (and collects them on `handle.diagnostics`) rather than\nfailing outright. A few mistakes stop the game before it starts instead:\n`run()` reports a `runtime-error` in the `startup` phase and throws. The\nmain one is that **`draw()` must not declare any parameters** (a\n`TypeError`, since mixing it up with `update(dt)` is a very common typo).\nThe others are a `TOUCH_CONTROLS` that isn't a valid mapping, a `seed` that\nisn't a string or a finite number, an invalid `projectId` or `assetUrls`, and\na top-level statement that throws.\n\nEverything else is a warning:\n\n| `code` | Meaning |\n| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |\n| `hook-typo` | A top-level function is named nearly like a hook (e.g. `onKeyDwon`), or is a hook spelled another way (`on_key_down`), which is never called. |\n| `constant-typo` | A top-level assignment is named nearly like a setting, such as `WIDTH` or `TITLE`. |\n| `duplicate-config` | The same setting was given both on the module and in the run options. |\n| `hook-arity` | `update` declares more than one parameter. |\n| `hook-parameter` | An input hook parameter isn't one of its documented names; says what it receives. |\n| `invalid-config` | `WIDTH`/`HEIGHT`/`PIXEL_DENSITY`/`ICON` isn't usable. |\n| `asset-name` | A quoted string that looks like an asset filename doesn't match the naming grammar. |\n| `runtime-error` | An uncaught error, or a failure loading/saving storage. |\n\nThe two typo checks only look at names the script _defines_ at top level\n(`function drow()`, `const udpate = () => {}`, `WIDHT = 800`), never at names\nit merely uses (`row`, `col`, `rows[y]`), and they stay quiet when the real\nname is also defined. Short names get one edit of slack and longer ones two.\n\nA `runtime-error` raised by the game's own code (a hook that threw, or a\ntop-level statement at startup) also carries `line`: the 1-based line in the\ngame source where the error was thrown, when the engine's stack trace names it.\nA `hook-typo` or `constant-typo` warning carries the line that declares the\nmisspelled name, and `draw()` declaring parameters fails on the line that\ndeclares it.\nA syntax error carries one too: where the engine gives no position (V8 gives\nnone for compiled game code), it's the first line whose part of the source\nfails the same way, so a missing `)` is placed on the line it's missing from,\nand a block left open on the line it opens on. A name the game never defined\nis placed the same way where the engine gives no position (JavaScriptCore): on\nthe one line that reads it, when only one does. `handle.failure.line` is the\nsame number. Errors thrown inside the library have no `line`.\n\nThe sandbox shows these under Messages, and marks a problem's line in the editor; import-mode hosts can pass\na `diagnostics` callback to log or display them however they like. Without\none, a run reports them to the browser console: a warning with\n`console.warn` and an error with `console.error`, each starting\n`js-game-zero:` and ending with its line, if it has one. A mistake that\nmakes `run()` itself throw isn't logged as well. A game that fails while\nrunning removes a canvas it made, so on a page with no callback, the\nconsole is where to find out why.\n",
37
+ "screen.md": "# Screen And Drawing\n\n`screen` is the global canvas surface. Everything you draw happens inside\n`draw()`, in screen pixels: `[0, 0]` is the top-left corner, x grows right,\ny grows down, and `[WIDTH, HEIGHT]` is the bottom-right corner.\n\n## Short forms\n\nSix short calls draw the most common shapes and text, with each position\ngiven as separate numbers rather than an `[x, y]` list:\n\n| Call | Draws |\n| -------------------------------------------------------- | ------------------------------------------------------- |\n| `screen.circle(x, y, radius, color)` | a filled circle centered on `x, y` |\n| `screen.rect(x, y, width, height, color)` | a filled rectangle whose top-left corner is `x, y` |\n| `screen.roundedRect(x, y, width, height, radius, color)` | the same rectangle with its corners rounded `radius` px |\n| `screen.triangle(x1, y1, x2, y2, x3, y3, color)` | a filled triangle with those three corners |\n| `screen.line(x1, y1, x2, y2, color, width = 1)` | a line from `x1, y1` to `x2, y2`, `width` px thick |\n| `screen.text(text, x, y, color, size = 24)` | text whose top-left corner is `x, y`, `size` tall |\n\n`color` is optional and defaults to `\"white\"`; it takes any of the\n[colors](#colors) below.\n\n```js\nfunction draw() {\n screen.clear();\n screen.rect(0, 220, 400, 80, \"green\");\n screen.roundedRect(280, 20, 100, 40, 12, \"purple\");\n screen.circle(200, 150, 25, \"gold\");\n screen.triangle(160, 40, 120, 120, 200, 120);\n screen.line(20, 20, 200, 120, \"white\", 3);\n screen.text(\"Score: \" + score, 20, 30);\n}\n```\n\n`screen.text` takes words in quotes or a number. To place text by its\ncenter, wrap it or give it an outline, use\n[`screen.draw.text`](#screendrawtexttext-pos-options--screendrawtexttext-options).\n\nThe short forms fill their shapes. The outline versions, and the versions\nthat take `[x, y]` lists, stay under `screen.draw` below. Mind the names:\n**`screen.circle` fills, but `screen.draw.circle` draws only an outline**,\nand the same goes for `screen.rect` and `screen.draw.rect`, and for\n`screen.roundedRect` and `screen.draw.roundedRect`.\n\nA wrong call throws a `TypeError` that says what to change. A list, as in\n`screen.circle([200, 150], 25)`, points to the `screen.draw` method that\ntakes lists (here `screen.draw.filledCircle`). A missing or non-number\nargument is named, and an extra one, such as an outline width, points to\nthe outline method. `screen.roundedRect` takes its radius before its color,\nas `screen.circle` does, and a color where the radius belongs says so.\n\n## Colors\n\nEvery drawing method accepts a `Color`, which is either a CSS color string\n(`\"white\"`, `\"#0f172a\"`, `\"rgb(20, 30, 40)\"`) or an RGB/RGBA tuple of\nintegers `0`–`255`: `[249, 115, 22]` or `[249, 115, 22, 128]` (the fourth\nvalue is alpha out of 255, not a `0`–`1` fraction). Passing anything else\nthrows a `TypeError` that says what was wrong:\n\n- A misspelled color name suggests the right one: `\"lightblu\"` fails with\n `Did you mean \"lightblue\"?`. (A canvas would otherwise ignore the color\n and keep drawing in the previous one.)\n- A hex code needs 3 or 6 hex digits (or 4 or 8, with alpha).\n- A fractional alpha such as `[255, 0, 0, 0.5]` fails with the 0–255 value\n to use instead (`128`).\n\nDrawing options are checked the same way: an option a method doesn't have,\nsuch as `{ colour: \"red\" }`, throws and names the closest real option.\n\n### `rgb(red, green, blue, alpha?)`\n\nMixes a color from red, green and blue light, each `0`–`255`, with an\noptional alpha out of 255. It returns the matching list, so\n`rgb(255, 140, 105)` is `[255, 140, 105]` and works anywhere a color does.\nIt's ready to use in a game file; in import mode, import it from\n`js-game-zero`.\n\n```js\nlet coral = rgb(255, 140, 105);\nlet glass = rgb(120, 200, 255, 128); // half see-through\nscreen.circle(200, 150, 25, coral);\n```\n\nIts numbers are checked like a list's, with the same hints (a fractional\nalpha such as `0.5` names `128` instead), and a missing one is named:\n`rgb(255, 140)` fails with `rgb is missing blue`.\n\n## `screen.clear()`\n\nResets the whole canvas to opaque black.\n\n```js\nfunction draw() {\n screen.clear();\n}\n```\n\n## `screen.fill(color, bottomColor?)`\n\nFills the canvas with a solid color, or a top-to-bottom gradient when a\nsecond color is given: `color` at the top fading to `bottomColor` at the\nbottom.\n\n```js\nscreen.fill(\"#0f172a\"); // solid\nscreen.fill(\"#0f172a\", \"#164e63\"); // gradient, dark at top to teal at bottom\n```\n\n## `screen.draw.image(image, pos)` / `screen.blit(image, pos)`\n\n`screen.draw.image` and `screen.blit` (Pygame Zero's name) do the same\nthing. Draws an already-loaded image (by asset name, or a `SurfaceLike` you already\nhold) at a position. `pos` is a `[x, y]` point for the image's top-left\ncorner, or a rect-like value (its top-left corner is used; blit does not\nscale the image to the rect's size).\n\n```js\nscreen.draw.image(\"player.svg\", [40, 40]);\n```\n\nIf the named image hasn't finished loading yet, nothing is drawn: it returns\na `Promise` that resolves once the image has loaded, and the runtime holds\nthe frames that follow until then, so the image shows from the next one.\nIf it can't be loaded, the promise rejects and the run reports the missing\nimage by name, so `draw()` needn't handle the promise itself.\nImages touched while the script starts up load before the first frame (see\n[`docs/reference/runtime.md`](runtime.md#preload-gate)).\nPrefer `Actor` for anything that moves or needs collision — drawing an image\ndirectly is for background art and tiles.\n\n## `screen.bounds()`\n\nReturns a `ZRect` covering the whole canvas (`[0, 0, WIDTH, HEIGHT]`), handy\nfor clamping or collision against the screen edge.\n\n```js\nif (!screen.bounds().contains(player.rect()))\n player.center = [WIDTH / 2, HEIGHT / 2];\n```\n\n## `PIXEL_DENSITY` and `screen.density`\n\nOn a phone, one screen pixel covers several of the device's own pixels.\nThe canvas has a pixel for each of them, so text, outlines and SVG pictures\nstay sharp. `screen.density` says how many canvas pixels make up one screen\npixel along each side: between 2 and 3 on most phones and 1 on a computer\nmonitor. Your game's numbers don't change with it: a game 360 pixels wide\nis 360 wide at any density.\n\nSet `PIXEL_DENSITY` next to `WIDTH` and `HEIGHT` to choose the density\nyourself. Without it, the game uses the device's own, up to 3.\n\n```js\nWIDTH = 360;\nHEIGHT = 640;\nPIXEL_DENSITY = 1; // one canvas pixel per screen pixel, even on a phone\n```\n\nSee [pixel density](runtime.md#pixel-density) for how hosts size and read\nthe canvas.\n\n## `screen.draw.line(start, end, color, width = 1)`\n\n```js\nscreen.draw.line([20, 20], [200, 120], \"white\", 3);\n```\n\n## `screen.draw.circle(pos, radius, color, width = 1)` / `filledCircle(pos, radius, color)`\n\n`screen.draw.circle` draws an outline; `filledCircle` and the short form\n[`screen.circle`](#short-forms) fill.\n\n```js\nscreen.draw.circle([160, 90], 40, \"#38bdf8\", 2); // outline\nscreen.draw.filledCircle([160, 90], 40, \"#38bdf8\"); // filled\n```\n\n## `screen.draw.rect(rect, color, width = 1)` / `filledRect(rect, color)`\n\n`rect` accepts anything geometry accepts: a `Rect`, an `Actor` (via its\n`.rect()` duck type — see [`docs/reference/rect.md`](rect.md)), or a plain\n`[left, top, width, height]` tuple.\n\n```js\nscreen.draw.filledRect([40, 40, 120, 60], \"#f97316\");\nscreen.draw.rect(player.rect(), \"yellow\", 2); // highlight a hitbox\n```\n\n## `screen.draw.roundedRect(rect, radius, color, width = 1)` / `filledRoundedRect(rect, radius, color)`\n\nA rectangle with its corners rounded: each corner is a quarter of a circle\n`radius` pixels round. `rect` takes what `screen.draw.rect` takes, and the\nradius comes before the color, as a circle's does. `roundedRect` draws an\noutline, `width` pixels thick; `filledRoundedRect` and the short form\n[`screen.roundedRect`](#short-forms) fill.\n\n```js\nscreen.draw.filledRoundedRect([40, 40, 160, 48], 12, \"#f97316\"); // a button\nscreen.draw.roundedRect([40, 40, 160, 48], 12, \"white\", 2); // its border\nscreen.draw.filledRoundedRect([40, 110, 160, 30], 15, \"#22c55e\"); // a pill\n```\n\nA radius of `0` gives square corners. A radius bigger than half the\nrectangle's height (or width, if that's shorter) is drawn as that half, so\nits short sides are half circles: a pill, or a circle for a square. A\nnegative radius, or a color where the radius belongs, throws a `TypeError`\nthat says what to change.\n\n## `screen.draw.polygon(points, color, width = 1)` / `filledPolygon(points, color)`\n\nNeeds at least three points.\n\n```js\nscreen.draw.filledPolygon(\n [\n [238, 40],\n [292, 64],\n [270, 126],\n [214, 118],\n ],\n \"#f8fafc\",\n);\n```\n\n## `screen.draw.text(text, pos?, options?)` / `screen.draw.text(text, options)`\n\nDraws text with a large set of beginner-friendly options — this is a\nJavaScript-native reimplementation of pgzero's `ptext` module, including its\nword-wrapping algorithm and gradient/outline/shadow effects.\n\n```js\nscreen.draw.text(\"Score: 12\", [16, 16], {\n size: 24,\n color: \"white\",\n});\n```\n\nWhen the options say where the text goes (`center`, `topRight`, ...), pass\nthem second, in place of the position:\n\n```js\nscreen.draw.text(\"Game Over\", { center: [WIDTH / 2, HEIGHT / 2], size: 40 });\n```\n\nSeveral options have two names: a readable one, and the shorter one Pygame\nZero uses. Either works, but not both at once:\n\n| Readable name | Pygame Zero name |\n| --------------- | ---------------- |\n| `size` | `fontsize` |\n| `outline` | `owidth` |\n| `outlineColor` | `ocolor` |\n| `shadowColor` | `scolor` |\n| `gradientColor` | `gcolor` |\n\n### Position options\n\nGive **exactly one** of the following ways to position the text (mixing\nthem throws a `TypeError`):\n\n- `pos` (or the second positional argument) — a `[x, y]` point. Combined\n with `anchor` (default `\"topLeft\"`) to decide which part of the text box\n sits at that point.\n- A named anchor point directly: `topLeft`, `topRight`, `bottomLeft`,\n `bottomRight`, `midTop`, `midLeft`, `midRight`, `midBottom`, `center` —\n each takes a `[x, y]` point for that corner/edge/center of the text.\n- Scalar axis keywords, combined per-axis: `left`/`centerX`/`right` for the\n horizontal position, `top`/`centerY`/`bottom` for the vertical position.\n You can mix one horizontal and one vertical scalar (`{ right: 300, top: 10\n}`), or use just one axis and fall back to `pos` for the other.\n\n```js\nscreen.draw.text(\"Game Over\", {\n center: [WIDTH / 2, HEIGHT / 2],\n size: 40,\n});\nscreen.draw.text(\"HUD\", { right: WIDTH - 10, top: 10 }); // top-right corner, 10px margin\n```\n\n### Font options\n\n- `size` / `fontsize` (default `24`)\n- `fontname` — the stem of a font asset under `fonts/` (e.g. `\"pixel\"` for\n `fonts/pixel.ttf`); loads asynchronously the first time it's used.\n- `sysfontname` — a literal CSS font family name instead (e.g. `\"Georgia\"`).\n Mutually exclusive with `fontname`.\n- `bold`, `italic`, `underline` — booleans.\n- `color` (default `\"white\"`), `background` (fills a box behind the text).\n- `gradientColor` / `gcolor` — a second color for a top-to-bottom gradient\n across the text.\n\n### Layout options\n\n- `width` — wraps text to fit this many pixels; without it, text is only\n broken at explicit newlines (`\\n`). Wrapping is greedy word-wrap: a single\n word longer than `width` is placed on its own (overflowing) line rather\n than being cut.\n- `widthem` — like `width`, but expressed as a multiple of `size`.\n- `lineheight` — pixels between lines (default `size * 1.2`).\n- `align` — `\"left\"` (default), `\"center\"`, or `\"right\"`, for multi-line text.\n A [`textbox`](#screendrawtextboxtext-rect-options) follows its `anchor`\n instead, so it centers by default.\n- `strip` — trims trailing spaces from each wrapped line (default `true`).\n- `anchor` — overrides which part of the text's bounding box sits at the\n resolved position; either one of the named anchor strings above, or a\n `[horizontal, vertical]` pair where each side is `\"left\"`/`\"center\"`/`\"right\"`,\n `\"top\"`/`\"center\"`/`\"bottom\"`, or a literal pixel-offset number.\n- `angle` — rotates the text counterclockwise around the anchor point, in\n degrees.\n\n### Effects\n\n- `outline` / `owidth` + `outlineColor` / `ocolor` — an outline stroke\n around the glyphs, `outline` pixels thick.\n- `shadow` (a number or `[x, y]` offset) + `shadowColor` / `scolor`\n (default `\"black\"`) — a drop shadow.\n- `alpha` — overall opacity, `0`–`1` (default `1`).\n\n## `screen.draw.textbox(text, rect, options?)`\n\nLike `draw.text`, but instead of a fixed `size` you give a rectangle and\n`textbox` picks the **largest font size that fits inside it** (shrinking\ndown to `minSize`, default `4`, if necessary; `minFontsize` is another name\nfor it, but not both at once). Text is centered in the rect by default.\nPass `anchor` (e.g. `\"topLeft\"`) to place it elsewhere in the rect; its\nlines line up the same way (on the left for `\"topLeft\"`) unless you give\n`align`. The rect places the text, so `textbox` takes none of\n`draw.text`'s position options (`pos`, `center`, `topLeft`, `left`, ...)\nand no `width`; giving one throws a `TypeError` that says so.\n\n```js\nscreen.draw.textbox(\"You win!\", [40, 60, 240, 80], {\n color: \"white\",\n size: 48, // upper bound; shrinks to fit if \"You win!\" would overflow\n});\n```\n\n## `screen.measureText(text, options?)`\n\nHow big `screen.draw.text` would draw some text. It returns\n`{ width, height, lines }`, in screen pixels, and draws nothing:\n\n- `width` — the widest line's width.\n- `height` — the lines' height together: one `lineheight` (`size * 1.2`\n unless given) for each line.\n- `lines` — the lines the text breaks into, at each `\\n` and, with a\n `width` option, where it wraps.\n\nIt takes the options `screen.draw.text` takes (`size`, `fontname`,\n`sysfontname`, `bold`, `italic`, `width`, `widthem`, `lineheight`,\n`strip`) and measures the text the way `screen.draw.text` lays it out\nwith them. Options that only place or color the text (`center`, `color`,\n`outline`, `angle`, ...) are allowed too, and change nothing, so you can\nmeasure a label with the same options you draw it with. A position before\nthe options, as in `screen.draw.text(text, [x, y], options)`, is allowed\nfor the same reason.\n\n```js\nlet options = { midLeft: [40, 200], size: 20 };\nscreen.draw.text(\"Play again\", options);\nlet label = screen.measureText(\"Play again\", options);\n// A dot just right of the label, whatever its font and size.\nscreen.circle(40 + label.width + 12, 200, 5, \"gold\");\n```\n\nMeasuring a button's label tells you how big to draw the button:\n\n```js\nlet size = screen.measureText(\"Start\", { size: 24 });\nlet box = [100, 100, size.width + 32, size.height + 16];\nscreen.draw.filledRoundedRect(box, 12, \"#2563eb\");\nscreen.draw.text(\"Start\", {\n center: [100 + box[2] / 2, 100 + box[3] / 2],\n size: 24,\n});\n```\n\nWithout a `width` option, the size it returns is the size of the box\n`screen.draw.text` places, so `center` (or any other anchor) puts the text\nwhere that size says it goes. With a `width` option, `screen.draw.text`'s\nbox is that wide, and the `width` it returns says how much of it the\nlines fill. An outline or shadow draws a little outside the measured size.\n\nMeasure where you draw, in `draw()`: a font from `fonts/` measures right\nonce it has loaded, and the game holds its frames while it loads.\n\n`text` is words in quotes or a number. A wrong call throws a `TypeError`\nthat says what to change: an option `screen.draw.text` doesn't have is\nnamed with the closest real one, and a number after the text, as in\n`screen.measureText(\"Score\", 32)`, says to give the size as an option,\n`{ size: 32 }`.\n",
38
38
  "storage.md": "# Storage\n\n`storage` is a persistent, dictionary-like object for saving small amounts\nof data between play sessions — high scores, unlocked levels, settings.\n\n```js\nlet highScore = storage.get(\"highScore\") ?? 0;\n\nfunction update() {\n if (score > highScore) {\n highScore = score;\n storage.set(\"highScore\", highScore);\n }\n}\n```\n\n## Auto-load and auto-save\n\nYou don't need to call `save()` or `load()` yourself in normal use:\n\n- **Auto-load**: the default browser adapter has a synchronous `loadSync()`\n path, so a top-level read like `let score = storage.get(\"score\") ?? 0`\n sees previously-saved data immediately. So does a custom adapter with a\n `loadSync()`, or whose `load()` returns its text rather than a Promise. A\n custom asynchronous adapter is loaded once during startup and delays\n frames/`handle.ready`, but cannot retroactively change values already\n copied into top-level variables, and what it loads replaces anything the\n top level stored before it arrived.\n- **Auto-save**: any mutation (`storage.set(...)`, `storage.score = 5`,\n `storage.delete(...)`, `storage.clear()`) schedules a save a fraction of a\n second later, and within a second even while the game keeps changing it\n every frame; rapid changes coalesce into one write instead of saving on\n every single mutation. Storage is also flushed immediately when the tab is\n hidden or closed, and when the game stops, so nothing is lost. A change\n inside a stored object or list, such as `storage.highScores.push(score)`,\n is saved too, with the next of those saves; set the key again\n (`storage.highScores = storage.highScores`) to save it sooner.\n\n## Reading and writing\n\nTwo equivalent styles work — plain method calls, or attribute-style access\nthrough a proxy (closer to pgzero's `storage` object):\n\n```js\nstorage.set(\"score\", 10);\nstorage.get(\"score\"); // 10\n\nstorage.score = 10; // same as storage.set(\"score\", 10)\nstorage.score; // 10\n```\n\n| Method | Behavior |\n| --------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| `storage.get(key, defaultValue?)` | Returns the stored value, or `defaultValue` (`undefined` if omitted) if `key` isn't set. |\n| `storage.set(key, value)` | Stores `value` under `key`. |\n| `storage.setDefault(key, defaultValue)` | Sets `key` to `defaultValue` only if it isn't already set; returns the resulting value either way. |\n| `storage.has(key)` | `boolean`. |\n| `storage.delete(key)` | Removes `key`; also works as `delete storage.key`. Returns `true` if something was removed. |\n| `storage.clear()` | Removes everything. |\n| `storage.keys()` | Every stored key, alphabetically sorted. |\n| `storage.path` | The backing location: a scoped `localStorage` key in the browser, or a file path in the sandbox. |\n\nValues must be JSON-serializable — plain objects, arrays, strings, numbers,\nbooleans, and `null`. Storing anything else (a class instance, a function,\n`undefined`, `NaN`, `Infinity`, a circular reference), anywhere inside the\nvalue, throws a `JsonStorageError` naming the offending path, e.g.\n`NaN is not a JSON value at storage.highScores[2].score`, and stores nothing.\nA value changed inside a stored object or list after it was stored is only\nchecked when storage saves: until it's a JSON value again, saving fails,\nwith a warning naming its path, and nothing in storage is saved.\n\n## Gotchas\n\n- **Don't shadow method names.** `storage.save`, `storage.load`, `storage.path`\n and the other method names above are real properties on `storage` itself,\n not just proxy-forwarded keys — writing `storage.save = 5` overwrites the\n actual `save` method instead of storing a value, and another spelling of\n a method, such as `storage.setdefault`, throws naming the method. Pick key\n names that don't collide with the method list.\n- Storage is scoped per project: in the browser it's namespaced by project\n id (so two different games on the same site don't share data, as long as\n each has its own `projectId`; games run without one all share the id\n `default`); in the sandbox it's a JSON file inside that project's own\n directory.\n\n## Manual save/load\n\nFor advanced cases — forcing an immediate write instead of waiting for the\ndebounce, or working outside of `run()` — `storage.save()` and\n`storage.load()` are both available directly and return `Promise<void>`.\n"
39
39
  });
@@ -309,17 +309,17 @@ in order, rather than one of its bindings, through `read`, `inspect` and
309
309
  `jgz`'s `inspect` alike. Each thing drawn has its `kind` and what it is and
310
310
  where, as the game gave them:
311
311
 
312
- | `kind` | Fields | From |
313
- | --------- | ----------------------------------------------------------------------------- | --------------------------------------------------------- |
314
- | `fill` | `color`, and `to` for a gradient | `screen.fill` |
315
- | `clear` | | `screen.clear` |
316
- | `image` | `image` (its name), `x`, `y` (its top-left) | `screen.blit`, `screen.draw.image` |
317
- | `actor` | `image`, `x`, `y` (its `pos`: its center, unless its `anchor` says otherwise) | an Actor's `draw()` |
318
- | `circle` | `x`, `y`, `radius`, `color`, `filled` | `screen.circle`, `screen.draw.circle`, `filledCircle` |
319
- | `rect` | `x`, `y`, `width`, `height`, `color`, `filled` | `screen.rect`, `screen.draw.rect`, `filledRect` |
320
- | `polygon` | `x`, `y` (its first point), `points` (how many), `color`, `filled` | `screen.triangle`, `screen.draw.polygon`, `filledPolygon` |
321
- | `line` | `x`, `y`, `x2`, `y2`, `color` | `screen.line`, `screen.draw.line` |
322
- | `text` | `text`, `x`, `y`, `width`, `height` (its box), and `color` when it has one | `screen.text`, `screen.draw.text`, `screen.draw.textbox` |
312
+ | `kind` | Fields | From |
313
+ | --------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
314
+ | `fill` | `color`, and `to` for a gradient | `screen.fill` |
315
+ | `clear` | | `screen.clear` |
316
+ | `image` | `image` (its name), `x`, `y` (its top-left) | `screen.blit`, `screen.draw.image` |
317
+ | `actor` | `image`, `x`, `y` (its `pos`: its center, unless its `anchor` says otherwise) | an Actor's `draw()` |
318
+ | `circle` | `x`, `y`, `radius`, `color`, `filled` | `screen.circle`, `screen.draw.circle`, `filledCircle` |
319
+ | `rect` | `x`, `y`, `width`, `height`, `color`, `filled`, and `radius` when rounded | `screen.rect`, `screen.draw.rect`, `filledRect`, and the `roundedRect` forms |
320
+ | `polygon` | `x`, `y` (its first point), `points` (how many), `color`, `filled` | `screen.triangle`, `screen.draw.polygon`, `filledPolygon` |
321
+ | `line` | `x`, `y`, `x2`, `y2`, `color` | `screen.line`, `screen.draw.line` |
322
+ | `text` | `text`, `x`, `y`, `width`, `height` (its box), and `color` when it has one | `screen.text`, `screen.draw.text`, `screen.draw.textbox` |
323
323
 
324
324
  `$drawn[0].kind`, `$drawn[2].image` and the like read single things. A frame
325
325
  keeps its first 200 things; before the first frame `$drawn` is empty. So a
@@ -6,16 +6,17 @@ y grows down, and `[WIDTH, HEIGHT]` is the bottom-right corner.
6
6
 
7
7
  ## Short forms
8
8
 
9
- Five short calls draw the most common shapes and text, with each position
9
+ Six short calls draw the most common shapes and text, with each position
10
10
  given as separate numbers rather than an `[x, y]` list:
11
11
 
12
- | Call | Draws |
13
- | ------------------------------------------------ | -------------------------------------------------- |
14
- | `screen.circle(x, y, radius, color)` | a filled circle centered on `x, y` |
15
- | `screen.rect(x, y, width, height, color)` | a filled rectangle whose top-left corner is `x, y` |
16
- | `screen.triangle(x1, y1, x2, y2, x3, y3, color)` | a filled triangle with those three corners |
17
- | `screen.line(x1, y1, x2, y2, color, width = 1)` | a line from `x1, y1` to `x2, y2`, `width` px thick |
18
- | `screen.text(text, x, y, color, size = 24)` | text whose top-left corner is `x, y`, `size` tall |
12
+ | Call | Draws |
13
+ | -------------------------------------------------------- | ------------------------------------------------------- |
14
+ | `screen.circle(x, y, radius, color)` | a filled circle centered on `x, y` |
15
+ | `screen.rect(x, y, width, height, color)` | a filled rectangle whose top-left corner is `x, y` |
16
+ | `screen.roundedRect(x, y, width, height, radius, color)` | the same rectangle with its corners rounded `radius` px |
17
+ | `screen.triangle(x1, y1, x2, y2, x3, y3, color)` | a filled triangle with those three corners |
18
+ | `screen.line(x1, y1, x2, y2, color, width = 1)` | a line from `x1, y1` to `x2, y2`, `width` px thick |
19
+ | `screen.text(text, x, y, color, size = 24)` | text whose top-left corner is `x, y`, `size` tall |
19
20
 
20
21
  `color` is optional and defaults to `"white"`; it takes any of the
21
22
  [colors](#colors) below.
@@ -24,6 +25,7 @@ given as separate numbers rather than an `[x, y]` list:
24
25
  function draw() {
25
26
  screen.clear();
26
27
  screen.rect(0, 220, 400, 80, "green");
28
+ screen.roundedRect(280, 20, 100, 40, 12, "purple");
27
29
  screen.circle(200, 150, 25, "gold");
28
30
  screen.triangle(160, 40, 120, 120, 200, 120);
29
31
  screen.line(20, 20, 200, 120, "white", 3);
@@ -38,13 +40,15 @@ center, wrap it or give it an outline, use
38
40
  The short forms fill their shapes. The outline versions, and the versions
39
41
  that take `[x, y]` lists, stay under `screen.draw` below. Mind the names:
40
42
  **`screen.circle` fills, but `screen.draw.circle` draws only an outline**,
41
- and the same goes for `screen.rect` and `screen.draw.rect`.
43
+ and the same goes for `screen.rect` and `screen.draw.rect`, and for
44
+ `screen.roundedRect` and `screen.draw.roundedRect`.
42
45
 
43
46
  A wrong call throws a `TypeError` that says what to change. A list, as in
44
47
  `screen.circle([200, 150], 25)`, points to the `screen.draw` method that
45
48
  takes lists (here `screen.draw.filledCircle`). A missing or non-number
46
49
  argument is named, and an extra one, such as an outline width, points to
47
- the outline method.
50
+ the outline method. `screen.roundedRect` takes its radius before its color,
51
+ as `screen.circle` does, and a color where the radius belongs says so.
48
52
 
49
53
  ## Colors
50
54
 
@@ -183,6 +187,26 @@ screen.draw.filledRect([40, 40, 120, 60], "#f97316");
183
187
  screen.draw.rect(player.rect(), "yellow", 2); // highlight a hitbox
184
188
  ```
185
189
 
190
+ ## `screen.draw.roundedRect(rect, radius, color, width = 1)` / `filledRoundedRect(rect, radius, color)`
191
+
192
+ A rectangle with its corners rounded: each corner is a quarter of a circle
193
+ `radius` pixels round. `rect` takes what `screen.draw.rect` takes, and the
194
+ radius comes before the color, as a circle's does. `roundedRect` draws an
195
+ outline, `width` pixels thick; `filledRoundedRect` and the short form
196
+ [`screen.roundedRect`](#short-forms) fill.
197
+
198
+ ```js
199
+ screen.draw.filledRoundedRect([40, 40, 160, 48], 12, "#f97316"); // a button
200
+ screen.draw.roundedRect([40, 40, 160, 48], 12, "white", 2); // its border
201
+ screen.draw.filledRoundedRect([40, 110, 160, 30], 15, "#22c55e"); // a pill
202
+ ```
203
+
204
+ A radius of `0` gives square corners. A radius bigger than half the
205
+ rectangle's height (or width, if that's shorter) is drawn as that half, so
206
+ its short sides are half circles: a pill, or a circle for a square. A
207
+ negative radius, or a color where the radius belongs, throws a `TypeError`
208
+ that says what to change.
209
+
186
210
  ## `screen.draw.polygon(points, color, width = 1)` / `filledPolygon(points, color)`
187
211
 
188
212
  Needs at least three points.
@@ -311,3 +335,58 @@ screen.draw.textbox("You win!", [40, 60, 240, 80], {
311
335
  size: 48, // upper bound; shrinks to fit if "You win!" would overflow
312
336
  });
313
337
  ```
338
+
339
+ ## `screen.measureText(text, options?)`
340
+
341
+ How big `screen.draw.text` would draw some text. It returns
342
+ `{ width, height, lines }`, in screen pixels, and draws nothing:
343
+
344
+ - `width` — the widest line's width.
345
+ - `height` — the lines' height together: one `lineheight` (`size * 1.2`
346
+ unless given) for each line.
347
+ - `lines` — the lines the text breaks into, at each `\n` and, with a
348
+ `width` option, where it wraps.
349
+
350
+ It takes the options `screen.draw.text` takes (`size`, `fontname`,
351
+ `sysfontname`, `bold`, `italic`, `width`, `widthem`, `lineheight`,
352
+ `strip`) and measures the text the way `screen.draw.text` lays it out
353
+ with them. Options that only place or color the text (`center`, `color`,
354
+ `outline`, `angle`, ...) are allowed too, and change nothing, so you can
355
+ measure a label with the same options you draw it with. A position before
356
+ the options, as in `screen.draw.text(text, [x, y], options)`, is allowed
357
+ for the same reason.
358
+
359
+ ```js
360
+ let options = { midLeft: [40, 200], size: 20 };
361
+ screen.draw.text("Play again", options);
362
+ let label = screen.measureText("Play again", options);
363
+ // A dot just right of the label, whatever its font and size.
364
+ screen.circle(40 + label.width + 12, 200, 5, "gold");
365
+ ```
366
+
367
+ Measuring a button's label tells you how big to draw the button:
368
+
369
+ ```js
370
+ let size = screen.measureText("Start", { size: 24 });
371
+ let box = [100, 100, size.width + 32, size.height + 16];
372
+ screen.draw.filledRoundedRect(box, 12, "#2563eb");
373
+ screen.draw.text("Start", {
374
+ center: [100 + box[2] / 2, 100 + box[3] / 2],
375
+ size: 24,
376
+ });
377
+ ```
378
+
379
+ Without a `width` option, the size it returns is the size of the box
380
+ `screen.draw.text` places, so `center` (or any other anchor) puts the text
381
+ where that size says it goes. With a `width` option, `screen.draw.text`'s
382
+ box is that wide, and the `width` it returns says how much of it the
383
+ lines fill. An outline or shadow draws a little outside the measured size.
384
+
385
+ Measure where you draw, in `draw()`: a font from `fonts/` measures right
386
+ once it has loaded, and the game holds its frames while it loads.
387
+
388
+ `text` is words in quotes or a number. A wrong call throws a `TypeError`
389
+ that says what to change: an option `screen.draw.text` doesn't have is
390
+ named with the closest real one, and a number after the text, as in
391
+ `screen.measureText("Score", 32)`, says to give the size as an option,
392
+ `{ size: 32 }`.
@@ -78,6 +78,15 @@ export type DrawText = {
78
78
  (text: string, options?: TextOptions): void;
79
79
  (text: string, pos: Point | undefined, options?: TextOptions): void;
80
80
  };
81
+ /** How big `screen.measureText` finds text, as `screen.draw.text` would draw it. */
82
+ export type TextSize = {
83
+ /** Its widest line's width. */
84
+ readonly width: number;
85
+ /** Its lines' height together: as many lines as it has, each `lineheight` tall. */
86
+ readonly height: number;
87
+ /** The lines it breaks into: at each newline, and where a `width` wraps it. */
88
+ readonly lines: readonly string[];
89
+ };
81
90
  export type TextLayout = {
82
91
  readonly lines: readonly string[];
83
92
  readonly font: string;
@@ -134,6 +143,9 @@ export declare class Screen {
134
143
  filledPolygon: (points: Point[], color: Color) => void;
135
144
  rect: (rect: RectLike, color: Color, width?: number) => void;
136
145
  filledRect: (rect: RectLike, color: Color) => void;
146
+ /** Its corners are rounded `radius` round, at most half its shorter side. */
147
+ roundedRect: (rect: RectLike, radius: number, color: Color, width?: number) => void;
148
+ filledRoundedRect: (rect: RectLike, radius: number, color: Color) => void;
137
149
  text: DrawText;
138
150
  textbox: (text: string, rect: RectLike, options?: TextboxOptions) => void;
139
151
  /** Same as `screen.blit`: draws an image with its top-left at `pos`. */
@@ -160,6 +172,11 @@ export declare class Screen {
160
172
  circle(x: number, y: number, radius: number, color?: Color): void;
161
173
  /** A filled rectangle whose top-left corner is at `x, y`. */
162
174
  rect(x: number, y: number, width: number, height: number, color?: Color): void;
175
+ /**
176
+ * A filled rectangle like `screen.rect`'s, with its corners rounded
177
+ * `radius` round. The radius comes before the color, as a circle's does.
178
+ */
179
+ roundedRect(x: number, y: number, width: number, height: number, radius: number, color?: Color): void;
163
180
  triangle(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, color?: Color): void;
164
181
  line(x1: number, y1: number, x2: number, y2: number, color?: Color, width?: number): void;
165
182
  /**
@@ -169,11 +186,22 @@ export declare class Screen {
169
186
  * under `screen.draw.text`.
170
187
  */
171
188
  text(text: string | number, x: number, y: number, color?: Color, size?: number): void;
189
+ /**
190
+ * How big `screen.draw.text` would draw `text`, measured the way it lays
191
+ * it out: its widest line's width, its lines' height together, and the
192
+ * lines. It takes what `screen.draw.text` takes, so a label is measured
193
+ * with the arguments it's drawn with; where it goes doesn't change its
194
+ * size. It draws nothing.
195
+ */
196
+ measureText(text: string | number, options?: TextOptions): TextSize;
197
+ measureText(text: string | number, pos: Point | undefined, options?: TextOptions): TextSize;
172
198
  private drawSurface;
173
199
  private drawLine;
174
200
  private drawCircle;
175
201
  private drawPolygon;
176
202
  private drawRect;
203
+ /** `call` is how the method that drew it is called, for its errors. */
204
+ private drawRoundedRect;
177
205
  private drawText;
178
206
  private drawTextbox;
179
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-game-zero",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A beginner-friendly framework for browser games in the spirit of Pygame Zero: set WIDTH and HEIGHT, write draw(), and play. Actors, drawing, input, sound, animation and storage, with a script-tag build.",