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 { MeshGradient } from "@paper-design/shaders-react";
|
||||||
import type React from "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 {
|
interface HeroShaderBackgroundProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -10,45 +11,37 @@ interface HeroShaderBackgroundProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function HeroShaderBackground({ children, imageSrc }: HeroShaderBackgroundProps) {
|
export function HeroShaderBackground({ children, imageSrc }: HeroShaderBackgroundProps) {
|
||||||
return (
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
<div className="relative isolate overflow-hidden border-b border-[var(--surface-border)]">
|
const [shaderActive, setShaderActive] = useState(false);
|
||||||
<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>
|
|
||||||
|
|
||||||
|
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
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="absolute inset-0 bg-cover bg-center opacity-[0.58] saturate-[1.12] contrast-[1.04]"
|
className="absolute inset-0 bg-cover bg-center opacity-[0.58] saturate-[1.12] contrast-[1.04]"
|
||||||
style={{ backgroundImage: `url("${imageSrc}")` }}
|
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 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ị */}
|
||||||
<MeshGradient
|
{shaderActive ? (
|
||||||
className="absolute inset-0 h-full w-full"
|
<div aria-hidden className="absolute inset-0 overflow-hidden opacity-40">
|
||||||
colors={["#f8fbf2", "#d6b468", "#e0eddf", "#245f4b", "#ffffff"]}
|
<MeshGradient
|
||||||
speed={0.28}
|
className="absolute inset-0 h-full w-full"
|
||||||
/>
|
colors={["#f8fbf2", "#d6b468", "#e0eddf", "#245f4b", "#ffffff"]}
|
||||||
<MeshGradient
|
speed={0.24}
|
||||||
className="absolute inset-0 h-full w-full opacity-45"
|
/>
|
||||||
colors={["#ffffff", "#e7cb81", "#dcebdd", "#0e3427"]}
|
</div>
|
||||||
speed={0.2}
|
) : null}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
|
|||||||
Reference in New Issue
Block a user