Skip to content

Commit d4e3d1c

Browse files
authored
feat: synced 배지 성공 시 컨페티 효과 추가 (#6)
1 parent 9d50659 commit d4e3d1c

4 files changed

Lines changed: 116 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ dist-ssr
2222
*.njsproj
2323
*.sln
2424
*.sw?
25+
AGENTS.md

src/adapters/confetti.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
const COLORS = ["#22c55e", "#14b8a6", "#3b82f6", "#f59e0b", "#facc15", "#fb7185"];
2+
const CONTAINER_ID = "algorithmhub-confetti-container";
3+
4+
type ConfettiPieceState = {
5+
element: HTMLSpanElement;
6+
angle: number;
7+
distance: number;
8+
rise: number;
9+
drift: number;
10+
rotationStart: number;
11+
rotationVelocity: number;
12+
};
13+
14+
function randomBetween(min: number, max: number) {
15+
return Math.random() * (max - min) + min;
16+
}
17+
18+
function getContainer() {
19+
let container = document.getElementById(CONTAINER_ID);
20+
21+
if (container) {
22+
return container;
23+
}
24+
25+
container = document.createElement("div");
26+
container.id = CONTAINER_ID;
27+
container.style.position = "fixed";
28+
container.style.inset = "0";
29+
container.style.pointerEvents = "none";
30+
container.style.overflow = "hidden";
31+
container.style.zIndex = "2147483646";
32+
document.body.appendChild(container);
33+
return container;
34+
}
35+
36+
export function burstConfetti(origin?: { x: number; y: number }) {
37+
const container = getContainer();
38+
const width = window.innerWidth;
39+
const startX = origin?.x ?? width / 2;
40+
const startY =
41+
origin?.y ?? Math.min(window.innerHeight * 0.24, 156);
42+
const pieces: ConfettiPieceState[] = [];
43+
const total = 20;
44+
45+
for (let index = 0; index < total; index += 1) {
46+
const piece = document.createElement("span");
47+
piece.style.position = "absolute";
48+
piece.style.left = `${startX}px`;
49+
piece.style.top = `${startY}px`;
50+
const shape = index % 3;
51+
piece.style.width = `${shape === 1 ? randomBetween(7, 9) : randomBetween(5, 10)}px`;
52+
piece.style.height = `${shape === 2 ? randomBetween(6, 8) : randomBetween(10, 16)}px`;
53+
piece.style.borderRadius = shape === 2 ? "999px" : `${randomBetween(1, 3)}px`;
54+
piece.style.background = COLORS[index % COLORS.length] ?? "#22c55e";
55+
piece.style.opacity = "1";
56+
piece.style.transform = `translate(0px, 0px) rotate(${randomBetween(0, 360)}deg)`;
57+
piece.style.willChange = "transform, opacity";
58+
container.appendChild(piece);
59+
pieces.push({
60+
element: piece,
61+
angle: ((index / total) * Math.PI * 2) + randomBetween(-0.2, 0.2),
62+
distance: randomBetween(80, 150),
63+
rise: randomBetween(18, 42),
64+
drift: randomBetween(110, 210),
65+
rotationStart: randomBetween(0, 360),
66+
rotationVelocity: randomBetween(260, 760),
67+
});
68+
}
69+
70+
const startAt = performance.now();
71+
const duration = 1100;
72+
73+
const animate = (now: number) => {
74+
const elapsed = now - startAt;
75+
const progress = Math.min(elapsed / duration, 1);
76+
const burstProgress = Math.min(progress / 0.45, 1);
77+
const fallProgress = Math.max((progress - 0.2) / 0.8, 0);
78+
79+
pieces.forEach((piece) => {
80+
const spreadDistance = piece.distance * (1 - Math.pow(1 - burstProgress, 2));
81+
const horizontal = Math.cos(piece.angle) * spreadDistance;
82+
const upward = Math.sin(piece.angle) * spreadDistance - piece.rise;
83+
const gravityDrop = Math.pow(fallProgress, 2) * piece.drift;
84+
const vertical = upward + gravityDrop;
85+
const rotation = piece.rotationStart + progress * piece.rotationVelocity;
86+
piece.element.style.transform =
87+
`translate(${horizontal}px, ${vertical}px) rotate(${rotation}deg)`;
88+
piece.element.style.opacity = String(1 - progress);
89+
});
90+
91+
if (progress < 1) {
92+
window.requestAnimationFrame(animate);
93+
return;
94+
}
95+
96+
pieces.forEach((piece) => piece.element.remove());
97+
if (!container.childElementCount) {
98+
container.remove();
99+
}
100+
};
101+
102+
window.requestAnimationFrame(animate);
103+
}

src/adapters/leetcode/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { RuntimeMessageResponse } from "../../core/types/messages";
99
import type { ExtensionSettings } from "../../core/types/domain";
1010
import type { UploadJob } from "../../core/types/upload";
1111
import type { PlatformAdapter } from "../types";
12+
import { burstConfetti } from "../confetti";
1213
import { uploadThroughBackground } from "../upload";
1314

1415
const SUBMIT_BUTTON_SELECTOR = '[data-e2e-locator="console-submit-button"]';
@@ -243,6 +244,11 @@ function setInlineStatus(
243244
}
244245

245246
if (tone === "success") {
247+
const rect = marker.getBoundingClientRect();
248+
burstConfetti({
249+
x: rect.left + rect.width / 2,
250+
y: Math.max(48, rect.top - 28),
251+
});
246252
marker.style.background = "#052e16";
247253
marker.style.color = "#bbf7d0";
248254
return;

src/adapters/programmers/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { ExtensionSettings } from "../../core/types/domain";
99
import type { RuntimeMessageResponse } from "../../core/types/messages";
1010
import type { UploadJob } from "../../core/types/upload";
1111
import type { PlatformAdapter } from "../types";
12+
import { burstConfetti } from "../confetti";
1213
import { uploadThroughBackground } from "../upload";
1314

1415
const STATUS_MARKER_ID = "algorithmhub-programmers-status-marker";
@@ -241,6 +242,11 @@ function setInlineStatus(
241242
}
242243

243244
if (tone === "success") {
245+
const rect = marker.getBoundingClientRect();
246+
burstConfetti({
247+
x: rect.left + rect.width / 2,
248+
y: Math.max(48, rect.top - 28),
249+
});
244250
marker.style.background = "#052e16";
245251
marker.style.color = "#bbf7d0";
246252
return;

0 commit comments

Comments
 (0)