├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── assets │ ├── images │ │ ├── apple.svg │ │ ├── bag.svg │ │ ├── black.jpg │ │ ├── blue.jpg │ │ ├── chip.jpeg │ │ ├── explore1.jpg │ │ ├── explore2.jpg │ │ ├── frame.png │ │ ├── hero.jpeg │ │ ├── pause.svg │ │ ├── play.svg │ │ ├── replay.svg │ │ ├── right.svg │ │ ├── search.svg │ │ ├── watch.svg │ │ ├── white.jpg │ │ └── yellow.jpg │ ├── react.svg │ └── videos │ │ ├── explore.mp4 │ │ ├── frame.mp4 │ │ ├── hero.mp4 │ │ ├── highlight-first.mp4 │ │ ├── hightlight-fourth.mp4 │ │ ├── hightlight-sec.mp4 │ │ ├── hightlight-third.mp4 │ │ └── smallHero.mp4 └── models │ └── scene.glb ├── src ├── App.css ├── App.jsx ├── components │ ├── Features.jsx │ ├── Footer.jsx │ ├── Hero.jsx │ ├── Highlights.jsx │ ├── HowItWorks.jsx │ ├── IPhone.jsx │ ├── Lights.jsx │ ├── Loader.jsx │ ├── Model.jsx │ ├── ModelView.jsx │ ├── Navbar.jsx │ └── VideoCarousel.jsx ├── constants │ └── index.js ├── index.css ├── main.jsx └── utils │ ├── animations.js │ └── index.js ├── tailwind.config.js └── vite.config.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh','@react-three'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | # Three.js 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Apple Web 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apple_web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@gsap/react": "^2.1.1", 14 | "@react-three/drei": "^9.107.0", 15 | "@react-three/fiber": "^8.16.8", 16 | "gsap": "^3.12.5", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "three": "^0.165.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.2.66", 23 | "@types/react-dom": "^18.2.22", 24 | "@vitejs/plugin-react": "^4.2.1", 25 | "autoprefixer": "^10.4.19", 26 | "eslint": "^8.57.0", 27 | "eslint-plugin-react": "^7.34.1", 28 | "eslint-plugin-react-hooks": "^4.6.0", 29 | "eslint-plugin-react-refresh": "^0.4.6", 30 | "postcss": "^8.4.38", 31 | "tailwindcss": "^3.4.4", 32 | "vite": "^5.3.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/assets/images/apple.svg: -------------------------------------------------------------------------------- 1 | 8 | 13 | -------------------------------------------------------------------------------- /public/assets/images/bag.svg: -------------------------------------------------------------------------------- 1 | 9 | 14 | -------------------------------------------------------------------------------- /public/assets/images/black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/black.jpg -------------------------------------------------------------------------------- /public/assets/images/blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/blue.jpg -------------------------------------------------------------------------------- /public/assets/images/chip.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/chip.jpeg -------------------------------------------------------------------------------- /public/assets/images/explore1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/explore1.jpg -------------------------------------------------------------------------------- /public/assets/images/explore2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/explore2.jpg -------------------------------------------------------------------------------- /public/assets/images/frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/frame.png -------------------------------------------------------------------------------- /public/assets/images/hero.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/hero.jpeg -------------------------------------------------------------------------------- /public/assets/images/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/replay.svg: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/assets/images/right.svg: -------------------------------------------------------------------------------- 1 | 9 | 13 | -------------------------------------------------------------------------------- /public/assets/images/search.svg: -------------------------------------------------------------------------------- 1 | 9 | 14 | -------------------------------------------------------------------------------- /public/assets/images/watch.svg: -------------------------------------------------------------------------------- 1 | 9 | 13 | -------------------------------------------------------------------------------- /public/assets/images/white.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/white.jpg -------------------------------------------------------------------------------- /public/assets/images/yellow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/images/yellow.jpg -------------------------------------------------------------------------------- /public/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/videos/explore.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/explore.mp4 -------------------------------------------------------------------------------- /public/assets/videos/frame.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/frame.mp4 -------------------------------------------------------------------------------- /public/assets/videos/hero.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/hero.mp4 -------------------------------------------------------------------------------- /public/assets/videos/highlight-first.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/highlight-first.mp4 -------------------------------------------------------------------------------- /public/assets/videos/hightlight-fourth.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/hightlight-fourth.mp4 -------------------------------------------------------------------------------- /public/assets/videos/hightlight-sec.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/hightlight-sec.mp4 -------------------------------------------------------------------------------- /public/assets/videos/hightlight-third.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/hightlight-third.mp4 -------------------------------------------------------------------------------- /public/assets/videos/smallHero.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/assets/videos/smallHero.mp4 -------------------------------------------------------------------------------- /public/models/scene.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanula12/threejs/2e8c2773dacb77b649924afd6851c22030717281/public/models/scene.glb -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import Navbar from "./components/Navbar" 2 | import Hero from "./components/Hero" 3 | import Highlights from "./components/Highlights" 4 | import Model from "./components/Model" 5 | import Features from "./components/Features" 6 | import HowItWorks from "./components/HowItWorks" 7 | import Footer from "./components/Footer" 8 | 9 | const App = () => { 10 | 11 | return ( 12 |
13 | < Navbar/> 14 | 15 | 16 | 17 | 18 | 19 |
21 | ) 22 | } 23 | 24 | export default App 25 | -------------------------------------------------------------------------------- /src/components/Features.jsx: -------------------------------------------------------------------------------- 1 | import { useGSAP } from '@gsap/react' 2 | import React, { useRef } from 'react' 3 | import { animateWithGsap } from "../utils/animations" 4 | import { explore1Img, explore2Img, exploreVideo } from "../utils" 5 | import gsap from 'gsap' 6 | const Features = () => { 7 | 8 | const videoRef = useRef(); 9 | 10 | useGSAP(() => { 11 | 12 | gsap.to('#exploreVideo', { 13 | 14 | scrollTrigger: { 15 | trigger: '#exploreVideo', 16 | toggleActions: 'play pause reverse restart ', 17 | start: '-10% bottom', 18 | }, 19 | onComplete: () => { 20 | videoRef.current.play(); 21 | } 22 | }) 23 | 24 | animateWithGsap('#features_title', { 25 | opacity: 1, 26 | y: 0, 27 | 28 | }) 29 | 30 | animateWithGsap('.g_grow', { 31 | scale: 1, 32 | opacity: 1, 33 | ease: 'power1.inOut', 34 | }, 35 | { scrub: 5.5 } 36 | ) 37 | 38 | animateWithGsap('.g_text', { 39 | y: 0, 40 | opacity: 1, 41 | ease: 'power1.inOut', 42 | duration: 1} 43 | 44 | ) 45 | 46 | },[]); 47 | 48 | return ( 49 |
51 |
52 |
53 |

54 | Explore the full story. 55 |

56 |
57 | 58 |
60 |
61 |

iPhone

62 | 63 |

64 | Forged In Titanium

65 |
66 | 67 |
68 |
69 | 74 |
75 | 76 |
77 |
78 |
79 | titanium 81 |
82 |
83 | titanium 85 |
86 |
87 | 88 |
89 |
90 |

91 | iPhone 15 Pro is {''} 92 | 93 | the first iPhone to feature an aerospace-grade titanium design 94 | , 95 | using the same alloy that spacecrafts use for missions to Mars. 96 |

97 |
98 |
99 |

100 | Titanium has one of the best strength-to-weight ratios of any metal, making these our {' '} 101 | 102 | lightest Pro models ever. 103 | 104 | You'll notice the difference the moment you pick one up. 105 |

106 |
107 |
108 |
109 | 110 |
111 | 112 |
113 | 114 |
115 |
116 | ) 117 | } 118 | 119 | export default Features 120 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { footerLinks } from '../constants' 3 | 4 | const Footer = () => { 5 | return ( 6 | 7 | 44 | ) 45 | } 46 | 47 | export default Footer 48 | -------------------------------------------------------------------------------- /src/components/Hero.jsx: -------------------------------------------------------------------------------- 1 | import gsap from "gsap" 2 | import { useGSAP } from "@gsap/react" 3 | import { heroVideo, smallHeroVideo } from "../utils" 4 | import { useEffect, useState } from "react" 5 | const Hero = () => { 6 | const [videoSrc, setVideoSrc] = useState(window.innerWidth < 760 ? smallHeroVideo : heroVideo) 7 | 8 | const handleVideoSrcSet = () => { 9 | if(window.innerWidth < 760) { 10 | setVideoSrc(smallHeroVideo) 11 | } else { 12 | setVideoSrc(heroVideo) 13 | } 14 | } 15 | 16 | useEffect(() => { 17 | window.addEventListener('resize', handleVideoSrcSet); 18 | 19 | return () => { 20 | window.removeEventListener('reisze', handleVideoSrcSet) 21 | } 22 | }, []) 23 | 24 | useGSAP(() => { 25 | gsap.to('#hero', { opacity: 1, delay: 2 }) 26 | gsap.to('#cta', { opacity: 1, y: -50, delay: 2 }) 27 | }, []) 28 | 29 | return ( 30 |
31 |
32 |

iPhone 15 Pro

33 |
34 | 37 |
38 |
39 | 40 |
44 | Buy 45 |

From $199/month or $999

46 |
47 |
48 | ) 49 | } 50 | 51 | export default Hero 52 | -------------------------------------------------------------------------------- /src/components/Highlights.jsx: -------------------------------------------------------------------------------- 1 | import { useGSAP } from "@gsap/react" 2 | import gsap from "gsap" 3 | import { rightImg, watchImg } from "../utils" 4 | import VideoCarousel from "./VideoCarousel" 5 | 6 | const Highlights = () => { 7 | 8 | useGSAP(() => { 9 | gsap.to("#title", { 10 | opacity: 1, 11 | y: 0, 12 | duration: 1.5, 13 | rotate:360, 14 | ease: "power1.out", 15 | }) 16 | 17 | gsap.to(".link", { 18 | opacity: 1, 19 | y: 0, 20 | duration: 1.5, 21 | stagger: 0.25, 22 | 23 | }) 24 | }, []) 25 | return ( 26 |
28 | 29 |
30 | 31 |
32 |

33 | Get the highlights. 34 |

35 | 36 | 37 |
38 | 39 |

Watch the film 40 | watch 41 |

42 |

Watch the event 43 | right 44 |

45 |
46 | 47 |
48 | 49 | 50 | 51 |
52 | 53 |
54 | ) 55 | } 56 | 57 | export default Highlights 58 | -------------------------------------------------------------------------------- /src/components/HowItWorks.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { chipImg, frameImg, frameVideo } from '../utils' 3 | import { useGSAP } from '@gsap/react' 4 | import gsap from 'gsap' 5 | import { animateWithGsap } from '../utils/animations' 6 | 7 | const HowItWorks = () => { 8 | 9 | const videoRef = useRef(); 10 | 11 | useGSAP(() => { 12 | 13 | gsap.from('#chip', { 14 | scrollTrigger: { 15 | trigger: '#chip', 16 | start: 'top 80%', 17 | }, 18 | opacity: 0, 19 | scale: 2, 20 | duration: 2, 21 | ease: 'power1.inOut', 22 | }) 23 | 24 | animateWithGsap('.g_fadeIn', { 25 | opacity: 1, 26 | y: 0, 27 | duration: 1, 28 | ease: 'power2.inOut' 29 | }) 30 | 31 | }, []) 32 | return ( 33 |
34 |
35 |
36 | chip 37 |
38 | 39 |
40 |

41 | A17 Pro chip. 42 |
A monster win for gaming. 43 |

44 | 45 |

46 | It's here. The biggest redesign in the history of Apple GPUs. 47 |

48 |
49 | 50 |
51 |
52 |
53 | frame 58 |
59 |
60 | 63 |
64 |
65 |

Honkai: Star Rail

66 |
67 | 68 |
69 |
70 |

71 | A17 Pro is an entirely new class of iPhone chip that delivers our {' '} 72 | 73 | best graphic performance by far 74 | . 75 |

76 | 77 |

78 | Mobile {' '} 79 | 80 | games will look and feel so immersive 81 | , 82 | with incredibly detailed environments and characters. 83 |

84 |
85 | 86 | 87 |
88 |

New

89 |

Pro-class GPU

90 |

with 6 cores

91 |
92 |
93 |
94 |
95 | ) 96 | } 97 | 98 | export default HowItWorks 99 | -------------------------------------------------------------------------------- /src/components/IPhone.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Auto-generated by: https://github.com/pmndrs/gltfjsx 3 | Author: polyman (https://sketchfab.com/Polyman_3D) 4 | License: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/) 5 | Source: https://sketchfab.com/3d-models/apple-iphone-15-pro-max-black-df17520841214c1792fb8a44c6783ee7 6 | Title: Apple iPhone 15 Pro Max Black 7 | */ 8 | 9 | import * as THREE from 'three'; 10 | import React, { useEffect, useRef } from "react"; 11 | import { useGLTF, useTexture } from "@react-three/drei"; 12 | 13 | function Model(props) { 14 | const { nodes, materials } = useGLTF("/models/scene.glb"); 15 | 16 | const texture = useTexture(props.item.img); 17 | 18 | useEffect(() => { 19 | Object.entries(materials).map((material) => { 20 | // these are the material names that can't be changed color 21 | if ( 22 | material[0] !== "zFdeDaGNRwzccye" && 23 | material[0] !== "ujsvqBWRMnqdwPx" && 24 | material[0] !== "hUlRcbieVuIiOXG" && 25 | material[0] !== "jlzuBkUzuJqgiAK" && 26 | material[0] !== "xNrofRCqOXXHVZt" 27 | ) { 28 | material[1].color = new THREE.Color(props.item.color[0]); 29 | } 30 | material[1].needsUpdate = true; 31 | }); 32 | }, [materials, props.item]); 33 | 34 | return ( 35 | 36 | 43 | 50 | 57 | 64 | 71 | 78 | 85 | 92 | 99 | 106 | 113 | 120 | 127 | 134 | 141 | 148 | 149 | 150 | 157 | 164 | 171 | 178 | 185 | 192 | 199 | 206 | 213 | 220 | 227 | 234 | 241 | 248 | 255 | 256 | ); 257 | } 258 | 259 | export default Model; 260 | 261 | useGLTF.preload("/models/scene.glb"); -------------------------------------------------------------------------------- /src/components/Lights.jsx: -------------------------------------------------------------------------------- 1 | import { Environment, Lightformer } from "@react-three/drei"; 2 | 3 | const Lights = () => { 4 | return ( 5 | // group different lights and lightformers. We can use group to organize lights, cameras, meshes, and other objects in the scene. 6 | 7 | {/** 8 | * @description Environment is used to create a background environment for the scene 9 | * https://github.com/pmndrs/drei?tab=readme-ov-file#environment 10 | */} 11 | 12 | 13 | {/** 14 | * @description Lightformer used to create custom lights with various shapes and properties in a 3D scene. 15 | * https://github.com/pmndrs/drei?tab=readme-ov-file#lightformer 16 | */} 17 | 24 | 31 | 38 | 39 | 40 | 41 | {/** 42 | * @description spotLight is used to create a light source positioned at a specific point 43 | * in the scene that emits light in a specific direction. 44 | * https://threejs.org/docs/#api/en/lights/SpotLight 45 | */} 46 | 54 | 62 | 69 | 70 | ); 71 | }; 72 | 73 | export default Lights; -------------------------------------------------------------------------------- /src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import { Html } from '@react-three/drei' 2 | import React from 'react' 3 | 4 | const Loader = () => { 5 | return ( 6 | 7 |
8 |
9 | Loading... 10 |
11 |
12 | 13 | ) 14 | } 15 | 16 | export default Loader -------------------------------------------------------------------------------- /src/components/Model.jsx: -------------------------------------------------------------------------------- 1 | import { useGSAP } from "@gsap/react" 2 | import gsap from "gsap"; 3 | import ModelView from "./ModelView"; 4 | import { useEffect, useRef, useState } from "react"; 5 | import { yellowImg } from "../utils"; 6 | 7 | import * as THREE from 'three'; 8 | import { Canvas } from "@react-three/fiber"; 9 | import { View } from "@react-three/drei"; 10 | import { models, sizes } from "../constants"; 11 | import { animateWithGsapTimeline } from "../utils/animations"; 12 | 13 | const Model = () => { 14 | const [size, setSize] = useState('small'); 15 | const [model, setModel] = useState({ 16 | title: 'iPhone 15 Pro in Natural Titanium', 17 | color: ['#8F8A81', '#FFE7B9', '#6F6C64'], 18 | img: yellowImg, 19 | }) 20 | 21 | // camera control for the model view 22 | const cameraControlSmall = useRef(); 23 | const cameraControlLarge = useRef(); 24 | 25 | // model 26 | const small = useRef(new THREE.Group()); 27 | const large = useRef(new THREE.Group()); 28 | 29 | // rotation 30 | const [smallRotation, setSmallRotation] = useState(0); 31 | const [largeRotation, setLargeRotation] = useState(0); 32 | 33 | const tl = gsap.timeline(); 34 | 35 | useEffect(() => { 36 | if(size === 'large') { 37 | animateWithGsapTimeline(tl, small, smallRotation, '#view1', '#view2', { 38 | transform: 'translateX(-100%)', 39 | duration: 2 40 | }) 41 | } 42 | 43 | if(size ==='small') { 44 | animateWithGsapTimeline(tl, large, largeRotation, '#view2', '#view1', { 45 | transform: 'translateX(0)', 46 | duration: 2 47 | }) 48 | } 49 | }, [size]) 50 | 51 | useGSAP(() => { 52 | gsap.to('#heading', { y: 0, opacity: 1 }) 53 | }, []); 54 | 55 | return ( 56 |
57 |
58 |

59 | Take a closer look. 60 |

61 | 62 |
63 |
64 | 73 | 74 | 83 | 84 | 96 | 97 | 98 |
99 | 100 |
101 |

{model.title}

102 | 103 |
104 |
    105 | {models.map((item, i) => ( 106 |
  • setModel(item)} /> 107 | ))} 108 |
