Przeglądaj źródła

fix(app): reduce session spinner CPU usage (#42952)

Luke Parker 1 dzień temu
rodzic
commit
be8f975a99

+ 1 - 0
packages/session-ui/package.json

@@ -25,6 +25,7 @@
     "./v2/prompt-input/types": "./src/v2/components/prompt-input/types.ts"
   },
   "scripts": {
+    "generate:progress-indicator": "bun script/generate-session-progress-indicator.ts",
     "typecheck": "tsgo -b",
     "test": "bun test src --only-failures"
   },

+ 133 - 0
packages/session-ui/script/generate-session-progress-indicator.ts

@@ -0,0 +1,133 @@
+import { deflateSync } from "node:zlib"
+
+const size = 16
+const frameRate = 90
+const duration = 1.2
+const frameCount = frameRate * duration
+const opacity = [0.2, 0.5, 0.75, 1]
+// Each digit selects one dot opacity for one of the eight source key poses.
+const poses = [
+  "0000000000003000031000321",
+  "0000000000003000320032100",
+  "0000000000333002100010000",
+  "3000023000123000000000000",
+  "1230001300003000000000000",
+  "0012300230003000000000000",
+  "0000100022003330000000000",
+  "0000000000003330002200001",
+].map((pose) => Array.from(pose, (value) => opacity[Number(value)]))
+const crcTable = Array.from({ length: 256 }, (_, value) =>
+  Array.from({ length: 8 }).reduce<number>((crc) => ((crc & 1) !== 0 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1), value),
+)
+
+await Bun.write(new URL("../src/v2/components/session-progress-indicator-v2-1x.png", import.meta.url), apng(1))
+
+function apng(scale: number) {
+  const frameSize = size * scale
+  const header = Buffer.alloc(13)
+  header.writeUInt32BE(frameSize, 0)
+  header.writeUInt32BE(frameSize, 4)
+  header[8] = 8
+  header[9] = 6
+
+  const animation = Buffer.alloc(8)
+  animation.writeUInt32BE(frameCount, 0)
+  animation.writeUInt32BE(0, 4)
+  const chunks = [chunk("IHDR", header), chunk("acTL", animation)]
+  const sequence = { value: 0 }
+
+  Array.from({ length: frameCount }, (_, frame) => {
+    const control = Buffer.alloc(26)
+    control.writeUInt32BE(sequence.value++, 0)
+    control.writeUInt32BE(frameSize, 4)
+    control.writeUInt32BE(frameSize, 8)
+    control.writeUInt16BE(frame % 9 === 0 ? 12 : 11, 20)
+    control.writeUInt16BE(1000, 22)
+    chunks.push(chunk("fcTL", control))
+
+    const data = deflateSync(pixels(scale, frame), { level: 9 })
+    if (frame === 0) {
+      chunks.push(chunk("IDAT", data))
+      return
+    }
+    const frameData = Buffer.alloc(data.length + 4)
+    frameData.writeUInt32BE(sequence.value++, 0)
+    data.copy(frameData, 4)
+    chunks.push(chunk("fdAT", frameData))
+  })
+
+  return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), ...chunks, chunk("IEND", Buffer.alloc(0))])
+}
+
+function pixels(scale: number, frame: number) {
+  const frameSize = size * scale
+  const stride = frameSize * 4 + 1
+  const pixels = Buffer.alloc(stride * frameSize)
+  const progress = (frame / frameCount) * poses.length
+  const current = Math.floor(progress)
+  const next = (current + 1) % poses.length
+  const mix = easeOut(progress - current)
+
+  Array.from({ length: frameSize }, (_, y) => (pixels[y * stride] = 0))
+  poses[current].forEach((value, index) => {
+    draw(
+      pixels,
+      stride,
+      (1 + (index % 5) * 3) * scale,
+      (1 + Math.floor(index / 5) * 3) * scale,
+      3 * scale,
+      value + (poses[next][index] - value) * mix,
+    )
+  })
+  return pixels
+}
+
+function draw(pixels: Buffer, stride: number, x: number, y: number, size: number, opacity: number) {
+  const left = Math.floor(x)
+  const top = Math.floor(y)
+  const right = Math.ceil(x + size)
+  const bottom = Math.ceil(y + size)
+
+  Array.from({ length: bottom - top }, (_, row) => top + row).forEach((py) =>
+    Array.from({ length: right - left }, (_, column) => left + column).forEach((px) => {
+      const coverageX = Math.max(0, Math.min(px + 1, x + size) - Math.max(px, x))
+      const coverageY = Math.max(0, Math.min(py + 1, y + size) - Math.max(py, y))
+      const offset = py * stride + 1 + px * 4
+      pixels[offset] = 255
+      pixels[offset + 1] = 255
+      pixels[offset + 2] = 255
+      pixels[offset + 3] = Math.round(opacity * coverageX * coverageY * 255)
+    }),
+  )
+}
+
+function easeOut(progress: number) {
+  const curve = (value: number, first: number, second: number) => {
+    const inverse = 1 - value
+    return 3 * inverse * inverse * value * first + 3 * inverse * value * value * second + value * value * value
+  }
+  const parameter = Array.from({ length: 16 }).reduce<[number, number]>(
+    (range) => {
+      const middle = (range[0] + range[1]) / 2
+      return curve(middle, 0, 0.58) < progress ? [middle, range[1]] : [range[0], middle]
+    },
+    [0, 1],
+  )
+  return curve((parameter[0] + parameter[1]) / 2, 0, 1)
+}
+
+function chunk(type: string, data: Buffer) {
+  const name = Buffer.from(type)
+  const result = Buffer.alloc(data.length + 12)
+  result.writeUInt32BE(data.length, 0)
+  name.copy(result, 4)
+  data.copy(result, 8)
+  result.writeUInt32BE(crc32(Buffer.concat([name, data])), data.length + 8)
+  return result
+}
+
+function crc32(data: Buffer) {
+  return (
+    (data.reduce<number>((crc, value) => crcTable[(crc ^ value) & 0xff] ^ (crc >>> 8), 0xffffffff) ^ 0xffffffff) >>> 0
+  )
+}

