import * as THREE from 'three'; import { FontLoader } from 'three/addons/loaders/FontLoader.js'; import { Font } from 'three/examples/jsm/loaders/FontLoader.js'; import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; import { lerp, randFloat } from 'three/src/math/MathUtils.js'; import { OrbitControls } from 'three/examples/jsm/Addons.js'; import { depth } from 'three/examples/jsm/nodes/Nodes.js'; class PickHelper { constructor() { this.raycaster = new THREE.Raycaster(); this.pickedObject = null; this.lastObjectPicked = null; this.sameObjectPicked = false; this.initalPick = false; } pick(normalizedPosition, scene, camera, time) { // restore the color if there is a picked object if (this.pickedObject) { this.lastObjectPicked = this.pickedObject; this.sameObjectPicked = false; this.pickedObject = undefined; } // cast a ray through the frustum this.raycaster.setFromCamera(normalizedPosition, camera); // get the list of objects the ray intersected const intersectedObjects = this.raycaster.intersectObjects(scene.children); if (intersectedObjects.length) { // pick the first object. It's the closest one for (let i = 0; i < intersectedObjects.length; i++){ if(intersectedObjects[i].object.geometry.type != "SphereGeometry"){ this.pickedObject = intersectedObjects[i].object; if(intersectedObjects[i].object == this.lastObjectPicked) this.sameObjectPicked = true; } } if (this.sameObjectPicked) this.pickedObject = this.lastObjectPicked; } } } class Article { constructor(texture, title, filename, id){ this.geom = new THREE.TetrahedronGeometry(1, 2); this.material = new THREE.MeshLambertMaterial({map: texture}); this.mesh = new THREE.Mesh(this.geom, this.material); this.html = filename; this.name = title; this.id = id; this.hover = false; this.hoverScale = false; this.hoverLerpTime = 0; this.speed = randFloat(1.5, 2); this.rotationSpeed = randFloat(0.5, 1.5); this.scale = 1; } AddToScene(scene, aspect){ this.mesh.position.x = (Math.random() - 0.5) * 8 * aspect; this.mesh.position.y = 8 + Math.random() * 15; scene.add(this.mesh); } UpdateRotation(time){ this.mesh.rotation.x += this.rotationSpeed * time; this.mesh.rotation.y += this.rotationSpeed * time; } UpdatePosition(time, picker){ this.BoundsCheck(); this.HoverCheck(picker, time) if (!this.hover) this.mesh.position.y -= this.speed * time; } BoundsCheck(){ if (this.mesh.position.y < -8) { const aspect = window.innerWidth / (window.innerHeight - 100); this.mesh.position.y = 8 + (Math.random() * 3); this.mesh.position.x = (Math.random() - 0.5) * 8 * aspect; this.speed = randFloat(1.5, 2) } } UpdateScale(multipler){ this.mesh.scale.set(multipler, multipler, multipler); } HoverCheck(picker, time){ const hoverIncrement = 0.01; const offHoverDecrement = 0.01; if (picker.pickedObject === this.mesh) { this.hover = true; if (this.hoverLerpTime < 1) { this.hoverLerpTime += hoverIncrement; } if (this.hoverLerpTime > 1) { this.hoverLerpTime = 1; } this.scale = lerp(this.scale, 2, this.hoverLerpTime); } else { this.hover = false; if (this.hoverLerpTime > 0) { this.hoverLerpTime -= offHoverDecrement; } if (this.hoverLerpTime < 0) { this.hoverLerpTime = 0; } this.scale = lerp(this.scale, 1, 1 - this.hoverLerpTime); } this.UpdateScale(this.scale); } } let scene, camera, renderer, cube; let texture, planeMat, mesh, moloch_txt; let lastTime = 0; // Keep track of the last frame time let textGeo, textWidth, textMaterial, textMesh; let text_Geometries = []; let isHovering = false; const pickPosition = {x: 0, y: 0}; const pickHelper = new PickHelper(); const object_list = [] const object_count = 20; const fontLoader = new FontLoader(); let group = new THREE.Group(); let pastArticle; function init() { // Set to a value outside the screen range pickPosition.x = -100000; pickPosition.y = -100000; const loader = new THREE.TextureLoader(); texture = loader.load('/images/website/checker.png'); // Create a renderer and attach it to our document renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.shadowMap.enabled = true; renderer.setSize(window.innerWidth, window.innerHeight - 100); document.getElementById('container').appendChild(renderer.domElement); document.getElementById('container').style.overflowY = 'hidden'; // Create the scene scene = new THREE.Scene(); scene.background = new THREE.Color('black'); // Camera Setup const aspect = window.innerWidth / (window.innerHeight - 100); // Adjust for nav height const frustumSize = 10; camera = new THREE.OrthographicCamera( frustumSize * aspect / -2, frustumSize * aspect / 2, frustumSize / 2, frustumSize / -2, 0.1, 5000 ); //const controls = new OrbitControls(camera, renderer.domElement); camera.position.z = 20; // Fetch JSON data fetch('../json/articles.json') .then(response => response.json()) .then(jsonData => { for (let i = 0; i < jsonData.length; i++) { let temp_txt = loader.load('../images/' + jsonData[i]['image']); temp_txt.minFilter = THREE.NearestFilter; let title = jsonData[i]['name'] let filename = jsonData[i]['filename'] let article = new Article(temp_txt, title, filename, i); article.AddToScene(scene, aspect); object_list.push(article); } }) // Plane Setup { const planeSize = 40; texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.magFilter = THREE.NearestFilter; texture.colorSpace = THREE.SRGBColorSpace; const repeats = planeSize / 1; texture.repeat.set(repeats, repeats); const planeGeo = new THREE.SphereGeometry(10); planeMat = new THREE.MeshPhongMaterial({ map: texture, side: THREE.DoubleSide, }); mesh = new THREE.Mesh(planeGeo, planeMat); mesh.position.z = -20 scene.add(mesh); } // Light Setup { const color = 0xFFFFFF; const intensity = 3; const light = new THREE.DirectionalLight(color, intensity); light.castShadow = true; light.position.set(1, 1, 10); light.target.position.set(-0, 0, -0); light.shadow.camera.top = 25; light.shadow.camera.bottom = -25; light.shadow.camera.left = -25; light.shadow.camera.right = 25; light.shadow.camera.zoom = 1; scene.add(light); scene.add(light.target); } // Start with an initial timestamp animate(0); } function animate(time) { requestAnimationFrame(animate); // Calculate the time elapsed since the last frame const deltaTime = (time - lastTime) / 1000; // Convert time to seconds lastTime = time; pickHelper.pick(pickPosition, scene, camera, time); for (let i = 0; i < object_list.length; i++){ object_list[i].UpdateRotation(deltaTime); object_list[i].UpdatePosition(deltaTime, pickHelper); } // Update the plane texture offset const scrollSpeed = 0.2; planeMat.map.offset.y += scrollSpeed * deltaTime; planeMat.map.offset.x += scrollSpeed / 0.75 * deltaTime; ChangeCursor(); // Render the scene from the perspective of the camera renderer.render(scene, camera); } // Handle window resize window.addEventListener('resize', () => { const aspect = window.innerWidth / (window.innerHeight - 100); const frustumSize = 10; camera.left = -frustumSize * aspect / 2; camera.right = frustumSize * aspect / 2; camera.top = frustumSize / 2; camera.bottom = -frustumSize / 2; camera.updateProjectionMatrix(); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight - 100); }); function getCanvasRelativePosition(event) { const rect = document.querySelector('#container').getBoundingClientRect(); return { x: (event.clientX - rect.left) * window.innerWidth / rect.width, y: (event.clientY - rect.top ) * (window.innerHeight - 100) / rect.height, }; } function setPickPosition(event) { const pos = getCanvasRelativePosition(event); pickPosition.x = (pos.x / window.innerWidth ) * 2 - 1; pickPosition.y = (pos.y / ( window.innerHeight-100 ) ) * -2 + 1; // note we flip Y } function clearPickPosition() { // unlike the mouse which always has a position // if the user stops touching the screen we want // to stop picking. For now we just pick a value // unlikely to pick something pickPosition.x = -100000; pickPosition.y = -100000; } function objectClicked(event) { if (pickHelper.pickedObject) { // Find the corresponding Article object const pickedArticle = object_list.find(article => article.mesh === pickHelper.pickedObject); if (pickedArticle) { window.location.href = pickedArticle.html; } } } function ChangeCursor(){ const pickedArticle = object_list.find(article => article.mesh === pickHelper.pickedObject); if (pickedArticle) { document.body.style.cursor = 'pointer'; if (pickedArticle.name != pastArticle) console.log('updated!') UpdateText(pickedArticle.name); }else{ document.body.style.cursor = 'default'; ClearTextGeoList(); isHovering = false } } function UpdateText(text) { MeasureText(text); pastArticle = text } function ClearTextGeoList() { scene.remove(group) text_Geometries = []; } // Preload font let cachedFont = null; fontLoader.load('fonts/Redaction 50_Regular.json', (font) => { cachedFont = font; }); function MeasureText(text) { if (!cachedFont) { // Font is not loaded yet, handle appropriately (e.g., show loading indicator or retry later) return; } ClearTextGeoList(); // Adjust font size based on window dimensions let initialFontSize = 1; if (window.innerWidth < 1024) { initialFontSize = 0.5; } else if (window.innerWidth < 512) { initialFontSize = 0.25; } const aspect = window.innerWidth / (window.innerHeight - 100); // Adjust for nav height const orthoWidth = 10 * aspect; const lines = splitTextIntoLines(text, orthoWidth, initialFontSize); const numLines = lines.length; const totalHeight = (numLines - 1) * (1.5 * initialFontSize); const startY = totalHeight / 2; for (let i = 0; i < numLines; i++) { const line = lines[i]; const textGeo = createTextGeometry(line.text, line.fontSize); textGeo.computeBoundingBox(); const textMaterial = new THREE.MeshNormalMaterial(); const textMesh = new THREE.Mesh(textGeo, textMaterial); const centerOffsetX = (textGeo.boundingBox.max.x - textGeo.boundingBox.min.x) / 2; const centerOffsetY = (textGeo.boundingBox.max.y - textGeo.boundingBox.min.y) / 2; const centerOffsetZ = (textGeo.boundingBox.max.z - textGeo.boundingBox.min.z) / 2; textGeo.translate(-centerOffsetX, -centerOffsetY, -centerOffsetZ); textMesh.rotation.x = Math.PI / 2 * 0.05; textMesh.position.y = startY - (i * (1.5 * initialFontSize)); textMesh.position.z = 5; text_Geometries.push(textMesh); } group = new THREE.Group(); text_Geometries.forEach(mesh => group.add(mesh)); scene.add(group); } function splitTextIntoLines(text, maxWidth, initialFontSize) { const words = text.split(" "); const lines = []; let currentLine = ""; let currentFontSize = initialFontSize; for (let i = 0; i < words.length; i++) { const testLine = currentLine + (currentLine ? " " : "") + words[i]; const textGeo = createTextGeometry(testLine, currentFontSize); textGeo.computeBoundingBox(); const proportion = textGeo.boundingBox.max.x / maxWidth; if (proportion > 0.8) { // Assuming 80% width utilization threshold lines.push({ text: currentLine, fontSize: currentFontSize }); currentLine = words[i]; // Adjust fontSize based on line length if needed } else { currentLine = testLine; } } if (currentLine !== "") { lines.push({ text: currentLine, fontSize: currentFontSize }); } return lines; } function createTextGeometry(text, size) { return new TextGeometry(text, { height: 2, depth: 1, font: cachedFont, size: size, curveSegments: 1 }); } window.addEventListener('mousemove', setPickPosition); window.addEventListener('mouseout', clearPickPosition); window.addEventListener('mouseleave', clearPickPosition); window.addEventListener('click', objectClicked) // Initialize the application init();