@meycult/core 0.1.0 → 0.2.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/package.json +3 -6
- package/src/brand/BrandName.tsx +4 -2
- package/src/brand/LaunchButton.tsx +5 -3
- package/src/brand/Logo.tsx +25 -6
- package/src/brand/PyramidName.tsx +10 -0
- package/src/brand/Sigil.tsx +10 -5
- package/src/brand/TokenIcons.tsx +12 -6
- package/src/brand.ts +4 -1
- package/src/components/CultCountdown.tsx +89 -14
- package/src/game/categories.ts +23 -0
- package/src/game/cults.ts +88 -0
- package/src/game/quests.ts +62 -0
- package/src/game/types.ts +27 -0
- package/src/index.ts +64 -21
- package/src/product.ts +17 -0
- package/src/styles/theme.css +97 -6
- package/src/tokens.ts +10 -2
- package/src/ui/CultButton.tsx +55 -0
- package/src/ui/RarityBadge.tsx +18 -0
- package/src/ui/animated-shiny-text.tsx +2 -2
- package/src/ui/badges.tsx +43 -0
- package/src/ui/card.tsx +20 -92
- package/src/ui/shimmer-button.tsx +5 -3
- package/src/ui/virtues.tsx +66 -0
- package/src/unveil/LaunchStage.tsx +3 -2
- package/src/unveil/bg.ts +20 -11
- package/src/unveil/bgStyles.ts +0 -1
- package/src/unveil/blocks.tsx +51 -14
- package/src/unveil/effects.tsx +182 -86
- package/src/unveil.ts +2 -21
- package/src/auth/Gate.tsx +0 -14
- package/src/auth/types.ts +0 -12
- package/src/brand/GodEmperor.tsx +0 -8
- package/src/cn.ts +0 -6
- package/src/lib/images.ts +0 -10
- package/src/lib/money.ts +0 -12
- package/src/lib/store.ts +0 -19
- package/src/shell/types.ts +0 -13
- package/src/supabase/browser.ts +0 -103
- package/src/supabase/callback.ts +0 -15
- package/src/supabase/proxy.ts +0 -36
- package/src/supabase/server.ts +0 -31
- package/src/ui/Callout.tsx +0 -33
package/src/unveil/bg.ts
CHANGED
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
* MeyCult Unveil background state. Kept set: glyphs, veil, ash, breath.
|
|
3
3
|
* The six retired layers live in @meyvoid/core backgrounds for reuse.
|
|
4
4
|
*/
|
|
5
|
-
import { createPersistStore } from '
|
|
5
|
+
import { createPersistStore } from '@meyvoid/core'
|
|
6
6
|
|
|
7
|
-
export type BgEffect = 'runes' | 'veil' | 'ash' | 'breath'
|
|
7
|
+
export type BgEffect = 'wall' | 'runes' | 'veil' | 'ash' | 'breath'
|
|
8
8
|
|
|
9
9
|
export const BG_EFFECTS: Array<{ id: BgEffect; label: string }> = [
|
|
10
|
+
{ id: 'wall', label: 'Pyramid Wall' },
|
|
10
11
|
{ id: 'runes', label: 'Egyptian Glyphs' },
|
|
11
12
|
{ id: 'veil', label: 'Veil Fog' },
|
|
12
13
|
{ id: 'ash', label: 'Ash Flow-Field' },
|
|
13
14
|
{ id: 'breath', label: 'Breath Vignette' },
|
|
14
15
|
]
|
|
15
16
|
|
|
16
|
-
export const BG_DEFAULTS: BgEffect[] = ['runes', 'veil', 'ash', 'breath']
|
|
17
|
+
export const BG_DEFAULTS: BgEffect[] = ['wall', 'runes', 'veil', 'ash', 'breath']
|
|
18
|
+
|
|
19
|
+
/** Fresh defaults — also used to reset stale persisted sets on version bump. */
|
|
20
|
+
const BG_INITIAL = {
|
|
21
|
+
effects: BG_DEFAULTS,
|
|
22
|
+
density: 75,
|
|
23
|
+
speed: 0.6,
|
|
24
|
+
opacity: 0.8,
|
|
25
|
+
grain: true,
|
|
26
|
+
} as const
|
|
17
27
|
|
|
18
28
|
interface BgState {
|
|
19
29
|
effects: BgEffect[]
|
|
@@ -29,13 +39,9 @@ interface BgState {
|
|
|
29
39
|
|
|
30
40
|
export const useUnveilBg = createPersistStore<BgState>(
|
|
31
41
|
'meycult-unveil-bg-v1',
|
|
32
|
-
|
|
42
|
+
3,
|
|
33
43
|
(set) => ({
|
|
34
|
-
|
|
35
|
-
density: 50,
|
|
36
|
-
speed: 0.6,
|
|
37
|
-
opacity: 0.8,
|
|
38
|
-
grain: true,
|
|
44
|
+
...BG_INITIAL,
|
|
39
45
|
toggle: (e) =>
|
|
40
46
|
set((s) => ({
|
|
41
47
|
effects: s.effects.includes(e)
|
|
@@ -44,9 +50,12 @@ export const useUnveilBg = createPersistStore<BgState>(
|
|
|
44
50
|
})),
|
|
45
51
|
setMany: (effects) => set({ effects }),
|
|
46
52
|
set: (p) => set(p),
|
|
47
|
-
reset: () =>
|
|
48
|
-
set({ effects: BG_DEFAULTS, density: 50, speed: 0.6, opacity: 0.8, grain: true }),
|
|
53
|
+
reset: () => set({ ...BG_INITIAL }),
|
|
49
54
|
}),
|
|
55
|
+
{
|
|
56
|
+
// version bumps reset to fresh defaults (new effects ship enabled)
|
|
57
|
+
migrate: () => ({ ...BG_INITIAL }) as unknown as BgState,
|
|
58
|
+
},
|
|
50
59
|
)
|
|
51
60
|
|
|
52
61
|
/** Read-only ?bg=veil,runes share param (applied once on mount). */
|
package/src/unveil/bgStyles.ts
CHANGED
|
@@ -8,6 +8,5 @@ export const BG_STYLES = `
|
|
|
8
8
|
@keyframes mey-veil-drift{from{transform:translateX(-8vw)}to{transform:translateX(8vw)}}
|
|
9
9
|
.mey-breath{position:absolute;inset:0;pointer-events:none;box-shadow:inset 0 0 180px 60px rgba(0,0,0,.75);animation:mey-breath 7s ease-in-out infinite}
|
|
10
10
|
@keyframes mey-breath{50%{box-shadow:inset 0 0 240px 90px rgba(0,0,0,.9)}}
|
|
11
|
-
.mey-bg-canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
|
|
12
11
|
@media (prefers-reduced-motion:reduce){.mey-veil i:nth-child(2),.mey-breath{animation:none}}
|
|
13
12
|
`
|
package/src/unveil/blocks.tsx
CHANGED
|
@@ -10,26 +10,56 @@ import CultEye from '../brand/CultEye'
|
|
|
10
10
|
import { CultCountdown } from '../components/CultCountdown'
|
|
11
11
|
import { UNVEIL_AT, unveilDayLabel, unveilLocalLabel } from '../unveil'
|
|
12
12
|
|
|
13
|
-
export
|
|
13
|
+
export interface UnveilHeroSizes {
|
|
14
|
+
/** px overrides for live tuning — undefined keeps the class defaults */
|
|
15
|
+
mey?: number
|
|
16
|
+
unveil?: number
|
|
17
|
+
gap?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UnveilHeroRoutes {
|
|
21
|
+
countdown?: string
|
|
22
|
+
crowdfunding?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function UnveilHero({
|
|
26
|
+
sizes,
|
|
27
|
+
routes,
|
|
28
|
+
}: {
|
|
29
|
+
sizes?: UnveilHeroSizes
|
|
30
|
+
/** Composition — hosts override when paths differ. Defaults are the web routes. */
|
|
31
|
+
routes?: UnveilHeroRoutes
|
|
32
|
+
}) {
|
|
33
|
+
const countdownHref = routes?.countdown ?? '/unveil/countdown'
|
|
34
|
+
const crowdfundingHref = routes?.crowdfunding ?? '/#crowdfunding'
|
|
14
35
|
return (
|
|
15
36
|
<section className="section section--hero">
|
|
16
|
-
<a href=
|
|
37
|
+
<a href={countdownHref} aria-label="Open the full-page countdown" data-testid="unveil-sigil">
|
|
17
38
|
<div className="w-[200px] sm:w-[280px]">
|
|
18
39
|
<CultEye fluid />
|
|
19
40
|
</div>
|
|
20
41
|
</a>
|
|
21
42
|
|
|
22
43
|
<div className="mt-2 flex flex-col items-center leading-none">
|
|
23
|
-
<span
|
|
44
|
+
<span
|
|
45
|
+
className="text-[64px] sm:text-[96px] font-bold tracking-wider"
|
|
46
|
+
style={sizes?.mey !== undefined ? { fontSize: sizes.mey } : undefined}
|
|
47
|
+
>
|
|
24
48
|
<span className="brand-mey">Mey</span>
|
|
25
49
|
<span className="brand-cult">Cult</span>
|
|
26
50
|
</span>
|
|
27
|
-
<span
|
|
51
|
+
<span
|
|
52
|
+
className="brand-unveil unveil-glow mt-[-12px] sm:mt-[-19px] text-[56px] sm:text-[85px]"
|
|
53
|
+
style={{
|
|
54
|
+
...(sizes?.unveil !== undefined ? { fontSize: sizes.unveil } : undefined),
|
|
55
|
+
...(sizes?.gap !== undefined ? { marginTop: sizes.gap } : undefined),
|
|
56
|
+
}}
|
|
57
|
+
>Unveil</span>
|
|
28
58
|
</div>
|
|
29
59
|
<p className="mt-3 text-sm text-text-muted tracking-wide">
|
|
30
60
|
{unveilDayLabel(UNVEIL_AT)} SAST · Stellenbosch
|
|
31
61
|
</p>
|
|
32
|
-
<p className="text-xs text-text-muted">{unveilLocalLabel(UNVEIL_AT)} your time</p>
|
|
62
|
+
<p className="text-xs text-text-muted" suppressHydrationWarning>{unveilLocalLabel(UNVEIL_AT)} your time</p>
|
|
33
63
|
|
|
34
64
|
<div className="mt-8 w-full max-w-2xl">
|
|
35
65
|
<CultCountdown />
|
|
@@ -39,13 +69,13 @@ export function UnveilHero() {
|
|
|
39
69
|
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
|
40
70
|
<a
|
|
41
71
|
className="px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider"
|
|
42
|
-
href=
|
|
72
|
+
href={crowdfundingHref}
|
|
43
73
|
>
|
|
44
74
|
Notify me
|
|
45
75
|
</a>
|
|
46
76
|
<a
|
|
47
77
|
className="px-6 py-2.5 rounded-lg border border-line/40 text-sm uppercase tracking-wider text-text"
|
|
48
|
-
href=
|
|
78
|
+
href={countdownHref}
|
|
49
79
|
>
|
|
50
80
|
Full countdown
|
|
51
81
|
</a>
|
|
@@ -78,7 +108,7 @@ export function WhereWhen({
|
|
|
78
108
|
<a href={href} target="_blank" rel="noreferrer" className="block w-full group">
|
|
79
109
|
<div
|
|
80
110
|
className="w-full aspect-[2/1] overflow-hidden rounded-xl transition-transform duration-300 group-hover:scale-[1.02]"
|
|
81
|
-
style={{ border: '1px solid
|
|
111
|
+
style={{ border: '1px solid var(--color-line)', boxShadow: '0 0 40px rgba(255,230,0,0.12)' }}
|
|
82
112
|
>
|
|
83
113
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
84
114
|
<img src={map.src} alt={map.alt} className="w-full h-full object-cover" />
|
|
@@ -90,7 +120,7 @@ export function WhereWhen({
|
|
|
90
120
|
) : (
|
|
91
121
|
<div
|
|
92
122
|
className="w-full aspect-[2/1] rounded-xl flex items-center justify-center"
|
|
93
|
-
style={{ border: '1px solid
|
|
123
|
+
style={{ border: '1px solid var(--color-line)', background: 'rgba(0,0,0,0.3)' }}
|
|
94
124
|
>
|
|
95
125
|
<div className="w-[64px]">
|
|
96
126
|
<CultEye fluid />
|
|
@@ -138,7 +168,14 @@ export function CardGrid({ cards, cols = 3 }: { cards: UnveilCard[]; cols?: 2 |
|
|
|
138
168
|
)
|
|
139
169
|
}
|
|
140
170
|
|
|
141
|
-
export
|
|
171
|
+
export interface HypeRowRoutes {
|
|
172
|
+
crowdfunding?: string
|
|
173
|
+
whitepaper?: string
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function HypeRow({ qrUrl, routes }: { qrUrl: string; routes?: HypeRowRoutes }) {
|
|
177
|
+
const crowdfundingHref = routes?.crowdfunding ?? '/#crowdfunding'
|
|
178
|
+
const whitepaperHref = routes?.whitepaper ?? '/whitepaper'
|
|
142
179
|
return (
|
|
143
180
|
<div className="grid grid-cols-1 sm:grid-cols-[2fr_1fr] gap-8 items-center max-w-3xl mx-auto">
|
|
144
181
|
<div>
|
|
@@ -153,10 +190,10 @@ export function HypeRow({ qrUrl }: { qrUrl: string }) {
|
|
|
153
190
|
Free to play. Real USDC cash-out. Sweepstakes-legal.
|
|
154
191
|
</p>
|
|
155
192
|
<div className="mt-4 flex flex-wrap gap-3">
|
|
156
|
-
<a className="px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider" href=
|
|
193
|
+
<a className="px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider" href={crowdfundingHref}>
|
|
157
194
|
Get notified
|
|
158
195
|
</a>
|
|
159
|
-
<a className="px-6 py-2.5 rounded-lg border border-line/40 text-sm uppercase tracking-wider text-text" href=
|
|
196
|
+
<a className="px-6 py-2.5 rounded-lg border border-line/40 text-sm uppercase tracking-wider text-text" href={whitepaperHref}>
|
|
160
197
|
Read the whitepaper
|
|
161
198
|
</a>
|
|
162
199
|
</div>
|
|
@@ -181,7 +218,7 @@ export function HypeRow({ qrUrl }: { qrUrl: string }) {
|
|
|
181
218
|
)
|
|
182
219
|
}
|
|
183
220
|
|
|
184
|
-
export function NotifyPanel() {
|
|
221
|
+
export function NotifyPanel({ crowdfundingHref = '/#crowdfunding' }: { crowdfundingHref?: string }) {
|
|
185
222
|
return (
|
|
186
223
|
<div className="landing-card p-8 max-w-lg mx-auto text-center">
|
|
187
224
|
<p className="text-accent font-bold text-lg mb-2">Be there at 20:00.</p>
|
|
@@ -190,7 +227,7 @@ export function NotifyPanel() {
|
|
|
190
227
|
</p>
|
|
191
228
|
<a
|
|
192
229
|
className="inline-block px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider"
|
|
193
|
-
href=
|
|
230
|
+
href={crowdfundingHref}
|
|
194
231
|
>
|
|
195
232
|
Join the list
|
|
196
233
|
</a>
|
package/src/unveil/effects.tsx
CHANGED
|
@@ -2,74 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/** MeyCult Unveil background effects — 4 canvas + 6 CSS layers. */
|
|
4
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
|
-
}
|
|
5
|
+
import { useCanvas, reducedMotion, type FxProps } from '@meyvoid/core/backgrounds'
|
|
73
6
|
|
|
74
7
|
/**
|
|
75
8
|
* Old-Egypt / illuminati glyph ascension — all cult yellow. Authentic
|
|
@@ -90,18 +23,34 @@ const GLYPHS = [
|
|
|
90
23
|
'◉', // the eye
|
|
91
24
|
]
|
|
92
25
|
|
|
93
|
-
|
|
26
|
+
interface RunePart {
|
|
27
|
+
x: number
|
|
28
|
+
y: number
|
|
29
|
+
vy: number
|
|
30
|
+
tw: number
|
|
31
|
+
size: number
|
|
32
|
+
glyph: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Glyph budget scales with viewport area: 150 particles at 1080p. */
|
|
36
|
+
const RUNE_REF_AREA = 1920 * 1080
|
|
37
|
+
const RUNE_REF_COUNT = 150
|
|
38
|
+
|
|
39
|
+
function makeRuneParts(n: number): RunePart[] {
|
|
40
|
+
return Array.from({ length: n }, (_, i) => ({
|
|
41
|
+
x: Math.random(),
|
|
42
|
+
y: Math.random(),
|
|
43
|
+
vy: 0.0002 + Math.random() * 0.0006,
|
|
44
|
+
tw: Math.random() * Math.PI * 2,
|
|
45
|
+
size: 10 + Math.random() * 12,
|
|
46
|
+
glyph: GLYPHS[i % GLYPHS.length],
|
|
47
|
+
}))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function RuneCanvas({ density = 50, speed = 0.6, opacity = 0.8, z = 3 }: FxProps) {
|
|
94
51
|
const ref = useRef<HTMLCanvasElement>(null)
|
|
95
|
-
const parts = useRef(
|
|
96
|
-
|
|
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
|
-
)
|
|
52
|
+
const parts = useRef<RunePart[]>([])
|
|
53
|
+
const sized = useRef({ w: 0, h: 0 })
|
|
105
54
|
useEffect(() => {
|
|
106
55
|
// ensure the hieroglyph webfont is ready before first glyphs drift in
|
|
107
56
|
try {
|
|
@@ -113,6 +62,20 @@ export function RuneCanvas({ density = 50, speed = 0.6, opacity = 0.8 }: FxProps
|
|
|
113
62
|
useCanvas(
|
|
114
63
|
ref,
|
|
115
64
|
(ctx, w, h, t) => {
|
|
65
|
+
// rebuild the pool when the viewport bucket changes (CSS px, so DPR
|
|
66
|
+
// never inflates the count) — cheap compare, layout read only on resize
|
|
67
|
+
if (w !== sized.current.w || h !== sized.current.h) {
|
|
68
|
+
sized.current = { w, h }
|
|
69
|
+
const cw = ref.current?.clientWidth || w
|
|
70
|
+
const ch = ref.current?.clientHeight || h
|
|
71
|
+
const want = Math.max(
|
|
72
|
+
40,
|
|
73
|
+
Math.min(300, Math.round((RUNE_REF_COUNT * cw * ch) / RUNE_REF_AREA)),
|
|
74
|
+
)
|
|
75
|
+
if (Math.abs(want - parts.current.length) > 8) {
|
|
76
|
+
parts.current = makeRuneParts(want)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
116
79
|
const n = Math.floor((density / 100) * parts.current.length)
|
|
117
80
|
for (let i = 0; i < n; i++) {
|
|
118
81
|
const p = parts.current[i]
|
|
@@ -124,6 +87,22 @@ export function RuneCanvas({ density = 50, speed = 0.6, opacity = 0.8 }: FxProps
|
|
|
124
87
|
}
|
|
125
88
|
const x = (p.x + Math.sin(p.tw) * 0.008) * w
|
|
126
89
|
const y = p.y * h
|
|
90
|
+
// mini stone rect: the glyph rides a tiny carved block, same
|
|
91
|
+
// treatment as the wall at glyph scale
|
|
92
|
+
const rw = p.size * 1.15
|
|
93
|
+
const rh = p.size * 1.15
|
|
94
|
+
const rx = x - rw / 2
|
|
95
|
+
const ry = y - rh / 2 - p.size * 0.08
|
|
96
|
+
ctx.globalAlpha = (0.5 + 0.5 * Math.abs(Math.sin(p.tw))) * opacity
|
|
97
|
+
ctx.fillStyle = '#141003'
|
|
98
|
+
ctx.fillRect(rx, ry, rw, rh)
|
|
99
|
+
ctx.fillStyle = 'rgba(255,230,0,0.10)'
|
|
100
|
+
ctx.fillRect(rx, ry, rw, 1)
|
|
101
|
+
ctx.fillStyle = 'rgba(0,0,0,0.6)'
|
|
102
|
+
ctx.fillRect(rx, ry + rh - 1, rw, 1)
|
|
103
|
+
ctx.strokeStyle = 'rgba(0,0,0,0.7)'
|
|
104
|
+
ctx.lineWidth = 1
|
|
105
|
+
ctx.strokeRect(rx + 0.5, ry + 0.5, rw - 1, rh - 1)
|
|
127
106
|
ctx.globalAlpha = (0.15 + 0.5 * Math.abs(Math.sin(p.tw))) * opacity
|
|
128
107
|
ctx.font = `${p.size}px ${GLYPH_FONT}`
|
|
129
108
|
ctx.fillStyle = '#FFE600'
|
|
@@ -134,10 +113,10 @@ export function RuneCanvas({ density = 50, speed = 0.6, opacity = 0.8 }: FxProps
|
|
|
134
113
|
},
|
|
135
114
|
true,
|
|
136
115
|
)
|
|
137
|
-
return <canvas ref={ref} className="mey-bg-canvas" aria-hidden="true" />
|
|
116
|
+
return <canvas ref={ref} className="mey-bg-canvas" style={{ zIndex: z }} aria-hidden="true" />
|
|
138
117
|
}
|
|
139
118
|
|
|
140
|
-
export function AshCanvas({ density = 50, speed = 0.5, opacity = 0.8 }: FxProps) {
|
|
119
|
+
export function AshCanvas({ density = 50, speed = 0.5, opacity = 0.8, z = 2 }: FxProps) {
|
|
141
120
|
const ref = useRef<HTMLCanvasElement>(null)
|
|
142
121
|
const parts = useRef(
|
|
143
122
|
Array.from({ length: 220 }, () => ({
|
|
@@ -182,12 +161,129 @@ export function AshCanvas({ density = 50, speed = 0.5, opacity = 0.8 }: FxProps)
|
|
|
182
161
|
},
|
|
183
162
|
true,
|
|
184
163
|
)
|
|
185
|
-
return <canvas ref={ref} className="mey-bg-canvas" aria-hidden="true" />
|
|
164
|
+
return <canvas ref={ref} className="mey-bg-canvas" style={{ zIndex: z }} aria-hidden="true" />
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Pyramid wall — baked dark-stone texture. Static: rendered once per resize
|
|
169
|
+
* to an offscreen canvas, then blitted. Running-bond courses, top-lit
|
|
170
|
+
* bevels, per-block tonal jitter, speckle noise, baked vignette.
|
|
171
|
+
*/
|
|
172
|
+
function mulberry32(seed: number): () => number {
|
|
173
|
+
let a = seed >>> 0
|
|
174
|
+
return () => {
|
|
175
|
+
a |= 0
|
|
176
|
+
a = (a + 0x6d2b79f5) | 0
|
|
177
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
|
178
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
179
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function WallCanvas({ opacity = 0.9, z = 0 }: FxProps) {
|
|
184
|
+
const ref = useRef<HTMLCanvasElement>(null)
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
const canvas = ref.current
|
|
187
|
+
if (!canvas || reducedMotion()) return
|
|
188
|
+
const ctx = canvas.getContext('2d')
|
|
189
|
+
if (!ctx) return
|
|
190
|
+
const bake = () => {
|
|
191
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 1.5)
|
|
192
|
+
const rect = canvas.getBoundingClientRect()
|
|
193
|
+
const w = Math.max(1, Math.floor(rect.width * dpr))
|
|
194
|
+
const h = Math.max(1, Math.floor(rect.height * dpr))
|
|
195
|
+
canvas.width = w
|
|
196
|
+
canvas.height = h
|
|
197
|
+
const off = document.createElement('canvas')
|
|
198
|
+
off.width = w
|
|
199
|
+
off.height = h
|
|
200
|
+
const c = off.getContext('2d')
|
|
201
|
+
if (!c) return
|
|
202
|
+
const rand = mulberry32((w * 73856093) ^ (h * 19349663))
|
|
203
|
+
// base: near-black warm basalt
|
|
204
|
+
c.fillStyle = '#0e0b06'
|
|
205
|
+
c.fillRect(0, 0, w, h)
|
|
206
|
+
// courses of small blocks, running bond — fixed 88 CSS px course
|
|
207
|
+
// height (not viewport- or page-derived), so bricks read the same
|
|
208
|
+
// size on every display. Block width capped at course height:
|
|
209
|
+
// more blocks on wide screens, never slabs.
|
|
210
|
+
const bh = 88 * dpr
|
|
211
|
+
const rowCount = Math.ceil(h / bh)
|
|
212
|
+
const MAX_BLOCK_RATIO = 1.0
|
|
213
|
+
for (let r = 0; r < rowCount; r++) {
|
|
214
|
+
const y = r * bh
|
|
215
|
+
const rawBlocks = 5 + ((r * 7) % 4)
|
|
216
|
+
const bw = Math.min(w / rawBlocks, bh * MAX_BLOCK_RATIO)
|
|
217
|
+
const span = bw * Math.ceil(w / bw)
|
|
218
|
+
const x0 = (w - span) / 2
|
|
219
|
+
const bond = r % 2 === 0 ? 0 : -bw / 2
|
|
220
|
+
for (let x = x0 + bond; x < w; x += bw) {
|
|
221
|
+
const tone = (rand() - 0.5) * 14
|
|
222
|
+
const base = 14 + tone
|
|
223
|
+
// block body: subtle vertical falloff, lighter crown
|
|
224
|
+
const g = c.createLinearGradient(0, y, 0, y + bh)
|
|
225
|
+
g.addColorStop(0, `rgb(${base + 9},${base + 1},${Math.max(0, base - 9)})`)
|
|
226
|
+
g.addColorStop(0.12, `rgb(${base + 4},${base - 2},${Math.max(0, base - 10)})`)
|
|
227
|
+
g.addColorStop(1, `rgb(${Math.max(0, base - 6)},${Math.max(0, base - 9)},${Math.max(0, base - 13)})`)
|
|
228
|
+
c.fillStyle = g
|
|
229
|
+
c.fillRect(x + 1.5, y + 1.5, bw - 3, bh - 3)
|
|
230
|
+
// top bevel catches the cult light
|
|
231
|
+
c.fillStyle = 'rgba(255,230,0,0.055)'
|
|
232
|
+
c.fillRect(x + 1.5, y + 1.5, bw - 3, Math.max(1, dpr))
|
|
233
|
+
c.fillStyle = 'rgba(255,255,255,0.03)'
|
|
234
|
+
c.fillRect(x + 1.5, y + 1.5 + dpr, bw - 3, Math.max(1, dpr * 0.5))
|
|
235
|
+
// foot shadow
|
|
236
|
+
c.fillStyle = 'rgba(0,0,0,0.55)'
|
|
237
|
+
c.fillRect(x + 1.5, y + bh - 1.5 - dpr * 1.5, bw - 3, dpr * 1.5)
|
|
238
|
+
// occasional stained block
|
|
239
|
+
if (rand() < 0.18) {
|
|
240
|
+
c.fillStyle = `rgba(0,0,0,${0.12 + rand() * 0.18})`
|
|
241
|
+
c.fillRect(x + 1.5, y + 1.5, bw - 3, bh - 3)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// mortar shadow grid
|
|
246
|
+
c.fillStyle = 'rgba(0,0,0,0.8)'
|
|
247
|
+
for (let r = 0; r <= rowCount; r++) c.fillRect(0, r * bh - 1, w, 2)
|
|
248
|
+
// speckle noise
|
|
249
|
+
const specks = Math.floor((w * h) / 900)
|
|
250
|
+
for (let i = 0; i < specks; i++) {
|
|
251
|
+
const sx = rand() * w
|
|
252
|
+
const sy = rand() * h
|
|
253
|
+
c.fillStyle = rand() < 0.12 ? 'rgba(255,230,0,0.05)' : 'rgba(0,0,0,0.35)'
|
|
254
|
+
c.fillRect(sx, sy, dpr, dpr)
|
|
255
|
+
}
|
|
256
|
+
// baked vignette
|
|
257
|
+
const v = c.createRadialGradient(
|
|
258
|
+
w / 2, h / 2, Math.min(w, h) * 0.35,
|
|
259
|
+
w / 2, h / 2, Math.max(w, h) * 0.75,
|
|
260
|
+
)
|
|
261
|
+
v.addColorStop(0, 'rgba(0,0,0,0)')
|
|
262
|
+
v.addColorStop(1, 'rgba(0,0,0,0.55)')
|
|
263
|
+
c.fillStyle = v
|
|
264
|
+
c.fillRect(0, 0, w, h)
|
|
265
|
+
ctx.clearRect(0, 0, w, h)
|
|
266
|
+
ctx.drawImage(off, 0, 0)
|
|
267
|
+
}
|
|
268
|
+
bake()
|
|
269
|
+
const ro = new ResizeObserver(bake)
|
|
270
|
+
if (canvas.parentElement) ro.observe(canvas.parentElement)
|
|
271
|
+
return () => ro.disconnect()
|
|
272
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
273
|
+
}, [])
|
|
274
|
+
return (
|
|
275
|
+
<canvas
|
|
276
|
+
ref={ref}
|
|
277
|
+
className="mey-bg-canvas"
|
|
278
|
+
style={{ opacity, zIndex: z }}
|
|
279
|
+
aria-hidden="true"
|
|
280
|
+
/>
|
|
281
|
+
)
|
|
186
282
|
}
|
|
187
283
|
|
|
188
|
-
export function VeilFog({ opacity = 0.7 }: FxProps) {
|
|
284
|
+
export function VeilFog({ opacity = 0.7, z = 1 }: FxProps) {
|
|
189
285
|
return (
|
|
190
|
-
<div className="mey-veil" style={{ opacity }} aria-hidden="true">
|
|
286
|
+
<div className="mey-veil" style={{ opacity, zIndex: z }} aria-hidden="true">
|
|
191
287
|
<i />
|
|
192
288
|
<i />
|
|
193
289
|
</div>
|
|
@@ -195,6 +291,6 @@ export function VeilFog({ opacity = 0.7 }: FxProps) {
|
|
|
195
291
|
}
|
|
196
292
|
|
|
197
293
|
|
|
198
|
-
export function BreathVignette() {
|
|
199
|
-
return <div className="mey-breath" aria-hidden="true" />
|
|
294
|
+
export function BreathVignette({ z = 4 }: FxProps) {
|
|
295
|
+
return <div className="mey-breath" style={{ zIndex: z }} aria-hidden="true" />
|
|
200
296
|
}
|
package/src/unveil.ts
CHANGED
|
@@ -55,24 +55,5 @@ export function unveilLocalLabel(at: number = UNVEIL_AT): string {
|
|
|
55
55
|
}).format(new Date(at))
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
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')
|
|
58
|
+
// Pure time math lives in @meyvoid/core — re-exported here for compat.
|
|
59
|
+
export { diffParts, pad2, type CountdownParts } from '@meyvoid/core'
|
package/src/auth/Gate.tsx
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { RequireAuthProps } from './types'
|
|
2
|
-
|
|
3
|
-
/** Framework-free auth gate — host passes user/loading from its own auth wiring. */
|
|
4
|
-
export function RequireAuth({
|
|
5
|
-
user,
|
|
6
|
-
loading,
|
|
7
|
-
loadingFallback = null,
|
|
8
|
-
loginFallback = null,
|
|
9
|
-
children,
|
|
10
|
-
}: RequireAuthProps) {
|
|
11
|
-
if (loading) return <>{loadingFallback}</>
|
|
12
|
-
if (!user) return <>{loginFallback}</>
|
|
13
|
-
return <>{children}</>
|
|
14
|
-
}
|
package/src/auth/types.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export interface SessionProfile {
|
|
2
|
-
id: string
|
|
3
|
-
email?: string | null
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface RequireAuthProps {
|
|
7
|
-
user: SessionProfile | null
|
|
8
|
-
loading: boolean
|
|
9
|
-
loadingFallback?: React.ReactNode
|
|
10
|
-
loginFallback?: React.ReactNode
|
|
11
|
-
children: React.ReactNode
|
|
12
|
-
}
|
package/src/brand/GodEmperor.tsx
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export default function GodEmperorBrand() {
|
|
2
|
-
return (
|
|
3
|
-
<span style={{ fontFamily: 'var(--font-logo)', fontWeight: 700, textTransform: 'none' }}>
|
|
4
|
-
<span style={{ color: '#ffffff' }}>God</span>
|
|
5
|
-
<span style={{ color: 'var(--color-premium-400)' }}>Emperor</span>
|
|
6
|
-
</span>
|
|
7
|
-
)
|
|
8
|
-
}
|
package/src/cn.ts
DELETED
package/src/lib/images.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export function isAbsoluteUrl(src: string): boolean {
|
|
2
|
-
return /^https?:\/\//.test(src) || src.startsWith('data:') || src.startsWith('blob:')
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
/** Resolve a product/asset src against an optional storage base. */
|
|
6
|
-
export function resolveImage(src: string, base?: string): string {
|
|
7
|
-
if (!src) return src
|
|
8
|
-
if (isAbsoluteUrl(src) || !base) return src
|
|
9
|
-
return `${base.replace(/\/$/, '')}/${src.replace(/^\//, '')}`
|
|
10
|
-
}
|
package/src/lib/money.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
const SYMBOLS: Record<string, string> = {
|
|
2
|
-
USD: '$',
|
|
3
|
-
ZAR: 'R',
|
|
4
|
-
EUR: '€',
|
|
5
|
-
GBP: '£',
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** Format integer minor units (cents) — e.g. formatPrice(1999) → '$19.99'. */
|
|
9
|
-
export function formatPrice(cents: number, currency = 'USD'): string {
|
|
10
|
-
const symbol = SYMBOLS[currency] ?? `${currency} `
|
|
11
|
-
return `${symbol}${(cents / 100).toFixed(2)}`
|
|
12
|
-
}
|
package/src/lib/store.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { create, type StateCreator } from 'zustand'
|
|
2
|
-
import { persist, type PersistOptions } from 'zustand/middleware'
|
|
3
|
-
|
|
4
|
-
/** Persisted zustand store with a stable shape — mirrors the meybuddy store pattern. */
|
|
5
|
-
export function createPersistStore<T>(
|
|
6
|
-
name: string,
|
|
7
|
-
version: number,
|
|
8
|
-
initializer: StateCreator<T, [], []>,
|
|
9
|
-
options?: Partial<PersistOptions<T, T>>,
|
|
10
|
-
) {
|
|
11
|
-
return create<T>()(
|
|
12
|
-
persist(initializer, {
|
|
13
|
-
name,
|
|
14
|
-
version,
|
|
15
|
-
skipHydration: true,
|
|
16
|
-
...options,
|
|
17
|
-
}),
|
|
18
|
-
)
|
|
19
|
-
}
|
package/src/shell/types.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { ComponentType, AnchorHTMLAttributes } from 'react'
|
|
2
|
-
|
|
3
|
-
export interface NavLink {
|
|
4
|
-
label: string
|
|
5
|
-
href: string
|
|
6
|
-
external?: boolean
|
|
7
|
-
branded?: boolean
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Injectable link — host passes next/link (web) or react-router Link (app). Defaults to <a>. */
|
|
11
|
-
export type LinkComponent = ComponentType<
|
|
12
|
-
AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }
|
|
13
|
-
>
|