Files
2026-09-12 04:58:59 +03:00

191 lines
5.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from "react";
import { clone, type Asset } from "../engine/schema.ts";
import { sliceImage, validateImage } from "../engine/two-d.ts";
import { Field2D } from "./Inspector2D.tsx";
export function SpriteEditor({
asset,
save,
}: {
asset: Asset;
save: (a: Asset) => Promise<boolean>;
}) {
const [draft, setDraft] = useState(clone(asset)),
[w, setW] = useState(asset.image!.frames[0].width),
[h, setH] = useState(asset.image!.frames[0].height),
[margin, setMargin] = useState(0),
[spacing, setSpacing] = useState(0),
[selected, setSelected] = useState(0),
[error, setError] = useState("");
const im = draft.image!,
frame = im.frames[selected] || im.frames[0];
const update = (v: any) => setDraft({ ...draft, image: { ...im, ...v } });
const modify = (v: any) =>
update({
frames: im.frames.map((f, i) => (i === selected ? { ...f, ...v } : f)),
});
return (
<div className="sprite-editor">
<div className="sprite-sheet">
<div style={{ position: "relative", width: "100%" }}>
<img
src={draft.uri}
alt={draft.name}
style={{
width: "100%",
display: "block",
imageRendering: im.filter === "nearest" ? "pixelated" : "auto",
}}
/>
{im.frames.map((f, i) => (
<button
key={i}
className={"sprite-rect " + (selected === i ? "selected" : "")}
title={"Кадр " + i}
style={{
left: (f.x / im.width) * 100 + "%",
top: (f.y / im.height) * 100 + "%",
width: (f.width / im.width) * 100 + "%",
height: (f.height / im.height) * 100 + "%",
}}
onClick={() => setSelected(i)}
>
{i}
<i
style={{
left: f.pivot[0] * 100 + "%",
top: f.pivot[1] * 100 + "%",
}}
/>
</button>
))}
</div>
</div>
<div>
<p>
{im.width} × {im.height} · {im.frames.length} кадров
</p>
<Field2D
label="Pixels Per Unit"
value={im.pixelsPerUnit}
min={0.001}
onChange={(pixelsPerUnit) => update({ pixelsPerUnit })}
/>
<label className="property">
Фильтр
<select
value={im.filter}
onChange={(e) => update({ filter: e.target.value })}
>
<option value="nearest">Pixel Art</option>
<option value="linear">Сглаживание</option>
</select>
</label>
<h3>Нарезка по сетке</h3>
<Field2D
label="Ширина кадра"
value={w}
min={1}
step={1}
onChange={setW}
/>
<Field2D
label="Высота кадра"
value={h}
min={1}
step={1}
onChange={setH}
/>
<Field2D
label="Поля"
value={margin}
min={0}
step={1}
onChange={setMargin}
/>
<Field2D
label="Интервал"
value={spacing}
min={0}
step={1}
onChange={setSpacing}
/>
<button
onClick={() => {
try {
update({
frames: sliceImage(im.width, im.height, w, h, margin, spacing),
});
setSelected(0);
setError("");
} catch (e) {
setError(String(e));
}
}}
>
Нарезать
</button>
<h3>Кадр {selected}</h3>
<label className="property">
Имя
<input
value={frame.name}
onChange={(e) => modify({ name: e.target.value })}
/>
</label>
{(["x", "y", "width", "height"] as const).map((k) => (
<Field2D
key={k}
label={k}
value={frame[k]}
step={1}
min={k === "x" || k === "y" ? 0 : 1}
onChange={(v) => modify({ [k]: v })}
/>
))}
{[0, 1].map((i) => (
<Field2D
key={i}
label={"Pivot " + (i ? "Y" : "X")}
value={frame.pivot[i]}
min={0}
onChange={(v) =>
modify({ pivot: frame.pivot.map((x, j) => (j === i ? v : x)) })
}
/>
))}
<button
onClick={() =>
update({
frames: im.frames.map((f) => ({ ...f, pivot: [...frame.pivot] })),
})
}
>
Применить pivot ко всем
</button>
<p className="small-note">
Pivot: 01, от верхнего левого угла. Изменения применяются ко всем
спрайтам ресурса. Если нарезка удаляет используемые кадры, сначала
исправьте клипы и Tilemap.
</p>
{error && <p role="alert">{error}</p>}
<button
className="primary"
onClick={async () => {
try {
validateImage(draft);
if (!(await save(draft)))
setError(
"Не удалось сохранить: проверьте консоль и ссылки на кадры.",
);
} catch (e) {
setError(String(e));
}
}}
>
Сохранить ресурс
</button>
</div>
</div>
);
}