castle-web-cli 0.4.3 → 0.4.5

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.
Files changed (97) hide show
  1. package/dist/index.js +8 -2
  2. package/dist/init.js +1 -1
  3. package/kits/basic-2d/CLAUDE.md +3 -3
  4. package/package.json +1 -1
  5. package/kits/basic-2d-frozen/.prettierrc +0 -8
  6. package/kits/basic-2d-frozen/CLAUDE.md +0 -131
  7. package/kits/basic-2d-frozen/behaviors/Camera.jsx +0 -43
  8. package/kits/basic-2d-frozen/behaviors/Collider.jsx +0 -71
  9. package/kits/basic-2d-frozen/behaviors/Drawing.jsx +0 -139
  10. package/kits/basic-2d-frozen/behaviors/Layout.jsx +0 -16
  11. package/kits/basic-2d-frozen/drawings/floor.drawing +0 -70
  12. package/kits/basic-2d-frozen/editors/App.jsx +0 -152
  13. package/kits/basic-2d-frozen/editors/CodeEditor.jsx +0 -112
  14. package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +0 -222
  15. package/kits/basic-2d-frozen/editors/FileBrowser.jsx +0 -143
  16. package/kits/basic-2d-frozen/editors/PlayOnly.jsx +0 -21
  17. package/kits/basic-2d-frozen/editors/SceneEditor.jsx +0 -1012
  18. package/kits/basic-2d-frozen/editors/behaviorRegistry.js +0 -24
  19. package/kits/basic-2d-frozen/editors/editorHistory.js +0 -52
  20. package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +0 -83
  21. package/kits/basic-2d-frozen/engine/SceneUI.jsx +0 -67
  22. package/kits/basic-2d-frozen/engine/TouchControls.jsx +0 -136
  23. package/kits/basic-2d-frozen/engine/autoInspector.jsx +0 -51
  24. package/kits/basic-2d-frozen/engine/files.js +0 -62
  25. package/kits/basic-2d-frozen/engine/scene.js +0 -420
  26. package/kits/basic-2d-frozen/engine/ui.jsx +0 -344
  27. package/kits/basic-2d-frozen/engine/ui.module.css +0 -928
  28. package/kits/basic-2d-frozen/eslint.config.js +0 -50
  29. package/kits/basic-2d-frozen/index.html +0 -11
  30. package/kits/basic-2d-frozen/main.jsx +0 -10
  31. package/kits/basic-2d-frozen/package-lock.json +0 -2706
  32. package/kits/basic-2d-frozen/package.json +0 -41
  33. package/kits/basic-2d-frozen/scenes/main.scene +0 -108
  34. package/kits/basic-2d-frozen/vite.config.js +0 -1
  35. package/kits/rpg-2d/.prettierrc +0 -8
  36. package/kits/rpg-2d/behaviors/Camera.tsx +0 -52
  37. package/kits/rpg-2d/behaviors/Collider.tsx +0 -98
  38. package/kits/rpg-2d/behaviors/Dialog.tsx +0 -184
  39. package/kits/rpg-2d/behaviors/Drawing.tsx +0 -161
  40. package/kits/rpg-2d/behaviors/Friend.tsx +0 -45
  41. package/kits/rpg-2d/behaviors/Layout.tsx +0 -29
  42. package/kits/rpg-2d/behaviors/PlayerController.tsx +0 -255
  43. package/kits/rpg-2d/behaviors/Portal.tsx +0 -60
  44. package/kits/rpg-2d/behaviors/QuestLog.tsx +0 -90
  45. package/kits/rpg-2d/behaviors/SaveMenu.tsx +0 -123
  46. package/kits/rpg-2d/behaviors/Tilemap.tsx +0 -90
  47. package/kits/rpg-2d/drawings/bld-home.drawing +0 -8136
  48. package/kits/rpg-2d/drawings/env-crate.drawing +0 -509
  49. package/kits/rpg-2d/drawings/env-fence.drawing +0 -536
  50. package/kits/rpg-2d/drawings/env-flower-bed.drawing +0 -607
  51. package/kits/rpg-2d/drawings/env-fountain.drawing +0 -2622
  52. package/kits/rpg-2d/drawings/env-hedge.drawing +0 -601
  53. package/kits/rpg-2d/drawings/env-house-blue.drawing +0 -1
  54. package/kits/rpg-2d/drawings/env-house-green.drawing +0 -1
  55. package/kits/rpg-2d/drawings/env-tree-oak.drawing +0 -1540
  56. package/kits/rpg-2d/drawings/env-tree-pine.drawing +0 -1315
  57. package/kits/rpg-2d/drawings/floor.drawing +0 -70
  58. package/kits/rpg-2d/drawings/fx-sparkle.drawing +0 -926
  59. package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +0 -1099
  60. package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +0 -4177
  61. package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +0 -1099
  62. package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +0 -4177
  63. package/kits/rpg-2d/drawings/player-idle-down.drawing +0 -1070
  64. package/kits/rpg-2d/drawings/player-idle-left.drawing +0 -1070
  65. package/kits/rpg-2d/drawings/player-idle-right.drawing +0 -1070
  66. package/kits/rpg-2d/drawings/player-idle-up.drawing +0 -1070
  67. package/kits/rpg-2d/drawings/player-walk-down.drawing +0 -4148
  68. package/kits/rpg-2d/drawings/player-walk-left.drawing +0 -4148
  69. package/kits/rpg-2d/drawings/player-walk-right.drawing +0 -4148
  70. package/kits/rpg-2d/drawings/player-walk-up.drawing +0 -4148
  71. package/kits/rpg-2d/editors/App.tsx +0 -163
  72. package/kits/rpg-2d/editors/CodeEditor.tsx +0 -120
  73. package/kits/rpg-2d/editors/DrawingEditor.tsx +0 -278
  74. package/kits/rpg-2d/editors/FileBrowser.tsx +0 -191
  75. package/kits/rpg-2d/editors/PlayOnly.tsx +0 -26
  76. package/kits/rpg-2d/editors/SceneEditor.tsx +0 -1093
  77. package/kits/rpg-2d/editors/behaviorRegistry.ts +0 -33
  78. package/kits/rpg-2d/editors/editorHistory.ts +0 -75
  79. package/kits/rpg-2d/editors/editorProps.ts +0 -10
  80. package/kits/rpg-2d/engine/ScenePlayer.tsx +0 -130
  81. package/kits/rpg-2d/engine/SceneUI.tsx +0 -74
  82. package/kits/rpg-2d/engine/TouchControls.tsx +0 -157
  83. package/kits/rpg-2d/engine/autoInspector.tsx +0 -111
  84. package/kits/rpg-2d/engine/drawing.ts +0 -81
  85. package/kits/rpg-2d/engine/files.ts +0 -215
  86. package/kits/rpg-2d/engine/scene.ts +0 -484
  87. package/kits/rpg-2d/engine/ui.module.css +0 -928
  88. package/kits/rpg-2d/engine/ui.tsx +0 -483
  89. package/kits/rpg-2d/eslint.config.js +0 -46
  90. package/kits/rpg-2d/index.html +0 -11
  91. package/kits/rpg-2d/main.tsx +0 -14
  92. package/kits/rpg-2d/package-lock.json +0 -3149
  93. package/kits/rpg-2d/package.json +0 -46
  94. package/kits/rpg-2d/scenes/main.scene +0 -203
  95. package/kits/rpg-2d/tsconfig.json +0 -17
  96. package/kits/rpg-2d/vite-env.d.ts +0 -7
  97. package/kits/rpg-2d/vite.config.js +0 -1
