<!DOCTYPE html>
<html>

<head>

</head>

<body ng-app="app" ng-controller="CTRLController">

  <particle-text color="#5F04B4" motion-color="#B388FF" spacing="3" ease="0.2" size="2" font-size="30" height-padding="5" text="{{logo}}" radius="20">
  </particle-text>
  <script data-require="jquery@3.1.1" data-semver="3.1.1" src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
  <script data-require="angularjs@1.6.4" data-semver="1.6.4" src="https://code.angularjs.org/1.6.4/angular.min.js"></script>
  <script src="main.js"></script>
  <script src="image.service.js"></script>
  <script src="shuffle.filter.js"></script>
  <script src="particle.service.js"></script>
  <script src="particle-animator.service.js"></script>
  <script src="particle-text.directive.js"></script>
  <script src="controller.js"></script>
  
</body>

</html>

(function() {
    'use strict';
    angular
        .module('app')
        .factory('Particle', ParticleFactory);


    function ParticleFactory () {


        class Particle {
            constructor(x, y, originX, originY, color, atributes) {
                this.originalColor = this.color = color;
                this.originX       = originX;
                this.originY       = originY;
                this.atributes     = atributes;

                this.x  = x;
                this.y  = y;
                this.vx = 0;
                this.vy = 0;
            }

            inOrigin() {
                return Math.abs(this.originY - this.y) < 1
                    && Math.abs(this.originX - this.x) < 1;
            }

            update({x: mx, y: my}, r) {
                let rx = mx - this.x;
                let ry = my - this.y;
                let distance = rx * rx + ry * ry;

                if (distance < r) {
                    let force = -r / distance;
                    let angle = Math.atan2(ry, rx);
                    this.vx += force * Math.cos(angle);
                    this.vy += force * Math.sin(angle);
                }
                this.x += (this.vx *= this.atributes.friction)
                       + (this.originX - this.x)
                       * this.atributes.ease;

                this.y += (this.vy *= this.atributes.friction)
                       + (this.originY - this.y)
                       * this.atributes.ease;

                this.color = this.inOrigin()
                    ? this.originalColor
                    : this.atributes.motionColor;

                this.color = this.color
                    || this.originalColor;
            }

            reset(x, y, color) {
                this.originalColor = color;
                this.originX = x;
                this.originY = y;
            }

            draw(context) {
                context.fillStyle = this.color;
                context.fillRect(this.x, this.y,
                    this.atributes.size,
                    this.atributes.size);
            }
        }
        class ParticleBuilder {

            constructor() {
                this.x = this.originX = 0;
                this.y = this.originY = 0 ;
                this.color = '#000000';

                this.commonAttr = {
                    friction:  0.95,
                    ease: 0.1,
                    size:  3,
                    motionColor: undefined,
                };
            }

            build() {
                return new Particle(
                    this.x, this.y,
                    this.originX,
                    this.originY,
                    this.color,
                    this.commonAttr);
            }

            setColor(color) {
                this.color = color;
                return this;
            }

            setOriginX(x) {
                this.originX = x;
                return this;
            }
            setOriginY(y) {
                this.originY = y;
                return this;
            }

            setX(x) {
                this.x = x;
                return this;
            }

            setY(y) {
                this.y = y;
                return this;
            }

            setLocation (x, y) {
                return this
                    .setX(x)
                    .setY(y)
                    .setOriginX(x)
                    .setOriginY(y);
            }


            setMotionColor (color) {
                this.commonAttr.motionColor = color
                    ||  this.commonAttr.motionColor;
                return this;
            }

            setEase(ease) {
                this.commonAttr.ease = ease
                    ||  this.commonAttr.ease;
                return this;
            }

            setFriction(friction) {
                this.commonAttr.friction = friction
                    || this.commonAttr.friction;
                return this;
            }

            setSize(size) {
                this.commonAttr.size = size
                    || this.commonAttr.size;
                return this;
            }
        }


        class ParticleObserver {

            constructor(builder) {
                this.particles = [];
                this.builder = builder;
            }

            update(mouse, r) {
                for(let p of  this.particles) {
                    p.update(mouse, r);
                }
            }

            render(context) {
                let canvas = context.canvas;
                context.clearRect(0, 0, canvas.width,  canvas.height);
                for(let p of  this.particles) {
                    p.draw(context);
                }
            }

            reset(index, x, y, color) {
                if(index < this.particles.length) {
                    this.particles[index].reset(x, y, color);
                }
                else {
                    let p = this.builder
                        .setOriginX(x)
                        .setOriginY(y)
                        .setColor(color)
                        .build();
                    this.particles.push(p);
                }
            }

            resize(len) {
                this.particles.length = len;
            }

        }

        return {
            builder : () => new ParticleBuilder(),
            observer : builder => new ParticleObserver(builder)
        };
    }
})();
(function() {
    'use strict';
    angular
        .module('app')
        .factory('ParticleAnimator', ParticleImageAnimatorService);

    ParticleImageAnimatorService.$inject = ['Particle', '$window', 'ImageUtil', 'shuffleFilter'];

    function ParticleImageAnimatorService (Particle, $window, ImageUtil, shuffleFilter) {

        class ParticleImage {
            constructor(builder, context) {
                this.observer  = Particle.observer(builder);
                this.context   = context;
                this.spacing   = 3;
                this.animating = false;
                this.color = null;
            }
            setSpacing(spacing){
                this.spacing = spacing
                    || this.spacing;
                return this;
            }

            setColor(color){
                this.color = color;
                return this;
            }

            reset(callback) {
                let context = this.context;
                callback(context);

                let height = context.canvas.height;
                let width  = context.canvas.width;
                shuffleFilter(this.observer.particles);
                this.observer.builder
                    .setX(Math.random() * width)
                    .setY(Math.random() * height);

                let gen = ImageUtil
                    .forEachAlphaPixel(context, this.spacing);
                let i = 0;
                for (let [x, y, {R, G, B, A}] of gen) {
                    let color = this.color || `rgba(${R},${G},${B},${A})`;
                    this.observer.reset(i++, x, y, color);
                }
                this.observer.resize(i);
                return this;
            }


            start(mouse, radius) {
                this.animating = true;
                animate(this, mouse, radius);
                return this;
            }

            stop() {
                this.animating = false;
                return this;
            }
        }

        function animate(self, mouse, radius) {
            if(!self.animating) {
                return;
            }
            self.observer.update(mouse, radius);
            self.observer.render(self.context);
            $window.requestAnimationFrame(
                ()=> animate(self, mouse, radius));
        }

        return {
            create : (builder, context) => new ParticleImage(builder, context),
        };
    }
})();
angular.module('app')
    .directive("particleText", ParticleText);

