|
|
|
@ -59,16 +59,21 @@ export default function Point({point, index, totalPoints, result, showContent, s |
|
|
|
|
|
|
|
|
|
|
|
function getContent(){ |
|
|
|
function getContent(){ |
|
|
|
if(!payload) return ''; |
|
|
|
if(!payload) return ''; |
|
|
|
|
|
|
|
// console.log("Point contentType:", contentType, payload); |
|
|
|
|
|
|
|
// if(payload.normalized_time===-1){ |
|
|
|
|
|
|
|
// return <span>{payload.text}</span> |
|
|
|
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
|
|
switch(contentType){ |
|
|
|
switch(contentType){ |
|
|
|
case PointContentType.Full: |
|
|
|
case PointContentType.Full: |
|
|
|
return payload.content.slice(0, textToShow) + (payload.content.length > textToShow ? '...' : ''); |
|
|
|
return payload.content.slice(0, textToShow) + (payload.content.length > textToShow ? '...' : ''); |
|
|
|
case PointContentType.Teaser: |
|
|
|
case PointContentType.Teaser: |
|
|
|
return payload.teaser; |
|
|
|
return `<span>${payload.teaser}</span>`; |
|
|
|
case PointContentType.Blur: |
|
|
|
case PointContentType.Blur: |
|
|
|
case PointContentType.Symbol: |
|
|
|
case PointContentType.Symbol: |
|
|
|
const start=payload.content.indexOf(payload.teaser); |
|
|
|
const start=payload.content.indexOf(payload.teaser); |
|
|
|
if(start===-1) return payload.teaser; |
|
|
|
if(start===-1) |
|
|
|
|
|
|
|
return `<span>${payload.teaser}</span>`; |
|
|
|
|
|
|
|
|
|
|
|
const before=payload.content.substring(0, start); |
|
|
|
const before=payload.content.substring(0, start); |
|
|
|
const after=payload.content.substring(start + payload.teaser.length); |
|
|
|
const after=payload.content.substring(start + payload.teaser.length); |
|
|
|
@ -82,6 +87,7 @@ export default function Point({point, index, totalPoints, result, showContent, s |
|
|
|
useFrame((state) => { |
|
|
|
useFrame((state) => { |
|
|
|
if (!meshRef.current || !refText.current || !payload) return; |
|
|
|
if (!meshRef.current || !refText.current || !payload) return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const time = state.clock.elapsedTime; |
|
|
|
const time = state.clock.elapsedTime; |
|
|
|
const lifeTimeOffset = (payload.number / payload.total) || 0; |
|
|
|
const lifeTimeOffset = (payload.number / payload.total) || 0; |
|
|
|
|
|
|
|
|
|
|
|
@ -106,15 +112,23 @@ export default function Point({point, index, totalPoints, result, showContent, s |
|
|
|
|
|
|
|
|
|
|
|
// 更新 HTML 樣式 |
|
|
|
// 更新 HTML 樣式 |
|
|
|
if (refText.current) { |
|
|
|
if (refText.current) { |
|
|
|
refText.current.style.opacity *= alpha; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 結合深度縮放與物體本身的脈衝狀態 |
|
|
|
// 結合深度縮放與物體本身的脈衝狀態 |
|
|
|
const finalHtmlScale = depthScaleFactor * (currentMeshScale / PointSize); |
|
|
|
const finalHtmlScale = depthScaleFactor * (currentMeshScale / PointSize); |
|
|
|
refText.current.style.transform = `translate(-50%, -50%) scale(${finalHtmlScale})`; |
|
|
|
refText.current.style.transform = `translate(-50%, -50%) scale(${finalHtmlScale})`; |
|
|
|
|
|
|
|
|
|
|
|
// 加入 CSS 濾鏡:越遠越模糊 |
|
|
|
if(payload.normalized_time===-1){ |
|
|
|
const blurAmount = distance > DEPTH_CONFIG.FADE_START ? (distance - DEPTH_CONFIG.FADE_START) / 20 : 0; |
|
|
|
// meshRef.current.scale.set(PointSize, PointSize, PointSize); |
|
|
|
refText.current.style.filter = `blur(${Math.min(blurAmount, 4)}px)`; |
|
|
|
refText.current.style.opacity = 1; |
|
|
|
|
|
|
|
refText.current.style.transform = `translate(-50%, -50%) scale(1)`; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}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, 4)}px)`; |
|
|
|
|
|
|
|
} |
|
|
|
// 效能優化 |
|
|
|
// 效能優化 |
|
|
|
// refText.current.style.display = alpha <= 0.01 ? 'none' : 'block'; |
|
|
|
// refText.current.style.display = alpha <= 0.01 ? 'none' : 'block'; |
|
|
|
} |
|
|
|
} |
|
|
|
@ -130,8 +144,11 @@ export default function Point({point, index, totalPoints, result, showContent, s |
|
|
|
// return; |
|
|
|
// return; |
|
|
|
|
|
|
|
|
|
|
|
if(!payload) return; |
|
|
|
if(!payload) return; |
|
|
|
const lifeTime=payload.normalized_time || 0; |
|
|
|
|
|
|
|
console.log("Point lifeTime:", lifeTime); |
|
|
|
const lifeTime=payload.normalized_time; |
|
|
|
|
|
|
|
if(lifeTime===-1) return; // keyword 不參與動畫 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// console.log("Point lifeTime:", lifeTime); |
|
|
|
|
|
|
|
|
|
|
|
// animate point size based on lifeTime |
|
|
|
// animate point size based on lifeTime |
|
|
|
const targetSize=1; |
|
|
|
const targetSize=1; |
|
|
|
@ -210,17 +227,36 @@ export default function Point({point, index, totalPoints, result, showContent, s |
|
|
|
|
|
|
|
|
|
|
|
useEffect(()=>{ |
|
|
|
useEffect(()=>{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(!result) return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log("Point result:", result); |
|
|
|
|
|
|
|
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({ |
|
|
|
setPayload({ |
|
|
|
...JSON.parse(result.payload.metadata), |
|
|
|
...JSON.parse(result.payload.metadata??'{}'), |
|
|
|
keywords: result.payload.keywords || '[]', |
|
|
|
keywords: result.payload.keywords || [], |
|
|
|
number: result.payload.number || '0', |
|
|
|
number: result.payload.number || 0, |
|
|
|
total: result.payload.total || '1', |
|
|
|
total: result.payload.total || 1, |
|
|
|
content: JSON.parse(result.payload.metadata).text || '', |
|
|
|
content: JSON.parse(result.payload.metadata??'{}').text || '', |
|
|
|
teaser: result.payload.teaser || '', |
|
|
|
teaser: result.payload.teaser || '', |
|
|
|
normalized_time: result.payload.normalized_time || 0, |
|
|
|
normalized_time: result.payload.normalized_time || 0, |
|
|
|
|
|
|
|
type: result.type || 'data', |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
},[point]); |
|
|
|
},[result]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
@ -233,11 +269,11 @@ export default function Point({point, index, totalPoints, result, showContent, s |
|
|
|
/> |
|
|
|
/> |
|
|
|
<Html> |
|
|
|
<Html> |
|
|
|
<div ref={refText} className='text-white p-2 select-none text-center opacity-0'> |
|
|
|
<div ref={refText} className='text-white p-2 select-none text-center opacity-0'> |
|
|
|
{<div className={`flex flex-row justify-center flex-wrap text-[1rem] ${showKeyword ? 'block' : 'hidden'}`}> |
|
|
|
{payload?.keywords && <div className={`flex flex-row justify-center flex-wrap text-[1rem] ${showKeyword ? 'block' : 'hidden'}`}> |
|
|
|
{payload?.keywords.map((el, index)=><span key={index} className='px-2' style={{transform: `translate(${Math.random() * KeywordOffset}px,${Math.random() * KeywordOffset}px)`}}>{el}</span>)} |
|
|
|
{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>} |
|
|
|
</div>} |
|
|
|
{<div |
|
|
|
{<div |
|
|
|
className={`text-[2rem] text-white rounded p-2 text-center w-[50vw] splittext ${showContent ? 'block' : 'hidden'}`} |
|
|
|
className={`text-[2rem] text-white rounded p-2 text-center max-w-[50vw] min-w-[20vw] splittext ${showContent ? 'block' : 'hidden'} ${payload?.type??''}`} |
|
|
|
// style={{width: `${Math.random()*(payload?.content.length/5 || 10) + 20}vw`}} |
|
|
|
// style={{width: `${Math.random()*(payload?.content.length/5 || 10) + 20}vw`}} |
|
|
|
dangerouslySetInnerHTML={{__html: getContent()}} |
|
|
|
dangerouslySetInnerHTML={{__html: getContent()}} |
|
|
|
></div>} |
|
|
|
></div>} |
|
|
|
|