|
<!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> |