block by nbremer 625e8bde76a099a12a8f3643ec75c77f

Canvas CMYK Halftone effect

Full Screen

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.

index.html

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