emscripten: redesign shell, always use whole window for game canvas

This commit is contained in:
Andrei Alexeyev 2024-10-24 01:08:32 +02:00
parent e3ff572d90
commit 021584a83b
No known key found for this signature in database
GPG key ID: 72D26128040B9690
4 changed files with 156 additions and 80 deletions

5
emscripten/postamble.js Normal file
View file

@ -0,0 +1,5 @@
ENV["TAISEI_NOASYNC"] = "1";
ENV["TAISEI_NOUNLOAD"] = "1";
ENV["TAISEI_PREFER_SDL_VIDEODRIVERS"] = "emscripten";
ENV["TAISEI_RENDERER"] = "gles30";

View file

@ -47,6 +47,7 @@
color: #eeeeee; color: #eeeeee;
overflow: overlay; overflow: overlay;
margin: 0px; margin: 0px;
text-shadow: 2px 2px black;
} }
div.emscripten { div.emscripten {
@ -62,32 +63,67 @@
} }
div.header { div.header {
padding: 0px 16px; padding: 10vh 24px;
text-align: center; text-align: center;
} }
div.footer { div.footer {
text-align: center; text-align: center;
position: absolute; position: absolute;
bottom: 16px; bottom: 0;
padding-bottom: 16px;
width: 100%; width: 100%;
transition: all 0.25s ease-in-out;
} }
div.centered { div.footer.backdrop {
position: absolute; background-color: #000000C0;
bottom: 50%; }
div.footer.hidden {
opacity: 0.0;
background-color: #00000000;
}
div.footer.hidden:hover {
opacity: 1.0;
background-color: #000000C0;
}
div#infoContainer {
position: fixed;
top: 50%;
left: 0; left: 0;
right: 0; right: 0;
transform: translateY(50%); transform: translateY(-50%);
background-color: #000000C0; background-color: #000000C0;
padding: 16px 0px; padding: 16px 0px;
z-index: 1; z-index: 1;
transition: all 0.25s ease-in-out;
}
div#infoContainer.top {
top: 0%;
transform: translateY(0%);
background-color: #00000000;
opacity: 0.5;
}
div#infoContainer.top:hover {
top: 0%;
transform: translateY(0%);
background-color: #000000C0;
opacity: 1.0;
} }
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */ /* the canvas *must not* have any border or padding, or mouse coords will be wrong */
canvas.emscripten { canvas.emscripten {
border: 0px none; border: 0px none;
background-color: none; background-color: none;
position: fixed;
display: block;
width: 100vw;
height: 100vh;
} }
#spinner { #spinner {
@ -105,10 +141,9 @@
} }
#logToggleContainer { #logToggleContainer {
display: /* inline-block */ none; display: none;
position: relative; position: relative;
left: 50%; transform: translateX(25%);
transform: translateX(-50%);
opacity: 0.5; opacity: 0.5;
} }
@ -184,18 +219,11 @@
</style> </style>
</head> </head>
<body> <body>
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1 style="z-index: -10;"></canvas>
<div class="header"> <div class="header">
<h3>Note: this web port is experimental and may not perform as well as the original game, which you can&nbsp;<a href="https://taisei-project.org/download">download here</a>.</h3> <h3 id="warning">WARNING: This game is not designed to run in a browser.<br/><a href="https://taisei-project.org/download">Download</a> the desktop version for a smoother experience.</h3>
</div> </div>
<div class="centered"> <div id="infoContainer">
<div class="spinner" id="spinner"><img src="scythe.webp" class="spinner"/></div>
<div class="emscripten" id="status">Girls are now downloading, please wait warmly…</div>
<div class="emscripten">
<progress value="0" max="100" id="progress" ></progress>
</div>
<div class="emscripten_border" id="canvasContainer" hidden>
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
</div>
<div id="logToggleContainer" hidden> <div id="logToggleContainer" hidden>
<input type="checkbox" name="logToggle" id="logToggle" onclick="toggleLog()"/> <input type="checkbox" name="logToggle" id="logToggle" onclick="toggleLog()"/>
<label for="logToggle"> Show log</label> <label for="logToggle"> Show log</label>
@ -203,13 +231,18 @@
<div id="logContainer" hidden> <div id="logContainer" hidden>
<textarea class="emscripten" id="output" rows="12" readonly></textarea> <textarea class="emscripten" id="output" rows="12" readonly></textarea>
</div> </div>
<div class="spinner" id="spinner"><img src="scythe.webp" class="spinner"/></div>
<div class="emscripten" id="status">Girls are now downloading, please wait warmly…</div>
<div class="emscripten">
<progress value="0" max="100" id="progress" ></progress>
</div>
</div> </div>
<div class="footer"> <div id="footer" class="footer">
<a href="https://taisei-project.org/">Taisei Project</a>&nbsp;·&nbsp; <a href="https://taisei-project.org/">Taisei Project</a>&nbsp;·&nbsp;
<a href="https://taisei-project.org/code">Source code</a>&nbsp;·&nbsp; <a href="https://taisei-project.org/code">Source code</a>&nbsp;·&nbsp;
<a href="https://taisei-project.org/discord">Discord</a> <a href="https://taisei-project.org/discord">Discord</a>
<br/> <br/>
Powered by&nbsp;<a href="https://emscripten.org/">Emscripten</a> Web port powered by&nbsp;<a href="https://emscripten.org/">Emscripten</a>
</div> </div>
<!--<script type="text/javascript" src="webgl-debug.js"></script>--> <!--<script type="text/javascript" src="webgl-debug.js"></script>-->
<script type="application/javascript"> <script type="application/javascript">
@ -219,7 +252,8 @@
var progressElement = E('progress'); var progressElement = E('progress');
var spinnerElement = E('spinner'); var spinnerElement = E('spinner');
var canvasElement = E('canvas'); var canvasElement = E('canvas');
var canvasContainerElement = E('canvasContainer'); var infoContainerElement = E('infoContainer');
var footerElement = E('footer');
var logToggleElement = E('logToggle'); var logToggleElement = E('logToggle');
var logToggleContainerElement = E('logToggleContainer'); var logToggleContainerElement = E('logToggleContainer');
var logContainerElement = E('logContainer'); var logContainerElement = E('logContainer');
@ -231,15 +265,15 @@
var setStatus = (() => { var setStatus = (() => {
var ss = {}; var ss = {};
return (text, force) => { return (text, force) => {
if (!text && !force) return; if(!text && !force) return;
if (!ss.last) ss.last = { time: Date.now(), text: '' }; if(!ss.last) ss.last = { time: Date.now(), text: '' };
if (text === ss.last.text) return; if(text === ss.last.text) return;
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/); var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
var now = Date.now(); var now = Date.now();
if (m && now - ss.last.time < 30) return; // if this is a progress update, skip it if too soon if(m && now - ss.last.time < 30) return; // if this is a progress update, skip it if too soon
ss.last.time = now; ss.last.time = now;
ss.last.text = text; ss.last.text = text;
if (m) { if(m) {
text = m[1]; text = m[1];
progressElement.value = parseInt(m[2])*100; progressElement.value = parseInt(m[2])*100;
progressElement.max = parseInt(m[4])*100; progressElement.max = parseInt(m[4])*100;
@ -249,16 +283,14 @@
progressElement.value = null; progressElement.value = null;
progressElement.max = null; progressElement.max = null;
progressElement.hidden = true; progressElement.hidden = true;
if (!text) spinnerElement.hidden = true; if(!text) spinnerElement.hidden = true;
} }
statusElement.innerText = text.replace(/^Downloading(?: data)?\.\.\./, dlMessage).replace('...', '…'); statusElement.innerText = text.replace(/^Downloading(?: data)?\.\.\./, dlMessage).replace('...', '…');
console.log("[STATUS] " + statusElement.innerText); console.log("[STATUS] " + statusElement.innerText);
}; };
})(); })();
window.onerror = function(error) { window.onerror = error => setStatus('Error: ' + error);
setStatus('Error: ' + error);
};
window['toggleLog'] = function toggleLog() { window['toggleLog'] = function toggleLog() {
logContainerElement.hidden = !logToggleElement.checked; logContainerElement.hidden = !logToggleElement.checked;
@ -266,13 +298,13 @@
} }
var glContext = canvasElement.getContext('webgl2', { var glContext = canvasElement.getContext('webgl2', {
'alpha' : false, alpha : false,
'antialias' : false, antialias : false,
'depth' : false, depth : false,
'powerPreference' : 'high-performance', powerPreference : 'high-performance',
'premultipliedAlpha' : true, premultipliedAlpha : true,
'preserveDrawingBuffer' : false, preserveDrawingBuffer : false,
'stencil' : false, stencil : false,
}); });
if(!glContext) { if(!glContext) {
@ -281,36 +313,53 @@
// glContext = WebGLDebugUtils.makeDebugContext(glContext); // glContext = WebGLDebugUtils.makeDebugContext(glContext);
canvasElement.addEventListener("webglcontextlost", function(e) { canvasElement.addEventListener("webglcontextlost", e => {
alert('WebGL context lost. You will need to reload the page.'); alert('WebGL context lost. You will need to reload the page.');
e.preventDefault(); e.preventDefault();
}, false); }, false);
document.addEventListener('keydown', function(event) {
if(event.key === 'F11' || event.key === 'F12') {
event.stopImmediatePropagation();
}
});
window.onresize = function() {
canvasElement.width = canvasElement.offsetWidth;
canvasElement.height = canvasElement.offsetHeight;
};
logOutputElement.value = ''; // clear browser cache logOutputElement.value = ''; // clear browser cache
var taisei; var taisei = {
taiseiScriptElement.addEventListener('load', async() => { canvas: canvasElement,
taisei = await createTaisei({ preinitializedWebGLContext: glContext,
'canvas': canvasElement, onFirstFrame: function() {
'preinitializedWebGLContext': glContext, canvasElement.style.zIndex = "0";
'onFirstFrame': function() { logToggleContainerElement.style.display = "inline-block";
canvasContainerElement.hidden = false; infoContainerElement.classList.add('top');
logToggleContainerElement.style.display = "inline-block"; footerElement.insertBefore(E('warning'), footerElement.childNodes[0]);
setStatus('', true); footerElement.classList.add('backdrop');
}, setTimeout(function() {
'print': function(text) { footerElement.classList.add('hidden');
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); }, 7000);
console.log(text); setStatus('', true);
logOutputElement.value += text + "\n"; },
logOutputElement.scrollTop = logOutputElement.scrollHeight; // focus on bottom print: function(text) {
}, if(arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
'printErr': function(text) { console.log(text);
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); logOutputElement.value += text + "\n";
console.error(text); logOutputElement.scrollTop = logOutputElement.scrollHeight; // focus on bottom
}, },
'setStatus': setStatus, printErr: function(text) {
}); if(arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.error(text);
},
setStatus: setStatus,
};
taiseiScriptElement.addEventListener('load', async () => {
await createTaisei(taisei);
taisei.initFilesystem(); taisei.initFilesystem();
taisei.callMain(); taisei.callMain();
}); });

View file

@ -301,7 +301,7 @@ if host_machine.system() == 'emscripten'
'-s', 'EXIT_RUNTIME=0', '-s', 'EXIT_RUNTIME=0',
'-s', 'EXPORT_NAME=createTaisei', '-s', 'EXPORT_NAME=createTaisei',
'-s', 'EXPORTED_FUNCTIONS=["_main", "_vfs_sync_callback"]', '-s', 'EXPORTED_FUNCTIONS=["_main", "_vfs_sync_callback"]',
'-s', 'EXPORTED_RUNTIME_METHODS=["ccall","callMain"]', '-s', 'EXPORTED_RUNTIME_METHODS=["ccall","callMain","FS","ENV"]',
'-s', 'FETCH_SUPPORT_INDEXEDDB=0', '-s', 'FETCH_SUPPORT_INDEXEDDB=0',
'-s', 'FETCH', '-s', 'FETCH',
'-s', 'FILESYSTEM=1', '-s', 'FILESYSTEM=1',

View file

@ -186,6 +186,12 @@ static VideoCapabilityState video_query_capability_switch(VideoCapability cap) {
static VideoCapabilityState video_query_capability_webcanvas(VideoCapability cap) { static VideoCapabilityState video_query_capability_webcanvas(VideoCapability cap) {
switch(cap) { switch(cap) {
case VIDEO_CAP_EXTERNAL_RESIZE: case VIDEO_CAP_EXTERNAL_RESIZE:
return VIDEO_ALWAYS_ENABLED;
case VIDEO_CAP_FULLSCREEN:
return VIDEO_NEVER_AVAILABLE;
case VIDEO_CAP_CHANGE_RESOLUTION:
return VIDEO_NEVER_AVAILABLE; return VIDEO_NEVER_AVAILABLE;
case VIDEO_CAP_VSYNC_ADAPTIVE: case VIDEO_CAP_VSYNC_ADAPTIVE:
@ -498,12 +504,30 @@ static void video_new_window_internal(uint display, uint w, uint h, uint32_t fla
video_new_window_internal(display, RESX, RESY, flags & ~SDL_WINDOW_FULLSCREEN_DESKTOP, true); video_new_window_internal(display, RESX, RESY, flags & ~SDL_WINDOW_FULLSCREEN_DESKTOP, true);
} }
static bool restrict_to_capability(bool enabled, VideoCapability cap) {
VideoCapabilityState capval = video_query_capability(cap);
switch(capval) {
case VIDEO_ALWAYS_ENABLED:
return true;
case VIDEO_NEVER_AVAILABLE:
return false;
default:
return enabled;
}
}
static void video_new_window(uint display, uint w, uint h, bool fs, bool resizable) { static void video_new_window(uint display, uint w, uint h, bool fs, bool resizable) {
uint32_t flags = SDL_WINDOW_ALLOW_HIGHDPI; uint32_t flags = SDL_WINDOW_ALLOW_HIGHDPI;
if(fs || video_query_capability(VIDEO_CAP_FULLSCREEN) == VIDEO_ALWAYS_ENABLED) { fs = restrict_to_capability(fs, VIDEO_CAP_FULLSCREEN);
resizable = restrict_to_capability(resizable, VIDEO_CAP_EXTERNAL_RESIZE);
if(fs) {
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
} else if(resizable && video.backend != VIDEO_BACKEND_EMSCRIPTEN) { } else if(resizable) {
flags |= SDL_WINDOW_RESIZABLE; flags |= SDL_WINDOW_RESIZABLE;
} }
@ -538,7 +562,7 @@ INLINE bool should_recreate_on_size_change(void) {
/* Resize failures are impossible to detect under some WMs */ /* Resize failures are impossible to detect under some WMs */
video.backend == VIDEO_BACKEND_X11 || video.backend == VIDEO_BACKEND_X11 ||
/* Needed to work around various SDL bugs and/or HTML/DOM quirks */ /* Needed to work around various SDL bugs and/or HTML/DOM quirks */
video.backend == VIDEO_BACKEND_EMSCRIPTEN || // video.backend == VIDEO_BACKEND_EMSCRIPTEN ||
0); 0);
return env_get("TAISEI_VIDEO_RECREATE_ON_RESIZE", defaultval); return env_get("TAISEI_VIDEO_RECREATE_ON_RESIZE", defaultval);
@ -553,20 +577,6 @@ INLINE bool should_recreate_on_fullscreen_change(void) {
return env_get("TAISEI_VIDEO_RECREATE_ON_FULLSCREEN", defaultval); return env_get("TAISEI_VIDEO_RECREATE_ON_FULLSCREEN", defaultval);
} }
static bool restrict_to_capability(bool enabled, VideoCapability cap) {
VideoCapabilityState capval = video_query_capability(cap);
switch(capval) {
case VIDEO_ALWAYS_ENABLED:
return true;
case VIDEO_NEVER_AVAILABLE:
return false;
default:
return enabled;
}
}
void video_set_mode(uint display, uint w, uint h, bool fs, bool resizable) { void video_set_mode(uint display, uint w, uint h, bool fs, bool resizable) {
fs = restrict_to_capability(fs, VIDEO_CAP_FULLSCREEN); fs = restrict_to_capability(fs, VIDEO_CAP_FULLSCREEN);
@ -585,6 +595,14 @@ void video_set_mode(uint display, uint w, uint h, bool fs, bool resizable) {
return; return;
} }
if(
!restrict_to_capability(true, VIDEO_CAP_CHANGE_RESOLUTION) &&
video.current.width > 0 && video.current.height > 0
) {
w = video.current.width;
h = video.current.height;
}
bool display_changed = display != video_current_display(); bool display_changed = display != video_current_display();
bool size_changed = w != video.current.width || h != video.current.height; bool size_changed = w != video.current.width || h != video.current.height;
bool fullscreen_changed = video_is_fullscreen() != fs; bool fullscreen_changed = video_is_fullscreen() != fs;
@ -599,7 +617,7 @@ void video_set_mode(uint display, uint w, uint h, bool fs, bool resizable) {
return; return;
} }
if(size_changed) { if(size_changed && !fs) {
if(!fullscreen_changed && should_recreate_on_size_change()) { if(!fullscreen_changed && should_recreate_on_size_change()) {
video_new_window(display, w, h, fs, resizable); video_new_window(display, w, h, fs, resizable);
return; return;
@ -626,6 +644,10 @@ SDL_Window *video_get_window(void) {
} }
void video_set_fullscreen(bool fullscreen) { void video_set_fullscreen(bool fullscreen) {
if(video_query_capability(VIDEO_CAP_FULLSCREEN) != VIDEO_AVAILABLE) {
return;
}
video_set_mode( video_set_mode(
SDL_GetWindowDisplayIndex(video.window), SDL_GetWindowDisplayIndex(video.window),
video.intended.width, video.intended.width,