初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
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;
|
||||
|
||||
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(savedV2, savedV1 = null) {
|
||||
const current = record(savedV2);
|
||||
const legacy = record(savedV1);
|
||||
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(current?.pixelEditEnabled),
|
||||
normalBrushSize: normalizeBrushSize(
|
||||
current?.normalBrushSize ?? source.brushSize,
|
||||
BRUSH_MODE_NORMAL,
|
||||
),
|
||||
pixelBrushSize: normalizeBrushSize(current?.pixelBrushSize, BRUSH_MODE_PIXEL),
|
||||
};
|
||||
}
|
||||
|
||||
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)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user