初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,585 @@
|
||||
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;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user