ivy-gpu-art-studio / js /threejs-renderer.js
Elysia-Suite's picture
Upload 23 files
e5d943e verified
/**
* 🌿 Ivy's Creative Studio
* Tab 6: Three.js 3D Renderer
*
* Interactive 3D scenes with Three.js
* Now with 8 scenes, 8 palettes, 6 materials, and effects!
*/
class ThreeJSRenderer {
constructor() {
this.scene = null;
this.camera = null;
this.renderer = null;
this.controls = null;
this.objects = [];
this.animationId = null;
this.isActive = false;
this.clock = new THREE.Clock();
// Parameters
this.params = {
sceneType: "cubes",
materialType: "standard",
palette: "rainbow",
objectCount: 50,
speed: 1.0,
scale: 1.0,
wireframe: false,
autoRotate: true,
shadows: false,
bloom: false
};
}
init(canvas) {
this.canvas = canvas;
// Create scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x0a0a0f);
this.scene.fog = new THREE.Fog(0x0a0a0f, 10, 50);
// Use fallback dimensions if canvas is hidden (will be resized properly on start())
const width = canvas.clientWidth || 800;
const height = canvas.clientHeight || 500;
// Create camera
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
this.camera.position.set(0, 5, 15);
// Create renderer
this.renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true
});
this.renderer.setSize(width, height);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Add lights
this.setupLights();
// Add controls
this.controls = new THREE.OrbitControls(this.camera, canvas);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.autoRotate = this.params.autoRotate;
this.controls.autoRotateSpeed = 0.5;
// Create initial scene
this.createScene();
// Handle resize
this.handleResize = this.handleResize.bind(this);
window.addEventListener("resize", this.handleResize);
}
setupLights() {
// Ambient light
const ambient = new THREE.AmbientLight(0x404040, 0.5);
this.scene.add(ambient);
// Directional light
const directional = new THREE.DirectionalLight(0xffffff, 1);
directional.position.set(5, 10, 7);
this.scene.add(directional);
// Point lights for color
const pointLight1 = new THREE.PointLight(0x6366f1, 1, 50);
pointLight1.position.set(-10, 5, 0);
this.scene.add(pointLight1);
const pointLight2 = new THREE.PointLight(0x8b5cf6, 1, 50);
pointLight2.position.set(10, 5, 0);
this.scene.add(pointLight2);
this.lights = { ambient, directional, pointLight1, pointLight2 };
}
clearScene() {
// Remove all objects except lights and camera
for (const obj of this.objects) {
this.scene.remove(obj);
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
if (Array.isArray(obj.material)) {
obj.material.forEach(m => m.dispose());
} else {
obj.material.dispose();
}
}
}
this.objects = [];
}
createScene() {
this.clearScene();
switch (this.params.sceneType) {
case "cubes":
this.createCubesScene();
break;
case "particles":
this.createParticlesScene();
break;
case "terrain":
this.createTerrainScene();
break;
case "galaxy":
this.createGalaxyScene();
break;
case "ivy":
this.createIvyScene();
break;
case "torus":
this.createTorusScene();
break;
case "crystals":
this.createCrystalsScene();
break;
case "ocean":
this.createOceanScene();
break;
}
}
getColor(index, total) {
const t = index / total;
switch (this.params.palette) {
case "ivy":
return new THREE.Color().setHSL(0.35 + t * 0.1, 0.7, 0.4 + t * 0.2);
case "rainbow":
return new THREE.Color().setHSL(t, 0.8, 0.5);
case "neon":
const neonHues = [0.85, 0.15, 0.55, 0.75];
return new THREE.Color().setHSL(neonHues[index % 4], 1.0, 0.5);
case "fire":
return new THREE.Color().setHSL(0.05 + t * 0.08, 1.0, 0.4 + t * 0.2);
case "ocean":
return new THREE.Color().setHSL(0.55 + t * 0.1, 0.7, 0.4 + t * 0.3);
case "pastel":
return new THREE.Color().setHSL(t, 0.5, 0.75);
case "cosmic":
return new THREE.Color().setHSL(0.7 + t * 0.2, 0.8, 0.3 + t * 0.4);
case "monochrome":
return new THREE.Color().setHSL(0.7, 0.0, 0.3 + t * 0.5);
default:
return new THREE.Color().setHSL(t, 0.8, 0.5);
}
}
createMaterial(color) {
const baseProps = {
color: color,
wireframe: this.params.wireframe
};
switch (this.params.materialType) {
case "standard":
return new THREE.MeshStandardMaterial({
...baseProps,
metalness: 0.3,
roughness: 0.6
});
case "phong":
return new THREE.MeshPhongMaterial({
...baseProps,
shininess: 100,
specular: 0x444444
});
case "toon":
return new THREE.MeshToonMaterial(baseProps);
case "glass":
return new THREE.MeshPhysicalMaterial({
...baseProps,
metalness: 0.0,
roughness: 0.0,
transmission: 0.9,
thickness: 0.5,
transparent: true,
opacity: 0.8
});
case "metal":
return new THREE.MeshStandardMaterial({
...baseProps,
metalness: 1.0,
roughness: 0.2
});
case "emissive":
return new THREE.MeshStandardMaterial({
...baseProps,
metalness: 0.0,
roughness: 0.5,
emissive: color,
emissiveIntensity: 0.5
});
default:
return new THREE.MeshStandardMaterial(baseProps);
}
}
createCubesScene() {
const count = this.params.objectCount;
const scale = this.params.scale;
for (let i = 0; i < count; i++) {
const size = (Math.random() * 1 + 0.5) * scale;
const geometry = new THREE.BoxGeometry(size, size, size);
const material = this.createMaterial(this.getColor(i, count));
const cube = new THREE.Mesh(geometry, material);
// Random position in sphere
const radius = 8 + Math.random() * 8;
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
cube.position.set(radius * Math.sin(phi) * Math.cos(theta), radius * Math.cos(phi) - 2, radius * Math.sin(phi) * Math.sin(theta));
cube.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
// Store animation data
cube.userData = {
rotationSpeed: {
x: (Math.random() - 0.5) * 0.02,
y: (Math.random() - 0.5) * 0.02,
z: (Math.random() - 0.5) * 0.02
},
floatOffset: Math.random() * Math.PI * 2,
floatSpeed: Math.random() * 0.5 + 0.5,
originalY: cube.position.y
};
if (this.params.shadows) {
cube.castShadow = true;
cube.receiveShadow = true;
}
this.scene.add(cube);
this.objects.push(cube);
}
}
createParticlesScene() {
const count = this.params.objectCount * 100;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const i3 = i * 3;
// Spherical distribution
const radius = 5 + Math.random() * 10;
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i3 + 1] = radius * Math.cos(phi);
positions[i3 + 2] = radius * Math.sin(phi) * Math.sin(theta);
const color = this.getColor(i, count);
colors[i3] = color.r;
colors[i3 + 1] = color.g;
colors[i3 + 2] = color.b;
}
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.1,
vertexColors: true,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending
});
const particles = new THREE.Points(geometry, material);
particles.userData = { isParticles: true };
this.scene.add(particles);
this.objects.push(particles);
}
createTerrainScene() {
const size = 30;
const segments = this.params.objectCount;
const geometry = new THREE.PlaneGeometry(size, size, segments, segments);
const positions = geometry.attributes.position;
// Generate terrain heights using noise
for (let i = 0; i < positions.count; i++) {
const x = positions.getX(i);
const y = positions.getY(i);
// Simple noise-like height
let height = 0;
height += Math.sin(x * 0.5) * Math.cos(y * 0.5) * 2;
height += Math.sin(x * 0.2 + y * 0.3) * 1.5;
height += Math.random() * 0.2;
positions.setZ(i, height);
}
geometry.computeVertexNormals();
const material = new THREE.MeshStandardMaterial({
color: this.getColor(0, 1),
wireframe: this.params.wireframe,
side: THREE.DoubleSide,
flatShading: true
});
const terrain = new THREE.Mesh(geometry, material);
terrain.rotation.x = -Math.PI / 2;
terrain.position.y = -3;
terrain.userData = { isTerrain: true };
this.scene.add(terrain);
this.objects.push(terrain);
// Add water plane
const waterGeometry = new THREE.PlaneGeometry(size, size);
const waterMaterial = new THREE.MeshStandardMaterial({
color: 0x1e90ff,
transparent: true,
opacity: 0.6,
metalness: 0.8,
roughness: 0.2
});
const water = new THREE.Mesh(waterGeometry, waterMaterial);
water.rotation.x = -Math.PI / 2;
water.position.y = -2;
water.userData = { isWater: true };
this.scene.add(water);
this.objects.push(water);
}
createGalaxyScene() {
const count = this.params.objectCount * 200;
const branches = 5;
const spin = 2;
const radius = 15;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const i3 = i * 3;
const r = Math.random() * radius;
const branchAngle = ((i % branches) / branches) * Math.PI * 2;
const spinAngle = r * spin;
// Add randomness
const randomX = (Math.random() - 0.5) * (radius - r) * 0.3;
const randomY = (Math.random() - 0.5) * (radius - r) * 0.1;
const randomZ = (Math.random() - 0.5) * (radius - r) * 0.3;
positions[i3] = Math.cos(branchAngle + spinAngle) * r + randomX;
positions[i3 + 1] = randomY;
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * r + randomZ;
// Color gradient from center (white) to edge (colored)
const mixRatio = r / radius;
const branchColor = this.getColor(i % branches, branches);
colors[i3] = 1 - mixRatio * (1 - branchColor.r);
colors[i3 + 1] = 1 - mixRatio * (1 - branchColor.g);
colors[i3 + 2] = 1 - mixRatio * (1 - branchColor.b);
}
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.05,
vertexColors: true,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});
const galaxy = new THREE.Points(geometry, material);
galaxy.userData = { isGalaxy: true };
this.scene.add(galaxy);
this.objects.push(galaxy);
// Center glow
const glowGeometry = new THREE.SphereGeometry(0.5, 32, 32);
const glowMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.8
});
const glow = new THREE.Mesh(glowGeometry, glowMaterial);
this.scene.add(glow);
this.objects.push(glow);
}
// 🌿 IVY SCENE - Organic vines in 3D!
createIvyScene() {
const ivyGreen = new THREE.Color(0x22c55e);
const darkGreen = new THREE.Color(0x166534);
const leafGreen = new THREE.Color(0x4ade80);
// Create multiple vine spirals
const numVines = Math.min(this.params.objectCount, 8);
for (let v = 0; v < numVines; v++) {
const vineAngle = (v / numVines) * Math.PI * 2;
const vineRadius = 3 + Math.random() * 2;
// Vine stem - spiral tube
const curve = new THREE.CatmullRomCurve3([]);
const segments = 50;
for (let i = 0; i < segments; i++) {
const t = i / segments;
const y = t * 15 - 7;
const spiralAngle = vineAngle + t * Math.PI * 4;
const r = vineRadius * (1 - t * 0.3);
curve.points.push(new THREE.Vector3(Math.cos(spiralAngle) * r, y, Math.sin(spiralAngle) * r));
}
const tubeGeometry = new THREE.TubeGeometry(curve, 64, 0.1, 8, false);
const tubeMaterial = new THREE.MeshStandardMaterial({
color: darkGreen,
roughness: 0.8,
metalness: 0.1
});
const vine = new THREE.Mesh(tubeGeometry, tubeMaterial);
vine.userData = { isVine: true, vineIndex: v };
this.scene.add(vine);
this.objects.push(vine);
// Add leaves along the vine
const numLeaves = 15 + Math.floor(Math.random() * 10);
for (let l = 0; l < numLeaves; l++) {
const t = (l / numLeaves) * 0.9 + 0.05;
const point = curve.getPoint(t);
// Leaf shape (heart-like)
const leafShape = new THREE.Shape();
const leafSize = 0.3 + Math.random() * 0.2;
leafShape.moveTo(0, 0);
leafShape.bezierCurveTo(leafSize * 0.5, leafSize * 0.3, leafSize * 0.8, leafSize * 0.8, 0, leafSize * 1.2);
leafShape.bezierCurveTo(-leafSize * 0.8, leafSize * 0.8, -leafSize * 0.5, leafSize * 0.3, 0, 0);
const leafGeometry = new THREE.ShapeGeometry(leafShape);
const leafMaterial = new THREE.MeshStandardMaterial({
color: leafGreen.clone().lerp(ivyGreen, Math.random()),
side: THREE.DoubleSide,
roughness: 0.6,
metalness: 0.0
});
const leaf = new THREE.Mesh(leafGeometry, leafMaterial);
leaf.position.copy(point);
// Random rotation
leaf.rotation.x = Math.random() * Math.PI;
leaf.rotation.y = Math.random() * Math.PI * 2;
leaf.rotation.z = Math.random() * 0.5;
leaf.userData = { isLeaf: true, initialRotation: leaf.rotation.clone() };
this.scene.add(leaf);
this.objects.push(leaf);
}
}
// Add some floating particles (pollen/sparkles)
const particleCount = 500;
const particleGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 20;
positions[i * 3 + 1] = (Math.random() - 0.5) * 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
}
particleGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
const particleMaterial = new THREE.PointsMaterial({
size: 0.05,
color: 0xffffff,
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending
});
const particles = new THREE.Points(particleGeometry, particleMaterial);
particles.userData = { isParticles: true };
this.scene.add(particles);
this.objects.push(particles);
}
start() {
this.isActive = true;
this.canvas.classList.remove("hidden");
// Wait for the canvas to be visible and have dimensions before resizing
// Double RAF ensures the browser has completed layout calculations
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this.handleResize();
this.animate();
});
});
}
stop() {
this.isActive = false;
this.canvas.classList.add("hidden");
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
reset() {
this.camera.position.set(0, 5, 15);
this.camera.lookAt(0, 0, 0);
this.controls.reset();
}
setSceneType(type) {
this.params.sceneType = type;
this.createScene();
}
setMaterialType(type) {
this.params.materialType = type;
this.createScene();
}
setPalette(palette) {
this.params.palette = palette;
this.createScene();
}
setObjectCount(count) {
this.params.objectCount = count;
this.createScene();
}
setSpeed(speed) {
this.params.speed = speed;
}
setScale(scale) {
this.params.scale = scale;
this.createScene();
}
setWireframe(enabled) {
this.params.wireframe = enabled;
for (const obj of this.objects) {
if (obj.material && !obj.userData.isParticles && !obj.userData.isGalaxy) {
obj.material.wireframe = enabled;
}
}
}
setAutoRotate(enabled) {
this.params.autoRotate = enabled;
if (this.controls) {
this.controls.autoRotate = enabled;
}
}
setShadows(enabled) {
this.params.shadows = enabled;
this.renderer.shadowMap.enabled = enabled;
if (this.lights && this.lights.directional) {
this.lights.directional.castShadow = enabled;
}
this.createScene();
}
setBloom(enabled) {
this.params.bloom = enabled;
// Note: Full bloom requires post-processing pass
// For now we enhance emissive materials
if (enabled) {
this.scene.background = new THREE.Color(0x050508);
} else {
this.scene.background = new THREE.Color(0x0a0a0f);
}
}
// === NEW SCENES ===
createTorusScene() {
const count = Math.floor(this.params.objectCount / 5);
const scale = this.params.scale;
for (let i = 0; i < count; i++) {
const radius = (1 + Math.random() * 0.5) * scale;
const tube = (0.3 + Math.random() * 0.2) * scale;
const geometry = new THREE.TorusKnotGeometry(radius, tube, 100, 16, 2 + (i % 5), 3 + (i % 7));
const material = this.createMaterial(this.getColor(i, count));
const torus = new THREE.Mesh(geometry, material);
const angle = (i / count) * Math.PI * 2;
const distance = 5 + Math.random() * 5;
torus.position.set(Math.cos(angle) * distance, (Math.random() - 0.5) * 6, Math.sin(angle) * distance);
torus.userData = {
rotationSpeed: {
x: (Math.random() - 0.5) * 0.01,
y: (Math.random() - 0.5) * 0.02,
z: (Math.random() - 0.5) * 0.01
}
};
this.scene.add(torus);
this.objects.push(torus);
}
}
createCrystalsScene() {
const count = this.params.objectCount;
const scale = this.params.scale;
for (let i = 0; i < count; i++) {
// Create crystal-like geometry (octahedron or icosahedron)
const geometryType =
Math.random() > 0.5
? new THREE.OctahedronGeometry((0.5 + Math.random()) * scale, 0)
: new THREE.IcosahedronGeometry((0.4 + Math.random() * 0.6) * scale, 0);
const material = this.createMaterial(this.getColor(i, count));
if (material.transparent === undefined) {
material.transparent = true;
material.opacity = 0.7 + Math.random() * 0.3;
}
const crystal = new THREE.Mesh(geometryType, material);
// Cluster formation
const cluster = Math.floor(i / 10);
const clusterAngle = cluster * 0.8;
const clusterRadius = 3 + cluster * 0.5;
crystal.position.set(
Math.cos(clusterAngle) * clusterRadius + (Math.random() - 0.5) * 3,
(Math.random() - 0.5) * 8 - 2,
Math.sin(clusterAngle) * clusterRadius + (Math.random() - 0.5) * 3
);
crystal.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
crystal.userData = {
rotationSpeed: {
x: (Math.random() - 0.5) * 0.005,
y: (Math.random() - 0.5) * 0.01,
z: (Math.random() - 0.5) * 0.005
},
floatOffset: Math.random() * Math.PI * 2,
floatSpeed: Math.random() * 0.3 + 0.2,
originalY: crystal.position.y
};
this.scene.add(crystal);
this.objects.push(crystal);
}
}
createOceanScene() {
const scale = this.params.scale;
// Create water plane
const waterGeometry = new THREE.PlaneGeometry(40 * scale, 40 * scale, 128, 128);
const waterMaterial = new THREE.MeshPhongMaterial({
color: this.getColor(0, 1),
shininess: 100,
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide
});
const water = new THREE.Mesh(waterGeometry, waterMaterial);
water.rotation.x = -Math.PI / 2;
water.position.y = -2;
water.userData = { isWater: true, geometry: waterGeometry };
this.scene.add(water);
this.objects.push(water);
// Add floating objects
const floatCount = Math.floor(this.params.objectCount / 3);
for (let i = 0; i < floatCount; i++) {
const size = (0.3 + Math.random() * 0.5) * scale;
const geometry = new THREE.SphereGeometry(size, 16, 16);
const material = this.createMaterial(this.getColor(i, floatCount));
const sphere = new THREE.Mesh(geometry, material);
sphere.position.set((Math.random() - 0.5) * 30, -1.5 + Math.random() * 0.5, (Math.random() - 0.5) * 30);
sphere.userData = {
floatOffset: Math.random() * Math.PI * 2,
floatSpeed: Math.random() * 0.5 + 0.3,
originalY: sphere.position.y,
waveX: sphere.position.x,
waveZ: sphere.position.z
};
this.scene.add(sphere);
this.objects.push(sphere);
}
}
handleResize() {
if (!this.canvas || !this.isActive) return;
// Get dimensions from parent container for proper sizing
const container = this.canvas.parentElement;
const width = container?.clientWidth || this.canvas.clientWidth || window.innerWidth;
const height = container?.clientHeight || this.canvas.clientHeight || window.innerHeight;
// Skip if dimensions are still zero
if (width === 0 || height === 0) return;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
// Set renderer size (this sets canvas width/height attributes)
this.renderer.setSize(width, height, false);
// Force canvas to fill container via CSS (Three.js setSize overrides this)
this.canvas.style.width = "100%";
this.canvas.style.height = "100%";
}
animate() {
if (!this.isActive) return;
this.animationId = requestAnimationFrame(() => this.animate());
const delta = this.clock.getDelta();
const elapsed = this.clock.getElapsedTime();
const speed = this.params.speed;
// Update controls
this.controls.update();
// Animate objects
for (const obj of this.objects) {
if (obj.userData.rotationSpeed) {
obj.rotation.x += obj.userData.rotationSpeed.x * speed;
obj.rotation.y += obj.userData.rotationSpeed.y * speed;
obj.rotation.z += obj.userData.rotationSpeed.z * speed;
// Float animation
if (obj.userData.floatOffset !== undefined) {
obj.position.y = obj.userData.originalY + Math.sin(elapsed * obj.userData.floatSpeed * speed + obj.userData.floatOffset) * 0.5;
}
}
if (obj.userData.isParticles || obj.userData.isGalaxy) {
obj.rotation.y += 0.001 * speed;
}
if (obj.userData.isWater && obj.userData.geometry) {
// Animate water waves
const positions = obj.userData.geometry.attributes.position;
for (let i = 0; i < positions.count; i++) {
const x = positions.getX(i);
const y = positions.getY(i);
const wave = Math.sin(x * 0.5 + elapsed * speed) * 0.3 + Math.sin(y * 0.3 + elapsed * speed * 0.7) * 0.2;
positions.setZ(i, wave);
}
positions.needsUpdate = true;
obj.userData.geometry.computeVertexNormals();
}
// Floating objects on water
if (obj.userData.waveX !== undefined) {
const wx = obj.userData.waveX;
const wz = obj.userData.waveZ;
const wave = Math.sin(wx * 0.5 + elapsed * speed) * 0.3 + Math.sin(wz * 0.3 + elapsed * speed * 0.7) * 0.2;
obj.position.y = obj.userData.originalY + wave;
obj.rotation.x = Math.sin(elapsed * speed + obj.userData.floatOffset) * 0.1;
obj.rotation.z = Math.cos(elapsed * speed * 0.7 + obj.userData.floatOffset) * 0.1;
}
// 🌿 Ivy leaf animation
if (obj.userData.isLeaf) {
const init = obj.userData.initialRotation;
obj.rotation.x = init.x + Math.sin(elapsed * 0.5 * speed + obj.position.y) * 0.1;
obj.rotation.y = init.y + Math.sin(elapsed * 0.3 * speed + obj.position.x) * 0.15;
obj.rotation.z = init.z + Math.cos(elapsed * 0.4 * speed) * 0.1;
}
}
// Animate point lights
if (this.lights) {
this.lights.pointLight1.position.x = Math.sin(elapsed * 0.5) * 10;
this.lights.pointLight2.position.x = Math.cos(elapsed * 0.5) * 10;
}
this.renderer.render(this.scene, this.camera);
}
dispose() {
this.stop();
this.clearScene();
if (this.renderer) {
this.renderer.dispose();
}
window.removeEventListener("resize", this.handleResize);
}
}
// Export
window.ThreeJSRenderer = ThreeJSRenderer;