From 459e1d271a814c7ce47eb77cb506333cc579fbdf Mon Sep 17 00:00:00 2001 From: tuyenio Date: Tue, 23 Jun 2026 11:05:00 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20port=20component=20n=E1=BB=81n=20hero?= =?UTF-8?q?=20(shader=20+=20panorama)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/hero-panorama-background.tsx | 111 ++++++++++++++++++ .../components/ui/hero-shader-background.tsx | 65 ++++++++++ 2 files changed, 176 insertions(+) create mode 100644 src/app/components/ui/hero-panorama-background.tsx create mode 100644 src/app/components/ui/hero-shader-background.tsx diff --git a/src/app/components/ui/hero-panorama-background.tsx b/src/app/components/ui/hero-panorama-background.tsx new file mode 100644 index 0000000..a700b4b --- /dev/null +++ b/src/app/components/ui/hero-panorama-background.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import * as THREE from "three"; + +interface HeroPanoramaBackgroundProps { + imageSrc: string; +} + +export function HeroPanoramaBackground({ imageSrc }: HeroPanoramaBackgroundProps) { + const mountRef = useRef(null); + + useEffect(() => { + const mount = mountRef.current; + + if (!mount) { + return; + } + + const renderer = new THREE.WebGLRenderer({ + alpha: true, + antialias: true, + powerPreference: "high-performance", + }); + renderer.outputColorSpace = THREE.SRGBColorSpace; + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.6)); + renderer.setSize(mount.clientWidth, mount.clientHeight); + renderer.domElement.className = "h-full w-full"; + mount.appendChild(renderer.domElement); + + const camera = new THREE.PerspectiveCamera( + 58, + mount.clientWidth / Math.max(mount.clientHeight, 1), + 0.1, + 1000, + ); + camera.position.set(0, 0, 0.1); + + const scene = new THREE.Scene(); + const geometry = new THREE.SphereGeometry(500, 96, 64); + geometry.scale(-1, 1, 1); + + const material = new THREE.MeshBasicMaterial({ + color: 0xffffff, + transparent: true, + opacity: 0, + }); + const sphere = new THREE.Mesh(geometry, material); + sphere.rotation.y = -0.28; + scene.add(sphere); + + let frame = 0; + let disposed = false; + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + const loader = new THREE.TextureLoader(); + + loader.load( + imageSrc, + (texture) => { + if (disposed) { + texture.dispose(); + return; + } + + texture.colorSpace = THREE.SRGBColorSpace; + texture.minFilter = THREE.LinearFilter; + material.map = texture; + material.opacity = 1; + material.needsUpdate = true; + }, + undefined, + () => { + material.opacity = 0; + }, + ); + + const resize = () => { + const width = mount.clientWidth; + const height = Math.max(mount.clientHeight, 1); + camera.aspect = width / height; + camera.updateProjectionMatrix(); + renderer.setSize(width, height); + }; + + const animate = () => { + if (!reduceMotion) { + sphere.rotation.y += 0.00075; + } + + renderer.render(scene, camera); + frame = requestAnimationFrame(animate); + }; + + const resizeObserver = new ResizeObserver(resize); + resizeObserver.observe(mount); + animate(); + + return () => { + disposed = true; + cancelAnimationFrame(frame); + resizeObserver.disconnect(); + geometry.dispose(); + material.map?.dispose(); + material.dispose(); + renderer.dispose(); + renderer.domElement.remove(); + }; + }, [imageSrc]); + + return
; +} diff --git a/src/app/components/ui/hero-shader-background.tsx b/src/app/components/ui/hero-shader-background.tsx new file mode 100644 index 0000000..2ba727b --- /dev/null +++ b/src/app/components/ui/hero-shader-background.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { MeshGradient } from "@paper-design/shaders-react"; +import type React from "react"; +import { HeroPanoramaBackground } from "@/app/components/ui/hero-panorama-background"; + +interface HeroShaderBackgroundProps { + children: React.ReactNode; + imageSrc: string; +} + +export function HeroShaderBackground({ children, imageSrc }: HeroShaderBackgroundProps) { + return ( +
+ + + + + + + + + + +
+ +
+ +
+ + +
+ +
+
+ + {children} +
+ ); +}