feat(web): fill browser window and fix localhost loading
Use a custom pygbag template: canvas uses the full window (gui_divider 1), black page background, and redirect localhost to 127.0.0.1 because the pygbag runtime otherwise fetches pygame from a local :8000 dev proxy and the game never starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -4,9 +4,11 @@ RUN pip install --no-cache-dir pygbag==0.9.3
|
|||||||
COPY main.py ./
|
COPY main.py ./
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
COPY assets/ ./assets/
|
COPY assets/ ./assets/
|
||||||
|
# Full-window canvas template; kept outside /app so it is not packed into the game.
|
||||||
|
COPY web/pygbag.tmpl /pygbag.tmpl
|
||||||
# Do not publish the desktop leaderboard or developer files.
|
# Do not publish the desktop leaderboard or developer files.
|
||||||
RUN printf '[]\n' > src/highscore.json \
|
RUN printf '[]\n' > src/highscore.json \
|
||||||
&& python -m pygbag --build --no_opt --disable-sound-format-error --title "Silent FH" .
|
&& python -m pygbag --build --no_opt --disable-sound-format-error --title "Silent FH" --template /pygbag.tmpl .
|
||||||
|
|
||||||
FROM nginxinc/nginx-unprivileged:1.28-alpine
|
FROM nginxinc/nginx-unprivileged:1.28-alpine
|
||||||
COPY --from=build /app/build/web/ /usr/share/nginx/html/
|
COPY --from=build /app/build/web/ /usr/share/nginx/html/
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ docker build -t silent-fh:web .
|
|||||||
docker run -d --name silent-fh --restart unless-stopped -p 8080:8080 silent-fh:web
|
docker run -d --name silent-fh --restart unless-stopped -p 8080:8080 silent-fh:web
|
||||||
```
|
```
|
||||||
|
|
||||||
Open **http://localhost:8080**. Allow the initial runtime download to finish,
|
Open **http://localhost:8080** (it redirects to `127.0.0.1`, because the
|
||||||
|
Pygbag runtime treats `localhost` as its dev server). The game scales to fill
|
||||||
|
the browser window while keeping its aspect ratio. Allow the initial runtime download to finish,
|
||||||
click the game/name field, enter a name, and press Enter. A desktop browser
|
click the game/name field, enter a name, and press Enter. A desktop browser
|
||||||
with a keyboard is recommended; touch controls are not implemented.
|
with a keyboard is recommended; touch controls are not implemented.
|
||||||
Reload the page to start again after quitting the game.
|
Reload the page to start again after quitting the game.
|
||||||
|
|||||||
+443
@@ -0,0 +1,443 @@
|
|||||||
|
<html lang="en-us"><script src="{{cookiecutter.cdn}}pythons.js" type=module id="site" data-python="python{{cookiecutter.PYBUILD}}" data-LINES=42 data-COLUMNS=132 data-os="vtx,snd,gui" async defer>#<!--
|
||||||
|
|
||||||
|
print("""
|
||||||
|
Loading {{cookiecutter.title}} from {{cookiecutter.archive}}.apk
|
||||||
|
Pygbag Version : {{cookiecutter.version}}
|
||||||
|
Template Version : 0.9.3
|
||||||
|
Python : {{cookiecutter.PYBUILD}}
|
||||||
|
CDN URL : {{cookiecutter.cdn}}
|
||||||
|
Screen : {{cookiecutter.width}}x{{cookiecutter.height}}
|
||||||
|
Title : {{cookiecutter.title}}
|
||||||
|
Folder : {{cookiecutter.directory}}
|
||||||
|
Authors : {{cookiecutter.authors}}
|
||||||
|
SPDX-License-Identifier: {{cookiecutter.spdx}}
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
import platform
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# do not rename
|
||||||
|
async def custom_site():
|
||||||
|
import embed
|
||||||
|
platform.document.body.style.background = "#000"
|
||||||
|
|
||||||
|
platform.window.transfer.hidden = true
|
||||||
|
platform.window.canvas.style.visibility = "visible"
|
||||||
|
|
||||||
|
|
||||||
|
bundle = "{{cookiecutter.archive}}"
|
||||||
|
|
||||||
|
# the C or js loader could do that but be explicit.
|
||||||
|
appdir = Path(f"/data/data/{bundle}") # /data/data/{{cookiecutter.archive}}
|
||||||
|
appdir.mkdir()
|
||||||
|
|
||||||
|
|
||||||
|
# unpack filesystem from compressed archive into work dir
|
||||||
|
if platform.window.location.host.find('.itch.zone')>0:
|
||||||
|
import zipfile
|
||||||
|
async with platform.fopen("{{cookiecutter.archive}}.apk", "rb") as archive:
|
||||||
|
with zipfile.ZipFile(archive) as zip_ref:
|
||||||
|
zip_ref.extractall(appdir.as_posix())
|
||||||
|
else:
|
||||||
|
import tarfile
|
||||||
|
async with platform.fopen("{{cookiecutter.archive}}.tar.gz", "rb") as archive:
|
||||||
|
tar = tarfile.open(fileobj=archive, mode="r:gz")
|
||||||
|
tar.extractall(path=appdir.as_posix(), filter='tar')
|
||||||
|
tar.close()
|
||||||
|
|
||||||
|
# preloader will change to work dir and prepend it to sys.path
|
||||||
|
platform.run_main(PyConfig, loaderhome= appdir / "assets", loadermain=None)
|
||||||
|
|
||||||
|
|
||||||
|
# wait preloading complete : that includes images and wasm compilation of bundled modules
|
||||||
|
while embed.counter()<0:
|
||||||
|
await asyncio.sleep(.1)
|
||||||
|
|
||||||
|
main = appdir / "assets" / "main.py"
|
||||||
|
|
||||||
|
# TODO: test for window.webkitAudioContext and block aio loop until gesture if accessing media manager play
|
||||||
|
# before a gesture is recorded ( touch or click )
|
||||||
|
|
||||||
|
# test/wait user media interaction
|
||||||
|
if not platform.window.MM.UME:
|
||||||
|
|
||||||
|
# now that apk is mounted we have access to font cache
|
||||||
|
# but we need to fill __file__ that is not yet set
|
||||||
|
__import__(__name__).__file__ = main
|
||||||
|
|
||||||
|
# now make a prompt
|
||||||
|
msg = "Ready to start ! Please click/touch page"
|
||||||
|
platform.window.infobox.innerText = msg
|
||||||
|
print("\n"*4, f" * Waiting for media user engagement. {msg} *" , "\n"*4)
|
||||||
|
|
||||||
|
while not platform.window.MM.UME:
|
||||||
|
await asyncio.sleep(.1)
|
||||||
|
|
||||||
|
# start async top level machinery if not started and add a console in any case if requested.
|
||||||
|
await TopLevel_async_handler.start_toplevel(platform.shell, console=window.python.config.debug)
|
||||||
|
|
||||||
|
# now that apk is mounted we have access to font cache
|
||||||
|
# but we need to fill __file__/__name__ that are not yet set
|
||||||
|
__import__(__name__).__file__ = main
|
||||||
|
|
||||||
|
|
||||||
|
def ui_callback(pkg):
|
||||||
|
platform.window.infobox.innerText = f"installing {pkg}"
|
||||||
|
|
||||||
|
await shell.source(main, callback=ui_callback)
|
||||||
|
|
||||||
|
# if you don't reach that step
|
||||||
|
# your main.py has an infinite sync loop somewhere !
|
||||||
|
|
||||||
|
platform.window.infobox.style.display = "none"
|
||||||
|
platform.window.config.gui_divider = 1
|
||||||
|
platform.window.window_resize()
|
||||||
|
print("default.tmpl: done")
|
||||||
|
|
||||||
|
shell.interactive()
|
||||||
|
|
||||||
|
asyncio.run( custom_site() )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# BEGIN BLOCK
|
||||||
|
#
|
||||||
|
# now this is the html part you can (and should) customize
|
||||||
|
# It is not mandatory : pygame-script when it reads the first line (also called
|
||||||
|
# shebang ) of above code create absolute minimal widget set
|
||||||
|
# required for running with default rules
|
||||||
|
#
|
||||||
|
# do not alter that comment block it is separating python code from html code
|
||||||
|
# =============================================================================
|
||||||
|
# --></script><head><!--
|
||||||
|
//=============================================================================
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
{%- if cookiecutter.comment != "" -%}
|
||||||
|
{{cookiecutter.comment}}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
--><script type="application/javascript">
|
||||||
|
// END BLOCK
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// this dict is available under PyConfig.config from __main__
|
||||||
|
|
||||||
|
config = {
|
||||||
|
xtermjs : "{{cookiecutter.xtermjs}}" ,
|
||||||
|
_sdl2 : "canvas",
|
||||||
|
user_canvas : 0,
|
||||||
|
user_canvas_managed : 0,
|
||||||
|
gui_divider : 1,
|
||||||
|
ume_block : {{cookiecutter.ume_block}},
|
||||||
|
can_close : {{cookiecutter.can_close}},
|
||||||
|
archive : "{{cookiecutter.archive}}",
|
||||||
|
gui_debug : 2,
|
||||||
|
cdn : "{{cookiecutter.cdn}}",
|
||||||
|
autorun : {{cookiecutter.autorun}},
|
||||||
|
PYBUILD : "{{cookiecutter.PYBUILD}}",
|
||||||
|
fb_ar : 1.77,
|
||||||
|
fb_width : "{{cookiecutter.width}}",
|
||||||
|
fb_height : "{{cookiecutter.height}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_infobox() {
|
||||||
|
infobox.style.display = "block";
|
||||||
|
|
||||||
|
// Measure box
|
||||||
|
const w = infobox.offsetWidth;
|
||||||
|
const h = infobox.offsetHeight;
|
||||||
|
|
||||||
|
// Center in viewport
|
||||||
|
const left = (window.innerWidth - w) / 2;
|
||||||
|
const top = (window.innerHeight - h) / 2;
|
||||||
|
|
||||||
|
infobox.style.left = left + "px";
|
||||||
|
infobox.style.top = top + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<title>{{cookiecutter.title}}</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="viewport" content="height=device-height, initial-scale=1.0">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png" sizes="16x16">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#status {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-left: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgb(120, 120, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress {
|
||||||
|
height: 20px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#infobox {
|
||||||
|
position: fixed; /* center relative to viewport */
|
||||||
|
background: green;
|
||||||
|
color: blue;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 12px 24px;
|
||||||
|
/* display: none; */
|
||||||
|
z-index: 999999;
|
||||||
|
}
|
||||||
|
div.emscripten { text-align: center; }
|
||||||
|
div.emscripten_border { border: 1px solid black; }
|
||||||
|
div.thick_border { border: 4px solid black; }
|
||||||
|
|
||||||
|
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
|
||||||
|
/* average size of droid screen 470dp x 320dp */
|
||||||
|
canvas.emscripten {
|
||||||
|
border: 0px none;
|
||||||
|
background-color: transparent;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 5;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: arial;
|
||||||
|
margin: 0;
|
||||||
|
padding: none;
|
||||||
|
background-color: #000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topright{
|
||||||
|
position:absolute;
|
||||||
|
top:0px;
|
||||||
|
right:0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomright {
|
||||||
|
position:absolute;
|
||||||
|
top: 40%;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trinfo{
|
||||||
|
position: relative;
|
||||||
|
right: 0px;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framed{
|
||||||
|
position: relative;
|
||||||
|
top: 150px;
|
||||||
|
right: 10px;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// On "localhost" the pygbag runtime expects its dev proxy on :8000 and
|
||||||
|
// fails to fetch pygame. 127.0.0.1 uses the public CDN instead.
|
||||||
|
if (location.hostname === "localhost")
|
||||||
|
location.replace(location.href.replace("//localhost", "//127.0.0.1"))
|
||||||
|
/*
|
||||||
|
if (navigator.serviceWorker)
|
||||||
|
navigator.serviceWorker.register("{{cookiecutter.cdn}}pygbag{{cookiecutter.version}}.js")
|
||||||
|
else
|
||||||
|
console.warn("Service workers not supported")
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="{{cookiecutter.cdn}}/browserfs.min.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="transfer" align=center>
|
||||||
|
<!-- <div class="spinner" id='spinner'></div> -->
|
||||||
|
<div class="emscripten" id="status">Downloading...</div>
|
||||||
|
<div class="emscripten">
|
||||||
|
<progress value="0" max="100" id="progress"></progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<canvas class="emscripten" id="canvas"
|
||||||
|
width="1px"
|
||||||
|
height="1px"
|
||||||
|
oncontextmenu="event.preventDefault()" tabindex=1>
|
||||||
|
</canvas>
|
||||||
|
|
||||||
|
<canvas class="emscripten" id="canvas3d"
|
||||||
|
width="{{cookiecutter.width}}px"
|
||||||
|
height="{{cookiecutter.height}}px"
|
||||||
|
oncontextmenu="event.preventDefault()" tabindex=1 hidden>
|
||||||
|
</canvas>
|
||||||
|
|
||||||
|
<div id="infobox">Loading, please wait ...</div>
|
||||||
|
|
||||||
|
<div id=html></div>
|
||||||
|
|
||||||
|
<div id=crt class=bottomright >
|
||||||
|
|
||||||
|
<div id="system" hidden>
|
||||||
|
<div class="button-container">
|
||||||
|
<button id="aiostop" disabled>AIO ⏏︎</button>
|
||||||
|
<button id="aiopaused_true" disabled>AIO ■</button>
|
||||||
|
<button id="aiopaused_false" disabled>AIO ▶</button>
|
||||||
|
<button id="pygame_mixer_music_pause" disabled>Music ■</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-container">
|
||||||
|
<div id=load_min>min</div>
|
||||||
|
<div id=load_avg>avg</div>
|
||||||
|
<div id=load_max>max</div>
|
||||||
|
<button id="load_rst" disabled>RESET</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="level">(battery level unknown)</div>
|
||||||
|
<div id="stateBattery">(charging state unknown)</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id=box class="emscripten_border" hidden=true>
|
||||||
|
|
||||||
|
<div id="info" class="trinfo"></div>
|
||||||
|
|
||||||
|
<iframe id="iframe" class="framed" name="iframe"
|
||||||
|
width="470px" height="90%"
|
||||||
|
allowtransparency="true"
|
||||||
|
style="z-index: 10;"
|
||||||
|
style="background: #FFFFFF;"
|
||||||
|
frameborder="1"
|
||||||
|
allowfullscreen="true"
|
||||||
|
webkitallowfullscreen="true"
|
||||||
|
msallowfullscreen="true"
|
||||||
|
mozallowfullscreen="true"
|
||||||
|
sandbox="allow-same-origin allow-top-navigation allow-scripts allow-pointer-lock"
|
||||||
|
allow="autoplay; fullscreen *; geolocation; microphone; camera; midi; monetization; xr-spatial-tracking; gamepad; gyroscope; accelerometer; xr; cross-origin-isolated"
|
||||||
|
src="{{cookiecutter.cdn}}empty.html"
|
||||||
|
scrolling="yes">
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="dlg" hidden>
|
||||||
|
<input type="file" id="dlg_multifile" multiple accept="image/*">
|
||||||
|
<label for="dlg_multifile">Select files</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pyconsole">
|
||||||
|
<div id="terminal" tabIndex=1 align="left"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
|
||||||
|
globalThis.__canvas_resized = (self, ecw, ech) => {
|
||||||
|
console.warn("TODO: panda3d canvas monitor", self, ecw, ech)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function custom_onload(debug_hidden) {
|
||||||
|
// this is called before anything python is loaded
|
||||||
|
// make your js customization here
|
||||||
|
console.log(__FILE__, "custom_onload")
|
||||||
|
|
||||||
|
pyconsole.hidden = debug_hidden
|
||||||
|
system.hidden = debug_hidden
|
||||||
|
transfer.hidden = debug_hidden
|
||||||
|
info.hidden = debug_hidden
|
||||||
|
box.hidden = debug_hidden
|
||||||
|
|
||||||
|
show_infobox()
|
||||||
|
}
|
||||||
|
|
||||||
|
function custom_prerun(){
|
||||||
|
// no python main and no (MEMFS + VFS) yet.
|
||||||
|
console.log(__FILE__, "custom_prerun")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function custom_postrun(){
|
||||||
|
// python main and no VFS filesystem yet.
|
||||||
|
console.log(__FILE__, "custom_postrun")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function debug() {
|
||||||
|
// allow to gain access to dev tools from js console
|
||||||
|
// but only on desktop. difficult to reach when in iframe
|
||||||
|
python.config.debug = true
|
||||||
|
custom_onload(false)
|
||||||
|
Module.PyRun_SimpleString("shell.uptime()")
|
||||||
|
window_resize()
|
||||||
|
}
|
||||||
|
|
||||||
|
function info_inline(data){
|
||||||
|
document.getElementById("info").innerHTML = data
|
||||||
|
}
|
||||||
|
|
||||||
|
function info_online(url) {
|
||||||
|
// display info about current APK
|
||||||
|
fetch( url /*, options */)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((html) => {
|
||||||
|
info_inline(html);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function frame_online(url) {
|
||||||
|
window.frames["iframe"].location = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user