|
|
|
|
@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react'; |
|
|
|
|
import * as THREE from 'three'; |
|
|
|
|
import { useFrame, useThree } from '@react-three/fiber'; |
|
|
|
|
import gsap from 'gsap'; |
|
|
|
|
import { normalize } from 'umap-js/dist/matrix'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const PointScale=200; |
|
|
|
|
@ -10,11 +11,12 @@ const PointSize=1; |
|
|
|
|
|
|
|
|
|
const PointDuration=3; |
|
|
|
|
const KeywordOffset=0; |
|
|
|
|
const SceneDuration=10; // 總場景時間,用於計算延遲 |
|
|
|
|
|
|
|
|
|
const DEPTH_CONFIG = { |
|
|
|
|
NEAR: 600, // 在此距離內顯示最清晰 |
|
|
|
|
FAR: 1000, // 超過此距離後幾乎隱藏或變成符號 |
|
|
|
|
FADE_START: 400 |
|
|
|
|
NEAR: 1200, // 在此距離內顯示最清晰 |
|
|
|
|
FAR: 1500, // 超過此距離後幾乎隱藏或變成符號 |
|
|
|
|
FADE_START: 800 |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -35,13 +37,14 @@ export default function Point({point, index, totalPoints, result, showContent, s |
|
|
|
|
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='░▒▓█'; |
|
|
|
|
const characters='*@#$%^&'; |
|
|
|
|
let result=''; |
|
|
|
|
for(let i=0; i<length; i++){ |
|
|
|
|
if(Math.random() < 0.8) |
|
|
|
|
@ -101,7 +104,7 @@ export default function Point({point, index, totalPoints, result, showContent, s |
|
|
|
|
|
|
|
|
|
// 更新 HTML 樣式 |
|
|
|
|
if (refText.current) { |
|
|
|
|
refText.current.style.opacity = alpha; |
|
|
|
|
refText.current.style.opacity *= alpha; |
|
|
|
|
// 結合深度縮放與物體本身的脈衝狀態 |
|
|
|
|
const finalHtmlScale = depthScaleFactor * (currentMeshScale / PointSize); |
|
|
|
|
refText.current.style.transform = `translate(-50%, -50%) scale(${finalHtmlScale})`; |
|
|
|
|
@ -114,51 +117,54 @@ export default function Point({point, index, totalPoints, result, showContent, s |
|
|
|
|
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; |
|
|
|
|
// return; |
|
|
|
|
|
|
|
|
|
if(!payload) return; |
|
|
|
|
const lifeTime=payload.number/payload.total; |
|
|
|
|
console.log("Point lifeTime:", lifeTime); |
|
|
|
|
const lifeTime=payload.normalized_time || 0; |
|
|
|
|
// console.log("Point lifeTime:", lifeTime); |
|
|
|
|
|
|
|
|
|
// animate point size based on lifeTime |
|
|
|
|
const targetSize=1; |
|
|
|
|
const initialSize=targetSize; |
|
|
|
|
|
|
|
|
|
const timeline=gsap.timeline({ repeat:-1}); |
|
|
|
|
timeline.set(meshRef.current.scale, { |
|
|
|
|
x: initialSize, y: initialSize, z: initialSize, |
|
|
|
|
}); |
|
|
|
|
timeline.set(refText.current, { |
|
|
|
|
scale: initialSize, |
|
|
|
|
opacity: 0, |
|
|
|
|
}); |
|
|
|
|
timeline.fromTo(meshRef.current.scale, { |
|
|
|
|
x: initialSize, y: initialSize, z: initialSize, |
|
|
|
|
}, { |
|
|
|
|
x: PointSize, y: PointSize, z: PointSize, |
|
|
|
|
duration:PointDuration, |
|
|
|
|
delay: lifeTime *PointDuration, |
|
|
|
|
ease: "power3.in", |
|
|
|
|
onUpdate:()=>{ |
|
|
|
|
if(refText.current){ |
|
|
|
|
refText.current.style.transform=`scale(${meshRef.current.scale.x / PointSize})`; |
|
|
|
|
refText.current.style.opacity=`${meshRef.current.scale.x / PointSize}`; |
|
|
|
|
} |
|
|
|
|
timeline.fromTo(refAnimation.current,{t:0}, { |
|
|
|
|
t:0, |
|
|
|
|
onUpdate: ()=>{ |
|
|
|
|
setTextStyle(refAnimation.current.t); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
timeline.to(meshRef.current.scale, { |
|
|
|
|
x: targetSize, y: targetSize, z: targetSize, |
|
|
|
|
duration:PointDuration, |
|
|
|
|
ease: "power3.out", |
|
|
|
|
onUpdate:()=>{ |
|
|
|
|
if(refText.current){ |
|
|
|
|
refText.current.style.transform=`scale(${meshRef.current.scale.x / PointSize})`; |
|
|
|
|
refText.current.style.opacity=`${meshRef.current.scale.x / PointSize}`; |
|
|
|
|
} |
|
|
|
|
}, 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(); |
|
|
|
|
|
|
|
|
|
@ -177,6 +183,7 @@ export default function Point({point, index, totalPoints, result, showContent, s |
|
|
|
|
total: result.payload.total || '1', |
|
|
|
|
content: JSON.parse(result.payload.metadata).text || '', |
|
|
|
|
teaser: result.payload.teaser || '', |
|
|
|
|
normalized_time: result.payload.normalized_time || 0, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
},[point]); |
|
|
|
|
@ -191,13 +198,13 @@ export default function Point({point, index, totalPoints, result, showContent, s |
|
|
|
|
emissiveIntensity={0.5} |
|
|
|
|
/> |
|
|
|
|
<Html> |
|
|
|
|
<div ref={refText} className='text-white p-2 w-[20vw] select-none translate-x-[-50%] translate-y-[-50%] text-center opacity-0'> |
|
|
|
|
<div ref={refText} className='text-white p-2 select-none text-center opacity-0'> |
|
|
|
|
{showKeyword && <div className='flex flex-row justify-center flex-wrap text-[1rem]'> |
|
|
|
|
{payload?.keywords.map((el, index)=><span key={index} className='px-2' style={{transform: `translate(${Math.random() * KeywordOffset}px,${Math.random() * KeywordOffset}px)`}}>{el}</span>)} |
|
|
|
|
</div>} |
|
|
|
|
{showContent && <div |
|
|
|
|
className='text-[2rem] whitespace-pre-wrap text-white rounded p-2 text-left' |
|
|
|
|
style={{width: `${Math.random()*(payload?.content.length/5 || 10) + 20}vw`}} |
|
|
|
|
className='text-[2rem] text-white rounded p-2 text-center w-[100vw]' |
|
|
|
|
// style={{width: `${Math.random()*(payload?.content.length/5 || 10) + 20}vw`}} |
|
|
|
|
dangerouslySetInnerHTML={{__html: getContent()}} |
|
|
|
|
></div>} |
|
|
|
|
</div> |
|
|
|
|
|