justinooo

Claude Animated Logo (Vue 3)

Sep 16th, 2025 (edited)
830
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. <template>
  2.     <div ref="containerRef" class="claude-icon-container">
  3.         <svg
  4.             overflow="visible"
  5.             width="100%"
  6.             height="100%"
  7.             viewBox="0 0 100 101"
  8.             fill="none"
  9.             xmlns="http://www.w3.org/2000/svg"
  10.             role="presentation"
  11.             class="claude-icon"
  12.         >
  13.             <path v-for="(path, index) in paths" :key="index" :d="path" fill="currentColor" />
  14.         </svg>
  15.     </div>
  16. </template>
  17.  
  18. <script setup lang="ts">
  19. import { onMounted, onUnmounted, ref } from "vue";
  20.  
  21. type Point = { x: number; y: number };
  22. type FocalPointRef = { current: Point };
  23.  
  24. type PathSegment = { type: "M" | "L"; x: number; y: number } | { type: "Z" };
  25.  
  26. const paths = [
  27.     "M82.5003 55.5000 L95.0003 56.5000 L98.0003 58.5000 L100.0000 61.5000 L100.0000 63.6587 L94.5003 66.0000 L66.5005 59.0000 L55.0003 58.5000 L58.0000 48.0000 L66.0005 54.0000 L82.5003 55.5000 M82.5003 55.5000",
  28.     "M89.0008 80.9991 L89.5008 83.4991 L88.0008 85.4991 L86.5007 84.9991 L78.0007 78.9991 L65.0007 67.4991 L55.0007 60.4991 L58.0000 51.0000 L62.9999 54.0001 L66.0007 59.4991 L89.0008 80.9991 M89.0008 80.9991",
  29.     "M77.5007 86.9997 L77.5007 90.9997 L77.0006 92.4997 L75.0004 93.4997 L71.5006 93.0339 L47.4669 57.2642 L56.9998 50.0002 L64.9994 64.5004 L65.7507 69.7497 L77.5007 86.9997 M77.5007 86.9997",
  30.     "M51.9998 98.0000 L50.5002 100.0000 L47.5002 101.0000 L45.0001 99.0000 L43.5000 96.0000 L51.0003 55.4999 L55.5001 55.9999 L51.9998 98.0000 M51.9998 98.0000",
  31.     "M27.0004 92.9999 L25.0003 93.4999 L22.0003 91.9999 L22.5004 89.4999 L52.0003 50.5000 L56.0004 55.9999 L34.0003 85.0000 L27.0004 92.9999 M27.0004 92.9999",
  32.     "M17.5002 79.0264 L12.5005 79.0264 L10.5124 76.7369 L10.5124 74.0000 L19.0005 68.0000 L53.5082 46.0337 L57.0005 52.0000 L17.5002 79.0264 M17.5002 79.0264",
  33.     "M2.5003 53.0000 L0.2370 50.5000 L0.2373 48.2759 L2.5003 47.5000 L28.0000 49.0000 L53.0000 51.0000 L52.1885 55.9782 L4.5000 53.5000 L2.5003 53.0000 M2.5003 53.0000",
  34.     "M8.4990 27.0019 L7.4999 23.0001 L10.5003 19.5001 L14.0003 20.0001 L15.0003 20.0001 L36.0000 35.5000 L42.5000 40.5000 L51.5000 47.5000 L46.5000 56.0000 L42.0002 52.5000 L39.0001 49.5000 L10.0000 29.0001 L8.4990 27.0019 M8.4990 27.0019",
  35.     "M23.4253 5.1588 L26.5075 1.2217 L28.5175 0.7632 L32.5063 1.3458 L34.4748 2.8868 L48.8202 34.6902 L54.0089 49.8008 L47.9378 53.1760 L24.8009 11.1886 L23.4253 5.1588 M23.4253 5.1588",
  36.     "M55.5002 4.5000 L58.5005 2.5000 L61.0002 3.5000 L63.5002 7.0000 L56.6511 48.1620 L52.0005 45.0000 L50.0005 39.5000 L53.5003 8.5000 L55.5002 4.5000 M55.5002 4.5000",
  37.     "M80.1032 10.5903 L84.9968 11.6171 L86.2958 13.2179 L87.5346 17.0540 L87.0213 19.5007 L58.5000 58.5000 L49.0000 49.0000 L75.3008 14.4873 L80.1032 10.5903 M80.1032 10.5903",
  38.     "M96.0000 40.0000 L99.5002 42.0000 L99.5002 43.5000 L98.5000 47.0000 L56.0000 57.0000 L52.0040 47.0708 L96.0000 40.0000 M96.0000 40.0000",
  39. ].reverse();
  40.  
  41. const containerRef = ref<HTMLElement>();
  42. const focalPointRef: FocalPointRef = {
  43.     current: { x: 0, y: 0 },
  44. };
  45.  
  46. let animator: ClaudeAnimator | undefined;
  47.  
  48. const quantize = (value: number, step: number) => Math.round(value / step) * step;
  49.  
  50. const handleMouseMove = (event: MouseEvent) => {
  51.     focalPointRef.current = {
  52.         x: quantize(event.clientX, 10),
  53.         y: quantize(event.clientY, 10),
  54.     };
  55.     animator?.updateFocalPoint();
  56. };
  57.  
  58. onMounted(() => {
  59.     focalPointRef.current = {
  60.         x: window.innerWidth / 2,
  61.         y: window.innerHeight / 2,
  62.     };
  63.  
  64.     if (containerRef.value) {
  65.         animator = new ClaudeAnimator(containerRef.value, focalPointRef);
  66.     }
  67.  
  68.     window.addEventListener("mousemove", handleMouseMove);
  69. });
  70.  
  71. onUnmounted(() => {
  72.     window.removeEventListener("mousemove", handleMouseMove);
  73.     animator?.dispose();
  74. });
  75.  
  76. class ClaudeAnimator {
  77.     private readonly arms: SVGPathElement[];
  78.     private readonly armPaths: PathSegment[][];
  79.     private readonly armWidths: number[] = [1.01, 0.95, 1.05, 0.9, 1.1, 0.85, 1.15, 0.8, 1.2, 0.75, 1.25, 0.7];
  80.     private readonly closenessRamp = new ClosenessRamp(0, 50, 0.34);
  81.     private lastFocalPoint: Point;
  82.  
  83.     constructor(
  84.         private readonly container: HTMLElement,
  85.         private readonly focalPointRef: FocalPointRef
  86.     ) {
  87.         this.arms = Array.from(this.container.querySelectorAll("path")).reverse();
  88.         this.armPaths = this.arms.map(arm => parsePathData(arm.getAttribute("d") || ""));
  89.         this.lastFocalPoint = { ...this.focalPointRef.current };
  90.         this.renderArms();
  91.     }
  92.  
  93.     updateFocalPoint() {
  94.         const current = this.focalPointRef.current;
  95.         if (current.x === this.lastFocalPoint.x && current.y === this.lastFocalPoint.y) return;
  96.  
  97.         const within = this.distanceToPoint(current) < 70;
  98.         const wasWithin = this.distanceToPoint(this.lastFocalPoint) < 70;
  99.         this.lastFocalPoint = { ...current };
  100.  
  101.         if (within || wasWithin) {
  102.             shuffle(this.armWidths);
  103.             this.renderArms();
  104.         }
  105.  
  106.         if (within !== wasWithin) {
  107.             this.closenessRamp.setTarget(within ? 1 : 0, () => this.renderArms());
  108.         }
  109.     }
  110.  
  111.     dispose() {
  112.         this.closenessRamp.dispose();
  113.     }
  114.  
  115.     private renderArms() {
  116.         const rect = this.container.getBoundingClientRect();
  117.         const centerX = rect.left + rect.width / 2;
  118.         const centerY = rect.top + rect.height / 2;
  119.         const focal = this.focalPointRef.current;
  120.         const dx = focal.x - centerX;
  121.         const dy = focal.y - centerY;
  122.         const distance = Math.sqrt(dx * dx + dy * dy);
  123.         let angle = (Math.atan2(dy, dx) * 180) / Math.PI;
  124.         if (angle < 0) angle += 360;
  125.  
  126.         this.arms.forEach((arm, index) => {
  127.             const armAngle = (index * 30) % 360;
  128.             let angleDiff = Math.abs(angle - armAngle);
  129.             if (angleDiff > 180) angleDiff = 360 - angleDiff;
  130.  
  131.             const closeness = this.closenessRamp.getCurrent();
  132.             const distanceFactor = lerpClamped(distance, [49, 70], [1, 0]) * closeness;
  133.             const innerDistanceFactor = lerpClamped(distance, [5, 25], [1, 0]) * closeness;
  134.  
  135.             let radialScale = 1;
  136.             if (angleDiff <= 50) {
  137.                 const bonus = index % 2 === 0 ? 0.6 : 0.48;
  138.                 radialScale = 1 + bonus * distanceFactor * (1 - angleDiff / 50);
  139.             }
  140.  
  141.             const angleScale = lerpClamped(angleDiff, [90, 180], [1, 1 - 0.4 * distanceFactor]);
  142.             radialScale *= angleScale;
  143.             radialScale = lerpClamped(innerDistanceFactor, [0, 1], [radialScale, 0.6]);
  144.  
  145.             let transformScale = (1 - this.armWidths[index]) * 0.3 + 1 + lerpClamped(angleDiff, [0, 90], [0.2, 0]);
  146.             transformScale = lerpClamped(innerDistanceFactor, [0, 1], [transformScale, 1.4]);
  147.  
  148.             const originalSegments = this.armPaths[index];
  149.             const transformedPath = buildPathData(originalSegments, ([x, y]) => {
  150.                 const offsetX = x - 50;
  151.                 const offsetY = y - 50;
  152.                 const radialDistance = Math.sqrt(offsetX * offsetX + offsetY * offsetY);
  153.                 const radialFactor = lerpClamped(radialDistance, [20, 30], [0, 1]);
  154.                 const shift = (radialScale - 1) * radialFactor * 40;
  155.                 if (shift === 0) return [x, y] as const;
  156.                 const direction = Math.atan2(offsetY, offsetX);
  157.                 return [x + shift * Math.cos(direction), y + shift * Math.sin(direction)] as const;
  158.             });
  159.  
  160.             arm.setAttribute("d", transformedPath);
  161.             arm.style.transformOrigin = "50px 50px";
  162.             arm.style.transform = `rotate(${armAngle}deg) scaleY(${transformScale}) rotate(${-armAngle}deg)`;
  163.         });
  164.     }
  165.  
  166.     private distanceToPoint(point: Point) {
  167.         const rect = this.container.getBoundingClientRect();
  168.         const centerX = rect.left + rect.width / 2;
  169.         const centerY = rect.top + rect.height / 2;
  170.         const dx = point.x - centerX;
  171.         const dy = point.y - centerY;
  172.         return Math.sqrt(dx * dx + dy * dy);
  173.     }
  174. }
  175.  
  176. class ClosenessRamp {
  177.     private current: number;
  178.     private target: number;
  179.     private intervalId: number | undefined;
  180.     private callback: ((value: number) => void) | undefined;
  181.  
  182.     constructor(
  183.         initial: number,
  184.         private readonly msPerTick: number,
  185.         private readonly amountPerTick: number
  186.     ) {
  187.         this.current = initial;
  188.         this.target = initial;
  189.     }
  190.  
  191.     getCurrent() {
  192.         return this.current;
  193.     }
  194.  
  195.     setTarget(target: number, callback?: (value: number) => void) {
  196.         if (this.target === target) return;
  197.         this.target = target;
  198.         this.callback = callback;
  199.  
  200.         if (this.intervalId !== undefined) {
  201.             window.clearInterval(this.intervalId);
  202.         }
  203.  
  204.         this.intervalId = window.setInterval(() => {
  205.             if (this.current === this.target) {
  206.                 if (this.intervalId !== undefined) {
  207.                     window.clearInterval(this.intervalId);
  208.                     this.intervalId = undefined;
  209.                 }
  210.                 return;
  211.             }
  212.  
  213.             const diff = this.target - this.current;
  214.             const delta = Math.sign(diff) * Math.min(Math.abs(diff), this.amountPerTick);
  215.             this.current = parseFloat((this.current + delta).toFixed(5));
  216.             this.callback?.(this.current);
  217.         }, this.msPerTick);
  218.     }
  219.  
  220.     dispose() {
  221.         if (this.intervalId !== undefined) {
  222.             window.clearInterval(this.intervalId);
  223.             this.intervalId = undefined;
  224.         }
  225.     }
  226. }
  227.  
  228. const shuffle = (array: number[]) => {
  229.     for (let i = array.length - 1; i > 0; i--) {
  230.         const j = Math.floor(Math.random() * (i + 1));
  231.         [array[i], array[j]] = [array[j], array[i]];
  232.     }
  233. };
  234.  
  235. const lerpClamped = (value: number, [inMin, inMax]: [number, number], [outMin, outMax]: [number, number]) => {
  236.     if (inMin === inMax) {
  237.         return outMin;
  238.     }
  239.     const min = Math.min(inMin, inMax);
  240.     const max = Math.max(inMin, inMax);
  241.     const clamped = Math.min(Math.max(value, min), max);
  242.     const ratio = (clamped - inMin) / (inMax - inMin);
  243.     return outMin + ratio * (outMax - outMin);
  244. };
  245.  
  246. const parsePathData = (d: string): PathSegment[] => {
  247.     const segments: PathSegment[] = [];
  248.     const tokens = d.match(/[MLZ][^MLZ]*/g);
  249.     if (!tokens) return segments;
  250.  
  251.     tokens.forEach(token => {
  252.         const command = token[0] as "M" | "L" | "Z";
  253.         if (command === "Z") {
  254.             segments.push({ type: "Z" });
  255.             return;
  256.         }
  257.  
  258.         const coords = token
  259.             .slice(1)
  260.             .trim()
  261.             .split(/[\s,]+/)
  262.             .filter(Boolean);
  263.  
  264.         for (let i = 0; i < coords.length; i += 2) {
  265.             const x = Number.parseFloat(coords[i]);
  266.             const y = Number.parseFloat(coords[i + 1]);
  267.             if (Number.isFinite(x) && Number.isFinite(y)) {
  268.                 segments.push({ type: command, x, y });
  269.             }
  270.         }
  271.     });
  272.  
  273.     return segments;
  274. };
  275.  
  276. const buildPathData = (segments: PathSegment[], transform: (point: [number, number]) => readonly [number, number]) => {
  277.     const parts: string[] = [];
  278.     segments.forEach(segment => {
  279.         if (segment.type === "Z") {
  280.             parts.push("Z");
  281.             return;
  282.         }
  283.  
  284.         const [x, y] = transform([segment.x, segment.y]);
  285.         parts.push(`${segment.type}${x.toFixed(4)} ${y.toFixed(4)}`);
  286.     });
  287.     return parts.join(" ").trim();
  288. };
  289. </script>
  290.  
  291. <style scoped>
  292. .claude-icon-container {
  293.     display: inline-block;
  294.     width: 100%;
  295.     height: 100%;
  296. }
  297.  
  298. .claude-icon {
  299.     display: block;
  300.     width: 100%;
  301.     height: 100%;
  302. }
  303. </style>
  304.  
Advertisement
Add Comment
Please, Sign In to add comment