<!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);
}
}
})();