Skip to content
This repository was archived by the owner on Jan 3, 2019. It is now read-only.

Commit f22b2fd

Browse files
Ruth JohnRuth John
authored andcommitted
🦄
1 parent 88f33f0 commit f22b2fd

File tree

15 files changed

+1875
-288
lines changed

15 files changed

+1875
-288
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
Styles for Layer Stack library
3+
*/
4+
5+
.bg-stack-container {
6+
position: absolute;
7+
perspective: 1000px;
8+
will-change: transform;
9+
pointer-events: none; /* allow clicks to go through */
10+
}
11+
12+
.bg-stack-container.animated .bg-stack {
13+
/* TODO: log bug with Firefox because it won't support calc() in rotate transforms */
14+
transform: rotateX(55deg) rotateY(0) rotateZ(calc(var(--xoffset, 0) * 1deg)) scale(.65);
15+
}
16+
17+
/* TODO: log bug with Chrome because it falsely says it doesn't support calc in rotate transform */
18+
/*@supports (transform: rotateZ(calc(var(--xoffset, 0) * 1deg))) {
19+
body {
20+
background-color: lime;
21+
}
22+
}*/
23+
24+
.bg-stack {
25+
position: relative;
26+
width: 100%;
27+
height: 100%;
28+
perspective: 1000px;
29+
transition: transform 1s ease-out;
30+
}
31+
32+
.bg-stack__layer {
33+
position: absolute;
34+
width: 100%;
35+
height: 100%;
36+
will-change: transform;
37+
transition: transform 1s ease-out, box-shadow 2s, opacity 2s;
38+
}
39+
40+
.bg-stack-container.animated .bg-stack__layer {
41+
opacity: 0.85;
42+
box-shadow: 0 0 20px black;
43+
}
44+
45+
/* Offset transforms for each layer are generated in JS. */
46+
47+
/* Utility classes use !important to enforce themselves. */
48+
.u-invisible {
49+
visibility: hidden !important;
50+
}

