├── README.md
├── demo.html
├── layerstack.css
├── layerstack.js
└── package.json
/README.md:
--------------------------------------------------------------------------------
1 | # LayerStack
2 |
3 | Small JavaScript library that splits an element's multiple backgrounds into separate layers (DOM elements) so that each layer corresponds to one of the original element's backgrounds.
4 |
5 | Visualize in 3D.
6 |
7 | See an example in `demo.html`.
8 |
9 | ## Usage
10 |
11 | Include both `layerstack.js` and `layerstack.css` into the page, then create an instance of LayerStack with a reference to the DOM element that you want to inspect.
12 |
13 | ```html
14 |
15 |
16 |
20 | ```
21 |
22 | Move the mouse cursor to expand the layer and rotate stack.
23 |
24 | ## API
25 |
26 | **Work in progress. This is just a prototype.**
27 |
28 | ### Methods
29 | - `.destroy()`
30 |
31 | Removes all the LayerStack DOM nodes and utility stylesheets from the page. Restores visibility of the original element.
32 |
33 | **Example**
34 | ```javascript
35 | stack.destroy();
36 | ```
37 |
38 | You can pass an optional object literal with options for the destroy action. Possible values:
39 |
40 | ```javascript
41 | options: {
42 | immediate: {Boolean} // if true, remove everything ASAP without running animation. Default is false.
43 | }
44 | ```
45 |
46 | **Example**
47 | ```javascript
48 | stack.destroy({ immediate: true });
49 | ```
50 |
51 | - `.on()`
52 |
53 | Add an event listener for an event triggered by the LayerStack instance. Expects two parameters: an event name as a string and a function to be called after the event triggers.
54 |
55 | ```javascript
56 | stack.on('afterdestroy', function() {
57 | /* called after everything has been destroyed */
58 | })
59 | ```
60 |
61 | ### Events
62 |
63 | - "afterdestroy"
64 |
65 | Event called after the effects of the `destroy()` method have taken place. Use to invalidate reference to LayerStack instance itself when you want to reuse it again the same page context.
66 |
67 | **Example**
68 | ```javascript
69 | stack.on('afterdestroy', function() {
70 | // invalidate LayerStack reference
71 | stack = undefined;
72 | })
73 | stack.destroy();
74 | ```
75 |
--------------------------------------------------------------------------------
/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Exploded Layers
6 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/layerstack.css:
--------------------------------------------------------------------------------
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 | }
51 |
--------------------------------------------------------------------------------
/layerstack.js:
--------------------------------------------------------------------------------
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 | }
219 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "layerstack",
3 | "version": "1.0.0",
4 | "description": "Small library to visualize multiple CSS backgrounds as a stack of layers",
5 | "main": "layerstack.js",
6 | "scripts": {
7 | "start": "http-server -o"
8 | },
9 | "author": "",
10 | "license": "UNLICENSED"
11 | }
12 |
--------------------------------------------------------------------------------