first app vibe

This commit is contained in:
2025-07-28 22:43:55 +01:00
parent d70b6714c3
commit af090f5bf0
2530 changed files with 1410652 additions and 0 deletions

677
app/src/main.ts Normal file
View File

@@ -0,0 +1,677 @@
import './style.css'
import * as THREE from 'three'
import * as CANNON from 'cannon-es'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
interface PhysicsObject {
mesh: THREE.Object3D
body: CANNON.Body
}
interface ModelGeometry {
vertices: number[]
indices: number[]
boundingBox: THREE.Box3
}
class AAFHomepage {
private scene: THREE.Scene
private camera: THREE.PerspectiveCamera
private renderer: THREE.WebGLRenderer
private world: CANNON.World
private physicsObjects: PhysicsObject[] = []
private mouse: THREE.Vector2
private raycaster: THREE.Raycaster
private attractionPoint: THREE.Vector3
private loader: GLTFLoader
private mouseWorldPosition: THREE.Vector3
private lastTime: number = 0
private modelGeometry: ModelGeometry | null = null
constructor() {
console.log('AAFHomepage constructor started')
this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
const canvas = document.getElementById('three-canvas') as HTMLCanvasElement
console.log('Canvas element:', canvas)
if (!canvas) {
console.error('Canvas element not found!')
throw new Error('Canvas element not found')
}
this.renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true
})
this.world = new CANNON.World()
this.mouse = new THREE.Vector2()
this.raycaster = new THREE.Raycaster()
this.attractionPoint = new THREE.Vector3(0, 0, 0)
this.loader = new GLTFLoader()
this.mouseWorldPosition = new THREE.Vector3()
// Initialize the scene
this.init()
}
private init(): void {
console.log('Init method started')
// Setup renderer
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.setPixelRatio(window.devicePixelRatio)
this.renderer.shadowMap.enabled = true
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
this.renderer.outputColorSpace = THREE.SRGBColorSpace
this.renderer.toneMapping = THREE.ACESFilmicToneMapping
this.renderer.toneMappingExposure = 1
console.log('Renderer configured')
// Setup camera (moved closer to the action)
this.camera.position.set(0, 0, 6)
this.camera.lookAt(this.attractionPoint)
console.log('Camera positioned')
// Setup physics world
this.world.gravity.set(0, 0, 0)
// Use SAPBroadphase for better collision detection with many objects
this.world.broadphase = new CANNON.SAPBroadphase(this.world)
// Enable collision detection and response
this.world.allowSleep = false // Prevent objects from sleeping
// Configure contact material for better collisions
const defaultMaterial = new CANNON.Material('default')
const defaultContactMaterial = new CANNON.ContactMaterial(defaultMaterial, defaultMaterial, {
friction: 0.4,
restitution: 0.3,
contactEquationStiffness: 1e8,
contactEquationRelaxation: 3,
frictionEquationStiffness: 1e8,
frictionEquationRelaxation: 3
})
this.world.addContactMaterial(defaultContactMaterial)
this.world.defaultMaterial = defaultMaterial
console.log('Physics world setup')
// Setup lighting
this.setupLighting()
console.log('Lighting setup complete')
// Load and create objects from the main model
this.loadAndCreateObjects()
console.log('Model loading initiated')
// Setup event listeners
this.setupEventListeners()
console.log('Event listeners setup')
// Start animation loop
this.animate()
console.log('Animation loop started')
}
private setupLighting(): void {
// Ambient light for general illumination
const ambientLight = new THREE.AmbientLight(0x404040, 0.3)
this.scene.add(ambientLight)
// Key light (main studio light)
const keyLight = new THREE.DirectionalLight(0xffffff, 1.2)
keyLight.position.set(10, 10, 5)
keyLight.castShadow = true
keyLight.shadow.mapSize.width = 2048
keyLight.shadow.mapSize.height = 2048
keyLight.shadow.camera.near = 0.1
keyLight.shadow.camera.far = 50
keyLight.shadow.camera.left = -10
keyLight.shadow.camera.right = 10
keyLight.shadow.camera.top = 10
keyLight.shadow.camera.bottom = -10
this.scene.add(keyLight)
// Fill light (softer, opposite side)
const fillLight = new THREE.DirectionalLight(0x8888ff, 0.4)
fillLight.position.set(-8, 5, 3)
this.scene.add(fillLight)
// Rim light (for edge definition)
const rimLight = new THREE.DirectionalLight(0xffaa88, 0.6)
rimLight.position.set(0, -10, -5)
this.scene.add(rimLight)
// Point lights for additional atmosphere
const pointLight1 = new THREE.PointLight(0x4444ff, 0.5, 30)
pointLight1.position.set(-5, 5, 10)
this.scene.add(pointLight1)
const pointLight2 = new THREE.PointLight(0xff4444, 0.3, 25)
pointLight2.position.set(5, -5, -8)
this.scene.add(pointLight2)
}
private extractModelGeometry(gltf: any): ModelGeometry | null {
const geometries: THREE.BufferGeometry[] = []
const boundingBox = new THREE.Box3()
// Update world matrices first
gltf.scene.updateMatrixWorld(true)
// Traverse the model to find all mesh geometries
gltf.scene.traverse((child: any) => {
if (child instanceof THREE.Mesh && child.geometry) {
// Apply the mesh's transform to the geometry
const geometry = child.geometry.clone() as THREE.BufferGeometry
// Only apply matrix if it's not identity
if (!child.matrixWorld.equals(new THREE.Matrix4())) {
geometry.applyMatrix4(child.matrixWorld)
}
geometries.push(geometry)
// Update bounding box
const positionAttr = geometry.getAttribute('position') as THREE.BufferAttribute
if (positionAttr) {
const geometryBoundingBox = new THREE.Box3().setFromBufferAttribute(positionAttr)
boundingBox.union(geometryBoundingBox)
}
}
})
if (geometries.length === 0) {
console.warn('No geometries found in model')
return null
}
console.log(`Found ${geometries.length} geometries to merge`)
// Merge all geometries
const combinedGeometry = this.mergeBufferGeometries(geometries)
// Extract vertices and faces
const positionAttribute = combinedGeometry.getAttribute('position') as THREE.BufferAttribute
if (!positionAttribute) {
console.error('No position attribute found in combined geometry')
return null
}
const vertices: number[] = Array.from(positionAttribute.array as Float32Array)
let indices: number[] = []
if (combinedGeometry.index) {
indices = Array.from(combinedGeometry.index.array)
} else {
// Generate indices if none exist
for (let i = 0; i < vertices.length / 3; i++) {
indices.push(i)
}
}
console.log(`Extracted ${vertices.length / 3} vertices and ${indices.length / 3} triangles`)
combinedGeometry.dispose()
return {
vertices,
indices,
boundingBox
}
}
private mergeBufferGeometries(geometries: THREE.BufferGeometry[]): THREE.BufferGeometry {
const merged = new THREE.BufferGeometry()
const attributes: { [key: string]: THREE.BufferAttribute[] } = {}
let indexOffset = 0
const mergedIndices: number[] = []
// Collect all attributes
geometries.forEach(geometry => {
const attributeNames = Object.keys(geometry.attributes)
attributeNames.forEach(name => {
if (!attributes[name]) attributes[name] = []
const attr = geometry.attributes[name]
if (attr instanceof THREE.BufferAttribute) {
attributes[name].push(attr)
}
})
// Handle indices
if (geometry.index) {
const indices = Array.from(geometry.index.array)
indices.forEach(index => mergedIndices.push(index + indexOffset))
indexOffset += geometry.attributes.position.count
}
})
// Merge attributes
Object.keys(attributes).forEach(name => {
const attributeArrays = attributes[name]
const itemSize = attributeArrays[0].itemSize
const normalized = attributeArrays[0].normalized
let totalCount = 0
attributeArrays.forEach(attr => totalCount += attr.count)
const mergedArray = new Float32Array(totalCount * itemSize)
let offset = 0
attributeArrays.forEach(attr => {
mergedArray.set(attr.array as Float32Array, offset)
offset += attr.array.length
})
merged.setAttribute(name, new THREE.BufferAttribute(mergedArray, itemSize, normalized))
})
if (mergedIndices.length > 0) {
merged.setIndex(mergedIndices)
}
return merged
}
private downsampleGeometry(geometry: ModelGeometry, factor: number = 0.3): ModelGeometry {
// Simple downsampling by taking every Nth vertex
const step = Math.max(1, Math.floor(1 / factor))
const downsampledVertices: number[] = []
const downsampledIndices: number[] = []
const vertexMap = new Map<number, number>()
// Downsample vertices
for (let i = 0; i < geometry.vertices.length; i += step * 3) {
if (i + 2 < geometry.vertices.length) {
const originalIndex = i / 3
const newIndex = downsampledVertices.length / 3
vertexMap.set(originalIndex, newIndex)
downsampledVertices.push(
geometry.vertices[i],
geometry.vertices[i + 1],
geometry.vertices[i + 2]
)
}
}
// Downsample indices, keeping only triangles where all vertices are kept
for (let i = 0; i < geometry.indices.length; i += 3) {
const v1 = geometry.indices[i]
const v2 = geometry.indices[i + 1]
const v3 = geometry.indices[i + 2]
if (vertexMap.has(v1) && vertexMap.has(v2) && vertexMap.has(v3)) {
downsampledIndices.push(
vertexMap.get(v1)!,
vertexMap.get(v2)!,
vertexMap.get(v3)!
)
}
}
return {
vertices: downsampledVertices,
indices: downsampledIndices,
boundingBox: geometry.boundingBox
}
}
private createCollisionShape(geometry: ModelGeometry, scale: number = 1): CANNON.Shape {
try {
// For performance, use a simplified approach
// If the geometry is too complex, fall back to a hull or sphere
const vertexCount = geometry.vertices.length / 3
if (vertexCount > 100) {
// For complex meshes, use bounding box approximation
const size = geometry.boundingBox.getSize(new THREE.Vector3())
const scaledSize = size.multiplyScalar(scale)
return new CANNON.Box(new CANNON.Vec3(
scaledSize.x * 0.5,
scaledSize.y * 0.5,
scaledSize.z * 0.5
))
}
// For simpler meshes, try convex hull
const scaledVertices = geometry.vertices.map(v => v * scale)
const vertices: CANNON.Vec3[] = []
for (let i = 0; i < scaledVertices.length; i += 3) {
vertices.push(new CANNON.Vec3(
scaledVertices[i],
scaledVertices[i + 1],
scaledVertices[i + 2]
))
}
// Limit faces to prevent performance issues
const faces: number[][] = []
const maxFaces = Math.min(geometry.indices.length / 3, 50)
for (let i = 0; i < maxFaces * 3; i += 3) {
const v1 = geometry.indices[i]
const v2 = geometry.indices[i + 1]
const v3 = geometry.indices[i + 2]
if (v1 < vertices.length && v2 < vertices.length && v3 < vertices.length) {
faces.push([v1, v2, v3])
}
}
if (faces.length > 0) {
const shape = new CANNON.ConvexPolyhedron({ vertices, faces })
return shape
} else {
throw new Error('No valid faces')
}
} catch (error) {
console.warn('Failed to create complex collision shape, falling back to sphere:', error)
// Fallback to sphere using bounding box
const size = geometry.boundingBox.getSize(new THREE.Vector3())
const radius = Math.max(size.x, size.y, size.z) * 0.5 * scale
return new CANNON.Sphere(Math.max(0.3, radius))
}
}
private async loadAndCreateObjects(): Promise<void> {
try {
console.log('Loading main_model.glb...')
const gltf = await this.loader.loadAsync('/models/main_model.glb')
console.log('Model loaded successfully:', gltf)
// Extract and downsample the geometry for collision detection
const originalGeometry = this.extractModelGeometry(gltf)
if (originalGeometry) {
console.log('Extracted geometry with', originalGeometry.vertices.length / 3, 'vertices')
this.modelGeometry = this.downsampleGeometry(originalGeometry, 0.3) // 30% of original detail
console.log('Downsampled to', this.modelGeometry.vertices.length / 3, 'vertices for collision')
} else {
console.warn('Failed to extract geometry, will use simple collision shapes')
this.modelGeometry = null
}
// Create multiple instances of the loaded model
const numInstances = 20
gltf.scene.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.castShadow = true
child.receiveShadow = true
}
})
for (let i = 0; i < numInstances; i++) {
// Clone the model
const modelClone = gltf.scene.clone()
// Start objects close to the attraction point for immediate clustering
const x = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
const y = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
const z = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
modelClone.position.set(x, y, z)
// Random scale variation (40% of current size)
const scale = (0.5 + Math.random() * 0.5) // Scaled down to 40%
modelClone.scale.setScalar(scale)
// Create physics body with custom collision shape or fallback to sphere
let shape: CANNON.Shape
if (this.modelGeometry) {
shape = this.createCollisionShape(this.modelGeometry, scale)
} else {
// Fallback to sphere if geometry extraction failed
const radius = Math.max(0.3, 0.8 * scale)
shape = new CANNON.Sphere(radius)
}
const body = new CANNON.Body({
mass: 1,
material: this.world.defaultMaterial,
type: CANNON.Body.DYNAMIC
})
body.addShape(shape)
body.position.set(x, y, z)
// Enable rotational dynamics
body.angularDamping = 0.1 // Small damping for realistic rotation
body.linearDamping = 0.05 // Small linear damping
// Prevent objects from sleeping during collisions
body.sleepSpeedLimit = 0.1
body.sleepTimeLimit = 1
// Add collision event listener with rotational effects
body.addEventListener('collide', (event: any) => {
console.log('Collision detected!')
// Add some spin on collision for more dynamic movement
const impactStrength = event.contact?.getImpactVelocityAlongNormal() || 1
const spinForce = Math.min(impactStrength * 0.5, 2) // Cap the spin force
body.angularVelocity.set(
body.angularVelocity.x + (Math.random() - 0.5) * spinForce,
body.angularVelocity.y + (Math.random() - 0.5) * spinForce,
body.angularVelocity.z + (Math.random() - 0.5) * spinForce
)
})
// Add gentle initial velocity and rotation
body.velocity.set(
(Math.random() - 0.5) * 1, // Reduced from 3 to 1
(Math.random() - 0.5) * 1, // Reduced from 3 to 1
(Math.random() - 0.5) * 1 // Reduced from 3 to 1
)
// Add initial angular velocity for natural rotation
body.angularVelocity.set(
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2
)
this.scene.add(modelClone)
this.world.addBody(body)
this.physicsObjects.push({ mesh: modelClone, body })
}
console.log('Created', numInstances, 'instances of main_model with custom collision shapes')
} catch (error) {
console.error('Failed to load main_model.glb:', error)
console.log('Falling back to placeholder objects...')
this.createFallbackObjects()
}
}
private createFallbackObjects(): void {
// Fallback geometric objects if model loading fails
const geometries = [
new THREE.BoxGeometry(0.5, 0.5, 0.5),
new THREE.SphereGeometry(0.3, 16, 16),
new THREE.CylinderGeometry(0.2, 0.2, 0.6, 12),
]
const materials = [
new THREE.MeshStandardMaterial({ color: 0x4a9eff, metalness: 0.7, roughness: 0.3 }),
new THREE.MeshStandardMaterial({ color: 0xff6b4a, metalness: 0.5, roughness: 0.4 }),
new THREE.MeshStandardMaterial({ color: 0x4aff6b, metalness: 0.8, roughness: 0.2 }),
]
for (let i = 0; i < 15; i++) {
const geometry = geometries[i % geometries.length]
const material = materials[i % materials.length]
const mesh = new THREE.Mesh(geometry, material)
mesh.castShadow = true
mesh.receiveShadow = true
// Start objects close to the attraction point for immediate clustering
const x = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
const y = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
const z = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
mesh.position.set(x, y, z)
// Create physics body with rotation enabled
const shape = new CANNON.Sphere(0.3)
const body = new CANNON.Body({
mass: 1,
material: this.world.defaultMaterial,
type: CANNON.Body.DYNAMIC
})
body.addShape(shape)
body.position.set(x, y, z)
// Enable rotational dynamics
body.angularDamping = 0.1
body.linearDamping = 0.05
body.velocity.set(
(Math.random() - 0.5) * 1, // Reduced from 2 to 1
(Math.random() - 0.5) * 1, // Reduced from 2 to 1
(Math.random() - 0.5) * 1 // Reduced from 2 to 1
)
// Add initial angular velocity
body.angularVelocity.set(
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2
)
this.scene.add(mesh)
this.world.addBody(body)
this.physicsObjects.push({ mesh, body })
}
}
private setupEventListeners(): void {
// Mouse move event
window.addEventListener('mousemove', (event) => {
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
// Update raycaster
this.raycaster.setFromCamera(this.mouse, this.camera)
// Calculate mouse position in world space
const distance = 10 // Distance from camera
this.mouseWorldPosition.copy(this.raycaster.ray.direction)
this.mouseWorldPosition.multiplyScalar(distance)
this.mouseWorldPosition.add(this.camera.position)
})
// Window resize event
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight)
})
}
private updatePhysics(deltaTime: number): void {
// Use a smaller, more stable timestep for better collision detection
const fixedTimeStep = 1/60 // 60 Hz - more stable than 120 Hz
const maxSubSteps = 5 // Increased substeps for better collision accuracy
this.world.step(fixedTimeStep, deltaTime, maxSubSteps)
// Apply forces to objects
this.physicsObjects.forEach((obj) => {
// Attraction to center point
const attractionForce = new CANNON.Vec3()
attractionForce.x = this.attractionPoint.x - obj.body.position.x
attractionForce.y = this.attractionPoint.y - obj.body.position.y
attractionForce.z = this.attractionPoint.z - obj.body.position.z
const distance = Math.sqrt(
attractionForce.x * attractionForce.x +
attractionForce.y * attractionForce.y +
attractionForce.z * attractionForce.z
)
if (distance > 0) {
const strength = 16.0 / (distance * distance + 1) // Reduced from 8.0 to prevent objects moving too fast
attractionForce.scale(strength, attractionForce)
obj.body.force.set(
obj.body.force.x + attractionForce.x,
obj.body.force.y + attractionForce.y,
obj.body.force.z + attractionForce.z
)
}
// Mouse repulsion
const repulsionForce = new CANNON.Vec3()
repulsionForce.x = obj.body.position.x - this.mouseWorldPosition.x
repulsionForce.y = obj.body.position.y - this.mouseWorldPosition.y
repulsionForce.z = obj.body.position.z - this.mouseWorldPosition.z
const mouseDistance = Math.sqrt(
repulsionForce.x * repulsionForce.x +
repulsionForce.y * repulsionForce.y +
repulsionForce.z * repulsionForce.z
)
if (mouseDistance < 3 && mouseDistance > 0) {
const repulsionStrength = 5.0 / (mouseDistance * mouseDistance + 0.1)
repulsionForce.scale(repulsionStrength, repulsionForce)
obj.body.force.set(
obj.body.force.x + repulsionForce.x,
obj.body.force.y + repulsionForce.y,
obj.body.force.z + repulsionForce.z
)
// Add rotational torque from mouse interaction
const torque = new CANNON.Vec3(
(Math.random() - 0.5) * repulsionStrength * 0.1,
(Math.random() - 0.5) * repulsionStrength * 0.1,
(Math.random() - 0.5) * repulsionStrength * 0.1
)
obj.body.torque.set(
obj.body.torque.x + torque.x,
obj.body.torque.y + torque.y,
obj.body.torque.z + torque.z
)
}
// Apply damping
obj.body.velocity.scale(0.99, obj.body.velocity)
// Update mesh position to match physics body
obj.mesh.position.copy(obj.body.position as any)
obj.mesh.quaternion.copy(obj.body.quaternion as any)
})
}
private animate(currentTime: number = 0): void {
requestAnimationFrame((time) => this.animate(time))
// Calculate delta time
const deltaTime = this.lastTime > 0 ? (currentTime - this.lastTime) / 1000 : 1/60
this.lastTime = currentTime
// Clamp delta time to prevent large jumps
const clampedDeltaTime = Math.min(deltaTime, 1/30)
this.updatePhysics(clampedDeltaTime)
this.renderer.render(this.scene, this.camera)
}
}
// Initialize the application
const app = new AAFHomepage()
export default app

47
app/src/style.css Normal file
View File

@@ -0,0 +1,47 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: #000;
color: #fff;
}
#app {
width: 100vw;
height: 100vh;
position: relative;
}
.logo {
position: absolute;
top: 2rem;
left: 2rem;
z-index: 100;
font-size: 1.5rem;
font-weight: 600;
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
letter-spacing: 0.5px;
user-select: none;
}
#three-canvas {
width: 100%;
height: 100%;
display: block;
cursor: crosshair;
}
@media (max-width: 768px) {
.logo {
top: 1rem;
left: 1rem;
font-size: 1.25rem;
}
}

1
app/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />