├── .npmignore ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ └── lighthouse-on-vercel-preview-url.yml ├── website ├── components │ ├── image │ │ ├── image.module.scss │ │ └── index.js │ ├── webgl │ │ └── particles │ │ │ ├── fragment.glsl │ │ │ └── vertex.glsl │ ├── grid-debugger │ │ ├── docs.md │ │ ├── grid-debugger.module.scss │ │ └── index.js │ ├── header │ │ ├── index.js │ │ └── header.module.scss │ ├── page-transition │ │ ├── page-transition.module.scss │ │ └── index.js │ ├── navigation │ │ ├── navigation.module.scss │ │ └── index.js │ ├── appear-title │ │ ├── appear-title.module.scss │ │ └── index.js │ ├── sticky │ │ ├── docs.md │ │ └── index.js │ ├── parallax │ │ ├── docs.md │ │ └── index.js │ ├── scrollbar │ │ ├── scrollbar.module.scss │ │ └── index.js │ ├── card │ │ ├── index.js │ │ └── card.module.scss │ ├── isomorphic │ │ └── index.js │ ├── stats │ │ └── index.js │ ├── marquee │ │ ├── index.js │ │ └── marquee.module.scss │ ├── cursor │ │ ├── cursor.module.scss │ │ └── index.js │ ├── horizontal-slides │ │ ├── horizontal-slides.module.scss │ │ └── index.js │ ├── list-item │ │ ├── index.js │ │ └── list-item.module.scss │ ├── real-viewport │ │ └── index.js │ ├── intro │ │ └── intro.module.scss │ ├── button │ │ ├── index.js │ │ └── button.module.scss │ ├── link │ │ └── index.js │ ├── feature-cards │ │ ├── feature-cards.module.scss │ │ └── index.js │ ├── footer │ │ ├── footer.module.scss │ │ └── index.js │ └── custom-head │ │ └── index.js ├── public │ ├── favicon.ico │ ├── models │ │ ├── arm.glb │ │ └── arm2.glb │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-70x70.png │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── apple-touch-icon.png │ ├── fonts │ │ ├── Slussen-Bold.woff2 │ │ ├── Slussen-Medium.woff2 │ │ ├── Slussen-Regular.woff2 │ │ ├── Slussen-Semibold.woff2 │ │ ├── Slussen-Expanded-Black.woff2 │ │ └── Slussen-Compressed-Black.woff2 │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── robots.txt │ ├── sitemap.xml │ ├── manifest.json │ ├── site.webmanifest │ ├── sitemap-0.xml │ └── safari-pinned-tab.svg ├── .prettierrc ├── next-sitemap.config.js ├── pages │ ├── index.js │ ├── docs │ │ ├── docs.module.scss │ │ └── index.js │ ├── _document.js │ ├── _app.js │ └── home │ │ └── home.module.scss ├── icons │ ├── arrow-buttons.svg │ ├── arrow-diagonal.svg │ ├── github.svg │ ├── enter-lenis.svg │ └── sfdr.svg ├── styles │ ├── _variables.scss │ ├── _scroll.scss │ ├── _colors.scss │ ├── _spacers.scss │ ├── _utils.scss │ ├── _themes.scss │ ├── _functions.scss │ ├── _easings.scss │ ├── _fonts.scss │ ├── _reset.scss │ ├── global.scss │ ├── _layout.scss │ └── _font-style.scss ├── lib │ ├── analytics.js │ ├── slugify.js │ ├── maths.js │ └── store.js ├── .eslintrc.json ├── layouts │ └── default │ │ ├── layout.module.scss │ │ └── index.js ├── hooks │ └── use-scroll.js ├── jsconfig.json ├── content │ └── projects.js ├── package.json ├── README.md └── next.config.js ├── .npmrc ├── .prettierrc ├── dist ├── types │ ├── debounce.d.ts │ ├── nanoevents.d.ts │ ├── maths.d.ts │ ├── animate.d.ts │ ├── dimensions.d.ts │ ├── virtual-scroll.d.ts │ └── index.d.ts ├── lenis.modern.mjs ├── lenis.js ├── lenis.mjs └── lenis.umd.js ├── .husky └── pre-commit ├── src ├── debounce.js ├── nanoevents.js ├── maths.js ├── observed-element.js ├── dimensions.js ├── animate.js └── virtual-scroll.js ├── .vscode └── settings.json ├── .gitignore ├── package.json └── bundled └── lenis.min.js /.npmignore: -------------------------------------------------------------------------------- 1 | website -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @studio-freight/devs 2 | -------------------------------------------------------------------------------- /website/components/image/image.module.scss: -------------------------------------------------------------------------------- 1 | .image { 2 | } 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/favicon.ico -------------------------------------------------------------------------------- /dist/types/debounce.d.ts: -------------------------------------------------------------------------------- 1 | export function debounce(callback: any, delay: any): (...args: any[]) => void; 2 | -------------------------------------------------------------------------------- /website/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /website/public/models/arm.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/models/arm.glb -------------------------------------------------------------------------------- /website/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/favicon-16x16.png -------------------------------------------------------------------------------- /website/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/favicon-32x32.png -------------------------------------------------------------------------------- /website/public/models/arm2.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/models/arm2.glb -------------------------------------------------------------------------------- /website/public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/mstile-70x70.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | cd website && npx lint-staged 5 | -------------------------------------------------------------------------------- /website/public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/mstile-144x144.png -------------------------------------------------------------------------------- /website/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/mstile-150x150.png -------------------------------------------------------------------------------- /website/public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/mstile-310x150.png -------------------------------------------------------------------------------- /website/public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/mstile-310x310.png -------------------------------------------------------------------------------- /website/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/apple-touch-icon.png -------------------------------------------------------------------------------- /website/public/fonts/Slussen-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/fonts/Slussen-Bold.woff2 -------------------------------------------------------------------------------- /website/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /website/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /website/public/fonts/Slussen-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/fonts/Slussen-Medium.woff2 -------------------------------------------------------------------------------- /website/public/fonts/Slussen-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/fonts/Slussen-Regular.woff2 -------------------------------------------------------------------------------- /website/public/fonts/Slussen-Semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/fonts/Slussen-Semibold.woff2 -------------------------------------------------------------------------------- /website/public/fonts/Slussen-Expanded-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/fonts/Slussen-Expanded-Black.woff2 -------------------------------------------------------------------------------- /website/public/fonts/Slussen-Compressed-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixkaito/lenis/main/website/public/fonts/Slussen-Compressed-Black.woff2 -------------------------------------------------------------------------------- /website/next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteUrl: process.env.WEBSITE_URL || 'https://lenis.studiofreight.com', 3 | generateRobotsTxt: true, // (optional) 4 | } 5 | -------------------------------------------------------------------------------- /website/pages/index.js: -------------------------------------------------------------------------------- 1 | import home, { getStaticProps as homeGetStaticProps } from './home' 2 | 3 | export const getStaticProps = homeGetStaticProps 4 | 5 | export default home 6 | -------------------------------------------------------------------------------- /dist/types/nanoevents.d.ts: -------------------------------------------------------------------------------- 1 | export function createNanoEvents(): { 2 | events: {}; 3 | emit(event: any, ...args: any[]): void; 4 | on(event: any, cb: any): () => void; 5 | }; 6 | -------------------------------------------------------------------------------- /website/icons/arrow-buttons.svg: -------------------------------------------------------------------------------- 1 | Diagonal Arrow -------------------------------------------------------------------------------- /website/icons/arrow-diagonal.svg: -------------------------------------------------------------------------------- 1 | Diagonal Arrow -------------------------------------------------------------------------------- /website/public/robots.txt: -------------------------------------------------------------------------------- 1 | # * 2 | User-agent: * 3 | Allow: / 4 | 5 | # Host 6 | Host: https://lenis.studiofreight.com 7 | 8 | # Sitemaps 9 | Sitemap: https://lenis.studiofreight.com/sitemap.xml 10 | -------------------------------------------------------------------------------- /website/public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://lenis.studiofreight.com/sitemap-0.xml 4 | -------------------------------------------------------------------------------- /website/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | /* Breakpoints */ 2 | $mobile-breakpoint: 800px; 3 | 4 | // Viewport Sizes 5 | $desktop-width: 1440px; 6 | $desktop-height: 850px; 7 | 8 | $mobile-width: 375px; 9 | $mobile-height: 650px; 10 | -------------------------------------------------------------------------------- /website/components/image/index.js: -------------------------------------------------------------------------------- 1 | import cn from 'clsx' 2 | import NextImage from 'next/future/image' 3 | import s from './image.module.scss' 4 | 5 | export function Image({ className, ...props }) { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /website/components/webgl/particles/fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform float uTime; 2 | uniform vec3 uColor; 3 | 4 | void main() { 5 | float distanceToCenter = distance(gl_PointCoord, vec2(0.5)); 6 | float strength = 0.05 / distanceToCenter - 0.1; 7 | 8 | gl_FragColor = vec4(uColor,strength); 9 | } -------------------------------------------------------------------------------- /src/debounce.js: -------------------------------------------------------------------------------- 1 | export function debounce(callback, delay) { 2 | let timer 3 | return function () { 4 | let args = arguments 5 | let context = this 6 | clearTimeout(timer) 7 | timer = setTimeout(function () { 8 | callback.apply(context, args) 9 | }, delay) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /website/lib/analytics.js: -------------------------------------------------------------------------------- 1 | export const GTM_ID = 2 | process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID || 'G-XXZ273XT00' 3 | export const GA_ID = process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS || '' 4 | 5 | export const pageview = (url) => { 6 | window.dataLayer.push({ 7 | event: 'pageview', 8 | page: url, 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /dist/types/maths.d.ts: -------------------------------------------------------------------------------- 1 | export function clamp(min: any, input: any, max: any): number; 2 | export function truncate(value: any, decimals?: number): number; 3 | export function lerp(x: any, y: any, t: any): number; 4 | export function damp(x: any, y: any, lambda: any, dt: any): number; 5 | export function modulo(n: any, d: any): number; 6 | -------------------------------------------------------------------------------- /website/pages/docs/docs.module.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | height: 100vh; 3 | width: 100%; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | 8 | .wrapper { 9 | height: 33vw; 10 | width: 33vw; 11 | overflow-y: auto; 12 | } 13 | 14 | .content { 15 | font-size: 1.4vw; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /website/components/grid-debugger/docs.md: -------------------------------------------------------------------------------- 1 | # Grid Debugger 2 | 3 | ## Usage 4 | 5 | Make sure the grid is added in `` component, in order to match the grid to the design you need to add `` elements accordingly, and then edit the css grid and spacing. 6 | 7 | ## Example 8 | 9 | ```javascript 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /website/components/header/index.js: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react' 2 | import s from './header.module.scss' 3 | 4 | export const Header = forwardRef((_, ref) => { 5 | return ( 6 |
7 |

hi

