Add adaptive coordinate guides to canvas editor

This commit is contained in:
2026-09-10 17:48:04 +08:00
parent 8d368de3b5
commit a493fe1757
9 changed files with 292 additions and 13 deletions
@@ -12,8 +12,10 @@ import {
PixelUndoHistory,
brushModeSpec,
brushStampPoints,
chooseCoordinateGuideStep,
clampCanvasView,
clampFloatingButton,
coordinateGuideTicks,
linePoints,
normalizeBrushSize,
restoreCanvasToolState,
@@ -31,8 +33,9 @@ import {
} from "../scene-board.js";
const canvas = getBoardOverlay();
const localState = createLocalState("canvas-tools", 2);
const previousLocalState = createLocalState("canvas-tools", 1).load(null);
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)
|| {};
@@ -45,12 +48,15 @@ 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");
@@ -61,6 +67,7 @@ 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;
@@ -71,6 +78,7 @@ 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();
const undoHistory = new PixelUndoHistory();
@@ -148,11 +156,130 @@ function viewportBounds() {
};
}
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 step = chooseCoordinateGuideStep(rect.width / 64);
const xTicks = coordinateGuideTicks({
boardStart: rect.left,
boardSize: rect.width,
visibleStart: left,
visibleEnd: right,
step,
});
const yTicks = coordinateGuideTicks({
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() {
@@ -254,9 +381,11 @@ function enterCanvasEditor() {
assistive.hidden = false;
closeAssistiveMenu();
updateEditorModeControls();
updateCoordinateGuideControl();
window.requestAnimationFrame(() => {
resetCanvasView();
resetAssistivePosition();
scheduleCoordinateGuideRender();
assistiveToggle.focus();
});
announce("已进入全屏画布编辑;左侧悬浮按钮可撤销、移动视线或退出");
@@ -271,6 +400,7 @@ function exitCanvasEditor({ returnToTools = true } = {}) {
viewGesture = null;
closeAssistiveMenu();
assistive.hidden = true;
hideCoordinateGuide();
delete canvas.dataset.canvasEditing;
document.body.classList.remove("canvas-edit-active");
delete document.body.dataset.canvasEditorMode;
@@ -307,9 +437,21 @@ function persistTools() {
pixelEditEnabled,
normalBrushSize,
pixelBrushSize,
coordinateGuideEnabled,
});
}
function toggleCoordinateGuide() {
coordinateGuideEnabled = !coordinateGuideEnabled;
updateCoordinateGuideControl();
persistTools();
scheduleCoordinateGuideRender();
closeAssistiveMenu({ restoreFocus: true });
announce(coordinateGuideEnabled
? "已显示画布坐标系,标尺和辅助线会随视角缩放"
: "已隐藏画布坐标系");
}
function renderBrushSize() {
const mode = brushMode();
const brushSize = currentBrushSize();
@@ -477,6 +619,7 @@ 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);
@@ -563,6 +706,7 @@ refreshColorTrigger(paintColor);
refreshColorTrigger(backgroundColor);
setActiveButton(toolButtons, toolButtons.find((button) => button.dataset.canvasTool === tool));
renderBrushSize();
updateCoordinateGuideControl();
persistTools();
updateEditorModeControls();
@@ -581,5 +725,6 @@ registerWorkspace({
exitCanvasEditor({ returnToTools: false });
finishDrawing();
pixelGrid.hidden = true;
hideCoordinateGuide();
},
});