main
reng 1 month ago
parent 73e9105d5c
commit 6810c12e2c
  1. 2
      .gitignore
  2. 1071
      v2/app/package-lock.json
  3. 1
      v2/app/package.json
  4. 6
      v2/app/src/App.css
  5. 3
      v2/app/src/App.jsx
  6. 41
      v2/app/src/components/graph.jsx
  7. 114
      v2/app/src/components/point.jsx

2
.gitignore vendored

@ -3,4 +3,4 @@
/v2/scrapper/node_modules
/v2/scrapper/scrapped*/
/v2/scrapper/processed*/
/v2/app/src/.env
.env

File diff suppressed because it is too large Load Diff

@ -19,6 +19,7 @@
"@tauri-apps/plugin-opener": "^2",
"gsap": "^3.14.2",
"html-to-text": "^9.0.5",
"leva": "^0.10.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.18",

@ -6,4 +6,10 @@ button, input, select{
form{
@apply border p-2 bg-gray-200;
}
.symbol{
/* filter: blur(3px); */
/* @apply text-sm opacity-50; */
}

@ -13,6 +13,7 @@ function App() {
const [results, setResults]=useState([]);
const [selectTab, setSelectTab]=useState('text');
function preText(){
files.forEach(async (file)=>{
@ -68,7 +69,7 @@ function App() {
const collection = document.querySelector('select[name="collection"]').value;
console.log("Selected collection:", collection);
// const id=themes.find(item=>item.title===collection)?.id;
searchByTheme(collection, 20).then(res=>{
searchByTheme(collection, 50).then(res=>{
setResults(res);
});

@ -3,16 +3,27 @@ import { Canvas } from '@react-three/fiber'
import { useEffect, useState } from 'react'
import { generateLinesFromPoints, get3dCoordinates } from '../utils/draw';
import { OrbitControls, PerspectiveCamera, Bounds, useBounds, Line } from '@react-three/drei'
import Point from './point.jsx';
import Point, {PointContentType} from './point.jsx';
import { useControls, button } from "leva";
export default function Graph({results}){
const [points, setPoints]=useState();
const [lines, setLines]=useState();
const [drawLines, setDrawLines]=useState(false);
// const [drawLines, setDrawLines]=useState(false);
// const [showContent, setShowContent]=useState(true);
// const [showKeyword, setShowKeyword]=useState(false);
const { showContent, contentType, showKeywords, showLines, textToShow } = useControls({
showContent: { value: true },
contentType: { options: Object.values(PointContentType), value: PointContentType.Teaser },
showKeywords: { value: false },
showLines: { value: false },
textToShow: { value: 20, min: 5, max: 100, step: 5 },
});
const [showContent, setShowContent]=useState(true);
const [showKeyword, setShowKeyword]=useState(false);
const bounds = useBounds();
function zoomToFit(){
@ -52,37 +63,25 @@ export default function Graph({results}){
}, [results]);
return (<>
<button className='self-end text-xs' onClick={zoomToFit}>Zoom to Fit</button>
<div className='flex flex-row items-center'>
<input type="checkbox" id="toggle-content" className='self-end mr-2' defaultChecked={showContent}
onChange={(e)=>setShowContent(e.target.checked)} />
<label htmlFor="toggle-content" className='self-end text-xs mr-4'>Show Content</label>
<input type="checkbox" id="toggle-content" className='self-end mr-2' defaultChecked={drawLines}
onChange={(e)=>setDrawLines(e.target.checked)} />
<label htmlFor="toggle-content" className='self-end text-xs mr-4'>Draw line</label>
<input type="checkbox" id="toggle-content" className='self-end mr-2' defaultChecked={showKeyword}
onChange={(e)=>setShowKeyword(e.target.checked)} />
<label htmlFor="toggle-content" className='self-end text-xs mr-4'>Show keyword</label>
</div>
{/* <button className='self-end text-xs' onClick={zoomToFit}>Zoom to Fit</button> */}
<Canvas className='w-full aspect-[16/9] border border-gray-300'>
<color attach="background" args={['#000000']} />
{/* <PerspectiveCamera makeDefault position={[20, 20, 20]} /> */}
<PerspectiveCamera makeDefault position={[30, 30, 30]} fov={40} />
<OrbitControls makeDefault minDistance={2} maxDistance={100} />
<OrbitControls makeDefault minDistance={2} maxDistance={1200} />
<ambientLight intensity={0.5} />
{/* <pointLight position={[10, 10, 10]} /> */}
<Bounds fit clip observe margin={1.2}>
<group>
{Array.isArray(points) && points?.map((point, index)=>(
<Point key={index} point={point} index={index} totalPoints={points.length} result={results?.[index]}
showContent={showContent} showKeyword={showKeyword} />
showContent={showContent} showKeyword={showKeywords} contentType={contentType} textToShow={textToShow} />
))}
</group>
<group>
{drawLines && Array.isArray(lines) && lines?.map((line, index)=>(
{showLines && Array.isArray(lines) && lines?.map((line, index)=>(
<Line
key={index}
points={line} // Array of [x, y, z] or Three.Vector3

@ -1,32 +1,129 @@
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';
export const PointScale=200;
const PointSize=2;
const PointSize=1;
const PointDuration=3;
const KeywordOffset=0;
const TextToShow=12;
const DEPTH_CONFIG = {
NEAR: 600, //
FAR: 1000, //
FADE_START: 400
};
export default function Point({point, index, totalPoints, result, showContent, showKeyword}) {
export const PointContentType={
Full: 'full',
Teaser: 'teaser',
Blur: 'blur',
Symbol: 'symbol',
}
export default function Point({point, index, totalPoints, result, showContent, showKeyword, contentType, textToShow}) {
const [payload, setPayload]=useState();
const [showPayload, setShowPayload]=useState(false);
const meshRef=useRef();
const refText=useRef();
const { camera } = useThree();
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<length; i++){
if(Math.random() < 0.8)
result+=characters.charAt(Math.floor(Math.random() * characters.length));
else
result+=' ';
}
return result.slice(0, textToShow/2);
}
function getContent(){
if(!payload) return '';
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 `<span class=${contentType}>${getRandomText(before)}</span>${payload.teaser}<span class=${contentType}>${getRandomText(after)}</span>`;
}
}
//
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, 1);
// 調 ()
const depthScaleFactor = THREE.MathUtils.clamp(1.5 - (distance / 300), 0.5, 1.2);
// HTML
if (refText.current) {
refText.current.style.opacity = alpha;
//
const finalHtmlScale = depthScaleFactor * (currentMeshScale / PointSize);
refText.current.style.transform = `translate(-50%, -50%) scale(${finalHtmlScale})`;
// 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';
}
});
useEffect(()=>{
return;
if(!payload) return;
const lifeTime=payload.number/payload.total;
console.log("Point lifeTime:", lifeTime);
// animate point size based on lifeTime
const targetSize=1.0;
const targetSize=1;
const initialSize=targetSize;
const timeline=gsap.timeline({ repeat:-1});
@ -79,6 +176,7 @@ export default function Point({point, index, totalPoints, result, showContent, s
number: result.payload.number || '0',
total: result.payload.total || '1',
content: JSON.parse(result.payload.metadata).text || '',
teaser: result.payload.teaser || '',
});
},[point]);
@ -97,9 +195,11 @@ export default function Point({point, index, totalPoints, result, showContent, s
{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 && <pre className='text-lg w-[25vw] whitespace-pre-wrap text-white rounded p-2'>{(()=>{
return `${payload?.content.slice(0, TextToShow)}${payload?.content.length>TextToShow ? '...' : ''}`;
})()}</pre>}
{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`}}
dangerouslySetInnerHTML={{__html: getContent()}}
></div>}
</div>
</Html>
</mesh>

Loading…
Cancel
Save