// 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(); })();