├── .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 | CodropsInfinite 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 | 
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 |
--------------------------------------------------------------------------------