New encoding format!!!!

This commit is contained in:
Yaro Kasear 2026-01-09 13:48:58 -06:00
parent 1ee3a05ab9
commit 296f29db0c
3 changed files with 316 additions and 8 deletions

View file

@ -8,8 +8,6 @@
Shared basics (both modes)
------------------------- */
.grid-widget { /* no container-type here */ }
/* drawing stack */
.grid-widget [data-grid] {
position: relative;

View file

@ -51,6 +51,312 @@ function initGridWidget(root, opts = {}) {
let ctx;
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) {
const x = Number(n);
return Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : fallback;
@ -367,7 +673,8 @@ function initGridWidget(root, opts = {}) {
destroy() {
ro.disconnect();
window.removeEventListener('resize', resizeAndSetupCanvas);
}
},
decode
};
}
@ -1060,7 +1367,7 @@ function initGridWidget(root, opts = {}) {
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result);
const data = decode(JSON.parse(reader.result));
if (Number.isFinite(Number(data.cellSize)) && Number(data.cellSize) >= 1) {
cellSizeEl.value = data.cellSize;
@ -1093,11 +1400,14 @@ function initGridWidget(root, opts = {}) {
});
exportEl.addEventListener('click', () => {
/*
const payload = {
version: 1,
cellSize: cellSize,
shapes: stripCaches(shapes)
};
*/
const payload = encode();
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
@ -1313,7 +1623,7 @@ function initGridWidget(root, opts = {}) {
if (script?.textContent?.trim()) {
try {
const parsed = JSON.parse(script.textContent);
api.setDoc(parsed);
api.setDoc(api.decode(parsed));
} catch (err) {
console.error("viewer JSON.parse failed:", err, script.textContent);
}
@ -1324,14 +1634,14 @@ function initGridWidget(root, opts = {}) {
if (src) {
fetch(src, { credentials: 'same-origin' })
.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(() => { });
return;
}
const raw = root.dataset.doc;
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