├── canvas-image-worker.js ├── canvas-image.js ├── index.html ├── readme.md ├── style.css └── target.jpg /canvas-image-worker.js: -------------------------------------------------------------------------------- 1 | function draw (data) { 2 | 3 | if (data.processing.greyscaleMethod == "luminance") { 4 | 5 | greyscale_luminance(data.image.data); 6 | 7 | } else if (data.processing.greyscaleMethod == "average") { 8 | 9 | greyscale_average(data.image.data); 10 | 11 | } 12 | 13 | if (data.processing.ditherMethod == "atkinson") { 14 | 15 | dither_atkinson(data.image.data, data.image.width, (data.processing.greyscaleMethod == "")); 16 | 17 | } else if (data.processing.ditherMethod == "threshold") { 18 | 19 | dither_threshold(data.image.data, data.processing.ditherThreshold); 20 | } 21 | 22 | if (data.processing.replaceColours == true) { 23 | 24 | replace_colours(data.image.data, data.processing.replaceColourMap.black, data.processing.replaceColourMap.white); 25 | 26 | } 27 | 28 | return data; 29 | } 30 | 31 | // Convert image data to greyscale based on luminance. 32 | function greyscale_luminance (image) { 33 | 34 | for (var i = 0; i <= image.data.length; i += 4) { 35 | 36 | image.data[i] = image.data[i + 1] = image.data[i + 2] = parseInt(image.data[i] * 0.21 + image.data[i + 1] * 0.71 + image.data[i + 2] * 0.07, 10); 37 | 38 | } 39 | 40 | return image; 41 | } 42 | 43 | // Convert image data to greyscale based on average of R, G and B values. 44 | function greyscale_average (image) { 45 | 46 | for (var i = 0; i <= image.data.length; i += 4) { 47 | 48 | image.data[i] = image.data[i + 1] = image.data[i + 2] = parseInt((image.data[i] + image.data[i + 1] + image.data[i + 2]) / 3, 10); 49 | 50 | } 51 | 52 | return image; 53 | } 54 | 55 | // Apply Atkinson Dither to Image Data 56 | function dither_atkinson (image, imageWidth, drawColour) { 57 | skipPixels = 4; 58 | 59 | if (!drawColour) 60 | drawColour = false; 61 | 62 | if(drawColour == true) 63 | skipPixels = 1; 64 | 65 | imageLength = image.data.length; 66 | 67 | for (currentPixel = 0; currentPixel <= imageLength; currentPixel += skipPixels) { 68 | 69 | if (image.data[currentPixel] <= 128) { 70 | 71 | newPixelColour = 0; 72 | 73 | } else { 74 | 75 | newPixelColour = 255; 76 | 77 | } 78 | 79 | err = parseInt((image.data[currentPixel] - newPixelColour) / 8, 10); 80 | image.data[currentPixel] = newPixelColour; 81 | 82 | image.data[currentPixel + 4] += err; 83 | image.data[currentPixel + 8] += err; 84 | image.data[currentPixel + (4 * imageWidth) - 4] += err; 85 | image.data[currentPixel + (4 * imageWidth)] += err; 86 | image.data[currentPixel + (4 * imageWidth) + 4] += err; 87 | image.data[currentPixel + (8 * imageWidth)] += err; 88 | 89 | if (drawColour == false) 90 | image.data[currentPixel + 1] = image.data[currentPixel + 2] = image.data[currentPixel]; 91 | 92 | } 93 | 94 | return image.data; 95 | } 96 | 97 | function dither_threshold (image, threshold_value) { 98 | 99 | for (var i = 0; i <= image.data.length; i += 4) { 100 | 101 | image.data[i] = (image.data[i] > threshold_value) ? 255 : 0; 102 | image.data[i + 1] = (image.data[i + 1] > threshold_value) ? 255 : 0; 103 | image.data[i + 2] = (image.data[i + 2] > threshold_value) ? 255 : 0; 104 | 105 | } 106 | } 107 | 108 | function replace_colours (image, black, white) { 109 | 110 | for (var i = 0; i <= image.data.length; i += 4) { 111 | 112 | image.data[i] = (image.data[i] < 127) ? black.r : white.r; 113 | image.data[i + 1] = (image.data[i + 1] < 127) ? black.g : white.g; 114 | image.data[i + 2] = (image.data[i + 2] < 127) ? black.b : white.b; 115 | image.data[i + 3] = (((image.data[i]+image.data[i+1]+image.data[i+2])/3) < 127) ? black.a : white.a; 116 | 117 | } 118 | 119 | } 120 | 121 | self.addEventListener('message', function (e) { 122 | self.postMessage(draw(e.data)); 123 | }, false); -------------------------------------------------------------------------------- /canvas-image.js: -------------------------------------------------------------------------------- 1 | var imageDisplay, displayCanvas, displayContext, displayImage, displayImageData, originalImage; 2 | var worker = new Worker('canvas-image-worker.js'); 3 | var fileReader = new FileReader(); 4 | 5 | function draw () { 6 | 7 | displayImage = new Image(); 8 | displayImage.src = originalImage; 9 | 10 | displayCanvas.width = displayImage.width; 11 | displayCanvas.height = displayImage.height; 12 | 13 | displayContext = displayCanvas.getContext('2d'); 14 | 15 | displayContext.drawImage(displayImage, 0, 0); 16 | 17 | displayImageData = displayContext.getImageData(0,0,displayCanvas.width,displayCanvas.height); 18 | 19 | var tmpGreyscaleMethod = (document.getElementById('rdo_greyscale_luminance').checked) ? "luminance" : (document.getElementById('rdo_greyscale_average').checked) ? "average" : (document.getElementById('rdo_greyscale_disable').checked) ? "" : "luminance" ; 20 | var tmpDitherMethod = (document.getElementById('rdo_dither_atkinson').checked) ? "atkinson" : (document.getElementById('rdo_dither_threshold').checked) ? "threshold" : "atkinson" ; 21 | var tmpDitherThreshold = document.getElementById('threshold').value; 22 | var tmpReplaceColours = document.getElementById('chk_replace_colours').checked; 23 | var tmpReplaceBlack = { 24 | r: document.getElementById('rep_black_r').value, 25 | g: document.getElementById('rep_black_g').value, 26 | b: document.getElementById('rep_black_b').value, 27 | a: document.getElementById('rep_black_a').value 28 | } 29 | var tmpReplaceWhite = { 30 | r: document.getElementById('rep_white_r').value, 31 | g: document.getElementById('rep_white_g').value, 32 | b: document.getElementById('rep_white_b').value, 33 | a: document.getElementById('rep_white_a').value 34 | } 35 | 36 | if (window.console && window.console.time) { 37 | console.log("Starting Web Worker for image (" + displayCanvas.width + "x" + displayCanvas.height + ", Greyscale Method: " + tmpGreyscaleMethod + ", Dither Method: " + tmpDitherMethod + ")"); 38 | console.time("Web worker took"); 39 | } 40 | 41 | worker.postMessage( { 42 | image: { 43 | data: displayImageData, 44 | width: displayCanvas.width, 45 | height: displayCanvas.height 46 | }, 47 | processing: { 48 | greyscaleMethod: tmpGreyscaleMethod, 49 | ditherMethod: tmpDitherMethod, 50 | ditherThreshold: tmpDitherThreshold, 51 | replaceColours: tmpReplaceColours, 52 | replaceColourMap: { 53 | black: tmpReplaceBlack, 54 | white: tmpReplaceWhite 55 | } 56 | } 57 | }); 58 | 59 | } 60 | 61 | worker.addEventListener('message', function (e) { 62 | 63 | displayContext = displayCanvas.getContext('2d'); 64 | 65 | if (window.console && window.console.time) 66 | console.timeEnd("Web worker took"); 67 | 68 | displayContext.putImageData(e.data.image.data, 0, 0); 69 | 70 | if (document.getElementById('rdo_format_png').checked == true) { 71 | 72 | imageDisplay.src = displayCanvas.toDataURL("image/png"); 73 | 74 | } else if (document.getElementById('rdo_format_gif').checked == true) { 75 | 76 | imageDisplay.src = displayCanvas.toDataURL("image/gif"); 77 | 78 | } 79 | 80 | }, false); 81 | 82 | fileReader.onload = function (e) { 83 | originalImage = e.target.result; 84 | document.getElementById('displayImage').src = e.target.result; 85 | } 86 | 87 | function handleFileSelect (e) { 88 | var files = e.target.files; 89 | 90 | fileReader.readAsDataURL(e.target.files[0]); 91 | } 92 | 93 | function setup () { 94 | 95 | // Detect Canvas Support 96 | displayCanvas = document.createElement('canvas'); 97 | imageDisplay = document.getElementById('displayImage'); 98 | 99 | if (displayCanvas.getContext) { 100 | 101 | document.getElementById('renderbtn').onclick = function() { draw(); }; 102 | document.getElementById('fileSelect').addEventListener('change', handleFileSelect, false); 103 | originalImage = document.getElementById('displayImage').src; 104 | 105 | } else { 106 | 107 | alert("Hi there, you're using an older browser which doesn't support Canvas, so unfortunately I can't show you this demo. Sorry!"); 108 | 109 | } 110 | 111 | } 112 | 113 | window.onload = setup; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Canvas Dither 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 |
Custom Image
15 |
16 | 17 |
18 |
Greyscale Conversion Method
19 |
20 |
21 |
22 |
23 |
24 |
Dithering Method
25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
Output Format
44 |
45 |
46 |
47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Canvas Dither 2 | 3 | ## Description 4 | Simple demo of image processing in Javascript using HTML5 and Canvas. 5 | 6 | ## Recent Changes 7 | - Includes a timer for performance testing 8 | - Now able to dither/process each channel (R, G and B) separately, producing interesting effects 9 | - Now allows for choosing your own image to process 10 | - Moved processing to a Web Worker 11 | - Now able to output images in three different formats! (Assuming your browser supports it) 12 | - Renders to an `` element rather than just to a canvas 13 | - Can filter pixels using a simple threshold as well as Atkinson dithering (which, obviously, is what you're here for) 14 | 15 | ## Future Plans 16 | - Create fallback for when web workers are not available 17 | - Enable drag-and-drop custom image selection 18 | - Improve compatibility 19 | - Probably going to re-style to look more like classic Mac OS (currently borrows a lot of design language and ideas from OS X, like [fractal-thing](https://github.com/geoffstokes/fractal-thing).) 20 | 21 | ## Version History 22 | ### v1.0 23 | - Full implementation of algorithm in main JS thread 24 | - Renders to an `` element 25 | 26 | ### v2.0 27 | - Moved to Web Workers 28 | 29 | ### v3.0 30 | - Allowed custom image uploads 31 | - Deal more effectively with large/broken/invalid images 32 | 33 | ### v3.1 34 | - Enabled processing each channel separately, all required channels are processed in one pass 35 | - Tidied up some stuff, made JS create its canvas itself and render off screen 36 | - Added timers for performance testing 37 | 38 | ## Notes 39 | Takes a lot of learned stuff from my [fractal-thing](https://github.com/ticky/fractal-thing) project (and improves on it significantly). 40 | 41 | Example image is by Keven Law, and [sourced from flickr](http://www.flickr.com/photos/kevenlaw/2308263346/). Image is licensed under Creative Commons Attribution-ShareAlike 2.0 Generic. 42 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html,body { 2 | font-family: "Lucida Grande", "Lucida Sans", Helvetica, sans-serif; 3 | font-size: 12px; 4 | padding: 0; 5 | background-color: #ededed; 6 | -webkit-font-smoothing: antialiased; 7 | } 8 | 9 | #imageWrapper { 10 | text-align: center; 11 | margin-bottom: 2em; 12 | } 13 | 14 | #imageWell { 15 | background-color: #ededed; 16 | display: inline-block; 17 | padding: 1em; 18 | border: 1px solid #7c7c7c; 19 | box-shadow: 0 0.2em 0.2em #AAA inset; 20 | border-radius: 0.5em; 21 | min-width: 320px; 22 | min-height: 240px; 23 | max-width: 90%; 24 | max-height: 768px; 25 | overflow: scroll; 26 | } 27 | 28 | dl.preferences { 29 | text-align: right; 30 | width: 520px; 31 | margin: 0 auto; 32 | padding: 1em; 33 | background-color: #e5e5e5; 34 | box-shadow: 0 0.2em 0.2em #AAA inset; 35 | border-radius: 0.5em; 36 | } 37 | 38 | dl.preferences dt { 39 | text-align: right; 40 | clear: left; 41 | float: left; 42 | width: 250px; 43 | margin: 0; 44 | padding: 5px; 45 | } 46 | 47 | dl.preferences dd { 48 | text-align: left; 49 | float: left; 50 | width: 250px; 51 | margin: 0; 52 | padding: 5px; 53 | } 54 | 55 | dl.preferences dd label { 56 | padding-left: 0.2em; 57 | } -------------------------------------------------------------------------------- /target.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticky/canvas-dither/5878632edaaa3452d4b48238ef0c361f54d2fe60/target.jpg --------------------------------------------------------------------------------