├── app ├── utils │ ├── math.js │ ├── colors.js │ └── text.js ├── .DS_Store ├── pages │ ├── Collections │ │ └── index.js │ ├── About │ │ └── index.js │ ├── Home │ │ └── index.js │ └── Detail │ │ └── index.js ├── shaders │ ├── home-fragment.glsl │ ├── plane-fragment.glsl │ ├── plane-vertex.glsl │ ├── collections-fragment.glsl │ ├── collections-vertex.glsl │ └── home-vertex.glsl ├── classes │ ├── Colors.js │ ├── AsyncLoad.js │ ├── Animation.js │ ├── Detection.js │ ├── Component.js │ ├── Button.js │ └── Page.js ├── animations │ ├── Label.js │ ├── Title.js │ ├── Highlight.js │ └── Paragraph.js ├── components │ ├── Navigation.js │ ├── Canvas │ │ ├── About │ │ │ ├── index.js │ │ │ ├── Media.js │ │ │ └── Gallery.js │ │ ├── Transition.js │ │ ├── Home │ │ │ ├── Media.js │ │ │ └── index.js │ │ ├── Detail │ │ │ └── index.js │ │ ├── Collections │ │ │ ├── Media.js │ │ │ └── index.js │ │ └── index.js │ └── Preloader.js └── index.js ├── shared ├── cool.txt └── logo.svg ├── .gitignore ├── styles ├── utils │ ├── vendors.scss │ ├── responsive.scss │ ├── functions.scss │ ├── variables.scss │ └── mixins.scss ├── shared │ ├── pages.scss │ ├── descriptions.scss │ ├── vertical-titles.scss │ ├── titles.scss │ └── links.scss ├── base │ ├── fonts.scss │ ├── reset.scss │ └── base.scss ├── pages │ ├── about │ │ ├── about.scss │ │ ├── gallery.scss │ │ ├── highlight.scss │ │ └── content.scss │ ├── collections │ │ └── collections.scss │ ├── details │ │ └── details.scss │ └── home │ │ └── home.scss ├── components │ └── preloader.scss ├── index.scss └── layout │ └── navigation.scss ├── fonts ├── .DS_Store ├── bodoni.woff └── bodoni.woff2 ├── views ├── partials │ ├── preloader.pug │ └── navigation.pug ├── pages │ ├── home.pug │ ├── collections.pug │ ├── about.pug │ └── detail.pug └── base.pug ├── webpack.config.build.js ├── webpack.config.development.js ├── package.json ├── webpack.config.js └── app.js /app/utils/math.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /shared/cool.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env 3 | yarn.lock 4 | public -------------------------------------------------------------------------------- /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lunalubra/floral/HEAD/app/.DS_Store -------------------------------------------------------------------------------- /styles/utils/vendors.scss: -------------------------------------------------------------------------------- 1 | .dg.ac { 2 | z-index: 99999 !important; 3 | } 4 | -------------------------------------------------------------------------------- /fonts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lunalubra/floral/HEAD/fonts/.DS_Store -------------------------------------------------------------------------------- /fonts/bodoni.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lunalubra/floral/HEAD/fonts/bodoni.woff -------------------------------------------------------------------------------- /fonts/bodoni.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lunalubra/floral/HEAD/fonts/bodoni.woff2 -------------------------------------------------------------------------------- /app/utils/colors.js: -------------------------------------------------------------------------------- 1 | export const COLOR_BRIGHT_GRAY = '#37384C'; 2 | export const COLOR_QUARTER_SPANISH_WHITE = '#F9F1E7'; 3 | -------------------------------------------------------------------------------- /views/partials/preloader.pug: -------------------------------------------------------------------------------- 1 | .preloader 2 | p.preloader__text!=preloader.data.title.replace(/\n/g, '
') 3 | 4 | .preloader__number 5 | .preloader__number__text 0% 6 | -------------------------------------------------------------------------------- /styles/shared/pages.scss: -------------------------------------------------------------------------------- 1 | %page { 2 | @extend %cover; 3 | 4 | opacity: 0; 5 | visibility: hidden; 6 | z-index: z('content'); 7 | } 8 | 9 | %page__wrapper { 10 | color: inherit; 11 | } 12 | -------------------------------------------------------------------------------- /styles/utils/responsive.scss: -------------------------------------------------------------------------------- 1 | $breakpoints: ( 2 | 'phone': 768px, 3 | 'tablet': 1024px, 4 | 'desktop': 1920px, 5 | ) !default; 6 | 7 | @import '../../node_modules/include-media/dist/_include-media.scss'; 8 | -------------------------------------------------------------------------------- /app/pages/Collections/index.js: -------------------------------------------------------------------------------- 1 | import Page from 'classes/Page' 2 | 3 | export default class Collections extends Page { 4 | constructor () { 5 | super({ 6 | id: 'collections', 7 | 8 | element: '.collections' 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /styles/base/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: $font-bodoni-flf; 3 | src: url('../fonts/bodoni.woff2') format('woff2'), 4 | url('../fonts/bodoni.woff') format('woff'); 5 | font-weight: 500; 6 | font-style: normal; 7 | font-display: swap; 8 | } 9 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /styles/utils/functions.scss: -------------------------------------------------------------------------------- 1 | @function z($name) { 2 | @if index($z-indexes, $name) { 3 | @return (length($z-indexes) - index($z-indexes, $name)) + 1; 4 | } @else { 5 | @warn 'There is no item "#{$name}" in this list; Choose one of: #{$z-indexes}'; 6 | 7 | @return null; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /webpack.config.build.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const { merge } = require('webpack-merge') 4 | const config = require('./webpack.config') 5 | 6 | module.exports = merge(config, { 7 | mode: 'production', 8 | 9 | output: { 10 | path: path.join(__dirname, 'public') 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /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 | 12 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 13 | } 14 | -------------------------------------------------------------------------------- /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/classes/Colors.js: -------------------------------------------------------------------------------- 1 | import GSAP from 'gsap' 2 | 3 | class Colors { 4 | change ({ 5 | backgroundColor, 6 | color 7 | }) { 8 | GSAP.to(document.documentElement, { 9 | background: backgroundColor, 10 | color, 11 | duration: 1.5 12 | }) 13 | } 14 | } 15 | 16 | export const ColorsManager = new Colors() 17 | -------------------------------------------------------------------------------- /styles/utils/variables.scss: -------------------------------------------------------------------------------- 1 | $color-bright-gray: #37384c; 2 | $color-cadet-blue: #b2b8c3; 3 | $color-contessa: #768662; 4 | $color-quicksand: #658979; 5 | $color-quarter-spanish-white: #f9f1e7; 6 | 7 | $font-bodoni-flf: 'BodoniFLF'; 8 | $font-san-serif: sans-serif; 9 | 10 | $ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1); 11 | 12 | $z-indexes: ('preloader', 'navigation', 'content', 'canvas'); 13 | -------------------------------------------------------------------------------- /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 | 8 | element: '.about', 9 | elements: { 10 | wrapper: '.about__wrapper', 11 | navigation: document.querySelector('.navigation'), 12 | title: '.about__title' 13 | } 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const path = require('path') 3 | 4 | const config = require('./webpack.config') 5 | 6 | module.exports = merge(config, { 7 | mode: 'development', 8 | 9 | devtool: 'inline-source-map', 10 | 11 | devServer: { 12 | writeToDisk: true 13 | }, 14 | 15 | output: { 16 | path: path.resolve(__dirname, 'public') 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /styles/pages/about/about.scss: -------------------------------------------------------------------------------- 1 | .about { 2 | @extend %page; 3 | 4 | // background: $color-cadet-blue; 5 | // color: $color-bright-gray; 6 | } 7 | 8 | .about__wrapper { 9 | padding-top: 10rem; 10 | } 11 | 12 | .about__title { 13 | @extend %title-100; 14 | 15 | margin: auto; 16 | max-width: 126rem; 17 | text-align: center; 18 | 19 | @include media(' { 12 | entries.forEach(entry => { 13 | if (entry.isIntersecting) { 14 | if (!this.element.src) { 15 | this.element.src = this.element.getAttribute('data-src') 16 | this.element.onload = _ => { 17 | this.element.classList.add('loaded') 18 | } 19 | } 20 | } 21 | }) 22 | }) 23 | 24 | this.observer.observe(this.element) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/classes/Animation.js: -------------------------------------------------------------------------------- 1 | import Component from 'classes/Component' 2 | 3 | export default class Animation extends Component { 4 | constructor ({ element, elements }) { 5 | super({ 6 | element, 7 | elements 8 | }) 9 | 10 | this.createObserver() 11 | 12 | this.animateOut() 13 | } 14 | 15 | createObserver () { 16 | this.observer = new window.IntersectionObserver(entries => { 17 | entries.forEach(entry => { 18 | if (entry.isIntersecting) { 19 | this.animateIn() 20 | } else { 21 | this.animateOut() 22 | } 23 | }) 24 | }) 25 | 26 | this.observer.observe(this.element) 27 | } 28 | 29 | animateIn () { 30 | 31 | } 32 | 33 | animateOut () { 34 | 35 | } 36 | 37 | onResize () { 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /styles/components/preloader.scss: -------------------------------------------------------------------------------- 1 | .preloader { 2 | @extend %center; 3 | @extend %cover; 4 | 5 | background: $color-contessa; 6 | color: $color-quarter-spanish-white; 7 | text-align: center; 8 | z-index: z('preloader'); 9 | } 10 | 11 | .preloader__text { 12 | font-size: 0; 13 | max-width: 66rem; 14 | 15 | span { 16 | display: inline-block; 17 | overflow: hidden; 18 | } 19 | 20 | span span { 21 | @extend %title-53; 22 | } 23 | } 24 | 25 | .preloader__number { 26 | @extend %description-14; 27 | 28 | bottom: 7.3rem; 29 | left: 50%; 30 | overflow: hidden; 31 | position: absolute; 32 | transform: translateX(-50%); 33 | 34 | @include media(' { 32 | if (entry instanceof window.HTMLElement || entry instanceof window.NodeList || Array.isArray(entry)) { 33 | this.elements[key] = entry 34 | } else { 35 | this.elements[key] = document.querySelectorAll(entry) 36 | 37 | if (this.elements[key].length === 0) { 38 | this.elements[key] = null 39 | } else if (this.elements[key].length === 1) { 40 | this.elements[key] = document.querySelector(entry) 41 | } 42 | } 43 | }) 44 | } 45 | 46 | addEventListeners () { 47 | 48 | } 49 | 50 | removeEventListeners () { 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /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(this.path, { 15 | strokeDashoffset: this.pathLength, 16 | strokeDasharray: `${this.pathLength} ${this.pathLength}`, 17 | }, { 18 | strokeDashoffset: 0, 19 | strokeDasharray: `${this.pathLength} ${this.pathLength}`, 20 | }) 21 | } 22 | 23 | onMouseEnter () { 24 | this.timeline.play() 25 | } 26 | 27 | onMouseLeave () { 28 | this.timeline.reverse() 29 | } 30 | 31 | addEventListeners () { 32 | this.onMouseEnterEvent = this.onMouseEnter.bind(this) 33 | this.onMouseLeaveEvent = this.onMouseLeave.bind(this) 34 | 35 | this.element.addEventListener('mouseenter', this.onMouseEnterEvent) 36 | this.element.addEventListener('mouseleave', this.onMouseLeaveEvent) 37 | } 38 | 39 | removeEventListeners () { 40 | this.element.removeEventListener('mouseenter', this.onMouseEnterEvent) 41 | this.element.removeEventListener('mouseleave', this.onMouseLeaveEvent) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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 | 11 | background: $color-contessa; 12 | color: $color-quarter-spanish-white; 13 | font-size: calc(100vw / 1920 * 10); 14 | line-height: 1; 15 | overflow: hidden; 16 | position: fixed; 17 | 18 | @include media('${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.products 26 | a.collections__gallery__link(href=Link(product.products_product)) 27 | figure.collections__gallery__media(data-index=index) 28 | img.collections__gallery__media__image(alt=product.products_product.data.image.alt data-src=product.products_product.data.image.url) 29 | 30 | .collections__content 31 | each collection, index in collections 32 | artilce.collections__article(class=`${index === 0 ? 'collections__article--active' : ''}`) 33 | h2.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 | -------------------------------------------------------------------------------- /app/components/Canvas/About/index.js: -------------------------------------------------------------------------------- 1 | import { Plane, Transform } from 'ogl' 2 | import GSAP from 'gsap' 3 | 4 | import map from 'lodash/map' 5 | 6 | import Gallery from './Gallery' 7 | 8 | export default class { 9 | constructor ({ gl, scene, sizes }) { 10 | this.gl = gl 11 | this.sizes = sizes 12 | 13 | this.group = new Transform() 14 | 15 | this.createGeometry() 16 | this.createGalleries() 17 | 18 | this.onResize({ 19 | sizes: this.sizes 20 | }) 21 | 22 | this.group.setParent(scene) 23 | 24 | this.show() 25 | } 26 | 27 | createGeometry () { 28 | this.geometry = new Plane(this.gl) 29 | } 30 | 31 | createGalleries () { 32 | this.galleriesElements = document.querySelectorAll('.about__gallery') 33 | 34 | this.galleries = map(this.galleriesElements, (element, index) => { 35 | return new Gallery({ 36 | element, 37 | geometry: this.geometry, 38 | index, 39 | gl: this.gl, 40 | scene: this.group, 41 | sizes: this.sizes 42 | }) 43 | }) 44 | } 45 | 46 | /** 47 | * Animations. 48 | */ 49 | show () { 50 | map(this.galleries, gallery => gallery.show()) 51 | } 52 | 53 | hide () { 54 | map(this.galleries, gallery => gallery.hide()) 55 | } 56 | 57 | /** 58 | * Events. 59 | */ 60 | onResize (event) { 61 | map(this.galleries, gallery => gallery.onResize(event)) 62 | } 63 | 64 | onTouchDown (event) { 65 | map(this.galleries, gallery => gallery.onTouchDown(event)) 66 | } 67 | 68 | onTouchMove (event) { 69 | map(this.galleries, gallery => gallery.onTouchMove(event)) 70 | } 71 | 72 | onTouchUp (event) { 73 | map(this.galleries, gallery => gallery.onTouchUp(event)) 74 | } 75 | 76 | onWheel ({ pixelX, pixelY }) { 77 | 78 | } 79 | 80 | /** 81 | * Update. 82 | */ 83 | update (scroll) { 84 | map(this.galleries, gallery => gallery.update(scroll)) 85 | } 86 | 87 | /** 88 | * Destroy. 89 | */ 90 | destroy () { 91 | map(this.galleries, gallery => gallery.destroy()) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /views/partials/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 125.21 51.96") 4 | path(fill="currentColor" d="M45.67,21.91a.74.74,0,0,1-.27.58,1,1,0,0,1-.67.23,1.37,1.37,0,0,1-.68-.15v.07a1.45,1.45,0,0,0,.9,1.25,3.84,3.84,0,0,0,1.88.48,9.84,9.84,0,0,0,1.75-.14,7.23,7.23,0,0,0,1.53-.51,9.1,9.1,0,0,0,1.65-1A13.21,13.21,0,0,0,53.57,21a36.1,36.1,0,0,0,4.26-6l.07-.13q-5.25,1.17-7.56,3.27a4.31,4.31,0,0,0-.9,1c-.21.35-.35.53-.42.53s-.11,0-.11-.08a.47.47,0,0,1,.11-.31,6.81,6.81,0,0,1,3.12-2.84,21.48,21.48,0,0,1,6.07-2.18h.08a22.62,22.62,0,0,1,6-6.42,9,9,0,0,1,4.76-2c.24,0,.36,0,.36.11s0,.11-.12.13l-.79.22a6.63,6.63,0,0,0-1.65.83,15,15,0,0,0-2,1.58,26.45,26.45,0,0,0-4.09,5c1.46-.38,2.55-.69,3.28-.95a1.71,1.71,0,0,0,.53-.62,13,13,0,0,1,1.42.09,7.92,7.92,0,0,1,.94.14,8.33,8.33,0,0,0-3.3,3,6.48,6.48,0,0,0-.66,1.15,2.16,2.16,0,0,1-.23.5.2.2,0,0,1-.13,0c-.13,0-.19-.07-.19-.21a5.08,5.08,0,0,1,.55-1.53,13,13,0,0,1,.93-1.78c-.42.13-.95.27-1.6.42l-2,.43A33.64,33.64,0,0,1,57,19.2a20.84,20.84,0,0,1-3.4,3.4A10.58,10.58,0,0,1,47,24.79a4.64,4.64,0,0,1-2.47-.67,2,2,0,0,1-1.13-1.6,1.18,1.18,0,0,1,1-1.23,2.22,2.22,0,0,1,.51-.06,1,1,0,0,1,.51.17A.58.58,0,0,1,45.67,21.91ZM51.83,9.85a1.48,1.48,0,0,0,1.65,1.69,6.9,6.9,0,0,0,4.41-2,8.29,8.29,0,0,0,1.86-2.1,5.07,5.07,0,0,0,.56-1.28c0-.35.09-.52.16-.52s.15.19.15.55a3.77,3.77,0,0,1-.28,1.31A5.91,5.91,0,0,1,59.57,9a7.85,7.85,0,0,1-1.13,1.26,8.14,8.14,0,0,1-1.39,1,5.94,5.94,0,0,1-2.92.9,3.25,3.25,0,0,1-2.05-.55,1.87,1.87,0,0,1-.7-1.56A4,4,0,0,1,52,8a7.63,7.63,0,0,1,1.67-1.9,14.63,14.63,0,0,1,6-2.82,26.12,26.12,0,0,1,5.61-.5A21.89,21.89,0,0,1,67.94,3,40.34,40.34,0,0,0,72,3.35a8.74,8.74,0,0,0,2-.2,6,6,0,0,0,1.6-.57,5.23,5.23,0,0,0,2.1-2c.11-.18.22-.26.33-.26s.15,0,.15.09A.48.48,0,0,1,78,.75q-2.5,4-8,4a30.19,30.19,0,0,1-3.36-.25C64.06,4.17,62.34,4,61.51,4a12,12,0,0,0-2.42.24,13.5,13.5,0,0,0-2.24.66,12.7,12.7,0,0,0-2,1,10.34,10.34,0,0,0-1.6,1.22A3.94,3.94,0,0,0,51.83,9.85Z") 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "floema", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "backend:build": "node app.js", 6 | "backend:development": "nodemon app.js", 7 | "frontend:build": "webpack --progress --config webpack.config.build.js", 8 | "frontend:development": "webpack serve --progress --config webpack.config.development.js", 9 | "start": "concurrently --kill-others \"yarn run backend:development\" \"yarn run frontend:development\"" 10 | }, 11 | "author": "Luis Henrique Bizarro", 12 | "devDependencies": { 13 | "babel-loader": "^8.2.2", 14 | "clean-webpack-plugin": "^4.0.0-alpha.0", 15 | "concurrently": "^6.1.0", 16 | "copy-webpack-plugin": "^8.1.1", 17 | "css-loader": "^5.2.2", 18 | "eslint": "^7.24.0", 19 | "eslint-config-standard": "^16.0.2", 20 | "eslint-loader": "^4.0.2", 21 | "eslint-plugin-import": "^2.22.1", 22 | "eslint-plugin-promise": "^4.3.1", 23 | "eslint-plugin-standard": "^5.0.0", 24 | "file-loader": "^6.2.0", 25 | "glslify-loader": "^2.0.0", 26 | "gsap": "^3.6.1", 27 | "image-minimizer-webpack-plugin": "^2.2.0", 28 | "imagemin": "^7.0.1", 29 | "imagemin-gifsicle": "^7.0.0", 30 | "imagemin-jpegtran": "^7.0.0", 31 | "imagemin-optipng": "^8.0.0", 32 | "imagemin-svgo": "^9.0.0", 33 | "include-media": "^1.4.9", 34 | "mini-css-extract-plugin": "^1.5.0", 35 | "normalize-wheel": "^1.0.1", 36 | "ogl": "^0.0.73", 37 | "postcss-loader": "^5.2.0", 38 | "prefix": "^1.0.0", 39 | "raw-loader": "^4.0.2", 40 | "sass": "^1.32.10", 41 | "sass-loader": "^11.0.1", 42 | "terser-webpack-plugin": "^5.1.1", 43 | "webpack": "^5.33.2", 44 | "webpack-cli": "^4.6.0", 45 | "webpack-dev-server": "^3.11.2", 46 | "webpack-merge": "^5.7.3" 47 | }, 48 | "dependencies": { 49 | "@prismicio/client": "^4.0.0", 50 | "body-parser": "^1.19.0", 51 | "dotenv": "^8.2.0", 52 | "errorhandler": "^1.5.1", 53 | "express": "^4.17.1", 54 | "method-override": "^3.0.0", 55 | "morgan": "^1.10.0", 56 | "nodemon": "^2.0.7", 57 | "prismic-dom": "^2.2.5", 58 | "pug": "^3.0.2", 59 | "ua-parser-js": "^0.7.28" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /views/pages/about.pug: -------------------------------------------------------------------------------- 1 | extends ../base.pug 2 | 3 | block variables 4 | - var template = 'about' 5 | 6 | block content 7 | .about(data-background="#9dad7f" data-color="#F9F1E7") 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(alt=media.image.alt data-src=media.image.url) 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!=PrismicDOM.RichText.asHtml(section.primary.description).replace(/

/g, '

') 26 | 27 | figure.about__content__media 28 | img.about__content__media__image(alt=section.primary.image.alt data-src=section.primary.image.url) 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 | .about__highlight__medias 39 | each media in section.items 40 | figure.about__highlight__media 41 | img.about__highlight__media__image(alt=media.image.alt data-src=media.image.url) 42 | 43 | if section.slice_type == 'gallery' 44 | section.about__gallery 45 | .about__gallery__wrapper 46 | each media in section.items 47 | figure.about__gallery__media 48 | img.about__gallery__media__image(alt=media.image.alt data-src=media.image.url) 49 | -------------------------------------------------------------------------------- /styles/pages/about/highlight.scss: -------------------------------------------------------------------------------- 1 | .about__highlight { 2 | margin: 20rem 0; 3 | 4 | &:first-child { 5 | margin-top: 0; 6 | } 7 | 8 | &:last-child { 9 | margin-bottom: 0; 10 | } 11 | } 12 | 13 | .about__highlight__wrapper { 14 | height: 104rem; 15 | margin: auto; 16 | position: relative; 17 | width: 144rem; 18 | 19 | .about__highlight:nth-child(4) & { 20 | width: 104rem; 21 | } 22 | 23 | @include media('<=phone') { 24 | height: 316.2rem; 25 | width: 100%; 26 | 27 | .about__highlight:nth-child(4) & { 28 | height: 119.3rem; 29 | width: 100%; 30 | } 31 | } 32 | } 33 | 34 | .about__highlight__label { 35 | @extend %description-12; 36 | 37 | left: 50%; 38 | letter-spacing: 0.1rem; 39 | position: absolute; 40 | top: 9rem; 41 | transform: translateX(-50%); 42 | text-align: center; 43 | text-transform: uppercase; 44 | width: 9.6rem; 45 | 46 | @include media('<=phone') { 47 | font-size: 2.4rem; 48 | margin-top: 6.5rem; 49 | top: 90rem; 50 | width: 19rem; 51 | } 52 | } 53 | 54 | .about__highlight__title { 55 | @extend %title-200; 56 | 57 | padding-bottom: 2rem; 58 | left: 50%; 59 | position: absolute; 60 | top: 50%; 61 | transform: translate(-50%, -50%) rotate(-90deg); 62 | 63 | @include media('<=phone') { 64 | font-size: 30rem; 65 | margin-top: 6.5rem; 66 | } 67 | } 68 | 69 | .about__highlight__media { 70 | height: 76rem; 71 | position: absolute; 72 | width: 54rem; 73 | 74 | .about__highlight:nth-child(4) & { 75 | height: 52.8rem; 76 | width: 36rem; 77 | } 78 | 79 | &:first-child { 80 | left: 0; 81 | top: 0; 82 | } 83 | 84 | &:last-child { 85 | bottom: 0; 86 | right: 0; 87 | } 88 | 89 | @include media('<=phone') { 90 | .about__highlight:last-child & { 91 | height: 76rem; 92 | width: 54rem; 93 | 94 | &:first-child { 95 | left: 50%; 96 | top: 0; 97 | transform: translateX(-50%); 98 | } 99 | 100 | &:last-child { 101 | bottom: 0; 102 | right: 50%; 103 | transform: translateX(50%); 104 | } 105 | } 106 | } 107 | } 108 | 109 | .about__highlight__media__image { 110 | @extend %cover; 111 | 112 | object-fit: cover; 113 | } 114 | -------------------------------------------------------------------------------- /app/components/Preloader.js: -------------------------------------------------------------------------------- 1 | import { Texture } from 'ogl' 2 | import GSAP from 'gsap' 3 | 4 | import Component from 'classes/Component' 5 | 6 | import { split } from 'utils/text' 7 | 8 | export default class Preloader extends Component { 9 | constructor ({ canvas }) { 10 | super({ 11 | element: '.preloader', 12 | elements: { 13 | title: '.preloader__text', 14 | number: '.preloader__number', 15 | numberText: '.preloader__number__text' 16 | } 17 | }) 18 | 19 | this.canvas = canvas 20 | 21 | window.TEXTURES = {} 22 | 23 | split({ 24 | element: this.elements.title, 25 | expression: '
' 26 | }) 27 | 28 | split({ 29 | element: this.elements.title, 30 | expression: '
' 31 | }) 32 | 33 | this.elements.titleSpans = this.elements.title.querySelectorAll('span span') 34 | 35 | this.length = 0 36 | 37 | this.createLoader() 38 | } 39 | 40 | createLoader () { 41 | window.ASSETS.forEach(image => { 42 | const texture = new Texture(this.canvas.gl, { 43 | generateMipmaps: false 44 | }) 45 | 46 | const media = new window.Image() 47 | 48 | media.crossOrigin = 'anonymous' 49 | media.src = image 50 | media.onload = _ => { 51 | texture.image = media 52 | 53 | this.onAssetLoaded() 54 | } 55 | 56 | window.TEXTURES[image] = texture 57 | }) 58 | } 59 | 60 | onAssetLoaded (image) { 61 | this.length += 1 62 | 63 | const percent = this.length / window.ASSETS.length 64 | 65 | this.elements.numberText.innerHTML = `${Math.round(percent * 100)}%` 66 | 67 | if (percent === 1) { 68 | this.onLoaded() 69 | } 70 | } 71 | 72 | onLoaded () { 73 | return new Promise(resolve => { 74 | this.emit('completed') 75 | 76 | this.animateOut = GSAP.timeline({ 77 | delay: 1 78 | }) 79 | 80 | this.animateOut.to(this.elements.titleSpans, { 81 | duration: 1.5, 82 | ease: 'expo.out', 83 | stagger: 0.1, 84 | y: '100%' 85 | }) 86 | 87 | this.animateOut.to(this.elements.numberText, { 88 | duration: 1.5, 89 | ease: 'expo.out', 90 | stagger: 0.1, 91 | y: '100%' 92 | }, '-=1.4') 93 | 94 | this.animateOut.to(this.element, { 95 | autoAlpha: 0, 96 | duration: 1 97 | }) 98 | 99 | this.animateOut.call(_ => { 100 | this.destroy() 101 | }) 102 | }) 103 | } 104 | 105 | destroy () { 106 | this.element.parentNode.removeChild(this.element) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /styles/pages/collections/collections.scss: -------------------------------------------------------------------------------- 1 | .collections { 2 | @extend %page; 3 | 4 | z-index: 0; 5 | // background: $color-quicksand; 6 | // color: $color-quarter-spanish-white; 7 | } 8 | 9 | .collections__wrapper { 10 | @extend %cover; 11 | 12 | overflow: hidden; 13 | } 14 | 15 | .collections__titles { 16 | @extend %vertical-titles; 17 | 18 | transition: transform 1s ease; 19 | 20 | @include media('') 33 | 34 | .detail__information__list 35 | each item in product.data.informations 36 | p.detail__information__item 37 | strong.detail__information__item__title=item.informations_label 38 | span.detail__information__item__description=item.informations_description 39 | 40 | a.detail__information__link(href=product.data.link_url.url target="_blank")=product.data.link_text 41 | 42 | a.detail__button(href=Link(home.data.collections)) Close 43 | svg.detail__button__icon(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 124 60") 44 | path(fill="none" stroke="currentColor" d="M62,0.5c33.97,0,61.5,13.21,61.5,29.5S95.97,59.5,62,59.5S0.5,46.29,0.5,30S28.03,0.5,62,0.5z") 45 | path.detail__button__icon__path(fill="none" stroke="#CB0001" d="M62,0.5c33.97,0,61.5,13.21,61.5,29.5S95.97,59.5,62,59.5S0.5,46.29,0.5,30S28.03,0.5,62,0.5z") 46 | -------------------------------------------------------------------------------- /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 | if (element.id === 'collections') { 55 | const { index, medias } = element 56 | const media = medias[index] 57 | 58 | this.createProgram(media.texture) 59 | this.createMesh(media.mesh) 60 | 61 | this.transition = 'detail' 62 | } else { 63 | this.createProgram(element.texture) 64 | this.createMesh(element.mesh) 65 | 66 | this.transition = 'collections' 67 | } 68 | } 69 | 70 | /** 71 | * Animations. 72 | */ 73 | animate (element, onComplete) { 74 | const timeline = GSAP.timeline() 75 | 76 | timeline.to(this.mesh.scale, { 77 | duration: 1.5, 78 | ease: 'expo.inOut', 79 | x: element.scale.x, 80 | y: element.scale.y, 81 | z: element.scale.z 82 | }, 0) 83 | 84 | timeline.to(this.mesh.position, { 85 | duration: 1.5, 86 | ease: 'expo.inOut', 87 | x: element.position.x, 88 | y: element.position.y, 89 | z: element.position.z 90 | }, 0) 91 | 92 | timeline.to(this.mesh.rotation, { 93 | duration: 1.5, 94 | ease: 'expo.inOut', 95 | x: element.rotation.x, 96 | y: element.rotation.y, 97 | z: element.rotation.z 98 | }, 0) 99 | 100 | timeline.call(_ => { 101 | onComplete() 102 | }) 103 | 104 | timeline.call(_ => { 105 | this.scene.removeChild(this.mesh) 106 | }, null, '+=0.2') 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/utils/text.js: -------------------------------------------------------------------------------- 1 | import each from 'lodash/each' 2 | 3 | export function split ({ element, expression = ' ', append = true }) { 4 | const words = splitText(element.innerHTML.toString().trim(), expression) 5 | 6 | let innerHTML = '' 7 | 8 | each(words, line => { 9 | if (line.indexOf('
') > -1) { 10 | const lines = line.split('
') 11 | 12 | each(lines, (line, index) => { 13 | innerHTML += (index > 0) ? '
' + parseLine(line) : parseLine(line) 14 | }) 15 | } else { 16 | innerHTML += parseLine(line) 17 | } 18 | }) 19 | 20 | element.innerHTML = innerHTML 21 | 22 | const spans = element.querySelectorAll('span') 23 | 24 | if (append) { 25 | each(spans, span => { 26 | const isSingleLetter = span.textContent.length === 1 27 | const isNotEmpty = span.innerHTML.trim() !== '' 28 | const isNotAndCharacter = span.textContent !== '&' 29 | const isNotDashCharacter = span.textContent !== '-' 30 | 31 | if (isSingleLetter && isNotEmpty && isNotAndCharacter && isNotDashCharacter) { 32 | span.innerHTML = `${span.textContent} ` 33 | } 34 | }) 35 | } 36 | 37 | return spans 38 | } 39 | 40 | export function calculate (spans) { 41 | const lines = [] 42 | let words = [] 43 | 44 | let position = spans[0].offsetTop 45 | 46 | each(spans, (span, index) => { 47 | if (span.offsetTop === position) { 48 | words.push(span) 49 | } 50 | 51 | if (span.offsetTop !== position) { 52 | lines.push(words) 53 | 54 | words = [] 55 | words.push(span) 56 | 57 | position = span.offsetTop 58 | } 59 | 60 | if (index + 1 === spans.length) { 61 | lines.push(words) 62 | } 63 | }) 64 | 65 | return lines 66 | } 67 | 68 | function splitText (text, expression) { 69 | const splits = text.split('
') 70 | 71 | let words = [] 72 | 73 | each(splits, (item, index) => { 74 | if (index > 0) { 75 | words.push('
') 76 | } 77 | 78 | words = words.concat(item.split(expression)) 79 | 80 | let isLink = false 81 | let link = '' 82 | 83 | const innerHTML = [] 84 | 85 | each(words, word => { 86 | if (!isLink && (word.includes('') || word.includes('/strong>'))) { 97 | innerHTML.push(link) 98 | 99 | link = '' 100 | } 101 | 102 | if (!isLink && link === '') { 103 | innerHTML.push(word) 104 | } 105 | 106 | if (isLink && (word.includes('/a>') || word.includes('/strong>'))) { 107 | isLink = false 108 | } 109 | }) 110 | 111 | words = innerHTML 112 | }) 113 | 114 | return words 115 | } 116 | 117 | function parseLine (line) { 118 | line = line.trim() 119 | 120 | if (line === '' || line === ' ') { 121 | return line 122 | } else { 123 | return (line === '
') ? '
' : `${line}` + ((line.length > 1) ? ' ' : '') 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 5 | const CopyWebpackPlugin = require('copy-webpack-plugin') 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 7 | const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') 8 | const TerserPlugin = require('terser-webpack-plugin') 9 | 10 | const IS_DEVELOPMENT = process.env.NODE_ENV === 'dev' 11 | 12 | const dirApp = path.join(__dirname, 'app') 13 | const dirShared = path.join(__dirname, 'shared') 14 | const dirStyles = path.join(__dirname, 'styles') 15 | const dirNode = 'node_modules' 16 | 17 | module.exports = { 18 | entry: [ 19 | path.join(dirApp, 'index.js'), 20 | path.join(dirStyles, 'index.scss') 21 | ], 22 | 23 | resolve: { 24 | modules: [ 25 | dirApp, 26 | dirShared, 27 | dirStyles, 28 | dirNode 29 | ] 30 | }, 31 | 32 | plugins: [ 33 | new webpack.DefinePlugin({ 34 | IS_DEVELOPMENT 35 | }), 36 | 37 | new CopyWebpackPlugin({ 38 | patterns: [ 39 | { 40 | from: './shared', 41 | to: '' 42 | } 43 | ] 44 | }), 45 | 46 | new MiniCssExtractPlugin({ 47 | filename: '[name].css', 48 | chunkFilename: '[id].css' 49 | }), 50 | 51 | new ImageMinimizerPlugin({ 52 | minimizerOptions: { 53 | plugins: [ 54 | ['gifsicle', { interlaced: true }], 55 | ['jpegtran', { progressive: true }], 56 | ['optipng', { optimizationLevel: 8 }] 57 | ] 58 | } 59 | }), 60 | 61 | new CleanWebpackPlugin() 62 | ], 63 | 64 | module: { 65 | rules: [ 66 | { 67 | test: /\.js$/, 68 | use: { 69 | loader: 'babel-loader' 70 | } 71 | }, 72 | 73 | { 74 | test: /\.scss$/, 75 | use: [ 76 | { 77 | loader: MiniCssExtractPlugin.loader, 78 | options: { 79 | publicPath: '' 80 | } 81 | }, 82 | { 83 | loader: 'css-loader' 84 | }, 85 | { 86 | loader: 'postcss-loader' 87 | }, 88 | { 89 | loader: 'sass-loader' 90 | } 91 | ] 92 | }, 93 | 94 | { 95 | test: /\.(jpe?g|png|gif|svg|woff2?|fnt|webp)$/, 96 | loader: 'file-loader', 97 | options: { 98 | name (file) { 99 | return '[hash].[ext]' 100 | } 101 | } 102 | }, 103 | 104 | { 105 | test: /\.(jpe?g|png|gif|svg|webp)$/i, 106 | use: [ 107 | { 108 | loader: ImageMinimizerPlugin.loader 109 | } 110 | ] 111 | }, 112 | 113 | { 114 | test: /\.(glsl|frag|vert)$/, 115 | loader: 'raw-loader', 116 | exclude: /node_modules/ 117 | }, 118 | 119 | { 120 | test: /\.(glsl|frag|vert)$/, 121 | loader: 'glslify-loader', 122 | exclude: /node_modules/ 123 | } 124 | ] 125 | }, 126 | 127 | optimization: { 128 | minimize: true, 129 | minimizer: [new TerserPlugin()] 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/components/Canvas/Home/Media.js: -------------------------------------------------------------------------------- 1 | import GSAP from 'gsap' 2 | import { Mesh, Program } from 'ogl' 3 | 4 | import fragment from 'shaders/home-fragment.glsl' 5 | import vertex from 'shaders/home-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.createTexture() 22 | this.createProgram() 23 | this.createMesh() 24 | this.createBounds({ 25 | sizes: this.sizes 26 | }) 27 | } 28 | 29 | createTexture () { 30 | const image = this.element 31 | 32 | this.texture = window.TEXTURES[image.getAttribute('data-src')] 33 | } 34 | 35 | createProgram () { 36 | this.program = new Program(this.gl, { 37 | fragment, 38 | vertex, 39 | uniforms: { 40 | uAlpha: { value: 0 }, 41 | uSpeed: { value: 0 }, 42 | uViewportSizes: { value: [this.sizes.width, this.sizes.height] }, 43 | tMap: { value: this.texture } 44 | } 45 | }) 46 | } 47 | 48 | createMesh () { 49 | this.mesh = new Mesh(this.gl, { 50 | geometry: this.geometry, 51 | program: this.program 52 | }) 53 | 54 | this.mesh.setParent(this.scene) 55 | this.mesh.rotation.z = GSAP.utils.random(-Math.PI * 0.03, Math.PI * 0.03) 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 | GSAP.fromTo(this.program.uniforms.uAlpha, { 73 | value: 0 74 | }, { 75 | value: 0.4 76 | }) 77 | } 78 | 79 | hide () { 80 | GSAP.to(this.program.uniforms.uAlpha, { 81 | value: 0 82 | }) 83 | } 84 | 85 | /** 86 | * Events. 87 | */ 88 | onResize (sizes, scroll) { 89 | this.extra = { 90 | x: 0, 91 | y: 0 92 | } 93 | 94 | this.createBounds(sizes) 95 | this.updateX(scroll && scroll.x) 96 | this.updateY(scroll && scroll.y) 97 | } 98 | 99 | /** 100 | * Loop. 101 | */ 102 | updateScale () { 103 | this.height = this.bounds.height / window.innerHeight 104 | this.width = this.bounds.width / window.innerWidth 105 | 106 | this.mesh.scale.x = this.sizes.width * this.width 107 | this.mesh.scale.y = this.sizes.height * this.height 108 | } 109 | 110 | updateX (x = 0) { 111 | this.x = (this.bounds.left + x) / window.innerWidth 112 | 113 | this.mesh.position.x = (-this.sizes.width / 2) + (this.mesh.scale.x / 2) + (this.x * this.sizes.width) + this.extra.x 114 | } 115 | 116 | updateY (y = 0) { 117 | this.y = (this.bounds.top + y) / window.innerHeight 118 | 119 | this.mesh.position.y = (this.sizes.height / 2) - (this.mesh.scale.y / 2) - (this.y * this.sizes.height) + this.extra.y 120 | } 121 | 122 | update (scroll, speed) { 123 | this.updateX(scroll.x) 124 | this.updateY(scroll.y) 125 | 126 | this.program.uniforms.uSpeed.value = speed 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /styles/pages/details/details.scss: -------------------------------------------------------------------------------- 1 | .detail { 2 | @extend %center; 3 | @extend %page; 4 | 5 | // background: $color-quicksand; 6 | // color: $color-quarter-spanish-white; 7 | 8 | @include media(' { 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 | } 101 | 102 | onTouchMove () { 103 | 104 | } 105 | 106 | onTouchUp () { 107 | 108 | } 109 | 110 | /** 111 | * Loop. 112 | */ 113 | updateScale () { 114 | this.height = this.bounds.height / window.innerHeight 115 | this.width = this.bounds.width / window.innerWidth 116 | 117 | this.mesh.scale.x = this.sizes.width * this.width 118 | this.mesh.scale.y = this.sizes.height * this.height 119 | } 120 | 121 | updateX () { 122 | this.x = (this.bounds.left) / window.innerWidth 123 | 124 | this.mesh.position.x = (-this.sizes.width / 2) + (this.mesh.scale.x / 2) + (this.x * this.sizes.width) 125 | } 126 | 127 | updateY () { 128 | this.y = (this.bounds.top) / window.innerHeight 129 | 130 | this.mesh.position.y = (this.sizes.height / 2) - (this.mesh.scale.y / 2) - (this.y * this.sizes.height) 131 | } 132 | 133 | update () { 134 | this.updateX() 135 | this.updateY() 136 | } 137 | 138 | /** 139 | * Destroy. 140 | */ 141 | destroy () { 142 | this.scene.removeChild(this.mesh) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /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') 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(this.opacity, { 76 | multiplier: 0 77 | }, { 78 | multiplier: 1 79 | }) 80 | } 81 | 82 | hide () { 83 | GSAP.to(this.opacity, { 84 | multiplier: 0 85 | }) 86 | } 87 | 88 | /** 89 | * Events. 90 | */ 91 | onResize (sizes, scroll) { 92 | this.extra = { 93 | x: 0, 94 | y: 0 95 | } 96 | 97 | this.createBounds(sizes) 98 | this.updateX(scroll && scroll.x) 99 | } 100 | 101 | /** 102 | * Loop. 103 | */ 104 | updateScale () { 105 | this.height = this.bounds.height / window.innerHeight 106 | this.width = this.bounds.width / window.innerWidth 107 | 108 | this.mesh.scale.x = this.sizes.width * this.width 109 | this.mesh.scale.y = this.sizes.height * this.height 110 | } 111 | 112 | updateX (x = 0) { 113 | this.x = (this.bounds.left + x) / window.innerWidth 114 | 115 | this.mesh.position.x = (-this.sizes.width / 2) + (this.mesh.scale.x / 2) + (this.x * this.sizes.width) + this.extra.x 116 | } 117 | 118 | update (scroll, index) { 119 | this.updateX(scroll) 120 | 121 | const amplitude = 0.1 122 | const frequency = 1 123 | 124 | this.mesh.rotation.z = -0.02 * Math.PI * Math.sin(this.index / frequency) 125 | this.mesh.position.y = amplitude * Math.sin(this.index / frequency) 126 | 127 | this.opacity.target = index === this.index ? 1 : 0.4 128 | this.opacity.current = GSAP.utils.interpolate(this.opacity.current, this.opacity.target, this.opacity.lerp) 129 | 130 | this.program.uniforms.uAlpha.value = this.opacity.multiplier * this.opacity.current 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /app/components/Canvas/About/Media.js: -------------------------------------------------------------------------------- 1 | import GSAP from 'gsap' 2 | import { Mesh, Program } from 'ogl' 3 | 4 | import Detection from 'classes/Detection' 5 | 6 | import fragment from 'shaders/plane-fragment.glsl' 7 | import vertex from 'shaders/plane-vertex.glsl' 8 | 9 | export default class { 10 | constructor ({ element, geometry, gl, index, scene, sizes }) { 11 | this.element = element 12 | this.geometry = geometry 13 | this.gl = gl 14 | this.index = index 15 | this.scene = scene 16 | this.sizes = sizes 17 | 18 | this.extra = { 19 | x: 0, 20 | y: 0 21 | } 22 | 23 | this.createTexture() 24 | this.createProgram() 25 | this.createMesh() 26 | this.createBounds({ 27 | sizes: this.sizes 28 | }) 29 | } 30 | 31 | createTexture () { 32 | const image = this.element.querySelector('img') 33 | 34 | this.texture = window.TEXTURES[image.getAttribute('data-src')] 35 | } 36 | 37 | createProgram () { 38 | this.program = new Program(this.gl, { 39 | fragment, 40 | vertex, 41 | uniforms: { 42 | uAlpha: { value: 0 }, 43 | tMap: { value: this.texture } 44 | } 45 | }) 46 | } 47 | 48 | createMesh () { 49 | this.mesh = new Mesh(this.gl, { 50 | geometry: this.geometry, 51 | program: this.program 52 | }) 53 | 54 | this.mesh.setParent(this.scene) 55 | } 56 | 57 | createBounds ({ sizes }) { 58 | this.sizes = sizes 59 | 60 | this.bounds = this.element.getBoundingClientRect() 61 | 62 | this.updateScale() 63 | this.updateX() 64 | this.updateY() 65 | } 66 | 67 | /** 68 | * Animations. 69 | */ 70 | show () { 71 | GSAP.fromTo(this.program.uniforms.uAlpha, { 72 | value: 0 73 | }, { 74 | value: 1 75 | }) 76 | } 77 | 78 | hide () { 79 | GSAP.to(this.program.uniforms.uAlpha, { 80 | value: 0 81 | }) 82 | } 83 | 84 | /** 85 | * Events. 86 | */ 87 | onResize (sizes, scroll, width) { 88 | this.extra = 0 89 | this.widthTotal = width 90 | 91 | this.createBounds(sizes) 92 | this.updateX(scroll) 93 | this.updateY(0) 94 | } 95 | 96 | /** 97 | * Loop. 98 | */ 99 | updateRotation () { 100 | this.mesh.rotation.z = GSAP.utils.mapRange(-this.sizes.width / 2, this.sizes.width / 2, Math.PI * 0.1, -Math.PI * 0.1, this.mesh.position.x) 101 | } 102 | 103 | updateScale () { 104 | this.height = this.bounds.height / window.innerHeight 105 | this.width = this.bounds.width / window.innerWidth 106 | 107 | this.mesh.scale.x = this.sizes.width * this.width 108 | this.mesh.scale.y = this.sizes.height * this.height 109 | } 110 | 111 | updateX (x = 0) { 112 | this.x = (this.bounds.left + x) / window.innerWidth 113 | 114 | this.mesh.position.x = (-this.sizes.width / 2) + (this.mesh.scale.x / 2) + (this.x * this.sizes.width) + this.extra 115 | } 116 | 117 | updateY (y = 0) { 118 | this.y = (this.bounds.top + y) / window.innerHeight 119 | 120 | const extra = Detection.isPhone() ? 15 : 40 121 | 122 | this.mesh.position.y = (this.sizes.height / 2) - (this.mesh.scale.y / 2) - (this.y * this.sizes.height) 123 | this.mesh.position.y += Math.cos((this.mesh.position.x / this.sizes.width) * Math.PI * 0.1) * extra - extra 124 | } 125 | 126 | update (scroll) { 127 | this.updateRotation() 128 | this.updateScale() 129 | this.updateX(scroll) 130 | this.updateY(0) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /styles/pages/about/content.scss: -------------------------------------------------------------------------------- 1 | .about__content { 2 | display: flex; 3 | justify-content: center; 4 | margin: 20rem auto; 5 | position: relative; 6 | 7 | &:first-child { 8 | margin-top: 0; 9 | } 10 | 11 | &:last-child { 12 | margin-bottom: 0; 13 | } 14 | 15 | &:nth-child(5) { 16 | justify-content: flex-end; 17 | } 18 | 19 | &:nth-child(8) { 20 | justify-content: flex-start; 21 | } 22 | 23 | @include media(' { 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 | /** 54 | * Animations. 55 | */ 56 | show () { 57 | map(this.medias, media => media.show()) 58 | } 59 | 60 | hide () { 61 | map(this.medias, media => media.hide()) 62 | } 63 | 64 | /** 65 | * Events. 66 | */ 67 | onResize (event) { 68 | this.bounds = this.elementWrapper.getBoundingClientRect() 69 | 70 | this.sizes = event.sizes 71 | 72 | this.width = this.bounds.width / window.innerWidth * this.sizes.width 73 | 74 | this.scroll.current = this.scroll.target = 0 75 | 76 | map(this.medias, media => media.onResize(event, this.scroll.current, this.width)) 77 | } 78 | 79 | onTouchDown ({ x, y }) { 80 | this.scroll.start = this.scroll.current 81 | } 82 | 83 | onTouchMove ({ x, y }) { 84 | const distance = x.start - x.end 85 | 86 | this.scroll.target = this.scroll.start - distance 87 | } 88 | 89 | onTouchUp ({ x, y }) { 90 | 91 | } 92 | 93 | /** 94 | * Update. 95 | */ 96 | update (scroll) { 97 | const distance = (scroll.current - scroll.target) * 0.1 98 | const y = scroll.current / window.innerHeight 99 | 100 | if (this.scroll.current < this.scroll.target) { 101 | this.direction = 'right' 102 | 103 | this.scroll.velocity = -1 104 | } else if (this.scroll.current > this.scroll.target) { 105 | this.direction = 'left' 106 | 107 | this.scroll.velocity = 1 108 | } 109 | 110 | this.scroll.target -= this.scroll.velocity 111 | this.scroll.target += distance 112 | 113 | this.scroll.current = GSAP.utils.interpolate(this.scroll.current, this.scroll.target, this.scroll.lerp) 114 | 115 | map(this.medias, (media, index) => { 116 | const scaleX = media.mesh.scale.x / 2 + 0.25 117 | 118 | if (this.direction === 'left') { 119 | const x = media.mesh.position.x + scaleX 120 | 121 | if (x < -this.sizes.width / 2) { 122 | media.extra += this.width 123 | } 124 | } else if (this.direction === 'right') { 125 | const x = media.mesh.position.x - scaleX 126 | 127 | if (x > this.sizes.width / 2) { 128 | media.extra -= this.width 129 | } 130 | } 131 | 132 | media.update(this.scroll.current) 133 | }) 134 | 135 | this.group.position.y = y * this.sizes.height 136 | } 137 | 138 | /** 139 | * Destroy. 140 | */ 141 | destroy () { 142 | this.scene.removeChild(this.group) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /styles/pages/home/home.scss: -------------------------------------------------------------------------------- 1 | .home { 2 | @extend %center; 3 | @extend %page; 4 | 5 | // background: $color-contessa; 6 | // color: $color-quarter-spanish-white; 7 | overflow: hidden; 8 | text-align: center; 9 | 10 | &:before, 11 | &:after { 12 | content: ''; 13 | height: 24rem; 14 | left: 0; 15 | position: absolute; 16 | width: 100%; 17 | z-index: 1; 18 | } 19 | 20 | &:before { 21 | background: linear-gradient(to bottom, $color-contessa 0%, rgba($color-contessa, 0) 100%); 22 | top: 0; 23 | } 24 | 25 | &:after { 26 | background: linear-gradient(to top, $color-contessa 0%, rgba($color-contessa, 0) 100%); 27 | bottom: 0; 28 | } 29 | } 30 | 31 | .home__wrapper { 32 | 33 | } 34 | 35 | .home__titles { 36 | @extend %vertical-titles; 37 | } 38 | 39 | .home__titles__label { 40 | @extend %vertical-titles__label; 41 | } 42 | 43 | .home__titles__title { 44 | @extend %vertical-titles__title; 45 | } 46 | 47 | .home__gallery { 48 | @extend %cover; 49 | 50 | height: 172.5rem; 51 | width: 241.8rem + 9rem; 52 | visibility: hidden; 53 | } 54 | 55 | .home__gallery__media { 56 | position: absolute; 57 | 58 | &:nth-child(1) { 59 | height: 41.8rem; 60 | left: 9.4rem; 61 | top: 16.4rem; 62 | width: 27.8rem; 63 | } 64 | 65 | &:nth-child(2) { 66 | height: 41.8rem; 67 | left: 54.1rem; 68 | top: 19.2rem; 69 | width: 27.8rem; 70 | } 71 | 72 | &:nth-child(3) { 73 | height: 41.8rem; 74 | left: 106.3rem; 75 | top: 10.7rem; 76 | width: 27.8rem; 77 | } 78 | 79 | &:nth-child(4) { 80 | height: 60.8rem; 81 | left: 154.1rem; 82 | top: 0; 83 | width: 40.6rem; 84 | } 85 | 86 | &:nth-child(5) { 87 | height: 41.8rem; 88 | left: 207rem; 89 | top: 10.7rem; 90 | width: 27.8rem; 91 | } 92 | 93 | &:nth-child(6) { 94 | height: 52.3rem; 95 | left: 0; 96 | top: 69.1rem; 97 | width: 37.5rem; 98 | } 99 | 100 | &:nth-child(7) { 101 | height: 60.8rem; 102 | left: 46.7rem; 103 | top: 66.2rem; 104 | width: 40.6rem; 105 | } 106 | 107 | &:nth-child(8) { 108 | height: 58.2rem; 109 | left: 101.5rem; 110 | top: 64.7rem; 111 | width: 39.7rem; 112 | } 113 | 114 | &:nth-child(9) { 115 | height: 41.8rem; 116 | left: 160rem; 117 | top: 63.8rem; 118 | width: 27.8rem; 119 | } 120 | 121 | &:nth-child(10) { 122 | height: 58.2rem; 123 | left: 202.1rem; 124 | top: 64.7rem; 125 | width: 39.7rem; 126 | } 127 | 128 | &:nth-child(11) { 129 | height: 41.8rem; 130 | left: 13.1rem; 131 | top: 128.9rem; 132 | width: 27.8rem; 133 | } 134 | 135 | &:nth-child(12) { 136 | height: 41.8rem; 137 | left: 54.1rem; 138 | top: 130.4rem; 139 | width: 27.8rem; 140 | } 141 | 142 | &:nth-child(13) { 143 | height: 41.8rem; 144 | left: 105.7rem; 145 | top: 128.6rem; 146 | width: 27.8rem; 147 | } 148 | 149 | &:nth-child(14) { 150 | height: 41.8rem; 151 | left: 164.7rem; 152 | top: 110.9rem; 153 | width: 27.8rem; 154 | } 155 | 156 | &:nth-child(15) { 157 | height: 41.8rem; 158 | left: 206.3rem; 159 | top: 128.6rem; 160 | width: 27.8rem; 161 | } 162 | } 163 | 164 | .home__gallery__media__image { 165 | @extend %cover; 166 | 167 | opacity: 1; 168 | } 169 | 170 | .home__link { 171 | @extend %description-14; 172 | 173 | align-items: center; 174 | bottom: 5rem; 175 | display: flex; 176 | height: 6rem; 177 | justify-content: center; 178 | left: 50%; 179 | letter-spacing: 0.05rem; 180 | position: absolute; 181 | text-transform: uppercase; 182 | transform: translateX(-50%); 183 | width: 28.8rem; 184 | z-index: 2; 185 | 186 | @include media(' { 24 | return Prismic.getApi(process.env.PRISMIC_ENDPOINT, { 25 | accessToken: process.env.PRISMIC_ACCESS_TOKEN, 26 | req 27 | }); 28 | }; 29 | 30 | const handleLinkResolver = (doc) => { 31 | if (doc.type === 'product') { 32 | return `/detail/${doc.uid}`; 33 | } 34 | 35 | if (doc.type === 'collections') { 36 | return '/collections'; 37 | } 38 | 39 | if (doc.type === 'about') { 40 | return '/about'; 41 | } 42 | 43 | return '/'; 44 | }; 45 | 46 | app.use((req, res, next) => { 47 | const ua = UAParser(req.headers['user-agent']); 48 | 49 | res.locals.isDesktop = ua.device.type === undefined; 50 | res.locals.isPhone = ua.device.type === 'mobile'; 51 | res.locals.isTablet = ua.device.type === 'tablet'; 52 | 53 | res.locals.Link = handleLinkResolver; 54 | 55 | res.locals.Numbers = (index) => { 56 | return index == 0 57 | ? 'One' 58 | : index == 1 59 | ? 'Two' 60 | : index == 2 61 | ? 'Three' 62 | : index == 3 63 | ? 'Four' 64 | : ''; 65 | }; 66 | 67 | res.locals.PrismicDOM = PrismicDOM; 68 | 69 | next(); 70 | }); 71 | 72 | app.set('views', path.join(__dirname, 'views')); 73 | app.set('view engine', 'pug'); 74 | 75 | const handleRequest = async (api) => { 76 | const about = await api.getSingle('about'); 77 | const home = await api.getSingle('home'); 78 | const meta = await api.getSingle('meta'); 79 | const navigation = await api.getSingle('navigation'); 80 | const preloader = await api.getSingle('preloader'); 81 | 82 | const { results: collections } = await api.query( 83 | Prismic.Predicates.at('document.type', 'collection'), 84 | { 85 | fetchLinks: 'product.image' 86 | } 87 | ); 88 | 89 | const assets = []; 90 | 91 | home.data.gallery.forEach((item) => { 92 | assets.push(item.image.url); 93 | }); 94 | 95 | about.data.gallery.forEach((item) => { 96 | assets.push(item.image.url); 97 | }); 98 | 99 | about.data.body.forEach((section) => { 100 | if (section.slice_type === 'gallery') { 101 | section.items.forEach((item) => { 102 | assets.push(item.image.url); 103 | }); 104 | } 105 | }); 106 | 107 | collections.forEach((collection) => { 108 | collection.data.products.forEach((item) => { 109 | assets.push(item.products_product.data.image.url); 110 | }); 111 | }); 112 | 113 | return { 114 | about, 115 | assets, 116 | collections, 117 | home, 118 | meta, 119 | navigation, 120 | preloader 121 | }; 122 | }; 123 | 124 | app.get('/', async (req, res) => { 125 | const api = await initApi(req); 126 | const defaults = await handleRequest(api); 127 | 128 | res.render('pages/home', { 129 | ...defaults 130 | }); 131 | }); 132 | 133 | app.get('/about', async (req, res) => { 134 | const api = await initApi(req); 135 | const defaults = await handleRequest(api); 136 | 137 | res.render('pages/about', { 138 | ...defaults 139 | }); 140 | }); 141 | 142 | app.get('/collections', async (req, res) => { 143 | const api = await initApi(req); 144 | const defaults = await handleRequest(api); 145 | 146 | res.render('pages/collections', { 147 | ...defaults 148 | }); 149 | }); 150 | 151 | app.get('/detail/:uid', async (req, res) => { 152 | const api = await initApi(req); 153 | const defaults = await handleRequest(api); 154 | 155 | const product = await api.getByUID('product', req.params.uid, { 156 | fetchLinks: 'collection.title' 157 | }); 158 | 159 | res.render('pages/detail', { 160 | ...defaults, 161 | product 162 | }); 163 | }); 164 | 165 | app.listen(port, () => { 166 | console.log(`Example app listening at http://localhost:${port}`); 167 | }); 168 | -------------------------------------------------------------------------------- /app/classes/Page.js: -------------------------------------------------------------------------------- 1 | import GSAP from 'gsap' 2 | import Prefix from 'prefix' 3 | 4 | import each from 'lodash/each' 5 | import map from 'lodash/map' 6 | 7 | import Highlight from 'animations/Highlight' 8 | import Label from 'animations/Label' 9 | import Paragraph from 'animations/Paragraph' 10 | import Title from 'animations/Title' 11 | 12 | import AsyncLoad from 'classes/AsyncLoad' 13 | import { ColorsManager } from 'classes/Colors' 14 | 15 | export default class Page { 16 | constructor ({ 17 | element, 18 | elements, 19 | id 20 | }) { 21 | this.selector = element 22 | this.selectorChildren = { 23 | ...elements, 24 | 25 | animationsHighlights: '[data-animation="highlight"]', 26 | animationsLabels: '[data-animation="label"]', 27 | animationsParagraphs: '[data-animation="paragraph"]', 28 | animationsTitles: '[data-animation="title"]', 29 | 30 | preloaders: '[data-src]' 31 | } 32 | 33 | this.id = id 34 | 35 | this.transformPrefix = Prefix('transform') 36 | } 37 | 38 | create () { 39 | this.element = document.querySelector(this.selector) 40 | this.elements = {} 41 | 42 | this.scroll = { 43 | current: 0, 44 | target: 0, 45 | last: 0, 46 | limit: 0 47 | } 48 | 49 | each(this.selectorChildren, (entry, key) => { 50 | if (entry instanceof window.HTMLElement || entry instanceof window.NodeList || Array.isArray(entry)) { 51 | this.elements[key] = entry 52 | } else { 53 | this.elements[key] = document.querySelectorAll(entry) 54 | 55 | if (this.elements[key].length === 0) { 56 | this.elements[key] = null 57 | } else if (this.elements[key].length === 1) { 58 | this.elements[key] = document.querySelector(entry) 59 | } 60 | } 61 | }) 62 | 63 | this.createAnimations() 64 | this.createPrealoder() 65 | } 66 | 67 | createAnimations () { 68 | this.animations = [] 69 | 70 | // Highlights. 71 | this.animationsHighlights = map(this.elements.animationsHighlights, element => { 72 | return new Highlight({ 73 | element 74 | }) 75 | }) 76 | 77 | this.animations.push(...this.animationsHighlights) 78 | 79 | // Titles. 80 | this.animationsTitles = map(this.elements.animationsTitles, element => { 81 | return new Title({ 82 | element 83 | }) 84 | }) 85 | 86 | this.animations.push(...this.animationsTitles) 87 | 88 | // Paragraphs. 89 | this.animationsParagraphs = map(this.elements.animationsParagraphs, element => { 90 | return new Paragraph({ 91 | element 92 | }) 93 | }) 94 | 95 | this.animations.push(...this.animationsParagraphs) 96 | 97 | // Labels. 98 | this.animationsLabels = map(this.elements.animationsLabels, element => { 99 | return new Label({ 100 | element 101 | }) 102 | }) 103 | 104 | this.animations.push(...this.animationsLabels) 105 | } 106 | 107 | createPrealoder () { 108 | this.preloaders = map(this.elements.preloaders, element => { 109 | return new AsyncLoad({ element }) 110 | }) 111 | } 112 | 113 | /** 114 | * Animations. 115 | */ 116 | show (animation) { 117 | return new Promise(resolve => { 118 | ColorsManager.change({ 119 | backgroundColor: this.element.getAttribute('data-background'), 120 | color: this.element.getAttribute('data-color') 121 | }) 122 | 123 | if (animation) { 124 | this.animationIn = animation 125 | } else { 126 | this.animationIn = GSAP.timeline() 127 | 128 | this.animationIn.fromTo(this.element, { 129 | autoAlpha: 0 130 | }, { 131 | autoAlpha: 1 132 | }) 133 | } 134 | 135 | this.animationIn.call(_ => { 136 | this.addEventListeners() 137 | 138 | resolve() 139 | }) 140 | }) 141 | } 142 | 143 | hide () { 144 | return new Promise(resolve => { 145 | this.destroy() 146 | 147 | this.animationOut = GSAP.timeline() 148 | 149 | this.animationOut.to(this.element, { 150 | autoAlpha: 0, 151 | onComplete: resolve 152 | }) 153 | }) 154 | } 155 | 156 | /** 157 | * Events. 158 | */ 159 | onResize () { 160 | if (this.elements.wrapper) { 161 | this.scroll.limit = this.elements.wrapper.clientHeight - window.innerHeight 162 | } 163 | 164 | each(this.animations, animation => animation.onResize()) 165 | } 166 | 167 | onWheel ({ pixelY }) { 168 | this.scroll.target += pixelY 169 | } 170 | 171 | /** 172 | * Loop. 173 | */ 174 | update () { 175 | this.scroll.target = GSAP.utils.clamp(0, this.scroll.limit, this.scroll.target) 176 | 177 | this.scroll.current = GSAP.utils.interpolate(this.scroll.current, this.scroll.target, 0.1) 178 | 179 | if (this.scroll.current < 0.01) { 180 | this.scroll.current = 0 181 | } 182 | 183 | if (this.elements.wrapper) { 184 | this.elements.wrapper.style[this.transformPrefix] = `translateY(-${this.scroll.current}px)` 185 | } 186 | } 187 | 188 | /** 189 | * Listeners. 190 | */ 191 | addEventListeners () { 192 | 193 | } 194 | 195 | removeEventListeners () { 196 | 197 | } 198 | 199 | /** 200 | * Destroy. 201 | */ 202 | destroy () { 203 | this.removeEventListeners() 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /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') 24 | 25 | this.titlesElement = document.querySelector('.collections__titles') 26 | 27 | this.collectionsElements = document.querySelectorAll('.collections__article') 28 | this.collectionsElementsActive = 'collections__article--active' 29 | 30 | this.mediasElements = document.querySelectorAll('.collections__gallery__media') 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 78 | 79 | this.update() 80 | 81 | this.transition.animate({ 82 | position: { x: 0, y: media.mesh.position.y, z: 0 }, 83 | rotation: media.mesh.rotation, 84 | scale: media.mesh.scale 85 | }, _ => { 86 | media.opacity.multiplier = 1 87 | 88 | map(this.medias, item => { 89 | if (media !== item) { 90 | item.show() 91 | } 92 | }) 93 | 94 | this.scroll.current = this.scroll.target = this.scroll.start = this.scroll.last = scroll 95 | }) 96 | } else { 97 | map(this.medias, media => media.show()) 98 | } 99 | } 100 | 101 | hide () { 102 | map(this.medias, media => media.hide()) 103 | } 104 | 105 | /** 106 | * Events. 107 | */ 108 | onResize (event) { 109 | this.sizes = event.sizes 110 | 111 | this.bounds = this.galleryWrapperElement.getBoundingClientRect() 112 | 113 | this.scroll.last = this.scroll.target = 0 114 | 115 | map(this.medias, media => media.onResize(event, this.scroll)) 116 | 117 | this.scroll.limit = this.bounds.width - this.medias[0].element.clientWidth 118 | } 119 | 120 | onTouchDown ({ x, y }) { 121 | this.scroll.last = this.scroll.current 122 | } 123 | 124 | onTouchMove ({ x, y }) { 125 | const distance = x.start - x.end 126 | 127 | this.scroll.target = this.scroll.last - distance 128 | } 129 | 130 | onTouchUp ({ x, y }) { 131 | 132 | } 133 | 134 | onWheel ({ pixelY }) { 135 | this.scroll.target += pixelY 136 | } 137 | 138 | /** 139 | * Changed. 140 | */ 141 | onChange (index) { 142 | this.index = index 143 | 144 | const selectedCollection = parseInt(this.mediasElements[this.index].getAttribute('data-index')) 145 | 146 | map(this.collectionsElements, (element, elementIndex) => { 147 | if (elementIndex === selectedCollection) { 148 | element.classList.add(this.collectionsElementsActive) 149 | } else { 150 | element.classList.remove(this.collectionsElementsActive) 151 | } 152 | }) 153 | 154 | this.titlesElement.style[this.transformPrefix] = `translateY(-${25 * selectedCollection}%) translate(-50%, -50%) rotate(-90deg)` 155 | } 156 | 157 | /** 158 | * Update. 159 | */ 160 | update () { 161 | this.scroll.target = GSAP.utils.clamp(-this.scroll.limit, 0, this.scroll.target) 162 | 163 | this.scroll.current = GSAP.utils.interpolate(this.scroll.current, this.scroll.target, this.scroll.lerp) 164 | 165 | this.galleryElement.style[this.transformPrefix] = `translateX(${this.scroll.current}px)` 166 | 167 | if (this.scroll.last < this.scroll.current) { 168 | this.scroll.direction = 'right' 169 | } else if (this.scroll.last > this.scroll.current) { 170 | this.scroll.direction = 'left' 171 | } 172 | 173 | this.scroll.last = this.scroll.current 174 | 175 | const index = Math.floor(Math.abs((this.scroll.current - (this.medias[0].bounds.width / 2)) / this.scroll.limit) * (this.medias.length - 1)) 176 | 177 | if (this.index !== index) { 178 | this.onChange(index) 179 | } 180 | 181 | map(this.medias, (media, index) => { 182 | media.update(this.scroll.current, this.index) 183 | }) 184 | } 185 | 186 | /** 187 | * Destroy. 188 | */ 189 | destroy () { 190 | this.scene.removeChild(this.group) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /app/components/Canvas/Home/index.js: -------------------------------------------------------------------------------- 1 | import { Plane, Transform } from 'ogl' 2 | import GSAP from 'gsap' 3 | 4 | import map from 'lodash/map' 5 | 6 | import Media from './Media' 7 | 8 | export default class { 9 | constructor ({ gl, scene, sizes }) { 10 | this.gl = gl 11 | this.scene = scene 12 | this.sizes = sizes 13 | 14 | this.group = new Transform() 15 | 16 | this.galleryElement = document.querySelector('.home__gallery') 17 | this.mediasElements = document.querySelectorAll('.home__gallery__media__image') 18 | 19 | this.x = { 20 | current: 0, 21 | target: 0, 22 | lerp: 0.1 23 | } 24 | 25 | this.y = { 26 | current: 0, 27 | target: 0, 28 | lerp: 0.1 29 | } 30 | 31 | this.scrollCurrent = { 32 | x: 0, 33 | y: 0 34 | } 35 | 36 | this.scroll = { 37 | x: 0, 38 | y: 0 39 | } 40 | 41 | this.speed = { 42 | current: 0, 43 | target: 0, 44 | lerp: 0.1 45 | } 46 | 47 | this.createGeometry() 48 | this.createGallery() 49 | 50 | this.onResize({ 51 | sizes: this.sizes 52 | }) 53 | 54 | this.group.setParent(this.scene) 55 | 56 | this.show() 57 | } 58 | 59 | createGeometry () { 60 | this.geometry = new Plane(this.gl, { 61 | heightSegments: 20, 62 | widthSegments: 20 63 | }) 64 | } 65 | 66 | createGallery () { 67 | this.medias = map(this.mediasElements, (element, index) => { 68 | return new Media({ 69 | element, 70 | geometry: this.geometry, 71 | index, 72 | gl: this.gl, 73 | scene: this.group, 74 | sizes: this.sizes 75 | }) 76 | }) 77 | } 78 | 79 | /** 80 | * Animations. 81 | */ 82 | show () { 83 | map(this.medias, media => media.show()) 84 | } 85 | 86 | hide () { 87 | map(this.medias, media => media.hide()) 88 | } 89 | 90 | /** 91 | * Events. 92 | */ 93 | onResize (event) { 94 | this.galleryBounds = this.galleryElement.getBoundingClientRect() 95 | 96 | this.sizes = event.sizes 97 | 98 | this.gallerySizes = { 99 | height: this.galleryBounds.height / window.innerHeight * this.sizes.height, 100 | width: this.galleryBounds.width / window.innerWidth * this.sizes.width, 101 | } 102 | 103 | this.scroll.x = this.x.target = 0 104 | this.scroll.y = this.y.target = 0 105 | 106 | map(this.medias, media => media.onResize(event, this.scroll)) 107 | } 108 | 109 | onTouchDown ({ x, y }) { 110 | this.speed.target = 1 111 | 112 | this.scrollCurrent.x = this.scroll.x 113 | this.scrollCurrent.y = this.scroll.y 114 | } 115 | 116 | onTouchMove ({ x, y }) { 117 | const xDistance = x.start - x.end 118 | const yDistance = y.start - y.end 119 | 120 | this.x.target = this.scrollCurrent.x - xDistance 121 | this.y.target = this.scrollCurrent.y - yDistance 122 | } 123 | 124 | onTouchUp ({ x, y }) { 125 | this.speed.target = 0 126 | } 127 | 128 | onWheel ({ pixelX, pixelY }) { 129 | this.x.target += pixelX 130 | this.y.target += pixelY 131 | } 132 | 133 | /** 134 | * Update. 135 | */ 136 | update () { 137 | this.speed.current = GSAP.utils.interpolate(this.speed.current, this.speed.target, this.speed.lerp) 138 | 139 | this.x.current = GSAP.utils.interpolate(this.x.current, this.x.target, this.x.lerp) 140 | this.y.current = GSAP.utils.interpolate(this.y.current, this.y.target, this.y.lerp) 141 | 142 | if (this.scroll.x < this.x.current) { 143 | this.x.direction = 'right' 144 | } else if (this.scroll.x > this.x.current) { 145 | this.x.direction = 'left' 146 | } 147 | 148 | if (this.scroll.y < this.y.current) { 149 | this.y.direction = 'top' 150 | } else if (this.scroll.y > this.y.current) { 151 | this.y.direction = 'bottom' 152 | } 153 | 154 | this.scroll.x = this.x.current 155 | this.scroll.y = this.y.current 156 | 157 | map(this.medias, (media, index) => { 158 | const offsetX = this.sizes.width * 0.6 159 | const scaleX = media.mesh.scale.x / 2 160 | 161 | if (this.x.direction === 'left') { 162 | const x = media.mesh.position.x + scaleX 163 | 164 | if (x < -offsetX) { 165 | media.extra.x += this.gallerySizes.width 166 | 167 | media.mesh.rotation.z = GSAP.utils.random(-Math.PI * 0.03, Math.PI * 0.03) 168 | } 169 | } else if (this.x.direction === 'right') { 170 | const x = media.mesh.position.x - scaleX 171 | 172 | if (x > offsetX) { 173 | media.extra.x -= this.gallerySizes.width 174 | 175 | media.mesh.rotation.z = GSAP.utils.random(-Math.PI * 0.03, Math.PI * 0.03) 176 | } 177 | } 178 | 179 | const offsetY = this.sizes.height * 0.6 180 | const scaleY = media.mesh.scale.y / 2 181 | 182 | if (this.y.direction === 'top') { 183 | const y = media.mesh.position.y + scaleY 184 | 185 | if (y < -offsetY) { 186 | media.extra.y += this.gallerySizes.height 187 | 188 | media.mesh.rotation.z = GSAP.utils.random(-Math.PI * 0.03, Math.PI * 0.03) 189 | } 190 | } else if (this.y.direction === 'bottom') { 191 | const y = media.mesh.position.y - scaleY 192 | 193 | if (y > offsetY) { 194 | media.extra.y -= this.gallerySizes.height 195 | 196 | media.mesh.rotation.z = GSAP.utils.random(-Math.PI * 0.03, Math.PI * 0.03) 197 | } 198 | } 199 | 200 | media.update(this.scroll, this.speed.current) 201 | }) 202 | } 203 | 204 | /** 205 | * Destroy. 206 | */ 207 | destroy () { 208 | this.scene.removeChild(this.group) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import NormalizeWheel from 'normalize-wheel'; 2 | 3 | import each from 'lodash/each'; 4 | 5 | import Canvas from 'components/Canvas'; 6 | 7 | import Navigation from 'components/Navigation'; 8 | import Preloader from 'components/Preloader'; 9 | 10 | import About from 'pages/About'; 11 | import Collections from 'pages/Collections'; 12 | import Detail from 'pages/Detail'; 13 | import Home from 'pages/Home'; 14 | 15 | class App { 16 | constructor() { 17 | this.createContent(); 18 | 19 | this.createCanvas(); 20 | this.createPreloader(); 21 | this.createNavigation(); 22 | this.createPages(); 23 | 24 | this.addEventListeners(); 25 | this.addLinkListeners(); 26 | 27 | this.onResize(); 28 | 29 | this.update(); 30 | } 31 | 32 | createNavigation() { 33 | this.navigation = new Navigation({ 34 | template: this.template 35 | }); 36 | } 37 | 38 | createPreloader() { 39 | this.preloader = new Preloader({ 40 | canvas: this.canvas 41 | }); 42 | 43 | this.preloader.once('completed', this.onPreloaded.bind(this)); 44 | } 45 | 46 | createCanvas() { 47 | this.canvas = new Canvas({ 48 | template: this.template 49 | }); 50 | } 51 | 52 | createContent() { 53 | this.content = document.querySelector('.content'); 54 | this.template = this.content.getAttribute('data-template'); 55 | } 56 | 57 | createPages() { 58 | this.pages = { 59 | about: new About(), 60 | collections: new Collections(), 61 | detail: new Detail(), 62 | home: new Home() 63 | }; 64 | 65 | this.page = this.pages[this.template]; 66 | this.page.create(); 67 | } 68 | 69 | /** 70 | * Events. 71 | */ 72 | onPreloaded() { 73 | this.onResize(); 74 | 75 | this.canvas.onPreloaded(); 76 | 77 | this.page.show(); 78 | } 79 | 80 | onPopState() { 81 | this.onChange({ 82 | url: window.location.pathname, 83 | push: false 84 | }); 85 | } 86 | 87 | async onChange({ url, push = true }) { 88 | this.canvas.onChangeStart(this.template, url); 89 | 90 | await this.page.hide(); 91 | 92 | const request = await window.fetch(url); 93 | 94 | if (request.status === 200) { 95 | const html = await request.text(); 96 | const div = document.createElement('div'); 97 | 98 | if (push) { 99 | window.history.pushState({}, '', url); 100 | } 101 | 102 | div.innerHTML = html; 103 | 104 | const divContent = div.querySelector('.content'); 105 | 106 | this.template = divContent.getAttribute('data-template'); 107 | 108 | this.navigation.onChange(this.template); 109 | 110 | this.content.setAttribute('data-template', this.template); 111 | this.content.innerHTML = divContent.innerHTML; 112 | 113 | this.canvas.onChangeEnd(this.template); 114 | 115 | this.page = this.pages[this.template]; 116 | this.page.create(); 117 | 118 | this.onResize(); 119 | 120 | this.page.show(); 121 | 122 | this.addLinkListeners(); 123 | } else { 124 | this.onChange({ ur: '/' }); 125 | } 126 | } 127 | 128 | onResize() { 129 | if (this.page && this.page.onResize) { 130 | this.page.onResize(); 131 | } 132 | 133 | window.requestAnimationFrame((_) => { 134 | if (this.canvas && this.canvas.onResize) { 135 | this.canvas.onResize(); 136 | } 137 | }); 138 | } 139 | 140 | onTouchDown(event) { 141 | if (this.canvas && this.canvas.onTouchDown) { 142 | this.canvas.onTouchDown(event); 143 | } 144 | } 145 | 146 | onTouchMove(event) { 147 | if (this.canvas && this.canvas.onTouchMove) { 148 | this.canvas.onTouchMove(event); 149 | } 150 | } 151 | 152 | onTouchUp(event) { 153 | if (this.canvas && this.canvas.onTouchUp) { 154 | this.canvas.onTouchUp(event); 155 | } 156 | } 157 | 158 | onWheel(event) { 159 | const normalizedWheel = NormalizeWheel(event); 160 | 161 | if (this.canvas && this.canvas.onWheel) { 162 | this.canvas.onWheel(normalizedWheel); 163 | } 164 | 165 | if (this.page && this.page.onWheel) { 166 | this.page.onWheel(normalizedWheel); 167 | } 168 | } 169 | 170 | /** 171 | * Loop. 172 | */ 173 | update() { 174 | if (this.page && this.page.update) { 175 | this.page.update(); 176 | } 177 | 178 | if (this.canvas && this.canvas.update) { 179 | this.canvas.update(this.page.scroll); 180 | } 181 | 182 | this.frame = window.requestAnimationFrame(this.update.bind(this)); 183 | } 184 | 185 | /*** 186 | * Listeners. 187 | */ 188 | addEventListeners() { 189 | window.addEventListener('popstate', this.onPopState.bind(this)); 190 | window.addEventListener('mousewheel', this.onWheel.bind(this)); 191 | 192 | window.addEventListener('mousedown', this.onTouchDown.bind(this)); 193 | window.addEventListener('mousemove', this.onTouchMove.bind(this)); 194 | window.addEventListener('mouseup', this.onTouchUp.bind(this)); 195 | 196 | window.addEventListener('touchstart', this.onTouchDown.bind(this)); 197 | window.addEventListener('touchmove', this.onTouchMove.bind(this)); 198 | window.addEventListener('touchend', this.onTouchUp.bind(this)); 199 | 200 | window.addEventListener('resize', this.onResize.bind(this)); 201 | } 202 | 203 | addLinkListeners() { 204 | const links = document.querySelectorAll('a'); 205 | 206 | each(links, (link) => { 207 | link.onclick = (event) => { 208 | event.preventDefault(); 209 | 210 | const { href } = link; 211 | 212 | this.onChange({ url: href }); 213 | }; 214 | }); 215 | } 216 | } 217 | 218 | new App(); 219 | -------------------------------------------------------------------------------- /app/components/Canvas/index.js: -------------------------------------------------------------------------------- 1 | import GSAP from 'gsap' 2 | import { Camera, Renderer, Transform } from 'ogl' 3 | 4 | import About from './About' 5 | import Collections from './Collections' 6 | import Detail from './Detail' 7 | import Home from './Home' 8 | 9 | import Transition from './Transition' 10 | 11 | export default class Canvas { 12 | constructor ({ template }) { 13 | this.template = template 14 | 15 | this.x = { 16 | start: 0, 17 | distance: 0, 18 | end: 0 19 | } 20 | 21 | this.y = { 22 | start: 0, 23 | distance: 0, 24 | end: 0 25 | } 26 | 27 | this.createRenderer() 28 | this.createCamera() 29 | this.createScene() 30 | 31 | this.onResize() 32 | } 33 | 34 | createRenderer () { 35 | this.renderer = new Renderer({ 36 | alpha: true, 37 | antialias: true 38 | }) 39 | 40 | this.gl = this.renderer.gl 41 | 42 | document.body.appendChild(this.gl.canvas) 43 | } 44 | 45 | createCamera () { 46 | this.camera = new Camera(this.gl) 47 | this.camera.position.z = 5 48 | } 49 | 50 | createScene () { 51 | this.scene = new Transform() 52 | } 53 | 54 | /** 55 | * Home. 56 | */ 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 | /** 73 | * About. 74 | */ 75 | createAbout () { 76 | this.about = new About({ 77 | gl: this.gl, 78 | scene: this.scene, 79 | sizes: this.sizes 80 | }) 81 | } 82 | 83 | destroyAbout () { 84 | if (!this.about) return 85 | 86 | this.about.destroy() 87 | this.about = null 88 | } 89 | 90 | /** 91 | * Collections. 92 | */ 93 | createCollections () { 94 | this.collections = new Collections({ 95 | gl: this.gl, 96 | scene: this.scene, 97 | sizes: this.sizes, 98 | transition: this.transition 99 | }) 100 | } 101 | 102 | destroyCollections () { 103 | if (!this.collections) return 104 | 105 | this.collections.destroy() 106 | this.collections = null 107 | } 108 | 109 | /** 110 | * Detail. 111 | */ 112 | createDetail () { 113 | this.detail = new Detail({ 114 | gl: this.gl, 115 | scene: this.scene, 116 | sizes: this.sizes, 117 | transition: this.transition 118 | }) 119 | } 120 | 121 | destroyDetail () { 122 | if (!this.detail) return 123 | 124 | this.detail.destroy() 125 | this.detail = null 126 | } 127 | 128 | /** 129 | * Events. 130 | */ 131 | onPreloaded () { 132 | this.onChangeEnd(this.template) 133 | } 134 | 135 | onChangeStart (template, url) { 136 | if (this.about) { 137 | this.about.hide() 138 | } 139 | 140 | if (this.collections) { 141 | this.collections.hide() 142 | } 143 | 144 | if (this.detail) { 145 | this.detail.hide() 146 | } 147 | 148 | if (this.home) { 149 | this.home.hide() 150 | } 151 | 152 | this.isFromCollectionsToDetail = this.template === 'collections' && url.indexOf('detail') > -1 153 | this.isFromDetailToCollections = this.template === 'detail' && url.indexOf('collections') > -1 154 | 155 | if (this.isFromCollectionsToDetail || this.isFromDetailToCollections) { 156 | this.transition = new Transition({ 157 | gl: this.gl, 158 | scene: this.scene, 159 | sizes: this.sizes, 160 | url 161 | }) 162 | 163 | this.transition.setElement(this.collections || this.detail) 164 | } 165 | } 166 | 167 | onChangeEnd (template) { 168 | if (template === 'about') { 169 | this.createAbout() 170 | } else if (this.about) { 171 | this.destroyAbout() 172 | } 173 | 174 | if (template === 'collections') { 175 | this.createCollections() 176 | } else if (this.collections) { 177 | this.destroyCollections() 178 | } 179 | 180 | if (template === 'detail') { 181 | this.createDetail() 182 | } else if (this.detail) { 183 | this.destroyDetail() 184 | } 185 | 186 | if (template === 'home') { 187 | this.createHome() 188 | } else { 189 | this.destroyHome() 190 | } 191 | 192 | this.template = template 193 | } 194 | 195 | onResize () { 196 | this.renderer.setSize(window.innerWidth, window.innerHeight) 197 | 198 | this.camera.perspective({ 199 | aspect: window.innerWidth / window.innerHeight 200 | }) 201 | 202 | const fov = this.camera.fov * (Math.PI / 180) 203 | const height = 2 * Math.tan(fov / 2) * this.camera.position.z 204 | const width = height * this.camera.aspect 205 | 206 | this.sizes = { 207 | height, 208 | width 209 | } 210 | 211 | const values = { 212 | sizes: this.sizes 213 | } 214 | 215 | if (this.about) { 216 | this.about.onResize(values) 217 | } 218 | 219 | if (this.collections) { 220 | this.collections.onResize(values) 221 | } 222 | 223 | if (this.detail) { 224 | this.detail.onResize(values) 225 | } 226 | 227 | if (this.home) { 228 | this.home.onResize(values) 229 | } 230 | } 231 | 232 | onTouchDown (event) { 233 | this.isDown = true 234 | 235 | this.x.start = event.touches ? event.touches[0].clientX : event.clientX 236 | this.y.start = event.touches ? event.touches[0].clientY : event.clientY 237 | 238 | const values = { 239 | x: this.x, 240 | y: this.y, 241 | } 242 | 243 | if (this.about) { 244 | this.about.onTouchDown(values) 245 | } 246 | 247 | if (this.collections) { 248 | this.collections.onTouchDown(values) 249 | } 250 | 251 | if (this.detail) { 252 | this.detail.onTouchDown(values) 253 | } 254 | 255 | if (this.home) { 256 | this.home.onTouchDown(values) 257 | } 258 | } 259 | 260 | onTouchMove (event) { 261 | if (!this.isDown) return 262 | 263 | const x = event.touches ? event.touches[0].clientX : event.clientX 264 | const y = event.touches ? event.touches[0].clientY : event.clientY 265 | 266 | this.x.end = x 267 | this.y.end = y 268 | 269 | const values = { 270 | x: this.x, 271 | y: this.y, 272 | } 273 | 274 | if (this.about) { 275 | this.about.onTouchMove(values) 276 | } 277 | 278 | if (this.collections) { 279 | this.collections.onTouchMove(values) 280 | } 281 | 282 | if (this.detail) { 283 | this.detail.onTouchMove(values) 284 | } 285 | 286 | if (this.home) { 287 | this.home.onTouchMove(values) 288 | } 289 | } 290 | 291 | onTouchUp (event) { 292 | this.isDown = false 293 | 294 | const x = event.changedTouches ? event.changedTouches[0].clientX : event.clientX 295 | const y = event.changedTouches ? event.changedTouches[0].clientY : event.clientY 296 | 297 | this.x.end = x 298 | this.y.end = y 299 | 300 | const values = { 301 | x: this.x, 302 | y: this.y, 303 | } 304 | 305 | if (this.about) { 306 | this.about.onTouchUp(values) 307 | } 308 | 309 | if (this.collections) { 310 | this.collections.onTouchUp(values) 311 | } 312 | 313 | if (this.detail) { 314 | this.detail.onTouchUp(values) 315 | } 316 | 317 | if (this.home) { 318 | this.home.onTouchUp(values) 319 | } 320 | } 321 | 322 | onWheel (event) { 323 | if (this.collections) { 324 | this.collections.onWheel(event) 325 | } 326 | 327 | if (this.home) { 328 | this.home.onWheel(event) 329 | } 330 | } 331 | 332 | /** 333 | * Loop. 334 | */ 335 | update (scroll) { 336 | if (this.about) { 337 | this.about.update(scroll) 338 | } 339 | 340 | if (this.collections) { 341 | this.collections.update() 342 | } 343 | 344 | if (this.detail) { 345 | this.detail.update() 346 | } 347 | 348 | if (this.home) { 349 | this.home.update() 350 | } 351 | 352 | this.renderer.render({ 353 | camera: this.camera, 354 | scene: this.scene 355 | }) 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /shared/logo.svg: -------------------------------------------------------------------------------- 1 | Recurso 1logo 2 | --------------------------------------------------------------------------------