Skip to content

Commit 57c83ca

Browse files
authored
Merge pull request #55 from OpencodeNIT-R/development
Development
2 parents 99970de + 3862d5b commit 57c83ca

8 files changed

Lines changed: 111 additions & 43 deletions

File tree

‎src/app/globals.css‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
192192
/* --d is the disc's diameter and --cy its centre. Rings, disc, orbiters, copy and astronaut all hang off the two,
193193
so the composition holds together at any screen shape. Wide screens keep the copy centred over the disc. */
194194
.hero{--d:min(84vh,72vw);--cy:53%}
195-
.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%}
195+
/* will-change: the entrance and leave animations scale the rings; without it each frame re-rasters them at the new scale. */
196+
.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}
196197
.hero-disc{position:absolute;left:calc(50% - var(--d) / 2);top:calc(var(--cy) - var(--d) / 2);width:var(--d);height:var(--d)}
197198
/* 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. */
198199
.hero-sweep{position:absolute;left:calc(50% - var(--d) / 2);top:calc(var(--cy) - var(--d) / 2);width:var(--d);height:var(--d)}
@@ -211,7 +212,9 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
211212
.hero-copy-disc{display:contents}
212213
/* --ls is the tracking; the same amount of left padding balances the trailing letter-space so the text centres. */
213214
.hero-track{padding-left:var(--ls)}
214-
.hero-kicker{--ls:.42em;gap:14px;font-size:clamp(11px,1.05vw,16px);letter-spacing:var(--ls)}
215+
.hero-kicker{--ls:.42em;gap:.55em;font-size:clamp(11px,1.05vw,16px);letter-spacing:var(--ls)}
216+
/* Second kicker line: smaller and lighter, so NIT ROURKELA leads. --ls is in em, so the centring pad shrinks with it. */
217+
.hero-kicker-sub{font-size:.72em;font-weight:500;opacity:.78}
215218
.hero-title{margin:clamp(10px,2vh,22px) 0 clamp(8px,1.4vh,16px);font-size:clamp(36px,12.4vw,224px)}
216219
.hero-sub{--ls:.32em;gap:18px;font-size:clamp(12px,1.35vw,21px);letter-spacing:var(--ls)}
217220
.hero-sub-rule{width:clamp(24px,4vw,64px);flex:none}
@@ -232,7 +235,7 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
232235
.hero-copy-disc{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:none;height:var(--d)}
233236
/* .hero-copy starts at the disc's top edge here. */
234237
.hero-sweep{top:0}
235-
.hero-kicker{--ls:.26em;gap:8px;font-size:clamp(9.5px,calc(var(--d) * .033),15px)}
238+
.hero-kicker{--ls:.26em;gap:.5em;font-size:clamp(9.5px,calc(var(--d) * .033),15px)}
236239
.hero-title{margin:calc(var(--d) * .03) 0 calc(var(--d) * .026);font-size:calc(var(--d) * .122)}
237240
.hero-sub{--ls:.18em;gap:10px;font-size:clamp(10px,calc(var(--d) * .034),17px)}
238241
.hero-sub-rule{width:calc(var(--d) * .05)}
@@ -273,6 +276,9 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
273276

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

501510
/* ---------- schedule ---------- */

‎src/app/opengraph-image.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ export const size = OG_SIZE;
99
export const contentType = 'image/png';
1010

1111
export default function OpengraphImage() {
12-
return ogCard({ kicker: 'NIT ROURKELA SAC PRESENTS', title: 'INNOVISION', subtitle: '2026 · THE CELESTIAL ODYSSEY' });
12+
return ogCard({ kicker: 'NIT ROURKELA', kickerSub: 'STUDENT ACTIVITY CENTRE PRESENTS', title: 'INNOVISION', subtitle: '2026 · THE CELESTIAL ODYSSEY' });
1313
}