BIN
packages/session-ui/src/v2/components/session-progress-indicator-v2-1x.png


+ 10 - 860
packages/session-ui/src/v2/components/session-progress-indicator-v2.css

@@ -1,875 +1,25 @@
 [data-component="session-progress-indicator-v2"] {
-  --_duration: 1200ms;
   display: block;
   flex-shrink: 0;
   color: var(--v2-icon-icon-muted, #808080);
 }
 
-[data-component="session-progress-indicator-v2"] [data-dot] {
-  fill: currentColor;
-  opacity: 0.2;
-  animation-duration: var(--_duration);
-  animation-timing-function: ease-out;
-  animation-iteration-count: infinite;
-  animation-fill-mode: both;
-}
-
-@keyframes session-progress-indicator-v2-dot-0 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 1;
-  }
-  50% {
-    opacity: 0.5;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="0"] {
-  animation-name: session-progress-indicator-v2-dot-0;
-}
-
-@keyframes session-progress-indicator-v2-dot-5 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.75;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="5"] {
-  animation-name: session-progress-indicator-v2-dot-5;
-}
-
-@keyframes session-progress-indicator-v2-dot-10 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 1;
-  }
-  37.5% {
-    opacity: 0.5;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="10"] {
-  animation-name: session-progress-indicator-v2-dot-10;
-}
-
-@keyframes session-progress-indicator-v2-dot-15 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.75;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
+[data-component="session-progress-indicator-v2"] [data-frame-content] {
+  image-rendering: pixelated;
+  image-rendering: crisp-edges;
 }
 
