586 lines
20 KiB
JavaScript
586 lines
20 KiB
JavaScript
import {
|
||
announce,
|
||
createLocalState,
|
||
registerWorkspace,
|
||
setActiveButton,
|
||
} from "../core.js";
|
||
import { refreshColorTrigger, setColorValue } from "../color-picker.js";
|
||
import { hexToRgb, rgbToHex } from "../color-math.js";
|
||
import {
|
||
BRUSH_MODE_NORMAL,
|
||
BRUSH_MODE_PIXEL,
|
||
PixelUndoHistory,
|
||
brushModeSpec,
|
||
brushStampPoints,
|
||
clampCanvasView,
|
||
clampFloatingButton,
|
||
linePoints,
|
||
normalizeBrushSize,
|
||
restoreCanvasToolState,
|
||
zoomCanvasView,
|
||
} from "../canvas-tools-model.js";
|
||
import { setControlReady, setControlUnavailable } from "../interaction-feedback.js";
|
||
import {
|
||
boardPoint,
|
||
commitPixelChanges,
|
||
fillPixelLayer,
|
||
getBoardOverlay,
|
||
getInteractionMode,
|
||
getPixelBuffer,
|
||
renderPixelDraft,
|
||
} from "../scene-board.js";
|
||
|
||
const canvas = getBoardOverlay();
|
||
const localState = createLocalState("canvas-tools", 2);
|
||
const previousLocalState = createLocalState("canvas-tools", 1).load(null);
|
||
const legacyState = createLocalState("canvas", 3).load(null)
|
||
|| createLocalState("canvas", 2).load(null)
|
||
|| {};
|
||
const toolButtons = Array.from(document.querySelectorAll("[data-canvas-tool]"));
|
||
const paintColor = document.getElementById("paint-color");
|
||
const backgroundColor = document.getElementById("background-color");
|
||
const brushSizeInput = document.getElementById("brush-size");
|
||
const brushSizePresets = Array.from(document.querySelectorAll("[data-brush-size]"));
|
||
const pixelEditToggle = document.getElementById("pixel-edit-toggle");
|
||
const pixelEditStatus = document.getElementById("pixel-edit-status");
|
||
const pixelGrid = document.getElementById("pixel-grid");
|
||
const boardStage = document.getElementById("board-stage");
|
||
const canvasWorkbench = document.querySelector(".canvas-workbench");
|
||
const startEditButton = document.getElementById("start-canvas-edit");
|
||
const assistive = document.getElementById("canvas-edit-assistive");
|
||
const assistiveToggle = document.getElementById("canvas-assistive-toggle");
|
||
const assistiveMenu = document.getElementById("canvas-assistive-menu");
|
||
const editorModeHint = document.getElementById("canvas-edit-mode-hint");
|
||
const undoButton = document.getElementById("undo-canvas-edit");
|
||
const exitEditButton = document.getElementById("exit-canvas-edit");
|
||
const moveViewButton = document.getElementById("move-canvas-view");
|
||
const continueEditButton = document.getElementById("continue-canvas-edit");
|
||
const savedTools = restoreCanvasToolState(localState.load(null), previousLocalState || legacyState);
|
||
let tool = savedTools.tool;
|
||
let previousDrawingTool = tool;
|
||
let pixelEditEnabled = savedTools.pixelEditEnabled;
|
||
let normalBrushSize = savedTools.normalBrushSize;
|
||
let pixelBrushSize = savedTools.pixelBrushSize;
|
||
let drawing = false;
|
||
let drawingPointerId = null;
|
||
let lastPoint = null;
|
||
let drawingBefore = null;
|
||
let editorActive = false;
|
||
let editorMode = "draw";
|
||
let view = { scale: 1, x: 0, y: 0 };
|
||
let viewGesture = null;
|
||
let assistiveDrag = null;
|
||
let suppressAssistiveClick = false;
|
||
let assistivePosition = { x: 36, y: window.innerHeight / 2 };
|
||
const viewPointers = new Map();
|
||
const undoHistory = new PixelUndoHistory();
|
||
|
||
paintColor.value = savedTools.paintColor || paintColor.value;
|
||
backgroundColor.value = savedTools.backgroundColor || backgroundColor.value;
|
||
|
||
function brushMode() {
|
||
return pixelEditEnabled ? BRUSH_MODE_PIXEL : BRUSH_MODE_NORMAL;
|
||
}
|
||
|
||
function currentBrushSize() {
|
||
return pixelEditEnabled ? pixelBrushSize : normalBrushSize;
|
||
}
|
||
|
||
function setPixel(x, y, rgb) {
|
||
if (x < 0 || x > 63 || y < 0 || y > 63) return;
|
||
const offset = (y * 64 + x) * 3;
|
||
const pixels = getPixelBuffer();
|
||
pixels[offset] = rgb.r;
|
||
pixels[offset + 1] = rgb.g;
|
||
pixels[offset + 2] = rgb.b;
|
||
}
|
||
|
||
function activeRgb() {
|
||
return tool === "eraser" ? hexToRgb(backgroundColor.value) : hexToRgb(paintColor.value);
|
||
}
|
||
|
||
function stampBrush(centerX, centerY, rgb) {
|
||
brushStampPoints(centerX, centerY, currentBrushSize(), brushMode())
|
||
.forEach((point) => setPixel(point.x, point.y, rgb));
|
||
}
|
||
|
||
function paintLine(from, to) {
|
||
const rgb = activeRgb();
|
||
linePoints(from, to).forEach((point) => stampBrush(point.x, point.y, rgb));
|
||
}
|
||
|
||
function paintEvent(event) {
|
||
const samples = typeof event.getCoalescedEvents === "function" ? event.getCoalescedEvents() : [event];
|
||
for (const sample of samples.length ? samples : [event]) {
|
||
const point = boardPoint(sample);
|
||
paintLine(lastPoint || point, point);
|
||
lastPoint = point;
|
||
}
|
||
renderPixelDraft();
|
||
}
|
||
|
||
function pixelSnapshot() {
|
||
return Uint8Array.from(getPixelBuffer());
|
||
}
|
||
|
||
function updateUndoControl() {
|
||
if (undoHistory.size > 0) setControlReady(undoButton);
|
||
else setControlUnavailable(undoButton, "还没有可以撤销的像素操作。");
|
||
}
|
||
|
||
function clearUndoHistory() {
|
||
undoHistory.clear();
|
||
updateUndoControl();
|
||
}
|
||
|
||
function recordPixelOperation(before) {
|
||
const recorded = undoHistory.record(before, getPixelBuffer());
|
||
updateUndoControl();
|
||
return recorded;
|
||
}
|
||
|
||
function viewportBounds() {
|
||
return {
|
||
viewportWidth: window.innerWidth,
|
||
viewportHeight: window.innerHeight,
|
||
baseSize: Math.max(1, boardStage.offsetWidth),
|
||
minVisible: 48,
|
||
};
|
||
}
|
||
|
||
function applyCanvasView(next = view) {
|
||
view = clampCanvasView(next, viewportBounds());
|
||
boardStage.style.setProperty("--canvas-view-scale", String(view.scale));
|
||
boardStage.style.setProperty("--canvas-view-x", `${view.x}px`);
|
||
boardStage.style.setProperty("--canvas-view-y", `${view.y}px`);
|
||
}
|
||
|
||
function resetCanvasView() {
|
||
view = { scale: 1, x: 0, y: 0 };
|
||
applyCanvasView();
|
||
}
|
||
|
||
function applyAssistivePosition(next = assistivePosition) {
|
||
assistivePosition = clampFloatingButton(next, {
|
||
viewportWidth: window.innerWidth,
|
||
viewportHeight: window.innerHeight,
|
||
size: 56,
|
||
inset: 8,
|
||
});
|
||
assistive.style.setProperty("--assistive-x", `${assistivePosition.x}px`);
|
||
assistive.style.setProperty("--assistive-y", `${assistivePosition.y}px`);
|
||
assistive.dataset.menuHorizontal = assistivePosition.x > window.innerWidth / 2 ? "left" : "right";
|
||
assistive.dataset.menuVertical = assistivePosition.y > window.innerHeight / 2 ? "up" : "down";
|
||
}
|
||
|
||
function resetAssistivePosition() {
|
||
applyAssistivePosition({ x: 36, y: window.innerHeight / 2 });
|
||
}
|
||
|
||
function closeAssistiveMenu({ restoreFocus = false } = {}) {
|
||
assistiveMenu.hidden = true;
|
||
assistiveToggle.setAttribute("aria-expanded", "false");
|
||
assistiveToggle.setAttribute("aria-label", "打开画布编辑菜单");
|
||
if (restoreFocus && editorActive) assistiveToggle.focus();
|
||
}
|
||
|
||
function openAssistiveMenu() {
|
||
applyAssistivePosition();
|
||
assistiveMenu.hidden = false;
|
||
assistiveToggle.setAttribute("aria-expanded", "true");
|
||
assistiveToggle.setAttribute("aria-label", "关闭画布编辑菜单");
|
||
const firstAvailable = assistiveMenu.querySelector('button:not([aria-disabled="true"])');
|
||
firstAvailable?.focus();
|
||
}
|
||
|
||
function toggleAssistiveMenu() {
|
||
if (assistiveMenu.hidden) openAssistiveMenu();
|
||
else closeAssistiveMenu({ restoreFocus: true });
|
||
}
|
||
|
||
function updateEditorModeControls() {
|
||
const moving = editorMode === "move";
|
||
if (editorActive) document.body.dataset.canvasEditorMode = editorMode;
|
||
else delete document.body.dataset.canvasEditorMode;
|
||
editorModeHint.textContent = moving
|
||
? "移动模式:单指拖动画布,双指捏合缩放;桌面可使用滚轮缩放。"
|
||
: "绘画模式:直接在画布上绘制。";
|
||
if (moving) {
|
||
setControlUnavailable(moveViewButton, "当前已经是移动画布视线模式。");
|
||
setControlReady(continueEditButton);
|
||
} else {
|
||
setControlReady(moveViewButton);
|
||
setControlUnavailable(continueEditButton, "当前已经是绘画模式。");
|
||
}
|
||
updateUndoControl();
|
||
}
|
||
|
||
function resetViewGesture() {
|
||
const points = Array.from(viewPointers.values());
|
||
if (points.length >= 2) {
|
||
const [first, second] = points;
|
||
viewGesture = {
|
||
kind: "pinch",
|
||
startView: { ...view },
|
||
startMidpoint: { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 },
|
||
startDistance: Math.max(1, Math.hypot(second.x - first.x, second.y - first.y)),
|
||
};
|
||
} else if (points.length === 1) {
|
||
viewGesture = { kind: "pan", startView: { ...view }, startPoint: { ...points[0] } };
|
||
} else {
|
||
viewGesture = null;
|
||
}
|
||
}
|
||
|
||
function setEditorMode(mode) {
|
||
if (!editorActive) return;
|
||
if (mode === "move") finishDrawing();
|
||
editorMode = mode === "move" ? "move" : "draw";
|
||
viewPointers.clear();
|
||
viewGesture = null;
|
||
updateEditorModeControls();
|
||
closeAssistiveMenu({ restoreFocus: true });
|
||
announce(editorMode === "move"
|
||
? "已进入移动画布视线模式:单指拖动,双指缩放"
|
||
: "已继续编辑画布");
|
||
}
|
||
|
||
function enterCanvasEditor() {
|
||
if (editorActive || getInteractionMode() !== "pixels") return;
|
||
editorActive = true;
|
||
editorMode = "draw";
|
||
canvas.dataset.canvasEditing = "true";
|
||
document.body.classList.add("canvas-edit-active");
|
||
assistive.hidden = false;
|
||
closeAssistiveMenu();
|
||
updateEditorModeControls();
|
||
window.requestAnimationFrame(() => {
|
||
resetCanvasView();
|
||
resetAssistivePosition();
|
||
assistiveToggle.focus();
|
||
});
|
||
announce("已进入全屏画布编辑;左侧悬浮按钮可撤销、移动视线或退出");
|
||
}
|
||
|
||
function exitCanvasEditor({ returnToTools = true } = {}) {
|
||
if (!editorActive) return;
|
||
finishDrawing();
|
||
editorActive = false;
|
||
editorMode = "draw";
|
||
viewPointers.clear();
|
||
viewGesture = null;
|
||
closeAssistiveMenu();
|
||
assistive.hidden = true;
|
||
delete canvas.dataset.canvasEditing;
|
||
document.body.classList.remove("canvas-edit-active");
|
||
delete document.body.dataset.canvasEditorMode;
|
||
boardStage.style.removeProperty("--canvas-view-scale");
|
||
boardStage.style.removeProperty("--canvas-view-x");
|
||
boardStage.style.removeProperty("--canvas-view-y");
|
||
view = { scale: 1, x: 0, y: 0 };
|
||
if (returnToTools) {
|
||
window.requestAnimationFrame(() => {
|
||
canvasWorkbench.scrollIntoView({ block: "start", behavior: "smooth" });
|
||
startEditButton.focus({ preventScroll: true });
|
||
});
|
||
}
|
||
announce("已退出画布编辑,可以重新选择颜色和画笔工具");
|
||
}
|
||
|
||
function undoPixelOperation() {
|
||
const previous = undoHistory.undo();
|
||
if (!previous) {
|
||
updateUndoControl();
|
||
return;
|
||
}
|
||
getPixelBuffer().set(previous);
|
||
commitPixelChanges("pixel-undo");
|
||
updateUndoControl();
|
||
announce("已撤销上一个像素操作");
|
||
}
|
||
|
||
function persistTools() {
|
||
localState.save({
|
||
paintColor: paintColor.value,
|
||
backgroundColor: backgroundColor.value,
|
||
tool,
|
||
pixelEditEnabled,
|
||
normalBrushSize,
|
||
pixelBrushSize,
|
||
});
|
||
}
|
||
|
||
function renderBrushSize() {
|
||
const mode = brushMode();
|
||
const brushSize = currentBrushSize();
|
||
const spec = brushModeSpec(mode);
|
||
brushSizeInput.min = String(spec.min);
|
||
brushSizeInput.max = String(spec.max);
|
||
brushSizeInput.value = String(brushSize);
|
||
document.getElementById("brush-size-value").textContent = pixelEditEnabled
|
||
? `${brushSize} × ${brushSize}`
|
||
: `${brushSize} px`;
|
||
const preview = document.getElementById("brush-size-preview");
|
||
const previewSize = Math.max(4, brushSize * 2);
|
||
preview.style.width = `${previewSize}px`;
|
||
preview.style.height = `${previewSize}px`;
|
||
preview.dataset.shape = pixelEditEnabled ? "square" : "round";
|
||
brushSizePresets.forEach((button, index) => {
|
||
const value = spec.presets[index];
|
||
button.dataset.brushSize = String(value);
|
||
button.textContent = String(value);
|
||
});
|
||
setActiveButton(
|
||
brushSizePresets,
|
||
brushSizePresets.find((button) => Number(button.dataset.brushSize) === brushSize),
|
||
);
|
||
pixelEditToggle.setAttribute("aria-pressed", String(pixelEditEnabled));
|
||
pixelEditToggle.textContent = pixelEditEnabled ? "退出单像素编辑" : "开启单像素编辑";
|
||
pixelEditStatus.hidden = !pixelEditEnabled;
|
||
pixelEditStatus.textContent = pixelEditEnabled
|
||
? `画布已按 64×64 方形像素分格,当前笔尖为 ${brushSize}×${brushSize}。`
|
||
: "";
|
||
}
|
||
|
||
function setBrushSize(value) {
|
||
const normalized = normalizeBrushSize(value, brushMode());
|
||
if (pixelEditEnabled) pixelBrushSize = normalized;
|
||
else normalBrushSize = normalized;
|
||
renderBrushSize();
|
||
persistTools();
|
||
}
|
||
|
||
function updatePixelGridVisibility() {
|
||
pixelGrid.hidden = !(pixelEditEnabled && getInteractionMode() === "pixels");
|
||
}
|
||
|
||
function togglePixelEditMode() {
|
||
pixelEditEnabled = !pixelEditEnabled;
|
||
renderBrushSize();
|
||
updatePixelGridVisibility();
|
||
persistTools();
|
||
announce(pixelEditEnabled
|
||
? `已进入单像素编辑模式:画布已按 64×64 方形像素分格,笔尖为 ${pixelBrushSize}×${pixelBrushSize} 正方形。`
|
||
: `已退出单像素编辑模式:恢复 ${normalBrushSize} px 圆形笔尖。`);
|
||
}
|
||
|
||
function selectTool(button) {
|
||
tool = button.dataset.canvasTool;
|
||
if (tool !== "eyedropper") previousDrawingTool = tool;
|
||
setActiveButton(toolButtons, button);
|
||
persistTools();
|
||
}
|
||
|
||
toolButtons.forEach((button) => button.addEventListener("click", () => selectTool(button)));
|
||
|
||
canvas.addEventListener("pointerdown", (event) => {
|
||
if (!editorActive || getInteractionMode() !== "pixels") return;
|
||
if (editorMode === "move") {
|
||
event.preventDefault();
|
||
viewPointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
||
canvas.setPointerCapture(event.pointerId);
|
||
resetViewGesture();
|
||
return;
|
||
}
|
||
if (drawing) return;
|
||
if (tool === "eyedropper") {
|
||
const point = boardPoint(event);
|
||
const offset = (point.y * 64 + point.x) * 3;
|
||
const pixels = getPixelBuffer();
|
||
setColorValue(paintColor, rgbToHex({
|
||
r: pixels[offset],
|
||
g: pixels[offset + 1],
|
||
b: pixels[offset + 2],
|
||
}), { recordHistory: true });
|
||
tool = previousDrawingTool;
|
||
setActiveButton(toolButtons, toolButtons.find((button) => button.dataset.canvasTool === tool));
|
||
persistTools();
|
||
announce(`已吸取像素层 (${point.x}, ${point.y}) 的颜色`);
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
drawing = true;
|
||
drawingPointerId = event.pointerId;
|
||
drawingBefore = pixelSnapshot();
|
||
lastPoint = boardPoint(event);
|
||
canvas.setPointerCapture(event.pointerId);
|
||
paintEvent(event);
|
||
});
|
||
|
||
canvas.addEventListener("pointermove", (event) => {
|
||
if (editorActive && editorMode === "move" && viewPointers.has(event.pointerId)) {
|
||
event.preventDefault();
|
||
viewPointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
||
const points = Array.from(viewPointers.values());
|
||
if (viewGesture?.kind === "pinch" && points.length >= 2) {
|
||
const [first, second] = points;
|
||
const midpoint = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
|
||
const distance = Math.max(1, Math.hypot(second.x - first.x, second.y - first.y));
|
||
const scaled = zoomCanvasView(
|
||
viewGesture.startView,
|
||
viewGesture.startView.scale * (distance / viewGesture.startDistance),
|
||
viewGesture.startMidpoint,
|
||
viewportBounds(),
|
||
);
|
||
applyCanvasView({
|
||
...scaled,
|
||
x: scaled.x + midpoint.x - viewGesture.startMidpoint.x,
|
||
y: scaled.y + midpoint.y - viewGesture.startMidpoint.y,
|
||
});
|
||
} else if (viewGesture?.kind === "pan" && points.length === 1) {
|
||
applyCanvasView({
|
||
...viewGesture.startView,
|
||
x: viewGesture.startView.x + points[0].x - viewGesture.startPoint.x,
|
||
y: viewGesture.startView.y + points[0].y - viewGesture.startPoint.y,
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
if (drawing && drawingPointerId === event.pointerId && editorActive
|
||
&& editorMode === "draw" && getInteractionMode() === "pixels") paintEvent(event);
|
||
});
|
||
|
||
function finishDrawing() {
|
||
if (!drawing) return;
|
||
drawing = false;
|
||
drawingPointerId = null;
|
||
lastPoint = null;
|
||
const changed = recordPixelOperation(drawingBefore);
|
||
drawingBefore = null;
|
||
if (changed) commitPixelChanges("pixel-draw");
|
||
persistTools();
|
||
}
|
||
|
||
function finishCanvasPointer(event) {
|
||
if (viewPointers.delete(event.pointerId)) {
|
||
resetViewGesture();
|
||
return;
|
||
}
|
||
if (drawingPointerId === event.pointerId) finishDrawing();
|
||
}
|
||
|
||
canvas.addEventListener("pointerup", finishCanvasPointer);
|
||
canvas.addEventListener("pointercancel", finishCanvasPointer);
|
||
canvas.addEventListener("lostpointercapture", finishCanvasPointer);
|
||
canvas.addEventListener("wheel", (event) => {
|
||
if (!editorActive || editorMode !== "move") return;
|
||
event.preventDefault();
|
||
const factor = Math.exp(-event.deltaY * 0.002);
|
||
applyCanvasView(zoomCanvasView(view, view.scale * factor, {
|
||
x: event.clientX,
|
||
y: event.clientY,
|
||
}, viewportBounds()));
|
||
}, { passive: false });
|
||
|
||
paintColor.addEventListener("input", persistTools);
|
||
backgroundColor.addEventListener("input", persistTools);
|
||
brushSizeInput.addEventListener("input", () => setBrushSize(brushSizeInput.value));
|
||
brushSizePresets.forEach((button) => button.addEventListener("click", () => setBrushSize(button.dataset.brushSize)));
|
||
pixelEditToggle.addEventListener("click", togglePixelEditMode);
|
||
document.getElementById("fill-canvas").addEventListener("click", () => {
|
||
const before = pixelSnapshot();
|
||
const rgb = hexToRgb(backgroundColor.value);
|
||
fillPixelLayer([rgb.r, rgb.g, rgb.b]);
|
||
recordPixelOperation(before);
|
||
announce("背景颜色已填充到像素层,文字元素保持不变");
|
||
});
|
||
document.getElementById("clear-canvas").addEventListener("click", () => {
|
||
const before = pixelSnapshot();
|
||
fillPixelLayer([0, 0, 0]);
|
||
recordPixelOperation(before);
|
||
announce("像素层已清空,文字元素保持不变");
|
||
});
|
||
|
||
startEditButton.addEventListener("click", enterCanvasEditor);
|
||
undoButton.addEventListener("click", undoPixelOperation);
|
||
exitEditButton.addEventListener("click", () => exitCanvasEditor());
|
||
moveViewButton.addEventListener("click", () => setEditorMode("move"));
|
||
continueEditButton.addEventListener("click", () => setEditorMode("draw"));
|
||
|
||
assistiveToggle.addEventListener("pointerdown", (event) => {
|
||
if (!editorActive) return;
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
assistiveDrag = {
|
||
pointerId: event.pointerId,
|
||
startX: event.clientX,
|
||
startY: event.clientY,
|
||
origin: { ...assistivePosition },
|
||
moved: false,
|
||
};
|
||
assistiveToggle.setPointerCapture(event.pointerId);
|
||
});
|
||
|
||
assistiveToggle.addEventListener("pointermove", (event) => {
|
||
if (!assistiveDrag || assistiveDrag.pointerId !== event.pointerId) return;
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
const dx = event.clientX - assistiveDrag.startX;
|
||
const dy = event.clientY - assistiveDrag.startY;
|
||
if (Math.hypot(dx, dy) > 6) assistiveDrag.moved = true;
|
||
if (assistiveDrag.moved) {
|
||
closeAssistiveMenu();
|
||
applyAssistivePosition({ x: assistiveDrag.origin.x + dx, y: assistiveDrag.origin.y + dy });
|
||
}
|
||
});
|
||
|
||
function finishAssistiveDrag(event) {
|
||
if (!assistiveDrag || assistiveDrag.pointerId !== event.pointerId) return;
|
||
suppressAssistiveClick = assistiveDrag.moved;
|
||
assistiveDrag = null;
|
||
event.stopPropagation();
|
||
}
|
||
|
||
assistiveToggle.addEventListener("pointerup", finishAssistiveDrag);
|
||
assistiveToggle.addEventListener("pointercancel", finishAssistiveDrag);
|
||
assistiveToggle.addEventListener("lostpointercapture", finishAssistiveDrag);
|
||
assistiveToggle.addEventListener("click", () => {
|
||
if (suppressAssistiveClick) {
|
||
suppressAssistiveClick = false;
|
||
return;
|
||
}
|
||
toggleAssistiveMenu();
|
||
});
|
||
|
||
document.addEventListener("keydown", (event) => {
|
||
if (!editorActive || event.key !== "Escape") return;
|
||
event.preventDefault();
|
||
if (!assistiveMenu.hidden) closeAssistiveMenu({ restoreFocus: true });
|
||
else exitCanvasEditor();
|
||
});
|
||
|
||
window.addEventListener("resize", () => {
|
||
if (!editorActive) return;
|
||
applyCanvasView();
|
||
applyAssistivePosition();
|
||
});
|
||
|
||
document.addEventListener("matrix:composition-applied", clearUndoHistory);
|
||
document.addEventListener("matrix:canvas-history-checkpoint", clearUndoHistory);
|
||
document.addEventListener("matrix:scene-replaced", clearUndoHistory);
|
||
|
||
refreshColorTrigger(paintColor);
|
||
refreshColorTrigger(backgroundColor);
|
||
setActiveButton(toolButtons, toolButtons.find((button) => button.dataset.canvasTool === tool));
|
||
renderBrushSize();
|
||
persistTools();
|
||
updateEditorModeControls();
|
||
|
||
registerWorkspace({
|
||
id: "canvas",
|
||
title: "像素画布",
|
||
elementId: "workspace-canvas",
|
||
order: 30,
|
||
persistence: "local",
|
||
capabilities: ["scene-template-save"],
|
||
onEnter() {
|
||
renderPixelDraft();
|
||
updatePixelGridVisibility();
|
||
},
|
||
onLeave() {
|
||
exitCanvasEditor({ returnToTools: false });
|
||
finishDrawing();
|
||
pixelGrid.hidden = true;
|
||
},
|
||
});
|