This commit is contained in:
33333-33333 2026-07-09 16:08:11 +09:00
commit f1c3af2b89
49 changed files with 989 additions and 310 deletions

View file

@ -18,18 +18,33 @@
undo: "\u21B6 \u4E00\u3064\u623B\u3059",
clear: "\uD83D\uDDD1 \u5168\u6D88\u3057",
});
const DEFAULT_GRID_SIZE = 20;
function normalizedGridSize(value = DEFAULT_GRID_SIZE) {
return Math.max(10, Math.min(80, Math.round(Number(value || DEFAULT_GRID_SIZE) || DEFAULT_GRID_SIZE)));
}
function snapValue(value, gridSize = DEFAULT_GRID_SIZE) {
const grid = normalizedGridSize(gridSize);
return Math.round((Number(value) || 0) / grid) * grid;
}
function snapPoint(point, gridSize = DEFAULT_GRID_SIZE) {
return { x: snapValue(point?.x, gridSize), y: snapValue(point?.y, gridSize) };
}
function templateSegments(name) {
if (name === "bar") return [[-130, 0, 130, 0]];
if (name === "cross") return [[-118, 0, 118, 0], [0, -90, 0, 90]];
if (name === "bar") return [[-120, 0, 120, 0]];
if (name === "cross") return [[-120, 0, 120, 0], [0, -80, 0, 80]];
if (name === "circle") {
const segments = [];
const n = 18, r = 108;
for (let i = 0; i < n; i += 1) {
const a = i / n * Math.PI * 2;
const b = (i + 1) / n * Math.PI * 2;
segments.push([Math.cos(a) * r, Math.sin(a) * r, Math.cos(b) * r, Math.sin(b) * r]);
}
return segments;
// 24-sided lattice circle. More vertices preserve a round silhouette while
// every endpoint remains on the shared 20px editor grid.
const points = [
[120, 0], [120, 40], [100, 60], [80, 80], [60, 100], [40, 120],
[0, 120], [-40, 120], [-60, 100], [-80, 80], [-100, 60], [-120, 40],
[-120, 0], [-120, -40], [-100, -60], [-80, -80], [-60, -100], [-40, -120],
[0, -120], [40, -120], [60, -100], [80, -80], [100, -60], [120, -40],
];
return points.map((point, index) => {
const next = points[(index + 1) % points.length];
return [point[0], point[1], next[0], next[1]];
});
}
return null;
}
@ -65,23 +80,32 @@
function cleanSegments(segments = [], opts = {}) {
const limit = Math.max(1, Number(opts.limit || 96) || 96);
const bound = Math.max(1, Number(opts.bound || 420) || 420);
const minLength = Math.max(0, Number(opts.minLength || 4) || 4);
const fallback = Array.isArray(opts.fallback) ? opts.fallback : [-78, 0, 78, 0];
const gridSize = normalizedGridSize(opts.gridSize);
const snap = opts.snap !== false;
const minLength = Math.max(gridSize, Number(opts.minLength || gridSize) || gridSize);
const fallback = Array.isArray(opts.fallback) ? opts.fallback : [-80, 0, 80, 0];
const coord = value => {
const raw = snap ? snapValue(value, gridSize) : (Number(value) || 0);
return Math.max(-bound, Math.min(bound, raw));
};
const out = [];
for (const seg of segments || []) {
if (!Array.isArray(seg) || seg.length < 4) continue;
const x1 = Math.max(-bound, Math.min(bound, Number(seg[0]) || 0));
const y1 = Math.max(-bound, Math.min(bound, Number(seg[1]) || 0));
const x2 = Math.max(-bound, Math.min(bound, Number(seg[2]) || 0));
const y2 = Math.max(-bound, Math.min(bound, Number(seg[3]) || 0));
const x1 = coord(seg[0]);
const y1 = coord(seg[1]);
const x2 = coord(seg[2]);
const y2 = coord(seg[3]);
if (Math.hypot(x2 - x1, y2 - y1) >= minLength) out.push([x1, y1, x2, y2]);
if (out.length >= limit) break;
}
if (!out.length) out.push(fallback.slice(0, 4));
if (!out.length) {
const snappedFallback = fallback.slice(0, 4).map(value => coord(value));
out.push(snappedFallback);
}
return out;
}
function drawEditorBase(ctx, canvas, cx = canvas?.width / 2 || 0, cy = canvas?.height / 2 || 0) {
function drawEditorBase(ctx, canvas, cx = canvas?.width / 2 || 0, cy = canvas?.height / 2 || 0, gridSize = DEFAULT_GRID_SIZE) {
if (!ctx || !canvas) return false;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
@ -90,8 +114,9 @@
ctx.translate(cx, cy);
ctx.strokeStyle = "rgba(100,80,60,0.16)";
ctx.lineWidth = 1;
for (let x = -cx; x <= cx; x += 40) { ctx.beginPath(); ctx.moveTo(x, -cy); ctx.lineTo(x, cy); ctx.stroke(); }
for (let y = -cy; y <= cy; y += 40) { ctx.beginPath(); ctx.moveTo(-cx, y); ctx.lineTo(cx, y); ctx.stroke(); }
const grid = normalizedGridSize(gridSize);
for (let x = -cx; x <= cx; x += grid) { ctx.beginPath(); ctx.moveTo(x, -cy); ctx.lineTo(x, cy); ctx.stroke(); }
for (let y = -cy; y <= cy; y += grid) { ctx.beginPath(); ctx.moveTo(-cx, y); ctx.lineTo(cx, y); ctx.stroke(); }
ctx.strokeStyle = "rgba(70,60,50,0.38)";
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(-cx, 0); ctx.lineTo(cx, 0); ctx.moveTo(0, -cy); ctx.lineTo(0, cy); ctx.stroke();
@ -173,19 +198,20 @@
const prefix = opts.prefix || "rotator";
const cx = opts.cx ?? canvas.width / 2;
const cy = opts.cy ?? canvas.height / 2;
const fallback = Array.isArray(opts.fallback) ? opts.fallback : [-78, 0, 78, 0];
const gridSize = normalizedGridSize(opts.gridSize);
const fallback = Array.isArray(opts.fallback) ? opts.fallback : [-80, 0, 80, 0];
let mode = "line";
let segments = (opts.segments || []).map(seg => Array.isArray(seg) ? seg.slice(0, 4) : seg).filter(seg => Array.isArray(seg) && seg.length >= 4);
let segments = cleanSegments(opts.segments || [], { fallback, limit: opts.limit, bound: opts.bound, minLength: opts.minLength, gridSize, snap: true });
let draft = null;
let drawing = false;
const lineWidth = () => Math.max(1, Number(typeof opts.lineWidth === "function" ? opts.lineWidth() : opts.lineWidth || 12) || 12);
const strokeStyle = () => typeof opts.strokeStyle === "function" ? opts.strokeStyle() : opts.strokeStyle;
const clean = () => { segments = cleanSegments(segments, { fallback, limit: opts.limit, bound: opts.bound, minLength: opts.minLength }); };
const toLocal = (e) => canvasLocalPoint(canvas, e, cx, cy);
const clean = () => { segments = cleanSegments(segments, { fallback, limit: opts.limit, bound: opts.bound, minLength: opts.minLength, gridSize, snap: true }); };
const toLocal = (e) => snapPoint(canvasLocalPoint(canvas, e, cx, cy), gridSize);
const draw = () => {
drawEditorBase(ctx, canvas, cx, cy);
drawEditorBase(ctx, canvas, cx, cy, gridSize);
if (typeof opts.drawBeforeSegments === "function") opts.drawBeforeSegments(ctx, { cx, cy, lineWidth: lineWidth(), segments: segments.slice() });
const lw = lineWidth();
drawSegments(ctx, segments, { lineWidth: lw, strokeStyle: strokeStyle() || "rgba(80,150,205,0.92)" });
@ -240,7 +266,7 @@
const last = segments[segments.length - 1];
if (!last) return;
const lx = last[2], ly = last[3];
if (Math.hypot(p.x - lx, p.y - ly) >= 5) {
if (Math.hypot(p.x - lx, p.y - ly) >= gridSize) {
last[2] = p.x; last[3] = p.y;
segments.push([p.x, p.y, p.x, p.y]);
}
@ -257,7 +283,7 @@
if (!draft) return;
if (mode === "line") {
const seg = [draft[0], draft[1], p.x, p.y];
if (Math.hypot(seg[2] - seg[0], seg[3] - seg[1]) >= 4) segments.push(seg);
if (Math.hypot(seg[2] - seg[0], seg[3] - seg[1]) >= gridSize) segments.push(seg);
}
draft = null;
clean();
@ -281,7 +307,7 @@
draw,
getSegments: () => segments.map(seg => seg.slice(0, 4)),
setMode,
setSegments(next = []) { segments = next.map(seg => Array.isArray(seg) ? seg.slice(0, 4) : seg).filter(seg => Array.isArray(seg) && seg.length >= 4); draft = null; draw(); },
setSegments(next = []) { segments = cleanSegments(next, { fallback, limit: opts.limit, bound: opts.bound, minLength: opts.minLength, gridSize, snap: true }); draft = null; draw(); },
};
}
@ -289,5 +315,9 @@
decorateToolbar,
createSegmentEditorController,
syncInputPair,
snapPoint,
cleanSegments,
templateSegments,
gridSize: DEFAULT_GRID_SIZE,
});
})(typeof window !== "undefined" ? window : globalThis);