Add branded web loading screen
This commit is contained in:
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
@@ -21,7 +21,7 @@ variant/thread_support=false
|
||||
vram_texture_compression/for_desktop=true
|
||||
vram_texture_compression/for_mobile=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/canvas_resize_policy=2
|
||||
html/focus_canvas_on_start=true
|
||||
|
||||
@@ -13,6 +13,7 @@ var _is_dark := true
|
||||
func _enter_tree() -> void:
|
||||
_is_dark = _load_preference()
|
||||
Style.set_dark_mode(_is_dark)
|
||||
_sync_web_preference()
|
||||
|
||||
|
||||
func is_dark_mode() -> bool:
|
||||
@@ -25,6 +26,7 @@ func set_dark_mode(value: bool) -> void:
|
||||
_is_dark = value
|
||||
Style.set_dark_mode(_is_dark)
|
||||
_save_preference()
|
||||
_sync_web_preference()
|
||||
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")
|
||||
if config.save(SETTINGS_PATH) != OK:
|
||||
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
|
||||
)
|
||||
|
||||
@@ -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 \
|
||||
&& godot --headless --editor --path /src --quit \
|
||||
&& 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/medmind-loader-icon.png \
|
||||
&& test -s /build/medmind-inter.ttf \
|
||||
&& test -s /build/index.wasm \
|
||||
&& test -s /build/index.pck
|
||||
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user