Skip to content

Commit 6dc07a5

Browse files
authored
Merge pull request #16 from CompassSecurity/feature/anaglyph
feature/frontend:Anaglyph effect
2 parents f6fffc4 + 02cb1a7 commit 6dc07a5

3 files changed

Lines changed: 233 additions & 36 deletions

File tree

frontend/src/components/TopBar.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
DropdownMenuSeparator,
2121
DropdownMenuTrigger,
2222
} from '@/components/ui/dropdown-menu';
23+
import { useAnaglyph } from '@/composables/useAnaglyph';
2324
import { useAssessmentDetailStore } from '@/stores/assessmentDetail';
2425
import { useAuthStore } from '@/stores/auth';
2526
import { usePreferencesStore } from '@/stores/preferences';
@@ -31,6 +32,7 @@ const route = useRoute();
3132
const isDark = useDark();
3233
const toggleDark = useToggle(isDark);
3334
const showAboutModal = ref(false);
35+
const { registerClick: onLogoClick } = useAnaglyph();
3436
3537
// Breadcrumb: show assessment name when on assessment or activity routes
3638
const assessmentId = computed(() => route.params.id as string | undefined);
@@ -71,7 +73,7 @@ const handleLogout = async () => {
7173
<!-- Logo + Breadcrumb -->
7274
<div class="flex items-center gap-1 min-w-0">
7375
<h1 class="text-2xl font-bold shrink-0">
74-
<RouterLink to="/" class="hover:opacity-80 transition-opacity">RAPTR</RouterLink>
76+
<RouterLink to="/" class="hover:opacity-80 transition-opacity" @click="onLogoClick">RAPTR</RouterLink>
7577
</h1>
7678
<template v-if="isAssessmentRoute && assessmentName">
7779
<ChevronRight class="h-4 w-4 text-muted-foreground shrink-0" />
@@ -155,6 +157,7 @@ const handleLogout = async () => {
155157
<!-- About Modal -->
156158
<AboutModal v-model:open="showAboutModal" />
157159

160+
158161
<!-- Logout -->
159162
<Button @click="handleLogout" variant="destructive" size="sm" class="ml-2">
160163
<LogOut class="mr-2 h-4 w-4" />
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ref, watch } from 'vue';
2+
3+
const REQUIRED_CLICKS = 5;
4+
const TIME_WINDOW_MS = 2000;
5+
const HTML_CLASS = 'anaglyph-active';
6+
7+
/**
8+
* Anaglyph composable — tracks rapid clicks and toggles a whole-page
9+
* anaglyph effect by adding/removing a CSS class on <html>.
10+
*
11+
* Call `registerClick()` on each click. If 5 clicks land within 2 seconds,
12+
* the effect toggles on/off.
13+
*/
14+
export function useAnaglyph() {
15+
const activated = ref(false);
16+
const clickTimestamps: number[] = [];
17+
18+
function registerClick() {
19+
const now = Date.now();
20+
clickTimestamps.push(now);
21+
22+
// Keep only clicks within the time window
23+
while (
24+
clickTimestamps.length > 0 &&
25+
now - clickTimestamps[0] > TIME_WINDOW_MS
26+
) {
27+
clickTimestamps.shift();
28+
}
29+
30+
if (clickTimestamps.length >= REQUIRED_CLICKS) {
31+
activated.value = !activated.value;
32+
clickTimestamps.length = 0;
33+
}
34+
}
35+
36+
// Sync the CSS class on <html>
37+
watch(activated, (active) => {
38+
if (active) {
39+
document.documentElement.classList.add(HTML_CLASS);
40+
} else {
41+
document.documentElement.classList.remove(HTML_CLASS);
42+
}
43+
});
44+
45+
return { activated, registerClick };
46+
}

frontend/src/style.css

Lines changed: 183 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,41 @@
44
@custom-variant dark (&:is(.dark *));
55

66
@theme inline {
7-
--radius-sm: calc(var(--radius) - 4px);
8-
--radius-md: calc(var(--radius) - 2px);
9-
--radius-lg: var(--radius);
10-
--radius-xl: calc(var(--radius) + 4px);
11-
--color-background: var(--background);
12-
--color-foreground: var(--foreground);
13-
--color-card: var(--card);
14-
--color-card-foreground: var(--card-foreground);
15-
--color-popover: var(--popover);
16-
--color-popover-foreground: var(--popover-foreground);
17-
--color-primary: var(--primary);
18-
--color-primary-foreground: var(--primary-foreground);
19-
--color-secondary: var(--secondary);
20-
--color-secondary-foreground: var(--secondary-foreground);
21-
--color-muted: var(--muted);
22-
--color-muted-foreground: var(--muted-foreground);
23-
--color-accent: var(--accent);
24-
--color-accent-foreground: var(--accent-foreground);
25-
--color-destructive: var(--destructive);
26-
--color-border: var(--border);
27-
--color-input: var(--input);
28-
--color-ring: var(--ring);
29-
--color-chart-1: var(--chart-1);
30-
--color-chart-2: var(--chart-2);
31-
--color-chart-3: var(--chart-3);
32-
--color-chart-4: var(--chart-4);
33-
--color-chart-5: var(--chart-5);
34-
--color-sidebar: var(--sidebar);
35-
--color-sidebar-foreground: var(--sidebar-foreground);
36-
--color-sidebar-primary: var(--sidebar-primary);
37-
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
38-
--color-sidebar-accent: var(--sidebar-accent);
39-
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
40-
--color-sidebar-border: var(--sidebar-border);
41-
--color-sidebar-ring: var(--sidebar-ring);
7+
--radius-sm: calc(var(--radius) - 4px);
8+
--radius-md: calc(var(--radius) - 2px);
9+
--radius-lg: var(--radius);
10+
--radius-xl: calc(var(--radius) + 4px);
11+
--color-background: var(--background);
12+
--color-foreground: var(--foreground);
13+
--color-card: var(--card);
14+
--color-card-foreground: var(--card-foreground);
15+
--color-popover: var(--popover);
16+
--color-popover-foreground: var(--popover-foreground);
17+
--color-primary: var(--primary);
18+
--color-primary-foreground: var(--primary-foreground);
19+
--color-secondary: var(--secondary);
20+
--color-secondary-foreground: var(--secondary-foreground);
21+
--color-muted: var(--muted);
22+
--color-muted-foreground: var(--muted-foreground);
23+
--color-accent: var(--accent);
24+
--color-accent-foreground: var(--accent-foreground);
25+
--color-destructive: var(--destructive);
26+
--color-border: var(--border);
27+
--color-input: var(--input);
28+
--color-ring: var(--ring);
29+
--color-chart-1: var(--chart-1);
30+
--color-chart-2: var(--chart-2);
31+
--color-chart-3: var(--chart-3);
32+
--color-chart-4: var(--chart-4);
33+
--color-chart-5: var(--chart-5);
34+
--color-sidebar: var(--sidebar);
35+
--color-sidebar-foreground: var(--sidebar-foreground);
36+
--color-sidebar-primary: var(--sidebar-primary);
37+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
38+
--color-sidebar-accent: var(--sidebar-accent);
39+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
40+
--color-sidebar-border: var(--sidebar-border);
41+
--color-sidebar-ring: var(--sidebar-ring);
4242
}
4343

4444
:root {
@@ -131,3 +131,151 @@
131131
html {
132132
font-size: 14px;
133133
}
134+
135+
/* ══════════════════════════════════════════════════════════
136+
Whole-page anaglyph 3D effect
137+
Activated by toggling .anaglyph-active on <html>
138+
══════════════════════════════════════════════════════════ */
139+
140+
/* Smooth transition for entering / leaving the effect */
141+
html.anaglyph-active * {
142+
transition: text-shadow 0.4s ease, box-shadow 0.4s ease, filter 0.4s ease;
143+
}
144+
145+
/* ── Text: red/cyan text-shadow on everything ────────────── */
146+
html.anaglyph-active * {
147+
text-shadow:
148+
-3px 0 rgba(255, 0, 0, 0.7),
149+
3px 0 rgba(0, 255, 255, 0.7) !important;
150+
animation: anaglyph-text-wobble 3s ease-in-out infinite;
151+
}
152+
153+
/* ── SVG icons (Lucide etc.) ─────────────────────────────── */
154+
html.anaglyph-active svg {
155+
filter:
156+
drop-shadow(-2px 0 rgba(255, 0, 0, 0.65)) drop-shadow(2px 0 rgba(0, 255, 255, 0.65));
157+
animation: anaglyph-icon-wobble 3s ease-in-out infinite;
158+
}
159+
160+
/* ── Images ──────────────────────────────────────────────── */
161+
html.anaglyph-active img {
162+
filter:
163+
drop-shadow(-3px 0 rgba(255, 0, 0, 0.5)) drop-shadow(3px 0 rgba(0, 255, 255, 0.5));
164+
}
165+
166+
/* ── Interactive elements: buttons, inputs ───────────────── */
167+
html.anaglyph-active button,
168+
html.anaglyph-active [role="button"],
169+
html.anaglyph-active input,
170+
html.anaglyph-active select,
171+
html.anaglyph-active textarea {
172+
box-shadow:
173+
-3px 0 0 rgba(255, 0, 0, 0.25),
174+
3px 0 0 rgba(0, 255, 255, 0.25) !important;
175+
}
176+
177+
/* ── Cards, tables, bordered containers ──────────────────── */
178+
html.anaglyph-active [data-slot="card"],
179+
html.anaglyph-active table,
180+
html.anaglyph-active .border,
181+
html.anaglyph-active [class*="rounded-lg"][class*="border"] {
182+
box-shadow:
183+
-3px 0 0 rgba(255, 0, 0, 0.18),
184+
3px 0 0 rgba(0, 255, 255, 0.18);
185+
}
186+
187+
/* ── Top nav bar ─────────────────────────────────────────── */
188+
html.anaglyph-active nav.border-b {
189+
box-shadow:
190+
0 2px 0 rgba(255, 0, 0, 0.2),
191+
0 -2px 0 rgba(0, 255, 255, 0.2);
192+
}
193+
194+
/* ── CRT scanline overlay ────────────────────────────────── */
195+
html.anaglyph-active body::after {
196+
content: '';
197+
position: fixed;
198+
inset: 0;
199+
z-index: 99999;
200+
pointer-events: none;
201+
background: repeating-linear-gradient(0deg,
202+
rgba(255, 255, 255, 0.025) 0px,
203+
rgba(255, 255, 255, 0.025) 1px,
204+
transparent 1px,
205+
transparent 3px);
206+
opacity: 0;
207+
animation: anaglyph-fade-in 0.5s ease forwards, anaglyph-scanline-scroll 8s linear infinite;
208+
}
209+
210+
/* ── Wobble animations ───────────────────────────────────── */
211+
@keyframes anaglyph-text-wobble {
212+
213+
0%,
214+
100% {
215+
text-shadow:
216+
-3px 0 rgba(255, 0, 0, 0.7),
217+
3px 0 rgba(0, 255, 255, 0.7);
218+
}
219+
220+
25% {
221+
text-shadow:
222+
-4px 1px rgba(255, 0, 0, 0.75),
223+
4px -1px rgba(0, 255, 255, 0.75);
224+
}
225+
226+
50% {
227+
text-shadow:
228+
-2px -0.5px rgba(255, 0, 0, 0.65),
229+
2px 0.5px rgba(0, 255, 255, 0.65);
230+
}
231+
232+
75% {
233+
text-shadow:
234+
-4px 0.5px rgba(255, 0, 0, 0.7),
235+
4px -0.5px rgba(0, 255, 255, 0.7);
236+
}
237+
}
238+
239+
@keyframes anaglyph-icon-wobble {
240+
241+
0%,
242+
100% {
243+
filter:
244+
drop-shadow(-2px 0 rgba(255, 0, 0, 0.65)) drop-shadow(2px 0 rgba(0, 255, 255, 0.65));
245+
}
246+
247+
25% {
248+
filter:
249+
drop-shadow(-3px 0.5px rgba(255, 0, 0, 0.7)) drop-shadow(3px -0.5px rgba(0, 255, 255, 0.7));
250+
}
251+
252+
50% {
253+
filter:
254+
drop-shadow(-1.5px -0.5px rgba(255, 0, 0, 0.6)) drop-shadow(1.5px 0.5px rgba(0, 255, 255, 0.6));
255+
}
256+
257+
75% {
258+
filter:
259+
drop-shadow(-3px 0.3px rgba(255, 0, 0, 0.65)) drop-shadow(3px -0.3px rgba(0, 255, 255, 0.65));
260+
}
261+
}
262+
263+
@keyframes anaglyph-scanline-scroll {
264+
0% {
265+
background-position: 0 0;
266+
}
267+
268+
100% {
269+
background-position: 0 100px;
270+
}
271+
}
272+
273+
@keyframes anaglyph-fade-in {
274+
from {
275+
opacity: 0;
276+
}
277+
278+
to {
279+
opacity: 1;
280+
}
281+
}

0 commit comments

Comments
 (0)