├── .prettierignore ├── pixel-flow.gif ├── public ├── favicon.ico ├── media │ ├── 32.jpg │ ├── 51.jpg │ ├── 66.jpg │ └── 68.jpg ├── styles │ ├── calibration.css │ └── index.css ├── index.html ├── jquery.html └── scripts │ ├── index-old.js │ └── index.mjs ├── .gitignore ├── src ├── jquery.js └── pixel-flow.js ├── rollup.config.js ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | lib 4 | jquery.js* 5 | gh-pages 6 | -------------------------------------------------------------------------------- /pixel-flow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamesNimlos/pixel-flow/HEAD/pixel-flow.gif -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamesNimlos/pixel-flow/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/media/32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamesNimlos/pixel-flow/HEAD/public/media/32.jpg -------------------------------------------------------------------------------- /public/media/51.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamesNimlos/pixel-flow/HEAD/public/media/51.jpg -------------------------------------------------------------------------------- /public/media/66.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamesNimlos/pixel-flow/HEAD/public/media/66.jpg -------------------------------------------------------------------------------- /public/media/68.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamesNimlos/pixel-flow/HEAD/public/media/68.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | dist 4 | jquery.js* 5 | public/scripts/pixel-flow.js 6 | # keep gh-pages from being committed in main 7 | gh-pages 8 | .cache 9 | -------------------------------------------------------------------------------- /src/jquery.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** https://github.com/JamesNimlos/pixel-flow 3 | ** 4 | ** Developed by 5 | ** - James Nimlos 6 | ** 7 | ** Licensed under MIT license 8 | */ 9 | 10 | 'use strict' 11 | import $ from 'jquery' 12 | import PixelFlow from './pixel-flow.js' 13 | 14 | //set up default options 15 | var pluginName = 'PixelFlow', 16 | defaults = { 17 | resolution: 16, 18 | offsetX: 0, 19 | offsetY: 0 20 | } 21 | 22 | $.fn[pluginName] = function(options) { 23 | var $cvs 24 | this.each(function() { 25 | var r = $.data(this, 'plugin_' + pluginName) 26 | if (!r) { 27 | r = new PixelFlow(this, options) 28 | $.data(r.canvas, 'plugin_' + pluginName, r) 29 | } else { 30 | if ('undefined' === typeof o) { 31 | return $cvs.add(r.canvas) 32 | } else if ('string' !== typeof o || !r[o]) { 33 | void jQuery.error( 34 | 'Method ' + o + ' does not exist on jQuery(el).' + pluginName 35 | ) 36 | } else { 37 | r[o].apply(r, [].slice.call(a, 1)) 38 | } 39 | } 40 | if (typeof $cvs === 'undefined') $cvs = $(r.canvas) 41 | else $cvs.add(r.canvas) 42 | }) 43 | return $cvs 44 | } 45 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json' 2 | import sourceMaps from 'rollup-plugin-sourcemaps' 3 | import cleanup from 'rollup-plugin-cleanup' 4 | 5 | const banner = ` 6 | /* 7 | ** pixel-flow@${pkg.version} 8 | ** https://github.com/JamesNimlos/pixel-flow 9 | ** 10 | ** Developed by 11 | ** - James Nimlos 12 | ** 13 | ** Licensed under MIT license 14 | */ 15 | ` 16 | 17 | const plugins = [cleanup(), sourceMaps()] 18 | export default [ 19 | { 20 | input: 'src/jquery.js', 21 | external: ['jquery'], 22 | output: [ 23 | { 24 | file: 'jquery.js', 25 | format: 'umd', 26 | sourcemap: true, 27 | name: 'pixel-flow', 28 | globals: { 29 | jquery: 'jQuery', 30 | }, 31 | banner, 32 | }, 33 | ], 34 | plugins, 35 | }, 36 | { 37 | input: 'src/pixel-flow.js', 38 | output: [ 39 | { 40 | file: pkg.module, 41 | format: 'es', 42 | sourcemap: true, 43 | banner, 44 | }, 45 | { 46 | file: pkg.main, 47 | format: 'cjs', 48 | sourcemap: true, 49 | banner, 50 | exports: 'auto', 51 | }, 52 | ], 53 | plugins, 54 | }, 55 | ] 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixel-flow", 3 | "version": "2.0.1", 4 | "description": "an image pixelating filter plugin", 5 | "main": "lib/pixel-flow.cjs.js", 6 | "module": "lib/pixel-flow.esm.js", 7 | "files": [ 8 | "lib", 9 | "jquery.js", 10 | "jquery.js.map" 11 | ], 12 | "repository": "https://github.com/JamesNimlos/pixel-flow.git", 13 | "author": "JamesNimlos ", 14 | "license": "MIT", 15 | "private": false, 16 | "devDependencies": { 17 | "@jamesnimlos/gh-pages": "^1.0.0", 18 | "jquery": "^3.5.1", 19 | "parcel-bundler": "^1.12.4", 20 | "prettier": "^2.0.5", 21 | "rollup": "^2.23.0", 22 | "rollup-plugin-cleanup": "^3.1.1", 23 | "rollup-plugin-sourcemaps": "^0.6.2" 24 | }, 25 | "scripts": { 26 | "dev": "parcel public/index.html public/jquery.html --public-url /pixel-flow/", 27 | "fmt": "prettier --write '{./**/,}*.{js,css,md,html}'", 28 | "build": "rollup --config", 29 | "clean": "rm -rf dist lib gh-pages .cache jquery.js jquery.js.map", 30 | "build:demo": "parcel build public/index.html public/jquery.html --public-url /pixel-flow/", 31 | "gh-pages": "yarn build:demo && gh-pages -u dist" 32 | }, 33 | "prettier": { 34 | "singleQuote": true, 35 | "semi": false 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/styles/calibration.css: -------------------------------------------------------------------------------- 1 | /* 2 | NORMALIZES CSS 3 | 4 | SUPPLEMENTAL CALIBRATION EXISTS IN SITESPECIFIC.CSS 5 | Targets text highlighting and such (not required) 6 | */ 7 | 8 | html, 9 | body, 10 | div, 11 | span, 12 | object, 13 | iframe, 14 | h1, 15 | h2, 16 | h3, 17 | h4, 18 | h5, 19 | h6, 20 | p, 21 | del, 22 | dfn, 23 | em, 24 | img, 25 | ins, 26 | kbd, 27 | q, 28 | samp, 29 | small, 30 | strong, 31 | b, 32 | i, 33 | dl, 34 | dt, 35 | dd, 36 | ol, 37 | ul, 38 | li, 39 | fieldset, 40 | form, 41 | label, 42 | table, 43 | tbody, 44 | tfoot, 45 | thead, 46 | tr, 47 | th, 48 | td, 49 | article, 50 | aside, 51 | footer, 52 | header, 53 | nav, 54 | section { 55 | margin: 0; 56 | padding: 0; 57 | border: 0; 58 | outline: 0; 59 | font-size: 100%; 60 | vertical-align: baseline; 61 | background: transparent; 62 | font-weight: normal; 63 | } 64 | html { 65 | /* background: #fff; */ 66 | color: rgba(255, 255, 255, 1); 67 | } 68 | 69 | * { 70 | padding: 0; 71 | margin: 0; 72 | } 73 | 74 | h1, 75 | h2, 76 | h3, 77 | h4, 78 | h5, 79 | h6, 80 | p { 81 | margin: 0; 82 | padding: 0; 83 | /* font-size: 12px;*/ 84 | cursor: text; 85 | } 86 | input { 87 | border: 0px none; 88 | outline: none; 89 | } 90 | clear, 91 | .clear { 92 | clear: both; 93 | display: block; 94 | height: 0; 95 | width: 0; 96 | } 97 | 98 | /* 99 | end of calibration.css 100 | */ 101 | -------------------------------------------------------------------------------- /public/styles/index.css: -------------------------------------------------------------------------------- 1 | /*Colors 2 | DARK TEAL : #00878B rgba( 0, 135, 139, 1); 3 | TEAL : #00AFB2 rgba( 0, 175, 178, 1); 4 | LIGHT TEAL : #32BFC1 rgba( 50, 191, 193, 1); 5 | ORANGE : #B24400 rgba(178, 68, 0, 1); 6 | LIGHT ORANGE: #FF8032 rgba(255, 128, 50, 1); 7 | BLACK : #0D0D0D rgba( 13, 13, 13, 1); 8 | NEAR-BLACK : #121212 rgba( 18, 18, 18, 1); 9 | Dark-Grey : #292929 rgba( 41, 41, 41, 1); 10 | GREY : #656565 rgba(101, 101, 101, 1); 11 | LIGHT-GREY : #AAAAAA rgba(170, 170, 170, 1); 12 | NEAR-WHITE : #F4F4F4 rgba(244, 244, 244, 1); 13 | WHITE : #FFFFFF rgba(255, 255, 255, 1); 14 | */ 15 | html { 16 | font-family: 'Raleway', sans-serif; 17 | font-size: 16px; 18 | color: #656565; 19 | } 20 | h1, 21 | h2, 22 | h3, 23 | h4, 24 | h5, 25 | h6 { 26 | font-weight: 400; 27 | font-family: 'Merriweather', serif; 28 | } 29 | h5 { 30 | font-size: 110%; 31 | } 32 | h4 { 33 | font-size: 120%; 34 | } 35 | h3 { 36 | font-size: 130%; 37 | } 38 | h2 { 39 | font-size: 140%; 40 | } 41 | h1 { 42 | font-size: 150%; 43 | } 44 | *, 45 | *:before, 46 | *:after { 47 | box-sizing: border-box; 48 | font-family: inherit; 49 | font-size: inherit; 50 | color: inherit; 51 | } 52 | a { 53 | cursor: pointer; 54 | text-decoration: none; 55 | color: #00878b; 56 | } 57 | a:hover { 58 | color: #00afb2; 59 | } 60 | /*clearAfter*/ 61 | .clearAfter:after { 62 | clear: both; 63 | content: ''; 64 | height: 0; 65 | display: block; 66 | } 67 | 68 | /*button*/ 69 | button, 70 | .btn { 71 | background-color: transparent; 72 | border: 1px solid #00878b; 73 | color: #656565; 74 | cursor: pointer; 75 | padding: 10px 14px 14px 14px; 76 | } 77 | button:hover, 78 | .btn:hover { 79 | color: #f4f4f4; 80 | background-color: #00878b; 81 | } 82 | button:disabled, 83 | button:hover:disabled, 84 | .disabled button, 85 | .btn:disabled, 86 | .btn:hover:disabled, 87 | .disabled .btn { 88 | color: #f4f4f4; 89 | border-color: #f4f4f4; 90 | background-color: transparent; 91 | cursor: not-allowed; 92 | } 93 | 94 | #content { 95 | font-family: 'Raleway', sans-serif; 96 | font-weight: 300; 97 | max-width: 1024px; 98 | width: 90%; 99 | margin: 20px auto; 100 | } 101 | b { 102 | font-weight: 600; 103 | } 104 | .img-wrapper { 105 | max-width: 100%; 106 | text-align: center; 107 | padding: 20px 0; 108 | } 109 | .img-wrapper img, 110 | .img-wrapper canvas { 111 | max-width: 100%; 112 | height: auto; 113 | } 114 | .btn-wrapper { 115 | padding: 20px 0; 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PixelFlow](./pixel-flow.gif) 2 |
3 | [![npm](https://img.shields.io/npm/l/pixel-flow.svg?maxAge=2592000)](https://www.npmjs.com/package/pixel-flow) 4 | [![npm](https://img.shields.io/npm/v/pixel-flow.svg?maxAge=2592000)](https://www.npmjs.com/package/pixel-flow) 5 | [![npm](https://img.shields.io/npm/dm/pixel-flow.svg?maxAge=2592000)](https://www.npmjs.com/package/pixel-flow) 6 | [![npm](https://img.shields.io/npm/dt/pixel-flow.svg?maxAge=2592000)](https://www.npmjs.com/package/pixel-flow) 7 | 8 | an image pixelating JavaScript library 9 | 10 | ## [Demo](https://jamesnimlos.github.io/pixel-flow/) 11 | 12 | The main demo uses vanilla JavaScript and presents a few images with example manipulations using the library. 13 | 14 | There is also a [demo using the jQuery plugin](https://jamesnimlos.github.io/pixel-flow/jquery.html) and Green Sock Tween in order to animate the pixelating effect, see the original blog post for more information. 15 | 16 | #### [Blog Post](http://devnimlos.com/professional/pixelflow) 17 | 18 | The blog post includes a write up of the build process and thought process used for version 1. Version 2 was a full re-factor but the logic used is still the same. 19 | 20 | --- 21 | 22 | ## Usage 23 | 24 | The library can be installed from npm 25 | 26 | ```bash 27 | npm install --save pixel-flow 28 | ``` 29 | 30 | ```javascript 31 | let images = Array.from(document.querySelectorAll('img')) 32 | let pixelFlows = images.map((img) => new PixelFlow(img, { resolution: 32 })) 33 | // be aware, creating the pixelate images removes the images from the DOM 34 | 35 | // then you can manipulate individual PixelFlow instances 36 | // wait 5 seconds 37 | setTimeout(() => { 38 | pixelFlows.forEach((pixelFlow) => { 39 | // animate the pixelated images back to normal over 2 seconds 40 | pixelFlow.simpleanimate(0, 2) 41 | }) 42 | }, 5000) 43 | ``` 44 | 45 | ### jQuery plugin 46 | 47 | The jQuery plugin can be installed from npm as well. 48 | 49 | ```bash 50 | npm install --save pixel-flow jquery 51 | ``` 52 | 53 | > This library does not come with jQuery packaged, you must install separately. 54 | 55 | ```javascript 56 | import $ from 'jquery' 57 | import 'pixel-flow/jquery' 58 | // Converts the image to a pixelated image at 32 pixel resolution 59 | var $pixel = $('img').first().PixelFlow({ resolution: 32 }) 60 | 61 | // Runs animation on that same image to return to base image. 62 | // Notice I'm selecting the canvas that replaced the image. 63 | $('canvas').first().PixelFlow('simpleanimate', 0, 2000) 64 | 65 | // You should use the original returned reference since the 66 | // element is no longer an img element but a canvas 67 | $pixel.PixelFlow('update', { resolution: 32 }) 68 | 69 | // or you can access the instance directly by fetching it from 70 | // the jQuery data on the $pixel 71 | var pixel = $pixel.data('plugin_PixelFlow') 72 | pixel.rebase() 73 | ``` 74 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PixelFlow 5 | 6 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
Demo page for PixelFlow.js
18 |

A plugin utilizing a <canvas/> element to pixelate an image

19 |

20 | view jQuery Demo 23 |

24 |

25 | Back to Github 30 |

31 |
32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 |
50 | 51 |
52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 |
60 | 61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 |
70 | 71 |
72 |
73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /public/jquery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PixelFlow 5 | 6 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
Demo page for PixelFlow.js
18 |

A plugin utilizing a <canvas/> element to pixelate an image

19 |

view main demo

20 |

21 | Back to Github 26 |

27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 |
38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 |
56 | 57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 |
66 | 67 |
68 |
69 | 70 | 71 | 75 | 76 | -------------------------------------------------------------------------------- /public/scripts/index-old.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery' 2 | import '../../src/jquery' 3 | 4 | // global variables 5 | var funcOpts = [ 6 | { 7 | linearGradient: { location: [0, 0, 0.65, 1], resolution: [32, 0] }, 8 | drawPixels: { resolution: 16, offsetY: 6 }, 9 | lg: { location: [-1, 0, 0.65, 1] }, 10 | wg: [ 11 | { location: [0.85, 0.9, 1, 1], resolution: [16, 0] }, 12 | { location: [0.7, 0.7, 0.85, 0.85], resolution: [0, 16], rebase: false } 13 | ] 14 | }, 15 | { 16 | linearGradient: { location: [0, 0.1, 0.5, 1], resolution: [32, 0] }, 17 | drawPixels: { resolution: 8 }, 18 | lg: { location: [-1, 0.1, 0.45, 1] }, 19 | wg: [ 20 | { location: [0.85, 0.9, 1, 1], resolution: [16, 0] }, 21 | { location: [0.7, 0.7, 0.85, 0.85], resolution: [0, 16], rebase: false } 22 | ] 23 | }, 24 | { 25 | linearGradient: { location: [0, 0, 0.48, 1], resolution: [32, 0] }, 26 | drawPixels: { resolution: 6 }, 27 | lg: { location: [-1, 0, 0.48, 1] }, 28 | wg: [ 29 | { location: [0.85, 0.9, 1, 1], resolution: [16, 0] }, 30 | { location: [0.7, 0.7, 0.85, 0.85], resolution: [0, 16], rebase: false } 31 | ] 32 | }, 33 | { 34 | linearGradient: { location: [0, 0.5, 0.9, 1], resolution: [0, 32] }, 35 | drawPixels: { resolution: 32 }, 36 | lg: { location: [0, 0.5, 0.9, 1], resolution: [0, 32] }, 37 | wg: [ 38 | { location: [0.85, 0.9, 1, 1], resolution: [16, 0] }, 39 | { location: [0.7, 0.7, 0.85, 0.85], resolution: [0, 16], rebase: false } 40 | ] 41 | } 42 | ] 43 | 44 | // window.load to wait for images 45 | $(document).ready(function() { 46 | var $imgs = $('.img-wrapper img') 47 | Promise.all( 48 | $imgs.map(function(_, img) { 49 | return waitForImage(img) 50 | }) 51 | ).then(function() { 52 | $imgs.PixelFlow({ resolution: 32 }) 53 | 54 | $('.btn-wrapper button').on('click', function(ev) { 55 | var $btn = $(this), 56 | func = $btn.attr('data-func'), 57 | id = $btn.closest('.img-wrapper')[0].id.split('_')[1] 58 | 59 | if (func === 'animateGradient') { 60 | return startGradientAnimation.call(this, ev) 61 | } 62 | 63 | if (func === 'animateGradient_wave') { 64 | return startWaveGradientAnimation.call(this, ev) 65 | } 66 | 67 | $btn 68 | .closest('.img-wrapper') 69 | .find('canvas') 70 | .PixelFlow(func, $.extend({ offsetX: 0 }, funcOpts[id][func] || {})) 71 | }) 72 | }) 73 | }) 74 | 75 | function startGradientAnimation(ev) { 76 | var $btn = $(this), 77 | id = $btn.closest('.img-wrapper')[0].id.split('_')[1], 78 | pixelFlow = $btn 79 | .closest('.img-wrapper') 80 | .find('canvas') 81 | .data('plugin_PixelFlow') 82 | 83 | pixelFlow.offsetX = pixelFlow.width 84 | 85 | TweenMax.fromTo( 86 | pixelFlow, 87 | 2, 88 | { 89 | offsetX: pixelFlow.width, 90 | autoCSS: false 91 | }, 92 | { 93 | offsetX: 0, 94 | autoCSS: false, 95 | onUpdate: tick, 96 | roundProps: 'offsetX', 97 | onUpdateParams: ['{self}', pixelFlow, id] // "{self}" is the tween instance 98 | } 99 | ) 100 | 101 | function tick(tween, pF, key) { 102 | pF.linearGradient(funcOpts[key].lg) 103 | } 104 | } 105 | 106 | function startWaveGradientAnimation() { 107 | var $btn = $(this), 108 | id = $btn.closest('.img-wrapper')[0].id.split('_')[1], 109 | pixelFlow = $btn 110 | .closest('.img-wrapper') 111 | .find('canvas') 112 | .data('plugin_PixelFlow') 113 | 114 | pixelFlow.offsetX = -pixelFlow.width 115 | 116 | TweenMax.fromTo( 117 | pixelFlow, 118 | 2, 119 | { 120 | offsetX: -pixelFlow.width, 121 | autoCSS: false 122 | }, 123 | { 124 | offsetX: 0, 125 | autoCSS: false, 126 | onUpdate: tick, 127 | yoyo: true, 128 | repeat: 1, 129 | roundProps: 'offsetX', 130 | onUpdateParams: ['{self}', pixelFlow, id] // "{self}" is the tween instance 131 | } 132 | ) 133 | 134 | function tick(tween, pF, key) { 135 | pF.linearGradient(funcOpts[key].wg[0]) 136 | pF.linearGradient(funcOpts[key].wg[1]) 137 | } 138 | } 139 | 140 | function waitForImage(img) { 141 | return new Promise(function(resolve, reject) { 142 | var imgObj = new Image() 143 | imgObj.onload = function() { 144 | resolve(img) 145 | } 146 | imgObj.onerror = function() { 147 | reject() 148 | } 149 | imgObj.src = img.src 150 | }) 151 | } 152 | -------------------------------------------------------------------------------- /public/scripts/index.mjs: -------------------------------------------------------------------------------- 1 | import PixelFlow from '../../src/pixel-flow.js' 2 | 3 | function domReady() { 4 | return new Promise(resolve => { 5 | if ( 6 | document.readyState === 'interactive' || 7 | document.readystate === 'completed' 8 | ) { 9 | Promise.resolve().then(resolve) 10 | } else { 11 | document.addEventListener('DOMContentLoaded', () => resolve()) 12 | } 13 | }) 14 | } 15 | 16 | function waitForImage(img) { 17 | return new Promise(function(resolve, reject) { 18 | var imgObj = new Image() 19 | imgObj.onload = function() { 20 | resolve(img) 21 | } 22 | imgObj.onerror = function() { 23 | reject() 24 | } 25 | imgObj.src = img.src 26 | }) 27 | } 28 | 29 | function runAnimation(tick, duration) { 30 | duration = duration * 1000 31 | const start = Date.now() 32 | function ticker() { 33 | const now = Date.now() 34 | const completionRatio = (now - start) / duration 35 | if (completionRatio >= 1) { 36 | tick(1) 37 | } else { 38 | tick(completionRatio) 39 | window.requestAnimationFrame(ticker) 40 | } 41 | } 42 | window.requestAnimationFrame(ticker) 43 | } 44 | 45 | function easeOutQuad(t) { 46 | return t * (2 - t) 47 | } 48 | 49 | function easeInOutQuad(ratio) { 50 | return ratio < 0.5 ? 2 * ratio * ratio : -1 + (4 - 2 * ratio) * ratio 51 | } 52 | 53 | function runGradientAnimation(pixelFlow, options) { 54 | const startOffsetX = pixelFlow.width 55 | runAnimation(ratioComplete => { 56 | const offsetX = Math.round( 57 | startOffsetX - startOffsetX * easeOutQuad(ratioComplete) 58 | ) 59 | pixelFlow.linearGradient(Object.assign({ offsetX }, options)) 60 | }, 2) 61 | } 62 | 63 | // runs across to right, then back left 64 | function runWaveAnimation(pixelFlow, [optionsLeft, optionsRight]) { 65 | const width = pixelFlow.width 66 | runAnimation(ratioComplete => { 67 | const offsetMultiplier = -easeInOutQuad(Math.abs(ratioComplete - 0.5) * 2) 68 | const offsetX = Math.round(width * offsetMultiplier) 69 | pixelFlow.linearGradient(Object.assign({ offsetX }, optionsLeft)) 70 | pixelFlow.linearGradient(Object.assign({ offsetX }, optionsRight)) 71 | }, 4) 72 | } 73 | 74 | !(function() { 75 | let pixelFlows 76 | var transformOptions = [ 77 | { 78 | linearGradient: { location: [0, 0, 0.65, 1], resolution: [32, 0] }, 79 | drawPixels: { resolution: 16, offsetY: 6 }, 80 | lg: { location: [-1, 0, 0.65, 1] }, 81 | wg: [ 82 | { location: [0.85, 0.9, 1, 1], resolution: [16, 0] }, 83 | { location: [0.7, 0.7, 0.85, 0.85], resolution: [0, 16], rebase: false } 84 | ] 85 | }, 86 | { 87 | linearGradient: { location: [0, 0.1, 0.5, 1], resolution: [32, 0] }, 88 | drawPixels: { resolution: 8 }, 89 | lg: { location: [-1, 0.1, 0.45, 1] }, 90 | wg: [ 91 | { location: [0.85, 0.9, 1, 1], resolution: [16, 0] }, 92 | { location: [0.7, 0.7, 0.85, 0.85], resolution: [0, 16], rebase: false } 93 | ] 94 | }, 95 | { 96 | linearGradient: { location: [0, 0, 0.48, 1], resolution: [32, 0] }, 97 | drawPixels: { resolution: 6 }, 98 | lg: { location: [-1, 0, 0.48, 1] }, 99 | wg: [ 100 | { location: [0.85, 0.9, 1, 1], resolution: [16, 0] }, 101 | { location: [0.7, 0.7, 0.85, 0.85], resolution: [0, 16], rebase: false } 102 | ] 103 | }, 104 | { 105 | linearGradient: { location: [0, 0.5, 0.9, 1], resolution: [0, 32] }, 106 | drawPixels: { resolution: 32 }, 107 | lg: { location: [0, 0.5, 0.9, 1], resolution: [0, 32] }, 108 | wg: [ 109 | { location: [0.85, 0.9, 1, 1], resolution: [16, 0] }, 110 | { location: [0.7, 0.7, 0.85, 0.85], resolution: [0, 16], rebase: false } 111 | ] 112 | } 113 | ] 114 | 115 | function buttonListener(index) { 116 | return event => { 117 | const button = event.target 118 | const pixelFlow = pixelFlows[index] 119 | const requestedTransform = button.dataset.func 120 | 121 | if (requestedTransform === 'animateGradient') { 122 | runGradientAnimation(pixelFlow, transformOptions[index].lg) 123 | } else if (requestedTransform === 'animateGradient_wave') { 124 | runWaveAnimation(pixelFlow, transformOptions[index].wg) 125 | } else { 126 | pixelFlow[requestedTransform]( 127 | Object.assign( 128 | { offsetX: 0 }, 129 | transformOptions[index][requestedTransform] || {} 130 | ) 131 | ) 132 | } 133 | } 134 | } 135 | 136 | domReady() 137 | .then(() => { 138 | let images = Array.from(document.querySelectorAll('.img-wrapper img')) 139 | return Promise.all(images.map(waitForImage)) 140 | }) 141 | .then(images => { 142 | pixelFlows = images.map(img => new PixelFlow(img, { resolution: 32 })) 143 | 144 | const imageWrappers = Array.from( 145 | document.querySelectorAll('.img-wrapper') 146 | ) 147 | for (let i = 0; i < pixelFlows.length; i++) { 148 | const pixelFlow = pixelFlows[i] 149 | const imgWrap = imageWrappers[i] 150 | const buttons = Array.from(imgWrap.getElementsByTagName('button')) 151 | buttons.forEach(b => 152 | b.addEventListener('click', buttonListener(i), false) 153 | ) 154 | } 155 | }) 156 | })() 157 | -------------------------------------------------------------------------------- /src/pixel-flow.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** https://github.com/JamesNimlos/pixel-flow 3 | ** 4 | ** Developed by 5 | ** - James Nimlos 6 | ** 7 | ** Licensed under MIT license 8 | */ 9 | 10 | 'use strict' 11 | 12 | // utility functions 13 | function evenNum(num) { 14 | if (typeof num !== 'number') return NaN 15 | return ~~num - (~~num % 2) 16 | } 17 | 18 | function convPerc(percentage) { 19 | return Number(percentage.replace(/[\s%]/g, '')) / 100 20 | } 21 | 22 | var defaultOptions = { 23 | resolution: 16, 24 | offsetX: 0, 25 | offsetY: 0 26 | } 27 | 28 | /** 29 | * @param {HTMLImageElement} img 30 | * @param {Object} [options] 31 | */ 32 | function PixelFlow(img, options) { 33 | if (!(img instanceof HTMLImageElement)) 34 | return ( 35 | window.console && 36 | console.error('The provided element is not an HTMLImageElement.') 37 | ) 38 | 39 | this.options = Object.assign({}, defaultOptions, options) 40 | 41 | this.img = img 42 | 43 | try { 44 | this.setUpCanvas(img) 45 | 46 | this.drawPixels() 47 | 48 | // replace image with canvas 49 | img.parentNode.replaceChild(this.canvas, img) 50 | } catch (err) { 51 | window.console && console.error('PixelFlow could not not be created.') 52 | window.console && console.error(err) 53 | } 54 | } 55 | 56 | /** 57 | * Factory function 58 | * @param {HTMLImageElement} img 59 | * @param {Object} opts 60 | * @return {PixelFlow} 61 | */ 62 | PixelFlow.build = function(img, opts) { 63 | return new PixelFlow(img, opts) 64 | } 65 | 66 | /** 67 | * Draws a full column on the canvas 68 | * @param {number} left - pixel location of left side of column 69 | * @param {number} colWidth - width of the column being drawn in pixels 70 | * @param {number} [pixHeight] - height of each 'pixel' being drawn 71 | * @return {void} 72 | */ 73 | PixelFlow.prototype.drawCol = function(left, colWidth, pixHeight) { 74 | if (colWidth <= 2 || pixHeight <= 2) return 75 | if (left + colWidth < 0) return 76 | //local variables 77 | var w = this.width 78 | var h = this.height 79 | var options = this.options 80 | var res = colWidth 81 | var resH = pixHeight || res 82 | var offsetX = options.offestX 83 | var offsetY = options.offsetY 84 | var rows = h / resH + 1 85 | 86 | var row 87 | var x = left || 0 88 | var y 89 | var pixelY 90 | var pixelX 91 | var pixelIndex 92 | 93 | // skip if outside canvas 94 | if (x + res <= 0 || x >= w) return 95 | // normalize x so shapes around edges get color 96 | pixelX = Math.max(Math.min(x, w - 1), 0) 97 | 98 | for (row = 0; row < rows; row++) { 99 | y = (row - 0.5) * resH + offsetY 100 | // normalize y so shapes around edges get color 101 | pixelY = Math.max(Math.min(y, h - 1), 0) 102 | 103 | pixelIndex = (pixelX + pixelY * w) * 4 104 | 105 | this.drawPixel(pixelIndex, x, y, res, resH) 106 | } 107 | } 108 | 109 | /** 110 | * Draws an individual block or 'pixel' on the canvas 111 | * @param {number} pixelIndex - index of the pixel from the image data 112 | * @param {number} x - horizontal position of the 'pixel' from left edge being 0 113 | * @param {number} y - vertical position of the top left corner of the 'pixel' 114 | * @param {number} w - width of the 'pixel' 115 | * @param {number} h - height of the 'pixel' 116 | * @return {PixelFlow} 117 | */ 118 | PixelFlow.prototype.drawPixel = function(pixelIndex, x, y, w, h) { 119 | var ctx = this.ctx, 120 | imgData = this.imgData, 121 | red = imgData[pixelIndex], 122 | green = imgData[pixelIndex + 1], 123 | blue = imgData[pixelIndex + 2], 124 | alpha = 1, 125 | pixelAlpha = alpha * (imgData[pixelIndex + 3] / 255) 126 | 127 | // sets the color using pixelIndex reference for the 'pixel' 128 | ctx.fillStyle = 129 | 'rgba(' + red + ',' + green + ',' + blue + ',' + pixelAlpha + ')' 130 | 131 | // draws pixel 132 | ctx.fillRect(x, y, w, h) 133 | 134 | return this 135 | } 136 | 137 | /** 138 | * Draws the entire pixelation using the current settings on the 139 | * PixelFlow instance. 'Pixel' size is constant throughout. 140 | * @return {PixelFlow} 141 | */ 142 | PixelFlow.prototype.drawPixels = function(options) { 143 | //local variables 144 | options = Object.assign(this.options, options) 145 | var w = this.width, 146 | h = this.height, 147 | ctx = this.ctx, 148 | imgData = this.imgData, 149 | res = options.resolution, 150 | size = options.size || res, 151 | alpha = 1, 152 | offsetX = options.offsetX, 153 | offsetY = options.offsetY, 154 | cols = w / res + 1, 155 | rows = h / res + 1, 156 | halfSize = size / 2 157 | 158 | if (res < 4) return this.rebase() 159 | 160 | var row, col, x, y, pixelY, pixelX, pixelIndex, red, green, blue, pixelAlpha 161 | 162 | for (row = 0; row < rows; row++) { 163 | y = (row - 0.5) * res + offsetY 164 | // normalize y so shapes around edges get color 165 | pixelY = Math.max(Math.min(y, h - 1), 0) 166 | 167 | for (col = 0; col < cols; col++) { 168 | x = (col - 0.5) * res + offsetX 169 | // normalize y so shapes around edges get color 170 | pixelX = Math.max(Math.min(x, w - 1), 0) 171 | pixelIndex = (pixelX + pixelY * w) * 4 172 | red = imgData[pixelIndex + 0] 173 | green = imgData[pixelIndex + 1] 174 | blue = imgData[pixelIndex + 2] 175 | pixelAlpha = alpha * (imgData[pixelIndex + 3] / 255) 176 | 177 | ctx.fillStyle = 178 | 'rgba(' + red + ',' + green + ',' + blue + ',' + pixelAlpha + ')' 179 | 180 | // square 181 | ctx.fillRect(x - halfSize, y - halfSize, size, size) 182 | } // col 183 | } // row 184 | 185 | return this 186 | } 187 | 188 | /** 189 | * Draws a linear, vertical gradient using the provided options 190 | * Example options object: 191 | * var options = { 192 | * resolution : [ 32, 0 ], 193 | * location : [ 0, 0.25, 0.75, 1] 194 | * } 195 | * This will generate a gradient that starts at size 32 pixels on the left 196 | * and will be that size until 25% of the way through the image, then it will 197 | * begin decreasing linearly until it should be normal resolution 198 | * (anything less than 4) at or just after 75% of the way throught the image. 199 | * 200 | * @param {Object} options 201 | * @return {PixelFlow} 202 | */ 203 | PixelFlow.prototype.linearGradient = function(options) { 204 | // TODO: create a better default system. 205 | options = Object.assign( 206 | {}, 207 | this.options, 208 | { location: [0, 0.25, 0.75, 1], resolution: [32, 0], rebase: true }, 209 | options 210 | ) 211 | 212 | if (options.rebase) this.rebase() 213 | // needs to wait until after a rebase 214 | this.options = options 215 | 216 | if ( 217 | !Array.isArray(options.location) || 218 | !Array.isArray(options.resolution) || 219 | options.location.length < 4 || 220 | options.resolution.length < 2 221 | ) { 222 | window.console && 223 | console.error( 224 | 'You have not provided the necessary options for a linear gradient.' 225 | ) 226 | return this 227 | } 228 | 229 | var startRes = evenNum(options.resolution[0]), 230 | endRes = evenNum(options.resolution[1]), 231 | startPoint = options.location[0] || 0, 232 | gradStart = options.location[1] || 0.25, 233 | gradEnd = options.location[2] || 0.75, 234 | endPoint = options.location[3] || 1, 235 | offsetX = options.offsetX || this.offsetX || 0 236 | 237 | if (typeof startPoint === 'string') startPoint = convPerc(startPoint) 238 | if (typeof gradStart === 'string') gradStart = convPerc(gradStart) 239 | if (typeof gradEnd === 'string') gradEnd = convPerc(gradEnd) 240 | if (typeof endPoint === 'string') endPoint = convPerc(endPoint) 241 | 242 | // calculate cols 243 | var w = this.width 244 | 245 | // points to pixels 246 | startPoint *= w 247 | gradStart *= w 248 | gradEnd *= w 249 | endPoint *= w 250 | 251 | var cols = [] 252 | if (startRes > 0) cols.push(evenNum(startPoint)) 253 | 254 | // modify start points for best spacing 255 | // gradStart -= (gradStart - startPoint) % startRes; 256 | // gradEnd += endRes - (( endPoint - gradEnd ) % (endRes || 1)); 257 | 258 | // TODO: change this to a factory which could return an addColRange function 259 | // using one of the different types between linear, exponential, bezier 260 | function addColRange(arr, leftStart, startWidth, endWidth, rightEnd) { 261 | if (rightEnd < leftStart) return 262 | 263 | if (startWidth === endWidth) { 264 | if (startWidth === 0) return 265 | 266 | var place = leftStart 267 | while (place <= rightEnd) { 268 | place += startWidth 269 | cols.push(evenNum(place)) // sub even pixels negatively affects presentation 270 | } 271 | } else { 272 | var Rl = startWidth || 4 // in case resolution is zero 273 | var Rs = endWidth || 4 274 | var t = rightEnd - leftStart 275 | // var mx = ( t / Rl ); 276 | // var mi = ( t / Rs ); 277 | 278 | // var mc = Math.floor( ( mx + mi ) / 2 ); 279 | // var s = 2 * ( t - ( mc * Rs ) ) / ( mc * ( mc + 1 ) ); 280 | var place = leftStart 281 | var cWidth = startWidth 282 | var exp 283 | 284 | do { 285 | // linear regression relationship but could be changed 286 | // TODO: bezier curve regression 287 | exp = Rl - (Rl - Rs) * ((place - leftStart) / t) 288 | cWidth = exp 289 | place += evenNum(cWidth) 290 | cols.push(evenNum(place)) 291 | } while (place <= rightEnd && cWidth >= 2) 292 | } 293 | } 294 | //calc cols from gradStart to startPoint 295 | addColRange(cols, startPoint, startRes, startRes, gradStart) 296 | addColRange( 297 | cols, 298 | cols[cols.length - 1] || gradStart, 299 | startRes, 300 | endRes, 301 | gradEnd 302 | ) 303 | addColRange(cols, cols[cols.length - 1], endRes, endRes, endPoint) 304 | 305 | for (var c = 1; c < cols.length; c++) { 306 | this.drawCol(cols[c - 1] + offsetX, cols[c] - cols[c - 1]) 307 | } 308 | 309 | return this 310 | } 311 | 312 | /** 313 | * Returns the canvas to display the original image 314 | * @return {PixelFlow} 315 | */ 316 | PixelFlow.prototype.rebase = function() { 317 | this.options = { 318 | resolution: 0, 319 | offsetX: 0, 320 | offsetY: 0 321 | } 322 | 323 | this.ctx.drawImage(this.img, 0, 0) 324 | 325 | return this 326 | } 327 | 328 | /** 329 | * creates the canvas element and copies the image onto it 330 | * also creates a back-up canvas 331 | * @param {HTMLImageElement} img 332 | * @return {PixelFlow} 333 | */ 334 | PixelFlow.prototype.setUpCanvas = function(img) { 335 | // create canvas 336 | var canvas = (this.canvas = document.createElement('canvas')) 337 | this.ctx = canvas.getContext('2d') 338 | 339 | //make virtual duplicate for safe keeping of picture data 340 | this._copyCanvas = document.createElement('canvas') 341 | this._copyCtx = this._copyCanvas.getContext('2d') 342 | 343 | // copy basic attributes from img to canvas 344 | canvas.className = img.className 345 | canvas.id = img.id 346 | 347 | var w = (this.width = this.canvas.width = this._copyCanvas.width = 348 | img.naturalWidth % 2 === 0 ? img.naturalWidth : img.naturalWidth - 1) 349 | var h = (this.height = this.canvas.height = this._copyCanvas.height = 350 | img.naturalHeight % 2 === 0 ? img.naturalHeight : img.naturalHeight - 1) 351 | 352 | // draw on both canvases 353 | this.ctx.drawImage(img, 0, 0) 354 | this._copyCtx.drawImage(img, 0, 0) 355 | 356 | this.imgData = this._copyCtx.getImageData(0, 0, w, h).data 357 | 358 | this.ctx.clearRect(0, 0, w, h) 359 | 360 | return this 361 | } 362 | 363 | /** 364 | * @param {number} endResolution - resolution to stop the animation at 365 | * @param {number} duration - length of the animation 366 | * @return {PixelFlow} 367 | */ 368 | PixelFlow.prototype.simpleanimate = function(endResolution, duration) { 369 | var er = evenNum(endResolution) 370 | // if end resolution is the same as the start then exit 371 | if (this.options.resolution === er) return 372 | var startRes = this.options.resolution 373 | var res = startRes 374 | var startTime = Date.now() 375 | var elapsed = 0 376 | var dur = duration 377 | 378 | var PixelFlowAnimationLoop = function() { 379 | var time = Date.now() 380 | 381 | res = startRes + (er - startRes) * ((time - startTime) / duration) 382 | 383 | res = evenNum(res) 384 | 385 | if (res >= 2) { 386 | // since we only run for even numbers this happens 387 | // during long animations 388 | if (this.options.resolution !== res) { 389 | this.update({ resolution: evenNum(res) }) 390 | } 391 | } else { 392 | this.rebase({}) 393 | } 394 | 395 | if ((er > startRes && res < er) || (er < startRes && res > er)) { 396 | window.requestAnimationFrame(PixelFlowAnimationLoop) 397 | } 398 | }.bind(this) 399 | 400 | window.requestAnimationFrame(PixelFlowAnimationLoop) 401 | 402 | return this 403 | } 404 | 405 | /** 406 | * updates the canvas with the new options for resolution 407 | * @param {Object} options - options to update the canvas with 408 | * @return {PixelFlow} 409 | */ 410 | PixelFlow.prototype.update = function(options) { 411 | Object.assign(this.options, options) 412 | 413 | this.options.resolution = evenNum(this.options.resolution) 414 | 415 | this.drawPixels() 416 | 417 | return this 418 | } 419 | 420 | export default PixelFlow 421 | --------------------------------------------------------------------------------