Skip to content

Instantly share code, notes, and snippets.

@nbremer
Last active December 28, 2019 23:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nbremer/625e8bde76a099a12a8f3643ec75c77f to your computer and use it in GitHub Desktop.
Save nbremer/625e8bde76a099a12a8f3643ec75c77f to your computer and use it in GitHub Desktop.
Canvas CMYK Halftone effect
license: mit
height: 650

Originally inspired by Veltman's block. However, I didn't want to have cut-off dotted patterns (fitted to the shape you apply the pattern to).

This version creates the CMYK dots themselves, so you can have overlapping circles (this block randomly chooses between a version where the circles partially overlap, or not overlap at all), and where the edges of the circles are smooth (i.e. the CMYK dots get smaller on the outsides).

It's based on examples I found here and here. It will take a very long time to create! So it's more something to use to create a dataviz that will eventually be turned into a poster.

<!DOCTYPE html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- <meta name="viewport" content="user-scalable = yes"> -->
<script src="//d3js.org/d3.v4.min.js"></script>
</head>
<body>
<div style="font-family: monospace; text-align: center; margin-top: 20px">This will take a <span style="color: #E01A25;"><em><b>looooooong</b></em></span> time, maybe even 30 seconds *gasp*, so please wait a bit :)</div>
<div style="text-align: center;" id="chart"></div>
<script>
//////////////////////////////////////////////////////////////
/////////////////////// Create canvas ////////////////////////
//////////////////////////////////////////////////////////////
var container = d3.select("#chart");
var width = 960;
var height = 600;
//The target on which the halftone circles are down
var canvas_trg = container.append("canvas").attr("id", "canvas-target")
var ctx_trg = canvas_trg.node().getContext("2d");
crispyCanvas(canvas_trg, ctx_trg, 2);
ctx_trg.globalCompositeOperation = "multiply";
//The source, which will not be drawn
var canvas_src = document.createElement('canvas');
canvas_src.width = width;
canvas_src.height = height;
var ctx_src = canvas_src.getContext('2d');
//////////////////////////////////////////////////////////////
//////////////// Initialize helpers and scales ///////////////
//////////////////////////////////////////////////////////////
var pixelsPerPoint = 4; //The resolution in a way
var colors = [
{ angle: 15, hex: "#00FFFF", name: "c" },
{ angle: 75, hex: "#FF00FF", name: "m" },
{ angle: 0, hex: "#FFFF00", name: "y" },
{ angle: 45, hex: "#000000", name: "k" }
];
//Create random data
var n = 30;
var fill_colors = ["#EFB605","#E44415","#BA0350","#723F98","#0D898E","#7EB852"];
var nodes = d3.range(n).map(function (d,i) {
return { radius: Math.random() * 40 + 10, color: fill_colors[i % fill_colors.length] }
});
///////////////////////////////////////////////////////////////////////////
/////////////////////////// Run force simulation //////////////////////////
///////////////////////////////////////////////////////////////////////////
//Randomly pick between overlap and padding
var coll_version = Math.random() > 0.5 ? function(d) {return d.radius*0.8;} : function(d) {return d.radius + 6;};
//Set-up the simulation
var simulation = d3.forceSimulation(nodes)
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collide', d3.forceCollide(coll_version).strength(0.7))
.alphaDecay(.01)
.stop();
//Run the simulation "manually"
for (var i = 0; i < 300; ++i) simulation.tick();
///////////////////////////////////////////////////////////////////////////
/////////////////////////// Create color circles //////////////////////////
///////////////////////////////////////////////////////////////////////////
//Wait a little before running the halftone creation, so the top title is vibile
setTimeout(create_CMYK_halftone, 1000);
function create_CMYK_halftone() {
nodes.forEach(function (d) {
var rad = d.radius * 1.1; //radius of the circle
var loc = { x: Math.round(d.x - rad), y: Math.round(d.y - rad), width: Math.round(rad * 2), height: Math.round(rad * 2) }; //almost smallest rectangle that sits right around the circle
//var loc = {x: 0, y: 0, width: width, height: height};
//Clear the place where the new circle will be drawn
//Don't use clearRect, since then you won't get soft edges around the circles
ctx_src.fillStyle = "#ffffff";
ctx_src.fillRect(loc.x, loc.y, loc.width, loc.height);
//Create the actual circle on the source
ctx_src.fillStyle = d.color;
ctx_src.beginPath();
ctx_src.arc(d.x, d.y, d.radius, 0, 2 * Math.PI, false);
ctx_src.fill();
//Save the pixel information of the new circle
var imageData = ctx_src.getImageData(loc.x, loc.y, loc.width, loc.height);
//Calculate the longest side of the imageData rectangle
var hypotenuse = Math.sqrt(loc.width * loc.width + loc.height * loc.height);
hypotenuse = Math.ceil(hypotenuse / pixelsPerPoint) * pixelsPerPoint;
//Loop over the 4 colors
//The contents of this loop is mostly based on https://github.com/patrickmatte/color-halftone-filter
for (var c = 0; c < colors.length; c++) {
//Set the color and fill style of the target canvas
var color = colors[c];
ctx_trg.fillStyle = color.hex;
var h = { x: hypotenuse * Math.cos(color.angle * Math.PI / 180), y: hypotenuse * Math.sin(color.angle * Math.PI / 180) }
var v = { x: hypotenuse * Math.cos((color.angle + 90) * Math.PI / 180), y: hypotenuse * Math.sin((color.angle + 90) * Math.PI / 180) }
var origin = { x: loc.width / 2 - h.x / 2 - v.x / 2, y: loc.height / 2 - h.y / 2 - v.y / 2 };
var rectangle = { x: 0, y: 0, width: loc.width - 1, height: loc.height - 1 };
//Loop over the "pixels" within the circle area
for (var y = pixelsPerPoint / 2; y <= hypotenuse; y += pixelsPerPoint) {
var yRatio = y / hypotenuse;
var pos = { x: v.x * yRatio, y: v.y * yRatio };
for (var x = pixelsPerPoint / 2; x <= hypotenuse; x += pixelsPerPoint) {
var xRatio = x / hypotenuse;
var point = { x: pos.x + h.x * xRatio + origin.x, y: pos.y + h.y * xRatio + origin.y };
if (does_contain(point, rectangle)) {
//This small inner loop is mostly based on https://gist.github.com/ucnv/249486 to get a higher resolution and softer edges
var pixels = ctx_src.getImageData(point.x + loc.x, point.y + loc.y, pixelsPerPoint, pixelsPerPoint).data;
var sum = 0, count = 0;
for (var i = 0; i < pixels.length; i += 4) {
if (pixels[i + 3] === 0) continue; //Move on if transparent
var r = 255 - pixels[i];
var g = 255 - pixels[i + 1];
var b = 255 - pixels[i + 2];
var k = Math.min(r, g, b);
if (color.name !== 'k' && k === 255) sum += 0;
else if (color.name === 'k') sum += k / 255;
else if (color.name === 'c') sum += (r - k) / (255 - k);
else if (color.name === 'm') sum += (g - k) / (255 - k);
else if (color.name === 'y') sum += (b - k) / (255 - k);
count++;
}//for i
if (count === 0) continue;
var rate = sum / count;
var radius = Math.SQRT1_2 * pixelsPerPoint * rate;
// var pixel = Math.round(point.y) * loc.width + Math.round(point.x);
// var dataIndex = pixel * 4;
// if(imageData.data[dataIndex + 3] === 0) continue;
// // var pixelCMYK = rgb2cmyk(imageData.data[dataIndex], imageData.data[dataIndex + 1], imageData.data[dataIndex + 2]);
// // var radius = pixelsPerPoint / 1.5 * pixelCMYK[color.name] / 100;
// var pixelCMYK = rgbToCMYK(imageData.data[dataIndex], imageData.data[dataIndex + 1], imageData.data[dataIndex + 2]);
// var radius = pixelsPerPoint * pixelCMYK[color.name];
//Draw the mini dot
ctx_trg.beginPath();
ctx_trg.arc(point.x + loc.x, point.y + loc.y, radius, 0, 2 * Math.PI, false);
ctx_trg.fill();
}//if
}//for x
}//for y
}//for c
})//for p
}//function create_CMYK_halftone
// //For testing
// nodes.forEach(function (d, i) {
// ctx_src.fillStyle = d.color;
// ctx_src.beginPath();
// ctx_src.arc(d.x, d.y, d.radius, 0, 2 * Math.PI, false);
// ctx_src.fill();
// })//forEach
//Does the point lie in the rectangle
function does_contain(point, rectangle) {
return (point.x >= rectangle.x && point.x <= rectangle.x + rectangle.width && point.y >= rectangle.y && point.y <= rectangle.y + rectangle.height) ? true : false;
}//function does_contain
//Retina non-blurry canvas
function crispyCanvas(canvas, ctx, sf) {
canvas
.attr('width', sf * width)
.attr('height', sf * height)
.style('width', width + "px")
.style('height', height + "px");
ctx.scale(sf, sf);
}//function crispyCanvas
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment