Skip to content

Commit e3c900a

Browse files
refactor grid visualizer
1 parent 7a4a73e commit e3c900a

File tree

3 files changed

+128
-104
lines changed

3 files changed

+128
-104
lines changed

app/ui/_components.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ import {
2323
AudioBarVisualizer,
2424
audioBarVisualizerVariants,
2525
} from '@/components/livekit/audio-visualizer/audio-bar-visualizer/audio-bar-visualizer';
26-
import { AudioGridVisualizer } from '@/components/livekit/audio-visualizer/audio-grid-visualizer/audio-grid-visualizer';
26+
import {
27+
AudioGridVisualizer,
28+
type GridOptions,
29+
} from '@/components/livekit/audio-visualizer/audio-grid-visualizer/audio-grid-visualizer';
2730
import { gridVariants } from '@/components/livekit/audio-visualizer/audio-grid-visualizer/demos';
2831
import { AudioOscilloscopeVisualizer } from '@/components/livekit/audio-visualizer/audio-oscilloscope-visualizer/audio-oscilloscope-visualizer';
2932
import {
@@ -294,7 +297,9 @@ export const COMPONENTS = {
294297
audioTrack={micTrackRef!}
295298
barCount={parseInt(barCount) || undefined}
296299
className="mx-auto"
297-
/>
300+
>
301+
<div className="data-[lk-highlighted=true]:!bg-primary rounded-full" />
302+
</AudioBarVisualizer>
298303
</div>
299304
<details>
300305
<summary className="text-muted-foreground font-mono text-xs uppercase">
@@ -536,7 +541,7 @@ export const COMPONENTS = {
536541
key={`${demoIndex}-${rowCount}-${columnCount}`}
537542
state={state}
538543
audioTrack={micTrackRef!}
539-
options={demoOptions}
544+
{...(demoOptions as GridOptions)}
540545
/>
541546
</div>
542547

components/livekit/audio-visualizer/audio-grid-visualizer/audio-grid-visualizer.tsx

Lines changed: 89 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,148 @@
1-
import { CSSProperties, ComponentType, JSX, memo, useMemo } from 'react';
1+
import { CSSProperties, type ReactNode, memo, useMemo } from 'react';
2+
import { type VariantProps, cva } from 'class-variance-authority';
23
import { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client';
34
import {
45
type AgentState,
56
type TrackReferenceOrPlaceholder,
67
useMultibandTrackVolume,
78
} from '@livekit/components-react';
8-
import { cn } from '@/lib/utils';
9+
import { cloneSingleChild, cn } from '@/lib/utils';
910
import { type Coordinate, useGridAnimator } from './hooks/useGridAnimator';
1011

11-
type GridComponentType =
12-
| ComponentType<{ style?: CSSProperties; className?: string }>
13-
| keyof JSX.IntrinsicElements;
12+
export const audioGridVisualizerVariants = cva(
13+
[
14+
'grid',
15+
'[&_>_*]:size-1 [&_>_*]:rounded-full',
16+
'[&_>_*]:bg-foreground/10 [&_>_[data-lk-highlighted=true]]:bg-foreground [&_>_[data-lk-highlighted=true]]:scale-125 [&_>_[data-lk-highlighted=true]]:shadow-[0px_0px_10px_2px_rgba(255,255,255,0.4)]',
17+
],
18+
{
19+
variants: {
20+
size: {
21+
icon: ['gap-[2px] [&_>_*]:size-[4px]'],
22+
sm: ['gap-[4px] [&_>_*]:size-[4px]'],
23+
md: ['gap-[8px] [&_>_*]:size-[8px]'],
24+
lg: ['gap-[8px] [&_>_*]:size-[8px]'],
25+
xl: ['gap-[8px] [&_>_*]:size-[8px]'],
26+
},
27+
},
28+
defaultVariants: {
29+
size: 'md',
30+
},
31+
}
32+
);
1433

1534
export interface GridOptions {
1635
radius?: number;
1736
interval?: number;
1837
rowCount?: number;
1938
columnCount?: number;
20-
className?: string;
21-
baseClassName?: string;
22-
offClassName?: string;
23-
onClassName?: string;
24-
GridComponent?: GridComponentType;
2539
transformer?: (index: number, rowCount: number, columnCount: number) => CSSProperties;
40+
className?: string;
41+
children?: ReactNode;
2642
}
2743

28-
function useGrid(options: GridOptions) {
44+
const sizeDefaults = {
45+
icon: 3,
46+
sm: 5,
47+
md: 5,
48+
lg: 5,
49+
xl: 5,
50+
};
51+
52+
function useGrid(
53+
size: VariantProps<typeof audioGridVisualizerVariants>['size'] = 'md',
54+
columnCount = sizeDefaults[size as keyof typeof sizeDefaults],
55+
rowCount = sizeDefaults[size as keyof typeof sizeDefaults]
56+
) {
2957
return useMemo(() => {
30-
const { columnCount = 5, rowCount } = options;
31-
3258
const _columnCount = columnCount;
3359
const _rowCount = rowCount ?? columnCount;
3460
const items = new Array(_columnCount * _rowCount).fill(0).map((_, idx) => idx);
3561

3662
return { columnCount: _columnCount, rowCount: _rowCount, items };
37-
}, [options]);
63+
}, [columnCount, rowCount]);
3864
}
3965

4066
interface GridCellProps {
4167
index: number;
4268
state: AgentState;
43-
options: GridOptions;
69+
interval: number;
70+
transformer?: (index: number, rowCount: number, columnCount: number) => CSSProperties;
4471
rowCount: number;
45-
volumeBands: number[];
4672
columnCount: number;
73+
volumeBands: number[];
4774
highlightedCoordinate: Coordinate;
48-
Component: GridComponentType;
75+
children: ReactNode;
4976
}
5077

5178
const GridCell = memo(function GridCell({
5279
index,
5380
state,
54-
options,
81+
interval,
82+
transformer,
5583
rowCount,
56-
volumeBands,
5784
columnCount,
85+
volumeBands,
5886
highlightedCoordinate,
59-
Component,
87+
children,
6088
}: GridCellProps) {
61-
const { interval = 100, baseClassName, onClassName, offClassName, transformer } = options;
62-
6389
if (state === 'speaking') {
6490
const y = Math.floor(index / columnCount);
6591
const rowMidPoint = Math.floor(rowCount / 2);
6692
const volumeChunks = 1 / (rowMidPoint + 1);
6793
const distanceToMid = Math.abs(rowMidPoint - y);
6894
const threshold = distanceToMid * volumeChunks;
69-
const isOn = volumeBands[index % columnCount] >= threshold;
95+
const isHighlighted = volumeBands[index % columnCount] >= threshold;
7096

71-
return <Component className={cn(baseClassName, isOn ? onClassName : offClassName)} />;
97+
return cloneSingleChild(children, {
98+
'data-lk-index': index,
99+
'data-lk-highlighted': isHighlighted,
100+
});
72101
}
73102

74103
let transformerStyle: CSSProperties | undefined;
75104
if (transformer) {
76105
transformerStyle = transformer(index, rowCount, columnCount);
77106
}
78107

79-
const isOn =
108+
const isHighlighted =
80109
highlightedCoordinate.x === index % columnCount &&
81110
highlightedCoordinate.y === Math.floor(index / columnCount);
82111

83-
const transitionDurationInSeconds = interval / (isOn ? 1000 : 100);
84-
85-
return (
86-
<Component
87-
style={{
88-
transitionProperty: 'all',
89-
transitionDuration: `${transitionDurationInSeconds}s`,
90-
transitionTimingFunction: 'ease-out',
91-
...transformerStyle,
92-
}}
93-
className={cn(baseClassName, isOn ? onClassName : offClassName)}
94-
/>
95-
);
112+
const transitionDurationInSeconds = interval / (isHighlighted ? 1000 : 100);
113+
114+
return cloneSingleChild(children, {
115+
'data-lk-index': index,
116+
'data-lk-highlighted': isHighlighted,
117+
style: {
118+
transitionProperty: 'all',
119+
transitionDuration: `${transitionDurationInSeconds}s`,
120+
transitionTimingFunction: 'ease-out',
121+
...transformerStyle,
122+
},
123+
});
96124
});
97125

98-
export interface AudioGridVisualizerProps {
126+
export type AudioGridVisualizerProps = GridOptions & {
99127
state: AgentState;
100-
options: GridOptions;
101128
audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;
102-
}
129+
className?: string;
130+
children?: ReactNode;
131+
} & VariantProps<typeof audioGridVisualizerVariants>;
103132

104-
export function AudioGridVisualizer({ state, options, audioTrack }: AudioGridVisualizerProps) {
105-
const { radius, interval = 100, className, GridComponent = 'div' } = options;
106-
const { columnCount, rowCount, items } = useGrid(options);
133+
export function AudioGridVisualizer({
134+
size = 'md',
135+
state,
136+
radius,
137+
rowCount: _rowCount = 5,
138+
columnCount: _columnCount = 5,
139+
transformer,
140+
interval = 100,
141+
className,
142+
children,
143+
audioTrack,
144+
}: AudioGridVisualizerProps) {
145+
const { columnCount, rowCount, items } = useGrid(size, _columnCount, _rowCount);
107146
const highlightedCoordinate = useGridAnimator(state, rowCount, columnCount, interval, radius);
108147
const volumeBands = useMultibandTrackVolume(audioTrack, {
109148
bands: columnCount,
@@ -113,21 +152,23 @@ export function AudioGridVisualizer({ state, options, audioTrack }: AudioGridVis
113152

114153
return (
115154
<div
116-
className={cn('grid gap-1', className)}
155+
className={cn(audioGridVisualizerVariants({ size }), className)}
117156
style={{ gridTemplateColumns: `repeat(${columnCount}, 1fr)` }}
118157
>
119158
{items.map((idx) => (
120159
<GridCell
121160
key={idx}
122161
index={idx}
123162
state={state}
124-
options={options}
163+
interval={interval}
164+
transformer={transformer}
125165
rowCount={rowCount}
126166
columnCount={columnCount}
127167
volumeBands={volumeBands}
128168
highlightedCoordinate={highlightedCoordinate}
129-
Component={GridComponent}
130-
/>
169+
>
170+
{children ?? <div />}
171+
</GridCell>
131172
))}
132173
</div>
133174
);

0 commit comments

Comments
 (0)