Make hacker cursor trails visible on all desktop deck pages.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Remove Command-Deck-only gating, boost 0/1 particle intensity, add crosshair cursor, and relabel the settings toggle so operators can find the effect.
This commit is contained in:
@@ -348,10 +348,10 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`layout${isMobile ? ' layout--mobile' : ''}${othersOnline ? ' layout--comrades-online' : ''}`}
|
className={`layout${isMobile ? ' layout--mobile' : ''}${othersOnline ? ' layout--comrades-online' : ''}${!isMobile && glowParticles ? ' layout--hacker-cursor' : ''}`}
|
||||||
data-operator-deck={operatorDeckId(location.pathname)}
|
data-operator-deck={operatorDeckId(location.pathname)}
|
||||||
>
|
>
|
||||||
{!isMobile && glowParticles && showDeckEffects && <CursorFire />}
|
{!isMobile && glowParticles && <CursorFire />}
|
||||||
<AmbientBackground weather={pageWeather} />
|
<AmbientBackground weather={pageWeather} />
|
||||||
{glowParticles && <SacredGeometryLayer />}
|
{glowParticles && <SacredGeometryLayer />}
|
||||||
<nav className="sidebar sidebar--desktop desktop-only">
|
<nav className="sidebar sidebar--desktop desktop-only">
|
||||||
|
|||||||
@@ -2,12 +2,31 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 9998;
|
z-index: 9999;
|
||||||
mix-blend-mode: screen;
|
mix-blend-mode: screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layout--hacker-cursor {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout--hacker-cursor a,
|
||||||
|
.layout--hacker-cursor button,
|
||||||
|
.layout--hacker-cursor input,
|
||||||
|
.layout--hacker-cursor select,
|
||||||
|
.layout--hacker-cursor textarea,
|
||||||
|
.layout--hacker-cursor label,
|
||||||
|
.layout--hacker-cursor [role='button'],
|
||||||
|
.layout--hacker-cursor .nav-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.cursor-hacker-fx {
|
.cursor-hacker-fx {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layout--hacker-cursor {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import './CursorFire.css';
|
|||||||
|
|
||||||
const BIT_CHARS = '01';
|
const BIT_CHARS = '01';
|
||||||
const HEX_CHARS = '0123456789ABCDEF';
|
const HEX_CHARS = '0123456789ABCDEF';
|
||||||
const MAX_PARTICLES = 280;
|
const MAX_PARTICLES = 480;
|
||||||
const EMIT_PER_FRAME = 5;
|
const EMIT_PER_FRAME = 12;
|
||||||
const EMIT_WINDOW_MS = 90;
|
const EMIT_WINDOW_MS = 140;
|
||||||
const FONT_STACK = '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace';
|
const FONT_STACK = '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace';
|
||||||
|
|
||||||
interface Particle {
|
interface Particle {
|
||||||
@@ -21,18 +21,18 @@ interface Particle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pickChar(): string {
|
function pickChar(): string {
|
||||||
if (Math.random() < 0.72) return BIT_CHARS[Math.floor(Math.random() * 2)];
|
if (Math.random() < 0.88) return BIT_CHARS[Math.floor(Math.random() * 2)];
|
||||||
return HEX_CHARS[Math.floor(Math.random() * HEX_CHARS.length)];
|
return HEX_CHARS[Math.floor(Math.random() * HEX_CHARS.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
function colorForLife(life: number, tint: Particle['tint']): string {
|
function colorForLife(life: number, tint: Particle['tint']): string {
|
||||||
const a = Math.min(1, life * 0.92);
|
const a = Math.min(1, life * 1.05);
|
||||||
if (tint === 'cyan') {
|
if (tint === 'cyan') {
|
||||||
if (life > 0.55) return `rgba(0, 232, 245, ${a})`;
|
if (life > 0.5) return `rgba(0, 255, 255, ${a})`;
|
||||||
return `rgba(0, 210, 170, ${a * 0.8})`;
|
return `rgba(0, 240, 200, ${a * 0.92})`;
|
||||||
}
|
}
|
||||||
if (life > 0.55) return `rgba(0, 255, 136, ${a})`;
|
if (life > 0.5) return `rgba(80, 255, 160, ${a})`;
|
||||||
return `rgba(0, 190, 96, ${a * 0.75})`;
|
return `rgba(0, 255, 120, ${a * 0.9})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function glowForTint(tint: Particle['tint']): string {
|
function glowForTint(tint: Particle['tint']): string {
|
||||||
@@ -90,17 +90,17 @@ export default function CursorFire() {
|
|||||||
const { x, y } = mouse.current;
|
const { x, y } = mouse.current;
|
||||||
|
|
||||||
for (let i = 0; i < EMIT_PER_FRAME; i++) {
|
for (let i = 0; i < EMIT_PER_FRAME; i++) {
|
||||||
const spread = 10;
|
const spread = 14;
|
||||||
particles.current.push({
|
particles.current.push({
|
||||||
x: x + (Math.random() - 0.5) * spread,
|
x: x + (Math.random() - 0.5) * spread,
|
||||||
y: y + (Math.random() - 0.5) * (spread * 0.4),
|
y: y + (Math.random() - 0.5) * (spread * 0.45),
|
||||||
vx: (Math.random() - 0.5) * 1.4,
|
vx: (Math.random() - 0.5) * 1.8,
|
||||||
vy: -(Math.random() * 2.6 + 1.6),
|
vy: -(Math.random() * 3.2 + 2.1),
|
||||||
life: 1,
|
life: 1,
|
||||||
decay: Math.random() * 0.024 + 0.018,
|
decay: Math.random() * 0.016 + 0.012,
|
||||||
char: pickChar(),
|
char: pickChar(),
|
||||||
fontSize: Math.random() * 10 + 9,
|
fontSize: Math.random() * 16 + 16,
|
||||||
tint: Math.random() < 0.55 ? 'cyan' : 'green',
|
tint: Math.random() < 0.5 ? 'cyan' : 'green',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,10 +124,10 @@ export default function CursorFire() {
|
|||||||
p.life -= p.decay;
|
p.life -= p.decay;
|
||||||
p.fontSize *= 0.985;
|
p.fontSize *= 0.985;
|
||||||
|
|
||||||
if (p.life <= 0 || p.fontSize < 6) continue;
|
if (p.life <= 0 || p.fontSize < 8) continue;
|
||||||
alive.push(p);
|
alive.push(p);
|
||||||
|
|
||||||
const glow = 6 + p.life * 10;
|
const glow = 10 + p.life * 18;
|
||||||
ctx.shadowBlur = glow;
|
ctx.shadowBlur = glow;
|
||||||
ctx.shadowColor = glowForTint(p.tint);
|
ctx.shadowColor = glowForTint(p.tint);
|
||||||
ctx.font = `600 ${p.fontSize}px ${FONT_STACK}`;
|
ctx.font = `600 ${p.fontSize}px ${FONT_STACK}`;
|
||||||
|
|||||||
@@ -990,7 +990,7 @@ describe('Layout', () => {
|
|||||||
expect(screen.getByRole('link', { name: /Onion/i })).toBeInTheDocument();
|
expect(screen.getByRole('link', { name: /Onion/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mounts MatrixRain and CursorFire on Command Deck only', async () => {
|
it('mounts MatrixRain on Command Deck only and CursorFire on all desktop deck pages', async () => {
|
||||||
const { container: deck } = render(
|
const { container: deck } = render(
|
||||||
<MemoryRouter initialEntries={['/dashboard']} future={routerFuture}>
|
<MemoryRouter initialEntries={['/dashboard']} future={routerFuture}>
|
||||||
<Layout>
|
<Layout>
|
||||||
@@ -1015,6 +1015,7 @@ describe('Layout', () => {
|
|||||||
expect(screen.getByText('crucible')).toBeInTheDocument();
|
expect(screen.getByText('crucible')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(crucible.querySelector('.matrix-rain-canvas')).toBeNull();
|
expect(crucible.querySelector('.matrix-rain-canvas')).toBeNull();
|
||||||
expect(crucible.querySelector('.cursor-hacker-fx')).toBeNull();
|
expect(crucible.querySelector('.cursor-hacker-fx')).toBeTruthy();
|
||||||
|
expect(crucible.querySelector('.layout--hacker-cursor')).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -580,8 +580,8 @@ export default function SettingsPage() {
|
|||||||
<NeonCard accent="cyan" className="settings-section operator-deck-card operator-interactive">
|
<NeonCard accent="cyan" className="settings-section operator-deck-card operator-interactive">
|
||||||
<h2 className="font-display">Deck Atmosphere</h2>
|
<h2 className="font-display">Deck Atmosphere</h2>
|
||||||
<p className="section-desc">
|
<p className="section-desc">
|
||||||
Background glow particles and sparkles sit behind the UI (pointer-events off). Turn off on
|
Rising 0/1 and hex glyphs follow your pointer on desktop deck pages. Background glow
|
||||||
low-power devices if you want a calmer deck.
|
particles sit behind the UI. Turn off on low-power devices for a calmer deck.
|
||||||
</p>
|
</p>
|
||||||
<div className="form-group checkbox-group">
|
<div className="form-group checkbox-group">
|
||||||
<label className="checkbox-label">
|
<label className="checkbox-label">
|
||||||
@@ -591,7 +591,7 @@ export default function SettingsPage() {
|
|||||||
checked={glowParticles}
|
checked={glowParticles}
|
||||||
onChange={(e) => setGlowParticles(e.target.checked)}
|
onChange={(e) => setGlowParticles(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span>Glow particles & sparkles</span>
|
<span>Hacker cursor trail</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group" style={{ marginTop: '1.25rem' }}>
|
<div className="form-group" style={{ marginTop: '1.25rem' }}>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe('visualPrefs', () => {
|
|||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('defaults glow particles to on', () => {
|
it('defaults hacker cursor trail to on', () => {
|
||||||
expect(loadGlowParticlesEnabled()).toBe(true);
|
expect(loadGlowParticlesEnabled()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user