diff --git a/src/components/HeroSection.module.css b/src/components/HeroSection.module.css index 8c5e8ef..c107a4f 100644 --- a/src/components/HeroSection.module.css +++ b/src/components/HeroSection.module.css @@ -26,12 +26,9 @@ background: transparent; color: var(--color-blue-700); cursor: pointer; - transition: color 0.2s ease, transform 0.2s ease; -} - -.iconButton:hover { - color: var(--color-blue-500); - transform: translateY(-1px); + transition: + color var(--motion-duration-fast) ease, + transform var(--motion-duration-fast) var(--motion-ease-out); } .iconButton:focus-visible { @@ -44,10 +41,6 @@ color: var(--text-primary); } -[data-theme="dark"] .iconButton:hover { - color: var(--accent-default); -} - .controlIcon, .themeIcon { color: currentColor; @@ -202,11 +195,7 @@ align-items: center; gap: 8px; color: inherit; - transition: opacity 0.2s ease; -} - -.contactItem:hover { - opacity: 0.75; + transition: opacity var(--motion-duration-fast) ease; } .contactItem:focus-visible { @@ -230,3 +219,121 @@ background: var(--color-blue-500); opacity: 0.7; } + +@media (max-width: 1024px) { + .topBar { + padding: 48px var(--layout-page-gutter) 0; + } + + .heroContent { + padding: 72px var(--layout-page-gutter) 0; + } + + .heroTitle { + font-size: 54px; + line-height: 64px; + } + + .heroButton { + width: 156px; + } +} + +@media (max-width: 640px) { + .heroContentWrapper { + min-height: 760px; + height: auto; + } + + .topBar { + padding-top: 24px; + } + + .iconButton { + width: 40px; + height: 40px; + } + + .heroContent { + min-height: 760px; + padding: 82px var(--layout-page-gutter) 48px; + } + + .avatarWrapper { + margin-bottom: 20px; + } + + .avatarRingOuter { + padding: 4px; + border-width: 2px; + } + + .avatarContainer { + width: 148px; + height: 148px; + } + + .heroTitle { + max-width: 300px; + margin-bottom: 10px; + font-size: 40px; + line-height: 48px; + } + + .heroSubtitle { + max-width: 280px; + margin-bottom: 28px; + font-size: 12px; + line-height: 18px; + } + + .actionButtons { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + width: 100%; + max-width: 360px; + gap: 8px; + margin-bottom: 28px; + } + + .heroButton { + width: auto; + min-width: 0; + height: 48px; + padding: 0 12px; + border-width: 2px; + font-size: 14px; + } + + .contactBar { + flex-direction: column; + gap: 8px; + max-width: 100%; + font-size: 12px; + line-height: 18px; + } + + .contactItem { + max-width: 100%; + overflow-wrap: anywhere; + } + + .contactDivider { + display: none; + } +} + +@media (hover: hover) and (pointer: fine) { + .iconButton:hover { + color: var(--color-blue-500); + transform: translateY(-1px); + } + + .contactItem:hover { + opacity: 0.75; + } + + [data-theme="dark"] .iconButton:hover { + color: var(--accent-default); + } +} diff --git a/src/components/ScenicHeroJourney.module.css b/src/components/ScenicHeroJourney.module.css index ddc1406..0fc9576 100644 --- a/src/components/ScenicHeroJourney.module.css +++ b/src/components/ScenicHeroJourney.module.css @@ -2,24 +2,22 @@ position: relative; width: 100%; max-width: 1440px; - height: 1730px; + height: auto; overflow: hidden; background-color: var(--bg-page); } -.artworkContainer { - position: absolute; - top: 0; - left: 0; +.heroStage { + position: relative; width: 100%; - height: 1576px; - z-index: 1; - pointer-events: none; + height: 680px; + overflow: hidden; + background: var(--bg-page); } -.artworkImage { - object-fit: contain; - object-position: top center; - width: 100%; - height: 1576px; +@media (max-width: 640px) { + .heroStage { + min-height: 760px; + height: auto; + } } diff --git a/src/components/ScenicHeroJourney.tsx b/src/components/ScenicHeroJourney.tsx index b2dc4ea..4e0075f 100644 --- a/src/components/ScenicHeroJourney.tsx +++ b/src/components/ScenicHeroJourney.tsx @@ -1,37 +1,19 @@ 'use client'; import React from 'react'; -import Image from 'next/image'; -import { useTheme } from '@/context/ThemeContext'; import { HeroSection } from './HeroSection'; import { RiverJourney } from './RiverJourney'; +import { HeroLandscapeScene } from './scenes/HeroLandscapeScene'; import styles from './ScenicHeroJourney.module.css'; export const ScenicHeroJourney: React.FC = () => { - const { theme } = useTheme(); - return (
- {/* Seamless Continuous Landscape & River Artwork */} -
- Mountain landscape to river journey artwork -
+
+ + +
- {/* Hero Section Component */} - - - {/* About Me River Journey Component */}
); diff --git a/src/components/scenes/HeroLandscapeScene.tsx b/src/components/scenes/HeroLandscapeScene.tsx new file mode 100644 index 0000000..90e02ae --- /dev/null +++ b/src/components/scenes/HeroLandscapeScene.tsx @@ -0,0 +1,150 @@ +'use client'; + +import * as THREE from 'three'; +import { useTheme } from '@/context/ThemeContext'; +import { ThreeCanvas, type SceneFactory } from './ThreeCanvas'; +import { + addSoftLights, + createMesa, + createTree, + fitOrthographicCamera, + palettes, +} from './scenePrimitives'; +import styles from './Scenes.module.css'; + +const createHeroScene: SceneFactory = ({ pointer, theme, reduceMotion }) => { + const palette = palettes[theme]; + const scene = new THREE.Scene(); + const camera = new THREE.OrthographicCamera(-12, 12, 6, -6, 0.1, 50); + const cameraBase = new THREE.Vector3(0, 8.5, 17); + camera.position.copy(cameraBase); + camera.lookAt(0, 2.2, 0); + addSoftLights(scene, theme, 15); + + const world = new THREE.Group(); + scene.add(world); + + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(30, 18), + new THREE.MeshStandardMaterial({ color: palette.ground, roughness: 1 }), + ); + ground.rotation.x = -Math.PI / 2; + ground.position.set(0, -0.03, 1); + ground.receiveShadow = true; + world.add(ground); + + const clearing = new THREE.Mesh( + new THREE.PlaneGeometry(7.6, 4.6), + new THREE.MeshStandardMaterial({ + color: palette.water, + roughness: 0.72, + transparent: true, + opacity: theme === 'dark' ? 0.26 : 0.18, + }), + ); + clearing.rotation.x = -Math.PI / 2; + clearing.position.set(0, 0.015, 1.2); + world.add(clearing); + + const secondaryMountains = new THREE.Group(); + world.add(secondaryMountains); + const mountainData = [ + [-10.6, -1.7, 4.6, 4.8, 4.2, 0], [-7.2, -2.7, 4.4, 4.4, 4.3, 0], + [-4.3, -3.6, 2.7, 3.0, 2.7, 1], [-11.1, 3.3, 3.3, 3.6, 2.8, 1], + [-8.8, 4.2, 3.2, 3.0, 2.5, 1], [10.2, -2.1, 4.7, 4.7, 4.3, 0], + [6.8, -3.1, 4.0, 4.1, 4.0, 0], [4.2, -3.8, 2.5, 2.7, 2.6, 1], + [10.8, 3.6, 3.8, 3.6, 3.1, 1], [8.1, 4.3, 3.0, 3.1, 2.5, 1], + ] as const; + + mountainData.forEach(([x, z, width, height, depth, secondary]) => { + const mesa = createMesa(width, height, depth, palette); + mesa.position.set(x, 0, z); + (secondary ? secondaryMountains : world).add(mesa); + }); + + const secondaryTrees = new THREE.Group(); + world.add(secondaryTrees); + const treeData = [ + [-5.6, -0.3, 1, 0], [-4.6, 0.2, 0.85, 0], [-3.8, -1.4, 0.72, 1], + [-8.2, 2.0, 0.78, 1], [-7.1, 3.0, 1.0, 0], [-5.8, 3.5, 0.72, 0], + [5.7, -0.4, 1.0, 0], [4.7, 0.2, 0.82, 0], [3.9, -1.5, 0.7, 1], + [8.4, 2.2, 0.82, 1], [7.2, 3.2, 1.0, 0], [5.9, 3.8, 0.72, 0], + ] as const; + const animatedTrees: THREE.Group[] = []; + treeData.forEach(([x, z, scale, secondary]) => { + const tree = createTree(palette, scale, secondary === 1); + tree.position.set(x, 0, z); + (secondary ? secondaryTrees : world).add(tree); + animatedTrees.push(tree); + }); + + const celestial = new THREE.Group(); + let celestialBaseY = 6.25; + celestial.position.set(-5.8, 6.25, -1.5); + celestial.lookAt(camera.position); + scene.add(celestial); + + if (theme === 'light') { + const sun = new THREE.Mesh( + new THREE.CircleGeometry(0.62, 32), + new THREE.MeshBasicMaterial({ color: 0xffd75a, toneMapped: false }), + ); + celestial.add(sun); + for (let index = 0; index < 12; index += 1) { + const angle = (index / 12) * Math.PI * 2; + const ray = new THREE.Mesh( + new THREE.PlaneGeometry(0.13, 0.42), + new THREE.MeshBasicMaterial({ color: 0xffd75a, toneMapped: false }), + ); + ray.position.set(Math.sin(angle) * 1.05, Math.cos(angle) * 1.05, 0); + ray.rotation.z = -angle; + celestial.add(ray); + } + } else { + const moon = new THREE.Mesh( + new THREE.SphereGeometry(0.66, 24, 16), + new THREE.MeshStandardMaterial({ + color: palette.snow, + emissive: palette.accent, + emissiveIntensity: 0.22, + roughness: 0.8, + }), + ); + celestial.add(moon); + const moonLight = new THREE.PointLight(palette.accent, 7, 16, 2); + celestial.add(moonLight); + } + + return { + scene, + camera, + resize: (width, height) => { + const mobile = width < 640; + const tablet = width < 1024; + secondaryMountains.visible = !mobile; + secondaryTrees.visible = !mobile; + world.scale.setScalar(mobile ? 0.9 : tablet ? 0.96 : 1); + celestial.position.x = mobile ? -2.35 : tablet ? -4.1 : -5.8; + celestialBaseY = mobile ? 6.8 : 6.25; + celestial.position.y = celestialBaseY; + fitOrthographicCamera(camera, width, height, mobile ? 14.5 : tablet ? 12.5 : 11.5); + }, + update: (elapsed) => { + celestial.position.y = celestialBaseY + Math.sin(elapsed * 0.55) * 0.08; + celestial.rotation.z = theme === 'light' ? elapsed * 0.025 : 0; + animatedTrees.forEach((tree, index) => { + tree.rotation.z = Math.sin(elapsed * 0.7 + index) * 0.006; + }); + if (!reduceMotion) { + camera.position.x += (cameraBase.x + pointer.x * 0.2 - camera.position.x) * 0.035; + camera.position.y += (cameraBase.y + pointer.y * 0.1 - camera.position.y) * 0.035; + camera.lookAt(0, 2.2, 0); + } + }, + }; +}; + +export function HeroLandscapeScene() { + const { theme } = useTheme(); + return ; +} diff --git a/src/components/scenes/Scenes.module.css b/src/components/scenes/Scenes.module.css new file mode 100644 index 0000000..d03fce9 --- /dev/null +++ b/src/components/scenes/Scenes.module.css @@ -0,0 +1,39 @@ +.heroScene, +.aboutScene { + position: absolute; + inset: 0; + overflow: hidden; + background: var(--bg-page); + pointer-events: none; +} + +.heroScene { + z-index: 0; +} + +.heroScene::after { + content: ''; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 50% 42%, var(--bg-page) 0 14%, transparent 44%), + linear-gradient(to bottom, transparent 68%, var(--bg-page) 100%); + opacity: 0.42; +} + +.heroScene canvas, +.aboutScene canvas { + display: block; + width: 100%; + height: 100%; +} + +.aboutScene { + z-index: 0; +} + +@media (max-width: 640px) { + .heroScene::after { + opacity: 0.62; + } +} diff --git a/src/styles/globals.css b/src/styles/globals.css index 1b97788..599c8d1 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -142,7 +142,12 @@ body { font-weight: 600; cursor: pointer; text-decoration: none; - transition: all 0.2s ease; + transition: + color var(--motion-duration-fast) ease, + background-color var(--motion-duration-fast) ease, + border-color var(--motion-duration-fast) ease, + box-shadow var(--motion-duration-default) ease, + transform var(--motion-duration-fast) var(--motion-ease-out); border: 1px solid transparent; } diff --git a/src/styles/tokens.css b/src/styles/tokens.css index 545a9d6..c70a616 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -81,6 +81,10 @@ --line-height-label-small: 18px; --line-height-button: 20px; --letter-spacing-eyebrow: 0.16em; + --motion-duration-fast: 140ms; + --motion-duration-default: 200ms; + --motion-ease-out: cubic-bezier(0.23, 1, 0.32, 1); + --motion-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* Semantic Tokens (Light) */ --bg-page: #FFFFFF;