stream-chat-react-native-core 9.10.2 → 9.11.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/ai-components/README.md +50 -2
- package/lib/commonjs/ai-components/index.js +24 -0
- package/lib/commonjs/ai-components/index.js.map +1 -1
- package/lib/commonjs/ai-components/message-composer/ComposerView.js +4 -2
- package/lib/commonjs/ai-components/message-composer/ComposerView.js.map +1 -1
- package/lib/commonjs/ai-components/message-composer/SpeechToTextButton.js +4 -1
- package/lib/commonjs/ai-components/message-composer/SpeechToTextButton.js.map +1 -1
- package/lib/commonjs/ai-components/native/AIDictation.js +6 -0
- package/lib/commonjs/ai-components/native/AIDictation.js.map +1 -1
- package/lib/commonjs/ai-components/store/dictation/store.js +4 -2
- package/lib/commonjs/ai-components/store/dictation/store.js.map +1 -1
- package/lib/commonjs/ai-components/store/dictation/useDictationState.js +4 -2
- package/lib/commonjs/ai-components/store/dictation/useDictationState.js.map +1 -1
- package/lib/commonjs/ai-components/store/index.js +22 -0
- package/lib/commonjs/ai-components/store/index.js.map +1 -1
- package/lib/commonjs/ai-components/theme.js +3 -0
- package/lib/commonjs/ai-components/theme.js.map +1 -1
- package/lib/commonjs/ai-components/transcription/useDictation.js +7 -3
- package/lib/commonjs/ai-components/transcription/useDictation.js.map +1 -1
- package/lib/commonjs/ai-components/transcription/useDictationAudioLevel.js +32 -0
- package/lib/commonjs/ai-components/transcription/useDictationAudioLevel.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceGlow.js +79 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceGlow.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceVisualizationView.js +147 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceVisualizationView.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceWave.js +58 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceWave.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/bounce.js +60 -0
- package/lib/commonjs/ai-components/voice-visualization/bounce.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/driver.js +208 -0
- package/lib/commonjs/ai-components/voice-visualization/driver.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/glowShader.js +218 -0
- package/lib/commonjs/ai-components/voice-visualization/glowShader.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/index.js +18 -0
- package/lib/commonjs/ai-components/voice-visualization/index.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/math.js +70 -0
- package/lib/commonjs/ai-components/voice-visualization/math.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/palette.js +47 -0
- package/lib/commonjs/ai-components/voice-visualization/palette.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/simplex.js +77 -0
- package/lib/commonjs/ai-components/voice-visualization/simplex.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/styles.js +33 -0
- package/lib/commonjs/ai-components/voice-visualization/styles.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/types.js +4 -0
- package/lib/commonjs/ai-components/voice-visualization/types.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/useReduceMotion.js +28 -0
- package/lib/commonjs/ai-components/voice-visualization/useReduceMotion.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/useVoiceVisualization.js +129 -0
- package/lib/commonjs/ai-components/voice-visualization/useVoiceVisualization.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/waveGeometry.js +124 -0
- package/lib/commonjs/ai-components/voice-visualization/waveGeometry.js.map +1 -0
- package/lib/commonjs/components/MessageInput/MessageComposer.js +2 -1
- package/lib/commonjs/components/MessageInput/MessageComposer.js.map +1 -1
- package/lib/commonjs/version.json +1 -1
- package/lib/module/ai-components/README.md +50 -2
- package/lib/module/ai-components/index.js +24 -0
- package/lib/module/ai-components/index.js.map +1 -1
- package/lib/module/ai-components/message-composer/ComposerView.js +4 -2
- package/lib/module/ai-components/message-composer/ComposerView.js.map +1 -1
- package/lib/module/ai-components/message-composer/SpeechToTextButton.js +4 -1
- package/lib/module/ai-components/message-composer/SpeechToTextButton.js.map +1 -1
- package/lib/module/ai-components/native/AIDictation.js +6 -0
- package/lib/module/ai-components/native/AIDictation.js.map +1 -1
- package/lib/module/ai-components/store/dictation/store.js +4 -2
- package/lib/module/ai-components/store/dictation/store.js.map +1 -1
- package/lib/module/ai-components/store/dictation/useDictationState.js +4 -2
- package/lib/module/ai-components/store/dictation/useDictationState.js.map +1 -1
- package/lib/module/ai-components/store/index.js +22 -0
- package/lib/module/ai-components/store/index.js.map +1 -1
- package/lib/module/ai-components/theme.js +3 -0
- package/lib/module/ai-components/theme.js.map +1 -1
- package/lib/module/ai-components/transcription/useDictation.js +7 -3
- package/lib/module/ai-components/transcription/useDictation.js.map +1 -1
- package/lib/module/ai-components/transcription/useDictationAudioLevel.js +32 -0
- package/lib/module/ai-components/transcription/useDictationAudioLevel.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/VoiceGlow.js +79 -0
- package/lib/module/ai-components/voice-visualization/VoiceGlow.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js +147 -0
- package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/VoiceWave.js +58 -0
- package/lib/module/ai-components/voice-visualization/VoiceWave.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/bounce.js +60 -0
- package/lib/module/ai-components/voice-visualization/bounce.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/driver.js +208 -0
- package/lib/module/ai-components/voice-visualization/driver.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/glowShader.js +218 -0
- package/lib/module/ai-components/voice-visualization/glowShader.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/index.js +18 -0
- package/lib/module/ai-components/voice-visualization/index.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/math.js +70 -0
- package/lib/module/ai-components/voice-visualization/math.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/palette.js +47 -0
- package/lib/module/ai-components/voice-visualization/palette.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/simplex.js +77 -0
- package/lib/module/ai-components/voice-visualization/simplex.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/styles.js +33 -0
- package/lib/module/ai-components/voice-visualization/styles.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/types.js +4 -0
- package/lib/module/ai-components/voice-visualization/types.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/useReduceMotion.js +28 -0
- package/lib/module/ai-components/voice-visualization/useReduceMotion.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js +129 -0
- package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/waveGeometry.js +124 -0
- package/lib/module/ai-components/voice-visualization/waveGeometry.js.map +1 -0
- package/lib/module/components/MessageInput/MessageComposer.js +2 -1
- package/lib/module/components/MessageInput/MessageComposer.js.map +1 -1
- package/lib/module/version.json +1 -1
- package/lib/typescript/ai-components/index.d.ts +3 -1
- package/lib/typescript/ai-components/index.d.ts.map +1 -1
- package/lib/typescript/ai-components/message-composer/ComposerView.d.ts.map +1 -1
- package/lib/typescript/ai-components/native/AIDictation.d.ts +6 -0
- package/lib/typescript/ai-components/native/AIDictation.d.ts.map +1 -1
- package/lib/typescript/ai-components/store/dictation/store.d.ts +7 -0
- package/lib/typescript/ai-components/store/dictation/store.d.ts.map +1 -1
- package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts +1 -0
- package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts.map +1 -1
- package/lib/typescript/ai-components/store/index.d.ts +2 -0
- package/lib/typescript/ai-components/store/index.d.ts.map +1 -1
- package/lib/typescript/ai-components/theme.d.ts +13 -0
- package/lib/typescript/ai-components/theme.d.ts.map +1 -1
- package/lib/typescript/ai-components/transcription/useDictation.d.ts +9 -1
- package/lib/typescript/ai-components/transcription/useDictation.d.ts.map +1 -1
- package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts +11 -0
- package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts +13 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts +26 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts +13 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/bounce.d.ts +20 -0
- package/lib/typescript/ai-components/voice-visualization/bounce.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/driver.d.ts +96 -0
- package/lib/typescript/ai-components/voice-visualization/driver.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts +7 -0
- package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/index.d.ts +4 -0
- package/lib/typescript/ai-components/voice-visualization/index.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/math.d.ts +26 -0
- package/lib/typescript/ai-components/voice-visualization/math.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/palette.d.ts +18 -0
- package/lib/typescript/ai-components/voice-visualization/palette.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/simplex.d.ts +11 -0
- package/lib/typescript/ai-components/voice-visualization/simplex.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/styles.d.ts +24 -0
- package/lib/typescript/ai-components/voice-visualization/styles.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/types.d.ts +45 -0
- package/lib/typescript/ai-components/voice-visualization/types.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts +10 -0
- package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts +18 -0
- package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts +25 -0
- package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts.map +1 -0
- package/lib/typescript/components/MessageInput/MessageComposer.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ai-components/README.md +50 -2
- package/src/ai-components/__tests__/VoiceVisualization.test.tsx +208 -0
- package/src/ai-components/__tests__/dictation.test.tsx +58 -1
- package/src/ai-components/__tests__/voiceDriver.test.ts +317 -0
- package/src/ai-components/__tests__/voicePalette.test.ts +124 -0
- package/src/ai-components/index.ts +3 -1
- package/src/ai-components/message-composer/ComposerView.tsx +6 -3
- package/src/ai-components/message-composer/SpeechToTextButton.tsx +5 -2
- package/src/ai-components/native/AIDictation.ts +11 -0
- package/src/ai-components/store/dictation/store.ts +9 -0
- package/src/ai-components/store/dictation/useDictationState.ts +2 -1
- package/src/ai-components/store/index.ts +2 -0
- package/src/ai-components/theme.ts +16 -0
- package/src/ai-components/transcription/useDictation.ts +16 -3
- package/src/ai-components/transcription/useDictationAudioLevel.ts +38 -0
- package/src/ai-components/voice-visualization/VoiceGlow.tsx +79 -0
- package/src/ai-components/voice-visualization/VoiceVisualizationView.tsx +179 -0
- package/src/ai-components/voice-visualization/VoiceWave.tsx +67 -0
- package/src/ai-components/voice-visualization/bounce.ts +61 -0
- package/src/ai-components/voice-visualization/driver.ts +240 -0
- package/src/ai-components/voice-visualization/glowShader.ts +227 -0
- package/src/ai-components/voice-visualization/index.ts +3 -0
- package/src/ai-components/voice-visualization/math.ts +73 -0
- package/src/ai-components/voice-visualization/palette.ts +59 -0
- package/src/ai-components/voice-visualization/simplex.ts +92 -0
- package/src/ai-components/voice-visualization/styles.ts +42 -0
- package/src/ai-components/voice-visualization/types.ts +44 -0
- package/src/ai-components/voice-visualization/useReduceMotion.ts +34 -0
- package/src/ai-components/voice-visualization/useVoiceVisualization.ts +153 -0
- package/src/ai-components/voice-visualization/waveGeometry.ts +137 -0
- package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewPollLabel.test.tsx +1 -1
- package/src/components/MessageInput/MessageComposer.tsx +4 -2
- package/src/version.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { type SharedValue, useSharedValue } from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
import { AIDictation } from '../native/AIDictation';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The microphone's live level while dictation listens, from 0 to 1, for the `audioLevel` of
|
|
9
|
+
* `VoiceWave` and `VoiceGlow`. It is `null` while dictation is not listening, which lets a
|
|
10
|
+
* visualization animate speech on its own.
|
|
11
|
+
*
|
|
12
|
+
* The level is a shared value rather than state, so a level changing many times a second
|
|
13
|
+
* re-renders nothing: the visualizations read it once a frame on the UI thread.
|
|
14
|
+
*/
|
|
15
|
+
export const useDictationAudioLevel = (): SharedValue<number | null> => {
|
|
16
|
+
const level = useSharedValue<number | null>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const unsubscribeLevel = AIDictation.addLevelListener((payload) => {
|
|
20
|
+
level.value = typeof payload?.level === 'number' ? payload.level : null;
|
|
21
|
+
});
|
|
22
|
+
const unsubscribeState = AIDictation.addStateListener(({ state }) => {
|
|
23
|
+
if (state !== 'listening' && state !== 'starting') level.value = null;
|
|
24
|
+
});
|
|
25
|
+
const unsubscribeError = AIDictation.addErrorListener(() => {
|
|
26
|
+
level.value = null;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
unsubscribeLevel();
|
|
31
|
+
unsubscribeState();
|
|
32
|
+
unsubscribeError();
|
|
33
|
+
level.value = null;
|
|
34
|
+
};
|
|
35
|
+
}, [level]);
|
|
36
|
+
|
|
37
|
+
return level;
|
|
38
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { useDerivedValue } from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
import { Canvas, Fill, Shader } from '@shopify/react-native-skia';
|
|
6
|
+
|
|
7
|
+
import { driverReact, driverScale } from './driver';
|
|
8
|
+
import { getGlowEffect } from './glowShader';
|
|
9
|
+
import { GLOW_STYLE } from './styles';
|
|
10
|
+
import type { VoiceVisualizationProps } from './types';
|
|
11
|
+
import type { VoiceVisualization } from './useVoiceVisualization';
|
|
12
|
+
import { type VoiceCanvasSize, VoiceVisualizationView } from './VoiceVisualizationView';
|
|
13
|
+
|
|
14
|
+
export type VoiceGlowProps = VoiceVisualizationProps;
|
|
15
|
+
|
|
16
|
+
const GlowCanvas = ({
|
|
17
|
+
size: { height, width },
|
|
18
|
+
visualization: { dark, driver },
|
|
19
|
+
}: {
|
|
20
|
+
size: VoiceCanvasSize;
|
|
21
|
+
visualization: VoiceVisualization;
|
|
22
|
+
}) => {
|
|
23
|
+
const effect = getGlowEffect();
|
|
24
|
+
const uniforms = useDerivedValue(() => {
|
|
25
|
+
const d = driver.value;
|
|
26
|
+
const color = (i: number) => [d.colors[i].hue, d.colors[i].saturation, d.colors[i].value, 0];
|
|
27
|
+
return {
|
|
28
|
+
bright: d.bright,
|
|
29
|
+
col0: color(0),
|
|
30
|
+
col1: color(1),
|
|
31
|
+
col2: color(2),
|
|
32
|
+
col3: color(3),
|
|
33
|
+
col4: color(4),
|
|
34
|
+
count: d.count,
|
|
35
|
+
dark: dark.value,
|
|
36
|
+
flow: d.flow,
|
|
37
|
+
flowSpin: d.flowSpin,
|
|
38
|
+
hover: [d.hoverX, d.hoverY],
|
|
39
|
+
hoverAmt: d.hoverAmount,
|
|
40
|
+
load: d.load,
|
|
41
|
+
mic: d.mic,
|
|
42
|
+
orbSpin: d.orbSpin,
|
|
43
|
+
react: driverReact(d),
|
|
44
|
+
resolution: [width, height],
|
|
45
|
+
sat: d.sat,
|
|
46
|
+
scale: driverScale(d),
|
|
47
|
+
tap: [d.tapX, d.tapY],
|
|
48
|
+
tapTime: d.tapAge,
|
|
49
|
+
time: d.time,
|
|
50
|
+
voice: d.voice,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!effect) return null;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Canvas style={{ height, width }}>
|
|
58
|
+
<Fill>
|
|
59
|
+
<Shader source={effect} uniforms={uniforms} />
|
|
60
|
+
</Fill>
|
|
61
|
+
</Canvas>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const renderGlow = (visualization: VoiceVisualization, size: VoiceCanvasSize) => (
|
|
66
|
+
<GlowCanvas size={size} visualization={visualization} />
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A voice agent as a soft, airy globe: color masses orbit inside it and swell with speech, and the
|
|
71
|
+
* gaps between them let the background show through. Tapping the globe bounces it and ripples it
|
|
72
|
+
* from the touch, and so does a hovering mouse or pen.
|
|
73
|
+
*
|
|
74
|
+
* It has no size of its own and draws in the middle of the view it is given, about half as wide
|
|
75
|
+
* as its shorter side, so give it a square.
|
|
76
|
+
*/
|
|
77
|
+
export const VoiceGlow = (props: VoiceGlowProps) => (
|
|
78
|
+
<VoiceVisualizationView {...props} renderCanvas={renderGlow} visualStyle={GLOW_STYLE} />
|
|
79
|
+
);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import React, { type ReactNode, useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type GestureResponderEvent,
|
|
4
|
+
type LayoutChangeEvent,
|
|
5
|
+
PixelRatio,
|
|
6
|
+
type PointerEvent,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
View,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
import Animated from 'react-native-reanimated';
|
|
12
|
+
|
|
13
|
+
import type { VoiceVisualizationStyle } from './styles';
|
|
14
|
+
import type { VoiceAgentState, VoiceVisualizationProps } from './types';
|
|
15
|
+
import { useVoiceVisualization, type VoiceVisualization } from './useVoiceVisualization';
|
|
16
|
+
|
|
17
|
+
/** What the renderer draws in: the canvas size, in its own units, and the screen's pixels per unit. */
|
|
18
|
+
export type VoiceCanvasSize = { height: number; pixelRatio: number; width: number };
|
|
19
|
+
|
|
20
|
+
type ViewProps = VoiceVisualizationProps & {
|
|
21
|
+
/** Draws the visualization into a canvas of the given size. */
|
|
22
|
+
renderCanvas: (visualization: VoiceVisualization, size: VoiceCanvasSize) => ReactNode;
|
|
23
|
+
visualStyle: VoiceVisualizationStyle;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEFAULT_STATE_LABELS: Record<VoiceAgentState, string> = {
|
|
27
|
+
connecting: 'Connecting',
|
|
28
|
+
idle: 'Ready',
|
|
29
|
+
listening: 'Listening',
|
|
30
|
+
speaking: 'Speaking',
|
|
31
|
+
thinking: 'Thinking',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What VoiceWave and VoiceGlow share: the motion, the canvas kept at the view's size and at most
|
|
36
|
+
* the style's density, reduced motion, taps and hover, and accessibility.
|
|
37
|
+
*
|
|
38
|
+
* It names itself "Voice agent" with its state as the value, as on iOS and Android. Touches are
|
|
39
|
+
* only watched, never claimed, so a press handler on the view or a parent still gets them, and
|
|
40
|
+
* only the shape takes them: around it they reach whatever is behind.
|
|
41
|
+
*/
|
|
42
|
+
export const VoiceVisualizationView = ({
|
|
43
|
+
accessibilityLabel = 'Voice agent',
|
|
44
|
+
accessibilityValue,
|
|
45
|
+
audioLevel,
|
|
46
|
+
colors,
|
|
47
|
+
expressivity,
|
|
48
|
+
onLayout,
|
|
49
|
+
pointerEvents = 'box-none',
|
|
50
|
+
renderCanvas,
|
|
51
|
+
state,
|
|
52
|
+
stateLabels,
|
|
53
|
+
style,
|
|
54
|
+
visualStyle,
|
|
55
|
+
...restProps
|
|
56
|
+
}: ViewProps) => {
|
|
57
|
+
const visualization = useVoiceVisualization(
|
|
58
|
+
{ audioLevel, colors, expressivity, state },
|
|
59
|
+
visualStyle,
|
|
60
|
+
);
|
|
61
|
+
const { hover, opacity, tap } = visualization;
|
|
62
|
+
const [layout, setLayout] = useState({ height: 0, width: 0 });
|
|
63
|
+
|
|
64
|
+
const handleLayout = useCallback(
|
|
65
|
+
(event: LayoutChangeEvent) => {
|
|
66
|
+
onLayout?.(event);
|
|
67
|
+
const { height, width } = event.nativeEvent.layout;
|
|
68
|
+
setLayout((current) =>
|
|
69
|
+
current.width === width && current.height === height ? current : { height, width },
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
[onLayout],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const side = Math.min(layout.width, layout.height);
|
|
76
|
+
const pixelRatio = PixelRatio.get();
|
|
77
|
+
// Drawn at no more than the style's density, then scaled up to the view: the glow's soft
|
|
78
|
+
// gradients look the same at 2x with fewer pixels to fill, while the wave's line needs 3x.
|
|
79
|
+
const density = Math.min(1, visualStyle.maxScale / pixelRatio);
|
|
80
|
+
const canvasSize = useMemo<VoiceCanvasSize>(
|
|
81
|
+
() => ({
|
|
82
|
+
height: layout.height * density,
|
|
83
|
+
pixelRatio: pixelRatio * density,
|
|
84
|
+
width: layout.width * density,
|
|
85
|
+
}),
|
|
86
|
+
[density, layout.height, layout.width, pixelRatio],
|
|
87
|
+
);
|
|
88
|
+
const hitWidth = visualStyle.hitArea.width * side;
|
|
89
|
+
const hitHeight = visualStyle.hitArea.height * side;
|
|
90
|
+
|
|
91
|
+
/** A touch on the hit area, centred on the view and normalized to its shorter side, y up. */
|
|
92
|
+
const toLocalPoint = useCallback(
|
|
93
|
+
(locationX: number, locationY: number) => ({
|
|
94
|
+
x: (locationX - hitWidth / 2) / (side || 1),
|
|
95
|
+
y: (hitHeight / 2 - locationY) / (side || 1),
|
|
96
|
+
}),
|
|
97
|
+
[hitHeight, hitWidth, side],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const handleTouchEnd = useCallback(
|
|
101
|
+
(event: GestureResponderEvent) => {
|
|
102
|
+
const { x, y } = toLocalPoint(event.nativeEvent.locationX, event.nativeEvent.locationY);
|
|
103
|
+
if (visualStyle.contains(x, y)) tap(x, y);
|
|
104
|
+
},
|
|
105
|
+
[tap, toLocalPoint, visualStyle],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const handlePointerMove = useCallback(
|
|
109
|
+
(event: PointerEvent) => {
|
|
110
|
+
// Only a hovering mouse or pen ripples it; a finger has no hover.
|
|
111
|
+
const { buttons, offsetX, offsetY, pointerType } = event.nativeEvent;
|
|
112
|
+
if (pointerType === 'touch') return;
|
|
113
|
+
const { x, y } = toLocalPoint(offsetX, offsetY);
|
|
114
|
+
const hovering = buttons === 0 && visualStyle.contains(x, y);
|
|
115
|
+
hover(hovering ? x : null, hovering ? y : null);
|
|
116
|
+
},
|
|
117
|
+
[hover, toLocalPoint, visualStyle],
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const handlePointerLeave = useCallback(() => hover(null, null), [hover]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<View
|
|
124
|
+
accessibilityLabel={accessibilityLabel}
|
|
125
|
+
accessibilityRole='image'
|
|
126
|
+
accessibilityValue={
|
|
127
|
+
accessibilityValue ?? { text: stateLabels?.[state] ?? DEFAULT_STATE_LABELS[state] }
|
|
128
|
+
}
|
|
129
|
+
accessible
|
|
130
|
+
onLayout={handleLayout}
|
|
131
|
+
pointerEvents={pointerEvents}
|
|
132
|
+
style={style}
|
|
133
|
+
{...restProps}
|
|
134
|
+
>
|
|
135
|
+
{side > 0 ? (
|
|
136
|
+
<>
|
|
137
|
+
<Animated.View
|
|
138
|
+
pointerEvents='none'
|
|
139
|
+
style={[
|
|
140
|
+
styles.canvas,
|
|
141
|
+
{
|
|
142
|
+
height: canvasSize.height,
|
|
143
|
+
left: (layout.width - canvasSize.width) / 2,
|
|
144
|
+
opacity,
|
|
145
|
+
top: (layout.height - canvasSize.height) / 2,
|
|
146
|
+
transform: [{ scale: 1 / density }],
|
|
147
|
+
width: canvasSize.width,
|
|
148
|
+
},
|
|
149
|
+
]}
|
|
150
|
+
>
|
|
151
|
+
{renderCanvas(visualization, canvasSize)}
|
|
152
|
+
</Animated.View>
|
|
153
|
+
<View
|
|
154
|
+
// A transparent view with only handlers could be flattened away, leaving no target.
|
|
155
|
+
collapsable={false}
|
|
156
|
+
onPointerLeave={handlePointerLeave}
|
|
157
|
+
onPointerMove={handlePointerMove}
|
|
158
|
+
onTouchEnd={handleTouchEnd}
|
|
159
|
+
style={[
|
|
160
|
+
styles.hitArea,
|
|
161
|
+
{
|
|
162
|
+
height: hitHeight,
|
|
163
|
+
left: (layout.width - hitWidth) / 2,
|
|
164
|
+
top: (layout.height - hitHeight) / 2,
|
|
165
|
+
width: hitWidth,
|
|
166
|
+
},
|
|
167
|
+
]}
|
|
168
|
+
testID='voice-visualization-hit-area'
|
|
169
|
+
/>
|
|
170
|
+
</>
|
|
171
|
+
) : null}
|
|
172
|
+
</View>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const styles = StyleSheet.create({
|
|
177
|
+
canvas: { position: 'absolute' },
|
|
178
|
+
hitArea: { position: 'absolute' },
|
|
179
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { useDerivedValue } from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
import { Canvas, LinearGradient, Path, vec } from '@shopify/react-native-skia';
|
|
6
|
+
|
|
7
|
+
import { WAVE_STYLE } from './styles';
|
|
8
|
+
import type { VoiceVisualizationProps } from './types';
|
|
9
|
+
import type { VoiceVisualization } from './useVoiceVisualization';
|
|
10
|
+
import { type VoiceCanvasSize, VoiceVisualizationView } from './VoiceVisualizationView';
|
|
11
|
+
import {
|
|
12
|
+
WAVE_END,
|
|
13
|
+
WAVE_POSITIONS,
|
|
14
|
+
WAVE_START,
|
|
15
|
+
waveColors,
|
|
16
|
+
wavePath,
|
|
17
|
+
waveStrokeWidth,
|
|
18
|
+
} from './waveGeometry';
|
|
19
|
+
|
|
20
|
+
export type VoiceWaveProps = VoiceVisualizationProps;
|
|
21
|
+
|
|
22
|
+
const WaveCanvas = ({
|
|
23
|
+
size: { height, pixelRatio, width },
|
|
24
|
+
visualization: { dark, driver },
|
|
25
|
+
}: {
|
|
26
|
+
size: VoiceCanvasSize;
|
|
27
|
+
visualization: VoiceVisualization;
|
|
28
|
+
}) => {
|
|
29
|
+
const path = useDerivedValue(() => wavePath(driver.value, width, height));
|
|
30
|
+
const colors = useDerivedValue(() => waveColors(driver.value, dark.value));
|
|
31
|
+
const m = Math.min(width, height);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Canvas style={{ height, width }}>
|
|
35
|
+
<Path
|
|
36
|
+
path={path}
|
|
37
|
+
strokeCap='round'
|
|
38
|
+
strokeJoin='round'
|
|
39
|
+
strokeWidth={waveStrokeWidth(width, height, pixelRatio)}
|
|
40
|
+
style='stroke'
|
|
41
|
+
>
|
|
42
|
+
<LinearGradient
|
|
43
|
+
colors={colors}
|
|
44
|
+
end={vec(width / 2 + WAVE_END * m, 0)}
|
|
45
|
+
positions={WAVE_POSITIONS}
|
|
46
|
+
start={vec(width / 2 + WAVE_START * m, 0)}
|
|
47
|
+
/>
|
|
48
|
+
</Path>
|
|
49
|
+
</Canvas>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const renderWave = (visualization: VoiceVisualization, size: VoiceCanvasSize) => (
|
|
54
|
+
<WaveCanvas size={size} visualization={visualization} />
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A voice agent as a single string, flat at its ends, carrying a wave packet that erupts with
|
|
59
|
+
* speech while its colors flow along it. Tapping the string ripples it from the touch, and so does
|
|
60
|
+
* a hovering mouse or pen.
|
|
61
|
+
*
|
|
62
|
+
* It has no size of its own and draws in the middle of the view it is given, about 80% as wide as
|
|
63
|
+
* its shorter side, so give it a square or a wide box.
|
|
64
|
+
*/
|
|
65
|
+
export const VoiceWave = (props: VoiceWaveProps) => (
|
|
66
|
+
<VoiceVisualizationView {...props} renderCanvas={renderWave} visualStyle={WAVE_STYLE} />
|
|
67
|
+
);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounce of a tap: a dip to 93% and an overshoot to 103% before settling, over 340 ms.
|
|
3
|
+
*
|
|
4
|
+
* These are authored keyframes rather than a spring: the ease-out curve times the whole run and
|
|
5
|
+
* the keyframes are linear between their stops.
|
|
6
|
+
*/
|
|
7
|
+
export const BOUNCE_DURATION = 0.34;
|
|
8
|
+
|
|
9
|
+
const OFFSETS = [0, 0.3, 0.62, 1];
|
|
10
|
+
const SCALES = [1, 0.93, 1.03, 1];
|
|
11
|
+
|
|
12
|
+
/** How long ago the last bounce started, in seconds; infinite before the first. */
|
|
13
|
+
export type BounceState = { age: number };
|
|
14
|
+
|
|
15
|
+
export const createBounce = (): BounceState => ({ age: Number.POSITIVE_INFINITY });
|
|
16
|
+
|
|
17
|
+
export const startBounce = (bounce: BounceState) => {
|
|
18
|
+
'worklet';
|
|
19
|
+
bounce.age = 0;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const advanceBounce = (bounce: BounceState, seconds: number) => {
|
|
23
|
+
'worklet';
|
|
24
|
+
bounce.age += seconds;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** CSS `ease-out`, `cubic-bezier(0, 0, 0.58, 1)`. */
|
|
28
|
+
export const easeOut = (x: number) => {
|
|
29
|
+
'worklet';
|
|
30
|
+
const bx = 3 * 0.58;
|
|
31
|
+
const ax = 1 - bx;
|
|
32
|
+
const by = 3;
|
|
33
|
+
const ay = 1 - by;
|
|
34
|
+
let low = 0;
|
|
35
|
+
let high = 1;
|
|
36
|
+
for (let i = 0; i < 24; i++) {
|
|
37
|
+
const middle = (low + high) / 2;
|
|
38
|
+
if ((ax * middle + bx) * middle * middle < x) low = middle;
|
|
39
|
+
else high = middle;
|
|
40
|
+
}
|
|
41
|
+
const t = (low + high) / 2;
|
|
42
|
+
return (ay * t + by) * t * t;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The keyframed scale at `progress`, from 0 to 1. */
|
|
46
|
+
export const scaleAt = (progress: number) => {
|
|
47
|
+
'worklet';
|
|
48
|
+
for (let i = 1; i < OFFSETS.length; i++) {
|
|
49
|
+
if (progress <= OFFSETS[i]) {
|
|
50
|
+
const fraction = (progress - OFFSETS[i - 1]) / (OFFSETS[i] - OFFSETS[i - 1]);
|
|
51
|
+
return SCALES[i - 1] + (SCALES[i] - SCALES[i - 1]) * fraction;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return 1;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const bounceScale = (bounce: BounceState) => {
|
|
58
|
+
'worklet';
|
|
59
|
+
if (bounce.age >= BOUNCE_DURATION) return 1;
|
|
60
|
+
return scaleAt(easeOut(Math.max(bounce.age, 0) / BOUNCE_DURATION));
|
|
61
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { advanceBounce, type BounceState, bounceScale, createBounce, startBounce } from './bounce';
|
|
2
|
+
import { MAXIMUM_COLORS } from './palette';
|
|
3
|
+
import type { Hsv, VoiceAgentState } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The share of the way the live level moves each frame at 60 Hz, toward a louder or a quieter
|
|
7
|
+
* level.
|
|
8
|
+
*/
|
|
9
|
+
export type LevelResponse = { rise: number; fall: number };
|
|
10
|
+
|
|
11
|
+
export const LEVEL_RESPONSE = {
|
|
12
|
+
/** Follows the level closely, both ways. The glow's. */
|
|
13
|
+
steady: { fall: 0.35, rise: 0.35 },
|
|
14
|
+
/**
|
|
15
|
+
* Follows the level like a VU meter: about 0.1 s up to a louder reading, about 0.3 s down from
|
|
16
|
+
* it, so a meter's per-syllable jumps swell with the phrase instead. The wave's.
|
|
17
|
+
*/
|
|
18
|
+
smooth: { fall: 0.05, rise: 0.16 },
|
|
19
|
+
} satisfies Record<string, LevelResponse>;
|
|
20
|
+
|
|
21
|
+
export type VoiceDriverInput = {
|
|
22
|
+
state: VoiceAgentState;
|
|
23
|
+
/** One to five colors. */
|
|
24
|
+
colors: Hsv[];
|
|
25
|
+
/** From 0 to 2. */
|
|
26
|
+
expressivity: number;
|
|
27
|
+
/** From 0 to 1, or null when no live audio drives the motion. */
|
|
28
|
+
audioLevel: number | null;
|
|
29
|
+
reduceMotion: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The motion of a voice visualization: where it is in time, how it moves, and its colors. A plain
|
|
34
|
+
* object rather than a class, so it can live in a shared value and be advanced on the UI thread.
|
|
35
|
+
*/
|
|
36
|
+
export type VoiceDriverState = {
|
|
37
|
+
speed: number;
|
|
38
|
+
bright: number;
|
|
39
|
+
sat: number;
|
|
40
|
+
load: number;
|
|
41
|
+
flow: number;
|
|
42
|
+
baseReact: number;
|
|
43
|
+
expressivity: number;
|
|
44
|
+
time: number;
|
|
45
|
+
flowSpin: number;
|
|
46
|
+
orbSpin: number;
|
|
47
|
+
count: number;
|
|
48
|
+
tapX: number;
|
|
49
|
+
tapY: number;
|
|
50
|
+
tapAge: number;
|
|
51
|
+
hoverX: number;
|
|
52
|
+
hoverY: number;
|
|
53
|
+
hoverAmount: number;
|
|
54
|
+
isHovering: boolean;
|
|
55
|
+
mic: number;
|
|
56
|
+
voice: number;
|
|
57
|
+
/** The colors drawn, eased in place toward the input's color slots. */
|
|
58
|
+
colors: Hsv[];
|
|
59
|
+
bounce: BounceState;
|
|
60
|
+
levelResponse: LevelResponse;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* How a state moves. `load`, `flow` and `react` each select a motion pattern, one per state, so
|
|
65
|
+
* easing between states crossfades the patterns.
|
|
66
|
+
*/
|
|
67
|
+
type VoiceMotion = {
|
|
68
|
+
bright: number;
|
|
69
|
+
flow: number;
|
|
70
|
+
load: number;
|
|
71
|
+
react: number;
|
|
72
|
+
sat: number;
|
|
73
|
+
speed: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const MOTIONS: Record<VoiceAgentState, VoiceMotion> = {
|
|
77
|
+
connecting: { bright: 1, flow: 0, load: 1, react: 0, sat: 1, speed: 1 },
|
|
78
|
+
idle: { bright: 1, flow: 0, load: 0, react: 0.12, sat: 1, speed: 0.4 },
|
|
79
|
+
listening: { bright: 1, flow: 0, load: 0, react: 0.45, sat: 1, speed: 0.9 },
|
|
80
|
+
speaking: { bright: 1, flow: 0, load: 0, react: 1, sat: 1, speed: 1.25 },
|
|
81
|
+
thinking: { bright: 1, flow: 1, load: 0, react: 0, sat: 1, speed: 1.25 },
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const NO_TAP = 100;
|
|
85
|
+
const MAX_FRAME = 0.05;
|
|
86
|
+
/** How fast time runs under reduced motion. */
|
|
87
|
+
export const REDUCED = 0.07;
|
|
88
|
+
const DRIVERS = 0.08;
|
|
89
|
+
const GENTLE = 0.12;
|
|
90
|
+
const VOICE = 0.1;
|
|
91
|
+
|
|
92
|
+
/** Expressivity scales amplitude and speed, each with a floor so the motion breathes at 0. */
|
|
93
|
+
export const scales = (expressivity: number) => {
|
|
94
|
+
'worklet';
|
|
95
|
+
return { amplitude: 0.15 + 0.85 * expressivity, speed: 0.6 + 0.4 * expressivity };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The share of the way a value moves in `dt` seconds when it moves `perFrame` of the way each
|
|
100
|
+
* frame at 60 Hz.
|
|
101
|
+
*/
|
|
102
|
+
export const ease = (perFrame: number, dt: number) => {
|
|
103
|
+
'worklet';
|
|
104
|
+
return 1 - Math.pow(1 - perFrame, dt * 60);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const rem = (x: number, m: number) => {
|
|
108
|
+
'worklet';
|
|
109
|
+
return ((x % m) + m) % m;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Eases an HSV color in place, its hue taking the shorter way round the wheel. */
|
|
113
|
+
const easeHsv = (color: Hsv, target: Hsv, fraction: number) => {
|
|
114
|
+
'worklet';
|
|
115
|
+
const hueStep = (rem(target.hue - color.hue + 1.5, 1) - 0.5) * fraction;
|
|
116
|
+
color.hue = rem(color.hue + hueStep + 1, 1);
|
|
117
|
+
color.saturation += (target.saturation - color.saturation) * fraction;
|
|
118
|
+
color.value += (target.value - color.value) * fraction;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** The five color slots the renderers read; past the palette they repeat its first color. */
|
|
122
|
+
export const slots = (colors: Hsv[]): Hsv[] => {
|
|
123
|
+
'worklet';
|
|
124
|
+
const first = colors[0] ?? { hue: 0, saturation: 0, value: 0 };
|
|
125
|
+
const result: Hsv[] = [];
|
|
126
|
+
for (let i = 0; i < MAXIMUM_COLORS; i++) result.push(colors[i] ?? first);
|
|
127
|
+
return result;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** A driver settled on `input`, so the first frame shows it rather than easing into it. */
|
|
131
|
+
export const createDriverState = (
|
|
132
|
+
input: VoiceDriverInput,
|
|
133
|
+
levelResponse: LevelResponse = LEVEL_RESPONSE.steady,
|
|
134
|
+
): VoiceDriverState => {
|
|
135
|
+
const m = MOTIONS[input.state];
|
|
136
|
+
const s = scales(input.expressivity);
|
|
137
|
+
return {
|
|
138
|
+
baseReact: m.react * s.amplitude,
|
|
139
|
+
bounce: createBounce(),
|
|
140
|
+
bright: m.bright,
|
|
141
|
+
// Copies, as they are eased in place and the palette belongs to the caller.
|
|
142
|
+
colors: slots(input.colors).map((color) => ({ ...color })),
|
|
143
|
+
count: input.colors.length,
|
|
144
|
+
expressivity: input.expressivity,
|
|
145
|
+
flow: m.flow * s.amplitude,
|
|
146
|
+
flowSpin: 0,
|
|
147
|
+
hoverAmount: 0,
|
|
148
|
+
hoverX: 0,
|
|
149
|
+
hoverY: 0,
|
|
150
|
+
isHovering: false,
|
|
151
|
+
levelResponse,
|
|
152
|
+
load: m.load * s.amplitude,
|
|
153
|
+
mic: 0,
|
|
154
|
+
orbSpin: 0,
|
|
155
|
+
sat: m.sat,
|
|
156
|
+
speed: m.speed * s.speed,
|
|
157
|
+
tapAge: NO_TAP,
|
|
158
|
+
tapX: 0,
|
|
159
|
+
tapY: 0,
|
|
160
|
+
time: 0,
|
|
161
|
+
voice: 0,
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Live audio lifts the motion to its level, whatever the state. */
|
|
166
|
+
export const driverReact = (d: VoiceDriverState) => {
|
|
167
|
+
'worklet';
|
|
168
|
+
return d.baseReact + (Math.max(d.baseReact, d.mic) - d.baseReact) * d.voice;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/** The scale of a tap's bounce. */
|
|
172
|
+
export const driverScale = (d: VoiceDriverState) => {
|
|
173
|
+
'worklet';
|
|
174
|
+
return bounceScale(d.bounce);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** A tap at (x, y), centred and normalized to the shorter side with y up. */
|
|
178
|
+
export const tapDriver = (d: VoiceDriverState, input: VoiceDriverInput, x: number, y: number) => {
|
|
179
|
+
'worklet';
|
|
180
|
+
if (input.reduceMotion) return;
|
|
181
|
+
d.tapX = x;
|
|
182
|
+
d.tapY = y;
|
|
183
|
+
d.tapAge = 0;
|
|
184
|
+
startBounce(d.bounce);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** Where a pointer hovers, or null once it has left. */
|
|
188
|
+
export const hoverDriver = (
|
|
189
|
+
d: VoiceDriverState,
|
|
190
|
+
input: VoiceDriverInput,
|
|
191
|
+
x: number | null,
|
|
192
|
+
y: number | null,
|
|
193
|
+
) => {
|
|
194
|
+
'worklet';
|
|
195
|
+
if (x === null || y === null || input.reduceMotion) {
|
|
196
|
+
d.isHovering = false;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
d.hoverX = x;
|
|
200
|
+
d.hoverY = y;
|
|
201
|
+
d.isHovering = true;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Advances the motion by `seconds`, easing each value toward the input's motion by a fixed share
|
|
206
|
+
* per frame at 60 Hz, converted to time, so it moves the same at any refresh rate.
|
|
207
|
+
*/
|
|
208
|
+
export const advanceDriver = (d: VoiceDriverState, input: VoiceDriverInput, seconds: number) => {
|
|
209
|
+
'worklet';
|
|
210
|
+
// Clamped so a frame after a stall cannot leap the motion.
|
|
211
|
+
const dt = Math.min(Math.max(seconds, 0), MAX_FRAME);
|
|
212
|
+
if (input.reduceMotion) d.isHovering = false;
|
|
213
|
+
const m = MOTIONS[input.state];
|
|
214
|
+
const s = scales(input.expressivity);
|
|
215
|
+
const drivers = ease(DRIVERS, dt);
|
|
216
|
+
d.speed += (m.speed * s.speed - d.speed) * drivers;
|
|
217
|
+
d.bright += (m.bright - d.bright) * drivers;
|
|
218
|
+
d.sat += (m.sat - d.sat) * drivers;
|
|
219
|
+
d.load += (m.load * s.amplitude - d.load) * drivers;
|
|
220
|
+
d.flow += (m.flow * s.amplitude - d.flow) * drivers;
|
|
221
|
+
d.baseReact += (m.react * s.amplitude - d.baseReact) * drivers;
|
|
222
|
+
d.expressivity += (input.expressivity - d.expressivity) * drivers;
|
|
223
|
+
// Integrated rather than computed as time times a weight, so a change of state eases the turns
|
|
224
|
+
// instead of jumping their angles.
|
|
225
|
+
const step = dt * (input.reduceMotion ? REDUCED : 1) * d.speed;
|
|
226
|
+
d.time += step;
|
|
227
|
+
d.flowSpin += step * d.flow;
|
|
228
|
+
d.orbSpin += step * (1 + 0.5 * d.flow + 0.1 * d.baseReact);
|
|
229
|
+
d.tapAge += dt;
|
|
230
|
+
advanceBounce(d.bounce, dt);
|
|
231
|
+
const gentle = ease(GENTLE, dt);
|
|
232
|
+
d.hoverAmount += ((d.isHovering ? 1 : 0) - d.hoverAmount) * gentle;
|
|
233
|
+
d.voice += ((input.audioLevel === null ? 0 : 1) - d.voice) * ease(VOICE, dt);
|
|
234
|
+
const level = input.audioLevel ?? 0;
|
|
235
|
+
const response = level > d.mic ? d.levelResponse.rise : d.levelResponse.fall;
|
|
236
|
+
d.mic += (level - d.mic) * ease(response, dt);
|
|
237
|
+
const targets = slots(input.colors);
|
|
238
|
+
for (let i = 0; i < d.colors.length; i++) easeHsv(d.colors[i], targets[i], gentle);
|
|
239
|
+
d.count += (input.colors.length - d.count) * gentle;
|
|
240
|
+
};
|