personal garden & website
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

440 lines
12 KiB

6 months ago
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;
6 months ago
}
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;
6 months ago
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;
6 months ago
function init() {
// Set to a value outside the screen range
pickPosition.x = -100000;
pickPosition.y = -100000;
6 months ago
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;
temp_txt.magFilter = THREE.NearestFilter;
temp_txt.colorSpace = THREE.SRGBColorSpace;
6 months ago
let title = jsonData[i]['name']
let filename = jsonData[i]['filename']
6 months ago
let article = new Article(temp_txt, title, filename.replace(/\.[^/.]+$/, ""), i);
6 months ago
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);
6 months ago
}else{
document.body.style.cursor = 'default';
ClearTextGeoList();
isHovering = false
6 months ago
}
}
function UpdateText(text) {
MeasureText(text);
pastArticle = text
6 months ago
}
function ClearTextGeoList() {
scene.remove(group)
text_Geometries = [];
6 months ago
}
// Preload font
let cachedFont = null;
fontLoader.load('fonts/Redaction 50_Regular.json', (font) => {
cachedFont = font;
});
6 months ago
function MeasureText(text) {
if (!cachedFont) {
// Font is not loaded yet, handle appropriately (e.g., show loading indicator or retry later)
return;
}
6 months ago
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);
6 months ago
const numLines = lines.length;
const totalHeight = (numLines - 1) * (1.5 * initialFontSize);
const startY = totalHeight / 2;
6 months ago
for (let i = 0; i < numLines; i++) {
const line = lines[i];
const textGeo = createTextGeometry(line.text, line.fontSize);
textGeo.computeBoundingBox();
6 months ago
const textMaterial = new THREE.MeshNormalMaterial();
const textMesh = new THREE.Mesh(textGeo, textMaterial);
6 months ago
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;
6 months ago
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;
6 months ago
text_Geometries.push(textMesh);
}
6 months ago
group = new THREE.Group();
text_Geometries.forEach(mesh => group.add(mesh));
scene.add(group);
}
6 months ago
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
6 months ago
});
}
6 months ago
window.addEventListener('mousemove', setPickPosition);
window.addEventListener('mouseout', clearPickPosition);
window.addEventListener('mouseleave', clearPickPosition);
window.addEventListener('click', objectClicked)
// Initialize the application
init();