├── .github └── workflows │ └── ci-workflow.yaml ├── .gitignore ├── .nvmrc ├── client.js ├── demo.client ├── index.html ├── lamborghini.gif └── parrot.jpg ├── demo.server ├── app.js ├── lamborghini.gif └── parrot.jpg ├── dist ├── ditherjs.dist.js └── jquery.ditherjs.dist.js ├── jquery.js ├── lib ├── algorithms │ ├── atkinsonDither.js │ ├── errorDiffusionDither.js │ └── orderedDither.js ├── client.js ├── ditherjs.js ├── server.js └── utils.js ├── package.json ├── readme.md ├── server.js ├── spec ├── ditherjs.spec.js └── hsl.jpg └── webpack.config.js /.github/workflows/ci-workflow.yaml: -------------------------------------------------------------------------------- 1 | name: Run the specs 2 | on: [push, workflow_dispatch] 3 | 4 | jobs: 5 | ci: 6 | name: Test and coverage 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [12, 13, 14] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Node ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: Install platform dependencies 22 | run: sudo apt-get install -y libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev 23 | 24 | - name: Install project dependencies 25 | run: | 26 | npm explore npm -g -- npm install node-gyp@latest 27 | npm install 28 | npm install canvas # optional, needed for server tests 29 | 30 | - name: Run tests 31 | run: npm run coverage 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | coverage 4 | .nyc_output 5 | **/npm-debug.log 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.10.0 2 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/client'); 2 | -------------------------------------------------------------------------------- /demo.client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo.client/lamborghini.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielepiccone/ditherjs/aa719357cdccd8384c2ed4323c2636ba0babe799/demo.client/lamborghini.gif -------------------------------------------------------------------------------- /demo.client/parrot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielepiccone/ditherjs/aa719357cdccd8384c2ed4323c2636ba0babe799/demo.client/parrot.jpg -------------------------------------------------------------------------------- /demo.server/app.js: -------------------------------------------------------------------------------- 1 | var http = require("http"); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var DitherJs = require('../server.js'); 5 | 6 | var server = http.createServer(function(req, res) { 7 | fs.readFile(path.resolve(__dirname, "parrot.jpg"), function (err, data) { 8 | if (err) { 9 | throw err; 10 | } 11 | res.writeHead(200, {"Content-Type": "image/jpeg"}); 12 | var options = { 13 | step: 3 14 | }; 15 | res.write(new DitherJs(options).dither(data)); 16 | res.end(); 17 | }); 18 | }); 19 | 20 | 21 | server.listen(8081); 22 | console.log('Demo running on port 8081'); 23 | -------------------------------------------------------------------------------- /demo.server/lamborghini.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielepiccone/ditherjs/aa719357cdccd8384c2ed4323c2636ba0babe799/demo.server/lamborghini.gif -------------------------------------------------------------------------------- /demo.server/parrot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielepiccone/ditherjs/aa719357cdccd8384c2ed4323c2636ba0babe799/demo.server/parrot.jpg -------------------------------------------------------------------------------- /dist/ditherjs.dist.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.DitherJS=e():t.DitherJS=e()}(this,function(){return function(t){function e(o){if(r[o])return r[o].exports;var n=r[o]={exports:{},id:o,loaded:!1};return t[o].call(n.exports,n,n.exports,e),n.loaded=!0,n.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){t.exports=r(4)},function(t,e){function r(t,e,r,o,n){for(var i,a,s,l,f,p,c,h,u,y,d,m,x,g,v=new Uint8ClampedArray(t),w=new Uint8ClampedArray(t),C=1/8,D=function(t,e){return 4*t+4*e*n},E=0;E", 6 | "main": "dist/ditherjs.dist.js", 7 | "homepage": "https://dpiccone.github.io/ditherjs/", 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:dpiccone/ditherjs.git" 11 | }, 12 | "scripts": { 13 | "preversion": "webpack -p", 14 | "test": "mocha spec/", 15 | "coverage": "nyc mocha spec/", 16 | "demo:client": "webpack-dev-server --content-base demo.client", 17 | "demo:server": "node demo.server/app.js" 18 | }, 19 | "keywords": [ 20 | "graphic", 21 | "graphics", 22 | "palette", 23 | "palettes", 24 | "pattern", 25 | "color", 26 | "dithering", 27 | "pixel" 28 | ], 29 | "devDependencies": { 30 | "domino": "1.0.25", 31 | "express": "4.14.0", 32 | "mocha": "3.0.2", 33 | "nyc": "8.3.0", 34 | "unexpected": "10.16.0", 35 | "webpack": "1.13.2", 36 | "webpack-dev-server": "1.15.0" 37 | }, 38 | "browser": { 39 | "canvas": false 40 | }, 41 | "license": "CC-BY-SA-4.0" 42 | } 43 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ditherJS 2 | 3 | [![License: CC BY-SA 4.0](https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey.svg)](http://creativecommons.org/licenses/by-sa/4.0/) 4 | 5 | A javascript library which dithers an image using a fixed palette. 6 | 7 | Run `npm run demo:client` or `npm run demo:sever` to see it in action. 8 | 9 | ## Installation and dependencies 10 | 11 | ```sh 12 | $ npm install ditherjs --save 13 | ``` 14 | 15 | Both client and server are exposed as commonJS modules to be used with webpack or browserify. 16 | 17 | The client-side version is also published with an UMD compatible wrapper and a jQuery plugin, those versions are in `./dist` 18 | 19 | The server-side version needs [node-canvas](https://github.com/Automattic/node-canvas) installed as a peer dependency to work, this is also needed to run run the tests during development. 20 | 21 | ```sh 22 | $ npm install ditherjs canvas --save 23 | ``` 24 | 25 | ## Usage and options 26 | 27 | Any DitherJS instance exposes a `dither(target, [options])` method which accepts a *selector* a *Node* or a *buffer* as a target and an optional options object. 28 | 29 | The options can be passed directly to the method or directly in the constructor. 30 | 31 | ```javascript 32 | var options = { 33 | "step": 1, // The step for the pixel quantization n = 1,2,3... 34 | "palette": defaultPalette, // an array of colors as rgb arrays 35 | "algorithm": "ordered" // one of ["ordered", "diffusion", "atkinson"] 36 | }; 37 | ``` 38 | 39 | A default palette is provided which is CGA Palette 1 40 | 41 | ![Rick dangerhous II](http://www.rickdangerous.co.uk/cga20a.png) 42 | 43 | The palette structure is as an array of rgb colors `[[r,g,b]..]` 44 | 45 | ### Client 46 | 47 | 48 | ```javascript 49 | var DitherJS = require('ditherjs'); 50 | 51 | var ditherjs = new DitherJS([,options]); 52 | ditherjs.dither(selector,[,options]); // should target elements 53 | ``` 54 | 55 | as a jQuery plugin 56 | ```javascript 57 | $('.dither').ditherJS(options); 58 | ``` 59 | 60 | or directly on the element 61 | ```html 62 | 63 | ``` 64 | 65 | ## Server 66 | 67 | ```javascript 68 | var DitherJS = require('ditherjs/server'); 69 | 70 | var ditherjs = new DitherJS([,options]); 71 | 72 | // Get a buffer that can be loaded into a canvas 73 | var buffer = fs.readFileSync('./myBeautifulFile.jpg|gif|png'); 74 | 75 | ditherjs.dither(buffer,[,options]); 76 | ``` 77 | 78 | ### Testimonials 79 | 80 | Useful as a comb to a bald man. -Anon 81 | 82 | author 2014 [Daniele Piccone](http://www.danielepiccone.com) 83 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/server'); 2 | -------------------------------------------------------------------------------- /spec/ditherjs.spec.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var unexpected = require('unexpected'); 3 | var expect = unexpected.clone(); 4 | 5 | var utils = require('../lib/utils.js'); 6 | var noop = function () {}; 7 | 8 | describe('ditherjs', function () { 9 | [require('../server.js'),require('../client.js')].forEach(function (DitherJS) { 10 | 11 | it('should expose the dithering algorithms', function () { 12 | expect(DitherJS.orderedDither, 'to be defined'); 13 | expect(DitherJS.atkinsonDither, 'to be defined'); 14 | expect(DitherJS.errorDiffusionDither, 'to be defined'); 15 | }); 16 | 17 | it('should expose the dither() method', function () { 18 | expect(DitherJS.prototype.dither, 'to be defined'); 19 | }); 20 | 21 | describe('constructor', function () { 22 | var ditherjs = new DitherJS(); 23 | 24 | it('should get all the defaults with ', function () { 25 | expect(ditherjs.options.algorithm, 'to be', 'ordered'); 26 | expect(ditherjs.options.step, 'to be', 1); 27 | expect(ditherjs.options.className, 'to be', 'dither'); 28 | expect(ditherjs.options.palette, 'to be', utils.defaultPalette); 29 | }); 30 | }); 31 | 32 | describe('ditherImageData', function () { 33 | var ditherjs = new DitherJS(); 34 | 35 | var mockImageData = { 36 | data: [], 37 | height: 0, 38 | width: 0 39 | }; 40 | mockImageData.data.set = noop; 41 | 42 | it('should accept all the algorithms', function () { 43 | ['atkinson','diffusion','ordered'].forEach(function (algorithm) { 44 | var options = { algorithm: algorithm }; 45 | ditherjs.ditherImageData(mockImageData, options); 46 | }); 47 | }); 48 | 49 | it('should throw an error for an unknown algorithm', function () { 50 | var options = { algorithm: 'mo' }; 51 | try { 52 | ditherjs.ditherImageData(mockImageData, options); 53 | } catch (err) { 54 | expect(err, 'to be', utils.errors.InvalidAlgorithm); 55 | } 56 | }); 57 | 58 | it('should mutate the imageData', function () { 59 | var called = false; 60 | mockImageData.data.set = function () { called = true;}; 61 | ditherjs.ditherImageData(mockImageData); 62 | expect(called, 'to be true'); 63 | }); 64 | }); 65 | 66 | describe('colorDistance', function () { 67 | var ditherjs = new DitherJS(); 68 | var fn = ditherjs.colorDistance; 69 | 70 | it('should calculate the euclidean distance between colors', function () { 71 | expect(fn([1,0,0],[0,0,0]),'to be', 1); 72 | expect(fn([1,1,0],[1,1,0]),'to be', 0); 73 | expect(fn([1,0,0],[0,0,1]),'to be', 1.4142135623730951); 74 | }); 75 | }); 76 | 77 | describe('approximateColor', function () { 78 | var ditherjs = new DitherJS(); 79 | var fn = ditherjs.approximateColor.bind(ditherjs); 80 | 81 | it('approximates the color to the closest one', function () { 82 | var palette = utils.defaultPalette; 83 | expect(fn([128,0,0], palette), 'to satisfy', [0,0,0]); 84 | expect(fn([128,0,128], palette), 'to satisfy', [255,0,255]); 85 | expect(fn([0,128,128], palette), 'to satisfy', [0,255,255]); 86 | }); 87 | }); 88 | }); 89 | 90 | }); 91 | 92 | describe('ditherjs.server', function() { 93 | var DitherJS = require('../server.js'); 94 | 95 | it('should augment the base class', function () { 96 | [ 97 | '_bufferToImageData', 98 | '_imageDataToBuffer' 99 | ].forEach(function (method) { 100 | expect(DitherJS.prototype.hasOwnProperty(method), 'to be', true); 101 | }); 102 | }); 103 | 104 | describe('_bufferToImageData', function () { 105 | var ditherjs = new DitherJS(); 106 | 107 | it('should get ImageData from Buffer', function () { 108 | var buffer = fs.readFileSync(__dirname + '/hsl.jpg'); 109 | var input = ditherjs._bufferToImageData(buffer); 110 | expect(input.height, 'to be defined'); 111 | expect(input.width, 'to be defined'); 112 | expect(input.data.length, 'to be', 262144); 113 | expect(input.data.constructor.name, 'to be', 'Uint8ClampedArray'); 114 | }); 115 | }); 116 | 117 | describe('_imageDataToBuffer', function () { 118 | var ditherjs = new DitherJS(); 119 | 120 | it('should get Buffer from ImageData', function () { 121 | var ImageData = require('canvas').ImageData; 122 | var imageData = new ImageData(10,10); 123 | 124 | var outBuffer = ditherjs._imageDataToBuffer(imageData); 125 | expect(outBuffer.constructor.name, 'to be', 'Buffer'); 126 | }); 127 | }); 128 | }); 129 | 130 | describe('ditherjs.client', function() { 131 | var DitherJS = require('../client.js'); 132 | 133 | var domino = require('domino'); 134 | global.window = domino.createWindow(); 135 | global.document = window.document; 136 | 137 | 138 | it('should augment the base class', function () { 139 | [ 140 | '_replaceElementWithCtx', 141 | '_fromImgElement', 142 | '_fromSelector' 143 | ].forEach(function (method) { 144 | expect(DitherJS.prototype.hasOwnProperty(method), 'to be', true); 145 | }); 146 | }); 147 | 148 | describe('dither', function () { 149 | var ditherjs = new DitherJS(); 150 | 151 | it('should call _fromImgElement if the argument is a Node', function () { 152 | var node = document.createElement('img'); 153 | document.body.appendChild(node); 154 | 155 | var called = false; 156 | 157 | var mockInstance = { 158 | _fromImgElement: function () { called = true; } 159 | }; 160 | 161 | ditherjs.dither.call(mockInstance, node); 162 | expect(called, 'to be', true); 163 | }); 164 | 165 | it('should call _fromSelector if the argument is a string', function () { 166 | var called = false; 167 | 168 | var mockInstance = { 169 | _fromSelector: function () { called = true; } 170 | }; 171 | 172 | ditherjs.dither.call(mockInstance, '.foo'); 173 | expect(called, 'to be', true); 174 | }); 175 | }); 176 | 177 | describe('_replaceElementWithCtx', function () { 178 | var ditherjs = new DitherJS(); 179 | 180 | var element = document.createElement('img'); 181 | element.className = 'boo dither bar'; 182 | document.body.appendChild(element); 183 | 184 | it('should get the canvas context out of the element', function () { 185 | // TODO getContext() not implemented in Domino 186 | try { 187 | ditherjs._replaceElementWithCtx(element) ; 188 | } catch (err) { 189 | expect(err, 'to be defined'); 190 | } 191 | }); 192 | }); 193 | 194 | describe('_fromImgElement', function () { 195 | var ditherjs = new DitherJS(); 196 | 197 | var element = document.createElement('img'); 198 | document.body.appendChild(element); 199 | 200 | it('should get the image, process it and put that in the context', function () { 201 | var MOCK_DATA = ['panda']; 202 | 203 | ditherjs.ditherImageData = function (data) { 204 | expect(data, 'to be', MOCK_DATA); 205 | data.push('mango'); 206 | }; 207 | 208 | ditherjs._replaceElementWithCtx = function () { 209 | return { 210 | drawImage: noop, 211 | getImageData: function () { return MOCK_DATA; }, 212 | putImageData: function (data) { 213 | return expect(data, 'to satisfy', ['panda','mango']); 214 | } 215 | }; 216 | }; 217 | 218 | ditherjs._fromImgElement(element); 219 | }); 220 | }); 221 | 222 | describe('_fromSelector', function () { 223 | var ditherjs = new DitherJS(); 224 | 225 | var element = document.createElement('img'); 226 | element.className = 'mommo'; 227 | document.body.appendChild(element); 228 | 229 | it('should call _fromImgElement on the element', function () { 230 | var mockDitherCalled = false; 231 | var mockDither = { 232 | _fromImgElement: function () { mockDitherCalled = true; } 233 | }; 234 | 235 | ditherjs._fromSelector.call(mockDither,'.mommo'); 236 | 237 | element.onload(); 238 | expect(mockDitherCalled, 'to be', true); 239 | }); 240 | }); 241 | 242 | }); 243 | 244 | 245 | describe('algorithms', function () { 246 | var buffer = fs.readFileSync(__dirname + '/hsl.jpg'); 247 | var DitherJS = require('../server.js'); // also ../client 248 | var ditherjs = new DitherJS(); 249 | 250 | describe('ordered dither', function () { 251 | it('should work', function () { 252 | var input = ditherjs._bufferToImageData(buffer); 253 | 254 | var output = DitherJS.orderedDither.call(ditherjs, input.data, utils.defaultPalette, 1, input.width, input.height); 255 | 256 | var test = Array.prototype.slice.call(output,0, 128); 257 | 258 | expect( 259 | test, 260 | 'to satisfy', 261 | [ 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 ] 262 | ); 263 | }); 264 | }); 265 | 266 | describe('atkinson dither', function () { 267 | it('should work', function () { 268 | var input = ditherjs._bufferToImageData(buffer); 269 | 270 | var output = DitherJS.atkinsonDither.call(ditherjs, input.data, utils.defaultPalette, 1, input.width, input.height); 271 | 272 | var test = Array.prototype.slice.call(output,0, 128); 273 | 274 | expect( 275 | test, 276 | 'to satisfy', 277 | [ 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 0, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 ] 278 | ); 279 | }); 280 | }); 281 | 282 | describe('error diffusion dither', function () { 283 | it('should work', function () { 284 | var input = ditherjs._bufferToImageData(buffer); 285 | 286 | var output = DitherJS.errorDiffusionDither.call(ditherjs, input.data, utils.defaultPalette, 1, input.width, input.height); 287 | 288 | var test = Array.prototype.slice.call(output,0, 128); 289 | 290 | expect( 291 | test, 292 | 'to satisfy', 293 | [ 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 ] 294 | ); 295 | }); 296 | }); 297 | }); 298 | -------------------------------------------------------------------------------- /spec/hsl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielepiccone/ditherjs/aa719357cdccd8384c2ed4323c2636ba0babe799/spec/hsl.jpg -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | "ditherjs": "./client.js", 4 | "jquery.ditherjs": "./jquery.js", 5 | }, 6 | output: { 7 | path: "./dist", 8 | filename: "[name].dist.js", 9 | library: 'DitherJS', 10 | libraryTarget: 'umd' 11 | } 12 | }; 13 | --------------------------------------------------------------------------------