‎src/components/innovision/HomeView.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,12 @@ export default function HomeView({ v }: { v: V }) {
5555
</div>
5656
<div className="hero-copy" style={{ display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center", pointerEvents: "none" }}>
5757
{/* Wide screens: display contents, so copy and buttons share one centred column. Stacked screens: a box the size of the disc.
58-
The page's one heading is the whole visible line-up: NIT ROURKELA SAC PRESENTS / INNOVISION / 2026 · THE CELESTIAL ODYSSEY.
58+
The page's one heading is the whole visible line-up: NIT ROURKELA / STUDENT ACTIVITY CENTRE PRESENTS / INNOVISION / 2026 · THE CELESTIAL ODYSSEY.
5959
Its parts are spans (phrasing content); the spaces between them are not rendered between flex items but keep the words apart in the text. */}
60-
<h1 aria-label="NIT Rourkela SAC presents Innovision 2026: The Celestial Odyssey" className="hero-copy-disc" style={{ margin: "0", fontSize: "inherit", fontWeight: "inherit" }}>
61-
<span data-h-kicker="" className="hero-kicker" style={{ display: "flex", alignItems: "center", color: "#ECE8DF", fontFamily: "var(--font-sans)", fontWeight: "700", whiteSpace: "nowrap" }}>
62-
<span className="hero-track">NIT ROURKELA SAC PRESENTS</span>
60+
<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" }}>
61+
<span data-h-kicker="" className="hero-kicker" style={{ display: "flex", flexDirection: "column", alignItems: "center", color: "#ECE8DF", fontFamily: "var(--font-sans)", fontWeight: "700", whiteSpace: "nowrap" }}>
62+
<span className="hero-track">NIT ROURKELA</span>{" "}
63+
<span className="hero-track hero-kicker-sub">STUDENT ACTIVITY CENTRE PRESENTS</span>
6364
</span>{" "}
6465
<span className="hero-title" style={{ display: "flex", fontFamily: "var(--font-display)", fontWeight: "400", lineHeight: ".95", letterSpacing: ".01em", color: "#fff", mixBlendMode: "difference", whiteSpace: "nowrap" }}>
6566
{v.heroChars.map((c, cI) => (

‎src/components/innovision/Innovision.tsx‎

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/ban-ts-comment -- GSAP class component uses @ts-ignore extensively for dynamic internal state */
22
// @ts-nocheck
33
"use client";
4-
import { CLICKABLE, preloadClick, unlockClick, playClick } from './clickSound';
4+
import { CLICKABLE, preloadClick, warmClick, unlockClick, playClick } from './clickSound';
55
/* eslint-disable @typescript-eslint/no-explicit-any -- view-model is dynamically constructed and consumed across many child views */
66
/* eslint-disable @typescript-eslint/no-unused-vars -- some destructured vars are kept for future use */
77

@@ -262,14 +262,17 @@ export class Innovision extends Component<Props, State> {
262262
try { this.hintSeen = localStorage.getItem(HINT_KEY) === '1'; } catch {}
263263
this.setState({ saved, compact: innerWidth < 1100, narrow: innerWidth < 720, coarse: matchMedia('(pointer: coarse)').matches });
264264
if (lowPowerDevice()) this.goLowPower();
265-
// Resize work forces layout (title fit, map panels), so it runs at most once per frame.
266-
let rz = 0;
265+
// Resize work forces layout (title fit, map panels), so it runs at most once per frame. Both depend on the width
266+
// only: a phone's toolbar or keyboard changes just the height, and re-tweening the map panels then cost a second of layout.
267+
let rz = 0, rw = innerWidth;
267268
this.listen(window, 'resize', () => {
268269
if (rz) return;
269270
rz = requestAnimationFrame(() => {
270271
rz = 0;
271272
const c = innerWidth < 1100, n = innerWidth < 720;
272273
if (c !== this.state.compact || n !== this.state.narrow) this.setState({ compact: c, narrow: n }, () => { if (this.state.view === 'detail') this.setupDetailScroll(true); });
274+
if (innerWidth === rw) return;
275+
rw = innerWidth;
273276
this.fitTitle();
274277
if (this.ctx) this.openMap(this.mapOpen);
275278
});
@@ -666,6 +669,8 @@ export class Innovision extends Component<Props, State> {
666669
/** The only sound is the click (clickSound.ts): every button, link or tab clicked anywhere answers with a tick. */
667670
sound() {
668671
preloadClick();
672+
// Created while the loader counts up, so that a first tap during the landing animation doesn't stall it either.
673+
this.cleanups.push(this.idle(warmClick));
669674
['pointerdown', 'keydown'].forEach((ev) => this.listen(window, ev, unlockClick, { once: true, capture: true }));
670675
this.listen(document, 'click', (e) => {
671676
const t = (e.target as Element | null)?.closest?.(CLICKABLE);
@@ -702,6 +707,12 @@ export class Innovision extends Component<Props, State> {
702707
// The overlay and view a visitor is most likely to open next are fetched ahead too (their own hover/tap also does).
703708
// The pages of their own too, so MERCH (and REGISTER while closed) open at once (a no-op on the dev server).
704709
this.cleanups.push(this.idle(() => { this.prefetch('auth'); this.prefetch('detail'); OWN_PAGES.forEach((p) => this.props.router?.prefetch(p)); }));
710+
// The menu (where the top bar has a MENU button) and the register / log-in overlay are mounted, closed, ahead of
711+
// time: mounting one on its first tap blocked a phone for ~100 ms just as its reveal began. One per idle period.
712+
this.cleanups.push(this.idle(() => {
713+
if (this.state.compact) void this.mount('menu');
714+
this.cleanups.push(this.idle(() => { void this.mount('auth'); }));
715+
}));
705716
const queue = [...PRELOAD_DEFERRED];
706717
let cancel = () => {}, live = true;
707718
const batch = () => {
@@ -1528,7 +1539,9 @@ export class Innovision extends Component<Props, State> {
15281539
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; }) : [];
15291540
}
15301541
hover = (e: MouseEvent<HTMLElement>) => {
1531-
if (this.reduce || !this.ctx) return;
1542+
// Touch screens fire mouseenter on every tap: the scramble swaps in letters of other widths, so the tapped label
1543+
// (MENU, REGISTER) changed width for half a second and the top bar around it shook. Hover effects are for mice only.
1544+
if (this.reduce || !this.ctx || this.state.coarse) return;
15321545
const sp = e.currentTarget.querySelector<HTMLElement>('[data-scr]');
15331546
if (!sp) return;
15341547
sp.dataset.text = sp.dataset.text || sp.textContent || '';
@@ -1922,7 +1935,8 @@ export class Innovision extends Component<Props, State> {
19221935
pagePush(z) {
19231936
const sc = z ? String(1 + .12 * z) : '', o = this.authO, to = z && o ? o.x.toFixed(0) + 'px ' + o.y.toFixed(0) + 'px' : '';
19241937
// @ts-ignore
1925-
(this.page || []).forEach((el) => { el.style.scale = sc; el.style.transformOrigin = to; });
1938+
// will-change while pushed: without it every frame of the zoom re-rastered the whole page (the home page is many screens tall).
1939+
(this.page || []).forEach((el) => { el.style.scale = sc; el.style.transformOrigin = to; el.style.willChange = sc ? 'transform' : ''; });
19261940
const veil = this.$ && this.$('[data-rift-veil]'); if (veil) veil.style.opacity = String(z * .85);
19271941
}
19281942
// @ts-ignore
@@ -2038,13 +2052,14 @@ export class Innovision extends Component<Props, State> {
20382052
return;
20392053
}
20402054
g.set(ins, { autoAlpha: 0, y: 26 }); g.set(planet, { autoAlpha: 0, scale: .7, yPercent: 16 }); g.set(ui, { scale: .94 });
2055+
this.authLayers(true);
20412056
const r = this._rift = this.riftNew(0, 0, 1, 0);
20422057
this.riftDraw();
20432058
const k = this.speed();
20442059
this.warp(2.4 / k);
20452060
// @ts-ignore
20462061
this.authTl = g.timeline({ onUpdate: () => this.riftDraw(), onComplete: () => {
2047-
this._rift = null; sec.style.clipPath = 'none';
2062+
this._rift = null; sec.style.clipPath = 'none'; this.authLayers(false);
20482063
this.pagePush(0); this.authBusy = false; this.focusAuth();
20492064
} })
20502065
.fromTo(sec, { autoAlpha: fromDisc ? 0 : 1 }, { autoAlpha: 1, duration: fromDisc ? .35 : .01, ease: 'power1.out' }, 0)
@@ -2065,14 +2080,15 @@ export class Innovision extends Component<Props, State> {
20652080
this.page = this.livePage();
20662081
// @ts-ignore
20672082
const done = () => {
2068-
this._rift = null;
2083+
this._rift = null; this.authLayers(false);
20692084
g.set(root, { autoAlpha: 0, pointerEvents: 'none' }); g.set(sec, { autoAlpha: 1 }); sec.style.clipPath = 'none';
20702085
this.pagePush(0);
20712086
this.authSpin(false); this.authClosing = false; this.authBusy = false;
20722087
this.setState({ auth: false, err: {}, busyLbl: '', drag: '' });
20732088
if (after) after();
20742089
};
20752090
if (this.reduce) { g.to(sec, { autoAlpha: 0, duration: .3, onComplete: () => { g.set(sec, { autoAlpha: 1 }); done(); } }); return; }
2091+
this.authLayers(true);
20762092
const r = this._rift = this.riftNew(1, 1, 0, 1);
20772093
this.riftDraw();
20782094
// @ts-ignore
@@ -2090,11 +2106,19 @@ export class Innovision extends Component<Props, State> {
20902106
if (this.authTl) this.authTl.kill();
20912107
clearTimeout(this._wait); this.warpStop(); this._rift = null;
20922108
const sec = this.$('[data-auth]'); if (sec) { sec.style.clipPath = 'none'; gsap.set(sec, { autoAlpha: 1 }); }
2109+
this.authLayers(false);
20932110
gsap.set(this.$('[data-auth-root]'), { autoAlpha: 0, pointerEvents: 'none' });
20942111
this.pagePush(0);
20952112
this.authSpin(false); this.authBusy = false; this.authClosing = false;
20962113
this.setState({ auth: false, busyLbl: '', drag: '' });
20972114
}
2115+
/**
2116+
* Gives the form and its planet their own layers while the rift opens or closes: both scale, and the rift's clip
2117+
* changes every frame, which otherwise re-rastered them each frame. Dropped once still, so the form stays crisp.
2118+
*/
2119+
authLayers(on: boolean) {
2120+
this.$$('[data-a-ui],[data-a-planet-wrap]').forEach((el) => { el.style.willChange = on ? 'transform' : ''; });
2121+
}
20982122
// @ts-ignore
20992123
authSpin(on) {
21002124
// @ts-ignore

‎src/components/innovision/Loader.tsx‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const ORBS: [string, string, number, number, number, string][] = [
1111
['26%', '1px solid rgba(20,19,18,.3)', 0, 128, 16, 'planet-mars.webp'],
1212
];
1313
const DISC = 'min(58vmin, 540px, 100vh - 330px)';
14+
/** The rings scale in and out; their own layer keeps the conic gradients and masks from re-rastering every frame. */
15+
const RING = { position: "absolute", borderRadius: "50%", willChange: "transform" } as const;
1416

1517
/** Orbital preloader. */
1618
export default function Loader({ v }: { v: V }) {
@@ -29,13 +31,13 @@ export default function Loader({ v }: { v: V }) {
2931
<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>
3032
<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)" }} />
3133
<div style={{ position: "absolute", left: `calc(50% - ${DISC} / 2)`, top: `calc(40% - ${DISC} / 2)`, width: DISC, height: DISC }}>
32-
<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>
33-
<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>
34+
<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>
35+
<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>
3436
<svg data-l-fade="" viewBox="0 0 100 100" style={{ position: "absolute", inset: "0", width: "100%", height: "100%", overflow: "visible" }}>
3537
<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>
3638
</svg>
3739
{ORBS.map(([inset, border, i, a0, size, img]) => (
38-
<div key={i} data-l-ring="" style={{ position: "absolute", inset, borderRadius: "50%", border }}>
40+
<div key={i} data-l-ring="" style={{ ...RING, inset, border }}>
3941
<div data-l-orb="" data-i={i} data-a0={a0} style={{ position: "absolute", inset: "0" }}>
4042
<div style={{ position: "absolute", left: "50%", top: "0", width: size + "%", height: size + "%", margin: `-${size / 2}% 0 0 -${size / 2}%` }}>
4143
<span data-l-lock="" style={{ position: "absolute", inset: "-30%", borderRadius: "50%", border: "1.5px solid #8a6a2a", opacity: "0" }}></span>
@@ -60,7 +62,10 @@ export default function Loader({ v }: { v: V }) {
6062
</div>
6163
<div style={{ position: "absolute", left: "0", right: "0", bottom: "clamp(30px,6.5vh,80px)", display: "flex", flexDirection: "column", alignItems: "center", gap: "10px", textAlign: "center" }}>
6264
<div data-l-fade="" style={{ overflow: "hidden" }}>
63-
<div data-l-line="" style={{ fontSize: "12px", fontWeight: "500", letterSpacing: ".34em" }}>NIT ROURKELA SAC PRESENTS</div>
65+
<div data-l-line="" style={{ fontSize: "12px", fontWeight: "500", letterSpacing: ".34em" }}>
66+
NIT ROURKELA
67+
<span style={{ display: "block", marginTop: "6px", fontSize: "10px", letterSpacing: ".24em", opacity: ".75" }}>STUDENT ACTIVITY CENTRE PRESENTS</span>
68+
</div>
6469
</div>
6570
<div data-l-fade="" style={{ overflow: "hidden" }}>
6671
<div data-l-line="" style={{ fontFamily: "var(--font-display)", fontWeight: "400", fontSize: "clamp(24px,2.6vw,38px)", letterSpacing: ".12em" }}>INNOVISION 2026</div>

0 commit comments

Comments
 (0)