🎉 Add Text Editor WASM Playground

This commit is contained in:
Aitor Moreno
2025-08-19 16:10:10 +02:00
committed by Elena Torro
parent b12d44150b
commit 15eee0d8d8
6 changed files with 1049 additions and 1 deletions

View File

@@ -340,3 +340,5 @@ $RECYCLE.BIN/
/playwright/.cache/
vite.config.js.timestamp*
render_wasm.*

View File

@@ -7,6 +7,7 @@
"scripts": {
"dev": "vite",
"coverage": "vitest run --coverage",
"wasm:update": "cp ../resources/public/js/render_wasm.wasm ./src/wasm/render_wasm.wasm && cp ../resources/public/js/render_wasm.js ./src/wasm/render_wasm.js",
"test": "vitest --run",
"test:watch": "vitest",
"test:watch:ui": "vitest --ui",

View File

@@ -0,0 +1,502 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Playwrite+ES:wght@100..400&family=Playwrite+NZ:wght@100..400&family=Playwrite+US+Trad:wght@100..400&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Penpot - Text Editor Playground</title>
<style>
#output {
white-space: pre-wrap;
}
</style>
</head>
<body>
<form>
<fieldset>
<legend>Styles</legend>
<!-- Font -->
<div class="form-group">
<label for="font-family">Font family</label>
<select id="font-family">
<option value="Open+Sans">Open Sans</option>
<option value="sourcesanspro">Source Sans Pro</option>
<option value="whatever">Whatever</option>
</select>
</div>
<div class="form-group">
<label for="font-size">Font size</label>
<input id="font-size" type="number" value="14" />
</div>
<div class="form-group">
<label for="font-weight">Font weight</label>
<select id="font-weight">
<option value="100">100</option>
<option value="200">200</option>
<option value="300">300</option>
<option value="400">400 (normal)</option>
<option value="500">500</option>
<option value="600">600</option>
<option value="700">700 (bold)</option>
<option value="800">800</option>
<option value="900">900</option>
</select>
</div>
<div class="form-group">
<label for="font-style">Font style</label>
<select id="font-style">
<option value="normal">normal</option>
<option value="italic">italic</option>
<option value="oblique">oblique</option>
</select>
</div>
<!-- Text attributes -->
<div class="form-group">
<label for="line-height">Line height</label>
<input id="line-height" type="number" value="1.0" />
</div>
<div class="form-group">
<label for="letter-spacing">Letter spacing</label>
<input id="letter-spacing" type="number" value="0.0" />
</div>
<div class="form-group">
<label for="direction-ltr">LTR</label>
<input id="direction-ltr" type="radio" name="direction" value="ltr" checked />
</div>
<div class="form-group">
<label for="direction-rtl">RTL</label>
<input id="direction-rtl" type="radio" name="direction" value="rtl" />
</div>
<!-- Text Align -->
<div class="form-group">
<label for="text-align-left">Align left</label>
<input id="text-align-left" type="radio" name="text-align" value="left" checked />
</div>
<div class="form-group">
<label for="text-align-center">Align center</label>
<input id="text-align-center" type="radio" name="text-align" value="center" />
</div>
<div class="form-group">
<label for="text-align-right">Align right</label>
<input id="text-align-right" type="radio" name="text-align" value="right" />
</div>
<div class="form-group">
<label for="text-align-justify">Align justify</label>
<input id="text-align-justify" type="radio" name="text-align" value="justify" />
</div>
<!-- Text Transform -->
<div class="form-group">
<label for="text-transform-none">None</label>
<input id="text-transform-none" type="radio" name="text-transform" value="none" checked />
</div>
<div class="form-group">
<label for="text-transform-uppercase">Uppercase</label>
<input id="text-transform-uppercase" type="radio" name="text-transform" value="uppercase" checked />
</div>
<div class="form-group">
<label for="text-transform-capitalize">Capitalize</label>
<input id="text-transform-capitalize" type="radio" name="text-transform" value="capitalize" />
</div>
<div class="form-group">
<label for="text-transform-lowercase">Lowercase</label>
<input id="text-transform-lowercase" type="radio" name="text-transform" value="lowercase" />
</div>
</fieldset>
<fieldset>
<legend>Debug</legend>
<div class="form-group">
<label for="direction">Direction</label>
<input id="direction" readonly type="text" />
</div>
<div class="form-group">
<label for="focus-node">Focus Node</label>
<input id="focus-node" readonly type="text" />
</div>
<div class="form-group">
<label for="focus-offset">Focus offset</label>
<input id="focus-offset" readonly type="number">
</div>
<div class="form-group">
<label for="focus-inline">Focus Inline</label>
<input id="focus-inline" readonly type="text" />
</div>
<div class="form-group">
<label for="focus-paragraph">Focus Paragraph</label>
<input id="focus-paragraph" readonly type="text" />
</div>
<div class="form-group">
<label for="anchor-node">Anchor Node</label>
<input id="anchor-node" readonly type="text" />
</div>
<div class="form-group">
<label for="anchor-offset">Anchor offset</label>
<input id="anchor-offset" readonly type="number">
</div>
<div class="form-group">
<label for="anchor-inline">Anchor Inline</label>
<input id="anchor-inline" readonly type="text" />
</div>
<div class="form-group">
<label for="anchor-paragraph">Anchor Paragraph</label>
<input id="anchor-paragraph" readonly type="text" />
</div>
<div class="form-group">
<label for="start-container">Start container</label>
<input id="start-container" readonly type="text" />
</div>
<div class="form-group">
<label for="start-offset">Start offset</label>
<input id="start-offset" readonly type="text" />
</div>
<div class="form-group">
<label for="end-container">End container</label>
<input id="end-container" readonly type="text" />
</div>
<div class="form-group">
<label for="end-offset">End offset</label>
<input id="end-offset" readonly type="text" />
</div>
<div class="form-group">
<label for="multi">Multi?</label>
<input id="multi" readonly type="checkbox" />
</div>
<div class="form-group">
<label for="multi-inline">Multi inline?</label>
<input id="multi-inline" readonly type="checkbox" />
</div>
<div class="form-group">
<label for="multi-paragraph">Multi paragraph?</label>
<input id="multi-paragraph" readonly type="checkbox" />
</div>
<div class="form-group">
<label for="is-text-focus">Is text focus?</label>
<input id="is-text-focus" readonly type="checkbox" />
</div>
<div class="form-group">
<label for="is-text-anchor">Is text anchor?</label>
<input id="is-text-anchor" readonly type="checkbox" />
</div>
<div class="form-group">
<label for="is-paragraph-start">Is paragraph start?</label>
<input id="is-paragraph-start" readonly type="checkbox" />
</div>
<div class="form-group">
<label for="is-paragraph-end">Is paragraph end?</label>
<input id="is-paragraph-end" readonly type="checkbox" />
</div>
<div class="form-group">
<label for="is-inline-start">Is inline start?</label>
<input id="is-inline-start" readonly type="checkbox" />
</div>
<div class="form-group">
<label for="is-inline-end">Is inline end?</label>
<input id="is-inline-end" readonly type="checkbox">
</div>
</fieldset>
</form>
<!--
Editor
-->
<div class="text-editor-container align-top">
<div
id="text-editor-selection-imposter"
class="text-editor-selection-imposter"></div>
<div
class="text-editor-content"
contenteditable="true"
role="textbox"
aria-multiline="true"
aria-autocomplete="none"
spellcheck="false"
autocapitalize="false"></div>
</div>
<!--
Text output
-->
<div id="output"></div>
<canvas id="canvas"></canvas>
<script type="module">
import "./style.css";
import "./editor/TextEditor.css";
import { TextEditor } from "./editor/TextEditor";
import { SelectionControllerDebug } from "./editor/debug/SelectionControllerDebug";
import initWasmModule from './wasm/render_wasm.js';
import {
init, assignCanvas, render, setupInteraction, useShape, setShapeChildren, addTextShape, updateTextShape, hexToU32ARGB,getRandomInt, getRandomColor, getRandomFloat, addShapeSolidFill, addShapeSolidStrokeFill
} from './wasm/lib.js';
const searchParams = new URLSearchParams(location.search);
const debug = searchParams.has("debug")
? searchParams.get("debug").split(",")
: [];
const textEditorSelectionImposterElement = document.getElementById(
"text-editor-selection-imposter",
);
const textEditorElement = document.querySelector(".text-editor-content");
const textEditor = new TextEditor(textEditorElement, {
styleDefaults: {
"font-family": "sourcesanspro",
"font-size": "14",
"font-weight": "500",
"font-style": "normal",
"line-height": "1.2",
"letter-spacing": "0",
direction: "ltr",
"text-align": "left",
"text-transform": "none",
"text-decoration": "none",
"--typography-ref-id": '["~#\'",null]',
"--typography-ref-file": '["~#\'",null]',
"--font-id": '["~#\'","sourcesanspro"]',
"--fills": '[["^ ","~:fill-color","#000000","~:fill-opacity",1]]',
},
selectionImposterElement: textEditorSelectionImposterElement,
debug: new SelectionControllerDebug({
direction: document.getElementById("direction"),
multiElement: document.getElementById("multi"),
multiInlineElement: document.getElementById("multi-inline"),
multiParagraphElement: document.getElementById("multi-paragraph"),
isParagraphStart: document.getElementById("is-paragraph-start"),
isParagraphEnd: document.getElementById("is-paragraph-end"),
isInlineStart: document.getElementById("is-inline-start"),
isInlineEnd: document.getElementById("is-inline-end"),
isTextAnchor: document.getElementById("is-text-anchor"),
isTextFocus: document.getElementById("is-text-focus"),
focusNode: document.getElementById("focus-node"),
focusOffset: document.getElementById("focus-offset"),
focusInline: document.getElementById("focus-inline"),
focusParagraph: document.getElementById("focus-paragraph"),
anchorNode: document.getElementById("anchor-node"),
anchorOffset: document.getElementById("anchor-offset"),
anchorInline: document.getElementById("anchor-inline"),
anchorParagraph: document.getElementById("anchor-paragraph"),
startContainer: document.getElementById("start-container"),
startOffset: document.getElementById("start-offset"),
endContainer: document.getElementById("end-container"),
endOffset: document.getElementById("end-offset"),
}),
});
const fontFamilyElement = document.getElementById("font-family");
const fontSizeElement = document.getElementById("font-size");
const fontWeightElement = document.getElementById("font-weight");
const fontStyleElement = document.getElementById("font-style");
const directionLTRElement = document.getElementById("direction-ltr");
const directionRTLElement = document.getElementById("direction-rtl");
const lineHeightElement = document.getElementById("line-height");
const letterSpacingElement = document.getElementById("letter-spacing");
const textAlignLeftElement = document.getElementById("text-align-left");
const textAlignCenterElement = document.getElementById("text-align-center");
const textAlignRightElement = document.getElementById("text-align-right");
const textAlignJustifyElement = document.getElementById("text-align-justify");
function onDirectionChange(e) {
if (debug.includes("events")) {
console.log(e);
}
if (e.target.checked) {
textEditor.applyStylesToSelection({
direction: e.target.value,
});
}
}
directionLTRElement.addEventListener("change", onDirectionChange);
directionRTLElement.addEventListener("change", onDirectionChange);
function onTextAlignChange(e) {
if (debug.includes("events")) {
console.log(e);
}
if (e.target.checked) {
textEditor.applyStylesToSelection({
"text-align": e.target.value,
});
}
}
textAlignLeftElement.addEventListener("change", onTextAlignChange);
textAlignCenterElement.addEventListener("change", onTextAlignChange);
textAlignRightElement.addEventListener("change", onTextAlignChange);
textAlignJustifyElement.addEventListener("change", onTextAlignChange);
fontFamilyElement.addEventListener("change", (e) => {
if (debug.includes("events")) {
console.log(e);
}
textEditor.applyStylesToSelection({
"font-family": e.target.value,
});
});
fontWeightElement.addEventListener("change", (e) => {
if (debug.includes("events")) {
console.log(e);
}
textEditor.applyStylesToSelection({
"font-weight": e.target.value,
});
});
fontSizeElement.addEventListener("change", (e) => {
if (debug.includes("events")) {
console.log(e);
}
textEditor.applyStylesToSelection({
"font-size": e.target.value,
});
});
lineHeightElement.addEventListener("change", (e) => {
if (debug.includes("events")) {
console.log(e);
}
textEditor.applyStylesToSelection({
"line-height": e.target.value,
});
});
letterSpacingElement.addEventListener("change", (e) => {
if (debug.includes("events")) {
console.log(e);
}
textEditor.applyStylesToSelection({
"letter-spacing": e.target.value,
});
});
fontStyleElement.addEventListener("change", (e) => {
if (debug.includes("events")) {
console.log(e);
}
textEditor.applyStylesToSelection({
"font-style": e.target.value,
});
});
function formatHTML(html, options) {
const spaces = options?.spaces ?? 4;
let indent = 0;
return html.replace(/<\/?(.*?)>/g, (fullMatch) => {
let str = fullMatch + "\n";
if (fullMatch.startsWith("</")) {
--indent;
str = " ".repeat(indent * spaces) + str;
} else {
str = " ".repeat(indent * spaces) + str;
++indent;
if (fullMatch === "<br>") --indent;
}
return str;
});
}
const fontSize = 120;
const children = [];
const uuid = crypto.randomUUID();
children.push(uuid);
const outputElement = document.getElementById("output");
textEditorElement.addEventListener("input", (e) => {
if (debug.includes("events")) {
console.log(e);
}
outputElement.textContent = formatHTML(textEditor.element.innerHTML);
});
textEditor.addEventListener("needslayout", (e) => {
console.log("needs layout");
useShape(uuid);
console.log("useShape", uuid);
updateTextShape(fontSize, textEditor.root);
console.log("updateTextShape", fontSize);
render();
})
textEditor.addEventListener("stylechange", (e) => {
if (debug.includes("events")) {
console.log(e);
}
const fontSize = parseInt(e.detail.getPropertyValue("font-size"), 10);
const fontWeight = e.detail.getPropertyValue("font-weight");
const fontStyle = e.detail.getPropertyValue("font-style");
const fontFamily = e.detail.getPropertyValue("font-family");
fontFamilyElement.value = fontFamily;
fontSizeElement.value = fontSize;
fontStyleElement.value = fontStyle;
fontWeightElement.value = fontWeight;
const textAlign = e.detail.getPropertyValue("text-align");
textAlignLeftElement.checked = textAlign === "left";
textAlignCenterElement.checked = textAlign === "center";
textAlignRightElement.checked = textAlign === "right";
textAlignJustifyElement.checked = textAlign === "justify";
const direction = e.detail.getPropertyValue("direction");
directionLTRElement.checked = direction === "ltr";
directionRTLElement.checked = direction === "rtl";
});
const canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const MIN_LINES = 1;
const MAX_LINES = 5;
const MIN_WORDS = 1;
const MAX_WORDS = 10;
initWasmModule().then(Module => {
init(Module);
assignCanvas(canvas);
Module._set_canvas_background(hexToU32ARGB("#FABADA", 1));
Module._set_view(1, 0, 0);
Module._init_shapes_pool(1);
setupInteraction(canvas);
useShape(uuid);
Module._set_parent(0, 0, 0, 0);
Module._set_shape_type(5);
const x1 = 0;
const y1 = 0;
const width = 1000;
const height = 500;
Module._set_shape_selrect(x1, y1, x1 + width, y1 + height);
/*
if (Math.random() < 0.3) {
const numStrokes = getRandomInt(1, 3);
for (let j = 0; j < numStrokes; j++) {
const strokeWidth = getRandomInt(1, 10);
Module._add_shape_center_stroke(strokeWidth, 0, 0, 0);
const color = getRandomColor();
const argb2 = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
addShapeSolidStrokeFill(argb2);
}
}
*/
addTextShape(fontSize, "Hello, World!");
useShape("00000000-0000-0000-0000-000000000000");
setShapeChildren(children);
render()
});
</script>
</body>
</html>

