Skip to content

Instantly share code, notes, and snippets.

@shshaw
Last active September 11, 2015 14:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shshaw/9f0f3b5b667d058e0ae9 to your computer and use it in GitHub Desktop.
Save shshaw/9f0f3b5b667d058e0ae9 to your computer and use it in GitHub Desktop.
Parallax Sky Background
<script>console.clear();</script>

Parallax Sky Background

Scroll around a beautiful parallax sky, built with canvas for buttery-smooth performance.

A Pen by Shaw on CodePen.

License.

Canvallax = (function() {
var W = window,
D = document,
R = D.documentElement,
B = D.body,
WSCROLL = { x: 0, y: 0 };
function onScroll(){
WSCROLL.x = R.scrollLeft || B.scrollLeft;
WSCROLL.y = R.scrollTop || B.scrollTop;
}
onScroll();
W.addEventListener('scroll', onScroll);
// Check for canvas support.
if ( !W.CanvasRenderingContext2D ) { return function(){ return false; }; }
function Init(options) {
var C = this;
C.canvas = D.createElement('canvas');
C.ctx = C.canvas.getContext('2d');
C.resize();
W.addEventListener('resize', C.resize.bind(C));
options = Canvallax.extend(Canvallax.defaults,options || {});
C.canvas.className = 'canvallax ' + options.className;
options.parent.insertBefore(C.canvas, options.parent.firstChild);
C.elements = [];
C.addElements(options.elements || []);
C.x = ( options.x !== undefined ? options.x : WSCROLL.x );
C.y = ( options.y !== undefined ? options.y : WSCROLL.y );
C.animating = true;
C.animation = {
fps: options.fps || 60,
then: Date.now()
};
C.animation.interval = 1000 / C.animation.fps || 1;
C.damping = options.damping || 5;
C.damping = ( !C.damping || C.damping < 1 ? 1 : C.damping );
var _x = 0,
_y = 0;
C.render = function() {
var i = 0,
len = C.elements.length,
el, distance;
C.now = Date.now();
if ( C.animating !== false ) { requestAnimationFrame(C.render.bind(C)); }
if ( options.scroll ) {
C.x = ( options.scroll === 'invert' || options.scroll === 'invertx' ? -WSCROLL.x : WSCROLL.x );
C.y = ( options.scroll === 'invert' || options.scroll === 'inverty' ? -WSCROLL.y : WSCROLL.y );
}
_x += ( -C.x - _x ) / C.damping;
_y += ( -C.y - _y ) / C.damping;
C.ctx.clearRect(0, 0, C.canvas.width, C.canvas.height);
for (; i < len; i += 1) {
el = C.elements[i];
distance = el.distance || 1;
C.ctx.globalAlpha = el.opacity || 1;
if ( el.scale ) {
C.ctx.scale(distance,distance);
C.ctx.translate(_x, _y);
} else {
C.ctx.translate(_x * distance, _y * distance);
}
el._render(C.ctx,C);
C.ctx.setTransform(1, 0, 0, 1, 0, 0);
}
};
C.render();
return C;
}
////////////////////////////////////////
function Canvallax(options){ return new Init(options); }
var fn = Canvallax.prototype = Init.prototype = {};
fn.resize = function() {
this.canvas.width = W.innerWidth;
this.canvas.height = W.innerHeight;
};
fn.addElement = function(element){
element.ctx = this.ctx;
if ( element.setup ) { element.setup.call(element, this.ctx); }
this.elements.push(element);
};
fn.addElements = function(elements){
var i = 0,
len = elements.length;
for (; i < len; i++) {
this.addElement(elements[i]);
}
};
fn.stop = fn.pause = function(){
this.animating = false;
};
fn.play = function(){
this.animation = true;
this.render();
};
////////////////////////////////////////
Canvallax.defaults = {
scroll: true,
parent: B
};
Canvallax.extend = function(target) {
target = target || {};
var length = arguments.length,
i = 1;
if ( arguments.length === 1) {
target = this;
i = 0;
}
for (; i < length; i++) {
if (!arguments[i]) { continue; }
for (var key in arguments[i]) {
if ( arguments[i].hasOwnProperty(key) ) { target[key] = arguments[i][key]; }
}
}
return target;
};
////////////////////////////////////////
Canvallax.Element = function(options) {
Canvallax.extend(this,Canvallax.Element.defaults,options);
if ( options.init ) { options.init.call(this); }
if ( this.animation && this.animation.fps && this.animation.frames ) {
this.animation.interval = 1000 / this.animation.fps || 1;
this.animation.frame = this.animation.frame || 0;
this.animation.then = Date.now();
}
};
Canvallax.Element.defaults = {
x: 0,
y: 0,
distance: 1,
onRender: false,
scale: true
};
var elFn = Canvallax.Element.prototype = {};
elFn._render = function(ctx,C) {
this.animate.call(this,ctx,C);
if ( this.render ) { this.render.call(this,ctx,C); }
};
// http://codetheory.in/controlling-the-frame-rate-with-requestanimationframe/
elFn.animate = function(ctx,C){
if ( this.animation ) {
this.animation.delta = C.now - this.animation.then;
if (this.animation.delta > this.animation.interval) {
// Just `then = now` is not enough.
// Lets say we set fps at 10 which means each frame must take 100ms
// Now frame executes in 16ms (60fps) so the loop iterates 7 times (16*7 = 112ms) until delta > interval === true
// Eventually this lowers down the FPS as 112*10 = 1120ms (NOT 1000ms).
// So we have to get rid of that extra 12ms by subtracting delta (112) % interval (100).
this.animation.then = C.now - (this.animation.delta % this.animation.interval);
this.animation.frame++;
if ( this.animation.frame >= this.animation.frames ) { this.animation.frame = 0; }
}
}
};
Canvallax.createElement = function(init,options){
//var el = new Canvallax.Element;
//if ( init ) { init.call(el); }
return Canvallax.Element; //el;
}
return Canvallax;
})();
(function(Canvallax){
var can = Canvallax({
className: 'bg-canvas',
scroll: true, // (boolean|'invert'|'invertx'|'inverty') If true, the X and Y of the scene are tied to document's scroll for a typical parallax experience. Set to false if you want to control the scene's X and Y manually.
x: 0, // Starting x. If tied to scroll, this will be overridden on render.
y: 0, // Starting y. If tied to scroll, this will be overridden on render.
damping: 1
});
if ( !can ) {
App.body.className += ' no-bg-canvas';
return false;
}
/*
// Animate the x and y with GSAP, not scroll.
TweenMax.to(can,4,{
//x: 1000,
y: 1000,
yoyo: true,
ease: Cubic.easeInOut,
repeat: -1,
});
*/
////////////////////////////////////////
var origWidth = width = document.body.clientWidth,
origHeight = height = document.body.clientHeight;
window.addEventListener('resize', function(){
height = document.body.clientHeight;
var i = can.elements.length,
max = document.body.clientWidth,
heightScale = height / origHeight;
console.log(height,origHeight,heightScale);
while (i--){
can.elements[i].maxX = max; //document.body.clientWidth;
can.elements[i].origY = can.elements[i].origY || can.elements[i].y;
can.elements[i].y = can.elements[i].origY * heightScale;
}
});
////////////////////////////////////////
// Adapted from http://bost.ocks.org/mike/algorithms/
function bestCandidateSampler(width, height, numCandidates) {
var samples = [];
function findDistance(a, b) {
var dx = a[0] - b[0],
dy = a[1] - b[1];
return dx * dx + dy * dy;
}
function findClosest(c){
var i = samples.length,
sample,
closest,
distance,
closestDistance;
while(i--){
sample = samples[i];
distance = findDistance(sample,c);
if ( !closestDistance || distance < closestDistance ) {
closest = sample;
closestDistance = distance;
}
}
return closest;
}
function getSample() {
var bestCandidate,
bestDistance = 0,
i = 0,
c, d;
c = [Math.random() * width, Math.random() * height];
if ( samples.length < 1 ) {
bestCandidate = c;
} else {
for (; i < numCandidates; i++) {
c = [Math.random() * width, Math.random() * height];
d = findDistance(findClosest(c), c);
if (d > bestDistance) {
bestDistance = d;
bestCandidate = c;
}
}
}
samples.push(bestCandidate);
//console.log('bestCandidate',bestCandidate);
return bestCandidate;
}
getSample.all = function(){ return samples; };
getSample.samples = samples;
return getSample;
}
var getCandidate = bestCandidateSampler(width,height,10);
var CLOUD_COUNT = 40,
CLOUD_WIDTH = 510,
CLOUD_HEIGHT = 260;
CLOUD_COUNT = Math.floor(( width * height ) / (CLOUD_WIDTH * CLOUD_HEIGHT));
console.log(CLOUD_COUNT); // How many clouds to best fill the total area available
function cloudRender(ctx){
//this.x = (this.x * this.distance) < this.maxX ? this.x + this.speed : -this.tex.w;
this.x = (this.x * this.distance) > -this.tex.w ? this.x - this.speed : offsetDistance(this.maxX,this.distance); // / (this.distance || 1);
ctx.drawImage(this.image, this.tex.x, this.tex.y, this.tex.w, this.tex.h, this.x, this.y, this.tex.w, this.tex.h);
}
function Cloud(options) {
Canvallax.Element.call(this, options);
this.tex = options.tex || {
x: 0,
y: options.texOffset || 0,
w: this.image.width,
h: this.image.height
};
//console.log(options.texOffset);
this.maxX = options.maxX || 0;
this.speed = options.speed || 0.25;
this.render = cloudRender;
}
Cloud.prototype = Canvallax.Element.prototype;
function offsetDistance(width,distance){
var d = ( width / distance ) / ( 2 - distance);
return d;
}
function randomRange(min, max) {
return Math.random() * (max - min) + min;
}
function randomizedCloud(image){
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
width = canvas.width = randomRange(400,660), //CLOUD_WIDTH,
height = canvas.height = randomRange(200,280),//CLOUD_HEIGHT,
w = image.width,
h = image.height,
halfw = w / 2,
halfh = h / 2,
i = Math.ceil(randomRange(20,90)), //60
flip,
randScale,
randTex,
maxScale = 2.5,
fullPi = Math.PI / 2;
while (i--){
randScale = randomRange(0.4,maxScale);
ctx.globalAlpha = Math.random() - 0.2;
ctx.translate(randomRange(halfw, width - ( w * maxScale * 1.3)),randomRange(halfh,height - (h* maxScale)));
ctx.scale(randScale,randomRange(randScale - 0.3,randScale));
ctx.translate(halfw,halfh);
ctx.rotate(randomRange(0,fullPi));
ctx.drawImage(image, -halfw, -halfh);//, w, h, 0, 0, w, h);
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
//document.body.appendChild(canvas);
//var img = document.createElement('img');
//img.src = canvas.toDataURL();
return canvas;
}
var cloudImg = new Image();
cloudImg.addEventListener('load',function(){
var i = CLOUD_COUNT, //Math.ceil(CLOUD_COUNT * 0.5),
el, rand, pos, tex;
while(i--) {
rand = Math.round(randomRange(0.5, 1.1) * 10) / 10;
pos = getCandidate();
tex = randomizedCloud(cloudImg);
cloud = new Cloud({
image: tex,
maxX: width,
x: pos[0],
y: pos[1],
opacity: (rand < 0.8 ? 0.8 : rand ),
distance: rand,
speed: rand * randomRange(0.05,0.25),
});
can.addElement(cloud);
}
can.render();
});
cloudImg.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAABGCAMAAABG8BK2AAAAYFBMVEX///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8GYpHzAAAAIHRSTlMAAwcLDxIWGh8kKzI5QEhPVl9qc3uEi5Sdpa64wcrV4c6KdP8AAAOeSURBVHgBndQJkvM8DoNhS+nv/ieONBH8FIs1nb83ZLfF1wBFZ17vNMZ8fPz79/GY46Xre8F85kRzzhdk/A2TOpSARpF/g0mdaI8XZjg47leA32Jo3wbEooNE/SFGrPOAiPxC+xaDAxBTxQqFoW8wGGof89E4j8nQUceo621h/KY8GuXx4Mg10vRZlPd55JCEnXlIQX7cE8pNp8Dob4oY8LjZTny8QEMo2o2hSqT5YImxchVNbpgZ3oVR4xVUgOlGS5fFKCPSWDY4Yu1xIKyEEqLfE0UUKcoJ9MFVsAhnaOpEGW4gyxFyLitOeRhZO8NJur1hKpWWCV03oaG9yYcYXvD7UPbetVOMNwqlWH6/b7sp29G1Z5g6LFGaY9uYB2A6M7evtVco3U3M1CCopOlNY8Q7cdZahxIMTpv7meR2NghWDqdu8MTZzwMqN+RaBkmdUgf7wMfN1d1cNSAFMdhQ3tLXu2STuclylwYL5QYhsGyLz499bcgxK1F5SmumvaoRrIwSjYutaGbVZIgze6wegLO6i8MD2kJ1N1cYKAUXTLnRWxtnYrMONWRRD+EsA3utw4mnKxNCZRgplK5T05ycvaaZCmtAOEKl+p4ge70k06mcdrQoPgGpbY1EboNyY7/ZHVsfK1BLhBAdCA5MGd719b0gUOJmg9vc5hf3E+K2y1EQDnCjxoGs3t84WiA6zI253rVy6MRnSKNxJtQcRY3xL90MkN6DYGZHI3zBMaF1NcoUQ3Oll+9RBqHb1uIzSTmv9LtQOJbC7Ci/fRsivh0+sBrYwix3GKe8No5yHJTDaZqhqOp8KC6BdmXrFKFaXUUWk7uCLsRPbuAZrmsPjCANBKChIZj/97IBzSJq3pgpymiYRmnxMLG9YbinUGCk5rxbUhJDy/yH+C5U3wP1FQWDL5AtWpNQm0IRz/ys+psCTSKuCObTjY3KhLoCrbUY7ZgIqHRXrTSFHxwfFjcM1fWF8devtJPK2XtM6nAOpcrCXDlVR967qRi6XOY78NJsi95gcsJynMXF2k6gAFFhVFGLz0loz5u0HGoEGF6qOoVgC6cOB6iBb0Ixue66p+p8F+v17VkhUTqGXP6pVptC8JU07z0Ga5VqjmUpvBvuC8wpej4bJiXK+ZHnazfXyfUSipxPVJQvMTjppUKlFUVvv8PIVftrSIRD+Q6DQ7a47wzE9xjaYJk1Fty1v8CYauOLm+fv3Cikjv4lBqVCAP6n/gfZhdXQlm1mfwAAAABJRU5ErkJggg==';
////////////////////////////////////////
// Full cloud texture image
/*
var texture = new Image(),
CLOUD_SPRITE_DISTANCE = 300;
texture.addEventListener('load', function() {
//return false;
var i = Math.ceil(CLOUD_COUNT * 0.5),
cloud, rand, pos, tex;
while(i--) {
rand = Math.round(randomRange(0.3, 0.9) * 10) / 10;
pos = getCandidate();
tex = Math.floor(rand < 0.6 ? randomRange(4,6) : randomRange(0,3)) * CLOUD_SPRITE_DISTANCE;
cloud = new Cloud({
image: texture,
tex: {
x: 0,
y: tex || 0,
w: 510,
h: 260
},
maxX: width,
opacity: (rand < 0.8 ? 0.8 : rand ),
x: pos[0] - randomRange(0,width * 0.25), //randomRange(-300,window.innerWidth),
y: pos[1], //Math.ceil((1 - rand) * (CLOUD_SPRITE_COUNT + 1)) * CLOUD_SPRITE_DISTANCE, // Randomize the texture based on distance. Further away clouds are blurry.
distance: rand, //Math.random()
speed: rand * randomRange(0.025,0.2),
});
can.addElement(cloud);
}
});
texture.src = 'http://i.imgur.com/48yRQIF.png';
/**/
})(Canvallax);
//})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/TweenMax.min.js"></script>
<script src="http://codepen.io/shshaw/pen/epmrgO"></script>
canvas { display: block; margin: 2em; }
.bg-canvas{
position: fixed;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
margin: 0;
}
body {
background-image: linear-gradient(to top, lighten(#97a8c0,20%) 40%, #97a8c0 80%);
background-size: cover;
width: 400vw;
height: 400vh;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment