Loading splash: animated logo over a dark overlay on desktop

index.html carries the overlay markup and styles inline so it paints before
any script; public/splash.js (external, for the CSP) plays the logo
animation from icons/ultimo-splash.html, holds, fades out after ~3.25 s and
removes itself. Skipped at phone sizes (same query as useIsMobile) and in
pop-out compose windows; a click skips it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 17:23:56 +01:00
co-authored by Claude Opus 5.5
parent 05db376b89
commit f82c7eb444
3 changed files with 448 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ultimo Splash</title>
<style>
/* ---- Splash styles (copy into your site) ---------------------------- */
:root { --splash-bg: #0f0f12; }
#ultimo-splash {
position: fixed; inset: 0; z-index: 9999;
display: grid; place-items: center;
background: var(--splash-bg);
transition: opacity .35s ease, visibility 0s linear .35s;
cursor: pointer;
}
#ultimo-splash.is-done { opacity: 0; visibility: hidden; pointer-events: none; }
#ultimo-splash svg { width: min(46vmin, 340px); height: auto; overflow: visible; }
/* ---- Demo page only (remove on your site) ---------------------------- */
html, body { margin: 0; height: 100%; }
body { font-family: system-ui, sans-serif; background: #f6f6f7; color: #1b1b1f;
display: grid; place-items: center; }
.demo { text-align: center; }
.demo button { font: inherit; padding: 12px 20px; border-radius: 10px; border: 1px solid #c9c9cf;
background: #fff; cursor: pointer; min-height: 44px; }
</style>
</head>
<body>
<!-- ======================= SPLASH MARKUP (copy) ======================= -->
<div id="ultimo-splash" aria-hidden="true">
<svg viewBox="-10 -6 65 56" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="ul-g1" x1="54.340946" y1="122.84999" x2="54.340946" y2="162.66415" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.75684298,0,0,0.80151105,-19.989667,-137.1)">
<stop offset="0.20198774" stop-color="#f89629"/><stop offset="1" stop-color="#f74176"/>
</linearGradient>
<radialGradient id="ul-r1" cx="8.0343275" cy="13.452971" fx="8.0343275" fy="13.452971" r="16.678839" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.29273442,-0.00257918,0.01127876,-1.2801289,10.212159,-15.400302)">
<stop offset="0" stop-color="#fa4435" stop-opacity="1"/><stop offset="1" stop-color="#fa7367" stop-opacity="0"/>
</radialGradient>
<radialGradient id="ul-r2" cx="34.523952" cy="11.253304" fx="34.523952" fy="11.253304" r="16.740065" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.27572675,-0.00557307,0.0193391,0.95679742,24.457243,-42.568342)">
<stop offset="0" stop-color="#fadc56" stop-opacity="1"/><stop offset="1" stop-color="#faae3a" stop-opacity="0"/>
</radialGradient>
<linearGradient id="ul-gs" x1="0" y1="5.11" x2="0" y2="37.01" gradientUnits="userSpaceOnUse">
<stop offset="0.202" stop-color="#f89629"/><stop offset="1" stop-color="#f74176"/>
</linearGradient>
<filter id="ul-sh" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="0" dy="0.35" stdDeviation="0.5" flood-color="#000" flood-opacity="0.42"/>
</filter>
<filter id="ul-blur" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="1.1"/>
</filter>
<g id="ul-logo" transform="translate(1.425541,43.741704)">
<path id="ul-main" fill="url(#ul-g1)" d="m 35.878324,-39.647594 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592603,-4.12477 -4.1103803,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.7640196 7.0382003,7.3825696 14.4526703,7.3825696 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.4422096 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 v -15.48541 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.8590102,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -10e-4,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247013,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z"/>
<path fill="url(#ul-r1)" d="m 2.3483903,-36.677919 c -0.1292796,0.384121 -0.2004748,0.792004 -0.2005045,1.214913 v 15.550989 c -4e-5,0.005 -0.029506,3.84363 2.172994,7.60522 2.20396,3.7640196 7.0383952,7.3824946 14.4528652,7.3824946 h 4.580598 c 5.433936,0 9.490679,-1.96731 12.151197,-4.504117 -1.517177,-1.2182566 -3.218664,-2.5953336 -5.013648,-4.0550496 -0.258984,0.201994 -0.541601,0.394146 -0.850594,0.570508 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -1.45833,-0.8249 -2.313817,-2.003692 -2.8592572,-2.935222 -1.1966406,-2.04365 -1.2298996,-4.071586 -1.2298996,-4.071586 l -0.00103,-0.01395 v -0.01602 -11.696444 -0.01395 l 0.00362,-0.0083 c 0.05128,0.0487 0.1879857,0.16313 0.2418457,0.20774 l 5.4249921,4.545459 5.514909,4.571297 c 0.0844,0.06984 0.185312,0.130661 0.298173,0.182418 -5.07081,-4.173874 -9.542211,-7.882397 -11.4706242,-9.500169 -2.2977093,-1.895939 -4.3807977,-3.616666 -6.1055705,-5.042586 z"/>
<path fill="url(#ul-r2)" d="m 39.930076,-36.730629 c -1.718284,1.420541 -3.7895,3.131915 -6.072498,5.015715 -1.93499,1.62329 -6.430112,5.351263 -11.522294,9.54257 0.08232,-0.04302 0.156783,-0.0915 0.221692,-0.145211 l 5.514909,-4.571297 5.48752,-4.573881 c 0.0199,-0.0164 0.04075,-0.03316 0.05994,-0.04806 l 0.0279,-0.02119 0.222726,-0.190169 v 11.598258 0.02894 c 0,0 -0.06656,2.081964 -1.311548,4.183724 -0.57202,0.96572 -1.445392,2.157957 -2.917652,2.998267 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -0.308682,-0.174605 -0.590588,-0.364744 -0.848011,-0.565341 -1.8055532,1.467965 -3.5132252,2.849693 -5.0307004,4.0674516 2.6425302,2.518552 6.6894504,4.460193 12.1212244,4.460193 h 4.580599 c 7.4177,0 12.269411,-3.665883 14.506091,-7.4419226 2.23668,-3.77604 2.270146,-7.635193 2.270146,-7.635193 l 0.0021,-0.03411 v -15.485354 c 1.06e-4,-0.420906 -0.07162,-0.827177 -0.202078,-1.209745 z"/>
</g>
<!-- Mask that reveals the logo's U as the stroke draws -->
<mask id="ul-mask-u" maskUnits="userSpaceOnUse" x="-20" y="-20" width="100" height="100">
<path id="ul-stroke" d="M7.73,8.31 L7.73,23.8 C7.73,30.6 13.6,34.65 22.57,34.65 C31.5,34.65 37.4,30.6 37.4,23.8 L37.4,8.31"
pathLength="1" fill="none" stroke="#fff" stroke-width="9.6" stroke-linecap="round"
stroke-dasharray="1 2" stroke-dashoffset="1"/>
</mask>
<!-- Logo colours limited to the top-flap area -->
<!-- Area between the U's arms: inside it the logo's own fill is exactly the flap -->
<clipPath id="ul-clip-flap"><path d="M10.093,11.066 L35.296,11.066 L35.296,12.1 L22.69,23.6 L10.093,12.1 Z"/></clipPath>
<!-- Darkens the coloured flap while it is mid-fold -->
<filter id="ul-dark" x="0" y="0" width="1" height="1">
<feComponentTransfer>
<feFuncR id="ul-dr" type="linear" slope="1"/><feFuncG id="ul-dg" type="linear" slope="1"/><feFuncB id="ul-db" type="linear" slope="1"/>
</feComponentTransfer>
</filter>
</defs>
<!-- Envelope (white/grey paper) -->
<g id="ul-env">
<g id="ul-rest">
<!-- Thin paper margin tucked under the U so no background shows along its inner edge -->
<rect id="ul-under" x="9.8" y="12.1" width="25.8" height="20.4" fill="rgb(250,250,253)" opacity="0" mask="url(#ul-mask-u)"/>
<rect x="10.093" y="11.066" width="25.203" height="21.064" fill="rgb(226,226,230)"/>
<path id="ul-fl" filter="url(#ul-sh)"/>
<path id="ul-fr" filter="url(#ul-sh)"/>
<path id="ul-fb" filter="url(#ul-sh)"/>
</g>
<!-- Top flap: exact outline of the logo's V flap (shown folded); flipped about the top edge while folding -->
<!-- Inside of the flap = white paper; outside = the logo's colours (clipped to the flap) -->
<g id="ul-ftg" filter="url(#ul-sh)">
<path id="ul-ft"
d="M10.093,11.066 L35.296,11.066 L35.296,12.019 L35.208,12.089 L24.206,21.234 C23.554,21.773 21.927,21.773 21.275,21.234 L10.335,12.117 L10.093,11.910 Z"/>
<g id="ul-fto" opacity="0" filter="url(#ul-dark)">
<use href="#ul-logo" clip-path="url(#ul-clip-flap)"/>
</g>
</g>
</g>
<!-- U drawn over the envelope: the real logo artwork (with its side gradients) revealed by a growing stroke -->
<g id="ul-ug" opacity="0">
<use href="#ul-logo" mask="url(#ul-mask-u)"/>
</g>
<!-- Final logo + glow -->
<use id="ul-glow" href="#ul-logo" filter="url(#ul-blur)" opacity="0"/>
<use id="ul-final" href="#ul-logo" opacity="0"/>
</svg>
</div>
<!-- ===================================================================== -->
<!-- Demo page content (remove on your site) -->
<main class="demo">
<p>Your website content</p>
<button type="button" id="replay">Replay intro</button>
</main>
<script>
/* ======================= SPLASH SCRIPT (copy) ======================== */
(function () {
var ONCE_PER_SESSION = false; // true = show only on the first page view per browser session
var SPEED = 1; // 1 = ~2.6 s to final logo; 0.8 = faster, 1.2 = slower
var HOLD_MS = 450; // how long the finished logo stays before the splash fades
var root = document.getElementById('ultimo-splash');
if (!root) return;
var $ = function (id) { return document.getElementById(id); };
var env = $('ul-env'), rest = $('ul-rest'), fl = $('ul-fl'), fr = $('ul-fr'), fb = $('ul-fb'), ft = $('ul-ft');
var stroke = $('ul-stroke'), glow = $('ul-glow'), fin = $('ul-final');
var ug = $('ul-ug'), under = $('ul-under');
var ftg = $('ul-ftg'), fto = $('ul-fto'), dr = $('ul-dr'), dg = $('ul-dg'), db = $('ul-db');
// Envelope body edges (logo coordinates) and the four flaps.
var L = 10.093, R = 35.296, T = 11.066, B = 32.13, CX = 22.69, CY = 21.6;
var FLAPS = {
l: { axis: 'x', h: L, a: T, b: B, mid: CY, depth: 12.2, dir: -1, r: 3.2 },
r: { axis: 'x', h: R, a: T, b: B, mid: CY, depth: 12.2, dir: 1, r: 3.2 },
b: { axis: 'y', h: B, a: L, b: R, mid: CX, depth: 14.0, dir: 1, r: 3.6 }
};
var END = 2450; // ms (at SPEED 1) when the logo is fully formed
function clamp(x) { return x < 0 ? 0 : x > 1 ? 1 : x; }
function seg(t, a, b) { return clamp((t - a) / (b - a)); }
function ease(x) { return x < .5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; }
function easeOut(x) { return 1 - Math.pow(1 - x, 3); }
function n(v) { return Math.round(v * 1000) / 1000; }
// Flap outline: base fixed exactly on the body edge, apex swings over the hinge, tip rounded.
function flapD(f, p) {
var s = Math.cos(Math.PI * p), ax, ay, A, C;
if (f.axis === 'x') { ax = f.h + f.dir * f.depth * s; ay = f.mid; A = [f.h, f.a]; C = [f.h, f.b]; }
else { ax = f.mid; ay = f.h + f.dir * f.depth * s; A = [f.a, f.h]; C = [f.b, f.h]; }
function toward(P) {
var dx = P[0] - ax, dy = P[1] - ay, l = Math.hypot(dx, dy) || 1, r = Math.min(f.r, l * 0.45);
return n(ax + dx * r / l) + ',' + n(ay + dy * r / l);
}
return 'M' + A[0] + ',' + A[1] + ' L' + toward(A) + ' Q' + n(ax) + ',' + n(ay) + ' ' + toward(C) +
' L' + C[0] + ',' + C[1] + ' Z';
}
// Paper shading: inner face (grey) before the flip, outer face (white) after it.
function faceC(p) {
var lift = Math.sin(Math.PI * p);
var c = Math.round(p < .5 ? 230 - 42 * lift : 250 - 36 * lift);
return 'rgb(' + c + ',' + c + ',' + (c + 3) + ')';
}
function setFlap(el, f, p) {
el.setAttribute('d', flapD(f, p));
el.setAttribute('fill', faceC(p));
}
function render(ms) {
var intro = easeOut(seg(ms, 0, 220));
setFlap(fl, FLAPS.l, ease(seg(ms, 180, 560)));
setFlap(fr, FLAPS.r, ease(seg(ms, 240, 620)));
setFlap(fb, FLAPS.b, ease(seg(ms, 560, 920)));
var pt = ease(seg(ms, 880, 1260)), st = -Math.cos(Math.PI * pt);
if (Math.abs(st) < 0.001) st = 0.001;
// Top flap: white inside while open; once it flips past vertical its outside (logo colours) shows
ftg.setAttribute('transform', 'translate(0 ' + T + ') scale(1 ' + n(st) + ') translate(0 ' + (-T) + ')');
ft.setAttribute('fill', faceC(pt));
ft.setAttribute('opacity', pt >= 0.5 ? 0 : 1); // white inside face only while it faces the viewer
fto.setAttribute('opacity', pt >= 0.5 ? 1 : 0);
var slope = n(1 - 0.35 * Math.sin(Math.PI * pt));
dr.setAttribute('slope', slope); dg.setAttribute('slope', slope); db.setAttribute('slope', slope);
var draw = ease(seg(ms, 1180, 1800));
var fade = ease(seg(ms, 2150, 2450)); // white envelope (all but the top flap) vanishes after the logo has formed
var dis = ease(seg(ms, 1800, 2150)); // dissolve: U stroke + top flap -> final logo
var sc = 0.9 + 0.1 * intro;
env.setAttribute('transform', 'translate(22.5 22) scale(' + sc + ') translate(-22.5 -22)');
env.setAttribute('opacity', intro);
rest.setAttribute('opacity', 1 - fade);
ftg.setAttribute('opacity', 1 - fade); // coloured flap sits under the logo until the envelope goes
stroke.setAttribute('stroke-dashoffset', 1 - draw);
// Stroke stays solid under the logo while it dissolves in, then is removed once fully covered
ug.setAttribute('opacity', draw > 0 && dis < 1 ? 1 : 0);
under.setAttribute('opacity', draw > 0 ? 1 : 0);
fin.setAttribute('opacity', dis);
glow.setAttribute('opacity', 0.35 * Math.sin(Math.PI * seg(ms, 1900, 2450)));
}
var raf = 0, start = 0, hideTimer = 0;
function finish() {
cancelAnimationFrame(raf);
render(99999);
root.classList.add('is-done');
document.dispatchEvent(new CustomEvent('ultimo-splash:done'));
}
function tick(now) {
if (!start) start = now;
var ms = (now - start) / SPEED;
render(ms);
if (ms < END) raf = requestAnimationFrame(tick);
else hideTimer = setTimeout(finish, HOLD_MS);
}
function play() {
cancelAnimationFrame(raf); clearTimeout(hideTimer);
root.classList.remove('is-done');
start = 0; render(0);
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) {
render(99999); hideTimer = setTimeout(finish, 700); return;
}
raf = requestAnimationFrame(tick);
}
try {
if (ONCE_PER_SESSION && sessionStorage.getItem('ultimo-splash-seen')) { root.classList.add('is-done'); return; }
sessionStorage.setItem('ultimo-splash-seen', '1');
} catch (e) {}
root.addEventListener('click', finish); // click / tap skips the intro
window.ultimoSplash = { play: play, skip: finish, seek: function (ms) { cancelAnimationFrame(raf); clearTimeout(hideTimer); root.classList.remove('is-done'); render(ms); } };
play();
})();
/* ===================================================================== */
// Demo only
document.getElementById('replay').addEventListener('click', function () { window.ultimoSplash.play(); });
</script>
</body>
</html>
+91
View File
@@ -13,8 +13,99 @@
<meta name="apple-mobile-web-app-title" content="Ultimo" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title>Ultimo Email</title>
<style>
/* Loading splash (public/splash.js). Dark grey like the app's dark theme. */
#ultimo-splash {
position: fixed; inset: 0; z-index: 9999;
display: grid; place-items: center;
background: #1b1e20;
transition: opacity .35s ease, visibility 0s linear .35s;
cursor: pointer;
}
#ultimo-splash.is-done { opacity: 0; visibility: hidden; pointer-events: none; }
#ultimo-splash svg { width: min(46vmin, 340px); height: auto; overflow: visible; }
/* Never on a phone (same query as useIsMobile). */
@media (max-width: 767px), (pointer: coarse) and (max-height: 500px) {
#ultimo-splash { display: none; }
}
</style>
</head>
<body>
<div id="ultimo-splash" aria-hidden="true">
<svg viewBox="-10 -6 65 56" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="ul-g1" x1="54.340946" y1="122.84999" x2="54.340946" y2="162.66415" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.75684298,0,0,0.80151105,-19.989667,-137.1)">
<stop offset="0.20198774" stop-color="#f89629"/><stop offset="1" stop-color="#f74176"/>
</linearGradient>
<radialGradient id="ul-r1" cx="8.0343275" cy="13.452971" fx="8.0343275" fy="13.452971" r="16.678839" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.29273442,-0.00257918,0.01127876,-1.2801289,10.212159,-15.400302)">
<stop offset="0" stop-color="#fa4435" stop-opacity="1"/><stop offset="1" stop-color="#fa7367" stop-opacity="0"/>
</radialGradient>
<radialGradient id="ul-r2" cx="34.523952" cy="11.253304" fx="34.523952" fy="11.253304" r="16.740065" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.27572675,-0.00557307,0.0193391,0.95679742,24.457243,-42.568342)">
<stop offset="0" stop-color="#fadc56" stop-opacity="1"/><stop offset="1" stop-color="#faae3a" stop-opacity="0"/>
</radialGradient>
<linearGradient id="ul-gs" x1="0" y1="5.11" x2="0" y2="37.01" gradientUnits="userSpaceOnUse">
<stop offset="0.202" stop-color="#f89629"/><stop offset="1" stop-color="#f74176"/>
</linearGradient>
<filter id="ul-sh" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="0" dy="0.35" stdDeviation="0.5" flood-color="#000" flood-opacity="0.42"/>
</filter>
<filter id="ul-blur" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="1.1"/>
</filter>
<g id="ul-logo" transform="translate(1.425541,43.741704)">
<path id="ul-main" fill="url(#ul-g1)" d="m 35.878324,-39.647594 c -2.15234,-7.4e-4 -4.0678,1.84724 -4.0671,4.1266 v 2.85212 c -0.073,-0.005 -0.14656,-0.007 -0.2202,-0.007 h -10.50012 -10.49855 c -0.0418,-7e-5 -0.0835,5.9e-4 -0.12516,0.002 v -2.78918 c -1.6e-4,-2.27807 -1.9592603,-4.12477 -4.1103803,-4.12494 -2.15111,1.8e-4 -4.20874,1.84687 -4.2089,4.12494 v 15.55127 c -4e-5,0.005 -0.0295,3.84339 2.173,7.60498 2.20396,3.7640196 7.0382003,7.3825696 14.4526703,7.3825696 h 4.58067 c 7.4177,0 12.26918,-3.66617 14.50586,-7.4422096 2.23668,-3.77604 2.27039,-7.63481 2.27039,-7.63481 l 0.002,-0.0344 v -15.48541 c 7e-4,-2.27872 -2.10202,-4.12642 -4.25374,-4.1266 z m -2.00792,7.9252 v 11.59858 0.0286 c 0,0 -0.0664,2.0824 -1.31139,4.18416 -0.57202,0.96572 -1.44541,2.15762 -2.91767,2.99793 -1.47227,0.8403 -3.46066,1.33079 -6.27223,1.33079 h -4.58106 c -2.82381,0 -4.79864,-0.47938 -6.25697,-1.30429 -1.45833,-0.8249 -2.31357,-2.00343 -2.8590102,-2.93496 -1.1966401,-2.04365 -1.2300401,-4.07193 -1.2300401,-4.07193 l -10e-4,-0.0141 v -0.0157 -11.69637 l 1.96e-4,-0.0142 0.0035,-0.008 c 0.05128,0.0487 0.18806,0.16304 0.24192,0.20765 l 5.4247013,4.54513 5.51504,4.57144 c 0.65194,0.53945 2.27903,0.53945 2.93097,0 l 5.51504,-4.57144 5.487378,-4.57351 c 0.0199,-0.0164 0.04083,-0.0331 0.06003,-0.048 l 0.02765,-0.0215 z"/>
<path fill="url(#ul-r1)" d="m 2.3483903,-36.677919 c -0.1292796,0.384121 -0.2004748,0.792004 -0.2005045,1.214913 v 15.550989 c -4e-5,0.005 -0.029506,3.84363 2.172994,7.60522 2.20396,3.7640196 7.0383952,7.3824946 14.4528652,7.3824946 h 4.580598 c 5.433936,0 9.490679,-1.96731 12.151197,-4.504117 -1.517177,-1.2182566 -3.218664,-2.5953336 -5.013648,-4.0550496 -0.258984,0.201994 -0.541601,0.394146 -0.850594,0.570508 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -1.45833,-0.8249 -2.313817,-2.003692 -2.8592572,-2.935222 -1.1966406,-2.04365 -1.2298996,-4.071586 -1.2298996,-4.071586 l -0.00103,-0.01395 v -0.01602 -11.696444 -0.01395 l 0.00362,-0.0083 c 0.05128,0.0487 0.1879857,0.16313 0.2418457,0.20774 l 5.4249921,4.545459 5.514909,4.571297 c 0.0844,0.06984 0.185312,0.130661 0.298173,0.182418 -5.07081,-4.173874 -9.542211,-7.882397 -11.4706242,-9.500169 -2.2977093,-1.895939 -4.3807977,-3.616666 -6.1055705,-5.042586 z"/>
<path fill="url(#ul-r2)" d="m 39.930076,-36.730629 c -1.718284,1.420541 -3.7895,3.131915 -6.072498,5.015715 -1.93499,1.62329 -6.430112,5.351263 -11.522294,9.54257 0.08232,-0.04302 0.156783,-0.0915 0.221692,-0.145211 l 5.514909,-4.571297 5.48752,-4.573881 c 0.0199,-0.0164 0.04075,-0.03316 0.05994,-0.04806 l 0.0279,-0.02119 0.222726,-0.190169 v 11.598258 0.02894 c 0,0 -0.06656,2.081964 -1.311548,4.183724 -0.57202,0.96572 -1.445392,2.157957 -2.917652,2.998267 -1.47227,0.8403 -3.460398,1.330668 -6.271968,1.330668 h -4.581116 c -2.82381,0 -4.798652,-0.479403 -6.256982,-1.304313 -0.308682,-0.174605 -0.590588,-0.364744 -0.848011,-0.565341 -1.8055532,1.467965 -3.5132252,2.849693 -5.0307004,4.0674516 2.6425302,2.518552 6.6894504,4.460193 12.1212244,4.460193 h 4.580599 c 7.4177,0 12.269411,-3.665883 14.506091,-7.4419226 2.23668,-3.77604 2.270146,-7.635193 2.270146,-7.635193 l 0.0021,-0.03411 v -15.485354 c 1.06e-4,-0.420906 -0.07162,-0.827177 -0.202078,-1.209745 z"/>
</g>
<!-- Mask that reveals the logo's U as the stroke draws -->
<mask id="ul-mask-u" maskUnits="userSpaceOnUse" x="-20" y="-20" width="100" height="100">
<path id="ul-stroke" d="M7.73,8.31 L7.73,23.8 C7.73,30.6 13.6,34.65 22.57,34.65 C31.5,34.65 37.4,30.6 37.4,23.8 L37.4,8.31"
pathLength="1" fill="none" stroke="#fff" stroke-width="9.6" stroke-linecap="round"
stroke-dasharray="1 2" stroke-dashoffset="1"/>
</mask>
<!-- Logo colours limited to the top-flap area -->
<!-- Area between the U's arms: inside it the logo's own fill is exactly the flap -->
<clipPath id="ul-clip-flap"><path d="M10.093,11.066 L35.296,11.066 L35.296,12.1 L22.69,23.6 L10.093,12.1 Z"/></clipPath>
<!-- Darkens the coloured flap while it is mid-fold -->
<filter id="ul-dark" x="0" y="0" width="1" height="1">
<feComponentTransfer>
<feFuncR id="ul-dr" type="linear" slope="1"/><feFuncG id="ul-dg" type="linear" slope="1"/><feFuncB id="ul-db" type="linear" slope="1"/>
</feComponentTransfer>
</filter>
</defs>
<!-- Envelope (white/grey paper) -->
<g id="ul-env">
<g id="ul-rest">
<!-- Thin paper margin tucked under the U so no background shows along its inner edge -->
<rect id="ul-under" x="9.8" y="12.1" width="25.8" height="20.4" fill="rgb(250,250,253)" opacity="0" mask="url(#ul-mask-u)"/>
<rect x="10.093" y="11.066" width="25.203" height="21.064" fill="rgb(226,226,230)"/>
<path id="ul-fl" filter="url(#ul-sh)"/>
<path id="ul-fr" filter="url(#ul-sh)"/>
<path id="ul-fb" filter="url(#ul-sh)"/>
</g>
<!-- Top flap: exact outline of the logo's V flap (shown folded); flipped about the top edge while folding -->
<!-- Inside of the flap = white paper; outside = the logo's colours (clipped to the flap) -->
<g id="ul-ftg" filter="url(#ul-sh)">
<path id="ul-ft"
d="M10.093,11.066 L35.296,11.066 L35.296,12.019 L35.208,12.089 L24.206,21.234 C23.554,21.773 21.927,21.773 21.275,21.234 L10.335,12.117 L10.093,11.910 Z"/>
<g id="ul-fto" opacity="0" filter="url(#ul-dark)">
<use href="#ul-logo" clip-path="url(#ul-clip-flap)"/>
</g>
</g>
</g>
<!-- U drawn over the envelope: the real logo artwork (with its side gradients) revealed by a growing stroke -->
<g id="ul-ug" opacity="0">
<use href="#ul-logo" mask="url(#ul-mask-u)"/>
</g>
<!-- Final logo + glow -->
<use id="ul-glow" href="#ul-logo" filter="url(#ul-blur)" opacity="0"/>
<use id="ul-final" href="#ul-logo" opacity="0"/>
</svg>
</div>
<script src="/splash.js"></script>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
+120
View File
@@ -0,0 +1,120 @@
// Loading splash: the animated Ultimo logo over a dark overlay while the app
// starts (markup and styles inline in index.html, so it paints before any script
// runs; this file is separate because the CSP allows no inline scripts). About
// 3.25 s in all: ~2.45 s to the finished logo, a short hold, then a .35 s fade.
// Desktop and tablet only; a click skips it. Source: frontend/icons/ultimo-splash.html.
(function () {
var SPEED = 1; // 1 = ~2.6 s to final logo; 0.8 = faster, 1.2 = slower
var HOLD_MS = 450; // how long the finished logo stays before the splash fades
var root = document.getElementById('ultimo-splash');
if (!root) return;
// Not on a phone (same query as useIsMobile), and not in a pop-out compose window.
var PHONE = '(max-width: 767px), (pointer: coarse) and (max-height: 500px)';
if ((window.matchMedia && matchMedia(PHONE).matches) || location.pathname.indexOf('/compose/') === 0) {
root.parentNode.removeChild(root);
return;
}
var $ = function (id) { return document.getElementById(id); };
var env = $('ul-env'), rest = $('ul-rest'), fl = $('ul-fl'), fr = $('ul-fr'), fb = $('ul-fb'), ft = $('ul-ft');
var stroke = $('ul-stroke'), glow = $('ul-glow'), fin = $('ul-final');
var ug = $('ul-ug'), under = $('ul-under');
var ftg = $('ul-ftg'), fto = $('ul-fto'), dr = $('ul-dr'), dg = $('ul-dg'), db = $('ul-db');
// Envelope body edges (logo coordinates) and the four flaps.
var L = 10.093, R = 35.296, T = 11.066, B = 32.13, CX = 22.69, CY = 21.6;
var FLAPS = {
l: { axis: 'x', h: L, a: T, b: B, mid: CY, depth: 12.2, dir: -1, r: 3.2 },
r: { axis: 'x', h: R, a: T, b: B, mid: CY, depth: 12.2, dir: 1, r: 3.2 },
b: { axis: 'y', h: B, a: L, b: R, mid: CX, depth: 14.0, dir: 1, r: 3.6 }
};
var END = 2450; // ms (at SPEED 1) when the logo is fully formed
function clamp(x) { return x < 0 ? 0 : x > 1 ? 1 : x; }
function seg(t, a, b) { return clamp((t - a) / (b - a)); }
function ease(x) { return x < .5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; }
function easeOut(x) { return 1 - Math.pow(1 - x, 3); }
function n(v) { return Math.round(v * 1000) / 1000; }
// Flap outline: base fixed exactly on the body edge, apex swings over the hinge, tip rounded.
function flapD(f, p) {
var s = Math.cos(Math.PI * p), ax, ay, A, C;
if (f.axis === 'x') { ax = f.h + f.dir * f.depth * s; ay = f.mid; A = [f.h, f.a]; C = [f.h, f.b]; }
else { ax = f.mid; ay = f.h + f.dir * f.depth * s; A = [f.a, f.h]; C = [f.b, f.h]; }
function toward(P) {
var dx = P[0] - ax, dy = P[1] - ay, l = Math.hypot(dx, dy) || 1, r = Math.min(f.r, l * 0.45);
return n(ax + dx * r / l) + ',' + n(ay + dy * r / l);
}
return 'M' + A[0] + ',' + A[1] + ' L' + toward(A) + ' Q' + n(ax) + ',' + n(ay) + ' ' + toward(C) +
' L' + C[0] + ',' + C[1] + ' Z';
}
// Paper shading: inner face (grey) before the flip, outer face (white) after it.
function faceC(p) {
var lift = Math.sin(Math.PI * p);
var c = Math.round(p < .5 ? 230 - 42 * lift : 250 - 36 * lift);
return 'rgb(' + c + ',' + c + ',' + (c + 3) + ')';
}
function setFlap(el, f, p) {
el.setAttribute('d', flapD(f, p));
el.setAttribute('fill', faceC(p));
}
function render(ms) {
var intro = easeOut(seg(ms, 0, 220));
setFlap(fl, FLAPS.l, ease(seg(ms, 180, 560)));
setFlap(fr, FLAPS.r, ease(seg(ms, 240, 620)));
setFlap(fb, FLAPS.b, ease(seg(ms, 560, 920)));
var pt = ease(seg(ms, 880, 1260)), st = -Math.cos(Math.PI * pt);
if (Math.abs(st) < 0.001) st = 0.001;
// Top flap: white inside while open; once it flips past vertical its outside (logo colours) shows
ftg.setAttribute('transform', 'translate(0 ' + T + ') scale(1 ' + n(st) + ') translate(0 ' + (-T) + ')');
ft.setAttribute('fill', faceC(pt));
ft.setAttribute('opacity', pt >= 0.5 ? 0 : 1); // white inside face only while it faces the viewer
fto.setAttribute('opacity', pt >= 0.5 ? 1 : 0);
var slope = n(1 - 0.35 * Math.sin(Math.PI * pt));
dr.setAttribute('slope', slope); dg.setAttribute('slope', slope); db.setAttribute('slope', slope);
var draw = ease(seg(ms, 1180, 1800));
var fade = ease(seg(ms, 2150, 2450)); // white envelope (all but the top flap) vanishes after the logo has formed
var dis = ease(seg(ms, 1800, 2150)); // dissolve: U stroke + top flap -> final logo
var sc = 0.9 + 0.1 * intro;
env.setAttribute('transform', 'translate(22.5 22) scale(' + sc + ') translate(-22.5 -22)');
env.setAttribute('opacity', intro);
rest.setAttribute('opacity', 1 - fade);
ftg.setAttribute('opacity', 1 - fade); // coloured flap sits under the logo until the envelope goes
stroke.setAttribute('stroke-dashoffset', 1 - draw);
// Stroke stays solid under the logo while it dissolves in, then is removed once fully covered
ug.setAttribute('opacity', draw > 0 && dis < 1 ? 1 : 0);
under.setAttribute('opacity', draw > 0 ? 1 : 0);
fin.setAttribute('opacity', dis);
glow.setAttribute('opacity', 0.35 * Math.sin(Math.PI * seg(ms, 1900, 2450)));
}
var raf = 0, start = 0, hideTimer = 0;
function finish() {
cancelAnimationFrame(raf);
render(99999);
root.classList.add('is-done');
// Gone from the page once faded out (the .35 s opacity transition).
setTimeout(function () { if (root.parentNode) root.parentNode.removeChild(root); }, 400);
}
function tick(now) {
if (!start) start = now;
var ms = (now - start) / SPEED;
render(ms);
if (ms < END) raf = requestAnimationFrame(tick);
else hideTimer = setTimeout(finish, HOLD_MS);
}
function play() {
cancelAnimationFrame(raf); clearTimeout(hideTimer);
root.classList.remove('is-done');
start = 0; render(0);
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) {
render(99999); hideTimer = setTimeout(finish, 700); return;
}
raf = requestAnimationFrame(tick);
}
root.addEventListener('click', finish); // a click skips the intro
play();
})();