109 | 110 | 117 |
118 |
119 |
120 |
121 |
122 | ) 123 | } 124 | 125 | export default Model -------------------------------------------------------------------------------- /src/components/ModelView.jsx: -------------------------------------------------------------------------------- 1 | import { Html, OrbitControls, PerspectiveCamera, View } from "@react-three/drei" 2 | 3 | import * as THREE from 'three' 4 | import Lights from './Lights'; 5 | import Loader from './Loader'; 6 | import IPhone from './IPhone'; 7 | import { Suspense } from "react"; 8 | 9 | const ModelView = ({ index, groupRef, gsapType, controlRef, setRotationState, size, item }) => { 10 | return ( 11 | 16 | {/* Ambient Light */} 17 | 18 | 19 | 20 | 21 | 22 | 23 | setRotationState(controlRef.current.getAzimuthalAngle())} 31 | /> 32 | 33 | 34 | }> 35 | 40 | 41 | 42 | 43 | ) 44 | } 45 | 46 | export default ModelView -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { navLists } from "../constants" 2 | import { appleImg, bagImg, searchImg } from "../utils" 3 | const Navbar = () => { 4 | return ( 5 |
7 | 30 |
31 | ) 32 | } 33 | 34 | export default Navbar 35 | -------------------------------------------------------------------------------- /src/components/VideoCarousel.jsx: -------------------------------------------------------------------------------- 1 | import gsap from "gsap"; 2 | import { useGSAP } from "@gsap/react"; 3 | import { ScrollTrigger } from "gsap/all"; 4 | gsap.registerPlugin(ScrollTrigger); 5 | import { useEffect, useRef, useState } from "react"; 6 | 7 | import { hightlightsSlides } from "../constants"; 8 | import { pauseImg, playImg, replayImg } from "../utils"; 9 | 10 | const VideoCarousel = () => { 11 | const videoRef = useRef([]); 12 | const videoSpanRef = useRef([]); 13 | const videoDivRef = useRef([]); 14 | 15 | // video and indicator 16 | const [video, setVideo] = useState({ 17 | isEnd: false, 18 | startPlay: false, 19 | videoId: 0, 20 | isLastVideo: false, 21 | isPlaying: false, 22 | }); 23 | 24 | const [loadedData, setLoadedData] = useState([]); 25 | const { isEnd, isLastVideo, startPlay, videoId, isPlaying } = video; 26 | 27 | useGSAP(() => { 28 | // slider animation to move the video out of the screen and bring the next video in 29 | gsap.to("#slider", { 30 | transform: `translateX(${-100 * videoId}%)`, 31 | duration: 2, 32 | ease: "power2.inOut", // show visualizer https://gsap.com/docs/v3/Eases 33 | }); 34 | 35 | // video animation to play the video when it is in the view 36 | gsap.to("#video", { 37 | scrollTrigger: { 38 | trigger: "#video", 39 | toggleActions: "restart none none none", 40 | }, 41 | onComplete: () => { 42 | setVideo((pre) => ({ 43 | ...pre, 44 | startPlay: true, 45 | isPlaying: true, 46 | })); 47 | }, 48 | }); 49 | }, [isEnd, videoId]); 50 | 51 | useEffect(() => { 52 | let currentProgress = 0; 53 | let span = videoSpanRef.current; 54 | 55 | if (span[videoId]) { 56 | // animation to move the indicator 57 | let anim = gsap.to(span[videoId], { 58 | onUpdate: () => { 59 | // get the progress of the video 60 | const progress = Math.ceil(anim.progress() * 100); 61 | 62 | if (progress != currentProgress) { 63 | currentProgress = progress; 64 | 65 | // set the width of the progress bar 66 | gsap.to(videoDivRef.current[videoId], { 67 | width: 68 | window.innerWidth < 760 69 | ? "10vw" // mobile 70 | : window.innerWidth < 1200 71 | ? "10vw" // tablet 72 | : "4vw", // laptop 73 | }); 74 | 75 | // set the background color of the progress bar 76 | gsap.to(span[videoId], { 77 | width: `${currentProgress}%`, 78 | backgroundColor: "white", 79 | }); 80 | } 81 | }, 82 | 83 | // when the video is ended, replace the progress bar with the indicator and change the background color 84 | onComplete: () => { 85 | if (isPlaying) { 86 | gsap.to(videoDivRef.current[videoId], { 87 | width: "12px", 88 | }); 89 | gsap.to(span[videoId], { 90 | backgroundColor: "#afafaf", 91 | }); 92 | } 93 | }, 94 | }); 95 | 96 | if (videoId == 0) { 97 | anim.restart(); 98 | } 99 | 100 | // update the progress bar 101 | const animUpdate = () => { 102 | anim.progress( 103 | videoRef.current[videoId].currentTime / 104 | hightlightsSlides[videoId].videoDuration 105 | ); 106 | }; 107 | 108 | if (isPlaying) { 109 | // ticker to update the progress bar 110 | gsap.ticker.add(animUpdate); 111 | } else { 112 | // remove the ticker when the video is paused (progress bar is stopped) 113 | gsap.ticker.remove(animUpdate); 114 | } 115 | } 116 | }, [videoId, startPlay]); 117 | 118 | useEffect(() => { 119 | if (loadedData.length > 3) { 120 | if (!isPlaying) { 121 | videoRef.current[videoId].pause(); 122 | } else { 123 | startPlay && videoRef.current[videoId].play(); 124 | } 125 | } 126 | }, [startPlay, videoId, isPlaying, loadedData]); 127 | 128 | // vd id is the id for every video until id becomes number 3 129 | const handleProcess = (type, i) => { 130 | switch (type) { 131 | case "video-end": 132 | setVideo((pre) => ({ ...pre, isEnd: true, videoId: i + 1 })); 133 | break; 134 | 135 | case "video-last": 136 | setVideo((pre) => ({ ...pre, isLastVideo: true })); 137 | break; 138 | 139 | case "video-reset": 140 | setVideo((pre) => ({ ...pre, videoId: 0, isLastVideo: false })); 141 | break; 142 | 143 | case "pause": 144 | setVideo((pre) => ({ ...pre, isPlaying: !pre.isPlaying })); 145 | break; 146 | 147 | case "play": 148 | setVideo((pre) => ({ ...pre, isPlaying: !pre.isPlaying })); 149 | break; 150 | 151 | default: 152 | return video; 153 | } 154 | }; 155 | 156 | const handleLoadedMetaData = (i, e) => setLoadedData((pre) => [...pre, e]); 157 | 158 | return ( 159 | <> 160 |
161 | {hightlightsSlides.map((list, i) => ( 162 |
163 |
164 |
165 | 186 |
187 | 188 |
189 | {list.textLists.map((text, i) => ( 190 |

191 | {text} 192 |

193 | ))} 194 |
195 |
196 |
197 | ))} 198 |
199 | 200 |
201 |
202 | {videoRef.current.map((_, i) => ( 203 | (videoDivRef.current[i] = el)} 207 | > 208 | (videoSpanRef.current[i] = el)} 211 | /> 212 | 213 | ))} 214 |
215 | 216 | 229 |
230 | 231 | ); 232 | }; 233 | 234 | export default VideoCarousel; 235 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | blackImg, 3 | blueImg, 4 | highlightFirstVideo, 5 | highlightFourthVideo, 6 | highlightSecondVideo, 7 | highlightThirdVideo, 8 | whiteImg, 9 | yellowImg, 10 | } from "../utils"; 11 | 12 | export const navLists = ["Store", "Mac", "iPhone", "Support"]; 13 | 14 | export const hightlightsSlides = [ 15 | { 16 | id: 1, 17 | textLists: [ 18 | "Enter A17 Pro.", 19 | "Game‑changing chip.", 20 | "Groundbreaking performance.", 21 | ], 22 | video: highlightFirstVideo, 23 | videoDuration: 4, 24 | }, 25 | { 26 | id: 2, 27 | textLists: ["Titanium.", "So strong. So light. So Pro."], 28 | video: highlightSecondVideo, 29 | videoDuration: 5, 30 | }, 31 | { 32 | id: 3, 33 | textLists: [ 34 | "iPhone 15 Pro Max has the", 35 | "longest optical zoom in", 36 | "iPhone ever. Far out.", 37 | ], 38 | video: highlightThirdVideo, 39 | videoDuration: 2, 40 | }, 41 | { 42 | id: 4, 43 | textLists: ["All-new Action button.", "What will yours do?."], 44 | video: highlightFourthVideo, 45 | videoDuration: 3.63, 46 | }, 47 | ]; 48 | 49 | export const models = [ 50 | { 51 | id: 1, 52 | title: "iPhone 15 Pro in Natural Titanium", 53 | color: ["#8F8A81", "#ffe7b9", "#6f6c64"], 54 | img: yellowImg, 55 | }, 56 | { 57 | id: 2, 58 | title: "iPhone 15 Pro in Blue Titanium", 59 | color: ["#53596E", "#6395ff", "#21242e"], 60 | img: blueImg, 61 | }, 62 | { 63 | id: 3, 64 | title: "iPhone 15 Pro in White Titanium", 65 | color: ["#C9C8C2", "#ffffff", "#C9C8C2"], 66 | img: whiteImg, 67 | }, 68 | { 69 | id: 4, 70 | title: "iPhone 15 Pro in Black Titanium", 71 | color: ["#454749", "#3b3b3b", "#181819"], 72 | img: blackImg, 73 | }, 74 | ]; 75 | 76 | export const sizes = [ 77 | { label: '6.1"', value: "small" }, 78 | { label: '6.7"', value: "large" }, 79 | ]; 80 | 81 | export const footerLinks = [ 82 | "Privacy Policy", 83 | "Terms of Use", 84 | "Sales Policy", 85 | "Legal", 86 | "Site Map", 87 | ]; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | body { 12 | color: white; 13 | width: 100dvw; 14 | overflow-x: hidden; 15 | height: 100%; 16 | background: #000; 17 | border-color: #3b3b3b; 18 | user-select: none; 19 | } 20 | 21 | canvas { 22 | touch-action: none; 23 | } 24 | 25 | .screen-max-width { 26 | margin-inline-start: auto; 27 | margin-inline-end: auto; 28 | position: relative; 29 | max-width: 1120px; 30 | } 31 | 32 | @layer utilities { 33 | .flex-center { 34 | @apply flex items-center justify-center 35 | } 36 | 37 | .nav-height { 38 | @apply h-[calc(100vh-60px)] 39 | } 40 | 41 | .btn { 42 | @apply px-5 py-2 rounded-3xl bg-blue my-5 hover:bg-transparent border border-transparent hover:border hover:text-blue hover:border-blue 43 | } 44 | 45 | .color-container { 46 | @apply flex items-center justify-center px-4 py-4 rounded-full bg-gray-300 backdrop-blur 47 | } 48 | 49 | .size-btn-container { 50 | @apply flex items-center justify-center p-1 rounded-full bg-gray-300 backdrop-blur ml-3 gap-1 51 | } 52 | 53 | .size-btn { 54 | @apply w-10 h-10 text-sm flex justify-center items-center bg-white text-black rounded-full transition-all 55 | } 56 | 57 | .common-padding { 58 | @apply sm:py-32 py-20 sm:px-10 px-5 59 | } 60 | 61 | .section-heading { 62 | @apply text-gray lg:text-6xl md:text-5xl text-3xl lg:mb-0 mb-5 font-medium opacity-0 translate-y-20 63 | } 64 | 65 | .feature-text { 66 | @apply text-gray max-w-md text-lg md:text-xl font-semibold opacity-0 translate-y-[100px] 67 | } 68 | 69 | .feature-text-container { 70 | @apply w-full flex-center flex-col md:flex-row mt-10 md:mt-16 gap-5 71 | } 72 | 73 | .feature-video { 74 | @apply w-full h-full object-cover object-center scale-150 opacity-0 75 | } 76 | 77 | .feature-video-container { 78 | @apply w-full flex flex-col md:flex-row gap-5 items-center 79 | } 80 | 81 | .link { 82 | @apply text-blue hover:underline cursor-pointer flex items-center text-xl opacity-0 translate-y-20 83 | } 84 | 85 | .control-btn { 86 | @apply ml-4 p-4 rounded-full bg-gray-300 backdrop-blur flex-center 87 | } 88 | 89 | .hero-title { 90 | @apply text-center font-semibold text-3xl text-gray-100 opacity-0 max-md:mb-10 91 | } 92 | 93 | .hiw-title { 94 | @apply text-4xl md:text-7xl font-semibold text-center 95 | } 96 | 97 | .hiw-subtitle { 98 | @apply text-gray font-semibold text-xl md:text-2xl py-10 text-center 99 | } 100 | 101 | .hiw-video { 102 | @apply absolute w-[95%] h-[90%] rounded-[56px] overflow-hidden 103 | } 104 | 105 | .hiw-text-container { 106 | @apply flex md:flex-row flex-col justify-between items-start gap-24 107 | } 108 | 109 | .hiw-text { 110 | @apply text-gray text-xl font-normal md:font-semibold 111 | } 112 | 113 | .hiw-bigtext { 114 | @apply text-white text-3xl md:text-5xl font-normal md:font-semibold my-2 115 | } 116 | 117 | .video-carousel_container { 118 | @apply relative sm:w-[70vw] w-[88vw] md:h-[70vh] sm:h-[50vh] h-[35vh] 119 | } 120 | 121 | .g_fadeIn { 122 | @apply opacity-0 translate-y-[100px] 123 | } 124 | } -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/utils/animations.js: -------------------------------------------------------------------------------- 1 | import gsap from "gsap" 2 | 3 | import { ScrollTrigger } from "gsap/all" 4 | gsap.registerPlugin(ScrollTrigger); 5 | 6 | export const animateWithGsap = (target, animationProps, scrollProps) => { 7 | gsap.to(target, { 8 | ...animationProps, 9 | scrollTrigger: { 10 | trigger: target, 11 | toggleActions: 'restart reverse restart reverse', 12 | start: 'top 85%', 13 | ...scrollProps, 14 | } 15 | }) 16 | } 17 | 18 | export const animateWithGsapTimeline = (timeline, rotationRef, rotationState, firstTarget, secondTarget, animationProps) => { 19 | timeline.to(rotationRef.current.rotation, { 20 | y: rotationState, 21 | duration: 1, 22 | ease: 'power2.inOut' 23 | }) 24 | 25 | timeline.to( 26 | firstTarget, 27 | { 28 | ...animationProps, 29 | ease: 'power2.inOut' 30 | }, 31 | '<' 32 | ) 33 | 34 | timeline.to( 35 | secondTarget, 36 | { 37 | ...animationProps, 38 | ease: 'power2.inOut' 39 | }, 40 | '<' 41 | ) 42 | } -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import hero from "/assets/images/hero.jpeg"; 2 | 3 | export const heroImg = hero; 4 | 5 | import hmv from "/assets/videos/hero.mp4"; 6 | import smallmv from "/assets/videos/smallHero.mp4"; 7 | import highlightFirstmv from "/assets/videos/highlight-first.mp4"; 8 | import highlightSectmv from "/assets/videos/hightlight-third.mp4"; 9 | import highlightThirdmv from "/assets/videos/hightlight-sec.mp4"; 10 | import highlightFourthmv from "/assets/videos/hightlight-fourth.mp4"; 11 | import exploremv from "/assets/videos/explore.mp4"; 12 | import framemv from "/assets/videos/frame.mp4"; 13 | 14 | import apple from "/assets/images/apple.svg"; 15 | import search from "/assets/images/search.svg"; 16 | import bag from "/assets/images/bag.svg"; 17 | import watch from "/assets/images/watch.svg"; 18 | import right from "/assets/images/right.svg"; 19 | import replay from "/assets/images/replay.svg"; 20 | import play from "/assets/images/play.svg"; 21 | import pause from "/assets/images/pause.svg"; 22 | 23 | import yellow from "/assets/images/yellow.jpg"; 24 | import blue from "/assets/images/blue.jpg"; 25 | import white from "/assets/images/white.jpg"; 26 | import black from "/assets/images/black.jpg"; 27 | import explore1 from "/assets/images/explore1.jpg"; 28 | import explore2 from "/assets/images/explore2.jpg"; 29 | import chip from "/assets/images/chip.jpeg"; 30 | import frame from "/assets/images/frame.png"; 31 | 32 | export const heroVideo = hmv; 33 | export const smallHeroVideo = smallmv; 34 | export const highlightFirstVideo = highlightFirstmv; 35 | export const highlightSecondVideo = highlightSectmv; 36 | export const highlightThirdVideo = highlightThirdmv; 37 | export const highlightFourthVideo = highlightFourthmv; 38 | export const exploreVideo = exploremv; 39 | export const frameVideo = framemv; 40 | 41 | export const appleImg = apple; 42 | export const searchImg = search; 43 | export const bagImg = bag; 44 | export const watchImg = watch; 45 | export const rightImg = right; 46 | export const replayImg = replay; 47 | export const playImg = play; 48 | export const pauseImg = pause; 49 | 50 | export const yellowImg = yellow; 51 | export const blueImg = blue; 52 | export const whiteImg = white; 53 | export const blackImg = black; 54 | export const explore1Img = explore1; 55 | export const explore2Img = explore2; 56 | export const chipImg = chip; 57 | export const frameImg = frame; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | blue: "#2997FF", 11 | gray: { 12 | DEFAULT: "#86868b", 13 | 100: "#94928d", 14 | 200: "#afafaf", 15 | 300: "#42424570", 16 | }, 17 | zinc: "#101010", 18 | } 19 | }, 20 | }, 21 | plugins: [], 22 | } 23 | 24 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------