eb36f3fMake the signal view stationary by default with a play toggleJeremy Magland 1import { useEffect, useRef, useState } from 'react'
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 2import { LatentSource, LATENT_SEED } from '../model/latent'
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 3
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 4/** Display samples generated per second while playing; px per sample fixed. */
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 5const RATE = 220
6const PX_PER_SAMPLE = 2
8/** A nice round gridline step ≤ span/2. */
9function niceStep(span: number): number {
10 const raw = span / 2
11 const mag = 10 ** Math.floor(Math.log10(raw))
12 for (const m of [5, 2.5, 2, 1]) if (m * mag <= raw) return m * mag
13 return mag
14}
16/**
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 17 * A window of the quantized signal z, drawn as connected line segments.
18 * The underlying randomness is a fixed latent sequence indexed by absolute
19 * sample position — parameter changes re-render the same window of latent
20 * data (no resampling), so the trace morphs smoothly. Stationary by default;
21 * the play toggle advances the window through the latent sequence.
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 22 */
23export default function ScrollingView(props: {
24 kernel: Float64Array
25 sigma: number
26 dither: boolean
27 /** Predicted std of the quantized signal, for a stable y-scale. */
28 sigmaY: number
29}) {
30 const canvasRef = useRef<HTMLCanvasElement>(null)
eb36f3fMake the signal view stationary by default with a play toggleJeremy Magland 31 const [playing, setPlaying] = useState(false)
32 const playingRef = useRef(playing)
33 playingRef.current = playing
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 34 // Latent noise and window position survive parameter changes.
35 const latentRef = useRef<LatentSource | null>(null)
36 if (!latentRef.current) latentRef.current = new LatentSource(LATENT_SEED)
37 const posRef = useRef(0)
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 38
39 useEffect(() => {
40 const canvas = canvasRef.current
41 if (!canvas) return
42 const ctx = canvas.getContext('2d')
43 if (!ctx) return
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 44 const latent = latentRef.current!
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 45
46 const scale = Math.max(4 * props.sigmaY, 3.5)
47 const gridStep = niceStep(scale)
49 let styles = getComputedStyle(canvas)
50 const scheme = window.matchMedia('(prefers-color-scheme: dark)')
51 const refreshStyles = () => {
52 styles = getComputedStyle(canvas)
53 }
54 scheme.addEventListener('change', refreshStyles)
56 let raf = 0
57 let last = performance.now()
58 let carry = 0
60 const draw = (now: number) => {
61 raf = requestAnimationFrame(draw)
62 const dt = Math.min(0.25, (now - last) / 1000)
63 last = now
eb36f3fMake the signal view stationary by default with a play toggleJeremy Magland 64 if (playingRef.current) {
65 carry += dt * RATE
66 const n = Math.floor(carry)
67 carry -= n
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 68 posRef.current += n
70 carry = 0
71 }
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 72
73 const dpr = window.devicePixelRatio || 1
74 const w = canvas.clientWidth
75 const h = canvas.clientHeight
76 if (w === 0 || h === 0) return
77 if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {
78 canvas.width = Math.round(w * dpr)
79 canvas.height = Math.round(h * dpr)
80 }
81 ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 83 const visible = Math.floor(w / PX_PER_SAMPLE)
84 // The window ends at posRef and never reaches before index 0, so the
85 // first thing shown is the start of the compression block.
86 if (posRef.current < visible) posRef.current = visible
87 const win = latent.window(posRef.current - visible, visible, props.kernel, props.sigma, props.dither)
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 89 const surface = styles.getPropertyValue('--surface')
90 ctx.fillStyle = surface
91 ctx.fillRect(0, 0, w, h)
93 const yOf = (v: number) => h / 2 - (v / scale) * (h / 2 - 12)
95 ctx.lineWidth = 1
96 ctx.font = '11px system-ui, sans-serif'
97 ctx.textAlign = 'left'
98 for (let g = -2; g <= 2; g++) {
99 const v = g * gridStep
100 if (Math.abs(v) > scale) continue
101 const y = Math.round(yOf(v)) + 0.5
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 102 if (g !== 0) {
103 ctx.strokeStyle = styles.getPropertyValue('--grid')
104 ctx.beginPath()
105 ctx.moveTo(0, y)
106 ctx.lineTo(w, y)
107 ctx.stroke()
108 }
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 109 // A surface-colored halo keeps the label readable over the trace.
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 110 const label = `${v > 0 ? '+' : ''}${+v.toPrecision(3)}`
111 ctx.fillStyle = styles.getPropertyValue('--muted')
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 112 ctx.strokeStyle = surface
113 ctx.lineWidth = 3
114 ctx.strokeText(label, 6, y - 4)
115 ctx.fillText(label, 6, y - 4)
116 ctx.lineWidth = 1
117 }
118 const zeroY = Math.round(yOf(0)) + 0.5
119 ctx.strokeStyle = styles.getPropertyValue('--baseline')
120 ctx.beginPath()
121 ctx.moveTo(0, zeroY)
122 ctx.lineTo(w, zeroY)
123 ctx.stroke()
125 ctx.strokeStyle = styles.getPropertyValue('--series-1')
126 ctx.lineWidth = 2
127 ctx.lineJoin = 'round'
128 ctx.beginPath()
129 for (let i = 0; i < visible; i++) {
5bab85aRatio-first chart, quantization-floor theory formula, line-segment view, fixed latent dataJeremy Magland 130 const x = w - (visible - i) * PX_PER_SAMPLE + PX_PER_SAMPLE / 2
131 const y = yOf(win[i])
36e8ceaInteractive explorer for compressibility of quantized filtered Gaussian time seriesJeremy Magland 132 if (i === 0) ctx.moveTo(x, y)
133 else ctx.lineTo(x, y)
134 }
135 ctx.stroke()
136 }
137 raf = requestAnimationFrame(draw)
139 return () => {
140 cancelAnimationFrame(raf)
141 scheme.removeEventListener('change', refreshStyles)
142 }
143 }, [props.kernel, props.sigma, props.dither, props.sigmaY])
146 <div className="scroll-wrap">
147 <canvas ref={canvasRef} className="scroll-canvas" />
148 <button className="play-btn" onClick={() => setPlaying(p => !p)}>
149 {playing ? '⏸ pause' : '▶ play'}
150 </button>
151 </div>
152 )