├── .gitignore ├── README.md ├── assets ├── demo.mp4 ├── guardian_480.mp4 ├── nyt1_480.mp4 ├── nyt2_480.mp4 ├── polygraph_480.mp4 ├── scrollytelling.gif └── scrollytelling.jpg ├── css ├── prism.css └── style.css ├── demo ├── d3.v4.min.js ├── graphic.css ├── graphic.js ├── graphscroll │ ├── d3.v4.min.js │ ├── graph-scroll.js │ └── index.html ├── inview │ ├── in-view.min.js │ └── index.html ├── rollyourown │ └── index.html ├── scrollmagic │ ├── ScrollMagic.min.js │ └── index.html ├── scrollstory │ ├── index.html │ ├── jquery-3.1.1.min.js │ └── jquery.scrollstory.min.js └── waypoints │ ├── index.html │ └── noframework.waypoints.min.js ├── index.html └── js └── prism.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Makefile -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to implement scrollytelling with six different libraries 2 | 3 | Check out the full article [here](https://pudding.cool/process/how-to-implement-scrollytelling). 4 | 5 | All the code is available in the [demo](demo) folder. 6 | -------------------------------------------------------------------------------- /assets/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-pudding/how-to-implement-scrollytelling/9e059c3c4f843cc9f2f1b3a0443d81f9273dda9d/assets/demo.mp4 -------------------------------------------------------------------------------- /assets/guardian_480.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-pudding/how-to-implement-scrollytelling/9e059c3c4f843cc9f2f1b3a0443d81f9273dda9d/assets/guardian_480.mp4 -------------------------------------------------------------------------------- /assets/nyt1_480.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-pudding/how-to-implement-scrollytelling/9e059c3c4f843cc9f2f1b3a0443d81f9273dda9d/assets/nyt1_480.mp4 -------------------------------------------------------------------------------- /assets/nyt2_480.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-pudding/how-to-implement-scrollytelling/9e059c3c4f843cc9f2f1b3a0443d81f9273dda9d/assets/nyt2_480.mp4 -------------------------------------------------------------------------------- /assets/polygraph_480.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-pudding/how-to-implement-scrollytelling/9e059c3c4f843cc9f2f1b3a0443d81f9273dda9d/assets/polygraph_480.mp4 -------------------------------------------------------------------------------- /assets/scrollytelling.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-pudding/how-to-implement-scrollytelling/9e059c3c4f843cc9f2f1b3a0443d81f9273dda9d/assets/scrollytelling.gif -------------------------------------------------------------------------------- /assets/scrollytelling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-pudding/how-to-implement-scrollytelling/9e059c3c4f843cc9f2f1b3a0443d81f9273dda9d/assets/scrollytelling.jpg -------------------------------------------------------------------------------- /css/prism.css: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism-okaidia&languages=clike+javascript */ 2 | /** 3 | * okaidia theme for JavaScript, CSS and HTML 4 | * Loosely based on Monokai textmate theme by http://www.monokai.nl/ 5 | * @author ocodia 6 | */ 7 | 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: #f8f8f2; 11 | background: none; 12 | text-shadow: 0 1px rgba(0, 0, 0, 0.3); 13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 14 | text-align: left; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | word-wrap: normal; 19 | line-height: 1.5; 20 | 21 | -moz-tab-size: 4; 22 | -o-tab-size: 4; 23 | tab-size: 4; 24 | 25 | -webkit-hyphens: none; 26 | -moz-hyphens: none; 27 | -ms-hyphens: none; 28 | hyphens: none; 29 | } 30 | 31 | /* Code blocks */ 32 | pre[class*="language-"] { 33 | padding: 1em; 34 | margin: .5em 0; 35 | overflow: auto; 36 | border-radius: 0.3em; 37 | } 38 | 39 | :not(pre) > code[class*="language-"], 40 | pre[class*="language-"] { 41 | background: #272822; 42 | } 43 | 44 | /* Inline code */ 45 | :not(pre) > code[class*="language-"] { 46 | padding: .1em; 47 | border-radius: .3em; 48 | white-space: normal; 49 | } 50 | 51 | .token.comment, 52 | .token.prolog, 53 | .token.doctype, 54 | .token.cdata { 55 | color: slategray; 56 | } 57 | 58 | .token.punctuation { 59 | color: #f8f8f2; 60 | } 61 | 62 | .namespace { 63 | opacity: .7; 64 | } 65 | 66 | .token.property, 67 | .token.tag, 68 | .token.constant, 69 | .token.symbol, 70 | .token.deleted { 71 | color: #f92672; 72 | } 73 | 74 | .token.boolean, 75 | .token.number { 76 | color: #ae81ff; 77 | } 78 | 79 | .token.selector, 80 | .token.attr-name, 81 | .token.string, 82 | .token.char, 83 | .token.builtin, 84 | .token.inserted { 85 | color: #a6e22e; 86 | } 87 | 88 | .token.operator, 89 | .token.entity, 90 | .token.url, 91 | .language-css .token.string, 92 | .style .token.string, 93 | .token.variable { 94 | color: #f8f8f2; 95 | } 96 | 97 | .token.atrule, 98 | .token.attr-value, 99 | .token.function { 100 | color: #e6db74; 101 | } 102 | 103 | .token.keyword { 104 | color: #66d9ef; 105 | } 106 | 107 | .token.regex, 108 | .token.important { 109 | color: #fd971f; 110 | } 111 | 112 | .token.important, 113 | .token.bold { 114 | font-weight: bold; 115 | } 116 | .token.italic { 117 | font-style: italic; 118 | } 119 | 120 | .token.entity { 121 | cursor: help; 122 | } 123 | 124 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | /* reset stuff */ 2 | * { 3 | box-sizing: border-box; 4 | } 5 | a, 6 | abbr, 7 | acronym, 8 | address, 9 | applet, 10 | article, 11 | aside, 12 | audio, 13 | b, 14 | big, 15 | blockquote, 16 | body, 17 | canvas, 18 | caption, 19 | center, 20 | cite, 21 | code, 22 | dd, 23 | del, 24 | details, 25 | dfn, 26 | div, 27 | dl, 28 | dt, 29 | em, 30 | embed, 31 | fieldset, 32 | figcaption, 33 | figure, 34 | footer, 35 | form, 36 | h1, 37 | h2, 38 | h3, 39 | h4, 40 | h5, 41 | h6, 42 | header, 43 | hgroup, 44 | html, 45 | i, 46 | iframe, 47 | img, 48 | ins, 49 | kbd, 50 | label, 51 | legend, 52 | li, 53 | mark, 54 | menu, 55 | nav, 56 | object, 57 | ol, 58 | output, 59 | p, 60 | pre, 61 | q, 62 | ruby, 63 | s, 64 | samp, 65 | section, 66 | small, 67 | span, 68 | strike, 69 | strong, 70 | sub, 71 | summary, 72 | sup, 73 | table, 74 | tbody, 75 | td, 76 | tfoot, 77 | th, 78 | thead, 79 | time, 80 | tr, 81 | tt, 82 | u, 83 | ul, 84 | var, 85 | video { 86 | margin: 0; 87 | padding: 0; 88 | border: 0; 89 | font-size: 100%; 90 | font: inherit; 91 | vertical-align: baseline; 92 | } 93 | article, 94 | aside, 95 | details, 96 | figcaption, 97 | figure, 98 | footer, 99 | header, 100 | hgroup, 101 | main, 102 | menu, 103 | nav, 104 | section { 105 | display: block; 106 | } 107 | body, 108 | html { 109 | height: 100%; 110 | } 111 | a img { 112 | border: none; 113 | } 114 | blockquote { 115 | quotes: none; 116 | } 117 | blockquote:after, 118 | blockquote:before { 119 | content: ""; 120 | content: none; 121 | } 122 | table { 123 | border-collapse: collapse; 124 | border-spacing: 0; 125 | } 126 | caption, 127 | td, 128 | th { 129 | text-align: left; 130 | font-weight: 400; 131 | vertical-align: middle; 132 | } 133 | html { 134 | -webkit-text-size-adjust: 100%; 135 | -ms-text-size-adjust: 100%; 136 | text-size-adjust: 100%; 137 | } 138 | body { 139 | font-feature-settings: "kern" 1, "onum" 1, "liga" 0; 140 | } 141 | b, 142 | strong { 143 | font-weight: 700; 144 | } 145 | em, 146 | i { 147 | font-style: italic; 148 | } 149 | ul { 150 | list-style-type: none; 151 | } 152 | img, 153 | video { 154 | display: block; 155 | width: 100%; 156 | } 157 | button { 158 | cursor: pointer; 159 | border: none; 160 | outline: 0; 161 | margin: 0; 162 | padding: 0; 163 | font-size: 1rem; 164 | } 165 | sub, 166 | sup { 167 | font-size: 75%; 168 | line-height: 0; 169 | position: relative; 170 | vertical-align: baseline; 171 | } 172 | sup { 173 | top: -0.5em; 174 | } 175 | sub { 176 | bottom: -0.25em; 177 | } 178 | .hide-accessible { 179 | width: 1px; 180 | height: 1px; 181 | overflow: hidden; 182 | position: absolute; 183 | } 184 | .hide-invisible { 185 | visibility: hidden; 186 | } 187 | .hide-display { 188 | display: none; 189 | } 190 | .tk-whitney { 191 | font-family: Helvetica, Arial, sans-serif; 192 | letter-spacing: 0.04em; 193 | font-weight: 300; 194 | } 195 | .loaded-whitney .tk-whitney { 196 | font-family: "Whitney SSm A", "Whitney SSm B", Helvetica, Arial, sans-serif; 197 | letter-spacing: normal; 198 | } 199 | .tk-mercury { 200 | font-family: Georgia, Times, serif; 201 | letter-spacing: 0.04em; 202 | font-weight: 400; 203 | } 204 | .loaded-mercury .tk-mercury { 205 | font-family: "Mercury SSm A", "Mercury SSm B", Georgia, Times, serif; 206 | letter-spacing: normal; 207 | } 208 | body { 209 | background: #fff; 210 | font-size: 17px; 211 | } 212 | @media only screen and (min-width: 25em) { 213 | .hack-for-mq-cache { 214 | height: 0; 215 | } 216 | } 217 | @media only screen and (min-width: 40em) { 218 | .hack-for-mq-cache { 219 | height: 0; 220 | } 221 | } 222 | @media only screen and (min-width: 50em) { 223 | .hack-for-mq-cache { 224 | height: 0; 225 | } 226 | } 227 | @media only screen and (min-width: 60em) { 228 | .hack-for-mq-cache { 229 | height: 0; 230 | } 231 | } 232 | 233 | body { 234 | font-size: 17px; 235 | font-weight: 400; 236 | background-color: #fffffc; 237 | color: #1a1a1a; 238 | line-height: 1.8; 239 | font-family: "Whitney SSm A", "Whitney SSm B", Helvetica, sans-serif; 240 | -webkit-font-smoothing: antialiased; 241 | -moz-osx-font-smoothing: grayscale; 242 | } 243 | 244 | main { 245 | padding: 0.75rem; 246 | padding-bottom: 5rem; 247 | } 248 | 249 | p { 250 | margin: 1.5rem 0; 251 | } 252 | 253 | a { 254 | color: #1a1a1a; 255 | text-decoration: none; 256 | border-bottom: 1px dotted currentColor; 257 | } 258 | 259 | a:hover { 260 | color: #f33; 261 | } 262 | 263 | ol, 264 | ul { 265 | list-style-type: none; 266 | } 267 | 268 | header { 269 | z-index: 1000; 270 | margin: 1rem auto; 271 | max-width: 12rem; 272 | padding: 1rem; 273 | } 274 | header a.logo { 275 | display: block; 276 | border: none; 277 | } 278 | header a.logo svg { 279 | fill: #2a2a2a; 280 | display: block; 281 | width: 100%; 282 | } 283 | 284 | .intro { 285 | max-width: 60rem; 286 | margin: 4rem auto 2rem auto; 287 | } 288 | 289 | .intro p { 290 | max-width: 40rem; 291 | margin-left: auto; 292 | margin-right: auto; 293 | } 294 | 295 | .hed { 296 | font-family: "Mercury SSm A", "Mercury SSm B", Georgia, serif; 297 | font-size: 2rem; 298 | font-weight: bold; 299 | line-height: 1.4; 300 | margin-bottom: 3rem; 301 | margin-left: auto; 302 | margin-right: auto; 303 | max-width: 40rem; 304 | } 305 | 306 | .hed a { 307 | border: none; 308 | } 309 | 310 | .byline { 311 | color: #777; 312 | height: 100%; 313 | overflow: hidden; 314 | } 315 | 316 | .byline__author { 317 | float: left; 318 | /*display: block;*/ 319 | } 320 | 321 | .byline__date { 322 | float: right; 323 | } 324 | 325 | .disclaimer { 326 | clear: both; 327 | font-size: 0.9rem; 328 | } 329 | 330 | .update { 331 | background-color: #eee; 332 | font-size: 0.9rem; 333 | padding: 1rem; 334 | } 335 | 336 | .video-example { 337 | display: -webkit-flex; 338 | display: -ms-flexbox; 339 | display: flex; 340 | -webkit-flex-wrap: wrap; 341 | -ms-flex-wrap: wrap; 342 | flex-wrap: wrap; 343 | -webkit-align-items: flex-start; 344 | -ms-flex-align: flex-start; 345 | align-items: flex-start; 346 | } 347 | 348 | .video-example { 349 | background-color: #1a1a1a; 350 | margin: 3rem auto; 351 | } 352 | 353 | .example-link { 354 | display: block; 355 | width: 49.25%; 356 | margin: 0.25%; 357 | border: none; 358 | } 359 | 360 | .example-link:nth-child(1) { 361 | margin-left: 0.5%; 362 | margin-top: 0.5%; 363 | } 364 | 365 | .example-link:nth-child(2) { 366 | margin-right: 0.5%; 367 | margin-top: 0.5%; 368 | } 369 | 370 | .example-link:nth-child(3) { 371 | margin-left: 0.5%; 372 | margin-bottom: 0.5%; 373 | } 374 | 375 | .example-link:nth-child(4) { 376 | margin-right: 0.5%; 377 | margin-bottom: 0.5%; 378 | } 379 | 380 | .video-demo { 381 | display: block; 382 | max-width: 40rem; 383 | margin: 0 auto; 384 | border: 1px solid #eee; 385 | } 386 | 387 | .outro { 388 | max-width: 40rem; 389 | margin: 0 auto; 390 | margin-bottom: 5rem; 391 | } 392 | 393 | .library { 394 | padding: 1rem 0; 395 | margin: 2rem auto; 396 | max-width: 50rem; 397 | } 398 | 399 | .library__text { 400 | margin: 0 auto; 401 | max-width: 40rem; 402 | } 403 | 404 | .library__hed { 405 | font-size: 1.25rem; 406 | text-transform: uppercase; 407 | font-weight: 700; 408 | margin-bottom: 1rem; 409 | } 410 | 411 | .library__review { 412 | } 413 | 414 | .library__demo { 415 | text-transform: uppercase; 416 | } 417 | 418 | .library__code { 419 | font-size: 0.85rem; 420 | height: 20vh; 421 | overflow: hidden; 422 | position: relative; 423 | } 424 | 425 | .library__code.is-expand { 426 | height: auto; 427 | } 428 | 429 | .library__code:after { 430 | content: ""; 431 | display: block; 432 | width: 100%; 433 | position: absolute; 434 | left: 0; 435 | bottom: 0; 436 | height: 3rem; 437 | background-image: linear-gradient( 438 | to bottom, 439 | rgba(0, 0, 0, 0), 440 | rgba(39, 40, 34, 1) 80% 441 | ); 442 | } 443 | 444 | .code__button { 445 | display: block; 446 | width: 100%; 447 | background: #1a1a1a; 448 | color: #efefef; 449 | border-top: 1px solid #666; 450 | padding: 0.5rem; 451 | text-align: center; 452 | text-transform: uppercase; 453 | font-size: 0.9rem; 454 | font-weight: 700; 455 | cursor: pointer; 456 | } 457 | 458 | .code__button:hover { 459 | background-color: #f33; 460 | } 461 | 462 | .back-to-blog { 463 | text-align: center; 464 | } 465 | 466 | .demo-links { 467 | height: 100%; 468 | overflow: hidden; 469 | max-width: 40rem; 470 | margin: 0 auto; 471 | text-align: center; 472 | } 473 | 474 | .demo-links li { 475 | float: left; 476 | margin-right: 1rem; 477 | } 478 | -------------------------------------------------------------------------------- /demo/graphic.css: -------------------------------------------------------------------------------- 1 | /* GRAPHIC CODE */ 2 | .graphic { 3 | width: 100%; 4 | position: relative; 5 | } 6 | 7 | .graphic__prose { 8 | width: 24rem; 9 | } 10 | 11 | .graphic__prose .trigger { 12 | padding: 0; 13 | margin: 0; 14 | min-height: 240px; 15 | 16 | } 17 | 18 | .graphic__vis { 19 | position: absolute; 20 | top: 0; 21 | margin-left: 30rem; 22 | -webkit-transform: translate3d(0, 0, 0); 23 | -moz-transform: translate3d(0, 0, 0); 24 | transform: translate3d(0, 0, 0); 25 | height: 100vh; 26 | } 27 | 28 | .graphic__vis.is-fixed { 29 | position: fixed; 30 | } 31 | 32 | .graphic__vis.is-bottom { 33 | top: auto; 34 | bottom: 0; 35 | } 36 | 37 | .graphic__vis svg { 38 | border: 1px dashed black; 39 | top: 50%; 40 | position: relative; 41 | -webkit-transform: translateY(-50%); 42 | -moz-transform: translateY(-50%); 43 | transform: translateY(-50%); 44 | 45 | } 46 | 47 | .item circle { 48 | stroke: #666; 49 | stroke-width: 1px; 50 | fill: #fff; 51 | } 52 | 53 | .item text { 54 | fill: #666; 55 | font-size: 12px; 56 | text-anchor: middle; 57 | alignment-baseline: middle; 58 | } 59 | 60 | 61 | 62 | /* graph-scroll.js version */ 63 | .graphic__vis.graph-scroll-fixed { 64 | position: fixed; 65 | right: auto; 66 | } 67 | 68 | .graphic__vis.graph-scroll-below { 69 | top: auto; 70 | bottom: 0; 71 | } -------------------------------------------------------------------------------- /demo/graphic.js: -------------------------------------------------------------------------------- 1 | /* 2 | I've created a function here that is a simple d3 chart. 3 | This could be anthing that has discrete steps, as simple as changing 4 | the background color, or playing/pausing a video. 5 | The important part is that it exposes and update function that 6 | calls a new thing on a scroll trigger. 7 | */ 8 | window.createGraphic = function(graphicSelector) { 9 | var graphicEl = d3.select('.graphic') 10 | var graphicVisEl = graphicEl.select('.graphic__vis') 11 | var graphicProseEl = graphicEl.select('.graphic__prose') 12 | 13 | var margin = 20 14 | var size = 400 15 | var chartSize = size - margin * 2 16 | var scaleX = null 17 | var scaleR = null 18 | var data = [8, 6, 7, 5, 3, 0, 9] 19 | var extent = d3.extent(data) 20 | var minR = 10 21 | var maxR = 24 22 | 23 | // actions to take on each step of our scroll-driven story 24 | var steps = [ 25 | function step0() { 26 | // circles are centered and small 27 | var t = d3.transition() 28 | .duration(800) 29 | .ease(d3.easeQuadInOut) 30 | 31 | 32 | var item = graphicVisEl.selectAll('.item') 33 | 34 | item.transition(t) 35 | .attr('transform', translate(chartSize / 2, chartSize / 2)) 36 | 37 | item.select('circle') 38 | .transition(t) 39 | .attr('r', minR) 40 | 41 | item.select('text') 42 | .transition(t) 43 | .style('opacity', 0) 44 | }, 45 | 46 | function step1() { 47 | var t = d3.transition() 48 | .duration(800) 49 | .ease(d3.easeQuadInOut) 50 | 51 | // circles are positioned 52 | var item = graphicVisEl.selectAll('.item') 53 | 54 | item.transition(t) 55 | .attr('transform', function(d, i) { 56 | return translate(scaleX(i), chartSize / 2) 57 | }) 58 | 59 | item.select('circle') 60 | .transition(t) 61 | .attr('r', minR) 62 | 63 | item.select('text') 64 | .transition(t) 65 | .style('opacity', 0) 66 | }, 67 | 68 | function step2() { 69 | var t = d3.transition() 70 | .duration(800) 71 | .ease(d3.easeQuadInOut) 72 | 73 | // circles are sized 74 | var item = graphicVisEl.selectAll('.item') 75 | 76 | item.select('circle') 77 | .transition(t) 78 | .delay(function(d, i) { return i * 200 }) 79 | .attr('r', function(d, i) { 80 | return scaleR(d) 81 | }) 82 | 83 | item.select('text') 84 | .transition(t) 85 | .delay(function(d, i) { return i * 200 }) 86 | .style('opacity', 1) 87 | }, 88 | ] 89 | 90 | // update our chart 91 | function update(step) { 92 | steps[step].call() 93 | } 94 | 95 | // little helper for string concat if using es5 96 | function translate(x, y) { 97 | return 'translate(' + x + ',' + y + ')' 98 | } 99 | 100 | function setupCharts() { 101 | var svg = graphicVisEl.append('svg') 102 | .attr('width', size + 'px') 103 | .attr('height', size + 'px') 104 | 105 | var chart = svg.append('g') 106 | .classed('chart', true) 107 | .attr('transform', 'translate(' + margin + ',' + margin + ')') 108 | 109 | scaleR = d3.scaleLinear() 110 | scaleX = d3.scaleBand() 111 | 112 | var domainX = d3.range(data.length) 113 | 114 | scaleX 115 | .domain(domainX) 116 | .range([0, chartSize]) 117 | .padding(1) 118 | 119 | scaleR 120 | .domain(extent) 121 | .range([minR, maxR]) 122 | 123 | var item = chart.selectAll('.item') 124 | .data(data) 125 | .enter().append('g') 126 | .classed('item', true) 127 | .attr('transform', translate(chartSize / 2, chartSize / 2)) 128 | 129 | item.append('circle') 130 | .attr('cx', 0) 131 | .attr('cy', 0) 132 | 133 | item.append('text') 134 | .text(function(d) { return d }) 135 | .attr('y', 1) 136 | .style('opacity', 0) 137 | } 138 | 139 | function setupProse() { 140 | var height = window.innerHeight * 0.5 141 | graphicProseEl.selectAll('.trigger') 142 | .style('height', height + 'px') 143 | } 144 | 145 | function init() { 146 | setupCharts() 147 | setupProse() 148 | update(0) 149 | } 150 | 151 | init() 152 | 153 | return { 154 | update: update, 155 | } 156 | } -------------------------------------------------------------------------------- /demo/graphscroll/graph-scroll.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3')) : 3 | typeof define === 'function' && define.amd ? define(['exports', 'd3'], factory) : 4 | (factory((global.d3 = global.d3 || {}),global.d3)); 5 | }(this, function (exports,d3) { 'use strict'; 6 | 7 | function graphScroll() { 8 | var windowHeight, 9 | dispatch = d3.dispatch("scroll", "active"), 10 | sections = d3.select('null'), 11 | i = NaN, 12 | sectionPos = [], 13 | n, 14 | graph = d3.select('null'), 15 | isFixed = null, 16 | isBelow = null, 17 | container = d3.select('body'), 18 | containerStart = 0, 19 | belowStart, 20 | eventId = Math.random(), 21 | offset = 0 22 | 23 | function reposition(){ 24 | var i1 = 0 25 | sectionPos.forEach(function(d, i){ 26 | if (d < pageYOffset - containerStart + offset) i1 = i 27 | }) 28 | i1 = Math.min(n - 1, i1) 29 | if (i != i1){ 30 | sections.classed('graph-scroll-active', function(d, i){ return i === i1 }) 31 | 32 | dispatch.call('active', null, i1) 33 | 34 | i = i1 35 | } 36 | 37 | var isBelow1 = pageYOffset > belowStart 38 | if (isBelow != isBelow1){ 39 | isBelow = isBelow1 40 | graph.classed('graph-scroll-below', isBelow) 41 | } 42 | var isFixed1 = !isBelow && pageYOffset > containerStart 43 | if (isFixed != isFixed1){ 44 | isFixed = isFixed1 45 | graph.classed('graph-scroll-fixed', isFixed) 46 | } 47 | } 48 | 49 | function resize(){ 50 | sectionPos = [] 51 | var startPos 52 | sections.each(function(d, i){ 53 | if (!i) startPos = this.getBoundingClientRect().top 54 | sectionPos.push(this.getBoundingClientRect().top - startPos) }) 55 | 56 | var containerBB = container.node().getBoundingClientRect() 57 | var graphBB = graph.node().getBoundingClientRect() 58 | 59 | containerStart = containerBB.top + pageYOffset 60 | belowStart = containerBB.bottom - graphBB.height + pageYOffset 61 | } 62 | 63 | function keydown() { 64 | if (!isFixed) return 65 | var delta 66 | switch (d3.event.keyCode) { 67 | case 39: // right arrow 68 | if (d3.event.metaKey) return 69 | case 40: // down arrow 70 | case 34: // page down 71 | delta = d3.event.metaKey ? Infinity : 1 ;break 72 | case 37: // left arrow 73 | if (d3.event.metaKey) return 74 | case 38: // up arrow 75 | case 33: // page up 76 | delta = d3.event.metaKey ? -Infinity : -1 ;break 77 | case 32: // space 78 | delta = d3.event.shiftKey ? -1 : 1 79 | ;break 80 | default: return 81 | } 82 | 83 | var i1 = Math.max(0, Math.min(i + delta, n - 1)) 84 | d3.select(document.documentElement) 85 | .interrupt() 86 | .transition() 87 | .duration(500) 88 | .tween("scroll", function() { 89 | var i = d3.interpolateNumber(pageYOffset, sectionPos[i1] + containerStart) 90 | return function(t) { scrollTo(0, i(t)) } 91 | }) 92 | 93 | d3.event.preventDefault() 94 | } 95 | 96 | 97 | var rv ={} 98 | 99 | rv.container = function(_x){ 100 | if (!_x) return container 101 | 102 | container = _x 103 | return rv 104 | } 105 | 106 | rv.graph = function(_x){ 107 | if (!_x) return graph 108 | 109 | graph = _x 110 | return rv 111 | } 112 | 113 | rv.eventId = function(_x){ 114 | if (!_x) return eventId 115 | 116 | eventId = _x 117 | return rv 118 | } 119 | 120 | rv.sections = function (_x){ 121 | if (!_x) return sections 122 | 123 | sections = _x 124 | n = sections.size() 125 | 126 | d3.select(window) 127 | .on('scroll.gscroll' + eventId, reposition) 128 | .on('resize.gscroll' + eventId, resize) 129 | .on('keydown.gscroll' + eventId, keydown) 130 | 131 | resize() 132 | d3.timer(function() { 133 | reposition() 134 | return true 135 | }) 136 | 137 | return rv 138 | } 139 | 140 | rv.on = function() { 141 | var value = dispatch.on.apply(dispatch, arguments); 142 | return value === dispatch ? rv : value; 143 | } 144 | 145 | rv.offset = function(_x) { 146 | if (!_x) return rv 147 | 148 | offset = _x 149 | return rv 150 | } 151 | 152 | return rv 153 | } 154 | 155 | exports.graphScroll = graphScroll; 156 | 157 | Object.defineProperty(exports, '__esModule', { value: true }); 158 | 159 | })); -------------------------------------------------------------------------------- /demo/graphscroll/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollytelling demo: graph-scroll.js 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 47 | 48 | 49 |
50 | 57 |
58 |
59 |
60 |

← Back to the blog

61 |

How to implement scrollytelling with six different libraries

62 |
63 | 64 | 73 | 74 |
75 |

graph-scroll.js

76 |
77 |
78 |

Step 1 in the graphic. It triggers in the middle of the viewport. For this graphic, it is the same as the initial state so the reader doesn’t miss anything.

79 |

Step 2 arrives. The graphic should be locking into a fixed position right about now. We could have a whole bunch of these “fixed” steps.

80 |

Step 3 concludes our brief tour. The graphic should now go back to its original in-flow position, elegantly snapping back into place.

81 |
82 |
83 |
84 |

← Back to the blog

85 |
86 | 87 | 88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /demo/inview/in-view.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * in-view 0.6.1 - Get notified when a DOM element enters or exits the viewport. 3 | * Copyright (c) 2016 Cam Wiegert - https://camwiegert.github.io/in-view 4 | * License: MIT 5 | */ 6 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.inView=e():t.inView=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return t[r].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{default:t}}var i=n(2),o=r(i);t.exports=o.default},function(t,e){function n(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{default:t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(9),o=r(i),u=n(3),f=r(u),s=n(4),c=function(){if("undefined"!=typeof window){var t=100,e=["scroll","resize","load"],n={history:[]},r={offset:{},threshold:0,test:s.inViewport},i=(0,o.default)(function(){n.history.forEach(function(t){n[t].check()})},t);e.forEach(function(t){return addEventListener(t,i)}),window.MutationObserver&&addEventListener("DOMContentLoaded",function(){new MutationObserver(i).observe(document.body,{attributes:!0,childList:!0,subtree:!0})});var u=function(t){if("string"==typeof t){var e=[].slice.call(document.querySelectorAll(t));return n.history.indexOf(t)>-1?n[t].elements=e:(n[t]=(0,f.default)(e,r),n.history.push(t)),n[t]}};return u.offset=function(t){if(void 0===t)return r.offset;var e=function(t){return"number"==typeof t};return["top","right","bottom","left"].forEach(e(t)?function(e){return r.offset[e]=t}:function(n){return e(t[n])?r.offset[n]=t[n]:null}),r.offset},u.threshold=function(t){return"number"==typeof t&&t>=0&&t<=1?r.threshold=t:r.threshold},u.test=function(t){return"function"==typeof t?r.test=t:r.test},u.is=function(t){return r.test(t,r)},u.offset(0),u}};e.default=c},function(t,e){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n-1,o=n&&!i,u=!n&&i;o&&(t.current.push(e),t.emit("enter",e)),u&&(t.current.splice(r,1),t.emit("exit",e))}),this}},{key:"on",value:function(t,e){return this.handlers[t].push(e),this}},{key:"once",value:function(t,e){return this.singles[t].unshift(e),this}},{key:"emit",value:function(t,e){for(;this.singles[t].length;)this.singles[t].pop()(e);for(var n=this.handlers[t].length;--n>-1;)this.handlers[t][n](e);return this}}]),t}();e.default=function(t,e){return new i(t,e)}},function(t,e){"use strict";function n(t,e){var n=t.getBoundingClientRect(),r=n.top,i=n.right,o=n.bottom,u=n.left,f=n.width,s=n.height,c={t:o,r:window.innerWidth-u,b:window.innerHeight-r,l:i},a={x:e.threshold*f,y:e.threshold*s};return c.t>e.offset.top+a.y&&c.r>e.offset.right+a.x&&c.b>e.offset.bottom+a.y&&c.l>e.offset.left+a.x}Object.defineProperty(e,"__esModule",{value:!0}),e.inViewport=n},function(t,e){(function(e){var n="object"==typeof e&&e&&e.Object===Object&&e;t.exports=n}).call(e,function(){return this}())},function(t,e,n){var r=n(5),i="object"==typeof self&&self&&self.Object===Object&&self,o=r||i||Function("return this")();t.exports=o},function(t,e,n){function r(t,e,n){function r(e){var n=x,r=m;return x=m=void 0,E=e,w=t.apply(r,n)}function a(t){return E=t,j=setTimeout(h,e),M?r(t):w}function l(t){var n=t-O,r=t-E,i=e-n;return _?c(i,g-r):i}function d(t){var n=t-O,r=t-E;return void 0===O||n>=e||n<0||_&&r>=g}function h(){var t=o();return d(t)?p(t):void(j=setTimeout(h,l(t)))}function p(t){return j=void 0,T&&x?r(t):(x=m=void 0,w)}function v(){void 0!==j&&clearTimeout(j),E=0,x=O=m=j=void 0}function y(){return void 0===j?w:p(o())}function b(){var t=o(),n=d(t);if(x=arguments,m=this,O=t,n){if(void 0===j)return a(O);if(_)return j=setTimeout(h,e),r(O)}return void 0===j&&(j=setTimeout(h,e)),w}var x,m,g,w,j,O,E=0,M=!1,_=!1,T=!0;if("function"!=typeof t)throw new TypeError(f);return e=u(e)||0,i(n)&&(M=!!n.leading,_="maxWait"in n,g=_?s(u(n.maxWait)||0,e):g,T="trailing"in n?!!n.trailing:T),b.cancel=v,b.flush=y,b}var i=n(1),o=n(8),u=n(10),f="Expected a function",s=Math.max,c=Math.min;t.exports=r},function(t,e,n){var r=n(6),i=function(){return r.Date.now()};t.exports=i},function(t,e,n){function r(t,e,n){var r=!0,f=!0;if("function"!=typeof t)throw new TypeError(u);return o(n)&&(r="leading"in n?!!n.leading:r,f="trailing"in n?!!n.trailing:f),i(t,e,{leading:r,maxWait:e,trailing:f})}var i=n(7),o=n(1),u="Expected a function";t.exports=r},function(t,e){function n(t){return t}t.exports=n}])}); -------------------------------------------------------------------------------- /demo/inview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollytelling demo: in-view.js 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 46 | 47 | 48 |
49 | 56 |
57 |
58 | 62 | 63 | 72 | 73 |
74 |

in-view.js

75 |
76 |
77 |

Step 1 in the graphic. It triggers in the middle of the viewport. For this graphic, it is the same as the initial state so the reader doesn’t miss anything.

78 |

Step 2 arrives. The graphic should be locking into a fixed position right about now. We could have a whole bunch of these “fixed” steps.

79 |

Step 3 concludes our brief tour. The graphic should now go back to its original in-flow position, elegantly snapping back into place.

80 |
81 |
82 |
83 |

← Back to the blog

84 |
85 | 86 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /demo/rollyourown/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollytelling demo: Roll your own 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 46 | 47 | 48 |
49 | 56 |
57 |
58 | 62 | 63 | 72 | 73 |
74 |

Roll your own

75 |
76 |
77 |

Step 1 in the graphic. It triggers in the middle of the viewport. For this graphic, it is the same as the initial state so the reader doesn’t miss anything.

78 |

Step 2 arrives. The graphic should be locking into a fixed position right about now. We could have a whole bunch of these “fixed” steps.

79 |

Step 3 concludes our brief tour. The graphic should now go back to its original in-flow position, elegantly snapping back into place.

80 |
81 |
82 |
83 |

← Back to the blog

84 |
85 | 86 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /demo/scrollmagic/ScrollMagic.min.js: -------------------------------------------------------------------------------- 1 | /*! ScrollMagic v2.0.5 | (c) 2015 Jan Paepke (@janpaepke) | license & info: http://scrollmagic.io */ 2 | !function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():e.ScrollMagic=t()}(this,function(){"use strict";var e=function(){};e.version="2.0.5",window.addEventListener("mousewheel",function(){});var t="data-scrollmagic-pin-spacer";e.Controller=function(r){var o,s,a="ScrollMagic.Controller",l="FORWARD",c="REVERSE",u="PAUSED",f=n.defaults,d=this,h=i.extend({},f,r),g=[],p=!1,v=0,m=u,w=!0,y=0,S=!0,b=function(){for(var e in h)f.hasOwnProperty(e)||delete h[e];if(h.container=i.get.elements(h.container)[0],!h.container)throw a+" init failed.";w=h.container===window||h.container===document.body||!document.body.contains(h.container),w&&(h.container=window),y=z(),h.container.addEventListener("resize",T),h.container.addEventListener("scroll",T),h.refreshInterval=parseInt(h.refreshInterval)||f.refreshInterval,E()},E=function(){h.refreshInterval>0&&(s=window.setTimeout(A,h.refreshInterval))},x=function(){return h.vertical?i.get.scrollTop(h.container):i.get.scrollLeft(h.container)},z=function(){return h.vertical?i.get.height(h.container):i.get.width(h.container)},C=this._setScrollPos=function(e){h.vertical?w?window.scrollTo(i.get.scrollLeft(),e):h.container.scrollTop=e:w?window.scrollTo(e,i.get.scrollTop()):h.container.scrollLeft=e},F=function(){if(S&&p){var e=i.type.Array(p)?p:g.slice(0);p=!1;var t=v;v=d.scrollPos();var n=v-t;0!==n&&(m=n>0?l:c),m===c&&e.reverse(),e.forEach(function(e){e.update(!0)})}},L=function(){o=i.rAF(F)},T=function(e){"resize"==e.type&&(y=z(),m=u),p!==!0&&(p=!0,L())},A=function(){if(!w&&y!=z()){var e;try{e=new Event("resize",{bubbles:!1,cancelable:!1})}catch(t){e=document.createEvent("Event"),e.initEvent("resize",!1,!1)}h.container.dispatchEvent(e)}g.forEach(function(e){e.refresh()}),E()};this._options=h;var O=function(e){if(e.length<=1)return e;var t=e.slice(0);return t.sort(function(e,t){return e.scrollOffset()>t.scrollOffset()?1:-1}),t};return this.addScene=function(t){if(i.type.Array(t))t.forEach(function(e){d.addScene(e)});else if(t instanceof e.Scene)if(t.controller()!==d)t.addTo(d);else if(g.indexOf(t)<0){g.push(t),g=O(g),t.on("shift.controller_sort",function(){g=O(g)});for(var n in h.globalSceneOptions)t[n]&&t[n].call(t,h.globalSceneOptions[n])}return d},this.removeScene=function(e){if(i.type.Array(e))e.forEach(function(e){d.removeScene(e)});else{var t=g.indexOf(e);t>-1&&(e.off("shift.controller_sort"),g.splice(t,1),e.remove())}return d},this.updateScene=function(t,n){return i.type.Array(t)?t.forEach(function(e){d.updateScene(e,n)}):n?t.update(!0):p!==!0&&t instanceof e.Scene&&(p=p||[],-1==p.indexOf(t)&&p.push(t),p=O(p),L()),d},this.update=function(e){return T({type:"resize"}),e&&F(),d},this.scrollTo=function(n,r){if(i.type.Number(n))C.call(h.container,n,r);else if(n instanceof e.Scene)n.controller()===d&&d.scrollTo(n.scrollOffset(),r);else if(i.type.Function(n))C=n;else{var o=i.get.elements(n)[0];if(o){for(;o.parentNode.hasAttribute(t);)o=o.parentNode;var s=h.vertical?"top":"left",a=i.get.offset(h.container),l=i.get.offset(o);w||(a[s]-=d.scrollPos()),d.scrollTo(l[s]-a[s],r)}}return d},this.scrollPos=function(e){return arguments.length?(i.type.Function(e)&&(x=e),d):x.call(d)},this.info=function(e){var t={size:y,vertical:h.vertical,scrollPos:v,scrollDirection:m,container:h.container,isDocument:w};return arguments.length?void 0!==t[e]?t[e]:void 0:t},this.loglevel=function(){return d},this.enabled=function(e){return arguments.length?(S!=e&&(S=!!e,d.updateScene(g,!0)),d):S},this.destroy=function(e){window.clearTimeout(s);for(var t=g.length;t--;)g[t].destroy(e);return h.container.removeEventListener("resize",T),h.container.removeEventListener("scroll",T),i.cAF(o),null},b(),d};var n={defaults:{container:window,vertical:!0,globalSceneOptions:{},loglevel:2,refreshInterval:100}};e.Controller.addOption=function(e,t){n.defaults[e]=t},e.Controller.extend=function(t){var n=this;e.Controller=function(){return n.apply(this,arguments),this.$super=i.extend({},this),t.apply(this,arguments)||this},i.extend(e.Controller,n),e.Controller.prototype=n.prototype,e.Controller.prototype.constructor=e.Controller},e.Scene=function(n){var o,s,a="BEFORE",l="DURING",c="AFTER",u=r.defaults,f=this,d=i.extend({},u,n),h=a,g=0,p={start:0,end:0},v=0,m=!0,w=function(){for(var e in d)u.hasOwnProperty(e)||delete d[e];for(var t in u)L(t);C()},y={};this.on=function(e,t){return i.type.Function(t)&&(e=e.trim().split(" "),e.forEach(function(e){var n=e.split("."),r=n[0],i=n[1];"*"!=r&&(y[r]||(y[r]=[]),y[r].push({namespace:i||"",callback:t}))})),f},this.off=function(e,t){return e?(e=e.trim().split(" "),e.forEach(function(e){var n=e.split("."),r=n[0],i=n[1]||"",o="*"===r?Object.keys(y):[r];o.forEach(function(e){for(var n=y[e]||[],r=n.length;r--;){var o=n[r];!o||i!==o.namespace&&"*"!==i||t&&t!=o.callback||n.splice(r,1)}n.length||delete y[e]})}),f):f},this.trigger=function(t,n){if(t){var r=t.trim().split("."),i=r[0],o=r[1],s=y[i];s&&s.forEach(function(t){o&&o!==t.namespace||t.callback.call(f,new e.Event(i,t.namespace,f,n))})}return f},f.on("change.internal",function(e){"loglevel"!==e.what&&"tweenChanges"!==e.what&&("triggerElement"===e.what?E():"reverse"===e.what&&f.update())}).on("shift.internal",function(){S(),f.update()}),this.addTo=function(t){return t instanceof e.Controller&&s!=t&&(s&&s.removeScene(f),s=t,C(),b(!0),E(!0),S(),s.info("container").addEventListener("resize",x),t.addScene(f),f.trigger("add",{controller:s}),f.update()),f},this.enabled=function(e){return arguments.length?(m!=e&&(m=!!e,f.update(!0)),f):m},this.remove=function(){if(s){s.info("container").removeEventListener("resize",x);var e=s;s=void 0,e.removeScene(f),f.trigger("remove")}return f},this.destroy=function(e){return f.trigger("destroy",{reset:e}),f.remove(),f.off("*.*"),null},this.update=function(e){if(s)if(e)if(s.enabled()&&m){var t,n=s.info("scrollPos");t=d.duration>0?(n-p.start)/(p.end-p.start):n>=p.start?1:0,f.trigger("update",{startPos:p.start,endPos:p.end,scrollPos:n}),f.progress(t)}else T&&h===l&&O(!0);else s.updateScene(f,!1);return f},this.refresh=function(){return b(),E(),f},this.progress=function(e){if(arguments.length){var t=!1,n=h,r=s?s.info("scrollDirection"):"PAUSED",i=d.reverse||e>=g;if(0===d.duration?(t=g!=e,g=1>e&&i?0:1,h=0===g?a:l):0>e&&h!==a&&i?(g=0,h=a,t=!0):e>=0&&1>e&&i?(g=e,h=l,t=!0):e>=1&&h!==c?(g=1,h=c,t=!0):h!==l||i||O(),t){var o={progress:g,state:h,scrollDirection:r},u=h!=n,p=function(e){f.trigger(e,o)};u&&n!==l&&(p("enter"),p(n===a?"start":"end")),p("progress"),u&&h!==l&&(p(h===a?"start":"end"),p("leave"))}return f}return g};var S=function(){p={start:v+d.offset},s&&d.triggerElement&&(p.start-=s.info("size")*d.triggerHook),p.end=p.start+d.duration},b=function(e){if(o){var t="duration";F(t,o.call(f))&&!e&&(f.trigger("change",{what:t,newval:d[t]}),f.trigger("shift",{reason:t}))}},E=function(e){var n=0,r=d.triggerElement;if(s&&r){for(var o=s.info(),a=i.get.offset(o.container),l=o.vertical?"top":"left";r.parentNode.hasAttribute(t);)r=r.parentNode;var c=i.get.offset(r);o.isDocument||(a[l]-=s.scrollPos()),n=c[l]-a[l]}var u=n!=v;v=n,u&&!e&&f.trigger("shift",{reason:"triggerElementPosition"})},x=function(){d.triggerHook>0&&f.trigger("shift",{reason:"containerResize"})},z=i.extend(r.validate,{duration:function(e){if(i.type.String(e)&&e.match(/^(\.|\d)*\d+%$/)){var t=parseFloat(e)/100;e=function(){return s?s.info("size")*t:0}}if(i.type.Function(e)){o=e;try{e=parseFloat(o())}catch(n){e=-1}}if(e=parseFloat(e),!i.type.Number(e)||0>e)throw o?(o=void 0,0):0;return e}}),C=function(e){e=arguments.length?[e]:Object.keys(z),e.forEach(function(e){var t;if(z[e])try{t=z[e](d[e])}catch(n){t=u[e]}finally{d[e]=t}})},F=function(e,t){var n=!1,r=d[e];return d[e]!=t&&(d[e]=t,C(e),n=r!=d[e]),n},L=function(e){f[e]||(f[e]=function(t){return arguments.length?("duration"===e&&(o=void 0),F(e,t)&&(f.trigger("change",{what:e,newval:d[e]}),r.shifts.indexOf(e)>-1&&f.trigger("shift",{reason:e})),f):d[e]})};this.controller=function(){return s},this.state=function(){return h},this.scrollOffset=function(){return p.start},this.triggerPosition=function(){var e=d.offset;return s&&(e+=d.triggerElement?v:s.info("size")*f.triggerHook()),e};var T,A;f.on("shift.internal",function(e){var t="duration"===e.reason;(h===c&&t||h===l&&0===d.duration)&&O(),t&&_()}).on("progress.internal",function(){O()}).on("add.internal",function(){_()}).on("destroy.internal",function(e){f.removePin(e.reset)});var O=function(e){if(T&&s){var t=s.info(),n=A.spacer.firstChild;if(e||h!==l){var r={position:A.inFlow?"relative":"absolute",top:0,left:0},o=i.css(n,"position")!=r.position;A.pushFollowers?d.duration>0&&(h===c&&0===parseFloat(i.css(A.spacer,"padding-top"))?o=!0:h===a&&0===parseFloat(i.css(A.spacer,"padding-bottom"))&&(o=!0)):r[t.vertical?"top":"left"]=d.duration*g,i.css(n,r),o&&_()}else{"fixed"!=i.css(n,"position")&&(i.css(n,{position:"fixed"}),_());var u=i.get.offset(A.spacer,!0),f=d.reverse||0===d.duration?t.scrollPos-p.start:Math.round(g*d.duration*10)/10;u[t.vertical?"top":"left"]+=f,i.css(A.spacer.firstChild,{top:u.top,left:u.left})}}},_=function(){if(T&&s&&A.inFlow){var e=h===l,t=s.info("vertical"),n=A.spacer.firstChild,r=i.isMarginCollapseType(i.css(A.spacer,"display")),o={};A.relSize.width||A.relSize.autoFullWidth?e?i.css(T,{width:i.get.width(A.spacer)}):i.css(T,{width:"100%"}):(o["min-width"]=i.get.width(t?T:n,!0,!0),o.width=e?o["min-width"]:"auto"),A.relSize.height?e?i.css(T,{height:i.get.height(A.spacer)-(A.pushFollowers?d.duration:0)}):i.css(T,{height:"100%"}):(o["min-height"]=i.get.height(t?n:T,!0,!r),o.height=e?o["min-height"]:"auto"),A.pushFollowers&&(o["padding"+(t?"Top":"Left")]=d.duration*g,o["padding"+(t?"Bottom":"Right")]=d.duration*(1-g)),i.css(A.spacer,o)}},N=function(){s&&T&&h===l&&!s.info("isDocument")&&O()},P=function(){s&&T&&h===l&&((A.relSize.width||A.relSize.autoFullWidth)&&i.get.width(window)!=i.get.width(A.spacer.parentNode)||A.relSize.height&&i.get.height(window)!=i.get.height(A.spacer.parentNode))&&_()},D=function(e){s&&T&&h===l&&!s.info("isDocument")&&(e.preventDefault(),s._setScrollPos(s.info("scrollPos")-((e.wheelDelta||e[s.info("vertical")?"wheelDeltaY":"wheelDeltaX"])/3||30*-e.detail)))};this.setPin=function(e,n){var r={pushFollowers:!0,spacerClass:"scrollmagic-pin-spacer"};if(n=i.extend({},r,n),e=i.get.elements(e)[0],!e)return f;if("fixed"===i.css(e,"position"))return f;if(T){if(T===e)return f;f.removePin()}T=e;var o=T.parentNode.style.display,s=["top","left","bottom","right","margin","marginLeft","marginRight","marginTop","marginBottom"];T.parentNode.style.display="none";var a="absolute"!=i.css(T,"position"),l=i.css(T,s.concat(["display"])),c=i.css(T,["width","height"]);T.parentNode.style.display=o,!a&&n.pushFollowers&&(n.pushFollowers=!1);var u=T.parentNode.insertBefore(document.createElement("div"),T),d=i.extend(l,{position:a?"relative":"absolute",boxSizing:"content-box",mozBoxSizing:"content-box",webkitBoxSizing:"content-box"});if(a||i.extend(d,i.css(T,["width","height"])),i.css(u,d),u.setAttribute(t,""),i.addClass(u,n.spacerClass),A={spacer:u,relSize:{width:"%"===c.width.slice(-1),height:"%"===c.height.slice(-1),autoFullWidth:"auto"===c.width&&a&&i.isMarginCollapseType(l.display)},pushFollowers:n.pushFollowers,inFlow:a},!T.___origStyle){T.___origStyle={};var h=T.style,g=s.concat(["width","height","position","boxSizing","mozBoxSizing","webkitBoxSizing"]);g.forEach(function(e){T.___origStyle[e]=h[e]||""})}return A.relSize.width&&i.css(u,{width:c.width}),A.relSize.height&&i.css(u,{height:c.height}),u.appendChild(T),i.css(T,{position:a?"relative":"absolute",margin:"auto",top:"auto",left:"auto",bottom:"auto",right:"auto"}),(A.relSize.width||A.relSize.autoFullWidth)&&i.css(T,{boxSizing:"border-box",mozBoxSizing:"border-box",webkitBoxSizing:"border-box"}),window.addEventListener("scroll",N),window.addEventListener("resize",N),window.addEventListener("resize",P),T.addEventListener("mousewheel",D),T.addEventListener("DOMMouseScroll",D),O(),f},this.removePin=function(e){if(T){if(h===l&&O(!0),e||!s){var n=A.spacer.firstChild;if(n.hasAttribute(t)){var r=A.spacer.style,o=["margin","marginLeft","marginRight","marginTop","marginBottom"];margins={},o.forEach(function(e){margins[e]=r[e]||""}),i.css(n,margins)}A.spacer.parentNode.insertBefore(n,A.spacer),A.spacer.parentNode.removeChild(A.spacer),T.parentNode.hasAttribute(t)||(i.css(T,T.___origStyle),delete T.___origStyle)}window.removeEventListener("scroll",N),window.removeEventListener("resize",N),window.removeEventListener("resize",P),T.removeEventListener("mousewheel",D),T.removeEventListener("DOMMouseScroll",D),T=void 0}return f};var R,k=[];return f.on("destroy.internal",function(e){f.removeClassToggle(e.reset)}),this.setClassToggle=function(e,t){var n=i.get.elements(e);return 0!==n.length&&i.type.String(t)?(k.length>0&&f.removeClassToggle(),R=t,k=n,f.on("enter.internal_class leave.internal_class",function(e){var t="enter"===e.type?i.addClass:i.removeClass;k.forEach(function(e){t(e,R)})}),f):f},this.removeClassToggle=function(e){return e&&k.forEach(function(e){i.removeClass(e,R)}),f.off("start.internal_class end.internal_class"),R=void 0,k=[],f},w(),f};var r={defaults:{duration:0,offset:0,triggerElement:void 0,triggerHook:.5,reverse:!0,loglevel:2},validate:{offset:function(e){if(e=parseFloat(e),!i.type.Number(e))throw 0;return e},triggerElement:function(e){if(e=e||void 0){var t=i.get.elements(e)[0];if(!t)throw 0;e=t}return e},triggerHook:function(e){var t={onCenter:.5,onEnter:1,onLeave:0};if(i.type.Number(e))e=Math.max(0,Math.min(parseFloat(e),1));else{if(!(e in t))throw 0;e=t[e]}return e},reverse:function(e){return!!e}},shifts:["duration","offset","triggerHook"]};e.Scene.addOption=function(e,t,n,i){e in r.defaults||(r.defaults[e]=t,r.validate[e]=n,i&&r.shifts.push(e))},e.Scene.extend=function(t){var n=this;e.Scene=function(){return n.apply(this,arguments),this.$super=i.extend({},this),t.apply(this,arguments)||this},i.extend(e.Scene,n),e.Scene.prototype=n.prototype,e.Scene.prototype.constructor=e.Scene},e.Event=function(e,t,n,r){r=r||{};for(var i in r)this[i]=r[i];return this.type=e,this.target=this.currentTarget=n,this.namespace=t||"",this.timeStamp=this.timestamp=Date.now(),this};var i=e._util=function(e){var t,n={},r=function(e){return parseFloat(e)||0},i=function(t){return t.currentStyle?t.currentStyle:e.getComputedStyle(t)},o=function(t,n,o,s){if(n=n===document?e:n,n===e)s=!1;else if(!f.DomElement(n))return 0;t=t.charAt(0).toUpperCase()+t.substr(1).toLowerCase();var a=(o?n["offset"+t]||n["outer"+t]:n["client"+t]||n["inner"+t])||0;if(o&&s){var l=i(n);a+="Height"===t?r(l.marginTop)+r(l.marginBottom):r(l.marginLeft)+r(l.marginRight)}return a},s=function(e){return e.replace(/^[^a-z]+([a-z])/g,"$1").replace(/-([a-z])/g,function(e){return e[1].toUpperCase()})};n.extend=function(e){for(e=e||{},t=1;t-1};var a=0,l=["ms","moz","webkit","o"],c=e.requestAnimationFrame,u=e.cancelAnimationFrame;for(t=0;!c&&t=0},f.DomElement=function(e){return"object"==typeof HTMLElement?e instanceof HTMLElement:e&&"object"==typeof e&&null!==e&&1===e.nodeType&&"string"==typeof e.nodeName};var d=n.get={};return d.elements=function(t){var n=[];if(f.String(t))try{t=document.querySelectorAll(t)}catch(r){return n}if("nodelist"===f(t)||f.Array(t))for(var i=0,o=n.length=t.length;o>i;i++){var s=t[i];n[i]=f.DomElement(s)?s:d.elements(s)}else(f.DomElement(t)||t===document||t===e)&&(n=[t]);return n},d.scrollTop=function(t){return t&&"number"==typeof t.scrollTop?t.scrollTop:e.pageYOffset||0},d.scrollLeft=function(t){return t&&"number"==typeof t.scrollLeft?t.scrollLeft:e.pageXOffset||0},d.width=function(e,t,n){return o("width",e,t,n)},d.height=function(e,t,n){return o("height",e,t,n)},d.offset=function(e,t){var n={top:0,left:0};if(e&&e.getBoundingClientRect){var r=e.getBoundingClientRect();n.top=r.top,n.left=r.left,t||(n.top+=d.scrollTop(),n.left+=d.scrollLeft())}return n},n.addClass=function(e,t){t&&(e.classList?e.classList.add(t):e.className+=" "+t)},n.removeClass=function(e,t){t&&(e.classList?e.classList.remove(t):e.className=e.className.replace(RegExp("(^|\\b)"+t.split(" ").join("|")+"(\\b|$)","gi")," "))},n.css=function(e,t){if(f.String(t))return i(e)[s(t)];if(f.Array(t)){var n={},r=i(e);return t.forEach(function(e){n[e]=r[s(e)]}),n}for(var o in t){var a=t[o];a==parseFloat(a)&&(a+="px"),e.style[s(o)]=a}},n}(window||{});return e}); -------------------------------------------------------------------------------- /demo/scrollmagic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollytelling demo: ScrollMagic.js 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 46 | 47 | 48 |
49 | 56 |
57 |
58 | 62 | 63 | 72 | 73 |
74 |

ScrollMagic

75 |
76 |
77 |

Step 1 in the graphic. It triggers in the middle of the viewport. For this graphic, it is the same as the initial state so the reader doesn’t miss anything.

78 |

Step 2 arrives. The graphic should be locking into a fixed position right about now. We could have a whole bunch of these “fixed” steps.

79 |

Step 3 concludes our brief tour. The graphic should now go back to its original in-flow position, elegantly snapping back into place.

80 |
81 |
82 |
83 |

← Back to the blog

84 |
85 | 86 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /demo/scrollstory/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollytelling demo: ScrollStory.js 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 46 | 47 | 48 |
49 | 56 |
57 |
58 | 62 | 63 | 72 | 73 |
74 |

ScrollStory

75 |
76 |
77 |

Step 1 in the graphic. It triggers in the middle of the viewport. For this graphic, it is the same as the initial state so the reader doesn’t miss anything.

78 |

Step 2 arrives. The graphic should be locking into a fixed position right about now. We could have a whole bunch of these “fixed” steps.

79 |

Step 3 concludes our brief tour. The graphic should now go back to its original in-flow position, elegantly snapping back into place.

80 |
81 |
82 |
83 |

← Back to the blog

84 |
85 | 86 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /demo/scrollstory/jquery.scrollstory.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve ScrollStory - v0.3.7 - 2016-07-15 3 | * https://github.com/sjwilliams/scrollstory 4 | * Copyright (c) 2016 Josh Williams; Licensed MIT 5 | */ 6 | (function(factory){if(typeof define==="function"&&define.amd){define(["jquery",undefined],factory)}else{factory(jQuery,undefined)}})(function($,undefined){var pluginName="scrollStory";var defaults={content:null,contentSelector:".story",keyboard:true,scrollOffset:0,triggerOffset:0,scrollEvent:"scroll",autoActivateFirstItem:false,disablePastLastItem:true,speed:800,easing:"swing",throttleType:"throttle",scrollSensitivity:100,throttleTypeOptions:null,autoUpdateOffsets:true,debug:false,enabled:true,setup:$.noop,itembuild:$.noop,itemfocus:$.noop,itemblur:$.noop,itemfilter:$.noop,itemunfilter:$.noop,itementerviewport:$.noop,itemexitviewport:$.noop,categoryfocus:$.noop,categeryblur:$.noop,containeractive:$.noop,containerinactive:$.noop,containerresize:$.noop,containerscroll:$.noop,updateoffsets:$.noop,triggeroffsetupdate:$.noop,scrolloffsetupdate:$.noop,complete:$.noop};var instanceCounter=0;var dateNow=Date.now||function(){return(new Date).getTime()};var debounce=function(func,wait,immediate){var result;var timeout=null;return function(){var context=this,args=arguments;var later=function(){timeout=null;if(!immediate){result=func.apply(context,args)}};var callNow=immediate&&!timeout;clearTimeout(timeout);timeout=setTimeout(later,wait);if(callNow){result=func.apply(context,args)}return result}};var throttle=function(func,wait,options){var context,args,result;var timeout=null;var previous=0;options||(options={});var later=function(){previous=options.leading===false?0:dateNow();timeout=null;result=func.apply(context,args)};return function(){var now=dateNow();if(!previous&&options.leading===false){previous=now}var remaining=wait-(now-previous);context=this;args=arguments;if(remaining<=0){clearTimeout(timeout);timeout=null;previous=now;result=func.apply(context,args)}else if(!timeout&&options.trailing!==false){timeout=setTimeout(later,remaining)}return result}};var $window=$(window);var winHeight=$window.height();var offsetToPx=function(offset){var pxOffset;if(offsetIsAPercentage(offset)){pxOffset=offset.slice(0,-1);pxOffset=Math.round(winHeight*(parseInt(pxOffset,10)/100))}else{pxOffset=parseInt(offset,10)}return pxOffset};var offsetIsAPercentage=function(offset){return typeof offset==="string"&&offset.slice(-1)==="%"};function ScrollStory(element,options){this.el=element;this.$el=$(element);this.options=$.extend({},defaults,options);this.useNativeScroll=typeof this.options.scrollEvent==="string"&&this.options.scrollEvent.indexOf("scroll")===0;this._defaults=defaults;this._name=pluginName;this._instanceId=function(){return pluginName+"_"+instanceCounter}();this.init()}ScrollStory.prototype={init:function(){this._items=[];this._itemsById={};this._categories=[];this._tags=[];this._isActive=false;this._activeItem;this._previousItems=[];this.$el.on("setup",this._onSetup.bind(this));this.$el.on("containeractive",this._onContainerActive.bind(this));this.$el.on("containerinactive",this._onContainerInactive.bind(this));this.$el.on("itemblur",this._onItemBlur.bind(this));this.$el.on("itemfocus",this._onItemFocus.bind(this));this.$el.on("itementerviewport",this._onItemEnterViewport.bind(this));this.$el.on("itemexitviewport",this._onItemExitViewport.bind(this));this.$el.on("itemfilter",this._onItemFilter.bind(this));this.$el.on("itemunfilter",this._onItemUnfilter.bind(this));this.$el.on("categoryfocus",this._onCategoryFocus.bind(this));this.$el.on("triggeroffsetupdate",this._onTriggerOffsetUpdate.bind(this));this._trigger("setup",null,this);this.addItems(this.options.content,{handleRepaint:false});this.updateOffsets();this._trigger("complete",null,this);this._handleRepaint();if(this.options.keyboard){$(document).keydown(function(e){var captured=true;switch(e.keyCode){case 37:if(e.metaKey){return}this.previous();break;case 39:this.next();break;default:captured=false}return!captured}.bind(this))}this.$trigger=$('
').css({position:"fixed",width:"100%",height:"1px",top:offsetToPx(this.options.triggerOffset)+"px",left:"0px",backgroundColor:"#ff0000","-webkit-transform":"translateZ(0)","-webkit-backface-visibility":"hidden",zIndex:1e3}).attr("id",pluginName+"Trigger-"+this._instanceId);if(this.options.debug){this.$trigger.appendTo("body")}var scrollThrottle,scrollHandler;if(this.useNativeScroll){scrollThrottle=this.options.throttleType==="throttle"?throttle:debounce;scrollHandler=scrollThrottle(this._handleScroll.bind(this),this.options.scrollSensitivity,this.options.throttleTypeOptions);$window.on("scroll",scrollHandler)}else{scrollHandler=this._handleScroll.bind(this);if(typeof this.options.scrollEvent==="function"){this.options.scrollEvent(scrollHandler)}else{$window.on(this.options.scrollEvent,function(){scrollHandler()})}}var resizeThrottle=debounce(this._handleResize,100);$window.on("DOMContentLoaded load resize",resizeThrottle.bind(this));instanceCounter=instanceCounter+1},index:function(index,callback){if(typeof index==="number"&&this.getItemByIndex(index)){this.setActiveItem(this.getItemByIndex(index),{},callback)}else{return this.getActiveItem().index}},next:function(){this.index(this.index()+1)},previous:function(){this.index(this.index()-1)},getActiveItem:function(){return this._activeItem},setActiveItem:function(item,options,callback){options=options||{};if(item.id&&this.getItemById(item.id)){this._scrollToItem(item,options,callback)}},each:function(callback){this.applyToAllItems(callback)},getLength:function(){return this.getItems().length},getItems:function(){return this._items},getItemById:function(id){return this._itemsById[id]},getItemByIndex:function(index){return this._items[index]},getItemsBy:function(truthTest){if(typeof truthTest!=="function"){throw new Error("You must provide a truthTest function")}return this.getItems().filter(function(item){return truthTest(item)})},getItemsWhere:function(properties){var keys,items=[];if($.isPlainObject(properties)){keys=Object.keys(properties);items=this.getItemsBy(function(item){var isMatch=keys.every(function(key){var match;if(typeof properties[key]==="function"){match=properties[key](item[key]);if(typeof match!=="boolean"){match=item[key]===match}}else{match=item[key]===properties[key]}return match});if(isMatch){return item}})}return items},getItemsInViewport:function(){return this.getItemsWhere({inViewport:true})},getPreviousItem:function(){return this._previousItems[0]},getPreviousItems:function(){return this._previousItems},getPercentScrollToLastItem:function(){return this._percentScrollToLastItem||0},getFilteredItems:function(){return this.getItemsWhere({filtered:true})},getUnFilteredItems:function(){return this.getItemsWhere({filtered:false})},getItemsByCategory:function(categorySlug){return this.getItemsWhere({category:categorySlug})},getCategorySlugs:function(){return this._categories},filter:function(item){if(!item.filtered){item.filtered=true;this._trigger("itemfilter",null,item)}},unfilter:function(item){if(item.filtered){item.filtered=false;this._trigger("itemunfilter",null,item)}},filterAll:function(callback){callback=$.isFunction(callback)?callback.bind(this):$.noop;var filterFnc=this.filter.bind(this);this.getItems().forEach(filterFnc)},unfilterAll:function(callback){callback=$.isFunction(callback)?callback.bind(this):$.noop;var unfilterFnc=this.unfilter.bind(this);this.getItems().forEach(unfilterFnc)},filterBy:function(truthTest,callback){callback=$.isFunction(callback)?callback.bind(this):$.noop;var filterFnc=this.filter.bind(this);this.getItemsBy(truthTest).forEach(filterFnc);callback()},filterWhere:function(properties,callback){callback=$.isFunction(callback)?callback.bind(this):$.noop;var filterFnc=this.filter.bind(this);this.getItemsWhere(properties).forEach(filterFnc);callback()},isContainerActive:function(){return this._isActive},disable:function(){this.options.enabled=false},enable:function(){this.options.enabled=true},updateTriggerOffset:function(offset){this.options.triggerOffset=offset;this.updateOffsets();this._trigger("triggeroffsetupdate",null,offsetToPx(offset))},updateScrollOffset:function(offset){this.options.scrollOffset=offset;this.updateOffsets();this._trigger("scrolloffsetupdate",null,offsetToPx(offset))},_setActiveItem:function(){var containerInActiveArea=this._distanceToFirstItemTopOffset<=0&&Math.abs(this._distanceToOffset)-this._height<0;var items=this.getItemsWhere({filtered:false});var activeItem;items.forEach(function(item){if(item.adjustedDistanceToOffset<=0){if(!activeItem){activeItem=item}else{if(activeItem.adjustedDistanceToOffset0){activeItem=items[0]}if(activeItem){this._focusItem(activeItem);if(!this._isActive){this._isActive=true;this._trigger("containeractive")}}else{this._blurAllItems();if(this._isActive){this._isActive=false;this._trigger("containerinactive")}}},_scrollToItem:function(item,opts,callback){callback=$.isFunction(callback)?callback.bind(this):$.noop;opts=$.extend(true,{scrollOffset:item.scrollOffset!==false?offsetToPx(item.scrollOffset):offsetToPx(this.options.scrollOffset),speed:this.options.speed,easing:this.options.easing},opts);var debouncedCallback=debounce(callback,100);var scrolllTop=item.el.offset().top-offsetToPx(opts.scrollOffset);$("html, body").stop(true).animate({scrollTop:scrolllTop},opts.speed,opts.easing,debouncedCallback)},applyToAllItems:function(callback,exceptions){exceptions=$.isArray(exceptions)?exceptions:[exceptions];callback=$.isFunction(callback)?callback.bind(this):$.noop;var items=this.getItems();var i=0;var length=items.length;var item;for(i=0;i=0){item.percentScrollComplete=0}else if(Math.abs(item.distanceToOffset)>=rect.height){item.percentScrollComplete=100}else{item.percentScrollComplete=Math.abs(item.distanceToOffset)/rect.height}previouslyInViewport=item.inViewport;item.inViewport=rect.bottom>0&&rect.right>0&&rect.left=0&&rect.left>=0&&rect.bottom<=wHeight&&rect.right<=wWidth;if(item.inViewport&&!previouslyInViewport){this._trigger("itementerviewport",null,item)}else if(!item.inViewport&&previouslyInViewport){this._trigger("itemexitviewport",null,item)}}this._distanceToFirstItemTopOffset=items[0].adjustedDistanceToOffset;this._distanceToOffset=this._topOffset-scrollTop-triggerOffset;var percentScrollToLastItem=0;if(this._distanceToOffset<0){percentScrollToLastItem=1-lastItem.distanceToOffset/(this._height-lastItem.height);percentScrollToLastItem=percentScrollToLastItem<1?percentScrollToLastItem:1}this._percentScrollToLastItem=percentScrollToLastItem},addItems:function(items,opts){opts=$.extend(true,{handleRepaint:true},opts);if(items instanceof $){this._prepItemsFromSelection(items)}else if(typeof items==="string"){this._prepItemsFromSelection(this.$el.find(items))}else if($.isArray(items)){this._prepItemsFromData(items)}else{this._prepItemsFromSelection(this.$el.find(this.options.contentSelector))}if(this.getItems().length<1){throw new Error("addItems found no valid items.")}if(opts.handleRepaint){this._handleRepaint()}},_handleRepaint:function(updateOffsets){updateOffsets=updateOffsets===false?false:true;if(updateOffsets){this.updateOffsets()}this._updateScrollPositions();this._setActiveItem()},_handleScroll:function(){if(this.options.enabled){this._handleRepaint(false);this._trigger("containerscroll")}},_handleResize:function(){winHeight=$window.height();if(this.options.enabled&&this.options.autoUpdateOffsets){if(offsetIsAPercentage(this.options.triggerOffset)){this.updateTriggerOffset(this.options.triggerOffset)}if(offsetIsAPercentage(this.options.scrollOffset)){this.updateScrollOffset(this.options.scrollOffset)}this._debouncedHandleRepaint();this._trigger("containerresize")}},_onSetup:function(){this.$el.addClass(pluginName)},_onContainerActive:function(){this.$el.addClass(pluginName+"Active")},_onContainerInactive:function(){this.$el.removeClass(pluginName+"Active")},_onItemFocus:function(ev,item){item.el.addClass("active");this._manageContainerClasses("scrollStoryActiveItem-",item.id);if(item.category){if(this.getPreviousItem()&&this.getPreviousItem().category!==item.category||!this.isContainerActive()){this._trigger("categoryfocus",null,item.category);if(this.getPreviousItem()){this._trigger("categoryblur",null,this.getPreviousItem().category)}}}},_onItemBlur:function(ev,item){this._previousItems.unshift(item);item.el.removeClass("active")},_onItemEnterViewport:function(ev,item){item.el.addClass("inviewport")},_onItemExitViewport:function(ev,item){item.el.removeClass("inviewport")},_onItemFilter:function(ev,item){item.el.addClass("filtered");if(this.options.autoUpdateOffsets){this._debouncedHandleRepaint()}},_onItemUnfilter:function(ev,item){item.el.removeClass("filtered");if(this.options.autoUpdateOffsets){this._debouncedHandleRepaint()}},_onCategoryFocus:function(ev,category){this._manageContainerClasses("scrollStoryActiveCategory-",category)},_onTriggerOffsetUpdate:function(ev,offset){this.$trigger.css({top:offset+"px"})},_manageContainerClasses:function(prefix,value){this.$el.removeClass(function(index,classes){return classes.split(" ").filter(function(c){return c.lastIndexOf(prefix,0)===0}).join(" ")});this.$el.addClass(prefix+value)},_prepItemsFromSelection:function($selection){var that=this;$selection.each(function(){that._addItem({},$(this))})},_prepItemsFromData:function(items){var that=this;var selector=this.options.contentSelector.replace(/\./g,"");var frag=document.createDocumentFragment();items.forEach(function(data){var $item=$('
');that._addItem(data,$item);frag.appendChild($item.get(0))});this.$el.append(frag)},_addItem:function(data,$el){var domData=$el.data();var item={index:this._items.length,el:$el,id:$el.attr("id")?$el.attr("id"):data.id?data.id:"story"+instanceCounter+"-"+this._items.length,data:$.extend({},data,domData),category:domData.category||data.category,tags:data.tags||[],scrollStory:this,active:false,filtered:false,scrollOffset:false,triggerOffset:false,inViewport:false};if(!$el.attr("id")){$el.attr("id",item.id)}$el.addClass("scrollStoryItem");this._items.push(item);this._itemsById[item.id]=item;this._trigger("itembuild",null,item);if(item.category&&this._categories.indexOf(item.category)===-1){this._categories.push(item.category)}},_trigger:function(eventType,event,data){var callback=this.options[eventType];var prop,orig;if($.isFunction(callback)){data=data||{};event=$.Event(event);event.target=this.el;event.type=eventType;orig=event.originalEvent;if(orig){for(prop in orig){if(!(prop in event)){event[prop]=orig[prop]}}}this.$el.trigger(event,data);var boundCb=this.options[eventType].bind(this);boundCb(event,data)}}};ScrollStory.prototype.debouncedUpdateOffsets=debounce(ScrollStory.prototype.updateOffsets,100);ScrollStory.prototype._debouncedHandleRepaint=debounce(ScrollStory.prototype._handleRepaint,100);$.fn[pluginName]=function(options){return this.each(function(){if(!$.data(this,"plugin_"+pluginName)){$.data(this,"plugin_"+pluginName,new ScrollStory(this,options))}})}}); -------------------------------------------------------------------------------- /demo/waypoints/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollytelling demo: Waypoints 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 46 | 47 | 48 |
49 | 56 |
57 |
58 | 62 | 63 | 72 | 73 |
74 |

Waypoints

75 |
76 |
77 |

Step 1 in the graphic. It triggers in the middle of the viewport. For this graphic, it is the same as the initial state so the reader doesn’t miss anything.

78 |

Step 2 arrives. The graphic should be locking into a fixed position right about now. We could have a whole bunch of these “fixed” steps.

79 |

Step 3 concludes our brief tour. The graphic should now go back to its original in-flow position, elegantly snapping back into place.

80 |
81 |
82 |
83 |

← Back to the blog

84 |
85 | 86 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /demo/waypoints/noframework.waypoints.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Waypoints - 4.0.1 3 | Copyright © 2011-2016 Caleb Troughton 4 | Licensed under the MIT license. 5 | https://github.com/imakewebthings/waypoints/blob/master/licenses.txt 6 | */ 7 | !function(){"use strict";function t(n){if(!n)throw new Error("No options passed to Waypoint constructor");if(!n.element)throw new Error("No element option passed to Waypoint constructor");if(!n.handler)throw new Error("No handler option passed to Waypoint constructor");this.key="waypoint-"+e,this.options=t.Adapter.extend({},t.defaults,n),this.element=this.options.element,this.adapter=new t.Adapter(this.element),this.callback=n.handler,this.axis=this.options.horizontal?"horizontal":"vertical",this.enabled=this.options.enabled,this.triggerPoint=null,this.group=t.Group.findOrCreate({name:this.options.group,axis:this.axis}),this.context=t.Context.findOrCreateByElement(this.options.context),t.offsetAliases[this.options.offset]&&(this.options.offset=t.offsetAliases[this.options.offset]),this.group.add(this),this.context.add(this),i[this.key]=this,e+=1}var e=0,i={};t.prototype.queueTrigger=function(t){this.group.queueTrigger(this,t)},t.prototype.trigger=function(t){this.enabled&&this.callback&&this.callback.apply(this,t)},t.prototype.destroy=function(){this.context.remove(this),this.group.remove(this),delete i[this.key]},t.prototype.disable=function(){return this.enabled=!1,this},t.prototype.enable=function(){return this.context.refresh(),this.enabled=!0,this},t.prototype.next=function(){return this.group.next(this)},t.prototype.previous=function(){return this.group.previous(this)},t.invokeAll=function(t){var e=[];for(var n in i)e.push(i[n]);for(var o=0,r=e.length;r>o;o++)e[o][t]()},t.destroyAll=function(){t.invokeAll("destroy")},t.disableAll=function(){t.invokeAll("disable")},t.enableAll=function(){t.Context.refreshAll();for(var e in i)i[e].enabled=!0;return this},t.refreshAll=function(){t.Context.refreshAll()},t.viewportHeight=function(){return window.innerHeight||document.documentElement.clientHeight},t.viewportWidth=function(){return document.documentElement.clientWidth},t.adapters=[],t.defaults={context:window,continuous:!0,enabled:!0,group:"default",horizontal:!1,offset:0},t.offsetAliases={"bottom-in-view":function(){return this.context.innerHeight()-this.adapter.outerHeight()},"right-in-view":function(){return this.context.innerWidth()-this.adapter.outerWidth()}},window.Waypoint=t}(),function(){"use strict";function t(t){window.setTimeout(t,1e3/60)}function e(t){this.element=t,this.Adapter=o.Adapter,this.adapter=new this.Adapter(t),this.key="waypoint-context-"+i,this.didScroll=!1,this.didResize=!1,this.oldScroll={x:this.adapter.scrollLeft(),y:this.adapter.scrollTop()},this.waypoints={vertical:{},horizontal:{}},t.waypointContextKey=this.key,n[t.waypointContextKey]=this,i+=1,o.windowContext||(o.windowContext=!0,o.windowContext=new e(window)),this.createThrottledScrollHandler(),this.createThrottledResizeHandler()}var i=0,n={},o=window.Waypoint,r=window.onload;e.prototype.add=function(t){var e=t.options.horizontal?"horizontal":"vertical";this.waypoints[e][t.key]=t,this.refresh()},e.prototype.checkEmpty=function(){var t=this.Adapter.isEmptyObject(this.waypoints.horizontal),e=this.Adapter.isEmptyObject(this.waypoints.vertical),i=this.element==this.element.window;t&&e&&!i&&(this.adapter.off(".waypoints"),delete n[this.key])},e.prototype.createThrottledResizeHandler=function(){function t(){e.handleResize(),e.didResize=!1}var e=this;this.adapter.on("resize.waypoints",function(){e.didResize||(e.didResize=!0,o.requestAnimationFrame(t))})},e.prototype.createThrottledScrollHandler=function(){function t(){e.handleScroll(),e.didScroll=!1}var e=this;this.adapter.on("scroll.waypoints",function(){(!e.didScroll||o.isTouch)&&(e.didScroll=!0,o.requestAnimationFrame(t))})},e.prototype.handleResize=function(){o.Context.refreshAll()},e.prototype.handleScroll=function(){var t={},e={horizontal:{newScroll:this.adapter.scrollLeft(),oldScroll:this.oldScroll.x,forward:"right",backward:"left"},vertical:{newScroll:this.adapter.scrollTop(),oldScroll:this.oldScroll.y,forward:"down",backward:"up"}};for(var i in e){var n=e[i],o=n.newScroll>n.oldScroll,r=o?n.forward:n.backward;for(var s in this.waypoints[i]){var l=this.waypoints[i][s];if(null!==l.triggerPoint){var a=n.oldScroll=l.triggerPoint,p=a&&h,u=!a&&!h;(p||u)&&(l.queueTrigger(r),t[l.group.id]=l.group)}}}for(var d in t)t[d].flushTriggers();this.oldScroll={x:e.horizontal.newScroll,y:e.vertical.newScroll}},e.prototype.innerHeight=function(){return this.element==this.element.window?o.viewportHeight():this.adapter.innerHeight()},e.prototype.remove=function(t){delete this.waypoints[t.axis][t.key],this.checkEmpty()},e.prototype.innerWidth=function(){return this.element==this.element.window?o.viewportWidth():this.adapter.innerWidth()},e.prototype.destroy=function(){var t=[];for(var e in this.waypoints)for(var i in this.waypoints[e])t.push(this.waypoints[e][i]);for(var n=0,o=t.length;o>n;n++)t[n].destroy()},e.prototype.refresh=function(){var t,e=this.element==this.element.window,i=e?void 0:this.adapter.offset(),n={};this.handleScroll(),t={horizontal:{contextOffset:e?0:i.left,contextScroll:e?0:this.oldScroll.x,contextDimension:this.innerWidth(),oldScroll:this.oldScroll.x,forward:"right",backward:"left",offsetProp:"left"},vertical:{contextOffset:e?0:i.top,contextScroll:e?0:this.oldScroll.y,contextDimension:this.innerHeight(),oldScroll:this.oldScroll.y,forward:"down",backward:"up",offsetProp:"top"}};for(var r in t){var s=t[r];for(var l in this.waypoints[r]){var a,h,p,u,d,f=this.waypoints[r][l],c=f.options.offset,w=f.triggerPoint,y=0,g=null==w;f.element!==f.element.window&&(y=f.adapter.offset()[s.offsetProp]),"function"==typeof c?c=c.apply(f):"string"==typeof c&&(c=parseFloat(c),f.options.offset.indexOf("%")>-1&&(c=Math.ceil(s.contextDimension*c/100))),a=s.contextScroll-s.contextOffset,f.triggerPoint=Math.floor(y+a-c),h=w=s.oldScroll,u=h&&p,d=!h&&!p,!g&&u?(f.queueTrigger(s.backward),n[f.group.id]=f.group):!g&&d?(f.queueTrigger(s.forward),n[f.group.id]=f.group):g&&s.oldScroll>=f.triggerPoint&&(f.queueTrigger(s.forward),n[f.group.id]=f.group)}}return o.requestAnimationFrame(function(){for(var t in n)n[t].flushTriggers()}),this},e.findOrCreateByElement=function(t){return e.findByElement(t)||new e(t)},e.refreshAll=function(){for(var t in n)n[t].refresh()},e.findByElement=function(t){return n[t.waypointContextKey]},window.onload=function(){r&&r(),e.refreshAll()},o.requestAnimationFrame=function(e){var i=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||t;i.call(window,e)},o.Context=e}(),function(){"use strict";function t(t,e){return t.triggerPoint-e.triggerPoint}function e(t,e){return e.triggerPoint-t.triggerPoint}function i(t){this.name=t.name,this.axis=t.axis,this.id=this.name+"-"+this.axis,this.waypoints=[],this.clearTriggerQueues(),n[this.axis][this.name]=this}var n={vertical:{},horizontal:{}},o=window.Waypoint;i.prototype.add=function(t){this.waypoints.push(t)},i.prototype.clearTriggerQueues=function(){this.triggerQueues={up:[],down:[],left:[],right:[]}},i.prototype.flushTriggers=function(){for(var i in this.triggerQueues){var n=this.triggerQueues[i],o="up"===i||"left"===i;n.sort(o?e:t);for(var r=0,s=n.length;s>r;r+=1){var l=n[r];(l.options.continuous||r===n.length-1)&&l.trigger([i])}}this.clearTriggerQueues()},i.prototype.next=function(e){this.waypoints.sort(t);var i=o.Adapter.inArray(e,this.waypoints),n=i===this.waypoints.length-1;return n?null:this.waypoints[i+1]},i.prototype.previous=function(e){this.waypoints.sort(t);var i=o.Adapter.inArray(e,this.waypoints);return i?this.waypoints[i-1]:null},i.prototype.queueTrigger=function(t,e){this.triggerQueues[e].push(t)},i.prototype.remove=function(t){var e=o.Adapter.inArray(t,this.waypoints);e>-1&&this.waypoints.splice(e,1)},i.prototype.first=function(){return this.waypoints[0]},i.prototype.last=function(){return this.waypoints[this.waypoints.length-1]},i.findOrCreate=function(t){return n[t.axis][t.name]||new i(t)},o.Group=i}(),function(){"use strict";function t(t){return t===t.window}function e(e){return t(e)?e:e.defaultView}function i(t){this.element=t,this.handlers={}}var n=window.Waypoint;i.prototype.innerHeight=function(){var e=t(this.element);return e?this.element.innerHeight:this.element.clientHeight},i.prototype.innerWidth=function(){var e=t(this.element);return e?this.element.innerWidth:this.element.clientWidth},i.prototype.off=function(t,e){function i(t,e,i){for(var n=0,o=e.length-1;o>n;n++){var r=e[n];i&&i!==r||t.removeEventListener(r)}}var n=t.split("."),o=n[0],r=n[1],s=this.element;if(r&&this.handlers[r]&&o)i(s,this.handlers[r][o],e),this.handlers[r][o]=[];else if(o)for(var l in this.handlers)i(s,this.handlers[l][o]||[],e),this.handlers[l][o]=[];else if(r&&this.handlers[r]){for(var a in this.handlers[r])i(s,this.handlers[r][a],e);this.handlers[r]={}}},i.prototype.offset=function(){if(!this.element.ownerDocument)return null;var t=this.element.ownerDocument.documentElement,i=e(this.element.ownerDocument),n={top:0,left:0};return this.element.getBoundingClientRect&&(n=this.element.getBoundingClientRect()),{top:n.top+i.pageYOffset-t.clientTop,left:n.left+i.pageXOffset-t.clientLeft}},i.prototype.on=function(t,e){var i=t.split("."),n=i[0],o=i[1]||"__default",r=this.handlers[o]=this.handlers[o]||{},s=r[n]=r[n]||[];s.push(e),this.element.addEventListener(n,e)},i.prototype.outerHeight=function(e){var i,n=this.innerHeight();return e&&!t(this.element)&&(i=window.getComputedStyle(this.element),n+=parseInt(i.marginTop,10),n+=parseInt(i.marginBottom,10)),n},i.prototype.outerWidth=function(e){var i,n=this.innerWidth();return e&&!t(this.element)&&(i=window.getComputedStyle(this.element),n+=parseInt(i.marginLeft,10),n+=parseInt(i.marginRight,10)),n},i.prototype.scrollLeft=function(){var t=e(this.element);return t?t.pageXOffset:this.element.scrollLeft},i.prototype.scrollTop=function(){var t=e(this.element);return t?t.pageYOffset:this.element.scrollTop},i.extend=function(){function t(t,e){if("object"==typeof t&&"object"==typeof e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}for(var e=Array.prototype.slice.call(arguments),i=1,n=e.length;n>i;i++)t(e[0],e[i]);return e[0]},i.inArray=function(t,e,i){return null==e?-1:e.indexOf(t,i)},i.isEmptyObject=function(t){for(var e in t)return!1;return!0},n.adapters.push({name:"noframework",Adapter:i}),n.Adapter=i}(); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | How to implement scrollytelling with six different libraries 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 49 | 50 | 51 | 52 |
53 | 62 |
63 |
64 |
65 |

How to implement scrollytelling with six different libraries

66 |

72 | Scrollytelling is the best (or the worst depending on your 73 | point of view). We love it here at 74 | The Pudding. But creating a scroll-driven story is hardly a standardized practice, and there are many libraries 75 | out there that can help make it happen. 76 | Check out my 77 | follow-up post, where we look at mobile solutions. 78 |

79 |

80 | “Scrollytelling” is when content (e.g., a graphic) is revealed or changed as the user scrolls. It does not alter scroll behavior, 81 | but simply monitors it. As Martha Stewart would say, “It’s a good thing.” Do not confuse this will scrolljacking, which 82 | manipulates the browser’s scroll mechanics with JavaScript, which is generally considered bad practice. Here are a few 83 | examples of the good stuff in action: 84 |

85 | 99 |

100 | In this post, I look at how to tackle a simple scroll-driven chart using six different libraries and share 101 | my thoughts on each implementation. It is inspired by Lisa Charlotte Rost’s 102 | great post comparing charting libraries. 103 |

104 |

105 | I'm not going to get into the different types of scrollytelling, or debate the serious moral and ethical implications of 106 | the practice. I defer to 107 | these 108 | posts for scrollytelling primers. 109 |

110 |

We want to make something like this:

111 | 112 |

113 | There are two goals here: 114 |
1. Use the "scroll-to-trigger" pattern where the trigger element (in our case a block of text) tells the chart to update 115 | to a new state. 116 |
2. We want the chart to stay fixed while the text moves and have it snap back into place as it enters and exits, because 117 | often the scrollytelling portion is not the entire story. 118 |

119 | 120 |

121 | Disclaimer: 122 | this post does not cover responsive solutions (though you should do it, and most libraries make it pretty painless). 123 | If you have the audacity to resize your browser, refresh the page. For mobile and responsive solutions, check out my 124 | follow-up post, 125 | 126 |

127 |

128 | Update (November 2017): I created my own scrollytelling library, 129 | scrollama.js. The goal of this library is to provide a simple interface for creating scroll-driven interactives. 130 | Scrollama is focused on perfomance by using IntersectionObserver in favor of scroll events. 131 | Check it out. 132 |

133 |
134 | 135 | 136 |
137 |
138 |

#1 - 139 | Waypoints 140 |

141 | 142 |

143 | Waypoints is a very solid choice. It has been around for a while, and supports both vanilla JS and jQuery integration. It 144 | has an easy to understand API, and good documentation. It has a lot of add-ons, like a debugger extension to reduce 145 | some head-scratching, inview detection, and sticky elements. Below is the core code used to implement this library. 146 | See the demo for the full code with comments. 147 |

148 | 149 |

150 | Demo → 151 |

152 |
153 | 154 |
155 |

156 | ...
157 | 
158 | var waypoints = triggerEls.map(function(el) {
159 | 	var step = +el.getAttribute('data-step')
160 | 
161 | 	return new Waypoint({
162 | 		element: el,
163 | 		handler: function(direction) {
164 | 			var nextStep = direction === 'down' ? step : Math.max(0, step - 1)
165 | 			graphic.update(nextStep)
166 | 		},
167 | 		offset: '50%',
168 | 	})
169 | })
170 | 
171 | var enterWaypoint = new Waypoint({
172 | 	element: graphicEl,
173 | 	handler: function(direction) {
174 | 		var fixed = direction === 'down'
175 | 		var bottom = false
176 | 		toggle(fixed, bottom)
177 | 	},
178 | })
179 | 
180 | var exitWaypoint = new Waypoint({
181 | 	element: graphicEl,
182 | 	handler: function(direction) {
183 | 		var fixed = direction === 'up'
184 | 		var bottom = !fixed
185 | 		toggle(fixed, bottom)
186 | 	},
187 | 	offset: 'bottom-in-view',
188 | })
189 | 
190 | ...
191 | 				
192 |
193 | 194 |
195 | 196 |
197 |
198 |

#2 - 199 | ScrollStory 200 |

201 |

202 | ScrollStory is a jQuery-based library used for some projects at The New York Times. It has a super clear API and supports 203 | tons of options. The two big drawbacks that I found were that it depends on jQuery (which is not a problem if you use 204 | it by default) and it requires some additional code to get the fixed graphic part working properly. If those two things 205 | are not important to you, I recommend exploring this one since it is setup to handle a variety of use cases. 206 |

207 |

208 | Demo → 209 |

210 |
211 |
212 |

213 | ...
214 | 
215 | var handleItemFocus = function(event, item) {
216 | 	var step = item.data.step
217 | 	graphic.update(step)
218 | }	
219 | 
220 | var handleContainerScroll = function(event) {
221 | 	var bottom = false
222 | 	var fixed = false
223 | 
224 | 	var bb = $graphicEl[0].getBoundingClientRect()
225 | 	var bottomFromTop = bb.bottom - viewportHeight
226 | 
227 | 	if (bb.top < 0 && bottomFromTop > 0) {
228 | 		bottom = false
229 | 		fixed = true
230 | 	} else if (bb.top < 0 && bottomFromTop < 0) {
231 | 		bottom = true
232 | 		fixed = false
233 | 	}
234 | 
235 | 	toggle(fixed, bottom)
236 | }
237 | 
238 | $graphicEl.scrollStory({
239 | 	contentSelector: '.trigger',
240 | 	triggerOffset: halfViewportHeight,
241 | 	itemfocus: handleItemFocus,
242 | 	containerscroll: handleContainerScroll,
243 | })
244 | 
245 | ...
246 | 				
247 |
248 | 249 |
250 | 251 |
252 |
253 |

#3 - 254 | ScrollMagic 255 |

256 |

257 | ScrollMagic is based on Superscrollorama, a previously popular library. Like Waypoints, it is quite robust, well-documented, 258 | and totally customizable. It has a great add-on for debugging. It also has no dependencies, which is nice. I do sometimes 259 | notice that the scroll events get a bit janky and won’t fire immediately, which is a built-in function the library has 260 | to deal with the scroll events. 261 |

262 |

263 | Demo → 264 |

265 |
266 |
267 |

268 | ...
269 | 
270 | var controller = new ScrollMagic.Controller()
271 | 
272 | var scenes = triggerEls.map(function(el) {
273 | 	var step = +el.getAttribute('data-step')
274 | 
275 | 	var scene = new ScrollMagic.Scene({
276 | 		triggerElement: el,
277 | 		triggerHook: 'onCenter',
278 | 	})
279 | 
280 | 	scene
281 | 		.on('enter', function(event) {
282 | 			graphic.update(step)
283 | 		})
284 | 		.on('leave', function(event) {
285 | 			var nextStep = Math.max(0, step - 1)
286 | 			graphic.update(nextStep)
287 | 		})
288 | 	
289 | 	scene.addTo(controller)
290 | })
291 | 
292 | var enterExitScene = new ScrollMagic.Scene({
293 | 	triggerElement: graphicEl,
294 | 	triggerHook: '0',
295 | 	duration: graphicEl.offsetHeight - viewportHeight,
296 | })
297 | 
298 | enterExitScene
299 | 	.on('enter', function(event) {
300 | 		var fixed = true
301 | 		var bottom = false
302 | 		toggle(fixed, bottom)
303 | 	})
304 | 	.on('leave', function(event) {
305 | 		var fixed = false
306 | 		var bottom = event.scrollDirection === 'FORWARD'
307 | 		toggle(fixed, bottom)
308 | 	})
309 | 
310 | enterExitScene.addTo(controller)
311 | 
312 | ...
313 | 				
314 |
315 | 316 |
317 | 318 |
319 |
320 |

#4 - 321 | graph-scroll.js 322 |

323 |

324 | graph-scroll.js is a d3 plugin, so only consider it if you are familiar with and using d3. It is really lightweight, and 325 | it was created by 326 | Adam Pearce who knows a thing or two about scroll-driven graphics. It is very singularly focused though, so it will 327 | likely require you to customize the library a bit to get what you want. That being said, it is only the library that 328 | specifically implements the transition to and from a fixed position graphic, which is great. 329 |

330 |

331 | Demo → 332 |

333 |
334 |
335 |

336 | ...
337 | 
338 | d3.graphScroll()
339 | 	.container(graphicEl)
340 | 	.graph(graphicVisEl)
341 | 	.sections(triggerEls)
342 | 	.offset(halfViewportHeight)
343 | 	.on('active', function(i) {
344 | 		graphic.update(i)
345 | 	})
346 | 
347 | ...
348 | 				
349 |
350 | 351 |
352 | 353 |
354 |
355 |

#5 - 356 | in-view.js 357 |

358 |

359 | in-view.js is a great library in general, and I tried to adapt it to my scrollytelling needs. While one of its features is 360 | performance optimization, that comes with the downside of only having a single global offset for triggering an enter/exit. 361 | I had to use a 362 | modified version that allows for creating multiple instances to make it work for this scenario. Also, the offsets 363 | are a bit confusing to use in this context. In short, this library is amazingly simple to use for just triggering, but 364 | definitely not designed for customized scrollytelling. 365 |

366 |

367 | Demo → 368 |

369 |
370 |
371 |

372 | ...
373 | 
374 | var inviewTrigger = inView()
375 | 
376 | inviewTrigger.offset({
377 | 	top: 0,
378 | 	right: 0,
379 | 	bottom: halfViewportHeight,
380 | 	left: 0,
381 | })
382 | 
383 | inviewTrigger('.trigger')
384 | 	.on('enter', function(el) {
385 | 		var step = +el.getAttribute('data-step')
386 | 		graphic.update(step)
387 | 	})
388 | 
389 | var inviewTop = inView()
390 | 
391 | inviewTop.offset({
392 | 	top: -999999,
393 | 	right: 0,
394 | 	bottom: window.innerHeight,
395 | 	left: 0,
396 | })
397 | 
398 | inviewTop('.graphic')
399 | 	.on('enter', function(el) {
400 | 		var fixed = true
401 | 		var bottom = false
402 | 		toggle(fixed, bottom)
403 | 	})
404 | 	.on('exit', function(el) {
405 | 		var fixed = false
406 | 		var bottom = false
407 | 		toggle(fixed, bottom)
408 | 	})
409 | 
410 | var inviewBottom = inView()
411 | 
412 | inviewBottom.offset({
413 | 	top: -999999,
414 | 	right: 0,
415 | 	bottom: graphicEl.offsetHeight,
416 | 	left: 0,
417 | })
418 | 
419 | inviewBottom('.graphic')
420 | 	.on('enter', function(el) {
421 | 		var fixed = false
422 | 		var bottom = true
423 | 		toggle(fixed, bottom)
424 | 	})
425 | 	.on('exit', function(el) {
426 | 		var fixed = true
427 | 		var bottom = false
428 | 		toggle(fixed, bottom)
429 | 	})
430 | 
431 | ...
432 | 				
433 |
434 | 435 |
436 | 437 |
438 |
439 |

#6 - 440 | Roll your own 441 |

442 | 443 |

444 | When in doubt, roll your own. There will be a bit more coding involved (and some fun math!), but you get to familiarize yourself 445 | with the core concepts of how scroll-driven libraries are created. It is all custom, so you can have it do whatever 446 | you want. You will want to consider performance optimizations like throttling and such. 447 |

448 |

449 | Demo → 450 |

451 |
452 |
453 |

454 | ...
455 | 
456 | var bbTop = 0	
457 | var bbBottom = 0
458 | var height = graphicEl.getBoundingClientRect().height
459 | var prevStep = 0
460 | var currentStep = 0
461 | var numSteps = triggerEls.length
462 | 
463 | var checkTrigger = function() {
464 | 	if (bbTop < viewportHeight && bbBottom > 0) {
465 | 		var progress = Math.abs(bbTop - halfViewportHeight) / height * numSteps
466 | 		var step = Math.floor(progress)
467 | 		currentStep = Math.min(Math.max(step, 0), numSteps - 1)
468 | 	}
469 | }
470 | 
471 | var checkEnterExit = function() {
472 | 	var bottomFromTop = bbBottom - viewportHeight
473 | 	var bottom
474 | 	var fixed
475 | 
476 | 	if (bbTop < 0 && bottomFromTop > 0) {
477 | 		bottom = false
478 | 		fixed = true
479 | 	} else if (bbTop < 0 && bottomFromTop < 0) {
480 | 		bottom = true
481 | 		fixed = false
482 | 	} else {
483 | 		bottom = false
484 | 		fixed = false
485 | 	}
486 | 	
487 | 	toggle(fixed, bottom)
488 | }
489 | 
490 | var handleScroll = function() {
491 | 	var bb = graphicEl.getBoundingClientRect()
492 | 	bbTop = bb.top
493 | 	bbBottom = bb.bottom
494 | 	
495 | 	checkTrigger()
496 | 	checkEnterExit()
497 | }
498 | 
499 | window.addEventListener('scroll', throttle(handleScroll, 50))
500 | 
501 | var render = function() {
502 | 	if (currentStep !== prevStep) {
503 | 		prevStep = currentStep
504 | 		graphic.update(currentStep)
505 | 	}
506 | 	
507 | 	window.requestAnimationFrame(render)
508 | }
509 | render()
510 | 
511 | ...
512 | 				
513 |
514 | 515 |
516 | 517 | 518 |
519 |

So what to chose? The best library all depends on your use case, but here are some recommendations based on my findings: 520 |

521 | For highly customized stories you will want ScrollMagic or Waypoints. 522 |

523 |

524 | For the beginner you might want to check out ScrollStory, especially if you lean on jQuery. 525 |

526 |

527 | For the d3 lover you should explore graph-scroll.js, but be ready to accept the defaults or be ready to tinker. 528 |

529 |

530 | The full code is available on 531 | github. If there are any good libraries out there I missed, give me a shout 532 | @codenberg. 533 |

534 |
535 |
536 | 537 | 538 | 566 | 567 | 568 | 569 | -------------------------------------------------------------------------------- /js/prism.js: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism-okaidia&languages=clike+javascript */ 2 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(\w+)\b/i,t=0,n=_self.Prism={util:{encode:function(e){return e instanceof a?new a(e.type,n.util.encode(e.content),e.alias):"Array"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(v instanceof a)){u.lastIndex=0;var b=u.exec(v),k=1;if(!b&&h&&m!=r.length-1){if(u.lastIndex=y,b=u.exec(e),!b)break;for(var w=b.index+(c?b[1].length:0),_=b.index+b[0].length,A=m,P=y,j=r.length;j>A&&_>P;++A)P+=r[A].length,w>=P&&(++m,y=P);if(r[m]instanceof a||r[A-1].greedy)continue;k=A-m,v=e.slice(y,P),b.index-=y}if(b){c&&(f=b[1].length);var w=b.index+f,b=b[0].slice(f),_=w+b.length,x=v.slice(0,w),O=v.slice(_),S=[m,k];x&&S.push(x);var N=new a(l,g?n.tokenize(b,g):b,d,b,h);S.push(N),O&&S.push(O),Array.prototype.splice.apply(r,S)}}}}}return r},hooks:{all:{},add:function(e,t){var a=n.hooks.all;a[e]=a[e]||[],a[e].push(t)},run:function(e,t){var a=n.hooks.all[e];if(a&&a.length)for(var r,i=0;r=a[i++];)r(t)}}},a=n.Token=function(e,t,n,a,r){this.type=e,this.content=t,this.alias=n,this.length=0|(a||"").length,this.greedy=!!r};if(a.stringify=function(e,t,r){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return a.stringify(n,t,e)}).join("");var i={type:e.type,content:a.stringify(e.content,t,r),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:r};if("comment"==i.type&&(i.attributes.spellcheck="true"),e.alias){var l="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,l)}n.hooks.run("wrap",i);var o=Object.keys(i.attributes).map(function(e){return e+'="'+(i.attributes[e]||"").replace(/"/g,""")+'"'}).join(" ");return"<"+i.tag+' class="'+i.classes.join(" ")+'"'+(o?" "+o:"")+">"+i.content+""},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var t=JSON.parse(e.data),a=t.language,r=t.code,i=t.immediateClose;_self.postMessage(n.highlight(r,n.languages[a],a)),i&&_self.close()},!1),_self.Prism):_self.Prism;var r=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return r&&(n.filename=r.src,document.addEventListener&&!r.hasAttribute("data-manual")&&("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n.highlightAll):window.setTimeout(n.highlightAll,16):document.addEventListener("DOMContentLoaded",n.highlightAll))),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 3 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:{pattern:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; 4 | Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*\*?|\/|~|\^|%|\.{3}/}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0,greedy:!0}}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\\\|\\?[^\\])*?`/,greedy:!0,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/()[\w\W]*?(?=<\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:"language-javascript"}}),Prism.languages.js=Prism.languages.javascript; 5 | --------------------------------------------------------------------------------