From e0117ab4bde192b31255d33dd472122135faa1d8 Mon Sep 17 00:00:00 2001 From: tuyenio Date: Thu, 25 Jun 2026 08:47:36 +0700 Subject: [PATCH] =?UTF-8?q?perf:=20hero=20b=E1=BB=8F=20canvas=20three.js?= =?UTF-8?q?=20+=20c=C3=B2n=201=20shader,=20gate=20theo=20t=E1=BA=A7m=20nh?= =?UTF-8?q?=C3=ACn=20(gi=E1=BA=A3m=203=20ng=E1=BB=AF=20c=E1=BA=A3nh=20WebG?= =?UTF-8?q?L)?= 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 | 57 ++++----- 2 files changed, 25 insertions(+), 143 deletions(-) delete mode 100644 src/app/components/ui/hero-panorama-background.tsx diff --git a/src/app/components/ui/hero-panorama-background.tsx b/src/app/components/ui/hero-panorama-background.tsx deleted file mode 100644 index a700b4b..0000000 --- a/src/app/components/ui/hero-panorama-background.tsx +++ /dev/null @@ -1,111 +0,0 @@ -"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 index 2ba727b..bcae720 100644 --- a/src/app/components/ui/hero-shader-background.tsx +++ b/src/app/components/ui/hero-shader-background.tsx @@ -2,7 +2,8 @@ import { MeshGradient } from "@paper-design/shaders-react"; import type React from "react"; -import { HeroPanoramaBackground } from "@/app/components/ui/hero-panorama-background"; +import { useEffect, useRef, useState } from "react"; +import { createRenderGate } from "@/app/lib/render-gate"; interface HeroShaderBackgroundProps { children: React.ReactNode; @@ -10,45 +11,37 @@ interface HeroShaderBackgroundProps { } export function HeroShaderBackground({ children, imageSrc }: HeroShaderBackgroundProps) { - return ( -
- - - - - - - - - + const rootRef = useRef(null); + const [shaderActive, setShaderActive] = useState(false); + useEffect(() => { + const el = rootRef.current; + if (!el) return; + return createRenderGate(el, setShaderActive); + }, []); + + return ( +
-
-
- - -
+ {/* Một lớp shader duy nhất, chỉ vẽ khi hero trong tầm nhìn + tab hiển thị */} + {shaderActive ? ( +
+ +
+ ) : null}