|
|
@@ -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
|
|
|
+ )
|
|
|
+}
|