orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1// Three star fields at different depths, offset against the pointer for
2// parallax and twinkling on a four-step cycle. Everything is a 5px square.
3
4const COLORS = ["white", "yellow", "red", "green", "blue"];
5
6export const retrostars = (cvs, { numStars = 50 } = {}) => {
7 const ctx = cvs.getContext("2d");
8
9 const resize = () => {
10 cvs.width = cvs.offsetWidth;
11 cvs.height = cvs.offsetHeight;
12 };
13 resize();
14 window.addEventListener("resize", resize);
15
16 const mouse = { x: 640, y: 400 };
17 let starSize = 0;
18
19 window.addEventListener(
20 "mousemove",
21 (e) => {
22 mouse.x = e.clientX;
23 mouse.y = e.clientY;
24 },
25 { passive: true }
26 );
27
28 const offsetStar = (maxOffset) => ({
29 x: -Math.floor((mouse.x / window.innerWidth) * maxOffset),
30 y: -Math.floor((mouse.y / window.innerHeight) * maxOffset),
31 });
32
33 const makeStar = () => ({
34 loc: [cvs.width * Math.random(), cvs.height * Math.random()],
35 color: COLORS[Math.floor(Math.random() * COLORS.length)],
36 });
37
38 const smallStars = [];
39 const mediumStars = [];
40 const largeStars = [];
41 for (let i = 0; i < numStars; i++) {
42 smallStars.push(makeStar());
43 if (mediumStars.length < i / 4) mediumStars.push(makeStar());
44 if (largeStars.length < i / 8) largeStars.push(makeStar());
45 }
46
47 const square = (color, x, y, w = 5, h = 5) => {
48 ctx.beginPath();
49 ctx.fillStyle = color;
50 ctx.fillRect(x, y, w, h);
51 ctx.closePath();
52 };
53
54 // A plus sign of 5px squares, the twinkling state for medium and large stars.
55 const plus = (color, x, y, arm = 5) => {
56 square(color, x, y);
57 square(color, x - arm, y);
58 square(color, x, y - arm);
59 square(color, x + arm, y);
60 square(color, x, y + arm);
61 };
62
63 let frame = null;
64 let running = false;
65 let twinkle = null;
66
67 const draw = () => {
68 ctx.clearRect(0, 0, cvs.width, cvs.height);
69
70 const smallOffset = offsetStar(25);
71 const mediumOffset = offsetStar(75);
72 const largeOffset = offsetStar(125);
73
74 smallStars.forEach(({ loc, color }) => {
75 square(color, loc[0] + smallOffset.x, loc[1] + smallOffset.y);
76 });
77
78 mediumStars.forEach(({ loc, color }) => {
79 const x = loc[0] + mediumOffset.x;
80 const y = loc[1] + mediumOffset.y;
81 if (starSize === 0 || starSize === 2) square(color, x, y);
82 else plus(color, x, y);
83 });
84
85 largeStars.forEach(({ loc, color }) => {
86 const x = loc[0] + largeOffset.x;
87 const y = loc[1] + largeOffset.y;
88 if (starSize === 0) {
89 square(color, x, y);
90 } else if (starSize === 1 || starSize === 3) {
91 plus(color, x, y);
92 } else {
93 // Peak twinkle. The hole punched out of the middle is what makes it
94 // read as a flare instead of just a bigger dot.
95 square(color, x - 5, y - 5, 15, 15);
96 square(color, x - 10, y);
97 square(color, x, y - 10);
98 square(color, x + 10, y);
99 square(color, x, y + 10);
100 square("black", x, y);
101 }
102 });
103
104 if (running) frame = window.requestAnimationFrame(draw);
105 };
106
107 return {
108 start() {
109 if (running) return;
110 running = true;
111 twinkle = window.setInterval(() => {
112 starSize = (starSize + 1) % 4;
113 }, 500);
114 frame = window.requestAnimationFrame(draw);
115 },
116 stop() {
117 running = false;
118 if (frame) window.cancelAnimationFrame(frame);
119 if (twinkle) window.clearInterval(twinkle);
120 frame = null;
121 twinkle = null;
122 },
123 };
124};