├── 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 | --------------------------------------------------------------------------------