1- import { useEffect , useMemo } from 'react' ;
1+ import { useEffect , useMemo , useCallback } from 'react' ;
22
33const style = {
44 backgroundColor : "#0f172a" ,
@@ -7,76 +7,146 @@ const style = {
77 connectorColor : "#ffffff" ,
88 connectorWidth : 2 ,
99 nodeRadius : 6 ,
10+ axisColor : "#999" ,
11+ textColor : "#fff" ,
12+ gridColor : "rgba(255,255,255,0.1)"
1013} ;
1114
12- const useCanvasDrawing = ( canvasRef , { wasmModule, width, height, nodes, xRange, curves, freqs, isLogarithmic, onWheel } ) => {
13- useEffect ( ( ) => {
14- if ( ! width || ! height ) return ;
15-
16- const canvas = canvasRef . current ;
17- const context = canvas . getContext ( '2d' ) ;
18-
19- // Clear canvas and set background
20- context . fillStyle = style . backgroundColor ;
21- context . fillRect ( 0 , 0 , width , height ) ;
2215
23- // Draw frequency bars
24- if ( freqs && freqs . length > 0 ) {
25- context . strokeStyle = style . barColor ;
26- context . lineWidth = 1 ;
27- context . beginPath ( ) ;
28- const logXRange = [ Math . log ( xRange [ 0 ] ) , Math . log ( xRange [ 1 ] ) ] ;
29- const logRange = logXRange [ 1 ] - logXRange [ 0 ] ;
30-
31- freqs . forEach ( ( [ freq , amp ] ) => {
32- const canvasX = freq <= 0 ? 0 : ( ( Math . log ( freq ) - logXRange [ 0 ] ) / logRange ) * width ;
33- const barHeight = amp * height ;
34- if ( canvasX >= 0 && canvasX <= width ) {
35- context . moveTo ( canvasX , height ) ;
36- context . lineTo ( canvasX , height - barHeight ) ;
37- }
38- } ) ;
39- context . stroke ( ) ;
16+ function drawAxes ( ctx , width , height , isLogarithmic , xRange ) {
17+ const paddingLeft = 45 ;
18+ const paddingBottom = 45 ; //
19+ const paddingTop = 10 ;
20+
21+ ctx . save ( ) ;
22+ ctx . strokeStyle = style . axisColor ;
23+ ctx . fillStyle = style . textColor ;
24+ ctx . lineWidth = 1 ;
25+ ctx . font = '12px sans-serif' ;
26+ ctx . textAlign = 'center' ;
27+ ctx . textBaseline = 'middle' ;
28+
29+ // ---- Axis lines ----
30+ ctx . beginPath ( ) ;
31+ ctx . moveTo ( paddingLeft , height - paddingBottom ) ;
32+ ctx . lineTo ( width - 10 , height - paddingBottom ) ;
33+ ctx . stroke ( ) ;
34+
35+ ctx . beginPath ( ) ;
36+ ctx . moveTo ( paddingLeft , paddingTop ) ;
37+ ctx . lineTo ( paddingLeft , height - paddingBottom ) ;
38+ ctx . stroke ( ) ;
39+
40+ // ---- Axis Labels ----
41+ ctx . fillText ( 'Frequency (Hz)' , width / 2 , height - 8 ) ;
42+
43+ ctx . save ( ) ;
44+ ctx . translate ( 15 , height / 2 ) ;
45+ ctx . rotate ( - Math . PI / 2 ) ;
46+ ctx . fillText ( 'Amplitude' , 0 , 0 ) ;
47+ ctx . restore ( ) ;
48+
49+ // ---- Frequency (Hz) ----
50+ let xTicks ;
51+ if ( isLogarithmic ) {
52+ const [ minFreq , maxFreq ] = xRange ;
53+ const logMin = Math . log10 ( minFreq ) ;
54+ const logMax = Math . log10 ( maxFreq ) ;
55+ xTicks = [ ] ;
56+ for ( let p = Math . ceil ( logMin ) ; p <= Math . floor ( logMax ) ; p ++ ) {
57+ const base = Math . pow ( 10 , p ) ;
58+ xTicks . push ( base ) ;
59+ if ( base * 2 <= maxFreq ) xTicks . push ( base * 2 ) ;
60+ if ( base * 5 <= maxFreq ) xTicks . push ( base * 5 ) ;
4061 }
41- } , [ freqs , width , height , xRange ] ) ; // Effect for frequency bars
62+ xTicks = xTicks . filter ( v => v >= minFreq && v <= maxFreq ) . sort ( ( a , b ) => a - b ) ;
63+ } else {
64+ const [ min , max ] = xRange ;
65+ const step = ( max - min ) / 5 ;
66+ xTicks = Array . from ( { length : 6 } , ( _ , i ) => min + i * step ) ;
67+ }
68+
69+ // ---- Draw X-axis ticks ----
70+ xTicks . forEach ( tick => {
71+ const x = isLogarithmic
72+ ? ( ( Math . log ( tick ) - Math . log ( xRange [ 0 ] ) ) /
73+ ( Math . log ( xRange [ 1 ] ) - Math . log ( xRange [ 0 ] ) ) ) *
74+ ( width - paddingLeft - 20 ) + paddingLeft
75+ : ( ( tick - xRange [ 0 ] ) / ( xRange [ 1 ] - xRange [ 0 ] ) ) *
76+ ( width - paddingLeft - 20 ) + paddingLeft ;
77+
78+ ctx . beginPath ( ) ;
79+ ctx . moveTo ( x , height - paddingBottom ) ;
80+ ctx . lineTo ( x , height - paddingBottom + 5 ) ;
81+ ctx . stroke ( ) ;
82+
83+ const label = tick >= 1000 ? `${ tick / 1000 } k` : `${ tick . toString ( ) } ` ;
84+ ctx . fillText ( label , x , height - paddingBottom + 15 ) ;
85+ } ) ;
86+
87+ // ---- Y-axis ticks ----
88+ const yTicks = [ - 12 , - 6 , 0 , 6 , 12 ] ; // This seems to be a placeholder, as nodes are 0-1.
89+ const toCanvasY = val => {
90+ const norm = ( val + 12 ) / 24 ; // [-12..12] → [0..1]
91+ return height - paddingBottom - norm * ( height - paddingBottom - paddingTop ) ;
92+ } ;
93+
94+ ctx . textAlign = 'right' ;
95+ yTicks . forEach ( val => {
96+ const y = toCanvasY ( val ) ;
97+ // Tick line
98+ ctx . strokeStyle = style . axisColor ;
99+ ctx . beginPath ( ) ;
100+ ctx . moveTo ( paddingLeft - 5 , y ) ;
101+ ctx . lineTo ( paddingLeft , y ) ;
102+ ctx . stroke ( ) ;
103+ // Label
104+ ctx . fillText ( `${ val } ` , paddingLeft - 8 , y ) ;
105+ // Grid line
106+ ctx . strokeStyle = style . gridColor ;
107+ ctx . beginPath ( ) ;
108+ ctx . moveTo ( paddingLeft , y ) ;
109+ ctx . lineTo ( width - 10 , y ) ;
110+ ctx . stroke ( ) ;
111+ } ) ;
112+
113+ ctx . restore ( ) ;
114+ }
42115
116+ const useCanvasDrawing = ( canvasRef , { wasmModule, width, height, nodes, xRange, curves, freqs, isLogarithmic, onWheel } ) => {
43117 const envelopePath = useMemo ( ( ) => {
44118 if ( ! wasmModule || nodes . length < 2 || ! width || ! height ) {
45119 return null ;
46120 }
47121
48- const logXRange = [ Math . log ( xRange [ 0 ] ) , Math . log ( xRange [ 1 ] ) ] ;
49- const logRange = logXRange [ 1 ] - logXRange [ 0 ] ;
50-
51- const getCanvasPoint = ( node ) => {
52- let canvasX ;
53- if ( isLogarithmic ) {
54- canvasX = node . x <= 0 ? 0 : ( ( Math . log ( node . x ) - logXRange [ 0 ] ) / ( logXRange [ 1 ] - logXRange [ 0 ] ) ) * width ;
55- } else {
56- canvasX = ( ( node . x - xRange [ 0 ] ) / ( xRange [ 1 ] - xRange [ 0 ] ) ) * width ;
57- }
122+ const getCanvasPoint = ( node , logX , xRangeLinear ) => {
123+ const canvasX = isLogarithmic
124+ ? node . x <= 0 ? 0 : ( ( Math . log ( node . x ) - logX . min ) / logX . range ) * width
125+ : ( ( node . x - xRange [ 0 ] ) / xRangeLinear ) * width ;
58126 const canvasY = ( 1 - node . y ) * height ;
59127 return { x : canvasX , y : canvasY } ;
60128 } ;
61129
130+ const logX = { min : Math . log ( xRange [ 0 ] ) , max : Math . log ( xRange [ 1 ] ) , range : Math . log ( xRange [ 1 ] ) - Math . log ( xRange [ 0 ] ) } ;
131+ const xRangeLinear = xRange [ 1 ] - xRange [ 0 ] ;
132+
62133 const path = new Path2D ( ) ;
63- const firstNodePoint = getCanvasPoint ( nodes [ 0 ] ) ;
134+ const firstNodePoint = getCanvasPoint ( nodes [ 0 ] , logX , xRangeLinear ) ;
64135 path . moveTo ( firstNodePoint . x , firstNodePoint . y ) ;
65136
66137 for ( let i = 0 ; i < nodes . length - 1 ; i ++ ) {
67138 const startNode = nodes [ i ] ;
68139 const endNode = nodes [ i + 1 ] ;
69140 const shape = curves [ i ] !== undefined ? curves [ i ] : 0 ;
70-
71141 const logStartX = Math . log ( startNode . x ) ;
72- const logEndX = Math . log ( endNode . x ) ;
73- const logXNodeRange = logEndX - logStartX ;
142+ const logXNodeRange = Math . log ( endNode . x ) - logStartX ;
74143
75- const segments = 100 ;
144+
145+ const segments = 20 ;
76146 for ( let j = 1 ; j <= segments ; j ++ ) {
77147 const t = j / segments ;
78148 const logCurrentX = logStartX + t * logXNodeRange ;
79- const currentX = ( ( logCurrentX - logXRange [ 0 ] ) / logRange ) * width ;
149+ const currentX = ( ( logCurrentX - logX . min ) / logX . range ) * width ;
80150 const linearY = startNode . y + t * ( endNode . y - startNode . y ) ;
81151 const curveOffset = wasmModule . applyShape ( t , shape ) ;
82152 const currentY = ( 1 - ( linearY - curveOffset * t * ( 1 - t ) ) ) * height ;
@@ -86,40 +156,62 @@ const useCanvasDrawing = (canvasRef, { wasmModule, width, height, nodes, xRange,
86156 return path ;
87157 } , [ wasmModule , nodes , curves , width , height , xRange , isLogarithmic ] ) ;
88158
89- useEffect ( ( ) => {
90- if ( ! width || ! height ) return ;
159+ const draw = useCallback ( ( context ) => {
160+ context . fillStyle = style . backgroundColor ;
161+ context . fillRect ( 0 , 0 , width , height ) ;
91162
92- const canvas = canvasRef . current ;
93- const context = canvas . getContext ( '2d' ) ;
163+ drawAxes ( context , width , height , isLogarithmic , xRange ) ;
164+
165+ const logXRange = { min : Math . log ( xRange [ 0 ] ) , max : Math . log ( xRange [ 1 ] ) , range : Math . log ( xRange [ 1 ] ) - Math . log ( xRange [ 0 ] ) } ;
166+
167+ if ( freqs && freqs . length > 0 ) {
168+ context . strokeStyle = style . barColor ;
169+ context . lineWidth = 1 ;
170+ context . beginPath ( ) ;
171+
172+ freqs . forEach ( ( [ freq , amp ] ) => {
173+ const canvasX = freq <= 0 ? 0 : ( ( Math . log ( freq ) - logXRange . min ) / logXRange . range ) * width ;
174+ const barHeight = amp * height ;
175+ if ( canvasX >= 0 && canvasX <= width ) {
176+ context . lineTo ( canvasX , height - barHeight ) ;
177+ }
178+ } ) ;
179+ context . stroke ( ) ;
180+ }
94181
95- // Draw the cached envelope path
96182 if ( envelopePath ) {
97183 context . strokeStyle = style . connectorColor ;
98184 context . lineWidth = style . connectorWidth ;
99185 context . stroke ( envelopePath ) ;
100186 }
101187
102- // Draw nodes (these need to be redrawn as they move)
103188 context . fillStyle = style . nodeColor ;
104- const logXRange = [ Math . log ( xRange [ 0 ] ) , Math . log ( xRange [ 1 ] ) ] ;
105- const logRange = logXRange [ 1 ] - logXRange [ 0 ] ;
106189 nodes . forEach ( node => {
107- const x = node . x <= 0 ? 0 : ( ( Math . log ( node . x ) - logXRange [ 0 ] ) / logRange ) * width ;
190+ const x = node . x <= 0 ? 0 : ( ( Math . log ( node . x ) - logXRange . min ) / logXRange . range ) * width ;
108191 const y = ( 1 - node . y ) * height ;
109192 context . beginPath ( ) ;
110193 context . arc ( x , y , style . nodeRadius , 0 , 2 * Math . PI ) ;
111194 context . fill ( ) ;
112195 } ) ;
196+ } , [ width , height , isLogarithmic , xRange , freqs , nodes , envelopePath ] ) ;
113197
114- if ( onWheel ) {
115- // The wheel event listener must be registered with { passive: false }
116- // to allow calling e.preventDefault().
117- canvas . addEventListener ( 'wheel' , onWheel , { passive : false } ) ;
118- return ( ) => {
119- canvas . removeEventListener ( 'wheel' , onWheel ) ;
120- } ;
121- }
122- } , [ canvasRef , width , height , nodes , xRange , onWheel , envelopePath ] ) ; // Effect for envelope and nodes
198+ useEffect ( ( ) => {
199+ if ( ! width || ! height || ! canvasRef . current ) return ;
200+
201+ const canvas = canvasRef . current ;
202+ const context = canvas . getContext ( '2d' ) ;
203+ let animationFrameId ;
204+
205+ const render = ( ) => {
206+ draw ( context ) ;
207+ animationFrameId = window . requestAnimationFrame ( render ) ;
208+ } ;
209+ render ( ) ;
210+
211+ return ( ) => {
212+ window . cancelAnimationFrame ( animationFrameId ) ;
213+ } ;
214+ } , [ draw , width , height , canvasRef ] ) ;
123215} ;
124216
125217export default useCanvasDrawing ;
0 commit comments