-[data-component="session-progress-indicator-v2"] [data-dot="15"] {
-  animation-name: session-progress-indicator-v2-dot-15;
-}
-
-@keyframes session-progress-indicator-v2-dot-20 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 1;
-  }
-  25% {
-    opacity: 0.5;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="20"] {
-  animation-name: session-progress-indicator-v2-dot-20;
-}
-
-@keyframes session-progress-indicator-v2-dot-1 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.75;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="1"] {
-  animation-name: session-progress-indicator-v2-dot-1;
-}
-
-@keyframes session-progress-indicator-v2-dot-6 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 1;
-  }
-  50% {
-    opacity: 0.5;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="6"] {
-  animation-name: session-progress-indicator-v2-dot-6;
-}
-
-@keyframes session-progress-indicator-v2-dot-11 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 1;
-  }
-  37.5% {
-    opacity: 0.75;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="11"] {
-  animation-name: session-progress-indicator-v2-dot-11;
-}
-
-@keyframes session-progress-indicator-v2-dot-16 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 1;
-  }
-  25% {
-    opacity: 0.5;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="16"] {
-  animation-name: session-progress-indicator-v2-dot-16;
-}
-
-@keyframes session-progress-indicator-v2-dot-21 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.75;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="21"] {
-  animation-name: session-progress-indicator-v2-dot-21;
-}
-
-@keyframes session-progress-indicator-v2-dot-2 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 1;
-  }
-  62.5% {
-    opacity: 0.5;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="2"] {
-  animation-name: session-progress-indicator-v2-dot-2;
-}
-
-@keyframes session-progress-indicator-v2-dot-7 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 1;
-  }
-  62.5% {
-    opacity: 0.75;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="7"] {
-  animation-name: session-progress-indicator-v2-dot-7;
-}
-
-@keyframes session-progress-indicator-v2-dot-12 {
-  0% {
-    opacity: 1;
-  }
-  12.5% {
-    opacity: 1;
-  }
-  25% {
-    opacity: 1;
-  }
-  37.5% {
-    opacity: 1;
-  }
-  50% {
-    opacity: 1;
-  }
-  62.5% {
-    opacity: 1;
-  }
-  75% {
-    opacity: 1;
-  }
-  87.5% {
-    opacity: 1;
-  }
-  100% {
-    opacity: 1;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="12"] {
-  animation-name: session-progress-indicator-v2-dot-12;
-}
-
-@keyframes session-progress-indicator-v2-dot-17 {
-  0% {
-    opacity: 1;
-  }
-  12.5% {
-    opacity: 0.75;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 1;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="17"] {
-  animation-name: session-progress-indicator-v2-dot-17;
-}
-
-@keyframes session-progress-indicator-v2-dot-22 {
-  0% {
-    opacity: 1;
-  }
-  12.5% {
-    opacity: 0.5;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 1;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="22"] {
-  animation-name: session-progress-indicator-v2-dot-22;
-}
-
-@keyframes session-progress-indicator-v2-dot-3 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.75;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="3"] {
-  animation-name: session-progress-indicator-v2-dot-3;
-}
-
-@keyframes session-progress-indicator-v2-dot-8 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 1;
-  }
-  75% {
-    opacity: 0.75;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="8"] {
-  animation-name: session-progress-indicator-v2-dot-8;
-}
-
-@keyframes session-progress-indicator-v2-dot-13 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 1;
-  }
-  87.5% {
-    opacity: 1;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="13"] {
-  animation-name: session-progress-indicator-v2-dot-13;
-}
-
-@keyframes session-progress-indicator-v2-dot-18 {
-  0% {
-    opacity: 0.5;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.75;
-  }
-  100% {
-    opacity: 0.5;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="18"] {
-  animation-name: session-progress-indicator-v2-dot-18;
-}
-
-@keyframes session-progress-indicator-v2-dot-23 {
-  0% {
-    opacity: 0.75;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.75;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="23"] {
-  animation-name: session-progress-indicator-v2-dot-23;
-}
-
-@keyframes session-progress-indicator-v2-dot-4 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 1;
-  }
-  75% {
-    opacity: 0.5;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="4"] {
-  animation-name: session-progress-indicator-v2-dot-4;
-}
-
-@keyframes session-progress-indicator-v2-dot-9 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.75;
-  }
-  87.5% {
-    opacity: 0.2;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="9"] {
-  animation-name: session-progress-indicator-v2-dot-9;
-}
-
-@keyframes session-progress-indicator-v2-dot-14 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 1;
-  }
-  87.5% {
-    opacity: 1;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="14"] {
-  animation-name: session-progress-indicator-v2-dot-14;
-}
-
-@keyframes session-progress-indicator-v2-dot-19 {
-  0% {
-    opacity: 0.2;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.75;
-  }
-  100% {
-    opacity: 0.2;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="19"] {
-  animation-name: session-progress-indicator-v2-dot-19;
-}
-
-@keyframes session-progress-indicator-v2-dot-24 {
-  0% {
-    opacity: 0.5;
-  }
-  12.5% {
-    opacity: 0.2;
-  }
-  25% {
-    opacity: 0.2;
-  }
-  37.5% {
-    opacity: 0.2;
-  }
-  50% {
-    opacity: 0.2;
-  }
-  62.5% {
-    opacity: 0.2;
-  }
-  75% {
-    opacity: 0.2;
-  }
-  87.5% {
-    opacity: 0.5;
-  }
-  100% {
-    opacity: 0.5;
-  }
-}
-
-[data-component="session-progress-indicator-v2"] [data-dot="24"] {
-  animation-name: session-progress-indicator-v2-dot-24;
+[data-component="session-progress-indicator-v2"] [data-reduced-motion] {
+  display: none;
+  fill: currentColor;
 }
 
 @media (prefers-reduced-motion: reduce) {
-  [data-component="session-progress-indicator-v2"] [data-dot] {
-    animation: none;
+  [data-component="session-progress-indicator-v2"] [data-frame-content] {
+    display: none;
   }
 
-  [data-component="session-progress-indicator-v2"] [data-dot="12"] {
-    opacity: 1;
+  [data-component="session-progress-indicator-v2"] [data-reduced-motion] {
+    display: block;
   }
 }

+ 39 - 1
packages/session-ui/src/v2/components/session-progress-indicator-v2.stories.tsx

@@ -1,5 +1,6 @@
 // @ts-nocheck
 import { SessionProgressIndicatorV2 } from "./session-progress-indicator-v2"
+import { createSignal, onCleanup, onMount } from "solid-js"
 
 const docs = `### Overview
 Animated 5×5 dot grid loader for in-progress session state.
@@ -10,7 +11,7 @@ Derived from Figma \`_sessionProgressIndicator\` with 8-frame rotation.
 - Accepts standard SVG props.
 
 ### Behavior
-- CSS keyframes drive per-dot opacity across 8 frames (1.2s loop).
+- A shared, pre-rendered alpha mask preserves the smooth opacity changes between 8 key poses (1.2s loop).
 - Center dot stays at full opacity throughout the cycle.
 
 ### Accessibility
@@ -48,6 +49,43 @@ export const Sizes = {
   ),
 }
 
+export const StressTest = {
+  parameters: {
+    layout: "fullscreen",
+  },
+  render: () => <StressGrid />,
+}
+
+function StressGrid() {
+  const measure = () => ({
+    columns: Math.max(1, Math.floor((window.innerWidth + 4) / 20)),
+    rows: Math.max(1, Math.floor((window.innerHeight + 4) / 20)),
+  })
+  const [grid, setGrid] = createSignal(measure())
+  const resize = () => setGrid(measure())
+  onMount(() => window.addEventListener("resize", resize))
+  onCleanup(() => window.removeEventListener("resize", resize))
+
+  return (
+    <div
+      style={{
+        display: "grid",
+        width: "100vw",
+        height: "100vh",
+        overflow: "hidden",
+        gap: "4px",
+        "grid-template-columns": `repeat(${grid().columns}, 16px)`,
+        "grid-template-rows": `repeat(${grid().rows}, 16px)`,
+        "place-content": "center",
+      }}
+    >
+      {Array.from({ length: grid().columns * grid().rows }, () => (
+        <SessionProgressIndicatorV2 />
+      ))}
+    </div>
+  )
+}
+
 export const OnDark = {
   render: () => (
     <div

+ 30 - 11
packages/session-ui/src/v2/components/session-progress-indicator-v2.tsx

@@ -1,18 +1,18 @@
-import { For, splitProps, type ComponentProps } from "solid-js"
+import { createUniqueId, splitProps, type ComponentProps } from "solid-js"
 import "./session-progress-indicator-v2.css"
 
-const grid = 5
-const dot = 2
-const gap = 1
-const origin = 1.5
-const dots = Array.from({ length: grid * grid }, (_, index) => ({
-  index,
-  x: origin + (index % grid) * (dot + gap),
-  y: origin + Math.floor(index / grid) * (dot + gap),
-}))
+const frames = new URL("./session-progress-indicator-v2-1x.png", import.meta.url).href
+const dots = Array.from({ length: 25 }, (_, index) => {
+  const x = 1.5 + (index % 5) * 3
+  const y = 1.5 + Math.floor(index / 5) * 3
+  return `M${x} ${y}h2v2h-2z`
+}).join("")
 
 export function SessionProgressIndicatorV2(props: ComponentProps<"svg">) {
   const [local, rest] = splitProps(props, ["class", "classList", "width", "height"])
+  const id = createUniqueId()
+  const filter = `session-progress-indicator-filter-${id}`
+  const clip = `session-progress-indicator-clip-${id}`
   return (
     <svg
       {...rest}
@@ -26,7 +26,26 @@ export function SessionProgressIndicatorV2(props: ComponentProps<"svg">) {
       data-component="session-progress-indicator-v2"
       aria-hidden={rest["aria-hidden"] ?? "true"}
     >
-      <For each={dots}>{(cell) => <rect data-dot={cell.index} x={cell.x} y={cell.y} width={dot} height={dot} />}</For>
+      <defs>
+        <filter id={filter} filterUnits="userSpaceOnUse" x={0} y={0} width={16} height={16}>
+          <feFlood flood-color="currentColor" result="color" />
+          <feComposite in="color" in2="SourceAlpha" operator="in" />
+        </filter>
+        <clipPath id={clip}>
+          <path d={dots} />
+        </clipPath>
+      </defs>
+      <image
+        data-frame-content
+        href={frames}
+        x={0}
+        y={0}
+        width={16}
+        height={16}
+        filter={`url(#${filter})`}
+        clip-path={`url(#${clip})`}
+      />
+      <rect data-reduced-motion x={7.5} y={7.5} width={2} height={2} />
     </svg>
   )
 }