121 lines
6.0 KiB
JavaScript
121 lines
6.0 KiB
JavaScript
// 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();
|
||
|
|
})();
|