feat(badges): rainbow animation option for punch-out hole markers
Adds left/right/center_rainbow to punch_holes cfg_json. When enabled, applies a CSS hue-rotate animation (2.5s loop) to the marker div using a saturated red base color so the full visible spectrum appears. Template form shows a Rainbow checkbox per slot; hides color pickers when active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,14 +64,17 @@ export interface BadgeTemplateCfg {
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
center?: boolean;
|
||||
fg?: string; // shared fallback stroke/line color
|
||||
bg?: string; // shared fallback fill color
|
||||
fg?: string; // shared fallback stroke/line color
|
||||
bg?: string; // shared fallback fill color
|
||||
left_fg?: string;
|
||||
left_bg?: string;
|
||||
left_rainbow?: boolean; // animated hue-rotate; overrides fg/bg base color with saturated red
|
||||
right_fg?: string;
|
||||
right_bg?: string;
|
||||
right_rainbow?: boolean;
|
||||
center_fg?: string;
|
||||
center_bg?: string;
|
||||
center_rainbow?: boolean;
|
||||
};
|
||||
|
||||
// Allow arbitrary extra keys to preserve forward-compatibility.
|
||||
|
||||
@@ -333,16 +333,23 @@ let punch_holes_right = $derived(!!template_cfg?.punch_holes?.right);
|
||||
let punch_holes_center = $derived(!!template_cfg?.punch_holes?.center);
|
||||
|
||||
// Per-slot colors: slot-specific override → shared fallback → component default.
|
||||
const PH_DEFAULT_FG = '#777777';
|
||||
const PH_DEFAULT_BG = 'rgba(255,255,255,0.4)';
|
||||
// When rainbow is on and no explicit fg/bg is set, use a saturated base color so
|
||||
// hue-rotate produces a full visible spectrum (gray has no hue to rotate).
|
||||
const PH_DEFAULT_FG = '#777777';
|
||||
const PH_DEFAULT_BG = 'rgba(255,255,255,0.4)';
|
||||
const PH_RAINBOW_FG = '#ff2200'; // saturated red → full spectrum via hue-rotate
|
||||
const PH_RAINBOW_BG = 'rgba(255,34,0,0.25)';
|
||||
let punch_holes_left_rainbow = $derived(!!template_cfg?.punch_holes?.left_rainbow);
|
||||
let punch_holes_right_rainbow = $derived(!!template_cfg?.punch_holes?.right_rainbow);
|
||||
let punch_holes_center_rainbow = $derived(!!template_cfg?.punch_holes?.center_rainbow);
|
||||
let punch_holes_colors = $derived.by(() => {
|
||||
const ph = template_cfg?.punch_holes;
|
||||
const shared_fg = ph?.fg || PH_DEFAULT_FG;
|
||||
const shared_bg = ph?.bg || PH_DEFAULT_BG;
|
||||
return {
|
||||
left: { fg: ph?.left_fg || shared_fg, bg: ph?.left_bg || shared_bg },
|
||||
right: { fg: ph?.right_fg || shared_fg, bg: ph?.right_bg || shared_bg },
|
||||
center: { fg: ph?.center_fg || shared_fg, bg: ph?.center_bg || shared_bg }
|
||||
left: { fg: ph?.left_fg || (ph?.left_rainbow ? PH_RAINBOW_FG : shared_fg), bg: ph?.left_bg || (ph?.left_rainbow ? PH_RAINBOW_BG : shared_bg) },
|
||||
right: { fg: ph?.right_fg || (ph?.right_rainbow ? PH_RAINBOW_FG : shared_fg), bg: ph?.right_bg || (ph?.right_rainbow ? PH_RAINBOW_BG : shared_bg) },
|
||||
center: { fg: ph?.center_fg || (ph?.center_rainbow ? PH_RAINBOW_FG : shared_fg), bg: ph?.center_bg || (ph?.center_rainbow ? PH_RAINBOW_BG : shared_bg) }
|
||||
};
|
||||
});
|
||||
|
||||
@@ -694,6 +701,7 @@ const code_to_icon: {
|
||||
registration variance (slots are only 3mm tall, registration matters). -->
|
||||
{#if punch_holes_left}
|
||||
<div class="punch_hole_marker pointer-events-none absolute"
|
||||
class:punch_hole_rainbow={punch_holes_left_rainbow}
|
||||
aria-hidden="true"
|
||||
style="top: calc(0.25in + 1mm); left: calc(0.375in + 1mm); width: calc(0.625in - 2mm); height: calc(0.125in - 2mm); z-index: 10;">
|
||||
<svg width="100%" height="100%" viewBox="0 0 50 8" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -705,6 +713,7 @@ const code_to_icon: {
|
||||
{/if}
|
||||
{#if punch_holes_right}
|
||||
<div class="punch_hole_marker pointer-events-none absolute"
|
||||
class:punch_hole_rainbow={punch_holes_right_rainbow}
|
||||
aria-hidden="true"
|
||||
style="top: calc(0.25in + 1mm); right: calc(0.375in + 1mm); width: calc(0.625in - 2mm); height: calc(0.125in - 2mm); z-index: 10;">
|
||||
<svg width="100%" height="100%" viewBox="0 0 50 8" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -716,6 +725,7 @@ const code_to_icon: {
|
||||
{/if}
|
||||
{#if punch_holes_center}
|
||||
<div class="punch_hole_marker pointer-events-none absolute"
|
||||
class:punch_hole_rainbow={punch_holes_center_rainbow}
|
||||
aria-hidden="true"
|
||||
style="top: calc(0.25in + 1mm); left: 50%; transform: translateX(-50%); width: calc(0.625in - 2mm); height: calc(0.125in - 2mm); z-index: 10;">
|
||||
<svg width="100%" height="100%" viewBox="0 0 50 8" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -1325,6 +1335,20 @@ const code_to_icon: {
|
||||
|
||||
|
||||
<style>
|
||||
/*
|
||||
* Punch-hole rainbow animation: continuously rotates the hue of the marker div.
|
||||
* A saturated base color (PH_RAINBOW_FG) is used so the full visible spectrum appears.
|
||||
* On print the animation freezes at whatever frame the browser captures — this is fine,
|
||||
* the marker still shows in some vivid color and remains within the hole boundary.
|
||||
*/
|
||||
@keyframes ae_punch_hole_rainbow {
|
||||
from { filter: hue-rotate(0deg); }
|
||||
to { filter: hue-rotate(360deg); }
|
||||
}
|
||||
.punch_hole_rainbow {
|
||||
animation: ae_punch_hole_rainbow 2.5s linear infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
* Force light-mode rendering on the badge print area.
|
||||
* Badges print on physical card stock (white by default, or a defined color set by
|
||||
|
||||
@@ -76,13 +76,16 @@ let cfg_header_padding_left = $state('');
|
||||
let cfg_punch_holes_left = $state(false);
|
||||
let cfg_punch_holes_right = $state(false);
|
||||
let cfg_punch_holes_center = $state(false);
|
||||
// Per-slot colors. Empty = use component default (#777777 fg, semi-transparent white bg).
|
||||
let cfg_punch_holes_left_fg = $state('');
|
||||
let cfg_punch_holes_left_bg = $state('');
|
||||
let cfg_punch_holes_right_fg = $state('');
|
||||
let cfg_punch_holes_right_bg = $state('');
|
||||
let cfg_punch_holes_center_fg = $state('');
|
||||
let cfg_punch_holes_center_bg = $state('');
|
||||
// Per-slot colors and rainbow. Empty color = use component default.
|
||||
let cfg_punch_holes_left_fg = $state('');
|
||||
let cfg_punch_holes_left_bg = $state('');
|
||||
let cfg_punch_holes_left_rainbow = $state(false);
|
||||
let cfg_punch_holes_right_fg = $state('');
|
||||
let cfg_punch_holes_right_bg = $state('');
|
||||
let cfg_punch_holes_right_rainbow = $state(false);
|
||||
let cfg_punch_holes_center_fg = $state('');
|
||||
let cfg_punch_holes_center_bg = $state('');
|
||||
let cfg_punch_holes_center_rainbow = $state(false);
|
||||
// Header bottom border. Empty color = no border.
|
||||
let cfg_header_border_color = $state('');
|
||||
let cfg_header_border_width = $state('');
|
||||
@@ -185,12 +188,15 @@ async function load_template(id: string) {
|
||||
cfg_punch_holes_left = parsed_cfg?.punch_holes?.left ?? false;
|
||||
cfg_punch_holes_right = parsed_cfg?.punch_holes?.right ?? false;
|
||||
cfg_punch_holes_center = parsed_cfg?.punch_holes?.center ?? false;
|
||||
cfg_punch_holes_left_fg = parsed_cfg?.punch_holes?.left_fg ?? '';
|
||||
cfg_punch_holes_left_bg = parsed_cfg?.punch_holes?.left_bg ?? '';
|
||||
cfg_punch_holes_right_fg = parsed_cfg?.punch_holes?.right_fg ?? '';
|
||||
cfg_punch_holes_right_bg = parsed_cfg?.punch_holes?.right_bg ?? '';
|
||||
cfg_punch_holes_center_fg = parsed_cfg?.punch_holes?.center_fg ?? '';
|
||||
cfg_punch_holes_center_bg = parsed_cfg?.punch_holes?.center_bg ?? '';
|
||||
cfg_punch_holes_left_fg = parsed_cfg?.punch_holes?.left_fg ?? '';
|
||||
cfg_punch_holes_left_bg = parsed_cfg?.punch_holes?.left_bg ?? '';
|
||||
cfg_punch_holes_left_rainbow = parsed_cfg?.punch_holes?.left_rainbow ?? false;
|
||||
cfg_punch_holes_right_fg = parsed_cfg?.punch_holes?.right_fg ?? '';
|
||||
cfg_punch_holes_right_bg = parsed_cfg?.punch_holes?.right_bg ?? '';
|
||||
cfg_punch_holes_right_rainbow = parsed_cfg?.punch_holes?.right_rainbow ?? false;
|
||||
cfg_punch_holes_center_fg = parsed_cfg?.punch_holes?.center_fg ?? '';
|
||||
cfg_punch_holes_center_bg = parsed_cfg?.punch_holes?.center_bg ?? '';
|
||||
cfg_punch_holes_center_rainbow = parsed_cfg?.punch_holes?.center_rainbow ?? false;
|
||||
cfg_header_border_color = parsed_cfg.header_border_color ?? '';
|
||||
cfg_header_border_width = parsed_cfg.header_border_width ?? '';
|
||||
cfg_header_padding_top = parsed_cfg.header_padding_top ?? '';
|
||||
@@ -282,12 +288,15 @@ async function handle_submit() {
|
||||
right: cfg_punch_holes_right,
|
||||
center: cfg_punch_holes_center
|
||||
};
|
||||
if (cfg_punch_holes_left_fg.trim()) ph.left_fg = cfg_punch_holes_left_fg.trim();
|
||||
if (cfg_punch_holes_left_bg.trim()) ph.left_bg = cfg_punch_holes_left_bg.trim();
|
||||
if (cfg_punch_holes_right_fg.trim()) ph.right_fg = cfg_punch_holes_right_fg.trim();
|
||||
if (cfg_punch_holes_right_bg.trim()) ph.right_bg = cfg_punch_holes_right_bg.trim();
|
||||
if (cfg_punch_holes_center_fg.trim()) ph.center_fg = cfg_punch_holes_center_fg.trim();
|
||||
if (cfg_punch_holes_center_bg.trim()) ph.center_bg = cfg_punch_holes_center_bg.trim();
|
||||
if (cfg_punch_holes_left_fg.trim()) ph.left_fg = cfg_punch_holes_left_fg.trim();
|
||||
if (cfg_punch_holes_left_bg.trim()) ph.left_bg = cfg_punch_holes_left_bg.trim();
|
||||
if (cfg_punch_holes_left_rainbow) ph.left_rainbow = true;
|
||||
if (cfg_punch_holes_right_fg.trim()) ph.right_fg = cfg_punch_holes_right_fg.trim();
|
||||
if (cfg_punch_holes_right_bg.trim()) ph.right_bg = cfg_punch_holes_right_bg.trim();
|
||||
if (cfg_punch_holes_right_rainbow) ph.right_rainbow = true;
|
||||
if (cfg_punch_holes_center_fg.trim()) ph.center_fg = cfg_punch_holes_center_fg.trim();
|
||||
if (cfg_punch_holes_center_bg.trim()) ph.center_bg = cfg_punch_holes_center_bg.trim();
|
||||
if (cfg_punch_holes_center_rainbow) ph.center_rainbow = true;
|
||||
cfg_obj.punch_holes = ph;
|
||||
} else {
|
||||
delete cfg_obj.punch_holes;
|
||||
@@ -431,35 +440,43 @@ function toggle_cfg_controls_auth_editable(key: string) {
|
||||
Markers help attendees know where to push out the perforations.
|
||||
Leave colors blank to use defaults (gray stroke, semi-transparent white fill).
|
||||
</p>
|
||||
{#snippet hole_colors(label: string, enabled: boolean, fg: string, bg: string, on_fg: (v: string) => void, on_bg: (v: string) => void)}
|
||||
{#snippet hole_colors(label: string, enabled: boolean, fg: string, bg: string, rainbow: boolean, on_fg: (v: string) => void, on_bg: (v: string) => void, on_rainbow: (v: boolean) => void)}
|
||||
<div class="space-y-1 rounded border border-surface-200-800 p-2">
|
||||
<label class="label flex items-center gap-2 font-medium">
|
||||
<input type="checkbox" checked={enabled} onchange={(e) => { if (e.target instanceof HTMLInputElement) { if (label === 'Left') cfg_punch_holes_left = e.target.checked; else if (label === 'Right') cfg_punch_holes_right = e.target.checked; else cfg_punch_holes_center = e.target.checked; } }} class="checkbox" />
|
||||
<span>{label} slot</span>
|
||||
</label>
|
||||
{#if enabled}
|
||||
<div class="grid grid-cols-2 gap-2 pl-6">
|
||||
<label class="label">
|
||||
<span class="text-xs">Stroke / line (fg)</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<input type="color" value={fg || '#777777'} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_fg(e.target.value); }} class="w-8 h-7 p-0" />
|
||||
<input type="text" value={fg} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_fg(e.target.value); }} class="input input-sm w-24" placeholder="#777777" />
|
||||
</div>
|
||||
<div class="pl-6 space-y-2">
|
||||
<label class="label flex items-center gap-2">
|
||||
<input type="checkbox" checked={rainbow} onchange={(e) => { if (e.target instanceof HTMLInputElement) on_rainbow(e.target.checked); }} class="checkbox" />
|
||||
<span class="text-sm">🌈 Rainbow</span>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-xs">Fill (bg)</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<input type="color" value={bg || '#ffffff'} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_bg(e.target.value); }} class="w-8 h-7 p-0" />
|
||||
<input type="text" value={bg} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_bg(e.target.value); }} class="input input-sm w-24" placeholder="#ffffff" />
|
||||
{#if !rainbow}
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="label">
|
||||
<span class="text-xs">Stroke / line (fg)</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<input type="color" value={fg || '#777777'} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_fg(e.target.value); }} class="w-8 h-7 p-0" />
|
||||
<input type="text" value={fg} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_fg(e.target.value); }} class="input input-sm w-24" placeholder="#777777" />
|
||||
</div>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="text-xs">Fill (bg)</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<input type="color" value={bg || '#ffffff'} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_bg(e.target.value); }} class="w-8 h-7 p-0" />
|
||||
<input type="text" value={bg} oninput={(e) => { if (e.target instanceof HTMLInputElement) on_bg(e.target.value); }} class="input input-sm w-24" placeholder="#ffffff" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
{@render hole_colors('Left', cfg_punch_holes_left, cfg_punch_holes_left_fg, cfg_punch_holes_left_bg, (v) => cfg_punch_holes_left_fg = v, (v) => cfg_punch_holes_left_bg = v)}
|
||||
{@render hole_colors('Right', cfg_punch_holes_right, cfg_punch_holes_right_fg, cfg_punch_holes_right_bg, (v) => cfg_punch_holes_right_fg = v, (v) => cfg_punch_holes_right_bg = v)}
|
||||
{@render hole_colors('Center', cfg_punch_holes_center, cfg_punch_holes_center_fg, cfg_punch_holes_center_bg, (v) => cfg_punch_holes_center_fg = v, (v) => cfg_punch_holes_center_bg = v)}
|
||||
{@render hole_colors('Left', cfg_punch_holes_left, cfg_punch_holes_left_fg, cfg_punch_holes_left_bg, cfg_punch_holes_left_rainbow, (v) => cfg_punch_holes_left_fg = v, (v) => cfg_punch_holes_left_bg = v, (v) => cfg_punch_holes_left_rainbow = v)}
|
||||
{@render hole_colors('Right', cfg_punch_holes_right, cfg_punch_holes_right_fg, cfg_punch_holes_right_bg, cfg_punch_holes_right_rainbow, (v) => cfg_punch_holes_right_fg = v, (v) => cfg_punch_holes_right_bg = v, (v) => cfg_punch_holes_right_rainbow = v)}
|
||||
{@render hole_colors('Center', cfg_punch_holes_center, cfg_punch_holes_center_fg, cfg_punch_holes_center_bg, cfg_punch_holes_center_rainbow, (v) => cfg_punch_holes_center_fg = v, (v) => cfg_punch_holes_center_bg = v, (v) => cfg_punch_holes_center_rainbow = v)}
|
||||
</div>
|
||||
|
||||
<label class="label">
|
||||
|
||||
Reference in New Issue
Block a user