├── .gitignore ├── README.md ├── demo └── index.html └── src ├── gsap-player.css └── gsap-player.js /.gitignore: -------------------------------------------------------------------------------- 1 | ### OSX ### 2 | *.DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two \r 7 | Icon 8 | # Thumbnails 9 | ._* 10 | # Files that might appear in the root of a volume 11 | .DocumentRevisions-V100 12 | .fseventsd 13 | .Spotlight-V100 14 | .TemporaryItems 15 | .Trashes 16 | .VolumeIcon.icns 17 | .com.apple.timemachine.donotpresent 18 | # Directories potentially created on remote AFP share 19 | .AppleDB 20 | .AppleDesktop 21 | Network Trash Folder 22 | Temporary Items 23 | .apdisk 24 | 25 | 26 | ### Node ### 27 | # Logs 28 | logs 29 | *.log 30 | npm-debug.log* 31 | 32 | # Runtime data 33 | pids 34 | *.pid 35 | *.seed 36 | *.pid.lock 37 | 38 | # Directory for instrumented libs generated by jscoverage/JSCover 39 | lib-cov 40 | 41 | # Coverage directory used by tools like istanbul 42 | coverage 43 | 44 | # nyc test coverage 45 | .nyc_output 46 | 47 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 48 | .grunt 49 | 50 | # node-waf configuration 51 | .lock-wscript 52 | 53 | # Compiled binary addons (http://nodejs.org/api/addons.html) 54 | build/Release 55 | 56 | # Dependency directories 57 | node_modules 58 | jspm_packages 59 | 60 | # Optional npm cache directory 61 | .npm 62 | 63 | # Optional eslint cache 64 | .eslintcache 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📽 GSAP Player 📽 2 | 3 | A small, customizable YouTube-like player for GSAP (GreenSock) Timelines 4 | 5 | Project Demo Page: [http://s.codepen.io/sdras/debug/Mpjxxq/](http://s.codepen.io/sdras/debug/Mpjxxq/) 6 | 7 | ![GSAP Player](https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/gsap-preview.png "Such a player") 8 | 9 | ## Usage 10 | 11 | The simplest possible use is loading gsap-player.js before the closing body tag (you do not need the CSS file) and implementing: 12 | 13 | ```js 14 | gsapPlayer({ playerTL: yourtimelinehere }); 15 | ``` 16 | 17 | This will append the player to document.body. The default timeline name is `tl`, if you're using tl, you only need to call `gsapPlayer();` 18 | 19 | ## Configurations 20 | 21 | ### Light Theme 22 | 23 | ![Light Theme](https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/lighttheme.png "Light Theme") 24 | 25 | GSAP Player will default to a dark theme, but you can also configure a light theme: 26 | 27 | ```js 28 | gsapPlayer({ light: true }); 29 | ``` 30 | 31 | ### Position 32 | 33 | By default, the GSAP player will be 80% wide and 40px from the bottom of the viewport. You can configure it to be full-width or flush to the bottom, or slightly higher or lower. Bottom must be in a string. 34 | 35 | ```js 36 | gsapPlayer({ 37 | fullWidth: true, 38 | bottom: '0' 39 | }); 40 | ``` 41 | 42 | If you use full-width and keep the container defaulted to document.body, you should be sure that you've either used a reset or placed `margin: 0 ` on the body due to strange browser defaults. 43 | 44 | ### Container 45 | 46 | By default, the player is instantiated on the body, but if you'd like to change the parent, you can do: 47 | 48 | ```js 49 | gsapPlayer({ container: '#foo' }); 50 | ``` 51 | 52 | You'll need to put position: relative on that containing element to enclose the player spacially in that div. Just a heads up: this won't put the animation in the container as well (the configuration is kept separate for flexibility. 53 | 54 | ## License 55 | 56 | (The MIT License) 57 | 58 | Copyright (c)2017 Sarah Drasner [@sarah_edo](https://twitter.com/sarah_edo) All rights reserved. 59 | 60 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 61 | 62 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 63 | 64 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 65 | 66 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GSAP Player Demo 6 | 7 | 8 | 9 | 32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 | 40 | 41 | 53 | 54 | 55 | 56 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/gsap-player.css: -------------------------------------------------------------------------------- 1 | #gsap-player input[type=range] { 2 | -webkit-appearance: none; 3 | width: 100%; 4 | margin: 5px 0; 5 | } 6 | #gsap-player input[type=range]:focus { 7 | outline: none; 8 | } 9 | #gsap-player input[type=range]::-webkit-slider-runnable-track { 10 | width: 100%; 11 | height: 9px; 12 | cursor: pointer; 13 | background: #7e7e7e; 14 | border-radius: 1px; 15 | } 16 | #gsap-player input[type=range]::-webkit-slider-thumb { 17 | box-shadow: 0.6px 0.6px 2.1px rgba(0, 0, 0, 0.15), 0px 0px 0.6px rgba(13, 13, 13, 0.15); 18 | border: 1px solid #bbb; 19 | height: 18px; 20 | width: 19px; 21 | border-radius: 100px; 22 | background: #7e7e7e; 23 | cursor: pointer; 24 | -webkit-appearance: none; 25 | margin-top: -5.6px; 26 | } 27 | #gsap-player input[type=range]:focus::-webkit-slider-runnable-track { 28 | background: #8b8b8b; 29 | } 30 | #gsap-player input[type=range]::-moz-range-track { 31 | width: 100%; 32 | height: 9px; 33 | cursor: pointer; 34 | background: #7e7e7e; 35 | border-radius: 1px; 36 | } 37 | #gsap-player input[type=range]::-moz-range-thumb { 38 | box-shadow: 0.6px 0.6px 2.1px rgba(0, 0, 0, 0.15), 0px 0px 0.6px rgba(13, 13, 13, 0.15); 39 | border: 1px solid #a0a0a0; 40 | height: 18px; 41 | width: 19px; 42 | border-radius: 100px; 43 | background: #7e7e7e; 44 | cursor: pointer; 45 | } 46 | #gsap-player input[type=range]::-ms-track { 47 | width: 100%; 48 | height: 9px; 49 | cursor: pointer; 50 | background: transparent; 51 | border-color: transparent; 52 | color: transparent; 53 | } 54 | #gsap-player input[type=range]::-ms-fill-lower { 55 | background: #717171; 56 | border: 1.1px solid #6e6e6e; 57 | border-radius: 2px; 58 | box-shadow: 0.9px 0.9px 1.7px rgba(0, 0, 0, 0), 0px 0px 0.9px rgba(13, 13, 13, 0); 59 | } 60 | #gsap-player input[type=range]::-ms-fill-upper { 61 | background: #7e7e7e; 62 | border: 1.1px solid #6e6e6e; 63 | border-radius: 2px; 64 | box-shadow: 0.9px 0.9px 1.7px rgba(0, 0, 0, 0), 0px 0px 0.9px rgba(13, 13, 13, 0); 65 | } 66 | #gsap-player input[type=range]::-ms-thumb { 67 | box-shadow: 0.6px 0.6px 2.1px rgba(0, 0, 0, 0.15), 0px 0px 0.6px rgba(13, 13, 13, 0.15); 68 | border: 1px solid #a0a0a0; 69 | height: 18px; 70 | width: 19px; 71 | border-radius: 100px; 72 | background: #7e7e7e; 73 | cursor: pointer; 74 | height: 9px; 75 | } 76 | #gsap-player input[type=range]:focus::-ms-fill-lower { 77 | background: #7e7e7e; 78 | } 79 | #gsap-player input[type=range]:focus::-ms-fill-upper { 80 | background: #8b8b8b; 81 | } 82 | 83 | /* player */ 84 | #gsap-player button { 85 | background: none; 86 | border: none; 87 | outline: 0; 88 | cursor: pointer; 89 | } 90 | #gsap-player .loop { 91 | margin: 0 0 0 10px; 92 | } 93 | #gsap-player { 94 | position: fixed; 95 | z-index: 1000; 96 | bottom: 40px; 97 | background: black; 98 | display: flex; 99 | align-items: center; 100 | padding: 10px; 101 | width: 80%; 102 | margin: 0 10%; 103 | opacity: 0.8; 104 | } 105 | #gsap-player .playpause { 106 | position: relative; 107 | margin: 0 0 0 10px; 108 | } 109 | #gsap-player .playpause svg { 110 | position: absolute; 111 | top: -5px; 112 | left: -5px; 113 | } 114 | #gsap-player .playpause .play { 115 | opacity: 0; 116 | } 117 | #gsap-player button, #gsap-player input { 118 | opacity: 0.8; 119 | transition: 0.2s opacity ease-in; 120 | -webkit-transition: 0.2s opacity ease-in; 121 | } 122 | #gsap-player:hover button, #gsap-player:hover input { 123 | opacity: 1; 124 | transition: 0.2s opacity ease-out; 125 | -webkit-transition: 0.2s opacity ease-out; 126 | } 127 | #gsap-player .speed-tooltip { 128 | display: flex; 129 | flex-direction: column; 130 | font-size: 10px; 131 | font-family: helvetica, sans-serif; 132 | opacity: 0; 133 | visibility: hidden; 134 | position: absolute; 135 | top: -60px; 136 | left: 32px; 137 | padding-top: 5px; 138 | } 139 | #gsap-player .speed-tooltip div { 140 | margin: 2px 10px; 141 | opacity: 0.7; 142 | transition: 0.2s opacity ease-in; 143 | -webkit-transition: 0.2s opacity ease-in; 144 | cursor: pointer; 145 | } 146 | #gsap-player .speed-tooltip div:hover { 147 | opacity: 1; 148 | transition: 0.2s opacity ease-out; 149 | -webkit-transition: 0.2s opacity ease-out; 150 | } 151 | #gsap-player .speed { 152 | color: white; 153 | margin: 0 10px 0 5px; 154 | } -------------------------------------------------------------------------------- /src/gsap-player.js: -------------------------------------------------------------------------------- 1 | var injectGsapPlayerStyle = (function(style){ 2 | return function(){ 3 | if ( !style ) { return; } 4 | document.head.insertAdjacentHTML('afterbegin',''); 5 | style = null; 6 | } 7 | })('#gsap-player input[type=range]{-webkit-appearance:none;width:100%;margin:5px 0}#gsap-player input[type=range]:focus{outline:0}#gsap-player input[type=range]::-webkit-slider-runnable-track{width:100%;height:9px;cursor:pointer;background:#7e7e7e;border-radius:1px}#gsap-player input[type=range]::-webkit-slider-thumb{box-shadow:.6px .6px 2.1px rgba(0,0,0,.15),0 0 .6px rgba(13,13,13,.15);border:1px solid #bbb;height:18px;width:19px;border-radius:100px;background:#7e7e7e;cursor:pointer;-webkit-appearance:none;margin-top:-5.6px}#gsap-player input[type=range]:focus::-webkit-slider-runnable-track{background:#8b8b8b}#gsap-player input[type=range]::-moz-range-track{width:100%;height:9px;cursor:pointer;background:#7e7e7e;border-radius:1px}#gsap-player input[type=range]::-moz-range-thumb{box-shadow:.6px .6px 2.1px rgba(0,0,0,.15),0 0 .6px rgba(13,13,13,.15);border:1px solid #a0a0a0;height:18px;width:19px;border-radius:100px;background:#7e7e7e;cursor:pointer}#gsap-player input[type=range]::-ms-track{width:100%;height:9px;cursor:pointer;background:0 0;border-color:transparent;color:transparent}#gsap-player input[type=range]::-ms-fill-lower{background:#717171;border:1.1px solid #6e6e6e;border-radius:2px;box-shadow:.9px .9px 1.7px transparent,0 0 .9px rgba(13,13,13,0)}#gsap-player input[type=range]::-ms-fill-upper{background:#7e7e7e;border:1.1px solid #6e6e6e;border-radius:2px;box-shadow:.9px .9px 1.7px transparent,0 0 .9px rgba(13,13,13,0)}#gsap-player input[type=range]::-ms-thumb{box-shadow:.6px .6px 2.1px rgba(0,0,0,.15),0 0 .6px rgba(13,13,13,.15);border:1px solid #a0a0a0;width:19px;border-radius:100px;background:#7e7e7e;cursor:pointer;height:9px}#gsap-player input[type=range]:focus::-ms-fill-lower{background:#7e7e7e}#gsap-player input[type=range]:focus::-ms-fill-upper{background:#8b8b8b}#gsap-player button{background:0 0;border:none;outline:0;cursor:pointer}#gsap-player .loop{margin:0 0 0 10px}#gsap-player{position:fixed;z-index:1000;bottom:40px;background:#000;display:flex;align-items:center;padding:10px;width:80%;margin:0 10%;opacity:.8}#gsap-player .playpause{position:relative;margin:0 0 0 10px}#gsap-player .playpause svg{position:absolute;top:-5px;left:-5px}#gsap-player .playpause .play{opacity:0}#gsap-player button,#gsap-player input{opacity:.8;transition:.2s opacity ease-in;-webkit-transition:.2s opacity ease-in}#gsap-player:hover button,#gsap-player:hover input{opacity:1;transition:.2s opacity ease-out;-webkit-transition:.2s opacity ease-out}#gsap-player .speed-tooltip{display:flex;flex-direction:column;font-size:10px;font-family:helvetica,sans-serif;opacity:0;visibility:hidden;position:absolute;top:-60px;left:32px;padding-top:5px}#gsap-player .speed-tooltip div{margin:2px 10px;opacity:.7;transition:.2s opacity ease-in;-webkit-transition:.2s opacity ease-in;cursor:pointer}#gsap-player .speed-tooltip div:hover{opacity:1;transition:.2s opacity ease-out;-webkit-transition:.2s opacity ease-out}#gsap-player .speed{color:#fff;margin:0 10px 0 5px}'); 8 | 9 | function gsapPlayer(params) { 10 | 'use strict'; 11 | 12 | injectGsapPlayerStyle(); 13 | 14 | params = params === undefined ? {} : params; 15 | var bottom = params.bottom === undefined ? '40px' : params.bottom, 16 | playerTL = params.playerTL === undefined ? tl : params.playerTL, 17 | container = params.container === undefined ? document.body : params.container, 18 | fullWidth = params.fullWidth === undefined ? false : params.fullWidth, 19 | light = params.light === undefined ? false : params.light; 20 | 21 | /*------------------------------------------ 22 | helper functions 23 | -------------------------------------------*/ 24 | 25 | //function to set multiple attributes at once 26 | function setAttributes(el, attrs) { 27 | for (let key in attrs) { 28 | el.setAttribute(key, attrs[key]); 29 | } 30 | } 31 | 32 | //function to create/append children because typing 33 | function append(type, parent) { 34 | var el = document.createElement(type); 35 | parent.appendChild(el); 36 | return el; 37 | } 38 | 39 | //create an SVG in the correct namespace, append a path 40 | function createSVG(parent, vbx, pathData) { 41 | var el = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 42 | var svgNS = el.namespaceURI; 43 | var path = document.createElementNS(svgNS, 'path'); 44 | parent.appendChild(el); 45 | el.appendChild(path); 46 | el.setAttribute('viewBox', vbx); 47 | path.setAttribute('d', pathData); 48 | return el; 49 | } 50 | 51 | /*------------------------------------------ 52 | Create the elements 53 | -------------------------------------------*/ 54 | 55 | var isPlaying = true, 56 | playerWidth, playerMargin, playerColor, iconColor; 57 | fullWidth ? playerWidth = '99%' : playerWidth = '80%'; 58 | fullWidth ? playerMargin = '0' : playerMargin = '0 10%'; 59 | light ? playerColor = 'white': playerColor = 'black'; 60 | light ? iconColor = '#333' : iconColor = '#ddd'; 61 | 62 | //gsap player 63 | if (container !== document.body) { container = document.querySelector(container) } 64 | var player = append('div', container); 65 | setAttributes(player, { 66 | 'id' : 'gsap-player', 67 | 'style': 'bottom: ' + bottom + '; background: ' + playerColor + '; width: ' + playerWidth + '; margin: ' + playerMargin, 68 | }); 69 | 70 | //playpause button 71 | var playpause = append('button', player); 72 | setAttributes(playpause, { 73 | 'class': 'playpause', 74 | }); 75 | 76 | //play svg 77 | var play = createSVG(playpause, '0 0 32 32', 'M6 4l20 12L6 28z'); 78 | setAttributes(play, { 79 | 'width' : '15px', 80 | 'height': '15px', 81 | 'class' : 'play', 82 | 'fill' : iconColor, 83 | }); 84 | 85 | //pause svg 86 | var pause = createSVG(playpause, '0 0 32 32', 'M4 4h10v24H4zm14 0h10v24H18z'); 87 | setAttributes(pause, { 88 | 'width' : '15px', 89 | 'height': '15px', 90 | 'class' : 'pause', 91 | 'fill' : iconColor, 92 | }); 93 | 94 | //speed button 95 | var speed = append('button', player); 96 | speed.innerHTML = '1x'; 97 | setAttributes(speed, { 98 | 'class': 'speed', 99 | 'style': 'color: ' + iconColor + '; font-size: 12px', 100 | }); 101 | 102 | //speed tooltip 103 | var sTooltip = append('div', player); 104 | var tenth = append('div', sTooltip); 105 | var half = append('div', sTooltip); 106 | var full = append('div', sTooltip); 107 | var double = append('div', sTooltip); 108 | tenth.innerHTML = '0.1x'; 109 | half.innerHTML = '0.5x'; 110 | full.innerHTML = '1x'; 111 | double.innerHTML = '2x'; 112 | setAttributes(sTooltip, { 113 | 'class' : 'speed-tooltip', 114 | 'style' : 'background: ' + playerColor + '; color: ' + iconColor, 115 | }); 116 | 117 | //range input 118 | var slider = append('input', player); 119 | setAttributes(slider, { 120 | 'type' : 'range', 121 | 'min' : 0, 122 | 'max' : 100, 123 | 'value': 0, 124 | 'id' : 'fader', 125 | 'step' : 0.1, 126 | }); 127 | 128 | //loop button 129 | var loop = append('button', player); 130 | var loopSVG = createSVG(loop, '0 0 32 32', 'M27.802 5.197C24.877 2.003 20.672 0 16 0 7.16 0 0 7.163 0 16h3C3 8.82 8.82 3 16 3c3.843 0 7.297 1.67 9.677 4.322L21 12h11V1l-4.2 4.197zM29 16c0 7.18-5.82 13-13 13-3.844 0-7.298-1.67-9.678-4.322L11 20H0v11l4.197-4.197C7.122 29.997 11.327 32 16 32c8.837 0 16-7.163 16-16h-3z'); 131 | loop.setAttribute('class', 'loop'); 132 | setAttributes(loopSVG, { 133 | 'width' : '15px', 134 | 'height': '15px', 135 | 'fill' : iconColor, 136 | }); 137 | 138 | /*------------------------------------------ 139 | timeline particulars 140 | -------------------------------------------*/ 141 | 142 | playerTL.progress( slider.value / 100 ); 143 | 144 | playerTL.eventCallback("onUpdate", function() { 145 | slider.value = playerTL.progress() * 100; 146 | 147 | if ( !playerTL.isActive() && playerTL.progress(1) ) { 148 | TweenMax.to(".play", 0.1, { 149 | opacity: 1 150 | }); 151 | TweenMax.to(".pause", 0.1, { 152 | opacity: 0 153 | }); 154 | isPlaying = false; 155 | } 156 | }); 157 | 158 | /*------------------------------------------ 159 | event listeners 160 | -------------------------------------------*/ 161 | 162 | slider.addEventListener('input', function() { 163 | this.setAttribute('value', this.value); 164 | playerTL.progress( this.value / 100 ); 165 | playerTL.resume(); 166 | }); 167 | 168 | speed.addEventListener('click', function() { 169 | TweenMax.to(".speed-tooltip", 0.2, { 170 | autoAlpha: 1 171 | }); 172 | 173 | var sToolDiv = document.querySelectorAll('.speed-tooltip div'); 174 | for (var i = 0; i < sToolDiv.length; i++) { 175 | sToolDiv[i].addEventListener('click', function() { 176 | 177 | var sNum = this.innerHTML; 178 | speed.innerHTML = sNum; 179 | sNum = sNum.slice(0, -1); 180 | playerTL.timeScale(sNum); 181 | 182 | TweenMax.to(".speed-tooltip", 0.2, { 183 | autoAlpha: 0 184 | }); 185 | 186 | }, false); 187 | } 188 | }); 189 | 190 | loop.addEventListener('click', function() { 191 | var loopSVG = this.querySelector('svg'); 192 | TweenMax.fromTo(loopSVG, 0.5, { 193 | rotation: 0 194 | }, { 195 | rotation: 360, 196 | transformOrigin: '50% 50%', 197 | ease: Circ.easeOut 198 | }) 199 | playerTL.progress(0); 200 | playerTL.restart(); 201 | 202 | if (!isPlaying) { 203 | TweenMax.to(play, 0.1, { 204 | opacity: 0 205 | }); 206 | TweenMax.to(pause, 0.1, { 207 | opacity: 1 208 | }); 209 | isPlaying = true; 210 | } 211 | }); 212 | 213 | playpause.addEventListener('click', function() { 214 | var play = this.querySelector('svg.play'), 215 | pause = this.querySelector('svg.pause'); 216 | 217 | if (isPlaying === true) { 218 | TweenMax.to(play, 0.1, { 219 | opacity: 1 220 | }); 221 | TweenMax.to(pause, 0.1, { 222 | opacity: 0 223 | }); 224 | 225 | playerTL.pause(); 226 | isPlaying = false; 227 | 228 | } else { 229 | TweenMax.to(play, 0.1, { 230 | opacity: 0 231 | }); 232 | TweenMax.to(pause, 0.1, { 233 | opacity: 1 234 | }); 235 | 236 | slider.value == 100 ? playerTL.restart() : playerTL.resume(); 237 | isPlaying = true; 238 | } 239 | }); 240 | 241 | }; 242 | --------------------------------------------------------------------------------