import { getSelectedLayer, boardExtent, editSnapshot, rememberEdit, editHistorySize, clearEditHistory, undoSceneEdit, setExpandedBoard, setMoveFrameVisible, moveSelectedLayer, previewLayerMove, cancelLayerPreview, clearPixelLayer, subscribeScene } from "../scene-board.js"; 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, chooseCoordinateGuideStep, clampCanvasView, clampFloatingButton, coordinateGuideTicks, 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", 3); const previousLocalState = createLocalState("canvas-tools", 2).load(null) || 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 coordinateGuide = document.getElementById("canvas-coordinate-guide"); const coordinateGuideContext = coordinateGuide.getContext("2d"); 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 coordinateGuideToggle = document.getElementById("toggle-canvas-coordinates"); 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 coordinateGuideEnabled = savedTools.coordinateGuideEnabled; 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 coordinateGuideFrame = null; let assistivePosition = { x: 36, y: window.innerHeight / 2 }; const viewPointers = new Map(); let expandedEnabled=Boolean(localState.load({}).expandedEnabled); let layerDrag=null; const expandedToggle=document.getElementById("toggle-expanded-board"); const moveLayerButton=document.getElementById("move-current-layer"); const exitLayerMoveButton=document.getElementById("exit-layer-move"); const moveControls=document.getElementById("layer-move-controls"); const offsetX=document.getElementById("layer-offset-x"),offsetY=document.getElementById("layer-offset-y"); 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) { const layer=getSelectedLayer(),{origin,size}=boardExtent(),worldX=x-64+layer.x,worldY=y-64+layer.y; if(x<0||x>=192||y<0||y>=192||worldX=origin+size||worldY=origin+size)return; const i=(y*192+x)*4; layer.pixels.set(tool==="eraser"?[0,0,0,0]:[rgb.r,rgb.g,rgb.b,255],i); } function activeRgb() { return hexToRgb(paintColor.value); } function stampBrush(centerX, centerY, rgb) { brushStampPoints(centerX+64-getSelectedLayer().x, centerY+64-getSelectedLayer().y, currentBrushSize(), brushMode(),192,192) .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 {scene:editSnapshot(),pixels:getSelectedLayer().pixels?.slice()};} function updateUndoControl() {if(editHistorySize()>0)setControlReady(undoButton);else setControlUnavailable(undoButton,"还没有可以撤销的编辑操作。");} function clearUndoHistory() {clearEditHistory();updateUndoControl();} function recordPixelOperation(before) { if(!before)return false; const l=getSelectedLayer(),changed=before.pixels?.some((v,i)=>v!==l.pixels[i]); if(changed)l.revision++; if(before.scene!==editSnapshot()){rememberEdit(before.scene);updateUndoControl();return true;}return false; } function viewportBounds() { return { viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, baseSize: Math.max(1, boardStage.offsetWidth), minVisible: 48, }; } function hideCoordinateGuide() { if (coordinateGuideFrame !== null) { window.cancelAnimationFrame(coordinateGuideFrame); coordinateGuideFrame = null; } coordinateGuide.hidden = true; } function renderCoordinateGuide() { coordinateGuideFrame = null; if (!editorActive || !coordinateGuideEnabled || getInteractionMode() !== "pixels") { coordinateGuide.hidden = true; return; } const width = window.innerWidth; const height = window.innerHeight; const ratio = Math.max(1, window.devicePixelRatio || 1); const physicalWidth = Math.max(1, Math.round(width * ratio)); const physicalHeight = Math.max(1, Math.round(height * ratio)); if (coordinateGuide.width !== physicalWidth || coordinateGuide.height !== physicalHeight) { coordinateGuide.width = physicalWidth; coordinateGuide.height = physicalHeight; } coordinateGuide.hidden = false; coordinateGuideContext.setTransform(ratio, 0, 0, ratio, 0, 0); coordinateGuideContext.clearRect(0, 0, width, height); const rect = boardStage.getBoundingClientRect(); const left = Math.max(0, rect.left); const top = Math.max(0, rect.top); const right = Math.min(width, rect.right); const bottom = Math.min(height, rect.bottom); if (right <= left || bottom <= top || rect.width <= 0) return; const extent=boardExtent(); const step = chooseCoordinateGuideStep(rect.width / extent.size); const xTicks = coordinateGuideTicks({ count: extent.size, origin: extent.origin, boardStart: rect.left, boardSize: rect.width, visibleStart: left, visibleEnd: right, step, }); const yTicks = coordinateGuideTicks({ count: extent.size, origin: extent.origin, boardStart: rect.top, boardSize: rect.height, visibleStart: top, visibleEnd: bottom, step, }); const rulerSize = 24; const rulerBottom = Math.min(bottom, top + rulerSize); const rulerRight = Math.min(right, left + rulerSize); coordinateGuideContext.save(); coordinateGuideContext.beginPath(); coordinateGuideContext.rect(left, top, right - left, bottom - top); coordinateGuideContext.clip(); coordinateGuideContext.beginPath(); for (const tick of xTicks) { coordinateGuideContext.moveTo(tick.position, top); coordinateGuideContext.lineTo(tick.position, bottom); } for (const tick of yTicks) { coordinateGuideContext.moveTo(left, tick.position); coordinateGuideContext.lineTo(right, tick.position); } coordinateGuideContext.strokeStyle = "rgba(255, 255, 255, 0.42)"; coordinateGuideContext.lineWidth = 1; coordinateGuideContext.stroke(); coordinateGuideContext.fillStyle = "rgba(255, 255, 255, 0.20)"; coordinateGuideContext.fillRect(left, top, right - left, rulerBottom - top); coordinateGuideContext.fillRect(left, top, rulerRight - left, bottom - top); coordinateGuideContext.fillStyle = "rgba(255, 255, 255, 0.34)"; coordinateGuideContext.fillRect(left, top, rulerRight - left, rulerBottom - top); coordinateGuideContext.beginPath(); for (const tick of xTicks) { coordinateGuideContext.moveTo(tick.position, rulerBottom - 7); coordinateGuideContext.lineTo(tick.position, rulerBottom); } for (const tick of yTicks) { coordinateGuideContext.moveTo(rulerRight - 7, tick.position); coordinateGuideContext.lineTo(rulerRight, tick.position); } coordinateGuideContext.strokeStyle = "rgba(255, 255, 255, 0.88)"; coordinateGuideContext.stroke(); coordinateGuideContext.fillStyle = "rgba(255, 255, 255, 0.96)"; coordinateGuideContext.font = "10px ui-monospace, SFMono-Regular, Consolas, monospace"; coordinateGuideContext.textBaseline = "top"; for (const tick of xTicks) { coordinateGuideContext.fillText(String(tick.value), tick.position + 2, top + 2); } for (const tick of yTicks) { coordinateGuideContext.fillText(String(tick.value), left + 2, tick.position + 2); } coordinateGuideContext.restore(); } function scheduleCoordinateGuideRender() { if (!editorActive || !coordinateGuideEnabled || getInteractionMode() !== "pixels") { hideCoordinateGuide(); return; } coordinateGuide.hidden = false; if (coordinateGuideFrame === null) { coordinateGuideFrame = window.requestAnimationFrame(renderCoordinateGuide); } } function updateCoordinateGuideControl() { coordinateGuideToggle.setAttribute("aria-pressed", String(coordinateGuideEnabled)); coordinateGuideToggle.textContent = coordinateGuideEnabled ? "隐藏坐标系" : "显示坐标系"; } 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`); scheduleCoordinateGuideRender(); } 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`); const room=assistivePosition.y>window.innerHeight/2?assistivePosition.y:window.innerHeight-assistivePosition.y; assistive.style.setProperty("--assistive-menu-height",`${Math.max(120,room+16)}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"; const textEditing = getSelectedLayer().type === "text"; moveLayerButton.setAttribute("aria-pressed",String(editorMode==="layer")); if (editorActive) document.body.dataset.canvasEditorMode = editorMode; else delete document.body.dataset.canvasEditorMode; editorModeHint.textContent = editorMode==="layer"?"移动图层:拖动或使用偏移坐标;黄色框为实际屏幕":moving ? "移动模式:单指拖动画布,双指捏合缩放;桌面可使用滚轮缩放。" : textEditing ? "文字编辑模式:拖动文字或使用缩放手柄。" : "绘画模式:直接在画布上绘制。"; if (moving) { setControlUnavailable(moveViewButton, "当前已经是移动画布视线模式。"); setControlReady(continueEditButton); } else { setControlReady(moveViewButton); if (editorMode === "layer") setControlReady(continueEditButton); else setControlUnavailable(continueEditButton, textEditing ? "当前已经是文字编辑模式。" : "当前已经是绘画模式。"); } 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; const pointerIds = [...viewPointers.keys()]; if (layerDrag) { pointerIds.push(layerDrag.id); layerDrag = null; cancelLayerPreview(); } viewPointers.clear(); viewGesture = null; for (const id of pointerIds) { if (canvas.hasPointerCapture(id)) canvas.releasePointerCapture(id); } if (mode === "move") finishDrawing(); editorMode = ["move","layer"].includes(mode)?mode:"draw"; if (editorMode === "draw") moveControls.hidden = true; setMoveFrameVisible(editorMode==="layer" || !moveControls.hidden); updateEditorModeControls(); closeAssistiveMenu({ restoreFocus: true }); announce(editorMode === "move" ? "已进入移动画布视线模式:单指拖动,双指缩放" : editorMode === "layer" ? "已进入移动当前图层模式:拖动图层或输入偏移" : "已继续编辑画布"); } function enterCanvasEditor() { if (editorActive || getInteractionMode() === "readonly") return; editorActive = true; editorMode = "draw"; canvas.dataset.canvasEditing = "true"; document.body.classList.add("canvas-edit-active"); assistive.hidden = false; setExpandedBoard(expandedEnabled); closeAssistiveMenu(); updateEditorModeControls(); updateCoordinateGuideControl(); window.requestAnimationFrame(() => { resetCanvasView(); resetAssistivePosition(); scheduleCoordinateGuideRender(); assistiveToggle.focus(); }); announce("已进入全屏画布编辑;左侧悬浮按钮可撤销、移动视线或退出"); } function exitCanvasEditor({ returnToTools = true } = {}) { if (!editorActive) return; finishDrawing(); editorActive = false; editorMode = "draw"; viewPointers.clear(); viewGesture = null; closeAssistiveMenu(); assistive.hidden = true; moveControls.hidden=true;setMoveFrameVisible(false);setExpandedBoard(false);cancelLayerPreview();layerDrag=null; hideCoordinateGuide(); 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() {if(undoSceneEdit())announce("已撤销上一个编辑操作");updateUndoControl();} function persistTools() { localState.save({ paintColor: paintColor.value, backgroundColor: backgroundColor.value, tool, pixelEditEnabled, normalBrushSize, pixelBrushSize, coordinateGuideEnabled, expandedEnabled, }); } function toggleCoordinateGuide() { coordinateGuideEnabled = !coordinateGuideEnabled; updateCoordinateGuideControl(); persistTools(); scheduleCoordinateGuideRender(); closeAssistiveMenu({ restoreFocus: true }); announce(coordinateGuideEnabled ? "已显示画布坐标系,标尺和辅助线会随视角缩放" : "已隐藏画布坐标系"); } 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() === "readonly") return; if (editorMode === "layer") { event.preventDefault();const l=getSelectedLayer();layerDrag={id:event.pointerId,start:boardPoint(event,{round:false}),x:l.x,y:l.y};canvas.setPointerCapture(event.pointerId);return; } if (editorMode === "move") { event.preventDefault(); viewPointers.set(event.pointerId, { x: event.clientX, y: event.clientY }); canvas.setPointerCapture(event.pointerId); resetViewGesture(); return; } if (getInteractionMode()!=="pixels" || drawing) return; if (tool === "eyedropper") { const point = boardPoint(event); const l=getSelectedLayer(); const offset = ((point.y+64-l.y)*192+point.x+64-l.x)*4; 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(layerDrag?.id===event.pointerId) {const p=boardPoint(event,{round:false});previewLayerMove(layerDrag.x+p.x-layerDrag.start.x,layerDrag.y+p.y-layerDrag.start.y);return;} 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(layerDrag?.id===event.pointerId) { const drag=layerDrag;layerDrag=null; if(event.type==="pointercancel"||event.type==="lostpointercapture")cancelLayerPreview(); else {const p=boardPoint(event,{round:false});try{moveSelectedLayer(drag.x+p.x-drag.start.x,drag.y+p.y-drag.start.y);}catch(e){cancelLayerPreview();announce(e.message,true);}} syncOffsets();return; } 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); coordinateGuideToggle.addEventListener("click", toggleCoordinateGuide); 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(); clearPixelLayer(); 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")); exitLayerMoveButton.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(); updateCoordinateGuideControl(); 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; hideCoordinateGuide(); }, }); function syncOffsets(){const l=getSelectedLayer();offsetX.value=l.x;offsetY.value=l.y;} function commitOffsets(){try{moveSelectedLayer(Number(offsetX.value),Number(offsetY.value));}catch(e){announce(e.message,true);}syncOffsets();} expandedToggle.setAttribute("aria-pressed",String(expandedEnabled)); expandedToggle.textContent=expandedEnabled?"隐藏扩展画面":"显示扩展画面"; expandedToggle.addEventListener("click",()=>{finishDrawing();expandedEnabled=!expandedEnabled;setExpandedBoard(expandedEnabled);expandedToggle.setAttribute("aria-pressed",String(expandedEnabled));expandedToggle.textContent=expandedEnabled?"隐藏扩展画面":"显示扩展画面";persistTools();updatePixelGridVisibility();applyCanvasView();}); moveLayerButton.addEventListener("click",()=>{finishDrawing();moveControls.hidden=false;syncOffsets();setEditorMode("layer");openAssistiveMenu();}); for(const input of [offsetX,offsetY]){ input.addEventListener("change",commitOffsets); input.addEventListener("keydown",e=>{if(e.key==="Enter"){e.preventDefault();input.blur();}}); } for(const button of document.querySelectorAll("[data-offset]"))button.addEventListener("click",()=>{ const l=getSelectedLayer(),axis=button.parentElement.dataset.axis,delta=Number(button.dataset.offset); try{moveSelectedLayer(l.x+(axis==="x"?delta:0),l.y+(axis==="y"?delta:0));}catch(e){announce(e.message,true);}syncOffsets(); }); document.addEventListener("matrix:edit-history",updateUndoControl); subscribeScene(({reason})=>{if(["selection","layer-move","undo","move-cancel"].includes(reason)){syncOffsets();updatePixelGridVisibility();}});