├── public ├── favicon.ico ├── fonts │ ├── Poppins-Medium.woff │ ├── Poppins-Regular.woff │ ├── Poppins-SemiBold.woff │ └── PlayfairDisplay-Italic.woff ├── glb │ └── lens-transformed2.glb ├── images │ ├── codrops-bg-tiny.png │ ├── maxim-berg-1_U2RcHnSjc-unsplash.jpg │ ├── maxim-berg-ANuuRuCRRAc-unsplash.jpg │ ├── maxim-berg-TcE45yIzJA0-unsplash.jpg │ └── maxim-berg-qsDfqZyTCAE-unsplash-crop.jpg ├── env │ └── empty_warehouse_01_1k.hdr ├── fonts.css ├── index.html └── styles.css ├── src ├── index.js ├── WebGLBackground.jsx ├── EffectsToggle.jsx ├── CodropsFrame.jsx ├── ImageCube.jsx ├── Text.jsx ├── Lens.jsx ├── Image.jsx └── App.jsx ├── .gitignore ├── package.json ├── readme.md └── LICENSE /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/Poppins-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/fonts/Poppins-Medium.woff -------------------------------------------------------------------------------- /public/glb/lens-transformed2.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/glb/lens-transformed2.glb -------------------------------------------------------------------------------- /public/fonts/Poppins-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/fonts/Poppins-Regular.woff -------------------------------------------------------------------------------- /public/fonts/Poppins-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/fonts/Poppins-SemiBold.woff -------------------------------------------------------------------------------- /public/images/codrops-bg-tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/images/codrops-bg-tiny.png -------------------------------------------------------------------------------- /public/env/empty_warehouse_01_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/env/empty_warehouse_01_1k.hdr -------------------------------------------------------------------------------- /public/fonts/PlayfairDisplay-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/fonts/PlayfairDisplay-Italic.woff -------------------------------------------------------------------------------- /public/images/maxim-berg-1_U2RcHnSjc-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/images/maxim-berg-1_U2RcHnSjc-unsplash.jpg -------------------------------------------------------------------------------- /public/images/maxim-berg-ANuuRuCRRAc-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/images/maxim-berg-ANuuRuCRRAc-unsplash.jpg -------------------------------------------------------------------------------- /public/images/maxim-berg-TcE45yIzJA0-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/images/maxim-berg-TcE45yIzJA0-unsplash.jpg -------------------------------------------------------------------------------- /public/images/maxim-berg-qsDfqZyTCAE-unsplash-crop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14islands/codrops-scroll-rig-tutorial/HEAD/public/images/maxim-berg-qsDfqZyTCAE-unsplash-crop.jpg -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | 3 | import App from './App' 4 | 5 | const root = createRoot(document.getElementById('root')) 6 | root.render() 7 | -------------------------------------------------------------------------------- /.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 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/WebGLBackground.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, Suspense } from "react"; 2 | import { useFrame, useThree } from "@react-three/fiber"; 3 | import { Image } from "@react-three/drei"; 4 | 5 | export function WebGLBackground() { 6 | const bg = useRef(); 7 | const viewport = useThree((s) => s.viewport); 8 | useFrame((_, delta) => { 9 | if (bg.current) bg.current.rotation.z -= delta * 0.5; 10 | }); 11 | return ( 12 | 13 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /public/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Playfair Display"; 3 | font-style: italic; 4 | font-weight: 400; 5 | font-display: swap; 6 | src: url("fonts/PlayfairDisplay-Italic.woff") format("woff"); 7 | } 8 | @font-face { 9 | font-family: "Poppins"; 10 | font-style: normal; 11 | font-weight: 400; 12 | font-display: swap; 13 | src: url("fonts/Poppins-Regular.woff") format("woff"); 14 | } 15 | @font-face { 16 | font-family: "Poppins"; 17 | font-style: normal; 18 | font-weight: 500; 19 | font-display: swap; 20 | src: url("fonts/Poppins-Medium.woff") format("woff"); 21 | } 22 | @font-face { 23 | font-family: "Poppins"; 24 | font-style: normal; 25 | font-weight: 600; 26 | font-display: swap; 27 | src: url("fonts/Poppins-SemiBold.woff") format("woff"); 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codrops-scroll-rig-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "@14islands/lerp": "1.0.3", 9 | "@14islands/r3f-scroll-rig": "8.12.3", 10 | "@react-spring/three": "9.7.3", 11 | "@react-three/drei": "9.86.3", 12 | "@react-three/fiber": "8.14.4", 13 | "maath": "^0.10.4", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0", 16 | "react-scripts": "5.0.1", 17 | "three": "0.157.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test --env=jsdom", 23 | "eject": "react-scripts eject" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Progressively Enhanced WebGL Lens Refraction 2 | 3 | Learn how to create a responsive WebGL layout powered by CSS and React Three Fiber. By [David Lindkvist](https://twitter.com/ffdead) @ [14islands](https://14islands.com). 4 | 5 | ![Featured Image](https://tympanus.net/codrops/wp-content/uploads/2023/10/webglLens.gif) 6 | 7 | [Article on Codrops](https://tympanus.net/codrops/?p=73607) 8 | 9 | [Demo](https://tympanus.net/Tutorials/WebGLLensRefraction/) 10 | 11 | ## Installation 12 | 13 | pnpm i 14 | pnpm start 15 | 16 | ## Credits 17 | 18 | - [react-three-fiber](https://github.com/react-spring/react-three-fiber) 19 | - [three.js](https://threejs.org/) 20 | - [React](https://reactjs.org/) 21 | - Image credits: [Maxim Berg](https://unsplash.com/@maxberg) 22 | - Lens refraction based on [Lens Refraction by Paul Henschel](https://codesandbox.io/s/2n98yj) 23 | -------------------------------------------------------------------------------- /src/EffectsToggle.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react' 2 | 3 | export default function EffectsToggle({ setEnabled, enabled }) { 4 | const wrapperRef = useRef() 5 | const textRef = useRef() 6 | 7 | useEffect(() => { 8 | const observer = new IntersectionObserver(([e]) => e.target.classList.toggle('is-pinned', e.intersectionRatio < 1), { threshold: [1] }) 9 | observer.observe(wrapperRef.current) 10 | return () => { 11 | observer.disconnect() 12 | } 13 | }, []) 14 | 15 | return ( 16 | <> 17 |
18 |
19 | 20 | WebGL Scroll Effects 21 | 22 | 23 |
24 |
25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/CodropsFrame.jsx: -------------------------------------------------------------------------------- 1 | export default function () { 2 | return ( 3 |
4 |
5 |

6 | Progressively enhanced WebGL & Lens Refraction{" "} 7 | by 14islands 8 |

9 |
10 | 15 | Back to the article 16 | 17 | 21 | 22 | 23 | 24 | Previous demo 25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 14islands 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 20 | React App 21 | 22 | 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/ImageCube.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { ScrollScene, UseCanvas, useScrollbar, useScrollRig, styles, useImageAsTexture } from '@14islands/r3f-scroll-rig' 3 | import { useFrame } from '@react-three/fiber' 4 | import { MeshWobbleMaterial } from '@react-three/drei' 5 | import { a, useSpring, config } from '@react-spring/three' 6 | 7 | export function ImageCube({ src, ...props }) { 8 | const el = useRef() 9 | const img = useRef() 10 | const { hasSmoothScrollbar } = useScrollRig() 11 | return ( 12 | <> 13 |
14 | This will be loaded as a texture 22 |
23 | 24 | {hasSmoothScrollbar && ( 25 | 26 | {(props) => } 27 | 28 | )} 29 | 30 | ) 31 | } 32 | 33 | function WebGLCube({ img, scale, inViewport }) { 34 | const mesh = useRef() 35 | const texture = useImageAsTexture(img) 36 | const { scroll } = useScrollbar() 37 | 38 | useFrame((_, delta) => { 39 | mesh.current.material.factor += scroll.velocity * 0.005 40 | mesh.current.material.factor *= 0.95 41 | }) 42 | 43 | const spring = useSpring({ 44 | scale: inViewport ? scale.times(1) : scale.times(0), 45 | config: inViewport ? config.wobbly : config.stiff, 46 | delay: inViewport ? 200 : 0 47 | }) 48 | 49 | return ( 50 | 51 | 52 | 63 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/Text.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { ScrollScene, UseCanvas, useScrollRig, styles } from '@14islands/r3f-scroll-rig' 3 | import { MeshDistortMaterial } from '@react-three/drei' 4 | import { WebGLText } from '@14islands/r3f-scroll-rig/powerups' 5 | 6 | export const Headline = ({ children, ...props }) => ( 7 | 8 | {children} 9 | 10 | ) 11 | 12 | export const Subtitle = ({ children, ...props }) => ( 13 | 14 | {children} 15 | 16 | ) 17 | 18 | export const BodyCopy = Text 19 | 20 | export function Text({ children, wobble, className, font = 'fonts/Poppins-Regular.woff', as: Tag = 'span', ...props }) { 21 | const el = useRef() 22 | const { hasSmoothScrollbar } = useScrollRig() 23 | return ( 24 | <> 25 | {/* 26 | This is the real DOM text that we want to replace with WebGL 27 | `styles.transparentColorWhenSmooth` sets the text to transparent when SmoothScrollbar is enabled 28 | The benefit of using transparent color is that the real DOM text is still selectable 29 | 30 | display: 'block' gives a more solid calculation for spans 31 | */} 32 | 33 | {children} 34 | 35 | 36 | {hasSmoothScrollbar && ( 37 | 38 | 39 | {(props) => ( 40 | // WebGLText is a helper component from the scroll-rig that will 41 | // use getComputedStyle to match font size, letter spacing and color 42 | 48 | {wobble && } 49 | {children} 50 | 51 | )} 52 | 53 | 54 | )} 55 | 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/Lens.jsx: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { useRef, useState } from "react"; 3 | import { createPortal, useFrame, useThree } from "@react-three/fiber"; 4 | import { useFBO, useGLTF, MeshTransmissionMaterial } from "@react-three/drei"; 5 | import { easing } from "maath"; 6 | 7 | export function Lens({ children, damping = 0.14, ...props }) { 8 | const ref = useRef(); 9 | const { nodes } = useGLTF("glb/lens-transformed2.glb"); 10 | const buffer = useFBO(); 11 | const viewport = useThree((state) => state.viewport); 12 | const [scene] = useState(() => new THREE.Scene()); 13 | useFrame((state, delta) => { 14 | // Tie lens to the pointer 15 | // getCurrentViewport gives us the width & height that would fill the screen in threejs units 16 | // By giving it a target coordinate we can offset these bounds, for instance width/height for a plane that 17 | // sits 15 units from 0/0/0 towards the camera (which is where the lens is) 18 | const viewport = state.viewport.getCurrentViewport(state.camera, [0, 0, 1]); 19 | 20 | easing.damp3( 21 | ref.current.position, 22 | [ 23 | (state.pointer.x * viewport.width) / 2, 24 | (state.pointer.y * viewport.height) / 2, 25 | 1, 26 | ], 27 | damping, 28 | delta 29 | ); 30 | // This is entirely optional but spares us one extra render of the scene 31 | // The createPortal below will mount the children of into the new THREE.Scene above 32 | // The following code will render that scene into a buffer, whose texture will then be fed into 33 | // a plane spanning the full screen and the lens transmission material 34 | state.gl.setRenderTarget(buffer); 35 | state.gl.setClearColor("#ecedef"); 36 | state.gl.render(scene, state.camera); 37 | state.gl.setRenderTarget(null); 38 | }); 39 | 40 | return ( 41 | <> 42 | {createPortal(children, scene)} 43 | 44 | 45 | 46 | 47 | 54 | 64 | 65 | 66 | ); 67 | } 68 | 69 | useGLTF.preload("/lens-transformed2.glb"); 70 | -------------------------------------------------------------------------------- /src/Image.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, Suspense } from "react"; 2 | import { 3 | UseCanvas, 4 | useScrollRig, 5 | useImageAsTexture, 6 | styles, 7 | } from "@14islands/r3f-scroll-rig"; 8 | import { ParallaxScrollScene } from "@14islands/r3f-scroll-rig/powerups"; 9 | import { Image as DreiImage, Circle } from "@react-three/drei"; 10 | import { useFrame } from "@react-three/fiber"; 11 | import { clamp } from "three/src/math/MathUtils"; 12 | import { DoubleSide } from "three"; 13 | 14 | export function Image({ src, parallaxSpeed = 1, ...props }) { 15 | const el = useRef(); 16 | const img = useRef(); 17 | const { hasSmoothScrollbar } = useScrollRig(); 18 | return ( 19 | <> 20 |
21 | This will be loaded as a texture 29 |
30 | 31 | {hasSmoothScrollbar && ( 32 | 33 | 34 | {(props) => ( 35 | }> 36 | 37 | 38 | )} 39 | 40 | 41 | )} 42 | 43 | ); 44 | } 45 | 46 | function WebGLImage({ imgRef, scrollState, dir, ...props }) { 47 | const ref = useRef(); 48 | 49 | // Load texture from the and suspend until it's ready 50 | const texture = useImageAsTexture(imgRef); 51 | 52 | useFrame(({ clock }) => { 53 | // scrollState.visibility is 0 when image enters viewport at bottom and 1 when image is fully visible 54 | ref.current.material.grayscale = clamp( 55 | 1 - scrollState.visibility ** 3, 56 | 0, 57 | 1 58 | ); 59 | // scrollState.progress is 0 when image enters viewport at bottom and 1 when image left the viewport at the top 60 | ref.current.material.zoom = 1 + scrollState.progress * 0.66; 61 | // scrollState.viewport is 0 when image enters viewport at bottom and 1 when image reached top of viewport 62 | ref.current.material.opacity = clamp(scrollState.viewport * 3, 0, 1); 63 | }); 64 | 65 | // Use the component from Drei 66 | return ; 67 | } 68 | 69 | function LoadingIndicator({ scale }) { 70 | const box = useRef(); 71 | useFrame(({ clock }) => { 72 | box.current.rotation.y = clock.getElapsedTime() * 5; 73 | }); 74 | return ( 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useRef, useState } from "react"; 2 | import { GlobalCanvas, SmoothScrollbar } from "@14islands/r3f-scroll-rig"; 3 | import { Environment, Loader } from "@react-three/drei"; 4 | 5 | import { BodyCopy, Headline, Subtitle } from "./Text"; 6 | import { Image } from "./Image"; 7 | import { ImageCube } from "./ImageCube"; 8 | import { WebGLBackground } from "./WebGLBackground"; 9 | import { Lens } from "./Lens"; 10 | import CodropsFrame from "./CodropsFrame"; 11 | import EffectsToggle from "./EffectsToggle"; 12 | 13 | import "@14islands/r3f-scroll-rig/css"; 14 | 15 | // Photos by Maxim Berg on Unsplash 16 | 17 | export default function App() { 18 | const eventSource = useRef(); 19 | const [enabled, setEnabled] = useState(true); 20 | 21 | return ( 22 | // We attach events onparent div in order to get events on both canvas and DOM 23 |
24 | 25 | 50 | 54 |
55 | 56 |
57 |
58 |

59 | 60 | RESPONSIVE {enabled ? "WEBGL" : "HTML"} 61 | 62 |

63 | 64 | Progressively enhance your React website with WebGL using 65 | r3f-scroll-rig, React Three Fiber and Three.js 66 | 67 |
68 |
69 |
70 | 74 |
75 |
76 |

77 | We use CSS to create a responsive layout. 78 | 79 | 80 | A Canvas on top tracks DOM elements and enhance them with WebGL. 81 | 82 | 83 |

84 |

85 | 86 | Try turning off WebGL using the button in the sticky header. 87 | You’ll notice smooth scrolling is disabled, and all scroll-bound 88 | WebGL effects disappears. 89 | 90 |

91 |
92 |
93 | 98 | 103 |
104 |
105 |

106 | 107 | Thanks to Threejs we can also render 3D geometry or models. The 108 | following image is replaced by a box. Try scrolling hard to make 109 | it wiggle. 110 | 111 |

112 |
113 |
114 | 118 |
119 |
120 |

121 | Most websites use a mix of WebGL and HTML. 122 | 123 | 124 | However, the Lens refraction requires all images and text to be 125 | WebGL. 126 | 127 | 128 |

129 |

130 | 131 | 132 | You can find the r3f-scroll-rig library on Github. Please use 133 | WebGL responsibly™. 134 | 135 | 136 |

137 |
138 |
139 | 140 |
141 |
142 | 143 | 153 |
154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | /** 2 | * This is not a CSS tutorial, be gentle... 😇 3 | */ 4 | 5 | @import url("/fonts.css"); 6 | 7 | *, 8 | *::after, 9 | *::before { 10 | box-sizing: border-box; 11 | } 12 | 13 | html { 14 | font-family: "Poppins", sans-serif; 15 | font-weight: 400; 16 | font-display: block; 17 | overscroll-behavior: contain; 18 | font-size: max(0.75rem, 0.8101851852vw); 19 | 20 | color: #6e6bcd; 21 | background: #ecedef; 22 | } 23 | 24 | ::selection { 25 | background: rgba(110, 107, 205, 0.5); 26 | } 27 | 28 | body { 29 | -webkit-font-smoothing: antialiased; 30 | -moz-osx-font-smoothing: grayscale; 31 | } 32 | 33 | html, 34 | body, 35 | #root { 36 | width: 100%; 37 | min-height: 100%; 38 | height: auto; 39 | margin: 0; 40 | padding: 0; 41 | } 42 | 43 | .container { 44 | padding-left: max(1.2rem, 40/1440 * 100vw); 45 | padding-right: max(1.2rem, 40/1440 * 100vw); 46 | } 47 | 48 | article { 49 | padding-top: 15vh; 50 | } 51 | 52 | section { 53 | margin: max(5px, 5vw) 0; 54 | } 55 | 56 | header { 57 | height: 40vh; 58 | max-height: 50vw; 59 | display: flex; 60 | flex-direction: column; 61 | justify-content: end; 62 | align-items: start; 63 | margin-bottom: max(2.5rem, 5vw); 64 | } 65 | .headerLayout { 66 | position: relative; 67 | } 68 | 69 | h2 { 70 | font-size: max(3.75rem, 187/1440 * 100vw); 71 | font-weight: 500; 72 | line-height: 0.8; 73 | letter-spacing: -0.03em; 74 | margin: 0; 75 | margin-left: -0.07em; 76 | } 77 | h2 span { 78 | display: inline-block; 79 | width: auto; 80 | } 81 | .subline { 82 | font-weight: 400; 83 | font-size: max(14px, calc(14 / 1440 * 100vw)); 84 | display: block; 85 | line-height: 1.5; 86 | letter-spacing: 0; 87 | margin-top: 2.5rem; 88 | width: 80%; 89 | margin-left: 0; 90 | } 91 | @media screen and (min-width: 64em) { 92 | .subline { 93 | position: absolute; 94 | left: 50%; 95 | bottom: 1.5%; 96 | width: max(300px, 22vw); 97 | height: auto; 98 | top: auto; 99 | display: block; 100 | } 101 | } 102 | 103 | footer { 104 | padding-top: max(2.5rem, 20vw); 105 | } 106 | 107 | .EffectsToggle { 108 | position: sticky; 109 | top: -1px; /* use to detect if pinned */ 110 | left: max(1.2rem, 40/1440 * 100vw); 111 | z-index: 1000; 112 | height: 20vh; 113 | font-size: 1.5rem; 114 | display: inline-block; 115 | font-family: "Playfair Display"; 116 | font-style: italic; 117 | letter-spacing: -0.03rem; 118 | } 119 | .EffectsToggle__Inner { 120 | display: flex; 121 | align-items: center; 122 | width: 100%; 123 | height: auto; 124 | padding: 0.5rem 1.2rem; 125 | margin-left: -1.2rem; 126 | margin-top: 1rem; 127 | border-radius: 2.5rem; 128 | transition: background 0.2s ease; 129 | } 130 | .EffectsToggle.is-pinned .EffectsToggle__Inner { 131 | background: #dbe1f6; 132 | } 133 | 134 | .EffectsToggle__Text { 135 | display: block; 136 | width: 10em; 137 | } 138 | .EffectsToggle button { 139 | appearance: none; 140 | border: 1px solid #6e6bcd; 141 | font-weight: 600; 142 | font-family: "Poppins"; 143 | font-style: normal; 144 | font-size: 1.25rem; 145 | 146 | text-align: center; 147 | letter-spacing: -0.02em; 148 | color: #6e6bcd; 149 | border-radius: 100%; 150 | background: transparent; 151 | cursor: pointer; 152 | width: 4.125rem; 153 | height: 2.0625rem; 154 | text-align: center; 155 | vertical-align: center; 156 | padding: 0; 157 | } 158 | 159 | h3 { 160 | font-family: "Playfair Display", sans-serif; 161 | font-weight: 400; 162 | font-style: italic; 163 | line-height: 1; 164 | font-size: max(2rem, 64/1440 * 100vw); 165 | margin: 10vw 0; 166 | } 167 | h3 span { 168 | width: 80%; 169 | } 170 | h3 em { 171 | text-align: right; 172 | display: block; 173 | margin-top: 2.5rem; 174 | } 175 | h3 em span { 176 | margin-left: auto; 177 | } 178 | 179 | @media screen and (min-width: 35em) { 180 | h3 em { 181 | margin-top: 0; 182 | } 183 | } 184 | 185 | h4 { 186 | font-weight: 400; 187 | font-size: max(1rem, 36/1440 * 100vw); 188 | line-height: 1.36; 189 | margin: max(5rem, 200/1440 * 100vw) 0; 190 | width: 77%; 191 | } 192 | p { 193 | margin-top: max(5rem, 5vw); 194 | } 195 | @media (min-width: 400px) { 196 | p { 197 | margin-left: 50%; 198 | width: max(200px, 22vw); 199 | } 200 | } 201 | 202 | .JellyPlaceholder { 203 | aspect-ratio: 16/10; 204 | width: 66%; 205 | margin: auto; 206 | } 207 | 208 | .ImageLandscape { 209 | aspect-ratio: 16/9; 210 | } 211 | .ImagePortrait { 212 | aspect-ratio: 10/16; 213 | width: 47.5%; 214 | } 215 | 216 | .ParallaxContainer { 217 | display: flex; 218 | margin: max(5rem, 300/1440 * 100vw) 0; 219 | } 220 | .aspect-16_11 { 221 | aspect-ratio: 16/11; 222 | width: 50%; 223 | } 224 | .aspect-9_13 { 225 | aspect-ratio: 9/13; 226 | width: 18%; 227 | margin: auto; 228 | } 229 | 230 | img { 231 | display: block; 232 | object-fit: cover; 233 | height: 100%; 234 | width: 100%; 235 | } 236 | 237 | /* CODROPS TEMPLATE */ 238 | 239 | a { 240 | text-decoration: none; 241 | color: var(--color-link); 242 | outline: none; 243 | cursor: pointer; 244 | } 245 | 246 | a:hover { 247 | color: var(--color-link-hover); 248 | outline: none; 249 | } 250 | 251 | /* Better focus styles from https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible */ 252 | a:focus { 253 | /* Provide a fallback style for browsers 254 | that don't support :focus-visible */ 255 | outline: none; 256 | background: lightgrey; 257 | } 258 | 259 | a:focus:not(:focus-visible) { 260 | /* Remove the focus indicator on mouse-focus for browsers 261 | that do support :focus-visible */ 262 | background: transparent; 263 | } 264 | 265 | a:focus-visible { 266 | /* Draw a very noticeable focus style for 267 | keyboard-focus on browsers that do support 268 | :focus-visible */ 269 | outline: 2px solid #6e6bcd; 270 | background: transparent; 271 | } 272 | 273 | .frame { 274 | width: 100%; 275 | min-height: 5vh; 276 | padding-bottom: 1.5rem; 277 | } 278 | .frame > * { 279 | margin: 1rem 0; 280 | } 281 | @media screen and (min-width: 45em) { 282 | .frame { 283 | width: 100%; 284 | height: 5vh; 285 | display: grid; 286 | padding-top: 1.5rem; 287 | display: flex; 288 | justify-content: space-between; 289 | align-items: center; 290 | } 291 | } 292 | 293 | .frame a, 294 | .frame button { 295 | pointer-events: auto; 296 | } 297 | 298 | .frame a:not(.frame__title-back) { 299 | white-space: nowrap; 300 | overflow: hidden; 301 | position: relative; 302 | display: inline; 303 | } 304 | 305 | .frame a:not(.frame__title-back)::before { 306 | content: ""; 307 | height: 1px; 308 | width: 100%; 309 | background: currentColor; 310 | position: absolute; 311 | top: 90%; 312 | transition: transform 0.3s; 313 | transform-origin: 0% 50%; 314 | } 315 | 316 | .frame a:not(.frame__title-back):hover::before { 317 | transform: scaleX(0); 318 | transform-origin: 100% 50%; 319 | } 320 | 321 | .frame__title { 322 | grid-area: title; 323 | display: flex; 324 | } 325 | 326 | .frame__title-main { 327 | font-size: inherit; 328 | margin: 0; 329 | font-weight: inherit; 330 | } 331 | 332 | .frame__title-back { 333 | position: relative; 334 | display: flex; 335 | align-items: flex-end; 336 | } 337 | 338 | /* .frame__title-back span { 339 | display: none; 340 | } */ 341 | 342 | .frame__title-back svg { 343 | fill: currentColor; 344 | } 345 | 346 | .frame__prev { 347 | grid-area: prev; 348 | } 349 | --------------------------------------------------------------------------------