import { Html } from '@react-three/drei' import { useEffect, useRef, useState } from 'react'; import * as THREE from 'three'; import { useFrame, useThree } from '@react-three/fiber'; import gsap from 'gsap'; import { SplitText } from 'gsap/SplitText'; gsap.registerPlugin(SplitText) ; export const PointScale=200; const PointSize=1; const PointDuration=5; const KeywordOffset=0; const SceneDuration=0.5; // 總場景時間,用於計算延遲 const DEPTH_CONFIG = { NEAR: 1200, // 在此距離內顯示最清晰 FAR: 1500, // 超過此距離後幾乎隱藏或變成符號 FADE_START: 800 }; export const PointContentType={ Full: 'full', Teaser: 'teaser', Blur: 'blur', Symbol: 'symbol', } export default function Point({point, index, totalPoints, result, showContent, showKeyword, contentType, textToShow, color}) { const [payload, setPayload]=useState(); const [showPayload, setShowPayload]=useState(false); const meshRef=useRef(); const refText=useRef(); const { camera } = useThree(); const refAnimation=useRef({t:0}); function getRandomText(text){ if(contentType==PointContentType.Blur) return text.slice(0, textToShow) + '...'; const length=text.length; const characters='*@#$%^&'; let result=''; for(let i=0; i{payload.text} // } switch(contentType){ case PointContentType.Full: return payload.content.slice(0, textToShow) + (payload.content.length > textToShow ? '...' : ''); case PointContentType.Teaser: return `${payload.teaser}`; case PointContentType.Blur: case PointContentType.Symbol: const start=payload.content.indexOf(payload.teaser); if(start===-1) return `${payload.teaser}`; const before=payload.content.substring(0, start); const after=payload.content.substring(start + payload.teaser.length); return `${getRandomText(before)}${payload.teaser}${getRandomText(after)}`; } } // 每幀計算距離並更新樣式,同時處理縮放動畫 useFrame((state) => { if (!meshRef.current || !refText.current || !payload) return; const time = state.clock.elapsedTime; const lifeTimeOffset = (payload.number / payload.total) || 0; // 1. 處理物體本身的脈衝縮放動畫 (替代 GSAP) // 使用正弦波模擬 PointDuration 的循環 const pulseSpeed = Math.PI / PointDuration; const pulse = 1;//Math.abs(Math.sin(time * pulseSpeed + lifeTimeOffset * Math.PI)); const currentMeshScale = THREE.MathUtils.lerp(0.5, PointSize, pulse); meshRef.current.scale.set(currentMeshScale, currentMeshScale, currentMeshScale); // 2. 計算與攝影機的距離處理深度視覺效果 const worldPosition = new THREE.Vector3(); meshRef.current.getWorldPosition(worldPosition); const distance = camera.position.distanceTo(worldPosition); // 根據距離計算 alpha (0 到 1) let alpha = 1 - (distance - DEPTH_CONFIG.NEAR) / (DEPTH_CONFIG.FAR - DEPTH_CONFIG.NEAR); alpha = THREE.MathUtils.clamp(alpha, 0.2, 1); // 根據距離調整縮放感 (非線性縮放讓遠處文字縮小) const depthScaleFactor = THREE.MathUtils.clamp(1.5 - (distance / 300), 0.5, 1.2); // 更新 HTML 樣式 if (refText.current) { // 結合深度縮放與物體本身的脈衝狀態 const finalHtmlScale = depthScaleFactor * (currentMeshScale / PointSize); refText.current.style.transform = `translate(-50%, -50%) scale(${finalHtmlScale})`; if(payload.keywords.length==1){ // meshRef.current.scale.set(PointSize, PointSize, PointSize); refText.current.style.opacity = 1; refText.current.style.transform = `translate(-50%, -50%) scale(1)`; refText.current.style.filter = `none`; }else{ refText.current.style.opacity *= alpha; // 加入 CSS 濾鏡:越遠越模糊 const blurAmount = distance > DEPTH_CONFIG.FADE_START ? (distance - DEPTH_CONFIG.FADE_START) / 20 : 0; refText.current.style.filter = `blur(${Math.min(blurAmount, 2)}px)`; } // 效能優化 // refText.current.style.display = alpha <= 0.01 ? 'none' : 'block'; } }); function setTextStyle(t){ if(refText.current){ refText.current.style.opacity=t; refText.current.style.transform=`translate(-50%, -50%) scale(${t})`; } } useEffect(()=>{ // return; if(!payload) return; const lifeTime=payload.normalized_time; if(lifeTime===-1) return; // keyword 不參與動畫 // console.log("Point lifeTime:", lifeTime); // animate point size based on lifeTime const targetSize=1; const timeline=gsap.timeline({ repeat:-1}); timeline.fromTo(refAnimation.current,{t:0}, { t:0, onUpdate: ()=>{ setTextStyle(refAnimation.current.t); } }, lifeTime * SceneDuration); timeline.to(refAnimation.current,{ t: 1, duration: PointDuration, ease: 'power1.inOut', onComplete: ()=>{ // console.log("Show text for point:", index); }, onUpdate: ()=>{ setTextStyle(refAnimation.current.t); } }); // 根據 lifeTime 設定延遲時間 timeline.to(refAnimation.current,{ t: 0, duration: PointDuration, onComplete: ()=>{ // console.log("Hide text for point:", index); }, onUpdate: ()=>{ setTextStyle(refAnimation.current.t); } }); // timeline.to(refAnimation.current,{t:0}, { // duration: SceneDuration - (lifeTime * SceneDuration + PointDuration * 2), // }); // 剩餘時間保持隱藏 timeline.play(); return ()=>{ timeline.kill(); }; },[payload]); useEffect(()=>{ if(!showContent) return; if(!refText.current) return; // const tosplit=refText.current.getElementsByClassName('splittext')[0]; // if(!tosplit) return; // let split=SplitText.create(tosplit, {type: "words, chars"}); // console.log("SplitText chars:", split.chars.length); // gsap.fromTo(split.chars, { // // opacity: 0, // y: 50, // stagger: 0.05 // }, { // // opacity: 1, // y: -50, // duration: 0.5, // ease: 'power2.inOut', // repeat:-1, // }); // return ()=>{ // // split.revert(); // gsap.killTweensOf(split.chars); // }; },[showContent]) useEffect(()=>{ if(!result) return; console.log("Point result:", result, color); if(result?.type==='keyword'){ const title=JSON.parse(result.payload.text??'{}').title; const payloadKeyword={ ...JSON.parse(result.payload.text??'{}'), keywords: [title], number: 0, total: 1, content: title || '', teaser: title ||'', normalized_time: -1, type: result.type || 'keyword', }; setPayload(payloadKeyword); return; } setPayload({ ...JSON.parse(result.payload.metadata??'{}'), keywords: result.payload.keywords || [], number: result.payload.number || 0, total: result.payload.total || 1, content: JSON.parse(result.payload.metadata??'{}').text || '', teaser: result.payload.teaser || '', normalized_time: result.payload.normalized_time || 0, type: result.type || 'data', }); },[result]); return ( {showKeyword && payload?.keywords.length>1 && {payload?.keywords.map((el, index)=>{el})} } {/* {color!='white' && !showKeyword && payload?.keywords.length>1 && } */} {} ) }