Skip to content

Commit f8c72ae

Browse files
committed
chore: update
1 parent efc0d29 commit f8c72ae

9 files changed

Lines changed: 325 additions & 139 deletions

File tree

src/blog-back-button/index.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import styles from './index.module.scss';
33

44
export type BlogBackButtonProps = {
55
pathname: string;
6-
lang?: string;
6+
lang: string;
7+
blogPrefix?: string;
78
className?: string;
89
LinkComp?: LinkComp;
910
};
@@ -12,26 +13,35 @@ const getClassName = (...classNames: Array<string | undefined>) => {
1213
return classNames.filter(Boolean).join(' ');
1314
};
1415

15-
const getBlogPrefix = (lang?: string) => {
16-
return !lang || lang === 'en' ? '/blog' : `/${lang}/blog`;
16+
const getBlogPrefix = (lang: string, blogPrefix = '/blog') => {
17+
return !lang || lang === 'en' ? `${blogPrefix}` : `/${lang}${blogPrefix}`;
1718
};
1819

1920
const getLabel = (lang?: string) => {
2021
return lang === 'zh' ? '返回博客' : 'Back to blog';
2122
};
2223

24+
const trimTrailingSlashes = (value: string) => {
25+
let end = value.length;
26+
27+
while (end > 1 && value.charCodeAt(end - 1) === 47) {
28+
end -= 1;
29+
}
30+
31+
return value.slice(0, end) || '/';
32+
};
33+
2334
export function BlogBackButton({
2435
pathname,
2536
lang,
37+
blogPrefix,
2638
className,
2739
LinkComp,
2840
}: BlogBackButtonProps) {
29-
const blogPrefix = getBlogPrefix(lang);
30-
const normalizedPathname = pathname.replace(/\/+$/, '') || '/';
31-
const blogIndexPath = `${blogPrefix}/`;
32-
const isBlogSubpage =
33-
normalizedPathname.startsWith(blogPrefix) &&
34-
normalizedPathname !== blogPrefix;
41+
const resolvedBlogPrefix = getBlogPrefix(lang, blogPrefix);
42+
const normalizedPathname = trimTrailingSlashes(pathname);
43+
const blogIndexPath = `${resolvedBlogPrefix}/`;
44+
const isBlogSubpage = normalizedPathname.startsWith(`${resolvedBlogPrefix}/`);
3545

3646
if (!isBlogSubpage) {
3747
return null;

src/blog-list/BorderBeam.tsx

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { useEffect, useRef } from 'react';
22
import styles from './BorderBeam.module.scss';
33

4+
type ResizeSubscription = {
5+
observe: (target: HTMLDivElement) => void;
6+
disconnect: () => void;
7+
};
8+
49
export type BorderBeamProps = {
510
color?: string;
611
size?: number;
@@ -31,6 +36,9 @@ export function BorderBeam({
3136
return;
3237
}
3338

39+
let canvasWidth = 0;
40+
let canvasHeight = 0;
41+
3442
const updateCanvasSize = () => {
3543
const nextCanvas = canvasRef.current;
3644
const nextContainer = containerRef.current;
@@ -40,20 +48,53 @@ export function BorderBeam({
4048
}
4149

4250
const { width, height } = nextContainer.getBoundingClientRect();
43-
nextCanvas.width = width;
44-
nextCanvas.height = height;
51+
52+
if (width === 0 || height === 0) {
53+
canvasWidth = 0;
54+
canvasHeight = 0;
55+
nextCanvas.width = 0;
56+
nextCanvas.height = 0;
57+
return;
58+
}
59+
60+
canvasWidth = Math.round(width);
61+
canvasHeight = Math.round(height);
62+
63+
const devicePixelRatio = window.devicePixelRatio || 1;
64+
65+
nextCanvas.width = Math.max(
66+
1,
67+
Math.round(canvasWidth * devicePixelRatio),
68+
);
69+
nextCanvas.height = Math.max(
70+
1,
71+
Math.round(canvasHeight * devicePixelRatio),
72+
);
73+
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
74+
context.lineCap = 'round';
4575
};
4676

47-
const resizeObserver = new ResizeObserver(updateCanvasSize);
48-
resizeObserver.observe(container);
77+
const resizeSubscription: ResizeSubscription =
78+
typeof ResizeObserver !== 'undefined'
79+
? new ResizeObserver(updateCanvasSize)
80+
: {
81+
observe: (_target: HTMLDivElement) => {
82+
window.addEventListener('resize', updateCanvasSize);
83+
},
84+
disconnect: () => {
85+
window.removeEventListener('resize', updateCanvasSize);
86+
},
87+
};
88+
89+
resizeSubscription.observe(container);
4990
updateCanvasSize();
5091

5192
let animationFrameId = 0;
5293
const startTime = performance.now();
5394

5495
const getCoordinatesFromDistance = (distance: number) => {
55-
const width = canvas.width;
56-
const height = canvas.height;
96+
const width = canvasWidth;
97+
const height = canvasHeight;
5798
const perimeter = 2 * (width + height);
5899
const normalizedDistance = distance % perimeter;
59100

@@ -112,7 +153,7 @@ export function BorderBeam({
112153
};
113154

114155
const drawPath = (start: number, end: number) => {
115-
const perimeter = 2 * (canvas.width + canvas.height);
156+
const perimeter = 2 * (canvasWidth + canvasHeight);
116157

117158
if (end < start) {
118159
drawPathSegment(start, perimeter);
@@ -124,8 +165,13 @@ export function BorderBeam({
124165
};
125166

126167
const drawBeam = (progress: number) => {
127-
const width = canvas.width;
128-
const height = canvas.height;
168+
const width = canvasWidth;
169+
const height = canvasHeight;
170+
171+
if (width === 0 || height === 0) {
172+
return;
173+
}
174+
129175
const perimeter = 2 * (width + height);
130176
const beamLength = perimeter * 0.05;
131177
const positionStart = progress * perimeter;
@@ -142,7 +188,7 @@ export function BorderBeam({
142188
const elapsed = (currentTime - startTime) / 1000;
143189
const progress = (elapsed % duration) / duration;
144190

145-
context.clearRect(0, 0, canvas.width, canvas.height);
191+
context.clearRect(0, 0, canvasWidth, canvasHeight);
146192
drawBeam(progress);
147193
animationFrameId = window.requestAnimationFrame(animate);
148194
};
@@ -151,7 +197,7 @@ export function BorderBeam({
151197

152198
return () => {
153199
window.cancelAnimationFrame(animationFrameId);
154-
resizeObserver.disconnect();
200+
resizeSubscription.disconnect();
155201
};
156202
}, [color, duration, size]);
157203

src/blog-list/MeteorsBackground.tsx

Lines changed: 110 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import { useEffect, useRef } from 'react';
22

3+
type CanvasBounds = {
4+
width: number;
5+
height: number;
6+
};
7+
8+
type ResizeSubscription = {
9+
observe: (target: HTMLElement) => void;
10+
disconnect: () => void;
11+
};
12+
313
export type MeteorsBackgroundProps = {
414
gridSize?: number;
515
meteorCount?: number;
@@ -22,7 +32,7 @@ class Meteor {
2232

2333
constructor(
2434
private readonly gridSize: number,
25-
private readonly canvas: HTMLCanvasElement,
35+
private readonly bounds: CanvasBounds,
2636
) {
2737
this.reset();
2838
}
@@ -41,26 +51,26 @@ class Meteor {
4151
switch (this.direction) {
4252
case Direction.UP:
4353
this.x =
44-
Math.floor(getMiddlePosition(this.canvas.width) / this.gridSize) *
54+
Math.floor(getMiddlePosition(this.bounds.width) / this.gridSize) *
4555
this.gridSize;
46-
this.y = this.canvas.height;
56+
this.y = this.bounds.height;
4757
break;
4858
case Direction.RIGHT:
4959
this.x = 0;
5060
this.y =
51-
Math.floor(getMiddlePosition(this.canvas.height) / this.gridSize) *
61+
Math.floor(getMiddlePosition(this.bounds.height) / this.gridSize) *
5262
this.gridSize;
5363
break;
5464
case Direction.DOWN:
5565
this.x =
56-
Math.floor(getMiddlePosition(this.canvas.width) / this.gridSize) *
66+
Math.floor(getMiddlePosition(this.bounds.width) / this.gridSize) *
5767
this.gridSize;
5868
this.y = 0;
5969
break;
6070
case Direction.LEFT:
61-
this.x = this.canvas.width;
71+
this.x = this.bounds.width;
6272
this.y =
63-
Math.floor(getMiddlePosition(this.canvas.height) / this.gridSize) *
73+
Math.floor(getMiddlePosition(this.bounds.height) / this.gridSize) *
6474
this.gridSize;
6575
break;
6676
}
@@ -76,13 +86,13 @@ class Meteor {
7686
break;
7787
case Direction.RIGHT:
7888
this.x += this.speed;
79-
if (this.x > this.canvas.width) {
89+
if (this.x > this.bounds.width) {
8090
this.reset();
8191
}
8292
break;
8393
case Direction.DOWN:
8494
this.y += this.speed;
85-
if (this.y > this.canvas.height) {
95+
if (this.y > this.bounds.height) {
8696
this.reset();
8797
}
8898
break;
@@ -154,38 +164,106 @@ export function MeteorsBackground({
154164
return;
155165
}
156166

157-
const setCanvasSize = () => {
158-
canvas.width = window.innerWidth;
159-
canvas.height = window.innerHeight * 0.65;
160-
};
167+
const gridCanvas = document.createElement('canvas');
168+
const gridContext = gridCanvas.getContext('2d');
161169

162-
setCanvasSize();
163-
window.addEventListener('resize', setCanvasSize);
170+
if (!gridContext) {
171+
return;
172+
}
173+
174+
const bounds: CanvasBounds = {
175+
width: 0,
176+
height: 0,
177+
};
164178

165179
const meteors = Array.from(
166180
{ length: meteorCount },
167-
() => new Meteor(gridSize, canvas),
181+
() => new Meteor(gridSize, bounds),
168182
);
169183

184+
const drawGrid = (devicePixelRatio: number) => {
185+
if (bounds.width === 0 || bounds.height === 0) {
186+
gridCanvas.width = 0;
187+
gridCanvas.height = 0;
188+
return;
189+
}
190+
191+
gridCanvas.width = Math.max(
192+
1,
193+
Math.round(bounds.width * devicePixelRatio),
194+
);
195+
gridCanvas.height = Math.max(
196+
1,
197+
Math.round(bounds.height * devicePixelRatio),
198+
);
199+
gridContext.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
200+
gridContext.clearRect(0, 0, bounds.width, bounds.height);
201+
gridContext.strokeStyle = 'rgba(128, 128, 128, 0.1)';
202+
gridContext.lineWidth = 1;
203+
204+
for (let x = 0; x < bounds.width; x += gridSize) {
205+
gridContext.beginPath();
206+
gridContext.moveTo(x, 0);
207+
gridContext.lineTo(x, bounds.height);
208+
gridContext.stroke();
209+
}
210+
211+
for (let y = 0; y < bounds.height; y += gridSize) {
212+
gridContext.beginPath();
213+
gridContext.moveTo(0, y);
214+
gridContext.lineTo(bounds.width, y);
215+
gridContext.stroke();
216+
}
217+
};
218+
219+
const setCanvasSize = () => {
220+
const { width, height } = canvas.getBoundingClientRect();
221+
222+
if (width === 0 || height === 0) {
223+
bounds.width = 0;
224+
bounds.height = 0;
225+
canvas.width = 0;
226+
canvas.height = 0;
227+
return;
228+
}
229+
230+
bounds.width = Math.round(width);
231+
bounds.height = Math.round(height);
232+
233+
const devicePixelRatio = window.devicePixelRatio || 1;
234+
235+
canvas.width = Math.max(1, Math.round(bounds.width * devicePixelRatio));
236+
canvas.height = Math.max(1, Math.round(bounds.height * devicePixelRatio));
237+
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
238+
drawGrid(devicePixelRatio);
239+
240+
for (const meteor of meteors) {
241+
meteor.reset();
242+
}
243+
};
244+
245+
const resizeSubscription: ResizeSubscription =
246+
typeof ResizeObserver !== 'undefined'
247+
? new ResizeObserver(setCanvasSize)
248+
: {
249+
observe: (_target: HTMLElement) => {
250+
window.addEventListener('resize', setCanvasSize);
251+
},
252+
disconnect: () => {
253+
window.removeEventListener('resize', setCanvasSize);
254+
},
255+
};
256+
257+
resizeSubscription.observe(canvas);
258+
setCanvasSize();
259+
170260
let animationFrameId = 0;
171261

172262
const animate = () => {
173-
context.clearRect(0, 0, canvas.width, canvas.height);
174-
context.strokeStyle = 'rgba(128, 128, 128, 0.1)';
175-
context.lineWidth = 1;
176-
177-
for (let x = 0; x < canvas.width; x += gridSize) {
178-
context.beginPath();
179-
context.moveTo(x, 0);
180-
context.lineTo(x, canvas.height);
181-
context.stroke();
182-
}
263+
context.clearRect(0, 0, bounds.width, bounds.height);
183264

184-
for (let y = 0; y < canvas.height; y += gridSize) {
185-
context.beginPath();
186-
context.moveTo(0, y);
187-
context.lineTo(canvas.width, y);
188-
context.stroke();
265+
if (gridCanvas.width > 0 && gridCanvas.height > 0) {
266+
context.drawImage(gridCanvas, 0, 0, bounds.width, bounds.height);
189267
}
190268

191269
for (const meteor of meteors) {
@@ -200,7 +278,7 @@ export function MeteorsBackground({
200278

201279
return () => {
202280
window.cancelAnimationFrame(animationFrameId);
203-
window.removeEventListener('resize', setCanvasSize);
281+
resizeSubscription.disconnect();
204282
};
205283
}, [gridSize, meteorCount]);
206284

0 commit comments

Comments
 (0)