8 |
9 | ) 10 | }) 11 | 12 | Header.displayName = 'Header' 13 | -------------------------------------------------------------------------------- /website/styles/_scroll.scss: -------------------------------------------------------------------------------- 1 | html { 2 | overflow: overlay; 3 | // &:not(.dev) { 4 | // scrollbar-width: none !important; 5 | 6 | // body { 7 | // -ms-overflow-style: none; 8 | // } 9 | // body::-webkit-scrollbar { 10 | // width: 0 !important; 11 | // height: 0 !important; 12 | // } 13 | // } 14 | } 15 | -------------------------------------------------------------------------------- /website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "prettier"], 3 | "ignorePatterns": ["**/public/*.js"], 4 | "rules": { 5 | "no-unused-vars": "error", 6 | "react/no-unknown-property": "off", 7 | "react/no-unescaped-entities": "off", 8 | "@next/next/no-img-element": "off", 9 | "react-hooks/exhaustive-deps": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /website/layouts/default/layout.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/_functions'; 2 | 3 | .layout { 4 | background-color: var(--theme-primary); 5 | color: var(--theme-secondary); 6 | min-height: 100vh; 7 | display: flex; 8 | flex-direction: column; 9 | 10 | .main { 11 | // if content is empty, footer will remains sticky to bottom 12 | flex-grow: 1; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/components/page-transition/page-transition.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/_functions'; 2 | 3 | .transition { 4 | height: 100vh; 5 | width: 100%; 6 | position: fixed; 7 | z-index: 1; 8 | top: 0; 9 | left: 0; 10 | z-index: 1000; 11 | background-color: var(--theme-contrast); 12 | transform: translate(-100%, 0); 13 | } 14 | 15 | .page-wrapper { 16 | position: relative; 17 | z-index: 2; 18 | } 19 | -------------------------------------------------------------------------------- /website/components/header/header.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/_functions'; 2 | 3 | .header { 4 | position: sticky; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | z-index: 100; 9 | padding: mobile-vw(20px) 0; 10 | 11 | @include desktop { 12 | padding: desktop-vw(40px) 0; 13 | } 14 | 15 | .head { 16 | display: flex; 17 | justify-content: space-between; 18 | position: relative; 19 | z-index: 2; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /website/hooks/use-scroll.js: -------------------------------------------------------------------------------- 1 | import { useStore } from 'lib/store' 2 | import { useEffect } from 'react' 3 | 4 | export function useScroll(callback, deps = []) { 5 | const lenis = useStore(({ lenis }) => lenis) 6 | 7 | useEffect(() => { 8 | if (!lenis) return 9 | lenis.on('scroll', callback) 10 | lenis.emit() 11 | 12 | return () => { 13 | lenis.off('scroll', callback) 14 | } 15 | }, [lenis, callback, [...deps]]) 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Try to reproduce your issue by forking this [codepen](https://codepen.io/ClementRoche/pen/VwxgZEP). 15 | If you can't reproduce it, it means there is something wrong on your initial environment. 16 | -------------------------------------------------------------------------------- /website/components/navigation/navigation.module.scss: -------------------------------------------------------------------------------- 1 | .navigation { 2 | height: 0; 3 | background-color: green; 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | transition: 1s height var(--ease-out-expo); 9 | height: 100vh; 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: center; 13 | 14 | &.closed { 15 | pointer-events: none; 16 | height: 0; 17 | 18 | > * { 19 | opacity: 0; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /website/components/appear-title/appear-title.module.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | .line { 3 | display: inline-block; 4 | overflow: hidden; 5 | 6 | > * { 7 | display: inline-block; 8 | } 9 | } 10 | 11 | &.visible { 12 | .line > * { 13 | transition: 1.2s var(--ease-out-expo) transform; 14 | transition-delay: calc(200ms * var(--i)); 15 | } 16 | } 17 | 18 | &:not(.visible) { 19 | .line > * { 20 | transform: translateY(100%); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /website/components/grid-debugger/grid-debugger.module.scss: -------------------------------------------------------------------------------- 1 | .grid { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100vh; 7 | pointer-events: none; 8 | z-index: 10000; 9 | 10 | button { 11 | pointer-events: all; 12 | font-size: 50px; 13 | right: 0; 14 | position: absolute; 15 | } 16 | } 17 | 18 | .debugger { 19 | position: absolute; 20 | inset: 0; 21 | 22 | span { 23 | background: pink; 24 | opacity: 0.3; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /website/components/sticky/docs.md: -------------------------------------------------------------------------------- 1 | Sticky uses GSAP ScrollTrigger under the hood. 2 | docs: https://greensock.com/docs/v3/Plugins/ScrollTrigger 3 | 4 | start: pixel distance to viewport top. 5 | end: pixel distance to parent element bottom. 6 | target: element to be sticky, direct parent by default. 7 | pinType: 'fixed' or 'transform'. 'fixed' by default. 8 | 9 | ```javascript 10 | import { Sticky } from 'components/sticky' 11 | ; 12 |
13 |
14 | ``` 15 | -------------------------------------------------------------------------------- /website/components/parallax/docs.md: -------------------------------------------------------------------------------- 1 | Parallax uses GSAP ScrollTrigger under the hood. 2 | 3 | speed: parallax speed relative to viewport width. 4 | position: use 'top' if element is visible on first screen of your page. 5 | 6 | ```javascript 7 | import dynamic from 'next/dynamic' 8 | const Parallax = dynamic( 9 | () => import('components/parallax').then((mod) => mod.Parallax), 10 | { ssr: false } 11 | ) 12 | 13 | return ( 14 | 15 |
16 |
17 | ) 18 | ``` 19 | -------------------------------------------------------------------------------- /website/components/scrollbar/scrollbar.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/_functions'; 2 | 3 | // hide native scrollbar 4 | 5 | .scrollbar { 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | z-index: 100; 10 | width: 100%; 11 | padding: 0; 12 | 13 | @media (hover: none) { 14 | display: none; 15 | } 16 | 17 | .inner { 18 | width: 100%; 19 | height: desktop-vw(4px); 20 | position: relative; 21 | background-color: var(--pink); 22 | transform: scaleX(0); 23 | transform-origin: 0 50%; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.format.enable": true, 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.organizeImports": true, 6 | "source.fixAll.eslint": true 7 | }, 8 | "files.associations": { 9 | "*.js": "javascriptreact" 10 | }, 11 | "prettier.enable": true, 12 | "editor.defaultFormatter": "esbenp.prettier-vscode", 13 | "eslint.workingDirectories": ["./website"], 14 | "[markdown]": { 15 | "editor.defaultFormatter": "darkriszty.markdown-table-prettify" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dist/types/animate.d.ts: -------------------------------------------------------------------------------- 1 | export class Animate { 2 | advance(deltaTime: any): void; 3 | value: any; 4 | stop(): void; 5 | isRunning: boolean; 6 | fromTo(from: any, to: any, { lerp, duration, easing, onUpdate }: { 7 | lerp?: number; 8 | duration?: number; 9 | easing?: (t: any) => any; 10 | onUpdate: any; 11 | }): void; 12 | from: any; 13 | to: any; 14 | lerp: number; 15 | duration: number; 16 | easing: (t: any) => any; 17 | currentTime: number; 18 | onUpdate: any; 19 | } 20 | -------------------------------------------------------------------------------- /dist/types/dimensions.d.ts: -------------------------------------------------------------------------------- 1 | export class Dimensions { 2 | constructor(wrapper: any, content: any); 3 | wrapper: any; 4 | content: any; 5 | wrapperResizeObserver: ResizeObserver; 6 | contentResizeObserver: ResizeObserver; 7 | onWindowResize: () => void; 8 | width: any; 9 | height: any; 10 | destroy(): void; 11 | onWrapperResize: () => void; 12 | onContentResize: () => void; 13 | scrollHeight: any; 14 | scrollWidth: any; 15 | get limit(): { 16 | x: number; 17 | y: number; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /website/lib/slugify.js: -------------------------------------------------------------------------------- 1 | export function slugify(text) { 2 | return text 3 | .toString() // Cast to string (optional) 4 | .normalize('NFKD') // The normalize() using NFKD method returns the Unicode Normalization Form of a given string. 5 | .toLowerCase() // Convert the string to lowercase letters 6 | .trim() // Remove whitespace from both sides of a string (optional) 7 | .replace(/\s+/g, '-') // Replace spaces with - 8 | .replace(/[^\w\-]+/g, '') // Remove all non-word chars 9 | .replace(/\-\-+/g, '-') // Replace multiple - with single - 10 | } 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "github-actions" # See documentation for possible values 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /website/styles/_colors.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | 3 | // Each color filled here will create a css variable exposed globally 4 | 5 | $colors: ( 6 | 'white': rgb(239, 239, 239), 7 | 'grey': rgb(176, 176, 176), 8 | 'black': rgb(0, 0, 0), 9 | 'pink': rgb(255, 152, 162), 10 | ); 11 | 12 | :root { 13 | @each $name, $color in $colors { 14 | --#{$name}: #{$color}; 15 | // for safari use case: https://ambientimpact.com/web/snippets/safari-bug-with-gradients-that-fade-to-transparent 16 | --#{$name}-transparent: #{color.change($color, $alpha: 0)}; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /website/lib/maths.js: -------------------------------------------------------------------------------- 1 | function clamp(min, input, max) { 2 | return Math.max(min, Math.min(input, max)) 3 | } 4 | 5 | function mapRange(in_min, in_max, input, out_min, out_max) { 6 | return ((input - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min 7 | } 8 | 9 | function lerp(x, y, t) { 10 | return (1 - t) * x + t * y 11 | } 12 | 13 | function truncate(value, decimals) { 14 | return parseFloat(value.toFixed(decimals)) 15 | } 16 | 17 | const Maths = { lerp, clamp, mapRange, truncate } 18 | 19 | export { lerp, clamp, mapRange, truncate } 20 | export default Maths 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | .next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | /docs/dist 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | .eslintcache 38 | 39 | -------------------------------------------------------------------------------- /website/components/card/index.js: -------------------------------------------------------------------------------- 1 | import cn from 'clsx' 2 | import s from './card.module.scss' 3 | 4 | export const Card = ({ 5 | number, 6 | text, 7 | className, 8 | inverted, 9 | background = 'rgba(14, 14, 14, 0.15)', 10 | }) => { 11 | return ( 12 |
16 | {number && ( 17 |

{number.toString().padStart(2, '0')}

18 | )} 19 | {text &&

{text}

} 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /website/components/isomorphic/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const ClientOnly = ({ children }) => { 4 | const [isMounted, setIsMounted] = useState(false) 5 | 6 | useEffect(() => setIsMounted(true), []) 7 | 8 | if (!isMounted) { 9 | return null 10 | } 11 | 12 | return children || null 13 | } 14 | 15 | export const ServerOnly = ({ children }) => { 16 | const [isMounted, setIsMounted] = useState(false) 17 | 18 | useEffect(() => setIsMounted(true), []) 19 | 20 | if (isMounted) { 21 | return null 22 | } 23 | 24 | return children || null 25 | } 26 | -------------------------------------------------------------------------------- /website/components/stats/index.js: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@studio-freight/hamo' 2 | import { useEffect, useMemo } from 'react' 3 | import _Stats from 'stats.js' 4 | 5 | export const Stats = () => { 6 | const stats = useMemo(() => new _Stats(), []) 7 | 8 | useEffect(() => { 9 | stats.showPanel(0) // 0: fps, 1: ms, 2: mb, 3+: custom 10 | document.body.appendChild(stats.dom) 11 | 12 | return () => { 13 | stats.dom.remove() 14 | } 15 | }, [stats]) 16 | 17 | useFrame(() => { 18 | stats.begin() 19 | }, -Infinity) 20 | 21 | useFrame(() => { 22 | stats.end() 23 | }, Infinity) 24 | 25 | return null 26 | } 27 | -------------------------------------------------------------------------------- /website/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lenis", 3 | "short_name": "Lenis Smooth Scroll", 4 | "description": "How smooth scroll should be.", 5 | "icons": [ 6 | { 7 | "src": "/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png", 10 | "purpose": "maskable" 11 | }, 12 | { 13 | "src": "/android-chrome-512x512.png", 14 | "sizes": "512x512", 15 | "type": "image/png", 16 | "purpose": "any" 17 | } 18 | ], 19 | "theme_color": "#ff98a2", 20 | "background_color": "#efefef", 21 | "start_url": "/", 22 | "scope": ".", 23 | "display": "standalone", 24 | "orientation": "portrait" 25 | } 26 | -------------------------------------------------------------------------------- /website/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lenis", 3 | "short_name": "Lenis Smooth Scroll", 4 | "description": "How smooth scroll should be.", 5 | "icons": [ 6 | { 7 | "src": "/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png", 10 | "purpose": "maskable" 11 | }, 12 | { 13 | "src": "/android-chrome-512x512.png", 14 | "sizes": "512x512", 15 | "type": "image/png", 16 | "purpose": "any" 17 | } 18 | ], 19 | "theme_color": "#ff98a2", 20 | "background_color": "#efefef", 21 | "start_url": "/", 22 | "scope": ".", 23 | "display": "standalone", 24 | "orientation": "portrait" 25 | } 26 | -------------------------------------------------------------------------------- /website/components/marquee/index.js: -------------------------------------------------------------------------------- 1 | import cn from 'clsx' 2 | import s from './marquee.module.scss' 3 | 4 | const Marquee = ({ 5 | children, 6 | repeat = 2, 7 | duration = 5, 8 | offset = 0, 9 | inverted = false, 10 | className, 11 | }) => { 12 | return ( 13 |
20 | {new Array(repeat).fill(children).map((_, i) => ( 21 |
22 | {children} 23 |
24 | ))} 25 |
26 | ) 27 | } 28 | 29 | export { Marquee } 30 | -------------------------------------------------------------------------------- /website/public/sitemap-0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://lenis.studiofreight.com2023-03-21T15:39:31.818Zdaily0.7 4 | https://lenis.studiofreight.com/home2023-03-21T15:39:31.818Zdaily0.7 5 | -------------------------------------------------------------------------------- /src/nanoevents.js: -------------------------------------------------------------------------------- 1 | export let createNanoEvents = () => ({ 2 | events: {}, 3 | 4 | // Emit an event with the provided arguments 5 | emit(event, ...args) { 6 | let callbacks = this.events[event] || [] 7 | for (let i = 0, length = callbacks.length; i < length; i++) { 8 | callbacks[i](...args) 9 | } 10 | }, 11 | 12 | // Register a callback for the specified event 13 | on(event, cb) { 14 | // Add the callback to the event's callback list, or create a new list with the callback 15 | this.events[event]?.push(cb) || (this.events[event] = [cb]) 16 | 17 | // Return an unsubscribe function 18 | return () => { 19 | this.events[event] = this.events[event]?.filter((i) => cb !== i) 20 | } 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /website/components/cursor/cursor.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/_functions'; 2 | 3 | .container { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | height: 100vh; 8 | width: 100%; 9 | z-index: 10000; 10 | pointer-events: none; 11 | overflow: hidden; 12 | 13 | @media (hover: none) { 14 | display: none; 15 | } 16 | 17 | .cursor { 18 | position: absolute; 19 | transform: translate(-50%, -50%); 20 | border-radius: 100%; 21 | border: mobile-vw(2px) solid var(--pink); 22 | width: 40px; 23 | height: 40px; 24 | opacity: 0.4; 25 | transition: transform 600ms var(--ease-out-expo); 26 | 27 | @include desktop { 28 | border: desktop-vw(2px) solid var(--pink); 29 | } 30 | 31 | &.pointer { 32 | transform: translate(-50%, -50%) scale(0.5); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /website/components/horizontal-slides/horizontal-slides.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/_functions'; 2 | 3 | .wrapper { 4 | .inner { 5 | overflow: hidden; 6 | display: flex; 7 | 8 | .overflow { 9 | display: flex; 10 | 11 | > * { 12 | flex-shrink: 0; 13 | } 14 | } 15 | 16 | @include mobile { 17 | width: mobile-vw(375px); 18 | } 19 | 20 | @include desktop { 21 | position: sticky; 22 | --height: #{desktop-vw(440px)}; 23 | top: calc((100vh - var(--height)) / 2); 24 | } 25 | 26 | @include mobile { 27 | .cards { 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | justify-content: center; 32 | width: 100%; 33 | 34 | > * { 35 | margin-bottom: mobile-vw(32px); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /website/components/marquee/marquee.module.scss: -------------------------------------------------------------------------------- 1 | .marquee { 2 | display: flex; 3 | overflow: hidden; 4 | will-change: transform; 5 | 6 | .inner { 7 | display: flex; 8 | white-space: nowrap; 9 | animation: marquee var(--duration) linear infinite; 10 | } 11 | 12 | &.inverted { 13 | .inner { 14 | animation: marquee-inverted var(--duration) linear infinite; 15 | } 16 | } 17 | 18 | @keyframes marquee { 19 | 0% { 20 | transform: translate3d(calc(var(--offset) * -1), 0, 0); 21 | } 22 | 23 | 100% { 24 | transform: translate3d(calc(-100% - var(--offset)), 0, 0); 25 | } 26 | } 27 | 28 | @keyframes marquee-inverted { 29 | 0% { 30 | transform: translate3d(calc(-100% - var(--offset)), 0, 0); 31 | } 32 | 33 | 100% { 34 | transform: translate3d(calc(var(--offset) * -1), 0, 0); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /website/components/list-item/index.js: -------------------------------------------------------------------------------- 1 | import cn from 'clsx' 2 | import { Link } from 'components/link' 3 | import dynamic from 'next/dynamic' 4 | import s from './list-item.module.scss' 5 | 6 | const Arrow = dynamic(() => import('icons/arrow-diagonal.svg'), { ssr: false }) 7 | 8 | export const ListItem = ({ 9 | className, 10 | title, 11 | source, 12 | href, 13 | visible, 14 | index, 15 | }) => { 16 | return ( 17 | 22 |
23 |
24 | {title} 25 | 26 |
27 |
28 | {source} 29 |
30 |
31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/maths.js: -------------------------------------------------------------------------------- 1 | // Clamp a value between a minimum and maximum value 2 | export function clamp(min, input, max) { 3 | return Math.max(min, Math.min(input, max)) 4 | } 5 | 6 | // Truncate a floating-point number to a specified number of decimal places 7 | export function truncate(value, decimals = 0) { 8 | return parseFloat(value.toFixed(decimals)) 9 | } 10 | 11 | // Linearly interpolate between two values using an amount (0 <= t <= 1) 12 | export function lerp(x, y, t) { 13 | return (1 - t) * x + t * y 14 | } 15 | 16 | // http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/ 17 | export function damp(x, y, lambda, dt) { 18 | return lerp(x, y, 1 - Math.exp(-lambda * dt)) 19 | } 20 | 21 | // Calculate the modulo of the dividend and divisor while keeping the result within the same sign as the divisor 22 | // https://anguscroll.com/just/just-modulo 23 | export function modulo(n, d) { 24 | return ((n % d) + d) % d 25 | } 26 | -------------------------------------------------------------------------------- /website/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "jsx": "preserve", 5 | "target": "esnext", 6 | "module": "esnext", 7 | "lib": ["dom", "es2017"], 8 | "noEmit": true, 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictBindCallApply": true, 16 | "strictPropertyInitialization": true, 17 | "noImplicitThis": true, 18 | "alwaysStrict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "skipLibCheck": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "esModuleInterop": true, 25 | "resolveJsonModule": true, 26 | "isolatedModules": true 27 | }, 28 | "exclude": ["node_modules", "dist"], 29 | "include": ["**/*.js"] 30 | } 31 | -------------------------------------------------------------------------------- /website/styles/_spacers.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | // css custom properties exposed globally: 4 | // --spacer-{name}: creates spacer variables based on the grid and multipiers 5 | 6 | // config: 7 | $grid-base: 8px; 8 | $spacer-multipliers: ( 9 | xl: ( 10 | 24, 11 | 10, 12 | ), 13 | lg: ( 14 | 16, 15 | 8, 16 | ), 17 | md: ( 18 | 10, 19 | 6, 20 | ), 21 | sm: ( 22 | 8, 23 | 4, 24 | ), 25 | xs: ( 26 | 6, 27 | 4, 28 | ), 29 | ); 30 | 31 | // internal process, do not touch 32 | :root { 33 | @each $name, $value in $spacer-multipliers { 34 | --spacer-#{$name}: #{mobile-vw( 35 | (nth(map.get($spacer-multipliers, $name), 2) * $grid-base) 36 | )}; 37 | } 38 | @include desktop { 39 | @each $name, $value in $spacer-multipliers { 40 | --spacer-#{$name}: #{desktop-vw( 41 | (nth(map.get($spacer-multipliers, $name), 1) * $grid-base) 42 | )}; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /website/lib/store.js: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | export const useStore = create((set, get) => ({ 4 | headerData: undefined, 5 | setHeaderData: (headerData) => set({ headerData }), 6 | footerData: undefined, 7 | setFooterData: (footerData) => set({ footerData }), 8 | navIsOpen: false, 9 | setNavIsOpen: (toggle) => set({ navIsOpen: toggle, overflow: !toggle }), 10 | lenis: undefined, 11 | setLenis: (lenis) => set({ lenis }), 12 | overflow: true, 13 | setOverflow: (overflow) => set({ overflow }), 14 | triggerTransition: '', 15 | setTriggerTransition: (triggerTransition) => set({ triggerTransition }), 16 | thresholds: {}, 17 | addThreshold: ({ id, value }) => { 18 | let thresholds = { ...get().thresholds } 19 | thresholds[id] = value 20 | 21 | set({ thresholds }) 22 | }, 23 | // removeThreshold: (threshold) => { 24 | // set({ threshold }) 25 | // }, 26 | introOut: false, 27 | setIntroOut: (introOut) => set({ introOut }), 28 | })) 29 | -------------------------------------------------------------------------------- /website/components/real-viewport/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | export const RealViewport = () => { 4 | useEffect(() => { 5 | //https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ 6 | function onWindowResize() { 7 | document.documentElement.style.setProperty( 8 | '--vh', 9 | window.innerHeight * 0.01 + 'px' 10 | ) 11 | 12 | document.documentElement.style.setProperty( 13 | '--dvh', 14 | window.innerHeight * 0.01 + 'px' 15 | ) 16 | 17 | document.documentElement.style.setProperty( 18 | '--svh', 19 | document.documentElement.clientHeight * 0.01 + 'px' 20 | ) 21 | 22 | document.documentElement.style.setProperty('--lvh', '1vh') 23 | } 24 | 25 | window.addEventListener('resize', onWindowResize, false) 26 | onWindowResize() 27 | 28 | return () => { 29 | window.removeEventListener('resize', onWindowResize, false) 30 | } 31 | }, []) 32 | 33 | return null 34 | } 35 | -------------------------------------------------------------------------------- /dist/types/virtual-scroll.d.ts: -------------------------------------------------------------------------------- 1 | export class VirtualScroll { 2 | constructor(element: any, { wheelMultiplier, touchMultiplier, normalizeWheel }: { 3 | wheelMultiplier?: number; 4 | touchMultiplier?: number; 5 | normalizeWheel?: boolean; 6 | }); 7 | element: any; 8 | wheelMultiplier: number; 9 | touchMultiplier: number; 10 | normalizeWheel: boolean; 11 | touchStart: { 12 | x: any; 13 | y: any; 14 | }; 15 | emitter: { 16 | events: {}; 17 | emit(event: any, ...args: any[]): void; 18 | on(event: any, cb: any): () => void; 19 | }; 20 | on(event: any, callback: any): () => void; 21 | destroy(): void; 22 | onTouchStart: (event: any) => void; 23 | lastDelta: { 24 | x: number; 25 | y: number; 26 | } | { 27 | x: number; 28 | y: number; 29 | }; 30 | onTouchMove: (event: any) => void; 31 | onTouchEnd: (event: any) => void; 32 | onWheel: (event: any) => void; 33 | } 34 | -------------------------------------------------------------------------------- /website/styles/_utils.scss: -------------------------------------------------------------------------------- 1 | .aspect-ratio { 2 | position: relative; 3 | padding-bottom: calc(100% / var(--aspect-ratio)); 4 | width: 100%; 5 | height: 0; 6 | 7 | > :first-child { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 100%; 13 | } 14 | } 15 | 16 | .full-width { 17 | width: 100vw; 18 | position: relative; 19 | left: 50%; 20 | right: 50%; 21 | margin-left: -50vw; 22 | margin-right: -50vw; 23 | } 24 | 25 | .hidden-overflow { 26 | overflow: hidden; 27 | } 28 | 29 | .relative { 30 | position: relative; 31 | } 32 | 33 | .hide-on-desktop { 34 | @include desktop { 35 | display: none !important; 36 | } 37 | } 38 | 39 | .hide-on-mobile { 40 | @include mobile { 41 | display: none !important; 42 | } 43 | } 44 | 45 | html:not(.has-scroll-smooth) { 46 | .hide-on-native-scroll { 47 | display: none !important; 48 | } 49 | } 50 | 51 | html.has-scroll-smooth { 52 | .hide-on-smooth-scroll { 53 | display: none !important; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /website/components/grid-debugger/index.js: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from '@studio-freight/hamo' 2 | import cn from 'clsx' 3 | import { useMemo, useState } from 'react' 4 | import s from './grid-debugger.module.scss' 5 | 6 | export const GridDebugger = () => { 7 | const [visible, setVisible] = useState(false) 8 | const isMobile = useMediaQuery('(max-width: 800px)') 9 | 10 | const columns = useMemo(() => { 11 | return parseInt( 12 | getComputedStyle(document.documentElement).getPropertyValue( 13 | '--layout-columns-count' 14 | ) 15 | ) 16 | }, [isMobile]) 17 | 18 | return ( 19 |
20 | 27 | {visible && ( 28 |
29 | {new Array(columns).fill(0).map((_, key) => ( 30 | 31 | ))} 32 |
33 | )} 34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /website/components/navigation/index.js: -------------------------------------------------------------------------------- 1 | import cn from 'clsx' 2 | import { Link } from 'components/link' 3 | import { useStore } from 'lib/store' 4 | import { useRouter } from 'next/router' 5 | import { useEffect } from 'react' 6 | import { shallow } from 'zustand/shallow' 7 | import s from './navigation.module.scss' 8 | 9 | export const Navigation = () => { 10 | const [navIsOpen, setNavIsOpen] = useStore( 11 | (state) => [state.navIsOpen, state.setNavIsOpen], 12 | shallow 13 | ) 14 | 15 | const router = useRouter() 16 | 17 | useEffect(() => { 18 | const onRouteChange = () => { 19 | setNavIsOpen(false) 20 | } 21 | 22 | router.events.on('routeChangeStart', onRouteChange) 23 | 24 | return () => { 25 | router.events.off('routeChangeStart', onRouteChange) 26 | } 27 | }, []) 28 | 29 | return ( 30 |
31 | home 32 | gsap 33 | contact 34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /website/styles/_themes.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use 'sass:color'; 3 | 4 | // css classes exposed globally: 5 | // --theme-{name}: applies css variables from theme 6 | 7 | // config: 8 | // you must use colors filled in _colors.scss but any other colors could works 9 | $themes: ( 10 | 'light': ( 11 | 'primary': map.get($colors, 'white'), 12 | 'secondary': map.get($colors, 'black'), 13 | 'contrast': map.get($colors, 'pink'), 14 | ), 15 | 'dark': ( 16 | 'primary': map.get($colors, 'black'), 17 | 'secondary': map.get($colors, 'white'), 18 | 'contrast': map.get($colors, 'pink'), 19 | ), 20 | 'contrast': ( 21 | 'primary': map.get($colors, 'pink'), 22 | 'secondary': map.get($colors, 'black'), 23 | 'contrast': map.get($colors, 'white'), 24 | ), 25 | ); 26 | 27 | // internal process, do not touch 28 | @each $name, $theme in $themes { 29 | .theme-#{$name} { 30 | @each $name, $color in $theme { 31 | --theme-#{$name}: #{$color}; 32 | --theme-#{$name}-transparent: #{color.change($color, $alpha: 0)}; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /website/styles/_functions.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @import './_variables.scss'; 3 | 4 | // Breakpoint 5 | @mixin mobile { 6 | @media (max-width: #{$mobile-breakpoint}) { 7 | @content; 8 | } 9 | } 10 | 11 | @mixin desktop { 12 | @media (min-width: #{$mobile-breakpoint}) { 13 | @content; 14 | } 15 | } 16 | 17 | @function mobile-vw($pixels, $base-vw: $mobile-width) { 18 | @return math.div($pixels * 100vw, $base-vw); 19 | } 20 | 21 | @function mobile-vh($pixels, $base-vh: $mobile-height) { 22 | @return math.div($pixels * 100vh, $base-vh); 23 | } 24 | 25 | @function desktop-vw($pixels, $base-vw: $desktop-width) { 26 | @return math.div($pixels * 100vw, $base-vw); 27 | } 28 | 29 | @function desktop-vh($pixels, $base-vh: $desktop-height) { 30 | @return math.div($pixels * 100vh, $base-vh); 31 | } 32 | 33 | @function columns($columns) { 34 | @return calc( 35 | (#{$columns} * var(--layout-column-width)) + 36 | ((#{$columns} - 1) * var(--layout-columns-gap)) 37 | ); 38 | } 39 | 40 | @mixin hover { 41 | @media (hover: hover) { 42 | @content; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /website/styles/_easings.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53); 3 | --ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19); 4 | --ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22); 5 | --ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06); 6 | --ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035); 7 | --ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335); 8 | --ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94); 9 | --ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1); 10 | --ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1); 11 | --ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1); 12 | --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1); 13 | --ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1); 14 | --ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955); 15 | --ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1); 16 | --ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1); 17 | --ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1); 18 | --ease-in-out-expo: cubic-bezier(1, 0, 0, 1); 19 | --ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86); 20 | } 21 | -------------------------------------------------------------------------------- /website/components/card/card.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/_functions'; 2 | 3 | .wrapper { 4 | display: flex; 5 | justify-content: space-between; 6 | flex-direction: column; 7 | color: var(--theme-secondary); 8 | border: 1px solid; 9 | aspect-ratio: 1 / 1; 10 | padding: mobile-vw(24px); 11 | width: mobile-vw(343px); 12 | background-color: var(--background); 13 | backdrop-filter: blur(5px); 14 | 15 | &.inverted { 16 | color: var(--theme-primary); 17 | background-color: var(--theme-secondary); 18 | } 19 | 20 | @include desktop { 21 | width: columns(4); 22 | padding: desktop-vw(24px); 23 | } 24 | 25 | .number { 26 | color: var(--theme-contrast); 27 | font-stretch: condensed; 28 | font-weight: 900; 29 | line-height: 86%; 30 | letter-spacing: -0.02em; 31 | font-size: mobile-vw(56px); 32 | 33 | @include desktop { 34 | font-size: desktop-vw(96px); 35 | } 36 | } 37 | 38 | .text { 39 | text-transform: uppercase; 40 | font-stretch: expanded; 41 | line-height: 100%; 42 | letter-spacing: -0.01em; 43 | font-size: mobile-vw(20px); 44 | 45 | @include desktop { 46 | font-size: desktop-vw(28px); 47 | } 48 | 49 | span { 50 | font-stretch: normal; 51 | font-weight: 600; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /website/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Slussen'; 3 | src: url('/fonts/Slussen-Compressed-Black.woff2') format('woff2'); 4 | font-display: swap; 5 | font-weight: 900; 6 | font-stretch: compressed; 7 | } 8 | 9 | @font-face { 10 | font-family: 'Slussen'; 11 | src: url('/fonts/Slussen-Expanded-Black.woff2') format('woff2'); 12 | font-display: swap; 13 | font-weight: 900; 14 | font-stretch: expanded; 15 | } 16 | 17 | @font-face { 18 | font-family: 'Slussen'; 19 | src: url('/fonts/Slussen-Medium.woff2') format('woff2'); 20 | font-display: swap; 21 | font-weight: 500; 22 | } 23 | 24 | @font-face { 25 | font-family: 'Slussen'; 26 | src: url('/fonts/Slussen-Regular.woff2') format('woff2'); 27 | font-display: swap; 28 | font-weight: 400; 29 | } 30 | 31 | @font-face { 32 | font-family: 'Slussen'; 33 | src: url('/fonts/Slussen-Semibold.woff2') format('woff2'); 34 | font-display: swap; 35 | font-weight: 600; 36 | } 37 | 38 | @font-face { 39 | font-family: 'Slussen'; 40 | src: url('/fonts/Slussen-Bold.woff2') format('woff2'); 41 | font-display: swap; 42 | font-weight: 700; 43 | } 44 | 45 | @font-face { 46 | font-family: 'Respira'; 47 | src: url('/fonts/Respira-Black.woff2') format('woff2'); 48 | font-display: swap; 49 | font-weight: 900; 50 | } 51 | 52 | :root { 53 | --font-primary: 'Slussen'; 54 | } 55 | -------------------------------------------------------------------------------- /src/observed-element.js: -------------------------------------------------------------------------------- 1 | export class ObservedElement { 2 | constructor(element) { 3 | this.element = element 4 | 5 | // If the element is the window, add a resize event listener and trigger it initially 6 | if (element === window) { 7 | window.addEventListener('resize', this.onWindowResize) 8 | this.onWindowResize() 9 | } else { 10 | // If the element is not the window, observe its size using ResizeObserver 11 | this.width = this.element.offsetWidth 12 | this.height = this.element.offsetHeight 13 | 14 | this.resizeObserver = new ResizeObserver(this.onResize) 15 | this.resizeObserver.observe(this.element) 16 | } 17 | } 18 | 19 | // Clean up event listeners and disconnect the ResizeObserver when destroying the instance 20 | destroy() { 21 | window.removeEventListener('resize', this.onWindowResize) 22 | this.resizeObserver.disconnect() 23 | } 24 | 25 | // Update the width and height properties based on the observed element's size 26 | onResize = ([entry]) => { 27 | if (entry) { 28 | const { width, height } = entry.contentRect 29 | this.width = width 30 | this.height = height 31 | } 32 | } 33 | 34 | // Update the width and height properties based on the window's size 35 | onWindowResize = () => { 36 | this.width = window.innerWidth 37 | this.height = window.innerHeight 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /website/components/parallax/index.js: -------------------------------------------------------------------------------- 1 | import { gsap } from 'gsap' 2 | import { mapRange } from 'lib/maths' 3 | import { useEffect, useRef } from 'react' 4 | import { useWindowSize } from 'react-use' 5 | 6 | export function Parallax({ 7 | className, 8 | children, 9 | speed = 1, 10 | id = 'parallax', 11 | position, 12 | }) { 13 | const trigger = useRef() 14 | const target = useRef() 15 | 16 | const { width: windowWidth } = useWindowSize() 17 | 18 | useEffect(() => { 19 | const y = windowWidth * speed * 0.1 20 | 21 | const setY = gsap.quickSetter(target.current, 'y', 'px') 22 | const set3D = gsap.quickSetter(target.current, 'force3D') 23 | 24 | const timeline = gsap.timeline({ 25 | scrollTrigger: { 26 | id: id, 27 | trigger: trigger.current, 28 | scrub: true, 29 | start: 'top bottom', 30 | end: 'bottom top', 31 | onUpdate: (e) => { 32 | if (position === 'top') { 33 | setY(e.progress * y) 34 | } else { 35 | setY(-mapRange(0, 1, e.progress, -y, y)) 36 | } 37 | 38 | set3D(e.progress > 0 && e.progress < 1) 39 | }, 40 | }, 41 | }) 42 | 43 | return () => { 44 | timeline.kill() 45 | } 46 | }, [id, speed, position, windowWidth]) 47 | 48 | return ( 49 |
50 |
51 | {children} 52 |
53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /website/components/intro/intro.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/_functions'; 2 | 3 | $intro-in: 1500ms; 4 | $intro-out: 1500ms; 5 | 6 | .wrapper { 7 | height: 100vh; 8 | width: 100%; 9 | background-color: var(--pink); 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | z-index: 1000; 14 | overflow: hidden; 15 | 16 | @include mobile { 17 | display: none; 18 | } 19 | 20 | &.out { 21 | transition: transform $intro-out var(--ease-out-expo); 22 | transform: translate3d(0, -100%, 0); 23 | transition-delay: calc($intro-in + 0ms); 24 | } 25 | 26 | .start { 27 | transform: translate3d(0, calc(var(--index) * 5% + 100%), 0); 28 | } 29 | 30 | .relative { 31 | padding: desktop-vw(30px) desktop-vw(32.5px); 32 | 33 | transition: transform $intro-out var(--ease-out-expo); 34 | transform: translate3d(0, desktop-vh(850px), 0); 35 | transition-delay: calc($intro-in + 0ms); 36 | 37 | height: 100%; 38 | display: flex; 39 | flex-direction: column; 40 | justify-content: space-between; 41 | } 42 | 43 | .show { 44 | transition: transform $intro-in var(--ease-out-expo); 45 | transition-delay: calc(var(--index) * 75ms); 46 | transform: translate3d(0, 0, 0); 47 | } 48 | } 49 | 50 | .translate { 51 | @include desktop { 52 | transform: translate3d(0, -100%, 0); 53 | transition: transform $intro-out var(--ease-out-expo); 54 | } 55 | } 56 | 57 | .mobile { 58 | @include mobile { 59 | transform: translate3d(0, -105%, 0); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /website/styles/_reset.scss: -------------------------------------------------------------------------------- 1 | // https://github.com/elad2412/the-new-css-reset/blob/main/css/reset.css 2 | 3 | /*** The new CSS Reset - version 1.2.0 (last updated 23.7.2021) ***/ 4 | 5 | /** 6 | * 1. Correct the line height in all browsers. 7 | * 2. Prevent adjustments of font size after orientation changes in iOS. 8 | * 3. Improve font rendering. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | text-rendering: geometricPrecision; /* 2 */ 15 | -webkit-font-smoothing: antialiased; /* 3 */ 16 | -webkit-font-smoothing: subpixel-antialiased; /* 3 */ 17 | -moz-font-smoothing: antialiased; /* 3 */ 18 | font-smoothing: antialiased; /* 3 */ 19 | -moz-osx-font-smoothing: grayscale; /* 3 */ 20 | } 21 | 22 | /* Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property */ 23 | *:not(iframe, canvas, img, svg, video, svg *) { 24 | all: unset; 25 | display: revert; 26 | } 27 | 28 | /* Preferred box-sizing value */ 29 | *, 30 | *::before, 31 | *::after { 32 | box-sizing: border-box !important; 33 | } 34 | 35 | /* Remove list styles (bullets/numbers) */ 36 | ol, 37 | ul { 38 | list-style: none; 39 | } 40 | 41 | /* For images to not be able to exceed their container */ 42 | img { 43 | max-width: 100%; 44 | } 45 | 46 | /* removes spacing between cells in tables */ 47 | table { 48 | border-collapse: collapse; 49 | } 50 | 51 | /* revert the 'white-space' property for textarea elements on Safari */ 52 | textarea { 53 | white-space: revert; 54 | } 55 | -------------------------------------------------------------------------------- /website/components/button/index.js: -------------------------------------------------------------------------------- 1 | import cn from 'clsx' 2 | import { Link } from 'components/link' 3 | import dynamic from 'next/dynamic' 4 | import s from './button.module.scss' 5 | 6 | const Arrow = dynamic(() => import('icons/arrow-buttons.svg'), { ssr: false }) 7 | 8 | export const Button = ({ 9 | icon, 10 | arrow, 11 | children, 12 | href, 13 | onClick, 14 | className, 15 | style, 16 | }) => { 17 | return href ? ( 18 | 23 | {icon && {icon}} 24 | 25 | 26 | {children} {arrow && } 27 | 28 | 31 | 32 | 33 | ) : ( 34 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /website/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import './_reset.scss'; 2 | @import './_fonts.scss'; 3 | @import './_colors.scss'; 4 | @import './_easings.scss'; 5 | @import './_functions.scss'; 6 | @import './_layout.scss'; 7 | @import './_utils.scss'; 8 | @import './_font-style.scss'; 9 | @import './_themes.scss'; 10 | @import './_scroll.scss'; 11 | 12 | :root { 13 | --header-height: #{mobile-vw(58px)}; 14 | 15 | @include desktop { 16 | --header-height: #{desktop-vw(98px)}; 17 | } 18 | } 19 | 20 | html { 21 | font-weight: bolder; 22 | font-family: var(--font-primary); 23 | scrollbar-width: thin; 24 | 25 | &.nav { 26 | overflow: hidden; 27 | } 28 | } 29 | 30 | html.lenis-stopped { 31 | overflow: hidden; 32 | } 33 | 34 | // html, 35 | // body { 36 | // overscroll-behavior-y: none; 37 | // } 38 | 39 | body { 40 | min-height: 100vh; 41 | } 42 | 43 | // custom cursor 44 | // html.has-custom-cursor { 45 | // &, 46 | // * { 47 | // &, 48 | // &::before, 49 | // &::after { 50 | // cursor: none !important; 51 | // } 52 | // } 53 | // } 54 | 55 | a, 56 | button, 57 | input, 58 | label, 59 | textarea, 60 | select { 61 | color: inherit; 62 | cursor: pointer; 63 | } 64 | 65 | *::selection { 66 | background-color: var(--theme-contrast); 67 | color: var(--theme-primary); 68 | } 69 | 70 | svg.icon { 71 | path[fill], 72 | rect[fill], 73 | circle[fill] { 74 | fill: currentColor; 75 | } 76 | 77 | path[stroke], 78 | rect[stroke], 79 | circle[stroke] { 80 | stroke: currentColor; 81 | } 82 | } 83 | 84 | .intro { 85 | overflow: hidden; 86 | } 87 | -------------------------------------------------------------------------------- /website/components/link/index.js: -------------------------------------------------------------------------------- 1 | import NextLink from 'next/link' 2 | import { forwardRef, useMemo } from 'react' 3 | 4 | const SHALLOW_URLS = ['?demo=true'] 5 | 6 | export const Link = forwardRef( 7 | ({ href, children, className, scroll, shallow, ...props }, ref) => { 8 | const attributes = { 9 | ref, 10 | className, 11 | ...props, 12 | } 13 | 14 | const isProtocol = useMemo( 15 | () => href?.startsWith('mailto:') || href?.startsWith('tel:'), 16 | [href] 17 | ) 18 | 19 | const needsShallow = useMemo( 20 | () => !!SHALLOW_URLS.find((url) => href?.includes(url)), 21 | [href] 22 | ) 23 | 24 | const isAnchor = useMemo(() => href?.startsWith('#'), [href]) 25 | const isExternal = useMemo(() => href?.startsWith('http'), [href]) 26 | 27 | if (typeof href !== 'string') { 28 | return 29 | } 30 | 31 | if (isProtocol || isExternal) { 32 | return ( 33 | 39 | {children} 40 | 41 | ) 42 | } 43 | 44 | return ( 45 | 53 | {children} 54 | 55 | ) 56 | } 57 | ) 58 | 59 | Link.displayName = 'Link' 60 | -------------------------------------------------------------------------------- /website/content/projects.js: -------------------------------------------------------------------------------- 1 | export const projects = [ 2 | { 3 | title: 'Wyre', 4 | source: 'Studio Freight', 5 | href: 'https://sendwyre.com', 6 | }, 7 | { 8 | title: 'Lunchbox', 9 | source: 'Studio Freight', 10 | href: 'https://lunchbox.io', 11 | }, 12 | { 13 | title: 'Scroll Animation Ideas for Image Grids', 14 | source: 'Codrops', 15 | href: 'https://tympanus.net/Development/ScrollAnimationsGrid/', 16 | }, 17 | { 18 | title: 'Easol', 19 | source: 'Studio Freight', 20 | href: 'https://easol.com', 21 | }, 22 | { 23 | title: 'Repeat', 24 | source: 'Studio Freight', 25 | href: 'https://getrepeat.io', 26 | }, 27 | { 28 | title: 'How to Animate SVG Shapes on Scroll', 29 | source: 'Codrops', 30 | href: 'https://tympanus.net/Tutorials/OnScrollPathAnimations/', 31 | }, 32 | { 33 | title: 'Dragonfly', 34 | source: 'Studio Freight', 35 | href: 'https://dragonfly.xyz', 36 | }, 37 | { 38 | title: 'Yuga Labs', 39 | source: 'Antinomy Studio', 40 | href: 'https://yuga.com', 41 | }, 42 | { 43 | title: "Quentin Hocde's Portfolio", 44 | source: 'Quentin Hocde', 45 | href: 'https://quentinhocde.com/', 46 | }, 47 | { 48 | title: 'Houses Of', 49 | source: 'Felix P. & Shelby Kay', 50 | href: 'https://housesof.world/', 51 | }, 52 | { 53 | title: 'Heights Agency', 54 | source: 'Francesco Michelini', 55 | href: 'https://www.heights.agency/', 56 | }, 57 | { 58 | title: 'Goodship', 59 | source: 'Studio Freight', 60 | href: 'https://goodship.io/', 61 | }, 62 | ] 63 | -------------------------------------------------------------------------------- /src/dimensions.js: -------------------------------------------------------------------------------- 1 | import { debounce } from './debounce' 2 | 3 | export class Dimensions { 4 | constructor(wrapper, content) { 5 | this.wrapper = wrapper 6 | this.content = content 7 | 8 | if (this.wrapper === window) { 9 | window.addEventListener('resize', this.onWindowResize, false) 10 | this.onWindowResize() 11 | } else { 12 | this.wrapperResizeObserver = new ResizeObserver( 13 | debounce(this.onWrapperResize, 100) 14 | ) 15 | this.wrapperResizeObserver.observe(this.wrapper) 16 | this.onWrapperResize() 17 | } 18 | 19 | this.contentResizeObserver = new ResizeObserver( 20 | debounce(this.onContentResize, 100) 21 | ) 22 | this.contentResizeObserver.observe(this.content) 23 | this.onContentResize() 24 | } 25 | 26 | onWindowResize = () => { 27 | this.width = window.innerWidth 28 | this.height = window.innerHeight 29 | } 30 | 31 | destroy() { 32 | window.removeEventListener('resize', this.onWindowResize, false) 33 | 34 | this.wrapperResizeObserver?.disconnect() 35 | this.contentResizeObserver?.disconnect() 36 | } 37 | 38 | onWrapperResize = () => { 39 | this.width = this.wrapper.clientWidth 40 | this.height = this.wrapper.clientHeight 41 | } 42 | 43 | onContentResize = () => { 44 | const element = 45 | this.wrapper === window ? document.documentElement : this.wrapper 46 | this.scrollHeight = element.scrollHeight 47 | this.scrollWidth = element.scrollWidth 48 | } 49 | 50 | get limit() { 51 | return { 52 | x: this.scrollWidth - this.width, 53 | y: this.scrollHeight - this.height, 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/animate.js: -------------------------------------------------------------------------------- 1 | import { clamp, damp } from './maths' 2 | 3 | // Animate class to handle value animations with lerping or easing 4 | export class Animate { 5 | // Advance the animation by the given delta time 6 | advance(deltaTime) { 7 | if (!this.isRunning) return 8 | 9 | let completed = false 10 | 11 | if (this.lerp) { 12 | this.value = damp(this.value, this.to, this.lerp * 60, deltaTime) 13 | if (Math.round(this.value) === this.to) { 14 | this.value = this.to 15 | completed = true 16 | } 17 | } else { 18 | this.currentTime += deltaTime 19 | const linearProgress = clamp(0, this.currentTime / this.duration, 1) 20 | 21 | completed = linearProgress >= 1 22 | const easedProgress = completed ? 1 : this.easing(linearProgress) 23 | this.value = this.from + (this.to - this.from) * easedProgress 24 | } 25 | 26 | // Call the onUpdate callback with the current value and completed status 27 | this.onUpdate?.(this.value, { completed }) 28 | 29 | if (completed) { 30 | this.stop() 31 | } 32 | } 33 | 34 | // Stop the animation 35 | stop() { 36 | this.isRunning = false 37 | } 38 | 39 | // Set up the animation from a starting value to an ending value 40 | // with optional parameters for lerping, duration, easing, and onUpdate callback 41 | fromTo(from, to, { lerp = 0.1, duration = 1, easing = (t) => t, onUpdate }) { 42 | this.from = this.value = from 43 | this.to = to 44 | this.lerp = lerp 45 | this.duration = duration 46 | this.easing = easing 47 | this.currentTime = 0 48 | this.isRunning = true 49 | 50 | this.onUpdate = onUpdate 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /website/icons/github.svg: -------------------------------------------------------------------------------- 1 | GitHub -------------------------------------------------------------------------------- /website/pages/_document.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-document-import-in-page */ 2 | import { Head, Html, Main, NextScript } from 'next/document' 3 | 4 | export default function Document() { 5 | return ( 6 | 7 | 8 | 9 | 16 | 23 | 30 | 37 | 44 | 51 | 52 | 53 |
54 | 55 | 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /website/icons/enter-lenis.svg: -------------------------------------------------------------------------------- 1 | Enter Lenis -------------------------------------------------------------------------------- /website/components/appear-title/index.js: -------------------------------------------------------------------------------- 1 | import { useMediaQuery, useRect } from '@studio-freight/hamo' 2 | import cn from 'clsx' 3 | import { gsap } from 'gsap' 4 | import { SplitText } from 'gsap/dist/SplitText' 5 | import { useEffect, useRef, useState } from 'react' 6 | import { useIntersection, useWindowSize } from 'react-use' 7 | import s from './appear-title.module.scss' 8 | 9 | gsap.registerPlugin(SplitText) 10 | 11 | export function AppearTitle({ children, visible = true }) { 12 | const el = useRef() 13 | 14 | const [intersected, setIntersected] = useState(false) 15 | const intersection = useIntersection(el, { 16 | threshold: 1, 17 | }) 18 | 19 | useEffect(() => { 20 | if (intersection?.isIntersecting) { 21 | setIntersected(true) 22 | } 23 | }, [intersection]) 24 | 25 | const { width } = useWindowSize() 26 | const isMobile = useMediaQuery('(max-width: 800px)') 27 | 28 | const [rectRef, rect] = useRect() 29 | 30 | useEffect(() => { 31 | if (isMobile === false) { 32 | const splitted = new SplitText(el.current, { 33 | type: 'lines', 34 | lineThreshold: 0.3, 35 | tag: 'span', 36 | linesClass: s.line, 37 | }) 38 | 39 | splitted.lines.forEach((line, i) => { 40 | line.style.setProperty('--i', i) 41 | const html = line.innerHTML 42 | line.innerHTML = '' 43 | const content = document.createElement('span') 44 | content.innerHTML = html 45 | line.appendChild(content) 46 | }) 47 | 48 | return () => { 49 | splitted.revert() 50 | } 51 | } 52 | }, [width, rect, isMobile]) 53 | 54 | return ( 55 | { 57 | el.current = node 58 | rectRef(node) 59 | }} 60 | className={cn(s.title, intersected && visible && s.visible)} 61 | > 62 | {children} 63 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /website/components/scrollbar/index.js: -------------------------------------------------------------------------------- 1 | import { useScroll } from 'hooks/use-scroll' 2 | import { clamp, mapRange } from 'lib/maths' 3 | import { useStore } from 'lib/store' 4 | import { useEffect, useRef, useState } from 'react' 5 | import { useWindowSize } from 'react-use' 6 | import s from './scrollbar.module.scss' 7 | 8 | export function Scrollbar({}) { 9 | const progressBar = useRef() 10 | const { width: windowWidth, height: windowHeight } = useWindowSize() 11 | const lenis = useStore(({ lenis }) => lenis) 12 | 13 | useScroll(({ scroll, limit }) => { 14 | const progress = scroll / limit 15 | progressBar.current.style.transform = `scaleX(${progress})` 16 | }) 17 | 18 | const [clicked, setClicked] = useState(false) 19 | 20 | useEffect(() => { 21 | if (!clicked) return 22 | 23 | function onPointerMove(e) { 24 | e.preventDefault() 25 | 26 | const offset = (windowHeight - innerHeight) / 2 27 | const y = mapRange( 28 | 0, 29 | windowHeight, 30 | e.clientY, 31 | -offset, 32 | innerHeight + offset 33 | ) 34 | 35 | const progress = clamp(0, y / innerHeight, 1) 36 | const newPos = lenis.limit * progress 37 | 38 | lenis.direction === 'vertical' 39 | ? window.scrollTo(0, newPos) 40 | : window.scrollTo(newPos, 0) 41 | } 42 | 43 | function onPointerUp() { 44 | setClicked(false) 45 | } 46 | 47 | window.addEventListener('pointermove', onPointerMove, false) 48 | window.addEventListener('pointerup', onPointerUp, false) 49 | 50 | return () => { 51 | window.removeEventListener('pointermove', onPointerMove, false) 52 | window.removeEventListener('pointerup', onPointerUp, false) 53 | } 54 | }, [clicked, windowHeight, windowWidth, lenis]) 55 | 56 | return ( 57 |
58 |
59 |
60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /website/components/sticky/index.js: -------------------------------------------------------------------------------- 1 | import { gsap } from 'gsap' 2 | import { useEffect, useRef } from 'react' 3 | export function Sticky({ 4 | children, 5 | wrapperClass, 6 | className, 7 | start = 0, 8 | end = 0, 9 | target, 10 | id = 'sticky', 11 | enabled = true, 12 | pinType = 'fixed', 13 | }) { 14 | const pinSpacer = useRef() 15 | const trigger = useRef() 16 | const targetRef = useRef() 17 | 18 | useEffect(() => { 19 | if (!enabled || !pinSpacer.current || !trigger.current) return 20 | gsap.set(trigger.current, { clearProps: 'all' }) 21 | 22 | const timeline = gsap.timeline({ 23 | scrollTrigger: { 24 | id: id, 25 | pinType, 26 | pinSpacing: false, 27 | pinSpacer: pinSpacer.current, 28 | trigger: trigger.current, 29 | scrub: true, 30 | pin: true, 31 | start: `top top+=${parseFloat(start)}px`, 32 | end: () => { 33 | const targetRefRect = targetRef.current.getBoundingClientRect() 34 | const triggerRect = trigger.current.getBoundingClientRect() 35 | return `+=${ 36 | targetRefRect.bottom - triggerRect.bottom + parseFloat(end) 37 | }` 38 | }, 39 | invalidateOnRefresh: true, 40 | }, 41 | }) 42 | 43 | return () => { 44 | timeline.kill() 45 | } 46 | }, [id, start, enabled, end, pinType]) 47 | 48 | useEffect(() => { 49 | if (target) { 50 | targetRef.current = document.querySelector(target) 51 | } else { 52 | targetRef.current = pinSpacer.current.parentNode 53 | } 54 | }, [target]) 55 | 56 | return ( 57 |
{ 59 | pinSpacer.current = node 60 | }} 61 | className={wrapperClass} 62 | > 63 |
{ 65 | trigger.current = node 66 | }} 67 | className={className} 68 | > 69 | {children} 70 |
71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /website/components/page-transition/index.js: -------------------------------------------------------------------------------- 1 | import gsap from 'gsap' 2 | import { useStore } from 'lib/store' 3 | import { useRouter } from 'next/router' 4 | import { useEffect, useRef, useState } from 'react' 5 | import s from './page-transition.module.scss' 6 | 7 | export const PageTransition = () => { 8 | const curtainRef = useRef() 9 | const router = useRouter() 10 | const [pageLoaded, setPageloaded] = useState(false) 11 | const [curtainInComplete, setCurtainInComplete] = useState(false) 12 | const triggerTransition = useStore( 13 | ({ triggerTransition }) => triggerTransition 14 | ) 15 | const setTriggerTransition = useStore( 16 | ({ setTriggerTransition }) => setTriggerTransition 17 | ) 18 | const timeline = useRef(gsap.timeline()) 19 | 20 | useEffect(() => { 21 | if (!curtainInComplete) return 22 | const changeRouteComplete = () => { 23 | setPageloaded(true) 24 | } 25 | 26 | router.events.on('routeChangeComplete', changeRouteComplete) 27 | return () => { 28 | router.events.off('routeChangeComplete', changeRouteComplete) 29 | } 30 | }, [curtainInComplete]) 31 | 32 | useEffect(() => { 33 | if (triggerTransition === '') return 34 | timeline.current.to(curtainRef.current, { 35 | x: 0, 36 | duration: 0.7, 37 | startAt: { x: '-100%' }, 38 | onComplete: () => { 39 | router.push(triggerTransition) 40 | setCurtainInComplete(true) 41 | }, 42 | ease: 'circ.out', 43 | }) 44 | }, [triggerTransition]) 45 | 46 | useEffect(() => { 47 | if (!pageLoaded) return 48 | timeline.current.to(curtainRef.current, { 49 | x: '100%', 50 | paused: !pageLoaded, 51 | duration: 0.7, 52 | startAt: { x: 0 }, 53 | onComplete: () => { 54 | setTriggerTransition('') 55 | setCurtainInComplete(false) 56 | setPageloaded(false) 57 | }, 58 | ease: 'circ.out', 59 | }) 60 | }, [pageLoaded]) 61 | 62 | return
63 | } 64 | -------------------------------------------------------------------------------- /website/components/webgl/particles/vertex.glsl: -------------------------------------------------------------------------------- 1 | // Simplex 2D noise 2 | // 3 | vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); } 4 | 5 | float snoise(vec2 v){ 6 | const vec4 C = vec4(0.211324865405187, 0.366025403784439, 7 | -0.577350269189626, 0.024390243902439); 8 | vec2 i = floor(v + dot(v, C.yy) ); 9 | vec2 x0 = v - i + dot(i, C.xx); 10 | vec2 i1; 11 | i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); 12 | vec4 x12 = x0.xyxy + C.xxzz; 13 | x12.xy -= i1; 14 | i = mod(i, 289.0); 15 | vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) 16 | + i.x + vec3(0.0, i1.x, 1.0 )); 17 | vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), 18 | dot(x12.zw,x12.zw)), 0.0); 19 | m = m*m ; 20 | m = m*m ; 21 | vec3 x = 2.0 * fract(p * C.www) - 1.0; 22 | vec3 h = abs(x) - 0.5; 23 | vec3 ox = floor(x + 0.5); 24 | vec3 a0 = x - ox; 25 | m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); 26 | vec3 g; 27 | g.x = a0.x * x0.x + h.x * x0.y; 28 | g.yz = a0.yz * x12.xz + h.yz * x12.yw; 29 | return 130.0 * dot(m, g); 30 | } 31 | 32 | attribute float size; 33 | attribute float speed; 34 | attribute vec3 noise; 35 | attribute float scale; 36 | 37 | uniform float uTime; 38 | uniform float uScroll; 39 | uniform vec2 uResolution; 40 | 41 | void main() { 42 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 43 | 44 | modelPosition.x += snoise(vec2(noise.x, uTime * speed)) * scale; 45 | modelPosition.y += snoise(vec2(noise.y, uTime * speed)) * scale; 46 | modelPosition.z += snoise(vec2(noise.z, uTime * speed)) * scale; 47 | 48 | float depth = (1.0 / - (viewMatrix * modelPosition).z); 49 | 50 | modelPosition.y += uScroll * depth * 100.; 51 | modelPosition.y = mod(modelPosition.y, uResolution.y) - uResolution.y/2.; 52 | 53 | vec4 viewPosition = viewMatrix * modelPosition; 54 | vec4 projectionPostion = projectionMatrix * viewPosition; 55 | 56 | gl_Position = projectionPostion; 57 | gl_PointSize = size * 100.; 58 | gl_PointSize *= (1.0 / - viewPosition.z); 59 | } -------------------------------------------------------------------------------- /website/icons/sfdr.svg: -------------------------------------------------------------------------------- 1 | Studio Freight Darkroom -------------------------------------------------------------------------------- /website/components/feature-cards/feature-cards.module.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/_functions'; 2 | 3 | .features { 4 | height: 1600vh; 5 | 6 | @include desktop { 7 | min-height: desktop-vw(1310px); 8 | } 9 | 10 | .card { 11 | --d: 100vh; 12 | 13 | position: absolute; 14 | will-change: transform; 15 | transition-duration: 1.2s; 16 | transition-property: opacity, transform; 17 | transition-timing-function: var(--ease-out-expo); 18 | // transform: translate3d(0, calc(var(--progress) * -1 * var(--d)), 0); 19 | // transform: translateY(calc(var(--progress) * -1 * var(--d))); 20 | 21 | @include mobile { 22 | @for $i from 0 through 8 { 23 | &:nth-child(#{$i + 1}) { 24 | top: calc( 25 | ( 26 | ( 27 | (100 * var(--vh, 1vh)) - 28 | #{mobile-vw(440px)} - 29 | (var(--layout-margin)) 30 | ) / 31 | 8 32 | ) * 33 | $i 34 | ); 35 | } 36 | } 37 | } 38 | 39 | @include desktop { 40 | @for $i from 0 through 8 { 41 | &:nth-child(#{$i + 1}) { 42 | top: calc( 43 | ( 44 | (var(--d) - #{desktop-vw(440px)} - (2 * var(--layout-margin))) / 45 | 8 46 | ) * 47 | $i 48 | ); 49 | left: calc( 50 | ((100vw - #{desktop-vw(440px)} - (2 * var(--layout-margin))) / 8) * 51 | $i 52 | ); 53 | } 54 | } 55 | } 56 | 57 | &:not(.current) { 58 | transform: translate3d(100%, 100%, 0); 59 | opacity: 0; 60 | } 61 | } 62 | 63 | .title { 64 | text-align: end; 65 | padding-bottom: var(--layout-margin); 66 | 67 | @include desktop { 68 | padding: 0; 69 | position: absolute; 70 | right: var(--layout-margin); 71 | } 72 | } 73 | } 74 | 75 | .sticky { 76 | overflow: hidden; 77 | position: sticky; 78 | top: 0; 79 | height: 100vh; 80 | padding: var(--layout-margin); 81 | 82 | @include desktop { 83 | } 84 | 85 | > * { 86 | position: relative; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /website/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lenis-website", 3 | "version": "1.0.0", 4 | "description": "Lenis is a smooth scroll library to normalize the scrolling experience across devices", 5 | "scripts": { 6 | "dev": "npm run dev --prefix ..", 7 | "dev:website": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "eslint --cache --fix", 11 | "postbuild": "next-sitemap", 12 | "analyze": "cross-env ANALYZE=true next build", 13 | "analyze:server": "cross-env BUNDLE_ANALYZE=server next build", 14 | "analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@react-three/drei": "^9.58.1", 21 | "@react-three/fiber": "^8.12.0", 22 | "@studio-freight/hamo": "^0.6.12", 23 | "@studio-freight/tempus": "^0.0.36", 24 | "clsx": "^1.2.1", 25 | "glslify-loader": "^2.0.0", 26 | "gsap": "npm:gsap-trial@^3.11.5", 27 | "leva": "^0.9.34", 28 | "nanoevents": "^7.0.1", 29 | "next": "^13.2.4", 30 | "raw-loader": "^4.0.2", 31 | "react": "^18.2.0", 32 | "react-dom": "^18.2.0", 33 | "react-use": "^17.4.0", 34 | "three": "^0.150.1", 35 | "zustand": "4.3.6" 36 | }, 37 | "devDependencies": { 38 | "@builder.io/partytown": "^0.7.5", 39 | "@next/bundle-analyzer": "^13.2.4", 40 | "@size-limit/preset-app": "^8.2.4", 41 | "@svgr/webpack": "^6.5.1", 42 | "critters": "^0.0.16", 43 | "cross-env": "^7.0.3", 44 | "duplicate-package-checker-webpack-plugin": "^3.0.0", 45 | "eslint": "8.36.0", 46 | "eslint-config-next": "13.2.4", 47 | "eslint-config-prettier": "^8.8.0", 48 | "eslint-import-resolver-alias": "^1.1.2", 49 | "eslint-plugin-import": "^2.27.5", 50 | "next-pwa": "5.6.0", 51 | "next-seo": "^5.15.0", 52 | "next-sitemap": "^4.0.6", 53 | "prettier": "^2.8.6", 54 | "prettier-eslint": "^15.0.1", 55 | "sass": "^1.59.3", 56 | "stats.js": "^0.17.0" 57 | }, 58 | "pnpm": { 59 | "overrides": { 60 | "@radix-ui/react-portal": "0.1.4", 61 | "@radix-ui/react-primitive": "0.1.4", 62 | "extend-shallow": "3.0.2", 63 | "is-extendable": "1.0.1", 64 | "react-is": "18.2.0", 65 | "scheduler": "0.23.0", 66 | "throttle-debounce": "5.0.0", 67 | "zustand": "4.3.6" 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /website/components/horizontal-slides/index.js: -------------------------------------------------------------------------------- 1 | import { useMediaQuery, useRect } from '@studio-freight/hamo' 2 | import cn from 'clsx' 3 | import gsap from 'gsap' 4 | import { useScroll } from 'hooks/use-scroll' 5 | import { clamp, mapRange } from 'lib/maths' 6 | import { useEffect, useRef, useState } from 'react' 7 | import { useWindowSize } from 'react-use' 8 | 9 | import s from './horizontal-slides.module.scss' 10 | 11 | export const HorizontalSlides = ({ children }) => { 12 | const elementRef = useRef(null) 13 | const isMobile = useMediaQuery('(max-width: 800px)') 14 | const [wrapperRectRef, wrapperRect] = useRect() 15 | const [elementRectRef, elementRect] = useRect() 16 | 17 | const { height: windowHeight } = useWindowSize() 18 | 19 | const [windowWidth, setWindowWidth] = useState() 20 | 21 | useScroll(({ scroll }) => { 22 | if (!elementRect || !elementRef.current) return 23 | 24 | const start = wrapperRect.top - windowHeight 25 | const end = wrapperRect.top + wrapperRect.height - windowHeight 26 | 27 | let progress = mapRange(start, end, scroll, 0, 1) 28 | progress = clamp(0, progress, 1) 29 | 30 | const x = progress * (elementRect.width - windowWidth) 31 | 32 | const cards = [...elementRef.current.children] 33 | 34 | gsap.to(cards, { 35 | x: -x, 36 | stagger: 0.033, 37 | ease: 'none', 38 | duration: 0, 39 | }) 40 | }) 41 | 42 | useEffect(() => { 43 | const onResize = () => { 44 | setWindowWidth( 45 | Math.min(window.innerWidth, document.documentElement.offsetWidth) 46 | ) 47 | } 48 | 49 | window.addEventListener('resize', onResize, false) 50 | onResize() 51 | 52 | return () => { 53 | window.removeEventListener('resize', onResize, false) 54 | } 55 | }, []) 56 | 57 | return ( 58 |
67 |
68 | {/* {isMobile === false ? ( */} 69 |
{ 71 | elementRef.current = node 72 | elementRectRef(node) 73 | }} 74 | className={cn(s.overflow, 'hide-on-mobile')} 75 | > 76 | {children} 77 |
78 | {/* ) : ( */} 79 |
{children}
80 | {/* )} */} 81 |
82 |
83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /website/styles/_layout.scss: -------------------------------------------------------------------------------- 1 | // inspiration: https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins 2 | 3 | // css variables exposed globally: 4 | // --layout-columns-count: columns count in the layout 5 | // --layout-columns-gap: gap size between columns 6 | // --layout-margin: layout margin size (left or right) 7 | // --layout-width: 100vw minus 2 * --layout-margin 8 | // --layout-column-width: size of a single column 9 | 10 | // css classes exposed globally: 11 | // .layout-block: element takes the whole layout width 12 | // .layout-block-inner: same as .layout-block but using padding instead of margin 13 | // .layout-grid: extends .layout-block with grid behaviour using layout settings 14 | // .layout-grid-inner: same as .layout-grid but using padding instead of margin 15 | 16 | @use 'sass:map'; 17 | 18 | // config to fill 19 | // 'variable': (mobile, desktop) 20 | $layout: ( 21 | 'columns-count': ( 22 | 6, 23 | 12, 24 | ), 25 | 'columns-gap': ( 26 | 24px, 27 | 24px, 28 | ), 29 | 'margin': ( 30 | 16px, 31 | 40px, 32 | ), 33 | ); 34 | 35 | //internal process, do not touch 36 | :root { 37 | --layout-columns-count: #{nth(map.get($layout, 'columns-count'), 1)}; 38 | --layout-columns-gap: #{mobile-vw(nth(map.get($layout, 'columns-gap'), 1))}; 39 | --layout-margin: #{mobile-vw(nth(map.get($layout, 'margin'), 1))}; 40 | 41 | @include desktop { 42 | --layout-columns-count: #{nth(map.get($layout, 'columns-count'), 2)}; 43 | --layout-columns-gap: #{desktop-vw(nth(map.get($layout, 'columns-gap'), 2))}; 44 | --layout-margin: #{desktop-vw(nth(map.get($layout, 'margin'), 2))}; 45 | } 46 | 47 | --layout-width: calc(100vw - (2 * var(--layout-margin))); 48 | --layout-column-width: calc( 49 | ( 50 | var(--layout-width) - 51 | ((var(--layout-columns-count) - 1) * var(--layout-columns-gap)) 52 | ) / var(--layout-columns-count) 53 | ); 54 | } 55 | 56 | .layout-block { 57 | max-width: var(--layout-width); 58 | margin-left: auto; 59 | margin-right: auto; 60 | } 61 | 62 | .layout-block-inner { 63 | padding-left: var(--layout-margin); 64 | padding-right: var(--layout-margin); 65 | } 66 | 67 | .layout-grid { 68 | @extend .layout-block; 69 | 70 | display: grid; 71 | grid-template-columns: repeat(var(--layout-columns-count), minmax(0, 1fr)); 72 | grid-gap: var(--layout-columns-gap); 73 | } 74 | 75 | .layout-grid-inner { 76 | @extend .layout-block-inner; 77 | 78 | display: grid; 79 | grid-template-columns: repeat(var(--layout-columns-count), minmax(0, 1fr)); 80 | grid-gap: var(--layout-columns-gap); 81 | } 82 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # [Lenis Website] 2 | 3 | ## Setup 4 | 5 | The usual process for Next.js based apps/websites: 6 | 7 | 1. Install node modules: 8 | 9 | `$ pnpm i` 10 | 11 | 2. Get the .env variables from Vercel (check `.env.template`), after [installing Vercel CLI](https://vercel.com/docs/cli): 12 | 13 | `$ vc link` 14 | 15 | `$ vc env pull` 16 | 17 | 3. run development environment: 18 | 19 | `$ pnpm dev` 20 | 21 | ## Stack 22 | 23 | - [Lenis](https://github.com/studio-freight/lenis) // in this case we're using the local /bundled version, instead of the npm package. 24 | - [Tempus](https://github.com/studio-freight/tempus) 25 | - [Hamo](https://github.com/studio-freight/hamo) 26 | - [PNPM](https://pnpm.io/) 27 | - [Next.js](https://nextjs.org/) 28 | - [Three.js](https://threejs.org/) 29 | - [@react-three/drei](https://github.com/pmndrs/drei) 30 | - [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) 31 | - [GSAP](https://greensock.com/gsap/) 32 | - [Embla Carousel](https://github.com/davidcetinkaya/embla-carousel) 33 | - Sass (Modules) 34 | - [Zustand](https://github.com/pmndrs/zustand) 35 | - [React Hook Form](https://react-hook-form.com/) 36 | - GraphQL (CMS API) 37 | - [Next-Sitemap](https://github.com/iamvishnusankar/next-sitemap) (postbuild script) 38 | - [@svgr/webpack](https://github.com/gregberge/svgr/tree/main) (SVG Imports in `next.config.js`) 39 | 40 | ## Code Style & Linting 41 | 42 | - Eslint ([Next](https://nextjs.org/docs/basic-features/eslint#eslint-config) and [Prettier](https://github.com/prettier/eslint-config-prettier) plugins) 43 | - [Prettier](https://prettier.io/) with the following settings available in `.pretierrc`: 44 | ```json 45 | { 46 | "endOfLine": "auto", 47 | "semi": false, 48 | "singleQuote": true 49 | } 50 | ``` 51 | - [Husky + lint-staged precommit hooks](https://github.com/okonet/lint-staged) 52 | 53 | ## Third Party 54 | 55 | - [Vercel (Hosting & Continuous Deployment)](https://vercel.com/home) 56 | - [GitHub Versioning](https://github.com/) 57 | 58 | ## Folder Structure 59 | 60 | Alongside the usual Next.js folder structure (`/public`, `/pages`, etc.) We've added a few other folders to keep the code easier to read: 61 | 62 | - **/assets:** General Images/Videos and SVGs 63 | - **/components:** Reusable components with their respective Sass file 64 | - **/contentful:** Fragments/Queries/Renderers 65 | - **/config:** General settings (mostly Leva for now) 66 | - **/hooks:** Reusable Custom Hooks 67 | - **/layouts:** High level layout component 68 | - **/lib:** Reusable Scripts and State Store 69 | - **/styles:** Global styles and Sass partials 70 | -------------------------------------------------------------------------------- /website/pages/_app.js: -------------------------------------------------------------------------------- 1 | import { useDebug } from '@studio-freight/hamo' 2 | import { raf } from '@studio-freight/tempus' 3 | import { RealViewport } from 'components/real-viewport' 4 | import { gsap } from 'gsap' 5 | import { ScrollTrigger } from 'gsap/dist/ScrollTrigger' 6 | import { useScroll } from 'hooks/use-scroll' 7 | import { GTM_ID } from 'lib/analytics' 8 | import { useStore } from 'lib/store' 9 | import dynamic from 'next/dynamic' 10 | import Script from 'next/script' 11 | import { useEffect } from 'react' 12 | import 'styles/global.scss' 13 | 14 | if (typeof window !== 'undefined') { 15 | gsap.registerPlugin(ScrollTrigger) 16 | ScrollTrigger.defaults({ markers: process.env.NODE_ENV === 'development' }) 17 | 18 | // merge rafs 19 | gsap.ticker.lagSmoothing(0) 20 | gsap.ticker.remove(gsap.updateRoot) 21 | raf.add((time) => { 22 | gsap.updateRoot(time / 1000) 23 | }, 0) 24 | } 25 | 26 | const Stats = dynamic( 27 | () => import('components/stats').then(({ Stats }) => Stats), 28 | { ssr: false } 29 | ) 30 | 31 | const GridDebugger = dynamic( 32 | () => 33 | import('components/grid-debugger').then(({ GridDebugger }) => GridDebugger), 34 | { ssr: false } 35 | ) 36 | 37 | const Leva = dynamic(() => import('leva').then(({ Leva }) => Leva), { 38 | ssr: false, 39 | }) 40 | 41 | function MyApp({ Component, pageProps }) { 42 | const debug = useDebug() 43 | const lenis = useStore(({ lenis }) => lenis) 44 | 45 | useScroll(ScrollTrigger.update) 46 | 47 | useEffect(() => { 48 | if (lenis) { 49 | ScrollTrigger.refresh() 50 | lenis?.start() 51 | } 52 | }, [lenis]) 53 | 54 | useEffect(() => { 55 | window.history.scrollRestoration = 'manual' 56 | }, []) 57 | 58 | ScrollTrigger.defaults({ markers: process.env.NODE_ENV === 'development' }) 59 | 60 | return ( 61 | <> 62 |