Packages
open_fresco
0.1.0
Server-side OG-image / social-card rendering, built on Fresco. A scene model (text, image, shape, button) with fills/gradients, {{slot}} templating, measured word-wrap, anchoring, and responsive sizing — rendered to SVG or PNG (via the optional resvg dependency).
Current section
Files
Jump to
Current section
Files
priv/static/open_fresco.js
// OpenFresco editor hook — the browser half of the server-authoritative
// editor stage (see OpenFresco.Editor).
//
// Model: the server owns layout. The hook reports pointer gestures as
// canvas-space coordinates/deltas; the LiveComponent applies them via
// OpenFresco.Editor.Ops and re-renders the SVG. For responsiveness, a drag
// nudges the selected element's SVG group with a local transform for instant
// feedback, then pushes the final delta on release (authoritative re-render).
//
// Spread window.OpenFrescoHooks into your LiveSocket hooks:
// hooks: { ...window.OpenFrescoHooks, ... }
//
// STATUS: not yet browser-verified — the server-side mutation core is unit
// tested; exercise this hook in a host before relying on it.
(function () {
const OpenFrescoEditor = {
mounted() {
this.canvasW = parseFloat(this.el.dataset.canvasW) || 1200;
this.canvasH = parseFloat(this.el.dataset.canvasH) || 630;
this.drag = null;
this.onDown = (e) => this.pointerDown(e);
this.onMove = (e) => this.pointerMove(e);
this.onUp = (e) => this.pointerUp(e);
this.el.addEventListener("pointerdown", this.onDown);
window.addEventListener("pointermove", this.onMove);
window.addEventListener("pointerup", this.onUp);
this.el.addEventListener("keydown", (e) => this.onKey(e));
this.el.tabIndex = 0;
},
destroyed() {
this.el.removeEventListener("pointerdown", this.onDown);
window.removeEventListener("pointermove", this.onMove);
window.removeEventListener("pointerup", this.onUp);
},
svg() {
return this.el.querySelector("svg");
},
// Client px → canvas px, using the rendered SVG's box vs its viewBox.
toCanvas(clientX, clientY) {
const svg = this.svg();
if (!svg) return { x: 0, y: 0 };
const r = svg.getBoundingClientRect();
const sx = this.canvasW / (r.width || 1);
const sy = this.canvasH / (r.height || 1);
return { x: (clientX - r.left) * sx, y: (clientY - r.top) * sy };
},
// The <g|rect|text|image> carrying data-uuid nearest to the target — the
// server tags top-level elements; fall back to whole-canvas hit-test.
elementAt(target) {
let node = target;
while (node && node !== this.el) {
if (node.dataset && node.dataset.uuid) return node.dataset.uuid;
node = node.parentNode;
}
return null;
},
pointerDown(e) {
const pt = this.toCanvas(e.clientX, e.clientY);
// Ask the server what's under the point (authoritative hit-test),
// and begin a drag from here.
this.pushEventTo(this.el, "editor:select", { x: pt.x, y: pt.y });
this.drag = { startX: pt.x, startY: pt.y, id: this.el.dataset.selected || null, moved: false };
this.el.setPointerCapture && this.el.setPointerCapture(e.pointerId);
},
pointerMove(e) {
if (!this.drag) return;
const pt = this.toCanvas(e.clientX, e.clientY);
const dx = pt.x - this.drag.startX;
const dy = pt.y - this.drag.startY;
if (Math.abs(dx) + Math.abs(dy) > 1) this.drag.moved = true;
// Local visual feedback: nudge the selected element's node. The server
// re-render on release replaces this transform with real geometry.
const id = this.el.dataset.selected;
if (id) {
const node = this.el.querySelector(`[data-uuid="${cssEscape(id)}"]`);
if (node) node.setAttribute("transform", `translate(${dx} ${dy})`);
}
},
pointerUp(e) {
if (!this.drag) return;
const drag = this.drag;
this.drag = null;
const id = this.el.dataset.selected;
if (id && drag.moved) {
const pt = this.toCanvas(e.clientX, e.clientY);
this.pushEventTo(this.el, "editor:move", {
id: id,
dx: pt.x - drag.startX,
dy: pt.y - drag.startY,
});
}
},
onKey(e) {
const id = this.el.dataset.selected;
if (!id) return;
if (e.key === "Delete" || e.key === "Backspace") {
e.preventDefault();
this.pushEventTo(this.el, "editor:delete", { id });
} else if (e.key === "]") {
this.pushEventTo(this.el, "editor:front", { id });
}
},
};
function cssEscape(s) {
return window.CSS && CSS.escape ? CSS.escape(s) : String(s).replace(/"/g, '\\"');
}
window.OpenFrescoHooks = Object.assign({}, window.OpenFrescoHooks, { OpenFrescoEditor });
})();