Public
Edited
Nov 13, 2023
9 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// screens = {
// const canvas = DOM.canvas(width, height);
// const ctx = canvas.getContext('2d');
// ctx.globalCompositeOperation = 'multiply';
// const colors = ['c', 'm', 'y', 'k'];
// const angles = [15, 75, 0, 45];
// let i = 0;
// getHalftone(cmykValues, canvas, colors[i], spacing, angles[i]);
// // yield canvas;
// // while(true) {
// // await Promises.delay(1000);
// // if (++i == colors.length) i = 0;
// // ctx.clearRect(0, 0, width, height);
// // getHalftone(cmykValues, canvas, colors[i], spacing, angles[i]);
// // yield canvas;
// // }
// return canvas;
// }
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// scale for dot size depending on grid spacing and a pixel's color value
dotScale = d3.scaleSqrt().domain([0, 1]).range([0, spacing / 2]);
Insert cell
useAverageColors = false
Insert cell
// iterates through the map pixels and saves a cmyk value for each
cmykValues = {
const cmykValues = Array(width * height);
const data = map.getContext('2d').getImageData(0, 0, width, height).data;
for (let i = 0; i < data.length; i += 4) {
const x = (i/4) % width;
const y = Math.floor((i/4) / width);
const r = [];
const g = [];
const b = [];
if (useAverageColors) {
// get an average color of pixels around this one, based on spacing
for (let cx = x - Math.floor(spacing / 2); cx < x + Math.floor(spacing / 2); cx++) {
for (let cy = y - Math.floor(spacing / 2); cy < y + Math.floor(spacing / 2); cy++) {
if (cx < 0 || cx >= width || cy < 0 || cy >= height) continue;
const ci = 4 * (cy * width + cx);
// gray is sometimes rendered as semitransparent black?
if (data[ci] === 0 && data[ci + 1] === 0 && data[ci + 2] === 0) {
r.push(255 - data[ci + 3]);
g.push(255 - data[ci + 3]);
b.push(255 - data[ci + 3]);
} else {
r.push(data[ci]);
g.push(data[ci + 1]);
b.push(data[ci + 2]);
}
}
}
const mean_r = d3.mean(r);
const mean_g = d3.mean(g);
const mean_b = d3.mean(b);
cmykValues[i/4] = rgb2cmyk(mean_r, mean_g, mean_b);
} else {
if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0) {
r.push(255 - data[i + 3]);
g.push(255 - data[i + 3]);
b.push(255 - data[i + 3]);
} else {
r.push(data[i]);
g.push(data[i + 1]);
b.push(data[i + 2]);
}
cmykValues[i/4] = rgb2cmyk(r[0], g[0], b[0]);
}
}
return cmykValues;
}
Insert cell
function getHalftone(cmykValues, width, height, color = 'k', spacing = 5, rotationDegrees = 0) {
const angle = rotationDegrees * Math.PI / 180;
/* the tricky bit is getting the rotated dot grid.
the way it works here is essentially to iterate over a big square by row/column,
transform each coordinate with a rotation, then calculate the location of the transformed coordinate in
the coordinate space of the original image */
// the square's width/height is the diagonal of the map,
// ensuring it will always contain the whole map when rotated
const diagonal = Math.sqrt(width ** 2 + height ** 2);
// these bits help for using a [center, center] origin instead of [top, left]
const halfD = Math.floor(diagonal/2);
const halfW = Math.floor(width/2);
const halfH = Math.floor(height/2);
const minX = Math.floor((width - diagonal) / 2) - halfD;
const minY = Math.floor((height - diagonal) / 2) - halfD;
// get the right info for the specified color
let fillStyle;
let colorIndex;
if (color === 'c') {
fillStyle = 'rgba(0,255,255,1)';
colorIndex = 0;
} else if (color === 'm') {
fillStyle = 'rgba(255,0,255,1)';
colorIndex = 1;
} else if (color === 'y') {
fillStyle = 'rgba(255,255,0,1)';
colorIndex = 2;
} else {
fillStyle = 'rgba(0,0,0,1)';
colorIndex = 3;
}

const canvas = DOM.canvas(width, height);

const ctx = canvas.getContext('2d');
ctx.fillStyle = fillStyle;
ctx.beginPath();

let count = 0;
for (let x = minX; x < diagonal; x += spacing) {
for (let y = minY; y < diagonal; y += spacing) {
count += 1;
// convert a rotated coordinate to the original space
const rotatedX = x * Math.cos(angle) - y * Math.sin(angle) + halfW;
const rotatedY = x * Math.sin(angle) + y * Math.cos(angle) + halfH;
// transformed coordinate will sometimes be outside the map. skip it.
if (rotatedX < 0 || rotatedY < 0 || rotatedX >= width || rotatedY >= height) continue;
// get cmyk value at coordinate and draw the dot
const cmyk = cmykValues[Math.floor(rotatedY) * width + Math.floor(rotatedX)];
ctx.moveTo(rotatedX, rotatedY);
ctx.arc(rotatedX, rotatedY, dotScale(cmyk[colorIndex]), 0, Math.PI * 2, true);
}
}
console.log(count)
ctx.fill();
return canvas;
}
Insert cell
// from http://www.javascripter.net/faq/rgb2cmyk.htm
function rgb2cmyk (r,g,b) {
let computedC = 0;
let computedM = 0;
let computedY = 0;
let computedK = 0;

// BLACK
if (r==0 && g==0 && b==0) {
computedK = 1;
return [0,0,0,1];
}

computedC = 1 - (r/255);
computedM = 1 - (g/255);
computedY = 1 - (b/255);

const minCMY = Math.min(computedC,
Math.min(computedM,computedY));
computedC = (computedC - minCMY) / (1 - minCMY) ;
computedM = (computedM - minCMY) / (1 - minCMY) ;
computedY = (computedY - minCMY) / (1 - minCMY) ;
computedK = minCMY;

return [computedC,computedM,computedY,computedK];
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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