Published
Edited
Jan 5, 2022
Importers
11 stars
Insert cell
Insert cell
Insert cell
viewof textSettings = Inputs.form({
fontFamily: Inputs.radio(["serif", "sans-serif"], {
value: "serif",
label: "Font family"
}),
fontSize: Inputs.radio([40, 80, 100, 120], {
value: 100,
label: "Font size"
}),
fontVariant: Inputs.radio(["normal", "bold", "italic", "bold italic"], {
label: "Font variant",
value: "normal"
}),
text: Inputs.radio(["abc", "ABC", "def", "DEF", "ghi", "GHI", "&#$", "●▲■"], {
label: "Text",
value: "abc"
})
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
//
// Returns an image representing a text
//
function textImage(
text,
fontSize = 120,
fontFamily = "serif",
fontVariant = "normal",
zoom = 1
) {
let ctx = DOM.context2d(2 + fontSize * zoom, 2 + fontSize * zoom, 1);
ctx.scale(zoom, zoom);
ctx.textBaseline = "top";
const font = (ctx.font = `${fontVariant} ${fontSize}px ${fontFamily}`);
let x0 = ctx.measureText(text).actualBoundingBoxLeft;
let w = -x0 + ctx.measureText(text).actualBoundingBoxRight;
if (w > fontSize) {
ctx = DOM.context2d(w * zoom, 2 + fontSize * zoom, 1);
ctx.scale(zoom, zoom);
ctx.textBaseline = "top";
ctx.font = font;
}
ctx.fillText(text, x0, 0);
return ctx.canvas;
}
Insert cell
//
// Retrieves an ImageData object associated with an image
//
function getImageData(image) {
let ctx = DOM.context2d(image.width, image.height, 1);
ctx.drawImage(image, 0, 0);
return ctx.getImageData(0, 0, image.width, image.height);
}
Insert cell
//
// Binarizes the alpha channel of an ImageData object. Pixels with alpha >= alphaThreshold are mapped to 255, otherwise to 0
//
function binarizeImageData(imageData, alphaThreshold = 128) {
let data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i + 3] = (data[i + 3] >= alphaThreshold) * 255;
}
return imageData;
}
Insert cell
//
// Returns a function that returns the alpha channel of a pixel at given x,y coordinates
//
function getAlpha(imageData) {
const [w, h] = [imageData.width, imageData.height];
return (x, y) => imageData.data[(x + y * w) * 4 + 3];
}
Insert cell
imageData = binarizeImageData(getImageData(image), alphaThreshold)
Insert cell
balls = ballCover(imageData)
Insert cell
ballsNew = ballCoverNew(imageData, epsilon)
Insert cell
//
// Returns a set of balls for the Squared Distance Transformation (SDT) of the
// opaque pixel in the given imageData
//
function getSdtBalls(imageData) {
let I = imgDataToArray(imageData);
let F = Pow2Array(I);
let G = SDT_step1(F);
let H = SDT_step2(G);
let [nrows, ncols] = [imageData.height, imageData.width];
let sdtBalls = [];
for (let j = 0; j < nrows; j++) {
for (let i = 0; i < ncols; i++) {
let r = Math.sqrt(H[i + 1][j + 1]);
if (r > 0) sdtBalls.push(new Ball(i + 0.5, j + 0.5, r - 0.415));
}
}
return sdtBalls;
}
Insert cell
//
// A less inteligent method for covering the given imageData with balls.
// We start just like ballCover, computing the SDT of the image, but
// we merely filter the returned balls, by sorting the collection in reverse
// order of size and then checking the smaller balls to see if they are not
// already contained in some bigger ball examined earlier. We use an Online Ball
// tree for this search.
//
function ballCoverNew(imageData, epsilon = 1) {
let sdtBalls = getSdtBalls(imageData);
let bt = new OnlineBT();
let filtered = [];
sdtBalls.sort((b1, b2) => b2.r - b1.r);
for (let b of sdtBalls) {
let contained = false;
for (let cnode of bt.within(b, b.r + epsilon)) {
let c = cnode.entry;
let e = new Ball(c.x, c.y, c.r + epsilon);
if (e.encloses(b)) {
contained = true;
break;
}
}
if (!contained) {
filtered.push(b);
bt.insert(b);
}
}
return filtered;
}
Insert cell
function ballCover(imgD) {
let I = imgDataToArray(imgD);
let F = Pow2Array(I);
let dma = DMA(F);
if (dma.length > 0) {
let PD = new PowerDiagram(dma);
let balls = [];
for (let i = 0; i < PD.sites.length; i++) {
balls.push(
new Ball(PD.sites[i].center.x, PD.sites[i].center.y, PD.sites[i].radius)
);
}
if (balls.length == 0) return [new Ball(0, 0, 0.5)];
return balls;
} else {
return [new Ball(0, 0, 0.5)];
}
}
Insert cell
function Pow2Array(A) {
let _r = A[0].length;
let _c = A.length;

let dd = Math.max(_r, _c);
let dim = Math.ceil(Math.log2(dd));

let _rows = Math.pow(2, dim);
let _cols = _rows;

let ret = [];
for (var i = 0; i < _cols; i++) {
ret[i] = [];
for (var j = 0; j < _rows; j++) {
if (i >= A.length || j >= A[0].length) {
ret[i][j] = 0;
} else {
ret[i][j] = A[i][j];
}
}
}
return ret;
}
Insert cell
//
// Converts pixels in an ImageData object to a matrix containing 0s and 1s,
// according to whether the pixel alpha is < alphaMin or not.
// Matrix is padded with a 1-pixel border containing zeros, which help
// several image processing algorithms.
//
function imgDataToArray(imgD, alphaMin = 128) {
const w = imgD.width;
const h = imgD.height;
const _rows = h + 2;
const _cols = w + 2;

var mmap = [];

for (let x = -1, i = 0; i < _cols; x++, i++) {
mmap[i] = new Uint8Array(_rows);
if (i == 0 || i == _cols - 1) continue;
for (let y = -1, j = 0; j < _rows; y++, j++) {
if (j == 0 || j == _rows - 1) continue;
const ind = (x + y * w) * 4;
const alpha = imgD.data[ind + 3];
mmap[i][j] = alpha >= alphaMin;
}
}
return mmap;
}
Insert cell
Insert cell
import { PowerDiagram, Site } from "@lretondaro/power-diagram"
Insert cell
import {
DMA,
SDT_step1,
SDT_step2,
compute_RDMA
} from "@lretondaro/discrete-medial-axis"
Insert cell
import {
Ball,
BT,
OnlineBT
} from "@esperanc/omohundro-balltree-construction-algorithms"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more