package/dist/index.js CHANGED
@@ -45,14 +45,20 @@ function getWsPort(dir) {
45
45
  }
46
46
  function usage() {
47
47
  console.log(`Usage:
48
- castle-web init <dir> [--kit NAME] (kits: basic-2d (default), rpg-2d, none)
48
+ castle-web init <dir> [--kit NAME] (kits: basic-2d (default), none)
49
49
  castle-web serve [dir] [--port PORT] [--host HOST] [--open] [--detach]
50
50
  castle-web restart [--port PORT]
51
51
  castle-web screenshot [--out FILE] [--port PORT]
52
52
  castle-web save-preview-image [dir] [--port PORT] [--no-restart]
53
53
  castle-web save-deck [dir]
54
54
  castle-web get-deck <dir> [--deck-id ID]
55
- castle-web login`);
55
+ castle-web login
56
+
57
+ Kits:
58
+ basic-2d Actor / behavior / scene framework for 2D games. Includes an editor and Layout / Drawing / Collider / Camera built-ins. Default if --kit is omitted.
59
+ none Minimal vite + canvas setup with the castle-web SDK and no kit.
60
+
61
+ After init, read the scaffolded deck's CLAUDE.md (also AGENTS.md) for the kit's full author guide.`);
56
62
  process.exit(1);
57
63
  }
58
64
  async function main() {
package/dist/init.js CHANGED
@@ -162,7 +162,7 @@ function scaffoldFromKit(kit, projectDir) {
162
162
  if (cliDistAbs) {
163
163
  pkg.scripts[k] = pkg.scripts[k]
164
164
  .replace(/\.\.\/\.\.\/cli\/dist/g, cliDistAbs)
165
- .replace(/\.\.\/\.\.\/sdk/g, sdkIsLocal ? toPosixPath(sdkPath) : '');
165
+ .replace(/\.\.\/\.\.\/sdk/g, toPosixPath(sdkPath));
166
166
  }
167
167
  else {
168
168
  // Globally-installed: route through the `castle-web` binary on PATH.
@@ -12,9 +12,9 @@ Write the smallest game that satisfies what the user asked for. No sound, partic
12
12
 
13
13
  ## Workflow
14
14
 
15
- 1. Edit `behaviors/*.jsx` and `scenes/main.scene`.
16
- 2. `castle-web serve . --detach` (returns the URL, default port 5757). Curl once to confirm.
17
- 3. After every edit: `npm run restart` (no hot reload). Then re-check.
15
+ 1. **Serve first.** `castle-web serve . --detach` immediately (unless a serve is already running -- check `.castle/serve.json`). This makes your work visible to the user from the start.
16
+ 2. **Build incrementally.** Start with a minimal playable core (one mechanic, simplest scene), restart, verify, then add the next piece. Do NOT write the whole game in one shot. The user is watching the served page; demonstrate progress.
17
+ 3. **After every edit:** `npm run restart` (no hot reload). The served page refreshes; keep showing results as you go.
18
18
 
19
19
  Card size is **500 wide × 700 tall** (origin top-left, +y is down).
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"
@@ -1,8 +0,0 @@
1
- {
2
- "printWidth": 100,
3
- "tabWidth": 2,
4
- "singleQuote": true,
5
- "bracketSameLine": true,
6
- "trailingComma": "es5",
7
- "arrowParens": "always"
8
- }
@@ -1,131 +0,0 @@
1
- # basic-2d kit
2
-
3
- Actor / behavior / scene framework on a 500×700 canvas ("card"). To build a game you (a) write behaviors in `behaviors/*.jsx`, (b) place actors that use them in `scenes/*.scene` (plain JSON). Behaviors auto-register via file glob — drop a new file and it's picked up on next reload.
4
-
5
- This kit is plain JavaScript (no TypeScript). Use `.jsx` for files that contain JSX, `.js` otherwise. Don't add types or `.ts`/`.tsx` files.
6
-
7
- DO NOT READ `engine/`, `editors/`, OR the built-in `behaviors/` files (`Layout.jsx`, `Drawing.jsx`, `Collider.jsx`, `Camera.jsx`) to build a game. Those are the framework itself; their complete public API (props, helpers, calling conventions) is documented inline below. Read framework source only if you are explicitly modifying the framework.
8
-
9
- ## Scope
10
-
11
- Write the smallest game that satisfies what the user asked for. No sound, particles, menus, multi-level progression, or visual polish unless they specifically asked for it. A typical behavior is 30–80 lines — if yours is hitting 200, you're over-engineering: cut feel-good extras, fewer fields on props, fewer edge cases, fewer comments. Ship the core loop first; the user can ask for more.
12
-
13
- ## Workflow
14
-
15
- 1. Edit `behaviors/*.jsx` and `scenes/main.scene`.
16
- 2. `castle-web serve . --detach` (returns the URL, default port 5757). Curl once to confirm.
17
- 3. After every edit: `npm run restart` (no hot reload). Then re-check.
18
-
19
- Card size is **500 wide × 700 tall** (origin top-left, +y is down).
20
-
21
- ## Behavior shape
22
-
23
- A behavior is a class. Minimal contract:
24
-
25
- ```jsx
26
- // behaviors/MyThing.jsx
27
- export class MyThing {
28
- static behaviorName = 'MyThing'; // must match the key used in .scene
29
- static defaultProps = { speed: 200 };
30
-
31
- constructor(props) {
32
- this.props = props;
33
- }
34
-
35
- // Called every frame in play mode. dt is seconds.
36
- update(actor, scene, dt) {
37
- const layout = actor.components.Layout;
38
- layout.x += this.props.speed * dt; // mutate component in place
39
- }
40
-
41
- // Optional. Custom drawing (you usually don't need this; use a Drawing
42
- // component instead). ctx is in card units already.
43
- draw(actor, scene, ctx) {}
44
-
45
- // Optional. Return React nodes for game-time HUD. Coordinates are card
46
- // units. Read state your `update` set; do not start your own loops.
47
- ui(actor, scene) {
48
- return null;
49
- }
50
- }
51
- ```
52
-
53
- A fresh `Behavior` instance is constructed per actor per frame from the actor's component props — DO NOT store per-actor state on `this`. Persist transient state on `actor.runtime` (a free-form object the framework will not serialize) or on the component props themselves.
54
-
55
- ## Scene file (`scenes/main.scene`, plain JSON)
56
-
57
- ```json
58
- {
59
- "background": "#1b2030",
60
- "actors": [
61
- {
62
- "id": "paddle",
63
- "components": {
64
- "Layout": { "x": 210, "y": 640, "width": 80, "height": 12 },
65
- "Drawing": { "file": "drawings/floor.drawing" },
66
- "Collider": { "kind": "solid", "width": 80, "height": 12 },
67
- "Paddle": { "speed": 360 }
68
- }
69
- }
70
- ]
71
- }
72
- ```
73
-
74
- Rules: every actor needs a unique `id` (any string) and almost always a `Layout`. `components` keys are behavior names (the `static behaviorName`). Unspecified props fall back to that behavior's `defaultProps` — omit optional fields (`z`, `rotation`, actor `name`, `tint: '#ffffffff'`, scene `name`) to keep scenes compact, especially when generating many actors. Add a behavior to an actor by adding its key to `components`; remove it by deleting the key.
75
-
76
- ## Built-in behaviors
77
-
78
- - **Layout** — `{ x, y, width, height, z?, rotation? }`. Every actor needs one. `z` orders draw (low first). `rotation` is degrees about the center.
79
- - **Drawing** — `{ file: "drawings/foo.drawing", tint?: "#rrggbbaa" }`. Renders the pixel-art file scaled into the Layout box. `tint` multiplies; use white (`#ffffffff`) or omit for the original colors. For a solid-color rectangle, point `file` at `drawings/floor.drawing` (an 8×8 white pixel sprite) and set `tint` to your color.
80
- - **Collider** — `{ kind: 'solid'|'pickup', width, height, offsetX?, offsetY?, debug? }`. Just a rect; the framework does NOT auto-resolve collisions for you. Use it as data:
81
-
82
- ```jsx
83
- for (const other of scene.getActors()) {
84
- if (other.id === actor.id) continue;
85
- if (scene.overlaps(actor, other)) {
86
- /* react */
87
- }
88
- }
89
- ```
90
-
91
- - **Camera** — `{ target: actorId, followX, followY, roomWidth, roomHeight }`. Place on a dedicated actor; sets `scene.camera` clamped to the room. Omit entirely for a fixed view (no camera = no translation).
92
-
93
- ## SceneRuntime API (what `scene` exposes to behaviors)
94
-
95
- - `scene.time` — seconds since start.
96
- - `scene.keys` — `Set` of currently-held KeyboardEvent codes (e.g. `'ArrowLeft'`, `'KeyA'`, `'Space'`). Read in `update`.
97
- - `scene.pointer` — `{ x, y, down }` in world (card) coordinates, camera-adjusted.
98
- - `scene.getActor(id)` / `scene.getActors()` (sorted by Layout.z) / `scene.getComponent(actor, name)`.
99
- - `scene.actorWith('GameController')` / `scene.actorsWith('Brick')` — find one / all actors carrying a given behavior. Prefer these to `getActors().find(a => a.components.X)`.
100
- - `scene.colliderRect(actorOrId)` — rect from Layout + Collider, or null.
101
- - `scene.overlaps(a, b)` — true when two actors/ids with Collider overlap.
102
- - `scene.data` — the live scene data. Mutate `actor.components.X = {...}` to change props.
103
- - `scene.spawnActor({ components: { Layout: {...}, MyBehavior: {...} } })` — add a new actor at runtime. Returns the actor (with auto-minted `id` and `runtime = {}`). Use this; don't push to `scene.data.actors` by hand.
104
- - `scene.despawnActor(id)` — remove an actor at runtime. Use this; don't `splice` + `delete` by hand.
105
- - `scene.status` — string you can set/read for game-state ('playing', 'gameover', ...).
106
- - `actor.runtime` — per-instance scratchpad for transient state across frames (e.g. velocity, trail history). Not serialized.
107
-
108
- ## Input shortcuts
109
-
110
- ```jsx
111
- if (scene.keys.has('ArrowLeft')) layout.x -= speed * dt;
112
- if (scene.keys.has('ArrowRight')) layout.x += speed * dt;
113
- if (scene.keys.has('Space')) /* launch ball */ ;
114
- ```
115
-
116
- For HUD text use a behavior's `ui` hook (returns React); for in-world text or shapes, draw with `ctx` from `draw`. The deck has TouchControls overlay support out of the box for mobile play (arrow keys + a button); no setup needed.
117
-
118
- ## Common breakout-shaped recipe (sketch)
119
-
120
- - `Paddle` behavior: read keys, clamp x to `[0, 500 - layout.width]`.
121
- - `Ball` behavior: store `vx, vy` on `actor.runtime`; integrate; bounce off wall edges (`x<0`, `x+w>500`, `y<0`); on `scene.overlaps(actor, paddle)`, flip `vy`; for each `brick` in `scene.actorsWith('Brick')` check `scene.overlaps(actor, brick)` → flip `vy` and `scene.despawnActor(brick.id)`; if `y > 700` lose a life.
122
- - `Brick` behavior: typically just a marker — `kind: 'solid'` collider is enough. State (hit count) goes on `actor.runtime` or the brick's own props.
123
- - `GameController` (no Layout needed if you don't draw it): tracks score / lives / status; expose HUD via `ui()`.
124
-
125
- ## Don't
126
-
127
- - Don't `console.log` in tight loops — flood the serve log.
128
- - Don't keep per-actor state on the behavior class instance; it's recreated each frame. Use `actor.runtime` or component props.
129
- - Don't try to import from `editors/`; behaviors run in the play runtime too.
130
- - Don't add types or `.ts`/`.tsx` files. This kit is JavaScript.
131
- - Don't add a build step or change `vite.config.js` for a game — it's configured for you.
@@ -1,43 +0,0 @@
1
- import { cardSize } from '../engine/scene';
2
-
3
- // Follow camera: keeps the target actor centered in the viewport, clamped to
4
- // the room bounds so the empty area past the room edges never scrolls in.
5
- export class Camera {
6
- static behaviorName = 'Camera';
7
-
8
- static defaultProps = {
9
- target: '',
10
- followX: true,
11
- followY: true,
12
- roomWidth: 900,
13
- roomHeight: 1100,
14
- };
15
-
16
- constructor(props) {
17
- this.props = props;
18
- }
19
-
20
- update(_actor, scene) {
21
- const target = this.props.target ? scene.getActor(this.props.target) : undefined;
22
- const targetLayout = target?.components.Layout;
23
- if (!targetLayout) return;
24
-
25
- const centerX = targetLayout.x + targetLayout.width / 2;
26
- const centerY = targetLayout.y + targetLayout.height / 2;
27
- const desiredX = centerX - cardSize.width / 2;
28
- const desiredY = centerY - cardSize.height / 2;
29
-
30
- scene.camera = {
31
- x: this.props.followX
32
- ? clamp(desiredX, 0, this.props.roomWidth - cardSize.width)
33
- : 0,
34
- y: this.props.followY
35
- ? clamp(desiredY, 0, this.props.roomHeight - cardSize.height)
36
- : 0,
37
- };
38
- }
39
- }
40
-
41
- function clamp(value, min, max) {
42
- return Math.max(min, Math.min(Math.max(min, max), value));
43
- }
@@ -1,71 +0,0 @@
1
- import React from 'react';
2
- import { Panel, SelectField } from '../engine/ui';
3
- import { AutoFields } from '../engine/autoInspector';
4
-
5
- export class Collider {
6
- static behaviorName = 'Collider';
7
-
8
- static defaultProps = {
9
- kind: 'solid',
10
- width: 32,
11
- height: 32,
12
- offsetX: 0,
13
- offsetY: 0,
14
- debug: false,
15
- };
16
-
17
- constructor(props) {
18
- this.props = props;
19
- }
20
-
21
- draw(actor, _scene, ctx, options) {
22
- if (!options.showDebugColliders && !this.props.debug) return;
23
- const rect = getColliderRect(actor);
24
- if (!rect) return;
25
- ctx.save();
26
- ctx.strokeStyle = this.props.kind === 'pickup' ? '#ffe17a' : '#8db7ff';
27
- ctx.lineWidth = 2;
28
- ctx.strokeRect(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2);
29
- ctx.restore();
30
- }
31
-
32
- static Inspector({ component, setComponent }) {
33
- return (
34
- <Panel title="Collider">
35
- <SelectField
36
- label="Kind"
37
- value={component.kind}
38
- onChange={(kind) => setComponent({ kind: kind })}
39
- options={['solid', 'pickup']}
40
- />
41
- <AutoFields
42
- defaultProps={Collider.defaultProps}
43
- component={component}
44
- setComponent={setComponent}
45
- exclude={['kind']}
46
- />
47
- </Panel>
48
- );
49
- }
50
- }
51
-
52
- export function getColliderRect(actor) {
53
- const layout = actor.components.Layout;
54
- const collider = actor.components.Collider;
55
- if (!layout || !collider) return null;
56
- return {
57
- x: layout.x + (collider.offsetX ?? 0),
58
- y: layout.y + (collider.offsetY ?? 0),
59
- width: collider.width ?? layout.width,
60
- height: collider.height ?? layout.height,
61
- };
62
- }
63
-
64
- export function intersects(a, b) {
65
- return (
66
- a.x < b.x + b.width &&
67
- a.x + a.width > b.x &&
68
- a.y < b.y + b.height &&
69
- a.y + a.height > b.y
70
- );
71
- }
@@ -1,139 +0,0 @@
1
- import React from 'react';
2
- import { Panel, SelectField } from '../engine/ui';
3
- import { AutoFields } from '../engine/autoInspector';
4
-
5
- export class Drawing {
6
- static behaviorName = 'Drawing';
7
-
8
- static defaultProps = {
9
- file: 'drawings/floor.drawing',
10
- tint: '#ffffffff',
11
- };
12
-
13
- constructor(props) {
14
- this.props = props;
15
- }
16
-
17
- draw(actor, scene, ctx) {
18
- if (actor.runtime?.collected) return;
19
- const layout = actor.components.Layout;
20
- if (!layout) return;
21
- const drawing = scene.drawings[this.props.file];
22
- if (!drawing) return;
23
- drawPixelDrawing(
24
- ctx,
25
- drawing,
26
- layout.x,
27
- layout.y,
28
- layout.width,
29
- layout.height,
30
- this.props.tint
31
- );
32
- }
33
-
34
- static Inspector({ component, setComponent, files }) {
35
- const drawingFiles = getDrawingFiles(files);
36
- return (
37
- <Panel title="Drawing">
38
- <SelectField
39
- label="File"
40
- value={component.file}
41
- onChange={(file) => setComponent({ file })}
42
- options={drawingFiles}
43
- />
44
- <AutoFields
45
- defaultProps={Drawing.defaultProps}
46
- component={component}
47
- setComponent={setComponent}
48
- only={['tint']}
49
- />
50
- </Panel>
51
- );
52
- }
53
- }
54
-
55
- export function drawPixelDrawing(ctx, drawing, x, y, width, height, tint) {
56
- if (drawing.width <= 0 || drawing.height <= 0) return;
57
- // Blit the cached native-resolution canvas as a single image. Painting each
58
- // pixel as its own `fillRect` left hairline anti-aliased seams under a
59
- // rotation transform; one `drawImage` has no inter-pixel edges.
60
- const canvas = getDrawingCanvas(drawing, tint);
61
- const prevSmoothing = ctx.imageSmoothingEnabled;
62
- ctx.imageSmoothingEnabled = false;
63
- ctx.drawImage(canvas, x, y, width, height);
64
- ctx.imageSmoothingEnabled = prevSmoothing;
65
- }
66
-
67
- // Cache of native-resolution offscreen canvases keyed by drawing data and
68
- // tint. A drawing edit produces a fresh `DrawingData` object (see
69
- // DrawingEditor's `structuredClone`), so a new object naturally misses the
70
- // WeakMap and rebuilds.
71
- const drawingCanvasCache = new WeakMap();
72
-
73
- // Render the pixel art once, unrotated, at 1px per pixel into an offscreen
74
- // canvas, applying the tint. Cached so the per-frame draw is a single blit.
75
- function getDrawingCanvas(drawing, tint) {
76
- const tintKey = tint ?? '';
77
- let byTint = drawingCanvasCache.get(drawing);
78
- if (!byTint) {
79
- byTint = new Map();
80
- drawingCanvasCache.set(drawing, byTint);
81
- }
82
- const cached = byTint.get(tintKey);
83
- if (cached) return cached;
84
-
85
- const canvas = document.createElement('canvas');
86
- canvas.width = drawing.width;
87
- canvas.height = drawing.height;
88
- const octx = canvas.getContext('2d');
89
- if (octx) {
90
- const tintRgba = parseTint(tint);
91
- for (let py = 0; py < drawing.height; py++) {
92
- for (let px = 0; px < drawing.width; px++) {
93
- const color = drawing.pixels[py * drawing.width + px];
94
- if (!color || color === 'transparent' || color.endsWith('00')) continue;
95
- octx.fillStyle = tintRgba ? applyTint(color, tintRgba) : color;
96
- octx.fillRect(px, py, 1, 1);
97
- }
98
- }
99
- }
100
- byTint.set(tintKey, canvas);
101
- return canvas;
102
- }
103
-
104
- // Parse a hex color into [r, g, b, a] channels (0-255). Returns null when the
105
- // string is not a hex color so callers can fall back to the raw color.
106
- function parseHexColor(color) {
107
- const hex = color.trim().replace(/^#/, '');
108
- if (hex.length !== 6 && hex.length !== 8) return null;
109
- if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
110
- const r = parseInt(hex.slice(0, 2), 16);
111
- const g = parseInt(hex.slice(2, 4), 16);
112
- const b = parseInt(hex.slice(4, 6), 16);
113
- const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) : 255;
114
- return [r, g, b, a];
115
- }
116
-
117
- // Returns the tint as [r, g, b, a] channels, or null when there is no tint or
118
- // the tint is white (#ffffffff) -- both meaning "no change".
119
- function parseTint(tint) {
120
- if (!tint) return null;
121
- const rgba = parseHexColor(tint);
122
- if (!rgba) return null;
123
- if (rgba[0] === 255 && rgba[1] === 255 && rgba[2] === 255 && rgba[3] === 255) {
124
- return null;
125
- }
126
- return rgba;
127
- }
128
-
129
- // Multiply each channel of a pixel color by the tint color (both 0-255).
130
- function applyTint(color, tint) {
131
- const rgba = parseHexColor(color);
132
- if (!rgba) return color;
133
- const out = rgba.map((channel, i) => Math.round((channel * tint[i]) / 255));
134
- return '#' + out.map((channel) => channel.toString(16).padStart(2, '0')).join('');
135
- }
136
-
137
- function getDrawingFiles(files) {
138
- return Object.keys(files).filter((path) => path.endsWith('.drawing'));
139
- }
@@ -1,16 +0,0 @@
1
- export class Layout {
2
- static behaviorName = 'Layout';
3
-
4
- static defaultProps = {
5
- x: 0,
6
- y: 0,
7
- width: 32,
8
- height: 32,
9
- z: 0,
10
- rotation: 0,
11
- };
12
-
13
- constructor(props) {
14
- this.props = props;
15
- }
16
- }
@@ -1,70 +0,0 @@
1
- {
2
- "width": 8,
3
- "height": 8,
4
- "pixels": [
5
- "#ffffffff",
6
- "#ffffffff",
7
- "#ffffffff",
8
- "#ffffffff",
9
- "#ffffffff",
10
- "#ffffffff",
11
- "#ffffffff",
12
- "#ffffffff",
13
- "#ffffffff",
14
- "#d8d8d8ff",
15
- "#d8d8d8ff",
16
- "#d8d8d8ff",
17
- "#d8d8d8ff",
18
- "#d8d8d8ff",
19
- "#d8d8d8ff",
20
- "#ffffffff",
21
- "#ffffffff",
22
- "#d8d8d8ff",
23
- "#ffffffff",
24
- "#ffffffff",
25
- "#ffffffff",
26
- "#ffffffff",
27
- "#d8d8d8ff",
28
- "#ffffffff",
29
- "#ffffffff",
30
- "#d8d8d8ff",
31
- "#ffffffff",
32
- "#ffffffff",
33
- "#ffffffff",
34
- "#ffffffff",
35
- "#d8d8d8ff",
36
- "#ffffffff",
37
- "#ffffffff",
38
- "#d8d8d8ff",
39
- "#ffffffff",
40
- "#ffffffff",
41
- "#ffffffff",
42
- "#ffffffff",
43
- "#d8d8d8ff",
44
- "#ffffffff",
45
- "#ffffffff",
46
- "#d8d8d8ff",
47
- "#ffffffff",
48
- "#ffffffff",
49
- "#ffffffff",
50
- "#ffffffff",
51
- "#d8d8d8ff",
52
- "#ffffffff",
53
- "#ffffffff",
54
- "#d8d8d8ff",
55
- "#d8d8d8ff",
56
- "#d8d8d8ff",
57
- "#d8d8d8ff",
58
- "#d8d8d8ff",
59
- "#d8d8d8ff",
60
- "#ffffffff",
61
- "#ffffffff",
62
- "#ffffffff",
63
- "#ffffffff",
64
- "#ffffffff",
65
- "#ffffffff",
66
- "#ffffffff",
67
- "#ffffffff",
68
- "#ffffffff"
69
- ]
70
- }