├── .gitignore ├── styles ├── utils │ └── variables.scss ├── index.scss ├── base │ ├── loader.scss │ ├── base.scss │ └── frame.scss └── mixins │ └── links.scss ├── cover.jpg ├── favicon.ico ├── app ├── images │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ ├── 9.jpg │ ├── 10.jpg │ ├── 11.jpg │ └── 12.jpg ├── fonts │ ├── forma.png │ └── freight.png ├── shaders │ ├── background-fragment.glsl │ ├── background-vertex.glsl │ ├── text-vertex.glsl │ ├── text-fragment.glsl │ ├── image-vertex.glsl │ └── image-fragment.glsl ├── utils │ └── math.js ├── Background.js ├── Title.js ├── Number.js ├── Media.js └── index.js ├── InfiniteCircularGallery ├── 046c4e6a4d9afee84b47bbd491c844d9.jpg ├── 0849520e661c98b329e51d946e56d410.jpg ├── 0bbe7191e25aa7960588ce2dfdedf36c.png ├── 26bf8d0b87d385dc8c4973a33b6cbfaf.jpg ├── 2a466a9468dd55f353d6d1cdbe26e652.jpg ├── 30e5c860aeb19be878d365d0022c1c28.jpg ├── 3338c61dd22a1e5dbf18491278ffb743.jpg ├── 43dc789914eaf207689d9cb6f455d116.jpg ├── 6f0872f4a4b6c06bb014e6b32b578ff2.jpg ├── 7f4180c985ebabc8485ff69ae14bdcdd.jpg ├── 8fc86a14c785e2b6c25cf727215d5ab5.jpg ├── a1710b67888160d9a8a89e708a261720.jpg ├── c4170d08dcd3267f4d4c10c3c3f8364e.png ├── da8cecd55f5cecd389810356e78ff6ca.jpg ├── main.js.LICENSE.txt ├── index.html └── main.css ├── .editorconfig ├── webpack.config.development.js ├── webpack.config.build.js ├── README.md ├── LICENSE ├── index.html ├── package.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.cache 3 | package-lock.json -------------------------------------------------------------------------------- /styles/utils/variables.scss: -------------------------------------------------------------------------------- 1 | $color-background: #cbcabd; 2 | $color-gray: #545050; 3 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/cover.jpg -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/favicon.ico -------------------------------------------------------------------------------- /app/images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/1.jpg -------------------------------------------------------------------------------- /app/images/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/2.jpg -------------------------------------------------------------------------------- /app/images/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/3.jpg -------------------------------------------------------------------------------- /app/images/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/4.jpg -------------------------------------------------------------------------------- /app/images/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/5.jpg -------------------------------------------------------------------------------- /app/images/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/6.jpg -------------------------------------------------------------------------------- /app/images/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/7.jpg -------------------------------------------------------------------------------- /app/images/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/8.jpg -------------------------------------------------------------------------------- /app/images/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/9.jpg -------------------------------------------------------------------------------- /app/fonts/forma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/fonts/forma.png -------------------------------------------------------------------------------- /app/images/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/10.jpg -------------------------------------------------------------------------------- /app/images/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/11.jpg -------------------------------------------------------------------------------- /app/images/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/images/12.jpg -------------------------------------------------------------------------------- /app/fonts/freight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/app/fonts/freight.png -------------------------------------------------------------------------------- /InfiniteCircularGallery/046c4e6a4d9afee84b47bbd491c844d9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/046c4e6a4d9afee84b47bbd491c844d9.jpg -------------------------------------------------------------------------------- /InfiniteCircularGallery/0849520e661c98b329e51d946e56d410.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/0849520e661c98b329e51d946e56d410.jpg -------------------------------------------------------------------------------- /InfiniteCircularGallery/0bbe7191e25aa7960588ce2dfdedf36c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/0bbe7191e25aa7960588ce2dfdedf36c.png -------------------------------------------------------------------------------- /InfiniteCircularGallery/26bf8d0b87d385dc8c4973a33b6cbfaf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/26bf8d0b87d385dc8c4973a33b6cbfaf.jpg -------------------------------------------------------------------------------- /InfiniteCircularGallery/2a466a9468dd55f353d6d1cdbe26e652.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/2a466a9468dd55f353d6d1cdbe26e652.jpg -------------------------------------------------------------------------------- /InfiniteCircularGallery/30e5c860aeb19be878d365d0022c1c28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/30e5c860aeb19be878d365d0022c1c28.jpg -------------------------------------------------------------------------------- /InfiniteCircularGallery/3338c61dd22a1e5dbf18491278ffb743.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/3338c61dd22a1e5dbf18491278ffb743.jpg -------------------------------------------------------------------------------- /InfiniteCircularGallery/43dc789914eaf207689d9cb6f455d116.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/43dc789914eaf207689d9cb6f455d116.jpg -------------------------------------------------------------------------------- /InfiniteCircularGallery/6f0872f4a4b6c06bb014e6b32b578ff2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/6f0872f4a4b6c06bb014e6b32b578ff2.jpg -------------------------------------------------------------------------------- /InfiniteCircularGallery/7f4180c985ebabc8485ff69ae14bdcdd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/7f4180c985ebabc8485ff69ae14bdcdd.jpg -------------------------------------------------------------------------------- /InfiniteCircularGallery/8fc86a14c785e2b6c25cf727215d5ab5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/8fc86a14c785e2b6c25cf727215d5ab5.jpg -------------------------------------------------------------------------------- /InfiniteCircularGallery/a1710b67888160d9a8a89e708a261720.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/a1710b67888160d9a8a89e708a261720.jpg -------------------------------------------------------------------------------- /InfiniteCircularGallery/c4170d08dcd3267f4d4c10c3c3f8364e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/c4170d08dcd3267f4d4c10c3c3f8364e.png -------------------------------------------------------------------------------- /InfiniteCircularGallery/da8cecd55f5cecd389810356e78ff6ca.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bizarro/infinite-circular-webgl-gallery/HEAD/InfiniteCircularGallery/da8cecd55f5cecd389810356e78ff6ca.jpg -------------------------------------------------------------------------------- /styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './utils/variables.scss'; 2 | 3 | @import './base/base.scss'; 4 | @import './base/frame.scss'; 5 | @import './base/loader.scss'; 6 | 7 | @import './mixins/links.scss'; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /app/shaders/background-fragment.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | uniform float uAlpha; 4 | uniform vec3 uColor; 5 | 6 | void main() { 7 | gl_FragColor.rgb = uColor; 8 | gl_FragColor.a = 1.0; 9 | } 10 | -------------------------------------------------------------------------------- /app/shaders/background-vertex.glsl: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | attribute vec3 normal; 3 | 4 | uniform mat4 modelViewMatrix; 5 | uniform mat4 projectionMatrix; 6 | uniform mat3 normalMatrix; 7 | 8 | void main() { 9 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /app/shaders/text-vertex.glsl: -------------------------------------------------------------------------------- 1 | attribute vec2 uv; 2 | attribute vec3 position; 3 | 4 | uniform mat4 modelViewMatrix; 5 | uniform mat4 projectionMatrix; 6 | 7 | varying vec2 vUv; 8 | 9 | void main() { 10 | vUv = uv; 11 | 12 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 13 | } 14 | -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const path = require('path') 3 | 4 | const config = require('./webpack.config') 5 | 6 | module.exports = merge(config, { 7 | mode: 'development', 8 | 9 | devtool: 'inline-source-map', 10 | 11 | devServer: { 12 | writeToDisk: true 13 | }, 14 | 15 | output: { 16 | path: path.join(__dirname, 'InfiniteCircularGallery') 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /app/utils/math.js: -------------------------------------------------------------------------------- 1 | export function lerp (p1, p2, t) { 2 | return p1 + (p2 - p1) * t 3 | } 4 | 5 | export function map (num, min1, max1, min2, max2, round = false) { 6 | const num1 = (num - min1) / (max1 - min1) 7 | const num2 = (num1 * (max2 - min2)) + min2 8 | 9 | if (round) return Math.round(num2) 10 | 11 | return num2 12 | } 13 | 14 | export function random(min, max) { 15 | return Math.random() * (max - min) + min 16 | } 17 | -------------------------------------------------------------------------------- /app/shaders/text-fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform vec3 uColor; 2 | uniform sampler2D tMap; 3 | 4 | varying vec2 vUv; 5 | 6 | void main() { 7 | vec3 color = texture2D(tMap, vUv).rgb; 8 | 9 | float signed = max(min(color.r, color.g), min(max(color.r, color.g), color.b)) - 0.5; 10 | float d = fwidth(signed); 11 | float alpha = smoothstep(-d, d, signed); 12 | 13 | if (alpha < 0.02) discard; 14 | 15 | gl_FragColor = vec4(uColor, alpha); 16 | } 17 | -------------------------------------------------------------------------------- /webpack.config.build.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | const { merge } = require('webpack-merge') 6 | const config = require('./webpack.config') 7 | 8 | module.exports = merge(config, { 9 | mode: 'production', 10 | 11 | output: { 12 | path: path.join(__dirname, 'InfiniteCircularGallery') 13 | }, 14 | 15 | plugins: [ 16 | new CleanWebpackPlugin() 17 | ] 18 | }) 19 | -------------------------------------------------------------------------------- /app/shaders/image-vertex.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | attribute vec3 position; 4 | attribute vec2 uv; 5 | 6 | uniform mat4 modelViewMatrix; 7 | uniform mat4 projectionMatrix; 8 | 9 | uniform float uTime; 10 | uniform float uSpeed; 11 | 12 | varying vec2 vUv; 13 | 14 | void main() { 15 | vUv = uv; 16 | 17 | vec3 p = position; 18 | 19 | p.z = (sin(p.x * 4.0 + uTime) * 1.5 + cos(p.y * 2.0 + uTime) * 1.5) * (0.1 + uSpeed * 0.5); 20 | 21 | gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0); 22 | } 23 | -------------------------------------------------------------------------------- /InfiniteCircularGallery/main.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if an event is supported in the current execution environment. 3 | * 4 | * NOTE: This will not work correctly for non-generic events such as `change`, 5 | * `reset`, `load`, `error`, and `select`. 6 | * 7 | * Borrows from Modernizr. 8 | * 9 | * @param {string} eventNameSuffix Event name, e.g. "click". 10 | * @param {?boolean} capture Check if the capture phase is supported. 11 | * @return {boolean} True if the event is supported. 12 | * @internal 13 | * @license Modernizr 3.0.0pre (Custom Build) | MIT 14 | */ 15 | -------------------------------------------------------------------------------- /app/shaders/image-fragment.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | uniform vec2 uImageSizes; 4 | uniform vec2 uPlaneSizes; 5 | uniform sampler2D tMap; 6 | 7 | varying vec2 vUv; 8 | 9 | void main() { 10 | vec2 ratio = vec2( 11 | min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0), 12 | min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0) 13 | ); 14 | 15 | vec2 uv = vec2( 16 | vUv.x * ratio.x + (1.0 - ratio.x) * 0.5, 17 | vUv.y * ratio.y + (1.0 - ratio.y) * 0.5 18 | ); 19 | 20 | gl_FragColor.rgb = texture2D(tMap, uv).rgb; 21 | gl_FragColor.a = 1.0; 22 | } 23 | -------------------------------------------------------------------------------- /styles/base/loader.scss: -------------------------------------------------------------------------------- 1 | /* Page Loader */ 2 | html::after { 3 | content: ''; 4 | position: fixed; 5 | z-index: 1000; 6 | top: 50%; 7 | left: 50%; 8 | width: 60px; 9 | height: 60px; 10 | margin: -30px 0 0 -30px; 11 | border-radius: 50%; 12 | opacity: 0.4; 13 | background: $color-gray; 14 | animation: loaderAnim 0.7s linear infinite alternate forwards; 15 | transition: opacity 0.4s ease; 16 | } 17 | 18 | html.loaded::after { 19 | animation-play-state: paused; 20 | opacity: 0 !important; 21 | } 22 | 23 | @keyframes loaderAnim { 24 | to { 25 | opacity: 1; 26 | transform: scale3d(0.5,0.5,1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /InfiniteCircularGallery/index.html: -------------------------------------------------------------------------------- 1 | Infinite Circular Gallery | Codrops

