Files
matrix-screen-controller/核桃派软件源代码/app/static/canvas-tools-model.js
T

209 lines
7.0 KiB
JavaScript

export const BRUSH_MODE_NORMAL = "normal";
export const BRUSH_MODE_PIXEL = "pixel";
export const CANVAS_VIEW_MIN_SCALE = 1;
export const CANVAS_VIEW_MAX_SCALE = 8;
export const CANVAS_UNDO_LIMIT = 50;
export const COORDINATE_GUIDE_STEPS = Object.freeze([1, 2, 4, 8, 16, 32]);
export const COORDINATE_GUIDE_TARGET_SPACING = 48;
const MODE_SPECS = Object.freeze({
[BRUSH_MODE_NORMAL]: Object.freeze({ min: 1, max: 16, presets: Object.freeze([1, 2, 4, 8]) }),
[BRUSH_MODE_PIXEL]: Object.freeze({ min: 1, max: 4, presets: Object.freeze([1, 2, 3, 4]) }),
});
function record(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
export function brushModeSpec(mode) {
return MODE_SPECS[mode === BRUSH_MODE_PIXEL ? BRUSH_MODE_PIXEL : BRUSH_MODE_NORMAL];
}
export function normalizeBrushSize(value, mode) {
const spec = brushModeSpec(mode);
const parsed = Number.parseInt(value, 10);
return Math.max(spec.min, Math.min(spec.max, Number.isFinite(parsed) ? parsed : spec.min));
}
export function restoreCanvasToolState(savedV3, savedV2 = null) {
const current = record(savedV3);
const legacy = record(savedV2);
const source = current || legacy || {};
return {
paintColor: typeof source.paintColor === "string" ? source.paintColor : null,
backgroundColor: typeof source.backgroundColor === "string" ? source.backgroundColor : null,
tool: source.tool === "eraser" ? "eraser" : "brush",
pixelEditEnabled: Boolean(source.pixelEditEnabled),
normalBrushSize: normalizeBrushSize(
source.normalBrushSize ?? source.brushSize,
BRUSH_MODE_NORMAL,
),
pixelBrushSize: normalizeBrushSize(source.pixelBrushSize, BRUSH_MODE_PIXEL),
coordinateGuideEnabled: Boolean(current?.coordinateGuideEnabled),
};
}
export function chooseCoordinateGuideStep(
logicalPixelCssSize,
targetSpacing = COORDINATE_GUIDE_TARGET_SPACING,
) {
const pixelSize = Math.max(0, Number(logicalPixelCssSize) || 0);
const target = Math.max(1, Number(targetSpacing) || COORDINATE_GUIDE_TARGET_SPACING);
return COORDINATE_GUIDE_STEPS.reduce((best, candidate) => (
Math.abs(candidate * pixelSize - target) < Math.abs(best * pixelSize - target)
? candidate
: best
), COORDINATE_GUIDE_STEPS[0]);
}
export function coordinateGuideTicks({
boardStart,
boardSize,
visibleStart = Number.NEGATIVE_INFINITY,
visibleEnd = Number.POSITIVE_INFINITY,
step,
}) {
const start = Number(boardStart) || 0;
const size = Math.max(0, Number(boardSize) || 0);
const interval = COORDINATE_GUIDE_STEPS.includes(step) ? step : COORDINATE_GUIDE_STEPS[0];
const ticks = [];
for (let value = 0; value < 64; value += interval) {
const position = start + (value / 64) * size;
if (position >= visibleStart && position <= visibleEnd) ticks.push({ value, position });
}
return ticks;
}
export function linePoints(from, to) {
const points = [];
let x = from.x;
let y = from.y;
const dx = Math.abs(to.x - from.x);
const sx = from.x < to.x ? 1 : -1;
const dy = -Math.abs(to.y - from.y);
const sy = from.y < to.y ? 1 : -1;
let error = dx + dy;
while (true) {
points.push({ x, y });
if (x === to.x && y === to.y) break;
const doubled = 2 * error;
if (doubled >= dy) {
error += dy;
x += sx;
}
if (doubled <= dx) {
error += dx;
y += sy;
}
}
return points;
}
export function brushStampPoints(centerX, centerY, size, mode, width = 64, height = 64) {
const normalizedMode = mode === BRUSH_MODE_PIXEL ? BRUSH_MODE_PIXEL : BRUSH_MODE_NORMAL;
const diameter = normalizeBrushSize(size, normalizedMode);
const start = -Math.floor((diameter - 1) / 2);
const radius = Math.max(0.25, (diameter - 0.5) / 2);
const shapeCenter = (diameter - 1) / 2;
const points = [];
for (let localY = 0; localY < diameter; localY += 1) {
for (let localX = 0; localX < diameter; localX += 1) {
if (normalizedMode === BRUSH_MODE_NORMAL) {
const normalizedX = (localX - shapeCenter) / radius;
const normalizedY = (localY - shapeCenter) / radius;
if ((normalizedX * normalizedX) + (normalizedY * normalizedY) > 1) continue;
}
const x = centerX + start + localX;
const y = centerY + start + localY;
if (x >= 0 && x < width && y >= 0 && y < height) points.push({ x, y });
}
}
return points;
}
export function pixelBuffersEqual(left, right) {
if (!left || !right || left.length !== right.length) return false;
for (let index = 0; index < left.length; index += 1) {
if (left[index] !== right[index]) return false;
}
return true;
}
export class PixelUndoHistory {
constructor(limit = CANVAS_UNDO_LIMIT) {
this.limit = Math.max(1, Number.isInteger(limit) ? limit : CANVAS_UNDO_LIMIT);
this.entries = [];
}
get size() {
return this.entries.length;
}
record(before, after) {
if (pixelBuffersEqual(before, after)) return false;
this.entries.push(Uint8Array.from(before));
if (this.entries.length > this.limit) this.entries.splice(0, this.entries.length - this.limit);
return true;
}
undo() {
return this.entries.pop() || null;
}
clear() {
this.entries.length = 0;
}
}
export function normalizeCanvasScale(value) {
const scale = Number(value);
if (!Number.isFinite(scale)) return CANVAS_VIEW_MIN_SCALE;
return Math.max(CANVAS_VIEW_MIN_SCALE, Math.min(CANVAS_VIEW_MAX_SCALE, scale));
}
export function clampCanvasView(
view,
{ viewportWidth, viewportHeight, baseSize, minVisible = 48 },
) {
const scale = normalizeCanvasScale(view?.scale);
const width = Math.max(1, Number(baseSize) || 1) * scale;
const height = width;
const visibleX = Math.min(Math.max(1, Number(minVisible) || 1), width, viewportWidth);
const visibleY = Math.min(Math.max(1, Number(minVisible) || 1), height, viewportHeight);
const limitX = Math.max(0, (viewportWidth + width) / 2 - visibleX);
const limitY = Math.max(0, (viewportHeight + height) / 2 - visibleY);
return {
scale,
x: Math.max(-limitX, Math.min(limitX, Number(view?.x) || 0)),
y: Math.max(-limitY, Math.min(limitY, Number(view?.y) || 0)),
};
}
export function zoomCanvasView(view, nextScale, anchor, bounds) {
const scale = normalizeCanvasScale(nextScale);
const previousScale = normalizeCanvasScale(view?.scale);
const centerX = bounds.viewportWidth / 2;
const centerY = bounds.viewportHeight / 2;
const ratio = scale / previousScale;
const x = (anchor.x - centerX) - ratio * (anchor.x - centerX - (Number(view?.x) || 0));
const y = (anchor.y - centerY) - ratio * (anchor.y - centerY - (Number(view?.y) || 0));
return clampCanvasView({ scale, x, y }, bounds);
}
export function clampFloatingButton(position, {
viewportWidth,
viewportHeight,
size = 56,
inset = 8,
}) {
const half = size / 2;
const minX = inset + half;
const maxX = Math.max(minX, viewportWidth - inset - half);
const minY = inset + half;
const maxY = Math.max(minY, viewportHeight - inset - half);
return {
x: Math.max(minX, Math.min(maxX, Number(position?.x) || minX)),
y: Math.max(minY, Math.min(maxY, Number(position?.y) || minY)),
};
}