New encoding format!!!!
This commit is contained in:
parent
1ee3a05ab9
commit
296f29db0c
3 changed files with 316 additions and 8 deletions
|
|
@ -8,8 +8,6 @@
|
||||||
Shared basics (both modes)
|
Shared basics (both modes)
|
||||||
------------------------- */
|
------------------------- */
|
||||||
|
|
||||||
.grid-widget { /* no container-type here */ }
|
|
||||||
|
|
||||||
/* drawing stack */
|
/* drawing stack */
|
||||||
.grid-widget [data-grid] {
|
.grid-widget [data-grid] {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,312 @@ function initGridWidget(root, opts = {}) {
|
||||||
let ctx;
|
let ctx;
|
||||||
let dpr = 1;
|
let dpr = 1;
|
||||||
|
|
||||||
|
function shortenKeys(shapes) {
|
||||||
|
const keyMap = {
|
||||||
|
type: 't',
|
||||||
|
points: 'p',
|
||||||
|
color: 'cl', // avoid collision with x2
|
||||||
|
strokeWidth: 'sw',
|
||||||
|
strokeOpacity: 'so',
|
||||||
|
fillOpacity: 'fo',
|
||||||
|
fill: 'f',
|
||||||
|
|
||||||
|
x: 'x',
|
||||||
|
y: 'y',
|
||||||
|
w: 'w',
|
||||||
|
h: 'h',
|
||||||
|
|
||||||
|
x1: 'a',
|
||||||
|
y1: 'b',
|
||||||
|
x2: 'c',
|
||||||
|
y2: 'd'
|
||||||
|
};
|
||||||
|
|
||||||
|
return shapes.map((shape) => {
|
||||||
|
const out = {};
|
||||||
|
for (const key of Object.keys(shape)) {
|
||||||
|
const newKey = keyMap[key] || key;
|
||||||
|
out[newKey] = shape[key];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortenShapes(shapes) {
|
||||||
|
const shapeMap = { path: 'p', line: 'l', rect: 'r', ellipse: 'e', stateChange: 's' };
|
||||||
|
return shapes.map(shape => ({
|
||||||
|
...shape,
|
||||||
|
type: shapeMap[shape.type] || shape.type
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapseStateChanges(shapes) {
|
||||||
|
const out = [];
|
||||||
|
let pending = null;
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (pending) out.push(pending);
|
||||||
|
pending = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const shape of shapes) {
|
||||||
|
if (shape.type === "stateChange") {
|
||||||
|
if (!pending) pending = { ...shape };
|
||||||
|
else {
|
||||||
|
for (const [k, v] of Object.entries(shape)) {
|
||||||
|
if (k !== "type") pending[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
flush();
|
||||||
|
out.push(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
flush();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateCode(shapes) {
|
||||||
|
const state = {
|
||||||
|
...SHAPE_DEFAULTS,
|
||||||
|
color: "#000000",
|
||||||
|
fill: false,
|
||||||
|
fillOpacity: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const styleKeys = Object.keys(state);
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
for (const shape of shapes) {
|
||||||
|
const s = { ...shape };
|
||||||
|
const stateChange = {};
|
||||||
|
|
||||||
|
for (const key of styleKeys) {
|
||||||
|
if (!(key in s)) continue;
|
||||||
|
|
||||||
|
if (s[key] !== state[key]) {
|
||||||
|
stateChange[key] = s[key];
|
||||||
|
state[key] = s[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
delete s[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(stateChange).length > 0) {
|
||||||
|
out.push({ type: "stateChange", ...stateChange });
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeDeltas(shapes) {
|
||||||
|
return shapes.map(shape => {
|
||||||
|
if (shape.type === 'stateChange') return shape;
|
||||||
|
|
||||||
|
const s = { ...shape };
|
||||||
|
let points = [];
|
||||||
|
|
||||||
|
if (s.type === 'path') {
|
||||||
|
if (!Array.isArray(s.points) || s.points.length === 0) return s;
|
||||||
|
|
||||||
|
points = [Math.round(s.points[0].x * 100), Math.round(s.points[0].y * 100)];
|
||||||
|
let prev = s.points[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < s.points.length; i++) {
|
||||||
|
const cur = s.points[i];
|
||||||
|
points.push(Math.round((cur.x - prev.x) * 100), Math.round((cur.y - prev.y) * 100));
|
||||||
|
prev = cur;
|
||||||
|
}
|
||||||
|
} else if (s.type === 'line') {
|
||||||
|
points = [
|
||||||
|
Math.round(s.x1 * 100),
|
||||||
|
Math.round(s.y1 * 100),
|
||||||
|
Math.round((s.x2 - s.x1) * 100),
|
||||||
|
Math.round((s.y2 - s.y1) * 100)
|
||||||
|
];
|
||||||
|
delete s.x1; delete s.y1; delete s.x2; delete s.y2;
|
||||||
|
} else if (s.type === 'rect' || s.type === 'ellipse') {
|
||||||
|
points = [
|
||||||
|
Math.round(s.x * 100),
|
||||||
|
Math.round(s.y * 100),
|
||||||
|
Math.round(s.w * 100),
|
||||||
|
Math.round(s.h * 100)
|
||||||
|
];
|
||||||
|
delete s.x; delete s.y; delete s.w; delete s.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
s.points = points;
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeRuns(shapes) {
|
||||||
|
const out = [];
|
||||||
|
let run = null;
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (!run) return;
|
||||||
|
out.push(run);
|
||||||
|
run = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const shape of shapes) {
|
||||||
|
if (shape.type === 'path' || shape.type === 'stateChange') {
|
||||||
|
flush();
|
||||||
|
out.push(shape);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!run) {
|
||||||
|
run = { ...shape, points: [...shape.points] };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape.type === run.type) {
|
||||||
|
run.points.push(...shape.points);
|
||||||
|
} else {
|
||||||
|
flush();
|
||||||
|
run = { ...shape, points: [...shape.points] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flush();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode() {
|
||||||
|
const payload = {
|
||||||
|
v: 1,
|
||||||
|
cs: cellSize,
|
||||||
|
q: 100,
|
||||||
|
d: {
|
||||||
|
cl: "#000000",
|
||||||
|
f: false,
|
||||||
|
sw: 12,
|
||||||
|
so: 100,
|
||||||
|
fo: 100
|
||||||
|
},
|
||||||
|
s: shortenKeys(shortenShapes(encodeRuns(computeDeltas(collapseStateChanges(stateCode(stripCaches(shapes)))))))
|
||||||
|
};
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeLine(arr, q) {
|
||||||
|
const [x1, y1, dx, dy] = arr;
|
||||||
|
const x2 = x1 + dx;
|
||||||
|
const y2 = y1 + dy;
|
||||||
|
return { x1: x1 / q, y1: y1 / q, x2: x2 / q, y2: y2 / q };
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodePath(arr, q) {
|
||||||
|
let x = arr[0], y = arr[1];
|
||||||
|
const pts = [{ x: x / q, y: y / q }];
|
||||||
|
for (let i = 2; i < arr.length; i += 2) {
|
||||||
|
x += arr[i];
|
||||||
|
y += arr[i + 1];
|
||||||
|
pts.push({ x: x / q, y: y / q });
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decode(doc) {
|
||||||
|
const q = Number(doc?.q) || 100;
|
||||||
|
const cs = Number(doc?.cs) || 25;
|
||||||
|
|
||||||
|
const defaults = doc?.d || {};
|
||||||
|
const state = {
|
||||||
|
color: defaults.cl ?? "#000000",
|
||||||
|
fill: !!defaults.f,
|
||||||
|
strokeWidth: (Number(defaults.sw) ?? 12) / 100,
|
||||||
|
strokeOpacity: (Number(defaults.so) ?? 100) / 100,
|
||||||
|
fillOpacity: (Number(defaults.fo) ?? 100) / 100
|
||||||
|
};
|
||||||
|
|
||||||
|
const outShapes = [];
|
||||||
|
|
||||||
|
const applyStateChange = (op) => {
|
||||||
|
if ("cl" in op) state.color = op.cl;
|
||||||
|
if ("f" in op) state.fill = !!op.f;
|
||||||
|
if ("sw" in op) state.strokeWidth = Number(op.sw) / 100;
|
||||||
|
if ("so" in op) state.strokeOpacity = Number(op.so) / 100;
|
||||||
|
if ("fo" in op) state.fillOpacity = Number(op.fo) / 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ops = Array.isArray(doc?.s) ? doc.s : [];
|
||||||
|
for (const op of ops) {
|
||||||
|
if (!op || typeof op !== "object") continue;
|
||||||
|
|
||||||
|
const t = op.t;
|
||||||
|
if (t === "s") {
|
||||||
|
applyStateChange(op);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr = op.p;
|
||||||
|
if (!Array.isArray(arr) || arr.length === 0) continue;
|
||||||
|
|
||||||
|
if (t === "p") {
|
||||||
|
if (arr.length < 2 || (arr.length % 2) !== 0) continue;
|
||||||
|
|
||||||
|
outShapes.push({
|
||||||
|
type: "path",
|
||||||
|
points: decodePath(arr, q),
|
||||||
|
color: state.color,
|
||||||
|
strokeWidth: state.strokeWidth,
|
||||||
|
strokeOpacity: state.strokeOpacity
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((arr.length % 4) !== 0) continue;
|
||||||
|
|
||||||
|
if (t === "l") {
|
||||||
|
for (let i = 0; i < arr.length; i += 4) {
|
||||||
|
const seg = decodeLine(arr.slice(i, i + 4), q);
|
||||||
|
outShapes.push({
|
||||||
|
type: "line",
|
||||||
|
x1: seg.x1, y1: seg.y1, x2: seg.x2, y2: seg.y2,
|
||||||
|
color: state.color,
|
||||||
|
strokeWidth: state.strokeWidth,
|
||||||
|
strokeOpacity: state.strokeOpacity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t === "r" || t === "e") {
|
||||||
|
for (let i = 0; i < arr.length; i += 4) {
|
||||||
|
const x = arr[i] / q;
|
||||||
|
const y = arr[i + 1] / q;
|
||||||
|
const w = arr[i + 2] / q;
|
||||||
|
const h = arr[i + 3] / q;
|
||||||
|
|
||||||
|
outShapes.push({
|
||||||
|
type: (t === "r") ? "rect" : "ellipse",
|
||||||
|
x, y, w, h,
|
||||||
|
color: state.color,
|
||||||
|
fill: state.fill,
|
||||||
|
fillOpacity: state.fillOpacity,
|
||||||
|
strokeWidth: state.strokeWidth,
|
||||||
|
strokeOpacity: state.strokeOpacity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: Number(doc?.v) || 1,
|
||||||
|
cellSize: cs,
|
||||||
|
shapes: outShapes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function clamp01(n, fallback = 1) {
|
function clamp01(n, fallback = 1) {
|
||||||
const x = Number(n);
|
const x = Number(n);
|
||||||
return Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : fallback;
|
return Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : fallback;
|
||||||
|
|
@ -367,7 +673,8 @@ function initGridWidget(root, opts = {}) {
|
||||||
destroy() {
|
destroy() {
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
window.removeEventListener('resize', resizeAndSetupCanvas);
|
window.removeEventListener('resize', resizeAndSetupCanvas);
|
||||||
}
|
},
|
||||||
|
decode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1060,7 +1367,7 @@ function initGridWidget(root, opts = {}) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(reader.result);
|
const data = decode(JSON.parse(reader.result));
|
||||||
|
|
||||||
if (Number.isFinite(Number(data.cellSize)) && Number(data.cellSize) >= 1) {
|
if (Number.isFinite(Number(data.cellSize)) && Number(data.cellSize) >= 1) {
|
||||||
cellSizeEl.value = data.cellSize;
|
cellSizeEl.value = data.cellSize;
|
||||||
|
|
@ -1093,11 +1400,14 @@ function initGridWidget(root, opts = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
exportEl.addEventListener('click', () => {
|
exportEl.addEventListener('click', () => {
|
||||||
|
/*
|
||||||
const payload = {
|
const payload = {
|
||||||
version: 1,
|
version: 1,
|
||||||
cellSize: cellSize,
|
cellSize: cellSize,
|
||||||
shapes: stripCaches(shapes)
|
shapes: stripCaches(shapes)
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
const payload = encode();
|
||||||
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
|
@ -1313,7 +1623,7 @@ function initGridWidget(root, opts = {}) {
|
||||||
if (script?.textContent?.trim()) {
|
if (script?.textContent?.trim()) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(script.textContent);
|
const parsed = JSON.parse(script.textContent);
|
||||||
api.setDoc(parsed);
|
api.setDoc(api.decode(parsed));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("viewer JSON.parse failed:", err, script.textContent);
|
console.error("viewer JSON.parse failed:", err, script.textContent);
|
||||||
}
|
}
|
||||||
|
|
@ -1324,14 +1634,14 @@ function initGridWidget(root, opts = {}) {
|
||||||
if (src) {
|
if (src) {
|
||||||
fetch(src, { credentials: 'same-origin' })
|
fetch(src, { credentials: 'same-origin' })
|
||||||
.then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
|
.then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
|
||||||
.then(doc => api.setDoc(doc))
|
.then(doc => api.setDoc(api.decode(doc)))
|
||||||
.catch(() => { });
|
.catch(() => { });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw = root.dataset.doc;
|
const raw = root.dataset.doc;
|
||||||
if (raw) {
|
if (raw) {
|
||||||
try { api.setDoc(JSON.parse(raw)); } catch { }
|
try { api.setDoc(JSON.parse(api.decode(raw))); } catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue