├── .DS_Store ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── app.js ├── app ├── animations │ ├── Highlight.js │ ├── Label.js │ ├── Paragraph.js │ └── Title.js ├── classes │ ├── Animation.js │ ├── AsyncLoad.js │ ├── Button.js │ ├── Colors.js │ ├── Component.js │ ├── Detection.js │ └── Page.js ├── components │ ├── Canvas │ │ ├── About │ │ │ ├── Gallery.js │ │ │ ├── Media.js │ │ │ └── index.js │ │ ├── Collections │ │ │ ├── Media.js │ │ │ └── index.js │ │ ├── Detail │ │ │ └── index.js │ │ ├── Home │ │ │ ├── Media.js │ │ │ └── index.js │ │ ├── Transition.js │ │ └── index.js │ ├── Navigation.js │ └── Preloader.js ├── index.js ├── pages │ ├── About │ │ └── index.js │ ├── Collections │ │ └── index.js │ ├── Detail │ │ └── index.js │ └── Home │ │ └── index.js ├── shaders │ ├── collections-fragment.glsl │ ├── collections-vertex.glsl │ ├── home-fragment.glsl │ ├── home-vertex.glsl │ ├── plane-fragment.glsl │ └── plane-vertex.glsl └── utils │ ├── color.js │ └── text.js ├── fonts ├── george_x-webfont.woff ├── george_x-webfont.woff2 ├── suissebpintl-light-webfont.woff ├── suissebpintl-light-webfont.woff2 ├── suissebpintl-regular-webfont.woff ├── suissebpintl-regular-webfont.woff2 ├── suissebpintl-ultralight-webfont.woff └── suissebpintl-ultralight-webfont.woff2 ├── package-lock.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── cool.txt ├── favicon-16x16.png ├── favicon-32x32.png ├── george_x-webfont.2439ae4421675108a31a..woff2 ├── george_x-webfont.e9b3e24af9abd6a929f7..woff ├── main.css ├── main.js ├── main.js.LICENSE.txt ├── safari-pinned-tab.svg ├── site.webmanifest ├── suissebpintl-light-webfont.39f09bf2406ba7b4d05b..woff2 ├── suissebpintl-light-webfont.ce0a91a9ada4d0491ed6..woff ├── suissebpintl-regular-webfont.9f57dc57c8c3b9376927..woff2 ├── suissebpintl-regular-webfont.a7d0f1319327bf3a66cd..woff ├── suissebpintl-ultralight-webfont.3021905000cbcacc2fd7..woff └── suissebpintl-ultralight-webfont.d4b1f03703a50cf491fd..woff2 ├── shared ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── cool.txt ├── favicon-16x16.png ├── favicon-32x32.png ├── safari-pinned-tab.svg └── site.webmanifest ├── styles ├── base │ ├── base.scss │ ├── fonts.scss │ └── reset.scss ├── components │ └── preloader.scss ├── index.scss ├── layouts │ └── navigation.scss ├── pages │ ├── about │ │ ├── about.scss │ │ ├── content.scss │ │ ├── gallery.scss │ │ └── highlight.scss │ ├── collections │ │ └── collections.scss │ ├── details │ │ └── details.scss │ └── home │ │ └── home.scss ├── shared │ ├── descriptions.scss │ ├── links.scss │ ├── pages.scss │ ├── titles.scss │ └── vertical-titles.scss └── utils │ ├── functions.scss │ ├── mixins.scss │ ├── responsive.scss │ ├── variables.scss │ └── vendors.scss ├── vercel.json ├── views ├── _includes │ ├── head.pug │ ├── layout.pug │ ├── navigation.pug │ ├── preloader.pug │ └── scripts.pug ├── index.pug └── pages │ ├── about.pug │ ├── collections.pug │ ├── detail.pug │ └── home.pug ├── webpack.config.build.js ├── webpack.config.development.js └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/.DS_Store -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['standard', 'prettier'], 4 | globals: { 5 | IS_DEVELOPMENT: 'readonly' 6 | }, 7 | parserOptions: { 8 | ecmasVersion: 2021 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # logs 2 | yarn-debug.log* 3 | npm-error.log* 4 | 5 | # dependency directories 6 | node_modules/ 7 | 8 | # webpack build output 9 | public/ 10 | 11 | # environment variables 12 | .env 13 | 14 | # global rules 15 | .DS_Store 16 | .vercel 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hosted and Deployed on Heroku and Vercel 2 | 3 | - [Floema on Heroku](https://floema-ice.herokuapp.com) 4 | - [Floema on Vercel](https://floema-ice.vercel.app) 5 | 6 | 7 | # webpack-boilerplate 8 | A boilerplate which inspired from luis henrique bizzaro course on awwwards but intent to use up-to-date technologies. 9 | 10 | 11 | It has commits according to the lecture of the course 12 | 13 | # Setup 14 | 15 | `npm install` 16 | to install all the dependencies 17 | 18 | add `.env` file to the folder 19 | 20 | `npm start` 21 | to run development in your localhost 22 | 23 | # Migration Guide from @prismicio/client v5 to v6 24 | 25 | ## Step - 1 26 | 27 | - Remove `prismic-dom` module from file by doing `npm uninstall prismic-dom` 28 | - Update `@prismicio/client` module 29 | - add `@prismicio/helpers` and `node-fetch` by doing `npm i @prismicio/helpers node-fetch@2.6.7` // don't install the latest v of node-fetch it isn't compatible 30 | to cjs env. 31 | 32 | ## Step - 2 33 | - add PrismicH to the `app.js` 34 | ```js 35 | const PrismicH = require('@prismicio/helpers'); 36 | ``` 37 | - replace the declaration of **PrismicDOM** from **PrismicH** from everywhere 38 | - add node-fetch to the `app.js` 39 | ```js 40 | const fetch = require('node-fetch'); 41 | ``` 42 | things should look like this 43 | 44 | ```js 45 | const fetch = require('node-fetch'); 46 | const logger = require('morgan'); 47 | const path = require('path'); 48 | const express = require('express'); 49 | const errorHandler = require('errorhandler'); 50 | const bodyParser = require('body-parser'); 51 | const methodOverride = require('method-override'); 52 | 53 | const app = express(); 54 | const port = process.env.PORT || 8004; 55 | 56 | const Prismic = require('@prismicio/client'); 57 | const PrismicH = require('@prismicio/helpers'); 58 | const { application } = require('express'); 59 | const UAParser = require('ua-parser-js'); 60 | ``` 61 | 62 | ## Step - 3 63 | refactor the prismic init 64 | ```js 65 | const initApi = (req) => { 66 | return Prismic.createClient(process.env.PRISMIC_ENDPOINT, { 67 | accessToken: process.env.PRISMIC_ACCESS_TOKEN, 68 | req, 69 | fetch, 70 | }); 71 | }; 72 | ``` 73 | 74 | ## Step - 4 75 | now we have to fetch all the pages at once to resolve the issue of pages are not loading on time 76 | replace this 👇 from your `app.js/handleRequest function` 77 | ```js 78 | const meta = await api.getSingle('meta'); 79 | const preloader = await api.getSingle('preloader'); 80 | const navigation = await api.getSingle('navigation'); 81 | const home = await api.getSingle('home'); 82 | const about = await api.getSingle('about'); 83 | const { results: collections } = await api.query( Prismic.Predicates.at('document.type', 'collection'), { fetchLinks: 'product.image' } ); // prettier-ignore 84 | ``` 85 | to this 👇 86 | ```js 87 | const [meta, preloader, navigation, home, about, { results: collections }] = 88 | await Promise.all([ 89 | api.getSingle('meta'), 90 | api.getSingle('preloader'), 91 | api.getSingle('navigation'), 92 | api.getSingle('home'), 93 | api.getSingle('about'), 94 | api.query(Prismic.Predicates.at('document.type', 'collection'), { 95 | fetchLinks: 'product.image', 96 | }), 97 | ]); 98 | ``` 99 | after this all pages are gonna load on time except `/about` route coz you forgot to refactor `PrismicDOM` to `PrismicH` 😆 les do that also... 100 | 101 | ## Step - 5 102 | replace this 👇 103 | `.about__content__description!=PrismicDOM.RichText.asHtml(section.primary.description).replace(/
/g, '
')` 104 | 105 | to this 👇 106 | ```js 107 | .about__content__description!=PrismicDOM.asHTML(section.primary.description).replace(/
/g, '
')
108 | ```
109 |
110 | now we updated our repo to Prismic v6 :)
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | require('dotenv').config();
3 |
4 | const fetch = require('node-fetch');
5 | const logger = require('morgan');
6 | const path = require('path');
7 | const express = require('express');
8 | const errorHandler = require('errorhandler');
9 | const bodyParser = require('body-parser');
10 | const methodOverride = require('method-override');
11 |
12 | const app = express();
13 | const port = process.env.PORT || 8004;
14 |
15 | const Prismic = require('@prismicio/client');
16 | const PrismicH = require('@prismicio/helpers');
17 | const { application } = require('express');
18 | const UAParser = require('ua-parser-js');
19 |
20 | app.use(logger('dev'));
21 | app.use(bodyParser.json());
22 | app.use(bodyParser.urlencoded({ extended: false }));
23 | app.use(errorHandler());
24 | app.use(methodOverride());
25 | app.use(express.static(path.join(__dirname, 'public')));
26 |
27 | // Initialize the prismic.io api
28 | const initApi = (req) => {
29 | return Prismic.createClient(process.env.PRISMIC_ENDPOINT, {
30 | accessToken: process.env.PRISMIC_ACCESS_TOKEN,
31 | req,
32 | fetch,
33 | });
34 | };
35 |
36 | // Link Resolver
37 | const HandleLinkResolver = (doc) => {
38 | if (doc.type === 'product') {
39 | return `/detail/${doc.slug}`;
40 | }
41 |
42 | if (doc.type === 'collections') {
43 | return '/collections';
44 | }
45 |
46 | if (doc.type === 'about') {
47 | return `/about`;
48 | }
49 |
50 | // Default to homepage
51 | return '/';
52 | };
53 |
54 | // Middleware to inject prismic context
55 | app.use((req, res, next) => {
56 | const ua = UAParser(req.headers['user-agent']);
57 |
58 | res.locals.isDesktop = ua.device.type === undefined;
59 | res.locals.isPhone = ua.device.type === 'mobile';
60 | res.locals.isTablet = ua.device.type === 'tablet';
61 |
62 | res.locals.Link = HandleLinkResolver;
63 | res.locals.PrismicH = PrismicH;
64 | res.locals.Numbers = (index) => {
65 | return index === 0
66 | ? 'One'
67 | : index === 1
68 | ? 'Two'
69 | : index === 2
70 | ? 'Three'
71 | : index === 3
72 | ? 'Four'
73 | : '';
74 | };
75 |
76 | next();
77 | });
78 |
79 | app.set('view engine', 'pug');
80 | app.set('views', path.join(__dirname, 'views'));
81 | app.locals.basedir = app.get('views');
82 |
83 | const handleRequest = async (api) => {
84 | const [meta, preloader, navigation, home, about, { results: collections }] =
85 | await Promise.all([
86 | api.getSingle('meta'),
87 | api.getSingle('preloader'),
88 | api.getSingle('navigation'),
89 | api.getSingle('home'),
90 | api.getSingle('about'),
91 | api.query(Prismic.Predicates.at('document.type', 'collection'), {
92 | fetchLinks: 'product.image',
93 | }),
94 | ]);
95 |
96 | // console.log(about, home, collections);
97 |
98 | const assets = [];
99 |
100 | home.data.gallery.forEach((item) => {
101 | assets.push(item.image.url);
102 | });
103 |
104 | about.data.gallery.forEach((item) => {
105 | assets.push(item.image.url);
106 | });
107 |
108 | about.data.body.forEach((section) => {
109 | if (section.slice_type === 'gallery') {
110 | section.items.forEach((item) => {
111 | assets.push(item.image.url);
112 | });
113 | }
114 | });
115 |
116 | collections.forEach((collection) => {
117 | collection.data.list.forEach((item) => {
118 | assets.push(item.product.data.image.url);
119 | });
120 | });
121 |
122 | console.log(assets);
123 |
124 | return {
125 | assets,
126 | meta,
127 | home,
128 | collections,
129 | about,
130 | navigation,
131 | preloader,
132 | };
133 | };
134 |
135 | app.get('/', async (req, res) => {
136 | const api = await initApi(req);
137 | const defaults = await handleRequest(api);
138 |
139 | res.render('pages/home', {
140 | ...defaults,
141 | });
142 | });
143 |
144 | app.get('/about', async (req, res) => {
145 | const api = await initApi(req);
146 | const defaults = await handleRequest(api);
147 |
148 | res.render('pages/about', {
149 | ...defaults,
150 | });
151 | });
152 |
153 | app.get('/collections', async (req, res) => {
154 | const api = await initApi(req);
155 | const defaults = await handleRequest(api);
156 |
157 | res.render('pages/collections', {
158 | ...defaults,
159 | });
160 | });
161 |
162 | // app.get('/detail/:uid', async (req, res) => {
163 | // const api = await initApi(req);
164 | // const defaults = await handleRequest(api);
165 |
166 | // const product = await api.getByUID('product', req.params.uid, {
167 | // fetchLinks: 'collection.title',
168 | // });
169 |
170 | // if (product) {
171 | // res.render('pages/detail', {
172 | // product,
173 | // ...defaults,
174 | // });
175 | // } else {
176 | // res.status(404).send('Page not found');
177 | // }
178 | // });
179 |
180 | app.get('/detail/:uid', async (req, res) => {
181 | const api = await initApi(req);
182 | const defaults = await handleRequest(api);
183 |
184 | const product = await api.getByUID('product', req.params.uid, {
185 | fetchLinks: 'collection.title',
186 | });
187 |
188 | res.render('pages/detail', {
189 | ...defaults,
190 | product,
191 | });
192 | });
193 |
194 | app.listen(port, () => {
195 | console.log(`Example app listening at http://localhost:${port}`);
196 | });
197 |
198 | // "start": "concurrently --kill-others \"npm run backend:development\" \"npm run frontend:development\""
199 |
--------------------------------------------------------------------------------
/app/animations/Highlight.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 | import Animation from 'classes/Animation';
3 |
4 | export default class Highlight extends Animation {
5 | constructor({ element, elements }) {
6 | super({ element, elements });
7 | }
8 |
9 | animateIn() {
10 | GSAP.fromTo(
11 | this.element,
12 | {
13 | autoAlpha: 0,
14 | delay: 0.5,
15 | },
16 | {
17 | autoAlpha: 1,
18 | duration: 1,
19 | }
20 | );
21 | }
22 |
23 | animateOut() {
24 | GSAP.set(this.element, {
25 | autoAlpha: 0,
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/animations/Label.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 | import Animation from 'classes/Animation';
3 |
4 | export default class Label extends Animation {
5 | constructor({ element, elements }) {
6 | super({ element, elements });
7 | }
8 |
9 | animateIn() {
10 | GSAP.fromTo(
11 | this.element,
12 | {
13 | autoAlpha: 0,
14 | delay: 0.5,
15 | },
16 | {
17 | autoAlpha: 1,
18 | duration: 1,
19 | }
20 | );
21 | }
22 |
23 | animateOut() {
24 | GSAP.set(this.element, {
25 | autoAlpha: 0,
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/animations/Paragraph.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 | import Animation from 'classes/Animation';
3 |
4 | export default class Paragraph extends Animation {
5 | constructor({ element, elements }) {
6 | super({ element, elements });
7 | }
8 |
9 | animateIn() {
10 | GSAP.fromTo(
11 | this.element,
12 | {
13 | autoAlpha: 0,
14 | delay: 0.5,
15 | },
16 | {
17 | autoAlpha: 1,
18 | duration: 1,
19 | }
20 | );
21 | }
22 |
23 | animateOut() {
24 | GSAP.set(this.element, {
25 | autoAlpha: 0,
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/animations/Title.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 | import Animation from 'classes/Animation';
3 | export default class Title extends Animation {
4 | constructor({ element, elements }) {
5 | super({ element, elements });
6 | }
7 |
8 | animateIn() {
9 | GSAP.fromTo(
10 | this.element,
11 | {
12 | autoAlpha: 0,
13 | delay: 0.5,
14 | },
15 | {
16 | autoAlpha: 1,
17 | duration: 1,
18 | }
19 | );
20 | }
21 |
22 | animateOut() {
23 | GSAP.set(this.element, {
24 | autoAlpha: 0,
25 | });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/classes/Animation.js:
--------------------------------------------------------------------------------
1 | import Component from 'classes/Component';
2 |
3 | export default class Animation extends Component {
4 | constructor({ element, elements }) {
5 | super({ element, elements });
6 |
7 | this.createObserver();
8 |
9 | this.animateOut();
10 | }
11 |
12 | createObserver() {
13 | this.observer = new window.IntersectionObserver((entries) => {
14 | entries.forEach((entry) => {
15 | if (entry.isIntersecting) {
16 | this.animateIn();
17 | } else {
18 | this.animateOut();
19 | }
20 | });
21 | });
22 |
23 | this.observer.observe(this.element);
24 | }
25 |
26 | animateIn() {}
27 |
28 | animateOut() {}
29 |
30 | onResize() {}
31 | }
32 |
--------------------------------------------------------------------------------
/app/classes/AsyncLoad.js:
--------------------------------------------------------------------------------
1 | import Component from 'classes/Component';
2 |
3 | export default class AsyncLoad extends Component {
4 | constructor({ element }) {
5 | super({ element });
6 | this.createObserver();
7 | }
8 |
9 | createObserver() {
10 | this.observer = new window.IntersectionObserver((entries) => {
11 | entries.forEach((entry) => {
12 | if (entry.isIntersecting) {
13 | if (!this.element.src) {
14 | this.element.src = this.element.getAttribute('data-src');
15 | this.element.onload = _ => {
16 | this.element.classList.add('loaded');
17 | }
18 | }
19 | }
20 | });
21 | });
22 |
23 | this.observer.observe(this.element);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/classes/Button.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 |
3 | import Component from 'classes/Component';
4 |
5 | export default class Button extends Component {
6 | constructor({ element }) {
7 | super({ element });
8 |
9 | this.path = element.querySelector('path:last-child');
10 | this.pathLength = this.path.getTotalLength();
11 |
12 | this.timeline = GSAP.timeline({ paused: true });
13 |
14 | this.timeline.fromTo(
15 | this.path,
16 | {
17 | strokeDashoffset: this.pathLength,
18 | strokeDasharray: `${this.pathLength} ${this.pathLength}`,
19 | },
20 | {
21 | strokeDashoffset: 0,
22 | strokeDasharray: `${this.pathLength} ${this.pathLength}`,
23 | }
24 | );
25 | }
26 |
27 | onMouseEnter() {
28 | this.timeline.play();
29 | }
30 |
31 | onMouseLeave() {
32 | this.timeline.reverse();
33 | }
34 |
35 | addEventListeners() {
36 | this.onMouseEnterEvent = this.onMouseEnter.bind(this);
37 | this.onMouseLeaveEvent = this.onMouseLeave.bind(this);
38 |
39 | this.element.addEventListener('mouseenter', this.onMouseEnterEvent);
40 | this.element.addEventListener('mouseleave', this.onMouseLeaveEvent);
41 | }
42 |
43 | removeEventListeners() {
44 | this.element.removeEventListener('mouseenter', this.onMouseEnterEvent);
45 | this.element.removeEventListener('mouseleave', this.onMouseLeaveEvent);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/classes/Colors.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 |
3 | class Colors {
4 | change({ backgroundColor, color }) {
5 | GSAP.to(document.documentElement, {
6 | background: backgroundColor,
7 | color,
8 | duration: 1.5,
9 | });
10 | }
11 | }
12 |
13 | export const ColorsManager = new Colors();
14 |
--------------------------------------------------------------------------------
/app/classes/Component.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import EventEmitter from 'events';
3 | import each from 'lodash/each';
4 |
5 | export default class Component extends EventEmitter {
6 | constructor({ element, elements }) {
7 | super();
8 |
9 | this.selector = element;
10 | this.selectorChildren = {
11 | ...elements,
12 | };
13 |
14 | this.create();
15 |
16 | this.addEventListeners();
17 | }
18 |
19 | create() {
20 | if (this.selector instanceof window.HTMLElement) {
21 | this.element = this.selector;
22 | } else {
23 | this.element = document.querySelector(this.selector);
24 | }
25 |
26 | this.elements = {};
27 |
28 | each(this.selectorChildren, (entry, key) => {
29 | if (
30 | entry instanceof window.HTMLElement ||
31 | entry instanceof window.NodeList ||
32 | Array.isArray(entry)
33 | ) {
34 | this.elements[key] = entry;
35 | } else {
36 | this.elements[key] = document.querySelectorAll(entry);
37 |
38 | if (this.elements[key].length === 0) {
39 | this.elements[key] = null;
40 | } else if (this.elements[key].length === 1) {
41 | this.elements[key] = document.querySelector(entry);
42 | }
43 | }
44 | });
45 | }
46 |
47 | addEventListeners() {}
48 |
49 | removeEventListeners() {}
50 | }
51 |
--------------------------------------------------------------------------------
/app/classes/Detection.js:
--------------------------------------------------------------------------------
1 | class Detection {
2 | isPhone() {
3 | if (this.isPhoneChecked) {
4 | this.isPhoneChecked = true;
5 |
6 | this.isPhoneCheck = document.documentElement.classList.contains('phone');
7 | }
8 |
9 | return this.isPhoneCheck;
10 | }
11 |
12 | isDesktop() {
13 | if (this.DesktopChecked) {
14 | this.DesktopChecked = true;
15 |
16 | this.DesktopCheck =
17 | document.documentElement.classList.contains('desktop');
18 | }
19 |
20 | return this.DesktopCheck;
21 | }
22 |
23 | isTablet() {
24 | if (this.TabletChecked) {
25 | this.TabletChecked = true;
26 |
27 | this.TabletCheck = document.documentElement.classList.contains('tablet');
28 | }
29 |
30 | return this.TabletCheck;
31 | }
32 | }
33 |
34 | const DetectionManager = new Detection();
35 |
36 | export default DetectionManager;
37 |
--------------------------------------------------------------------------------
/app/classes/Page.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import GSAP from 'gsap';
3 |
4 | import Prefix from 'prefix';
5 |
6 | import each from 'lodash/each';
7 | import map from 'lodash/map';
8 |
9 | import Title from 'animations/Title';
10 | import Paragraph from 'animations/Paragraph';
11 | import Label from 'animations/Label';
12 | import Highlight from 'animations/Highlight';
13 |
14 | import AsyncLoad from 'classes/AsyncLoad';
15 |
16 | import { ColorsManager } from 'classes/Colors';
17 |
18 | export default class Page {
19 | constructor({ element, elements, id }) {
20 | this.selector = element;
21 | this.selectorChildren = {
22 | ...elements,
23 |
24 | aimationsHighlights: '[data-animation="highlight"]',
25 | animationsTitles: '[data-animation="title"]',
26 | animationsParagraphs: '[data-animation="paragraph"]',
27 | animationsLabels: '[data-animation="label"]',
28 |
29 | preloaders: '[data-src]',
30 | };
31 |
32 | this.id = id;
33 |
34 | this.transformPrefix = Prefix('transform');
35 | }
36 |
37 | create() {
38 | this.element = document.querySelector(this.selector);
39 | this.elements = {};
40 |
41 | this.scroll = {
42 | current: 0,
43 | target: 0,
44 | last: 0,
45 | limit: 0,
46 | };
47 |
48 | each(this.selectorChildren, (entry, key) => {
49 | if (
50 | entry instanceof window.HTMLElement ||
51 | entry instanceof window.NodeList ||
52 | Array.isArray(entry)
53 | ) {
54 | this.elements[key] = entry;
55 | } else {
56 | this.elements[key] = document.querySelectorAll(entry);
57 |
58 | if (this.elements[key].length === 0) {
59 | this.elements[key] = null;
60 | } else if (this.elements[key].length === 1) {
61 | this.elements[key] = document.querySelector(entry);
62 | }
63 | }
64 | });
65 |
66 | this.createAnimations();
67 |
68 | this.createPreloader();
69 | }
70 |
71 | createPreloader() {
72 | this.preloaders = map(this.elements.preloaders, (element) => {
73 | return new AsyncLoad({ element });
74 | });
75 | }
76 |
77 | // Animations
78 |
79 | createAnimations() {
80 | this.animations = [];
81 |
82 | // Titles
83 |
84 | this.animationsTitles = map(this.elements.animationsTitles, (element) => {
85 | return new Title({
86 | element,
87 | });
88 | });
89 |
90 | this.animations.push(...this.animationsTitles);
91 |
92 | // Paragraphs
93 |
94 | this.animationsParagraphs = map(
95 | this.elements.animationsParagraphs,
96 | (element) => {
97 | return new Paragraph({
98 | element,
99 | });
100 | }
101 | );
102 |
103 | this.animations.push(...this.animationsParagraphs);
104 |
105 | // Labels
106 |
107 | this.animationsLabels = map(this.elements.animationsLabels, (element) => {
108 | return new Label({
109 | element,
110 | });
111 | });
112 |
113 | this.animations.push(...this.animationsLabels);
114 |
115 | // Highlights
116 |
117 | this.aimationsHighlights = map(
118 | this.elements.aimationsHighlights,
119 | (element) => {
120 | return new Highlight({
121 | element,
122 | });
123 | }
124 | );
125 |
126 | this.animations.push(...this.aimationsHighlights);
127 | }
128 |
129 | show(animation) {
130 | return new Promise((resolve) => {
131 | ColorsManager.change({
132 | backgroundColor: this.element.getAttribute('data-background'),
133 | color: this.element.getAttribute('data-color'),
134 | });
135 |
136 | if (animation) {
137 | this.animationIn = animation;
138 | } else {
139 | this.animationIn = GSAP.timeline();
140 | this.animationIn.fromTo(
141 | this.element,
142 | {
143 | autoAlpha: 0,
144 | },
145 | {
146 | autoAlpha: 1,
147 | }
148 | );
149 | }
150 |
151 | this.animationIn.call((_) => {
152 | this.addEventListeners();
153 |
154 | resolve();
155 | });
156 | });
157 | }
158 |
159 | hide() {
160 | return new Promise((resolve) => {
161 | this.destroy();
162 |
163 | this.animationIn = GSAP.timeline();
164 |
165 | this.animationIn.to(this.element, {
166 | autoAlpha: 0,
167 | onComplete: resolve,
168 | });
169 | });
170 | }
171 |
172 | // Events
173 |
174 | onResize() {
175 | if (this.elements.wrapper) {
176 | this.scroll.limit =
177 | this.elements.wrapper.clientHeight - window.innerHeight;
178 | }
179 |
180 | each(this.animations, (animation) => animation.onResize());
181 | }
182 |
183 | onWheel({ pixelY }) {
184 | this.scroll.target += pixelY;
185 | }
186 |
187 | // Loop
188 |
189 | update() {
190 | this.scroll.target = GSAP.utils.clamp(
191 | 0,
192 | this.scroll.limit,
193 | this.scroll.target
194 | );
195 |
196 | this.scroll.current = GSAP.utils.interpolate(
197 | this.scroll.current,
198 | this.scroll.target,
199 | 0.1
200 | );
201 |
202 | if (this.scroll.current < 0.01) {
203 | this.scroll.current = 0;
204 | }
205 |
206 | if (this.elements.wrapper) {
207 | this.elements.wrapper.style[
208 | this.transformPrefix
209 | ] = `translateY(-${this.scroll.current}px)`;
210 | }
211 | }
212 |
213 | // Listeners
214 |
215 | addEventListeners() {}
216 |
217 | removeEventListeners() {}
218 |
219 | // Destroy
220 |
221 | destroy() {
222 | this.removeEventListeners();
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/app/components/Canvas/About/Gallery.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 |
3 | import { Transform } from 'ogl';
4 | import map from 'lodash/map';
5 |
6 | import Media from './Media';
7 |
8 | export default class Gallery {
9 | constructor({ element, geometry, index, gl, scene, sizes }) {
10 | this.element = element;
11 | this.elementsWrapper = element.querySelector('.about__gallery__wrapper');
12 |
13 | this.geometry = geometry;
14 | this.index = index;
15 | this.gl = gl;
16 | this.scene = scene;
17 | this.sizes = sizes;
18 |
19 | this.group = new Transform();
20 |
21 | this.scroll = {
22 | current: 0,
23 | target: 0,
24 | start: 0,
25 | lerp: 0.1,
26 | velocity: 1,
27 | };
28 |
29 | this.createMedias();
30 |
31 | this.onResize({
32 | sizes: this.sizes,
33 | });
34 |
35 | this.group.setParent(this.scene);
36 | }
37 |
38 | createMedias() {
39 | this.mediasElements = this.element.querySelectorAll( '.about__gallery__media' ); // prettier-ignore
40 |
41 | this.medias = map(this.mediasElements, (element, index) => {
42 | return new Media({
43 | element,
44 | geometry: this.geometry,
45 | index,
46 | gl: this.gl,
47 | scene: this.group,
48 | sizes: this.sizes,
49 | });
50 | });
51 | }
52 |
53 | // Animations
54 | show() {
55 | map(this.medias, (media) => media.show());
56 | }
57 |
58 | hide() {
59 | map(this.medias, (media) => media.hide());
60 | }
61 |
62 | // Events
63 |
64 | onResize(e) {
65 | this.bounds = this.elementsWrapper.getBoundingClientRect();
66 |
67 | this.sizes = e.sizes;
68 |
69 | this.width = (this.bounds.width / window.innerWidth) * this.sizes.width; // prettier-ignore };
70 |
71 | this.scroll.current = this.scroll.target = 0;
72 |
73 | map(this.medias, (media) => media.onResize(e, this.scroll.current));
74 | }
75 |
76 | onTouchDown({ x, y }) {
77 | this.scroll.start = this.scroll.current;
78 | }
79 |
80 | onTouchMove({ x, y }) {
81 | const distance = x.start - x.end;
82 |
83 | this.scroll.target = this.scroll.start - distance;
84 | }
85 |
86 | onTouchUp({ x, y }) {}
87 |
88 | // Update
89 |
90 | update(scroll) {
91 | const distance = (scroll.current - scroll.target) * 0.1;
92 | const y = scroll.current / window.innerHeight;
93 |
94 | if (this.scroll.current < this.scroll.target) {
95 | this.direction = 'right';
96 | this.scroll.velocity = -1;
97 | } else if (this.scroll.current > this.scroll.target) {
98 | this.direction = 'left';
99 | this.scroll.velocity = 1;
100 | }
101 |
102 | this.scroll.target -= this.scroll.velocity;
103 | this.scroll.target += distance;
104 |
105 | this.scroll.current = GSAP.utils.interpolate( this.scroll.current, this.scroll.target, this.scroll.lerp ); // prettier-ignore
106 |
107 | map(this.medias, (media, index) => {
108 | const scaleX = media.mesh.scale.x / 2 + 0.25;
109 |
110 | if (this.direction === 'left') {
111 | const x = media.mesh.position.x + scaleX;
112 |
113 | if (x < -this.sizes.width / 2) {
114 | media.extra += this.width;
115 | }
116 | } else if (this.direction === 'right') {
117 | const x = media.mesh.position.x - scaleX;
118 |
119 | if (x > this.sizes.width / 2) {
120 | media.extra -= this.width;
121 | }
122 | }
123 |
124 | media.update(this.scroll.current);
125 | });
126 |
127 | this.group.position.y = y * this.sizes.height;
128 | }
129 |
130 | // Destroy
131 | destroy() {
132 | this.scene.removeChild(this.group);
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/app/components/Canvas/About/Media.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { Mesh, Program } from 'ogl';
3 | import GSAP from 'gsap';
4 |
5 | import Detection from 'classes/Detection';
6 |
7 | import vertex from 'shaders/plane-vertex.glsl';
8 | import fragment from 'shaders/plane-fragment.glsl';
9 |
10 | export default class Media {
11 | constructor({ element, geometry, gl, index, scene, sizes }) {
12 | this.element = element;
13 | this.gl = gl;
14 | this.geometry = geometry;
15 | this.scene = scene;
16 | this.index = index;
17 | this.sizes = sizes;
18 |
19 | this.extra = {
20 | x: 0,
21 | y: 0,
22 | };
23 |
24 | this.createTexture();
25 | this.createProgram();
26 | this.createMesh();
27 | this.createBounds({
28 | sizes: this.sizes,
29 | });
30 | }
31 |
32 | createTexture() {
33 | const image = this.element.querySelector('img');
34 |
35 | this.texture = window.TEXTURES[image.getAttribute('data-src')];
36 | }
37 |
38 | createProgram() {
39 | this.program = new Program(this.gl, {
40 | fragment,
41 | vertex,
42 | uniforms: {
43 | uAlpha: { value: 0 },
44 | tMap: { value: this.texture },
45 | },
46 | });
47 | }
48 |
49 | createMesh() {
50 | this.mesh = new Mesh(this.gl, {
51 | geometry: this.geometry,
52 | program: this.program,
53 | });
54 |
55 | this.mesh.setParent(this.scene);
56 | }
57 |
58 | createBounds({ sizes }) {
59 | this.sizes = sizes;
60 |
61 | this.bounds = this.element.getBoundingClientRect();
62 |
63 | this.updateScale();
64 | this.updateX();
65 | this.updateY();
66 | }
67 |
68 | // Animations
69 | show() {
70 | GSAP.fromTo(
71 | this.program.uniforms.uAlpha,
72 | {
73 | value: 0,
74 | },
75 | {
76 | value: 1,
77 | }
78 | );
79 | }
80 |
81 | hide() {
82 | GSAP.to(this.program.uniforms.uAlpha, {
83 | value: 0,
84 | });
85 | }
86 |
87 | // Events
88 |
89 | onResize(sizes, scroll) {
90 | this.extra = 0;
91 |
92 | this.createBounds(sizes);
93 | this.updateX(scroll);
94 | this.updateY(0);
95 | }
96 |
97 | // Loop.
98 | updateRotation() {
99 | this.mesh.rotation.z = GSAP.utils.mapRange(
100 | -this.sizes.width / 2,
101 | this.sizes.width / 2,
102 | Math.PI * 0.1,
103 | -Math.PI * 0.1,
104 | this.mesh.position.x
105 | );
106 | }
107 |
108 | updateScale() {
109 | this.height = this.bounds.height / window.innerHeight;
110 | this.width = this.bounds.width / window.innerWidth;
111 |
112 | this.mesh.scale.x = this.sizes.width * this.width;
113 | this.mesh.scale.y = this.sizes.height * this.height;
114 |
115 | // scaling logic while rotation
116 | // const scale = GSAP.utils.mapRange(0, this.sizes.width / 2, 0.1, 0, Math.abs(this.mesh.position.x)); // prettier-ignore
117 |
118 | // this.mesh.scale.x += scale;
119 | // this.mesh.scale.y += scale;
120 | }
121 |
122 | updateX(x = 0) {
123 | this.x = (this.bounds.left + x) / window.innerWidth;
124 |
125 | this.mesh.position.x = (-this.sizes.width / 2) + (this.mesh.scale.x / 2) + (this.x * this.sizes.width) + this.extra; // prettier-ignore
126 | }
127 |
128 | updateY(y = 0) {
129 | this.y = (this.bounds.top + y) / window.innerHeight;
130 |
131 | const extra = Detection.isPhone() ? 20 : 40;
132 |
133 | this.mesh.position.y = (this.sizes.height / 2) - (this.mesh.scale.y / 2) - (this.y * this.sizes.height); // prettier-ignore
134 | this.mesh.position.y += Math.cos((this.mesh.position.x / this.sizes.width) * Math.PI * 0.1) * 58 - 58; // prettier-ignore
135 | }
136 |
137 | update(scroll) {
138 | this.updateRotation();
139 | this.updateScale();
140 | this.updateX(scroll);
141 | this.updateY(0);
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/app/components/Canvas/About/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | /* eslint-disable no-useless-return */
3 | import { Plane, Transform } from 'ogl';
4 |
5 | import GSAP from 'gsap';
6 |
7 | import map from 'lodash/map';
8 |
9 | import Gallery from './Gallery';
10 |
11 | export default class {
12 | constructor({ gl, scene, sizes }) {
13 | this.gl = gl;
14 | this.sizes = sizes;
15 |
16 | this.group = new Transform();
17 |
18 | this.createGeometry();
19 | this.createGalleries();
20 |
21 | this.onResize({
22 | sizes: this.sizes,
23 | });
24 |
25 | this.group.setParent(scene);
26 |
27 | this.show();
28 | }
29 |
30 | createGeometry() {
31 | this.geometry = new Plane(this.gl);
32 | }
33 |
34 | createGalleries() {
35 | this.galleriesElements = document.querySelectorAll('.about__gallery');
36 |
37 | this.galleries = map(this.galleriesElements, (element, index) => {
38 | return new Gallery({
39 | element,
40 | geometry: this.geometry,
41 | index,
42 | gl: this.gl,
43 | scene: this.group,
44 | sizes: this.sizes,
45 | });
46 | });
47 | }
48 |
49 | // Animations
50 | show() {
51 | map(this.galleries, (gallery) => gallery.show());
52 | }
53 |
54 | hide() {
55 | map(this.galleries, (gallery) => gallery.hide());
56 | }
57 |
58 | // Events
59 |
60 | onResize(e) {
61 | map(this.galleries, (gallery) => gallery.onResize(e));
62 | }
63 |
64 | onTouchDown(e) {
65 | map(this.galleries, (gallery) => gallery.onTouchDown(e));
66 | }
67 |
68 | onTouchMove(e) {
69 | map(this.galleries, (gallery) => gallery.onTouchMove(e));
70 | }
71 |
72 | onTouchUp(e) {
73 | map(this.galleries, (gallery) => gallery.onTouchUp(e));
74 | }
75 |
76 | onWheel({ pixelX, pixelY }) {}
77 |
78 | // Update
79 |
80 | update(scroll) {
81 | map(this.galleries, (gallery) => gallery.update(scroll));
82 | }
83 |
84 | // Destroy
85 | destroy() {
86 | map(this.galleries, (gallery) => gallery.destroy());
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/app/components/Canvas/Collections/Media.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 | import { Mesh, Program } from 'ogl';
3 |
4 | import fragment from 'shaders/collections-fragment.glsl';
5 | import vertex from 'shaders/collections-vertex.glsl';
6 |
7 | export default class {
8 | constructor({ element, geometry, gl, index, scene, sizes }) {
9 | this.element = element;
10 | this.geometry = geometry;
11 | this.gl = gl;
12 | this.index = index;
13 | this.scene = scene;
14 | this.sizes = sizes;
15 |
16 | this.extra = {
17 | x: 0,
18 | y: 0,
19 | };
20 |
21 | this.opacity = {
22 | current: 0,
23 | target: 0,
24 | lerp: 0.1,
25 | multiplier: 0,
26 | };
27 |
28 | this.createTexture();
29 | this.createProgram();
30 | this.createMesh();
31 | this.createBounds({
32 | sizes: this.sizes,
33 | });
34 | }
35 |
36 | createTexture() {
37 | const image = this.element.querySelector( '.collections__gallery__media__image' ); // prettier-ignore
38 |
39 | this.texture = window.TEXTURES[image.getAttribute('data-src')];
40 | }
41 |
42 | createProgram() {
43 | this.program = new Program(this.gl, {
44 | fragment,
45 | vertex,
46 | uniforms: {
47 | uAlpha: { value: 0 },
48 | tMap: { value: this.texture },
49 | },
50 | });
51 | }
52 |
53 | createMesh() {
54 | this.mesh = new Mesh(this.gl, {
55 | geometry: this.geometry,
56 | program: this.program,
57 | });
58 |
59 | this.mesh.setParent(this.scene);
60 | }
61 |
62 | createBounds({ sizes }) {
63 | this.sizes = sizes;
64 |
65 | this.bounds = this.element.getBoundingClientRect();
66 |
67 | this.updateScale();
68 | this.updateX();
69 | }
70 |
71 | /**
72 | * Animations.
73 | */
74 | show() {
75 | GSAP.fromTo(
76 | this.opacity,
77 | {
78 | multiplier: 0,
79 | },
80 | {
81 | multiplier: 1,
82 | }
83 | );
84 | }
85 |
86 | hide() {
87 | GSAP.to(this.opacity, {
88 | multiplier: 0,
89 | });
90 | }
91 |
92 | /**
93 | * Events.
94 | */
95 | onResize(sizes, scroll) {
96 | this.extra = {
97 | x: 0,
98 | y: 0,
99 | };
100 |
101 | this.createBounds(sizes);
102 | this.updateX(scroll && scroll.x);
103 | }
104 |
105 | /**
106 | * Loop.
107 | */
108 | updateScale() {
109 | this.height = this.bounds.height / window.innerHeight;
110 | this.width = this.bounds.width / window.innerWidth;
111 |
112 | this.mesh.scale.x = this.sizes.width * this.width;
113 | this.mesh.scale.y = this.sizes.height * this.height;
114 | }
115 |
116 | updateX(x = 0) {
117 | this.x = (this.bounds.left + x) / window.innerWidth;
118 |
119 | this.mesh.position.x =
120 | -this.sizes.width / 2 +
121 | this.mesh.scale.x / 2 +
122 | this.x * this.sizes.width +
123 | this.extra.x;
124 | }
125 |
126 | update(scroll, index) {
127 | this.updateX(scroll);
128 |
129 | const amplitude = 0.1;
130 | const frequency = 1;
131 |
132 | this.mesh.rotation.z = -0.02 * Math.PI * Math.sin(this.index / frequency);
133 | this.mesh.position.y = amplitude * Math.sin(this.index / frequency);
134 |
135 | this.opacity.target = index === this.index ? 1 : 0.4;
136 | this.opacity.current = GSAP.utils.interpolate(
137 | this.opacity.current,
138 | this.opacity.target,
139 | this.opacity.lerp
140 | );
141 |
142 | this.program.uniforms.uAlpha.value = this.opacity.multiplier;
143 | this.program.uniforms.uAlpha.value = this.opacity.multiplier * this.opacity.current; // prettier-ignore
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/app/components/Canvas/Collections/index.js:
--------------------------------------------------------------------------------
1 | import { Plane, Transform } from 'ogl';
2 | import GSAP from 'gsap';
3 | import Prefix from 'prefix';
4 |
5 | import map from 'lodash/map';
6 |
7 | import Media from './Media';
8 |
9 | export default class {
10 | constructor({ gl, scene, sizes, transition }) {
11 | this.id = 'collections';
12 |
13 | this.gl = gl;
14 | this.scene = scene;
15 | this.sizes = sizes;
16 | this.transition = transition;
17 |
18 | this.transformPrefix = Prefix('transform');
19 |
20 | this.group = new Transform();
21 |
22 | this.galleryElement = document.querySelector('.collections__gallery');
23 | this.galleryWrapperElement = document.querySelector( '.collections__gallery__wrapper' ); // prettier-ignore
24 |
25 | this.titlesElement = document.querySelector('.collections__titles');
26 |
27 | this.collectionsElements = document.querySelectorAll( '.collections__article' ); // prettier-ignore
28 | this.collectionsElementsActive = 'collections__article--active';
29 |
30 | this.mediasElements = document.querySelectorAll( '.collections__gallery__media' ); // prettier-ignore
31 |
32 | this.scroll = {
33 | current: 0,
34 | start: 0,
35 | target: 0,
36 | lerp: 0.1,
37 | velocity: 1,
38 | };
39 |
40 | this.createGeometry();
41 | this.createGallery();
42 |
43 | this.onResize({
44 | sizes: this.sizes,
45 | });
46 |
47 | this.group.setParent(this.scene);
48 |
49 | this.show();
50 | }
51 |
52 | createGeometry() {
53 | this.geometry = new Plane(this.gl);
54 | }
55 |
56 | createGallery() {
57 | this.medias = map(this.mediasElements, (element, index) => {
58 | return new Media({
59 | element,
60 | geometry: this.geometry,
61 | index,
62 | gl: this.gl,
63 | scene: this.group,
64 | sizes: this.sizes,
65 | });
66 | });
67 | }
68 |
69 | /**
70 | * Animations.
71 | */
72 | async show() {
73 | if (this.transition) {
74 | const { src } = this.transition.mesh.program.uniforms.tMap.value.image;
75 | const texture = window.TEXTURES[src];
76 | const media = this.medias.find((media) => media.texture === texture);
77 | const scroll = -media.bounds.left - media.bounds.width / 2 + window.innerWidth / 2; // prettier-ignore
78 |
79 | this.update();
80 |
81 | this.transition.animate(
82 | {
83 | position: { x: 0, y: media.mesh.position.y, z: 0 },
84 | rotation: media.mesh.rotation,
85 | scale: media.mesh.scale,
86 | },
87 | (_) => {
88 | media.opacity.multiplier = 1;
89 |
90 | map(this.medias, (item) => {
91 | if (media !== item) {
92 | item.show();
93 | }
94 | });
95 |
96 | this.scroll.current = this.scroll.target = this.scroll.start = this.scroll.last = scroll; // prettier-ignore
97 | }
98 | );
99 | } else {
100 | map(this.medias, (media) => media.show());
101 | }
102 | }
103 |
104 | hide() {
105 | map(this.medias, (media) => media.hide());
106 | }
107 |
108 | /**
109 | * Events.
110 | */
111 | onResize(event) {
112 | this.sizes = event.sizes;
113 |
114 | this.bounds = this.galleryWrapperElement.getBoundingClientRect();
115 |
116 | this.scroll.last = this.scroll.target = 0;
117 |
118 | map(this.medias, (media) => media.onResize(event, this.scroll));
119 |
120 | this.scroll.limit = this.bounds.width - this.medias[0].element.clientWidth;
121 | }
122 |
123 | onTouchDown({ x, y }) {
124 | this.scroll.last = this.scroll.current;
125 | }
126 |
127 | onTouchMove({ x, y }) {
128 | const distance = x.start - x.end;
129 |
130 | this.scroll.target = this.scroll.last - distance;
131 | }
132 |
133 | onTouchUp({ x, y }) {}
134 |
135 | onWheel({ pixelY }) {
136 | this.scroll.target += pixelY;
137 | }
138 |
139 | /**
140 | * Changed.
141 | */
142 | onChange(index) {
143 | this.index = index;
144 |
145 | const selectedCollection = parseInt( this.mediasElements[this.index].getAttribute('data-index')); // prettier-ignore
146 |
147 | map(this.collectionsElements, (element, elementIndex) => {
148 | if (elementIndex === selectedCollection) {
149 | element.classList.add(this.collectionsElementsActive);
150 | } else {
151 | element.classList.remove(this.collectionsElementsActive);
152 | }
153 | });
154 |
155 | this.titlesElement.style[this.transformPrefix] = `translateY(-${ 25 * selectedCollection }%) translate(-50%, -50%) rotate(-90deg)`; // prettier-ignore
156 | }
157 |
158 | /**
159 | * Update.
160 | */
161 | update() {
162 | this.scroll.target = GSAP.utils.clamp(
163 | -this.scroll.limit,
164 | 0,
165 | this.scroll.target
166 | );
167 |
168 | this.scroll.current = GSAP.utils.interpolate(
169 | this.scroll.current,
170 | this.scroll.target,
171 | this.scroll.lerp
172 | );
173 |
174 | this.galleryElement.style[
175 | this.transformPrefix
176 | ] = `translateX(${this.scroll.current}px)`;
177 |
178 | if (this.scroll.last < this.scroll.current) {
179 | this.scroll.direction = 'right';
180 | } else if (this.scroll.last > this.scroll.current) {
181 | this.scroll.direction = 'left';
182 | }
183 |
184 | this.scroll.last = this.scroll.current;
185 |
186 | const index = Math.floor( Math.abs( (this.scroll.current - this.medias[0].bounds.width / 2) / this.scroll.limit ) * (this.medias.length - 1) ); // prettier-ignore
187 |
188 | if (this.index !== index) {
189 | this.onChange(index);
190 | }
191 |
192 | map(this.medias, (media, index) => {
193 | media.update(this.scroll.current, this.index);
194 | media.mesh.rotation.z = Math.abs( GSAP.utils.mapRange(0, 1, -0.2, 0.2, index / (this.medias.length - 1)) ) - 0.1; // prettier-ignore
195 |
196 | media.mesh.position.y += Math.cos((media.mesh.position.x / this.sizes.width) * Math.PI * 0.1) * 40 - 40; }); // prettier-ignore
197 | }
198 |
199 | /**
200 | * Destroy.
201 | */
202 | destroy() {
203 | this.scene.removeChild(this.group);
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/app/components/Canvas/Detail/index.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 | import { Mesh, Plane, Program } from 'ogl';
3 |
4 | import fragment from 'shaders/plane-fragment.glsl';
5 | import vertex from 'shaders/plane-vertex.glsl';
6 |
7 | export default class {
8 | constructor({ gl, scene, sizes, transition }) {
9 | this.id = 'detail';
10 |
11 | this.element = document.querySelector('.detail__media__image');
12 |
13 | this.gl = gl;
14 | this.scene = scene;
15 | this.sizes = sizes;
16 | this.transition = transition;
17 |
18 | this.geometry = new Plane(this.gl);
19 |
20 | this.createTexture();
21 | this.createProgram();
22 | this.createMesh();
23 | this.createBounds({
24 | sizes: this.sizes,
25 | });
26 |
27 | this.show();
28 | }
29 |
30 | createTexture() {
31 | const image = this.element.getAttribute('data-src');
32 |
33 | this.texture = window.TEXTURES[image];
34 | }
35 |
36 | createProgram() {
37 | this.program = new Program(this.gl, {
38 | fragment,
39 | vertex,
40 | uniforms: {
41 | uAlpha: { value: 0 },
42 | tMap: { value: this.texture },
43 | },
44 | });
45 | }
46 |
47 | createMesh() {
48 | this.mesh = new Mesh(this.gl, {
49 | geometry: this.geometry,
50 | program: this.program,
51 | });
52 |
53 | this.mesh.rotation.z = Math.PI * 0.01;
54 |
55 | this.mesh.setParent(this.scene);
56 | }
57 |
58 | createBounds({ sizes }) {
59 | this.sizes = sizes;
60 |
61 | this.bounds = this.element.getBoundingClientRect();
62 |
63 | this.updateScale();
64 | this.updateX();
65 | this.updateY();
66 | }
67 |
68 | /**
69 | * Animations.
70 | */
71 | show() {
72 | if (this.transition) {
73 | this.transition.animate(this.mesh, (_) => {
74 | this.program.uniforms.uAlpha.value = 1;
75 | });
76 | } else {
77 | GSAP.to(this.program.uniforms.uAlpha, {
78 | value: 1,
79 | });
80 | }
81 | }
82 |
83 | hide() {
84 | GSAP.to(this.program.uniforms.uAlpha, {
85 | value: 0,
86 | });
87 | }
88 |
89 | /**
90 | * Events.
91 | */
92 | onResize(sizes) {
93 | this.createBounds(sizes);
94 | this.updateX();
95 | this.updateY();
96 | }
97 |
98 | onTouchDown() {}
99 |
100 | onTouchMove() {}
101 |
102 | onTouchUp() {}
103 |
104 | /**
105 | * Loop.
106 | */
107 | updateScale() {
108 | this.height = this.bounds.height / window.innerHeight;
109 | this.width = this.bounds.width / window.innerWidth;
110 |
111 | this.mesh.scale.x = this.sizes.width * this.width;
112 | this.mesh.scale.y = this.sizes.height * this.height;
113 | }
114 |
115 | updateX() {
116 | this.x = this.bounds.left / window.innerWidth;
117 |
118 | this.mesh.position.x =
119 | -this.sizes.width / 2 + this.mesh.scale.x / 2 + this.x * this.sizes.width;
120 | }
121 |
122 | updateY() {
123 | this.y = this.bounds.top / window.innerHeight;
124 |
125 | this.mesh.position.y =
126 | this.sizes.height / 2 -
127 | this.mesh.scale.y / 2 -
128 | this.y * this.sizes.height;
129 | }
130 |
131 | update() {
132 | this.updateX();
133 | }
134 |
135 | /**
136 | * Destroy.
137 | */
138 | destroy() {
139 | this.scene.removeChild(this.mesh);
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/app/components/Canvas/Home/Media.js:
--------------------------------------------------------------------------------
1 | import { Mesh, Program } from 'ogl';
2 | import GSAP from 'gsap';
3 |
4 | import vertex from 'shaders/home-vertex.glsl';
5 | import fragment from 'shaders/home-fragment.glsl';
6 |
7 | export default class Media {
8 | constructor({ element, geometry, gl, index, scene, sizes }) {
9 | this.element = element;
10 | this.gl = gl;
11 | this.geometry = geometry;
12 | this.scene = scene;
13 | this.index = index;
14 | this.sizes = sizes;
15 |
16 | this.extra = {
17 | x: 0,
18 | y: 0,
19 | };
20 |
21 | this.createTexture();
22 | this.createProgram();
23 | this.createMesh();
24 | }
25 |
26 | createTexture() {
27 | const image = this.element;
28 |
29 | this.texture = window.TEXTURES[image.getAttribute('data-src')];
30 | }
31 |
32 | createProgram() {
33 | this.program = new Program(this.gl, {
34 | fragment,
35 | vertex,
36 | uniforms: {
37 | uAlpha: { value: 0 },
38 | uSpeed: { value: 0 },
39 | uViewportSizes: { value: [this.sizes.width, this.sizes.height] },
40 | tMap: { value: this.texture },
41 | },
42 | });
43 | }
44 |
45 | createMesh() {
46 | this.mesh = new Mesh(this.gl, {
47 | geometry: this.geometry,
48 | program: this.program,
49 | });
50 |
51 | this.mesh.setParent(this.scene);
52 |
53 | this.mesh.rotation.z = GSAP.utils.random(-Math.PI * 0.03, Math.PI * 0.03); // prettier-ignore
54 | }
55 |
56 | createBounds({ sizes }) {
57 | this.sizes = sizes;
58 | this.bounds = this.element.getBoundingClientRect();
59 |
60 | this.updateScale();
61 | this.updateX();
62 | this.updateY();
63 | }
64 |
65 | // Animations
66 | show() {
67 | GSAP.fromTo(
68 | this.program.uniforms.uAlpha,
69 | {
70 | value: 0,
71 | },
72 | {
73 | value: 0.4,
74 | }
75 | );
76 | }
77 |
78 | hide() {
79 | GSAP.to(this.program.uniforms.uAlpha, {
80 | value: 0,
81 | });
82 | }
83 |
84 | // Events
85 |
86 | onResize(sizes, scroll) {
87 | this.extra = {
88 | x: 0,
89 | y: 0,
90 | };
91 |
92 | this.createBounds(sizes);
93 | this.updateX(scroll && scroll.x);
94 | this.updateY(scroll && scroll.y);
95 | }
96 |
97 | // Loop.
98 |
99 | updateScale() {
100 | this.height = this.bounds.height / window.innerHeight;
101 | this.width = this.bounds.width / window.innerWidth;
102 |
103 | this.mesh.scale.x = this.sizes.width * this.width;
104 | this.mesh.scale.y = this.sizes.height * this.height;
105 | }
106 |
107 | updateX(x = 0) {
108 | this.x = (this.bounds.left + x) / window.innerWidth;
109 |
110 | this.mesh.position.x = (-this.sizes.width / 2) + (this.mesh.scale.x / 2) + (this.x * this.sizes.width) + this.extra.x; // prettier-ignore
111 | }
112 |
113 | updateY(y = 0) {
114 | this.y = (this.bounds.top + y) / window.innerHeight;
115 |
116 | this.mesh.position.y = (this.sizes.height / 2) - (this.mesh.scale.y / 2) - (this.y * this.sizes.height) + this.extra.y; // prettier-ignore
117 | }
118 |
119 | update(scroll, speed) {
120 | this.updateX(scroll.x);
121 | this.updateY(scroll.y);
122 |
123 | this.program.uniforms.uSpeed.value = speed;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/app/components/Canvas/Home/index.js:
--------------------------------------------------------------------------------
1 | import { Plane, Transform } from 'ogl';
2 |
3 | import GSAP from 'gsap';
4 |
5 | import map from 'lodash/map';
6 |
7 | import Media from './Media';
8 |
9 | export default class {
10 | constructor({ gl, scene, sizes }) {
11 | this.gl = gl;
12 | this.scene = scene;
13 | this.sizes = sizes;
14 |
15 | this.group = new Transform();
16 |
17 | this.galleryElement = document.querySelector('.home__gallery');
18 | this.mediasElements = document.querySelectorAll( '.home__gallery__media__image' ); // prettier-ignore
19 |
20 | this.x = {
21 | current: 0,
22 | target: 0,
23 | lerp: 0.1,
24 | };
25 |
26 | this.y = {
27 | current: 0,
28 | target: 0,
29 | lerp: 0.1,
30 | };
31 |
32 | this.scrollCurrent = {
33 | x: 0,
34 | y: 0,
35 | };
36 |
37 | this.scroll = {
38 | x: 0,
39 | y: 0,
40 | };
41 |
42 | this.speed = {
43 | current: 0,
44 | target: 0,
45 | lerp: 0.1,
46 | };
47 |
48 | this.createGeometry();
49 | this.createGallery();
50 |
51 | this.onResize({
52 | sizes: this.sizes,
53 | });
54 |
55 | this.group.setParent(this.scene);
56 |
57 | this.show();
58 | }
59 |
60 | createGeometry() {
61 | this.geometry = new Plane(this.gl, {
62 | heightSegments: 20,
63 | widthSegments: 20,
64 | });
65 | }
66 |
67 | createGallery() {
68 | this.medias = map(this.mediasElements, (element, index) => {
69 | return new Media({
70 | element,
71 | geometry: this.geometry,
72 | index,
73 | gl: this.gl,
74 | scene: this.group,
75 | sizes: this.sizes,
76 | });
77 | });
78 | }
79 |
80 | // Animations
81 | show() {
82 | map(this.medias, (media) => media.show());
83 | }
84 |
85 | hide() {
86 | map(this.medias, (media) => media.hide());
87 | }
88 |
89 | // Events
90 |
91 | onResize(e) {
92 | this.galleryBounds = this.galleryElement.getBoundingClientRect();
93 |
94 | this.sizes = e.sizes;
95 |
96 | this.gallerySizes = {
97 | width: (this.galleryBounds.width / window.innerWidth) * this.sizes.width,
98 | height: (this.galleryBounds.height / window.innerHeight) * this.sizes.height, // prettier-ignore
99 | };
100 |
101 | this.scroll.x = this.x.target = 0;
102 | this.scroll.y = this.y.target = 0;
103 |
104 | map(this.medias, (media) => media.onResize(e, this.scroll));
105 | }
106 |
107 | onTouchDown({ x, y }) {
108 | this.speed.target = 1;
109 |
110 | this.scrollCurrent.x = this.scroll.x;
111 | this.scrollCurrent.y = this.scroll.y;
112 | }
113 |
114 | onTouchMove({ x, y }) {
115 | const xDistance = x.start - x.end;
116 | const yDistance = y.start - y.end;
117 |
118 | this.x.target = this.scrollCurrent.x - xDistance;
119 | this.y.target = this.scrollCurrent.y - yDistance;
120 | }
121 |
122 | onTouchUp({ x, y }) {
123 | this.speed.target = 0;
124 | }
125 |
126 | onWheel({ pixelX, pixelY }) {
127 | this.x.target += pixelX;
128 | this.y.target += pixelY;
129 | }
130 |
131 | // Update
132 |
133 | update() {
134 | this.speed.current = GSAP.utils.interpolate(this.speed.current, this.speed.target, this.speed.lerp); // prettier-ignore
135 |
136 | this.x.current = GSAP.utils.interpolate( this.x.current, this.x.target, this.x.lerp ); // prettier-ignore
137 | this.y.current = GSAP.utils.interpolate( this.y.current, this.y.target, this.y.lerp ); // prettier-ignore
138 |
139 | if (this.scroll.x < this.x.current) {
140 | this.x.direction = 'right';
141 | } else if (this.scroll.x > this.x.current) {
142 | this.x.direction = 'left';
143 | }
144 |
145 | if (this.scroll.y < this.y.current) {
146 | this.y.direction = 'top';
147 | } else if (this.scroll.y > this.y.current) {
148 | this.y.direction = 'bottom';
149 | }
150 |
151 | this.scroll.x = this.x.current;
152 | this.scroll.y = this.y.current;
153 |
154 | map(this.medias, (media, index) => {
155 | const offsetX = this.sizes.width * 0.6;
156 | const scaleX = media.mesh.scale.x / 2;
157 | if (this.x.direction === 'left') {
158 | const x = media.mesh.position.x + scaleX;
159 |
160 | if (x < -offsetX) {
161 | media.extra.x += this.gallerySizes.width;
162 |
163 | media.mesh.rotation.z = GSAP.utils.random(-Math.PI * 0.03, Math.PI * 0.03); // prettier-ignore
164 | }
165 | } else if (this.x.direction === 'right') {
166 | const x = media.mesh.position.x - scaleX;
167 |
168 | if (x > offsetX) {
169 | media.extra.x -= this.gallerySizes.width;
170 |
171 | media.mesh.rotation.z = GSAP.utils.random(-Math.PI * 0.03, Math.PI * 0.03); // prettier-ignore
172 | }
173 | }
174 |
175 | const offsetY = this.sizes.height * 0.6;
176 | const scaleY = media.mesh.scale.y / 2;
177 |
178 | if (this.y.direction === 'top') {
179 | const y = media.mesh.position.y + scaleY;
180 |
181 | if (y < -offsetY) {
182 | media.extra.y += this.gallerySizes.height;
183 |
184 | media.mesh.rotation.z = GSAP.utils.random(-Math.PI * 0.03, Math.PI * 0.03); // prettier-ignore
185 | }
186 | } else if (this.y.direction === 'bottom') {
187 | const y = media.mesh.position.y - scaleY;
188 |
189 | if (y > offsetY) {
190 | media.extra.y -= this.gallerySizes.height;
191 |
192 | media.mesh.rotation.z = GSAP.utils.random(-Math.PI * 0.03, Math.PI * 0.03); // prettier-ignore
193 | }
194 | }
195 |
196 | media.update(this.scroll, this.speed.current);
197 | });
198 | }
199 |
200 | // Destroy
201 | destroy() {
202 | this.scene.removeChild(this.group);
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/app/components/Canvas/Transition.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 | import { Mesh, Plane, Program } from 'ogl';
3 |
4 | import fragment from 'shaders/plane-fragment.glsl';
5 | import vertex from 'shaders/plane-vertex.glsl';
6 |
7 | export default class {
8 | constructor({ collections, gl, scene, sizes, url }) {
9 | this.collections = collections;
10 | this.gl = gl;
11 | this.scene = scene;
12 | this.sizes = sizes;
13 | this.url = url;
14 |
15 | this.geometry = new Plane(this.gl);
16 | }
17 |
18 | createProgram(texture) {
19 | this.program = new Program(this.gl, {
20 | fragment,
21 | vertex,
22 | uniforms: {
23 | uAlpha: { value: 1 },
24 | tMap: { value: texture },
25 | },
26 | });
27 | }
28 |
29 | createMesh(mesh) {
30 | this.mesh = new Mesh(this.gl, {
31 | geometry: this.geometry,
32 | program: this.program,
33 | });
34 |
35 | this.mesh.scale.x = mesh.scale.x;
36 | this.mesh.scale.y = mesh.scale.y;
37 | this.mesh.scale.z = mesh.scale.z;
38 |
39 | this.mesh.position.x = mesh.position.x;
40 | this.mesh.position.y = mesh.position.y;
41 | this.mesh.position.z = mesh.position.z + 0.01;
42 |
43 | this.mesh.rotation.x = mesh.rotation.x;
44 | this.mesh.rotation.y = mesh.rotation.y;
45 | this.mesh.rotation.z = mesh.rotation.z;
46 |
47 | this.mesh.setParent(this.scene);
48 | }
49 |
50 | /**
51 | * Element.
52 | */
53 | setElement(element) {
54 | console.log(element.id);
55 |
56 | if (element.id === 'collections') {
57 | const { index, medias } = element;
58 | const media = medias[index];
59 |
60 | this.createProgram(media.texture);
61 | this.createMesh(media.mesh);
62 |
63 | this.transition = 'detail';
64 | } else {
65 | this.createProgram(element.texture);
66 | this.createMesh(element.mesh);
67 |
68 | this.transition = 'collections';
69 | }
70 | }
71 |
72 | /**
73 | * Animations.
74 | */
75 | animate(element, onComplete) {
76 | const timeline = GSAP.timeline({});
77 |
78 | timeline.to(
79 | this.mesh.scale,
80 | {
81 | duration: 1.5,
82 | ease: 'expo.inOut',
83 | x: element.scale.x,
84 | y: element.scale.y,
85 | z: element.scale.z,
86 | },
87 | 0
88 | );
89 |
90 | timeline.to(
91 | this.mesh.position,
92 | {
93 | duration: 1.5,
94 | ease: 'expo.inOut',
95 | x: element.position.x,
96 | y: element.position.y,
97 | z: element.position.z,
98 | },
99 | 0
100 | );
101 |
102 | timeline.to(
103 | this.mesh.rotation,
104 | {
105 | duration: 1.5,
106 | ease: 'expo.inOut',
107 | x: element.rotation.x,
108 | y: element.rotation.y,
109 | z: element.rotation.z,
110 | },
111 | 0
112 | );
113 |
114 | timeline.call((_) => {
115 | onComplete();
116 | });
117 |
118 | timeline.call((_) => {
119 | this.scene.removeChild(this.mesh);
120 | }, null, '+=0.2'); // prettier-ignore
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/components/Canvas/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import GSAP from 'gsap';
3 | import { Camera, Renderer, Transform } from 'ogl';
4 |
5 | import About from './About';
6 | import Collections from './Collections';
7 | import Detail from './Detail';
8 | import Home from './Home';
9 |
10 | import Transition from './Transition';
11 |
12 | export default class Canvas {
13 | constructor({ template }) {
14 | this.template = template;
15 |
16 | this.x = {
17 | start: 0,
18 | distance: 0,
19 | end: 0,
20 | };
21 |
22 | this.y = {
23 | start: 0,
24 | distance: 0,
25 | end: 0,
26 | };
27 |
28 | this.createRenderer();
29 | this.createCamera();
30 | this.createScene();
31 |
32 | this.onResize();
33 | }
34 |
35 | createRenderer() {
36 | this.renderer = new Renderer({
37 | alpha: true,
38 | antialias: true,
39 | });
40 |
41 | this.gl = this.renderer.gl;
42 |
43 | document.body.appendChild(this.gl.canvas);
44 | }
45 |
46 | createCamera() {
47 | this.camera = new Camera(this.gl);
48 |
49 | this.camera.position.z = 5;
50 | }
51 |
52 | createScene() {
53 | this.scene = new Transform();
54 | }
55 |
56 | // Home
57 | createHome() {
58 | this.home = new Home({
59 | gl: this.gl,
60 | scene: this.scene,
61 | sizes: this.sizes,
62 | });
63 | }
64 |
65 | destroyHome() {
66 | if (!this.home) return;
67 |
68 | this.home.destroy();
69 | this.home = null;
70 | }
71 |
72 | // About
73 | createAbout() {
74 | this.about = new About({
75 | gl: this.gl,
76 | scene: this.scene,
77 | sizes: this.sizes,
78 | });
79 | }
80 |
81 | destroyAbout() {
82 | if (!this.about) return;
83 |
84 | this.about.destroy();
85 | this.about = null;
86 | }
87 |
88 | // Collections
89 | createCollections() {
90 | this.collections = new Collections({
91 | gl: this.gl,
92 | scene: this.scene,
93 | sizes: this.sizes,
94 | transition: this.transition,
95 | });
96 | }
97 |
98 | destroyCollections() {
99 | if (!this.collections) return;
100 |
101 | this.collections.destroy();
102 | this.collections = null;
103 | }
104 |
105 | // Detail
106 |
107 | createDetail() {
108 | this.detail = new Detail({
109 | gl: this.gl,
110 | scene: this.scene,
111 | sizes: this.sizes,
112 | transition: this.transition,
113 | });
114 | }
115 |
116 | destroyDetail() {
117 | if (!this.detail) return;
118 |
119 | this.detail.destroy();
120 | this.detail = null;
121 | }
122 |
123 | // Events
124 | onPreloaded() {
125 | this.onChangeEnd(this.template);
126 | }
127 |
128 | onChangeStart(template, url) {
129 | if (this.home) {
130 | this.home.hide();
131 | }
132 |
133 | if (this.collections) {
134 | this.collections.hide();
135 | }
136 |
137 | if (this.detail) {
138 | this.detail.hide();
139 | }
140 |
141 | if (this.about) {
142 | this.about.hide();
143 | }
144 |
145 | this.isFromCollectionsToDetail = this.template === 'collections' && url.indexOf('detail') > -1; // prettier-ignore
146 | this.isFromDetailToCollections = this.template === 'detail' && url.indexOf('collections') > -1; // prettier-ignore
147 |
148 | if (this.isFromCollectionsToDetail || this.isFromDetailToCollections) {
149 | this.transition = new Transition({
150 | gl: this.gl,
151 | scene: this.scene,
152 | sizes: this.sizes,
153 | url,
154 | });
155 |
156 | this.transition.setElement(this.collections || this.detail);
157 | }
158 | }
159 |
160 | onChangeEnd(template) {
161 | if (template === 'home') {
162 | this.createHome();
163 | } else {
164 | this.destroyHome();
165 | }
166 |
167 | if (template === 'about') {
168 | this.createAbout();
169 | } else if (this.about) {
170 | this.destroyAbout();
171 | }
172 |
173 | if (template === 'detail') {
174 | this.createDetail();
175 | } else if (this.detail) {
176 | this.destroyDetail();
177 | }
178 |
179 | if (template === 'collections') {
180 | this.createCollections();
181 | } else if (this.collections) {
182 | this.destroyCollections();
183 | }
184 |
185 | this.template = template;
186 | }
187 |
188 | onResize() {
189 | this.renderer.setSize(window.innerWidth, window.innerHeight);
190 |
191 | this.camera.perspective({
192 | aspect: window.innerWidth / window.innerHeight,
193 | });
194 |
195 | const fov = this.camera.fov * (Math.PI / 180);
196 | const height = 2 * Math.tan(fov / 2) * this.camera.position.z;
197 | const width = height * this.camera.aspect;
198 |
199 | this.sizes = {
200 | height,
201 | width,
202 | };
203 |
204 | const values = {
205 | sizes: this.sizes,
206 | };
207 |
208 | if (this.about) {
209 | this.about.onResize(values);
210 | }
211 |
212 | if (this.collections) {
213 | this.collections.onResize(values);
214 | }
215 |
216 | if (this.detail) {
217 | this.detail.onResize(values);
218 | }
219 |
220 | if (this.home) {
221 | this.home.onResize(values);
222 | }
223 | }
224 |
225 | onTouchDown(e) {
226 | this.isDown = true;
227 |
228 | this.x.start = e.touches ? e.touches[0].clientX : e.clientX;
229 | this.y.start = e.touches ? e.touches[0].clientY : e.clientY;
230 |
231 | const values = {
232 | x: this.x,
233 | y: this.y,
234 | };
235 |
236 | if (this.about) {
237 | this.about.onTouchDown(values);
238 | }
239 |
240 | if (this.collections) {
241 | this.collections.onTouchDown(values);
242 | }
243 |
244 | if (this.detail) {
245 | this.detail.onTouchDown(values);
246 | }
247 |
248 | if (this.home) {
249 | this.home.onTouchDown(values);
250 | }
251 | }
252 |
253 | onTouchMove(e) {
254 | if (!this.isDown) return;
255 |
256 | const x = e.touches ? e.touches[0].clientX : e.clientX;
257 | const y = e.touches ? e.touches[0].clientY : e.clientY;
258 |
259 | this.x.end = x;
260 | this.y.end = y;
261 |
262 | const values = {
263 | x: this.x,
264 | y: this.y,
265 | };
266 |
267 | if (this.about) {
268 | this.about.onTouchMove(values);
269 | }
270 |
271 | if (this.collections) {
272 | this.collections.onTouchMove(values);
273 | }
274 |
275 | if (this.detail) {
276 | this.detail.onTouchMove(values);
277 | }
278 |
279 | if (this.home) {
280 | this.home.onTouchMove(values);
281 | }
282 | }
283 |
284 | onTouchUp(e) {
285 | this.isDown = false;
286 |
287 | const x = e.changedTouches ? e.changedTouches[0].clientX : e.clientX;
288 | const y = e.changedTouches ? e.changedTouches[0].clientY : e.clientY;
289 |
290 | this.x.end = x;
291 | this.y.end = y;
292 |
293 | const values = {
294 | x: this.x,
295 | y: this.y,
296 | };
297 |
298 | if (this.about) {
299 | this.about.onTouchUp(values);
300 | }
301 |
302 | if (this.collections) {
303 | this.collections.onTouchUp(values);
304 | }
305 |
306 | if (this.detail) {
307 | this.detail.onTouchUp(values);
308 | }
309 |
310 | if (this.home) {
311 | this.home.onTouchUp(values);
312 | }
313 | }
314 |
315 | onWheel(e) {
316 | if (this.home) {
317 | this.home.onWheel(e);
318 | }
319 |
320 | if (this.collections) {
321 | this.collections.onWheel(e);
322 | }
323 | }
324 |
325 | // Loop.
326 |
327 | update(scroll) {
328 | if (this.about) {
329 | this.about.update(scroll);
330 | }
331 |
332 | if (this.collections) {
333 | this.collections.update();
334 | }
335 |
336 | if (this.detail) {
337 | this.detail.update();
338 | }
339 |
340 | if (this.home) {
341 | this.home.update();
342 | }
343 |
344 | this.renderer.render({
345 | camera: this.camera,
346 | scene: this.scene,
347 | });
348 | }
349 | }
350 |
--------------------------------------------------------------------------------
/app/components/Navigation.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 |
3 | import Component from 'classes/Component';
4 |
5 | import { COLOR_BRIGHT_GREY, COLOR_WHITE } from 'utils/color';
6 |
7 | export default class Navigation extends Component {
8 | constructor({ template }) {
9 | super({
10 | element: '.navigation',
11 | elements: {
12 | items: '.navigation__list__item',
13 | links: '.navigation__list__link',
14 | },
15 | });
16 |
17 | this.onChange(template);
18 | }
19 |
20 | onChange(template) {
21 | if (template === 'about') {
22 | GSAP.to(this.element, {
23 | color: COLOR_BRIGHT_GREY,
24 | duration: 1.5,
25 | });
26 |
27 | GSAP.to(this.elements.items[0], {
28 | autoAlpha: 1,
29 | delay: 0.75,
30 | duration: 0.75,
31 | });
32 |
33 | GSAP.to(this.elements.items[1], {
34 | autoAlpha: 0,
35 | duration: 0.75,
36 | });
37 | } else {
38 | GSAP.to(this.element, {
39 | color: COLOR_WHITE,
40 | duration: 1.5,
41 | });
42 |
43 | GSAP.to(this.elements.items[0], {
44 | autoAlpha: 0,
45 | duration: 0.75,
46 | });
47 |
48 | GSAP.to(this.elements.items[1], {
49 | autoAlpha: 1,
50 | delay: 0.75,
51 | duration: 0.75,
52 | });
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/components/Preloader.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | /* eslint-disable no-undef */
3 | import { Texture } from 'ogl';
4 |
5 | import GSAP from 'gsap';
6 |
7 | import Component from 'classes/Component';
8 |
9 | import each from 'lodash/each';
10 |
11 | import { split } from 'utils/text';
12 |
13 | export default class Preloader extends Component {
14 | constructor({ canvas }) {
15 | super({
16 | element: '.preloader',
17 | elements: {
18 | title: '.preloader__text',
19 | number: '.preloader__number',
20 | numberText: '.preloader__number__text',
21 | },
22 | });
23 |
24 | this.canvas = canvas;
25 |
26 | window.TEXTURES = {};
27 |
28 | split({
29 | element: this.elements.title,
30 | expression: ' /g, ' ')
26 |
27 | figure.about__content__media
28 | img.about__content__media__image(data-src=section.primary.image.url alt=section.primary.image.alt)
29 |
30 | if section.slice_type == 'highlight'
31 | section.about__highlight
32 | .about__highlight__wrapper
33 | if section.primary.label
34 | p.about__highlight__label(data-animation="label")=section.primary.label
35 |
36 | h3.about__highlight__title(data-animation='highlight')=section.primary.title
37 |
38 | each media in section.items
39 | figure.about__highlight__media
40 | img.about__highlight__media__image(data-src=media.image.url alt=media.image.alt)
41 |
42 | if section.slice_type == 'gallery'
43 | section.about__gallery
44 | .about__gallery__wrapper
45 | each media in section.items
46 | figure.about__gallery__media
47 | img.about__gallery__media__image(data-src=media.image.url alt=media.image.alt)
48 |
--------------------------------------------------------------------------------
/views/pages/collections.pug:
--------------------------------------------------------------------------------
1 | extends ../_includes/layout
2 |
3 | block variables
4 | - var template = 'collections'
5 |
6 | block content
7 | .collections(data-background="#c97164" data-color="#f9f1e7")
8 | .collections__wrapper
9 | .collections__titles
10 | each collection, index in collections
11 | .collections__titles__label!=`${home.data.collection}
',
31 | });
32 |
33 | split({
34 | element: this.elements.title,
35 | expression: '
',
36 | });
37 |
38 | this.elements.titleSpans =
39 | this.elements.title.querySelectorAll('span span');
40 |
41 | this.length = 0;
42 |
43 | this.createLoader();
44 | }
45 |
46 | createLoader() {
47 | each(window.ASSETS, (image) => {
48 | const texture = new Texture(this.canvas.gl, {
49 | generateMipmaps: false,
50 | });
51 |
52 | const media = new window.Image();
53 |
54 | media.crossOrigin = 'anonymous';
55 | media.src = image;
56 |
57 | media.onload = (_) => {
58 | texture.image = media;
59 |
60 | this.onAssetLoaded();
61 | };
62 |
63 | window.TEXTURES[image] = texture;
64 | });
65 | }
66 |
67 | onAssetLoaded(image) {
68 | this.length++;
69 |
70 | const percent = this.length / window.ASSETS.length;
71 |
72 | this.elements.numberText.innerHTML = `${Math.round(percent * 100)}%`;
73 |
74 | if (percent === 1) {
75 | this.onLoaded();
76 | }
77 | }
78 |
79 | onLoaded() {
80 | return new Promise((resolve) => {
81 | this.emit('completed');
82 |
83 | this.animateOut = GSAP.timeline({
84 | delay: 1,
85 | });
86 |
87 | this.animateOut.to(this.elements.titleSpans, {
88 | duration: 1.5,
89 | ease: 'expo.out',
90 | stagger: 0.1,
91 | y: '150%',
92 | });
93 |
94 | this.animateOut.to(
95 | this.elements.numberText,
96 | {
97 | duration: 1.5,
98 | ease: 'expo.out',
99 | stagger: 0.1,
100 | y: '100%',
101 | },
102 | '-=1.4'
103 | );
104 |
105 | this.animateOut.to(this.element, {
106 | autoAlpha: 0,
107 | duration: 1.5,
108 | });
109 |
110 | this.animateOut.call((_) => {
111 | this.destroy();
112 | });
113 | });
114 | }
115 |
116 | destroy() {
117 | this.element.parentNode.removeChild(this.element);
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/app/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | /* eslint-disable no-new */
3 |
4 | import NormalizeWheel from 'normalize-wheel';
5 |
6 | import each from 'lodash/each';
7 |
8 | import Canvas from 'components/Canvas';
9 | import Detection from 'classes/Detection';
10 |
11 | import Navigation from 'components/Navigation';
12 | import Preloader from 'components/Preloader';
13 |
14 | import About from 'pages/About';
15 | import Collections from 'pages/Collections';
16 | import Home from 'pages/Home';
17 | import Detail from 'pages/Detail';
18 |
19 | class App {
20 | constructor() {
21 | this.createContent();
22 |
23 | this.createCanvas();
24 | this.createPreloader();
25 | this.createNavigation();
26 | this.createPages();
27 |
28 | this.addEventListeners();
29 | this.addLinkListeners();
30 |
31 | this.onResize();
32 |
33 | this.update();
34 | }
35 |
36 | createNavigation() {
37 | this.navigation = new Navigation({
38 | template: this.template,
39 | });
40 | }
41 |
42 | createPreloader() {
43 | this.preloader = new Preloader({
44 | canvas: this.canvas,
45 | });
46 |
47 | this.preloader.once('completed', this.onPreloaded.bind(this));
48 | }
49 |
50 | createCanvas() {
51 | this.canvas = new Canvas({
52 | template: this.template,
53 | });
54 | }
55 |
56 | createContent() {
57 | this.content = document.querySelector('.content');
58 | this.template = this.content.getAttribute('data-template');
59 | }
60 |
61 | createPages() {
62 | this.pages = {
63 | about: new About(),
64 | collections: new Collections(),
65 | home: new Home(),
66 | detail: new Detail(),
67 | };
68 |
69 | this.page = this.pages[this.template];
70 | this.page.create();
71 | }
72 |
73 | /*
74 | * Events
75 | */
76 |
77 | onPreloaded() {
78 | this.onResize();
79 |
80 | this.canvas.onPreloaded();
81 |
82 | this.page.show();
83 | }
84 |
85 | onPopState() {
86 | this.onChange({
87 | url: window.location.pathname,
88 | push: true,
89 | });
90 | }
91 |
92 | async onChange({ url, push = true }) {
93 | this.canvas.onChangeStart(this.template, url);
94 |
95 | await this.page.hide();
96 |
97 | const res = await window.fetch(url);
98 |
99 | if (res.status === 200) {
100 | const html = await res.text();
101 | const div = document.createElement('div');
102 |
103 | if (push) {
104 | window.history.pushState({}, '', url);
105 | }
106 |
107 | div.innerHTML = html;
108 |
109 | const divContent = div.querySelector('.content');
110 |
111 | this.template = divContent.getAttribute('data-template');
112 |
113 | this.navigation.onChange(this.template);
114 |
115 | this.content.setAttribute('data-template', this.template);
116 | this.content.innerHTML = divContent.innerHTML;
117 |
118 | this.canvas.onChangeEnd(this.template);
119 |
120 | this.page = this.pages[this.template];
121 | this.page.create();
122 |
123 | this.onResize();
124 |
125 | this.page.show();
126 |
127 | this.addLinkListeners();
128 | } else {
129 | console.error(`response status: ${res.status}`);
130 | }
131 | }
132 |
133 | onResize() {
134 | if (this.page && this.page.onResize) {
135 | this.page.onResize();
136 | }
137 |
138 | window.requestAnimationFrame((_) => {
139 | if (this.canvas && this.canvas.onResize) {
140 | this.canvas.onResize();
141 | }
142 | });
143 | }
144 |
145 | onTouchDown(e) {
146 | if (this.canvas && this.canvas.onTouchDown) {
147 | this.canvas.onTouchDown(e);
148 | }
149 | }
150 |
151 | onTouchMove(e) {
152 | if (this.canvas && this.canvas.onTouchMove) {
153 | this.canvas.onTouchMove(e);
154 | }
155 | }
156 |
157 | onTouchUp(e) {
158 | if (this.canvas && this.canvas.onTouchUp) {
159 | this.canvas.onTouchUp(e);
160 | }
161 | }
162 |
163 | onWheel(e) {
164 | const normalizedWheel = NormalizeWheel(e);
165 |
166 | if (this.canvas && this.canvas.onWheel) {
167 | this.canvas.onWheel(normalizedWheel);
168 | }
169 |
170 | if (this.page && this.page.onWheel) {
171 | this.page.onWheel(normalizedWheel);
172 | }
173 | }
174 |
175 | /*
176 | * LOop
177 | */
178 |
179 | update() {
180 | if (this.page && this.page.update) {
181 | this.page.update();
182 | }
183 |
184 | if (this.canvas && this.canvas.update) {
185 | this.canvas.update(this.page.scroll);
186 | }
187 |
188 | this.frame = window.requestAnimationFrame(this.update.bind(this));
189 | }
190 |
191 | /*
192 | * Listeners
193 | */
194 |
195 | addEventListeners() {
196 | window.addEventListener('mousewheel', this.onWheel.bind(this));
197 |
198 | window.addEventListener('mousedown', this.onTouchDown.bind(this));
199 | window.addEventListener('mousemove', this.onTouchMove.bind(this));
200 | window.addEventListener('mouseup', this.onTouchUp.bind(this));
201 |
202 | window.addEventListener('touchstart', this.onTouchDown.bind(this));
203 | window.addEventListener('touchmove', this.onTouchMove.bind(this));
204 | window.addEventListener('touchend', this.onTouchUp.bind(this));
205 |
206 | window.addEventListener('resize', this.onResize.bind(this));
207 | }
208 |
209 | addLinkListeners() {
210 | const links = document.querySelectorAll('a');
211 |
212 | each(links, (link) => {
213 | link.onclick = (event) => {
214 | event.preventDefault();
215 |
216 | const { href } = link;
217 | this.onChange({ url: href });
218 | };
219 | });
220 | }
221 | }
222 |
223 | new App();
224 |
--------------------------------------------------------------------------------
/app/pages/About/index.js:
--------------------------------------------------------------------------------
1 | import Page from 'classes/Page';
2 |
3 | export default class About extends Page {
4 | constructor() {
5 | super({
6 | id: 'about',
7 | element: '.about',
8 | elements: {
9 | wrapper: '.about__wrapper',
10 | navigation: document.querySelector('.navigation'),
11 | title: '.about__title',
12 | },
13 | });
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/pages/Collections/index.js:
--------------------------------------------------------------------------------
1 | import Page from 'classes/Page';
2 |
3 | export default class Collections extends Page {
4 | constructor() {
5 | super({ id: 'collections', element: '.collections' });
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/pages/Detail/index.js:
--------------------------------------------------------------------------------
1 | import GSAP from 'gsap';
2 | import Button from 'classes/Button';
3 | import Page from 'classes/Page';
4 |
5 | export default class Detail extends Page {
6 | constructor() {
7 | super({
8 | id: 'detail',
9 | element: '.detail',
10 | elements: {
11 | button: '.detail__button',
12 | },
13 | });
14 | }
15 |
16 | create() {
17 | super.create();
18 |
19 | this.link = new Button({
20 | element: this.elements.button,
21 | });
22 | }
23 |
24 | show() {
25 | const timeline = GSAP.timeline({
26 | delay: 2,
27 | });
28 |
29 | timeline.fromTo(
30 | this.element,
31 | {
32 | autoAlpha: 0,
33 | },
34 | {
35 | autoAlpha: 1,
36 | }
37 | );
38 |
39 | super.show(timeline);
40 | }
41 |
42 | destroy() {
43 | super.destroy();
44 |
45 | this.link.removeEventListeners();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/pages/Home/index.js:
--------------------------------------------------------------------------------
1 | import Button from 'classes/Button';
2 | import Page from 'classes/Page';
3 |
4 | export default class Home extends Page {
5 | constructor() {
6 | super({
7 | id: 'home',
8 | element: '.home',
9 | elements: {
10 | navigation: document.querySelector('.navigation'),
11 | link: '.home__link',
12 | },
13 | });
14 | }
15 |
16 | create() {
17 | super.create();
18 |
19 | this.link = new Button ({
20 | element: this.elements.link
21 | })
22 | }
23 |
24 | destroy() {
25 | super.destroy()
26 |
27 | this.link.removeEventListeners()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/shaders/collections-fragment.glsl:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | uniform float uAlpha;
4 | uniform sampler2D tMap;
5 |
6 | varying vec4 vPosition;
7 | varying vec2 vUv;
8 |
9 | void main() {
10 | vec4 texture = texture2D(tMap, vUv);
11 |
12 | gl_FragColor = texture;
13 | gl_FragColor.a = uAlpha;
14 | }
15 |
--------------------------------------------------------------------------------
/app/shaders/collections-vertex.glsl:
--------------------------------------------------------------------------------
1 | attribute vec3 position;
2 | attribute vec2 uv;
3 |
4 | uniform mat4 modelViewMatrix;
5 | uniform mat4 projectionMatrix;
6 |
7 | varying vec4 vPosition;
8 | varying vec2 vUv;
9 |
10 | void main() {
11 | vUv = uv;
12 |
13 | vec4 newPosition = modelViewMatrix * vec4(position, 1.0);
14 |
15 | vPosition = newPosition;
16 |
17 | gl_Position = projectionMatrix * newPosition;
18 | }
19 |
--------------------------------------------------------------------------------
/app/shaders/home-fragment.glsl:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | uniform float uAlpha;
4 | uniform sampler2D tMap;
5 |
6 | varying vec2 vUv;
7 |
8 | void main() {
9 | vec4 texture = texture2D(tMap, vUv);
10 |
11 | gl_FragColor = texture;
12 | gl_FragColor.a = uAlpha;
13 | }
14 |
--------------------------------------------------------------------------------
/app/shaders/home-vertex.glsl:
--------------------------------------------------------------------------------
1 | #define PI 3.1415926535897932384626433832795
2 |
3 | attribute vec3 position;
4 | attribute vec2 uv;
5 |
6 | uniform float uSpeed;
7 | uniform vec2 uViewportSizes;
8 |
9 | uniform mat4 modelViewMatrix;
10 | uniform mat4 projectionMatrix;
11 |
12 | varying vec2 vUv;
13 |
14 | void main() {
15 | vUv = uv;
16 |
17 | vec4 newPosition = modelViewMatrix * vec4(position, 1.0);
18 |
19 | newPosition.z -= (sin(newPosition.y / uViewportSizes.y * PI + PI / 2.0) + sin(newPosition.x / uViewportSizes.x * PI + PI / 2.0)) * abs(uSpeed);
20 |
21 | gl_Position = projectionMatrix * newPosition;
22 | }
23 |
--------------------------------------------------------------------------------
/app/shaders/plane-fragment.glsl:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | uniform float uAlpha;
4 | uniform sampler2D tMap;
5 |
6 | varying vec2 vUv;
7 |
8 | void main() {
9 | vec4 texture = texture2D(tMap, vUv);
10 |
11 | gl_FragColor = texture;
12 | gl_FragColor.a = uAlpha;
13 | }
14 |
--------------------------------------------------------------------------------
/app/shaders/plane-vertex.glsl:
--------------------------------------------------------------------------------
1 | attribute vec3 position;
2 | attribute vec2 uv;
3 |
4 | uniform mat4 modelViewMatrix;
5 | uniform mat4 projectionMatrix;
6 |
7 | varying vec2 vUv;
8 |
9 | void main() {
10 | vUv = uv;
11 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
12 | }
--------------------------------------------------------------------------------
/app/utils/color.js:
--------------------------------------------------------------------------------
1 | export const COLOR_WHITE = '#f9f1e7';
2 | export const COLOR_BRIGHT_GREY = '#37384c';
--------------------------------------------------------------------------------
/app/utils/text.js:
--------------------------------------------------------------------------------
1 | import each from 'lodash/each';
2 |
3 | /**
4 | * Function that splits text into spans
5 | * @param {string || HTMLElement} element
6 | * @param {string} expression
7 | * @param {bool} append
8 | */
9 | export function split({ element, expression = ' ', append = true }) {
10 | const words = splitText(element.innerHTML.toString().trim(), expression);
11 |
12 | let innerHTML = '';
13 |
14 | each(words, (line) => {
15 | if (line.indexOf('
') > -1) {
16 | const lines = line.split('
');
17 |
18 | each(lines, (line, index) => {
19 | innerHTML += index > 0 ? '
' + parseLine(line) : parseLine(line);
20 | });
21 | } else {
22 | innerHTML += parseLine(line);
23 | }
24 | });
25 |
26 | element.innerHTML = innerHTML;
27 |
28 | const spans = element.querySelectorAll('span');
29 |
30 | if (append) {
31 | each(spans, (span) => {
32 | const isSingleLetter = span.textContent.length === 1;
33 | const isNotEmpty = span.innerHTML.trim() !== '';
34 | const isNotAndCharacter = span.textContent !== '&';
35 | const isNotDashCharacter = span.textContent !== '-';
36 |
37 | if (
38 | isSingleLetter &&
39 | isNotEmpty &&
40 | isNotAndCharacter &&
41 | isNotDashCharacter
42 | ) {
43 | span.innerHTML = `${span.textContent} `;
44 | }
45 | });
46 | }
47 |
48 | return spans;
49 | }
50 |
51 | export function calculate(spans) {
52 | if (!spans.length) {
53 | return;
54 | }
55 | const lines = [];
56 | let words = [];
57 |
58 | let position = spans[0].offsetTop;
59 |
60 | each(spans, (span, index) => {
61 | if (span.offsetTop === position) {
62 | words.push(span);
63 | }
64 |
65 | if (span.offsetTop !== position) {
66 | lines.push(words);
67 |
68 | words = [];
69 | words.push(span);
70 |
71 | position = span.offsetTop;
72 | }
73 |
74 | if (index + 1 === spans.length) {
75 | lines.push(words);
76 | }
77 | });
78 |
79 | return lines;
80 | }
81 |
82 | function splitText(text, expression) {
83 | const splits = text.split('
');
84 |
85 | let words = [];
86 |
87 | each(splits, (item, index) => {
88 | if (index > 0) {
89 | words.push('
');
90 | }
91 |
92 | words = words.concat(item.split(expression));
93 |
94 | let isLink = false;
95 | let link = '';
96 |
97 | const innerHTML = [];
98 |
99 | each(words, (word) => {
100 | if (!isLink && (word.includes('') || word.includes('/strong>'))) {
111 | innerHTML.push(link);
112 |
113 | link = '';
114 | }
115 |
116 | if (!isLink && link === '') {
117 | innerHTML.push(word);
118 | }
119 |
120 | if (isLink && (word.includes('/a>') || word.includes('/strong>'))) {
121 | isLink = false;
122 | }
123 | });
124 |
125 | words = innerHTML;
126 | });
127 |
128 | return words;
129 | }
130 |
131 | function parseLine(line) {
132 | line = line.trim();
133 |
134 | if (line === '' || line === ' ') {
135 | return line;
136 | } else {
137 | return line === '
'
138 | ? '
'
139 | : `${line}` + (line.length > 1 ? ' ' : '');
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/fonts/george_x-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/fonts/george_x-webfont.woff
--------------------------------------------------------------------------------
/fonts/george_x-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/fonts/george_x-webfont.woff2
--------------------------------------------------------------------------------
/fonts/suissebpintl-light-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/fonts/suissebpintl-light-webfont.woff
--------------------------------------------------------------------------------
/fonts/suissebpintl-light-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/fonts/suissebpintl-light-webfont.woff2
--------------------------------------------------------------------------------
/fonts/suissebpintl-regular-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/fonts/suissebpintl-regular-webfont.woff
--------------------------------------------------------------------------------
/fonts/suissebpintl-regular-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/fonts/suissebpintl-regular-webfont.woff2
--------------------------------------------------------------------------------
/fonts/suissebpintl-ultralight-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/fonts/suissebpintl-ultralight-webfont.woff
--------------------------------------------------------------------------------
/fonts/suissebpintl-ultralight-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/fonts/suissebpintl-ultralight-webfont.woff2
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "floema",
3 | "version": "1.0.0",
4 | "description": "A boilerplate which inspired from luis henrique bizzaro course on awwwards but intent to use latest and v5 techniques",
5 | "scripts": {
6 | "backend:build": "node app.js",
7 | "backend:development": "nodemon app.js",
8 | "frontend:build": "webpack --progress --config webpack.config.build.js",
9 | "frontend:development": "webpack serve --progress --config webpack.config.development.js",
10 | "start": "concurrently --kill-others \"npm run backend:development\" \"npm run frontend:development\""
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/whizzbbig/Webpack-boilerplate.git"
15 | },
16 | "keywords": [],
17 | "author": "whizzbbig",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/whizzbbig/Webpack-boilerplate/issues"
21 | },
22 | "homepage": "https://github.com/whizzbbig/Webpack-boilerplate#readme",
23 | "devDependencies": {
24 | "@babel/core": "^7.15.5",
25 | "babel-loader": "^8.2.2",
26 | "clean-webpack-plugin": "^4.0.0",
27 | "concurrently": "^6.3.0",
28 | "copy-webpack-plugin": "^9.0.1",
29 | "css-loader": "^6.3.0",
30 | "eslint": "^7.32.0",
31 | "eslint-config-prettier": "^8.3.0",
32 | "eslint-config-standard": "^16.0.3",
33 | "eslint-plugin-import": "^2.24.2",
34 | "eslint-plugin-node": "^11.1.0",
35 | "eslint-plugin-promise": "^5.1.0",
36 | "eslint-webpack-plugin": "^3.0.1",
37 | "glslify-loader": "^2.0.0",
38 | "gsap": "^3.8.0",
39 | "image-minimizer-webpack-plugin": "^2.2.0",
40 | "imagemin": "^8.0.1",
41 | "imagemin-gifsicle": "^7.0.0",
42 | "imagemin-jpegtran": "^7.0.0",
43 | "imagemin-optipng": "^8.0.0",
44 | "imagemin-svgo": "^10.0.0",
45 | "include-media": "^1.4.10",
46 | "lodash": "^4.17.21",
47 | "mini-css-extract-plugin": "^2.3.0",
48 | "node-fetch": "^2.6.7",
49 | "nodemon": "^2.0.15",
50 | "normalize-wheel": "^1.0.1",
51 | "ogl": "0.0.76",
52 | "postcss-loader": "^6.1.1",
53 | "prefix": "^1.0.0",
54 | "raw-loader": "^4.0.2",
55 | "sass": "^1.42.1",
56 | "sass-loader": "^12.1.0",
57 | "terser-webpack-plugin": "^5.2.4",
58 | "webpack": "^5.55.1",
59 | "webpack-cli": "^4.8.0",
60 | "webpack-dev-server": "^4.3.0",
61 | "webpack-merge": "^5.8.0"
62 | },
63 | "dependencies": {
64 | "@prismicio/client": "^6.2.0",
65 | "@prismicio/helpers": "^2.1.1",
66 | "body-parser": "^1.19.0",
67 | "dotenv": "^10.0.0",
68 | "errorhandler": "^1.5.1",
69 | "express": "^4.17.1",
70 | "method-override": "^3.0.0",
71 | "morgan": "^1.10.0",
72 | "pug": "^3.0.2",
73 | "ua-parser-js": "^1.0.2"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/cool.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/cool.txt
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/george_x-webfont.2439ae4421675108a31a..woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/george_x-webfont.2439ae4421675108a31a..woff2
--------------------------------------------------------------------------------
/public/george_x-webfont.e9b3e24af9abd6a929f7..woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/george_x-webfont.e9b3e24af9abd6a929f7..woff
--------------------------------------------------------------------------------
/public/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Floema",
3 | "short_name": "Floema",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png",
9 | "purpose": "any maskable"
10 | },
11 | {
12 | "src": "/android-chrome-512x512.png",
13 | "sizes": "512x512",
14 | "type": "image/png",
15 | "purpose": "any maskable"
16 | }
17 | ],
18 | "theme_color": "#f9f1e7",
19 | "background_color": "#f9f1e7",
20 | "display": "standalone",
21 | "start_url": "/"
22 | }
23 |
--------------------------------------------------------------------------------
/public/suissebpintl-light-webfont.39f09bf2406ba7b4d05b..woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/suissebpintl-light-webfont.39f09bf2406ba7b4d05b..woff2
--------------------------------------------------------------------------------
/public/suissebpintl-light-webfont.ce0a91a9ada4d0491ed6..woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/suissebpintl-light-webfont.ce0a91a9ada4d0491ed6..woff
--------------------------------------------------------------------------------
/public/suissebpintl-regular-webfont.9f57dc57c8c3b9376927..woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/suissebpintl-regular-webfont.9f57dc57c8c3b9376927..woff2
--------------------------------------------------------------------------------
/public/suissebpintl-regular-webfont.a7d0f1319327bf3a66cd..woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/suissebpintl-regular-webfont.a7d0f1319327bf3a66cd..woff
--------------------------------------------------------------------------------
/public/suissebpintl-ultralight-webfont.3021905000cbcacc2fd7..woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/suissebpintl-ultralight-webfont.3021905000cbcacc2fd7..woff
--------------------------------------------------------------------------------
/public/suissebpintl-ultralight-webfont.d4b1f03703a50cf491fd..woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/public/suissebpintl-ultralight-webfont.d4b1f03703a50cf491fd..woff2
--------------------------------------------------------------------------------
/shared/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/shared/android-chrome-192x192.png
--------------------------------------------------------------------------------
/shared/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/shared/android-chrome-512x512.png
--------------------------------------------------------------------------------
/shared/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/shared/apple-touch-icon.png
--------------------------------------------------------------------------------
/shared/cool.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/shared/cool.txt
--------------------------------------------------------------------------------
/shared/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/shared/favicon-16x16.png
--------------------------------------------------------------------------------
/shared/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whizzbbig/floema_/31d10150d15a2f83e988e22bc430f729e0dac227/shared/favicon-32x32.png
--------------------------------------------------------------------------------
/shared/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/shared/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Floema",
3 | "short_name": "Floema",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png",
9 | "purpose": "any maskable"
10 | },
11 | {
12 | "src": "/android-chrome-512x512.png",
13 | "sizes": "512x512",
14 | "type": "image/png",
15 | "purpose": "any maskable"
16 | }
17 | ],
18 | "theme_color": "#f9f1e7",
19 | "background_color": "#f9f1e7",
20 | "display": "standalone",
21 | "start_url": "/"
22 | }
23 |
--------------------------------------------------------------------------------
/styles/base/base.scss:
--------------------------------------------------------------------------------
1 | *,
2 | *::after,
3 | *::before {
4 | box-sizing: border-box;
5 | user-select: none
6 | }
7 |
8 | html {
9 | @extend %cover;
10 | $vw: 100vw;
11 | font-size: calc($vw / 1920px * 10px);
12 | position: fixed;
13 | line-height: 1;
14 | overflow: hidden;
15 | background: $color-contessa;
16 | color: $color-white;
17 |
18 | @include media('
')
3 |
4 | .preloader__number
5 | .preloader__number__text 0%
--------------------------------------------------------------------------------
/views/_includes/scripts.pug:
--------------------------------------------------------------------------------
1 | script.
2 | window.ASSETS = !{JSON.stringify(assets).replace(/<\//g, "<\\/")}
3 |
4 | script(src="/main.js")
5 |
--------------------------------------------------------------------------------
/views/index.pug:
--------------------------------------------------------------------------------
1 | extends /_includes/layout
2 |
3 | block content
4 |
--------------------------------------------------------------------------------
/views/pages/about.pug:
--------------------------------------------------------------------------------
1 | extends ../_includes/layout
2 |
3 | block variables
4 | - var template = 'about'
5 |
6 | block content
7 | .about(data-background="#b2b8c3" data-color="#37384c")
8 | .about__wrapper
9 | section.about__gallery
10 | .about__gallery__wrapper
11 | each media in about.data.gallery
12 | figure.about__gallery__media
13 | img.about__gallery__media__image(data-src=media.image.url alt=media.image.alt)
14 |
15 | each section in about.data.body
16 | if section.slice_type == 'title'
17 | h2.about__title(data-animation="title")!=section.primary.text.replace(/\n/g, '
')
18 |
19 | if section.slice_type == 'content'
20 | section.about__content(class=`${section.primary.type === 'Left' ? 'about__content--left' : 'about__content--right' }`)
21 | .about__content__wrapper
22 | .about__content__box
23 | p.about__content__label(data-animation="label")!=section.primary.label
24 |
25 | .about__content__description!=PrismicH.asHTML(section.primary.description).replace(/
${Numbers(index)}`
12 | .collections__titles__title=collection.data.title
13 |
14 | each collection, index in collections
15 | .collections__titles__label!=`${home.data.collection}
${Numbers(index)}`
16 | .collections__titles__title=collection.data.title
17 |
18 | each collection, index in collections
19 | .collections__titles__label!=`${home.data.collection}
${Numbers(index)}`
20 | .collections__titles__title=collection.data.title
21 |
22 | .collections__gallery
23 | .collections__gallery__wrapper
24 | each collection, index in collections
25 | each product in collection.data.list
26 | a.collections__gallery__link(href='/detail/' + product.product.uid)
27 | figure.collections__gallery__media(data-index=index)
28 | img.collections__gallery__media__image(data-src=product.product.data.image.url alt=product.product.data.image.alt)
29 |
30 | .collections__content
31 | each collection, index in collections
32 | article.collections__article(class=`${index === 0 ? 'collections__article--active' : ''}`)
33 | h1.collections__article__title=`${collection.data.title} ${home.data.collection}`
34 | p.collections__article__description=collection.data.description
35 |
36 | .collections__mobile
37 | each collection, index in collections
38 | .collections__mobile__item
39 | .collections__mobile__item__label!=`${home.data.collection}
${Numbers(index)}`
40 | .collections__mobile__item__title=collection.data.title
41 |
--------------------------------------------------------------------------------
/views/pages/detail.pug:
--------------------------------------------------------------------------------
1 | extends ../_includes/layout
2 |
3 | block variables
4 | - var template = 'detail'
5 |
6 | block content
7 | .detail(data-background="#c97164" data-color="#f9f1e7")
8 | .detail__wrapper
9 | figure.detail__media
10 | img.detail__media__image(data-src=product.data.image.url alt=product.data.image.alt)
11 |
12 | .detail__information
13 | p.detail__information__collection=product.data.collection.data.title
14 |
15 | h1.detail__information__title=product.data.title
16 |
17 | .detail__information__content
18 | .detail__information__highlights
19 | each highlight in product.data.highlights
20 | p.detail__information__highlight
21 | if highlight.highlights_icon.toLowerCase() == 'arrow'
22 | svg.detail__information__highlight__icon(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 41 41")
23 | path(stroke="currentColor" fill="none" d="M-1166.33,520.33a20,20,0,0,1,20,20,20,20,0,0,1-20,20,20,20,0,0,1-20-20A20,20,0,0,1-1166.33,520.33Zm0,.5a19.5,19.5,0,0,1,19.5,19.5,19.5,19.5,0,0,1-19.5,19.5,19.5,19.5,0,0,1-19.5-19.5A19.5,19.5,0,0,1-1166.33,520.83Zm10.75,9-.66,2.36,0,2.59-1.93-2.31-2.31-1.93,2.59-.05Zm-20.51,20.51.66-2.36,0-2.59,1.93,2.31,2.31,1.93-2.59.05Zm4.6-4.6,11.31-11.31" transform="translate(1186.83 -519.83)")
24 | else
25 | svg.detail__information__highlight__icon(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40")
26 | path(stroke="currentColor" fill="none" d="M-1078.13,529.42a19.51,19.51,0,0,1,19.5,19.5,19.5,19.5,0,0,1-19.5,19.5,19.5,19.5,0,0,1-19.5-19.5A19.51,19.51,0,0,1-1078.13,529.42Z" transform="translate(1098.13 -528.92)")
27 | path(fill="currentColor" d="M-1073.86,538.79l-2.76,8.6,8.72-2.63-8.16,4.13,8.15,4.27-8.71-2.82,2.82,8.9-4.26-8.34-4.14,8.15,2.63-8.71-8.59,2.75,8.15-4.2-8.34-4.33,8.78,2.83-2.82-8.79,4.32,8.35Z" transform="translate(1098.13 -528.92)")
28 |
29 |
30 | span.detail__information__highlight__text!=highlight.highlights_text.replace(/\n/, '
')
31 |
32 | .detail__information__list
33 | each item in product.data.informations
34 | p.detail__information__item
35 | strong.detail__information__item__title=item.informations_label
36 | span.detail__information__item__description=item.informations_description
37 |
38 | a.detail__information__link(href=product.data.link_url.url target="_blank")=product.data.link_text
39 |
40 | a.detail__button(href=Link(home.data.collections)) Close
41 | svg.detail__button__icon(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 125 61")
42 | path(stroke="currentColor" fill="none" d="M62.5.5c34.24,0,62,13.43,62,30s-27.76,30-62,30S.5,47.07.5,30.5,28.26.5,62.5.5Zm0,.5c34,0,61.5,13.21,61.5,29.5S96.47,60,62.5,60,1,46.79,1,30.5,28.53,1,62.5,1Z")
43 | path.detail__button__icon__path(stroke="#FFC400" fill="none" d="M62.5.5c34.24,0,62,13.43,62,30s-27.76,30-62,30S.5,47.07.5,30.5,28.26.5,62.5.5Zm0,.5c34,0,61.5,13.21,61.5,29.5S96.47,60,62.5,60,1,46.79,1,30.5,28.53,1,62.5,1Z")
44 |
--------------------------------------------------------------------------------
/views/pages/home.pug:
--------------------------------------------------------------------------------
1 | extends ../_includes/layout
2 |
3 | block variables
4 | - var template = 'home'
5 |
6 | block content
7 | .home(data-background="#c97164" data-color="#f9f1e7")
8 | .home__wrapper
9 | .home__titles
10 | each collection, index in collections
11 | .home__titles__label=`${home.data.collection} ${Numbers(index)}`
12 | .home__titles__title=collection.data.title
13 |
14 | .home__gallery
15 | each media in home.data.gallery
16 | figure.home__gallery__media
17 | img.home__gallery__media__image(data-src=media.image.url alt=media.image.alt)
18 |
19 | a.home__link(href=Link(home.data.collections))=home.data.button
20 | svg.home__link__icon(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 60")
21 | path(stroke="currentColor" fill="none" d="M456.5,98.5c-19.41,0-38.24.79-56,2.35a398.43,398.43,0,0,0-45.66,6.42c-6.49,1.35-12.37,2.83-17.49,4.41a91.29,91.29,0,0,0-13.13,5,30.12,30.12,0,0,0-8.12,5.43c-1.75,1.79-2.64,3.59-2.64,5.35s.89,3.56,2.64,5.35a30.12,30.12,0,0,0,8.12,5.43,91.29,91.29,0,0,0,13.13,5c5.12,1.58,11,3.06,17.49,4.41a398.43,398.43,0,0,0,45.66,6.42c17.72,1.56,36.55,2.35,56,2.35s38.24-.79,56-2.35a398.43,398.43,0,0,0,45.66-6.42c6.49-1.35,12.37-2.83,17.49-4.41a91.29,91.29,0,0,0,13.13-5,30.12,30.12,0,0,0,8.12-5.43c1.75-1.79,2.64-3.59,2.64-5.35s-.89-3.56-2.64-5.35a30.12,30.12,0,0,0-8.12-5.43,91.29,91.29,0,0,0-13.13-5c-5.12-1.58-11-3.06-17.49-4.41a398.43,398.43,0,0,0-45.66-6.42c-17.72-1.56-36.55-2.35-56-2.35m0-1c79.53,0,144,13.43,144,30s-64.47,30-144,30-144-13.43-144-30S377,97.5,456.5,97.5Z" transform="translate(-312.5 -97.5)")
22 | path.home__link__icon__path(stroke="#FFC400" fill="none" d="M456.5,98.5c-19.41,0-38.24.79-56,2.35a398.43,398.43,0,0,0-45.66,6.42c-6.49,1.35-12.37,2.83-17.49,4.41a91.29,91.29,0,0,0-13.13,5,30.12,30.12,0,0,0-8.12,5.43c-1.75,1.79-2.64,3.59-2.64,5.35s.89,3.56,2.64,5.35a30.12,30.12,0,0,0,8.12,5.43,91.29,91.29,0,0,0,13.13,5c5.12,1.58,11,3.06,17.49,4.41a398.43,398.43,0,0,0,45.66,6.42c17.72,1.56,36.55,2.35,56,2.35s38.24-.79,56-2.35a398.43,398.43,0,0,0,45.66-6.42c6.49-1.35,12.37-2.83,17.49-4.41a91.29,91.29,0,0,0,13.13-5,30.12,30.12,0,0,0,8.12-5.43c1.75-1.79,2.64-3.59,2.64-5.35s-.89-3.56-2.64-5.35a30.12,30.12,0,0,0-8.12-5.43,91.29,91.29,0,0,0-13.13-5c-5.12-1.58-11-3.06-17.49-4.41a398.43,398.43,0,0,0-45.66-6.42c-17.72-1.56-36.55-2.35-56-2.35m0-1c79.53,0,144,13.43,144,30s-64.47,30-144,30-144-13.43-144-30S377,97.5,456.5,97.5Z" transform="translate(-312.5 -97.5)")
--------------------------------------------------------------------------------
/webpack.config.build.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { merge } = require('webpack-merge');
3 | const config = require('./webpack.config');
4 |
5 | module.exports = merge(config, {
6 | mode: 'production',
7 |
8 | output: {
9 | path: path.resolve(__dirname, 'public'),
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/webpack.config.development.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { merge } = require('webpack-merge');
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 | devMiddleware: {
13 | writeToDisk: true,
14 | },
15 | },
16 |
17 | output: {
18 | path: path.resolve(__dirname, 'public'),
19 | assetModuleFilename: '[name][ext]',
20 | clean: true,
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const webpack = require('webpack');
4 |
5 | const CopyPlugin = require('copy-webpack-plugin');
6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
7 | const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
8 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
9 | const TerserPlugin = require('terser-webpack-plugin');
10 |
11 | const IS_DEVELOPMENT = process.env.NODE_ENV === 'dev';
12 |
13 | const dirApp = path.join(__dirname, 'app');
14 | const dirAssets = path.join(__dirname, 'assets');
15 | const dirShared = path.join(__dirname, 'shared');
16 | const dirStyles = path.join(__dirname, 'styles');
17 | const dirNode = 'node_modules';
18 |
19 | module.exports = {
20 | entry: [path.join(dirApp, 'index.js'), path.join(dirStyles, 'index.scss')],
21 |
22 | resolve: {
23 | modules: [dirApp, dirAssets, dirShared, dirStyles, dirNode],
24 | },
25 |
26 | plugins: [
27 | new webpack.DefinePlugin({
28 | IS_DEVELOPMENT,
29 | }),
30 |
31 | new CopyPlugin({
32 | patterns: [
33 | {
34 | from: './shared',
35 | to: '',
36 | },
37 | ],
38 | }),
39 |
40 | new MiniCssExtractPlugin({
41 | filename: '[name].css',
42 | }),
43 |
44 | new ImageMinimizerPlugin({
45 | minimizerOptions: {
46 | plugins: [
47 | // interlaced: Interlace gif for progressive rendering.
48 | ['gifsicle', { interlaced: true }],
49 |
50 | // progressive: Lossless conversion to progressive.
51 | ['jpegtran', { progressive: true }],
52 |
53 | // optimizationLevel (0-7): The optimization level 0 enables a set of
54 | // optimization operations that require minimal effort. There will be
55 | // no changes to image attributes like bit depth or color type, and no
56 | // recompression of existing IDAT datastreams. The optimization level
57 | // 1 enables a single IDAT compression trial. The trial chosen is what
58 | // OptiPNG thinks it’s probably the most effective.
59 | ['optipng', { optimizationLevel: 8 }],
60 | ],
61 | },
62 | }),
63 |
64 | new CleanWebpackPlugin(),
65 | ],
66 |
67 | module: {
68 | rules: [
69 | {
70 | test: /\.js$/,
71 | use: {
72 | loader: 'babel-loader',
73 | },
74 | },
75 |
76 | {
77 | test: /\.scss$/,
78 | use: [
79 | {
80 | loader: MiniCssExtractPlugin.loader,
81 | options: {
82 | publicPath: '',
83 | },
84 | },
85 |
86 | {
87 | loader: 'css-loader',
88 | },
89 |
90 | {
91 | loader: 'postcss-loader',
92 | },
93 |
94 | {
95 | loader: 'sass-loader',
96 | },
97 | ],
98 | },
99 |
100 | {
101 | test: /\.(png|jpg|gif|jpe?g|svg|woff2?|fnt|webp|mp4)$/,
102 | type: 'asset/resource',
103 | generator: {
104 | filename: '[name].[hash].[ext]',
105 | },
106 | },
107 |
108 | {
109 | test: /\.(jpe?g|png|gif|svg|webp)$/i,
110 | use: [
111 | {
112 | loader: ImageMinimizerPlugin.loader,
113 | },
114 | ],
115 | },
116 |
117 | {
118 | test: /\.(glsl|frag|vert)$/,
119 | type: 'asset/source', // replaced raw-loader
120 | exclude: /node_modules/,
121 | },
122 |
123 | {
124 | test: /\.(glsl|frag|vert)$/,
125 | loader: 'glslify-loader',
126 | exclude: /node_modules/,
127 | },
128 | ],
129 | },
130 |
131 | optimization: {
132 | minimize: true,
133 | minimizer: [new TerserPlugin()],
134 | },
135 | };
136 |
--------------------------------------------------------------------------------