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