Infinite Circular Gallery

using OGL with shaders

-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Infinite Circular WebGL Gallery 2 | 3 | A tutorial explaining how to build an infinite circular gallery using WebGL with OGL and GLSL Shaders. 4 | 5 | ![Final Result](cover.jpg) 6 | 7 | [Article on Codrops](https://tympanus.net/codrops/?p=53491) 8 | 9 | [Demo](https://codrops.com/Tutorials/InfiniteCircularGallery/) 10 | 11 | ## Installation 12 | 13 | Install dependencies: 14 | 15 | ``` 16 | npm install 17 | ``` 18 | 19 | Compile the code for development and start a local server: 20 | 21 | ``` 22 | npm start 23 | ``` 24 | 25 | Create the build: 26 | 27 | ``` 28 | npm run build 29 | ``` 30 | 31 | ## Credits 32 | 33 | - https://unsplash.com/@zane404 34 | 35 | ## Misc 36 | 37 | Follow Luis Henrique Bizarro: [Website](https://bizar.ro/), [Twitter](https://twitter.com/lhbizarro), [GitHub](https://github.com/lhbizarro) 38 | 39 | Follow Codrops: [Twitter](http://www.twitter.com/codrops), [Facebook](http://www.facebook.com/codrops), [GitHub](https://github.com/codrops), [Instagram](https://www.instagram.com/codropsss/) 40 | 41 | ## License 42 | [MIT](LICENSE) 43 | 44 | Made with :blue_heart: by [Codrops](http://www.codrops.com) 45 | -------------------------------------------------------------------------------- /styles/base/base.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | *, 7 | *::after, 8 | *::before { 9 | box-sizing: border-box; 10 | } 11 | 12 | html { 13 | background: $color-background; 14 | color: $color-gray; 15 | font-size: calc(100vw / 1920 * 10); 16 | height: 100%; 17 | left: 0; 18 | position: fixed; 19 | top: 0; 20 | user-select: none; 21 | width: 100%; 22 | } 23 | 24 | body { 25 | font-family: 'halyard-display', sans-serif; 26 | font-size: 15px; 27 | margin: 0; 28 | -moz-osx-font-smoothing: grayscale; 29 | -webkit-font-smoothing: antialiased; 30 | overscroll-behavior-y: none; 31 | 32 | .loaded &:after { 33 | opacity: 0.06; 34 | } 35 | } 36 | 37 | img { 38 | display: block; 39 | } 40 | 41 | canvas { 42 | height: 100%; 43 | left: 0; 44 | opacity: 0; 45 | position: fixed; 46 | top: 0; 47 | transition: opacity 1s ease; 48 | width: 100%; 49 | 50 | .loaded & { 51 | opacity: 1 52 | } 53 | } 54 | 55 | a { 56 | @extend %link--hidden; 57 | 58 | color: $color-gray; 59 | outline: none; 60 | text-decoration: none; 61 | 62 | &:hover, 63 | &:focus { 64 | outline: none; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /styles/mixins/links.scss: -------------------------------------------------------------------------------- 1 | %link__wrapper { 2 | display: inline-block; 3 | overflow: hidden; 4 | position: relative; 5 | vertical-align: top; 6 | } 7 | 8 | %link__line { 9 | background: currentColor; 10 | bottom: 0; 11 | content: ''; 12 | height: 1px; 13 | left: 0; 14 | position: absolute; 15 | transition: transform 0.4s ease; 16 | width: 100%; 17 | } 18 | 19 | %link__line--visible { 20 | transform: scaleX(1); 21 | transform-origin: left center; 22 | } 23 | 24 | %link__line--hidden { 25 | transform: scaleX(0); 26 | transform-origin: right center; 27 | } 28 | 29 | %link { 30 | @extend %link__wrapper; 31 | 32 | display: inline-block; 33 | 34 | &:after { 35 | @extend %link__line; 36 | @extend %link__line--visible; 37 | } 38 | 39 | &:hover { 40 | &:after { 41 | @extend %link__line--hidden; 42 | } 43 | } 44 | } 45 | 46 | %link--hidden { 47 | @extend %link__wrapper; 48 | 49 | display: inline-block; 50 | 51 | &:after { 52 | @extend %link__line; 53 | @extend %link__line--hidden; 54 | } 55 | 56 | &:hover { 57 | &:after { 58 | @extend %link__line--visible; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009 - 2020 [Codrops](https://tympanus.net/codrops) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Infinite Circular Gallery | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |

Infinite Circular Gallery

18 |

using OGL with shaders

19 |
20 | 25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /styles/base/frame.scss: -------------------------------------------------------------------------------- 1 | .frame { 2 | font-size: 15px; 3 | padding: 3rem; 4 | text-align: center; 5 | position: relative; 6 | z-index: 1000; 7 | } 8 | 9 | .frame__title { 10 | font-size: 15px; 11 | margin: 0 0 15px; 12 | font-weight: normal; 13 | } 14 | 15 | .frame__links { 16 | margin-top: 15px; 17 | } 18 | 19 | .frame__links a:not(:last-child) { 20 | margin-right: 15px; 21 | } 22 | 23 | @media screen and (min-width: 53em) { 24 | .frame { 25 | position: fixed; 26 | text-align: left; 27 | z-index: 100; 28 | top: 0; 29 | left: 0; 30 | display: grid; 31 | align-content: space-between; 32 | width: 100%; 33 | max-width: none; 34 | height: 100vh; 35 | padding: 3rem; 36 | pointer-events: none; 37 | grid-template-columns: 75% 25%; 38 | grid-template-rows: auto auto auto; 39 | grid-template-areas: 'title links' 40 | '... ...' 41 | 'credits demos'; 42 | } 43 | 44 | .frame__title-wrap { 45 | grid-area: title; 46 | display: flex; 47 | } 48 | 49 | .frame__title { 50 | margin: 0; 51 | } 52 | 53 | .frame__tagline { 54 | position: relative; 55 | margin: 0 0 0 1rem; 56 | padding: 0 0 0 1rem; 57 | opacity: 0.5; 58 | } 59 | 60 | .frame__demos { 61 | margin: 0; 62 | grid-area: demos; 63 | justify-self: end; 64 | } 65 | 66 | .frame__links { 67 | grid-area: links; 68 | padding: 0; 69 | margin: 0; 70 | justify-self: end; 71 | white-space: nowrap; 72 | } 73 | 74 | .frame a { 75 | pointer-events: auto; 76 | } 77 | 78 | .frame__credits { 79 | grid-area: credits; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infinite-circular-webgl-gallery", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "cross-env NODE_ENV=dev webpack serve --progress --config webpack.config.development.js", 6 | "build": "webpack --progress --config webpack.config.build.js", 7 | "lint": "eslint *.js" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "^7.12.10", 11 | "auto-bind": "^4.0.0", 12 | "autoprefix": "^1.0.1", 13 | "autoprefixer": "^10.2.4", 14 | "babel-eslint": "^10.1.0", 15 | "babel-loader": "^8.2.2", 16 | "browser-sync": "^2.26.14", 17 | "browser-sync-webpack-plugin": "^2.3.0", 18 | "clean-webpack-plugin": "^3.0.0", 19 | "cname-webpack-plugin": "^3.0.0", 20 | "concurrently": "^5.3.0", 21 | "cross-env": "^7.0.3", 22 | "css-loader": "^5.0.1", 23 | "cssnano": "^4.1.10", 24 | "dat.gui": "^0.7.7", 25 | "eslint": "^7.18.0", 26 | "eslint-config-standard": "^16.0.2", 27 | "eslint-loader": "^4.0.2", 28 | "eslint-plugin-flowtype": "^5.2.0", 29 | "eslint-plugin-import": "^2.22.1", 30 | "eslint-plugin-node": "^11.1.0", 31 | "eslint-plugin-promise": "^4.2.1", 32 | "eslint-plugin-standard": "^5.0.0", 33 | "file-loader": "^6.2.0", 34 | "fontfaceobserver": "^2.1.0", 35 | "glslify-loader": "^2.0.0", 36 | "gsap": "^3.6.0", 37 | "html-webpack-plugin": "^4.5.2", 38 | "include-media": "^1.4.9", 39 | "lodash": "^4.17.20", 40 | "mini-css-extract-plugin": "^1.3.5", 41 | "node-sass": "^5.0.0", 42 | "nodelist-foreach-polyfill": "^1.2.0", 43 | "normalize-wheel": "^1.0.1", 44 | "ogl": "0.0.61", 45 | "postcss": "^8.2.4", 46 | "postcss-loader": "^4.1.0", 47 | "prefix": "^1.0.0", 48 | "pug-loader": "^2.4.0", 49 | "raw-loader": "^4.0.2", 50 | "sass-loader": "^10.1.1", 51 | "stats.js": "^0.17.0", 52 | "webpack": "^5.19.0", 53 | "webpack-cli": "^4.4.0", 54 | "webpack-dev-server": "^3.11.2", 55 | "webpack-merge": "^5.7.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /InfiniteCircularGallery/main.css: -------------------------------------------------------------------------------- 1 | *{margin:0;padding:0}*,*::after,*::before{box-sizing:border-box}html{background:#cbcabd;color:#545050;font-size:calc(100vw / 1920 * 10);height:100%;left:0;position:fixed;top:0;user-select:none;width:100%}body{font-family:'halyard-display', sans-serif;font-size:15px;margin:0;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;overscroll-behavior-y:none}.loaded body:after{opacity:0.06}img{display:block}canvas{height:100%;left:0;opacity:0;position:fixed;top:0;transition:opacity 1s ease;width:100%}.loaded canvas{opacity:1}a{color:#545050;outline:none;text-decoration:none}a:hover,a:focus{outline:none}.frame{font-size:15px;padding:3rem;text-align:center;position:relative;z-index:1000}.frame__title{font-size:15px;margin:0 0 15px;font-weight:normal}.frame__links{margin-top:15px}.frame__links a:not(:last-child){margin-right:15px}@media screen and (min-width: 53em){.frame{position:fixed;text-align:left;z-index:100;top:0;left:0;display:grid;align-content:space-between;width:100%;max-width:none;height:100vh;padding:3rem;pointer-events:none;grid-template-columns:75% 25%;grid-template-rows:auto auto auto;grid-template-areas:'title links' '... ...' 'credits demos'}.frame__title-wrap{grid-area:title;display:flex}.frame__title{margin:0}.frame__tagline{position:relative;margin:0 0 0 1rem;padding:0 0 0 1rem;opacity:0.5}.frame__demos{margin:0;grid-area:demos;justify-self:end}.frame__links{grid-area:links;padding:0;margin:0;justify-self:end;white-space:nowrap}.frame a{pointer-events:auto}.frame__credits{grid-area:credits}}html::after{content:'';position:fixed;z-index:1000;top:50%;left:50%;width:60px;height:60px;margin:-30px 0 0 -30px;border-radius:50%;opacity:0.4;background:#545050;animation:loaderAnim 0.7s linear infinite alternate forwards;transition:opacity 0.4s ease}html.loaded::after{animation-play-state:paused;opacity:0 !important}@keyframes loaderAnim{to{opacity:1;transform:scale3d(0.5, 0.5, 1)}}a{display:inline-block;overflow:hidden;position:relative;vertical-align:top}a:after{background:currentColor;bottom:0;content:'';height:1px;left:0;position:absolute;transition:transform 0.4s ease;width:100%}a:hover:after{transform:scaleX(1);transform-origin:left center}a:after{transform:scaleX(0);transform-origin:right center}a{display:inline-block} 2 | 3 | -------------------------------------------------------------------------------- /app/Background.js: -------------------------------------------------------------------------------- 1 | import { Color, Mesh, Plane, Program } from 'ogl' 2 | 3 | import fragment from 'shaders/background-fragment.glsl' 4 | import vertex from 'shaders/background-vertex.glsl' 5 | 6 | import { random } from 'utils/math' 7 | 8 | export default class { 9 | constructor ({ gl, scene, viewport }) { 10 | this.gl = gl 11 | this.scene = scene 12 | this.viewport = viewport 13 | 14 | const geometry = new Plane(this.gl) 15 | const program = new Program(this.gl, { 16 | vertex, 17 | fragment, 18 | uniforms: { 19 | uColor: { value: new Color('#c4c3b6') } 20 | }, 21 | transparent: true 22 | }) 23 | 24 | this.meshes = [] 25 | 26 | for (let i = 0; i < 50; i++) { 27 | let mesh = new Mesh(this.gl, { 28 | geometry, 29 | program, 30 | }) 31 | 32 | const scale = random(0.75, 1) 33 | 34 | mesh.scale.x = 1.6 * scale 35 | mesh.scale.y = 0.9 * scale 36 | 37 | mesh.speed = random(0.75, 1) 38 | 39 | mesh.xExtra = 0 40 | 41 | mesh.x = mesh.position.x = random(-this.viewport.width * 0.5, this.viewport.width * 0.5) 42 | mesh.y = mesh.position.y = random(-this.viewport.height * 0.5, this.viewport.height * 0.5) 43 | 44 | this.meshes.push(mesh) 45 | 46 | this.scene.addChild(mesh) 47 | } 48 | } 49 | 50 | update (scroll, direction) { 51 | this.meshes.forEach(mesh => { 52 | mesh.position.x = mesh.x - scroll.current * mesh.speed - mesh.xExtra 53 | 54 | const viewportOffset = this.viewport.width * 0.5 55 | const widthTotal = this.viewport.width + mesh.scale.x 56 | 57 | mesh.isBefore = mesh.position.x < -viewportOffset 58 | mesh.isAfter = mesh.position.x > viewportOffset 59 | 60 | if (direction === 'right' && mesh.isBefore) { 61 | mesh.xExtra -= widthTotal 62 | 63 | mesh.isBefore = false 64 | mesh.isAfter = false 65 | } 66 | 67 | if (direction === 'left' && mesh.isAfter) { 68 | mesh.xExtra += widthTotal 69 | 70 | mesh.isBefore = false 71 | mesh.isAfter = false 72 | } 73 | 74 | mesh.position.y += 0.05 * mesh.speed 75 | 76 | if (mesh.position.y > this.viewport.height * 0.5 + mesh.scale.y) { 77 | mesh.position.y -= this.viewport.height + mesh.scale.y 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 6 | 7 | const IS_DEVELOPMENT = process.env.NODE_ENV === 'dev' 8 | 9 | const dirApp = path.join(__dirname, 'app') 10 | const dirAssets = path.join(__dirname, 'assets') 11 | const dirStyles = path.join(__dirname, 'styles') 12 | const dirNode = 'node_modules' 13 | 14 | module.exports = { 15 | entry: [ 16 | path.join(dirApp, 'index.js'), 17 | path.join(dirStyles, 'index.scss') 18 | ], 19 | 20 | resolve: { 21 | modules: [ 22 | dirApp, 23 | dirAssets, 24 | dirNode 25 | ] 26 | }, 27 | 28 | plugins: [ 29 | new webpack.DefinePlugin({ 30 | IS_DEVELOPMENT 31 | }), 32 | 33 | new webpack.ProvidePlugin({ 34 | 35 | }), 36 | 37 | new HtmlWebpackPlugin({ 38 | filename: 'index.html', 39 | template: path.join(__dirname, 'index.html') 40 | }), 41 | 42 | new MiniCssExtractPlugin({ 43 | filename: '[name].css', 44 | chunkFilename: '[id].css' 45 | }) 46 | ], 47 | 48 | module: { 49 | rules: [ 50 | { 51 | test: /\.pug$/, 52 | use: ['pug-loader'] 53 | }, 54 | 55 | { 56 | test: /\.js$/, 57 | use: { 58 | loader: 'babel-loader' 59 | } 60 | }, 61 | 62 | { 63 | test: /\.(sa|sc|c)ss$/, 64 | use: [ 65 | { 66 | loader: MiniCssExtractPlugin.loader, 67 | options: { 68 | publicPath: '' 69 | } 70 | }, 71 | { 72 | loader: 'css-loader', 73 | options: { 74 | sourceMap: false 75 | } 76 | }, 77 | { 78 | loader: 'postcss-loader', 79 | options: { 80 | sourceMap: false 81 | } 82 | }, 83 | { 84 | loader: 'sass-loader', 85 | options: { 86 | sourceMap: false 87 | } 88 | } 89 | ] 90 | }, 91 | 92 | { 93 | test: /\.(jpe?g|png|gif|svg|woff2?|fnt|webp)$/, 94 | loader: 'file-loader', 95 | options: { 96 | name (file) { 97 | return '[hash].[ext]' 98 | } 99 | } 100 | }, 101 | 102 | { 103 | test: /\.(glsl|frag|vert)$/, 104 | loader: 'raw-loader', 105 | exclude: /node_modules/ 106 | }, 107 | 108 | { 109 | test: /\.(glsl|frag|vert)$/, 110 | loader: 'glslify-loader', 111 | exclude: /node_modules/ 112 | } 113 | ] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/Title.js: -------------------------------------------------------------------------------- 1 | import AutoBind from 'auto-bind' 2 | import { Color, Geometry, Mesh, Program, Text, Texture } from 'ogl' 3 | 4 | import fragment from 'shaders/text-fragment.glsl' 5 | import vertex from 'shaders/text-vertex.glsl' 6 | 7 | import font from 'fonts/freight.json' 8 | import src from 'fonts/freight.png' 9 | 10 | export default class { 11 | constructor ({ gl, plane, renderer, text }) { 12 | AutoBind(this) 13 | 14 | this.gl = gl 15 | this.plane = plane 16 | this.renderer = renderer 17 | this.text = text 18 | 19 | this.createShader() 20 | this.createMesh() 21 | } 22 | 23 | createShader () { 24 | const texture = new Texture(this.gl, { generateMipmaps: false }) 25 | const textureImage = new Image() 26 | 27 | textureImage.src = src 28 | textureImage.onload = _ => texture.image = textureImage 29 | 30 | const vertex100 = `${vertex}` 31 | 32 | const fragment100 = ` 33 | #extension GL_OES_standard_derivatives : enable 34 | 35 | precision highp float; 36 | 37 | ${fragment} 38 | ` 39 | 40 | const vertex300 = `#version 300 es 41 | 42 | #define attribute in 43 | #define varying out 44 | 45 | ${vertex} 46 | ` 47 | 48 | const fragment300 = `#version 300 es 49 | 50 | precision highp float; 51 | 52 | #define varying in 53 | #define texture2D texture 54 | #define gl_FragColor FragColor 55 | 56 | out vec4 FragColor; 57 | 58 | ${fragment} 59 | ` 60 | 61 | let fragmentShader = fragment100 62 | let vertexShader = vertex100 63 | 64 | if (this.renderer.isWebgl2) { 65 | fragmentShader = fragment300 66 | vertexShader = vertex300 67 | } 68 | 69 | this.program = new Program(this.gl, { 70 | cullFace: null, 71 | depthTest: false, 72 | depthWrite: false, 73 | transparent: true, 74 | fragment: fragmentShader, 75 | vertex: vertexShader, 76 | uniforms: { 77 | uColor: { value: new Color('#545050') }, 78 | tMap: { value: texture } 79 | } 80 | }) 81 | } 82 | 83 | createMesh () { 84 | const text = new Text({ 85 | align: 'center', 86 | font, 87 | letterSpacing: -0.05, 88 | size: 0.08, 89 | text: this.text, 90 | wordSpacing: 0, 91 | }) 92 | 93 | const geometry = new Geometry(this.gl, { 94 | position: { size: 3, data: text.buffers.position }, 95 | uv: { size: 2, data: text.buffers.uv }, 96 | id: { size: 1, data: text.buffers.id }, 97 | index: { data: text.buffers.index } 98 | }) 99 | 100 | geometry.computeBoundingBox() 101 | 102 | this.mesh = new Mesh(this.gl, { geometry, program: this.program }) 103 | this.mesh.position.y = -this.plane.scale.y * 0.5 - 0.085 104 | this.mesh.setParent(this.plane) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/Number.js: -------------------------------------------------------------------------------- 1 | import AutoBind from 'auto-bind' 2 | import { Color, Geometry, Mesh, Program, Text, Texture } from 'ogl' 3 | 4 | import fragment from 'shaders/text-fragment.glsl' 5 | import vertex from 'shaders/text-vertex.glsl' 6 | 7 | import font from 'fonts/forma.json' 8 | import src from 'fonts/forma.png' 9 | 10 | export default class { 11 | constructor ({ gl, plane, renderer, text }) { 12 | AutoBind(this) 13 | 14 | this.gl = gl 15 | this.plane = plane 16 | this.renderer = renderer 17 | this.text = text + 1 18 | 19 | this.createShader() 20 | this.createMesh() 21 | } 22 | 23 | createShader () { 24 | const texture = new Texture(this.gl, { generateMipmaps: false }) 25 | const textureImage = new Image() 26 | 27 | textureImage.src = src 28 | textureImage.onload = _ => texture.image = textureImage 29 | 30 | const vertex100 = `${vertex}` 31 | 32 | const fragment100 = ` 33 | #extension GL_OES_standard_derivatives : enable 34 | 35 | precision highp float; 36 | 37 | ${fragment} 38 | ` 39 | 40 | const vertex300 = `#version 300 es 41 | 42 | #define attribute in 43 | #define varying out 44 | 45 | ${vertex} 46 | ` 47 | 48 | const fragment300 = `#version 300 es 49 | 50 | precision highp float; 51 | 52 | #define varying in 53 | #define texture2D texture 54 | #define gl_FragColor FragColor 55 | 56 | out vec4 FragColor; 57 | 58 | ${fragment} 59 | ` 60 | 61 | let fragmentShader = fragment100 62 | let vertexShader = vertex100 63 | 64 | if (this.renderer.isWebgl2) { 65 | fragmentShader = fragment300 66 | vertexShader = vertex300 67 | } 68 | 69 | this.program = new Program(this.gl, { 70 | cullFace: null, 71 | depthTest: false, 72 | depthWrite: false, 73 | transparent: true, 74 | fragment: fragmentShader, 75 | vertex: vertexShader, 76 | uniforms: { 77 | uColor: { value: new Color('#545050') }, 78 | tMap: { value: texture } 79 | } 80 | }) 81 | } 82 | 83 | createMesh () { 84 | const text = new Text({ 85 | align: 'center', 86 | font, 87 | size: 0.025, 88 | text: `${this.text < 10 ? `0${this.text}` : this.text}`, 89 | wordSpacing: 0, 90 | }) 91 | 92 | const geometry = new Geometry(this.gl, { 93 | position: { size: 3, data: text.buffers.position }, 94 | uv: { size: 2, data: text.buffers.uv }, 95 | id: { size: 1, data: text.buffers.id }, 96 | index: { data: text.buffers.index } 97 | }) 98 | 99 | geometry.computeBoundingBox() 100 | 101 | this.mesh = new Mesh(this.gl, { geometry, program: this.program }) 102 | this.mesh.position.y = -this.plane.scale.y * 0.5 - 0.03 103 | this.mesh.setParent(this.plane) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/Media.js: -------------------------------------------------------------------------------- 1 | import { Mesh, Program, Texture } from 'ogl' 2 | 3 | import fragment from 'shaders/image-fragment.glsl' 4 | import vertex from 'shaders/image-vertex.glsl' 5 | 6 | import { map } from 'utils/math' 7 | 8 | import Number from './Number' 9 | import Title from './Title' 10 | 11 | export default class { 12 | constructor ({ geometry, gl, image, index, length, renderer, scene, screen, text, viewport }) { 13 | this.extra = 0 14 | 15 | this.geometry = geometry 16 | this.gl = gl 17 | this.image = image 18 | this.index = index 19 | this.length = length 20 | this.renderer = renderer 21 | this.scene = scene 22 | this.screen = screen 23 | this.text = text 24 | this.viewport = viewport 25 | 26 | this.createShader() 27 | this.createMesh() 28 | this.createTitle() 29 | 30 | this.onResize() 31 | } 32 | 33 | createShader () { 34 | const texture = new Texture(this.gl, { 35 | generateMipmaps: false 36 | }) 37 | 38 | this.program = new Program(this.gl, { 39 | depthTest: false, 40 | depthWrite: false, 41 | fragment, 42 | vertex, 43 | uniforms: { 44 | tMap: { value: texture }, 45 | uPlaneSizes: { value: [0, 0] }, 46 | uImageSizes: { value: [0, 0] }, 47 | uViewportSizes: { value: [this.viewport.width, this.viewport.height] }, 48 | uSpeed: { value: 0 }, 49 | uTime: { value: 100 * Math.random() } 50 | }, 51 | transparent: true 52 | }) 53 | 54 | const image = new Image() 55 | 56 | image.src = this.image 57 | image.onload = _ => { 58 | texture.image = image 59 | 60 | this.program.uniforms.uImageSizes.value = [image.naturalWidth, image.naturalHeight] 61 | } 62 | } 63 | 64 | createMesh () { 65 | this.plane = new Mesh(this.gl, { 66 | geometry: this.geometry, 67 | program: this.program 68 | }) 69 | 70 | this.plane.setParent(this.scene) 71 | } 72 | 73 | createTitle () { 74 | this.number = new Number({ 75 | gl: this.gl, 76 | plane: this.plane, 77 | renderer: this.renderer, 78 | text: this.index % (this.length / 2), 79 | }) 80 | 81 | this.title = new Title({ 82 | gl: this.gl, 83 | plane: this.plane, 84 | renderer: this.renderer, 85 | text: this.text, 86 | }) 87 | } 88 | 89 | update (scroll, direction) { 90 | this.plane.position.x = this.x - scroll.current - this.extra 91 | this.plane.position.y = Math.cos((this.plane.position.x / this.widthTotal) * Math.PI) * 75 - 74.5 92 | this.plane.rotation.z = map(this.plane.position.x, -this.widthTotal, this.widthTotal, Math.PI, -Math.PI) 93 | 94 | this.speed = scroll.current - scroll.last 95 | 96 | this.program.uniforms.uTime.value += 0.04 97 | this.program.uniforms.uSpeed.value = this.speed 98 | 99 | const planeOffset = this.plane.scale.x / 2 100 | const viewportOffset = this.viewport.width 101 | 102 | this.isBefore = this.plane.position.x + planeOffset < -viewportOffset 103 | this.isAfter = this.plane.position.x - planeOffset > viewportOffset 104 | 105 | if (direction === 'right' && this.isBefore) { 106 | this.extra -= this.widthTotal 107 | 108 | this.isBefore = false 109 | this.isAfter = false 110 | } 111 | 112 | if (direction === 'left' && this.isAfter) { 113 | this.extra += this.widthTotal 114 | 115 | this.isBefore = false 116 | this.isAfter = false 117 | } 118 | } 119 | 120 | /** 121 | * Events. 122 | */ 123 | onResize ({ screen, viewport } = {}) { 124 | if (screen) { 125 | this.screen = screen 126 | } 127 | 128 | if (viewport) { 129 | this.viewport = viewport 130 | 131 | this.plane.program.uniforms.uViewportSizes.value = [this.viewport.width, this.viewport.height] 132 | } 133 | 134 | this.scale = this.screen.height / 1500 135 | 136 | this.plane.scale.y = this.viewport.height * (900 * this.scale) / this.screen.height 137 | this.plane.scale.x = this.viewport.width * (700 * this.scale) / this.screen.width 138 | 139 | this.plane.program.uniforms.uPlaneSizes.value = [this.plane.scale.x, this.plane.scale.y] 140 | 141 | this.padding = 2 142 | 143 | this.width = this.plane.scale.x + this.padding 144 | this.widthTotal = this.width * this.length 145 | 146 | this.x = this.width * this.index 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import { Renderer, Camera, Transform, Plane } from 'ogl' 2 | import NormalizeWheel from 'normalize-wheel' 3 | 4 | import debounce from 'lodash/debounce' 5 | 6 | import { lerp } from 'utils/math' 7 | 8 | import Image1 from 'images/1.jpg' 9 | import Image2 from 'images/2.jpg' 10 | import Image3 from 'images/3.jpg' 11 | import Image4 from 'images/4.jpg' 12 | import Image5 from 'images/5.jpg' 13 | import Image6 from 'images/6.jpg' 14 | import Image7 from 'images/7.jpg' 15 | import Image8 from 'images/8.jpg' 16 | import Image9 from 'images/9.jpg' 17 | import Image10 from 'images/10.jpg' 18 | import Image11 from 'images/11.jpg' 19 | import Image12 from 'images/12.jpg' 20 | 21 | import Media from './Media' 22 | import Background from './Background' 23 | 24 | export default class App { 25 | constructor () { 26 | document.documentElement.classList.remove('no-js') 27 | 28 | this.scroll = { 29 | ease: 0.05, 30 | current: 0, 31 | target: 0, 32 | last: 0 33 | } 34 | 35 | this.onCheckDebounce = debounce(this.onCheck, 200) 36 | 37 | this.createRenderer() 38 | this.createCamera() 39 | this.createScene() 40 | 41 | this.onResize() 42 | 43 | this.createGeometry() 44 | this.createMedias() 45 | this.createBackground() 46 | 47 | this.update() 48 | 49 | this.addEventListeners() 50 | 51 | this.createPreloader() 52 | } 53 | 54 | createPreloader () { 55 | Array.from(this.mediasImages).forEach(({ image: source }) => { 56 | const image = new Image() 57 | 58 | this.loaded = 0 59 | 60 | image.src = source 61 | image.onload = _ => { 62 | this.loaded += 1 63 | 64 | if (this.loaded === this.mediasImages.length) { 65 | document.documentElement.classList.remove('loading') 66 | document.documentElement.classList.add('loaded') 67 | } 68 | } 69 | }) 70 | } 71 | 72 | createRenderer () { 73 | this.renderer = new Renderer() 74 | 75 | this.gl = this.renderer.gl 76 | this.gl.clearColor(0.79607843137, 0.79215686274, 0.74117647058, 1) 77 | 78 | document.body.appendChild(this.gl.canvas) 79 | } 80 | 81 | createCamera () { 82 | this.camera = new Camera(this.gl) 83 | this.camera.fov = 45 84 | this.camera.position.z = 20 85 | } 86 | 87 | createScene () { 88 | this.scene = new Transform() 89 | } 90 | 91 | createGeometry () { 92 | this.planeGeometry = new Plane(this.gl, { 93 | heightSegments: 50, 94 | widthSegments: 100 95 | }) 96 | } 97 | 98 | createMedias () { 99 | this.mediasImages = [ 100 | { image: Image1, text: 'New Synagogue' }, 101 | { image: Image2, text: 'Paro Taktsang' }, 102 | { image: Image3, text: 'Petra' }, 103 | { image: Image4, text: 'Gooderham Building' }, 104 | { image: Image5, text: 'Catherine Palace' }, 105 | { image: Image6, text: 'Sheikh Zayed Mosque' }, 106 | { image: Image7, text: 'Madonna Corona' }, 107 | { image: Image8, text: 'Plaza de Espana' }, 108 | { image: Image9, text: 'Saint Martin' }, 109 | { image: Image10, text: 'Tugela Falls' }, 110 | { image: Image11, text: 'Sintra-Cascais' }, 111 | { image: Image12, text: 'The Prophet\'s Mosque' }, 112 | { image: Image1, text: 'New Synagogue' }, 113 | { image: Image2, text: 'Paro Taktsang' }, 114 | { image: Image3, text: 'Petra' }, 115 | { image: Image4, text: 'Gooderham Building' }, 116 | { image: Image5, text: 'Catherine Palace' }, 117 | { image: Image6, text: 'Sheikh Zayed Mosque' }, 118 | { image: Image7, text: 'Madonna Corona' }, 119 | { image: Image8, text: 'Plaza de Espana' }, 120 | { image: Image9, text: 'Saint Martin' }, 121 | { image: Image10, text: 'Tugela Falls' }, 122 | { image: Image11, text: 'Sintra-Cascais' }, 123 | { image: Image12, text: 'The Prophet\'s Mosque' }, 124 | ] 125 | 126 | this.medias = this.mediasImages.map(({ image, text }, index) => { 127 | const media = new Media({ 128 | geometry: this.planeGeometry, 129 | gl: this.gl, 130 | image, 131 | index, 132 | length: this.mediasImages.length, 133 | renderer: this.renderer, 134 | scene: this.scene, 135 | screen: this.screen, 136 | text, 137 | viewport: this.viewport 138 | }) 139 | 140 | return media 141 | }) 142 | } 143 | 144 | createBackground () { 145 | this.background = new Background({ 146 | gl: this.gl, 147 | scene: this.scene, 148 | viewport: this.viewport 149 | }) 150 | } 151 | 152 | /** 153 | * Events. 154 | */ 155 | onTouchDown (event) { 156 | this.isDown = true 157 | 158 | this.scroll.position = this.scroll.current 159 | this.start = event.touches ? event.touches[0].clientX : event.clientX 160 | } 161 | 162 | onTouchMove (event) { 163 | if (!this.isDown) return 164 | 165 | const x = event.touches ? event.touches[0].clientX : event.clientX 166 | const distance = (this.start - x) * 0.01 167 | 168 | this.scroll.target = this.scroll.position + distance 169 | } 170 | 171 | onTouchUp (event) { 172 | this.isDown = false 173 | 174 | this.onCheck() 175 | } 176 | 177 | onWheel (event) { 178 | const normalized = NormalizeWheel(event) 179 | const speed = normalized.pixelY 180 | 181 | this.scroll.target += speed * 0.005 182 | 183 | this.onCheckDebounce() 184 | } 185 | 186 | onCheck () { 187 | const { width } = this.medias[0] 188 | const itemIndex = Math.round(Math.abs(this.scroll.target) / width) 189 | const item = width * itemIndex 190 | 191 | if (this.scroll.target < 0) { 192 | this.scroll.target = -item 193 | } else { 194 | this.scroll.target = item 195 | } 196 | } 197 | 198 | /** 199 | * Resize. 200 | */ 201 | onResize () { 202 | this.screen = { 203 | height: window.innerHeight, 204 | width: window.innerWidth 205 | } 206 | 207 | this.renderer.setSize(this.screen.width, this.screen.height) 208 | 209 | this.camera.perspective({ 210 | aspect: this.gl.canvas.width / this.gl.canvas.height 211 | }) 212 | 213 | const fov = this.camera.fov * (Math.PI / 180) 214 | const height = 2 * Math.tan(fov / 2) * this.camera.position.z 215 | const width = height * this.camera.aspect 216 | 217 | this.viewport = { 218 | height, 219 | width 220 | } 221 | 222 | if (this.medias) { 223 | this.medias.forEach(media => media.onResize({ 224 | screen: this.screen, 225 | viewport: this.viewport 226 | })) 227 | } 228 | } 229 | 230 | /** 231 | * Update. 232 | */ 233 | update () { 234 | this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease) 235 | 236 | if (this.scroll.current > this.scroll.last) { 237 | this.direction = 'right' 238 | } else { 239 | this.direction = 'left' 240 | } 241 | 242 | if (this.medias) { 243 | this.medias.forEach(media => media.update(this.scroll, this.direction)) 244 | } 245 | 246 | if (this.background) { 247 | this.background.update(this.scroll, this.direction) 248 | } 249 | 250 | this.renderer.render({ 251 | scene: this.scene, 252 | camera: this.camera 253 | }) 254 | 255 | this.scroll.last = this.scroll.current 256 | 257 | window.requestAnimationFrame(this.update.bind(this)) 258 | } 259 | 260 | /** 261 | * Listeners. 262 | */ 263 | addEventListeners () { 264 | window.addEventListener('resize', this.onResize.bind(this)) 265 | 266 | window.addEventListener('mousewheel', this.onWheel.bind(this)) 267 | window.addEventListener('wheel', this.onWheel.bind(this)) 268 | 269 | window.addEventListener('mousedown', this.onTouchDown.bind(this)) 270 | window.addEventListener('mousemove', this.onTouchMove.bind(this)) 271 | window.addEventListener('mouseup', this.onTouchUp.bind(this)) 272 | 273 | window.addEventListener('touchstart', this.onTouchDown.bind(this)) 274 | window.addEventListener('touchmove', this.onTouchMove.bind(this)) 275 | window.addEventListener('touchend', this.onTouchUp.bind(this)) 276 | } 277 | } 278 | 279 | new App() 280 | --------------------------------------------------------------------------------