add timeline page (#1295)
This commit is contained in:
+2
-1
@@ -11,7 +11,8 @@
|
|||||||
"games": "Games",
|
"games": "Games",
|
||||||
"squadrons": "Squadrons",
|
"squadrons": "Squadrons",
|
||||||
"donate": "Donate",
|
"donate": "Donate",
|
||||||
"analytics": "Analytics"
|
"analytics": "Analytics",
|
||||||
|
"timeline": "Timeline"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"services": "Services",
|
"services": "Services",
|
||||||
|
|||||||
@@ -1094,6 +1094,9 @@ video {
|
|||||||
.\!mt-32 {
|
.\!mt-32 {
|
||||||
margin-top: 8rem !important;
|
margin-top: 8rem !important;
|
||||||
}
|
}
|
||||||
|
.-ml-1 {
|
||||||
|
margin-left: -0.25rem;
|
||||||
|
}
|
||||||
.mb-1\.5 {
|
.mb-1\.5 {
|
||||||
margin-bottom: 0.375rem;
|
margin-bottom: 0.375rem;
|
||||||
}
|
}
|
||||||
@@ -1121,6 +1124,9 @@ video {
|
|||||||
.mb-8 {
|
.mb-8 {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
.ml-0\.5 {
|
||||||
|
margin-left: 0.125rem;
|
||||||
|
}
|
||||||
.ml-1 {
|
.ml-1 {
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -1148,6 +1154,12 @@ video {
|
|||||||
.mt-1 {
|
.mt-1 {
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
.mt-2 {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: 5rem;
|
||||||
|
}
|
||||||
.mt-3 {
|
.mt-3 {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -1214,6 +1226,9 @@ video {
|
|||||||
.h-8 {
|
.h-8 {
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
}
|
}
|
||||||
|
.h-\[88px\] {
|
||||||
|
height: 88px;
|
||||||
|
}
|
||||||
.h-full {
|
.h-full {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -1235,6 +1250,9 @@ video {
|
|||||||
.w-5 {
|
.w-5 {
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
}
|
}
|
||||||
|
.w-\[440px\] {
|
||||||
|
width: 440px;
|
||||||
|
}
|
||||||
.w-auto {
|
.w-auto {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
@@ -1328,6 +1346,9 @@ video {
|
|||||||
.items-baseline {
|
.items-baseline {
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
.items-stretch {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
.justify-center {
|
.justify-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@@ -1412,6 +1433,9 @@ video {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.whitespace-nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.rounded {
|
.rounded {
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -1480,6 +1504,10 @@ video {
|
|||||||
.border-yellow-400\/30 {
|
.border-yellow-400\/30 {
|
||||||
border-color: rgb(250 204 21 / 0.3);
|
border-color: rgb(250 204 21 / 0.3);
|
||||||
}
|
}
|
||||||
|
.bg-\[\#141414\] {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(20 20 20 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
.bg-\[\#2A2A2A\] {
|
.bg-\[\#2A2A2A\] {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(42 42 42 / var(--tw-bg-opacity, 1));
|
background-color: rgb(42 42 42 / var(--tw-bg-opacity, 1));
|
||||||
@@ -1487,6 +1515,9 @@ video {
|
|||||||
.bg-\[rgba\(0\2c 0\2c 0\2c 0\.4\)\] {
|
.bg-\[rgba\(0\2c 0\2c 0\2c 0\.4\)\] {
|
||||||
background-color: rgba(0,0,0,0.4);
|
background-color: rgba(0,0,0,0.4);
|
||||||
}
|
}
|
||||||
|
.bg-\[rgba\(144\2c 238\2c 144\2c 0\.04\)\] {
|
||||||
|
background-color: rgba(144,238,144,0.04);
|
||||||
|
}
|
||||||
.bg-\[rgba\(144\2c 238\2c 144\2c 0\.15\)\] {
|
.bg-\[rgba\(144\2c 238\2c 144\2c 0\.15\)\] {
|
||||||
background-color: rgba(144,238,144,0.15);
|
background-color: rgba(144,238,144,0.15);
|
||||||
}
|
}
|
||||||
@@ -1620,6 +1651,9 @@ video {
|
|||||||
.pb-1 {
|
.pb-1 {
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
.pb-10 {
|
||||||
|
padding-bottom: 2.5rem;
|
||||||
|
}
|
||||||
.pb-12 {
|
.pb-12 {
|
||||||
padding-bottom: 3rem;
|
padding-bottom: 3rem;
|
||||||
}
|
}
|
||||||
@@ -1632,9 +1666,18 @@ video {
|
|||||||
.pb-20 {
|
.pb-20 {
|
||||||
padding-bottom: 5rem;
|
padding-bottom: 5rem;
|
||||||
}
|
}
|
||||||
|
.pb-28 {
|
||||||
|
padding-bottom: 7rem;
|
||||||
|
}
|
||||||
.pb-40 {
|
.pb-40 {
|
||||||
padding-bottom: 10rem;
|
padding-bottom: 10rem;
|
||||||
}
|
}
|
||||||
|
.pl-1 {
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
}
|
||||||
|
.pr-2 {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
.pt-3 {
|
.pt-3 {
|
||||||
padding-top: 0.75rem;
|
padding-top: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -1737,6 +1780,9 @@ video {
|
|||||||
.leading-tight {
|
.leading-tight {
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
.tracking-\[0\.25em\] {
|
||||||
|
letter-spacing: 0.25em;
|
||||||
|
}
|
||||||
.tracking-\[0\.2em\] {
|
.tracking-\[0\.2em\] {
|
||||||
letter-spacing: 0.2em;
|
letter-spacing: 0.2em;
|
||||||
}
|
}
|
||||||
@@ -1749,6 +1795,9 @@ video {
|
|||||||
.tracking-wider {
|
.tracking-wider {
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
.tracking-widest {
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
.text-\[\#1E1E1E\] {
|
.text-\[\#1E1E1E\] {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(30 30 30 / var(--tw-text-opacity, 1));
|
color: rgb(30 30 30 / var(--tw-text-opacity, 1));
|
||||||
@@ -1833,11 +1882,20 @@ video {
|
|||||||
.opacity-75 {
|
.opacity-75 {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
.shadow-2xl {
|
||||||
|
--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||||
|
--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
|
}
|
||||||
.shadow-xl {
|
.shadow-xl {
|
||||||
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||||
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
|
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
|
||||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
|
.shadow-black\/70 {
|
||||||
|
--tw-shadow-color: rgb(0 0 0 / 0.7);
|
||||||
|
--tw-shadow: var(--tw-shadow-colored);
|
||||||
|
}
|
||||||
.outline {
|
.outline {
|
||||||
outline-style: solid;
|
outline-style: solid;
|
||||||
}
|
}
|
||||||
@@ -1850,6 +1908,10 @@ video {
|
|||||||
--tw-blur: blur(8px);
|
--tw-blur: blur(8px);
|
||||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||||
}
|
}
|
||||||
|
.drop-shadow {
|
||||||
|
--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
|
||||||
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||||
|
}
|
||||||
.invert {
|
.invert {
|
||||||
--tw-invert: invert(100%);
|
--tw-invert: invert(100%);
|
||||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||||
@@ -1865,6 +1927,10 @@ video {
|
|||||||
--tw-backdrop-blur: blur(12px);
|
--tw-backdrop-blur: blur(12px);
|
||||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
||||||
}
|
}
|
||||||
|
.backdrop-blur-sm {
|
||||||
|
--tw-backdrop-blur: blur(4px);
|
||||||
|
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
||||||
|
}
|
||||||
.backdrop-filter {
|
.backdrop-filter {
|
||||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
||||||
}
|
}
|
||||||
@@ -1947,6 +2013,9 @@ tr.row-link td textarea {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
.hover\:border-\[rgba\(144\2c 238\2c 144\2c 0\.35\)\]:hover {
|
||||||
|
border-color: rgba(144,238,144,0.35);
|
||||||
|
}
|
||||||
.hover\:border-white\/20:hover {
|
.hover\:border-white\/20:hover {
|
||||||
border-color: rgb(255 255 255 / 0.2);
|
border-color: rgb(255 255 255 / 0.2);
|
||||||
}
|
}
|
||||||
@@ -1978,6 +2047,18 @@ tr.row-link td textarea {
|
|||||||
.hover\:underline:hover {
|
.hover\:underline:hover {
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
}
|
}
|
||||||
|
.focus\:outline-none:focus {
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.focus-visible\:ring-2:focus-visible {
|
||||||
|
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||||
|
}
|
||||||
|
.focus-visible\:ring-\[\#90EE90\]\/40:focus-visible {
|
||||||
|
--tw-ring-color: rgb(144 238 144 / 0.4);
|
||||||
|
}
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
|
|
||||||
.sm\:flex-row {
|
.sm\:flex-row {
|
||||||
@@ -2051,6 +2132,11 @@ tr.row-link td textarea {
|
|||||||
padding: 2.5rem;
|
padding: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lg\:px-12 {
|
||||||
|
padding-left: 3rem;
|
||||||
|
padding-right: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.lg\:px-8 {
|
.lg\:px-8 {
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
@@ -2061,6 +2147,10 @@ tr.row-link td textarea {
|
|||||||
padding-bottom: 5rem;
|
padding-bottom: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lg\:pb-14 {
|
||||||
|
padding-bottom: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.lg\:pb-16 {
|
.lg\:pb-16 {
|
||||||
padding-bottom: 4rem;
|
padding-bottom: 4rem;
|
||||||
}
|
}
|
||||||
@@ -2092,6 +2182,11 @@ tr.row-link td textarea {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lg\:text-6xl {
|
||||||
|
font-size: 3.75rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.lg\:text-9xl {
|
.lg\:text-9xl {
|
||||||
font-size: 8rem;
|
font-size: 8rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
@@ -899,6 +899,12 @@ app.get('/docs', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/timeline', (req, res) => {
|
||||||
|
res.render('timeline', {
|
||||||
|
botName: 'Toothless SQB Bot'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/premium', (req, res) => {
|
app.get('/premium', (req, res) => {
|
||||||
const stdCheckout = process.env.WHOP_PLAN_ID_STANDARD || null;
|
const stdCheckout = process.env.WHOP_PLAN_ID_STANDARD || null;
|
||||||
const proCheckout = process.env.WHOP_PLAN_ID_PRO || null;
|
const proCheckout = process.env.WHOP_PLAN_ID_PRO || null;
|
||||||
|
|||||||
@@ -165,6 +165,7 @@
|
|||||||
{ href: '/squadrons', key: 'squadrons' },
|
{ href: '/squadrons', key: 'squadrons' },
|
||||||
{ href: '/leaderboard/players', key: 'leaderboards' },
|
{ href: '/leaderboard/players', key: 'leaderboards' },
|
||||||
{ href: '/analytics', key: 'analytics' },
|
{ href: '/analytics', key: 'analytics' },
|
||||||
|
{ href: '/timeline', key: 'timeline' },
|
||||||
{ href: '/docs', key: 'docs' },
|
{ href: '/docs', key: 'docs' },
|
||||||
]; %>
|
]; %>
|
||||||
<% navLinks.forEach(link => { %>
|
<% navLinks.forEach(link => { %>
|
||||||
|
|||||||
@@ -0,0 +1,568 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= lang %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<title>Timeline - <%= botName %></title>
|
||||||
|
<meta name="description" content="The history of <%= botName %> — from a Discord side-project to the best War Thunder Squadron Battles bot.">
|
||||||
|
<meta name="theme-color" content="#90EE90">
|
||||||
|
<link rel="icon" type="image/png" href="/images/transparent_toothlessssss.png">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
||||||
|
<link rel="preload" href="/Fonts/symbols_skyquake.ttf" as="font" type="font/ttf" crossorigin="anonymous">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Tailwind CSS -->
|
||||||
|
<link rel="stylesheet" href="/css/output.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" media="print" onload="this.media='all'">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'skyquakesymbols';
|
||||||
|
src: url('/Fonts/symbols_skyquake.ttf') format('truetype');
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #1b1b1b;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-text {
|
||||||
|
background: linear-gradient(135deg, #F5F5DC 0%, #90EE90 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-accent { color: #F5F5DC; }
|
||||||
|
.text-muted { color: #90EE90; }
|
||||||
|
|
||||||
|
/* ── Serpentine timeline ───────────────────────────────────────── */
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SVG that holds the flowing connector line, drawn behind the cards */
|
||||||
|
.timeline-line {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-line .track {
|
||||||
|
fill: none;
|
||||||
|
stroke: rgba(144, 238, 144, 0.10);
|
||||||
|
stroke-width: 4;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-line .progress {
|
||||||
|
fill: none;
|
||||||
|
stroke: url(#timelineGradient);
|
||||||
|
stroke-width: 4;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
filter: drop-shadow(0 0 6px rgba(144, 238, 144, 0.45));
|
||||||
|
/* dash values are set by JS once we know the path length */
|
||||||
|
transition: stroke-dashoffset 0.05s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-line .comet {
|
||||||
|
fill: #F5F5DC;
|
||||||
|
filter: drop-shadow(0 0 10px rgba(245, 245, 220, 0.9)) drop-shadow(0 0 18px rgba(144, 238, 144, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The grid of milestone nodes. Default = single column (mobile). */
|
||||||
|
.timeline-grid {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 3rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.timeline-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6rem 2.5rem;
|
||||||
|
}
|
||||||
|
/* The line snakes 3 → 3 → 2: row 1 left→right, row 2 right→left,
|
||||||
|
row 3 left→right, so the connector never crosses itself. */
|
||||||
|
.timeline-node:nth-child(1) { grid-area: 1 / 1; }
|
||||||
|
.timeline-node:nth-child(2) { grid-area: 1 / 2; }
|
||||||
|
.timeline-node:nth-child(3) { grid-area: 1 / 3; }
|
||||||
|
.timeline-node:nth-child(4) { grid-area: 2 / 3; }
|
||||||
|
.timeline-node:nth-child(5) { grid-area: 2 / 2; }
|
||||||
|
.timeline-node:nth-child(6) { grid-area: 2 / 1; }
|
||||||
|
.timeline-node:nth-child(7) { grid-area: 3 / 1; }
|
||||||
|
.timeline-node:nth-child(8) { grid-area: 3 / 2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-card {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(135deg, rgba(62, 78, 62, 0.22) 0%, rgba(44, 44, 44, 0.22) 100%);
|
||||||
|
border: 1px solid rgba(245, 245, 220, 0.08);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1.75rem;
|
||||||
|
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.35s ease, box-shadow 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: rgba(144, 238, 144, 0.35);
|
||||||
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node marker — the dot that the line threads through */
|
||||||
|
.timeline-marker {
|
||||||
|
position: absolute;
|
||||||
|
top: -1.4rem;
|
||||||
|
left: 1.75rem;
|
||||||
|
width: 2.6rem;
|
||||||
|
height: 2.6rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #1b1b1b;
|
||||||
|
background: linear-gradient(135deg, #F5F5DC 0%, #90EE90 100%);
|
||||||
|
box-shadow: 0 0 0 5px rgba(27, 27, 27, 1), 0 0 18px rgba(144, 238, 144, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-index {
|
||||||
|
font-family: 'skyquakesymbols', 'Inter', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(144, 238, 144, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-date {
|
||||||
|
color: #90EE90;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-title {
|
||||||
|
color: #F5F5DC;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-desc {
|
||||||
|
color: rgba(255, 255, 255, 0.78);
|
||||||
|
line-height: 1.65;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reveal animation — staggered as the line draws past each node */
|
||||||
|
.timeline-node {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(28px);
|
||||||
|
transition: opacity 0.6s ease, transform 0.6s cubic-bezier(0.2, 0.9, 0.25, 1);
|
||||||
|
}
|
||||||
|
.timeline-node.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.timeline-node { opacity: 1; transform: none; transition: none; }
|
||||||
|
.timeline-line .progress { transition: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="text-white antialiased">
|
||||||
|
<%- include('partials/nav', { activePage: 'timeline' }) %>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<section class="pt-32 pb-10 lg:pt-40 lg:pb-14">
|
||||||
|
<div class="max-w-[1400px] mx-auto px-6 lg:px-8 text-center">
|
||||||
|
<p class="text-muted text-sm font-semibold tracking-[0.25em] uppercase mb-4">Our Story</p>
|
||||||
|
<h1 class="text-4xl lg:text-6xl font-extrabold mb-5 gradient-text">The Timeline</h1>
|
||||||
|
<p class="text-lg text-white/70 max-w-2xl mx-auto">
|
||||||
|
How <%= botName %> grew from a late-night Discord experiment into the
|
||||||
|
go-to companion for War Thunder Squadron Battles. Follow the line.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Timeline -->
|
||||||
|
<section class="pb-28">
|
||||||
|
<div class="max-w-[1200px] mx-auto px-6 lg:px-12">
|
||||||
|
<div class="timeline" id="timeline">
|
||||||
|
<!-- Flowing connector line (drawn by JS through the node markers) -->
|
||||||
|
<svg class="timeline-line" id="timelineLine" preserveAspectRatio="none" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="timelineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stop-color="#90EE90"/>
|
||||||
|
<stop offset="100%" stop-color="#F5F5DC"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path class="track" id="timelineTrack" d=""></path>
|
||||||
|
<path class="progress" id="timelineProgress" d=""></path>
|
||||||
|
<circle class="comet" id="timelineComet" r="6" cx="0" cy="0" style="opacity:0"></circle>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="timeline-grid">
|
||||||
|
<!--
|
||||||
|
EDITABLE STARTER CONTENT — swap the dates / copy below for the real
|
||||||
|
project history. The serpentine line auto-routes through every
|
||||||
|
.timeline-node in order, so you can add or remove milestones freely.
|
||||||
|
-->
|
||||||
|
<article class="timeline-node">
|
||||||
|
<div class="timeline-card">
|
||||||
|
<div class="timeline-marker"><i class="fas fa-seedling"></i></div>
|
||||||
|
<div class="pt-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="timeline-index">01 · Origin</span>
|
||||||
|
<span class="timeline-date">June 2024</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="timeline-title mb-3">Born as SNLK SQB BOT</h3>
|
||||||
|
<p class="timeline-desc">
|
||||||
|
Ordered into existence by ImApollo and first christened
|
||||||
|
<strong class="text-accent">SNLK SQB BOT</strong>. The earliest build is up
|
||||||
|
and running in a rough state within a week — data still fed in by hand — and
|
||||||
|
within weeks of that prototype it's already being shared to other servers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="timeline-node">
|
||||||
|
<div class="timeline-card">
|
||||||
|
<div class="timeline-marker"><i class="fas fa-tag"></i></div>
|
||||||
|
<div class="pt-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="timeline-index">02 · Rebrand</span>
|
||||||
|
<span class="timeline-date">Late 2024</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="timeline-title mb-3">Becoming SREBOT</h3>
|
||||||
|
<p class="timeline-desc">
|
||||||
|
The project outgrows its original name. SNLK SQB BOT is rebranded to
|
||||||
|
<strong class="text-accent">SREBOT</strong> — the identity it still carries today.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="timeline-node">
|
||||||
|
<div class="timeline-card">
|
||||||
|
<div class="timeline-marker"><i class="fas fa-robot"></i></div>
|
||||||
|
<div class="pt-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="timeline-index">03 · Automation</span>
|
||||||
|
<span class="timeline-date">March 13, 2025</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="timeline-title mb-3">Dagor & The First Logs</h3>
|
||||||
|
<p class="timeline-desc">
|
||||||
|
Dagor is introduced and the first automatic log prototypes come online.
|
||||||
|
Parsing really starts to take off, and the earliest scoreboards take shape —
|
||||||
|
no more hand-feeding the data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="timeline-node">
|
||||||
|
<div class="timeline-card">
|
||||||
|
<div class="timeline-marker"><i class="fas fa-fire"></i></div>
|
||||||
|
<div class="pt-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="timeline-index">04 · Momentum</span>
|
||||||
|
<span class="timeline-date">Mid 2025</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="timeline-title mb-3">Word Gets Around</h3>
|
||||||
|
<p class="timeline-desc">
|
||||||
|
Squadrons start to take notice. The bot picks up real momentum and grows
|
||||||
|
in popularity as more communities bring it into their servers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="timeline-node">
|
||||||
|
<div class="timeline-card">
|
||||||
|
<div class="timeline-marker"><i class="fas fa-laptop-code"></i></div>
|
||||||
|
<div class="pt-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="timeline-index">05 · The Website</span>
|
||||||
|
<span class="timeline-date">Mid–Late 2025</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="timeline-title mb-3">The Site Goes Live</h3>
|
||||||
|
<p class="timeline-desc">
|
||||||
|
Friendly competition with the Boris bot pushes the developers to raise
|
||||||
|
their game — and the SREBOT website is born, turning stats, scoreboards and
|
||||||
|
replays into something you can actually explore in a browser.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="timeline-node">
|
||||||
|
<div class="timeline-card">
|
||||||
|
<div class="timeline-marker"><i class="fas fa-handshake"></i></div>
|
||||||
|
<div class="pt-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="timeline-index">06 · Partnership</span>
|
||||||
|
<span class="timeline-date">January 2026</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="timeline-title mb-3">Teaming Up with Spectra</h3>
|
||||||
|
<p class="timeline-desc">
|
||||||
|
The sheer volume of requests to Gaijin forces a change of approach. SREBOT
|
||||||
|
consolidates and partners with Spectra to receive games directly, instead of
|
||||||
|
downloading and parsing every match on its own.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="timeline-node">
|
||||||
|
<div class="timeline-card">
|
||||||
|
<div class="timeline-marker"><i class="fas fa-coins"></i></div>
|
||||||
|
<div class="pt-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="timeline-index">07 · Monetization</span>
|
||||||
|
<span class="timeline-date">April 2026</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="timeline-title mb-3">Fueling the Future</h3>
|
||||||
|
<p class="timeline-desc">
|
||||||
|
Monetization brings in real income for the first time — funding better
|
||||||
|
servers and letting the project develop new features faster than ever.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="timeline-node">
|
||||||
|
<div class="timeline-card">
|
||||||
|
<div class="timeline-marker"><i class="fas fa-location-dot"></i></div>
|
||||||
|
<div class="pt-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="timeline-index">08 · Today</span>
|
||||||
|
<span class="timeline-date">Now</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="timeline-title mb-3">Where We Are Now</h3>
|
||||||
|
<p class="timeline-desc">
|
||||||
|
Automated parsing through Spectra, funded development, and a growing
|
||||||
|
community of squadrons — this is where the story stands today. The line
|
||||||
|
doesn't stop here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-20">
|
||||||
|
<a href="/" class="btn-primary px-8 py-4 rounded-xl text-base font-bold inline-flex items-center"
|
||||||
|
style="background: linear-gradient(135deg, #F5F5DC 0%, #E8E8D0 100%); color:#1E1E1E;">
|
||||||
|
<i class="fas fa-home mr-3"></i><%= t('common.backToHome') %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<%- include('partials/footer') %>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.__lang = '<%= lang %>';
|
||||||
|
window.__i18n = <%- localeJson %>;
|
||||||
|
window.__t = function(key) {
|
||||||
|
var parts = key.split('.'), obj = window.__i18n;
|
||||||
|
for (var i = 0; i < parts.length; i++) { obj = obj && obj[parts[i]]; }
|
||||||
|
return obj !== undefined ? obj : key;
|
||||||
|
};
|
||||||
|
window.switchLanguage = function(lang) {
|
||||||
|
var next = lang || (document.documentElement.lang === 'en' ? 'ru' : 'en');
|
||||||
|
if (next === document.documentElement.lang) return;
|
||||||
|
document.cookie = 'lang=' + next + ';path=/;max-age=31536000;SameSite=Lax';
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script src="/js/main.js?v=3"></script>
|
||||||
|
|
||||||
|
<!-- Serpentine timeline renderer -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var timeline = document.getElementById('timeline');
|
||||||
|
var svg = document.getElementById('timelineLine');
|
||||||
|
var track = document.getElementById('timelineTrack');
|
||||||
|
var progress = document.getElementById('timelineProgress');
|
||||||
|
var comet = document.getElementById('timelineComet');
|
||||||
|
if (!timeline || !svg) return;
|
||||||
|
|
||||||
|
var nodes = Array.prototype.slice.call(timeline.querySelectorAll('.timeline-node'));
|
||||||
|
var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
var drawProgress = 0; // 0 → 1, how much of the line is drawn
|
||||||
|
var animating = false;
|
||||||
|
|
||||||
|
// Build a rounded-corner path that threads through a list of points.
|
||||||
|
function roundedPath(pts, radius) {
|
||||||
|
if (pts.length < 2) return '';
|
||||||
|
var d = 'M ' + pts[0].x.toFixed(1) + ' ' + pts[0].y.toFixed(1);
|
||||||
|
for (var i = 1; i < pts.length - 1; i++) {
|
||||||
|
var p0 = pts[i - 1], p1 = pts[i], p2 = pts[i + 1];
|
||||||
|
var v1 = unit(p1, p0), v2 = unit(p1, p2);
|
||||||
|
var r1 = Math.min(radius, dist(p0, p1) / 2);
|
||||||
|
var r2 = Math.min(radius, dist(p2, p1) / 2);
|
||||||
|
var a = { x: p1.x + v1.x * r1, y: p1.y + v1.y * r1 };
|
||||||
|
var b = { x: p1.x + v2.x * r2, y: p1.y + v2.y * r2 };
|
||||||
|
d += ' L ' + a.x.toFixed(1) + ' ' + a.y.toFixed(1);
|
||||||
|
d += ' Q ' + p1.x.toFixed(1) + ' ' + p1.y.toFixed(1) + ' ' + b.x.toFixed(1) + ' ' + b.y.toFixed(1);
|
||||||
|
}
|
||||||
|
var last = pts[pts.length - 1];
|
||||||
|
d += ' L ' + last.x.toFixed(1) + ' ' + last.y.toFixed(1);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
function unit(from, to) {
|
||||||
|
var dx = to.x - from.x, dy = to.y - from.y, len = Math.hypot(dx, dy) || 1;
|
||||||
|
return { x: dx / len, y: dy / len };
|
||||||
|
}
|
||||||
|
function dist(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); }
|
||||||
|
|
||||||
|
function build() {
|
||||||
|
var box = timeline.getBoundingClientRect();
|
||||||
|
svg.setAttribute('viewBox', '0 0 ' + box.width + ' ' + box.height);
|
||||||
|
|
||||||
|
// Anchor point of each node = centre of its circular marker.
|
||||||
|
var pts = nodes.map(function (node) {
|
||||||
|
var marker = node.querySelector('.timeline-marker');
|
||||||
|
var r = (marker || node).getBoundingClientRect();
|
||||||
|
return { x: r.left - box.left + r.width / 2, y: r.top - box.top + r.height / 2 };
|
||||||
|
});
|
||||||
|
|
||||||
|
var d = roundedPath(pts, 36);
|
||||||
|
track.setAttribute('d', d);
|
||||||
|
progress.setAttribute('d', d);
|
||||||
|
|
||||||
|
var len = progress.getTotalLength();
|
||||||
|
progress.style.strokeDasharray = len;
|
||||||
|
progress.style.strokeDashoffset = len * (1 - drawProgress);
|
||||||
|
updateComet(len);
|
||||||
|
return { pts: pts, len: len };
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateComet(len) {
|
||||||
|
if (reduceMotion || drawProgress <= 0 || drawProgress >= 1) {
|
||||||
|
comet.style.opacity = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var pt = progress.getPointAtLength(len * drawProgress);
|
||||||
|
comet.setAttribute('cx', pt.x);
|
||||||
|
comet.setAttribute('cy', pt.y);
|
||||||
|
comet.style.opacity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reveal a node once the drawn line has reached its anchor.
|
||||||
|
function revealNodes() {
|
||||||
|
var len = progress.getTotalLength();
|
||||||
|
var drawnLen = len * drawProgress;
|
||||||
|
nodes.forEach(function (node) {
|
||||||
|
if (node.classList.contains('is-visible')) return;
|
||||||
|
var marker = node.querySelector('.timeline-marker');
|
||||||
|
var r = (marker || node).getBoundingClientRect();
|
||||||
|
var box = timeline.getBoundingClientRect();
|
||||||
|
var pt = { x: r.left - box.left + r.width / 2, y: r.top - box.top + r.height / 2 };
|
||||||
|
// approximate the node's distance along the path
|
||||||
|
var nodeLen = lengthAtPoint(pt, len);
|
||||||
|
if (drawnLen >= nodeLen - 8) node.classList.add('is-visible');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find roughly how far along the path a point sits (coarse sampling).
|
||||||
|
var _samples = null;
|
||||||
|
function lengthAtPoint(target, len) {
|
||||||
|
if (!_samples) {
|
||||||
|
_samples = [];
|
||||||
|
var steps = 120;
|
||||||
|
for (var i = 0; i <= steps; i++) {
|
||||||
|
var l = len * (i / steps);
|
||||||
|
var p = progress.getPointAtLength(l);
|
||||||
|
_samples.push({ l: l, x: p.x, y: p.y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var best = 0, bestD = Infinity;
|
||||||
|
for (var j = 0; j < _samples.length; j++) {
|
||||||
|
var dd = Math.hypot(_samples[j].x - target.x, _samples[j].y - target.y);
|
||||||
|
if (dd < bestD) { bestD = dd; best = _samples[j].l; }
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateDraw() {
|
||||||
|
if (animating) return;
|
||||||
|
animating = true;
|
||||||
|
var start = null;
|
||||||
|
var duration = 2600; // ms for the line to travel the whole serpentine
|
||||||
|
function step(ts) {
|
||||||
|
if (start === null) start = ts;
|
||||||
|
var p = Math.min((ts - start) / duration, 1);
|
||||||
|
// ease-in-out
|
||||||
|
drawProgress = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
||||||
|
var len = progress.getTotalLength();
|
||||||
|
progress.style.strokeDashoffset = len * (1 - drawProgress);
|
||||||
|
updateComet(len);
|
||||||
|
revealNodes();
|
||||||
|
if (p < 1) {
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
} else {
|
||||||
|
drawProgress = 1;
|
||||||
|
progress.style.strokeDashoffset = 0;
|
||||||
|
comet.style.opacity = 0;
|
||||||
|
nodes.forEach(function (n) { n.classList.add('is-visible'); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
build();
|
||||||
|
if (reduceMotion) {
|
||||||
|
drawProgress = 1;
|
||||||
|
progress.style.strokeDashoffset = 0;
|
||||||
|
nodes.forEach(function (n) { n.classList.add('is-visible'); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Start drawing once the timeline scrolls into view.
|
||||||
|
var io = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (e) {
|
||||||
|
if (e.isIntersecting) { animateDraw(); io.disconnect(); }
|
||||||
|
});
|
||||||
|
}, { threshold: 0.2 });
|
||||||
|
io.observe(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute geometry on resize (layout flips between 1 and 3 columns).
|
||||||
|
var resizeTimer;
|
||||||
|
window.addEventListener('resize', function () {
|
||||||
|
clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(function () { _samples = null; build(); }, 120);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
// Re-measure after fonts/icons settle so markers are positioned correctly.
|
||||||
|
window.addEventListener('load', function () { _samples = null; build(); });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user