an app to share curated trails
sidetrail.app
1"use client";
2
3import Link from "next/link";
4import "./FloatingAvatar.css";
5
6interface FloatingAvatarProps {
7 src?: string;
8 handle: string;
9 title: string;
10 contained?: boolean;
11 opaque?: boolean;
12 noLink?: boolean;
13}
14
15export function FloatingAvatar({
16 src,
17 handle,
18 title,
19 contained = false,
20 opaque = false,
21 noLink = false,
22}: FloatingAvatarProps) {
23 if (!src) return null;
24
25 // Simple string hash function (deterministic across server/client)
26 const hashString = (str: string) => {
27 let hash = 0;
28 for (let i = 0; i < str.length; i++) {
29 hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
30 }
31 return Math.abs(hash);
32 };
33
34 // Use handle for seeding - same handle always gets same animation
35 const baseHash = hashString(handle);
36
37 // Generate stable random values based on hash
38 const random = (offset: number) => {
39 // Simple LCG (Linear Congruential Generator) for deterministic randomness
40 const a = 1664525;
41 const c = 1013904223;
42 const m = 2 ** 32;
43 const value = ((baseHash + offset) * a + c) % m;
44 return value / m;
45 };
46
47 // More varied random animation duration (3s to 9s for bigger range)
48 const duration = 3 + random(0) * 6;
49 // Random animation delay (0s to 4s for more spread)
50 const delay = random(1) * 4;
51 // Random timing function index
52 const timingFunctions = ["ease-in-out", "ease-in", "ease-out", "linear"];
53 const timingFunction = timingFunctions[Math.floor(random(2) * timingFunctions.length)];
54
55 const imgElement = (
56 <img
57 src={src}
58 alt={handle}
59 title={title}
60 className={`FloatingAvatar ${contained ? "FloatingAvatar-contained" : ""} ${opaque ? "FloatingAvatar-opaque" : ""} ${noLink ? "" : "FloatingAvatar-clickable"}`}
61 style={{
62 animationDuration: `${duration}s`,
63 animationDelay: `${delay}s`,
64 animationTimingFunction: timingFunction,
65 }}
66 />
67 );
68
69 if (noLink) {
70 return imgElement;
71 }
72
73 return (
74 <Link
75 href={`/@${handle}/walking`}
76 prefetch={false}
77 onClick={(e) => e.stopPropagation()}
78 target="_blank"
79 rel="noopener noreferrer"
80 className="FloatingAvatar-link"
81 tabIndex={-1}
82 >
83 {imgElement}
84 </Link>
85 );
86}