Improve Android app UI and controller flows

This commit is contained in:
2026-10-09 16:43:58 +08:00
parent c0e26b97fe
commit 608229a497
90 changed files with 8103 additions and 1133 deletions
@@ -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();}});