├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .jshintrc ├── .stylelintrc ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── css │ ├── _abstracts │ │ ├── mixins.scss │ │ └── variables.scss │ ├── _base │ │ ├── custom-reset.scss │ │ └── global.scss │ ├── _critical │ │ └── init.scss │ ├── _fonts.css │ ├── _global-components │ │ ├── footer.scss │ │ ├── header.scss │ │ └── mini-cart.scss │ ├── _pages │ │ ├── 404.scss │ │ ├── about.scss │ │ └── home.scss │ └── main.scss ├── fonts │ └── .gitkeep ├── images │ ├── .gitkeep │ ├── meta │ │ ├── facebook-share.png │ │ └── twitter-share.png │ └── temp │ │ ├── shift-image-2.jpg │ │ ├── shift-image-3.jpg │ │ └── shift-image.jpg ├── js │ ├── _classes │ │ └── ScrollBasedAnims.js │ ├── _global │ │ ├── _renderer.js │ │ ├── anims.js │ │ ├── helpers.js │ │ └── storage.js │ ├── _initialize.js │ ├── _transitions │ │ └── basicFade.js │ ├── _worker │ │ ├── _init.js │ │ └── workers │ │ │ └── image-load.js │ ├── about │ │ ├── _renderer.js │ │ └── anims.js │ ├── home │ │ ├── _renderer.js │ │ └── anims.js │ ├── main.js │ └── routing.js └── templates │ ├── 404.php │ ├── about.php │ ├── footer.php │ ├── header.php │ ├── index.php │ └── parts │ ├── footer-content.php │ ├── global-scope.php │ ├── site │ ├── site-meta.php │ └── site-prefetch.php │ └── svgs │ ├── social-facebook.php │ ├── social-instagram.php │ └── social-twitter.php ├── web └── site │ ├── .htaccess │ ├── index.php │ └── robots.txt └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = tab 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.php] 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 2017, 5 | "sourceType": "module" 6 | }, 7 | "globals": { 8 | "angular": false, 9 | "module": false, 10 | "inject": false, 11 | "document": false 12 | }, 13 | "env": { 14 | "browser": true, 15 | "amd": true, 16 | "es6": true 17 | }, 18 | "rules": { 19 | "no-unused-vars": "off", 20 | "no-case-declarations": "off", 21 | "no-console": "off" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | 3 | # Application 4 | web/config.php 5 | !web/config.*.php 6 | web/cache/* 7 | web/app/views/* 8 | web/site/* 9 | !web/site/server 10 | !web/site/index.php 11 | !web/site/.htaccess 12 | !web/site/robots.txt 13 | 14 | # Dotenv 15 | !.env 16 | .env.* 17 | !.env.example 18 | 19 | # Vendor (e.g. Composer) 20 | web/vendor/* 21 | 22 | # Compiled 23 | compiled 24 | 25 | # Node Package Manager 26 | node_modules 27 | 28 | # Vagrant 29 | bin 30 | .vagrant 31 | 32 | .idea/ 33 | composer.phar 34 | composer.lock 35 | package.lock 36 | npm-debug.log 37 | 38 | # Stray Files 39 | .DS_Store -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | 4 | } 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Localhost Setup 2 | 1. From the project root in terminal, run NPM install 3 | 2. To start the project now, setup your watch: `npm run watch-dev` or `npm run watch` to minimize for prod builds 4 | 3. In the terminal, navigate to `web/site` and then type `php -S localhost:8000` to start your virtual server, 5 | or point MAMP to the `web/site` directory 6 | 7 | ## Core performance principles 8 | 9 | 1. Avoid network requests during animations whenever possible. Load images as blobs with web workers, prefetch priority pages on doc ready when the thread is idle, etc 10 | 2. Never autoplay videos. Use the scroll anims library to play/pause/restart them when in/out of view. See `playPauseVideos()` in the scroll based anims class. 11 | 3. Taking scroll measurements should be done only once (and on resize). Notice the order of image loading & function calls in `/js/_global/_renderer` `globalEvents` function. 12 | There is a reason the preload-critical image workers are fired, and when those + the lead-in transition (either initial or PJAX transition) is complete, we take the scroll 13 | measurements. It's a balance of waiting for unavoidable network requests to be finished, for animations to be finished, etc.. and then instantiating the scroll based anims. 14 | The actual class call and all calculations are very fast, but can cause dropped frames if you fire it too soon during your transitions. 15 | 4. Take all animation measurements once (lots of BCR calls) when the class is instantiated, never on the fly in the animation loop. Avoid anything else that might cause a document reflow 16 | such as adding/removing class names, querying an element's offsetHeight, etc. [What causes layout/reflow](https://gist.github.com/paulirish/5d52fb081b3570c81e3a). 17 | 5. Hyper optimize anything that fires in RAF. Use for loops with cached length values instead of .forEach (~8x faster), never check or set anything that doesn't need to be 18 | checked/set (use flags). 19 | 6. All media should have it's an intrinsic height set, except for elements whose height do not affect document flow. With the goal of only taking our animation measurements once, 20 | plus not waiting for all images to do so, it's critical that all images somehow have a height in the CSS. Consider using an `.ar-wrapper` container to set the aspect ratio with padding bottom 21 | and positioning the media absolutely within. 22 | 23 | ## PJAX specifics 24 | 25 | 1. Pacing is everything. It's not about what is actually happening, it's about what the user perceives to be happening. With highway we have total freedom during transitions & page renderers to pace things as needed. 26 | 2. Store important elements, timeline references, measurements etc in globalStorage objects (see `/js/_global/storage.js`). When moving between pages, you will often need to reverse a global timeline, 27 | update the namespace, check measurement values, etc. 28 | 3. Know when certain aspects of your transitions are complete by modifying the above mentioned globalStorage values. This helps greatly with the pacing. Notice the setInterval 29 | in the critical images callback in the global renderer. 30 | 31 | ### Advanced uses 32 | 1. By adding contextual transitions when instantiating highway in `/js/routing.js`, you can place `data-transition="transitionName"` to an anchor tag to use custom transitions on the fly. 33 | The contextual transitions can also be used when calling a redirect programmatically, ie, `H.redirect(fullHref, transitionName)` 34 | 2. Often times contextual transitions are used in conjunction with a strategy called "overlapping transitions", whereby you strategically call `from.remove()` in the transition when you know 35 | the next page's elements are in position and loaded. In this way, you can create transitions that make certain elements appear to persist across views. For example, from the footer of one page 36 | to the header of the next might have identical elements. By using a contextual transition to take you from the footer to the next page hero, and animating/timing your transition correctly, 37 | you can make those identical elements appear to have never changed when in reality they were replaced by the new elements. 38 | 39 | ## Scroll based anims class specifics 40 | 41 | #### Shout out to [Jesper Landberg](https://twitter.com/Jesper_Landberg) whose original scroll class logic guided and inspired this development 42 | 43 | ### (See comments in file) 44 | 1. The scroll based animations file can be set with a flag to use either virtual scroll or the native window scrollY value. Regardless, mobile and Firefox use the native scrollY value. 45 | 2. There are more advanced usages, but basically the pattern will always be the same. Create a data collection function that gets bounding data, call it in `getCache()`, then create an 46 | animation function that animates against that data and call it in the `run()` function. 47 | 3. If you want to use native scroll, you'll have to set some body styles accordingly. See `/css/_critical/init.scss`. the `data-smooth` attributes on the page sections won't do anything if 48 | `this.isVS` is set to false. 49 | 50 | ### Advanced uses 51 | With the data available regardless of device, there's really nothing you can't do with the `ScrollBasedAnims` class. 52 | 1. As you'll see with the hero and footer element animations, I like to use what I refer to as a measure elements (`.measure-el`). When nested inside of a section we want to animate 53 | and given a certain height & top positioning, these can be used to pan through more complicated timelines (what I sometimes refer to as scenes). The scene might be more complicated and the timeline created 54 | within the class instead of being extracted from the markup, but the core strategy remains the same. You collect the measure element bounding data so you can track it's progress, and you pan through 55 | a timeline based on that. Typically this involves animations that take place over several viewport heights. 56 | 2. Fixed positioning isn't as straight forward when using virtual scroll, but overall virtual scroll produces a better fixed position experience because we're animating the fixed element on every frame 57 | and never swapping css position value from fixed to unfixed which can cause jumps. Basically the strategy is to set the css position to fixed, and at the bottom of the section that it remains fixed through, you 58 | place a measure element (remember `.measure-el`) and as that element enters the viewport, transform the fixed position element up and out of view. See [this page](https://plenaire.co/products/remedy/), the `.measure-el` 59 | at the bottom of the `.right-content` in the first part of that page. I actually had to nudge that with JS because the left side collapses, but the point is as the measure element enters, the fixed element will scroll up 60 | out of view and back down when scrolling up. 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zero-shopify-prismic-php", 3 | "author": "Zero Studios", 4 | "license": "UNLICENSED", 5 | "private": true, 6 | "version": "0.9.9", 7 | "description": "A PHP based solution for centralizing Shopify and Prismic.", 8 | "devDependencies": { 9 | "@babel/core": "^7.8.6", 10 | "@babel/plugin-transform-runtime": "^7.8.3", 11 | "@babel/preset-env": "^7.8.6", 12 | "@babel/runtime": "^7.8.4", 13 | "ajv": "^6.12.0", 14 | "assets-loader": "^0.5", 15 | "babel-cli": "^6.26.0", 16 | "babel-loader": "^8.0.6", 17 | "babel-preset-env": "^1.7.0", 18 | "babel-preset-es2015": "^6.24.1", 19 | "copy-webpack-plugin": "^4.5.1", 20 | "core-js": "^3.6.4", 21 | "core-js-compat": "^3.6.4", 22 | "css-loader": "^0.26.4", 23 | "eslint": "^5.16.0", 24 | "eslint-loader": "^2.2.1", 25 | "file-loader": "^0.10.1", 26 | "friendly-errors-webpack-plugin": "^1.7.0", 27 | "generate-json-webpack-plugin": "^0.3.1", 28 | "glob": "^7.1.6", 29 | "mini-css-extract-plugin": "^0.4.0", 30 | "node-sass": "^4.13.1", 31 | "optimize-css-assets-webpack-plugin": "^4.0.2", 32 | "postcss-import": "^11.1.0", 33 | "postcss-loader": "^2.1.5", 34 | "postcss-scss": "^1.0.5", 35 | "sass-loader": "^7.3.1", 36 | "style-loader": "^0.13.2", 37 | "stylelint": "^8.0.0", 38 | "stylelint-scss": "^3.14.2", 39 | "stylelint-webpack-plugin": "^0.10.5", 40 | "uglifyjs-webpack-plugin": "^1.2.5", 41 | "url-loader": "^1.1.2", 42 | "webpack": "^4.41.6", 43 | "webpack-cli": "^3.3.11", 44 | "webpack-glob-entry": "^2.1.1", 45 | "webpack-google-cloud-storage-plugin": "^0.9.0" 46 | }, 47 | "dependencies": { 48 | "@dogstudio/highway": "^2.1.3", 49 | "flatpickr": "^2.4.2", 50 | "flickity": "^2.2.1", 51 | "form-serialize": "^0.7.2", 52 | "gsap": "^3.2.4", 53 | "intersection-observer": "^0.7.0", 54 | "lodash.throttle": "^4.1.1", 55 | "moment": "^2.24.0", 56 | "quicklink": "^1.0.1", 57 | "virtual-scroll": "^1.5.2" 58 | }, 59 | "optionalDependencies": { 60 | "ismobilejs": "^0.4.1", 61 | "js-cookie": "^2.2.0" 62 | }, 63 | "scripts": { 64 | "build": "webpack", 65 | "watch": "webpack --watch", 66 | "watch-dev": "webpack --watch --mode=development" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | syntax: "postcss-scss", 3 | plugins: { 4 | "postcss-import": {}, 5 | "autoprefixer": { 6 | browsers: ["last 2 versions","ie 11"] 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /src/css/_abstracts/mixins.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------ */ 2 | /* MIXINS 3 | 4 | - Global mixins used across the site 5 | /* ------------------------------------------------------------------ */ 6 | 7 | /* ----- MEDIA QUERIES ----- */ 8 | @mixin bp-320 { 9 | @media screen and (max-width: 320px) { 10 | @content; 11 | } 12 | } 13 | @mixin bp-370 { 14 | @media screen and (max-width: 370px) { 15 | @content; 16 | } 17 | } 18 | @mixin bp-480 { 19 | @media screen and (max-width: 480px) { 20 | @content; 21 | } 22 | } 23 | @mixin bp-767 { 24 | @media screen and (max-width: 767px) { 25 | @content; 26 | } 27 | } 28 | @mixin bp-959 { 29 | @media screen and (max-width: 959px) { 30 | @content; 31 | } 32 | } 33 | @mixin bp-1024 { 34 | @media screen and (max-width: 1024px) { 35 | @content; 36 | } 37 | } 38 | @mixin bp-1200 { 39 | @media screen and (max-width: 1200px) { 40 | @content; 41 | } 42 | } 43 | @mixin bp-1440 { 44 | @media screen and (max-width: 1440px) { 45 | @content; 46 | } 47 | } 48 | @mixin bp-1680 { 49 | @media screen and (max-width: 1680px) { 50 | @content; 51 | } 52 | } 53 | @mixin bp-1920 { 54 | @media screen and (max-width: 1920px) { 55 | @content; 56 | } 57 | } 58 | 59 | /* ----- SIZE ----- */ 60 | @function get-vw($target) { 61 | $vw-context: (1440 * 0.01) * 1px; 62 | @return ($target / $vw-context) * 1vw; 63 | } 64 | -------------------------------------------------------------------------------- /src/css/_abstracts/variables.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------ */ 2 | /* VARIABLES 3 | 4 | - Global variables used across the site 5 | - Colors, breakpoints, easings, global sizes etc 6 | /* ------------------------------------------------------------------ */ 7 | 8 | /* ----- COLORS ----- */ 9 | $white: #FFFFFF; 10 | $black: #2c2c2c; 11 | 12 | /* ----- FONTS ----- */ 13 | //$plaidXL: "Plaid-XL", sans-serif; 14 | //$moderatMd: "Moderat-Medium", sans-serif; 15 | //$moderatRg: "Moderat-Regular", sans-serif; 16 | 17 | /* ----- DEFAULT IMAGE PATH ----- */ 18 | $imgpath: "https://project.cdn.prismic.io/project/"; 19 | 20 | /* ----- EASINGS ----- */ 21 | 22 | /* Default Equations */ 23 | $linear: cubic-bezier(0.25, 0.25, 0.75, 0.75); 24 | $ease: cubic-bezier(0.25, 0.1, 0.25, 1); 25 | $ease-in: cubic-bezier(0.42, 0, 1, 1); 26 | $ease-out: cubic-bezier(0, 0, 0.58, 1); 27 | $ease-in-out: cubic-bezier(0.42, 0, 0.58, 1); 28 | 29 | /* Penner Equations (approximated) originally created by @robpenner */ 30 | $ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53); 31 | $ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19); 32 | $ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22); 33 | $ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06); 34 | $ease-in-sine: cubic-bezier(0.47, 0, 0.745, 0.715); 35 | $ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035); 36 | $ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335); 37 | $ease-in-back: cubic-bezier(0.6, -0.28, 0.735, 0.045); 38 | 39 | $ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94); 40 | $ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1); 41 | $ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1); 42 | $ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1); 43 | $ease-out-sine: cubic-bezier(0.39, 0.575, 0.565, 1); 44 | $ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1); 45 | $ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1); 46 | $ease-out-back: cubic-bezier(0.175, 0.885, 0.32, 1.275); 47 | 48 | $ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955); 49 | $ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1); 50 | $ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1); 51 | $ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1); 52 | $ease-in-out-sine: cubic-bezier(0.445, 0.05, 0.55, 0.95); 53 | $ease-in-out-expo: cubic-bezier(1, 0, 0, 1); 54 | $ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86); 55 | $ease-in-out-back: cubic-bezier(0.68, -0.55, 0.265, 1.55); 56 | -------------------------------------------------------------------------------- /src/css/_base/custom-reset.scss: -------------------------------------------------------------------------------- 1 | /* Main Scrollbar */ 2 | ::-webkit-scrollbar { 3 | width: 0; 4 | } 5 | 6 | ::-webkit-scrollbar-button{ 7 | 8 | } 9 | 10 | ::-webkit-scrollbar-track{ 11 | 12 | } 13 | 14 | ::-webkit-scrollbar-track-piece{ 15 | 16 | } 17 | 18 | ::-webkit-scrollbar-thumb{ 19 | 20 | } 21 | 22 | ::-webkit-scrollbar-corner{ 23 | 24 | } 25 | 26 | ::-webkit-resizer{ 27 | 28 | } 29 | 30 | h1,h2,h3,h4,h5,h5 { 31 | margin: 0; 32 | } 33 | ul { 34 | margin: 0; 35 | padding: 0; 36 | } 37 | li { 38 | list-style-type: none; 39 | } 40 | button, 41 | input, 42 | textarea, 43 | select { 44 | -webkit-appearance: none; 45 | outline: none; 46 | border-radius: 0; 47 | background-color: transparent; 48 | } 49 | input, textarea { 50 | background: transparent; 51 | border: none; 52 | 53 | } 54 | body { 55 | margin: 0; 56 | } 57 | 58 | a { 59 | text-decoration: none; 60 | color: inherit; 61 | } 62 | 63 | p { 64 | margin: 0; 65 | a { 66 | text-decoration: underline; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/css/_base/global.scss: -------------------------------------------------------------------------------- 1 | svg, img { 2 | height: 100%; 3 | display: block; 4 | } 5 | video { 6 | object-fit: cover; 7 | object-position: center; 8 | } 9 | .ar-wrapper { 10 | width: 100%; 11 | position: relative; 12 | height: 0; 13 | padding-bottom: 100%; 14 | } 15 | .bg { 16 | background-size: cover; 17 | background-position: center; 18 | } 19 | .btn { 20 | cursor: pointer; 21 | width: 225px; 22 | height: 50px; 23 | border-radius: 5px; 24 | font-size: 11px; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | text-transform: uppercase; 29 | } 30 | .measure-el { 31 | position: absolute; 32 | width: 1px; 33 | right: 0; 34 | } 35 | .container-120 { 36 | width: 100%; 37 | max-width: 1440px; 38 | margin: 0 auto; 39 | padding: 0 120px; 40 | display: flex; 41 | align-items: center; 42 | @include bp-1200 { 43 | padding: 0 80px; 44 | } 45 | @include bp-959 { 46 | padding: 0 40px; 47 | } 48 | @include bp-767 { 49 | padding: 0 30px; 50 | .left, .right { 51 | width: 100%; 52 | } 53 | } 54 | @include bp-370 { 55 | padding: 0 20px; 56 | } 57 | } 58 | .container-90 { 59 | width: 100%; 60 | max-width: 1440px; 61 | margin: 0 auto; 62 | padding: 0 90px; 63 | display: flex; 64 | align-items: center; 65 | @include bp-1200 { 66 | padding: 0 70px; 67 | } 68 | @include bp-959 { 69 | padding: 0 40px; 70 | } 71 | @include bp-767 { 72 | padding: 0 30px; 73 | .left, .right { 74 | width: 100%; 75 | } 76 | } 77 | @include bp-370 { 78 | padding: 0 20px; 79 | } 80 | } 81 | .cml-120 { 82 | margin-left: 120px; 83 | @include bp-1200 { 84 | margin-left: 80px; 85 | } 86 | @include bp-959 { 87 | margin-left: 40px; 88 | } 89 | @include bp-767 { 90 | margin-left: 30px; 91 | } 92 | @include bp-370 { 93 | margin-left: 20px; 94 | } 95 | } 96 | .hidden { 97 | display: none !important; 98 | visibility: hidden; 99 | } 100 | main { 101 | display: block; 102 | } 103 | .view { 104 | width: 100%; 105 | } 106 | .hero { 107 | .measure-el { 108 | height: 55%; 109 | top: 0; 110 | @include bp-767 { 111 | height: 80%; 112 | } 113 | } 114 | } 115 | section { 116 | position: relative; 117 | z-index: 2; 118 | } 119 | 120 | // TOUCH DEVICE STYLES... 121 | // THINGS ARE GONNA GET PRETTY SPECIFIC HERE 122 | html { 123 | &.touch { 124 | #footer { 125 | h5 { 126 | display: none; 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/css/_critical/init.scss: -------------------------------------------------------------------------------- 1 | /* 2 | This file is to store the minimum css needed to 3 | render the page upon first load. 4 | 5 | The DOM file load order will look like: 6 | 7 | _fonts.css 8 | _init.css 9 | main.js 10 | main.css 11 | 12 | -------------------------------------------------- */ 13 | @import "../_abstracts/variables"; 14 | 15 | /* Init CSS 16 | -------------------------------------------------- */ 17 | 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | html, 23 | html a { 24 | -webkit-font-smoothing: antialiased !important; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | // NATIVE SCROLL BODY STYLES 29 | //html, 30 | //body { 31 | // width: 100%; 32 | // overflow-x: hidden; 33 | // overflow-y: auto; 34 | // &.touch { 35 | // 36 | // } 37 | // &.locked { 38 | // overflow-y: hidden; 39 | // } 40 | //} 41 | 42 | // VS STYLES.. IF USING NATIVE SCROLL COMMENT THIS BLOCK OUT AND UNCOMMENT THE ABOVE BLOCK 43 | body { 44 | width: 100%; 45 | height: 100%; 46 | min-height: 100%; 47 | max-height: 100%; 48 | overflow: hidden; 49 | &.locked { 50 | overflow-y: hidden; 51 | } 52 | } 53 | html { 54 | width: 100%; 55 | &.touch { 56 | overflow-x: hidden; 57 | overflow-y: auto; 58 | body { 59 | height: auto; 60 | max-height: none; 61 | min-height: 0; 62 | overflow-x: hidden; 63 | overflow-y: auto; 64 | } 65 | } 66 | } 67 | #container { 68 | overflow: hidden; 69 | } 70 | #main { 71 | 72 | } 73 | #global-mask { 74 | position: fixed; 75 | top: 0; 76 | left: 0; 77 | width: 100%; 78 | height: 100%; 79 | background-color: $white; 80 | z-index: 100; 81 | } 82 | -------------------------------------------------------------------------------- /src/css/_fonts.css: -------------------------------------------------------------------------------- 1 | /* 2 | Store your @font-face in here 3 | -------------------------------------------------- */ 4 | @import url("https://fonts.googleapis.com/css?family=Roboto&display=swap"); -------------------------------------------------------------------------------- /src/css/_global-components/footer.scss: -------------------------------------------------------------------------------- 1 | #footer { 2 | background-color: #3f7647; 3 | height: 400px; 4 | width: 100%; 5 | position: relative; 6 | z-index: 2; 7 | h5 { 8 | font-size: 100px; 9 | font-family: sans-serif; 10 | color: #000000; 11 | text-transform: uppercase; 12 | white-space: nowrap; 13 | z-index: 3; 14 | margin-top: 60px; 15 | pointer-events: none; 16 | } 17 | .measure-el { 18 | height: 100%; 19 | top: 0; 20 | } 21 | } 22 | .footer-inner { 23 | display: flex; 24 | justify-content: center; 25 | } 26 | -------------------------------------------------------------------------------- /src/css/_global-components/header.scss: -------------------------------------------------------------------------------- 1 | #header { 2 | position: fixed; 3 | top: 15px; 4 | left: 0; 5 | width: 100%; 6 | height: 70px; 7 | z-index: 9; 8 | .container-90 { 9 | height: 100%; 10 | justify-content: space-between; 11 | align-items: flex-end; 12 | max-width: none; 13 | } 14 | a { 15 | text-transform: uppercase; 16 | font-weight: bold; 17 | font-family: sans-serif; 18 | letter-spacing: .02px; 19 | } 20 | } -------------------------------------------------------------------------------- /src/css/_global-components/mini-cart.scss: -------------------------------------------------------------------------------- 1 | #mini-cart { 2 | position: fixed; 3 | background: #FFFFFF; 4 | width: 40%; 5 | height: calc(var(--vh, 1vh) * 100); 6 | opacity: 0; 7 | visibility: hidden; 8 | pointer-events: none; 9 | display: none; 10 | } 11 | -------------------------------------------------------------------------------- /src/css/_pages/404.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshkirk/highway-scroll/9c955fd64b2b494a65379976301723a248bf0fea/src/css/_pages/404.scss -------------------------------------------------------------------------------- /src/css/_pages/about.scss: -------------------------------------------------------------------------------- 1 | .view-about { 2 | section { 3 | height: calc(var(--vh, 1vh) * 100); 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | position: relative; 8 | overflow: hidden; 9 | h1 { 10 | font-size: 180px; 11 | font-family: sans-serif; 12 | color: #000000; 13 | text-transform: uppercase; 14 | white-space: nowrap; 15 | position: absolute; 16 | left: 0; 17 | z-index: 3; 18 | pointer-events: none; 19 | @include bp-767 { 20 | color: #ffffff; 21 | font-size: 50px; 22 | } 23 | } 24 | .image-wrapper { 25 | width: 35%; 26 | position: relative; 27 | .ar-wrapper { 28 | padding-bottom: 60%; 29 | } 30 | @include bp-767 { 31 | width: 75%; 32 | } 33 | } 34 | img { 35 | width: 100%; 36 | height: 100%; 37 | z-index: 2; 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | } 42 | } 43 | .scale-bg { 44 | position: absolute; 45 | width: 100%; 46 | height: 100%; 47 | top: 0; 48 | left: 0; 49 | transform-origin: 50% 100%; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/css/_pages/home.scss: -------------------------------------------------------------------------------- 1 | /* 2 | HOME PAGE STYLES 3 | 4 | - Import all home page specific styles here 5 | -------------------------------------------------- */ 6 | #scroll-progress { 7 | position: fixed; 8 | left: 0; 9 | top: 0; 10 | height: 15px; 11 | width: 100%; 12 | z-index: 99; 13 | transform: translateZ(0); 14 | .track { 15 | width: 100%; 16 | height: 100%; 17 | background-color: #EBEBEB; 18 | } 19 | .indicator { 20 | width: 100%; 21 | height: 100%; 22 | background-color: #3f7647; 23 | position: absolute; 24 | top: 0; 25 | transform-origin: 0 50%; 26 | transform: scaleX(0); 27 | } 28 | } 29 | 30 | .view-home { 31 | section { 32 | height: calc(var(--vh, 1vh) * 100); 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | position: relative; 37 | overflow: hidden; 38 | h1 { 39 | font-size: 180px; 40 | font-family: sans-serif; 41 | color: #000000; 42 | text-transform: uppercase; 43 | white-space: nowrap; 44 | position: absolute; 45 | left: 0; 46 | z-index: 3; 47 | pointer-events: none; 48 | @include bp-767 { 49 | color: #ffffff; 50 | font-size: 50px; 51 | } 52 | } 53 | .image-wrapper { 54 | width: 35%; 55 | position: relative; 56 | .ar-wrapper { 57 | padding-bottom: 60%; 58 | } 59 | @include bp-767 { 60 | width: 75%; 61 | } 62 | } 63 | img { 64 | width: 100%; 65 | height: 100%; 66 | z-index: 2; 67 | position: absolute; 68 | top: 0; 69 | left: 0; 70 | } 71 | } 72 | .scale-bg { 73 | position: absolute; 74 | width: 100%; 75 | height: 100%; 76 | top: 0; 77 | left: 0; 78 | transform-origin: 50% 100%; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/css/main.scss: -------------------------------------------------------------------------------- 1 | /* 2 | ABSTRACTS 3 | 4 | -------------------------------------------------- */ 5 | @import "/_abstracts/variables"; 6 | @import "/_abstracts/mixins"; 7 | 8 | /* 9 | BASE 10 | 11 | -------------------------------------------------- */ 12 | @import "/_base/custom-reset"; 13 | @import "/_base/global"; 14 | 15 | 16 | /* 17 | PAGES / LOCAL TEMPLATES 18 | 19 | -------------------------------------------------- */ 20 | @import "/_pages/home"; 21 | @import "/_pages/about"; 22 | @import "/_pages/404"; 23 | 24 | 25 | /* 26 | GLOBAL COMPONENTS 27 | 28 | -------------------------------------------------- */ 29 | @import "_global-components/header"; 30 | @import "_global-components/footer"; 31 | @import "_global-components/mini-cart"; 32 | 33 | /* 34 | PLUGINS 35 | 36 | - Third party plugins 37 | -------------------------------------------------- */ 38 | -------------------------------------------------------------------------------- /src/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshkirk/highway-scroll/9c955fd64b2b494a65379976301723a248bf0fea/src/fonts/.gitkeep -------------------------------------------------------------------------------- /src/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshkirk/highway-scroll/9c955fd64b2b494a65379976301723a248bf0fea/src/images/.gitkeep -------------------------------------------------------------------------------- /src/images/meta/facebook-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshkirk/highway-scroll/9c955fd64b2b494a65379976301723a248bf0fea/src/images/meta/facebook-share.png -------------------------------------------------------------------------------- /src/images/meta/twitter-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshkirk/highway-scroll/9c955fd64b2b494a65379976301723a248bf0fea/src/images/meta/twitter-share.png -------------------------------------------------------------------------------- /src/images/temp/shift-image-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshkirk/highway-scroll/9c955fd64b2b494a65379976301723a248bf0fea/src/images/temp/shift-image-2.jpg -------------------------------------------------------------------------------- /src/images/temp/shift-image-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshkirk/highway-scroll/9c955fd64b2b494a65379976301723a248bf0fea/src/images/temp/shift-image-3.jpg -------------------------------------------------------------------------------- /src/images/temp/shift-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshkirk/highway-scroll/9c955fd64b2b494a65379976301723a248bf0fea/src/images/temp/shift-image.jpg -------------------------------------------------------------------------------- /src/js/_classes/ScrollBasedAnims.js: -------------------------------------------------------------------------------- 1 | import VirtualScroll from "virtual-scroll"; 2 | import { gsap, Expo, Linear, Sine } from "gsap"; 3 | import { globalStorage, domStorage } from "../_global/storage"; 4 | 5 | export class ScrollBasedAnims { 6 | constructor(options = {}) { 7 | this.isVS = true; // Whether to use VS or native scroll for all animations 8 | this.isMobile = globalStorage.isMobile; 9 | 10 | if (this.isMobile || globalStorage.isFirefox) { // Both mobile and FF perform much better without VS 11 | this.isVS = false; 12 | } 13 | 14 | this.bindMethods(); 15 | 16 | gsap.defaults({ 17 | ease: "Linear.easeNone" // You want the ease of animations to be 1:1 with the scroll of the user's mouse if native (or the VS ease) 18 | }); 19 | 20 | this.el = this.isVS ? domStorage.containerEl : document.body; 21 | 22 | this.thisPagesTLs = []; // Array that will hold all the [data-entrance] timelines 23 | this.offsetVal = 0; // Value used to check against so we don't play [data-entrance] timelines more than once 24 | this.body = document.body; 25 | this.direction = 'untouched'; // Updated to either "up" or "down" in the run function 26 | this.transitioning = false; 27 | 28 | const { 29 | sections = !this.isVS ? false : this.el.querySelectorAll('[data-smooth]'), // Only collect [data-smooth] sectoins if this.isVS 30 | dataFromElems = this.el.querySelectorAll('[data-from]'), // Elements that transition between the values placed on the element in the markup 31 | dataHeroFromElems = document.querySelectorAll('[data-h-from]'), // [data-h-from] math differs a little 32 | heroMeasureEl = this.el.querySelector('.hero .measure-el'), // Use css to make the .measure-el within the hero whatever height you want. When it's gone, the animation is complete 33 | footerMeasureEl = document.querySelector('#footer .measure-el'), // Same goes for the footer measure element 34 | scrollBasedElems = this.el.querySelectorAll('[data-entrance]'), // Elements that have a certain entrance once, when scrolled into view 35 | threshold = 200, // Default pixel distance for this.isVisible to say yes this element is visible. We only animate against elements that are visible 36 | ease = 0.18, // VS ease 37 | // PC users deserve some consideration too, generally scroll devices can vary greatly on PC and the scroll events emitted will be far less frequent/reliable for easing purposes, 38 | // so we just jack it up since we'll be easing it anyways. Only used if VS is turned on 39 | mouseMultiplier = (["Win32", "Win64", "Windows", "WinCE"].indexOf(window.navigator.platform) !== -1 ? 0.6 : 0.25), 40 | preventTouch = true, 41 | passive = true, 42 | scrollIndicator = document.getElementById('scroll-progress') // The progress bar on top. This is just an example of data collection and animation 43 | } = options; 44 | 45 | this.dom = { // Save values to this.dom so we can just destroy that on leave 46 | el: this.el, 47 | // namespace: this.namespace, 48 | sections: sections, 49 | dataFromElems: dataFromElems, 50 | dataHeroFromElems: dataHeroFromElems, 51 | scrollBasedElems: scrollBasedElems, 52 | heroMeasureEl: heroMeasureEl, 53 | footerMeasureEl: footerMeasureEl, 54 | scrollIndicator: scrollIndicator 55 | }; 56 | 57 | this.sections = null; 58 | this.dataFromElems = null; 59 | this.dataHeroFromElems = null; 60 | this.scrollBasedElems = null; 61 | // this.namespace = null; 62 | this.raf = null; 63 | 64 | this.state = { 65 | resizing: false 66 | }; 67 | 68 | this.data = { 69 | threshold: threshold, 70 | ease: ease, 71 | current: 0, 72 | currentScrollY: 0, 73 | last: 0, 74 | target: 0, 75 | height: 0, 76 | width: globalStorage.windowWidth, 77 | max: 0 78 | }; 79 | 80 | if (this.isVS) { // If VS, instantiate the VS and bind inputs/textarea so you can press space without scrolling down while focused 81 | this.vs = new VirtualScroll({ 82 | el: this.el, 83 | mouseMultiplier: mouseMultiplier, 84 | preventTouch: preventTouch, 85 | passive: passive 86 | }); 87 | 88 | const inputs = document.querySelectorAll('input:not(.bound), textarea:not(.bound)'); 89 | for (let i = 0; i < inputs.length; i++) { 90 | let input = inputs[i]; 91 | input.classList.add('bound'); 92 | input.addEventListener('focus', () => { 93 | document.removeEventListener('keydown', this.vs._onKeyDown) 94 | }); 95 | input.addEventListener('blur', () => { 96 | document.addEventListener('keydown', this.vs._onKeyDown) 97 | }); 98 | } 99 | } 100 | 101 | this.init(); 102 | 103 | if (globalStorage.windowWidth > 767) { // I turn [data-entrance] animations off on mobile because I prefer a more simple mobile experience with just a few key animations, instead of many elements fading in, etc. 104 | let length = this.dom.scrollBasedElems.length; 105 | for (let i = 0; i < length; i++) { 106 | const entranceEl = this.dom.scrollBasedElems[i]; 107 | const entranceType = entranceEl.dataset.entrance; 108 | const entranceTL = new gsap.timeline({ paused: true }); 109 | 110 | switch (entranceType) { 111 | case 'scale-bg': 112 | const bg = entranceEl.querySelector('.scale-bg'); 113 | entranceTL 114 | .fromTo(bg, 1.2, { scaleY: 0 }, { scaleY: 1, ease: Expo.easeOut, force3D: true }) 115 | 116 | this.thisPagesTLs.push(entranceTL); 117 | break; 118 | 119 | case "stagger-fade": 120 | let staggerEls = entranceEl.querySelectorAll('.stagger-el'); 121 | 122 | entranceTL 123 | .fromTo(staggerEls, 0.7, { autoAlpha:0 }, { autoAlpha: 1, stagger: .12, force3D: true }); 124 | 125 | this.thisPagesTLs.push(entranceTL); 126 | break; 127 | 128 | default: 129 | 130 | break; 131 | } 132 | } 133 | } 134 | } 135 | 136 | bindMethods() { 137 | ['run', 'event', 'resize'] 138 | .forEach(fn => this[fn] = this[fn].bind(this)); 139 | } 140 | 141 | init() { 142 | this.on(); 143 | } 144 | 145 | on() { // Note how we take all measurements (getBounding & getCache) whether it's VS or not. Using native scroll we're still going to animate everything exactly the same in run() 146 | if (this.isVS) { 147 | this.dom.el.classList.add('is-vs'); 148 | this.setStyles(); 149 | this.vs.on(this.event); 150 | } 151 | this.getBounding(); 152 | this.getCache(); 153 | this.requestAnimationFrame(); 154 | } 155 | 156 | setStyles() { // Only called if isVS 157 | this.dom.el.style.position = 'fixed'; 158 | this.dom.el.style.top = 0; 159 | this.dom.el.style.left = 0; 160 | this.dom.el.style.width = '100%'; 161 | } 162 | 163 | event(e) { 164 | this.data.target += Math.round(e.deltaY * -1); 165 | this.clamp(); 166 | } 167 | 168 | clamp() { 169 | this.data.target = Math.round(Math.min(Math.max(this.data.target, 0), this.data.max)); 170 | } 171 | 172 | run() { // The RAF engine 173 | if (this.state.resizing || this.transitioning) return; 174 | 175 | if (this.isVS) { // If isVS, use those the simulated pageY values 176 | 177 | this.data.current += (this.data.target - this.data.current) * this.data.ease; 178 | this.transformSections(); // If isVS, transform the [data-smooth] sections 179 | // Notice in both cases, this.getDirection() is called in between setting this.data.current and this.data.last, so we can measure deltas and know if we're going up or down 180 | this.getDirection(); 181 | this.data.last = this.data.current; 182 | 183 | } else { // else if native scroll do everything the same except we're just setting this.data.current to be the literal window.scrollY 184 | // checking the window.scrollY causes a reflow, but the performance of everything is so tight, that is okay :D 185 | this.data.current = window.scrollY; 186 | this.data.currentScrollY = this.data.current; 187 | if (this.data.current === this.data.last) { // Don't run the animation cycle if they haven't scrolled 188 | this.requestAnimationFrame(); 189 | return; 190 | } 191 | // Notice in both cases, this.getDirection() is called in between setting this.data.current and this.data.last, so we can measure deltas and know if we're going up or down 192 | this.getDirection(); 193 | this.data.last = this.data.current; 194 | } 195 | 196 | this.scrollProgress(); // Example animation function that animates a progress bar 197 | 198 | if (!globalStorage.reducedMotion) { // You need to make sure all elements have proper positioning if not animated, or remove this check 199 | this.checkScrollBasedLoadins(); 200 | this.animateDataHeroFromElems(); 201 | this.animateDataFromElems(); 202 | this.animateFooterReveal(); 203 | } 204 | this.playPauseVideos(); 205 | // Do it all over again. Note that even for native scroll, we're running a RAF and not just relying on the scroll event to fire the run() function. It's much more reliable and smooth. 206 | this.requestAnimationFrame(); 207 | } 208 | 209 | getDirection() { 210 | if (this.data.last - this.data.current < 0) { 211 | // DOWN 212 | if (this.direction === 'down' || this.data.current <= 0) { return; } 213 | this.direction = 'down'; 214 | } else if (this.data.last - this.data.current > 0) { 215 | // UP 216 | if (this.direction === 'up') { return; } 217 | this.direction = 'up'; 218 | } 219 | } 220 | 221 | playPauseVideos() { 222 | if (this.direction === "untouched") return; 223 | for (let i = 0; i < this.videosDataLength; i++) { 224 | let data = this.videosData[i]; 225 | let { isVisible } = this.isVisible(data, 50) 226 | if (isVisible) { 227 | if (!data.playing) { 228 | data.el.play(); 229 | data.playing = true; 230 | } 231 | } else if (!isVisible && data.playing) { 232 | data.el.pause(); 233 | data.el.currentTime = 0; 234 | data.playing = false; 235 | } 236 | } 237 | } 238 | 239 | scrollProgress() { 240 | if (this.direction === "untouched" || !this.dom.scrollIndicator) { return } 241 | 242 | let scrollProgress = this.data.current / this.data.max; 243 | 244 | gsap.set(this.scrollProgressData.scrollBar, { scaleX: scrollProgress }) 245 | } 246 | 247 | getSections() { // [data-smooth] sections, which are only present if isVS 248 | if (!this.dom.sections) return; 249 | this.sections = []; 250 | let length = this.dom.sections.length; 251 | for (let i = 0; i < length; i++) { 252 | let el = this.dom.sections[i] 253 | el.style.transform = ''; 254 | const bounds = el.getBoundingClientRect(); 255 | this.sections.push({ 256 | top: (bounds.top + this.data.currentScrollY), 257 | bottom: (bounds.bottom + this.data.currentScrollY) 258 | }); 259 | } 260 | } 261 | 262 | getVideos() { 263 | let playPauseVideos = document.querySelectorAll('.play-in-view') 264 | this.videosData = [] 265 | 266 | for (let i = 0; i < playPauseVideos.length; i++) { 267 | let bounds = playPauseVideos[i].getBoundingClientRect() 268 | this.videosData.push({ 269 | el: playPauseVideos[i], 270 | playing: false, 271 | top: (bounds.top + this.data.currentScrollY) > this.data.height ? (bounds.top + this.data.currentScrollY) : this.data.height, 272 | bottom: (bounds.bottom + this.data.currentScrollY), 273 | }) 274 | } 275 | this.videosDataLength = this.videosData.length 276 | } 277 | 278 | getScrollBasedSections() { // [data-entrance] sections that animate once when they hit a certain threshold in the viewport 279 | if (!this.dom.scrollBasedElems || this.isMobile) return 280 | this.scrollBasedElems = [] 281 | let length = this.dom.scrollBasedElems.length; 282 | for (let i = 0; i < length; i++) { 283 | if (i < this.offsetVal) { continue } 284 | let el = this.dom.scrollBasedElems[i] 285 | const bounds = el.getBoundingClientRect() 286 | this.scrollBasedElems.push({ 287 | el: el, 288 | played: false, 289 | top: (bounds.top + this.data.currentScrollY) > this.data.height ? (bounds.top + this.data.currentScrollY) : this.data.height, 290 | bottom: (bounds.bottom + this.data.currentScrollY), 291 | height: (bounds.bottom - bounds.top), 292 | offset: globalStorage.windowWidth < 768 ? (el.dataset.offsetMobile * globalStorage.windowHeight) : (el.dataset.offset * globalStorage.windowHeight) 293 | }) 294 | } 295 | 296 | } 297 | 298 | getDataFromElems() { // [data-from] elements that animate forward and backward as you scroll up and down. Note you can optionally have [data-mobile-from] & [data-mobile-to] on your elements 299 | if (!this.dom.dataFromElems) return; 300 | 301 | this.dataFromElems = []; 302 | 303 | let useMobile = globalStorage.windowWidth < 768; 304 | 305 | let length = this.dom.dataFromElems.length 306 | for (let i = 0; i < length; i++) { 307 | let el = this.dom.dataFromElems[i] 308 | 309 | let from, to, dur; 310 | const bounds = el.getBoundingClientRect() 311 | const tl = new gsap.timeline({ paused: true }) 312 | 313 | if (useMobile) { 314 | from = el.dataset.mobileFrom ? JSON.parse(el.dataset.mobileFrom) : JSON.parse(el.dataset.from); 315 | to = el.dataset.mobileTo ? JSON.parse(el.dataset.mobileTo) : JSON.parse(el.dataset.to); 316 | if (el.dataset.mobileDur) { 317 | dur = el.dataset.mobileDur; 318 | } else { 319 | dur = el.dataset.dur ? el.dataset.dur : 1; 320 | } 321 | } else { 322 | from = JSON.parse(el.dataset.from); 323 | to = JSON.parse(el.dataset.to); 324 | dur = el.dataset.dur ? el.dataset.dur : 1; 325 | } 326 | 327 | tl.fromTo(el, 1, from, to) 328 | 329 | this.dataFromElems.push({ 330 | el: el, 331 | tl: tl, 332 | top: (bounds.top + this.data.currentScrollY) > this.data.height ? (bounds.top + this.data.currentScrollY - (this.isMobile ? 40 : 0)) : this.data.height, 333 | bottom: (bounds.bottom + this.data.currentScrollY - (this.isMobile ? 40 : 0)), 334 | height: bounds.bottom - bounds.top, 335 | from: from, 336 | duration: dur, 337 | progress: { 338 | current: 0 339 | } 340 | }) 341 | } 342 | 343 | } 344 | 345 | getHeroMeasureEl() { // Measure the .measure-el within .hero so we know when the [data-h-from] element animations should be complete 346 | if (!this.dom.heroMeasureEl) return; 347 | const el = this.dom.heroMeasureEl; 348 | const bounds = el.getBoundingClientRect(); 349 | this.heroMeasureData = { 350 | top: (bounds.top + this.data.currentScrollY) > this.data.height ? (bounds.top + this.data.currentScrollY) : this.data.height, 351 | bottom: (bounds.bottom + this.data.currentScrollY), 352 | height: bounds.bottom - bounds.top, 353 | progress: { 354 | current: 0 355 | } 356 | }; 357 | } 358 | 359 | getFooterMeasureEl() { 360 | // Same as the getHeroMeasureEl() but the math is again slightly different. Here we're determining when a bottom-of-the-screen element is 100% in. 361 | // In the previous function we're gathering data so we can determine when an already-visible element is 100% out. 362 | // In the next function we'll get the elements that use a 0-1 intersectRatio() to animate forward and backward as they pass through the viewport 363 | if (!this.dom.footerMeasureEl || this.isMobile) return; 364 | const el = this.dom.footerMeasureEl; 365 | const bounds = el.getBoundingClientRect(); 366 | this.footerRevealTL = new gsap.timeline({ paused: true }) 367 | this.footerRevealTL 368 | .fromTo("#footer h5", 1, { y: 200, rotation: 540, opacity: 0 }, { y: 0, rotation: 0, opacity: 1 }); 369 | 370 | this.footerMeasureData = { 371 | top: (bounds.top + this.data.currentScrollY) > this.data.height ? (bounds.top + this.data.currentScrollY) : this.data.height, 372 | bottom: (bounds.bottom + this.data.currentScrollY), 373 | height: bounds.bottom - bounds.top, 374 | duration: (this.data.height / (bounds.bottom - bounds.top)).toFixed(2) 375 | }; 376 | } 377 | 378 | getDataHeroFromElems() { 379 | if (!this.dom.dataHeroFromElems) return; 380 | 381 | this.dataHeroFromElems = []; 382 | const useMobile = globalStorage.windowWidth < 768; 383 | for (let i = 0; i < this.dom.dataHeroFromElems.length; i++) { 384 | let el = this.dom.dataHeroFromElems[i] 385 | let from, to; 386 | const tl = new gsap.timeline({ paused: true }); 387 | 388 | if (useMobile) { 389 | from = el.dataset.hMobileFrom ? JSON.parse(el.dataset.hMobileFrom) : JSON.parse(el.dataset.hFrom); 390 | to = el.dataset.mobileTo ? JSON.parse(el.dataset.mobileTo) : JSON.parse(el.dataset.to); 391 | } else { 392 | from = JSON.parse(el.dataset.hFrom); 393 | to = JSON.parse(el.dataset.to); 394 | } 395 | 396 | tl.fromTo(el, 1, from, to); 397 | 398 | this.dataHeroFromElems.push({ 399 | el: el, 400 | tl: tl, 401 | progress: { 402 | current: 0 403 | } 404 | }) 405 | } 406 | } 407 | 408 | transformSections() { 409 | if (!this.sections) return; 410 | 411 | const translate = this.data.current.toFixed(2); 412 | 413 | let length = this.sections.length; 414 | for (let i = 0; i < length; i++) { 415 | let data = this.sections[i] 416 | const { isVisible } = this.isVisible(data); 417 | 418 | if (isVisible || this.state.resizing) gsap.set(this.dom.sections[i], { y: -translate, force3D: true, ease: Linear.easeNone }); 419 | } 420 | } 421 | 422 | animateDataHeroFromElems() { 423 | if (this.direction === "untouched" || !this.heroMeasureData) return; 424 | const { isVisible } = ( this.isVisible(this.heroMeasureData) ); 425 | if (!isVisible) return; 426 | let percentageThrough = (this.data.current / this.heroMeasureData.height).toFixed(3); 427 | 428 | if (percentageThrough <= 0) { 429 | percentageThrough = 0; 430 | } else if (percentageThrough >= 1) { 431 | percentageThrough = 1; 432 | } 433 | // console.log(percentageThrough) 434 | let length = this.dataHeroFromElems.length; 435 | for (let i = 0; i < length; i++) { 436 | let data = this.dataHeroFromElems[i] 437 | data.tl.progress(percentageThrough) 438 | } 439 | } 440 | 441 | animateFooterReveal() { 442 | if (this.direction === "untouched" || !this.footerMeasureData) return; 443 | const { isVisible, start, end } = ( this.isVisible(this.footerMeasureData, 0.01) ); 444 | if (!isVisible) { 445 | this.footerRevealTL.progress(0); 446 | return; 447 | } 448 | let percentageThrough = ((((start).toFixed(2) / this.data.height).toFixed(3) - 1) * -1) * this.footerMeasureData.duration; 449 | if (percentageThrough <= 0) { 450 | percentageThrough = 0; 451 | } else if (percentageThrough >= 1) { 452 | percentageThrough = 1; 453 | } 454 | console.log(percentageThrough); 455 | this.footerRevealTL.progress(percentageThrough); 456 | } 457 | 458 | animateDataFromElems() { 459 | if (this.direction === "untouched" || !this.dataFromElems) return 460 | 461 | let length = this.dataFromElems.length; 462 | for (let i = 0; i < length; i++) { 463 | let data = this.dataFromElems[i] 464 | 465 | const { isVisible, start, end } = this.isVisible(data, 0.01); 466 | 467 | if (isVisible) { 468 | 469 | this.intersectRatio(data, start, end) 470 | 471 | data.tl.progress(data.progress.current) 472 | } 473 | } 474 | } 475 | 476 | checkScrollBasedLoadins() { 477 | if (this.direction === "untouched" || !this.scrollBasedElems) { return } 478 | if (this.thisPagesTLs.length !== this.offsetVal) { 479 | let length = this.scrollBasedElems.length; 480 | for (let i = 0; i < length; i++) { 481 | let data = this.scrollBasedElems[i]; 482 | 483 | if (data.played) { continue } 484 | 485 | if ((this.data.current + data.offset) > data.top) { 486 | this.thisPagesTLs[this.offsetVal].play(); 487 | this.offsetVal++; 488 | data.played = true; 489 | } 490 | } 491 | } 492 | } 493 | 494 | intersectRatio(data, top, bottom) { 495 | const start = top - this.data.height; 496 | 497 | if (start > 0) { return } 498 | const end = (this.data.height + bottom + data.height) * data.duration; 499 | data.progress.current = Math.abs(start / end); 500 | data.progress.current = Math.max(0, Math.min(1, data.progress.current)); 501 | } 502 | 503 | isVisible(bounds, offset) { 504 | const threshold = !offset ? this.data.threshold : offset; 505 | const start = bounds.top - this.data.current; 506 | const end = bounds.bottom - this.data.current; 507 | const isVisible = start < (threshold + this.data.height) && end > -threshold; 508 | return { 509 | isVisible, 510 | start, 511 | end 512 | }; 513 | } 514 | 515 | requestAnimationFrame() { 516 | this.raf = requestAnimationFrame(this.run); 517 | } 518 | 519 | cancelAnimationFrame() { 520 | cancelAnimationFrame(this.raf); 521 | } 522 | 523 | getCache() { 524 | if (this.isVS) { 525 | this.getSections(); 526 | } 527 | this.getVideos(); 528 | if (globalStorage.reducedMotion) { return; } 529 | this.getScrollBasedSections(); 530 | this.getDataHeroFromElems(); 531 | this.getDataFromElems(); 532 | this.getHeroMeasureEl(); 533 | this.getFooterMeasureEl(); 534 | this.getScrollProgressData(); 535 | } 536 | 537 | getScrollProgressData() { 538 | if (!this.dom.scrollIndicator) return 539 | const el = this.dom.scrollIndicator; 540 | const scrollBar = el.querySelector('.indicator') 541 | const indicators = el.querySelectorAll('.indicator'); 542 | const trackHeight = Math.round(el.offsetHeight) 543 | const indicatorHeight = Math.round(indicators[0].offsetHeight); 544 | 545 | gsap.set(el, { height: trackHeight }) 546 | gsap.set(indicators, { height: indicatorHeight }) 547 | 548 | const scrollDist = trackHeight - scrollBar.offsetHeight; 549 | 550 | this.scrollProgressData = { 551 | scrollDist: scrollDist, 552 | scrollBar: scrollBar 553 | }; 554 | } 555 | 556 | getBounding() { 557 | this.data.height = globalStorage.windowHeight + (this.isMobile ? 100 : 0); 558 | this.data.max = (this.dom.el.getBoundingClientRect().height - this.data.height) + (this.isMobile ? 100 : 0); 559 | } 560 | 561 | resize(omnibar = false) { 562 | // Okay so this is some shit. We either lock the body height and overflow so the omnibar can't collapse, or we do the below.. 563 | // Basically if we can assume it's just the omnibar collapsing, don't re-measure things and just keep measuring off the un-collapsed omnibar height. This allows for 564 | // scroll based animations to continue smoothly during omnibar collapse. 565 | 566 | // I haven't been able to animate the footer reveal properly without locking the body so you'll notice the footer reveal is currently only being done on desktop :( 567 | // If you lock the body overflow so the body can't collapse you can remove the omnibar stuff from this function and animate an awesome footer reveal too, I wouldn't blame you. 568 | 569 | this.state.resizing = true; 570 | if (!omnibar) { 571 | this.getCache(); 572 | if (this.isVS) { 573 | this.transformSections(); 574 | } 575 | this.getBounding(); 576 | } 577 | this.state.resizing = false; 578 | } 579 | 580 | 581 | scrollTo(val) { 582 | this.state.scrollingTo = true; 583 | if (!this.isVS) { 584 | gsap.to(this.el, 1, { scrollTop: val, ease: Sine.easeInOut, onComplete: () => { this.state.scrolling = false } }) 585 | } else { 586 | gsap.to(this.data, 0.7, { target: val, ease: Sine.easeInOut, onComplete: () => { this.state.scrolling = false } }) 587 | } 588 | } 589 | 590 | destroy() { 591 | this.transitioning = true; // We destroy the class instance in the global onLeave function 592 | 593 | if (this.isVS) { 594 | this.vs.off(this.event); 595 | this.dom.el.classList.remove('is-vs'); 596 | this.vs.destroy(); 597 | } 598 | this.state.rafCancelled = true; 599 | this.cancelAnimationFrame(); 600 | 601 | this.dom = null; 602 | this.data = null; 603 | this.raf = null; 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /src/js/_global/_renderer.js: -------------------------------------------------------------------------------- 1 | /* 2 | Load Plugins / Functions 3 | -------------------------------------------------- */ 4 | import { LazyLoadWorker } from "../_worker/_init"; 5 | import { hasClass, getViewport, tracking } from "../_global/helpers"; 6 | import { globalStorage } from "../_global/storage"; 7 | import { globalEntrance, pageEntrance } from "../_global/anims"; 8 | import { ScrollBasedAnims } from "../_classes/ScrollBasedAnims"; 9 | import 'intersection-observer'; 10 | import quicklink from "quicklink/dist/quicklink.mjs"; 11 | 12 | /* --- Scroll variable --- */ 13 | export let $scroll; 14 | 15 | /* --- Setup our Image workers --- */ 16 | let ImageLoad = new LazyLoadWorker("image-load"); 17 | 18 | /* --- Global Events - Fire on Every Page --- */ 19 | const globalEvents = (namespace = null)=>{ 20 | 21 | globalStorage.namespace = namespace; 22 | 23 | /* --- Set our preload callback images sizes --- */ 24 | let type = "mobile"; 25 | 26 | if(globalStorage.windowWidth > 767 && !hasClass(document.body, "force-mobile")){ 27 | type = "desktop"; 28 | } 29 | 30 | /* 31 | * Load Critical Images 32 | * 33 | * The callback is meant to fire DOM Critical related 34 | * global functions. Everything page specific needs to 35 | * go within it's respective file. 36 | */ 37 | ImageLoad.loadImages("preload-critical", type, ()=>{ 38 | 39 | /* --- Critical Done --- */ 40 | globalEntrance(()=>{ 41 | 42 | /* --- Global Entrance is done --- */ 43 | 44 | }); 45 | 46 | let transitionFinished = setInterval(()=>{ 47 | if(globalStorage.transitionFinished === true){ 48 | clearInterval(transitionFinished); 49 | $scroll = new ScrollBasedAnims({}); 50 | pageEntrance(namespace); 51 | } 52 | }, 20); 53 | 54 | ImageLoad.loadImages("preload", type, ()=>{ 55 | 56 | /* --- All Images Done --- */ 57 | 58 | }); 59 | }); 60 | }; 61 | 62 | /* --- DOMContentLoaded Function --- */ 63 | export const onReady = ()=>{ 64 | let namespace = document.querySelector('[data-router-view]').dataset.routerView; 65 | let vh = globalStorage.windowHeight * 0.01; 66 | 67 | document.body.style.setProperty('--vh', `${vh}px`); 68 | document.body.style.setProperty('--vhu', `${vh}px`); // viewport height updated 69 | 70 | globalEvents(namespace); 71 | 72 | if (namespace === "home") { 73 | quicklink({urls:['/about']}); 74 | } else if (namespace === "about") { 75 | quicklink({urls:['/']}); 76 | } 77 | }; 78 | 79 | /* --- window.onload Function --- */ 80 | export const onLoad = ()=>{ 81 | 82 | }; 83 | 84 | /* --- window.onresize Function --- */ 85 | export const onResize = ()=>{ 86 | let newWidth = getViewport().width; 87 | let omnibar = false; 88 | if (globalStorage.windowWidth === newWidth && globalStorage.isMobile) { 89 | omnibar = true; 90 | } 91 | 92 | globalStorage.windowHeight = getViewport().height; 93 | globalStorage.windowWidth = newWidth; 94 | 95 | let vh = globalStorage.windowHeight * 0.01; 96 | if (!omnibar) { 97 | document.body.style.setProperty('--vh', `${vh}px`); 98 | } 99 | document.body.style.setProperty('--vhu', `${vh}px`); 100 | 101 | $scroll.resize(omnibar); 102 | }; 103 | 104 | /* 105 | * Highway NAVIGATE_OUT callback 106 | * 107 | * onLeave is fired when a highway transition has been 108 | * initiated. This callback is primarily used to unbind 109 | * events, or modify global settings that might be called 110 | * in onEnter/onEnterCompleted functions. 111 | */ 112 | export const onLeave = (from, trigger, location)=>{ 113 | 114 | /* --- Remove our scroll measurements --- */ 115 | $scroll.destroy(); 116 | 117 | /* --- Flag transition for load in animations --- */ 118 | globalStorage.transitionFinished = false; 119 | }; 120 | 121 | /* 122 | * Highway NAVIGATE_IN callback 123 | * 124 | * onEnter should only contain event bindings and non- 125 | * DOM related event measurements. Both view containers 126 | * are still loaded into the DOM during this callback. 127 | */ 128 | export const onEnter = (to, trigger, location)=>{ 129 | globalEvents(to.view.dataset.routerView); 130 | }; 131 | 132 | /* 133 | * Highway NAVIGATE_END callback 134 | * 135 | * onEnterCompleted should be your primary event callback. 136 | * The previous view's DOM node has been removed when this 137 | * event fires. 138 | */ 139 | export const onEnterCompleted = (from, to, trigger, location)=>{ 140 | 141 | /* --- Track Page Views through Ajax --- */ 142 | tracking("google", "set", "page", location.pathname); 143 | tracking("google", "send", { 144 | hitType: "pageview", 145 | page: location.pathname, 146 | title: to.page.title 147 | }); 148 | }; 149 | -------------------------------------------------------------------------------- /src/js/_global/anims.js: -------------------------------------------------------------------------------- 1 | import { gsap, Sine } from "gsap"; 2 | import {domStorage, globalStorage} from "./storage"; 3 | import { $scroll } from "../_global/_renderer" 4 | 5 | /* 6 | Page specific animations 7 | -------------------------------------------------- */ 8 | export const pageEntrance = (namespace)=>{ 9 | 10 | /* ----- Establish our timeline ----- */ 11 | let timeline = new gsap.timeline({ paused: true }); 12 | 13 | timeline.to(domStorage.globalMask, 1, { autoAlpha: 0, ease: Sine.easeInOut }); 14 | 15 | /* ----- Setup cases for specific load-ins ----- */ 16 | switch(namespace){ 17 | 18 | case "home": 19 | 20 | timeline.add(()=>{ console.log("home anims in go here"); }); 21 | timeline.play(); 22 | 23 | break; 24 | 25 | case "about": 26 | 27 | timeline.add(()=>{ console.log("about anims in go here"); }); 28 | timeline.play(); 29 | 30 | break; 31 | 32 | /* ----- Our default page entrance ----- */ 33 | default: 34 | 35 | timeline.add(()=>{ console.log("default anims in go here"); }); 36 | timeline.play(); 37 | 38 | break; 39 | } 40 | 41 | if (globalStorage.firstLoad) { 42 | globalStorage.firstLoad = false; 43 | } 44 | }; 45 | 46 | /* 47 | Global element animations 48 | -------------------------------------------------- */ 49 | export const globalEntrance = ()=>{ 50 | 51 | if(globalStorage.firstLoad === false){ 52 | return; 53 | } 54 | 55 | /* ----- Establish our timeline ----- */ 56 | let timeline = new gsap.timeline({ paused: true }); 57 | 58 | // timeline.to(".loader-icon", 0.5, { autoAlpha: 0, ease: Sine.easeInOut, onComplete: ()=>{ globalStorage.transitionFinished = true; }}); 59 | globalStorage.transitionFinished = true; ////// delete this if waiting for loader animations ^^^ 60 | 61 | timeline.play(); 62 | }; 63 | -------------------------------------------------------------------------------- /src/js/_global/helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | Has Class 3 | 4 | Returns true/false if element has classname. 5 | -------------------------------------------------- */ 6 | export const hasClass = (element, cls)=>{ 7 | return (' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1; 8 | }; 9 | 10 | /* 11 | Vanilla .eq() 12 | 13 | Returns element by index. 14 | 15 | Usage: eq.call(arrayOfElements, index); 16 | -------------------------------------------------- */ 17 | export const eq = (index)=>{ 18 | if(index >= 0 && index < this.length){ 19 | return this[index]; 20 | } else { 21 | return -1; 22 | } 23 | }; 24 | 25 | /* 26 | Vanilla array remove 27 | -------------------------------------------------- */ 28 | export const remove = (array, element)=>{ 29 | return array.filter(e => e !== element); 30 | }; 31 | 32 | 33 | /* 34 | Get Viewport 35 | 36 | returns the native height and width of the 37 | browser viewport. 38 | -------------------------------------------------- */ 39 | export const getViewport = function(){ 40 | let e = window, a = 'inner'; 41 | if(!('innerWidth' in window)){ 42 | a = 'client'; 43 | e = document.documentElement || document.body; 44 | } 45 | return { width : e[ a+'Width' ] , height : e[ a+'Height' ] }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/js/_global/storage.js: -------------------------------------------------------------------------------- 1 | /* 2 | Global Storage Object 3 | -------------------------------------------------- */ 4 | export const globalStorage = { 5 | "assetPath": (document.getElementById("site-data") && window.location.host.indexOf("localhost") < 0) ? document.getElementById("site-data").getAttribute("data-asset-path") : "/assets/code/", 6 | "firstLoad": true, 7 | "isMobile": false, 8 | "isSafari": false, 9 | "isChrome": false, 10 | "isFirefox": (navigator.userAgent.indexOf("Firefox") > -1), 11 | "windowHeight": "", 12 | "windowWidth": "", 13 | "transitionFinished": false, 14 | "queryParams": {}, 15 | "referrer": "", 16 | "reducedMotion": window.matchMedia("(prefers-reduced-motion: reduce)").matches, 17 | "headerShowing": true 18 | }; 19 | 20 | export const domStorage = { 21 | // "header": document.getElementById('header'), 22 | "mainEl": document.getElementById('main'), 23 | "containerEl": document.getElementById('container'), 24 | "globalMask": document.getElementById('global-mask'), 25 | "openMobileMenu": () => {}, 26 | "closeMobileMenu": () => {}, 27 | "resetMobileMenu": () => {} 28 | } 29 | 30 | export const ecommStorage = { 31 | "products": null, 32 | "cart": null, 33 | "checkout": "", 34 | "cartCountEls": "", 35 | "removingLastItem": false, 36 | "openMiniCart": () => {}, 37 | "closeMiniCart": () => {}, 38 | } 39 | 40 | 41 | // adjust storage values based on things we can determine right away 42 | let is_chrome = navigator.userAgent.indexOf('Chrome') > -1; 43 | let is_safari = navigator.userAgent.indexOf("Safari") > -1; 44 | let is_opera = navigator.userAgent.toLowerCase().indexOf("op") > -1; 45 | 46 | if ((is_chrome)&&(is_safari)) { is_safari = false; } 47 | if ((is_chrome)&&(is_opera)) { is_chrome = false; } 48 | 49 | if (is_safari) { globalStorage.isSafari = true } 50 | if (is_chrome) { globalStorage.isChrome = true } 51 | 52 | const motionMediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); 53 | globalStorage.reducedMotion = motionMediaQuery.matches; 54 | 55 | if (globalStorage.reducedMotion) { 56 | motionMediaQuery.addEventListener('change', () => { 57 | globalStorage.reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 58 | console.log('Prefers reduced motion: '+globalStorage.reducedMotion) 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /src/js/_initialize.js: -------------------------------------------------------------------------------- 1 | if ('scrollRestoration' in history) { 2 | history.scrollRestoration = 'manual'; 3 | } 4 | 5 | /* 6 | Init Routing 7 | -------------------------------------------------- */ 8 | import "./routing"; 9 | 10 | /* 11 | Load Plugins / Functions 12 | -------------------------------------------------- */ 13 | import { getViewport } from "./_global/helpers"; 14 | import { onReady, onLoad, onResize } from "./_global/_renderer"; 15 | import { globalStorage } from "./_global/storage"; 16 | 17 | 18 | /* 19 | Constants 20 | -------------------------------------------------- */ 21 | const isMobile = require("ismobilejs"); 22 | 23 | globalStorage.isMobile = isMobile.any; 24 | 25 | if (globalStorage.isMobile) { 26 | document.documentElement.classList.add('touch'); 27 | } 28 | 29 | /* 30 | Check for Reduced Motion changes 31 | -------------------------------------------------- */ 32 | if(globalStorage.reducedMotion){ 33 | window.matchMedia("(prefers-reduced-motion: reduce)").addEventListener("change", ()=>{ 34 | globalStorage.reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 35 | }); 36 | } 37 | 38 | /* 39 | Doc ready 40 | -------------------------------------------------- */ 41 | document.addEventListener("DOMContentLoaded", () => { 42 | 43 | /* --- Keep these up to date --- */ 44 | globalStorage.windowWidth = getViewport().width; 45 | globalStorage.windowHeight = getViewport().height; 46 | 47 | /* --- Fire onReady --- */ 48 | onReady(); 49 | }, false); 50 | 51 | /* 52 | Window onload 53 | -------------------------------------------------- */ 54 | window.onload = function(){ 55 | 56 | /* --- Keep these up to date --- */ 57 | globalStorage.windowWidth = getViewport().width; 58 | globalStorage.windowHeight = getViewport().height; 59 | 60 | /* --- Fire onLoad --- */ 61 | onLoad(); 62 | }; 63 | 64 | /* 65 | Window resize 66 | -------------------------------------------------- */ 67 | let resizeTimeout = setTimeout(()=>{},0); 68 | 69 | window.onresize = function(){ 70 | 71 | /* --- Clear the timeout if actively resizing --- */ 72 | clearTimeout(resizeTimeout); 73 | 74 | /* --- Delay resize event --- */ 75 | resizeTimeout = setTimeout(()=>{ 76 | 77 | /* --- Keep these up to date --- */ 78 | globalStorage.windowWidth = getViewport().width; 79 | globalStorage.windowHeight = getViewport().height; 80 | 81 | /* --- Fire onResize --- */ 82 | onResize(); 83 | }, 250); 84 | }; -------------------------------------------------------------------------------- /src/js/_transitions/basicFade.js: -------------------------------------------------------------------------------- 1 | import Highway from "@dogstudio/highway"; 2 | import { gsap, Sine } from "gsap"; 3 | import { globalStorage } from "../_global/storage"; 4 | import {$scroll} from "../_global/_renderer"; 5 | 6 | let globalMask = document.getElementById("global-mask"); 7 | 8 | /* 9 | Default Highway Transition 10 | -------------------------------------------------- */ 11 | class BasicFade extends Highway.Transition{ 12 | 13 | out({from, trigger, done}){ 14 | 15 | gsap.fromTo(globalMask, 0.5, { autoAlpha: 0 }, { autoAlpha: 1, ease: Sine.easeInOut, onComplete: () => { done() } }); 16 | } 17 | 18 | in({from, to, trigger, done}){ 19 | 20 | globalStorage.namespace = to.dataset.routerView; 21 | 22 | // Remove old view 23 | from.remove(); 24 | 25 | if (globalStorage.isMobile) { 26 | // Move to top of page 27 | document.body.scrollTop = 0; 28 | window.scroll(0,0); 29 | } 30 | 31 | globalStorage.transitionFinished = true; 32 | 33 | done(); 34 | } 35 | } 36 | 37 | export default BasicFade; 38 | -------------------------------------------------------------------------------- /src/js/_worker/_init.js: -------------------------------------------------------------------------------- 1 | import { hasClass, getViewport } from "../_global/helpers"; 2 | import { globalStorage } from "../_global/storage"; 3 | /* 4 | * Worker pool initialization file 5 | * 6 | * - Sets up our worker pool to give our 7 | * javaScript an array of worker threads 8 | * to work with 9 | * - Workers are used to run long-running 10 | * or heavy scripts, don't inject every 11 | * function into this. 12 | */ 13 | export class LazyLoadWorker { 14 | 15 | constructor(worker){ 16 | this.worker = new Worker("/assets/js/workers/image-load.js"); 17 | 18 | this.running = false; 19 | this.array = []; 20 | this.count = 0; 21 | this.total = 0; 22 | this.type = ""; 23 | 24 | /* --- Set our preload callback images sizes --- */ 25 | this.size = "mobile"; 26 | 27 | if(getViewport().width > 767 && !hasClass(document.body, "force-mobile")){ 28 | this.size = "desktop"; 29 | } 30 | 31 | this.bindEvents(); 32 | } 33 | 34 | bindEvents(){ 35 | 36 | this.worker.addEventListener("message", (event)=>{ 37 | 38 | const data = event.data; 39 | 40 | /* --- Make sure we get elements that might have duplicate images --- */ 41 | let images = document.querySelectorAll(`[data-preload-${this.size}="${data.url}"]`); 42 | 43 | for(let i = 0; i < images.length; i++){ 44 | 45 | let url = data.url; 46 | let rmv = (i === images.length - 1); 47 | 48 | this.preload(images[i], data.blob, rmv); 49 | } 50 | }); 51 | } 52 | 53 | loadImages(type, size, callback){ 54 | let images = document.querySelectorAll(`.${type}`); 55 | let protocol = window.location.protocol + "//"; 56 | 57 | this.array = []; 58 | this.count = 0; 59 | 60 | this.total = images.length; 61 | this.type = type; 62 | 63 | /* --- Loop through our images --- */ 64 | for(let i = 0; i < images.length; i++){ 65 | 66 | let url = images[i].getAttribute("data-preload-" + size); 67 | let tag = images[i].tagName.toLowerCase(); 68 | 69 | /* --- Fallback for local images --- */ 70 | if(url.indexOf("http") < 0){ 71 | url = protocol + window.location.host + url; 72 | } 73 | 74 | if(url !== "" && this.array.indexOf(url) < 0){ 75 | 76 | if(globalStorage.popstate || tag === "iframe" || tag === "video"){ 77 | /* --- Hot swap our src --- */ 78 | this.hotSwap(images[i], this.size); 79 | this.total--; 80 | } else { 81 | /* --- Send the url over to the worker --- */ 82 | this.array.push(url); 83 | this.message(url); 84 | } 85 | 86 | } else { 87 | 88 | /* --- Reduce total if there is no image --- */ 89 | this.total--; 90 | } 91 | } 92 | 93 | /* --- Set our callback interval --- */ 94 | let interval = setInterval(()=>{ 95 | 96 | if(this.count >= this.total){ 97 | clearInterval(interval); 98 | callback(type + " done!"); 99 | } 100 | 101 | }, 10); 102 | } 103 | 104 | hotSwap(element, size){ 105 | this.preloadSwap(element, size); 106 | } 107 | 108 | removeAttrs(element) { 109 | element.classList.remove('preload'); 110 | element.classList.remove('preload-critical'); 111 | element.removeAttribute('data-preload-desktop'); 112 | element.removeAttribute('data-preload-mobile'); 113 | } 114 | 115 | preload(element, blob, remove){ 116 | 117 | /* ----- If there is no URL defined, return clean ----- */ 118 | if(!blob || blob === ""){ 119 | 120 | this.removeAttrs(element); 121 | 122 | return; 123 | } 124 | 125 | let tag = element.tagName.toLowerCase(); 126 | let url = URL.createObjectURL(blob); 127 | 128 | switch(tag){ 129 | 130 | /* ----- Video Tags ----- */ 131 | case "video": 132 | 133 | let source = document.createElement("source"); 134 | 135 | source.setAttribute("src", url); 136 | 137 | element.appendChild(source); 138 | element.load(); 139 | element.play(); 140 | 141 | let checkVideoLoad = setInterval(()=>{ 142 | 143 | if(element.readyState > 3){ 144 | 145 | /* --- Video has loaded --- */ 146 | this.removeAttrs(element); 147 | clearInterval(checkVideoLoad); 148 | 149 | if(remove === true){ 150 | URL.revokeObjectURL(url); 151 | } 152 | 153 | this.count++; 154 | } 155 | 156 | }, 25); 157 | 158 | break; 159 | 160 | /* ----- Img Tags ----- */ 161 | case "img": 162 | 163 | /* --- Wait for Img tag to load --- */ 164 | element.onload = ()=>{ 165 | 166 | this.removeAttrs(element); 167 | 168 | if(remove === true){ 169 | URL.revokeObjectURL(url); 170 | } 171 | 172 | this.count++; 173 | }; 174 | 175 | element.onerror = ()=>{ 176 | console.log("LOAD FAILURE: " + url); 177 | this.count++; 178 | }; 179 | 180 | element.setAttribute("src", url); 181 | 182 | break; 183 | 184 | /* ----- Everything else, assumes background-image ----- */ 185 | default: 186 | 187 | /* --- Use Img load to measure for background-image --- */ 188 | element.style.backgroundImage = "url(" + url + ")"; 189 | this.removeAttrs(element); 190 | 191 | if(remove === true){ 192 | // URL.revokeObjectURL(url); 193 | } 194 | 195 | this.count++; 196 | 197 | break; 198 | } 199 | } 200 | 201 | preloadSwap(element, size){ 202 | 203 | let tag = element.tagName.toLowerCase(); 204 | let url = element.getAttribute("data-preload-" + size); 205 | 206 | switch(tag){ 207 | 208 | /* ----- Video Tags ----- */ 209 | case "video": 210 | 211 | let source = document.createElement("source"); 212 | 213 | source.setAttribute("src", url); 214 | 215 | element.appendChild(source); 216 | element.load(); 217 | element.play(); 218 | 219 | break; 220 | 221 | /* ----- Img Tags ----- */ 222 | case "img": 223 | case "iframe": 224 | 225 | element.setAttribute("src", url); 226 | 227 | 228 | break; 229 | 230 | /* ----- Everything else, assumes background-image ----- */ 231 | default: 232 | 233 | element.style.backgroundImage = "url(" + url + ")"; 234 | 235 | break; 236 | } 237 | 238 | this.removeAttrs(element); 239 | } 240 | 241 | /* --- Post our message to the worker --- */ 242 | message(message){ 243 | this.worker.postMessage(message); 244 | } 245 | } -------------------------------------------------------------------------------- /src/js/_worker/workers/image-load.js: -------------------------------------------------------------------------------- 1 | /* jshint ignore: start */ 2 | self.addEventListener("message", async event => { 3 | const url = event.data; 4 | 5 | const response = await fetch(url); 6 | const blob = await response.blob(); 7 | 8 | self.postMessage({ 9 | url: url, 10 | blob: blob 11 | }); 12 | }); -------------------------------------------------------------------------------- /src/js/about/_renderer.js: -------------------------------------------------------------------------------- 1 | import Highway from "@dogstudio/highway"; 2 | import { aboutSpecificFunc } from "./anims"; 3 | 4 | /* 5 | View Events for Highway 6 | 7 | - About Page 8 | - Events are listed in their execution order 9 | -------------------------------------------------- */ 10 | class AboutRenderer extends Highway.Renderer { 11 | 12 | onEnter() { 13 | console.log("onEnter"); 14 | aboutSpecificFunc(); 15 | } 16 | 17 | onEnterCompleted() { 18 | console.log("onEnterCompleted"); 19 | } 20 | 21 | onLeave() { 22 | console.log("onLeave"); 23 | } 24 | 25 | onLeaveCompleted() { 26 | console.log("onLeaveCompleted"); 27 | } 28 | } 29 | 30 | export default AboutRenderer; 31 | -------------------------------------------------------------------------------- /src/js/about/anims.js: -------------------------------------------------------------------------------- 1 | export const aboutSpecificFunc = () => { 2 | console.log('Hello from the about page'); 3 | }; 4 | -------------------------------------------------------------------------------- /src/js/home/_renderer.js: -------------------------------------------------------------------------------- 1 | import Highway from "@dogstudio/highway"; 2 | import { homeSpecificFunc } from "./anims"; 3 | 4 | /* 5 | View Events for Highway 6 | 7 | - Home Page 8 | - Events are listed in their execution order 9 | -------------------------------------------------- */ 10 | class HomeRenderer extends Highway.Renderer { 11 | 12 | onEnter() { 13 | console.log("onEnter"); 14 | homeSpecificFunc() 15 | } 16 | 17 | onEnterCompleted() { 18 | console.log("onEnterCompleted"); 19 | } 20 | 21 | onLeave() { 22 | console.log("onLeave"); 23 | } 24 | 25 | onLeaveCompleted() { 26 | console.log("onLeaveCompleted"); 27 | } 28 | } 29 | 30 | export default HomeRenderer; 31 | -------------------------------------------------------------------------------- /src/js/home/anims.js: -------------------------------------------------------------------------------- 1 | export const homeSpecificFunc = () => { 2 | console.log('Hello from the home page'); 3 | }; 4 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | Import and run any polyfills here 3 | -------------------------------------------------- */ 4 | const ua = navigator.userAgent || navigator.vendor || window.opera; 5 | const isInstagram = (ua.indexOf("Instagram") > -1) ? true : false; 6 | 7 | if(isInstagram){ 8 | document.body.className += " instagram-browser force-mobile"; 9 | } 10 | 11 | /* 12 | Run your initilize script, make sure this is last 13 | -------------------------------------------------- */ 14 | import "./_initialize"; -------------------------------------------------------------------------------- /src/js/routing.js: -------------------------------------------------------------------------------- 1 | /* 2 | Load Plugins / Functions 3 | -------------------------------------------------- */ 4 | import Highway from "@dogstudio/highway"; 5 | import { onLeave, onEnter, onEnterCompleted } from "./_global/_renderer"; 6 | /* --- Highway Renderers --- */ 7 | import HomeRenderer from "./home/_renderer"; 8 | import AboutRenderer from "./about/_renderer"; 9 | /* --- Highway Transitions --- */ 10 | import BasicFade from "./_transitions/basicFade"; 11 | 12 | /* 13 | Setup Core 14 | -------------------------------------------------- */ 15 | export const H = new Highway.Core({ 16 | renderers: { 17 | home: HomeRenderer, 18 | about: AboutRenderer 19 | }, 20 | transitions: { 21 | default: BasicFade, 22 | contextual: { // Add transitions here that you can use by placing data-transition on an anchor, or in an H.redirect(href, transition) 23 | // footerToHero: FooterToHero 24 | } 25 | } 26 | }); 27 | 28 | /* 29 | Global Events 30 | -------------------------------------------------- */ 31 | H.on("NAVIGATE_OUT", ({ from, trigger, location })=>{ 32 | onLeave(from, trigger, location); 33 | }); 34 | 35 | H.on("NAVIGATE_IN", ({ to, trigger, location })=>{ 36 | onEnter(to, trigger, location); 37 | }); 38 | 39 | H.on("NAVIGATE_END", ({ from, to, trigger, location })=>{ 40 | onEnterCompleted(from, to, trigger, location); 41 | }); 42 | -------------------------------------------------------------------------------- /src/templates/404.php: -------------------------------------------------------------------------------- 1 |
";
5 | print_r($document);
6 | echo "
"; 7 | /* --------------- */ 8 | ?> -------------------------------------------------------------------------------- /src/templates/about.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |

Gone when 55% of about hero

8 |
9 |
10 | 14 |
15 |
16 |
17 | 18 |
19 |
20 |

I'm going past your screen

23 |
24 |
25 | 26 | 31 |
32 |
33 |
34 |
35 |

I'm going past your screen

38 |
39 |
40 | 45 |
46 |
47 |
48 |
49 |
50 |

I'm going past your screen

53 |
54 |
55 | 56 | 61 |
62 |
63 |
64 |
65 |

I'm going past your screen

68 |
69 |
70 | 75 | 76 |
77 |
78 |
79 |
80 |
81 |

I'm going past your screen

84 |
85 |
86 | 87 | 92 |
93 |
94 |
95 | 96 |
97 |
98 |
99 |
100 | 101 | -------------------------------------------------------------------------------- /src/templates/footer.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | -------------------------------------------------------------------------------- /src/templates/header.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Site title 8 | 9 | 10 | 11 | 12 | 18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /src/templates/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |

Gone when 55% of home hero

8 |
9 |
10 | 14 |
15 |
16 |
17 | 18 |
19 |
20 |

I'm going past your screen

23 |
24 |
25 | 26 | 31 |
32 |
33 |
34 |
35 |

I'm going past your screen

38 |
39 |
40 | 45 |
46 |
47 |
48 |
49 |
50 |

I'm going past your screen

53 |
54 |
55 | 56 | 61 |
62 |
63 |
64 |
65 |

I'm going past your screen

68 |
69 |
70 | 75 | 76 |
77 |
78 |
79 |
80 |
81 |

I'm going past your screen

84 |
85 |
86 | 87 | 92 |
93 |
94 |
95 | 96 |
97 |
98 |
99 |
100 | 101 | -------------------------------------------------------------------------------- /src/templates/parts/footer-content.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/parts/global-scope.php: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/templates/parts/site/site-meta.php: -------------------------------------------------------------------------------- 1 |
";
 9 | print_r($settings);
10 | echo "
"; 11 | /* --------------- */ 12 | if(isset($WPGLOBAL["document"]) && isset($WPGLOBAL["document"]->data->meta_image->url)){ 13 | $fb_img = $WPGLOBAL["document"]->data->meta_image->url; 14 | $tw_img = $WPGLOBAL["document"]->data->meta_image->url; 15 | } 16 | ?> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | data)){ ?> 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | data) && isset($settings->data->favicon->url)){ ?> 36 | 37 | url; ?>" /> 38 | url; ?>" /> 39 | url; ?>" /> 40 | url; ?>" /> 41 | url; ?>" /> 42 | url; ?>" /> 43 | 44 | url; ?>" /> 45 | 46 | url; ?>" /> 47 | url; ?>" /> 48 | url; ?>" /> 49 | 50 | 51 | 52 | url; ?>" /> 53 | url; ?>" /> 54 | url; ?>" /> 55 | url; ?>" /> 56 | url; ?>" /> 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/templates/parts/site/site-prefetch.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/templates/parts/svgs/social-facebook.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/parts/svgs/social-instagram.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/parts/svgs/social-twitter.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/site/.htaccess: -------------------------------------------------------------------------------- 1 | # Turn off all directory indexes 2 | Options All -indexes 3 | 4 | 5 | RewriteEngine On 6 | RewriteCond %{REQUEST_FILENAME} !-f 7 | RewriteRule ^([^\.]+)$ $1.php [NC,L] 8 | 9 | 10 | ################################## 11 | ################################## 12 | ################################## 13 | ################################## 14 | ################################## 15 | ########## ########## 16 | ########## HIYA BUDDY ########## 17 | ########## ########## 18 | ################################## 19 | ################################## 20 | ################################## 21 | ################################## 22 | ################################## 23 | 24 | # ---------------------------------------------------------------------- 25 | # | Cross-origin images | 26 | # ---------------------------------------------------------------------- 27 | 28 | # Send the CORS header for images when browsers request it. 29 | # 30 | # https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image 31 | # https://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html 32 | 33 | 34 | 35 | 36 | SetEnvIf Origin ":" IS_CORS 37 | Header set Access-Control-Allow-Origin "*" env=IS_CORS 38 | 39 | 40 | 41 | 42 | 43 | # ---------------------------------------------------------------------- 44 | # | Cross-origin web fonts | 45 | # ---------------------------------------------------------------------- 46 | 47 | # Allow cross-origin access to web fonts. 48 | 49 | 50 | 51 | Header set Access-Control-Allow-Origin "*" 52 | 53 | 54 | 55 | 56 | # ---------------------------------------------------------------------- 57 | # | Media types | 58 | # ---------------------------------------------------------------------- 59 | 60 | # Serve resources with the proper media types (f.k.a. MIME types). 61 | # 62 | # https://www.iana.org/assignments/media-types/media-types.xhtml 63 | # https://httpd.apache.org/docs/current/mod/mod_mime.html#addtype 64 | 65 | 66 | 67 | # Data interchange 68 | 69 | AddType application/atom+xml atom 70 | AddType application/json json map topojson 71 | AddType application/ld+json jsonld 72 | AddType application/rss+xml rss 73 | AddType application/vnd.geo+json geojson 74 | AddType application/xml rdf xml 75 | 76 | 77 | # JavaScript 78 | 79 | # Servers should use text/javascript for JavaScript resources. 80 | # https://html.spec.whatwg.org/multipage/scripting.html#scriptingLanguages 81 | 82 | AddType text/javascript js mjs 83 | 84 | 85 | # Manifest files 86 | 87 | AddType application/manifest+json webmanifest 88 | AddType application/x-web-app-manifest+json webapp 89 | AddType text/cache-manifest appcache 90 | 91 | 92 | # Media files 93 | 94 | AddType audio/mp4 f4a f4b m4a 95 | AddType audio/ogg oga ogg opus 96 | AddType image/bmp bmp 97 | AddType image/svg+xml svg svgz 98 | AddType image/webp webp 99 | AddType video/mp4 f4v f4p m4v mp4 100 | AddType video/ogg ogv 101 | AddType video/webm webm 102 | AddType video/x-flv flv 103 | 104 | # Serving `.ico` image files with a different media type 105 | # prevents Internet Explorer from displaying them as images: 106 | # https://github.com/h5bp/html5-boilerplate/commit/37b5fec090d00f38de64b591bcddcb205aadf8ee 107 | 108 | AddType image/x-icon cur ico 109 | 110 | 111 | # WebAssembly 112 | 113 | AddType application/wasm wasm 114 | 115 | 116 | # Web fonts 117 | 118 | AddType font/woff woff 119 | AddType font/woff2 woff2 120 | AddType application/vnd.ms-fontobject eot 121 | AddType font/ttf ttf 122 | AddType font/collection ttc 123 | AddType font/otf otf 124 | 125 | 126 | # Other 127 | 128 | AddType application/octet-stream safariextz 129 | AddType application/x-bb-appworld bbaw 130 | AddType application/x-chrome-extension crx 131 | AddType application/x-opera-extension oex 132 | AddType application/x-xpinstall xpi 133 | AddType text/calendar ics 134 | AddType text/markdown markdown md 135 | AddType text/vcard vcard vcf 136 | AddType text/vnd.rim.location.xloc xloc 137 | AddType text/vtt vtt 138 | AddType text/x-component htc 139 | 140 | 141 | 142 | 143 | # ---------------------------------------------------------------------- 144 | # | Character encodings | 145 | # ---------------------------------------------------------------------- 146 | 147 | # Serve all resources labeled as `text/html` or `text/plain` 148 | # with the media type `charset` parameter set to `UTF-8`. 149 | # 150 | # https://httpd.apache.org/docs/current/mod/core.html#adddefaultcharset 151 | 152 | AddDefaultCharset utf-8 153 | 154 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 155 | 156 | # Serve the following file types with the media type `charset` 157 | # parameter set to `UTF-8`. 158 | # 159 | # https://httpd.apache.org/docs/current/mod/mod_mime.html#addcharset 160 | 161 | 162 | AddCharset utf-8 .atom \ 163 | .bbaw \ 164 | .css \ 165 | .geojson \ 166 | .ics \ 167 | .js \ 168 | .json \ 169 | .jsonld \ 170 | .manifest \ 171 | .markdown \ 172 | .md \ 173 | .mjs \ 174 | .rdf \ 175 | .rss \ 176 | .topojson \ 177 | .vtt \ 178 | .webapp \ 179 | .webmanifest \ 180 | .xloc \ 181 | .xml 182 | 183 | 184 | 185 | # ---------------------------------------------------------------------- 186 | # | Compression | 187 | # ---------------------------------------------------------------------- 188 | 189 | 190 | 191 | # Force compression for mangled `Accept-Encoding` request headers 192 | # https://developer.yahoo.com/blogs/ydn/pushing-beyond-gzipping-25601.html 193 | 194 | 195 | 196 | SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding 197 | RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding 198 | 199 | 200 | 201 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 202 | 203 | # Compress all output labeled with one of the following media types. 204 | # https://httpd.apache.org/docs/current/mod/mod_filter.html#addoutputfilterbytype 205 | 206 | 207 | AddOutputFilterByType DEFLATE "application/atom+xml" \ 208 | "application/javascript" \ 209 | "application/json" \ 210 | "application/ld+json" \ 211 | "application/manifest+json" \ 212 | "application/rdf+xml" \ 213 | "application/rss+xml" \ 214 | "application/schema+json" \ 215 | "application/vnd.geo+json" \ 216 | "application/vnd.ms-fontobject" \ 217 | "application/wasm" \ 218 | "application/x-font-ttf" \ 219 | "application/x-javascript" \ 220 | "application/x-web-app-manifest+json" \ 221 | "application/xhtml+xml" \ 222 | "application/xml" \ 223 | "font/collection" \ 224 | "font/eot" \ 225 | "font/opentype" \ 226 | "font/otf" \ 227 | "font/ttf" \ 228 | "image/bmp" \ 229 | "image/svg+xml" \ 230 | "image/vnd.microsoft.icon" \ 231 | "image/x-icon" \ 232 | "text/cache-manifest" \ 233 | "text/calendar" \ 234 | "text/css" \ 235 | "text/html" \ 236 | "text/javascript" \ 237 | "text/plain" \ 238 | "text/markdown" \ 239 | "text/vcard" \ 240 | "text/vnd.rim.location.xloc" \ 241 | "text/vtt" \ 242 | "text/x-component" \ 243 | "text/x-cross-domain-policy" \ 244 | "text/xml" 245 | 246 | 247 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 248 | 249 | # Map the following filename extensions to the specified 250 | # encoding type in order to make Apache serve the file types 251 | # with the appropriate `Content-Encoding` response header 252 | # (do note that this will NOT make Apache compress them!). 253 | # 254 | # If these files types would be served without an appropriate 255 | # `Content-Enable` response header, client applications (e.g.: 256 | # browsers) wouldn't know that they first need to uncompress 257 | # the response, and thus, wouldn't be able to understand the 258 | # content. 259 | # 260 | # https://httpd.apache.org/docs/current/mod/mod_mime.html#addencoding 261 | 262 | 263 | AddEncoding gzip svgz 264 | 265 | 266 | 267 | 268 | 269 | 270 | # ---------------------------------------------------------------------- 271 | # | ETags | 272 | # ---------------------------------------------------------------------- 273 | 274 | # Remove `ETags` as resources are sent with far-future expires headers. 275 | # 276 | # https://developer.yahoo.com/performance/rules.html#etags 277 | # https://tools.ietf.org/html/rfc7232#section-2.3 278 | 279 | # `FileETag None` doesn't work in all cases. 280 | 281 | Header unset ETag 282 | 283 | 284 | FileETag None 285 | 286 | 287 | # ---------------------------------------------------------------------- 288 | # | Expires headers | 289 | # ---------------------------------------------------------------------- 290 | 291 | # Serve resources with far-future expires headers. 292 | # 293 | # (!) If you don't control versioning with filename-based 294 | # cache busting, you should consider lowering the cache times 295 | # to something like one week. 296 | # 297 | # https://httpd.apache.org/docs/current/mod/mod_expires.html 298 | 299 | 300 | 301 | ExpiresActive on 302 | ExpiresDefault "access plus 1 month" 303 | 304 | # CSS 305 | 306 | ExpiresByType text/css "access plus 1 year" 307 | 308 | 309 | # Data interchange 310 | 311 | ExpiresByType application/atom+xml "access plus 1 hour" 312 | ExpiresByType application/rdf+xml "access plus 1 hour" 313 | ExpiresByType application/rss+xml "access plus 1 hour" 314 | 315 | ExpiresByType application/json "access plus 0 seconds" 316 | ExpiresByType application/ld+json "access plus 0 seconds" 317 | ExpiresByType application/schema+json "access plus 0 seconds" 318 | ExpiresByType application/vnd.geo+json "access plus 0 seconds" 319 | ExpiresByType application/xml "access plus 0 seconds" 320 | ExpiresByType text/calendar "access plus 0 seconds" 321 | ExpiresByType text/xml "access plus 0 seconds" 322 | 323 | 324 | # Favicon (cannot be renamed!) and cursor images 325 | 326 | ExpiresByType image/vnd.microsoft.icon "access plus 1 week" 327 | ExpiresByType image/x-icon "access plus 1 week" 328 | 329 | # HTML 330 | 331 | ExpiresByType text/html "access plus 0 seconds" 332 | 333 | 334 | # JavaScript 335 | 336 | ExpiresByType application/javascript "access plus 1 year" 337 | ExpiresByType application/x-javascript "access plus 1 year" 338 | ExpiresByType text/javascript "access plus 1 year" 339 | 340 | 341 | # Manifest files 342 | 343 | ExpiresByType application/manifest+json "access plus 1 week" 344 | ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" 345 | ExpiresByType text/cache-manifest "access plus 0 seconds" 346 | 347 | 348 | # Markdown 349 | 350 | ExpiresByType text/markdown "access plus 0 seconds" 351 | 352 | 353 | # Media files 354 | 355 | ExpiresByType audio/ogg "access plus 1 month" 356 | ExpiresByType image/bmp "access plus 1 month" 357 | ExpiresByType image/gif "access plus 1 month" 358 | ExpiresByType image/jpeg "access plus 1 month" 359 | ExpiresByType image/png "access plus 1 month" 360 | ExpiresByType image/svg+xml "access plus 1 month" 361 | ExpiresByType image/webp "access plus 1 month" 362 | ExpiresByType video/mp4 "access plus 1 month" 363 | ExpiresByType video/ogg "access plus 1 month" 364 | ExpiresByType video/webm "access plus 1 month" 365 | 366 | 367 | # WebAssembly 368 | 369 | ExpiresByType application/wasm "access plus 1 year" 370 | 371 | 372 | # Web fonts 373 | 374 | # Collection 375 | ExpiresByType font/collection "access plus 1 month" 376 | 377 | # Embedded OpenType (EOT) 378 | ExpiresByType application/vnd.ms-fontobject "access plus 1 month" 379 | ExpiresByType font/eot "access plus 1 month" 380 | 381 | # OpenType 382 | ExpiresByType font/opentype "access plus 1 month" 383 | ExpiresByType font/otf "access plus 1 month" 384 | 385 | # TrueType 386 | ExpiresByType application/x-font-ttf "access plus 1 month" 387 | ExpiresByType font/ttf "access plus 1 month" 388 | 389 | # Web Open Font Format (WOFF) 1.0 390 | ExpiresByType application/font-woff "access plus 1 month" 391 | ExpiresByType application/x-font-woff "access plus 1 month" 392 | ExpiresByType font/woff "access plus 1 month" 393 | 394 | # Web Open Font Format (WOFF) 2.0 395 | ExpiresByType application/font-woff2 "access plus 1 month" 396 | ExpiresByType font/woff2 "access plus 1 month" 397 | 398 | 399 | # Other 400 | 401 | ExpiresByType text/x-cross-domain-policy "access plus 1 week" 402 | 403 | -------------------------------------------------------------------------------- /web/site/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |

Gone when 55% of home hero

8 |
9 |
10 | 14 |
15 |
16 |
17 | 18 |
19 |
20 |

I'm going past your screen

23 |
24 |
25 | 26 | 31 |
32 |
33 |
34 |
35 |

I'm going past your screen

38 |
39 |
40 | 45 |
46 |
47 |
48 |
49 |
50 |

I'm going past your screen

53 |
54 |
55 | 56 | 61 |
62 |
63 |
64 |
65 |

I'm going past your screen

68 |
69 |
70 | 75 | 76 |
77 |
78 |
79 |
80 |
81 |

I'm going past your screen

84 |
85 |
86 | 87 | 92 |
93 |
94 |
95 | 96 |
97 |
98 |
99 |
100 | 101 | -------------------------------------------------------------------------------- /web/site/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /server -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* --- CDN Configuration --- 2 | * 3 | * Make sure to fill these out with the folder/bucket IDs from the CDN 4 | * Having this enabled will REQUIRE a cdnkey.json file 5 | */ 6 | 7 | /* global __dirname */ 8 | const webpack = require("webpack"); 9 | const path = require("path"); 10 | const entry = require("webpack-glob-entry"); 11 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 12 | const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 13 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 14 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 15 | const FriendlyErrorsPlugin = require("friendly-errors-webpack-plugin"); 16 | const StyleLintPlugin = require("stylelint-webpack-plugin"); 17 | const GenerateJSONPlugin = require("generate-json-webpack-plugin"); 18 | const WebpackGoogleCloudStoragePlugin = require("webpack-google-cloud-storage-plugin"); 19 | 20 | function recursiveIssuer(m) { 21 | if (m.issuer) { 22 | return recursiveIssuer(m.issuer); 23 | } else if (m.name) { 24 | return m.name; 25 | } else { 26 | return false; 27 | } 28 | } 29 | 30 | let compiledPath = "../../../../../compiled/"; 31 | 32 | module.exports = { 33 | mode: "production", 34 | entry: { 35 | "js/main.js": [ 36 | "./src/js/main.js" 37 | ], 38 | "js/workers/image-load.js": [ 39 | "./src/js/_worker/workers/image-load.js" 40 | ], 41 | "../../../../../compiled/init": [ 42 | "./src/css/_critical/init.scss" 43 | ], 44 | "../../../../../compiled/fonts": [ 45 | "./src/css/_fonts.css" 46 | ], 47 | "../../../../../compiled/main": [ 48 | "./src/css/main.scss" 49 | ] 50 | }, 51 | output: { 52 | path: path.resolve(__dirname, "./web/site/assets/"), 53 | filename: "[name]", 54 | chunkFilename: compiledPath + "[id].chunk" 55 | }, 56 | resolve: { 57 | extensions: [".webpack.js", ".web.js", ".js", ".jsx"] 58 | }, 59 | optimization: { 60 | splitChunks: { 61 | cacheGroups: { 62 | init: { 63 | name: "css/_init.css", 64 | test: (m,c,entry = "../../../../../compiled/init") => m.constructor.name === "CssModule" && recursiveIssuer(m) === entry, 65 | chunks: "initial", 66 | enforce: true 67 | }, 68 | fonts: { 69 | name: "css/_fonts.css", 70 | test: (m,c,entry = "../../../../../compiled/fonts") => m.constructor.name === "CssModule" && recursiveIssuer(m) === entry, 71 | chunks: "initial", 72 | enforce: true 73 | }, 74 | main: { 75 | name: "css/main.css", 76 | test: (m,c,entry = "../../../../../compiled/main") => m.constructor.name === "CssModule" && recursiveIssuer(m) === entry, 77 | chunks: "initial", 78 | enforce: true 79 | }, 80 | default: false 81 | } 82 | }, 83 | minimizer: [ 84 | new UglifyJsPlugin({ 85 | test: [ 86 | /\.js$/i 87 | ], 88 | sourceMap: false, 89 | uglifyOptions: { 90 | ecma: 5, 91 | compress: { 92 | warnings: false 93 | } 94 | } 95 | }), 96 | new OptimizeCssAssetsPlugin({ 97 | assetNameRegExp: /\.css$/i, 98 | cssProcessorOptions: { 99 | zindex: false 100 | } 101 | }) 102 | ] 103 | }, 104 | module: { 105 | rules: [ 106 | { 107 | test: /\.js$/, 108 | exclude: /node_modules/, 109 | use: [ 110 | { 111 | loader: "babel-loader", 112 | options: { 113 | presets: [ 114 | [ 115 | "@babel/preset-env", { 116 | "targets": { 117 | "node": "current", 118 | "browsers": [ "last 2 versions", "ie 11" ] 119 | } 120 | } 121 | ] 122 | ], 123 | plugins: [ 124 | [ 125 | "@babel/plugin-transform-runtime", { 126 | "regenerator": true 127 | } 128 | ] 129 | ] 130 | } 131 | }, 132 | { 133 | loader: "eslint-loader" 134 | } 135 | ] 136 | }, 137 | { 138 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 139 | loader: "url-loader?limit=100000" 140 | }, 141 | { 142 | test: /\.(sa|sc|c)ss$/, 143 | use: [ 144 | MiniCssExtractPlugin.loader, 145 | { 146 | loader: "css-loader", 147 | options: { 148 | importLoaders: 1, 149 | minimize: true 150 | } 151 | }, 152 | { 153 | loader: "postcss-loader", 154 | options: { 155 | config: { 156 | path: path.resolve(__dirname, "./postcss.config.js") 157 | } 158 | } 159 | }, 160 | { 161 | loader: "sass-loader" 162 | } 163 | ] 164 | } 165 | ] 166 | }, 167 | plugins: [ 168 | new MiniCssExtractPlugin({ 169 | filename: "[name]" 170 | }), 171 | new CopyWebpackPlugin([ 172 | { 173 | from: "./src/images", 174 | to: "./images", 175 | force: true, 176 | ignore: [ 177 | ".gitkeep" 178 | ] 179 | }, 180 | { 181 | from: "./src/fonts", 182 | to: "./fonts", 183 | force: true, 184 | ignore: [ 185 | ".gitkeep" 186 | ] 187 | }, 188 | // { 189 | // from: "./src/css/_fonts.css", 190 | // to: "./fonts", 191 | // force: true, 192 | // ignore: [ 193 | // ".gitkeep" 194 | // ] 195 | // }, 196 | { 197 | from: "./src/templates", 198 | to: "../../site", 199 | force: true, 200 | ignore: [ 201 | ".gitkeep" 202 | ] 203 | } 204 | ]), 205 | new StyleLintPlugin(), 206 | new webpack.NoEmitOnErrorsPlugin(), 207 | new FriendlyErrorsPlugin() 208 | ] 209 | }; --------------------------------------------------------------------------------