@meycult/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +45 -0
- package/package.json +72 -0
- package/src/auth/Gate.tsx +14 -0
- package/src/auth/types.ts +12 -0
- package/src/brand/BrandName.tsx +12 -0
- package/src/brand/CornerGlow.tsx +14 -0
- package/src/brand/CultEye.tsx +14 -0
- package/src/brand/GodEmperor.tsx +8 -0
- package/src/brand/LaunchButton.tsx +27 -0
- package/src/brand/Logo.tsx +24 -0
- package/src/brand/Sigil.tsx +28 -0
- package/src/brand/TokenIcons.tsx +48 -0
- package/src/brand.ts +7 -0
- package/src/cn.ts +6 -0
- package/src/components/CultCountdown.tsx +119 -0
- package/src/fonts/NewRocker-Regular.woff2 +0 -0
- package/src/fonts/NotoSansEgyptianHieroglyphs-Regular.woff2 +0 -0
- package/src/index.ts +61 -0
- package/src/lib/images.ts +10 -0
- package/src/lib/money.ts +12 -0
- package/src/lib/store.ts +19 -0
- package/src/shell/types.ts +13 -0
- package/src/styles/theme.css +396 -0
- package/src/supabase/browser.ts +103 -0
- package/src/supabase/callback.ts +15 -0
- package/src/supabase/proxy.ts +36 -0
- package/src/supabase/server.ts +31 -0
- package/src/tokens.ts +37 -0
- package/src/ui/Callout.tsx +33 -0
- package/src/ui/animated-shiny-text.tsx +42 -0
- package/src/ui/card.tsx +102 -0
- package/src/ui/shimmer-button.tsx +81 -0
- package/src/unveil/LaunchStage.tsx +32 -0
- package/src/unveil/Tablet.tsx +23 -0
- package/src/unveil/bg.ts +59 -0
- package/src/unveil/bgStyles.ts +13 -0
- package/src/unveil/blocks.tsx +199 -0
- package/src/unveil/effects.tsx +200 -0
- package/src/unveil.ts +78 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/** MeyCult Unveil background effects — 4 canvas + 6 CSS layers. */
|
|
4
|
+
import { useEffect, useRef } from 'react'
|
|
5
|
+
|
|
6
|
+
export interface FxProps {
|
|
7
|
+
density?: number // 0-100
|
|
8
|
+
speed?: number // 0-2 multiplier
|
|
9
|
+
opacity?: number // 0-1 master
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function reducedMotion(): boolean {
|
|
13
|
+
return (
|
|
14
|
+
typeof window !== 'undefined' &&
|
|
15
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Shared rAF canvas loop: DPR cap, resize, hidden-tab + offscreen pause. */
|
|
20
|
+
function useCanvas(
|
|
21
|
+
ref: React.RefObject<HTMLCanvasElement | null>,
|
|
22
|
+
render: (ctx: CanvasRenderingContext2D, w: number, h: number, t: number) => void,
|
|
23
|
+
active: boolean,
|
|
24
|
+
) {
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!active) return
|
|
27
|
+
const canvas = ref.current
|
|
28
|
+
if (!canvas || reducedMotion()) return
|
|
29
|
+
const ctx = canvas.getContext('2d')
|
|
30
|
+
if (!ctx) return
|
|
31
|
+
let raf = 0
|
|
32
|
+
let w = 0
|
|
33
|
+
let h = 0
|
|
34
|
+
const resize = () => {
|
|
35
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 1.5)
|
|
36
|
+
const rect = canvas.getBoundingClientRect()
|
|
37
|
+
w = Math.max(1, Math.floor(rect.width * dpr))
|
|
38
|
+
h = Math.max(1, Math.floor(rect.height * dpr))
|
|
39
|
+
canvas.width = w
|
|
40
|
+
canvas.height = h
|
|
41
|
+
}
|
|
42
|
+
resize()
|
|
43
|
+
const ro = new ResizeObserver(resize)
|
|
44
|
+
const parent = canvas.parentElement
|
|
45
|
+
if (parent) ro.observe(parent)
|
|
46
|
+
let visible = true
|
|
47
|
+
const io = new IntersectionObserver(([e]) => {
|
|
48
|
+
visible = e.isIntersecting
|
|
49
|
+
if (visible) last = performance.now()
|
|
50
|
+
})
|
|
51
|
+
io.observe(canvas)
|
|
52
|
+
let last = performance.now()
|
|
53
|
+
const loop = (t: number) => {
|
|
54
|
+
raf = requestAnimationFrame(loop)
|
|
55
|
+
if (!visible || document.hidden) {
|
|
56
|
+
last = t
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
void last
|
|
60
|
+
last = t
|
|
61
|
+
ctx.clearRect(0, 0, w, h)
|
|
62
|
+
render(ctx, w, h, t)
|
|
63
|
+
}
|
|
64
|
+
raf = requestAnimationFrame(loop)
|
|
65
|
+
return () => {
|
|
66
|
+
cancelAnimationFrame(raf)
|
|
67
|
+
ro.disconnect()
|
|
68
|
+
io.disconnect()
|
|
69
|
+
}
|
|
70
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
|
+
}, [active])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Old-Egypt / illuminati glyph ascension — all cult yellow. Authentic
|
|
76
|
+
* hieroglyphs via self-hosted 'Noto Sans Egyptian Hieroglyphs' (same
|
|
77
|
+
* pattern as New Rocker); serif fallback covers ☥/△/◉ everywhere.
|
|
78
|
+
*/
|
|
79
|
+
const GLYPH_FONT = `'"Noto Sans Egyptian Hieroglyphs", serif'`
|
|
80
|
+
const GLYPHS = [
|
|
81
|
+
'𓂀', '𓂀', // Eye of Horus
|
|
82
|
+
'☥', '☥', // ankh — life
|
|
83
|
+
'△', // pyramid
|
|
84
|
+
'𓆣', // scarab
|
|
85
|
+
'𓅓', // owl
|
|
86
|
+
'𓇳', // sun
|
|
87
|
+
'𓊵', // djed pillar
|
|
88
|
+
'𓆙', // cobra
|
|
89
|
+
'𓆄', // feather
|
|
90
|
+
'◉', // the eye
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
export function RuneCanvas({ density = 50, speed = 0.6, opacity = 0.8 }: FxProps) {
|
|
94
|
+
const ref = useRef<HTMLCanvasElement>(null)
|
|
95
|
+
const parts = useRef(
|
|
96
|
+
Array.from({ length: 60 }, (_, i) => ({
|
|
97
|
+
x: Math.random(),
|
|
98
|
+
y: Math.random(),
|
|
99
|
+
vy: 0.0002 + Math.random() * 0.0006,
|
|
100
|
+
tw: Math.random() * Math.PI * 2,
|
|
101
|
+
size: 10 + Math.random() * 12,
|
|
102
|
+
glyph: GLYPHS[i % GLYPHS.length],
|
|
103
|
+
})),
|
|
104
|
+
)
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
// ensure the hieroglyph webfont is ready before first glyphs drift in
|
|
107
|
+
try {
|
|
108
|
+
void document.fonts?.load(`64px ${GLYPH_FONT}`)
|
|
109
|
+
} catch {
|
|
110
|
+
/* serif fallback covers the safe subset */
|
|
111
|
+
}
|
|
112
|
+
}, [])
|
|
113
|
+
useCanvas(
|
|
114
|
+
ref,
|
|
115
|
+
(ctx, w, h, t) => {
|
|
116
|
+
const n = Math.floor((density / 100) * parts.current.length)
|
|
117
|
+
for (let i = 0; i < n; i++) {
|
|
118
|
+
const p = parts.current[i]
|
|
119
|
+
p.y -= p.vy * (0.4 + speed)
|
|
120
|
+
p.tw += 0.02 * (0.4 + speed)
|
|
121
|
+
if (p.y < -0.05) {
|
|
122
|
+
p.y = 1.05
|
|
123
|
+
p.x = Math.random()
|
|
124
|
+
}
|
|
125
|
+
const x = (p.x + Math.sin(p.tw) * 0.008) * w
|
|
126
|
+
const y = p.y * h
|
|
127
|
+
ctx.globalAlpha = (0.15 + 0.5 * Math.abs(Math.sin(p.tw))) * opacity
|
|
128
|
+
ctx.font = `${p.size}px ${GLYPH_FONT}`
|
|
129
|
+
ctx.fillStyle = '#FFE600'
|
|
130
|
+
ctx.fillText(p.glyph, x, y)
|
|
131
|
+
}
|
|
132
|
+
void t
|
|
133
|
+
ctx.globalAlpha = 1
|
|
134
|
+
},
|
|
135
|
+
true,
|
|
136
|
+
)
|
|
137
|
+
return <canvas ref={ref} className="mey-bg-canvas" aria-hidden="true" />
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function AshCanvas({ density = 50, speed = 0.5, opacity = 0.8 }: FxProps) {
|
|
141
|
+
const ref = useRef<HTMLCanvasElement>(null)
|
|
142
|
+
const parts = useRef(
|
|
143
|
+
Array.from({ length: 220 }, () => ({
|
|
144
|
+
x: Math.random(),
|
|
145
|
+
y: Math.random(),
|
|
146
|
+
life: 100 + Math.random() * 200,
|
|
147
|
+
})),
|
|
148
|
+
)
|
|
149
|
+
useCanvas(
|
|
150
|
+
ref,
|
|
151
|
+
(ctx, w, h, t) => {
|
|
152
|
+
// fade previous frame (destination-out keeps canvas transparent)
|
|
153
|
+
ctx.globalCompositeOperation = 'destination-out'
|
|
154
|
+
ctx.fillStyle = 'rgba(0,0,0,0.08)'
|
|
155
|
+
ctx.fillRect(0, 0, w, h)
|
|
156
|
+
ctx.globalCompositeOperation = 'source-over'
|
|
157
|
+
const n = Math.floor((density / 100) * parts.current.length)
|
|
158
|
+
const z = t * 0.0001 * (0.3 + speed)
|
|
159
|
+
for (let i = 0; i < n; i++) {
|
|
160
|
+
const p = parts.current[i]
|
|
161
|
+
const px = p.x * w
|
|
162
|
+
const py = p.y * h
|
|
163
|
+
const ang =
|
|
164
|
+
(Math.sin(px * 0.002 + z * 7) + Math.cos(py * 0.002 - z * 5)) * Math.PI
|
|
165
|
+
const nx = px + Math.cos(ang) * 2
|
|
166
|
+
const ny = py + Math.sin(ang) * 2
|
|
167
|
+
ctx.globalAlpha = 0.25 * opacity
|
|
168
|
+
ctx.strokeStyle = '#FFE600'
|
|
169
|
+
ctx.beginPath()
|
|
170
|
+
ctx.moveTo(px, py)
|
|
171
|
+
ctx.lineTo(nx, ny)
|
|
172
|
+
ctx.stroke()
|
|
173
|
+
p.x = (nx / w + 1) % 1
|
|
174
|
+
p.y = (ny / h + 1) % 1
|
|
175
|
+
if (--p.life <= 0) {
|
|
176
|
+
p.x = Math.random()
|
|
177
|
+
p.y = Math.random()
|
|
178
|
+
p.life = 200
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
ctx.globalAlpha = 1
|
|
182
|
+
},
|
|
183
|
+
true,
|
|
184
|
+
)
|
|
185
|
+
return <canvas ref={ref} className="mey-bg-canvas" aria-hidden="true" />
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function VeilFog({ opacity = 0.7 }: FxProps) {
|
|
189
|
+
return (
|
|
190
|
+
<div className="mey-veil" style={{ opacity }} aria-hidden="true">
|
|
191
|
+
<i />
|
|
192
|
+
<i />
|
|
193
|
+
</div>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
export function BreathVignette() {
|
|
199
|
+
return <div className="mey-breath" aria-hidden="true" />
|
|
200
|
+
}
|
package/src/unveil.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeyCult Unveil constants — single source of truth for the countdown.
|
|
3
|
+
* Target: Fri 25 Sept 2026, 20:00 SAST (Africa/Johannesburg, UTC+2, no DST).
|
|
4
|
+
* Overridable via NEXT_PUBLIC_UNVEIL_DATE (ISO with offset) for previews.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const UNVEIL_TZ = 'Africa/Johannesburg'
|
|
8
|
+
|
|
9
|
+
export const UNVEIL_DEFAULT_ISO = '2026-09-25T20:00:00+02:00'
|
|
10
|
+
|
|
11
|
+
/** Live window after T-0 before flipping to replay/ended state. */
|
|
12
|
+
export const LIVE_WINDOW_MS = 3 * 3_600_000
|
|
13
|
+
|
|
14
|
+
function resolveLaunchAt(): number {
|
|
15
|
+
const raw =
|
|
16
|
+
typeof process !== 'undefined'
|
|
17
|
+
? process.env?.NEXT_PUBLIC_UNVEIL_DATE || process.env?.VITE_UNVEIL_DATE
|
|
18
|
+
: undefined
|
|
19
|
+
const t = Date.parse(raw || UNVEIL_DEFAULT_ISO)
|
|
20
|
+
return Number.isFinite(t) ? t : Date.parse(UNVEIL_DEFAULT_ISO)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const UNVEIL_AT = resolveLaunchAt()
|
|
24
|
+
|
|
25
|
+
export type UnveilPhase = 'countdown' | 'live' | 'ended'
|
|
26
|
+
|
|
27
|
+
export function unveilPhase(now: number = Date.now()): UnveilPhase {
|
|
28
|
+
if (now < UNVEIL_AT) return 'countdown'
|
|
29
|
+
if (now < UNVEIL_AT + LIVE_WINDOW_MS) return 'live'
|
|
30
|
+
return 'ended'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** "25 Sept 2026, 20:00 SAST" — always rendered in Unveil timezone (SAST). */
|
|
34
|
+
export function unveilDayLabel(at: number = UNVEIL_AT): string {
|
|
35
|
+
return new Intl.DateTimeFormat('en-ZA', {
|
|
36
|
+
timeZone: UNVEIL_TZ,
|
|
37
|
+
weekday: 'short',
|
|
38
|
+
day: 'numeric',
|
|
39
|
+
month: 'short',
|
|
40
|
+
year: 'numeric',
|
|
41
|
+
hour: '2-digit',
|
|
42
|
+
minute: '2-digit',
|
|
43
|
+
hour12: false,
|
|
44
|
+
}).format(new Date(at))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Visitor-local rendering of the same instant. */
|
|
48
|
+
export function unveilLocalLabel(at: number = UNVEIL_AT): string {
|
|
49
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
50
|
+
day: 'numeric',
|
|
51
|
+
month: 'short',
|
|
52
|
+
hour: '2-digit',
|
|
53
|
+
minute: '2-digit',
|
|
54
|
+
timeZoneName: 'short',
|
|
55
|
+
}).format(new Date(at))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CountdownParts {
|
|
59
|
+
days: number
|
|
60
|
+
hours: number
|
|
61
|
+
minutes: number
|
|
62
|
+
seconds: number
|
|
63
|
+
done: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function diffParts(target: number, now: number = Date.now()): CountdownParts {
|
|
67
|
+
const delta = target - now
|
|
68
|
+
if (delta <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0, done: true }
|
|
69
|
+
return {
|
|
70
|
+
days: Math.floor(delta / 86_400_000),
|
|
71
|
+
hours: Math.floor(delta / 3_600_000) % 24,
|
|
72
|
+
minutes: Math.floor(delta / 60_000) % 60,
|
|
73
|
+
seconds: Math.floor(delta / 1_000) % 60,
|
|
74
|
+
done: false,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const pad2 = (n: number) => String(n).padStart(2, '0')
|