Add branded web loading screen

This commit is contained in:
hz
2026-07-16 19:00:46 -05:00
parent 6d1787b3eb
commit 0f5a1122d4
6 changed files with 477 additions and 1 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://boml17400d7mo"
path="res://.godot/imported/medmind-loader-mark.png-35f1239f43a45c2dbbe197762d84e98a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/branding/medmind-loader-mark.png"
dest_files=["res://.godot/imported/medmind-loader-mark.png-35f1239f43a45c2dbbe197762d84e98a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+1 -1
View File
@@ -21,7 +21,7 @@ variant/thread_support=false
vram_texture_compression/for_desktop=true vram_texture_compression/for_desktop=true
vram_texture_compression/for_mobile=true vram_texture_compression/for_mobile=true
html/export_icon=true html/export_icon=true
html/custom_html_shell="" html/custom_html_shell="res://web/medmind_shell.html"
html/head_include="<meta name=\"description\" content=\"MedMind pre-alpha - Think like a physician.\">" html/head_include="<meta name=\"description\" content=\"MedMind pre-alpha - Think like a physician.\">"
html/canvas_resize_policy=2 html/canvas_resize_policy=2
html/focus_canvas_on_start=true html/focus_canvas_on_start=true
+13
View File
@@ -13,6 +13,7 @@ var _is_dark := true
func _enter_tree() -> void: func _enter_tree() -> void:
_is_dark = _load_preference() _is_dark = _load_preference()
Style.set_dark_mode(_is_dark) Style.set_dark_mode(_is_dark)
_sync_web_preference()
func is_dark_mode() -> bool: func is_dark_mode() -> bool:
@@ -25,6 +26,7 @@ func set_dark_mode(value: bool) -> void:
_is_dark = value _is_dark = value
Style.set_dark_mode(_is_dark) Style.set_dark_mode(_is_dark)
_save_preference() _save_preference()
_sync_web_preference()
theme_changed.emit(_is_dark) theme_changed.emit(_is_dark)
@@ -59,3 +61,14 @@ func _save_preference() -> void:
config.set_value(SETTINGS_SECTION, SETTINGS_KEY, "dark" if _is_dark else "light") config.set_value(SETTINGS_SECTION, SETTINGS_KEY, "dark" if _is_dark else "light")
if config.save(SETTINGS_PATH) != OK: if config.save(SETTINGS_PATH) != OK:
push_warning("Unable to save color scheme preference.") push_warning("Unable to save color scheme preference.")
func _sync_web_preference() -> void:
if not OS.has_feature("web"):
return
JavaScriptBridge.eval(
"try { window.localStorage.setItem('medmind-color-scheme', %s); } catch (error) { console.warn('Theme preference storage is unavailable.', error); }" % JSON.stringify(
"dark" if _is_dark else "light"
),
true
)
+4
View File
@@ -33,7 +33,11 @@ RUN mkdir -p /build \
&& sed -i "s|custom_template/release=\"\"|custom_template/release=\"/root/.local/share/godot/export_templates/${GODOT_TEMPLATE_DIR}/web_nothreads_release.zip\"|" export_presets.cfg \ && sed -i "s|custom_template/release=\"\"|custom_template/release=\"/root/.local/share/godot/export_templates/${GODOT_TEMPLATE_DIR}/web_nothreads_release.zip\"|" export_presets.cfg \
&& godot --headless --editor --path /src --quit \ && godot --headless --editor --path /src --quit \
&& godot --verbose --headless --path /src --export-release Web /build/index.html \ && godot --verbose --headless --path /src --export-release Web /build/index.html \
&& cp /src/assets/branding/medmind-loader-mark.png /build/medmind-loader-icon.png \
&& cp /src/assets/fonts/Inter-Variable.ttf /build/medmind-inter.ttf \
&& test -s /build/index.html \ && test -s /build/index.html \
&& test -s /build/medmind-loader-icon.png \
&& test -s /build/medmind-inter.ttf \
&& test -s /build/index.wasm \ && test -s /build/index.wasm \
&& test -s /build/index.pck && test -s /build/index.pck
+419
View File
@@ -0,0 +1,419 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, viewport-fit=cover">
<title>$GODOT_PROJECT_NAME</title>
<script>
(function () {
let savedTheme = null;
try {
savedTheme = window.localStorage.getItem('medmind-color-scheme');
} catch (error) {
console.warn('Theme preference storage is unavailable.', error);
}
const systemTheme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
document.documentElement.dataset.theme = savedTheme === 'dark' || savedTheme === 'light'
? savedTheme
: systemTheme;
}());
</script>
<style>
@font-face {
font-family: "Inter";
src: url("medmind-inter.ttf") format("truetype");
font-style: normal;
font-weight: 100 900;
font-display: swap;
}
:root {
color-scheme: light;
--background: #f8faff;
--background-accent: rgba(124, 58, 237, 0.12);
--background-accent-secondary: rgba(37, 99, 235, 0.08);
--dot: rgba(99, 102, 241, 0.26);
--brand-dark: #0b1324;
--muted: #64748b;
--track: #e8eaf7;
--error-background: rgba(255, 255, 255, 0.94);
--error-border: #ef4444;
--error-text: #991b1b;
}
:root[data-theme="dark"] {
color-scheme: dark;
--background: #020b1d;
--background-accent: rgba(124, 58, 237, 0.3);
--background-accent-secondary: rgba(37, 99, 235, 0.18);
--dot: rgba(61, 91, 210, 0.68);
--brand-dark: #f8fafc;
--muted: #94a3b8;
--track: #111c38;
--error-background: rgba(17, 27, 48, 0.96);
--error-border: #fb7185;
--error-text: #fecdd3;
}
html, body, #canvas {
border: 0;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
}
body {
background: var(--background);
color: var(--brand-dark);
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
overflow: hidden;
touch-action: none;
}
#canvas {
display: block;
}
#canvas:focus {
outline: none;
}
#status {
align-items: center;
background:
radial-gradient(circle at 0% 100%, var(--background-accent-secondary) 0, transparent 31%),
radial-gradient(circle at 100% 0%, var(--background-accent) 0, transparent 32%),
var(--background);
display: flex;
inset: 0;
justify-content: center;
min-height: 100%;
position: fixed;
transition: opacity 180ms ease;
visibility: hidden;
z-index: 10;
}
#status::before,
#status::after {
background-image: radial-gradient(circle, var(--dot) 0 3px, transparent 3.5px);
background-size: 27px 27px;
content: "";
height: 150px;
position: absolute;
width: 180px;
z-index: 1;
}
#status::before {
left: clamp(24px, 3vw, 64px);
top: clamp(22px, 5vh, 64px);
}
#status::after {
bottom: clamp(22px, 5vh, 64px);
right: clamp(24px, 3vw, 64px);
}
.loader {
align-items: center;
display: flex;
flex-direction: column;
padding: 32px;
position: relative;
width: min(640px, calc(100vw - 48px));
z-index: 2;
}
.corner-accent {
border-radius: 50%;
height: min(42vw, 430px);
pointer-events: none;
position: absolute;
width: min(42vw, 430px);
z-index: 0;
}
.corner-accent--top {
background: radial-gradient(circle at 35% 65%, rgba(124, 58, 237, 0.04), rgba(99, 102, 241, 0.13));
right: max(-150px, -10vw);
top: max(-180px, -13vw);
}
.corner-accent--bottom {
background: radial-gradient(circle at 65% 35%, rgba(124, 58, 237, 0.02), rgba(99, 102, 241, 0.1));
bottom: max(-210px, -16vw);
left: max(-180px, -13vw);
}
:root[data-theme="dark"] .corner-accent--top {
background: radial-gradient(circle at 30% 70%, rgba(124, 58, 237, 0.18), rgba(49, 46, 129, 0.72));
}
:root[data-theme="dark"] .corner-accent--bottom {
background: radial-gradient(circle at 70% 30%, rgba(37, 99, 235, 0.1), rgba(30, 58, 138, 0.55));
}
.loader-logo-wrap {
height: clamp(112px, 16vw, 154px);
position: relative;
width: clamp(112px, 16vw, 154px);
}
.loader-logo {
display: block;
height: 100%;
object-fit: contain;
width: 100%;
}
.loader-logo-wrap::before,
.loader-logo-wrap::after {
background: #3154c4;
border-radius: 2px;
content: "";
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.loader-logo-wrap::before {
height: 8%;
width: 19%;
}
.loader-logo-wrap::after {
height: 19%;
width: 8%;
}
:root[data-theme="dark"] .loader-logo-wrap::before,
:root[data-theme="dark"] .loader-logo-wrap::after {
background: #ffffff;
}
.loader-wordmark {
font-size: clamp(52px, 7vw, 78px);
font-weight: 650;
letter-spacing: 0;
line-height: 1;
margin: 12px 0 0;
}
.wordmark-med {
color: var(--brand-dark);
}
.wordmark-mind {
background: linear-gradient(90deg, #2563eb 0%, #7c3aed 58%, #d946ef 100%);
background-clip: text;
color: transparent;
-webkit-background-clip: text;
}
.loader-tagline {
color: var(--muted);
font-size: clamp(20px, 2.3vw, 28px);
font-weight: 430;
letter-spacing: 0;
margin: 17px 0 clamp(36px, 6vh, 54px);
}
.progress-row {
align-items: center;
display: flex;
gap: 18px;
width: 100%;
}
.progress-track {
background: var(--track);
border-radius: 999px;
flex: 1;
height: 16px;
overflow: hidden;
}
.progress-value {
background: linear-gradient(90deg, #2563eb 0%, #7c3aed 60%, #a855f7 100%);
border-radius: inherit;
height: 100%;
transform-origin: left center;
transition: width 120ms ease-out;
width: 0%;
}
.progress-percent {
color: #3563f4;
font-size: 22px;
font-variant-numeric: tabular-nums;
font-weight: 650;
min-width: 60px;
}
.loader-message {
color: var(--muted);
font-size: 19px;
letter-spacing: 0;
margin: 24px 0 0;
text-align: center;
}
#status-notice {
background: var(--error-background);
border: 1px solid var(--error-border);
border-radius: 8px;
color: var(--error-text);
display: none;
font-size: 16px;
line-height: 1.5;
margin-top: 28px;
max-height: min(240px, 35vh);
overflow: auto;
padding: 16px 20px;
text-align: center;
width: min(540px, calc(100vw - 80px));
}
@media (max-width: 600px) {
#status::before,
#status::after {
background-size: 20px 20px;
height: 100px;
opacity: 0.72;
width: 120px;
}
.loader {
padding: 24px;
width: min(430px, calc(100vw - 32px));
}
.loader-wordmark {
font-size: clamp(46px, 15vw, 66px);
}
.progress-row {
gap: 12px;
}
.progress-track {
height: 13px;
}
.progress-percent {
font-size: 18px;
min-width: 48px;
}
}
@media (prefers-reduced-motion: reduce) {
#status,
.progress-value {
transition: none;
}
}
</style>
$GODOT_HEAD_INCLUDE
</head>
<body>
<canvas id="canvas">
Your browser does not support the canvas tag.
</canvas>
<noscript>Your browser does not support JavaScript.</noscript>
<div id="status" role="status" aria-live="polite">
<div class="corner-accent corner-accent--top"></div>
<div class="corner-accent corner-accent--bottom"></div>
<div class="loader">
<div class="loader-logo-wrap">
<img class="loader-logo" src="medmind-loader-icon.png" alt="MedMind">
</div>
<div class="loader-wordmark" aria-label="MedMind">
<span class="wordmark-med">Med</span><span class="wordmark-mind">Mind</span>
</div>
<p class="loader-tagline">Think like a physician.</p>
<div id="status-progress" class="progress-row" role="progressbar" aria-label="Loading MedMind" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div class="progress-track"><div id="status-progress-value" class="progress-value"></div></div>
<span id="status-progress-percent" class="progress-percent">0%</span>
</div>
<p id="status-message" class="loader-message">Loading your study experience...</p>
<div id="status-notice" role="alert"></div>
</div>
</div>
<script src="$GODOT_URL"></script>
<script>
const GODOT_CONFIG = $GODOT_CONFIG;
const engine = new Engine(GODOT_CONFIG);
(function () {
const statusOverlay = document.getElementById('status');
const statusProgress = document.getElementById('status-progress');
const progressValue = document.getElementById('status-progress-value');
const progressPercent = document.getElementById('status-progress-percent');
const statusMessage = document.getElementById('status-message');
const statusNotice = document.getElementById('status-notice');
let initializing = true;
function showLoader() {
statusOverlay.style.visibility = 'visible';
}
function updateProgress(current, total) {
const percentage = current > 0 && total > 0
? Math.max(0, Math.min(100, Math.round((current / total) * 100)))
: 0;
progressValue.style.width = percentage + '%';
progressPercent.textContent = percentage + '%';
statusProgress.setAttribute('aria-valuenow', String(percentage));
}
function displayFailureNotice(error) {
console.error(error);
const message = error instanceof Error
? error.message
: (typeof error === 'string' ? error : 'An unknown error occurred.');
statusProgress.style.display = 'none';
statusMessage.textContent = 'MedMind could not finish loading.';
statusNotice.textContent = message;
statusNotice.style.display = 'block';
initializing = false;
}
showLoader();
const missing = Engine.getMissingFeatures({ threads: false });
if (missing.length !== 0) {
displayFailureNotice('Required browser features are unavailable: ' + missing.join(', '));
return;
}
engine.startGame({
onProgress: updateProgress,
}).then(function () {
if (!initializing) {
return;
}
statusOverlay.style.opacity = '0';
statusOverlay.addEventListener('transitionend', function () {
statusOverlay.remove();
}, { once: true });
window.setTimeout(function () {
if (statusOverlay.isConnected) {
statusOverlay.remove();
}
}, 250);
initializing = false;
}, displayFailureNotice);
}());
</script>
</body>
</html>