mirror of
https://github.com/godotengine/godot.git
synced 2026-01-03 18:11:19 +03:00
HTML5 start-up overhaul
- Implement promise-based JS interface for custom HTML page integration - Add download progress callback - Add progress bar and indeterminate spinner to default HTML page - Try downloading files multiple times when failing - Get rid of godotfs.js - Separate steps for engine initialization, game initialization and game start - Allow multiple games on one HTML page - Substitution placeholders only used in .html file - Placeholders renamed: $GODOT_BASE => $GODOT_BASENAME, $GODOT_TMEM -> $GODOT_TOTAL_MEMORY - Emscripten Module is now Engine.RuntimeEnvironment (no longer a global)
This commit is contained in:
386
misc/dist/html/default.html
vendored
Normal file
386
misc/dist/html/default.html
vendored
Normal file
@@ -0,0 +1,386 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
<style type="text/css">
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
border: 0 none;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
background-color: #222226;
|
||||
font-family: 'Noto Sans', Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* Godot Engine default theme style
|
||||
* ================================ */
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
button.godot {
|
||||
font-family: 'Droid Sans', Arial, sans-serif; /* override user agent style */
|
||||
padding: 1px 5px;
|
||||
background-color: #37353f;
|
||||
background-image: linear-gradient(to bottom, #413e49, #3a3842);
|
||||
border: 1px solid #514f5d;
|
||||
border-radius: 1px;
|
||||
box-shadow: 0 0 1px 1px #2a2930;
|
||||
}
|
||||
|
||||
button.godot:hover {
|
||||
color: #f0f0f0;
|
||||
background-color: #44414e;
|
||||
background-image: linear-gradient(to bottom, #494652, #423f4c);
|
||||
border: 1px solid #5a5667;
|
||||
box-shadow: 0 0 1px 1px #26252b;
|
||||
}
|
||||
|
||||
button.godot:active {
|
||||
color: #fff;
|
||||
background-color: #3e3b46;
|
||||
background-image: linear-gradient(to bottom, #36343d, #413e49);
|
||||
border: 1px solid #4f4c59;
|
||||
box-shadow: 0 0 1px 1px #26252b;
|
||||
}
|
||||
|
||||
button.godot:disabled {
|
||||
color: rgba(230, 230, 230, 0.2);
|
||||
background-color: #3d3d3d;
|
||||
background-image: linear-gradient(to bottom, #434343, #393939);
|
||||
border: 1px solid #474747;
|
||||
box-shadow: 0 0 1px 1px #2d2b33;
|
||||
}
|
||||
|
||||
|
||||
/* Canvas / wrapper
|
||||
* ================ */
|
||||
|
||||
#container {
|
||||
display: inline-block; /* scale with canvas */
|
||||
vertical-align: top; /* prevent extra height */
|
||||
position: relative; /* root for absolutely positioned overlay */
|
||||
margin: 0;
|
||||
border: 0 none;
|
||||
padding: 0;
|
||||
background-color: #0c0c0c;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#canvas:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
/* 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: 244px;
|
||||
height: 7px;
|
||||
background-color: #38363A;
|
||||
border: 1px solid #444246;
|
||||
padding: 1px;
|
||||
box-shadow: 0 0 2px 1px #1B1C22;
|
||||
border-radius: 2px;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#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: 3px;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 6px 2px 0 2px;
|
||||
border-color: #2b2b2b transparent transparent transparent;
|
||||
transform-origin: center 14px;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/* Debug output
|
||||
* ============ */
|
||||
|
||||
#output-panel {
|
||||
display: none;
|
||||
max-width: 700px;
|
||||
font-size: small;
|
||||
margin: 6px auto 0;
|
||||
padding: 0 4px 4px;
|
||||
text-align: left;
|
||||
line-height: 2.2;
|
||||
}
|
||||
|
||||
#output-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#output-container {
|
||||
padding: 6px;
|
||||
background-color: #2c2a32;
|
||||
box-shadow: inset 0 0 1px 1px #232127;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
#output-scroll {
|
||||
line-height: 1;
|
||||
height: 12em;
|
||||
overflow-y: scroll;
|
||||
white-space: pre-wrap;
|
||||
font-size: small;
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
}
|
||||
</style>
|
||||
$GODOT_HEAD_INCLUDE
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<canvas id="canvas" oncontextmenu="event.preventDefault();" width="640" height="480">
|
||||
HTML5 canvas appears to be unsupported in the current browser.<br />
|
||||
Please try updating or use a different browser.
|
||||
</canvas>
|
||||
<div id="status">
|
||||
<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>
|
||||
<div id="output-panel" class="godot">
|
||||
<div id="output-header">
|
||||
Output:
|
||||
<button id='output-clear' class='godot' type='button' autocomplete='off'>Clear</button>
|
||||
</div>
|
||||
<div id="output-container"><div id="output-scroll"></div></div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="$GODOT_BASENAME.js"></script>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
|
||||
var game = new Engine;
|
||||
|
||||
(function() {
|
||||
|
||||
const BASENAME = '$GODOT_BASENAME';
|
||||
const MEMORY_SIZE = $GODOT_TOTAL_MEMORY;
|
||||
const DEBUG_ENABLED = $GODOT_DEBUG_ENABLED;
|
||||
const INDETERMINATE_STATUS_STEP_MS = 100;
|
||||
|
||||
var container = document.getElementById('container');
|
||||
var canvas = document.getElementById('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 initializing = true;
|
||||
var statusMode = 'hidden';
|
||||
var indeterminiateStatusAnimationId = 0;
|
||||
|
||||
setStatusMode('indeterminate');
|
||||
game.setCanvas(canvas);
|
||||
game.setAsmjsMemorySize(MEMORY_SIZE);
|
||||
|
||||
function setStatusMode(mode) {
|
||||
|
||||
if (statusMode === mode || !initializing)
|
||||
return;
|
||||
[statusProgress, statusIndeterminate, statusNotice].forEach(elem => {
|
||||
elem.style.display = 'none';
|
||||
});
|
||||
if (indeterminiateStatusAnimationId !== 0) {
|
||||
cancelAnimationFrame(indeterminiateStatusAnimationId);
|
||||
indeterminiateStatusAnimationId = 0;
|
||||
}
|
||||
switch (mode) {
|
||||
case 'progress':
|
||||
statusProgress.style.display = 'block';
|
||||
break;
|
||||
case 'indeterminate':
|
||||
statusIndeterminate.style.display = 'block';
|
||||
indeterminiateStatusAnimationId = requestAnimationFrame(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';
|
||||
}
|
||||
requestAnimationFrame(animateStatusIndeterminate);
|
||||
}
|
||||
|
||||
function setStatusNotice(text) {
|
||||
|
||||
while (statusNotice.lastChild) {
|
||||
statusNotice.removeChild(statusNotice.lastChild);
|
||||
}
|
||||
var lines = text.split('\n');
|
||||
lines.forEach((line, index) => {
|
||||
statusNotice.appendChild(document.createTextNode(line));
|
||||
statusNotice.appendChild(document.createElement('br'));
|
||||
});
|
||||
};
|
||||
|
||||
game.setProgressFunc((current, total) => {
|
||||
|
||||
if (total > 0) {
|
||||
statusProgressInner.style.width = current/total * 100 + '%';
|
||||
setStatusMode('progress');
|
||||
if (current === total) {
|
||||
// wait for progress bar animation
|
||||
setTimeout(() => {
|
||||
setStatusMode('indeterminate');
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
setStatusMode('indeterminate');
|
||||
}
|
||||
});
|
||||
|
||||
if (DEBUG_ENABLED) {
|
||||
var outputRoot = document.getElementById("output-panel");
|
||||
var outputScroll = document.getElementById("output-scroll");
|
||||
var OUTPUT_MSG_COUNT_MAX = 400;
|
||||
|
||||
document.getElementById('output-clear').addEventListener('click', () => {
|
||||
while (outputScroll.firstChild) {
|
||||
outputScroll.firstChild.remove();
|
||||
}
|
||||
});
|
||||
|
||||
outputRoot.style.display = 'block';
|
||||
|
||||
function print(text) {
|
||||
if (arguments.length > 1) {
|
||||
text = Array.prototype.slice.call(arguments).join(" ");
|
||||
}
|
||||
if (text.length <= 0) return;
|
||||
while (outputScroll.childElementCount >= OUTPUT_MSG_COUNT_MAX) {
|
||||
outputScroll.firstChild.remove();
|
||||
}
|
||||
var msg = document.createElement("div");
|
||||
if (String.prototype.trim.call(text).startsWith("**ERROR**")) {
|
||||
msg.style.color = "#d44";
|
||||
} else if (String.prototype.trim.call(text).startsWith("**WARNING**")) {
|
||||
msg.style.color = "#ccc000";
|
||||
} else if (String.prototype.trim.call(text).startsWith("**SCRIPT ERROR**")) {
|
||||
msg.style.color = "#c6d";
|
||||
}
|
||||
msg.textContent = text;
|
||||
var scrollToBottom = outputScroll.scrollHeight - (outputScroll.clientHeight + outputScroll.scrollTop) < 10;
|
||||
outputScroll.appendChild(msg);
|
||||
if (scrollToBottom) {
|
||||
outputScroll.scrollTop = outputScroll.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
function printError(text) {
|
||||
print('**ERROR**' + ":", text);
|
||||
}
|
||||
|
||||
game.setStdoutFunc(text => {
|
||||
print(text);
|
||||
console.log(text);
|
||||
});
|
||||
|
||||
game.setStderrFunc(text => {
|
||||
printError(text);
|
||||
console.warn(text);
|
||||
});
|
||||
}
|
||||
|
||||
game.start(BASENAME + '.pck').then(() => {
|
||||
setStatusMode('hidden');
|
||||
initializing = false;
|
||||
}, err => {
|
||||
if (DEBUG_ENABLED)
|
||||
printError(err.message);
|
||||
setStatusNotice(err.message);
|
||||
setStatusMode('notice');
|
||||
initializing = false;
|
||||
});
|
||||
})();
|
||||
//]]></script>
|
||||
</body>
|
||||
</html>
|
||||
151
misc/dist/html_fs/godotfs.js
vendored
151
misc/dist/html_fs/godotfs.js
vendored
@@ -1,151 +0,0 @@
|
||||
|
||||
var Module;
|
||||
if (typeof Module === 'undefined') Module = eval('(function() { try { return Module || {} } catch(e) { return {} } })()');
|
||||
if (!Module.expectedDataFileDownloads) {
|
||||
Module.expectedDataFileDownloads = 0;
|
||||
Module.finishedDataFileDownloads = 0;
|
||||
}
|
||||
Module.expectedDataFileDownloads++;
|
||||
(function() {
|
||||
|
||||
const PACK_FILE_NAME = '$GODOT_PACK_NAME';
|
||||
const PACK_FILE_SIZE = $GODOT_PACK_SIZE;
|
||||
function fetchRemotePackage(packageName, callback, errback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', packageName, true);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.onprogress = function(event) {
|
||||
var url = packageName;
|
||||
if (event.loaded && event.total) {
|
||||
if (!xhr.addedTotal) {
|
||||
xhr.addedTotal = true;
|
||||
if (!Module.dataFileDownloads) Module.dataFileDownloads = {};
|
||||
Module.dataFileDownloads[url] = {
|
||||
loaded: event.loaded,
|
||||
total: event.total
|
||||
};
|
||||
} else {
|
||||
Module.dataFileDownloads[url].loaded = event.loaded;
|
||||
}
|
||||
var total = 0;
|
||||
var loaded = 0;
|
||||
var num = 0;
|
||||
for (var download in Module.dataFileDownloads) {
|
||||
var data = Module.dataFileDownloads[download];
|
||||
total += data.total;
|
||||
loaded += data.loaded;
|
||||
num++;
|
||||
}
|
||||
total = Math.ceil(total * Module.expectedDataFileDownloads/num);
|
||||
if (Module['setStatus']) Module['setStatus']('Downloading data... (' + loaded + '/' + total + ')');
|
||||
} else if (!Module.dataFileDownloads) {
|
||||
if (Module['setStatus']) Module['setStatus']('Downloading data...');
|
||||
}
|
||||
};
|
||||
xhr.onload = function(event) {
|
||||
var packageData = xhr.response;
|
||||
callback(packageData);
|
||||
};
|
||||
xhr.send(null);
|
||||
};
|
||||
|
||||
function handleError(error) {
|
||||
console.error('package error:', error);
|
||||
};
|
||||
|
||||
var fetched = null, fetchedCallback = null;
|
||||
fetchRemotePackage(PACK_FILE_NAME, function(data) {
|
||||
if (fetchedCallback) {
|
||||
fetchedCallback(data);
|
||||
fetchedCallback = null;
|
||||
} else {
|
||||
fetched = data;
|
||||
}
|
||||
}, handleError);
|
||||
|
||||
function runWithFS() {
|
||||
|
||||
function assert(check, msg) {
|
||||
if (!check) throw msg + new Error().stack;
|
||||
}
|
||||
|
||||
function DataRequest(start, end, crunched, audio) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.crunched = crunched;
|
||||
this.audio = audio;
|
||||
}
|
||||
DataRequest.prototype = {
|
||||
requests: {},
|
||||
open: function(mode, name) {
|
||||
this.name = name;
|
||||
this.requests[name] = this;
|
||||
Module['addRunDependency']('fp ' + this.name);
|
||||
},
|
||||
send: function() {},
|
||||
onload: function() {
|
||||
var byteArray = this.byteArray.subarray(this.start, this.end);
|
||||
|
||||
this.finish(byteArray);
|
||||
|
||||
},
|
||||
finish: function(byteArray) {
|
||||
var that = this;
|
||||
Module['FS_createPreloadedFile'](this.name, null, byteArray, true, true, function() {
|
||||
Module['removeRunDependency']('fp ' + that.name);
|
||||
}, function() {
|
||||
if (that.audio) {
|
||||
Module['removeRunDependency']('fp ' + that.name); // workaround for chromium bug 124926 (still no audio with this, but at least we don't hang)
|
||||
} else {
|
||||
Module.printErr('Preloading file ' + that.name + ' failed');
|
||||
}
|
||||
}, false, true); // canOwn this data in the filesystem, it is a slide into the heap that will never change
|
||||
this.requests[this.name] = null;
|
||||
},
|
||||
};
|
||||
new DataRequest(0, PACK_FILE_SIZE, 0, 0).open('GET', '/' + PACK_FILE_NAME);
|
||||
|
||||
var PACKAGE_PATH;
|
||||
if (typeof window === 'object') {
|
||||
PACKAGE_PATH = window['encodeURIComponent'](window.location.pathname.toString().substring(0, window.location.pathname.toString().lastIndexOf('/')) + '/');
|
||||
} else {
|
||||
// worker
|
||||
PACKAGE_PATH = encodeURIComponent(location.pathname.toString().substring(0, location.pathname.toString().lastIndexOf('/')) + '/');
|
||||
}
|
||||
var PACKAGE_NAME = PACK_FILE_NAME;
|
||||
var REMOTE_PACKAGE_NAME = PACK_FILE_NAME;
|
||||
var PACKAGE_UUID = 'b39761ce-0348-4959-9b16-302ed8e1592e';
|
||||
|
||||
function processPackageData(arrayBuffer) {
|
||||
Module.finishedDataFileDownloads++;
|
||||
assert(arrayBuffer, 'Loading data file failed.');
|
||||
var byteArray = new Uint8Array(arrayBuffer);
|
||||
var curr;
|
||||
|
||||
// Reuse the bytearray from the XHR as the source for file reads.
|
||||
DataRequest.prototype.byteArray = byteArray;
|
||||
DataRequest.prototype.requests['/' + PACK_FILE_NAME].onload();
|
||||
Module['removeRunDependency']('datafile_datapack');
|
||||
|
||||
};
|
||||
Module['addRunDependency']('datafile_datapack');
|
||||
|
||||
if (!Module.preloadResults) Module.preloadResults = {};
|
||||
|
||||
Module.preloadResults[PACKAGE_NAME] = {fromCache: false};
|
||||
if (fetched) {
|
||||
processPackageData(fetched);
|
||||
fetched = null;
|
||||
} else {
|
||||
fetchedCallback = processPackageData;
|
||||
}
|
||||
|
||||
}
|
||||
if (Module['calledRun']) {
|
||||
runWithFS();
|
||||
} else {
|
||||
if (!Module['preRun']) Module['preRun'] = [];
|
||||
Module["preRun"].push(runWithFS); // FS is not initialized yet, wait for it
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user