Skip to content

Instantly share code, notes, and snippets.

@loklaan
Last active September 27, 2023 02:21
Show Gist options
  • Save loklaan/d581269d1b80f75740ab to your computer and use it in GitHub Desktop.
Save loklaan/d581269d1b80f75740ab to your computer and use it in GitHub Desktop.
Jittering Particles

#Canvas 2D Particles

With no external libraries, just the standard browser features, you can make cool 2d renderings.

Usage

Enable and disable mouse movement controlled Hue:

window.mouseEffect(bool isOn)

Enable or disable FPS logger in console:

window.consoleFps(bool isOn)

Secret keyboard shortcuts

Issues

Rendering makes heavy use of the CPU, which probably isn't a good thing.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>LANDING</title>
<link href='http://fonts.googleapis.com/css?family=Lato|Raleway' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="normalize.min.js">
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="particles"></canvas>
<div class="navbar">
<a href="#" class="btn bg-opaque-white">Blog</a>
<a href="#" class="btn bg-opaque-white">Portfolio</a>
<a href="#" class="btn bg-opaque-white">Say Hi</a>
</div>
<div class="center">
<div class="bg-opaque-white banner">
<h1>TITLE</h1>
<h2 id="title-mod">&amp; A WEBSITE</h2>
</div>
</div>
<script>
var modCount = 0,
mods = [
'press <em>space</em>',
'hold <em>up or down</em>',
'hold <em>right or left</em>'
];
function randomMod() {
return mods[Math.ceil(Math.random() * mods.length) - 1];
}
window.addEventListener('load', function() {
var currentMod = '',
el = document.getElementById('title-mod');
setInterval(function() {
var newMod = currentMod;
while (newMod === currentMod) {
newMod = randomMod().toUpperCase();
}
el.classList.remove('fadein');
el.classList.add('fadeout');
setTimeout(function() {
el.classList.remove('fadeout');
el.classList.add('fadein');
currentMod = el.innerHTML = newMod;
}, 520);
}, 5000);
});
</script>
<script src="visual.js" type="text/javascript"></script>
</body>
</html>
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}
body {
font-family: "Lato", "Open Sans", "sans-serif";
background-color: #F3F3F3;
color: #333333;
font-size: 1.2em;
}
h1,
h2 {
font-family: "Raleway", sans-serif;
font-size: 4em;
font-variant: small-caps;
font-weight: 400;
}
h2 {
font-size: 2.5em;
}
a {
color: #474747;
text-decoration: underline;
}
a:visited {
color: #474747;
}
.center
{
position: absolute;
margin: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 14em;
text-align: center;
}
.banner {
background-color: hsla(0, 0%, 100%, 0.5);
padding: 1em 0;
}
.navbar {
padding: 1em;
float: right;
}
#particles {
z-index: -1;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
height: 100%;
width: 100%;
}
button,
.btn {
text-align: center;
display: inline-block;
box-sizing: border-box;
color: #333333;
text-decoration: none;
padding: 10px;
margin: 2px;
background: none;
border: solid 1px #333333;
border-radius: 1px;
}
button:active,
.btn:active,
button:hover,
.btn:hover {
background-color: hsla(0, 0%, 100%, 0.3);
border-color: #8A8A8A;
color: #4A4A4A;
border-radius: 2px;
}
.bg-opaque-white {
background-color: hsla(0, 0%, 100%, 0.5);
}
.fadein {
-webkit-animation: fadein 0.7s linear;
-moz-animation: fadein 0.7s linear;
-o-animation: fadein 0.7s linear;
animation: fadein 0.7s linear;
}
.fadeout {
-webkit-animation: fadeout 0.55s linear;
-moz-animation: fadeout 0.55s linear;
-o-animation: fadeout 0.55s linear;
animation: fadeout 0.55s linear;
}
@-webkit-keyframes fadein {
0% { opacity: 0; text-shadow: 0 0 0.05em #333; }
75% { opacity: 1; }
100% { text-shadow: none; }
}
@-moz-keyframes fadein {
0% { opacity: 0; text-shadow: 0 0 0.05em #333; }
75% { opacity: 1; }
100% { text-shadow: none; }
}
@-o-keyframes fadein {
0% { opacity: 0; text-shadow: 0 0 0.05em #333; }
75% { opacity: 1; }
100% { text-shadow: none; }
}
@keyframes fadein {
0% { opacity: 0; text-shadow: 0 0 0.05em #333; }
75% { opacity: 1; }
100% { text-shadow: none; }
}
@-webkit-keyframes fadeout {
25% { opacity: 1; }
75% { text-shadow: 0 0 0.05em #333; }
100% { opacity: 0; }
}
@-moz-keyframes fadeout {
25% { opacity: 1; }
75% { text-shadow: 0 0 0.05em #333; }
100% { opacity: 0; }
}
@-o-keyframes fadeout {
25% { opacity: 1; }
75% { text-shadow: 0 0 0.05em #333; }
100% { opacity: 0; }
}
@keyframes fadeout {
25% { opacity: 1; }
75% { text-shadow: 0 0 0.05em #333; }
100% { opacity: 0; }
}
@media all and (max-width: 520px) {
.navbar {
float: none;
}
button,
.btn {
width: 100%;
display: block;
margin-top: 0;
margin-bottom: -1px;
}
}
;(function() {
'use strict';
// CONSTANTS lol not really
var SHOW_CONSOLE_FPS = false,
VISUAL_MOUSE_EFFECT = false,
VISUAL_SPEED_MULT = 5, // 'master' speed var
VISUAL_RESPEED_STEP = 0.5,
VISUAL_HUE = Math.random() * (255 + 1),
VISUAL_SATURATION = 48,
VISUAL_LIGHTING = 80,
PARTICLE_AMOUNT = 50,
PARTICLE_MAX_SIZE = 8, // px
PARTICLE_MIN_SIZE = 2,
PARTICLE_JITTER_RATE,
PARTICLE_BLINK_RATE,
KEYS = {
LEFT: 37,
RIGHT: 39,
UP: 38,
DOWN: 40,
SPACE: 32
};
var frames = 0, // fps counter
skip = false, // switch bool for skipping every second frame
userSpeed = 0,
userAmount = 0;
/**
* A single visualisation instance, handling updating and
* drawing of visual content.
*/
var Visual = function(canvasContext, width, height) {
var screen = canvasContext;
var self = this;
self.size = {
x: width,
y: height
};
console.log('~~~~');
console.log('Particle Visualisation started.');
console.log(' Particles: ' + PARTICLE_AMOUNT + '.');
console.log(' Size: ' + self.size.x + 'px * ' + self.size.y + 'px.');
console.log('~~~~');
self.particles = [];
for (var i = 0; i < PARTICLE_AMOUNT; i++) {
self.particles.push(new Particle(self, self.size));
}
// per frame jobs
self.tick = function() {
if (!skip) {
self.update();
self.draw(screen, self.size);
frames++;
}
skip = !skip;
self.animationRequest = window.requestAnimationFrame(self.tick);
};
// setup cheeky hidden controls
window.onkeydown = function(e) {
var key = e.keyCode;
if (key === KEYS.UP) {
self.particles.push(new Particle(self, self.size));
} else if (key === KEYS.DOWN) {
self.particles.shift();
} else if (key === KEYS.RIGHT) {
var restart = VISUAL_SPEED_MULT === 0;
_.cheekyRespeed(VISUAL_RESPEED_STEP);
if (restart) {
self.start();
}
} else if (key === KEYS.LEFT) {
var alreadyStopped = VISUAL_SPEED_MULT === 0;
_.cheekyRespeed(-(VISUAL_RESPEED_STEP));
if (VISUAL_SPEED_MULT === 0 && !alreadyStopped) {
self.stop();
}
} else if (key === KEYS.SPACE) {
self.changeHue(Math.random() * (255 + 1));
}
};
// first tick to start things off
self.tick();
};
Visual.prototype = {
update: function() {
this.particles.forEach(function(body) {
body.update();
});
},
draw: function(screen, size) {
// screen.clearRect(0, 0, size.x, size.y);
this.particles.forEach(function(particle) {
_.drawRect(screen, particle);
});
},
// pause drawing
stop: function() {
_.logInfo('Paused drawing.');
window.cancelAnimationFrame(this.animationRequest);
},
// unpause drawing
start: function() {
_.logInfo('Unpausing drawing.');
this.tick();
},
// proportionately move all visual content along resizing canvas
resize: function(width, height) {
var self = this;
self.stop();
self.particles.forEach(function(particle) {
particle.rePosition(self.size, {x: width, y: height});
});
self.size = {
x: width,
y: height
};
self.start();
},
// changes relative hue of all content
changeHue: function(hue) {
var change = -(VISUAL_HUE - hue);
this.particles.forEach(function(particle) {
particle.adjustHue(change);
});
},
// changes relative lighting of all content
changeLighting: function(light) {
var change = -(VISUAL_LIGHTING - light);
this.particles.forEach(function(particle) {
particle.adjustLighting(change);
});
},
_isCollision: function(ours, theirs) {
return (theirs < 0 || theirs > ours);
},
_isXCollision: function(theirs) {
return this._isCollision(this.size.x, theirs);
},
_isYCollision: function(theirs) {
return this._isCollision(this.size.y, theirs);
}
};
/**
* A single particle, holding state and methods to change.
*/
var Particle = function(visual, screenSize) {
var randomSize = 2 + _.inRange(PARTICLE_MIN_SIZE, PARTICLE_MAX_SIZE),
randomBlinkRate = _.zeroTo(PARTICLE_BLINK_RATE);
this.visual = visual;
this.size = {
x: randomSize,
y: randomSize
};
this.center = {
x: _.zeroTo(screenSize.x),
y: _.zeroTo(screenSize.y)
};
this.color = {
h: _.roofRange(VISUAL_HUE, 5),
s: _.roofRange(VISUAL_SATURATION, 5),
l: _.inRange(VISUAL_LIGHTING - 40, VISUAL_LIGHTING),
a: 1,
getHsla: function() {
return 'hsla('+this.h+','+this.s+'%,'+this.l+'%,'+this.a+')';
}
};
this.color._h = this.color.h;
this.color._s = this.color.s;
this.color._l = this.color.l;
this.dimming = true;
this.blinkRate = randomBlinkRate;
};
Particle.prototype = {
update: function() {
/* Color */
this.blink();
/* Center */
this.jitterCenter();
},
blink: function() {
if (this.color.a >= 1) {
this.dimming = true;
} else if (this.color.a <= 0) {
this.dimming = false;
}
this.color.a += this.dimming ? -(this.blinkRate) : this.blinkRate;
// this.color.s = _.roofRange((this.color.a * 20)^10, 10);
},
jitterCenter: function() {
this.center.x += (_.randomSign() * _.zeroTo(PARTICLE_JITTER_RATE));
this.center.y += (_.randomSign() * _.zeroTo(PARTICLE_JITTER_RATE));
if (this.visual._isXCollision(this.center.x)) {
this.center.x = this.center.x > this.visual.size.x ? this.visual.size.x - 1 : 1;
}
if (this.visual._isYCollision(this.center.y)) {
this.center.y = this.center.y > this.visual.size.y ? this.visual.size.y - 1 : 1;
}
this.center.y = Math.round(this.center.y);
this.center.y = Math.round(this.center.y);
},
rePosition: function(oldSize, newSize) {
this.center.x = newSize.x / (oldSize.x / this.center.x);
this.center.y = newSize.y / (oldSize.y / this.center.y);
},
adjustHue: function(amount) {
this.color.h = this.color._h + amount;
},
adjustLighting: function(amount) {
this.color.l = this.color._l + amount;
}
};
/**
* Utility object. Bad programmer. Bad.
*/
var _ = {
zeroTo: function(max) {
return Math.random() * max;
},
randomSign: function() {
return Math.random() > 0.5 ? -1 : 1;
},
roofRange: function(roof, range) {
return (Math.random() * range) + (roof - range);
},
inRange: function(min, max) {
return min + _.zeroTo(max - min);
},
getCenter: function(screenSize) {
return {
x: screenSize.x / 2,
y: screenSize.y / 2
};
},
drawRect: function(screen, rect) {
if (rect.color) {
screen.fillStyle = rect.color.getHsla();
}
screen.fillRect(rect.center.x - rect.size.x / 2,
rect.center.y - rect.size.y / 2,
rect.size.x,
rect.size.y);
},
logInfo: function(str) {
console.log('[info] ' + str);
},
cheekyRespeed: function(amount) {
VISUAL_SPEED_MULT += amount;
VISUAL_SPEED_MULT = VISUAL_SPEED_MULT < 0 ? 0 : VISUAL_SPEED_MULT;
PARTICLE_JITTER_RATE = 0.45 * VISUAL_SPEED_MULT;
PARTICLE_BLINK_RATE = 0.01 * VISUAL_SPEED_MULT;
}
};
_.cheekyRespeed(0);
// Init on browser window
window.onload = function() {
var canvas = document.getElementById('particles'),
canvasContext = canvas.getContext('2d'),
originalSize = getSize(),
currentSize,
visual,
intervalFps;
window.onresize = resizeCanvas;
setElementDimensions(canvas, originalSize.width, originalSize.height);
function resizeCanvas () {
var size = getSize();
if (visual) {
setTimeout(function() {
var newSize = getSize();
if (newSize.width === size.width &&
newSize.height === size.height) {
setElementDimensions(canvas, size.width, size.height);
visual.resize(size.width, size.height);
currentSize = size;
_.logInfo('Resized: ' + size.width + 'px * ' + size.height + 'px.');
}
}, 1500);
} else {
visual = new Visual(canvasContext, size.width, size.height);
currentSize = size;
}
}
function getSize() {
return {
width: window.innerWidth,
height: window.innerHeight
};
}
function setElementDimensions(el, width, height) {
el.setAttribute('width', width.toString());
el.setAttribute('height', height.toString());
}
window.mouseEffect = function(isOn) {
if (isOn === true) {
document.addEventListener('mousemove', processMouseEffect);
} else if (isOn === false) {
document.removeEventListener('mousemove', processMouseEffect);
}
};
var processMouseEffect = function(event) {
var mouseLoop;
if (mouseLoop) {
clearTimeout(mouseLoop);
}
mouseLoop = setTimeout(function() {
visual.stop();
visual.changeHue(255 * (event.pageX/currentSize.width));
visual.changeLighting(50 * (event.pageY/currentSize.height) + 25);
visual.start();
}, 5);
};
window.consoleFps = function(isOn) {
if (isOn === true) {
intervalFps = setInterval(function() {
_.logInfo('FPS: ' + frames);
frames = 0;
}, 5000);
} else if(isOn === false) {
clearInterval(intervalFps);
}
};
// init canvas
resizeCanvas();
// change color based on mouse position
if (VISUAL_MOUSE_EFFECT) {
mouseEffect(true);
}
if (SHOW_CONSOLE_FPS) {
window.consoleFps(true);
}
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment