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

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