├── .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: '
', 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(' 7 | 8 | meta(name="description" content=meta.data.description) 9 | 10 | link(rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png") 11 | link(rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png") 12 | link(rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png") 13 | link(rel="manifest" href="/site.webmanifest") 14 | link(rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5") 15 | meta(name="msapplication-TileColor" content="#da532c") 16 | meta(name="theme-color" content="#ffffff") 17 | 18 | 19 | 20 | meta(name="twitter:card" content="summary_large_image") 21 | meta(name="twitter:title" content=meta.data.title) 22 | meta(name="twitter:description" content=meta.data.description) 23 | meta(name="twitter:image" content=meta.data.image ? meta.data.image.url : '') 24 | 25 | 26 | 27 | meta(name="og:title" content=meta.data.title) 28 | meta(name="og:description" content=meta.data.description) 29 | meta(name="og:image" content=meta.data.image ? meta.data.image.url : '') 30 | meta(name="og:type" content="website") 31 | 32 | link(rel="stylesheet" type="text/css" href="/main.css") 33 | 34 | base(herf='/') -------------------------------------------------------------------------------- /views/_includes/layout.pug: -------------------------------------------------------------------------------- 1 | block variables 2 | doctype html 3 | html(class= isDesktop ? "desktop" : isTablet ? "tablet" : isPhone ? "phone" : "" lang="en") 4 | head 5 | include head 6 | body 7 | include preloader 8 | include navigation 9 | 10 | #content.content(data-template=template) 11 | block content 12 | 13 | include scripts -------------------------------------------------------------------------------- /views/_includes/navigation.pug: -------------------------------------------------------------------------------- 1 | nav.navigation 2 | a.navigation__link(href='/')=meta.data.title 3 | svg.navigation__link__icon(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.01 20.01") 4 | path(fill="currentColor" d="M-417.65-137.87h.06l3.52,9.47h-7.15Zm6.24,16.6H-410l-6.93-18.54h-.91L-425-121.27h1l2.38-6.27h7.78Zm-38.22,0v-17.37h.05l6.34,17.33h1l6.41-17.31h.05v17.34h1.34v-18.4h-1.87l-6.16,16.46h0l-6.11-16.46h-2v18.41Zm-21.26-18.41v18.41h10.83v-.85h-9.49v-8.33h6.43v-.85h-6.43v-7.53h8.72v-.85Zm-45.7,0h-1.33v18.41H-507v-.85h-9.6Zm-21.42,0v18.41h1.33v-9h6.4v-.85h-6.4v-7.66h9.34v-.85Zm45.67,19.17a1.84,1.84,0,0,1-1.8-.88,1,1,0,0,0-.08-.11l-.11-.13a16.35,16.35,0,0,0-1.37-1.52,1.87,1.87,0,0,1-.45-1.08,2,2,0,0,0-.11-.4l-.2-.3a10.37,10.37,0,0,1-1.3-2.32,4.5,4.5,0,0,1,.13-1.93,4.05,4.05,0,0,0,.12-.84,5.71,5.71,0,0,0-.59-1.55,1.92,1.92,0,0,1-.15-2.07c.57-1.3.88-2,1.27-2.23s1.49-.81,1.53-.84h0a1.66,1.66,0,0,0,.88-.85l.2-.31a3.37,3.37,0,0,0,.19-.32c.15-.28.23-.4.87-.7a5.86,5.86,0,0,1,3,0l.57.09a12.2,12.2,0,0,1,2.71.92l.23.09a8.35,8.35,0,0,1,2,1.45,2.8,2.8,0,0,0,1,.66c.17.08.35.16.52.26.56.33,1.2.87,1.29,1.38,0,.09,0,.18.05.29a14.83,14.83,0,0,1,.26,2.11,5,5,0,0,1-.42,2.9,7.16,7.16,0,0,0-.69,1.91,4.73,4.73,0,0,1-1.56,2.43,4.3,4.3,0,0,1-2,1.36l-.32.11-.38.13c-.43.13-.59.18-1.56,1.13s-1,.89-1.73.92a10.71,10.71,0,0,0-1.27.13A5.12,5.12,0,0,1-492.34-120.51Zm-3-15.92c-.11.05-1.17.61-1.5.81a6.08,6.08,0,0,0-1.15,2.1,1.63,1.63,0,0,0,.09,1.74,5.46,5.46,0,0,1,.67,1.73,4.23,4.23,0,0,1-.12.93,4.33,4.33,0,0,0-.13,1.79,10.54,10.54,0,0,0,1.26,2.24l.21.31a2.11,2.11,0,0,1,.14.49,1.8,1.8,0,0,0,.34.9,15.13,15.13,0,0,1,1.42,1.57l.11.13.1.14c.21.33.61.94,2.27.68a11.45,11.45,0,0,1,1.3-.13c.66,0,.73,0,1.54-.83,1-1,1.21-1.06,1.68-1.21l.37-.12.32-.11a4,4,0,0,0,1.88-1.29,4.54,4.54,0,0,0,1.49-2.28,7.45,7.45,0,0,1,.71-2,4.7,4.7,0,0,0,.39-2.74,14.64,14.64,0,0,0-.26-2.07c0-.1,0-.2-.05-.3-.07-.38-.62-.85-1.15-1.16-.15-.09-.31-.16-.48-.24a3.08,3.08,0,0,1-1.14-.74,7.85,7.85,0,0,0-1.91-1.37l-.23-.09a14.6,14.6,0,0,0-2.58-.9,3.63,3.63,0,0,1-.65-.09,5.57,5.57,0,0,0-2.78-.07c-.57.27-.61.34-.73.57a2.39,2.39,0,0,1-.21.34l-.18.3A2,2,0,0,1-495.32-136.43Zm4.26,10.54a3.49,3.49,0,0,1-1.06-.2l-.17,0A10.92,10.92,0,0,1-495.1-128a1.78,1.78,0,0,1-.46-.75,3.27,3.27,0,0,0-.91-1.37c-1-1-1.12-1.41-1.2-1.7l-.06-.17a.64.64,0,0,1,0-.49A3.06,3.06,0,0,1-496-134a1.67,1.67,0,0,0,1-1.75l0-.13h0c0-.11,0-.36.08-.92.08-1,.93-1.55,2.6-1.65,1.51-.09,2.14,0,2.71.61a5.94,5.94,0,0,0,1.64,1.26c.2.08.47.17.75.26a5.4,5.4,0,0,1,1.48.64c.59.46.54,1.18.4,2.53l0,.25a4.53,4.53,0,0,1-.3,1.63,3.12,3.12,0,0,1-.44.59,7,7,0,0,0-1.2,1.86,2.72,2.72,0,0,1-.59,1l0,0a1.85,1.85,0,0,1-.32.25,12.74,12.74,0,0,0-1.37,1A2.21,2.21,0,0,1-491.06-125.89Zm-3.61-9.87a2,2,0,0,1-1.2,2.08,2.73,2.73,0,0,0-1.55,1.28.35.35,0,0,0,0,.25,1.59,1.59,0,0,1,.07.2c.08.26.18.62,1.12,1.58a3.58,3.58,0,0,1,1,1.47,1.47,1.47,0,0,0,.38.65,11.08,11.08,0,0,0,2.68,1.81l.17,0a2.14,2.14,0,0,0,2.26-.31,13.07,13.07,0,0,1,1.39-1.06l.28-.21,0,0c.1-.1.19-.18.52-.92a7.64,7.64,0,0,1,1.26-1.93,3.27,3.27,0,0,0,.39-.51,4,4,0,0,0,.27-1.53l0-.25c.14-1.37.16-1.92-.29-2.26a4.83,4.83,0,0,0-1.38-.59,8.11,8.11,0,0,1-.78-.28,6.18,6.18,0,0,1-1.73-1.31c-.49-.49-1.07-.61-2.48-.53s-2.24.53-2.31,1.38c-.05.56-.07.84-.09,1Zm4.35,13.75a1.05,1.05,0,0,1-.65-.16,6,6,0,0,1-.71-.72c-.55-.61-1.31-1.44-1.87-1.39a2.6,2.6,0,0,1-1.84-.81,2.78,2.78,0,0,1-.9-2.12,2.18,2.18,0,0,0-1.43-2.1.78.78,0,0,1-.2-.11l-.13-.08a1.94,1.94,0,0,1-1.2-1.75c0-1.16,0-3.57,1.37-3.89l.52-.12a4.28,4.28,0,0,0,2.13-.79,1.92,1.92,0,0,0,.18-.31c.17-.37.36-.75,1.45-1l1.32-.3a3.59,3.59,0,0,0,1.06-1.54c.4-1.14,1.06-1.27,1.51-1.27h.22c1.37-.06,3-.05,3.36.53a6.52,6.52,0,0,1,.43.83c.36.77.8,1.72,1.39,2a6.2,6.2,0,0,1,2.42,2.45,4.65,4.65,0,0,0,.23.43,3,3,0,0,1,.37,2.86,5.36,5.36,0,0,1-1.78,2.56,2.15,2.15,0,0,0-.68,1.78,2.38,2.38,0,0,1,0,.46,1.18,1.18,0,0,0,0,.2c-.06.47-.17,1.25-1.79,2.45A10,10,0,0,1-490.32-122Zm-3.18-2.58c.7,0,1.44.83,2.05,1.5a6.14,6.14,0,0,0,.67.68c.52.41,3.13-.41,5-1.77,1.52-1.12,1.61-1.8,1.67-2.25a2,2,0,0,1,0-.24,2.43,2.43,0,0,0,0-.38,2.43,2.43,0,0,1,.79-2,5.11,5.11,0,0,0,1.7-2.46,2.68,2.68,0,0,0-.36-2.59,5.07,5.07,0,0,1-.24-.45,5.9,5.9,0,0,0-2.27-2.3A4.39,4.39,0,0,1-486-139a7.53,7.53,0,0,0-.41-.8c-.22-.33-1.26-.47-3.09-.39h-.24c-.25,0-.84,0-1.22,1.07a3.61,3.61,0,0,1-1.2,1.7l-.05,0-1.35.31c-1,.22-1.08.5-1.25.84a2.16,2.16,0,0,1-.2.36,4.31,4.31,0,0,1-2.32.91l-.51.12c-1.14.26-1.14,2.59-1.14,3.58a1.66,1.66,0,0,0,1,1.49.76.76,0,0,1,.19.12l.14.07a2.49,2.49,0,0,1,1.6,2.39,2.44,2.44,0,0,0,.81,1.88,2.35,2.35,0,0,0,1.6.73Zm4.16.28a8.43,8.43,0,0,1-3.26-.53,6.46,6.46,0,0,0-2.36-.55h0l-1.26-.06a.8.8,0,0,1-.65-.34c-.4-.61,0-1.84.4-3,.26-.68-.65-1.22-1.7-1.75s-1.08-.79-.47-2.64a3.4,3.4,0,0,1,2.38-2.57l.13-.06a2.64,2.64,0,0,0,1.3-1.9,3.19,3.19,0,0,1,.1-.35c.18-.49,1-1,1.82-1.4l.11-.06c.48-.25,3.25-.5,3.85.46a4.26,4.26,0,0,0,1.58,1.31,5.26,5.26,0,0,1,.49.3,6.93,6.93,0,0,1,2.58,4c.22,1.71.2,2.56-.45,3.12a2.58,2.58,0,0,0-1,1.67,2.78,2.78,0,0,1-.49,1.62,6.63,6.63,0,0,0-.86,1.51,1.94,1.94,0,0,1-1.58,1.15C-488.92-124.31-489.13-124.31-489.34-124.31Zm-5.61-1.39a6.54,6.54,0,0,1,2.49.59,9.49,9.49,0,0,0,3.7.47,1.65,1.65,0,0,0,1.34-1,6.8,6.8,0,0,1,.9-1.57,2.51,2.51,0,0,0,.43-1.45,2.84,2.84,0,0,1,1.13-1.88c.5-.43.58-1.05.35-2.85a6.66,6.66,0,0,0-2.44-3.79,5.06,5.06,0,0,0-.48-.29,4.49,4.49,0,0,1-1.68-1.41c-.48-.77-3.05-.56-3.45-.35l-.11.06c-.45.23-1.51.78-1.67,1.23a3.4,3.4,0,0,0-.1.33,2.92,2.92,0,0,1-1.47,2.1l-.13.06a3.11,3.11,0,0,0-2.21,2.39c-.59,1.8-.58,1.8.32,2.27s2.22,1.13,1.85,2.13c-.22.57-.8,2.11-.43,2.67a.46.46,0,0,0,.4.2Zm2.59,3.72a2.11,2.11,0,0,1-.49-.09,7.8,7.8,0,0,0-1.35-.24,2.44,2.44,0,0,1-2-1.32h0l-.4-1.1a2.91,2.91,0,0,1-.6-2.22V-127a2,2,0,0,0-.64-1.88c-.94-1.09-.94-2.06,0-3a2,2,0,0,0,.74-1.32.74.74,0,0,1,.27-.57,7.69,7.69,0,0,0,1.08-1.51c.13-2.11.29-2.42.38-2.48,2.79-2.68,5.47-1.62,5.5-1.61a13.93,13.93,0,0,1,1.92,1.41,1.47,1.47,0,0,0,.6.26,2.62,2.62,0,0,1,1.32.73c.63.67,1.39.53,2.66.29l.11,0c1.36-.26,1.81,1.1,2.2,2.28,0,.14.09.28.14.41.37,1.07-.2,1.54-.81,2a7.09,7.09,0,0,0-.65.58,1.81,1.81,0,0,0-.35,1.76,4.17,4.17,0,0,1,.08.71,1.71,1.71,0,0,1-.15.77,2.35,2.35,0,0,0-.19,1c-.06,1.12-.3,1.38-.55,1.65l-.06.07a3.49,3.49,0,0,1-1,.54,7.26,7.26,0,0,0-1.07.56,7.64,7.64,0,0,1-2,.86l-.21.08a2,2,0,0,0-.46.22,2.63,2.63,0,0,1-1.44.42,4.45,4.45,0,0,0-1.7.5,2.93,2.93,0,0,1-.64.25A1.15,1.15,0,0,1-492.36-122Zm-3.6-1.77a2.14,2.14,0,0,0,1.79,1.14,7.48,7.48,0,0,1,1.4.25,1.11,1.11,0,0,0,.6,0,2.42,2.42,0,0,0,.58-.23,4.44,4.44,0,0,1,1.82-.52,2.4,2.4,0,0,0,1.3-.38,2.41,2.41,0,0,1,.51-.25l.22-.07a7.9,7.9,0,0,0,1.91-.83,9.06,9.06,0,0,1,1.11-.59,3.42,3.42,0,0,0,.86-.47l.07-.07c.22-.24.41-.45.46-1.46a2.68,2.68,0,0,1,.22-1.08,1.33,1.33,0,0,0,.13-.65,5,5,0,0,0-.08-.66,2.1,2.1,0,0,1,.43-2,8,8,0,0,1,.68-.61c.61-.49,1-.82.71-1.68,0-.14-.09-.28-.14-.42-.39-1.18-.76-2.29-1.85-2.08l-.12,0c-1.32.25-2.2.41-2.94-.38a2.25,2.25,0,0,0-1.18-.64,1.89,1.89,0,0,1-.72-.33,12.77,12.77,0,0,0-1.83-1.36,5.16,5.16,0,0,0-5.17,1.56,8.56,8.56,0,0,0-.29,2.31l0,.06a7.62,7.62,0,0,1-1.18,1.63c-.1.08-.12.17-.15.38a2.32,2.32,0,0,1-.83,1.5c-.83.78-.82,1.56,0,2.52a2.2,2.2,0,0,1,.71,2.09v.08a2.7,2.7,0,0,0,.58,2.08Z" transform="translate(538.01 140.5)") 5 | 6 | ul.navigation__list 7 | each item in navigation.data.list 8 | li.navigation__list__item 9 | a.navigation__list__link(href=Link(item.link))=item.text 10 | 11 | -------------------------------------------------------------------------------- /views/_includes/preloader.pug: -------------------------------------------------------------------------------- 1 | .preloader 2 | p.preloader__text!=preloader.data.title.replace(/\n/g, '
') 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(/

/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}
${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 | --------------------------------------------------------------------------------