Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
/* --d is the disc's diameter and --cy its centre. Rings, disc, orbiters, copy and astronaut all hang off the two,
so the composition holds together at any screen shape. Wide screens keep the copy centred over the disc. */
.hero{--d:min(84vh,72vw);--cy:53%}
.hero-ring{position:absolute;left:calc(50% - var(--d) * var(--r));top:calc(var(--cy) - var(--d) * var(--r));width:calc(var(--d) * var(--r) * 2);height:calc(var(--d) * var(--r) * 2);border-radius:50%}
/* will-change: the entrance and leave animations scale the rings; without it each frame re-rasters them at the new scale. */
.hero-ring{position:absolute;left:calc(50% - var(--d) * var(--r));top:calc(var(--cy) - var(--d) * var(--r));width:calc(var(--d) * var(--r) * 2);height:calc(var(--d) * var(--r) * 2);border-radius:50%;will-change:transform}
.hero-disc{position:absolute;left:calc(50% - var(--d) / 2);top:calc(var(--cy) - var(--d) / 2);width:var(--d);height:var(--d)}
/* The radar sweep lives in .hero-copy (so it can sit between the title and the rest of the copy) but covers the disc exactly. */
.hero-sweep{position:absolute;left:calc(50% - var(--d) / 2);top:calc(var(--cy) - var(--d) / 2);width:var(--d);height:var(--d)}
Expand All @@ -211,7 +212,9 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
.hero-copy-disc{display:contents}
/* --ls is the tracking; the same amount of left padding balances the trailing letter-space so the text centres. */
.hero-track{padding-left:var(--ls)}
.hero-kicker{--ls:.42em;gap:14px;font-size:clamp(11px,1.05vw,16px);letter-spacing:var(--ls)}
.hero-kicker{--ls:.42em;gap:.55em;font-size:clamp(11px,1.05vw,16px);letter-spacing:var(--ls)}
/* Second kicker line: smaller and lighter, so NIT ROURKELA leads. --ls is in em, so the centring pad shrinks with it. */
.hero-kicker-sub{font-size:.72em;font-weight:500;opacity:.78}
.hero-title{margin:clamp(10px,2vh,22px) 0 clamp(8px,1.4vh,16px);font-size:clamp(36px,12.4vw,224px)}
.hero-sub{--ls:.32em;gap:18px;font-size:clamp(12px,1.35vw,21px);letter-spacing:var(--ls)}
.hero-sub-rule{width:clamp(24px,4vw,64px);flex:none}
Expand All @@ -232,7 +235,7 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
.hero-copy-disc{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:none;height:var(--d)}
/* .hero-copy starts at the disc's top edge here. */
.hero-sweep{top:0}
.hero-kicker{--ls:.26em;gap:8px;font-size:clamp(9.5px,calc(var(--d) * .033),15px)}
.hero-kicker{--ls:.26em;gap:.5em;font-size:clamp(9.5px,calc(var(--d) * .033),15px)}
.hero-title{margin:calc(var(--d) * .03) 0 calc(var(--d) * .026);font-size:calc(var(--d) * .122)}
.hero-sub{--ls:.18em;gap:10px;font-size:clamp(10px,calc(var(--d) * .034),17px)}
.hero-sub-rule{width:calc(var(--d) * .05)}
Expand Down Expand Up @@ -273,6 +276,9 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o

