Skip to content

Instantly share code, notes, and snippets.

@themsleeves
Last active January 27, 2018 20:46
Show Gist options
  • Save themsleeves/ac4c5794232fded9d28d56919d906175 to your computer and use it in GitHub Desktop.
Save themsleeves/ac4c5794232fded9d28d56919d906175 to your computer and use it in GitHub Desktop.
For tumblr theme purpose...
var MAX_LIFE = 50;
var canvas = document.querySelector('canvas');
var input = document.querySelector('input');
var field = {}
var hasFocus = false;
var caret = document.createElement('span');
caret.style.position = 'absolute';
caret.style.left = 0;
caret.style.top = 0;
caret.style.margin = 0;
caret.style.width = 'auto';
caret.style.visibility = 'hidden';
document.body.appendChild(caret);
function reposition() {
field = input.getBoundingClientRect();
}
window.onload = reposition;
window.onresize = reposition;
reposition();
input.onfocus = function() {hasFocus = true;}
input.onblur = function() {hasFocus = false;}
var keys = [8,9,13,16,17,18,27,32,33,34,35,36,37,38,39,40,46,91,93,112,113,114,115,116,117,118,119,120,121,122,123];
function spawnsCharacter(keyCode) {
return keys.indexOf(keyCode) === -1;
}
function burst(intensity) {
var behavior = [
this.behavior.force(-.015,-.015),
this.behavior.cohesion(50),
this.behavior.move()
];
var size = 1.25;
var force = .7;
var lifeMin = 0;
var progress = Math.min(field.width, caret.offsetWidth) / field.width;
var offset = field.left + (field.width * progress);
var rangeMin = Math.max(field.left, offset - 30);
var rangeMax = Math.min(field.right, offset + 10);
this.spray(intensity,function(){ return [
null,null,
Vector.create(
Random.between(rangeMin + 10, rangeMax - 20),
Random.between(field.top + 15, field.bottom - 15)
),
Vector.random(force),
size + Math.random(),
Random.between(lifeMin,0),behavior
]});
// top edge
this.spray(intensity * .5,function(){ return [
null,null,
Vector.create(
Random.between(rangeMin, rangeMax),
field.top
),
Vector.random(force),
size + Math.random(),
Random.between(lifeMin,0),behavior
]});
// bottom edge
this.spray(intensity * .5,function(){ return [
null,null,
Vector.create(
Random.between(rangeMin, rangeMax),
field.top + field.height
),
Vector.random(force),
size + Math.random(),
Random.between(lifeMin,0)
,behavior
]});
// left edge
if (input.value.length === 1) {
this.spray(intensity * 2,function(){ return [
null,null,
Vector.create(
field.left + (Math.random() * 20),
Random.between(field.top,field.bottom)
),
Vector.random(force),
size + Math.random(),
Random.between(lifeMin,0),behavior
]});
}
// right edge
if (rangeMax == field.right) {
this.spray(intensity * 2,function(){ return [
null,null,
Vector.create(
field.right,
Random.between(field.top,field.bottom)
),
Vector.random(force),
size + Math.random(),
Random.between(lifeMin,0),behavior
]});
}
}
// start particle simulation
simulate(
'2d', {
init: function() {
},
tick: function(particles) {
if (!particles){ return; }
particles.forEach(function(p){
if (p.life > MAX_LIFE) {
this.destroy(p);
}
});
},
beforePaint: function() {
this.clear();
},
paint: function(particle) {
var p = particle.position;
var s = particle.size;
var o = 1 - (particle.life / MAX_LIFE);
this.paint.circle(p.x, p.y, s, 'rgba(255,255,255,' + o +')');
this.paint.circle(p.x, p.y, s + 1.5, 'rgba(231,244,255,' + (o * .25) + ')');
// extra
var w = 2;
var wh = w * .5;
var h = 35;
var hh = h * .5;
this.context.rect(p.x -wh, p.y - hh, w, h);
this.context.fillStyle = 'rgba(231,244,255,' + (o * .025) + ')';
this.context.fill();
this.context.closePath();
},
afterPaint: function() {
// nothing
},
action: function(e) {
if (!spawnsCharacter(e.keyCode)) {
return;
}
caret.textContent = input.value;
burst.call(this,12);
input.classList.add('keyup');
setTimeout(function(){input.classList.remove('keyup')},100);
}
}
);
// "simulate" particle simulation logic
/**
* Constants
*/
PI_2 = Math.PI / 2;
PI_180 = Math.PI / 180;
/**
* Random
*/
var Random = {
between: function(min, max) {
return min + (Math.random() * (max - min));
}
}
/**
* 2D Vector Class
*/
function Vector(x, y) {
this._x = x || 0;
this._y = y || 0;
}
Vector.create = function(x, y) {
return new Vector(x, y);
};
Vector.add = function(a, b) {
return new Vector(a.x + b.x, a.y + b.y);
};
Vector.subtract = function(a, b) {
return new Vector(a.x - b.x, a.y - b.y);
};
Vector.random = function(range) {
var v = new Vector();
v.randomize(range);
return v;
};
Vector.distanceSquared = function(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return dx * dx + dy * dy;
};
Vector.distance = function(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
};
Vector.prototype = {
get x() {
return this._x;
},
get y() {
return this._y;
},
set x(value) {
this._x = value;
},
set y(value) {
this._y = value;
},
get magnitudeSquared() {
return this._x * this._x + this._y * this._y;
},
get magnitude() {
return Math.sqrt(this.magnitudeSquared);
},
get angle() {
return Math.atan2(this._y, this._x) * 180 / Math.PI;
},
clone: function() {
return new Vector(this._x, this._y);
},
add: function(v) {
this._x += v.x;
this._y += v.y;
},
subtract: function(v) {
this._x -= v.x;
this._y -= v.y;
},
multiply: function(value) {
this._x *= value;
this._y *= value;
},
divide: function(value) {
this._x /= value;
this._y /= value;
},
normalize: function() {
var magnitude = this.magnitude;
if (magnitude > 0) {
this.divide(magnitude);
}
},
limit: function(treshold) {
if (this.magnitude > treshold) {
this.normalize();
this.multiply(treshold);
}
},
randomize: function(amount) {
amount = amount || 1;
this._x = amount * 2 * (-.5 + Math.random());
this._y = amount * 2 * (-.5 + Math.random());
},
rotate: function(degrees) {
var magnitude = this.magnitude;
var angle = ((Math.atan2(this._x, this._y) * PI_HALF) + degrees) * PI_180;
this._x = magnitude * Math.cos(angle);
this._y = magnitude * Math.sin(angle);
},
flip: function() {
var temp = this._y;
this._y = this._x;
this._x = temp;
},
invert: function() {
this._x = -this._x;
this._y = -this._y;
},
toString: function() {
return this._x + ', ' + this._y;
}
}
/**
* Particle Class
*/
function Particle(id, group, position, velocity, size, life, behavior) {
this._id = id || 'default';
this._group = group || 'default';
this._position = position || new Vector();
this._velocity = velocity || new Vector();
this._size = size || 1;
this._life = Math.round(life || 0);
this._behavior = behavior || [];
}
Particle.prototype = {
get id() {
return this._id;
},
get group() {
return this._group;
},
get life() {
return this._life;
},
get size() {
return this._size;
},
set size(size) {
this._size = size;
},
get position() {
return this._position;
},
get velocity() {
return this._velocity;
},
update: function(stage) {
this._life++;
var i = 0;
var l = this._behavior.length;
for (; i < l; i++) {
this._behavior[i].call(stage, this);
}
},
toString: function() {
return 'Particle(' + this._id + ') ' + this._life + ' pos: ' + this._position + ' vec: ' + this._velocity;
}
}
// setup DOM
function simulate(dimensions, options) {
// private vars
var particles = [];
var destroyed = [];
var update = update || function() {};
var stage = stage || function() {};
var canvas;
var context;
if (!options) {
console.error('"options" object must be defined');
return;
}
if (!options.init) {
console.error('"init" function must be defined');
return;
}
if (!options.paint) {
console.error('"paint" function must be defined');
return;
}
if (!options.tick) {
options.tick = function() {};
}
if (!options.beforePaint) {
options.beforePaint = function() {};
}
if (!options.afterPaint) {
options.afterPaint = function() {};
}
if (!options.action) {
options.action = function() {};
}
if (document.readyState === 'interactive') {
setup();
} else {
document.addEventListener('DOMContentLoaded', setup);
}
// resizes canvas to fit window dimensions
function fitCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
// create canvas for drawing
function setup() {
// create
canvas = document.createElement('canvas');
document.body.appendChild(canvas);
// correct canvas size on window resize
window.addEventListener('resize', fitCanvas);
// go
go();
}
// canvas has been attached, let's go!
function go() {
// set initial canvas size
fitCanvas();
// get context for drawing
context = canvas.getContext(dimensions);
// simulation update loop
function act() {
// update particle states
var i = 0;
var l = particles.length;
var p;
for (; i < l; i++) {
particles[i].update(this);
}
// clean destroyed particles
while (p = destroyed.pop()) {
do {
// has not been found in destroyed array?
if (p !== particles[i]) {
continue;
}
// remove particle
particles.splice(i, 1);
} while (i-- >= 0)
}
// repaint context
options.beforePaint.call(this);
// repaint particles
i = 0;
l = particles.length;
for (; i < l; i++) {
options.paint.call(this, particles[i]);
}
// after particles have been painted
options.afterPaint.call(this);
}
function tick() {
// call update method, this allows for inserting particles later on
options.tick.call(this, particles);
// update particles here
act();
// on to the next frame
window.requestAnimationFrame(tick);
}
/**
* API
**/
function clear() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
function destroy(particle) {
destroyed.push(particle);
}
function add(id, group, position, velocity, size, life, behavior) {
particles.push(new Particle(id, group, position, velocity, size, life, behavior));
}
function spray(amount, config) {
var i = 0;
for (; i < amount; i++) {
add.apply(this, config());
}
}
function debug(particle) {
this.paint.circle(
particle.position.x,
particle.position.y,
particle.size,
'rgba(255,0,0,.75)'
);
context.beginPath();
context.moveTo(particle.position.x, particle.position.y);
context.lineTo(particle.position.x + (particle.velocity.x * 10), particle.position.y + (particle.velocity.y * 10));
context.strokeStyle = 'rgba(255,0,0,.1)';
context.stroke();
context.closePath();
};
this.clear = clear;
this.destroy = destroy;
this.add = add;
this.spray = spray;
this.debug = debug;
this.paint = {
circle: function(x, y, size, color) {
context.beginPath();
context.arc(x, y, size, 0, 2 * Math.PI, false);
context.fillStyle = color;
context.fill();
},
square: function(x, y, size, color) {
context.beginPath();
context.rect(x - (size * .5), y - (size * .5), size, size);
context.fillStyle = color;
context.fill();
}
}
this.behavior = {
cohesion: function(range, speed) {
range = Math.pow(range || 100, 2);
speed = speed || .001;
return function(particle) {
var center = new Vector();
var i = 0;
var l = particles.length;
var count = 0;
if (l <= 1) {
return;
}
for (; i < l; i++) {
// don't use self in group
if (particles[i] === particle || Vector.distanceSquared(particles[i].position, particle.position) > range) {
continue;
}
center.add(Vector.subtract(particles[i].position, particle.position));
count++;
}
if (count > 0) {
center.divide(count);
center.normalize();
center.multiply(particle.velocity.magnitude);
center.multiply(.05);
}
particle.velocity.add(center);
}
},
separation: function(distance) {
var distance = Math.pow(distance || 25, 2);
return function(particle) {
var heading = new Vector();
var i = 0;
var l = particles.length;
var count = 0;
var diff;
if (l <= 1) {
return;
}
for (; i < l; i++) {
// don't use self in group
if (particles[i] === particle || Vector.distanceSquared(particles[i].position, particle.position) > distance) {
continue;
}
// stay away from neighbours
diff = Vector.subtract(particle.position, particles[i].position);
diff.normalize();
heading.add(diff);
count++;
}
if (count > 0) {
// get average
heading.divide(count);
// make same length as current velocity (so particle won't speed up)
heading.normalize();
heading.multiply(particle.velocity.magnitude);
// limit force to make particle movement smoother
heading.limit(.1);
}
particle.velocity.add(heading);
}
},
alignment: function(range) {
range = Math.pow(range || 100, 2);
return function(particle) {
var i = 0;
var l = particles.length;
var count = 0;
var heading = new Vector();
if (l <= 1) {
return;
}
for (; i < l; i++) {
// don't use self in group also don't align when out of range
if (particles[i] === particle || Vector.distanceSquared(particles[i].position, particle.position) > range) {
continue;
}
heading.add(particles[i].velocity);
count++;
}
if (count > 0) {
heading.divide(count);
heading.normalize();
heading.multiply(particle.velocity.magnitude);
// limit
heading.multiply(.1);
}
particle.velocity.add(heading);
}
},
move: function() {
return function(particle) {
particle.position.add(particle.velocity);
// handle collisions?
}
},
eat: function(food) {
food = food || [];
return function(particle) {
var i = 0;
var l = particles.length;
var prey;
for (; i < l; i++) {
prey = particles[i];
// can't eat itself, also, needs to be tasty
if (prey === particle || food.indexOf(prey.group) === -1) {
continue;
}
// calculate force vector
if (Vector.distanceSquared(particle.position, neighbour.position) < 2 && particle.size >= neighbour.size) {
particle.size += neighbour.size;
destroy(neighbour);
}
}
}
},
force: function(x, y) {
return function(particle) {
particle.velocity.x += x;
particle.velocity.y += y;
}
},
limit: function(treshold) {
return function(particle) {
particle.velocity.limit(treshold);
}
},
attract: function(forceMultiplier, groups) {
forceMultiplier = forceMultiplier || 1;
groups = groups || [];
return function(particle) {
// attract other particles
var totalForce = new Vector(0, 0);
var force = new Vector(0, 0);
var i = 0;
var l = particles.length;
var distance;
var pull;
var attractor;
var grouping = groups.length;
for (; i < l; i++) {
attractor = particles[i];
// can't be attracted by itself or mismatched groups
if (attractor === particle || (grouping && groups.indexOf(attractor.group) === -1)) {
continue;
}
// calculate force vector
force.x = attractor.position.x - particle.position.x;
force.y = attractor.position.y - particle.position.y;
distance = force.magnitude;
force.normalize();
// the bigger the attractor the more force
force.multiply(attractor.size / distance);
totalForce.add(force);
}
totalForce.multiply(forceMultiplier);
particle.velocity.add(totalForce);
}
},
wrap: function(margin) {
return function(particle) {
// move around when particle reaches edge of screen
var position = particle.position;
var radius = particle.size * .5;
if (position.x + radius > canvas.width + margin) {
position.x = radius;
}
if (position.y + radius > canvas.height + margin) {
position.y = radius;
}
if (position.x - radius < -margin) {
position.x = canvas.width - radius;
}
if (position.y - radius < -margin) {
position.y = canvas.height - radius;
}
}
},
reflect: function() {
return function(particle) {
// bounce from edges
var position = particle.position;
var velocity = particle.velocity;
var radius = particle.size * .5;
if (position.x + radius > canvas.width) {
velocity.x = -velocity.x;
}
if (position.y + radius > canvas.height) {
velocity.y = -velocity.y;
}
if (position.x - radius < 0) {
velocity.x = -velocity.x;
}
if (position.y - radius < 0) {
velocity.y = -velocity.y;
}
}
},
edge: function(action) {
return function(particle) {
var position = particle.position;
var velocity = particle.velocity;
var radius = particle.size * .5;
if (position.x + radius > canvas.width) {
action(particle);
}
if (position.y + radius > canvas.height) {
action(particle);
}
if (position.x - radius < 0) {
action(particle);
}
if (position.y - radius < 0) {
action(particle);
}
}
}
}
// public
Object.defineProperties(this, {
'particles': {
get: function() {
return particles;
}
},
'width': {
get: function() {
return canvas.width;
}
},
'height': {
get: function() {
return canvas.height;
}
},
'context': {
get: function() {
return context;
}
}
});
// call init method so the scene can be setup
options.init.call(this)
// start ticking
tick();
// start listening to events
var self = this;
document.addEventListener('keyup', function(e) {
options.action.call(self, e);
});
}
};
(function(window, undefined) {
window.TumblrTags = function(username) {
this.callbacks = {
'ready': []
};
this.username = '';
this.domain = 'tumblr_tags';
this.uniqueId = '';
this.totalItems = -1;
this.postOffset = 0;
this.postsPerPage = 50;
this.posts = [];
this.tags = {};
this.scriptCounter = 0;
this.processed = 0;
this.enqueued = 0;
this.batchInterval = false;
this.batchSize = 7;
this.batchTimeoutInMs = 2000;
/**
* Initialize the tag fetcher.
*
* @param {string} username The tumblr username.
*
* @return void.
*/
this.initalize = function(username) {
this.username = username;
this.uniqueId = this.domain + '_' + Math.round(Math.random()*10000);
window[this.uniqueId] = this.handleItems.bind(this);
};
/**
* Starts loading all the scripts.
*
* @return void.
*/
this.load = function() {
if(!username) {
throw new Error('Invalid username.');
}
this.totalItems = -1;
this.postOffset = 0;
this.postsPerPage = 50;
this.enqueueScript();
};
/**
* Callback handler for JSONP script.
*
* @param {object} data The data from the JSONP script.
*
* @return void.
*/
this.handleItems = function(data) {
this.postOffset += data.posts.length;
this.processed++;
if(this.totalItems === -1) {
this.totalItems = data['posts-total'];
}
if ((this.postOffset < this.totalItems) && this.processed == this.enqueued && this.batchInterval == false) {
this.batchInterval = setTimeout(
this.runBatch.bind(this),
this.batchTimeoutInMs
)
}
this.posts = this.posts.concat(data.posts);
if(this.postOffset >= this.totalItems) {
this.fetchTags(this.posts);
this.removeScriptTags();
this.executeCallback('ready', [this.getTags()]);
}
};
/**
* Runs a batch of requests
*
* @return void.
*/
this.runBatch = function() {
var pages = Math.ceil(this.totalItems / this.postsPerPage);
var start = this.processed;
var end = this.processed + this.batchSize;
end = (end*this.postsPerPage > this.totalItems ? Math.ceil(this.totalItems/this.postsPerPage) : end);
for(var i = start; i < end; i++) {
this.enqueueScript(this.postsPerPage, i*this.postsPerPage);
}
this.batchInterval = false;
}
/**
* Removes all script tags added by this script.
*
* @return void.
*/
this.removeScriptTags = function() {
var scripts = document.querySelectorAll("[id*='" + this.uniqueId + "-']");
for(var i = 0; i < scripts.length; i++) {
scripts[i].parentNode.removeChild(scripts[i]);
}
}
/**
* Fetches all tags from an array of posts.
*
* @param {array} posts Posts to fetch tags from.
*
* @return void.
*/
this.fetchTags = function(posts) {
for(var i = 0; i < posts.length; i++) {
var post = this.posts[i];
if(post.tags) {
for(var n = 0; n < post.tags.length; n++) {
this.addTag(post.tags[n]);
}
}
}
};
/**
* Adds a tag to the taglist (if tag already exists, add to hits counter)
*
* @param {string} tag The tag
*
* @return void.
*/
this.addTag = function(tag) {
if(this.tags[tag]) {
this.tags[tag].hits += 1;
} else {
this.tags[tag] = {
'tag': tag,
'hits': 1
}
}
};
/**
* Fetches all tags from the blog.
* (you need to run load() before calling this method)
*
* @return array.
*/
this.getTags = function() {
var tags = [];
for(var prop in this.tags) {
if(!this.tags.hasOwnProperty(prop)) {
continue;
}
tags.push(this.tags[prop]);
}
return tags;
};
/**
* Enqueues a script to the body element.
* (The script will be removed once everything has been loaded)
*
* @param {int} limit Optional limit, default: 50, max: 50
* @param {int} offset Optional offset
*
* @return void.
*/
this.enqueueScript = function(limit, offset) {
limit = limit || this.postsPerPage;
offset = offset || this.postOffset;
this.enqueued++;
var id = this.uniqueId + '-' + this.scriptCounter;
var script = document.createElement('script');
script.id = id;
script.src = 'http://' + this.username + '.tumblr.com/api/read/json?callback=' + this.uniqueId + '&num=' + limit + '&start=' + offset;
document.body.appendChild(script);
};
this.initalize(username);
};
/* Add event handling to the prototype of the tumblr tag handler */
TumblrTags.prototype = {
/**
* Available callbacks. Should be overriden.
* @type {Object}
*/
callbacks: {},
/**
* Execute callbacks
*
* @param {String} event The event type.
* @param {Array} args Optional arguments.
*
* @return void.
*/
executeCallback: function(event, args) {
if (!this.callbacks[event]) {
throw new Error('Invalid event given. ' + String(event));
}
if (!args) {
args = [];
}
for (var i in this.callbacks[event]) {
if (!this.callbacks[event].hasOwnProperty(i)) continue;
this.callbacks[event][i].apply(null, args);
}
},
/**
* Register callback
*
* @param {String} event The event type.
* @param {function} callback The callback.
* @param {String} id Id of the callback.
* @return void.
*/
on: function(event, callback, id) {
if (!this.callbacks[event]) {
throw new Error('Invalid event given. ' + String(event));
}
if (!(callback instanceof Function)) {
throw new Error('Invalid callback given.');
}
if (!id) {
id = Math.random()*100000;
}
this.callbacks[event][id] = callback;
},
/**
* Unregister callback
*
* @param {String} event The event type.
* @param {String} id Id of the callback.
* @return void.
*/
off: function(event, id) {
if (!this.callbacks[event]) {
throw new Error('Invalid event given. ' + String(event));
}
var callbacks = [];
for (var i in this.callbacks[event]) {
if (!this.callbacks[event].hasOwnProperty(i)) continue;
if (i == id) continue;
callbacks[i] = this.callbacks[event][i];
}
this.callbacks[event] = callbacks;
},
};
})(window);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment