Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/svg/animations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ describe('getTowerAnimationCSS', () => {
expect(css).toContain('transform: translateY(-20px)');
});

it('returns wave animation when requested', () => {
const css = getTowerAnimationCSS('wave');
expect(css).toContain('@keyframes wave-up');
expect(css).toContain('transform: scaleY(1.15)');
});

it('returns bounce animation when requested', () => {
const css = getTowerAnimationCSS('bounce');
expect(css).toContain('@keyframes bounce-in');
expect(css).toContain('transform: translateY(-40px)');
});

it('returns static render when entrance is none', () => {
const css = getTowerAnimationCSS('none');
expect(css).toContain('transform: scaleY(1)');
Expand Down
35 changes: 34 additions & 1 deletion lib/svg/animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
const TOWER_BASE_Y = 10;

export function getTowerAnimationCSS(
entrance: 'rise' | 'fade' | 'slide' | 'none' = 'rise',
entrance: 'rise' | 'fade' | 'slide' | 'wave' | 'bounce' | 'none' = 'rise',
scale = 1.0
): string {
if (entrance === 'none') {
Expand Down Expand Up @@ -70,6 +70,39 @@ export function getTowerAnimationCSS(
to { opacity: 1; transform: translateY(0); }
}
`;
} else if (entrance === 'wave') {
const baseY = Math.round(TOWER_BASE_Y * scale * 100) / 100;
baseStyles = `
transform: scaleY(0);
transform-origin: 0 ${baseY}px;
animation: wave-up 1.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
`;
keyframes = `
@keyframes wave-up {
0% { transform: scaleY(0); }
50% { transform: scaleY(1.15); }
100% { transform: scaleY(1); }
}
`;
} else if (entrance === 'bounce') {
const slideOffset = Math.round(-40 * scale * 100) / 100;
const bounce1 = Math.round(-10 * scale * 100) / 100;
const bounce2 = Math.round(-4 * scale * 100) / 100;
baseStyles = `
opacity: 0;
transform: translateY(${slideOffset}px);
animation: bounce-in 1.2s cubic-bezier(0.28, 0.84, 0.42, 1) forwards;
`;
keyframes = `
@keyframes bounce-in {
0% { opacity: 0; transform: translateY(${slideOffset}px); }
50% { opacity: 1; transform: translateY(0); }
70% { transform: translateY(${bounce1}px); }
85% { transform: translateY(0); }
92% { transform: translateY(${bounce2}px); }
100% { opacity: 1; transform: translateY(0); }
}
`;
}

return `
Expand Down
2 changes: 1 addition & 1 deletion lib/svg/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ function renderStyle(
accent: string,
sf: number,
bg: string,
entrance: 'rise' | 'fade' | 'slide' | 'none' = 'rise'
entrance: 'rise' | 'fade' | 'slide' | 'wave' | 'bounce' | 'none' = 'rise'
): string {
const fs = (n: number) => Math.round(n * sf * 10) / 10;
const isLightBg = getLuminance(bg) > 0.5;
Expand Down
5 changes: 4 additions & 1 deletion lib/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,10 @@ const baseStreakParamsSchema = z.object({
// Glow effect — on by default. Accepts 'true'/'1' (true) or 'false' (false).
glow: z.string().optional().transform(toGlowFlag).default(true),
opacity: z.string().optional().transform(toOpacityValue),
entrance: z.enum(['rise', 'fade', 'slide', 'none']).catch('rise').default('rise'),
entrance: z
.enum(['rise', 'fade', 'slide', 'wave', 'bounce', 'none'])
.catch('rise')
.default('rise'),
badges: z.string().optional().transform(toBooleanFlag).default(false),

// Output format: 'svg' (default), 'json', or 'png' for image export.
Expand Down
2 changes: 1 addition & 1 deletion types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('Type Safety Assertions for types/index.ts', () => {
// Asserts that animations are restricted to exact string literals or undefined
expectTypeOf<BadgeParams>()
.toHaveProperty('entrance')
.toEqualTypeOf<'rise' | 'fade' | 'slide' | 'none' | undefined>();
.toEqualTypeOf<'rise' | 'fade' | 'slide' | 'wave' | 'bounce' | 'none' | undefined>();
});

it('Test 5: NotificationPreferences should enforce exact literal types for frequency', () => {
Expand Down
4 changes: 2 additions & 2 deletions types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ export interface BadgeParams {
/** Duration of the radar scan line animation (e.g. '4s', '8s', '12s'). Defaults to '8s'. */
speed: SpeedString;

/** Animation style for the isometric towers on load: 'rise' (default), 'fade', 'slide', or 'none'. */
entrance?: 'rise' | 'fade' | 'slide' | 'none';
/** Animation style for the isometric towers on load: 'rise' (default), 'fade', 'slide', 'wave', 'bounce', or 'none'. */
entrance?: 'rise' | 'fade' | 'slide' | 'wave' | 'bounce' | 'none';

/** Tower height scaling algorithm. 'linear' scales proportionally; 'log' uses logarithmic scale for high contributors. Defaults to 'linear'. */
scale: Scale;
Expand Down
Loading