This repository has no description
1// src/components/MatterLoadingAnimation.jsx
2import React, { useEffect, useRef, useState, useMemo } from "react";
3import Matter from "matter-js";
4import "./MatterLoadingAnimation.css";
5
6const MatterLoadingAnimation = () => {
7 const sceneRef = useRef(null);
8 const timeoutRef = useRef(null); // For circle appearance
9 const gravityTimerRef = useRef(null); // For dynamic gravity
10 const messageTimeoutRef = useRef(null); // For updating the message
11
12// Wrap messages in useMemo so they don't change on every render.
13const messages = useMemo(
14 () => [
15 "Counting total posts",
16 "Calculating posts per day",
17 "Measuring replies per day",
18 "Estimating reply percentages",
19 "Tracking repost activity",
20 "Differentiating self-reposts from reposts of others",
21 "Counting posts with images",
22 "Measuring image posts per day",
23 "Calculating posts with alt text",
24 "Determining posts without alt text",
25 "Highlighting text-only posts",
26 "Comparing text posts to media posts",
27 "Measuring posts containing mentions",
28 "Tracking posts with links",
29 "Assessing posts with video content",
30 "Calculating engagement metrics",
31 "Determining posting style",
32 "Identifying social interactions",
33 "Analyzing social status trends",
34 "Calculating total collections",
35 "Differentiating Bluesky and Atproto collections",
36 "Comparing records per day",
37 "Tracking Bluesky record percentages",
38 "Calculating account age in days",
39 "Measuring posts per account age",
40 "Estimating blobs per post",
41 "Evaluating profile completion",
42 "Analyzing follower-to-following ratios",
43 "Compiling narrative insights",
44 "Detecting log operations",
45 "Analyzing handle history",
46 "Tracking recent profile edits",
47 "Identifying active aliases",
48 "Calculating domain uniqueness",
49 "Generating Bluesky scores",
50 "Calculating Atproto scores",
51 "Evaluating combined scores",
52 ],
53 []
54 );
55
56
57 // State to hold the current message.
58 const [message, setMessage] = useState("Loading account data");
59 // State to control the fade effect.
60 const [fade, setFade] = useState(false);
61 const [circleCount, setCircleCount] = useState(0); // Track circle count
62 const [specialCircleCount, setSpecialCircleCount] = useState(0); // Special circle count
63
64
65 // Update the message on a random interval between 4 and 10 seconds.
66 useEffect(() => {
67 const updateMessage = () => {
68 // Random delay between 4000ms and 10000ms.
69 const delay = Math.random() * (10000 - 1000) + 4000;
70 messageTimeoutRef.current = setTimeout(() => {
71 // Trigger fade-out.
72 setFade(true);
73 // After fade-out duration (300ms), update the message and fade back in.
74 setTimeout(() => {
75 const randomMessage = messages[Math.floor(Math.random() * messages.length)];
76 setMessage(randomMessage);
77 setFade(false);
78 }, 300);
79 updateMessage();
80 }, delay);
81 };
82 updateMessage();
83 return () => {
84 clearTimeout(messageTimeoutRef.current);
85 };
86 }, [messages]);
87
88 useEffect(() => {
89 // Set the fixed dimensions for the canvas.
90 const width = 250;
91 const height = 250;
92
93 // Create engine and set initial gravity.
94 const engine = Matter.Engine.create();
95 engine.world.gravity.scale = 0.001;
96 // Start with base gravity values (we'll update these continuously).
97 engine.world.gravity.x = 0;
98 engine.world.gravity.y = 0;
99
100 // Create the renderer with the fixed dimensions.
101 const render = Matter.Render.create({
102 element: sceneRef.current,
103 engine: engine,
104 options: {
105 width,
106 height,
107 showIds: false,
108 wireframes: false,
109 pixelRatio: 'auto',
110 },
111 });
112 Matter.Render.run(render);
113
114 // Create and run the runner.
115 const runner = Matter.Runner.create();
116 Matter.Runner.run(runner, engine);
117
118 // Walls settings.
119 const wallThickness = 6;
120 const wallRenderOptions = {
121 fillStyle: "#f0f0f000", // Custom fill (transparent in this case)
122 };
123
124 // Create walls with custom styling (top, bottom, left, and right).
125 const walls = [
126 // Top wall.
127 Matter.Bodies.rectangle(
128 width / 2,
129 0,
130 width,
131 wallThickness,
132 { isStatic: true, render: wallRenderOptions }
133 ),
134 // Bottom wall.
135 Matter.Bodies.rectangle(
136 width / 2,
137 height,
138 width,
139 wallThickness,
140 { isStatic: true, render: wallRenderOptions }
141 ),
142 // Left wall.
143 Matter.Bodies.rectangle(
144 0,
145 height / 2,
146 wallThickness,
147 height,
148 { isStatic: true, render: wallRenderOptions }
149 ),
150 // Right wall.
151 Matter.Bodies.rectangle(
152 width,
153 height / 2,
154 wallThickness,
155 height,
156 { isStatic: true, render: wallRenderOptions }
157 ),
158 ];
159 Matter.World.add(engine.world, walls);
160
161 // Enable mouse control.
162 const mouse = Matter.Mouse.create(render.canvas);
163 const mouseConstraint = Matter.MouseConstraint.create(engine, {
164 mouse: mouse,
165 constraint: { stiffness: 0.2, render: { visible: false } },
166 });
167 Matter.World.add(engine.world, mouseConstraint);
168 render.mouse = mouse;
169
170 // Fit the render viewport to the scene.
171 Matter.Render.lookAt(render, {
172 min: { x: 0, y: 0 },
173 max: { x: width, y: height },
174 });
175
176 // SETTINGS FOR OUR BLUE CIRCLES.
177 const minRadius = 4;
178 const maxRadius = 12;
179 const growthDuration = 400; // milliseconds over which the circle grows
180
181 // Get custom circle render styling from CSS variables.
182 const rootStyles = getComputedStyle(document.documentElement);
183 const circleFill = rootStyles.getPropertyValue("--circle-fill").trim() || "#004f84c2";
184 const circleStroke = rootStyles.getPropertyValue("--circle-stroke").trim() || "#3498dbdc";
185 const circleLineWidth = parseFloat(rootStyles.getPropertyValue("--circle-linewidth")) || 6;
186
187 // Bounce easing function.
188 function bounceEaseOut(t) {
189 const n1 = 7.5625;
190 const d1 = 2.75;
191 if (t < 1 / d1) {
192 return n1 * t * t;
193 } else if (t < 2 / d1) {
194 t -= 1.5 / d1;
195 return n1 * t * t + 0.75;
196 } else if (t < 2.5 / d1) {
197 t -= 2.25 / d1;
198 return n1 * t * t + 0.9375;
199 } else {
200 t -= 2.625 / d1;
201 return n1 * t * t + 0.984375;
202 }
203 }
204
205 // Create a blue circle that "pops" up with a bounce effect.
206 // The circle will appear at a random position on the canvas.
207 const createCircle = () => {
208 // Determine the final target radius.
209 const targetRadius = Math.random() * (maxRadius - minRadius) + minRadius;
210 // Use a very small initial radius so we can "grow" it.
211 const initialRadius = 0.1;
212 // Randomly choose its starting position, ensuring it fits within the canvas.
213 const xPos = Math.random() * (width - 2 * targetRadius) + targetRadius;
214 const yPos = Math.random() * (height - 2 * targetRadius) + targetRadius;
215
216 // Determine if the circle will be special.
217 const isSpecial = Math.random() < 0.05; // 1 in 100 chance
218 const specialFill = "#FFD700"; // Gold color for special circles
219 const specialStroke = "#FFA500"; // Orange stroke for special circles
220
221 // Increment special circle count if the circle is special.
222 if (isSpecial) {
223 setSpecialCircleCount((prev) => prev + 1);
224 }
225
226 // Create the circle at the random position, with the tiny initial radius.
227 const circle = Matter.Bodies.circle(
228 xPos,
229 yPos,
230 initialRadius,
231 {
232 render: {
233 fillStyle: isSpecial ? specialFill : circleFill,
234 strokeStyle: isSpecial ? specialStroke : circleStroke,
235 lineWidth: circleLineWidth
236 },
237 restitution: 0.6
238 }
239 );
240 Matter.World.add(engine.world, circle);
241 setCircleCount((prev) => prev + 1); // Increment circle count
242
243 // Animate the growth of the circle.
244 let currentScale = 1; // Body initially at scale factor 1
245 const targetScale = targetRadius / initialRadius; // Scale factor needed to reach targetRadius
246 const startTime = performance.now();
247
248 const grow = (time) => {
249 const elapsed = time - startTime;
250 const progress = Math.min(elapsed / growthDuration, 1);
251 const easing = bounceEaseOut(progress);
252 const desiredScale = 1 + (targetScale - 1) * easing;
253 const scaleFactor = desiredScale / currentScale;
254 Matter.Body.scale(circle, scaleFactor, scaleFactor);
255 currentScale = desiredScale;
256 if (progress < 1) {
257 requestAnimationFrame(grow);
258 }
259 };
260 requestAnimationFrame(grow);
261 };
262
263 // Instead of a fixed interval, schedule the next circle appearance
264 // randomly between 100 and 500 ms.
265 const scheduleNextCircle = () => {
266 const delay = Math.random() * (2500 - 500) + 100; // delay in ms between 100 and 500
267 timeoutRef.current = setTimeout(() => {
268 createCircle();
269 scheduleNextCircle();
270 }, delay);
271 };
272 scheduleNextCircle();
273
274 // Now, update gravity dynamically to simulate water-like motion.
275 // Define base values and amplitudes:
276 const baseGravityX = 0;
277 const amplitudeX = 0.008; // gravity.x will vary by ±0.008 around baseGravityX
278 const baseGravityY = 0.001; // base vertical gravity
279 const amplitudeY = 0.008; // gravity.y will vary by ±0.008 around baseGravityY
280
281 const updateGravity = () => {
282 const t = performance.now() * 0.001; // convert time to seconds
283 engine.world.gravity.x = baseGravityX + amplitudeX * Math.sin(t);
284 engine.world.gravity.y = baseGravityY + amplitudeY * Math.cos(t);
285 };
286
287 // Update gravity approximately every 16 ms (~60fps).
288 gravityTimerRef.current = setInterval(updateGravity, 16);
289
290 // Cleanup on unmount.
291 return () => {
292 clearTimeout(timeoutRef.current);
293 clearInterval(gravityTimerRef.current);
294 Matter.Render.stop(render);
295 Matter.Runner.stop(runner);
296 Matter.World.clear(engine.world);
297 Matter.Engine.clear(engine);
298 render.canvas.remove();
299 render.textures = {};
300 };
301 }, []);
302
303 // Render the fixed-size 250x250 canvas and center it with dynamic loading text.
304 return (
305 <div
306 className={
307 'loading-container-1'
308 }
309 style={{
310 display: "flex",
311 flexDirection: "column",
312 alignItems: "center",
313 marginLeft: "20px",
314 marginRight: "20px",
315 justifyContent: "center",
316 minHeight: "80vh",
317 background: "none"
318 }}
319 >
320 <div
321 ref={sceneRef}
322 style={{
323 boxSizing: "border-box",
324 }}
325 />
326 <div className="loading-text-container">
327 <p className={`loading-text ${fade ? "fade" : ""}`} style={{ marginTop: "20px", fontSize: "1em" }}>
328 {message}<span className="dots"></span>
329 </p>
330 <div className="circle-counters">
331 <div className="counter-item">
332 <div className="legend-circle regular-circle" />
333 <p className="counter-item-regular">{circleCount}</p>
334 </div>
335 <div className="counter-item">
336 <div className="legend-circle special-circle" />
337 <p className="counter-item-special">{specialCircleCount}</p>
338 </div>
339 </div>
340 <div className="version-number">
341 <p>cred.blue v1.2</p>
342 </div>
343 </div>
344 </div>
345 );
346};
347
348export default MatterLoadingAnimation;