ParticleText.$inject = ['Particle', 'ParticleAnimator', '$document'];

function ParticleText(Particle, ParticleAnimator, $document) {
    return {
        restrict: 'E',
        template: '<canvas/>',
        link: function(scope, element, attrs) {

            let canvas       = element.find('canvas')[0];
            let context      = canvas.getContext('2d');
            let r            = parseInt(attrs.radius, 10) || 20;
            let radius       =  r * r;
            let mouse        =  {x: canvas.width, y: canvas.height};
            let fontFamilly  =  attrs.fontFamilly || 'Arial';
            let fontSize     = parseInt(attrs.fontSize, 10) || 30;
            let padding      = parseInt(attrs.heightPadding, 10) || 3;
            let color        = attrs.color || 'black';
            let text         = attrs.text;
            canvas.height    = fontSize + padding;
            context.globalAlpha = 0.7;
            addEvents();
            
            
            let builder = Particle.builder()
                .setFriction(parseFloat(attrs.friction))
                .setEase(parseFloat(attrs.ease))
                .setSize(parseInt(attrs.size, 10))
                .setMotionColor(attrs.motionColor);

            let pia = ParticleAnimator.create(builder, context)
                .setSpacing(parseInt(attrs.spacing, 10))
                .setColor(color);


            function paintText(context) {
                let font     = `${fontSize}pt ${fontFamilly}`;
                context.font = font;
                let textSize = context.measureText(text);
                let height   = context.canvas.height;
                let width    = context.canvas.width = textSize.width;

                context.clearRect(0, 0, width, height);
                context.font = font;
                context.fillText(text,  0, (height / 2) + (fontSize/2));
            }

            function addEvents() {
                $document.bind("mousemove", onMouseMove);
                $document.bind("touchstart", onTouchStart, false);
                $document.bind("touchmove", onTouchMove, false);
                $document.bind("touchend", onTouchend, false);
            }

            function onDestroy() {
                $document.unbind("mousemove", onMouseMove);
                $document.unbind("touchstart", onTouchStart);
                $document.unbind("touchmove", onTouchMove);
                $document.unbind("touchend", onTouchend);
            }

            function setMouse(x, y) {
                var rect = canvas.getBoundingClientRect();
                mouse.x = x - rect.left;
                mouse.y = y - rect.top;
            }

            function onMouseMove(event) {
                setMouse(event.clientX, event.clientY);
            }

            function onTouchStart(event) {
                setMouse(event.changedTouches[0].clientX,
                    event.changedTouches[0].clientY);
            }

            function onTouchMove(event) {
                event.preventDefault();
                setMouse(event.targetTouches[0].clientX,
                    event.targetTouches[0].clientY);
            }
            function onTouchend(event){
                event.preventDefault();
                setMouse(0, 0);
            }


            attrs.$observe('text', function (interpolatedText) {
                text = interpolatedText;
                pia.reset(paintText);
            });
            pia.reset(paintText);
            pia.start(mouse, radius);
            scope.$on('$destroy', onDestroy);
        }
    };
}

