coolbro-testing / index.html
Jesse Afolabi
Add 2 files
3a7a5f3 verified
Raw
History Blame Contribute Delete
70.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Babylon.js 8 Advanced 3D Application</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
<script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
<script src="https://cdn.babylonjs.com/proceduralTexturesLibrary/babylon.proceduralTextures.min.js"></script>
<script src="https://cdn.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script>
<style>
#renderCanvas {
width: 100%;
height: 100%;
touch-action: none;
outline: none;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
transition: opacity 0.5s;
}
.progress-bar {
width: 300px;
height: 4px;
background: #444;
margin-top: 20px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
width: 0%;
transition: width 0.3s;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.pulse {
animation: pulse 2s infinite;
}
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 200px;
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
text-align: center;
border-radius: 6px;
padding: 8px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
.panel-slide {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.panel-slide.hidden {
transform: translateY(100%);
opacity: 0;
}
</style>
</head>
<body class="bg-gray-900 text-gray-100 overflow-hidden">
<!-- Loading Screen -->
<div id="loadingOverlay" class="loading-overlay">
<div class="text-4xl font-bold mb-4 bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
Loading 3D World
</div>
<div class="text-gray-400 mb-2">Initializing Babylon.js 8...</div>
<div class="progress-bar">
<div id="progressFill" class="progress-fill"></div>
</div>
</div>
<!-- Main Container -->
<div class="flex flex-col h-screen">
<!-- Top Status Bar -->
<div class="bg-gray-800 py-2 px-4 flex justify-between items-center">
<div class="text-xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
Babylon.js 8 Advanced Demo
</div>
<div class="flex space-x-2">
<div id="fpsCounter" class="px-3 py-1 bg-gray-700 rounded-full text-xs">0 FPS</div>
<div id="objectCount" class="px-3 py-1 bg-gray-700 rounded-full text-xs">0 Objects</div>
<div id="sceneInfo" class="px-3 py-1 bg-gray-700 rounded-full text-xs">Default Scene</div>
</div>
</div>
<!-- Main Content Area -->
<div class="flex flex-1 overflow-hidden">
<!-- Side Panel (Collapsible) -->
<div id="sidePanel" class="w-64 bg-gray-800 border-r border-gray-700 flex flex-col transition-all duration-300 ease-in-out" style="min-width: 16rem;">
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
<h2 class="text-lg font-semibold">Scene Controls</h2>
<button id="togglePanel" class="text-gray-400 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M15.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
<div class="flex-1 overflow-y-auto">
<!-- Scene Management -->
<div class="p-4 border-b border-gray-700">
<h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Scene Management</h3>
<div class="space-y-2">
<button id="newScene" class="w-full px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
New Scene
</button>
<button id="saveScene" class="w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
</svg>
Save Scene
</button>
</div>
</div>
<!-- Environment Settings -->
<div class="p-4 border-b border-gray-700">
<h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Environment</h3>
<div class="space-y-3">
<div>
<label class="block text-xs text-gray-400 mb-1">Skybox</label>
<select id="skyboxSelect" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="default">Default</option>
<option value="sunset">Sunset</option>
<option value="night">Night</option>
<option value="studio">Studio</option>
<option value="park">Park</option>
<option value="none">None</option>
</select>
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Environment Intensity</label>
<input id="envIntensity" type="range" min="0" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer">
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-gray-400">Fog</label>
<button id="toggleFog" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs">
Toggle
</button>
</div>
</div>
</div>
<!-- Lighting -->
<div class="p-4 border-b border-gray-700">
<h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Lighting</h3>
<div class="space-y-3">
<div>
<label class="block text-xs text-gray-400 mb-1">Hemispheric Light</label>
<div class="flex space-x-2">
<button id="toggleHemiLight" class="flex-1 bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs">
Toggle
</button>
<button id="adjustHemiLight" class="flex-1 bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs">
Adjust
</button>
</div>
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Directional Light</label>
<div class="flex space-x-2">
<button id="toggleDirLight" class="flex-1 bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs">
Toggle
</button>
<button id="adjustDirLight" class="flex-1 bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs">
Adjust
</button>
</div>
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Light Intensity</label>
<input id="lightIntensity" type="range" min="0" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer">
</div>
</div>
</div>
<!-- Objects -->
<div class="p-4 border-b border-gray-700">
<h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Objects</h3>
<div class="grid grid-cols-2 gap-2">
<button id="addSphere" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-2 text-xs flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
Sphere
</button>
<button id="addBox" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-2 text-xs flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
Box
</button>
<button id="addCylinder" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-2 text-xs flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18.5V20m0 0v1.5m0-1.5h-1.5m1.5 0h1.5M5 10v4h14v-4H5z" />
</svg>
Cylinder
</button>
<button id="addPlane" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-2 text-xs flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Plane
</button>
</div>
<div class="mt-3">
<button id="clearObjects" class="w-full bg-red-600 hover:bg-red-700 rounded px-3 py-2 text-sm flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Clear All
</button>
</div>
</div>
<!-- Materials -->
<div class="p-4 border-b border-gray-700">
<h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Materials</h3>
<div class="space-y-3">
<div>
<label class="block text-xs text-gray-400 mb-1">Material Type</label>
<select id="materialType" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="standard">Standard</option>
<option value="pbr">PBR</option>
<option value="gradient">Gradient</option>
<option value="grid">Grid</option>
<option value="brick">Brick</option>
</select>
</div>
<div class="grid grid-cols-3 gap-2">
<button id="colorRed" class="bg-red-600 hover:bg-red-700 h-6 rounded"></button>
<button id="colorGreen" class="bg-green-600 hover:bg-green-700 h-6 rounded"></button>
<button id="colorBlue" class="bg-blue-600 hover:bg-blue-700 h-6 rounded"></button>
<button id="colorYellow" class="bg-yellow-600 hover:bg-yellow-700 h-6 rounded"></button>
<button id="colorPurple" class="bg-purple-600 hover:bg-purple-700 h-6 rounded"></button>
<button id="colorGray" class="bg-gray-600 hover:bg-gray-700 h-6 rounded"></button>
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Texture Scale</label>
<input id="textureScale" type="range" min="0.1" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer">
</div>
</div>
</div>
<!-- Post Processing -->
<div class="p-4">
<h3 class="text-sm font-semibold text-gray-400 uppercase mb-3">Effects</h3>
<div class="space-y-2">
<div class="flex items-center justify-between">
<label class="text-xs">Bloom</label>
<button id="toggleBloom" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs">
Enable
</button>
</div>
<div class="flex items-center justify-between">
<label class="text-xs">Depth of Field</label>
<button id="toggleDOF" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs">
Disable
</button>
</div>
<div class="flex items-center justify-between">
<label class="text-xs">Grain</label>
<button id="toggleGrain" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs">
Enable
</button>
</div>
<div class="flex items-center justify-between">
<label class="text-xs">SSAO</label>
<button id="toggleSSAO" class="bg-gray-700 hover:bg-gray-600 rounded px-2 py-1 text-xs">
Enable
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 3D Viewport -->
<div class="flex-1 relative">
<canvas id="renderCanvas"></canvas>
<!-- Viewport Controls -->
<div class="absolute bottom-4 right-4 flex space-x-2">
<button id="resetCamera" class="p-2 bg-gray-800 hover:bg-gray-700 rounded-full shadow-lg tooltip">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
<span class="tooltip-text">Reset Camera</span>
</button>
<button id="screenshot" class="p-2 bg-gray-800 hover:bg-gray-700 rounded-full shadow-lg tooltip">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="tooltip-text">Take Screenshot</span>
</button>
<button id="toggleFullscreen" class="p-2 bg-gray-800 hover:bg-gray-700 rounded-full shadow-lg tooltip">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
<span class="tooltip-text">Toggle Fullscreen</span>
</button>
</div>
<!-- Mini Stats -->
<div class="absolute top-4 left-4 bg-black bg-opacity-70 rounded-lg p-2 text-xs">
<div>Objects: <span id="miniObjCount">0</span></div>
<div>FPS: <span id="miniFps">0</span></div>
<div>Draw Calls: <span id="miniDrawCalls">0</span></div>
</div>
<!-- Selection Info -->
<div id="selectionInfo" class="absolute top-4 right-4 bg-black bg-opacity-70 rounded-lg p-3 max-w-xs hidden">
<div class="flex justify-between items-start mb-2">
<h3 class="font-semibold">Selected Object</h3>
<button id="closeSelection" class="text-gray-400 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="text-sm">
<div id="selectedName" class="truncate mb-1">Name:</div>
<div id="selectedType" class="text-gray-400 text-xs mb-2">Type:</div>
<div class="grid grid-cols-2 gap-2 text-xs">
<div>Position: <span id="selectedPosition">0,0,0</span></div>
<div>Rotation: <span id="selectedRotation">0,0,0</span></div>
<div>Scaling: <span id="selectedScaling">1,1,1</span></div>
<div>Material: <span id="selectedMaterial">Standard</span></div>
</div>
</div>
<div class="mt-3 pt-2 border-t border-gray-700">
<button id="deleteSelected" class="w-full bg-red-600 hover:bg-red-700 rounded px-2 py-1 text-xs">
Delete
</button>
</div>
</div>
</div>
</div>
</div>
<!-- WebGPU Warning Modal -->
<div id="webgpuModal" class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 hidden">
<div class="bg-gray-800 rounded-lg p-6 max-w-md mx-4">
<div class="text-xl font-bold mb-4 text-yellow-400 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
WebGPU Not Available
</div>
<p class="mb-4 text-gray-300">
Your browser doesn't support WebGPU or it's not enabled. The application will fall back to WebGL, which may affect performance.
</p>
<div class="flex justify-end space-x-3">
<button id="continueButton" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded">
Continue Anyway
</button>
<button id="learnButton" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded">
Learn More
</button>
</div>
</div>
</div>
<!-- Screenshot Preview -->
<div id="screenshotPreview" class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 hidden">
<div class="bg-gray-900 rounded-lg p-2 shadow-2xl max-w-5xl">
<div class="flex justify-between items-center mb-2">
<h3 class="text-lg font-semibold">Screenshot Preview</h3>
<button id="closePreview" class="text-gray-400 hover:text-white p-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<img id="previewImage" class="max-w-full max-h-[80vh]" src="" alt="Screenshot preview">
<div class="flex justify-between mt-3">
<button id="downloadScreenshot" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded">
Download
</button>
<button id="copyScreenshot" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded">
Copy to Clipboard
</button>
</div>
</div>
</div>
<script>
// Immediately Invoked Function Expression (IIFE) to encapsulate the application
const BabylonApp = (() => {
// Private variables
let engine, scene, canvas;
let hemisphericLight, directionalLight, shadowGenerator;
let advancedTexture, fpsLabel, objectCountLabel;
let bloomEffect, depthOfFieldEffect, grainEffect, ssaoEffect;
let currentSelection = null;
// Configuration
const config = {
defaultScene: {
groundSize: 20,
groundSegments: 64,
skyboxPath: 'https://assets.babylonjs.com/environments/skybox',
skyboxFiles: ['px.png', 'py.png', 'pz.png', 'nx.png', 'ny.png', 'nz.png']
},
materials: {
colors: {
red: new BABYLON.Color3(0.8, 0.2, 0.2),
green: new BABYLON.Color3(0.2, 0.8, 0.2),
blue: new BABYLON.Color3(0.2, 0.2, 0.8),
yellow: new BABYLON.Color3(0.8, 0.8, 0.2),
purple: new BABYLON.Color3(0.6, 0.2, 0.8),
gray: new BABYLON.Color3(0.5, 0.5, 0.5)
}
},
performance: {
updateInterval: 500 // ms
}
};
// State management
const state = {
currentMaterial: 'standard',
currentColor: config.materials.colors.red,
postProcessing: {
bloom: false,
depthOfField: false,
grain: false,
ssao: false
},
lights: {
hemispheric: true,
directional: true,
intensity: 1
},
environment: {
skybox: 'default',
intensity: 1,
fog: false
}
};
// DOM elements
const dom = {
// Loading screen
loadingOverlay: document.getElementById('loadingOverlay'),
progressFill: document.getElementById('progressFill'),
// Panels
sidePanel: document.getElementById('sidePanel'),
togglePanel: document.getElementById('togglePanel'),
// Status bars
fpsCounter: document.getElementById('fpsCounter'),
objectCount: document.getElementById('objectCount'),
sceneInfo: document.getElementById('sceneInfo'),
miniFps: document.getElementById('miniFps'),
miniObjCount: document.getElementById('miniObjCount'),
miniDrawCalls: document.getElementById('miniDrawCalls'),
// Selection info
selectionInfo: document.getElementById('selectionInfo'),
closeSelection: document.getElementById('closeSelection'),
selectedName: document.getElementById('selectedName'),
selectedType: document.getElementById('selectedType'),
selectedPosition: document.getElementById('selectedPosition'),
selectedRotation: document.getElementById('selectedRotation'),
selectedScaling: document.getElementById('selectedScaling'),
selectedMaterial: document.getElementById('selectedMaterial'),
deleteSelected: document.getElementById('deleteSelected'),
// Modals
webgpuModal: document.getElementById('webgpuModal'),
continueButton: document.getElementById('continueButton'),
learnButton: document.getElementById('learnButton'),
screenshotPreview: document.getElementById('screenshotPreview'),
closePreview: document.getElementById('closePreview'),
previewImage: document.getElementById('previewImage'),
downloadScreenshot: document.getElementById('downloadScreenshot'),
copyScreenshot: document.getElementById('copyScreenshot'),
// Scene management
newScene: document.getElementById('newScene'),
saveScene: document.getElementById('saveScene'),
// Environment
skyboxSelect: document.getElementById('skyboxSelect'),
envIntensity: document.getElementById('envIntensity'),
toggleFog: document.getElementById('toggleFog'),
// Lights
toggleHemiLight: document.getElementById('toggleHemiLight'),
toggleDirLight: document.getElementById('toggleDirLight'),
adjustHemiLight: document.getElementById('adjustHemiLight'),
adjustDirLight: document.getElementById('adjustDirLight'),
lightIntensity: document.getElementById('lightIntensity'),
// Objects
addSphere: document.getElementById('addSphere'),
addBox: document.getElementById('addBox'),
addCylinder: document.getElementById('addCylinder'),
addPlane: document.getElementById('addPlane'),
clearObjects: document.getElementById('clearObjects'),
// Materials
materialType: document.getElementById('materialType'),
colorRed: document.getElementById('colorRed'),
colorGreen: document.getElementById('colorGreen'),
colorBlue: document.getElementById('colorBlue'),
colorYellow: document.getElementById('colorYellow'),
colorPurple: document.getElementById('colorPurple'),
colorGray: document.getElementById('colorGray'),
textureScale: document.getElementById('textureScale'),
// Effects
toggleBloom: document.getElementById('toggleBloom'),
toggleDOF: document.getElementById('toggleDOF'),
toggleGrain: document.getElementById('toggleGrain'),
toggleSSAO: document.getElementById('toggleSSAO'),
// Viewport controls
resetCamera: document.getElementById('resetCamera'),
screenshot: document.getElementById('screenshot'),
toggleFullscreen: document.getElementById('toggleFullscreen')
};
// ==============================================
// Utility Functions
// ==============================================
// Create a loading progress updater using currying
const createProgressUpdater = (totalSteps) => {
let currentStep = 0;
return (increment = 1) => {
currentStep += increment;
const percentage = (currentStep / totalSteps) * 100;
dom.progressFill.style.width = `${percentage}%`;
// Hide loading screen when complete
if (currentStep >= totalSteps) {
setTimeout(() => {
dom.loadingOverlay.style.opacity = '0';
setTimeout(() => {
dom.loadingOverlay.style.display = 'none';
}, 500);
}, 300);
}
};
};
// Get a random position in a donut shape around origin
const getRandomPosition = (minRadius = 3, maxRadius = 8, yRange = [0.5, 3]) => {
const angle = Math.random() * Math.PI * 2;
const radius = minRadius + Math.random() * (maxRadius - minRadius);
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const y = yRange[0] + Math.random() * (yRange[1] - yRange[0]);
return new BABYLON.Vector3(x, y, z);
};
// Create a gradient material using higher-order functions
const createMaterialFactory = (scene) => (type) => (options = {}) => {
let material;
switch(type) {
case 'standard':
material = new BABYLON.StandardMaterial(`mat_${type}_${Date.now()}`, scene);
if (options.diffuseColor) material.diffuseColor = options.diffuseColor;
if (options.specularColor) material.specularColor = options.specularColor;
if (options.emissiveColor) material.emissiveColor = options.emissiveColor;
material.alpha = options.alpha || 1;
break;
case 'pbr':
material = new BABYLON.PBRMaterial(`mat_${type}_${Date.now()}`, scene);
if (options.albedoColor) material.albedoColor = options.albedoColor;
if (options.metallic !== undefined) material.metallic = options.metallic;
if (options.roughness !== undefined) material.roughness = options.roughness;
material.alpha = options.alpha || 1;
break;
case 'gradient':
material = new BABYLON.GradientMaterial(`mat_${type}_${Date.now()}`, scene);
if (options.topColor) material.topColor = options.topColor;
if (options.bottomColor) material.bottomColor = options.bottomColor;
if (options.offset !== undefined) material.offset = options.offset;
if (options.scale !== undefined) material.scale = options.scale;
break;
case 'grid':
material = new BABYLON.GridMaterial(`mat_${type}_${Date.now()}`, scene);
if (options.mainColor) material.mainColor = options.mainColor;
if (options.lineColor) material.lineColor = options.lineColor;
if (options.gridRatio !== undefined) material.gridRatio = options.gridRatio;
if (options.scale !== undefined) material.scale = options.scale;
break;
case 'brick':
const brickTexture = new BABYLON.BrickProceduralTexture('brickTex', 512, scene);
if (options.brickColor) brickTexture.brickColor = options.brickColor;
if (options.jointColor) brickTexture.jointColor = options.jointColor;
if (options.numberOfBricksHeight !== undefined) brickTexture.numberOfBricksHeight = options.numberOfBricksHeight;
material = new BABYLON.StandardMaterial(`mat_${type}_${Date.now()}`, scene);
material.diffuseTexture = brickTexture;
break;
}
return material;
};
// Format position/rotation/scaling for display
const formatVector = (vector) => {
const x = vector.x.toFixed(2);
const y = vector.y.toFixed(2);
const z = vector.z.toFixed(2);
return `${x}, ${y}, ${z}`;
};
// ==============================================
// Scene Initialization
// ==============================================
// Initialize the Babylon.js engine and scene
const initScene = async () => {
const updateProgress = createProgressUpdater(10);
try {
// Check for WebGPU support
const webGPUSupported = await BABYLON.WebGPUEngine.IsSupportedAsync;
if (!webGPUSupported) {
dom.webgpuModal.classList.remove('hidden');
updateProgress(1);
}
// Get the canvas
canvas = document.getElementById('renderCanvas');
// Initialize the engine
engine = new BABYLON.Engine(
canvas,
true,
{
preserveDrawingBuffer: true,
stencil: true,
antialias: true,
adaptToDeviceRatio: true
}
);
updateProgress(1);
// Create the scene
scene = new BABYLON.Scene(engine);
scene.autoClear = false;
scene.autoClearDepthAndStencil = false;
updateProgress(1);
// Create camera
const camera = new BABYLON.ArcRotateCamera(
"camera",
-Math.PI / 2,
Math.PI / 2,
15,
BABYLON.Vector3.Zero(),
scene
);
camera.attachControl(canvas, true);
camera.lowerRadiusLimit = 5;
camera.upperRadiusLimit = 30;
camera.wheelDeltaPercentage = 0.01;
camera.panningSensibility = 50;
camera.allowUpsideDown = false;
updateProgress(1);
// Create default lights
setupLights();
updateProgress(1);
// Create ground
createGround();
updateProgress(1);
// Setup shadow generator
setupShadows();
updateProgress(1);
// Setup skybox
setupSkybox('default');
updateProgress(1);
// Setup post-processing
setupPostProcessing();
updateProgress(1);
// Setup GUI
setupGUI();
updateProgress(1);
// Setup event listeners
setupEventListeners();
updateProgress(1);
// Setup picking/selection
setupPicking();
updateProgress(1);
// Run the render loop
engine.runRenderLoop(() => {
scene.render();
updateStats();
});
// Handle resize
window.addEventListener('resize', () => {
engine.resize();
});
} catch (error) {
console.error("Error initializing scene:", error);
}
};
// ==============================================
// Scene Setup Functions
// ==============================================
// Setup lights in the scene
const setupLights = () => {
// Hemispheric light (ambient)
hemisphericLight = new BABYLON.HemisphericLight(
"hemiLight",
new BABYLON.Vector3(0, 1, 0),
scene
);
hemisphericLight.intensity = state.lights.intensity;
hemisphericLight.diffuse = new BABYLON.Color3(1, 1, 1);
hemisphericLight.groundColor = new BABYLON.Color3(0.5, 0.5, 0.5);
// Directional light (sun-like)
directionalLight = new BABYLON.DirectionalLight(
"dirLight",
new BABYLON.Vector3(-1, -2, -1),
scene
);
directionalLight.position = new BABYLON.Vector3(0, 10, 0);
directionalLight.intensity = state.lights.intensity * 0.8;
directionalLight.shadowMinZ = 1;
directionalLight.shadowMaxZ = 20;
};
// Setup ground in the scene
const createGround = () => {
const ground = BABYLON.MeshBuilder.CreateGround(
"ground",
{
width: config.defaultScene.groundSize,
height: config.defaultScene.groundSize,
subdivisions: config.defaultScene.groundSegments
},
scene
);
const material = createMaterialFactory(scene)('pbr')({
albedoColor: new BABYLON.Color3(0.2, 0.2, 0.25),
metallic: 0.1,
roughness: 0.8
});
ground.material = material;
ground.receiveShadows = true;
shadowGenerator.addShadowCaster(ground);
};
// Setup shadows in the scene
const setupShadows = () => {
shadowGenerator = new BABYLON.ShadowGenerator(2048, directionalLight);
shadowGenerator.useBlurExponentialShadowMap = true;
shadowGenerator.blurKernel = 32;
shadowGenerator.bias = 0.0001;
shadowGenerator.normalBias = 0.02;
shadowGenerator.forceBackFacesOnly = false;
};
// Setup skybox
const setupSkybox = (type) => {
// Remove existing skybox if any
if (scene.skybox) {
scene.skybox.dispose();
}
if (type === 'none') return;
let skyboxPath = config.defaultScene.skyboxPath;
if (type !== 'default') {
skyboxPath = `https://assets.babylonjs.com/environments/${type}env`;
}
scene.environmentTexture = BABYLON.CubeTexture.CreateFromPrefilteredData(
skyboxPath + ".env",
scene
);
if (type !== 'default') {
const skybox = scene.createDefaultSkybox(scene.environmentTexture, true, 1000);
skybox.ignorePicking = true;
}
};
// Setup post-processing effects
const setupPostProcessing = () => {
// Create default pipeline
const defaultPipeline = new BABYLON.DefaultRenderingPipeline(
"defaultPipeline",
true,
scene,
[scene.activeCamera]
);
// Bloom effect
bloomEffect = new BABYLON.BloomEffect(
"bloom",
scene,
1.0, // Bloom scale
0.8, // Bloom threshold
4.0, // Bloom weight
200 // Blur kernel size
);
bloomEffect.enabled = state.postProcessing.bloom;
defaultPipeline.addEffect(bloomEffect);
// Depth of Field
depthOfFieldEffect = new BABYLON.DepthOfFieldEffect(
scene,
{ target: scene.activeCamera, focusDistance: 0.1, fStop: 1.4 }
);
depthOfFieldEffect.enabled = state.postProcessing.depthOfField;
defaultPipeline.addEffect(depthOfFieldEffect);
// Grain effect
grainEffect = new BABYLON.GrainEffect("grain", scene);
grainEffect.enabled = state.postProcessing.grain;
grainEffect.intensity = 0.3;
defaultPipeline.addEffect(grainEffect);
// SSAO effect
ssaoEffect = new BABYLON.SSAORenderingPipeline(
"ssao",
scene,
{ ssaoRatio: 0.5, combineRatio: 0.5 }
);
ssaoEffect.enabled = state.postProcessing.ssao;
};
// Setup GUI
const setupGUI = () => {
// Advanced Texture for GUI
advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI");
// FPS Label
fpsLabel = new BABYLON.GUI.TextBlock();
fpsLabel.text = "FPS: 0";
fpsLabel.color = "white";
fpsLabel.fontSize = 14;
fpsLabel.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
fpsLabel.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
fpsLabel.left = "20px";
fpsLabel.top = "20px";
fpsLabel.fontFamily = "Arial";
advancedTexture.addControl(fpsLabel);
// Object Count Label
objectCountLabel = new BABYLON.GUI.TextBlock();
objectCountLabel.text = "Objects: 0";
objectCountLabel.color = "white";
objectCountLabel.fontSize = 14;
objectCountLabel.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
objectCountLabel.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
objectCountLabel.left = "20px";
objectCountLabel.top = "40px";
objectCountLabel.fontFamily = "Arial";
advancedTexture.addControl(objectCountLabel);
};
// Setup object picking/selection
const setupPicking = () => {
scene.onPointerDown = (evt, pickResult) => {
if (!pickResult.hit || !pickResult.pickedMesh) {
hideSelectionInfo();
currentSelection = null;
return;
}
const mesh = pickResult.pickedMesh;
// Don't select ground, skybox, etc.
if (!mesh.name || mesh.name === 'ground' || mesh.name.startsWith('skybox') || mesh.name.startsWith('_default_')) {
hideSelectionInfo();
currentSelection = null;
return;
}
showSelectionInfo(mesh);
currentSelection = mesh;
};
};
// ==============================================
// Scene Management Functions
// ==============================================
// Create a new primitive object in the scene
const createPrimitive = (type, position = null) => {
if (!scene) return;
const meshPosition = position || getRandomPosition();
let mesh;
switch(type) {
case 'sphere':
mesh = BABYLON.MeshBuilder.CreateSphere(
`sphere_${Date.now()}`,
{ diameter: 1 + Math.random() * 2 },
scene
);
break;
case 'box':
mesh = BABYLON.MeshBuilder.CreateBox(
`box_${Date.now()}`,
{
size: 1 + Math.random() * 2,
width: 1 + Math.random() * 2,
height: 1 + Math.random() * 2,
depth: 1 + Math.random() * 2
},
scene
);
break;
case 'cylinder':
mesh = BABYLON.MeshBuilder.CreateCylinder(
`cylinder_${Date.now()}`,
{
diameterTop: 1 + Math.random(),
diameterBottom: 1 + Math.random(),
height: 1 + Math.random() * 2,
tessellation: 32
},
scene
);
break;
case 'plane':
mesh = BABYLON.MeshBuilder.CreatePlane(
`plane_${Date.now()}`,
{
size: 1 + Math.random() * 3,
width: 1 + Math.random() * 3,
height: 1 + Math.random() * 3
},
scene
);
break;
default:
return;
}
// Position the mesh
mesh.position = meshPosition;
// Random rotation
mesh.rotation = new BABYLON.Vector3(
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2
);
// Random scaling
const scale = 0.5 + Math.random();
mesh.scaling = new BABYLON.Vector3(scale, scale, scale);
// Apply material
applyCurrentMaterialToMesh(mesh);
// Add to shadow generator
shadowGenerator.addShadowCaster(mesh);
return mesh;
};
// Apply the current selected material to a mesh
const applyCurrentMaterialToMesh = (mesh) => {
if (!mesh) return;
const materialFactory = createMaterialFactory(scene);
let material;
switch(state.currentMaterial) {
case 'standard':
material = materialFactory('standard')({
diffuseColor: state.currentColor,
specularColor: new BABYLON.Color3(0.5, 0.5, 0.5),
emissiveColor: new BABYLON.Color3(0.1, 0.1, 0.1)
});
break;
case 'pbr':
material = materialFactory('pbr')({
albedoColor: state.currentColor,
metallic: Math.random(),
roughness: Math.random()
});
break;
case 'gradient':
material = materialFactory('gradient')({
topColor: state.currentColor,
bottomColor: state.currentColor.scale(0.5),
offset: Math.random() * 2,
scale: parseFloat(dom.textureScale.value)
});
break;
case 'grid':
material = materialFactory('grid')({
mainColor: state.currentColor,
lineColor: state.currentColor.scale(0.5),
gridRatio: 0.2 + Math.random() * 0.3,
scale: parseFloat(dom.textureScale.value)
});
break;
case 'brick':
const brickOptions = {
brickColor: state.currentColor,
jointColor: state.currentColor.scale(0.3),
numberOfBricksHeight: 5 + Math.floor(Math.random() * 10)
};
material = materialFactory('brick')(brickOptions);
break;
}
mesh.material = material;
};
// Clear all objects from the scene (except ground and lights)
const clearAllObjects = () => {
if (!scene) return;
scene.meshes.forEach(mesh => {
// Don't remove ground, skybox, or camera/lights
if (mesh.name &&
!mesh.name.startsWith('ground') &&
!mesh.name.startsWith('skybox') &&
!mesh.name.startsWith('camera') &&
!mesh.name.startsWith('hemiLight') &&
!mesh.name.startsWith('dirLight')) {
mesh.dispose();
}
});
hideSelectionInfo();
currentSelection = null;
};
// Reset the camera to default position
const resetCamera = () => {
if (!scene || !scene.activeCamera) return;
const camera = scene.activeCamera;
camera.alpha = -Math.PI / 2;
camera.beta = Math.PI / 2;
camera.radius = 15;
camera.target = BABYLON.Vector3.Zero();
};
// Take a screenshot of the current scene
const takeScreenshot = () => {
if (!engine) return;
// Create a temporary canvas
const tempCanvas = document.createElement('canvas');
const tempContext = tempCanvas.getContext('2d');
// Set canvas size
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
// Copy the contents
tempContext.drawImage(canvas, 0, 0);
// Show the preview
dom.previewImage.src = tempCanvas.toDataURL('image/png');
dom.screenshotPreview.classList.remove('hidden');
};
// Toggle fullscreen mode
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
canvas.requestFullscreen().catch(err => {
console.error(`Error attempting to enable fullscreen: ${err.message}`);
});
} else {
document.exitFullscreen();
}
};
// ==============================================
// UI Functions
// ==============================================
// Show the selection info panel
const showSelectionInfo = (mesh) => {
dom.selectedName.textContent = mesh.name;
dom.selectedType.textContent = mesh.getClassName();
dom.selectedPosition.textContent = formatVector(mesh.position);
dom.selectedRotation.textContent = formatVector(mesh.rotation);
dom.selectedScaling.textContent = formatVector(mesh.scaling);
dom.selectedMaterial.textContent = mesh.material ? mesh.material.getClassName() : 'None';
dom.selectionInfo.classList.remove('hidden');
};
// Hide the selection info panel
const hideSelectionInfo = () => {
dom.selectionInfo.classList.add('hidden');
};
// Delete the currently selected object
const deleteSelectedObject = () => {
if (!currentSelection) return;
currentSelection.dispose();
hideSelectionInfo();
currentSelection = null;
};
// Update performance stats
const updateStats = () => {
// Throttle the updates
if (!fpsLabel || !objectCountLabel || !scene) return;
// Update FPS
const fps = Math.round(engine.getFps());
fpsLabel.text = `FPS: ${fps}`;
dom.miniFps.textContent = fps;
// Update object count
const meshes = scene.meshes.filter(m => !m.name.startsWith('_default_')).length;
objectCountLabel.text = `Objects: ${meshes}`;
dom.miniObjCount.textContent = meshes;
// Update draw calls if supported
if (engine.getCurrentRenderCalls) {
dom.miniDrawCalls.textContent = engine.getCurrentRenderCalls();
}
};
// ==============================================
// Event Listeners
// ==============================================
// Setup event listeners for UI controls
const setupEventListeners = () => {
// Panel toggle
dom.togglePanel.addEventListener('click', () => {
dom.sidePanel.classList.toggle('-ml-64');
});
// Scene management
dom.newScene.addEventListener('click', () => {
clearAllObjects();
resetCamera();
});
dom.saveScene.addEventListener('click', () => {
alert('Scene saved (this would actually save the scene in a real application)');
});
// Environment
dom.skyboxSelect.addEventListener('change', (e) => {
setupSkybox(e.target.value);
});
dom.envIntensity.addEventListener('input', (e) => {
scene.environmentIntensity = parseFloat(e.target.value);
});
dom.toggleFog.addEventListener('click', () => {
state.environment.fog = !state.environment.fog;
if (state.environment.fog) {
scene.fogMode = BABYLON.Scene.FOGMODE_EXP;
scene.fogDensity = 0.02;
scene.fogColor = new BABYLON.Color3(0.1, 0.1, 0.15);
dom.toggleFog.textContent = 'Disable';
} else {
scene.fogMode = BABYLON.Scene.FOGMODE_NONE;
dom.toggleFog.textContent = 'Enable';
}
});
// Lights
dom.toggleHemiLight.addEventListener('click', () => {
state.lights.hemispheric = !state.lights.hemispheric;
hemisphericLight.intensity = state.lights.hemispheric ? state.lights.intensity : 0;
dom.toggleHemiLight.textContent = state.lights.hemispheric ? 'Disable' : 'Enable';
});
dom.toggleDirLight.addEventListener('click', () => {
state.lights.directional = !state.lights.directional;
directionalLight.intensity = state.lights.directional ? state.lights.intensity * 0.8 : 0;
dom.toggleDirLight.textContent = state.lights.directional ? 'Disable' : 'Enable';
});
dom.adjustHemiLight.addEventListener('click', () => {
hemisphericLight.diffuse = new BABYLON.Color3(
Math.random(),
Math.random(),
Math.random()
);
hemisphericLight.groundColor = new BABYLON.Color3(
Math.random(),
Math.random(),
Math.random()
);
});
dom.adjustDirLight.addEventListener('click', () => {
directionalLight.direction = new BABYLON.Vector3(
Math.random() * 2 - 1,
Math.random() * -1 - 0.5,
Math.random() * 2 - 1
).normalize();
});
dom.lightIntensity.addEventListener('input', (e) => {
state.lights.intensity = parseFloat(e.target.value);
hemisphericLight.intensity = state.lights.intensity;
directionalLight.intensity = state.lights.intensity * 0.8;
});
// Objects
dom.addSphere.addEventListener('click', () => createPrimitive('sphere'));
dom.addBox.addEventListener('click', () => createPrimitive('box'));
dom.addCylinder.addEventListener('click', () => createPrimitive('cylinder'));
dom.addPlane.addEventListener('click', () => createPrimitive('plane'));
dom.clearObjects.addEventListener('click', clearAllObjects);
// Materials
dom.materialType.addEventListener('change', (e) => {
state.currentMaterial = e.target.value;
if (currentSelection) {
applyCurrentMaterialToMesh(currentSelection);
dom.selectedMaterial.textContent = currentSelection.material ?
currentSelection.material.getClassName() : 'None';
}
});
const colorButtons = ['red', 'green', 'blue', 'yellow', 'purple', 'gray'];
colorButtons.forEach(color => {
document.getElementById(`color${color.charAt(0).toUpperCase() + color.slice(1)}`)
.addEventListener('click', () => {
state.currentColor = config.materials.colors[color];
if (currentSelection) {
applyCurrentMaterialToMesh(currentSelection);
}
});
});
dom.textureScale.addEventListener('input', (e) => {
if (currentSelection && currentSelection.material) {
const scale = parseFloat(e.target.value);
if (currentSelection.material instanceof BABYLON.GradientMaterial) {
currentSelection.material.scale = scale;
} else if (currentSelection.material instanceof BABYLON.GridMaterial) {
currentSelection.material.scale = scale;
}
}
});
// Effects
dom.toggleBloom.addEventListener('click', () => {
state.postProcessing.bloom = !state.postProcessing.bloom;
bloomEffect.enabled = state.postProcessing.bloom;
dom.toggleBloom.textContent = state.postProcessing.bloom ? 'Disable' : 'Enable';
});
dom.toggleDOF.addEventListener('click', () => {
state.postProcessing.depthOfField = !state.postProcessing.depthOfField;
depthOfFieldEffect.enabled = state.postProcessing.depthOfField;
dom.toggleDOF.textContent = state.postProcessing.depthOfField ? 'Disable' : 'Enable';
});
dom.toggleGrain.addEventListener('click', () => {
state.postProcessing.grain = !state.postProcessing.grain;
grainEffect.enabled = state.postProcessing.grain;
dom.toggleGrain.textContent = state.postProcessing.grain ? 'Disable' : 'Enable';
});
dom.toggleSSAO.addEventListener('click', () => {
state.postProcessing.ssao = !state.postProcessing.ssao;
ssaoEffect.enabled = state.postProcessing.ssao;
dom.toggleSSAO.textContent = state.postProcessing.ssao ? 'Disable' : 'Enable';
});
// Viewport controls
dom.resetCamera.addEventListener('click', resetCamera);
dom.screenshot.addEventListener('click', takeScreenshot);
dom.toggleFullscreen.addEventListener('click', toggleFullscreen);
// Selection info
dom.closeSelection.addEventListener('click', hideSelectionInfo);
dom.deleteSelected.addEventListener('click', deleteSelectedObject);
// Modal controls
dom.continueButton.addEventListener('click', () => {
dom.webgpuModal.classList.add('hidden');
});
dom.learnButton.addEventListener('click', () => {
window.open('https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API', '_blank');
});
dom.closePreview.addEventListener('click', () => {
dom.screenshotPreview.classList.add('hidden');
});
dom.downloadScreenshot.addEventListener('click', () => {
const link = document.createElement('a');
link.download = `screenshot-${new Date().toISOString()}.png`;
link.href = dom.previewImage.src;
link.click();
});
dom.copyScreenshot.addEventListener('click', () => {
navigator.clipboard.writeText(dom.previewImage.src).then(() => {
alert('Screenshot copied to clipboard!');
}).catch(err => {
console.error('Could not copy screenshot: ', err);
alert('Failed to copy screenshot to clipboard');
});
});
};
// ==============================================
// Public API
// ==============================================
return {
init: () => initScene(),
getScene: () => scene,
getEngine: () => engine
};
})();
// Initialize the application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
BabylonApp.init();
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body>
</html>