1// Vibe coded with AI
2(globalThis as any).canvas_2017 = function(canvas: HTMLCanvasElement) {
3 const isStandalone = canvas.getAttribute("data-standalone") === "true";
4 // Configuration interface for the checkerboard effect
5 interface CheckerboardConfig {
6 fps: number; // frames per second
7 color1: string; // first checkerboard color
8 color2: string; // second checkerboard color
9 opacity: number; // opacity of each checkerboard (0-1)
10 speedX1: number; // horizontal speed of first checkerboard (pixels per second)
11 speedY1: number; // vertical speed of first checkerboard (pixels per second)
12 speedX2: number; // horizontal speed of second checkerboard (pixels per second)
13 speedY2: number; // vertical speed of second checkerboard (pixels per second)
14 baseTileSize: number; // base size of checkerboard tiles
15 sizeVariation: number; // maximum variation in tile size (pixels)
16 sineFrequency1: number; // frequency of first sine wave for size variation
17 sineFrequency2: number; // frequency of second sine wave for size variation
18 sineOffset: number; // offset between the two sine waves (radians)
19 rotation: number; // rotation in degrees for the entire pattern
20 rotation2: number; // rotation in degrees for the entire pattern
21 }
22
23 // Default configuration
24 const config: CheckerboardConfig = {
25 fps: 30,
26 color1: "#1A1C17",
27 color2: "#1A1C17",
28 opacity: 0.3,
29 speedX1: -0.02, // moving left slowly
30 speedY1: -0.01, // moving up slowly
31 speedX2: -0.015, // moving left (slightly slower)
32 speedY2: 0.012, // moving down slowly
33 baseTileSize: 200,
34 sizeVariation: 1.5,
35 sineFrequency1: 0.0005,
36 sineFrequency2: 0.0008,
37 sineOffset: Math.PI / 2, // 90 degrees offset
38 rotation: 2, // 5 degree rotation
39 rotation2: -2, // 5 degree rotation
40 };
41
42 // Get the canvas context
43 const ctx = canvas.getContext("2d");
44 if (!ctx) {
45 console.error("Could not get canvas context");
46 return () => {};
47 }
48
49 // Make canvas transparent
50 if (isStandalone) {
51 canvas.style.backgroundColor = "#737D60";
52 } else {
53 canvas.style.backgroundColor = "transparent";
54 }
55
56 // Variables to track position and animation
57 let width = canvas.width;
58 let height = canvas.height;
59 let animationFrameId: number;
60 let lastFrameTime = 0;
61 const frameInterval = 1000 / config.fps;
62
63 // Position offsets for the two checkerboards (centered)
64 let offset1X = 0;
65 let offset1Y = 0;
66 let offset2X = 0;
67 let offset2Y = 0;
68
69 // Time variable for sine wave calculation
70 let time = 0;
71
72 // Convert rotation to radians
73 const rotationRad = (config.rotation * Math.PI) / 180;
74 const rotationRad2 = (config.rotation2 * Math.PI) / 180;
75
76 // Update canvas dimensions when resized
77 const updateDimensions = () => {
78 width = canvas.width = canvas.clientWidth;
79 height = canvas.height = canvas.clientHeight;
80 };
81
82 // Calculate the diagonal length of the canvas (to ensure rotation covers corners)
83 const calculateDiagonal = () => {
84 return Math.sqrt(width * width + height * height);
85 };
86
87 // Draw a single checkerboard pattern scaled from center with rotation
88 const drawCheckerboard = (
89 offsetX: number,
90 offsetY: number,
91 tileSize: number,
92 color1: string,
93 color2: string,
94 opacity: number,
95 rotationRad: number,
96 ) => {
97 ctx.globalAlpha = opacity;
98
99 // Get the center of the viewport
100 const centerX = width / 2;
101 const centerY = height / 2;
102
103 // Save the current transformation state
104 ctx.save();
105
106 // Move to the center of the canvas, rotate, then move back
107 ctx.translate(centerX, centerY);
108 ctx.rotate(rotationRad);
109
110 // Calculate the number of tiles needed to cover the rotated canvas
111 // We need to use the diagonal length to ensure we cover the corners when rotated
112 const diagonal = calculateDiagonal();
113 const tilesX = Math.ceil(diagonal / tileSize) + 6; // Added extra tiles for rotation
114 const tilesY = Math.ceil(diagonal / tileSize) + 6;
115
116 // Calculate how many tiles fit from center to edge (in each direction)
117 const halfTilesX = Math.ceil(tilesX / 2);
118 const halfTilesY = Math.ceil(tilesY / 2);
119
120 // Adjust the offset to be relative to the center
121 // The modulo ensures the pattern repeats smoothly even with scaling
122 const adjustedOffsetX = offsetX % (tileSize * 2);
123 const adjustedOffsetY = offsetY % (tileSize * 2);
124
125 // Draw the checker pattern, centered on the viewport
126 for (let y = -halfTilesY; y <= halfTilesY; y++) {
127 for (let x = -halfTilesX; x <= halfTilesX; x++) {
128 // Determine if this tile should be colored (creating checker pattern)
129 // We add a large number to ensure (x+y) is always positive for the modulo
130 if ((x + y + 1000) % 2 === 0) {
131 ctx.fillStyle = color1;
132 } else {
133 ctx.fillStyle = color2;
134 }
135
136 // Calculate the position of this tile relative to the center
137 // The adjusted offset creates the movement effect
138 const posX = (x * tileSize) + adjustedOffsetX;
139 const posY = (y * tileSize) + adjustedOffsetY;
140
141 // Draw the tile
142 ctx.fillRect(
143 posX - tileSize / 2,
144 posY - tileSize / 2,
145 tileSize,
146 tileSize,
147 );
148 }
149 }
150
151 // Restore the transformation state
152 ctx.restore();
153 };
154
155 // Animation loop
156 const animate = (currentTime: number) => {
157 animationFrameId = requestAnimationFrame(animate);
158
159 // Control frame rate
160 if (currentTime - lastFrameTime < frameInterval) {
161 return;
162 }
163
164 // Calculate the time elapsed since the last frame
165 const dt = currentTime - lastFrameTime;
166 lastFrameTime = currentTime;
167
168 // Increment time for sine wave calculation
169 time += dt;
170
171 // Update the position offsets based on speed and elapsed time
172 offset1X += config.speedX1 * dt;
173 offset1Y += config.speedY1 * dt;
174 offset2X += config.speedX2 * dt;
175 offset2Y += config.speedY2 * dt;
176
177 // Calculate the tile sizes using sine waves
178 const tileSize1 = config.baseTileSize
179 + Math.sin(time * config.sineFrequency1) * config.sizeVariation;
180 const tileSize2 = config.baseTileSize
181 + Math.sin(time * config.sineFrequency2 + config.sineOffset)
182 * config.sizeVariation;
183
184 // Clear canvas
185 ctx.clearRect(0, 0, width, height);
186
187 // Draw the two checkerboards
188 drawCheckerboard(
189 offset1X,
190 offset1Y,
191 tileSize1,
192 config.color1,
193 "transparent",
194 config.opacity,
195 rotationRad,
196 );
197
198 drawCheckerboard(
199 offset2X,
200 offset2Y,
201 tileSize2,
202 config.color2,
203 "transparent",
204 config.opacity,
205 rotationRad2,
206 );
207
208 // Reset global alpha
209 ctx.globalAlpha = 1.0;
210 };
211
212 // Initialize the animation
213 const init = () => {
214 // Set up resize handler
215 window.addEventListener("resize", updateDimensions);
216
217 // Initial setup
218 updateDimensions();
219
220 // Start animation
221 lastFrameTime = performance.now();
222 animationFrameId = requestAnimationFrame(animate);
223 };
224
225 // Start the animation
226 init();
227
228 // Return cleanup function
229 return () => {
230 window.removeEventListener("resize", updateDimensions);
231 cancelAnimationFrame(animationFrameId);
232 };
233};