View File

@@ -0,0 +1,541 @@
let Module = null;
let scale = 1;
let offsetX = 0;
let offsetY = 0;
let isPanning = false;
let lastX = 0;
let lastY = 0;
export function init(moduleInstance) {
Module = moduleInstance;
}
export function assignCanvas(canvas) {
const glModule = Module.GL;
const context = canvas.getContext("webgl2", {
antialias: true,
depth: true,
alpha: false,
stencil: true,
preserveDrawingBuffer: true,
});
const handle = glModule.registerContext(context, { majorVersion: 2 });
glModule.makeContextCurrent(handle);
context.getExtension("WEBGL_debug_renderer_info");
Module._init(canvas.width, canvas.height);
Module._set_render_options(0, 1);
}
export function hexToU32ARGB(hex, opacity = 1) {
const rgb = parseInt(hex.slice(1), 16);
const a = Math.floor(opacity * 0xFF);
const argb = (a << 24) | rgb;
return argb >>> 0;
}
export function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
export function getRandomColor() {
const r = getRandomInt(0, 256).toString(16).padStart(2, '0');
const g = getRandomInt(0, 256).toString(16).padStart(2, '0');
const b = getRandomInt(0, 256).toString(16).padStart(2, '0');
return `#${r}${g}${b}`;
}
export function getRandomFloat(min, max) {
return Math.random() * (max - min) + min;
}
function getU32(id) {
const hex = id.replace(/-/g, "");
const buffer = new Uint32Array(4);
for (let i = 0; i < 4; i++) {
buffer[i] = parseInt(hex.slice(i * 8, (i + 1) * 8), 16);
}
return buffer;
}
function heapU32SetUUID(id, heap, offset) {
const buffer = getU32(id);
heap.set(buffer, offset);
return buffer;
}
function ptr8ToPtr32(ptr8) {
return ptr8 >>> 2;
}
export function allocBytes(size) {
return Module._alloc_bytes(size);
}
export function getHeapU32() {
return Module.HEAPU32;
}
export function clearShapeFills() {
Module._clear_shape_fills();
}
export function addShapeSolidFill(argb) {
const ptr = allocBytes(160);
const heap = getHeapU32();
const dv = new DataView(heap.buffer);
dv.setUint8(ptr, 0x00, true);
dv.setUint32(ptr + 4, argb, true);
Module._add_shape_fill();
}
export function addShapeSolidStrokeFill(argb) {
const ptr = allocBytes(160);
const heap = getHeapU32();
const dv = new DataView(heap.buffer);
dv.setUint8(ptr, 0x00, true);
dv.setUint32(ptr + 4, argb, true);
Module._add_shape_stroke_fill();
}
function serializePathAttrs(svgAttrs) {
return Object.entries(svgAttrs).reduce((acc, [key, value]) => {
return acc + key + '\0' + value + '\0';
}, '');
}
export function draw_star(x, y, width, height) {
const len = 11; // 1 MOVE + 9 LINE + 1 CLOSE
const ptr = allocBytes(len * 28);
const heap = getHeapU32();
const dv = new DataView(heap.buffer);
const cx = x + width / 2;
const cy = y + height / 2;
const outerRadius = Math.min(width, height) / 2;
const innerRadius = outerRadius * 0.4;
const star = [];
for (let i = 0; i < 10; i++) {
const angle = Math.PI / 5 * i - Math.PI / 2;
const r = i % 2 === 0 ? outerRadius : innerRadius;
const px = cx + r * Math.cos(angle);
const py = cy + r * Math.sin(angle);
star.push([px, py]);
}
let offset = 0;
// MOVE to first point
dv.setUint16(ptr + offset + 0, 1, true); // MOVE
dv.setFloat32(ptr + offset + 20, star[0][0], true);
dv.setFloat32(ptr + offset + 24, star[0][1], true);
offset += 28;
// LINE to remaining points
for (let i = 1; i < star.length; i++) {
dv.setUint16(ptr + offset + 0, 2, true); // LINE
dv.setFloat32(ptr + offset + 20, star[i][0], true);
dv.setFloat32(ptr + offset + 24, star[i][1], true);
offset += 28;
}
// CLOSE the path
dv.setUint16(ptr + offset + 0, 4, true); // CLOSE
Module._set_shape_path_content();
const str = serializePathAttrs({
"fill": "none",
"stroke-linecap": "round",
"stroke-linejoin": "round",
});
const size = str.length;
offset = allocBytes(size);
Module.stringToUTF8(str, offset, size);
Module._set_shape_path_attrs(3);
}
export function setShapeChildren(shapeIds) {
const offset = allocBytes(shapeIds.length * 16);
const heap = getHeapU32();
let currentOffset = offset;
for (const id of shapeIds) {
heapU32SetUUID(id, heap, ptr8ToPtr32(currentOffset));
currentOffset += 16;
}
return Module._set_children();
}
export function useShape(id) {
const buffer = getU32(id);
Module._use_shape(...buffer);
}
export function set_parent(id) {
const buffer = getU32(id);
Module._set_parent(...buffer);
}
export function render() {
console.log('render')
Module._set_view(1, 0, 0);
Module._render_from_cache();
debouncedRender();
}
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
const debouncedRender = debounce(() => {
Module._render(Date.now());
}, 100);
export function setupInteraction(canvas) {
canvas.addEventListener("wheel", (e) => {
e.preventDefault();
const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9;
scale *= zoomFactor;
const mouseX = e.offsetX;
const mouseY = e.offsetY;
offsetX -= (mouseX - offsetX) * (zoomFactor - 1);
offsetY -= (mouseY - offsetY) * (zoomFactor - 1);
Module._set_view(scale, offsetX, offsetY);
Module._render_from_cache();
debouncedRender();
});
canvas.addEventListener("mousedown", (e) => {
isPanning = true;
lastX = e.offsetX;
lastY = e.offsetY;
});
canvas.addEventListener("mousemove", (e) => {
if (isPanning) {
const dx = e.offsetX - lastX;
const dy = e.offsetY - lastY;
offsetX += dx;
offsetY += dy;
lastX = e.offsetX;
lastY = e.offsetY;
Module._set_view(scale, offsetX, offsetY);
Module._render_from_cache();
debouncedRender();
}
});
canvas.addEventListener("mouseup", () => { isPanning = false; });
canvas.addEventListener("mouseout", () => { isPanning = false; });
}
const TextAlign = {
'left': 0,
'center': 1,
'right': 2,
'justify': 3,
}
function getTextAlign(textAlign) {
if (textAlign in TextAlign) {
return TextAlign[textAlign];
}
return 0;
}
function getTextDirection(textDirection) {
switch (textDirection) {
default:
case 'LTR': return 0;
case 'RTL': return 1;
}
}
function getTextDecoration(textDecoration) {
switch (textDecoration) {
default:
case 'none': return 0;
case 'underline': return 1;
case 'line-through': return 2;
case 'overline': return 3;
}
}
function getTextTransform(textTransform) {
switch (textTransform) {
default:
case 'none': return 0;
case 'uppercase': return 1;
case 'lowercase': return 2;
case 'capitalize': return 3;
}
}
function getFontStyle(fontStyle) {
switch (fontStyle) {
default:
case 'normal':
case 'oblique':
case 'italic':
return 0;
}
}
export function updateTextShape(fontSize, root) {
const paragraphAttrSize = 48;
const leafAttrSize = 56;
const fillSize = 160;
// Calculate fills
const fills = [
{
type: "solid",
color: "#ff00ff",
opacity: 1.0,
},
];
const totalFills = fills.length;
const totalFillsSize = totalFills * fillSize;
const paragraphs = root.children;
console.log("paragraphs", paragraphs.length);
Module._clear_shape_text();
for (const paragraph of paragraphs) {
let totalSize = paragraphAttrSize;
const leaves = paragraph.children;
const numLeaves = leaves.length;
console.log("leaves", numLeaves);
for (const leaf of leaves) {
const text = leaf.textContent;
const textBuffer = new TextEncoder().encode(text);
const textSize = textBuffer.byteLength;
console.log("text", text, textSize);
totalSize += leafAttrSize + totalFillsSize;
}
totalSize += paragraph.textContent.length;
console.log("Total Size", totalSize);
// Allocate buffer
const bufferPtr = allocBytes(totalSize);
const heap = new Uint8Array(Module.HEAPU8.buffer, bufferPtr, totalSize);
const dview = new DataView(heap.buffer, bufferPtr, totalSize);
const textAlign = getTextAlign(
paragraph.style.getPropertyValue("text-align"),
);
console.log("text-align", textAlign);
const textDirection = getTextDirection(
paragraph.style.getPropertyValue("text-direction"),
);
console.log("text-direction", textDirection);
const textDecoration = getTextDecoration(
paragraph.style.getPropertyValue("text-decoration"),
);
console.log("text-decoration", textDecoration);
const textTransform = getTextTransform(
paragraph.style.getPropertyValue("text-transform"),
);
console.log("text-transform", textTransform);
const lineHeight = parseFloat(
paragraph.style.getPropertyValue("line-height"),
);
console.log("line-height", lineHeight);
const letterSpacing = parseFloat(
paragraph.style.getPropertyValue("letter-spacing"),
);
console.log("letter-spacing", letterSpacing);
/*
num_leaves: u32,
text_align: u8,
text_direction: u8,
text_decoration: u8,
text_transform: u8,
line_height: f32,
letter_spacing: f32,
typography_ref_file: [u32; 4],
typography_ref_id: [u32; 4],
*/
// Set number of leaves
dview.setUint32(0, numLeaves, true);
// Serialize paragraph attributes
dview.setUint8(4, textAlign, true); // text-align: left
dview.setUint8(5, textDirection, true); // text-direction: LTR
dview.setUint8(6, textDecoration, true); // text-decoration: none
dview.setUint8(7, textTransform, true); // text-transform: none
dview.setFloat32(8, lineHeight, true); // line-height
dview.setFloat32(12, letterSpacing, true); // letter-spacing
dview.setUint32(16, 0, true); // typography-ref-file (UUID part 1)
dview.setUint32(20, 0, true); // typography-ref-file (UUID part 2)
dview.setUint32(24, 0, true); // typography-ref-file (UUID part 3)
dview.setUint32(28, 0, true); // typography-ref-file (UUID part 4)
dview.setUint32(32, 0, true); // typography-ref-id (UUID part 1)
dview.setUint32(36, 0, true); // typography-ref-id (UUID part 2)
dview.setUint32(40, 0, true); // typography-ref-id (UUID part 3)
dview.setUint32(44, 0, true); // typography-ref-id (UUID part 4)
let leafOffset = paragraphAttrSize;
for (const leaf of leaves) {
console.log(
"leafOffset",
leafOffset,
paragraphAttrSize,
leafAttrSize,
fillSize,
totalFills,
totalFillsSize,
);
const fontStyle = getFontStyle(leaf.style.getPropertyValue("font-style"));
const fontSize = parseFloat(leaf.style.getPropertyValue("font-size"));
console.log("font-size", fontSize);
const fontWeight = parseInt(
leaf.style.getPropertyValue("font-weight"),
10,
);
console.log("font-weight", fontWeight);
const text = leaf.textContent;
const textBuffer = new TextEncoder().encode(text);
const textSize = textBuffer.byteLength;
// Serialize leaf attributes
dview.setUint8(leafOffset + 0, fontStyle, true); // font-style: normal
dview.setUint8(leafOffset + 1, 0, true); // text-decoration: none
dview.setUint8(leafOffset + 2, 0, true); // text-transform: none
dview.setFloat32(leafOffset + 4, fontSize, true); // font-size
dview.setInt32(leafOffset + 8, fontWeight, true); // font-weight: normal
dview.setUint32(leafOffset + 12, 0, true); // font-id (UUID part 1)
dview.setUint32(leafOffset + 16, 0, true); // font-id (UUID part 2)
dview.setUint32(leafOffset + 20, 0, true); // font-id (UUID part 3)
dview.setUint32(leafOffset + 24, 0, true); // font-id (UUID part 4)
dview.setUint32(leafOffset + 28, 0, true); // font-family hash
dview.setUint32(leafOffset + 32, 0, true); // font-variant-id (UUID part 1)
dview.setUint32(leafOffset + 36, 0, true); // font-variant-id (UUID part 2)
dview.setUint32(leafOffset + 40, 0, true); // font-variant-id (UUID part 3)
dview.setUint32(leafOffset + 44, 0, true); // font-variant-id (UUID part 4)
dview.setUint32(leafOffset + 48, textSize, true); // text-length
dview.setUint32(leafOffset + 52, totalFills, true); // total fills count
// Serialize fills
let fillOffset = leafOffset + leafAttrSize;
fills.forEach((fill) => {
if (fill.type === "solid") {
const argb = hexToU32ARGB(fill.color, fill.opacity);
dview.setUint8(fillOffset + 0, 0x00, true); // Fill type: solid
dview.setUint32(fillOffset + 4, argb, true);
fillOffset += fillSize; // Move to the next fill
}
});
leafOffset += leafAttrSize + totalFillsSize;
}
const text = paragraph.textContent;
const textBuffer = new TextEncoder().encode(text);
// Add text content
const textOffset = leafOffset;
console.log('textOffset', textOffset);
heap.set(textBuffer, textOffset);
Module._set_shape_text_content();
}
}
export function addTextShape(fontSize, text) {
const numLeaves = 1; // Single text leaf for simplicity
const paragraphAttrSize = 48;
const leafAttrSize = 56;
const fillSize = 160;
const textBuffer = new TextEncoder().encode(text);
const textSize = textBuffer.byteLength;
// Calculate fills
const fills = [
{
type: "solid",
color: "#ff00ff",
opacity: 1.0,
},
];
const totalFills = fills.length;
const totalFillsSize = totalFills * fillSize;
// Calculate metadata and total buffer size
const metadataSize = paragraphAttrSize + leafAttrSize + totalFillsSize;
const totalSize = metadataSize + textSize;
// Allocate buffer
const bufferPtr = allocBytes(totalSize);
const heap = new Uint8Array(Module.HEAPU8.buffer, bufferPtr, totalSize);
const dview = new DataView(heap.buffer, bufferPtr, totalSize);
// Set number of leaves
dview.setUint32(0, numLeaves, true);
// Serialize paragraph attributes
dview.setUint8(4, 1); // text-align: left
dview.setUint8(5, 0); // text-direction: LTR
dview.setUint8(6, 0); // text-decoration: none
dview.setUint8(7, 0); // text-transform: none
dview.setFloat32(8, 1.2, true); // line-height
dview.setFloat32(12, 0, true); // letter-spacing
dview.setUint32(16, 0, true); // typography-ref-file (UUID part 1)
dview.setUint32(20, 0, true); // typography-ref-file (UUID part 2)
dview.setUint32(24, 0, true); // typography-ref-file (UUID part 3)
dview.setInt32(28, 0, true); // typography-ref-file (UUID part 4)
dview.setUint32(32, 0, true); // typography-ref-id (UUID part 1)
dview.setUint32(36, 0, true); // typography-ref-id (UUID part 2)
dview.setUint32(40, 0, true); // typography-ref-id (UUID part 3)
dview.setInt32(44, 0, true); // typography-ref-id (UUID part 4)
// Serialize leaf attributes
const leafOffset = paragraphAttrSize;
dview.setUint8(leafOffset, 0); // font-style: normal
dview.setFloat32(leafOffset + 4, fontSize, true); // font-size
dview.setUint32(leafOffset + 8, 400, true); // font-weight: normal
dview.setUint32(leafOffset + 12, 0, true); // font-id (UUID part 1)
dview.setUint32(leafOffset + 16, 0, true); // font-id (UUID part 2)
dview.setUint32(leafOffset + 20, 0, true); // font-id (UUID part 3)
dview.setInt32(leafOffset + 24, 0, true); // font-id (UUID part 4)
dview.setInt32(leafOffset + 28, 0, true); // font-family hash
dview.setUint32(leafOffset + 32, 0, true); // font-variant-id (UUID part 1)
dview.setUint32(leafOffset + 36, 0, true); // font-variant-id (UUID part 2)
dview.setUint32(leafOffset + 40, 0, true); // font-variant-id (UUID part 3)
dview.setInt32(leafOffset + 44, 0, true); // font-variant-id (UUID part 4)
dview.setInt32(leafOffset + 48, textSize, true); // text-length
dview.setInt32(leafOffset + 52, totalFills, true); // total fills count
// Serialize fills
let fillOffset = leafOffset + leafAttrSize;
fills.forEach((fill) => {
if (fill.type === "solid") {
const argb = hexToU32ARGB(fill.color, fill.opacity);
dview.setUint8(fillOffset, 0x00, true); // Fill type: solid
dview.setUint32(fillOffset + 4, argb, true);
fillOffset += fillSize; // Move to the next fill
}
});
// Add text content
const textOffset = metadataSize;
heap.set(textBuffer, textOffset);
// Call the WebAssembly function
Module._set_shape_text_content();
}