(function () {
    'use strict';

    angular
        .module('app')
        .factory('ImageUtil', ImageUtil);

    function ImageUtil () {
        function preloadImages(srcs) {
            function loadImage(src) {
                return new Promise(function(resolve, reject) {
                    let img = new Image();
                    img.onload = function() {
                        resolve(img);
                    };
                    img.onerror = img.onabort = function() {
                        reject(src);
                    };
                    img.src = src;
                });
            }
            return Promise.all(srcs.map(loadImage));
        }

        function* forEachPixel(context,  spacing) {
            let width  = context.canvas.width;
            let height = context.canvas.height;
            let pixels = context.getImageData(0, 0, width, height).data;

            for(let y = 0; y < height; y += spacing) {
                for (let x = 0; x < width; x += spacing) {
                    let i = (y * width + x) * 4;
                    let RGBA = {
                        R: pixels[i], G: pixels[i + 1],
                        B: pixels[i + 2], A: pixels[i + 3]
                    };
                    yield [x, y, RGBA];
                }
            }
        }

        function* forEachAlphaPixel(context,  spacing) {
            for(let a  of forEachPixel(context,  spacing)) {
                if(a[2].A > 0) {
                    yield a;
                }
            }
        }

        return {
            forEachPixel: forEachPixel,
            forEachAlphaPixel: forEachAlphaPixel,
            preloadImages: preloadImages
        };
    }
})();
(function () {
    'use strict';
    angular.module('app')
        .filter('shuffle', shuffle);

    function shuffle() {
        return function (a) {
            for (let i = a.length; i; i--) {
                let j = Math.floor(Math.random() * i);
                [ a[i - 1], a[j]] = [a[j], a[i - 1]];
            }
            return a;
        };
    }

})();
angular.module('app', []);
(function() {
    'use strict';

    angular
        .module('app')
        .controller('CTRLController', CTRLController);

    CTRLController.$inject = ['$scope', '$timeout'];

    function CTRLController ($scope, $timeout) {


        let logo  = ['Hello', 'Code', 'Review'];
        let current = 0;
        logoChange();
        function logoChange() {
            $scope.logo = logo[++current % logo.length];
            $timeout(logoChange, 1000);
        }
    }
})();