Add adaptive coordinate guides to canvas editor
This commit is contained in:
@@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user