/* ---------- compact menu ---------- */
.menu-panel{position:absolute;inset:0;display:flex;flex-direction:column;overflow:hidden;background:#141312;color:#ECE8DF}
/* The panel opens and closes by animating its clip-path circle. With its contents on their own layers only the mask
changes each frame; otherwise the full-screen starfield and list were re-rastered every frame of the reveal. */
.menu-panel>*{will-change:transform}
.menu-top{position:relative;display:flex;align-items:center;justify-content:space-between;gap:16px;flex:none;padding:clamp(14px,1.8vw,26px) clamp(16px,2.6vw,44px)}
.menu-brand{display:inline-flex;align-items:center;gap:10px;color:#ECE8DF;text-decoration:none}
.menu-brand:hover{color:oklch(0.84 0.09 85)}
Expand Down Expand Up @@ -496,6 +502,9 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
.hud-frost,.wn-pill{-webkit-backdrop-filter:none !important;backdrop-filter:none !important}
.hud-frost{background:rgba(236,232,223,.97) !important}
.wn-pill{background:rgba(236,232,223,.96) !important}
/* The bar keeps one height: easing its padding as it turns solid relaid and repainted it every frame, and the bar
visibly bounced mid-scroll. */
.hud-bar{padding:clamp(14px,1.8vw,26px) clamp(16px,2.6vw,44px) !important;transition:none !important}
}

/* ---------- schedule ---------- */
Expand Down
2 changes: 1 addition & 1 deletion src/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export const size = OG_SIZE;
export const contentType = 'image/png';

export default function OpengraphImage() {
return ogCard({ kicker: 'NIT ROURKELA SAC PRESENTS', title: 'INNOVISION', subtitle: '2026 · THE CELESTIAL ODYSSEY' });
return ogCard({ kicker: 'NIT ROURKELA', kickerSub: 'STUDENT ACTIVITY CENTRE PRESENTS', title: 'INNOVISION', subtitle: '2026 · THE CELESTIAL ODYSSEY' });
}
9 changes: 5 additions & 4 deletions src/components/innovision/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@ export default function HomeView({ v }: { v: V }) {
</div>
<div className="hero-copy" style={{ display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center", pointerEvents: "none" }}>
{/* Wide screens: display contents, so copy and buttons share one centred column. Stacked screens: a box the size of the disc.
The page's one heading is the whole visible line-up: NIT ROURKELA SAC PRESENTS / INNOVISION / 2026 · THE CELESTIAL ODYSSEY.
The page's one heading is the whole visible line-up: NIT ROURKELA / STUDENT ACTIVITY CENTRE PRESENTS / INNOVISION / 2026 · THE CELESTIAL ODYSSEY.
Its parts are spans (phrasing content); the spaces between them are not rendered between flex items but keep the words apart in the text. */}
<h1 aria-label="NIT Rourkela SAC presents Innovision 2026: The Celestial Odyssey" className="hero-copy-disc" style={{ margin: "0", fontSize: "inherit", fontWeight: "inherit" }}>
<span data-h-kicker="" className="hero-kicker" style={{ display: "flex", alignItems: "center", color: "#ECE8DF", fontFamily: "var(--font-sans)", fontWeight: "700", whiteSpace: "nowrap" }}>
<span className="hero-track">NIT ROURKELA SAC PRESENTS</span>
<h1 aria-label="NIT Rourkela Student Activity Centre presents Innovision 2026: The Celestial Odyssey" className="hero-copy-disc" style={{ margin: "0", fontSize: "inherit", fontWeight: "inherit" }}>
<span data-h-kicker="" className="hero-kicker" style={{ display: "flex", flexDirection: "column", alignItems: "center", color: "#ECE8DF", fontFamily: "var(--font-sans)", fontWeight: "700", whiteSpace: "nowrap" }}>
<span className="hero-track">NIT ROURKELA</span>{" "}
<span className="hero-track hero-kicker-sub">STUDENT ACTIVITY CENTRE PRESENTS</span>
</span>{" "}
<span className="hero-title" style={{ display: "flex", fontFamily: "var(--font-display)", fontWeight: "400", lineHeight: ".95", letterSpacing: ".01em", color: "#fff", mixBlendMode: "difference", whiteSpace: "nowrap" }}>
{v.heroChars.map((c, cI) => (
Expand Down
38 changes: 31 additions & 7 deletions src/components/innovision/Innovision.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/ban-ts-comment -- GSAP class component uses @ts-ignore extensively for dynamic internal state */
// @ts-nocheck
"use client";
import { CLICKABLE, preloadClick, unlockClick, playClick } from './clickSound';
import { CLICKABLE, preloadClick, warmClick, unlockClick, playClick } from './clickSound';
/* eslint-disable @typescript-eslint/no-explicit-any -- view-model is dynamically constructed and consumed across many child views */
/* eslint-disable @typescript-eslint/no-unused-vars -- some destructured vars are kept for future use */

Expand Down Expand Up @@ -262,14 +262,17 @@ export class Innovision extends Component<Props, State> {
try { this.hintSeen = localStorage.getItem(HINT_KEY) === '1'; } catch {}
this.setState({ saved, compact: innerWidth < 1100, narrow: innerWidth < 720, coarse: matchMedia('(pointer: coarse)').matches });
if (lowPowerDevice()) this.goLowPower();
// Resize work forces layout (title fit, map panels), so it runs at most once per frame.
let rz = 0;
// Resize work forces layout (title fit, map panels), so it runs at most once per frame. Both depend on the width
// only: a phone's toolbar or keyboard changes just the height, and re-tweening the map panels then cost a second of layout.
let rz = 0, rw = innerWidth;
this.listen(window, 'resize', () => {
if (rz) return;
rz = requestAnimationFrame(() => {
rz = 0;
const c = innerWidth < 1100, n = innerWidth < 720;
if (c !== this.state.compact || n !== this.state.narrow) this.setState({ compact: c, narrow: n }, () => { if (this.state.view === 'detail') this.setupDetailScroll(true); });
if (innerWidth === rw) return;
rw = innerWidth;
this.fitTitle();
if (this.ctx) this.openMap(this.mapOpen);
});
Expand Down Expand Up @@ -666,6 +669,8 @@ export class Innovision extends Component<Props, State> {
/** The only sound is the click (clickSound.ts): every button, link or tab clicked anywhere answers with a tick. */
sound() {
preloadClick();
// Created while the loader counts up, so that a first tap during the landing animation doesn't stall it either.
this.cleanups.push(this.idle(warmClick));
['pointerdown', 'keydown'].forEach((ev) => this.listen(window, ev, unlockClick, { once: true, capture: true }));
this.listen(document, 'click', (e) => {
const t = (e.target as Element | null)?.closest?.(CLICKABLE);
Expand Down Expand Up @@ -702,6 +707,12 @@ export class Innovision extends Component<Props, State> {
// The overlay and view a visitor is most likely to open next are fetched ahead too (their own hover/tap also does).
// The pages of their own too, so MERCH (and REGISTER while closed) open at once (a no-op on the dev server).
this.cleanups.push(this.idle(() => { this.prefetch('auth'); this.prefetch('detail'); OWN_PAGES.forEach((p) => this.props.router?.prefetch(p)); }));
// The menu (where the top bar has a MENU button) and the register / log-in overlay are mounted, closed, ahead of
// time: mounting one on its first tap blocked a phone for ~100 ms just as its reveal began. One per idle period.
this.cleanups.push(this.idle(() => {
if (this.state.compact) void this.mount('menu');
this.cleanups.push(this.idle(() => { void this.mount('auth'); }));
}));
const queue = [...PRELOAD_DEFERRED];
let cancel = () => {}, live = true;
const batch = () => {
Expand Down Expand Up @@ -1528,7 +1539,9 @@ export class Innovision extends Component<Props, State> {
this.pEls = v ? [...v.querySelectorAll<HTMLElement>('[data-depth]')].filter((el) => { const s = el.closest<HTMLElement>('[data-slide]'); return !s || +(s.dataset.slide || 0) === this.state.index; }) : [];
}
hover = (e: MouseEvent<HTMLElement>) => {
if (this.reduce || !this.ctx) return;
// Touch screens fire mouseenter on every tap: the scramble swaps in letters of other widths, so the tapped label
// (MENU, REGISTER) changed width for half a second and the top bar around it shook. Hover effects are for mice only.
if (this.reduce || !this.ctx || this.state.coarse) return;
const sp = e.currentTarget.querySelector<HTMLElement>('[data-scr]');
if (!sp) return;
sp.dataset.text = sp.dataset.text || sp.textContent || '';
Expand Down Expand Up @@ -1922,7 +1935,8 @@ export class Innovision extends Component<Props, State> {
pagePush(z) {
const sc = z ? String(1 + .12 * z) : '', o = this.authO, to = z && o ? o.x.toFixed(0) + 'px ' + o.y.toFixed(0) + 'px' : '';
// @ts-ignore
(this.page || []).forEach((el) => { el.style.scale = sc; el.style.transformOrigin = to; });
// will-change while pushed: without it every frame of the zoom re-rastered the whole page (the home page is many screens tall).
(this.page || []).forEach((el) => { el.style.scale = sc; el.style.transformOrigin = to; el.style.willChange = sc ? 'transform' : ''; });
const veil = this.$ && this.$('[data-rift-veil]'); if (veil) veil.style.opacity = String(z * .85);
}
// @ts-ignore
Expand Down Expand Up @@ -2038,13 +2052,14 @@ export class Innovision extends Component<Props, State> {
return;
}
g.set(ins, { autoAlpha: 0, y: 26 }); g.set(planet, { autoAlpha: 0, scale: .7, yPercent: 16 }); g.set(ui, { scale: .94 });
this.authLayers(true);
const r = this._rift = this.riftNew(0, 0, 1, 0);
this.riftDraw();
const k = this.speed();
this.warp(2.4 / k);
// @ts-ignore
this.authTl = g.timeline({ onUpdate: () => this.riftDraw(), onComplete: () => {
this._rift = null; sec.style.clipPath = 'none';
this._rift = null; sec.style.clipPath = 'none'; this.authLayers(false);
this.pagePush(0); this.authBusy = false; this.focusAuth();
} })
.fromTo(sec, { autoAlpha: fromDisc ? 0 : 1 }, { autoAlpha: 1, duration: fromDisc ? .35 : .01, ease: 'power1.out' }, 0)
Expand All @@ -2065,14 +2080,15 @@ export class Innovision extends Component<Props, State> {
this.page = this.livePage();
// @ts-ignore
const done = () => {
this._rift = null;
this._rift = null; this.authLayers(false);
g.set(root, { autoAlpha: 0, pointerEvents: 'none' }); g.set(sec, { autoAlpha: 1 }); sec.style.clipPath = 'none';
this.pagePush(0);
this.authSpin(false); this.authClosing = false; this.authBusy = false;
this.setState({ auth: false, err: {}, busyLbl: '', drag: '' });
if (after) after();
};
if (this.reduce) { g.to(sec, { autoAlpha: 0, duration: .3, onComplete: () => { g.set(sec, { autoAlpha: 1 }); done(); } }); return; }
this.authLayers(true);
const r = this._rift = this.riftNew(1, 1, 0, 1);
this.riftDraw();
// @ts-ignore
Expand All @@ -2090,11 +2106,19 @@ export class Innovision extends Component<Props, State> {
if (this.authTl) this.authTl.kill();
clearTimeout(this._wait); this.warpStop(); this._rift = null;
const sec = this.$('[data-auth]'); if (sec) { sec.style.clipPath = 'none'; gsap.set(sec, { autoAlpha: 1 }); }
this.authLayers(false);
gsap.set(this.$('[data-auth-root]'), { autoAlpha: 0, pointerEvents: 'none' });
this.pagePush(0);
this.authSpin(false); this.authBusy = false; this.authClosing = false;
this.setState({ auth: false, busyLbl: '', drag: '' });
}
/**
* Gives the form and its planet their own layers while the rift opens or closes: both scale, and the rift's clip
* changes every frame, which otherwise re-rastered them each frame. Dropped once still, so the form stays crisp.
*/
authLayers(on: boolean) {
this.$$('[data-a-ui],[data-a-planet-wrap]').forEach((el) => { el.style.willChange = on ? 'transform' : ''; });
}
// @ts-ignore
authSpin(on) {
// @ts-ignore
Expand Down
13 changes: 9 additions & 4 deletions src/components/innovision/Loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const ORBS: [string, string, number, number, number, string][] = [
['26%', '1px solid rgba(20,19,18,.3)', 0, 128, 16, 'planet-mars.webp'],
];
const DISC = 'min(58vmin, 540px, 100vh - 330px)';
/** The rings scale in and out; their own layer keeps the conic gradients and masks from re-rastering every frame. */
const RING = { position: "absolute", borderRadius: "50%", willChange: "transform" } as const;

/** Orbital preloader. */
export default function Loader({ v }: { v: V }) {
Expand All @@ -29,13 +31,13 @@ export default function Loader({ v }: { v: V }) {
<div data-l-fade="" data-l-corner="" style={{ position: "absolute", top: "clamp(16px,2.6vw,44px)", right: "clamp(16px,2.6vw,44px)", fontWeight: "500", fontSize: "13px", letterSpacing: ".3em" }}>MMXXVI</div>
<CornerFrame data-l-fade="" data-l-corner="" color="rgba(20,19,18,.45)" style={{ top: "calc(clamp(16px,2.6vw,44px) + 34px)", bottom: "clamp(16px,2.6vw,44px)" }} />
<div style={{ position: "absolute", left: `calc(50% - ${DISC} / 2)`, top: `calc(40% - ${DISC} / 2)`, width: DISC, height: DISC }}>
<div data-l-ring="" style={{ position: "absolute", inset: "-4%", borderRadius: "50%", background: "repeating-conic-gradient(from -.25deg,rgba(20,19,18,.5) 0deg .5deg,transparent .5deg 3.6deg)", WebkitMaskImage: "radial-gradient(circle closest-side,transparent 94%,#000 94.5%,#000 98.5%,transparent 99%)", maskImage: "radial-gradient(circle closest-side,transparent 94%,#000 94.5%,#000 98.5%,transparent 99%)" }}></div>
<div data-l-ring="" style={{ position: "absolute", inset: "-4%", borderRadius: "50%", background: "repeating-conic-gradient(from -.4deg,rgba(20,19,18,.8) 0deg .8deg,transparent .8deg 36deg)", WebkitMaskImage: "radial-gradient(circle closest-side,transparent 90%,#000 90.5%,#000 98.5%,transparent 99%)", maskImage: "radial-gradient(circle closest-side,transparent 90%,#000 90.5%,#000 98.5%,transparent 99%)" }}></div>
<div data-l-ring="" style={{ ...RING, inset: "-4%", background: "repeating-conic-gradient(from -.25deg,rgba(20,19,18,.5) 0deg .5deg,transparent .5deg 3.6deg)", WebkitMaskImage: "radial-gradient(circle closest-side,transparent 94%,#000 94.5%,#000 98.5%,transparent 99%)", maskImage: "radial-gradient(circle closest-side,transparent 94%,#000 94.5%,#000 98.5%,transparent 99%)" }}></div>
<div data-l-ring="" style={{ ...RING, inset: "-4%", background: "repeating-conic-gradient(from -.4deg,rgba(20,19,18,.8) 0deg .8deg,transparent .8deg 36deg)", WebkitMaskImage: "radial-gradient(circle closest-side,transparent 90%,#000 90.5%,#000 98.5%,transparent 99%)", maskImage: "radial-gradient(circle closest-side,transparent 90%,#000 90.5%,#000 98.5%,transparent 99%)" }}></div>
<svg data-l-fade="" viewBox="0 0 100 100" style={{ position: "absolute", inset: "0", width: "100%", height: "100%", overflow: "visible" }}>
<polyline data-l-link="" points="50,50 50,26 50,14 50,0" fill="none" stroke="#8a6a2a" strokeWidth=".45" strokeLinecap="round" strokeDasharray="1.2 1.8"></polyline>
</svg>
{ORBS.map(([inset, border, i, a0, size, img]) => (
<div key={i} data-l-ring="" style={{ position: "absolute", inset, borderRadius: "50%", border }}>
<div key={i} data-l-ring="" style={{ ...RING, inset, border }}>
<div data-l-orb="" data-i={i} data-a0={a0} style={{ position: "absolute", inset: "0" }}>
<div style={{ position: "absolute", left: "50%", top: "0", width: size + "%", height: size + "%", margin: `-${size / 2}% 0 0 -${size / 2}%` }}>
<span data-l-lock="" style={{ position: "absolute", inset: "-30%", borderRadius: "50%", border: "1.5px solid #8a6a2a", opacity: "0" }}></span>
Expand All @@ -60,7 +62,10 @@ export default function Loader({ v }: { v: V }) {
</div>
<div style={{ position: "absolute", left: "0", right: "0", bottom: "clamp(30px,6.5vh,80px)", display: "flex", flexDirection: "column", alignItems: "center", gap: "10px", textAlign: "center" }}>
<div data-l-fade="" style={{ overflow: "hidden" }}>
<div data-l-line="" style={{ fontSize: "12px", fontWeight: "500", letterSpacing: ".34em" }}>NIT ROURKELA SAC PRESENTS</div>
<div data-l-line="" style={{ fontSize: "12px", fontWeight: "500", letterSpacing: ".34em" }}>
NIT ROURKELA
<span style={{ display: "block", marginTop: "6px", fontSize: "10px", letterSpacing: ".24em", opacity: ".75" }}>STUDENT ACTIVITY CENTRE PRESENTS</span>
</div>
</div>
<div data-l-fade="" style={{ overflow: "hidden" }}>
<div data-l-line="" style={{ fontFamily: "var(--font-display)", fontWeight: "400", fontSize: "clamp(24px,2.6vw,38px)", letterSpacing: ".12em" }}>INNOVISION 2026</div>
Expand Down
Loading
Loading