├── 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 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
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 |
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 |
--------------------------------------------------------------------------------