diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte index a5b672da..792d73de 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte @@ -1419,24 +1419,58 @@ const code_to_icon: { * active they form a proper tri-phase RGB cycle. Two active slots are 120° apart. * Negative animation-delay starts each slot mid-cycle at its designated phase. */ -/* Cycle mode (default): fast linear hue rotation, 120° phase apart per slot */ -@keyframes ae_punch_hole_rainbow { - from { filter: hue-rotate(0deg); } - to { filter: hue-rotate(360deg); } +/* + * Perceptually uniform punch-hole rainbow via OKLCH + @property. + * + * WHY NOT hue-rotate(): CSS filter hue-rotate() works in RGB space — perceptual + * brightness varies across hues (yellow ≫ blue), so slots at different phases look + * noticeably lighter or darker than each other. + * + * FIX: @property lets the browser interpolate --ph-hue as a number. Applying colors + * as oklch(L C H) rotates hue at CONSTANT perceptual lightness — all three slots + * stay visually equally bright regardless of phase position. + * + * inherits: true — the animated value cascades to SVG children (rect, line) so + * each child reads the parent div's current --ph-hue without its own animation. + */ +@property --ph-hue { + syntax: ''; + inherits: true; + initial-value: 0; +} +@property --ph-lit { + syntax: ''; + inherits: true; + initial-value: 0.65; } -.punch_hole_rainbow_left { animation: ae_punch_hole_rainbow 2.5s 0.000s linear infinite; } -.punch_hole_rainbow_right { animation: ae_punch_hole_rainbow 2.5s -0.833s linear infinite; } -.punch_hole_rainbow_center { animation: ae_punch_hole_rainbow 2.5s -1.667s linear infinite; } -/* Pulse mode: slow breathing — dim→bright while hue shifts 180° and back. - Same 120° phase offsets (6s ÷ 3 = 2s per slot) so they breathe in turn. */ -@keyframes ae_punch_hole_pulse { - 0%, 100% { filter: hue-rotate(0deg) brightness(0.55); } - 50% { filter: hue-rotate(180deg) brightness(1.30); } +/* Cycle: hue rotates, lightness stays at 0.65 */ +@keyframes ae_ph_cycle { from { --ph-hue: 0; } to { --ph-hue: 360; } } + +/* Pulse: hue shifts 180° while lightness breathes 0.42 → 0.78 → 0.42 */ +@keyframes ae_ph_pulse { + 0%, 100% { --ph-hue: 0; --ph-lit: 0.42; } + 50% { --ph-hue: 180; --ph-lit: 0.78; } +} + +/* 120° phase offsets: 2.5s ÷ 3 ≈ 0.833s | 6s ÷ 3 = 2s */ +.punch_hole_rainbow_left { animation: ae_ph_cycle 2.5s 0.000s linear infinite; } +.punch_hole_rainbow_right { animation: ae_ph_cycle 2.5s -0.833s linear infinite; } +.punch_hole_rainbow_center { animation: ae_ph_cycle 2.5s -1.667s linear infinite; } +.punch_hole_pulse_left { animation: ae_ph_pulse 6s 0.000s ease-in-out infinite; } +.punch_hole_pulse_right { animation: ae_ph_pulse 6s -2.000s ease-in-out infinite; } +.punch_hole_pulse_center { animation: ae_ph_pulse 6s -4.000s ease-in-out infinite; } + +/* CSS overrides the static inline SVG fill/stroke attributes for animated markers. */ +.punch_hole_rainbow_left rect, .punch_hole_rainbow_right rect, .punch_hole_rainbow_center rect, +.punch_hole_pulse_left rect, .punch_hole_pulse_right rect, .punch_hole_pulse_center rect { + fill: oklch(var(--ph-lit) 0.20 var(--ph-hue) / 0.40); + stroke: oklch(var(--ph-lit) 0.25 var(--ph-hue)); +} +.punch_hole_rainbow_left line, .punch_hole_rainbow_right line, .punch_hole_rainbow_center line, +.punch_hole_pulse_left line, .punch_hole_pulse_right line, .punch_hole_pulse_center line { + stroke: oklch(var(--ph-lit) 0.25 var(--ph-hue)); } -.punch_hole_pulse_left { animation: ae_punch_hole_pulse 6s 0.000s ease-in-out infinite; } -.punch_hole_pulse_right { animation: ae_punch_hole_pulse 6s -2.000s ease-in-out infinite; } -.punch_hole_pulse_center { animation: ae_punch_hole_pulse 6s -4.000s ease-in-out infinite; } .punch_hole_print_rgb { display: none; } @media print {