1
0
Fork 0
godot/misc/dist/html/editor.html

835 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<meta name="author" content="Godot Engine">
<meta name="description" content="Use the Godot Engine editor directly in your web browser, without having to install anything.">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="application-name" content="Godot">
<meta name="apple-mobile-web-app-title" content="Godot">
<meta name="theme-color" content="#202531">
<meta name="msapplication-navbutton-color" content="#202531">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="msapplication-starturl" content="/latest">
<meta property="og:site_name" content="Godot Engine Web Editor">
<meta property="og:url" name="twitter:url" content="https://editor.godotengine.org/releases/stable/">
<meta property="og:title" name="twitter:title" content="Free and open source 2D and 3D game engine">
<meta property="og:description" name="twitter:description" content="Use the Godot Engine editor directly in your web browser, without having to install anything.">
<meta property="og:image" name="twitter:image" content="https://godotengine.org/assets/share-image.webp">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary">
<link id="-gd-engine-icon" rel="icon" type="image/png" href="favicon.png">
<link rel="apple-touch-icon" type="image/png" href="favicon.png">
<link rel="manifest" href="manifest.json">
<link rel="preload" href="inter-regular.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="inter-bold.woff2" as="font" crossorigin="anonymous">
<title>Godot Engine Web Editor (___GODOT_VERSION___)</title>
<style>
/* Use Inter in the same configuration as used in the editor (e.g. alternate lowercase `l`). */
@font-face {
font-family: "Inter";
src: url("inter-regular.woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter";
src: url("inter-bold.woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
html,
body,
button,
input {
/* Apply a modern sans-serif font stack in case the Inter font fails to load. */
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 14px;
font-feature-settings: "calt" 0, "ss04" 1, "tnum" 1;
}
*:focus-visible {
/* More visible outline for better keyboard navigation. */
outline: 0.1875rem solid #538dda;
/* Make the outline always appear above other elements. */
/* Otherwise, one of its sides can be hidden by tabs in the Download and More layouts. */
position: relative;
}
body {
touch-action: none;
margin: 0;
border: 0 none;
padding: 0;
font-size: 14px;
text-align: center;
background-color: #262626;
overflow: hidden;
}
h2 {
font-size: 21px;
}
a {
color: hsl(205, 100%, 75%);
text-decoration-color: hsla(205, 100%, 75%, 0.3);
text-decoration-thickness: 0.125rem;
}
a:hover {
filter: brightness(117.5%);
}
a:active {
filter: brightness(82.5%);
}
.welcome-modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: hsla(0, 0%, 0%, 0.5);
text-align: left;
}
.welcome-modal-title {
text-align: center;
}
.welcome-modal-content {
background-color: #262626;
box-shadow: 0 0.25rem 0.25rem hsla(0, 0%, 0%, 0.5);
line-height: 1.5;
max-width: 38rem;
margin: 4rem auto 0 auto;
color: white;
border-radius: 0.5rem;
padding: 0.125rem 1.25rem 1.25rem 1.25rem;
}
#tabs-buttons {
/* Remove spacing between items. */
font-size: 0;
/* Match the default background color of the editor window for a seamless appearance. */
background-color: #141414;
}
#tab-game {
/* Match the default background color of the editor window for a seamless appearance. */
background-color: #141414;
/* Make the background span the entire page height. */
min-height: 100vh;
}
#canvas, #gameCanvas {
display: block;
margin: 0;
color: white;
}
/* Don't show distracting focus outlines for the main tabs' contents. */
#tab-editor canvas:focus,
#tab-game canvas:focus,
#canvas:focus,
#gameCanvas:focus {
outline: none;
}
.godot {
color: #d0d0d0;
background-color: #1b1b1b;
background-image: linear-gradient(to bottom, #403e48, #35333c);
border: 1px solid #484848;
box-shadow: 0 0 1px 1px #141414;
}
.btn {
appearance: none;
color: #d0d0d0;
background-color: #424242;
border: 1px solid #484848;
/* Vertically center button text. */
padding: 0.4375rem 0.5625rem 0.5rem;
margin: 0 0.5rem;
border-radius: 0.25rem;
}
.btn:not(:disabled):hover {
color: #e4e4e4;
background-color: #4d4d4d;
border-color: #535353;
}
.btn:active {
color: #e5e5e5;
background-color: #505050;
border-color: #565656;
}
.btn:disabled {
color: #7f7f7f;
background-color: #3a3a3a;
border-color: #3a3a3a;
}
.btn.tab-btn,
.btn.close-btn {
/* Match tab colors from the default editor theme. */
background-color: transparent;
border-color: transparent;
}
.btn.tab-btn:not(:disabled):hover,
.btn.close-btn:not(:disabled):hover {
background-color: #212121;
border-color: #212121;
}
.btn.tab-btn {
/* Round top corners only. */
border-radius: 0.25rem 0.25rem 0 0;
}
.btn.next-to-close-btn {
/* Round top-left corner only. */
border-radius: 0.25rem 0 0 0;
}
.btn.close-btn {
/* Round top-right corner only. */
border-radius: 0 0.25rem 0 0;
margin-left: -0.75rem;
font-weight: 700;
}
/* Status display */
#status {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
/* don't consume click events - make children visible explicitly */
visibility: hidden;
}
#status-progress {
width: 366px;
height: 7px;
background-color: #38363A;
border: 1px solid #444246;
padding: 1px;
box-shadow: 0 0 2px 1px #1B1C22;
border-radius: 2px;
visibility: visible;
}
@media only screen and (orientation:portrait) {
#status-progress {
width: 61.8%;
}
}
#status-progress-inner {
height: 100%;
width: 0;
box-sizing: border-box;
transition: width 0.5s linear;
background-color: #202020;
border: 1px solid #222223;
box-shadow: 0 0 1px 1px #27282E;
border-radius: 3px;
}
#status-indeterminate {
visibility: visible;
position: relative;
}
#status-indeterminate > div {
width: 4.5px;
height: 0;
border-style: solid;
border-width: 9px 3px 0 3px;
border-color: #2b2b2b transparent transparent transparent;
transform-origin: center 21px;
position: absolute;
}
#status-indeterminate > div:nth-child(1) { transform: rotate( 22.5deg); }
#status-indeterminate > div:nth-child(2) { transform: rotate( 67.5deg); }
#status-indeterminate > div:nth-child(3) { transform: rotate(112.5deg); }
#status-indeterminate > div:nth-child(4) { transform: rotate(157.5deg); }
#status-indeterminate > div:nth-child(5) { transform: rotate(202.5deg); }
#status-indeterminate > div:nth-child(6) { transform: rotate(247.5deg); }
#status-indeterminate > div:nth-child(7) { transform: rotate(292.5deg); }
#status-indeterminate > div:nth-child(8) { transform: rotate(337.5deg); }
#status-notice {
margin: 0 100px;
line-height: 1.3;
visibility: visible;
padding: 4px 6px;
}
</style>
</head>
<body>
<div
id="welcome-modal"
class="welcome-modal"
role="dialog"
aria-labelledby="welcome-modal-title"
aria-describedby="welcome-modal-description"
onclick="if (event.target === this) closeWelcomeModal(false)"
>
<div class="welcome-modal-content">
<h2 id="welcome-modal-title" class="welcome-modal-title">Important - Please read before continuing</h2>
<div id="welcome-modal-description">
<p>
The Godot Web Editor has some limitations compared to the native version.<br>
Its main focus is education and experimentation;
<strong>it is not recommended for production</strong>.
</p>
<p>
For a more optimal experience, <a
href="https://godotengine.org/download/"
target="_blank"
rel="noopener"
>download</a> the native Godot Editor.
</p>
<p>
Refer to the
<a
href="https://docs.godotengine.org/en/stable/tutorials/editor/using_the_web_editor.html"
target="_blank"
rel="noopener"
>Web editor documentation</a> for usage instructions and limitations.
</p>
</div>
<div id="welcome-modal-missing-description" style="display: none">
<p>
<strong>The following features required by the Godot Web Editor are missing:</strong>
</p>
<ul id="welcome-modal-missing-list">
</ul>
<p>
If you are self-hosting the web editor,
refer to
<a
href="https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_web.html"
target="_blank"
rel="noopener"
>Exporting for the Web</a> for more information.
</p>
</div>
<div style="text-align: center">
<button id="welcome-modal-dismiss" class="btn" type="button" onclick="closeWelcomeModal(true)" style="margin-top: 1rem">
OK - Don't Show Again
</button>
</div>
</div>
</div>
<div id="tabs-buttons">
<button id="btn-tab-loader" class="btn tab-btn" onclick="showTab('loader')">Loader</button>
<button id="btn-tab-editor" class="btn tab-btn next-to-close-btn" disabled="disabled" onclick="showTab('editor')">Editor</button>
<button id="btn-close-editor" class="btn close-btn" disabled="disabled" onclick="closeEditor()">×</button>
<button id="btn-tab-game" class="btn tab-btn next-to-close-btn" disabled="disabled" onclick="showTab('game')">Game</button>
<button id="btn-close-game" class="btn close-btn" disabled="disabled" onclick="closeGame()">×</button>
<button id="btn-tab-update" class="btn tab-btn" style="display: none;">Update</button>
</div>
<div id="tabs">
<div id="tab-loader">
<div style="color: #e0e0e0;" id="persistence">
<br >
<img src="logo.svg" alt="Godot Engine logo" width="1024" height="414" style="width: auto; height: auto; max-width: min(85%, 50vh); max-height: 200px">
<br >
___GODOT_VERSION___ - <a href="releases/">Change version</a>
<br >
<br >
<label for="zip-file" style="margin-right: 1rem">Preload project ZIP:</label>
<input id="zip-file" type="file" name="files" style="margin-bottom: 0.5rem">
<br>
<a href="demo.zip" id="zip-demo">(Try this for example)</a>
<br >
<button id="startButton" class="btn" style="margin-top: 2rem; margin-bottom: 3rem; font-weight: 700">Start Godot Editor</button>
<br >
<button class="btn" onclick="clearPersistence()" style="margin-bottom: 1.5rem">Clear Persistent Data</button>
<br >
<a href="https://docs.godotengine.org/en/stable/tutorials/editor/using_the_web_editor.html">Web editor documentation</a>
</div>
</div>
<div id="tab-editor" style="display: none;">
<canvas id="editor-canvas" tabindex="1">
HTML5 canvas appears to be unsupported in the current browser.<br >
Please try updating or use a different browser.
</canvas>
</div>
<div id="tab-game" style="display: none;">
<canvas id="game-canvas" tabindex="2">
HTML5 canvas appears to be unsupported in the current browser.<br >
Please try updating or use a different browser.
</canvas>
</div>
<div id="tab-status" style="display: none;">
<div id="status-progress" style="display: none;" oncontextmenu="event.preventDefault();">
<div id="status-progress-inner"></div>
</div>
<div id="status-indeterminate" style="display: none;" oncontextmenu="event.preventDefault();">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div id="status-notice" class="godot" style="display: none;"></div>
</div>
</div>
<script>
window.addEventListener('load', () => {
function notifyUpdate(sw) {
const btn = document.getElementById('btn-tab-update');
btn.onclick = function () {
if (!window.confirm('Are you sure you want to update?\nClicking "OK" will reload all active instances!')) {
return;
}
sw.postMessage('update');
btn.innerHTML = 'Updating...';
btn.disabled = true;
};
btn.style.display = '';
}
if ('serviceWorker' in navigator) {
try {
navigator.serviceWorker.register('service.worker.js').then(function (reg) {
if (reg.waiting) {
notifyUpdate(reg.waiting);
}
reg.addEventListener('updatefound', function () {
const update = reg.installing;
update.addEventListener('statechange', function () {
if (update.state === 'installed') {
// It's a new install, claim and perform aggressive caching.
if (!reg.active) {
update.postMessage('claim');
} else {
notifyUpdate(update);
}
}
});
});
});
} catch (e) {
console.error('Error while registering service worker:', e);
}
}
const missing = Engine.getMissingFeatures({
threads: ___GODOT_THREADS_ENABLED___,
});
if (missing.length) {
// Display error dialog as threading support is required for the editor.
document.getElementById('startButton').disabled = 'disabled';
document.getElementById('welcome-modal-description').style.display = 'none';
document.getElementById('welcome-modal-missing-description').style.display = 'block';
document.getElementById('welcome-modal-dismiss').style.display = 'none';
const list = document.getElementById('welcome-modal-missing-list');
for (let i = 0; i < missing.length; i++) {
const node = document.createElement('li');
node.innerText = missing[i];
list.appendChild(node);
}
}
if (missing.length || localStorage.getItem('welcomeModalDismissed') !== 'true') {
document.getElementById('welcome-modal').style.display = 'block';
document.getElementById('welcome-modal-dismiss').focus();
}
});
function closeWelcomeModal(dontShowAgain) { // eslint-disable-line no-unused-vars
document.getElementById('welcome-modal').style.display = 'none';
if (dontShowAgain) {
localStorage.setItem('welcomeModalDismissed', 'true');
}
}
</script>
<script src="godot.editor.js"></script>
<script>
let editor = null;
let game = null;
let setStatusMode;
let setStatusNotice;
function clearPersistence() { // eslint-disable-line no-unused-vars
function deleteDB(path) {
return new Promise(function (resolve, reject) {
const req = indexedDB.deleteDatabase(path);
req.onsuccess = function () {
resolve();
};
req.onerror = function (err) {
reject(err);
};
req.onblocked = function (err) {
reject(err);
};
});
}
if (!window.confirm('Are you sure you want to delete all the locally stored files?\nClicking "OK" will permanently remove your projects and editor settings!')) {
return;
}
Promise.all([
deleteDB('/home/web_user'),
]).then(function (results) {
alert('Done.');
}).catch(function (err) {
alert('Error deleting local files. Please retry after reloading the page.');
});
}
const tabs = [
document.getElementById('tab-loader'),
document.getElementById('tab-editor'),
document.getElementById('tab-game'),
];
function showTab(name) {
tabs.forEach(function (elem) {
if (elem.id === `tab-${name}`) {
elem.style.display = 'block';
if (name === 'editor' || name === 'game') {
const canvas = document.getElementById(`${name}-canvas`);
canvas.focus();
}
} else {
elem.style.display = 'none';
}
});
}
function setButtonEnabled(id, enabled) {
if (enabled) {
document.getElementById(id).disabled = '';
} else {
document.getElementById(id).disabled = 'disabled';
}
}
function setLoaderEnabled(enabled) {
setButtonEnabled('btn-tab-loader', enabled);
setButtonEnabled('btn-tab-editor', !enabled);
setButtonEnabled('btn-close-editor', !enabled);
}
function setGameTabEnabled(enabled) {
setButtonEnabled('btn-tab-game', enabled);
setButtonEnabled('btn-close-game', enabled);
}
function closeGame() {
if (game) {
game.requestQuit();
}
}
function closeEditor() { // eslint-disable-line no-unused-vars
closeGame();
if (editor) {
editor.requestQuit();
}
}
function startEditor(zip) {
const INDETERMINATE_STATUS_STEP_MS = 100;
const persistentPaths = ['/home/web_user'];
let editorCanvas = document.getElementById('editor-canvas');
let gameCanvas = document.getElementById('game-canvas');
const statusProgress = document.getElementById('status-progress');
const statusProgressInner = document.getElementById('status-progress-inner');
const statusIndeterminate = document.getElementById('status-indeterminate');
const statusNotice = document.getElementById('status-notice');
const headerDiv = document.getElementById('tabs-buttons');
let initializing = true;
let statusMode = 'hidden';
showTab('status');
let animationCallbacks = [];
function animate(time) {
animationCallbacks.forEach((callback) => callback(time));
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
let lastScale = 0;
let lastWidth = 0;
let lastHeight = 0;
function adjustCanvasDimensions() {
const scale = window.devicePixelRatio || 1;
const headerHeight = headerDiv.offsetHeight + 1;
const width = window.innerWidth;
const height = window.innerHeight - headerHeight;
if (lastScale !== scale || lastWidth !== width || lastHeight !== height) {
editorCanvas.width = width * scale;
editorCanvas.height = height * scale;
editorCanvas.style.width = `${width}px`;
editorCanvas.style.height = `${height}px`;
lastScale = scale;
lastWidth = width;
lastHeight = height;
}
}
animationCallbacks.push(adjustCanvasDimensions);
adjustCanvasDimensions();
function replaceCanvas(from) {
const out = document.createElement('canvas');
out.id = from.id;
out.tabIndex = from.tabIndex;
from.parentNode.replaceChild(out, from);
lastScale = 0;
return out;
}
function animateStatusIndeterminate(ms) {
const i = Math.floor((ms / INDETERMINATE_STATUS_STEP_MS) % 8);
if (statusIndeterminate.children[i].style.borderTopColor === '') {
Array.prototype.slice.call(statusIndeterminate.children).forEach((child) => {
child.style.borderTopColor = '';
});
statusIndeterminate.children[i].style.borderTopColor = '#dfdfdf';
}
}
setStatusMode = function (mode) {
if (statusMode === mode || !initializing) {
return;
}
[statusProgress, statusIndeterminate, statusNotice].forEach((elem) => {
elem.style.display = 'none';
});
animationCallbacks = animationCallbacks.filter(function (value) {
return (value !== animateStatusIndeterminate);
});
switch (mode) {
case 'progress':
statusProgress.style.display = 'block';
break;
case 'indeterminate':
statusIndeterminate.style.display = 'block';
animationCallbacks.push(animateStatusIndeterminate);
break;
case 'notice':
statusNotice.style.display = 'block';
break;
case 'hidden':
break;
default:
throw new Error('Invalid status mode');
}
statusMode = mode;
};
setStatusNotice = function (text) {
while (statusNotice.lastChild) {
statusNotice.removeChild(statusNotice.lastChild);
}
const lines = text.split('\n');
lines.forEach((line) => {
statusNotice.appendChild(document.createTextNode(line));
statusNotice.appendChild(document.createElement('br'));
});
};
const gameConfig = {
'persistentPaths': persistentPaths,
'unloadAfterInit': false,
'canvas': gameCanvas,
'canvasResizePolicy': 1,
'onExit': function () {
gameCanvas = replaceCanvas(gameCanvas);
setGameTabEnabled(false);
showTab('editor');
game = null;
},
};
let OnEditorExit = function () {
showTab('loader');
setLoaderEnabled(true);
};
function Execute(args) {
const is_editor = args.filter(function (v) {
return v === '--editor' || v === '-e';
}).length !== 0;
const is_project_manager = args.filter(function (v) {
return v === '--project-manager';
}).length !== 0;
const is_game = !is_editor && !is_project_manager;
if (is_game) {
if (game) {
console.error('A game is already running. Close it first');
return;
}
setGameTabEnabled(true);
game = new Engine(gameConfig);
showTab('game');
game.init().then(function () {
requestAnimationFrame(function () {
game.start({ 'args': args, 'canvas': gameCanvas }).then(function () {
gameCanvas.focus();
});
});
});
} else { // New editor instances will be run in the same canvas. We want to wait for it to exit.
OnEditorExit = function (code) {
setLoaderEnabled(true);
setTimeout(function () {
editor.init().then(function () {
setLoaderEnabled(false);
OnEditorExit = function () {
showTab('loader');
setLoaderEnabled(true);
};
editor.start({ 'args': args, 'persistentDrops': is_project_manager, 'canvas': editorCanvas });
});
}, 0);
OnEditorExit = null;
};
}
}
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
// We need at least 6 free threads from the pool to start the editor.
// At least 4 more will be reserved for the godot thread pool (3 is the bare minimum with the multithreaded variant of the servers).
const concurrency = clamp(navigator.hardwareConcurrency ?? 1, 12, 24);
const editorConfig = {
'unloadAfterInit': false,
'onProgress': function progressFunction(current, total) {
if (total > 0) {
statusProgressInner.style.width = `${(current / total) * 100}%`;
setStatusMode('progress');
if (current === total) {
// wait for progress bar animation
setTimeout(() => {
setStatusMode('indeterminate');
}, 100);
}
} else {
setStatusMode('indeterminate');
}
},
'canvas': editorCanvas,
'canvasResizePolicy': 0,
'onExit': function () {
editorCanvas = replaceCanvas(editorCanvas);
if (OnEditorExit) {
OnEditorExit();
}
},
'onExecute': Execute,
'persistentPaths': persistentPaths,
'emscriptenPoolSize': concurrency,
'godotPoolSize': Math.floor(concurrency / 3), // Ensures at least 4 threads for the pool (see above).
};
editor = new Engine(editorConfig);
function displayFailureNotice(err) {
console.error(err);
if (err instanceof Error) {
setStatusNotice(err.message);
} else if (typeof err === 'string') {
setStatusNotice(err);
} else {
setStatusNotice('An unknown error occurred.');
}
setStatusMode('notice');
initializing = false;
}
if (!Engine.isWebGLAvailable()) {
displayFailureNotice('WebGL not available');
} else {
setStatusMode('indeterminate');
editor.init('godot.editor').then(function () {
if (zip) {
editor.copyToFS('/tmp/preload.zip', zip);
}
try {
// Avoid user creating project in the persistent root folder.
editor.copyToFS('/home/web_user/keep', new Uint8Array());
} catch (e) {
// File exists
}
showTab('editor');
setLoaderEnabled(false);
const args = ['--project-manager', '--single-window'];
editor.start({ 'args': args, 'persistentDrops': true }).then(function () {
setStatusMode('hidden');
initializing = false;
});
}).catch(displayFailureNotice);
}
}
function preloadZip(target) {
return new Promise(function (resolve, reject) {
if (target.files.length > 0) {
target.files[0].arrayBuffer().then(function (data) {
resolve(data);
});
} else {
resolve();
}
});
}
document.getElementById('startButton').onclick = function () {
preloadZip(document.getElementById('zip-file')).then(function (zip) {
startEditor(zip);
});
};
const zipFile = document.getElementById('zip-file');
const zipDemo = document.getElementById('zip-demo');
function updateZipDemoDisplay() {
console.log(zipFile.files.length);
// Hide "Try this for example" link when a file is selected.
zipDemo.style.display = zipFile.files.length === 0 ? 'inline' : 'none';
}
zipFile.addEventListener('change', (_pEvent) => {
updateZipDemoDisplay();
});
document.addEventListener('DOMContentLoaded', () => {
// Handle situation when a file is already specified on load (e.g. after a page refresh).
updateZipDemoDisplay();
});
</script>
</body>
</html>