Public
Edited
Feb 9
Importers
Insert cell
Insert cell
Insert cell
test.outerHTML
Insert cell
test = {
let row = 0;
let col = -1;
let chunk = BatchRenderSync.chunks.at(row)

let item = chunk.textObjects.at(col)

// let url = URL.createObjectURL(chunk.blob)
// invalidation.then(()=>URL.revokeObjectURL(url))

return svg`<svg width="1024" height="${chunk.chunkHeight*4}" style="outline:1px solid red;">
<defs>
<g id="text-renders">
<image id="chunk-${row}" href="${chunk.dataURL}" height="${chunk.chunkHeight}" width="${chunk.chunkWidth}">
</image>

<!-- full-width pattern-->
<pattern id="full-${row}" width=1 height=1>
<use href="#chunk-${row}"></use>
</pattern>

<!-- viewbox pattern-->
<pattern id="view-${row}" viewbox="${item.offset},${0},${item.width},${chunk.chunkHeight}" width=1 height=1>
<use href="#chunk-${row}"></use>
</pattern>

<!-- offset pattern-->
<pattern id="offs-${row}" width="1" height="1">
<use href="#chunk-${row}" x=${-item.offset}></use>
</pattern>

<!-- fixed pattern-->
<pattern id="fixt-${row}" width="${chunk.chunkWidth}" height="${chunk.chunkHeight}" patternUnits="userSpaceOnUse">
<use href="#chunk-${row}"></use>
</pattern>

</g>

<g id="text-paths">
<!-- single clip-path-->
<clipPath id="textClip-${item.id}">
<rect x="${item.offset}" y="0" width="${item.width}" height="${chunk.chunkHeight}" fill="black"></rect>
</clipPath>

<!-- clip + filter-->
<clipPath id="textCrop-${item.id}">
<rect width="${item.width}" height="${chunk.chunkHeight}" fill="black"></rect>
</clipPath>
<filter id="textOffset-${item.id}">
<feOffset in="SourceGraphic" dx="${-item.offset}"/>
</filter>
</g>

</defs>

<!--
<use href="#chunk-${row}" clip-path="url(#textCrop-${item.id})" filter="url(#textOffset-${item.id})"></use>-->

<!--
<svg viewbox="${item.offset},0,${item.width},${chunk.chunkHeight}" width=${item.width}>
<!--<use href="#chunk-${row}"></use>-->
<!--<rect fill="url(#full-${row})" width="${chunk.chunkWidth}" height="${chunk.chunkHeight}"></rect>-->
<!--</svg>-->

<!--
<use x="${-item.offset}" href="#chunk-${row}" clip-path="url('#textClip-${item.id}')"></use>-->


<!--
<rect fill="url(#view-${row})" width=${item.width} height=${chunk.chunkHeight}></rect>-->

<rect fill="url(#offs-${row})" width=${item.width} height=${chunk.chunkHeight}></rect>

<!--
<rect x="${item.offset}" width="${item.width}" height="${chunk.chunkHeight}"
transform="translate(${-item.offset}, 0)" fill="url(#fixt-${row})" fill-opacity="0.5"></rect>-->
</svg>`;
}

