Improve Android app UI and controller flows
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { getSelectedLayer, boardExtent, editSnapshot, rememberEdit, editHistorySize, clearEditHistory, undoSceneEdit, setExpandedBoard, setMoveFrameVisible, moveSelectedLayer, previewLayerMove, cancelLayerPreview, clearPixelLayer, subscribeScene } from "../scene-board.js";
|
||||
import {
|
||||
announce,
|
||||
createLocalState,
|
||||
@@ -81,7 +82,13 @@ let suppressAssistiveClick = false;
|
||||
let coordinateGuideFrame = null;
|
||||
let assistivePosition = { x: 36, y: window.innerHeight / 2 };
|
||||
const viewPointers = new Map();
|
||||
const undoHistory = new PixelUndoHistory();
|
||||
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;
|
||||
@@ -94,21 +101,19 @@ 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 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||worldX>=origin+size||worldY<origin||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 tool === "eraser" ? hexToRgb(backgroundColor.value) : hexToRgb(paintColor.value);
|
||||
return hexToRgb(paintColor.value);
|
||||
}
|
||||
|
||||
function stampBrush(centerX, centerY, rgb) {
|
||||
brushStampPoints(centerX, centerY, currentBrushSize(), brushMode())
|
||||
brushStampPoints(centerX+64-getSelectedLayer().x, centerY+64-getSelectedLayer().y, currentBrushSize(), brushMode(),192,192)
|
||||
.forEach((point) => setPixel(point.x, point.y, rgb));
|
||||
}
|
||||
|
||||
@@ -127,24 +132,14 @@ function paintEvent(event) {
|
||||
renderPixelDraft();
|
||||
}
|
||||
|
||||
function pixelSnapshot() {
|
||||
return Uint8Array.from(getPixelBuffer());
|
||||
}
|
||||
|
||||
function updateUndoControl() {
|
||||
if (undoHistory.size > 0) setControlReady(undoButton);
|
||||
else setControlUnavailable(undoButton, "还没有可以撤销的像素操作。");
|
||||
}
|
||||
|
||||
function clearUndoHistory() {
|
||||
undoHistory.clear();
|
||||
updateUndoControl();
|
||||
}
|
||||
|
||||
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) {
|
||||
const recorded = undoHistory.record(before, getPixelBuffer());
|
||||
updateUndoControl();
|
||||
return recorded;
|
||||
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() {
|
||||
@@ -191,8 +186,10 @@ function renderCoordinateGuide() {
|
||||
const bottom = Math.min(height, rect.bottom);
|
||||
if (right <= left || bottom <= top || rect.width <= 0) return;
|
||||
|
||||
const step = chooseCoordinateGuideStep(rect.width / 64);
|
||||
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,
|
||||
@@ -200,6 +197,7 @@ function renderCoordinateGuide() {
|
||||
step,
|
||||
});
|
||||
const yTicks = coordinateGuideTicks({
|
||||
count: extent.size, origin: extent.origin,
|
||||
boardStart: rect.top,
|
||||
boardSize: rect.height,
|
||||
visibleStart: top,
|
||||
@@ -296,6 +294,8 @@ function applyAssistivePosition(next = assistivePosition) {
|
||||
});
|
||||
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";
|
||||
}
|
||||
@@ -327,17 +327,20 @@ function toggleAssistiveMenu() {
|
||||
|
||||
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 = moving
|
||||
editorModeHint.textContent = editorMode==="layer"?"移动图层:拖动或使用偏移坐标;黄色框为实际屏幕":moving
|
||||
? "移动模式:单指拖动画布,双指捏合缩放;桌面可使用滚轮缩放。"
|
||||
: "绘画模式:直接在画布上绘制。";
|
||||
: textEditing ? "文字编辑模式:拖动文字或使用缩放手柄。" : "绘画模式:直接在画布上绘制。";
|
||||
if (moving) {
|
||||
setControlUnavailable(moveViewButton, "当前已经是移动画布视线模式。");
|
||||
setControlReady(continueEditButton);
|
||||
} else {
|
||||
setControlReady(moveViewButton);
|
||||
setControlUnavailable(continueEditButton, "当前已经是绘画模式。");
|
||||
if (editorMode === "layer") setControlReady(continueEditButton);
|
||||
else setControlUnavailable(continueEditButton, textEditing ? "当前已经是文字编辑模式。" : "当前已经是绘画模式。");
|
||||
}
|
||||
updateUndoControl();
|
||||
}
|
||||
@@ -361,24 +364,36 @@ function resetViewGesture() {
|
||||
|
||||
function setEditorMode(mode) {
|
||||
if (!editorActive) return;
|
||||
if (mode === "move") finishDrawing();
|
||||
editorMode = mode === "move" ? "move" : "draw";
|
||||
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() !== "pixels") return;
|
||||
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();
|
||||
@@ -400,6 +415,7 @@ function exitCanvasEditor({ returnToTools = true } = {}) {
|
||||
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");
|
||||
@@ -417,17 +433,7 @@ function exitCanvasEditor({ returnToTools = true } = {}) {
|
||||
announce("已退出画布编辑,可以重新选择颜色和画笔工具");
|
||||
}
|
||||
|
||||
function undoPixelOperation() {
|
||||
const previous = undoHistory.undo();
|
||||
if (!previous) {
|
||||
updateUndoControl();
|
||||
return;
|
||||
}
|
||||
getPixelBuffer().set(previous);
|
||||
commitPixelChanges("pixel-undo");
|
||||
updateUndoControl();
|
||||
announce("已撤销上一个像素操作");
|
||||
}
|
||||
function undoPixelOperation() {if(undoSceneEdit())announce("已撤销上一个编辑操作");updateUndoControl();}
|
||||
|
||||
function persistTools() {
|
||||
localState.save({
|
||||
@@ -438,6 +444,7 @@ function persistTools() {
|
||||
normalBrushSize,
|
||||
pixelBrushSize,
|
||||
coordinateGuideEnabled,
|
||||
expandedEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -516,7 +523,10 @@ function selectTool(button) {
|
||||
toolButtons.forEach((button) => button.addEventListener("click", () => selectTool(button)));
|
||||
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
if (!editorActive || getInteractionMode() !== "pixels") return;
|
||||
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 });
|
||||
@@ -524,10 +534,11 @@ canvas.addEventListener("pointerdown", (event) => {
|
||||
resetViewGesture();
|
||||
return;
|
||||
}
|
||||
if (drawing) return;
|
||||
if (getInteractionMode()!=="pixels" || drawing) return;
|
||||
if (tool === "eyedropper") {
|
||||
const point = boardPoint(event);
|
||||
const offset = (point.y * 64 + point.x) * 3;
|
||||
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],
|
||||
@@ -550,6 +561,7 @@ canvas.addEventListener("pointerdown", (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 });
|
||||
@@ -594,6 +606,12 @@ function finishDrawing() {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -625,13 +643,13 @@ document.getElementById("fill-canvas").addEventListener("click", () => {
|
||||
const rgb = hexToRgb(backgroundColor.value);
|
||||
fillPixelLayer([rgb.r, rgb.g, rgb.b]);
|
||||
recordPixelOperation(before);
|
||||
announce("背景颜色已填充到像素层,文字元素保持不变");
|
||||
announce("已开启当前画笔图层的背景");
|
||||
});
|
||||
document.getElementById("clear-canvas").addEventListener("click", () => {
|
||||
const before = pixelSnapshot();
|
||||
fillPixelLayer([0, 0, 0]);
|
||||
clearPixelLayer();
|
||||
recordPixelOperation(before);
|
||||
announce("像素层已清空,文字元素保持不变");
|
||||
announce("当前图层笔迹已清空,背景保持不变");
|
||||
});
|
||||
|
||||
startEditButton.addEventListener("click", enterCanvasEditor);
|
||||
@@ -639,6 +657,7 @@ 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;
|
||||
@@ -728,3 +747,20 @@ registerWorkspace({
|
||||
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();}});
|
||||
|
||||
Reference in New Issue
Block a user