Skip to content

Commit 77c13e1

Browse files
committed
feat: add bloglist component
1 parent 27bc35f commit 77c13e1

10 files changed

Lines changed: 1163 additions & 0 deletions

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
"./blog-avatar": {
5454
"types": "./dist/blog-avatar/index.d.ts",
5555
"import": "./dist/blog-avatar/index.js"
56+
},
57+
"./blog-list": {
58+
"types": "./dist/blog-list/index.d.ts",
59+
"import": "./dist/blog-list/index.js"
5660
}
5761
},
5862
"repository": {

rslib.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default defineConfig({
2626
'nav-icon': './src/nav-icon/index.tsx',
2727
benchmark: './src/benchmark/index.tsx',
2828
'blog-avatar': './src/blog-avatar/index.tsx',
29+
'blog-list': './src/blog-list/index.tsx',
2930
'tool-stack': './src/tool-stack/index.tsx',
3031
hero: './src/hero/index.tsx',
3132
'section-style': './src/section-style/index.tsx',
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.frame {
2+
position: absolute;
3+
inset: 0;
4+
pointer-events: none;
5+
}
6+
7+
.canvas {
8+
width: 100%;
9+
height: 100%;
10+
pointer-events: none;
11+
}

src/blog-list/BorderBeam.tsx

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { useEffect, useRef } from 'react';
2+
import styles from './BorderBeam.module.scss';
3+
4+
export type BorderBeamProps = {
5+
color?: string;
6+
size?: number;
7+
duration?: number;
8+
className?: string;
9+
};
10+
11+
export function BorderBeam({
12+
color = '#12e5e5',
13+
size = 3,
14+
duration = 4,
15+
className,
16+
}: BorderBeamProps) {
17+
const canvasRef = useRef<HTMLCanvasElement>(null);
18+
const containerRef = useRef<HTMLDivElement>(null);
19+
20+
useEffect(() => {
21+
const canvas = canvasRef.current;
22+
const container = containerRef.current;
23+
24+
if (!canvas || !container) {
25+
return;
26+
}
27+
28+
const context = canvas.getContext('2d', { alpha: true });
29+
30+
if (!context) {
31+
return;
32+
}
33+
34+
const updateCanvasSize = () => {
35+
const nextCanvas = canvasRef.current;
36+
const nextContainer = containerRef.current;
37+
38+
if (!nextCanvas || !nextContainer) {
39+
return;
40+
}
41+
42+
const { width, height } = nextContainer.getBoundingClientRect();
43+
nextCanvas.width = width;
44+
nextCanvas.height = height;
45+
};
46+
47+
const resizeObserver = new ResizeObserver(updateCanvasSize);
48+
resizeObserver.observe(container);
49+
updateCanvasSize();
50+
51+
let animationFrameId = 0;
52+
const startTime = performance.now();
53+
54+
const getCoordinatesFromDistance = (distance: number) => {
55+
const width = canvas.width;
56+
const height = canvas.height;
57+
const perimeter = 2 * (width + height);
58+
const normalizedDistance = distance % perimeter;
59+
60+
if (normalizedDistance < width) {
61+
return { x: normalizedDistance, y: 0 };
62+
}
63+
64+
if (normalizedDistance < width + height) {
65+
return { x: width, y: normalizedDistance - width };
66+
}
67+
68+
if (normalizedDistance < 2 * width + height) {
69+
return {
70+
x: width - (normalizedDistance - (width + height)),
71+
y: height,
72+
};
73+
}
74+
75+
return {
76+
x: 0,
77+
y: height - (normalizedDistance - (2 * width + height)),
78+
};
79+
};
80+
81+
const createBeamGradient = (start: number, end: number) => {
82+
const startCoord = getCoordinatesFromDistance(start);
83+
const endCoord = getCoordinatesFromDistance(end);
84+
const gradient = context.createLinearGradient(
85+
startCoord.x,
86+
startCoord.y,
87+
endCoord.x,
88+
endCoord.y,
89+
);
90+
91+
gradient.addColorStop(0, 'transparent');
92+
gradient.addColorStop(0.2, 'rgba(255, 53, 26, 0.3)');
93+
gradient.addColorStop(0.5, '#ff351a');
94+
gradient.addColorStop(0.8, '#ff351a');
95+
gradient.addColorStop(0.9, color);
96+
gradient.addColorStop(1, 'transparent');
97+
98+
return gradient;
99+
};
100+
101+
const drawPathSegment = (start: number, end: number) => {
102+
let current = start;
103+
const currentPoint = getCoordinatesFromDistance(current);
104+
context.moveTo(currentPoint.x, currentPoint.y);
105+
106+
const step = 1;
107+
while (current < end) {
108+
current += step;
109+
const point = getCoordinatesFromDistance(current);
110+
context.lineTo(point.x, point.y);
111+
}
112+
};
113+
114+
const drawPath = (start: number, end: number) => {
115+
const perimeter = 2 * (canvas.width + canvas.height);
116+
117+
if (end < start) {
118+
drawPathSegment(start, perimeter);
119+
drawPathSegment(0, end);
120+
return;
121+
}
122+
123+
drawPathSegment(start, end);
124+
};
125+
126+
const drawBeam = (progress: number) => {
127+
const width = canvas.width;
128+
const height = canvas.height;
129+
const perimeter = 2 * (width + height);
130+
const beamLength = perimeter * 0.05;
131+
const positionStart = progress * perimeter;
132+
const positionEnd = (positionStart + beamLength) % perimeter;
133+
134+
context.strokeStyle = createBeamGradient(positionStart, positionEnd);
135+
context.lineWidth = size;
136+
context.beginPath();
137+
drawPath(positionStart, positionEnd);
138+
context.stroke();
139+
};
140+
141+
const animate = (currentTime: number) => {
142+
const elapsed = (currentTime - startTime) / 1000;
143+
const progress = (elapsed % duration) / duration;
144+
145+
context.clearRect(0, 0, canvas.width, canvas.height);
146+
drawBeam(progress);
147+
animationFrameId = window.requestAnimationFrame(animate);
148+
};
149+
150+
animationFrameId = window.requestAnimationFrame(animate);
151+
152+
return () => {
153+
window.cancelAnimationFrame(animationFrameId);
154+
resizeObserver.disconnect();
155+
};
156+
}, [color, duration, size]);
157+
158+
return (
159+
<div
160+
ref={containerRef}
161+
className={className ? `${styles.frame} ${className}` : styles.frame}
162+
>
163+
<canvas ref={canvasRef} className={styles.canvas} />
164+
</div>
165+
);
166+
}

0 commit comments

Comments
 (0)