Insert cell
function dataURLtoBlob(dataurl) {
var arr = dataurl.split(","),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
Insert cell
BatchRenderSync = {
let canvas = document.createElement("canvas");

const options = { family: "Arial", size: 16, fill: "blue", resolution: 1,canvas,gap:0,maxCanvasWidth:4096};

let t1 = performance.now()
let chunks = renderTextToCanvasChunks(texts,options)

// invalidation.then(()=>chunks.forEach(chunk=> URL.revokeObjectURL(chunk.blobURL)))

return {
time: ~~(performance.now()-t1),
chunks
}
}
Insert cell
function renderTextToCanvasChunks(texts, {
gap = 0, // Gap between texts
fill = "black", // Default text color
family = "Arial", // Default font family
size = 10, // Default font size
resolution = 1, // Default resolution scaling
maxCanvasWidth = 4096, // Maximum safe canvas width
canvas = document.createElement('canvas')
} = {}) {
const output = []; // Array to store results: { dataURL, textObjects }

// Helper function to calculate the total width of a given textObjects array
const textObjects = texts.map(text=>measureText(text,arguments[1]))

// Partition the textObjects array into chunks that fit within maxCanvasWidth
let span = textObjects.length;
// let currentChunk = [];
let currentChunkWidth = 0;
let maxHeight = 0;

let last,textObj,prev = 0;
for (var i = 0; i < span; ++i) {
textObj = textObjects[i]
const newChunkWidth = currentChunkWidth + textObj.width + (i > prev /*currentChunk.length > 0*/ ? gap : 0);

if (newChunkWidth > maxCanvasWidth) {
// If adding this text exceeds the max width, process the current chunk
// Subtract last chunk width that exceeded the maxCanvasWidth
output.push(renderChunkToCanvas(textObjects.slice(prev,i), {
chunk: output.length,
gap, fill, family, size, resolution,canvas, chunkWidth: last.offset + last.width, chunkHeight:maxHeight,
}));
// Start a new chunk with the current text => deprecated in favour of array slicing
// currentChunk = [textObj];
last = Object.assign(textObj, {id:i,offset:0})
prev = i ; // store the last index for slicing next chunk
currentChunkWidth = textObj.width;
} else {
// Store maximum text height
if(textObj.height > maxHeight) maxHeight = textObj.height
// Add the text to the current chunk => deprecated in favour of array slicing
// currentChunk.push(textObj);
last = Object.assign(textObj, {id:i,offset:(newChunkWidth - textObj.width)})
currentChunkWidth = newChunkWidth;
}
}

// Process any remaining texts in the last chunk
if (i > prev /*currentChunk.length > 0*/) {
output.push(renderChunkToCanvas(textObjects.slice(prev,i), {
gap, fill, family, size, resolution,canvas,chunkWidth:last.offset + last.width, chunkHeight:maxHeight
}));
}

return output
}
Insert cell
// Helper function to render a single chunk of textObjects to a canvas

function renderChunkToCanvas(textObjects, {
gap = 0,
fill = "black",
family = "Arial",
size = 10, // in points
resolution = 1,
canvas = document.createElement('canvas'),
chunkWidth,chunkHeight,chunk = 0,
}) {

let subpixelOffset = Math.min(Math.max(1,1/resolution),2)
// Calculate the total width and height for the canvas
const totalWidth = chunkWidth ?? textObjects.reduce((sum, textObj) => sum + textObj.width + gap, 0) - gap ;
const maxHeight = (chunkHeight ?? Math.max(...textObjects.map(textObj => textObj.height))) + (subpixelOffset);

// Create a canvas with the calculated dimensions
//const canvas = document.createElement('canvas');
canvas.width = totalWidth;
canvas.height = maxHeight;

const ctx = canvas.getContext("2d");

// Set the font size and family, scaled by resolution
const font = `${size * resolution}pt ${family}`;
ctx.font = font;
ctx.fillStyle = fill;

// Render each text object onto the canvas
// let currentX = 0; // Starting X position

textObjects = textObjects.map(textObj => {
const { id,textContent, width, height, ascent, descent,left,metrics,offset} = textObj;

// Draw the text at the current position
ctx.fillText(
textContent,
offset ?? 0,//currentX + /*Math.abs*/(left), // Offset to align the left edge of the text
metrics.hangingBaseline+subpixelOffset // Align the baseline (ascent) of the text
);

// Move the X position forward by the width of the text plus the gap
// currentX += width + gap;
return {
id,chunk,
textContent,width:Math.round(width/resolution),height:Math.round(height/resolution),offset:Math.round((offset ?? 0)/resolution),
ascent: ascent/resolution, descent: descent/resolution
// metrics not needed anymore
}
});

/*const blob = await new Promise(resolve =>
canvas.toBlob(resolve));*/

/*const blobURL/*OffScreen*//* = await new Promise((keep) => (canvas.convertToBlob || canvas.toBlob).call(canvas).then((blob) => {
let link = URL.createObjectURL(blob)
keep(link)
//return link
}))*/

/*const blobURL = await new Promise(function (resolve, reject) {
canvas.toBlob((blob) => {
let link = URL.createObjectURL(blob);
resolve(link);
},"image/png");
});*/

// Return the rendered canvas as a Data URL and the corresponding textObjects
return {
dataURL: canvas.toDataURL("image/png"),chunkWidth: totalWidth/resolution,chunkHeight:maxHeight/resolution,
textObjects,// blobURL
}
}
Insert cell
function measureText(textContent, {
family = "Arial",
size = 10,
resolution = 1,
canvas = document.createElement('canvas')
} = {}) {
// Get the canvas context
const ctx = canvas.getContext("2d");

// Set the font size and family, scaled by resolution
const font = `${size * resolution}pt ${family}`;
ctx.font = font;

// Measure the text dimensions using advanced text metrics
const metrics = ctx.measureText(textContent);

// Calculate the actual bounding box dimensions
const ascent = Math.ceil(metrics.actualBoundingBoxAscent);
const descent = Math.ceil(metrics.actualBoundingBoxDescent);
const left = Math.round(metrics.actualBoundingBoxLeft);
const right = Math.round(metrics.actualBoundingBoxRight);

// Total width and height of the bounding box
const width = left + right;
const height = ascent + descent

// Return the desired output: textContent, width, and height
return {
textContent,
width,
height,
ascent,
descent,
left,right,
metrics
};
}
Insert cell
Insert cell
Insert cell
// merge imagedata directly

function mergeImageData(characters, kerningTable) {
// Step 1: Calculate total width and height
let totalWidth = 0;
let maxHeight = 0;

for (let i = 0; i < characters.length; i++) {
const { imageData } = characters[i];
totalWidth += imageData.width;
maxHeight = Math.max(maxHeight, imageData.height);

if (i > 0) {
const prevChar = characters[i - 1].char;
const currChar = characters[i].char;
const kerning = kerningTable[`${prevChar}${currChar}`] || 0;
totalWidth += kerning;
}
}

// Step 2: Create a composite array
const compositeData = new Uint8ClampedArray(totalWidth * maxHeight * 4);
let offsetX = 0;

for (let i = 0; i < characters.length; i++) {
const { char, imageData } = characters[i];
const { width, height, data } = imageData;

// Apply kerning
if (i > 0) {
const prevChar = characters[i - 1].char;
const kerning = kerningTable[`${prevChar}${char}`] || 0;
offsetX += kerning;
}

// Copy pixel data into the composite array
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const srcIndex = (y * width + x) * 4;
const destIndex = ((y * totalWidth) + (offsetX + x)) * 4;

// Blend with existing pixels (handle transparency)
const srcAlpha = data[srcIndex + 3] / 255;
const destAlpha = compositeData[destIndex + 3] / 255;
const alpha = srcAlpha + destAlpha * (1 - srcAlpha);

if (alpha > 0) {
compositeData[destIndex] = (
(data[srcIndex] * srcAlpha + compositeData[destIndex] * destAlpha * (1 - srcAlpha)) / alpha
);
compositeData[destIndex + 1] = (
(data[srcIndex + 1] * srcAlpha + compositeData[destIndex + 1] * destAlpha * (1 - srcAlpha)) / alpha
);
compositeData[destIndex + 2] = (
(data[srcIndex + 2] * srcAlpha + compositeData[destIndex + 2] * destAlpha * (1 - srcAlpha)) / alpha
);
compositeData[destIndex + 3] = alpha * 255;
}
}
}

// Move to the next position
offsetX += width;
}

// Step 3: Return the composite ImageData
return new ImageData(compositeData, totalWidth, maxHeight);
}
Insert cell
Insert cell
Insert cell
Insert cell
/* canvasLimits = await canvasSize.maxArea({
max: 18000,
min: 2048,
step: 1000,
// useWorker: true,
});*/
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more