‎VJtracks/layerstack/layerstack.js

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/*
2+
LayerStack
3+
4+
Splits an element's multiple backgrounds into separate layers (DOM elements)
5+
so that each layer corresponds to one of the original element's backgrounds.
6+
7+
Visualize in 3D.
8+
*/
9+
function LayerStack(el) {
10+
11+
// Ensure we're called with `new`
12+
if (!(this instanceof LayerStack)) {
13+
return new LayerStack(el);
14+
}
15+
16+
// Helpers
17+
var _qs = document.querySelector.bind(document);
18+
19+
// Store for event callbacks
20+
var _events = {};
21+
22+
if (!el) {
23+
throw TypeError(`Invalid element parameter. Expected DOM element, got ${el}.`);
24+
return;
25+
}
26+
27+
function getBackgroundLayers(el) {
28+
var style = window.getComputedStyle(el);
29+
30+
/*
31+
Create array of objects with layer data from `background-image` value.
32+
33+
Add an extra paranthesis at end of each image declaration,
34+
then split by one paranthesis and comma to yield an array of declarations.
35+
36+
If there is no backgrond-image value, the computed value is `none`.
37+
That yields an array with a single value onto which we can tack on stuff
38+
like `background-color`, if defined.
39+
*/
40+
layers = style.getPropertyValue('background-image')
41+
.replace(/\),/gi, ')),')
42+
.split('),')
43+
.map(function(img) {
44+
return {
45+
'background-image': img,
46+
'box-sizing': style.getPropertyValue('box-sizing'),
47+
// TODO: add support for separate border declarations
48+
'border': style.getPropertyValue('border')
49+
}
50+
});
51+
52+
// All other background properties can be split by comma.
53+
var props = ['background-size', 'background-repeat', 'background-position', 'background-origin'];
54+
55+
props.forEach(function(prop) {
56+
var values = style.getPropertyValue(prop).split(',');
57+
58+
/*
59+
If there's one background-* value, like background-repeat, but multiple background images,
60+
getGomputedStyle() in Firefox won't replicate the value to match the number of images.
61+
Do this manually.
62+
*/
63+
if (values.length === 1) {
64+
layers.forEach(function(layer) {
65+
layer[prop] = values[0].trim();
66+
})
67+
} else {
68+
values.forEach(function(value, index) {
69+
layers[index][prop] = value.trim();
70+
})
71+
}
72+
})
73+
74+
// Use backgrond-color only on base layer
75+
layers[layers.length - 1]['background-color'] = style.getPropertyValue('background-color');
76+
77+
return layers;
78+
}
79+
80+
function getTransformStyleSheetForLayers(layers) {
81+
var style = document.createElement('style');
82+
// offset step between layers
83+
var step = 75;
84+
85+
layers.forEach(function(layer, index) {
86+
// Concat transform style for this layer
87+
style.textContent += `.animated .bg-stack__layer:nth-child(${index + 1}) {
88+
transform: translateY(calc(var(--yoffset, 0) * ${index * -1 * step}px)) translateZ(calc(var(--yoffset, 0) * ${index * step}px));
89+
}\n`
90+
})
91+
92+
return style;
93+
}
94+
95+
var sourceEl = el;
96+
97+
var stack = document.createElement('div');
98+
stack.classList.add('bg-stack');
99+
100+
var stackContainer = document.createElement('div');
101+
stackContainer.classList.add('bg-stack-container');
102+
stackContainer.appendChild(stack);
103+
104+
var sourceElBgLayers = getBackgroundLayers(sourceEl);
105+
sourceElBgLayers.forEach(function(layer, index) {
106+
var stackLayer = document.createElement('div')
107+
stackLayer.classList.add('bg-stack__layer');
108+
109+
for (var prop in layer){
110+
stackLayer.style[prop] = layer[prop];
111+
}
112+
113+
// DOM order is reverse z-order; insert bottom layers at top of stack;
114+
stack.insertBefore(stackLayer, stack.firstChild)
115+
})
116+
117+
var sourceElBox = sourceEl.getBoundingClientRect();
118+
for (var key in sourceElBox) {
119+
stackContainer.style[key] = sourceElBox[key] + 'px';
120+
}
121+
122+
var transformStyleSheet = getTransformStyleSheetForLayers(sourceElBgLayers);
123+
document.head.appendChild(transformStyleSheet);
124+
125+
sourceEl.parentElement.appendChild(stackContainer);
126+
sourceEl.classList.add('u-invisible');
127+
128+
// Add animated class after appending to DOM so we get smooth transition
129+
requestAnimationFrame(function(){
130+
stackContainer.classList.add('animated');
131+
})
132+
133+
// Add stylesheet with dynamic values for CSS Variables set by mouse position
134+
var cssVars = document.createElement('style');
135+
document.head.appendChild(cssVars);
136+
137+
var maxRotation = 60; // max Z rotation (from -30deg to 30deg)
138+
var minRotation = 0;
139+
var minLayerOffset = 1;
140+
var maxLayerOffset = 4; // max Y layer offset; used in CSS as multiplier
141+
var maxWidth = window.innerWidth;
142+
var maxHeight = window.innerHeight;
143+
144+
var ticking = false;
145+
var mouseX = 0;
146+
var mouseY = 0;
147+
148+
function updateCSSVars() {
149+
ticking = false;
150+
151+
var xoffset = -1 * ((maxRotation / 2) - (maxRotation / maxWidth) * mouseX);
152+
var yoffset = maxLayerOffset - (maxLayerOffset / maxHeight) * mouseY;
153+
154+
cssVars.innerHTML = `:root { --xoffset: ${xoffset}; --yoffset: ${yoffset} }`;
155+
}
156+
157+
function update(e) {
158+
mouseX = e.clientX;
159+
mouseY = e.clientY;
160+
161+
if(!ticking) {
162+
requestAnimationFrame(updateCSSVars);
163+
}
164+
165+
ticking = true;
166+
}
167+
168+
document.addEventListener('mousemove', update);
169+
170+
return {
171+
/*
172+
Remove LayerStack DOM nodes and styling. Restore visibility of source object.
173+
@param {Object} options - config object for destroy action.
174+
175+
Config options:
176+
{Boolean} immediate - When true, destroy without running animation. Default `false`.
177+
178+
@example destroy({ immediate: true })
179+
*/
180+
destroy: function(options){
181+
document.removeEventListener('mousemove', update);
182+
stackContainer.classList.remove('animated');
183+
var self = this;
184+
185+
function destroyDOM(e) {
186+
transformStyleSheet.parentElement.removeChild(transformStyleSheet);
187+
cssVars.parentElement.removeChild(cssVars);
188+
stackContainer.removeEventListener('transitionend', destroyDOM)
189+
stackContainer.parentElement.removeChild(stackContainer);
190+
191+
sourceEl.classList.remove('u-invisible');
192+
self.trigger('afterdestroy');
193+
}
194+
195+
if (options && options.immediate) {
196+
destroyDOM();
197+
} else {
198+
stackContainer.addEventListener('transitionend', destroyDOM)
199+
}
200+
},
201+
202+
on: function(event, fn) {
203+
if (!_events[event]) {
204+
_events[event] = [];
205+
}
206+
_events[event].push(fn);
207+
},
208+
209+
trigger: function(event, data) {
210+
if (_events[event]) {
211+
_events[event].forEach(function(fn){
212+
fn.call(this, data);
213+
})
214+
}
215+
}
216+
217+
}
218+
}

0 commit comments

Comments
 (0)