perf: hero bỏ canvas three.js + còn 1 shader, gate theo tầm nhìn (giảm 3 ngữ cảnh WebGL)
This commit is contained in:
@@ -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<HTMLDivElement>(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 <div ref={mountRef} aria-hidden className="absolute inset-0" />;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="relative isolate overflow-hidden border-b border-[var(--surface-border)]">
|
||||
<svg aria-hidden className="absolute inset-0 h-0 w-0">
|
||||
<defs>
|
||||
<filter id="heritage-glass-effect" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feTurbulence baseFrequency="0.005" numOctaves="1" result="noise" />
|
||||
<feDisplacementMap in="SourceGraphic" in2="noise" scale="0.3" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="1 0 0 0 0.03
|
||||
0 1 0 0 0.04
|
||||
0 0 1 0 0.02
|
||||
0 0 0 0.88 0"
|
||||
result="tint"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [shaderActive, setShaderActive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = rootRef.current;
|
||||
if (!el) return;
|
||||
return createRenderGate(el, setShaderActive);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className="relative isolate overflow-hidden border-b border-[var(--surface-border)]"
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 bg-cover bg-center opacity-[0.58] saturate-[1.12] contrast-[1.04]"
|
||||
style={{ backgroundImage: `url("${imageSrc}")` }}
|
||||
/>
|
||||
<HeroPanoramaBackground imageSrc={imageSrc} />
|
||||
<div aria-hidden className="absolute inset-0 bg-[rgb(243_247_240_/_0.12)]" />
|
||||
|
||||
<div aria-hidden className="absolute inset-0 overflow-hidden opacity-48 mix-blend-soft-light">
|
||||
{/* Một lớp shader duy nhất, chỉ vẽ khi hero trong tầm nhìn + tab hiển thị */}
|
||||
{shaderActive ? (
|
||||
<div aria-hidden className="absolute inset-0 overflow-hidden opacity-40">
|
||||
<MeshGradient
|
||||
className="absolute inset-0 h-full w-full"
|
||||
colors={["#f8fbf2", "#d6b468", "#e0eddf", "#245f4b", "#ffffff"]}
|
||||
speed={0.28}
|
||||
/>
|
||||
<MeshGradient
|
||||
className="absolute inset-0 h-full w-full opacity-45"
|
||||
colors={["#ffffff", "#e7cb81", "#dcebdd", "#0e3427"]}
|
||||
speed={0.2}
|
||||
speed={0.24}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
aria-hidden
|
||||
|
||||
Reference in New Issue
Block a user