feat: add iframe-based World Builder with external URL support
- Add URL input bar to load external world builders (WorldLabs.ai, Three.js, Babylon.js) - Add preset quick access links for popular 3D world building tools - Add mode toggle between iframe and local canvas modes - Add loading states and error handling for iframe content - Keep local procedural world builder as alternative mode https://claude.ai/code/session_01MUG7BRZMG28eYmJHGbetRB
This commit is contained in:
parent
4806d73ba5
commit
8e073cd0cc
300
ui/index.html
300
ui/index.html
@ -734,6 +734,132 @@
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* World URL Bar */
|
||||||
|
.world-url-bar {
|
||||||
|
background: linear-gradient(180deg, #f5f5f5 0%, #e8e8e8 100%);
|
||||||
|
border-bottom: 1px solid #aaa;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-bar-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-presets {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
padding-top: 5px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-presets a {
|
||||||
|
color: #0066cc;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-presets a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Iframe Container */
|
||||||
|
.world-iframe-container {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 70px);
|
||||||
|
position: relative;
|
||||||
|
background: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.world-iframe-container iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe-loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
color: #e2e8f0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe-loading.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 4px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-top-color: #00ff88;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-hint {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Iframe Error State */
|
||||||
|
.iframe-error {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
color: #ff6b6b;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
padding: 20px 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe-error h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe-error p {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-row {
|
.stat-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -864,14 +990,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- World Builder Window -->
|
<!-- World Builder Window -->
|
||||||
<div class="xp-window" id="world-window">
|
<div class="xp-window" id="world-window" style="width: 800px; height: 600px;">
|
||||||
<div class="window-titlebar" onmousedown="startDrag(event, 'world-window')">
|
<div class="window-titlebar" onmousedown="startDrag(event, 'world-window')">
|
||||||
<div class="window-title">
|
<div class="window-title">
|
||||||
<svg width="16" height="16" viewBox="0 0 32 32">
|
<svg width="16" height="16" viewBox="0 0 32 32">
|
||||||
<circle cx="16" cy="16" r="12" fill="#22c55e"/>
|
<circle cx="16" cy="16" r="12" fill="#22c55e"/>
|
||||||
<ellipse cx="16" cy="16" rx="12" ry="5" fill="none" stroke="#fff" stroke-width="1"/>
|
<ellipse cx="16" cy="16" rx="12" ry="5" fill="none" stroke="#fff" stroke-width="1"/>
|
||||||
</svg>
|
</svg>
|
||||||
World Builder - Inspired by WorldLabs.ai
|
World Builder - External AI Worlds
|
||||||
</div>
|
</div>
|
||||||
<div class="window-controls">
|
<div class="window-controls">
|
||||||
<button class="window-btn btn-minimize" onclick="minimizeWindow('world-window')">_</button>
|
<button class="window-btn btn-minimize" onclick="minimizeWindow('world-window')">_</button>
|
||||||
@ -883,11 +1009,47 @@
|
|||||||
<span>File</span>
|
<span>File</span>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
<span>World</span>
|
<span>World</span>
|
||||||
<span>Generate</span>
|
<span onclick="toggleWorldMode()">Switch Mode</span>
|
||||||
<span>Help</span>
|
<span>Help</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-content" style="padding: 0; margin: 0; border: none;">
|
<div class="window-content" style="padding: 0; margin: 0; border: none; height: calc(100% - 80px);">
|
||||||
<div class="world-container">
|
<!-- URL Input Bar -->
|
||||||
|
<div class="world-url-bar" id="world-url-bar">
|
||||||
|
<div class="url-bar-inner">
|
||||||
|
<span class="url-label">World URL:</span>
|
||||||
|
<input type="text" class="xp-input url-input" id="world-url-input"
|
||||||
|
placeholder="Enter WorldLabs.ai or any world builder URL..."
|
||||||
|
value="https://www.worldlabs.ai/discover">
|
||||||
|
<button class="xp-btn primary" onclick="loadWorldUrl()">Go</button>
|
||||||
|
<button class="xp-btn" onclick="loadPresetWorld('worldlabs')">WorldLabs</button>
|
||||||
|
<button class="xp-btn" onclick="loadPresetWorld('threejs')">Three.js</button>
|
||||||
|
<button class="xp-btn" onclick="loadPresetWorld('babylonjs')">Babylon</button>
|
||||||
|
<button class="xp-btn" onclick="toggleWorldMode()">Local Mode</button>
|
||||||
|
</div>
|
||||||
|
<div class="url-presets">
|
||||||
|
<span class="preset-label">Quick Access:</span>
|
||||||
|
<a href="#" onclick="setWorldUrl('https://www.worldlabs.ai/discover'); return false;">WorldLabs Discover</a>
|
||||||
|
<a href="#" onclick="setWorldUrl('https://www.worldlabs.ai/create'); return false;">WorldLabs Create</a>
|
||||||
|
<a href="#" onclick="setWorldUrl('https://threejs.org/examples/#webgl_animation_keyframes'); return false;">Three.js Demo</a>
|
||||||
|
<a href="#" onclick="setWorldUrl('https://playground.babylonjs.com/'); return false;">Babylon Playground</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Iframe World Container -->
|
||||||
|
<div class="world-iframe-container" id="world-iframe-container">
|
||||||
|
<div class="iframe-loading" id="iframe-loading">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>Loading World Builder...</p>
|
||||||
|
<p class="loading-hint">Enter a URL above or click a preset to start building worlds</p>
|
||||||
|
</div>
|
||||||
|
<iframe id="world-iframe"
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
|
||||||
|
allow="accelerometer; camera; encrypted-media; fullscreen; gyroscope; microphone; web-share"
|
||||||
|
loading="lazy"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Local Canvas Mode (hidden by default) -->
|
||||||
|
<div class="world-container" id="world-local-container" style="display: none;">
|
||||||
<div class="world-sidebar">
|
<div class="world-sidebar">
|
||||||
<h3>World Elements</h3>
|
<h3>World Elements</h3>
|
||||||
<div class="world-element" draggable="true" data-type="star">* Star</div>
|
<div class="world-element" draggable="true" data-type="star">* Star</div>
|
||||||
@ -899,6 +1061,7 @@
|
|||||||
<h3 style="margin-top: 15px;">AI Generation</h3>
|
<h3 style="margin-top: 15px;">AI Generation</h3>
|
||||||
<button class="xp-btn" style="width: 100%; margin-bottom: 5px;" onclick="aiGenerateWorld()">Auto-Generate</button>
|
<button class="xp-btn" style="width: 100%; margin-bottom: 5px;" onclick="aiGenerateWorld()">Auto-Generate</button>
|
||||||
<button class="xp-btn" style="width: 100%;" onclick="evolveWorld()">Evolve World</button>
|
<button class="xp-btn" style="width: 100%;" onclick="evolveWorld()">Evolve World</button>
|
||||||
|
<button class="xp-btn" style="width: 100%; margin-top: 10px;" onclick="toggleWorldMode()">Iframe Mode</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="world-canvas-container">
|
<div class="world-canvas-container">
|
||||||
<div class="world-toolbar">
|
<div class="world-toolbar">
|
||||||
@ -922,7 +1085,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-statusbar">Mode: Creation | Objects: <span id="world-objects">0</span></div>
|
<div class="window-statusbar">Mode: <span id="world-mode-status">Iframe</span> | <span id="world-url-status">Ready to load external world</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- About Window -->
|
<!-- About Window -->
|
||||||
@ -1559,6 +1722,131 @@
|
|||||||
addWorldObject('star', x, y);
|
addWorldObject('star', x, y);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== IFRAME WORLD BUILDER =====
|
||||||
|
let worldMode = 'iframe'; // 'iframe' or 'local'
|
||||||
|
let currentWorldUrl = '';
|
||||||
|
|
||||||
|
const worldPresets = {
|
||||||
|
worldlabs: 'https://www.worldlabs.ai/discover',
|
||||||
|
worldlabs_create: 'https://www.worldlabs.ai/create',
|
||||||
|
threejs: 'https://threejs.org/examples/#webgl_animation_keyframes',
|
||||||
|
babylonjs: 'https://playground.babylonjs.com/',
|
||||||
|
aframe: 'https://aframe.io/examples/showcase/helloworld/',
|
||||||
|
sketchfab: 'https://sketchfab.com/3d-models?features=staffpicked&sort_by=-likeCount'
|
||||||
|
};
|
||||||
|
|
||||||
|
function setWorldUrl(url) {
|
||||||
|
document.getElementById('world-url-input').value = url;
|
||||||
|
loadWorldUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadWorldUrl() {
|
||||||
|
const input = document.getElementById('world-url-input');
|
||||||
|
const url = input.value.trim();
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
showNotification('World Builder', 'Please enter a URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate URL
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
} catch {
|
||||||
|
showNotification('World Builder', 'Invalid URL format');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWorldUrl = url;
|
||||||
|
const iframe = document.getElementById('world-iframe');
|
||||||
|
const loading = document.getElementById('iframe-loading');
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
loading.classList.remove('hidden');
|
||||||
|
loading.innerHTML = `
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>Loading: ${url}</p>
|
||||||
|
<p class="loading-hint">Connecting to external world builder...</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Set iframe src
|
||||||
|
iframe.src = url;
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
document.getElementById('world-url-status').textContent = `Loading: ${new URL(url).hostname}`;
|
||||||
|
|
||||||
|
// Handle load event
|
||||||
|
iframe.onload = () => {
|
||||||
|
loading.classList.add('hidden');
|
||||||
|
document.getElementById('world-url-status').textContent = `Connected: ${new URL(url).hostname}`;
|
||||||
|
showNotification('World Builder', `Loaded: ${new URL(url).hostname}`);
|
||||||
|
|
||||||
|
// Log to soul document
|
||||||
|
addSoulEntry('memory', `Explored a new world at ${new URL(url).hostname}. The universe expands...`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
iframe.onerror = () => {
|
||||||
|
loading.innerHTML = `
|
||||||
|
<div class="iframe-error">
|
||||||
|
<h3>Unable to Load World</h3>
|
||||||
|
<p>The site may block iframe embedding.</p>
|
||||||
|
<p>Try opening in a new tab: <a href="${url}" target="_blank" style="color: #00ff88;">${url}</a></p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('world-url-status').textContent = 'Error loading world';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPresetWorld(preset) {
|
||||||
|
const url = worldPresets[preset];
|
||||||
|
if (url) {
|
||||||
|
setWorldUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWorldMode() {
|
||||||
|
const iframeContainer = document.getElementById('world-iframe-container');
|
||||||
|
const urlBar = document.getElementById('world-url-bar');
|
||||||
|
const localContainer = document.getElementById('world-local-container');
|
||||||
|
const modeStatus = document.getElementById('world-mode-status');
|
||||||
|
|
||||||
|
if (worldMode === 'iframe') {
|
||||||
|
// Switch to local mode
|
||||||
|
worldMode = 'local';
|
||||||
|
iframeContainer.style.display = 'none';
|
||||||
|
urlBar.style.display = 'none';
|
||||||
|
localContainer.style.display = 'flex';
|
||||||
|
modeStatus.textContent = 'Local Canvas';
|
||||||
|
document.getElementById('world-url-status').textContent = 'Building locally';
|
||||||
|
initWorldCanvas();
|
||||||
|
showNotification('World Builder', 'Switched to Local Mode');
|
||||||
|
} else {
|
||||||
|
// Switch to iframe mode
|
||||||
|
worldMode = 'iframe';
|
||||||
|
iframeContainer.style.display = 'block';
|
||||||
|
urlBar.style.display = 'block';
|
||||||
|
localContainer.style.display = 'none';
|
||||||
|
modeStatus.textContent = 'Iframe';
|
||||||
|
document.getElementById('world-url-status').textContent = currentWorldUrl
|
||||||
|
? `Connected: ${new URL(currentWorldUrl).hostname}`
|
||||||
|
: 'Ready to load external world';
|
||||||
|
showNotification('World Builder', 'Switched to Iframe Mode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Enter key in URL input
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const urlInput = document.getElementById('world-url-input');
|
||||||
|
if (urlInput) {
|
||||||
|
urlInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
loadWorldUrl();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ===== INITIALIZATION =====
|
// ===== INITIALIZATION =====
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
updateTaskbar();
|
updateTaskbar();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user