├── CNAME ├── LICENSE ├── chainvas.min.js ├── cubic-bezier.js ├── environment.js ├── index.html ├── interaction.js └── style.css /CNAME: -------------------------------------------------------------------------------- 1 | cubic-bezier.com -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Lea Verou. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /chainvas.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Lea Verou. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | /** 25 | * Chainvas: Make APIs chainable 26 | * @author Lea Verou 27 | * MIT license http://www.opensource.org/licenses/mit-license.php 28 | */ 29 | (function(){var e=window.Chainvas={chainable:function(a){return function(){var b=a.apply(this,arguments);return b===void 0?this:b}},chainablizeOne:function(a,b){try{e.utils.hasOwnProperty(a,b)&&e.utils.isFunction(a[b])&&(a[b]=e.chainable(a[b]))}catch(c){}return this},chainablize:function(a,b){var c=a.prototype;if(b)for(var d=b.length;d--;)e.chainablizeOne(c,b[d]);else for(d in c)e.chainablizeOne(c,d);return this},helpers:function(a,b){var c=a.prototype,d;for(d in e.methods)c&&!(d in c)&&(c[d]=e.methods[d]); 30 | e.extend(c,b);return this},extend:function(a,b){return Chainvas.methods.prop.call(a,b)},global:function(a,b,c){typeof a==="string"&&(a=[a]);for(var d=a.length;d--;){var f=window[a[d]];f&&e.chainablize(f,c).helpers(f,b)}},methods:{prop:function(){if(arguments.length===1){var a=arguments[0],b;for(b in a)this[b]=a[b]}else arguments.length===2&&(this[arguments[0]]=arguments[1]);return this}},utils:{isFunction:function(a){var b=Object.prototype.toString.call(a);return b==="[object Function]"||b==="[object Object]"&& 31 | "call"in a&&"apply"in a&&/^\s*\bfunction\s+\w+\([\w,]*\) \{/.test(a+"")},hasOwnProperty:function(a,b){try{return a.hasOwnProperty(b)}catch(c){return b in a&&(!a.prototype||!(b in a.prototype)||a.prototype[b]!==a[b])}}}}})(); 32 | 33 | /** 34 | * Chainvas module: DOM 35 | */ 36 | (function(){var a=["CSSStyleDeclaration","DOMTokenList","Node","Element"];if(window.HTMLElement&&"addEventListener"in window.HTMLElement.prototype&&window.Components&&window.Components.interfaces)for(var p in Components.interfaces)if(p.match(/^nsIDOMHTML\w*Element$/)){var b=p.replace(/^nsIDOM/,"");window[b]&&a.push(b)}Chainvas.global(a)})(); 37 | 38 | /** 39 | * Chainvas module: Canvas 40 | */ 41 | Chainvas.global("CanvasRenderingContext2D",{circle:function(a,b,d){return this.beginPath().arc(a,b,d,0,2*Math.PI,!1).closePath()},roundRect:function(a,b,d,e,c){return this.beginPath().moveTo(a+c,b).lineTo(a+d-c,b).quadraticCurveTo(a+d,b,a+d,b+c).lineTo(a+d,b+e-c).quadraticCurveTo(a+d,b+e,a+d-c,b+e).lineTo(a+c,b+e).quadraticCurveTo(a,b+e,a,b+e-c).lineTo(a,b+c).quadraticCurveTo(a,b,a+c,b).closePath()}}); -------------------------------------------------------------------------------- /cubic-bezier.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Lea Verou. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | (function() { 25 | 26 | var self = window.CubicBezier = function(coordinates) { 27 | if (typeof coordinates === 'string') { 28 | if(coordinates[0] === '#') { 29 | coordinates = coordinates.slice(1); 30 | } 31 | 32 | this.coordinates = coordinates.split(','); 33 | } 34 | else { 35 | this.coordinates = coordinates; 36 | } 37 | 38 | if(!this.coordinates) { 39 | throw 'No offsets were defined'; 40 | } 41 | 42 | this.coordinates = this.coordinates.map(function(n) { return +n; }); 43 | 44 | for(var i=4; i--;) { 45 | var xy = this.coordinates[i]; 46 | if(isNaN(xy) || (!(i%2) && (xy < 0 || xy > 1))) { 47 | throw 'Wrong coordinate at ' + i + '(' + xy + ')'; 48 | } 49 | } 50 | 51 | this.coordinates.toString = function() { 52 | return this.map(self.prettifyNumber) + ''; 53 | } 54 | }; 55 | 56 | self.prototype = { 57 | get P1() { 58 | return this.coordinates.slice(0, 2); 59 | }, 60 | 61 | get P2() { 62 | return this.coordinates.slice(2); 63 | }, 64 | 65 | // Clipped to the range 0-1 66 | get clipped() { 67 | var coordinates = this.coordinates.slice(); 68 | 69 | for(var i=coordinates.length; i--;) { 70 | coordinates[i] = Math.max(0, Math.min(coordinates[i], 1)); 71 | } 72 | 73 | return new self(coordinates); 74 | }, 75 | 76 | get inRange() { 77 | var coordinates = this.coordinates; 78 | 79 | return Math.abs(coordinates[1] - .5) <= .5 && Math.abs(coordinates[3] - .5) <= .5; 80 | }, 81 | 82 | toString: function() { 83 | return 'cubic-bezier(' + this.coordinates + ')'; 84 | }, 85 | 86 | applyStyle: function(element) { 87 | element.style.setProperty(prefix + 'transition-timing-function', this, null); 88 | }, 89 | }; 90 | 91 | Chainvas.extend(self, { 92 | prettifyNumber: function(val) { 93 | return (Math.round(val * 100)/100 + '').replace(/^0\./, '.'); 94 | }, 95 | 96 | predefined: { 97 | 'ease': '.25,.1,.25,1', 98 | 'linear': '0,0,1,1', 99 | 'ease-in': '.42,0,1,1', 100 | 'ease-out': '0,0,.58,1', 101 | 'ease-in-out':'.42,0,.58,1' 102 | } 103 | }); 104 | 105 | })(); 106 | 107 | (function(){ 108 | 109 | var self = window.BezierCanvas = function(canvas, bezier, padding) { 110 | this.canvas = canvas; 111 | this.bezier = bezier; 112 | this.padding = self.getPadding(padding); 113 | 114 | // Convert to a cartesian coordinate system with axes from 0 to 1 115 | var ctx = this.canvas.getContext('2d'), 116 | p = this.padding; 117 | 118 | ctx.scale(canvas.width * (1 - p[1] - p[3]), -canvas.height * (1 - p[0] - p[2])); 119 | ctx.translate(p[3] / (1 - p[1] - p[3]), -1 - p[0] / (1 - p[0] - p[2])); 120 | }; 121 | 122 | self.prototype = { 123 | get offsets() { 124 | var p = this.padding, w = this.canvas.width, h = this.canvas.height; 125 | 126 | return [{ 127 | left: w * (this.bezier.coordinates[0] * (1 - p[3] - p[1]) - p[3]) + 'px', 128 | top: h * (1 - this.bezier.coordinates[1] * (1 - p[0] - p[2]) - p[0]) + 'px' 129 | }, { 130 | left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + 'px', 131 | top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0]) + 'px' 132 | }] 133 | }, 134 | 135 | offsetsToCoordinates: function(element) { 136 | var p = this.padding, w = this.canvas.width, h = this.canvas.height; 137 | 138 | // Convert padding percentage to actual padding 139 | p = p.map(function(a, i) { return a * (i % 2? w : h)}); 140 | 141 | return [ 142 | (parseInt(element.style.left) - p[3]) / (w + p[1] + p[3]), 143 | (h - parseInt(element.style.top) - p[2]) / (h - p[0] - p[2]) 144 | ]; 145 | }, 146 | 147 | plot: function(settings) { 148 | var xy = this.bezier.coordinates, 149 | ctx = this.canvas.getContext('2d'); 150 | 151 | var defaultSettings = { 152 | handleColor: 'rgba(0,0,0,.6)', 153 | handleThickness: .008, 154 | bezierColor: 'black', 155 | bezierThickness: .02 156 | }; 157 | 158 | settings || (settings = {}); 159 | 160 | for (var setting in defaultSettings) { 161 | (setting in settings) || (settings[setting] = defaultSettings[setting]); 162 | } 163 | 164 | ctx.clearRect(-.5,-.5, 2, 2); 165 | 166 | // Draw control handles 167 | ctx.beginPath().prop({ 168 | fillStyle: settings.handleColor, 169 | lineWidth: settings.handleThickness, 170 | strokeStyle: settings.handleColor 171 | }); 172 | 173 | ctx.moveTo(0, 0).lineTo(xy[0], xy[1]); 174 | ctx.moveTo(1,1).lineTo(xy[2], xy[3]); 175 | 176 | ctx.stroke().closePath(); 177 | 178 | ctx.circle(xy[0], xy[1], 1.5 * settings.handleThickness).fill() 179 | .circle(xy[2], xy[3], 1.5 * settings.handleThickness).fill(); 180 | 181 | // Draw bezier curve 182 | ctx.beginPath() 183 | .prop({ 184 | lineWidth: settings.bezierThickness, 185 | strokeStyle: settings.bezierColor 186 | }).moveTo(0,0) 187 | .bezierCurveTo(xy[0], xy[1], xy[2], xy[3], 1,1).stroke() 188 | .closePath(); 189 | } 190 | 191 | }; 192 | 193 | self.getPadding = function(padding) { 194 | var p = typeof padding === 'number'? [padding] : padding; 195 | 196 | if (p.length === 1) { 197 | p[1] = p[0]; 198 | } 199 | 200 | if (p.length === 2) { 201 | p[2] = p[0]; 202 | } 203 | 204 | if (p.length === 3) { 205 | p[3] = p[1]; 206 | } 207 | 208 | return p; 209 | } 210 | 211 | })(); -------------------------------------------------------------------------------- /environment.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Lea Verou. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | /** 25 | * Make the environment a bit friendlier 26 | */ 27 | function $(expr, con) { return (con || document).querySelector(expr); } 28 | function $$(expr, con) { return (con || document).querySelectorAll(expr); } 29 | 30 | /** 31 | * Find browser prefix 32 | */ 33 | var prefixes = ['', '-moz-', '-ms-', '-o-', '-webkit-'], 34 | prefix = (function(style) { 35 | for (var i=prefixes.length; i--;) { 36 | var prefix = prefixes[i]; 37 | 38 | style.setProperty(prefix + 'transition', '1s', null); 39 | 40 | if (style.cssText) { 41 | return prefix; 42 | } 43 | } 44 | 45 | return null; 46 | })(document.createElement('a').style); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | cubic-bezier.com 8 | 9 | 10 | 11 | 12 | 13 |
14 |

15 | 20 |
21 | 22 | 23 |
24 | 25 | 26 | 27 |
28 |
29 |

30 |
31 | 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 | 45 | 46 |
47 |

48 | Preview & compare 49 | 50 |

51 |

52 | 53 | 54 | 1 second 55 |

56 | 57 | 58 |
59 | 60 |
61 |

62 | Library 63 | 64 | 65 |

66 |

Click on a curve to compare it with the current one.

67 | 68 | 69 | 72 | 73 |
74 | 75 | 76 | 77 | 78 | 79 |
80 |
81 | 82 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /interaction.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Lea Verou. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | (function() { 25 | 26 | var self = window.bezierLibrary = { 27 | curves: {}, 28 | 29 | render: function() { 30 | var items = $$('a', library); 31 | 32 | for(var i=items.length; i--;) { 33 | library.removeChild(items[i]); 34 | } 35 | 36 | for (var name in self.curves) { 37 | try { var bezier = new CubicBezier(self.curves[name]); } 38 | catch(e) { continue; } 39 | 40 | self.add(name, bezier); 41 | } 42 | }, 43 | 44 | add: function (name, bezier) { 45 | var canvas = document.createElement('canvas').prop({ 46 | width:100, 47 | height:100 48 | }), 49 | a = document.createElement('a').prop({ 50 | href: '#' + bezier.coordinates, 51 | bezier: bezier, 52 | bezierCanvas: new BezierCanvas(canvas, bezier, .15) 53 | }); 54 | 55 | if(!bezier.applyStyle) console.log(bezier); 56 | bezier.applyStyle(a); 57 | 58 | library.insertBefore(a, $('footer', library)); 59 | 60 | a.appendChild(canvas) 61 | 62 | a.appendChild(document.createElement('span').prop({ 63 | textContent: name, 64 | title: name 65 | })); 66 | 67 | a.appendChild(document.createElement('button').prop({ 68 | innerHTML: '×', 69 | title: 'Remove from library', 70 | onclick: function(evt) { 71 | evt.stopPropagation(); 72 | 73 | if (confirm('Are you sure you want to delete this? There is no going back!')) { 74 | self.deleteItem(this.parentNode); 75 | } 76 | 77 | return false; 78 | } 79 | })); 80 | 81 | a.bezierCanvas.plot(self.thumbnailStyle); 82 | 83 | a.onclick = this.selectThumbnail; 84 | 85 | if (!/^a$/i.test(a.previousElementSibling.nodeName)) { 86 | a.onclick(); 87 | } 88 | }, 89 | 90 | selectThumbnail: function() { 91 | var selected = $('.selected', this.parentNode); 92 | 93 | if (selected) { 94 | selected.classList.remove('selected'); 95 | selected.bezierCanvas.plot(self.thumbnailStyle); 96 | } 97 | 98 | this.classList.add('selected'); 99 | 100 | this.bezierCanvas.plot(self.thumbnailStyleSelected); 101 | 102 | compare.style.cssText = this.style.cssText; 103 | 104 | compare.style.setProperty(prefix + 'transition-duration', getDuration() + 's', null); 105 | 106 | compareCanvas.bezier = this.bezier; 107 | 108 | compareCanvas.plot({ 109 | handleColor: 'rgba(255,255,255,.5)', 110 | bezierColor: 'white', 111 | handleThickness: .03, 112 | bezierThickness: .06 113 | }); 114 | }, 115 | 116 | deleteItem: function(a) { 117 | var name = $('span', a).textContent; 118 | 119 | delete bezierLibrary.curves[name]; 120 | 121 | bezierLibrary.save(); 122 | 123 | library.removeChild(a); 124 | 125 | if (a.classList.contains('selected')) { 126 | $('a:first-of-type', library).onclick(); 127 | } 128 | }, 129 | 130 | save: function(curves) { 131 | localStorage.curves = JSON.stringify(curves || self.curves); 132 | }, 133 | 134 | thumbnailStyle: { 135 | handleColor: 'rgba(0,0,0,.3)', 136 | handleThickness: .018, 137 | bezierThickness: .032 138 | }, 139 | 140 | thumbnailStyleSelected: { 141 | handleColor: 'rgba(255,255,255,.6)', 142 | bezierColor: 'white', 143 | handleThickness: .018, 144 | bezierThickness: .032 145 | } 146 | }; 147 | 148 | })(); 149 | 150 | /** 151 | * Init 152 | */ 153 | 154 | // Ensure global vars for ids (most browsers already do this anyway, so…) 155 | [ 156 | 'values', 'curve','P1','P2', 'current', 'compare', 'duration', 157 | 'library', 'save', 'copy', 'copyoptionstoggle', 'copybuttons', 'copyoptions', 'copystatement', 'copycss', 'copyvalue', 'go', 'import', 'export', 'json', 'importexport' 158 | ].forEach(function(id) { window[id] = $('#' + id); }); 159 | 160 | var ctx = curve.getContext("2d"), 161 | bezierCode = $('h1 code'), 162 | curveBoundingBox = curve.getBoundingClientRect(), 163 | bezierCanvas = new BezierCanvas(curve, null, [.25, 0]), 164 | currentCanvas = new BezierCanvas(current, null, .15), 165 | compareCanvas = new BezierCanvas(compare, null, .15), 166 | favicon = document.createElement('canvas'), 167 | faviconCtx = favicon.getContext('2d'), 168 | pixelDepth = window.devicePixelRatio || 1; 169 | 170 | // Add predefined curves 171 | if (!localStorage.curves) { 172 | bezierLibrary.save(CubicBezier.predefined); 173 | } 174 | 175 | bezierLibrary.curves = JSON.parse(localStorage.curves); 176 | 177 | bezierLibrary.render(); 178 | 179 | if(location.hash) { 180 | bezierCanvas.bezier = window.bezier = new CubicBezier(decodeURI(location.hash)); 181 | 182 | var offsets = bezierCanvas.offsets; 183 | 184 | P1.style.prop(offsets[0]); 185 | P2.style.prop(offsets[1]); 186 | } 187 | 188 | favicon.width = favicon.height = 16 * pixelDepth; 189 | 190 | update(); 191 | updateDelayed(); 192 | 193 | /** 194 | * Event handlers 195 | */ 196 | // Make the handles draggable 197 | P1.onmousedown = 198 | P2.onmousedown = function() { 199 | var me = this; 200 | 201 | document.onmousemove = function drag(e) { 202 | var x = e.pageX, y = e.pageY, 203 | left = curveBoundingBox.left, 204 | top = curveBoundingBox.top; 205 | 206 | if (x === 0 && y == 0) { 207 | return; 208 | } 209 | 210 | // Constrain x 211 | x = Math.min(Math.max(left, x), left + curveBoundingBox.width); 212 | 213 | me.style.prop({ 214 | left: x - left + 'px', 215 | top: y - top + 'px' 216 | }); 217 | 218 | update(); 219 | }; 220 | 221 | document.onmouseup = function () { 222 | me.focus(); 223 | 224 | document.onmousemove = document.onmouseup = null; 225 | } 226 | }; 227 | 228 | P1.onkeydown = 229 | P2.onkeydown = function(evt) { 230 | var code = evt.keyCode; 231 | 232 | if(code >= 37 && code <= 40) { 233 | evt.preventDefault(); 234 | 235 | // Arrow keys pressed 236 | var left = parseInt(this.style.left), 237 | top = parseInt(this.style.top) 238 | offset = 3 * (evt.shiftKey? 10 : 1); 239 | 240 | switch (code) { 241 | case 37: this.style.left = left - offset + 'px'; break; 242 | case 38: this.style.top = top - offset + 'px'; break; 243 | case 39: this.style.left = left + offset + 'px'; break; 244 | case 40: this.style.top = top + offset + 'px'; break; 245 | } 246 | 247 | update(); 248 | updateDelayed(); 249 | 250 | return false; 251 | } 252 | }; 253 | 254 | P1.onblur = 255 | P2.onblur = 256 | P1.onmouseup = 257 | P2.onmouseup = updateDelayed; 258 | 259 | curve.onclick = function(evt) { 260 | var left = curveBoundingBox.left, 261 | top = curveBoundingBox.top, 262 | x = evt.pageX - left, y = evt.pageY - top; 263 | 264 | // Find which point is closer 265 | var distP1 = distance(x, y, parseInt(P1.style.left), parseInt(P1.style.top)), 266 | distP2 = distance(x, y, parseInt(P2.style.left), parseInt(P2.style.top)); 267 | 268 | (distP1 < distP2? P1 : P2).style.prop({ 269 | left: x + 'px', 270 | top: y + 'px' 271 | }); 272 | 273 | update(); 274 | updateDelayed(); 275 | 276 | function distance(x1, y1, x2, y2) { 277 | return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); 278 | } 279 | }; 280 | 281 | curve.onmousemove = function(evt) { 282 | var left = curveBoundingBox.left, 283 | top = curveBoundingBox.top, 284 | height = curveBoundingBox.height, 285 | x = evt.pageX - left, y = evt.pageY - top; 286 | 287 | this.parentNode.setAttribute('data-time', Math.round(100 * x / curveBoundingBox.width)); 288 | this.parentNode.setAttribute('data-progression', Math.round(100 * (3*height/4 - y) / (height * .5))); 289 | }; 290 | copy.onclick = function(){ 291 | copystatement.select(); 292 | copystatement.setSelectionRange(0, 99999); 293 | document.execCommand("copy"); 294 | copybuttons.classList.remove('copyoptions-open'); 295 | copybuttons.classList.add('copied'); 296 | copybuttons.addEventListener("animationend", handleCopyAnimationComplete, false); 297 | } 298 | handleOptionCopy = function(){ 299 | this.select(); 300 | this.setSelectionRange(0, 99999); 301 | document.execCommand("copy"); 302 | copybuttons.classList.remove('copyoptions-open'); 303 | copybuttons.classList.add('copied'); 304 | copybuttons.addEventListener("animationend", handleCopyAnimationComplete, false); 305 | } 306 | function handleCopyAnimationComplete(){ 307 | copybuttons.removeEventListener("animationend", handleCopyAnimationComplete, false); 308 | copybuttons.classList.remove('copied'); 309 | } 310 | copycss.onclick = handleOptionCopy; 311 | copystatement.onclick = handleOptionCopy; 312 | copyvalue.onclick = handleOptionCopy; 313 | copyoptionstoggle.onclick = function () { 314 | copybuttons.classList.toggle('copyoptions-open'); 315 | } 316 | save.onclick = function() { 317 | var rawValues = bezier.coordinates + '', 318 | name = prompt('If you want, you can give it a short name', rawValues); 319 | 320 | if(name) { 321 | bezierLibrary.add(name, bezier); 322 | 323 | bezierLibrary.curves[name] = rawValues; 324 | 325 | bezierLibrary.save(); 326 | } 327 | }; 328 | 329 | go.onclick = function() { 330 | updateDelayed(); 331 | 332 | current.classList.toggle('move'); 333 | compare.classList.toggle('move'); 334 | }; 335 | 336 | duration.oninput = function() { 337 | var val = getDuration(); 338 | this.nextElementSibling.textContent = val + ' second' + (val == 1? '' : 's'); 339 | current.style.setProperty(prefix + 'transition-duration', val + 's', null); 340 | compare.style.setProperty(prefix + 'transition-duration', val + 's', null); 341 | updateCopyInputs(); 342 | }; 343 | 344 | window['import'].onclick = function() { 345 | json.value = ''; 346 | 347 | importexport.className = 'import'; 348 | 349 | json.focus(); 350 | }; 351 | 352 | window['export'].onclick = function() { 353 | json.value = localStorage.curves; 354 | 355 | 356 | importexport.className = 'export'; 357 | 358 | json.focus(); 359 | }; 360 | 361 | // Close button 362 | importexport.elements[2].onclick = function() { 363 | this.parentNode.removeAttribute('class'); 364 | 365 | return false; 366 | }; 367 | 368 | importexport.onsubmit = function() { 369 | if(this.className === 'import') { 370 | var overwrite = !confirm('Add to current curves? Clicking “Cancel” will overwrite them with the new ones.'); 371 | 372 | try { 373 | var newCurves = JSON.parse(json.value); 374 | } 375 | catch(e) { 376 | alert('Sorry mate, this doesn’t look like valid JSON so I can’t do much with it :('); 377 | return false; 378 | } 379 | 380 | if(overwrite) { 381 | bezierLibrary.curves = newCurves; 382 | } 383 | else { 384 | for(var name in newCurves) { 385 | var i = 0, newName = name; 386 | 387 | while(bezierLibrary.curves[newName]) { 388 | newName += '-' + ++i; 389 | } 390 | 391 | bezierLibrary.curves[newName] = newCurves[name]; 392 | } 393 | } 394 | 395 | bezierLibrary.render(); 396 | bezierLibrary.save(); 397 | } 398 | 399 | this.removeAttribute('class'); 400 | }; 401 | 402 | /** 403 | * Helper functions 404 | */ 405 | 406 | function getDuration() { 407 | return (isNaN(val = Math.round(duration.value * 10) / 10)) ? null : val; 408 | } 409 | 410 | function update() { 411 | // Redraw canvas 412 | bezierCanvas.bezier = 413 | currentCanvas.bezier = 414 | window.bezier = new CubicBezier( 415 | bezierCanvas.offsetsToCoordinates(P1) 416 | .concat(bezierCanvas.offsetsToCoordinates(P2)) 417 | ); 418 | 419 | bezierCanvas.plot(); 420 | 421 | currentCanvas.plot({ 422 | handleColor: 'rgba(255,255,255,.5)', 423 | bezierColor: 'white', 424 | handleThickness: .03, 425 | bezierThickness: .06 426 | }); 427 | 428 | 429 | updateCopyInputs(); 430 | var params = $$('.param', bezierCode), 431 | prettyOffsets = bezier.coordinates.toString().split(','); 432 | 433 | for(var i=params.length; i--;) { 434 | params[i].textContent = prettyOffsets[i]; 435 | } 436 | } 437 | function updateCopyInputs(){ 438 | copystatement.value = "cubic-bezier(" + bezier.coordinates.toString() + ")"; 439 | copycss.value = getDuration() + "s cubic-bezier(" + bezier.coordinates.toString() + ")"; 440 | copyvalue.value = bezier.coordinates.toString(); 441 | var items = document.querySelectorAll('#copyoptions input'); 442 | for (var i = items.length; i--;) { 443 | var width = copycss.value.length *11; 444 | items[i].style.width = width+'px'; 445 | } 446 | 447 | } 448 | 449 | // For actions that can wait 450 | function updateDelayed() { 451 | bezier.applyStyle(current); 452 | 453 | var hash = '#' + bezier.coordinates, 454 | size = 16 * pixelDepth; 455 | 456 | bezierCode.parentNode.href = hash; 457 | 458 | if(history.pushState) { 459 | history.pushState(null, null, hash); 460 | } 461 | else { 462 | location.hash = hash; 463 | } 464 | 465 | 466 | 467 | 468 | // Draw dynamic favicon 469 | 470 | faviconCtx 471 | .clearRect(0, 0, size, size) 472 | .prop('fillStyle', '#0ab') 473 | .roundRect(0, 0, size, size, 2) 474 | .fill() 475 | .drawImage(current, 0, 0, size, size); 476 | 477 | 478 | $('link[rel="shortcut icon"]').setAttribute('href', favicon.toDataURL()); 479 | 480 | document.title = bezier + ' ✿ cubic-bezier.com'; 481 | } 482 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Lea Verou. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | /** 25 | * Variables 26 | */ 27 | * { 28 | margin: 0; 29 | } 30 | 31 | #preview > canvas, 32 | #library > a, 33 | header > p { 34 | transition: 1s; 35 | } 36 | 37 | #importexport > textarea, 38 | code { 39 | font-family: Consolas, 'Andale Mono', monospace; 40 | } 41 | 42 | /** 43 | * Styles 44 | */ 45 | body { 46 | display: grid; 47 | grid-template: 48 | 'curve header header' auto 49 | 'curve preview library' auto 50 | 'curve . .' auto / 300px 480px 1fr; 51 | gap: 2rem; 52 | position: relative; 53 | padding: 1em max(3rem, 5%); 54 | background: white; 55 | font-family: 'Hiragino Kaku Gothic Pro', 'Segoe UI', 'Apple Gothic', Tahoma, 'Helvetica Neue', sans-serif; 56 | line-height: 1.4; 57 | } 58 | 59 | h1 { 60 | font-size: 220%; 61 | } 62 | 63 | a { 64 | color: #f08; 65 | } 66 | 67 | a:hover { 68 | text-decoration: none; 69 | } 70 | 71 | h1 > a { 72 | font-size: clamp(1.2rem, 2vw + .75rem, 2.5rem); 73 | white-space: nowrap; 74 | color: inherit; 75 | text-decoration: none; 76 | } 77 | 78 | button, .button { 79 | padding: .3em .5em; 80 | border: 0; 81 | background: #ccc; 82 | color: white; 83 | font-size: 50%; 84 | text-transform: uppercase; 85 | vertical-align: .25em; 86 | cursor: pointer; 87 | border-radius: .3em; 88 | } 89 | 90 | button:hover, .button:hover { 91 | background: #f08; 92 | } 93 | 94 | button:focus, .button:focus { 95 | box-shadow: 0 0 5px 2px yellow; 96 | } 97 | 98 | #copybuttons { 99 | display: inline-flex; 100 | vertical-align: middle; 101 | position: relative; 102 | } 103 | 104 | #copybuttons.copyoptions-open { 105 | width: auto; 106 | } 107 | 108 | #copybuttons.copyoptions-open #copy { 109 | background-color: #f08; 110 | border-bottom-left-radius: 0; 111 | } 112 | 113 | #copybuttons.copyoptions-open #copyoptionstoggle { 114 | background-color: #a50b5d; 115 | border-bottom-right-radius: 0; 116 | box-shadow: none; 117 | } 118 | 119 | #copy { 120 | border-top-right-radius: 0; 121 | border-bottom-right-radius: 0; 122 | padding-right: 0.3em; 123 | display: block; 124 | 125 | } 126 | 127 | #copyoptions { 128 | position: absolute; 129 | top: 100%; 130 | right: 0; 131 | background-color: #f08; 132 | padding: 0.2em 0.3em; 133 | border-radius: .2em 0 .2em .2em; 134 | overflow: hidden; 135 | z-index: 2; 136 | opacity: 0; 137 | font-size: 0.1em; 138 | transition: height 0.5s cubic-bezier(.1,.74,.33,.95); 139 | height: 20px; 140 | pointer-events: none; 141 | } 142 | 143 | #copyoptions > input { 144 | opacity: 1; 145 | background-color: #f08; 146 | margin-bottom: 0.4em; 147 | margin-top: 0.4em; 148 | display: block; 149 | text-transform: none; 150 | font-size: 0.65em; 151 | font-weight: 700; 152 | color: #fff; 153 | border: 0; 154 | cursor: pointer; 155 | padding: 0.2em 0.3em; 156 | border-radius: 0.2em; 157 | font-family: monospace; 158 | } 159 | 160 | #copyoptions > input:hover { 161 | background: #a50b5d; 162 | } 163 | 164 | #copybuttons.copyoptions-open #copyoptions { 165 | opacity: 1; 166 | font-size: 1em; 167 | height: 125px; 168 | pointer-events: auto; 169 | } 170 | 171 | #copyoptionstoggle { 172 | border-top-left-radius: 0; 173 | border-bottom-left-radius: 0; 174 | padding-left: 0.3em; 175 | display: block; 176 | } 177 | 178 | #copybuttons:hover #copy, 179 | #copybuttons:hover #copyoptionstoggle { 180 | background: #f08; 181 | } 182 | 183 | #copybuttons #copy:hover, 184 | #copybuttons #copyoptionstoggle:hover, 185 | #copybuttons.copyoptions-open:hover #copyoptionstoggle { 186 | background: #a50b5d; 187 | } 188 | 189 | @keyframes copied { 190 | 0% { background: #599133; } 191 | 100% { background: #ccc; } 192 | } 193 | #copybuttons.copied #copyoptionstoggle, 194 | #copybuttons.copied #copy { 195 | animation: 1s copied cubic-bezier(.51,.2,.81,.48); 196 | } 197 | 198 | input[type="range"] { 199 | -webkit-appearance: none; 200 | overflow: hidden; 201 | border: 1px solid #ddd; 202 | background: #eee; 203 | text-align: center; 204 | font: inherit; 205 | border-radius: 99px; 206 | box-shadow: 1px 1px 3px rgba(0,0,0,.5) inset; 207 | } 208 | 209 | input[type="range"]::-webkit-slider-thumb { 210 | -webkit-appearance: none; 211 | background: #f8b; 212 | height: 13px; 213 | width: 13px; 214 | border-radius: 50%; 215 | box-shadow: -1px -1px 5px rgba(0,0,0,.5) inset, 216 | -1px -1px 4px 3px #f08 inset, 217 | 1px 1px 3px rgba(0,0,0,.3), 218 | -199px 0 0 198px rgba(255, 0, 136, .2); 219 | } 220 | 221 | input[type="range"]:in-range { 222 | width: 14em; 223 | } 224 | 225 | header { 226 | grid-area: header; 227 | } 228 | 229 | header > h1 { 230 | display: flow-root; 231 | } 232 | 233 | #curve-display { 234 | grid-area: curve; 235 | justify-self: center; 236 | } 237 | 238 | .coordinate-plane { 239 | position: relative; 240 | margin-top: 2rem; 241 | } 242 | 243 | .coordinate-plane::before, 244 | .coordinate-plane::after { 245 | position: absolute; 246 | bottom: 25%; 247 | left: 0; 248 | width: 100%; 249 | padding: .3em .5em; 250 | box-sizing: border-box; 251 | color: rgba(0,0,0,.6); 252 | text-transform: uppercase; 253 | font-size: 75%; 254 | line-height: 1; 255 | } 256 | 257 | .coordinate-plane::before { 258 | content: 'Progression'; 259 | border-bottom: 1px solid; 260 | transform: rotate(-90deg); 261 | transform-origin: bottom left; 262 | } 263 | 264 | .coordinate-plane::after { 265 | content: 'Time'; 266 | border-top: 1px solid; 267 | margin-bottom: -1.5em; 268 | } 269 | 270 | .coordinate-plane:hover::before { 271 | content: 'Progression (' attr(data-progression) '%)'; 272 | } 273 | 274 | .coordinate-plane:hover::after { 275 | content: 'Time (' attr(data-time) '%)'; 276 | } 277 | 278 | #save { 279 | position: absolute; 280 | bottom: 90px; 281 | z-index: 0; 282 | right: 1em; 283 | font-size: 1em; 284 | } 285 | 286 | .control-point { 287 | position: absolute; 288 | z-index: 1; 289 | height: 20px; 290 | width: 20px; 291 | border: 1px solid rgba(0,0,0,.3); 292 | margin: -10px 0 0 -10px; 293 | outline: none; 294 | box-sizing: border-box; 295 | border-radius: 10px; 296 | } 297 | 298 | #P0, #P3 { 299 | background: white; 300 | pointer-events: none; 301 | } 302 | 303 | #P1, #P2 { 304 | cursor: pointer; 305 | } 306 | 307 | #P0 { 308 | left: 0; 309 | top: 75%; 310 | } 311 | 312 | #P1 { 313 | background: #f08; 314 | } 315 | 316 | #P2 { 317 | background: #0ab; 318 | } 319 | 320 | #P3 { 321 | left: 100%; 322 | top: 25%; 323 | } 324 | 325 | #P1x, #P1y { 326 | color: #f08; 327 | } 328 | 329 | #P2x, #P2y { 330 | color: #0ab; 331 | } 332 | 333 | canvas#curve { 334 | background: #f0f0f0; 335 | background: linear-gradient(-45deg, transparent 49%, rgba(0,0,0,.1) 49%, rgba(0,0,0,.1) 51%, transparent 51%) center no-repeat, 336 | repeating-linear-gradient(white, white 20px, transparent 20px, transparent 40px) no-repeat, 337 | linear-gradient(transparent, rgba(0,0,0,.06) 25%, rgba(0,0,0,.06) 75%, transparent); 338 | background-size: 100% 50%, 100% 50%, auto; 339 | background-position: 25%, 0, 0; 340 | 341 | -webkit-user-select: none; 342 | user-select: none; 343 | } 344 | 345 | section {} 346 | 347 | section > h1 { 348 | margin-bottom: .2em; 349 | font-size: 200%; 350 | font-weight: normal; 351 | } 352 | 353 | section > h1 + p { 354 | margin: -.5em 0 1em 0; 355 | color: gray; 356 | } 357 | 358 | #preview { 359 | grid-area: preview; 360 | position: relative; 361 | } 362 | 363 | #preview > canvas { 364 | display: block; 365 | position: relative; 366 | left: 0; 367 | margin-bottom: .5em; 368 | background: #0ab; 369 | border-radius: 5px; 370 | } 371 | 372 | #preview > #current { 373 | background: #f08; 374 | } 375 | 376 | #preview > .move { 377 | left: 100%; 378 | transform: translateX(-100%); 379 | } 380 | 381 | #library { 382 | grid-area: library; 383 | } 384 | 385 | #library > a { 386 | position: relative; 387 | float: left; 388 | width: 100px; 389 | margin: 0 .8em 1em 0; 390 | color: #999; 391 | font-size: 110%; 392 | text-align: center; 393 | text-decoration: none; 394 | cursor: pointer; 395 | 396 | } 397 | 398 | #library > a > canvas, 399 | #library > a > span { 400 | transition: inherit; 401 | } 402 | 403 | #library > a > canvas { 404 | display: block; 405 | position: relative; 406 | left: 0; 407 | background: #e5e5e5; 408 | 409 | border-radius: 5px; 410 | } 411 | 412 | #library > a:hover > canvas, 413 | #library > a:focus > canvas { 414 | background: #ACD0D5; 415 | } 416 | 417 | #library > a > span { 418 | display: block; 419 | overflow: hidden; 420 | white-space: nowrap; 421 | text-overflow: ellipsis; 422 | } 423 | 424 | #library > a:hover > span, 425 | #library > a:focus > span { 426 | color: #568C93; 427 | } 428 | 429 | #library > a > button { 430 | display: none; 431 | position: absolute; 432 | top: -5px; 433 | right: -5px; 434 | background: black; 435 | font-size: 80%; 436 | border-radius: 50%; 437 | box-shadow: 1px 1px 8px -1px black; 438 | } 439 | 440 | #library > a > button:hover { 441 | background: #f08; 442 | } 443 | 444 | #library > a:hover > button, 445 | #library > a:focus > button { 446 | display: block; 447 | } 448 | 449 | #library > a.selected { 450 | color: #0ab; 451 | } 452 | 453 | #library > a.selected > canvas { 454 | background: #0ab; 455 | } 456 | 457 | #library > footer { 458 | clear: both; 459 | font-size: 80%; 460 | color: #444; 461 | } 462 | 463 | #importexport { 464 | position: fixed; 465 | top: 2em; 466 | left: 50%; 467 | z-index: 2; 468 | width: 500px; 469 | padding: 1em 2.6em 1em 1em; 470 | margin-left: -250px; 471 | background: rgba(0, 0, 0, .7); 472 | color: white; 473 | text-align: center; 474 | border-radius: .8em; 475 | } 476 | 477 | #importexport:not([class]) { 478 | display: none; 479 | } 480 | 481 | #importexport > label { 482 | display: block; 483 | font-weight: bold; 484 | text-shadow: 1px 1px 2px black; 485 | } 486 | 487 | #importexport > textarea { 488 | display: block; 489 | width: 100%; 490 | height: 20em; 491 | padding: .8em; 492 | border: 1px solid black; 493 | margin: .5em 0; 494 | background: rgba(255, 255, 255, .85); 495 | font-size: inherit; 496 | text-shadow: 0 1px white; 497 | border-radius: .3em; 498 | box-shadow: .1em .1em .4em rgba(0,0,0,.5) inset; 499 | } 500 | 501 | #importexport > button { 502 | font-size: 120%; 503 | } 504 | 505 | #importexport > button:not(:hover) { 506 | background: black; 507 | } 508 | 509 | #importexport.export > button:first-of-type, 510 | #importexport.export > label:first-of-type, 511 | #importexport.import > label:nth-of-type(2) { 512 | display: none; 513 | } 514 | 515 | body > footer { 516 | grid-area: footer; 517 | padding: .5em 0; 518 | max-width: 100%; 519 | font-size: 120%; 520 | color: rgba(0, 0, 0, .1); 521 | box-sizing: border-box; 522 | } 523 | 524 | body > footer > a { 525 | font-weight: bold; 526 | } 527 | 528 | body > footer > a:not(:hover) { 529 | color: inherit; 530 | } 531 | 532 | body > footer > a.button { 533 | padding: .5em .6em; 534 | color: white; 535 | text-decoration: none; 536 | font-size: 65%; 537 | background: #ddd; 538 | } 539 | 540 | @media (max-width: 1330px){ 541 | body { 542 | grid-template: 543 | 'curve header' 544 | 'curve preview' 545 | 'curve library' / 300px 1fr; 546 | } 547 | } 548 | 549 | @media (max-width: 1000px){ 550 | body { 551 | grid-template: 552 | 'header header' 553 | 'curve preview' 554 | 'curve library' / 300px 1fr; 555 | } 556 | 557 | header { 558 | text-align: center; 559 | } 560 | 561 | header > h1 { 562 | display: flex; 563 | flex-wrap: wrap; 564 | justify-content: center; 565 | align-items: center; 566 | } 567 | } 568 | 569 | @media (max-width: 850px){ 570 | body { 571 | grid-template: 572 | 'footer' 573 | 'header' 574 | 'curve' 575 | 'preview' 576 | 'library'; 577 | padding: 0 5% 1em; 578 | } 579 | 580 | h1 > a { 581 | width: 100%; 582 | } 583 | 584 | #copybuttons { 585 | margin-left: auto; 586 | } 587 | } 588 | 589 | @media not all and (max-width: 850px){ 590 | body > footer { 591 | position: fixed; 592 | top: 1em; 593 | right: 100%; 594 | transform: rotate(-90deg); 595 | transform-origin: top right; 596 | white-space: nowrap; 597 | } 598 | } 599 | 600 | @media (max-width: 600px){ 601 | h1 > a { 602 | letter-spacing: -.05ch; 603 | } 604 | } 605 | 606 | /* Carbon Ads */ 607 | 608 | #carbonads { 609 | display: block; 610 | overflow: hidden; 611 | margin: 2em auto 0; 612 | padding: 1em; 613 | max-width: 360px; 614 | border: solid 1px hsl(0, 0%, 88%); 615 | border-radius: 4px; 616 | background-color: #f4f4f4; 617 | box-shadow: inset 0 1px #fafafa; 618 | font-size: 12px; 619 | line-height: 1.5; 620 | } 621 | 622 | #carbonads a { 623 | color: #4d4d4d; 624 | text-decoration: none; 625 | text-shadow: 0 1px #fafafa; 626 | transition: color .15s ease-in-out; 627 | } 628 | 629 | #carbonads a:hover { color: #ff0088; } 630 | 631 | #carbonads span { 632 | position: relative; 633 | display: block; 634 | overflow: hidden; 635 | } 636 | 637 | .carbon-img { 638 | float: left; 639 | margin-right: 1em; 640 | } 641 | 642 | .carbon-img img { display: block; } 643 | 644 | .carbon-text { 645 | display: block; 646 | float: left; 647 | max-width: calc(100% - 130px - 1em); 648 | text-align: left; 649 | } 650 | 651 | .carbon-poweredby { 652 | position: absolute; 653 | right: 0; 654 | bottom: 0; 655 | display: block; 656 | font-size: 11px; 657 | } 658 | --------------------------------------------------------------------------------