castle-web-cli 0.4.71 → 0.4.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-prompts.js +22 -3
- package/dist/agent.js +731 -313
- package/dist/init.js +1 -1
- package/dist/shell/assets/index-Dfn29Bkt.js +108 -0
- package/dist/shell/assets/{index-CVEnWuGV.css → index-WNbOHPBj.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/dist/vitePlugins.js +3 -2
- package/kits/basic-2d/CLAUDE.md +29 -8
- package/kits/basic-2d/behaviors/Collider.jsx +6 -4
- package/kits/basic-2d/behaviors/Layout.jsx +2 -2
- package/kits/basic-2d/behaviors/Sprite.jsx +210 -0
- package/kits/basic-2d/behaviors/tint.js +47 -0
- package/kits/basic-2d/docs/pxart-format.md +298 -0
- package/kits/basic-2d/drawings/pig.pxart +59 -0
- package/kits/basic-2d/editors/App.jsx +125 -76
- package/kits/basic-2d/editors/CodeEditor.jsx +9 -45
- package/kits/basic-2d/editors/FileBrowser.jsx +234 -47
- package/kits/basic-2d/editors/PlayOnly.jsx +9 -7
- package/kits/basic-2d/editors/PxArtEditor.jsx +662 -0
- package/kits/basic-2d/editors/SceneEditor.jsx +587 -221
- package/kits/basic-2d/editors/SelectionOverlay.jsx +808 -0
- package/kits/basic-2d/editors/SingleEditor.jsx +38 -20
- package/kits/basic-2d/editors/codeTheme.js +135 -0
- package/kits/basic-2d/editors/editorHistory.js +44 -17
- package/kits/basic-2d/editors/inspectorSheet.js +23 -0
- package/kits/basic-2d/editors/pixelCanvas.js +11 -0
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +55 -0
- package/kits/basic-2d/editors/pixelGeometry.js +45 -0
- package/kits/basic-2d/editors/pixelInspector.jsx +416 -0
- package/kits/basic-2d/editors/pxArtEditorModel.js +718 -0
- package/kits/basic-2d/editors/pxArtPlayback.js +92 -0
- package/kits/basic-2d/editors/pxArtTimeline.jsx +752 -0
- package/kits/basic-2d/editors/pxArtTimeline.module.css +506 -0
- package/kits/basic-2d/editors/pxArtTools.js +124 -0
- package/kits/basic-2d/editors/useArtboardFit.js +102 -0
- package/kits/basic-2d/engine/ScenePlayer.jsx +10 -4
- package/kits/basic-2d/engine/SceneUI.jsx +3 -11
- package/kits/basic-2d/engine/assets.js +15 -0
- package/kits/basic-2d/engine/files.js +57 -2
- package/kits/basic-2d/engine/pxart.js +985 -0
- package/kits/basic-2d/engine/scene.js +222 -41
- package/kits/basic-2d/engine/ui.jsx +155 -26
- package/kits/basic-2d/engine/ui.module.css +1280 -344
- package/kits/basic-2d/eslint.config.js +21 -0
- package/kits/basic-2d/index.html +13 -0
- package/kits/basic-2d/package.json +1 -0
- package/kits/basic-2d/pnpm-lock.yaml +5 -5
- package/kits/basic-2d/scenes/main.scene +19 -26
- package/kits/basic-2d/scripts/draw.mjs +121 -0
- package/kits/basic-3d/editors/PlayOnly.jsx +9 -1
- package/kits/basic-3d/engine/ScenePlayer.jsx +7 -1
- package/package.json +1 -1
- package/dist/shell/assets/index-BY21Og40.js +0 -106
- package/kits/basic-2d/behaviors/Drawing.jsx +0 -142
- package/kits/basic-2d/drawings/block.drawing +0 -70
- package/kits/basic-2d/drawings/default.drawing +0 -70
- package/kits/basic-2d/editors/DrawingEditor.jsx +0 -224
|
@@ -1,142 +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/default.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
|
-
// Fall back to default.drawing (a plain block) when the named file does not
|
|
22
|
-
// exist yet -- so a scene can reference drawings/foo.drawing before the
|
|
23
|
-
// drawing task has created it, and it shows a block until the real art lands.
|
|
24
|
-
const drawing = scene.drawings[this.props.file] ?? scene.drawings['drawings/default.drawing'];
|
|
25
|
-
if (!drawing) return;
|
|
26
|
-
drawPixelDrawing(
|
|
27
|
-
ctx,
|
|
28
|
-
drawing,
|
|
29
|
-
layout.x,
|
|
30
|
-
layout.y,
|
|
31
|
-
layout.width,
|
|
32
|
-
layout.height,
|
|
33
|
-
this.props.tint
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
static Inspector({ component, setComponent, files }) {
|
|
38
|
-
const drawingFiles = getDrawingFiles(files);
|
|
39
|
-
return (
|
|
40
|
-
<Panel title="Drawing">
|
|
41
|
-
<SelectField
|
|
42
|
-
label="File"
|
|
43
|
-
value={component.file}
|
|
44
|
-
onChange={(file) => setComponent({ file })}
|
|
45
|
-
options={drawingFiles}
|
|
46
|
-
/>
|
|
47
|
-
<AutoFields
|
|
48
|
-
defaultProps={Drawing.defaultProps}
|
|
49
|
-
component={component}
|
|
50
|
-
setComponent={setComponent}
|
|
51
|
-
only={['tint']}
|
|
52
|
-
/>
|
|
53
|
-
</Panel>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function drawPixelDrawing(ctx, drawing, x, y, width, height, tint) {
|
|
59
|
-
if (drawing.width <= 0 || drawing.height <= 0) return;
|
|
60
|
-
// Blit the cached native-resolution canvas as a single image. Painting each
|
|
61
|
-
// pixel as its own `fillRect` left hairline anti-aliased seams under a
|
|
62
|
-
// rotation transform; one `drawImage` has no inter-pixel edges.
|
|
63
|
-
const canvas = getDrawingCanvas(drawing, tint);
|
|
64
|
-
const prevSmoothing = ctx.imageSmoothingEnabled;
|
|
65
|
-
ctx.imageSmoothingEnabled = false;
|
|
66
|
-
ctx.drawImage(canvas, x, y, width, height);
|
|
67
|
-
ctx.imageSmoothingEnabled = prevSmoothing;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Cache of native-resolution offscreen canvases keyed by drawing data and
|
|
71
|
-
// tint. A drawing edit produces a fresh `DrawingData` object (see
|
|
72
|
-
// DrawingEditor's `structuredClone`), so a new object naturally misses the
|
|
73
|
-
// WeakMap and rebuilds.
|
|
74
|
-
const drawingCanvasCache = new WeakMap();
|
|
75
|
-
|
|
76
|
-
// Render the pixel art once, unrotated, at 1px per pixel into an offscreen
|
|
77
|
-
// canvas, applying the tint. Cached so the per-frame draw is a single blit.
|
|
78
|
-
function getDrawingCanvas(drawing, tint) {
|
|
79
|
-
const tintKey = tint ?? '';
|
|
80
|
-
let byTint = drawingCanvasCache.get(drawing);
|
|
81
|
-
if (!byTint) {
|
|
82
|
-
byTint = new Map();
|
|
83
|
-
drawingCanvasCache.set(drawing, byTint);
|
|
84
|
-
}
|
|
85
|
-
const cached = byTint.get(tintKey);
|
|
86
|
-
if (cached) return cached;
|
|
87
|
-
|
|
88
|
-
const canvas = document.createElement('canvas');
|
|
89
|
-
canvas.width = drawing.width;
|
|
90
|
-
canvas.height = drawing.height;
|
|
91
|
-
const octx = canvas.getContext('2d');
|
|
92
|
-
if (octx) {
|
|
93
|
-
const tintRgba = parseTint(tint);
|
|
94
|
-
for (let py = 0; py < drawing.height; py++) {
|
|
95
|
-
for (let px = 0; px < drawing.width; px++) {
|
|
96
|
-
const color = drawing.pixels[py * drawing.width + px];
|
|
97
|
-
if (!color || color === 'transparent' || color.endsWith('00')) continue;
|
|
98
|
-
octx.fillStyle = tintRgba ? applyTint(color, tintRgba) : color;
|
|
99
|
-
octx.fillRect(px, py, 1, 1);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
byTint.set(tintKey, canvas);
|
|
104
|
-
return canvas;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Parse a hex color into [r, g, b, a] channels (0-255). Returns null when the
|
|
108
|
-
// string is not a hex color so callers can fall back to the raw color.
|
|
109
|
-
function parseHexColor(color) {
|
|
110
|
-
const hex = color.trim().replace(/^#/, '');
|
|
111
|
-
if (hex.length !== 6 && hex.length !== 8) return null;
|
|
112
|
-
if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
|
|
113
|
-
const r = parseInt(hex.slice(0, 2), 16);
|
|
114
|
-
const g = parseInt(hex.slice(2, 4), 16);
|
|
115
|
-
const b = parseInt(hex.slice(4, 6), 16);
|
|
116
|
-
const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) : 255;
|
|
117
|
-
return [r, g, b, a];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Returns the tint as [r, g, b, a] channels, or null when there is no tint or
|
|
121
|
-
// the tint is white (#ffffffff) -- both meaning "no change".
|
|
122
|
-
function parseTint(tint) {
|
|
123
|
-
if (!tint) return null;
|
|
124
|
-
const rgba = parseHexColor(tint);
|
|
125
|
-
if (!rgba) return null;
|
|
126
|
-
if (rgba[0] === 255 && rgba[1] === 255 && rgba[2] === 255 && rgba[3] === 255) {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
return rgba;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Multiply each channel of a pixel color by the tint color (both 0-255).
|
|
133
|
-
function applyTint(color, tint) {
|
|
134
|
-
const rgba = parseHexColor(color);
|
|
135
|
-
if (!rgba) return color;
|
|
136
|
-
const out = rgba.map((channel, i) => Math.round((channel * tint[i]) / 255));
|
|
137
|
-
return '#' + out.map((channel) => channel.toString(16).padStart(2, '0')).join('');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function getDrawingFiles(files) {
|
|
141
|
-
return Object.keys(files).filter((path) => path.endsWith('.drawing'));
|
|
142
|
-
}
|
|
@@ -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
|
-
}
|
|
@@ -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
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { basename, formatJson, parseJsonFile } from '../engine/files';
|
|
3
|
-
import {
|
|
4
|
-
cx,
|
|
5
|
-
EditorBody,
|
|
6
|
-
EditorHeader,
|
|
7
|
-
IconButton,
|
|
8
|
-
NumberField,
|
|
9
|
-
Panel,
|
|
10
|
-
SheetGrabHandle,
|
|
11
|
-
styles,
|
|
12
|
-
theme,
|
|
13
|
-
useMobileSheet,
|
|
14
|
-
} from '../engine/ui';
|
|
15
|
-
import { useEditHistory } from './editorHistory';
|
|
16
|
-
const palette = [
|
|
17
|
-
'#00000000',
|
|
18
|
-
'#000000FF',
|
|
19
|
-
'#050505FF',
|
|
20
|
-
'#111111FF',
|
|
21
|
-
'#242234FF',
|
|
22
|
-
'#444444FF',
|
|
23
|
-
'#777777FF',
|
|
24
|
-
'#ffffffFF',
|
|
25
|
-
'#e3e6ffFF',
|
|
26
|
-
'#8db7ffFF',
|
|
27
|
-
'#ff5d5dFF',
|
|
28
|
-
'#ffe17aFF',
|
|
29
|
-
];
|
|
30
|
-
export function DrawingEditor({ path, text, onChange, onToggleFiles, filesOpen, bare = false }) {
|
|
31
|
-
const canvasRef = useRef(null);
|
|
32
|
-
const strokeRef = useRef({ active: false, recorded: false });
|
|
33
|
-
const [color, setColor] = useState('#ffffffFF');
|
|
34
|
-
const [inspectorOpen, setInspectorOpen] = useState(true);
|
|
35
|
-
const history = useEditHistory(text, onChange);
|
|
36
|
-
const inspectorSheet = useInspectorSheet(inspectorOpen);
|
|
37
|
-
const { value: drawing, error } = parseJsonFile(path, text);
|
|
38
|
-
const isWide = drawing ? drawing.width >= drawing.height : true;
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (!drawing || !canvasRef.current) return;
|
|
41
|
-
const canvas = canvasRef.current;
|
|
42
|
-
const ctx = canvas.getContext('2d');
|
|
43
|
-
if (!ctx) return;
|
|
44
|
-
canvas.width = drawing.width;
|
|
45
|
-
canvas.height = drawing.height;
|
|
46
|
-
ctx.clearRect(0, 0, drawing.width, drawing.height);
|
|
47
|
-
for (let y = 0; y < drawing.height; y++) {
|
|
48
|
-
for (let x = 0; x < drawing.width; x++) {
|
|
49
|
-
const pixel = drawing.pixels[y * drawing.width + x];
|
|
50
|
-
if (!pixel || pixel.endsWith('00')) continue;
|
|
51
|
-
ctx.fillStyle = pixel;
|
|
52
|
-
ctx.fillRect(x, y, 1, 1);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}, [drawing, text]);
|
|
56
|
-
function commitDrawing(nextDrawing) {
|
|
57
|
-
history.commit(formatJson(nextDrawing));
|
|
58
|
-
}
|
|
59
|
-
function resizeDrawing(nextWidth, nextHeight) {
|
|
60
|
-
if (!drawing) return;
|
|
61
|
-
const width = clampInteger(nextWidth, 1, 128);
|
|
62
|
-
const height = clampInteger(nextHeight, 1, 128);
|
|
63
|
-
if (width === drawing.width && height === drawing.height) return;
|
|
64
|
-
const pixels = Array(width * height).fill('#00000000');
|
|
65
|
-
const copiedWidth = Math.min(width, drawing.width);
|
|
66
|
-
const copiedHeight = Math.min(height, drawing.height);
|
|
67
|
-
for (let y = 0; y < copiedHeight; y++) {
|
|
68
|
-
for (let x = 0; x < copiedWidth; x++) {
|
|
69
|
-
pixels[y * width + x] = drawing.pixels[y * drawing.width + x] ?? '#00000000';
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
commitDrawing({ ...drawing, width, height, pixels });
|
|
73
|
-
}
|
|
74
|
-
function paintAt(event) {
|
|
75
|
-
if (!drawing) return;
|
|
76
|
-
const rect = event.currentTarget.getBoundingClientRect();
|
|
77
|
-
const x = Math.floor(((event.clientX - rect.left) / rect.width) * drawing.width);
|
|
78
|
-
const y = Math.floor(((event.clientY - rect.top) / rect.height) * drawing.height);
|
|
79
|
-
if (x < 0 || y < 0 || x >= drawing.width || y >= drawing.height) return;
|
|
80
|
-
if (drawing.pixels[y * drawing.width + x] === color) return;
|
|
81
|
-
const next = structuredClone(drawing);
|
|
82
|
-
next.pixels[y * next.width + x] = color;
|
|
83
|
-
// Record one undo snapshot per pointer stroke (not per painted pixel).
|
|
84
|
-
if (!strokeRef.current.recorded) {
|
|
85
|
-
history.recordSnapshot();
|
|
86
|
-
strokeRef.current.recorded = true;
|
|
87
|
-
}
|
|
88
|
-
onChange(formatJson(next));
|
|
89
|
-
}
|
|
90
|
-
function startPaint(event) {
|
|
91
|
-
strokeRef.current = { active: true, recorded: false };
|
|
92
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
93
|
-
paintAt(event);
|
|
94
|
-
}
|
|
95
|
-
function finishPaint() {
|
|
96
|
-
strokeRef.current = { active: false, recorded: false };
|
|
97
|
-
}
|
|
98
|
-
const canvasStyle = drawing
|
|
99
|
-
? {
|
|
100
|
-
width: isWide ? 'min(70vh, 520px)' : 'auto',
|
|
101
|
-
height: isWide ? 'auto' : 'min(70vh, 520px)',
|
|
102
|
-
}
|
|
103
|
-
: undefined;
|
|
104
|
-
const sharedActionButtons = (
|
|
105
|
-
<>
|
|
106
|
-
<IconButton icon="undo" label="Undo" onClick={history.undo} disabled={!history.canUndo} />
|
|
107
|
-
<IconButton icon="redo" label="Redo" onClick={history.redo} disabled={!history.canRedo} />
|
|
108
|
-
</>
|
|
109
|
-
);
|
|
110
|
-
return (
|
|
111
|
-
<>
|
|
112
|
-
{bare ? null : (
|
|
113
|
-
<EditorHeader
|
|
114
|
-
title={basename(path)}
|
|
115
|
-
subtitle={drawing ? `${drawing.width} x ${drawing.height} pixels` : error}
|
|
116
|
-
right={
|
|
117
|
-
<span className={styles.mobileOnly}>
|
|
118
|
-
{sharedActionButtons}
|
|
119
|
-
<IconButton
|
|
120
|
-
icon="palette"
|
|
121
|
-
label="Tools"
|
|
122
|
-
active={inspectorOpen}
|
|
123
|
-
onClick={() => setInspectorOpen((previous) => !previous)}
|
|
124
|
-
/>
|
|
125
|
-
</span>
|
|
126
|
-
}
|
|
127
|
-
onToggleFiles={onToggleFiles}
|
|
128
|
-
filesOpen={filesOpen}
|
|
129
|
-
/>
|
|
130
|
-
)}
|
|
131
|
-
<EditorBody>
|
|
132
|
-
<div className={styles.drawingEditor}>
|
|
133
|
-
<div className={styles.drawingTools}>{sharedActionButtons}</div>
|
|
134
|
-
<div className={styles.drawingCanvasWrap}>
|
|
135
|
-
<canvas
|
|
136
|
-
ref={canvasRef}
|
|
137
|
-
className={styles.drawingCanvas}
|
|
138
|
-
style={canvasStyle}
|
|
139
|
-
onPointerDown={startPaint}
|
|
140
|
-
onPointerMove={(event) => {
|
|
141
|
-
if (strokeRef.current.active && event.buttons === 1) paintAt(event);
|
|
142
|
-
}}
|
|
143
|
-
onPointerUp={finishPaint}
|
|
144
|
-
onPointerCancel={finishPaint}
|
|
145
|
-
/>
|
|
146
|
-
</div>
|
|
147
|
-
<DrawingInspector
|
|
148
|
-
sheet={inspectorSheet}
|
|
149
|
-
drawing={drawing}
|
|
150
|
-
color={color}
|
|
151
|
-
onSelectColor={setColor}
|
|
152
|
-
onResize={resizeDrawing}
|
|
153
|
-
/>
|
|
154
|
-
</div>
|
|
155
|
-
</EditorBody>
|
|
156
|
-
</>
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
function useInspectorSheet(inspectorOpen) {
|
|
160
|
-
const [snap, setSnap] = useState('high');
|
|
161
|
-
useEffect(() => {
|
|
162
|
-
if (inspectorOpen) setSnap('high');
|
|
163
|
-
}, [inspectorOpen]);
|
|
164
|
-
const effectiveSnap = inspectorOpen ? snap : 'hidden';
|
|
165
|
-
return useMobileSheet({
|
|
166
|
-
snap: effectiveSnap,
|
|
167
|
-
baseClassName: styles.inspector,
|
|
168
|
-
onTransition: (direction) => {
|
|
169
|
-
if (!inspectorOpen) return;
|
|
170
|
-
if (direction === 'tap') setSnap((previous) => (previous === 'high' ? 'low' : 'high'));
|
|
171
|
-
else if (direction === 'down') setSnap('low');
|
|
172
|
-
else if (direction === 'up') setSnap('high');
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
function DrawingInspector({ sheet, drawing, color, onSelectColor, onResize }) {
|
|
177
|
-
return (
|
|
178
|
-
<aside {...sheet.rootProps} className={cx(sheet.rootProps.className, styles.inspectorAuto)}>
|
|
179
|
-
<div {...sheet.grabProps}>
|
|
180
|
-
<SheetGrabHandle
|
|
181
|
-
label="Inspector"
|
|
182
|
-
hint={drawing ? `${drawing.width}x${drawing.height} · color` : 'drawing tools'}
|
|
183
|
-
/>
|
|
184
|
-
</div>
|
|
185
|
-
<div className={cx(styles.sheetBody, styles.inspectorBody)}>
|
|
186
|
-
<Panel title="Size">
|
|
187
|
-
<NumberField
|
|
188
|
-
label="Width"
|
|
189
|
-
value={drawing?.width ?? 1}
|
|
190
|
-
min={1}
|
|
191
|
-
max={128}
|
|
192
|
-
onChange={(width) => onResize(width, drawing?.height ?? 1)}
|
|
193
|
-
/>
|
|
194
|
-
<NumberField
|
|
195
|
-
label="Height"
|
|
196
|
-
value={drawing?.height ?? 1}
|
|
197
|
-
min={1}
|
|
198
|
-
max={128}
|
|
199
|
-
onChange={(height) => onResize(drawing?.width ?? 1, height)}
|
|
200
|
-
/>
|
|
201
|
-
</Panel>
|
|
202
|
-
<Panel title="Palette">
|
|
203
|
-
<div className={styles.palette}>
|
|
204
|
-
{palette.map((swatch) => (
|
|
205
|
-
<button
|
|
206
|
-
key={swatch}
|
|
207
|
-
className={cx(styles.swatch, color === swatch && styles.swatchSelected)}
|
|
208
|
-
title={swatch}
|
|
209
|
-
style={{
|
|
210
|
-
background: swatch.endsWith('00') ? theme.transparentChecker : swatch,
|
|
211
|
-
}}
|
|
212
|
-
onClick={() => onSelectColor(swatch)}
|
|
213
|
-
/>
|
|
214
|
-
))}
|
|
215
|
-
</div>
|
|
216
|
-
</Panel>
|
|
217
|
-
</div>
|
|
218
|
-
</aside>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
function clampInteger(value, min, max) {
|
|
222
|
-
if (!Number.isFinite(value)) return min;
|
|
223
|
-
return Math.max(min, Math.min(max, Math.round(value)));
|
|
224
|
-
}
|