Published
Edited
Sep 24, 2020
Importers
Insert cell
md`# Pixelsketch`
Insert cell
Insert cell
Insert cell
Insert cell
chromajs = require('chroma-js')
Insert cell
drawPixel = (x=0,y=0,size=10, fill=chromajs.hsl(0, 0,Math.floor(Math.random()*numClasses)/numClasses)) => {
return `<rect width=${size} height=${size} x=${x*size} y=${y*size} fill=${fill} stroke="#BBD7DB" />`
}
Insert cell
Insert cell
img = {
const {width, height, luminosities} = downsampleCanvas
const size = 20;
const w = width;
const h = height;
//const total = w * h//luminosities.length;
const lumiExcerpt = luminosities.filter((l,i) => i%width < w && Math.floor(i/height) < h)
const pixels = lumiExcerpt.map((p,i) => drawPixel(i%w,Math.floor(i/h), size, "#F5F5F5"))
const drawnPixels = lumiExcerpt.map((lumi,i) => {
const sketchRect = getSketchRectOfClass((1-lumi) * numClasses, true);
return getDrawnPixel(i%w, Math.floor(i/h), size, sketchRect)
})
const img = html`
<svg height=${h*size} width=${w*size}>
${pixels.join('\n')}
${drawnPixels.join('\n')}
</svg>`;
const speedScale = 0.2
img.querySelectorAll('path').forEach((p,i) => {
const len = p.getTotalLength();
const speed = len/1000;
p.style.transition = `stroke-dashoffset ${(speed * speedScale).toFixed(2)}s linear ${(i%w*Math.floor(i/h) * 0.1 * speedScale).toFixed(2)}s`
p.style.strokeDasharray = len
p.style.strokeDashoffset = len
})
return img;
}
Insert cell
templates = html`<svg id="templates" xmlns="http://www.w3.org/2000/svg" style="display: none;">
${sortedSketchRects.map(r => r.symbol).join('\n')}
</svg>`
Insert cell
img2 = {
const totalWidth = 450;
const sizeSequence = [9,15,30]
const {width, height, luminosities} = downsampleCanvas
const size = totalWidth/width;
const w = width;
const h = height;
//const total = w * h//luminosities.length;
const lumiExcerpt = luminosities.filter((l,i) => i%width < w && Math.floor(i/height) < h)
const pixels = lumiExcerpt.map((p,i) => drawPixel(i%w,Math.floor(i/h), size, "#F5F5F5"))
const drawnPixels = lumiExcerpt.map((lumi,i) => {
const sketchRect = getSketchRectOfClass((1-lumi) * numClasses, true);
return sketchRect ? getSymbolPixel(i%w, Math.floor(i/h), size, sketchRect.id) : ''
})
const startDelay = 2
const zoomDur = 1;
const zoomDelay = 2
const animations = sizeSequence.slice(1).map((t,i) => {
return `<animate attributeName="viewBox" to="0 0 ${t*size} ${t*size}" dur="${zoomDur}s" begin="${i*(zoomDur+zoomDelay)+startDelay}s" fill="freeze" />`
})
const img = html`
<svg id="sketchpaper" height=${h*size} width=${w*size} viewBox="0 0 ${sizeSequence[0]*size} ${sizeSequence[0]*size}" overflow=hidden>
<animateTransform attributeName="transform" type="rotate"
from="45"
to="0"
dur="5s" />
${animations.join('\n')}
${pixels.join('\n')}
${drawnPixels.join('\n')}
</svg>
`;
const getSeqStage = (c,r) => {
const size = sizeSequence.find((s,i) => {
const prevS = i === 0 ? 0 : sizeSequence[i-1];
const larger = c > r ? c : r;
return larger >= prevS && larger < s
});
return sizeSequence.indexOf(size)
}
// set up symbols
const symbolPaths = new Map();
templates.querySelectorAll(`symbol`).forEach(s => {
s.style.stroke = "#515151"
symbolPaths.set(s.id, s.querySelector('path'))
});
// set up instances
const speedScale = 0.1
const scribbles = img.querySelectorAll('use')
scribbles.forEach((p,i) => {
const {x,y} = p.dataset;
const href = p.getAttribute("xlink:href");
const path = symbolPaths.get(href.slice(1));
const len = +path.getAttribute("stroke-dasharray");
const stage = getSeqStage(+x,+y);
const stageFactor = 1 - stage / sizeSequence.length
let speed = len/1000 * zoomDur + stage * zoomDur;
let delay = stage * (zoomDur+zoomDelay) + stage * (+x * +y / scribbles.length * 20 * Math.random()) * speedScale
p.style.transition = `stroke-dashoffset ${(speed * stageFactor * speedScale).toFixed(2)}s ease-out ${delay.toFixed(2)}s`
p.style.strokeDashoffset = len;
})
yield img;

// animate instances: trigger dashoffset animation
await Promises.delay(1)
scribbles.forEach(p => {
p.style.strokeDashoffset = 0
})
return img;
}
Insert cell
maskedImg = {
const size = 500;
const x = 0, y= 0
const svg = html`
<svg height=500 width=500>
<defs>
<filter id="f1" x="0" y="0">
<feGaussianBlur in="SourceGraphic" stdDeviation="10" />
</filter>
</defs>
${getMasks(500,12)}

<image href="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png" height="500" width="500"/>
<image href="https://img.nzz.ch/2020/9/22/4de3223a-c025-4e7c-92f0-cb8ba8455eeb.jpeg?width=1360&height=906&fit=crop&quality=75" width="500" />
</svg>
`
yield svg;
const masks = svg.querySelectorAll("mask")
const maskPaths = [];
svg.querySelectorAll('image').forEach(el => {
const maskId = '#'+masks[Math.floor(Math.random()*masks.length)].getAttribute('id');
const clone = el.cloneNode(true);
clone.setAttribute("filter", "url(#f1)");
clone.style.opacity = 0.5;
el.setAttribute("mask", `url(${maskId})`)
el.parentElement.insertBefore(clone, el);
const path = svg.querySelector('mask'+maskId+' path')
maskPaths.push(path);
})
// animate instances: trigger dashoffset animation
maskPaths.forEach(p => p.style.transition = `stroke-dashoffset 2s ease-out 0s`)
await Promises.delay(1)
maskPaths.forEach(p => p.style.strokeDashoffset = 0)
}
Insert cell
Insert cell
getSymbolPixel = (x=0,y=0,size=20,id)=> {
const rot = Math.floor(Math.random() * 10)/10*12-6
const scale = [ Math.floor(Math.random() * 2) ? 1 : -1, Math.floor(Math.random() * 2) ? 1 : -1 ].map(v => v*(Math.random()*0.3+0.8));
const transform = `rotate(${rot}) scale(${scale.join(' ')}) translate(${x*size}, ${y*size})`;
return `
<g transform-origin="${x*size+size/2} ${y*size+size/2}" transform="${transform}">
<use data-x=${x} data-y=${y} width=${size} height=${size} xlink:href="#scribble-${id}"/>
</g>
`

}
Insert cell
html`<svg width=200 height=200>${getDrawnPixel(0,0,100)}</svg>`
Insert cell
getSketchRectOfClass(1)
Insert cell
html`<svg width=${800} height=${41*numClasses}>
${
Array.from(Array(numClasses).keys()).map((c) => {
return getSketchRectOfClass(c).map((p,i) => getDrawnPixel(i,c,40,p)).join('/n')
}).join('/n')
}
</svg>`
Insert cell
getSketchRectOfClass = (c, randomPick = false) => {
const all = sortedSketchRects.filter(p => p.class === c);
return randomPick ? all[ Math.floor(Math.random() * all.length ) ] : all
}
Insert cell
Insert cell
Insert cell
checkSketchRect = async (id, lineWidth=21, size=160, lineCap="round", lineJoin="miter") => {
const canvas = html`<canvas width=${size} height=${size} />`
const ctx = await canvas.getContext("2d");
// draw line
ctx.strokeStyle = '#FF0000';
ctx.lineWidth = lineWidth;
ctx.lineCap = lineCap;
ctx.lineJoin = lineJoin;
const d = getDValue(id)
const length = getLength(id)
const path = await new Path2D(d);
await ctx.stroke(path);
// count opaque pixels
const newData = await ctx.getImageData(0, 0, size, size);
const numPixels = newData.data.length/4;
let opaquePixels = 0;
for (let i = 0; i < newData.data.length; i += 4) {
if(newData.data[i+3] > 0 ){
opaquePixels++;
}
}
const ratio = opaquePixels / numPixels
return {
id,
ratio,
class: Math.floor(ratio * numClasses),
numPixels,
opaquePixels,
lineWidth,
d,
length,
size,
lineCap,
lineJoin,
//canvas
symbol: `
<symbol id="scribble-${id}" viewBox="0 0 ${size} ${size}" overflow=visible>
<path stroke-dashoffset="inherit" stroke-dasharray="${length}" stroke="inherit" stroke-linecap="${lineCap}" stroke-linejoin="${lineJoin}" fill=transparent stroke-width=${lineWidth} d="${d}" />
</symbol>
`,
mask: `
<mask id="scribble-mask-${id}" viewBox="0 0 ${size} ${size}" overflow=visible>
<path stroke-dashoffset="inherit" stroke-dasharray="${length}" stroke="inherit" stroke-linecap="${lineCap}" stroke-linejoin="${lineJoin}" fill=transparent stroke-width=${lineWidth} d="${d}" />
</mask>
`
};
}
Insert cell
getMasks = (size=160,lineWidth, lineCap, lineJoin) => `
${getSketchRectOfClass(numClasses-1).map(r => {
return `
<mask id="scribble-mask-${r.id}">
<svg viewBox="0 0 160 160" width=${size} height=${size} overflow=visible>
<path stroke-dashoffset="${r.length}" stroke-dasharray="${r.length}" stroke="#ffffff" stroke-linecap="${lineCap || r.lineCap}" stroke-linejoin="${lineJoin || r.lineJoin}" fill=transparent stroke-width=${lineWidth || r.lineWidth} d="${r.d}" />
</svg>
</mask>
`
}).join('\n')}
`
Insert cell
getMasks()
Insert cell
sortedSketchRects = {
const pathAr = Array.from(paths)
const sketchCanvases = await Promise.all(pathAr.map((p,i) => {
return checkSketchRect(i);
}));
return pathAr.map((p,index) => {
const canvas = sketchCanvases.find(c => c.id === index)
return { ...canvas }
}).sort((a,b) => b.ratio - a.ratio )
}
Insert cell
{
const c1 = await checkSketchRect(1);
const c2 = await checkSketchRect(2);
const pathAr = Array.from(paths)
const promises = pathAr.map((p,i) => {
return checkSketchRect(i);
});
const sketchCanvases = await Promise.all(promises);
return pathAr.map((p,i) => sketchCanvases.find(c => c.id === i).ratio);
//return [c1.ratio,c2.ratio]
}
Insert cell
getDValue = (id) => {
const ar = Array.from(paths);
return ar[id === undefined ? Math.floor(Math.random() * ar.length) : id].getAttribute('d')
}
Insert cell
getLength = (id) => {
const ar = Array.from(paths);
return ar[id === undefined ? Math.floor(Math.random() * ar.length) : id].getTotalLength()
}
Insert cell
console.log(JSON.stringify(getSketchRectOfClass(numClasses-1)))
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