├── .gitignore ├── .jshintrc ├── gulpfile.js ├── package.json ├── sandbox.html ├── js ├── vector.js ├── particle.js └── breathing-halftone.js ├── README.md └── dist ├── breathing-halftone.pkgd.min.js └── breathing-halftone.pkgd.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "devel": true, 4 | "strict": true, 5 | "undef": true, 6 | "unused": true 7 | } 8 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true, strict: false */ 2 | 3 | var gulp = require('gulp'); 4 | var concat = require('gulp-concat'); 5 | var rename = require('gulp-rename'); 6 | var uglify = require('gulp-uglify'); 7 | 8 | gulp.task( 'dist', function() { 9 | var jsFiles = [ 10 | 'js/vector.js', 11 | 'js/particle.js', 12 | 'js/breathing-halftone.js' 13 | ]; 14 | 15 | gulp.src( jsFiles ) 16 | .pipe( concat('breathing-halftone.pkgd.js') ) 17 | .pipe( gulp.dest('dist') ); 18 | 19 | gulp.src( jsFiles ) 20 | .pipe( rename('breathing-halftone.pkgd.min.js') ) 21 | .pipe( uglify({ preserveComments: 'some' }) ) 22 | .pipe( gulp.dest('dist') ); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "breathing-halftone", 3 | "version": "0.1.0", 4 | "description": "Images go whoa with lots of dots", 5 | "main": "js/breathing-halftone.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "gulp": "^3.5.5", 9 | "gulp-concat": "^2.1.7", 10 | "gulp-markdown": "^0.1.2", 11 | "gulp-rename": "^1.1.0", 12 | "highlight.js": "^8.0.0", 13 | "through2": "^0.4.1", 14 | "gulp-uglify": "^1.0.1" 15 | }, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/desandro/breathing-halftone.git" 22 | }, 23 | "author": "David DeSandro", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/desandro/breathing-halftone/issues" 27 | }, 28 | "homepage": "https://github.com/desandro/breathing-halftone" 29 | } 30 | -------------------------------------------------------------------------------- /sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | breathing halftone 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /js/vector.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Breathing Halftone 3 | * Images go whoa with lots of floaty dots 4 | * http://breathing-halftone.desandro.com 5 | */ 6 | 7 | ( function( window ) { 8 | 9 | 'use strict'; 10 | 11 | // ----- vars ----- // 12 | 13 | var Halftone = window.BreathingHalftone = window.BreathingHalftone || {}; 14 | 15 | // -------------------------- Vector -------------------------- // 16 | 17 | function Vector( x, y ) { 18 | this.set( x || 0, y || 0 ); 19 | } 20 | 21 | Vector.prototype.set = function( x, y ) { 22 | this.x = x; 23 | this.y = y; 24 | }; 25 | 26 | Vector.prototype.add = function( v ) { 27 | this.x += v.x; 28 | this.y += v.y; 29 | }; 30 | 31 | Vector.prototype.subtract = function( v ) { 32 | this.x -= v.x; 33 | this.y -= v.y; 34 | }; 35 | 36 | Vector.prototype.scale = function( s ) { 37 | this.x *= s; 38 | this.y *= s; 39 | }; 40 | 41 | Vector.prototype.multiply = function( v ) { 42 | this.x *= v.x; 43 | this.y *= v.y; 44 | }; 45 | 46 | // custom getter whaaaaaaat 47 | Object.defineProperty( Vector.prototype, 'magnitude', { 48 | get: function() { 49 | return Math.sqrt( this.x * this.x + this.y * this.y ); 50 | } 51 | }); 52 | 53 | // ----- class functions ----- // 54 | 55 | Vector.subtract = function( a, b ) { 56 | return new Vector( a.x - b.x, a.y - b.y ); 57 | }; 58 | 59 | Vector.add = function( a, b ) { 60 | return new Vector( a.x + b.x, a.y + b.y ); 61 | }; 62 | 63 | Vector.copy = function( v ) { 64 | return new Vector( v.x, v.y ); 65 | }; 66 | 67 | Halftone.Vector = Vector; 68 | 69 | })( window ); 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Breathing Halftone 2 | 3 | _Images go whoa with lots of floaty dots_ 4 | 5 | Made for [Yaron](http://yaronschoen.com/info) 6 | 7 | ## Install 8 | 9 | [breathing-halftone.pkgd.js](http://breathing-halftone.desandro.com/dist/breathing-halftone.pkgd.js) 10 | 11 | [breathing-halftone.pkgd.min.js](http://breathing-halftone.desandro.com/dist/breathing-halftone.pkgd.min.js) 12 | 13 | ## Usage 14 | 15 | ``` js 16 | // get the image 17 | // jquery 18 | var img = $('#hero img')[0]; 19 | // or vanilla JS 20 | var img = document.querySelector('#hero img'); 21 | 22 | // init halftone 23 | new BreathingHalftone( img, { 24 | // options... 25 | }); 26 | ``` 27 | 28 | Browsers that do not support `` will fall back to the original image. 29 | 30 | Set `data-src` to use a different source image, so you can display stylized halftone-y image as a fallback. 31 | 32 | ``` html 33 | 34 | ``` 35 | 36 | ## Options 37 | 38 | There are a bunch of options so you can fine-tune to your heart's content. 39 | 40 | ``` js 41 | // default options 42 | { 43 | // ----- dot size ----- // 44 | 45 | dotSize: 1/40, 46 | // size of dots 47 | // as a fraction of the diagonal of the image 48 | // smaller dots = more dots = poorer performance 49 | 50 | dotSizeThreshold: 0.05, 51 | // hides dots that are smaller than a percentage 52 | 53 | initVelocity: 0.02, 54 | // speed at which dots initially grow 55 | 56 | oscPeriod: 3, 57 | // duration in seconds of a cycle of dot size oscilliation or 'breathing' 58 | 59 | oscAmplitude: 0.2 60 | // percentage of change of oscillation 61 | 62 | // ----- color & layout ----- // 63 | 64 | isAdditive: false, 65 | // additive is black with RGB dots, 66 | // subtractive is white with CMK dots 67 | 68 | isRadial: false, 69 | // enables radial grid layout 70 | 71 | channels: [ 'red', 'green', 'blue' ], 72 | // layers of dots 73 | // 'lum' is another supported channel, for luminosity 74 | 75 | isChannelLens: true, 76 | // disables changing size of dots when displaced 77 | 78 | // ----- behavior ----- // 79 | 80 | friction: 0.06, 81 | // lower makes dots easier to move, higher makes it harder 82 | 83 | hoverDiameter: 0.3, 84 | // size of hover effect 85 | // as a fraction of the diagonal of the image 86 | 87 | hoverForce: -0.02, 88 | // amount of force of hover effect 89 | // negative values pull dots in, positive push out 90 | 91 | activeDiameter: 0.6, 92 | // size of click/tap effect 93 | // as a fraction of the diagonal of the image 94 | 95 | activeForce: 0.01 96 | // amount of force of hover effect 97 | // negative values pull dots in, positive push out 98 | } 99 | ``` 100 | 101 | ## Gotchas 102 | 103 | As the halftone is low resolution, you don't need a high resolution source image. 104 | 105 | Images must be hosted on the same domain as the site. Cross-domain images cannot be used for [security according to the `` spec](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#security-with-canvas-elements). 106 | 107 | Smaller dots = lots more dots = poorer browser performance. 108 | 109 | As [Firefox and IE do not support `darker` compositing](http://dropshado.ws/post/77229081704/firefox-doesnt-support-canvas-composite-darker), so these browsers will fallback to simple black and white design, using `channels: [ 'lum' ]`. 110 | 111 | ## MIT License 112 | 113 | Breathing Halftone is released under the [MIT License](http://desandro.mit-license.org/). Have at it. 114 | -------------------------------------------------------------------------------- /js/particle.js: -------------------------------------------------------------------------------- 1 | ( function( window ) { 2 | 3 | 'use strict'; 4 | 5 | // ----- vars ----- // 6 | 7 | var TAU = Math.PI * 2; 8 | 9 | function getNow() { 10 | return new Date(); 11 | } 12 | 13 | // -------------------------- -------------------------- // 14 | 15 | var Halftone = window.BreathingHalftone || {}; 16 | var Vector = Halftone.Vector; 17 | 18 | // -------------------------- Particle -------------------------- // 19 | 20 | function Particle( properties ) { 21 | this.channel = properties.channel; 22 | this.origin = properties.origin; 23 | this.parent = properties.parent; 24 | this.friction = properties.friction; 25 | 26 | this.position = Vector.copy( this.origin ); 27 | this.velocity = new Vector(); 28 | this.acceleration = new Vector(); 29 | 30 | this.naturalSize = properties.naturalSize; 31 | this.size = 0; 32 | this.sizeVelocity = 0; 33 | this.oscSize = 0; 34 | this.initSize = 0; 35 | this.initSizeVelocity = ( Math.random() * 0.5 + 0.5 ) * 36 | this.parent.options.initVelocity; 37 | 38 | this.oscillationOffset = Math.random() * TAU; 39 | this.oscillationMagnitude = Math.random(); 40 | this.isVisible = false; 41 | } 42 | 43 | Particle.prototype.applyForce = function( force ) { 44 | this.acceleration.add( force ); 45 | }; 46 | 47 | Particle.prototype.update = function() { 48 | // stagger starting 49 | if ( !this.isVisible && Math.random() > 0.03 ) { 50 | return; 51 | } 52 | this.isVisible = true; 53 | 54 | this.applyOriginAttraction(); 55 | 56 | // velocity 57 | this.velocity.add( this.acceleration ); 58 | this.velocity.scale( 1 - this.friction ); 59 | // position 60 | this.position.add( this.velocity ); 61 | // reset acceleration 62 | this.acceleration.set( 0, 0 ); 63 | 64 | this.calculateSize(); 65 | }; 66 | 67 | Particle.prototype.render = function( ctx ) { 68 | 69 | var size = this.size * this.oscSize; 70 | // apply initSize with easing 71 | var initSize = Math.cos( this.initSize * TAU / 2 ) * -0.5 + 0.5; 72 | size *= initSize; 73 | size = Math.max( 0, size ); 74 | ctx.beginPath(); 75 | ctx.arc( this.position.x, this.position.y, size, 0, TAU ); 76 | ctx.fill(); 77 | ctx.closePath(); 78 | }; 79 | 80 | Particle.prototype.calculateSize = function() { 81 | 82 | if ( this.initSize !== 1 ) { 83 | this.initSize += this.initSizeVelocity; 84 | this.initSize = Math.min( 1, this.initSize ); 85 | } 86 | 87 | var targetSize = this.naturalSize * this.getChannelValue(); 88 | 89 | // use accel/velocity to smooth changes in size 90 | var sizeAcceleration = ( targetSize - this.size ) * 0.1; 91 | this.sizeVelocity += sizeAcceleration; 92 | // friction 93 | this.sizeVelocity *= ( 1 - 0.3 ); 94 | this.size += this.sizeVelocity; 95 | 96 | // oscillation size 97 | var now = getNow(); 98 | var opts = this.parent.options; 99 | var oscSize = ( now / ( 1000 * opts.oscPeriod ) ) * TAU; 100 | oscSize = Math.cos( oscSize + this.oscillationOffset ); 101 | oscSize = oscSize * opts.oscAmplitude + 1; 102 | this.oscSize = oscSize; 103 | }; 104 | 105 | Particle.prototype.getChannelValue = function() { 106 | var channelValue; 107 | // return origin channel value if not lens 108 | var position = this.parent.options.isChannelLens ? this.position : this.origin; 109 | if ( this.parent.options.isChannelLens ) { 110 | channelValue = this.parent.getPixelChannelValue( position.x, position.y, this.channel ); 111 | } else { 112 | if ( !this.originChannelValue ) { 113 | this.originChannelValue = this.parent.getPixelChannelValue( position.x, position.y, this.channel ); 114 | } 115 | channelValue = this.originChannelValue; 116 | } 117 | 118 | return channelValue; 119 | }; 120 | 121 | Particle.prototype.applyOriginAttraction = function() { 122 | var attraction = Vector.subtract( this.position, this.origin ); 123 | attraction.scale( -0.02 ); 124 | this.applyForce( attraction ); 125 | }; 126 | 127 | Halftone.Particle = Particle; 128 | 129 | })( window ); 130 | -------------------------------------------------------------------------------- /dist/breathing-halftone.pkgd.min.js: -------------------------------------------------------------------------------- 1 | !function(t){"use strict";function i(t,e,s){for(var n in e){var a=e[n];t[n]=s&&"object"==typeof a&&!c(a)?i(t[n]||{},a,!0):a}return t}function e(t,i){var e=i.parentNode,s=i.nextElementSibling;s?e.insertBefore(t,s):e.appendChild(t)}function s(t,e){this.options=i({},this.constructor.defaults,!0),i(this.options,e,!0),this.img=t,l.canvas&&this.create()}function n(){var t=document.createElement("canvas"),i=t.getContext("2d");return{canvas:t,ctx:i}}function a(t,i,e){var s=t.prototype[i],n=i+"Timeout";t.prototype[i]=function(){var t=this[n];t&&clearTimeout(t);var i=arguments;this[n]=setTimeout(function(){s.apply(this,i),delete this[n]}.bind(this),e||100)}}var o=2*Math.PI,r=Math.sqrt(2),h=Object.prototype.toString,c=Array.isArray||function(t){return"[object Array]"===h.call(t)},l={};!function(){var t=document.createElement("canvas"),i=t.getContext&&t.getContext("2d");if(l.canvas=!!i,l.canvas){t.width=1,t.height=1,i.globalCompositeOperation="darker",i.fillStyle="#F00",i.fillRect(0,0,1,1),i.fillStyle="#999",i.fillRect(0,0,1,1);var e=i.getImageData(0,0,1,1).data;l.darker=153===e[0]&&0===e[1]}}();for(var v,d=0,u="webkit moz ms o".split(" "),p=t.requestAnimationFrame,g=t.cancelAnimationFrame,m=0;mi;i++){var a=this.channels[i];this.proxyCanvases[a]=n()}this.loadImage(),this.canvasPosition=new y,this.getCanvasPosition(),this.cursors={},this.addCursor("mouse",{pageX:-1e5,pageY:-1e5}),this.bindEvents()},s.prototype.getCanvasPosition=function(){var i=this.canvas.getBoundingClientRect(),e=i.left+t.pageXOffset,s=i.top+t.pageYOffset;this.canvasPosition.set(e,s),this.canvasScale=this.width?this.width/this.canvas.offsetWidth:1},s.prototype.loadImage=function(){var t=this.img.getAttribute("data-src")||this.img.src,i=new Image;i.onload=function(){this.onImgLoad()}.bind(this),i.src=t,this.img.src!==t&&(this.img.src=t)},s.prototype.onImgLoad=function(){this.getImgData(),this.resizeCanvas(),this.getCanvasPosition(),this.img.style.display="none",this.getCanvasPosition(),this.initParticles(),this.animate()},s.prototype.getImgData=function(){var t=n(),i=t.canvas,e=t.ctx;this.imgWidth=i.width=this.img.naturalWidth,this.imgHeight=i.height=this.img.naturalHeight,e.drawImage(this.img,0,0),this.imgData=e.getImageData(0,0,this.imgWidth,this.imgHeight).data},s.prototype.resizeCanvas=function(){var t=this.width=this.img.offsetWidth,i=this.height=this.img.offsetHeight;this.diagonal=Math.sqrt(t*t+i*i),this.imgScale=this.width/this.imgWidth,this.gridSize=this.options.dotSize*this.diagonal;for(var e in this.proxyCanvases){var s=this.proxyCanvases[e];s.canvas.width=t,s.canvas.height=i}this.canvas.width=t,this.canvas.height=i},s.prototype.initParticles=function(){var t=this.options.isRadial?"getRadialGridParticles":"getCartesianGridParticles";this.particles=[],this.channelParticles={};for(var i={red:1,green:2.5,blue:5,lum:4},e=0,s=this.channels.length;s>e;e++){var n=this.channels[e],a=i[n],o=this[t](n,a);this.channelParticles[n]=o,this.particles=this.particles.concat(o)}},s.prototype.animate=function(){this.isActive&&(this.update(),this.render(),p(this.animate.bind(this)))},s.prototype.update=function(){for(var t=0,i=this.particles.length;i>t;t++){var e=this.particles[t];for(var s in this.cursors){var n=this.cursors[s],a=n.isDown?"active":"hover",o=this.options[a+"Force"],r=this.options[a+"Diameter"],h=r/2*this.diagonal,c=y.subtract(e.position,n.position),l=Math.max(0,h-c.magnitude)/h;l=Math.cos(l*Math.PI)*-.5+.5,c.scale(l*o),e.applyForce(c)}e.update()}},s.prototype.render=function(){this.ctx.globalCompositeOperation="source-over",this.ctx.fillStyle=this.options.isAdditive?"black":"white",this.ctx.fillRect(0,0,this.width,this.height),this.ctx.globalCompositeOperation=this.options.isAdditive?"lighter":"darker";for(var t=0,i=this.channels.length;i>t;t++){var e=this.channels[t];this.renderGrid(e)}};var w={additive:{red:"#FF0000",green:"#00FF00",blue:"#0000FF",lum:"#FFF"},subtractive:{red:"#00FFFF",green:"#FF00FF",blue:"#FFFF00",lum:"#000"}};s.prototype.renderGrid=function(t){var i=this.proxyCanvases[t];i.ctx.fillStyle=this.options.isAdditive?"black":"white",i.ctx.fillRect(0,0,this.width,this.height);var e=this.options.isAdditive?"additive":"subtractive";i.ctx.fillStyle=w[e][t];for(var s=this.channelParticles[t],n=0,a=s.length;a>n;n++){var o=s[n];o.render(i.ctx)}this.ctx.drawImage(i.canvas,0,0)},s.prototype.getCartesianGridParticles=function(t,i){for(var e=[],s=this.width,n=this.height,a=Math.max(s,n)*r,o=this.gridSize,h=Math.ceil(a/o),c=Math.ceil(a/o),l=0;c>l;l++)for(var v=0;h>v;v++){var d=(v+.5)*o,u=(l+.5)*o;d-=(a-s)/2,u-=(a-n)/2,d-=s/2,u-=n/2;var p=d*Math.cos(i)-u*Math.sin(i),g=d*Math.sin(i)+u*Math.cos(i);p+=s/2,g+=n/2;var m=this.initParticle(t,p,g);m&&e.push(m)}return e},s.prototype.getRadialGridParticles=function(t,i){for(var e=[],s=this.width,n=this.height,a=Math.max(s,n)*r,h=this.gridSize,c=s/2,l=n/2,v=h,d=c+Math.cos(i)*v,u=l+Math.sin(i)*v,p=Math.ceil((a+v)/h),g=0;p>g;g++)for(var m=6*g||1,f=0;m>f;f++){var y=o*f/m+i,C=d+Math.cos(y)*g*h,w=u+Math.sin(y)*g*h,P=this.initParticle(t,C,w);P&&e.push(P)}return e},s.prototype.initParticle=function(t,i,e){var s=this.getPixelChannelValue(i,e,t);if(!(st||t>s||0>i||i>n)return 0;var a,o=4*(t+i*s);if("lum"===e)a=this.getPixelLum(o);else{var r=o+P[e];a=this.imgData[r]/255}return a=a||0,this.options.isAdditive||(a=1-a),a},s.prototype.getPixelLum=function(t){var i=this.imgData[t+0]/255,e=this.imgData[t+1]/255,s=this.imgData[t+2]/255,n=Math.max(i,e,s),a=Math.min(i,e,s);return(n+a)/2},s.prototype.bindEvents=function(){this.canvas.addEventListener("mousedown",this,!1),this.canvas.addEventListener("touchstart",this,!1),t.addEventListener("mousemove",this,!1),t.addEventListener("touchmove",this,!1),t.addEventListener("touchend",this,!1),t.addEventListener("resize",this,!1)},s.prototype.unbindEvents=function(){this.canvas.removeEventListener("mousedown",this,!1),this.canvas.removeEventListener("touchstart",this,!1),t.removeEventListener("mousemove",this,!1),t.removeEventListener("touchmove",this,!1),t.removeEventListener("touchend",this,!1),t.removeEventListener("resize",this,!1)},s.prototype.handleEvent=function(t){var i="on"+t.type;this[i]&&this[i](t)},s.prototype.onmousedown=function(i){i.preventDefault(),this.cursors.mouse.isDown=!0,t.addEventListener("mouseup",this,!1)},s.prototype.ontouchstart=function(t){t.preventDefault();for(var i=0,e=t.changedTouches.length;e>i;i++){var s=t.changedTouches[i],n=this.addCursor(s.identifier,s);n.isDown=!0}},s.prototype.addCursor=function(t,i){var e=this.setCursorPosition(i),s=this.cursors[t]={position:e,isDown:!1};return s},s.prototype.setCursorPosition=function(t,i){return i=i||new y,i.set(t.pageX,t.pageY),i.subtract(this.canvasPosition),i.scale(this.canvasScale),i},s.prototype.onmousemove=function(t){this.setCursorPosition(t,this.cursors.mouse.position)},s.prototype.ontouchmove=function(t){for(var i=0,e=t.changedTouches.length;e>i;i++){var s=t.changedTouches[i],n=this.cursors[s.identifier];n&&this.setCursorPosition(s,n.position)}},s.prototype.onmouseup=function(){this.cursors.mouse.isDown=!1,t.removeEventListener("mouseup",this,!1)},s.prototype.ontouchend=function(t){for(var i=0,e=t.changedTouches.length;e>i;i++){var s=t.changedTouches[i],n=this.cursors[s.identifier];n&&delete this.cursors[s.identifier]}},s.prototype.onresize=function(){this.getCanvasPosition()},a(s,"onresize",200),s.prototype.destroy=function(){this.isActive=!1,this.unbindEvents(),this.img.style.visibility="",this.img.style.display="",this.canvas.parentNode.removeChild(this.canvas)},s.Vector=y,s.Particle=C,t.BreathingHalftone=s}(window); -------------------------------------------------------------------------------- /js/breathing-halftone.js: -------------------------------------------------------------------------------- 1 | ( function( window ) { 2 | 3 | 'use strict'; 4 | 5 | // ----- vars ----- // 6 | 7 | var TAU = Math.PI * 2; 8 | var ROOT_2 = Math.sqrt( 2 ); 9 | 10 | // ----- helpers ----- // 11 | 12 | var objToString = Object.prototype.toString; 13 | var isArray = Array.isArray || function( obj ) { 14 | return objToString.call( obj ) === '[object Array]'; 15 | }; 16 | 17 | // extend objects 18 | function extend( a, b, isDeep ) { 19 | for ( var prop in b ) { 20 | var value = b[ prop ]; 21 | if ( isDeep && typeof value === 'object' && !isArray( value ) ) { 22 | // deep extend 23 | a[ prop ] = extend( a[ prop ] || {}, value, true ); 24 | } else { 25 | a[ prop ] = value; 26 | } 27 | } 28 | return a; 29 | } 30 | 31 | function insertAfter( elem, afterElem ) { 32 | var parent = afterElem.parentNode; 33 | var nextElem = afterElem.nextElementSibling; 34 | if ( nextElem ) { 35 | parent.insertBefore( elem, nextElem ); 36 | } else { 37 | parent.appendChild( elem ); 38 | } 39 | } 40 | 41 | // -------------------------- supports -------------------------- // 42 | 43 | var supports = {}; 44 | 45 | ( function() { 46 | // check canvas support 47 | var canvas = document.createElement('canvas'); 48 | var ctx = canvas.getContext && canvas.getContext('2d'); 49 | supports.canvas = !!ctx; 50 | if ( !supports.canvas ) { 51 | return; 52 | } 53 | 54 | // check darker composite support 55 | canvas.width = 1; 56 | canvas.height = 1; 57 | ctx.globalCompositeOperation = 'darker'; 58 | ctx.fillStyle = '#F00'; 59 | ctx.fillRect( 0, 0, 1, 1 ); 60 | ctx.fillStyle = '#999'; 61 | ctx.fillRect( 0, 0, 1, 1 ); 62 | var imgData = ctx.getImageData( 0, 0, 1, 1 ).data; 63 | supports.darker = imgData[0] === 153 && imgData[1] === 0; 64 | })(); 65 | 66 | // -------------------------- requestAnimationFrame -------------------------- // 67 | 68 | // https://gist.github.com/1866474 69 | 70 | var lastTime = 0; 71 | var prefixes = 'webkit moz ms o'.split(' '); 72 | // get unprefixed rAF and cAF, if present 73 | var requestAnimationFrame = window.requestAnimationFrame; 74 | var cancelAnimationFrame = window.cancelAnimationFrame; 75 | // loop through vendor prefixes and get prefixed rAF and cAF 76 | var prefix; 77 | for( var i = 0; i < prefixes.length; i++ ) { 78 | if ( requestAnimationFrame && cancelAnimationFrame ) { 79 | break; 80 | } 81 | prefix = prefixes[i]; 82 | requestAnimationFrame = requestAnimationFrame || window[ prefix + 'RequestAnimationFrame' ]; 83 | cancelAnimationFrame = cancelAnimationFrame || window[ prefix + 'CancelAnimationFrame' ] || 84 | window[ prefix + 'CancelRequestAnimationFrame' ]; 85 | } 86 | 87 | // fallback to setTimeout and clearTimeout if either request/cancel is not supported 88 | if ( !requestAnimationFrame || !cancelAnimationFrame ) { 89 | requestAnimationFrame = function( callback ) { 90 | var currTime = new Date().getTime(); 91 | var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) ); 92 | var id = setTimeout( function() { 93 | callback( currTime + timeToCall ); 94 | }, timeToCall ); 95 | lastTime = currTime + timeToCall; 96 | return id; 97 | }; 98 | 99 | cancelAnimationFrame = function( id ) { 100 | clearTimeout( id ); 101 | }; 102 | } 103 | 104 | // -------------------------- -------------------------- // 105 | 106 | var _Halftone = window.BreathingHalftone || {}; 107 | var Vector = _Halftone.Vector; 108 | var Particle = _Halftone.Particle; 109 | 110 | // -------------------------- BreathingHalftone -------------------------- // 111 | 112 | function Halftone( img, options ) { 113 | // set options 114 | this.options = extend( {}, this.constructor.defaults, true ); 115 | extend( this.options, options, true ); 116 | 117 | this.img = img; 118 | // bail if canvas is not supported 119 | if ( !supports.canvas ) { 120 | return; 121 | } 122 | 123 | this.create(); 124 | } 125 | 126 | Halftone.defaults = { 127 | // dot size 128 | dotSize: 1/40, 129 | dotSizeThreshold: 0.05, 130 | initVelocity: 0.02, 131 | oscPeriod: 3, 132 | oscAmplitude: 0.2, 133 | // layout and color 134 | isAdditive: false, 135 | isRadial: false, 136 | channels: [ 'red', 'green', 'blue' ], 137 | isChannelLens: true, 138 | // behavoir 139 | friction: 0.06, 140 | hoverDiameter: 0.3, 141 | hoverForce: -0.02, 142 | activeDiameter: 0.6, 143 | activeForce: 0.01 144 | }; 145 | 146 | function makeCanvasAndCtx() { 147 | var canvas = document.createElement('canvas'); 148 | var ctx = canvas.getContext('2d'); 149 | return { 150 | canvas: canvas, 151 | ctx: ctx 152 | }; 153 | } 154 | 155 | 156 | 157 | Halftone.prototype.create = function() { 158 | this.isActive = true; 159 | 160 | // create main canvas 161 | var canvasAndCtx = makeCanvasAndCtx(); 162 | this.canvas = canvasAndCtx.canvas; 163 | this.ctx = canvasAndCtx.ctx; 164 | // copy over class 165 | this.canvas.className = this.img.className; 166 | insertAfter( this.canvas, this.img ); 167 | // hide img visually 168 | this.img.style.visibility = 'hidden'; 169 | 170 | // fall back to lum channel if subtractive and darker isn't supported 171 | this.channels = !this.options.isAdditive && !supports.darker ? 172 | [ 'lum' ] : this.options.channels; 173 | 174 | // create separate canvases for each color 175 | this.proxyCanvases = {}; 176 | for ( var i=0, len = this.channels.length; i < len; i++ ) { 177 | var channel = this.channels[i]; 178 | this.proxyCanvases[ channel ] = makeCanvasAndCtx(); 179 | } 180 | 181 | this.loadImage(); 182 | 183 | // properties 184 | this.canvasPosition = new Vector(); 185 | this.getCanvasPosition(); 186 | // hash of mouse / touch events 187 | this.cursors = {}; 188 | // position -100,000, -100,000 so its not on screen 189 | this.addCursor( 'mouse', { pageX: -1e5, pageY: -1e5 }); 190 | 191 | this.bindEvents(); 192 | }; 193 | 194 | Halftone.prototype.getCanvasPosition = function() { 195 | var rect = this.canvas.getBoundingClientRect(); 196 | var x = rect.left + window.pageXOffset; 197 | var y = rect.top + window.pageYOffset; 198 | this.canvasPosition.set( x, y ); 199 | this.canvasScale = this.width ? this.width / this.canvas.offsetWidth : 1; 200 | }; 201 | 202 | // -------------------------- img -------------------------- // 203 | 204 | Halftone.prototype.loadImage = function() { 205 | // hack img load 206 | var src = this.img.getAttribute('data-src') || this.img.src; 207 | var loadingImg = new Image(); 208 | loadingImg.onload = function() { 209 | this.onImgLoad(); 210 | }.bind( this ); 211 | loadingImg.src = src; 212 | // set src on image, so we can get correct sizes 213 | if ( this.img.src !== src ) { 214 | this.img.src = src; 215 | } 216 | }; 217 | 218 | Halftone.prototype.onImgLoad = function() { 219 | this.getImgData(); 220 | this.resizeCanvas(); 221 | this.getCanvasPosition(); 222 | // hide image completely 223 | this.img.style.display = 'none'; 224 | this.getCanvasPosition(); 225 | this.initParticles(); 226 | this.animate(); 227 | }; 228 | 229 | Halftone.prototype.getImgData = function() { 230 | // get imgData 231 | var canvasAndCtx = makeCanvasAndCtx(); 232 | var imgCanvas = canvasAndCtx.canvas; 233 | var ctx = canvasAndCtx.ctx; 234 | this.imgWidth = imgCanvas.width = this.img.naturalWidth; 235 | this.imgHeight = imgCanvas.height = this.img.naturalHeight; 236 | ctx.drawImage( this.img, 0, 0 ); 237 | this.imgData = ctx.getImageData( 0, 0, this.imgWidth, this.imgHeight ).data; 238 | }; 239 | 240 | Halftone.prototype.resizeCanvas = function() { 241 | // width & height 242 | var w = this.width = this.img.offsetWidth; 243 | var h = this.height = this.img.offsetHeight; 244 | // size properties 245 | this.diagonal = Math.sqrt( w*w + h*h ); 246 | this.imgScale = this.width / this.imgWidth; 247 | this.gridSize = this.options.dotSize * this.diagonal; 248 | 249 | // set proxy canvases size 250 | for ( var prop in this.proxyCanvases ) { 251 | var proxy = this.proxyCanvases[ prop ]; 252 | proxy.canvas.width = w; 253 | proxy.canvas.height = h; 254 | } 255 | this.canvas.width = w; 256 | this.canvas.height = h; 257 | }; 258 | 259 | Halftone.prototype.initParticles = function() { 260 | 261 | var getParticlesMethod = this.options.isRadial ? 262 | 'getRadialGridParticles' : 'getCartesianGridParticles'; 263 | 264 | // all particles 265 | this.particles = []; 266 | // separate array of particles for each color 267 | this.channelParticles = {}; 268 | 269 | var angles = { red: 1, green: 2.5, blue: 5, lum: 4 }; 270 | 271 | for ( var i=0, len = this.channels.length; i < len; i++ ) { 272 | var channel = this.channels[i]; 273 | var angle = angles[ channel ]; 274 | var particles = this[ getParticlesMethod ]( channel, angle ); 275 | // associate with channel 276 | this.channelParticles[ channel ] = particles; 277 | // add to all collection 278 | this.particles = this.particles.concat( particles ); 279 | } 280 | 281 | }; 282 | 283 | Halftone.prototype.animate = function() { 284 | // do not animate if not active 285 | if ( !this.isActive ) { 286 | return; 287 | } 288 | this.update(); 289 | this.render(); 290 | requestAnimationFrame( this.animate.bind( this ) ); 291 | }; 292 | 293 | Halftone.prototype.update = function() { 294 | // displace particles with cursors (mouse, touches) 295 | 296 | for ( var i=0, len = this.particles.length; i < len; i++ ) { 297 | var particle = this.particles[i]; 298 | // apply forces for each cursor 299 | for ( var identifier in this.cursors ) { 300 | var cursor = this.cursors[ identifier ]; 301 | var cursorState = cursor.isDown ? 'active' : 'hover'; 302 | var forceScale = this.options[ cursorState + 'Force' ]; 303 | var diameter = this.options[ cursorState + 'Diameter' ]; 304 | var radius = diameter / 2 * this.diagonal; 305 | var force = Vector.subtract( particle.position, cursor.position ); 306 | var distanceScale = Math.max( 0, radius - force.magnitude ) / radius; 307 | // easeInOutSine 308 | distanceScale = Math.cos( distanceScale * Math.PI ) * -0.5 + 0.5; 309 | force.scale( distanceScale * forceScale ); 310 | particle.applyForce( force ); 311 | } 312 | 313 | particle.update(); 314 | } 315 | }; 316 | 317 | Halftone.prototype.render = function() { 318 | // clear 319 | this.ctx.globalCompositeOperation = 'source-over'; 320 | this.ctx.fillStyle = this.options.isAdditive ? 'black' : 'white'; 321 | this.ctx.fillRect( 0, 0, this.width, this.height ); 322 | 323 | // composite grids 324 | this.ctx.globalCompositeOperation = this.options.isAdditive ? 'lighter' : 'darker'; 325 | 326 | // render channels 327 | for ( var i=0, len = this.channels.length; i < len; i++ ) { 328 | var channel = this.channels[i]; 329 | this.renderGrid( channel ); 330 | } 331 | 332 | }; 333 | 334 | var channelFillStyles = { 335 | additive: { 336 | red: '#FF0000', 337 | green: '#00FF00', 338 | blue: '#0000FF', 339 | lum: '#FFF' 340 | }, 341 | subtractive: { 342 | red: '#00FFFF', 343 | green: '#FF00FF', 344 | blue: '#FFFF00', 345 | lum: '#000' 346 | } 347 | }; 348 | 349 | Halftone.prototype.renderGrid = function( channel ) { 350 | var proxy = this.proxyCanvases[ channel ]; 351 | // clear 352 | proxy.ctx.fillStyle = this.options.isAdditive ? 'black' : 'white'; 353 | proxy.ctx.fillRect( 0, 0, this.width, this.height ); 354 | 355 | // set fill color 356 | var blend = this.options.isAdditive ? 'additive' : 'subtractive'; 357 | proxy.ctx.fillStyle = channelFillStyles[ blend ][ channel ]; 358 | 359 | // render particles 360 | var particles = this.channelParticles[ channel ]; 361 | for ( var i=0, len = particles.length; i < len; i++ ) { 362 | var particle = particles[i]; 363 | particle.render( proxy.ctx ); 364 | } 365 | 366 | // draw proxy canvas to actual canvas as whole layer 367 | this.ctx.drawImage( proxy.canvas, 0, 0 ); 368 | }; 369 | 370 | Halftone.prototype.getCartesianGridParticles = function( channel, angle ) { 371 | var particles = []; 372 | 373 | var w = this.width; 374 | var h = this.height; 375 | 376 | var diag = Math.max( w, h ) * ROOT_2; 377 | 378 | var gridSize = this.gridSize; 379 | var cols = Math.ceil( diag / gridSize ); 380 | var rows = Math.ceil( diag / gridSize ); 381 | 382 | for ( var row = 0; row < rows; row++ ) { 383 | for ( var col = 0; col < cols; col++ ) { 384 | var x1 = ( col + 0.5 ) * gridSize; 385 | var y1 = ( row + 0.5 ) * gridSize; 386 | // offset for diagonal 387 | x1 -= ( diag - w ) / 2; 388 | y1 -= ( diag - h ) / 2; 389 | // shift to center 390 | x1 -= w / 2; 391 | y1 -= h / 2; 392 | // rotate grid 393 | var x2 = x1 * Math.cos( angle ) - y1 * Math.sin( angle ); 394 | var y2 = x1 * Math.sin( angle ) + y1 * Math.cos( angle ); 395 | // shift back 396 | x2 += w / 2; 397 | y2 += h / 2; 398 | 399 | var particle = this.initParticle( channel, x2, y2 ); 400 | if ( particle ) { 401 | particles.push( particle ); 402 | } 403 | } 404 | } 405 | 406 | return particles; 407 | }; 408 | 409 | Halftone.prototype.getRadialGridParticles = function( channel, angle ) { 410 | var particles = []; 411 | 412 | var w = this.width; 413 | var h = this.height; 414 | var diag = Math.max( w, h ) * ROOT_2; 415 | 416 | var gridSize = this.gridSize; 417 | 418 | var halfW = w / 2; 419 | var halfH = h / 2; 420 | var offset = gridSize; 421 | var centerX = halfW + Math.cos( angle ) * offset; 422 | var centerY = halfH + Math.sin( angle ) * offset; 423 | 424 | var maxLevel = Math.ceil( ( diag + offset ) / gridSize ); 425 | 426 | for ( var level=0; level < maxLevel; level++ ) { 427 | var max = level * 6 || 1; 428 | for ( var j=0; j < max; j++ ) { 429 | var theta = TAU * j / max + angle; 430 | var x = centerX + Math.cos( theta ) * level * gridSize; 431 | var y = centerY + Math.sin( theta ) * level * gridSize; 432 | var particle = this.initParticle( channel, x, y ); 433 | if ( particle ) { 434 | particles.push( particle ); 435 | } 436 | } 437 | } 438 | 439 | return particles; 440 | 441 | }; 442 | 443 | Halftone.prototype.initParticle = function( channel, x, y ) { 444 | // don't render if coords are outside image 445 | // don't display if under threshold 446 | var pixelChannelValue = this.getPixelChannelValue( x, y, channel ); 447 | if ( pixelChannelValue < this.options.dotSizeThreshold ) { 448 | return; 449 | } 450 | 451 | return new Particle({ 452 | channel: channel, 453 | parent: this, 454 | origin: new Vector( x, y ), 455 | naturalSize: this.gridSize * ROOT_2 / 2, 456 | friction: this.options.friction 457 | }); 458 | 459 | }; 460 | 461 | var channelOffset = { 462 | red: 0, 463 | green: 1, 464 | blue: 2 465 | }; 466 | 467 | Halftone.prototype.getPixelChannelValue = function( x, y, channel ) { 468 | x = Math.round( x / this.imgScale ); 469 | y = Math.round( y / this.imgScale ); 470 | var w = this.imgWidth; 471 | var h = this.imgHeight; 472 | 473 | // return 0 if position is outside of image 474 | if ( x < 0 || x > w || y < 0 || y > h ) { 475 | return 0; 476 | } 477 | 478 | var pixelIndex = ( x + y * w ) * 4; 479 | var value; 480 | // return 1; 481 | if ( channel === 'lum' ) { 482 | value = this.getPixelLum( pixelIndex ); 483 | } else { 484 | // rgb 485 | var index = pixelIndex + channelOffset[ channel ]; 486 | value = this.imgData[ index ] / 255; 487 | } 488 | 489 | value = value || 0; 490 | if ( !this.options.isAdditive ) { 491 | value = 1 - value; 492 | } 493 | 494 | return value; 495 | }; 496 | 497 | Halftone.prototype.getPixelLum = function( pixelIndex ) { 498 | // thx @jfsiii 499 | // https://github.com/jfsiii/chromath/blob/master/src/chromath.js 500 | var r = this.imgData[ pixelIndex + 0 ] / 255; 501 | var g = this.imgData[ pixelIndex + 1 ] / 255; 502 | var b = this.imgData[ pixelIndex + 2 ] / 255; 503 | var max = Math.max( r, g, b ); 504 | var min = Math.min( r, g, b ); 505 | return ( max + min ) / 2; 506 | }; 507 | 508 | // ----- bindEvents ----- // 509 | 510 | Halftone.prototype.bindEvents = function() { 511 | this.canvas.addEventListener( 'mousedown', this, false ); 512 | this.canvas.addEventListener( 'touchstart', this, false ); 513 | window.addEventListener( 'mousemove', this, false ); 514 | window.addEventListener( 'touchmove', this, false ); 515 | window.addEventListener( 'touchend', this, false ); 516 | window.addEventListener( 'resize', this, false ); 517 | }; 518 | 519 | Halftone.prototype.unbindEvents = function() { 520 | this.canvas.removeEventListener( 'mousedown', this, false ); 521 | this.canvas.removeEventListener( 'touchstart', this, false ); 522 | window.removeEventListener( 'mousemove', this, false ); 523 | window.removeEventListener( 'touchmove', this, false ); 524 | window.removeEventListener( 'touchend', this, false ); 525 | window.removeEventListener( 'resize', this, false ); 526 | }; 527 | 528 | Halftone.prototype.handleEvent = function( event ) { 529 | var method = 'on' + event.type; 530 | if ( this[ method ] ) { 531 | this[ method ]( event ); 532 | } 533 | }; 534 | 535 | Halftone.prototype.onmousedown = function( event ) { 536 | event.preventDefault(); 537 | this.cursors.mouse.isDown = true; 538 | window.addEventListener( 'mouseup', this, false ); 539 | }; 540 | 541 | Halftone.prototype.ontouchstart = function( event ) { 542 | event.preventDefault(); 543 | for ( var i=0, len = event.changedTouches.length; i < len; i++ ) { 544 | var touch = event.changedTouches[i]; 545 | var cursor = this.addCursor( touch.identifier, touch ); 546 | cursor.isDown = true; 547 | } 548 | }; 549 | 550 | /** 551 | * @param {MouseEvent or Touch} cursorEvent - with pageX and pageY 552 | */ 553 | Halftone.prototype.addCursor = function( identifier, cursorEvent ) { 554 | var position = this.setCursorPosition( cursorEvent ); 555 | var cursor = this.cursors[ identifier ] = { 556 | position: position, 557 | isDown: false 558 | }; 559 | return cursor; 560 | }; 561 | 562 | /** 563 | * @param {MouseEvent or Touch} cursorEvent - with pageX and pageY 564 | * @param {Vector} position - optional 565 | */ 566 | Halftone.prototype.setCursorPosition = function( cursorEvent, position ) { 567 | position = position || new Vector(); 568 | position.set( cursorEvent.pageX, cursorEvent.pageY ); 569 | position.subtract( this.canvasPosition ); 570 | position.scale( this.canvasScale ); 571 | return position; 572 | }; 573 | 574 | 575 | Halftone.prototype.onmousemove = function( event ) { 576 | this.setCursorPosition( event, this.cursors.mouse.position ); 577 | }; 578 | 579 | Halftone.prototype.ontouchmove = function( event ) { 580 | // move matching cursors 581 | for ( var i=0, len = event.changedTouches.length; i < len; i++ ) { 582 | var touch = event.changedTouches[i]; 583 | var cursor = this.cursors[ touch.identifier ]; 584 | if ( cursor ) { 585 | this.setCursorPosition( touch, cursor.position ); 586 | } 587 | } 588 | }; 589 | 590 | Halftone.prototype.onmouseup = function() { 591 | this.cursors.mouse.isDown = false; 592 | window.removeEventListener( 'mouseup', this, false ); 593 | }; 594 | 595 | Halftone.prototype.ontouchend = function( event ) { 596 | // remove matching cursors 597 | for ( var i=0, len = event.changedTouches.length; i < len; i++ ) { 598 | var touch = event.changedTouches[i]; 599 | var cursor = this.cursors[ touch.identifier ]; 600 | if ( cursor ) { 601 | delete this.cursors[ touch.identifier ]; 602 | } 603 | } 604 | }; 605 | 606 | 607 | function debounceProto( _class, methodName, threshold ) { 608 | // original method 609 | var method = _class.prototype[ methodName ]; 610 | var timeoutName = methodName + 'Timeout'; 611 | 612 | _class.prototype[ methodName ] = function() { 613 | var timeout = this[ timeoutName ]; 614 | if ( timeout ) { 615 | clearTimeout( timeout ); 616 | } 617 | var args = arguments; 618 | 619 | this[ timeoutName ] = setTimeout( function() { 620 | method.apply( this, args ); 621 | delete this[ timeoutName ]; 622 | }.bind( this ), threshold || 100 ); 623 | }; 624 | } 625 | 626 | Halftone.prototype.onresize = function() { 627 | this.getCanvasPosition(); 628 | }; 629 | 630 | debounceProto( Halftone, 'onresize', 200 ); 631 | 632 | // ----- destroy ----- // 633 | 634 | Halftone.prototype.destroy = function() { 635 | this.isActive = false; 636 | this.unbindEvents(); 637 | 638 | this.img.style.visibility = ''; 639 | this.img.style.display = ''; 640 | this.canvas.parentNode.removeChild( this.canvas ); 641 | }; 642 | 643 | // -------------------------- -------------------------- // 644 | 645 | Halftone.Vector = Vector; 646 | Halftone.Particle = Particle; 647 | window.BreathingHalftone = Halftone; 648 | 649 | 650 | })( window ); 651 | 652 | -------------------------------------------------------------------------------- /dist/breathing-halftone.pkgd.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Breathing Halftone 3 | * Images go whoa with lots of floaty dots 4 | * http://breathing-halftone.desandro.com 5 | */ 6 | 7 | ( function( window ) { 8 | 9 | 'use strict'; 10 | 11 | // ----- vars ----- // 12 | 13 | var Halftone = window.BreathingHalftone = window.BreathingHalftone || {}; 14 | 15 | // -------------------------- Vector -------------------------- // 16 | 17 | function Vector( x, y ) { 18 | this.set( x || 0, y || 0 ); 19 | } 20 | 21 | Vector.prototype.set = function( x, y ) { 22 | this.x = x; 23 | this.y = y; 24 | }; 25 | 26 | Vector.prototype.add = function( v ) { 27 | this.x += v.x; 28 | this.y += v.y; 29 | }; 30 | 31 | Vector.prototype.subtract = function( v ) { 32 | this.x -= v.x; 33 | this.y -= v.y; 34 | }; 35 | 36 | Vector.prototype.scale = function( s ) { 37 | this.x *= s; 38 | this.y *= s; 39 | }; 40 | 41 | Vector.prototype.multiply = function( v ) { 42 | this.x *= v.x; 43 | this.y *= v.y; 44 | }; 45 | 46 | // custom getter whaaaaaaat 47 | Object.defineProperty( Vector.prototype, 'magnitude', { 48 | get: function() { 49 | return Math.sqrt( this.x * this.x + this.y * this.y ); 50 | } 51 | }); 52 | 53 | // ----- class functions ----- // 54 | 55 | Vector.subtract = function( a, b ) { 56 | return new Vector( a.x - b.x, a.y - b.y ); 57 | }; 58 | 59 | Vector.add = function( a, b ) { 60 | return new Vector( a.x + b.x, a.y + b.y ); 61 | }; 62 | 63 | Vector.copy = function( v ) { 64 | return new Vector( v.x, v.y ); 65 | }; 66 | 67 | Halftone.Vector = Vector; 68 | 69 | })( window ); 70 | 71 | ( function( window ) { 72 | 73 | 'use strict'; 74 | 75 | // ----- vars ----- // 76 | 77 | var TAU = Math.PI * 2; 78 | 79 | function getNow() { 80 | return new Date(); 81 | } 82 | 83 | // -------------------------- -------------------------- // 84 | 85 | var Halftone = window.BreathingHalftone || {}; 86 | var Vector = Halftone.Vector; 87 | 88 | // -------------------------- Particle -------------------------- // 89 | 90 | function Particle( properties ) { 91 | this.channel = properties.channel; 92 | this.origin = properties.origin; 93 | this.parent = properties.parent; 94 | this.friction = properties.friction; 95 | 96 | this.position = Vector.copy( this.origin ); 97 | this.velocity = new Vector(); 98 | this.acceleration = new Vector(); 99 | 100 | this.naturalSize = properties.naturalSize; 101 | this.size = 0; 102 | this.sizeVelocity = 0; 103 | this.oscSize = 0; 104 | this.initSize = 0; 105 | this.initSizeVelocity = ( Math.random() * 0.5 + 0.5 ) * 106 | this.parent.options.initVelocity; 107 | 108 | this.oscillationOffset = Math.random() * TAU; 109 | this.oscillationMagnitude = Math.random(); 110 | this.isVisible = false; 111 | } 112 | 113 | Particle.prototype.applyForce = function( force ) { 114 | this.acceleration.add( force ); 115 | }; 116 | 117 | Particle.prototype.update = function() { 118 | // stagger starting 119 | if ( !this.isVisible && Math.random() > 0.03 ) { 120 | return; 121 | } 122 | this.isVisible = true; 123 | 124 | this.applyOriginAttraction(); 125 | 126 | // velocity 127 | this.velocity.add( this.acceleration ); 128 | this.velocity.scale( 1 - this.friction ); 129 | // position 130 | this.position.add( this.velocity ); 131 | // reset acceleration 132 | this.acceleration.set( 0, 0 ); 133 | 134 | this.calculateSize(); 135 | }; 136 | 137 | Particle.prototype.render = function( ctx ) { 138 | 139 | var size = this.size * this.oscSize; 140 | // apply initSize with easing 141 | var initSize = Math.cos( this.initSize * TAU / 2 ) * -0.5 + 0.5; 142 | size *= initSize; 143 | size = Math.max( 0, size ); 144 | ctx.beginPath(); 145 | ctx.arc( this.position.x, this.position.y, size, 0, TAU ); 146 | ctx.fill(); 147 | ctx.closePath(); 148 | }; 149 | 150 | Particle.prototype.calculateSize = function() { 151 | 152 | if ( this.initSize !== 1 ) { 153 | this.initSize += this.initSizeVelocity; 154 | this.initSize = Math.min( 1, this.initSize ); 155 | } 156 | 157 | var targetSize = this.naturalSize * this.getChannelValue(); 158 | 159 | // use accel/velocity to smooth changes in size 160 | var sizeAcceleration = ( targetSize - this.size ) * 0.1; 161 | this.sizeVelocity += sizeAcceleration; 162 | // friction 163 | this.sizeVelocity *= ( 1 - 0.3 ); 164 | this.size += this.sizeVelocity; 165 | 166 | // oscillation size 167 | var now = getNow(); 168 | var opts = this.parent.options; 169 | var oscSize = ( now / ( 1000 * opts.oscPeriod ) ) * TAU; 170 | oscSize = Math.cos( oscSize + this.oscillationOffset ); 171 | oscSize = oscSize * opts.oscAmplitude + 1; 172 | this.oscSize = oscSize; 173 | }; 174 | 175 | Particle.prototype.getChannelValue = function() { 176 | var channelValue; 177 | // return origin channel value if not lens 178 | var position = this.parent.options.isChannelLens ? this.position : this.origin; 179 | if ( this.parent.options.isChannelLens ) { 180 | channelValue = this.parent.getPixelChannelValue( position.x, position.y, this.channel ); 181 | } else { 182 | if ( !this.originChannelValue ) { 183 | this.originChannelValue = this.parent.getPixelChannelValue( position.x, position.y, this.channel ); 184 | } 185 | channelValue = this.originChannelValue; 186 | } 187 | 188 | return channelValue; 189 | }; 190 | 191 | Particle.prototype.applyOriginAttraction = function() { 192 | var attraction = Vector.subtract( this.position, this.origin ); 193 | attraction.scale( -0.02 ); 194 | this.applyForce( attraction ); 195 | }; 196 | 197 | Halftone.Particle = Particle; 198 | 199 | })( window ); 200 | 201 | ( function( window ) { 202 | 203 | 'use strict'; 204 | 205 | // ----- vars ----- // 206 | 207 | var TAU = Math.PI * 2; 208 | var ROOT_2 = Math.sqrt( 2 ); 209 | 210 | // ----- helpers ----- // 211 | 212 | var objToString = Object.prototype.toString; 213 | var isArray = Array.isArray || function( obj ) { 214 | return objToString.call( obj ) === '[object Array]'; 215 | }; 216 | 217 | // extend objects 218 | function extend( a, b, isDeep ) { 219 | for ( var prop in b ) { 220 | var value = b[ prop ]; 221 | if ( isDeep && typeof value === 'object' && !isArray( value ) ) { 222 | // deep extend 223 | a[ prop ] = extend( a[ prop ] || {}, value, true ); 224 | } else { 225 | a[ prop ] = value; 226 | } 227 | } 228 | return a; 229 | } 230 | 231 | function insertAfter( elem, afterElem ) { 232 | var parent = afterElem.parentNode; 233 | var nextElem = afterElem.nextElementSibling; 234 | if ( nextElem ) { 235 | parent.insertBefore( elem, nextElem ); 236 | } else { 237 | parent.appendChild( elem ); 238 | } 239 | } 240 | 241 | // -------------------------- supports -------------------------- // 242 | 243 | var supports = {}; 244 | 245 | ( function() { 246 | // check canvas support 247 | var canvas = document.createElement('canvas'); 248 | var ctx = canvas.getContext && canvas.getContext('2d'); 249 | supports.canvas = !!ctx; 250 | if ( !supports.canvas ) { 251 | return; 252 | } 253 | 254 | // check darker composite support 255 | canvas.width = 1; 256 | canvas.height = 1; 257 | ctx.globalCompositeOperation = 'darker'; 258 | ctx.fillStyle = '#F00'; 259 | ctx.fillRect( 0, 0, 1, 1 ); 260 | ctx.fillStyle = '#999'; 261 | ctx.fillRect( 0, 0, 1, 1 ); 262 | var imgData = ctx.getImageData( 0, 0, 1, 1 ).data; 263 | supports.darker = imgData[0] === 153 && imgData[1] === 0; 264 | })(); 265 | 266 | // -------------------------- requestAnimationFrame -------------------------- // 267 | 268 | // https://gist.github.com/1866474 269 | 270 | var lastTime = 0; 271 | var prefixes = 'webkit moz ms o'.split(' '); 272 | // get unprefixed rAF and cAF, if present 273 | var requestAnimationFrame = window.requestAnimationFrame; 274 | var cancelAnimationFrame = window.cancelAnimationFrame; 275 | // loop through vendor prefixes and get prefixed rAF and cAF 276 | var prefix; 277 | for( var i = 0; i < prefixes.length; i++ ) { 278 | if ( requestAnimationFrame && cancelAnimationFrame ) { 279 | break; 280 | } 281 | prefix = prefixes[i]; 282 | requestAnimationFrame = requestAnimationFrame || window[ prefix + 'RequestAnimationFrame' ]; 283 | cancelAnimationFrame = cancelAnimationFrame || window[ prefix + 'CancelAnimationFrame' ] || 284 | window[ prefix + 'CancelRequestAnimationFrame' ]; 285 | } 286 | 287 | // fallback to setTimeout and clearTimeout if either request/cancel is not supported 288 | if ( !requestAnimationFrame || !cancelAnimationFrame ) { 289 | requestAnimationFrame = function( callback ) { 290 | var currTime = new Date().getTime(); 291 | var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) ); 292 | var id = setTimeout( function() { 293 | callback( currTime + timeToCall ); 294 | }, timeToCall ); 295 | lastTime = currTime + timeToCall; 296 | return id; 297 | }; 298 | 299 | cancelAnimationFrame = function( id ) { 300 | clearTimeout( id ); 301 | }; 302 | } 303 | 304 | // -------------------------- -------------------------- // 305 | 306 | var _Halftone = window.BreathingHalftone || {}; 307 | var Vector = _Halftone.Vector; 308 | var Particle = _Halftone.Particle; 309 | 310 | // -------------------------- BreathingHalftone -------------------------- // 311 | 312 | function Halftone( img, options ) { 313 | // set options 314 | this.options = extend( {}, this.constructor.defaults, true ); 315 | extend( this.options, options, true ); 316 | 317 | this.img = img; 318 | // bail if canvas is not supported 319 | if ( !supports.canvas ) { 320 | return; 321 | } 322 | 323 | this.create(); 324 | } 325 | 326 | Halftone.defaults = { 327 | // dot size 328 | dotSize: 1/40, 329 | dotSizeThreshold: 0.05, 330 | initVelocity: 0.02, 331 | oscPeriod: 3, 332 | oscAmplitude: 0.2, 333 | // layout and color 334 | isAdditive: false, 335 | isRadial: false, 336 | channels: [ 'red', 'green', 'blue' ], 337 | isChannelLens: true, 338 | // behavoir 339 | friction: 0.06, 340 | hoverDiameter: 0.3, 341 | hoverForce: -0.02, 342 | activeDiameter: 0.6, 343 | activeForce: 0.01 344 | }; 345 | 346 | function makeCanvasAndCtx() { 347 | var canvas = document.createElement('canvas'); 348 | var ctx = canvas.getContext('2d'); 349 | return { 350 | canvas: canvas, 351 | ctx: ctx 352 | }; 353 | } 354 | 355 | 356 | 357 | Halftone.prototype.create = function() { 358 | this.isActive = true; 359 | 360 | // create main canvas 361 | var canvasAndCtx = makeCanvasAndCtx(); 362 | this.canvas = canvasAndCtx.canvas; 363 | this.ctx = canvasAndCtx.ctx; 364 | // copy over class 365 | this.canvas.className = this.img.className; 366 | insertAfter( this.canvas, this.img ); 367 | // hide img visually 368 | this.img.style.visibility = 'hidden'; 369 | 370 | // fall back to lum channel if subtractive and darker isn't supported 371 | this.channels = !this.options.isAdditive && !supports.darker ? 372 | [ 'lum' ] : this.options.channels; 373 | 374 | // create separate canvases for each color 375 | this.proxyCanvases = {}; 376 | for ( var i=0, len = this.channels.length; i < len; i++ ) { 377 | var channel = this.channels[i]; 378 | this.proxyCanvases[ channel ] = makeCanvasAndCtx(); 379 | } 380 | 381 | this.loadImage(); 382 | 383 | // properties 384 | this.canvasPosition = new Vector(); 385 | this.getCanvasPosition(); 386 | // hash of mouse / touch events 387 | this.cursors = {}; 388 | // position -100,000, -100,000 so its not on screen 389 | this.addCursor( 'mouse', { pageX: -1e5, pageY: -1e5 }); 390 | 391 | this.bindEvents(); 392 | }; 393 | 394 | Halftone.prototype.getCanvasPosition = function() { 395 | var rect = this.canvas.getBoundingClientRect(); 396 | var x = rect.left + window.pageXOffset; 397 | var y = rect.top + window.pageYOffset; 398 | this.canvasPosition.set( x, y ); 399 | this.canvasScale = this.width ? this.width / this.canvas.offsetWidth : 1; 400 | }; 401 | 402 | // -------------------------- img -------------------------- // 403 | 404 | Halftone.prototype.loadImage = function() { 405 | // hack img load 406 | var src = this.img.getAttribute('data-src') || this.img.src; 407 | var loadingImg = new Image(); 408 | loadingImg.onload = function() { 409 | this.onImgLoad(); 410 | }.bind( this ); 411 | loadingImg.src = src; 412 | // set src on image, so we can get correct sizes 413 | if ( this.img.src !== src ) { 414 | this.img.src = src; 415 | } 416 | }; 417 | 418 | Halftone.prototype.onImgLoad = function() { 419 | this.getImgData(); 420 | this.resizeCanvas(); 421 | this.getCanvasPosition(); 422 | // hide image completely 423 | this.img.style.display = 'none'; 424 | this.getCanvasPosition(); 425 | this.initParticles(); 426 | this.animate(); 427 | }; 428 | 429 | Halftone.prototype.getImgData = function() { 430 | // get imgData 431 | var canvasAndCtx = makeCanvasAndCtx(); 432 | var imgCanvas = canvasAndCtx.canvas; 433 | var ctx = canvasAndCtx.ctx; 434 | this.imgWidth = imgCanvas.width = this.img.naturalWidth; 435 | this.imgHeight = imgCanvas.height = this.img.naturalHeight; 436 | ctx.drawImage( this.img, 0, 0 ); 437 | this.imgData = ctx.getImageData( 0, 0, this.imgWidth, this.imgHeight ).data; 438 | }; 439 | 440 | Halftone.prototype.resizeCanvas = function() { 441 | // width & height 442 | var w = this.width = this.img.offsetWidth; 443 | var h = this.height = this.img.offsetHeight; 444 | // size properties 445 | this.diagonal = Math.sqrt( w*w + h*h ); 446 | this.imgScale = this.width / this.imgWidth; 447 | this.gridSize = this.options.dotSize * this.diagonal; 448 | 449 | // set proxy canvases size 450 | for ( var prop in this.proxyCanvases ) { 451 | var proxy = this.proxyCanvases[ prop ]; 452 | proxy.canvas.width = w; 453 | proxy.canvas.height = h; 454 | } 455 | this.canvas.width = w; 456 | this.canvas.height = h; 457 | }; 458 | 459 | Halftone.prototype.initParticles = function() { 460 | 461 | var getParticlesMethod = this.options.isRadial ? 462 | 'getRadialGridParticles' : 'getCartesianGridParticles'; 463 | 464 | // all particles 465 | this.particles = []; 466 | // separate array of particles for each color 467 | this.channelParticles = {}; 468 | 469 | var angles = { red: 1, green: 2.5, blue: 5, lum: 4 }; 470 | 471 | for ( var i=0, len = this.channels.length; i < len; i++ ) { 472 | var channel = this.channels[i]; 473 | var angle = angles[ channel ]; 474 | var particles = this[ getParticlesMethod ]( channel, angle ); 475 | // associate with channel 476 | this.channelParticles[ channel ] = particles; 477 | // add to all collection 478 | this.particles = this.particles.concat( particles ); 479 | } 480 | 481 | }; 482 | 483 | Halftone.prototype.animate = function() { 484 | // do not animate if not active 485 | if ( !this.isActive ) { 486 | return; 487 | } 488 | this.update(); 489 | this.render(); 490 | requestAnimationFrame( this.animate.bind( this ) ); 491 | }; 492 | 493 | Halftone.prototype.update = function() { 494 | // displace particles with cursors (mouse, touches) 495 | 496 | for ( var i=0, len = this.particles.length; i < len; i++ ) { 497 | var particle = this.particles[i]; 498 | // apply forces for each cursor 499 | for ( var identifier in this.cursors ) { 500 | var cursor = this.cursors[ identifier ]; 501 | var cursorState = cursor.isDown ? 'active' : 'hover'; 502 | var forceScale = this.options[ cursorState + 'Force' ]; 503 | var diameter = this.options[ cursorState + 'Diameter' ]; 504 | var radius = diameter / 2 * this.diagonal; 505 | var force = Vector.subtract( particle.position, cursor.position ); 506 | var distanceScale = Math.max( 0, radius - force.magnitude ) / radius; 507 | // easeInOutSine 508 | distanceScale = Math.cos( distanceScale * Math.PI ) * -0.5 + 0.5; 509 | force.scale( distanceScale * forceScale ); 510 | particle.applyForce( force ); 511 | } 512 | 513 | particle.update(); 514 | } 515 | }; 516 | 517 | Halftone.prototype.render = function() { 518 | // clear 519 | this.ctx.globalCompositeOperation = 'source-over'; 520 | this.ctx.fillStyle = this.options.isAdditive ? 'black' : 'white'; 521 | this.ctx.fillRect( 0, 0, this.width, this.height ); 522 | 523 | // composite grids 524 | this.ctx.globalCompositeOperation = this.options.isAdditive ? 'lighter' : 'darker'; 525 | 526 | // render channels 527 | for ( var i=0, len = this.channels.length; i < len; i++ ) { 528 | var channel = this.channels[i]; 529 | this.renderGrid( channel ); 530 | } 531 | 532 | }; 533 | 534 | var channelFillStyles = { 535 | additive: { 536 | red: '#FF0000', 537 | green: '#00FF00', 538 | blue: '#0000FF', 539 | lum: '#FFF' 540 | }, 541 | subtractive: { 542 | red: '#00FFFF', 543 | green: '#FF00FF', 544 | blue: '#FFFF00', 545 | lum: '#000' 546 | } 547 | }; 548 | 549 | Halftone.prototype.renderGrid = function( channel ) { 550 | var proxy = this.proxyCanvases[ channel ]; 551 | // clear 552 | proxy.ctx.fillStyle = this.options.isAdditive ? 'black' : 'white'; 553 | proxy.ctx.fillRect( 0, 0, this.width, this.height ); 554 | 555 | // set fill color 556 | var blend = this.options.isAdditive ? 'additive' : 'subtractive'; 557 | proxy.ctx.fillStyle = channelFillStyles[ blend ][ channel ]; 558 | 559 | // render particles 560 | var particles = this.channelParticles[ channel ]; 561 | for ( var i=0, len = particles.length; i < len; i++ ) { 562 | var particle = particles[i]; 563 | particle.render( proxy.ctx ); 564 | } 565 | 566 | // draw proxy canvas to actual canvas as whole layer 567 | this.ctx.drawImage( proxy.canvas, 0, 0 ); 568 | }; 569 | 570 | Halftone.prototype.getCartesianGridParticles = function( channel, angle ) { 571 | var particles = []; 572 | 573 | var w = this.width; 574 | var h = this.height; 575 | 576 | var diag = Math.max( w, h ) * ROOT_2; 577 | 578 | var gridSize = this.gridSize; 579 | var cols = Math.ceil( diag / gridSize ); 580 | var rows = Math.ceil( diag / gridSize ); 581 | 582 | for ( var row = 0; row < rows; row++ ) { 583 | for ( var col = 0; col < cols; col++ ) { 584 | var x1 = ( col + 0.5 ) * gridSize; 585 | var y1 = ( row + 0.5 ) * gridSize; 586 | // offset for diagonal 587 | x1 -= ( diag - w ) / 2; 588 | y1 -= ( diag - h ) / 2; 589 | // shift to center 590 | x1 -= w / 2; 591 | y1 -= h / 2; 592 | // rotate grid 593 | var x2 = x1 * Math.cos( angle ) - y1 * Math.sin( angle ); 594 | var y2 = x1 * Math.sin( angle ) + y1 * Math.cos( angle ); 595 | // shift back 596 | x2 += w / 2; 597 | y2 += h / 2; 598 | 599 | var particle = this.initParticle( channel, x2, y2 ); 600 | if ( particle ) { 601 | particles.push( particle ); 602 | } 603 | } 604 | } 605 | 606 | return particles; 607 | }; 608 | 609 | Halftone.prototype.getRadialGridParticles = function( channel, angle ) { 610 | var particles = []; 611 | 612 | var w = this.width; 613 | var h = this.height; 614 | var diag = Math.max( w, h ) * ROOT_2; 615 | 616 | var gridSize = this.gridSize; 617 | 618 | var halfW = w / 2; 619 | var halfH = h / 2; 620 | var offset = gridSize; 621 | var centerX = halfW + Math.cos( angle ) * offset; 622 | var centerY = halfH + Math.sin( angle ) * offset; 623 | 624 | var maxLevel = Math.ceil( ( diag + offset ) / gridSize ); 625 | 626 | for ( var level=0; level < maxLevel; level++ ) { 627 | var max = level * 6 || 1; 628 | for ( var j=0; j < max; j++ ) { 629 | var theta = TAU * j / max + angle; 630 | var x = centerX + Math.cos( theta ) * level * gridSize; 631 | var y = centerY + Math.sin( theta ) * level * gridSize; 632 | var particle = this.initParticle( channel, x, y ); 633 | if ( particle ) { 634 | particles.push( particle ); 635 | } 636 | } 637 | } 638 | 639 | return particles; 640 | 641 | }; 642 | 643 | Halftone.prototype.initParticle = function( channel, x, y ) { 644 | // don't render if coords are outside image 645 | // don't display if under threshold 646 | var pixelChannelValue = this.getPixelChannelValue( x, y, channel ); 647 | if ( pixelChannelValue < this.options.dotSizeThreshold ) { 648 | return; 649 | } 650 | 651 | return new Particle({ 652 | channel: channel, 653 | parent: this, 654 | origin: new Vector( x, y ), 655 | naturalSize: this.gridSize * ROOT_2 / 2, 656 | friction: this.options.friction 657 | }); 658 | 659 | }; 660 | 661 | var channelOffset = { 662 | red: 0, 663 | green: 1, 664 | blue: 2 665 | }; 666 | 667 | Halftone.prototype.getPixelChannelValue = function( x, y, channel ) { 668 | x = Math.round( x / this.imgScale ); 669 | y = Math.round( y / this.imgScale ); 670 | var w = this.imgWidth; 671 | var h = this.imgHeight; 672 | 673 | // return 0 if position is outside of image 674 | if ( x < 0 || x > w || y < 0 || y > h ) { 675 | return 0; 676 | } 677 | 678 | var pixelIndex = ( x + y * w ) * 4; 679 | var value; 680 | // return 1; 681 | if ( channel === 'lum' ) { 682 | value = this.getPixelLum( pixelIndex ); 683 | } else { 684 | // rgb 685 | var index = pixelIndex + channelOffset[ channel ]; 686 | value = this.imgData[ index ] / 255; 687 | } 688 | 689 | value = value || 0; 690 | if ( !this.options.isAdditive ) { 691 | value = 1 - value; 692 | } 693 | 694 | return value; 695 | }; 696 | 697 | Halftone.prototype.getPixelLum = function( pixelIndex ) { 698 | // thx @jfsiii 699 | // https://github.com/jfsiii/chromath/blob/master/src/chromath.js 700 | var r = this.imgData[ pixelIndex + 0 ] / 255; 701 | var g = this.imgData[ pixelIndex + 1 ] / 255; 702 | var b = this.imgData[ pixelIndex + 2 ] / 255; 703 | var max = Math.max( r, g, b ); 704 | var min = Math.min( r, g, b ); 705 | return ( max + min ) / 2; 706 | }; 707 | 708 | // ----- bindEvents ----- // 709 | 710 | Halftone.prototype.bindEvents = function() { 711 | this.canvas.addEventListener( 'mousedown', this, false ); 712 | this.canvas.addEventListener( 'touchstart', this, false ); 713 | window.addEventListener( 'mousemove', this, false ); 714 | window.addEventListener( 'touchmove', this, false ); 715 | window.addEventListener( 'touchend', this, false ); 716 | window.addEventListener( 'resize', this, false ); 717 | }; 718 | 719 | Halftone.prototype.unbindEvents = function() { 720 | this.canvas.removeEventListener( 'mousedown', this, false ); 721 | this.canvas.removeEventListener( 'touchstart', this, false ); 722 | window.removeEventListener( 'mousemove', this, false ); 723 | window.removeEventListener( 'touchmove', this, false ); 724 | window.removeEventListener( 'touchend', this, false ); 725 | window.removeEventListener( 'resize', this, false ); 726 | }; 727 | 728 | Halftone.prototype.handleEvent = function( event ) { 729 | var method = 'on' + event.type; 730 | if ( this[ method ] ) { 731 | this[ method ]( event ); 732 | } 733 | }; 734 | 735 | Halftone.prototype.onmousedown = function( event ) { 736 | event.preventDefault(); 737 | this.cursors.mouse.isDown = true; 738 | window.addEventListener( 'mouseup', this, false ); 739 | }; 740 | 741 | Halftone.prototype.ontouchstart = function( event ) { 742 | event.preventDefault(); 743 | for ( var i=0, len = event.changedTouches.length; i < len; i++ ) { 744 | var touch = event.changedTouches[i]; 745 | var cursor = this.addCursor( touch.identifier, touch ); 746 | cursor.isDown = true; 747 | } 748 | }; 749 | 750 | /** 751 | * @param {MouseEvent or Touch} cursorEvent - with pageX and pageY 752 | */ 753 | Halftone.prototype.addCursor = function( identifier, cursorEvent ) { 754 | var position = this.setCursorPosition( cursorEvent ); 755 | var cursor = this.cursors[ identifier ] = { 756 | position: position, 757 | isDown: false 758 | }; 759 | return cursor; 760 | }; 761 | 762 | /** 763 | * @param {MouseEvent or Touch} cursorEvent - with pageX and pageY 764 | * @param {Vector} position - optional 765 | */ 766 | Halftone.prototype.setCursorPosition = function( cursorEvent, position ) { 767 | position = position || new Vector(); 768 | position.set( cursorEvent.pageX, cursorEvent.pageY ); 769 | position.subtract( this.canvasPosition ); 770 | position.scale( this.canvasScale ); 771 | return position; 772 | }; 773 | 774 | 775 | Halftone.prototype.onmousemove = function( event ) { 776 | this.setCursorPosition( event, this.cursors.mouse.position ); 777 | }; 778 | 779 | Halftone.prototype.ontouchmove = function( event ) { 780 | // move matching cursors 781 | for ( var i=0, len = event.changedTouches.length; i < len; i++ ) { 782 | var touch = event.changedTouches[i]; 783 | var cursor = this.cursors[ touch.identifier ]; 784 | if ( cursor ) { 785 | this.setCursorPosition( touch, cursor.position ); 786 | } 787 | } 788 | }; 789 | 790 | Halftone.prototype.onmouseup = function() { 791 | this.cursors.mouse.isDown = false; 792 | window.removeEventListener( 'mouseup', this, false ); 793 | }; 794 | 795 | Halftone.prototype.ontouchend = function( event ) { 796 | // remove matching cursors 797 | for ( var i=0, len = event.changedTouches.length; i < len; i++ ) { 798 | var touch = event.changedTouches[i]; 799 | var cursor = this.cursors[ touch.identifier ]; 800 | if ( cursor ) { 801 | delete this.cursors[ touch.identifier ]; 802 | } 803 | } 804 | }; 805 | 806 | 807 | function debounceProto( _class, methodName, threshold ) { 808 | // original method 809 | var method = _class.prototype[ methodName ]; 810 | var timeoutName = methodName + 'Timeout'; 811 | 812 | _class.prototype[ methodName ] = function() { 813 | var timeout = this[ timeoutName ]; 814 | if ( timeout ) { 815 | clearTimeout( timeout ); 816 | } 817 | var args = arguments; 818 | 819 | this[ timeoutName ] = setTimeout( function() { 820 | method.apply( this, args ); 821 | delete this[ timeoutName ]; 822 | }.bind( this ), threshold || 100 ); 823 | }; 824 | } 825 | 826 | Halftone.prototype.onresize = function() { 827 | this.getCanvasPosition(); 828 | }; 829 | 830 | debounceProto( Halftone, 'onresize', 200 ); 831 | 832 | // ----- destroy ----- // 833 | 834 | Halftone.prototype.destroy = function() { 835 | this.isActive = false; 836 | this.unbindEvents(); 837 | 838 | this.img.style.visibility = ''; 839 | this.img.style.display = ''; 840 | this.canvas.parentNode.removeChild( this.canvas ); 841 | }; 842 | 843 | // -------------------------- -------------------------- // 844 | 845 | Halftone.Vector = Vector; 846 | Halftone.Particle = Particle; 847 | window.BreathingHalftone = Halftone; 848 | 849 | 850 | })( window ); 851 | 852 | --------------------------------------------------------------------------------