godot/misc/dist/html/editor.html
Fabio Alessandrelli d8bd54fbf2 [HTML5] Add PWA support to the editor page.
This allows to install it as an app, and provide offline support (after
the first run).
Practically, this boils down to adding a JSON file as a manifest, an
offline page to be displayed when the cached files are not avaialble,
and a JS file to cache resources and return them.

The reason for the "first run requirements" is that some browsers, will
emit an "install" by just visiting the page (to see if the JS code is
compatibile), and we do not want to force casual visitors to just
download the 10 MiB+ compressed editor WebAssembly file without pressing
the start button.

Special thanks to Hugo Locurcio (Calinou) for the initial work.
2021-03-08 17:06:24 +01:00

593 lines
17 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 xmlns='http://www.w3.org/1999/xhtml' lang='' xml:lang=''>
<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="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="#478cbf" />
<meta name="msapplication-navbutton-color" content="#478cbf" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="msapplication-starturl" content="/latest" />
<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" />
<title>Godot Engine Web Editor (@GODOT_VERSION@)</title>
<style>
*:focus {
/* More visible outline for better keyboard navigation. */
outline: 0.125rem solid hsl(220, 100%, 62.5%);
/* 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;
font-family: 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";
margin: 0;
border: 0 none;
padding: 0;
text-align: center;
background-color: #333b4f;
overflow: hidden;
}
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%);
}
#tabs-buttons {
/* Match the default background color of the editor window for a seamless appearance. */
background-color: #202531;
}
#tab-game {
/* Use a pure black background to better distinguish the running project */
/* from the editor window, and to use a more neutral background color (no tint). */
background-color: black;
/* 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: #e0e0e0;
background-color: #3b3943;
background-image: linear-gradient(to bottom, #403e48, #35333c);
border: 1px solid #45434e;
box-shadow: 0 0 1px 1px #2f2d35;
}
.btn {
appearance: none;
color: #e0e0e0;
background-color: #262c3b;
border: 1px solid #202531;
padding: 0.5rem 1rem;
margin: 0 0.5rem;
}
.btn:not(:disabled):hover {
color: #e0e1e5;
border-color: #666c7b;
}
.btn:active {
border-color: #699ce8;
color: #699ce8;
}
.btn:disabled {
color: #aaa;
border-color: #242937;
}
.btn.tab-btn {
padding: 0.3rem 1rem;
}
.btn.close-btn {
padding: 0.3rem 1rem;
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;
visibility: visible;
}
</style>
</head>
<body>
<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" 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" disabled="disabled" onclick="showTab('game')">Game</button>
<button id="btn-close-game" class="btn close-btn" disabled="disabled" onclick="closeGame()">×</button>
</div>
<div id='tabs'>
<div id='tab-loader'>
<div style="color: #e0e0e0;" id="persistence">
<label for="videoMode" style="display: none;">Select video driver:</label><br />
<select id="videoMode" style="display: none;">
<option value="GLES2" selected="selected">WebGL</option>
<option value="GLES3">WebGL 2</option>
</select>
<br />
<img src="logo.svg" width="1024" height="414" style="width: auto; height: auto; max-width: 85%; max-height: 250px" />
<br />
@GODOT_VERSION@
<br />
<a href="releases/">Need an old version?</a>
<br />
<br />
<br />
<label for="zip-file" style="margin-right: 1rem">Preload project ZIP:</label> <input id="zip-file" type="file" id="files" name="files" style="margin-bottom: 1rem"/>
<br />
<a href="demo.zip">(Try this for example)</a>
<br />
<br />
<button id="startButton" class="btn" style="margin-bottom: 4rem; 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/latest/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", () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("service.worker.js");
}
});
</script>
<script src='godot.tools.js'></script>
<script>//<![CDATA[
var editor = null;
var game = null;
var setStatusMode;
var setStatusNotice;
var video_driver = "GLES2";
function clearPersistence() {
function deleteDB(path) {
return new Promise(function(resolve, reject) {
var 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.");
});
}
function selectVideoMode() {
var select = document.getElementById('videoMode');
video_driver = select.selectedOptions[0].value;
}
var 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() {
closeGame();
if (editor) {
editor.requestQuit();
}
}
function startEditor(zip) {
const INDETERMINATE_STATUS_STEP_MS = 100;
const persistentPaths = ['/home/web_user'];
var editorCanvas = document.getElementById('editor-canvas');
var gameCanvas = document.getElementById('game-canvas');
var statusProgress = document.getElementById('status-progress');
var statusProgressInner = document.getElementById('status-progress-inner');
var statusIndeterminate = document.getElementById('status-indeterminate');
var statusNotice = document.getElementById('status-notice');
var headerDiv = document.getElementById('tabs-buttons');
var initializing = true;
var statusMode = 'hidden';
showTab('status');
var animationCallbacks = [];
function animate(time) {
animationCallbacks.forEach(callback => callback(time));
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
var lastScale = 0;
var lastWidth = 0;
var lastHeight = 0;
function adjustCanvasDimensions() {
var scale = window.devicePixelRatio || 1;
var headerHeight = headerDiv.offsetHeight + 1;
var width = window.innerWidth;
var 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();
setStatusMode = function setStatusMode(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;
};
function animateStatusIndeterminate(ms) {
var 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';
}
}
setStatusNotice = function setStatusNotice(text) {
while (statusNotice.lastChild) {
statusNotice.removeChild(statusNotice.lastChild);
}
var 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 () {
setGameTabEnabled(false);
showTab('editor');
game = null;
},
};
var 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_project_manager) {
args.push('--video-driver', video_driver);
}
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}).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});
});
}, 0);
OnEditorExit = null;
};
}
}
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() {
if (OnEditorExit) {
OnEditorExit();
}
},
'onExecute': Execute,
'persistentPaths': persistentPaths,
};
editor = new Engine(editorConfig);
function displayFailureNotice(err) {
var msg = err.message || err;
console.error(msg);
setStatusNotice(msg);
setStatusMode('notice');
initializing = false;
};
if (!Engine.isWebGLAvailable()) {
displayFailureNotice('WebGL not available');
} else {
setStatusMode('indeterminate');
editor.init('godot.tools').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
}
//selectVideoMode();
showTab('editor');
setLoaderEnabled(false);
editor.start({'args': ['--video-driver', video_driver]}).then(function() {
setStatusMode('hidden');
initializing = false;
});
}).catch(displayFailureNotice);
}
};
document.getElementById("startButton").onclick = function() {
preloadZip(document.getElementById('zip-file')).then(function(zip) {
startEditor(zip);
});
}
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();
}
});
}
//]]></script>
</body>
</html>