@testgorilla/tgo-ui 5.0.1 → 5.1.0
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/components/ai-audio-circle/ai-audio-circle.component.d.ts +59 -0
- package/components/ai-audio-circle/ai-audio-circle.module.d.ts +8 -0
- package/components/ai-audio-circle/index.d.ts +5 -0
- package/components/ai-audio-circle/public-api.d.ts +2 -0
- package/components/field/field.model.d.ts +1 -1
- package/fesm2022/testgorilla-tgo-ui-components-ai-audio-circle.mjs +418 -0
- package/fesm2022/testgorilla-tgo-ui-components-ai-audio-circle.mjs.map +1 -0
- package/fesm2022/testgorilla-tgo-ui-components-field.mjs +2 -2
- package/fesm2022/testgorilla-tgo-ui-components-field.mjs.map +1 -1
- package/fesm2022/testgorilla-tgo-ui.mjs +1 -0
- package/fesm2022/testgorilla-tgo-ui.mjs.map +1 -1
- package/package.json +29 -25
- package/public-api.d.ts +1 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { OnDestroy, WritableSignal, ElementRef, AfterViewInit } from '@angular/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
export declare enum InterviewState {
|
|
4
|
+
CandidateSpeaking = "candidate-speaking",
|
|
5
|
+
AiListening = "ai-listening",
|
|
6
|
+
AiSpeaking = "ai-speaking"
|
|
7
|
+
}
|
|
8
|
+
export declare class AiAudioCircleComponent implements OnDestroy, AfterViewInit {
|
|
9
|
+
pulseCanvasRef?: ElementRef<HTMLCanvasElement>;
|
|
10
|
+
audioTrack: import("@angular/core").InputSignal<MediaStreamTrack | undefined>;
|
|
11
|
+
candidateAudioTrack: import("@angular/core").InputSignal<MediaStreamTrack | undefined>;
|
|
12
|
+
state: import("@angular/core").ModelSignal<InterviewState>;
|
|
13
|
+
canvasSize: import("@angular/core").InputSignal<number>;
|
|
14
|
+
isSpeaking: WritableSignal<boolean>;
|
|
15
|
+
scaleValue: WritableSignal<number>;
|
|
16
|
+
audioContext?: AudioContext;
|
|
17
|
+
analyser?: AnalyserNode;
|
|
18
|
+
candidateAnalyser?: AnalyserNode;
|
|
19
|
+
dataArray?: Uint8Array<ArrayBuffer>;
|
|
20
|
+
candidateDataArray?: Uint8Array<ArrayBuffer>;
|
|
21
|
+
animationFrameId?: number;
|
|
22
|
+
source?: MediaStreamAudioSourceNode;
|
|
23
|
+
candidateSource?: MediaStreamAudioSourceNode;
|
|
24
|
+
pulseCtx?: CanvasRenderingContext2D;
|
|
25
|
+
currentAmp: number;
|
|
26
|
+
simTime: number;
|
|
27
|
+
baseRadius: import("@angular/core").Signal<number>;
|
|
28
|
+
readonly AI_SPEAKING_THRESHOLD = 0.02;
|
|
29
|
+
readonly CANDIDATE_SPEAKING_THRESHOLD = 0.5;
|
|
30
|
+
readonly SHADOW_COLOR_RGB: string;
|
|
31
|
+
pulseTime: number;
|
|
32
|
+
pulseScale: number;
|
|
33
|
+
pulseTargetScale: number;
|
|
34
|
+
pulseAnimationId?: number;
|
|
35
|
+
blobs: Array<{
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
vx: number;
|
|
39
|
+
vy: number;
|
|
40
|
+
radius: number;
|
|
41
|
+
color: string;
|
|
42
|
+
opacity: number;
|
|
43
|
+
}>;
|
|
44
|
+
constructor();
|
|
45
|
+
ngAfterViewInit(): void;
|
|
46
|
+
private initializeCanvas;
|
|
47
|
+
private initializeBlobs;
|
|
48
|
+
private setupAudioAnalysis;
|
|
49
|
+
private setupCandidateAudioAnalysis;
|
|
50
|
+
private analyzeAudio;
|
|
51
|
+
private startAnimations;
|
|
52
|
+
private mainLoop;
|
|
53
|
+
private animatePulse;
|
|
54
|
+
private hexToRgb;
|
|
55
|
+
private cleanup;
|
|
56
|
+
ngOnDestroy(): void;
|
|
57
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<AiAudioCircleComponent, never>;
|
|
58
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<AiAudioCircleComponent, "ui-ai-audio-circle", never, { "audioTrack": { "alias": "audioTrack"; "required": false; "isSignal": true; }; "candidateAudioTrack": { "alias": "candidateAudioTrack"; "required": false; "isSignal": true; }; "state": { "alias": "state"; "required": false; "isSignal": true; }; "canvasSize": { "alias": "canvasSize"; "required": false; "isSignal": true; }; }, { "state": "stateChange"; }, never, never, false, never>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import * as i1 from "./ai-audio-circle.component";
|
|
3
|
+
import * as i2 from "@angular/common";
|
|
4
|
+
export declare class AiAudioCircleModule {
|
|
5
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<AiAudioCircleModule, never>;
|
|
6
|
+
static ɵmod: i0.ɵɵNgModuleDeclaration<AiAudioCircleModule, [typeof i1.AiAudioCircleComponent], [typeof i2.CommonModule], [typeof i1.AiAudioCircleComponent]>;
|
|
7
|
+
static ɵinj: i0.ɵɵInjectorDeclaration<AiAudioCircleModule>;
|
|
8
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type FieldType = 'text' | 'password' | 'email' | 'number' | 'tel' | 'search' | 'collapsed-search' | 'validation-text' | 'textarea' | 'multi-line';
|
|
1
|
+
export type FieldType = 'text' | 'password' | 'email' | 'number' | 'tel' | 'search' | 'collapsed-search' | 'validation-text' | 'textarea' | 'textarea-scrollable' | 'multi-line';
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, model, signal, computed, effect, ViewChild, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
|
|
3
|
+
import { Color } from '@testgorilla/tgo-ui/components/core';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
|
|
6
|
+
var InterviewState;
|
|
7
|
+
(function (InterviewState) {
|
|
8
|
+
InterviewState["CandidateSpeaking"] = "candidate-speaking";
|
|
9
|
+
InterviewState["AiListening"] = "ai-listening";
|
|
10
|
+
InterviewState["AiSpeaking"] = "ai-speaking";
|
|
11
|
+
})(InterviewState || (InterviewState = {}));
|
|
12
|
+
class AiAudioCircleComponent {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.audioTrack = input();
|
|
15
|
+
this.candidateAudioTrack = input();
|
|
16
|
+
this.state = model(InterviewState.AiListening);
|
|
17
|
+
this.canvasSize = input(280);
|
|
18
|
+
this.isSpeaking = signal(false);
|
|
19
|
+
this.scaleValue = signal(1);
|
|
20
|
+
this.currentAmp = 0.02;
|
|
21
|
+
this.simTime = 0;
|
|
22
|
+
this.baseRadius = computed(() => this.canvasSize() * 0.38);
|
|
23
|
+
// Audio detection thresholds
|
|
24
|
+
this.AI_SPEAKING_THRESHOLD = 0.02;
|
|
25
|
+
this.CANDIDATE_SPEAKING_THRESHOLD = 0.5;
|
|
26
|
+
this.SHADOW_COLOR_RGB = this.hexToRgb(Color.BRAND_50);
|
|
27
|
+
this.pulseTime = 0;
|
|
28
|
+
this.pulseScale = 1;
|
|
29
|
+
this.pulseTargetScale = 1;
|
|
30
|
+
this.blobs = [];
|
|
31
|
+
effect(() => {
|
|
32
|
+
const audioTrack = this.audioTrack();
|
|
33
|
+
if (audioTrack) {
|
|
34
|
+
this.setupAudioAnalysis();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
effect(() => {
|
|
38
|
+
const candidateAudioTrack = this.candidateAudioTrack();
|
|
39
|
+
if (candidateAudioTrack) {
|
|
40
|
+
this.setupCandidateAudioAnalysis();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
ngAfterViewInit() {
|
|
45
|
+
this.initializeCanvas();
|
|
46
|
+
this.initializeBlobs();
|
|
47
|
+
this.startAnimations();
|
|
48
|
+
}
|
|
49
|
+
initializeCanvas() {
|
|
50
|
+
if (!this.pulseCanvasRef) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const pulseCanvas = this.pulseCanvasRef.nativeElement;
|
|
54
|
+
const dpr = window.devicePixelRatio || 1;
|
|
55
|
+
// Setup pulse canvas
|
|
56
|
+
const size = this.canvasSize();
|
|
57
|
+
pulseCanvas.width = size * dpr;
|
|
58
|
+
pulseCanvas.height = size * dpr;
|
|
59
|
+
pulseCanvas.style.width = `${size}px`;
|
|
60
|
+
pulseCanvas.style.height = `${size}px`;
|
|
61
|
+
const ctx = pulseCanvas.getContext('2d');
|
|
62
|
+
if (!ctx) {
|
|
63
|
+
console.error('[AI_AUDIO_CIRCLE] Failed to get 2d context');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.pulseCtx = ctx;
|
|
67
|
+
this.pulseCtx.scale(dpr, dpr);
|
|
68
|
+
}
|
|
69
|
+
initializeBlobs() {
|
|
70
|
+
const size = this.canvasSize();
|
|
71
|
+
const centerX = size / 2;
|
|
72
|
+
const centerY = size / 2;
|
|
73
|
+
const baseRadius = this.baseRadius();
|
|
74
|
+
const colors = [
|
|
75
|
+
{ color: Color.INFORMATIVE_30, opacity: 0.8 },
|
|
76
|
+
{ color: Color.INFORMATIVE_20, opacity: 0.7 },
|
|
77
|
+
{ color: Color.BRAND_50, opacity: 0.75 },
|
|
78
|
+
{ color: Color.BRAND_20, opacity: 0.6 },
|
|
79
|
+
{ color: Color.INFORMATIVE_30, opacity: 0.65 },
|
|
80
|
+
];
|
|
81
|
+
this.blobs = colors.map((c, i) => {
|
|
82
|
+
const angle = (i / colors.length) * Math.PI * 2;
|
|
83
|
+
const dist = baseRadius * 0.25;
|
|
84
|
+
return {
|
|
85
|
+
x: centerX + Math.cos(angle) * dist,
|
|
86
|
+
y: centerY + Math.sin(angle) * dist,
|
|
87
|
+
vx: (Math.random() - 0.5) * 0.4,
|
|
88
|
+
vy: (Math.random() - 0.5) * 0.4,
|
|
89
|
+
radius: baseRadius * (0.6 + Math.random() * 0.4),
|
|
90
|
+
color: c.color,
|
|
91
|
+
opacity: c.opacity,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
setupAudioAnalysis() {
|
|
96
|
+
const audioTrack = this.audioTrack();
|
|
97
|
+
if (!audioTrack) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
if (!this.audioContext) {
|
|
102
|
+
this.audioContext = new AudioContext();
|
|
103
|
+
}
|
|
104
|
+
this.analyser = this.audioContext.createAnalyser();
|
|
105
|
+
this.analyser.fftSize = 256;
|
|
106
|
+
this.analyser.smoothingTimeConstant = 0.8;
|
|
107
|
+
const bufferLength = this.analyser.frequencyBinCount;
|
|
108
|
+
this.dataArray = new Uint8Array(bufferLength);
|
|
109
|
+
const stream = new MediaStream([audioTrack]);
|
|
110
|
+
this.source = this.audioContext.createMediaStreamSource(stream);
|
|
111
|
+
this.source.connect(this.analyser);
|
|
112
|
+
this.analyzeAudio();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error('[AI_AUDIO_CIRCLE] Error setting up audio analysis:', error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
setupCandidateAudioAnalysis() {
|
|
119
|
+
const candidateAudioTrack = this.candidateAudioTrack();
|
|
120
|
+
if (!candidateAudioTrack) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
if (!this.audioContext) {
|
|
125
|
+
this.audioContext = new AudioContext();
|
|
126
|
+
}
|
|
127
|
+
this.candidateAnalyser = this.audioContext.createAnalyser();
|
|
128
|
+
this.candidateAnalyser.fftSize = 256;
|
|
129
|
+
this.candidateAnalyser.smoothingTimeConstant = 0.8;
|
|
130
|
+
const bufferLength = this.candidateAnalyser.frequencyBinCount;
|
|
131
|
+
this.candidateDataArray = new Uint8Array(bufferLength);
|
|
132
|
+
const stream = new MediaStream([candidateAudioTrack]);
|
|
133
|
+
this.candidateSource = this.audioContext.createMediaStreamSource(stream);
|
|
134
|
+
this.candidateSource.connect(this.candidateAnalyser);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error('[AI_AUDIO_CIRCLE] Error setting up candidate audio analysis:', error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
analyzeAudio() {
|
|
141
|
+
if (!this.analyser || !this.dataArray) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const analyze = () => {
|
|
145
|
+
if (!this.analyser || !this.dataArray) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Analyze AI audio
|
|
149
|
+
this.analyser.getByteFrequencyData(this.dataArray);
|
|
150
|
+
const sum = Array.from(this.dataArray).reduce((a, b) => a + b, 0);
|
|
151
|
+
const aiVolume = sum / this.dataArray.length / 255;
|
|
152
|
+
// Analyze candidate audio if available
|
|
153
|
+
let candidateVolume = 0;
|
|
154
|
+
if (this.candidateAnalyser && this.candidateDataArray) {
|
|
155
|
+
this.candidateAnalyser.getByteFrequencyData(this.candidateDataArray);
|
|
156
|
+
const candidateSum = Array.from(this.candidateDataArray).reduce((a, b) => a + b, 0);
|
|
157
|
+
candidateVolume = candidateSum / this.candidateDataArray.length / 255;
|
|
158
|
+
}
|
|
159
|
+
const aiSpeaking = aiVolume > this.AI_SPEAKING_THRESHOLD;
|
|
160
|
+
const candidateSpeaking = candidateVolume > this.CANDIDATE_SPEAKING_THRESHOLD;
|
|
161
|
+
if (candidateSpeaking) {
|
|
162
|
+
this.state.set(InterviewState.CandidateSpeaking);
|
|
163
|
+
}
|
|
164
|
+
else if (aiSpeaking) {
|
|
165
|
+
this.state.set(InterviewState.AiSpeaking);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
this.state.set(InterviewState.AiListening);
|
|
169
|
+
}
|
|
170
|
+
this.isSpeaking.set(aiSpeaking);
|
|
171
|
+
// Update amplitude for animations
|
|
172
|
+
const currentStateValue = this.state();
|
|
173
|
+
if (currentStateValue === InterviewState.AiSpeaking && aiSpeaking) {
|
|
174
|
+
const target = Math.pow(aiVolume, 0.8) * 1.3;
|
|
175
|
+
this.currentAmp += (target - this.currentAmp) * 0.4;
|
|
176
|
+
}
|
|
177
|
+
else if (currentStateValue === InterviewState.CandidateSpeaking) {
|
|
178
|
+
const target = 0.55;
|
|
179
|
+
this.currentAmp += (target - this.currentAmp) * 0.1;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const fixedBase = 0.2;
|
|
183
|
+
this.currentAmp += (fixedBase - this.currentAmp) * 0.05;
|
|
184
|
+
}
|
|
185
|
+
this.animationFrameId = requestAnimationFrame(analyze);
|
|
186
|
+
};
|
|
187
|
+
this.animationFrameId = requestAnimationFrame(analyze);
|
|
188
|
+
}
|
|
189
|
+
startAnimations() {
|
|
190
|
+
this.animatePulse();
|
|
191
|
+
this.mainLoop();
|
|
192
|
+
}
|
|
193
|
+
mainLoop() {
|
|
194
|
+
this.simTime += 0.05;
|
|
195
|
+
const currentStateValue = this.state();
|
|
196
|
+
if (currentStateValue === InterviewState.AiSpeaking) {
|
|
197
|
+
if (this.analyser && this.dataArray) {
|
|
198
|
+
// Use real audio amplitude (already handled in analyzeAudio)
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Simulated speaking
|
|
202
|
+
const rhythm = Math.sin(this.simTime * 0.8) * 0.5 + 0.5;
|
|
203
|
+
const spikes = Math.random() * 0.6;
|
|
204
|
+
const target = rhythm * spikes;
|
|
205
|
+
this.currentAmp += (target - this.currentAmp) * 0.2;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (currentStateValue === InterviewState.CandidateSpeaking) {
|
|
209
|
+
const target = 0.55;
|
|
210
|
+
this.currentAmp += (target - this.currentAmp) * 0.1;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const fixedBase = 0.2;
|
|
214
|
+
this.currentAmp += (fixedBase - this.currentAmp) * 0.05;
|
|
215
|
+
}
|
|
216
|
+
requestAnimationFrame(this.mainLoop);
|
|
217
|
+
}
|
|
218
|
+
animatePulse() {
|
|
219
|
+
if (!this.pulseCtx) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const ctx = this.pulseCtx;
|
|
223
|
+
this.pulseTime += 0.016;
|
|
224
|
+
const size = this.canvasSize();
|
|
225
|
+
const centerX = size / 2;
|
|
226
|
+
const centerY = size / 2;
|
|
227
|
+
const baseRadius = this.baseRadius();
|
|
228
|
+
ctx.clearRect(0, 0, size, size);
|
|
229
|
+
const currentStateValue = this.state();
|
|
230
|
+
const effectiveAmp = this.currentAmp;
|
|
231
|
+
const isListening = currentStateValue === InterviewState.AiListening;
|
|
232
|
+
const isCandidate = currentStateValue === InterviewState.CandidateSpeaking;
|
|
233
|
+
// Scale logic
|
|
234
|
+
if (isCandidate) {
|
|
235
|
+
// Candidate speaking: Fixed enlarged scale (115%), no breathing effect
|
|
236
|
+
this.pulseTargetScale = 1.15;
|
|
237
|
+
const breathing = 0;
|
|
238
|
+
this.pulseScale += (this.pulseTargetScale + breathing - this.pulseScale) * 0.05;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// AI speaking/listening: Scale reacts to audio amplitude, with optional breathing
|
|
242
|
+
// When AI is speaking: effectiveAmp is high, scale grows with voice
|
|
243
|
+
// When AI is listening: effectiveAmp is low (~0.2), scale stays near 1.0 with breathing effect
|
|
244
|
+
this.pulseTargetScale = 1 + effectiveAmp * 0.3;
|
|
245
|
+
const breathing = isListening ? Math.sin(this.pulseTime * 1.5) * 0.035 : 0;
|
|
246
|
+
this.pulseScale += (this.pulseTargetScale + breathing - this.pulseScale) * 0.05;
|
|
247
|
+
}
|
|
248
|
+
const currentRadius = baseRadius * this.pulseScale;
|
|
249
|
+
// Draw shadows
|
|
250
|
+
for (let i = 3; i >= 1; i--) {
|
|
251
|
+
const shadowRadius = currentRadius + i * 6;
|
|
252
|
+
const shadowOpacity = 0.24 - i * 0.04;
|
|
253
|
+
const shadowGradient = ctx.createRadialGradient(centerX, centerY, currentRadius * 0.98, centerX, centerY, shadowRadius);
|
|
254
|
+
shadowGradient.addColorStop(0, `rgba(${this.SHADOW_COLOR_RGB}, ${shadowOpacity})`);
|
|
255
|
+
shadowGradient.addColorStop(0.6, `rgba(${this.SHADOW_COLOR_RGB}, ${shadowOpacity * 0.4})`);
|
|
256
|
+
shadowGradient.addColorStop(1, `rgba(${this.SHADOW_COLOR_RGB}, 0)`);
|
|
257
|
+
ctx.beginPath();
|
|
258
|
+
ctx.arc(centerX, centerY, shadowRadius, 0, Math.PI * 2);
|
|
259
|
+
ctx.fillStyle = shadowGradient;
|
|
260
|
+
ctx.fill();
|
|
261
|
+
}
|
|
262
|
+
// Clip to circle
|
|
263
|
+
ctx.save();
|
|
264
|
+
ctx.beginPath();
|
|
265
|
+
ctx.arc(centerX, centerY, currentRadius, 0, Math.PI * 2);
|
|
266
|
+
ctx.clip();
|
|
267
|
+
// Background gradient
|
|
268
|
+
const bgGradient = ctx.createLinearGradient(centerX - currentRadius, centerY - currentRadius, centerX + currentRadius, centerY + currentRadius);
|
|
269
|
+
bgGradient.addColorStop(0, Color.BRAND_50);
|
|
270
|
+
bgGradient.addColorStop(0.5, Color.INFORMATIVE_20);
|
|
271
|
+
bgGradient.addColorStop(1, Color.INFORMATIVE_30);
|
|
272
|
+
ctx.beginPath();
|
|
273
|
+
ctx.arc(centerX, centerY, currentRadius, 0, Math.PI * 2);
|
|
274
|
+
ctx.fillStyle = bgGradient;
|
|
275
|
+
ctx.fill();
|
|
276
|
+
// Animate blobs
|
|
277
|
+
let speedMultiplier;
|
|
278
|
+
if (isCandidate) {
|
|
279
|
+
speedMultiplier = 2.0;
|
|
280
|
+
}
|
|
281
|
+
else if (isListening) {
|
|
282
|
+
speedMultiplier = 1.2;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
speedMultiplier = 0.8 + effectiveAmp * 1.5;
|
|
286
|
+
}
|
|
287
|
+
this.blobs.forEach((blob, index) => {
|
|
288
|
+
// Chaos forces
|
|
289
|
+
const chaos1 = Math.sin(this.pulseTime * 0.6 + index * 2.5 + blob.x * 0.02) * 0.15;
|
|
290
|
+
const chaos2 = Math.cos(this.pulseTime * 0.4 + index * 1.8 + blob.y * 0.015) * 0.12;
|
|
291
|
+
const chaos3 = Math.sin(this.pulseTime * 1.0 + index * 0.7) * 0.08;
|
|
292
|
+
// Apply forces
|
|
293
|
+
blob.vx += (chaos1 + chaos3 * Math.cos(this.pulseTime * 1.5 + index)) * 0.025 * speedMultiplier;
|
|
294
|
+
blob.vy += (chaos2 + chaos3 * Math.sin(this.pulseTime * 1.2 + index)) * 0.025 * speedMultiplier;
|
|
295
|
+
// Friction
|
|
296
|
+
blob.vx *= 0.98;
|
|
297
|
+
blob.vy *= 0.98;
|
|
298
|
+
// Max velocity
|
|
299
|
+
const maxVel = (isListening ? 1.5 : 2.2) * speedMultiplier;
|
|
300
|
+
const vel = Math.sqrt(blob.vx * blob.vx + blob.vy * blob.vy);
|
|
301
|
+
if (vel > maxVel) {
|
|
302
|
+
blob.vx = (blob.vx / vel) * maxVel;
|
|
303
|
+
blob.vy = (blob.vy / vel) * maxVel;
|
|
304
|
+
}
|
|
305
|
+
// Update position
|
|
306
|
+
blob.x += blob.vx;
|
|
307
|
+
blob.y += blob.vy;
|
|
308
|
+
// Keep within bounds
|
|
309
|
+
const dx = blob.x - centerX;
|
|
310
|
+
const dy = blob.y - centerY;
|
|
311
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
312
|
+
const maxDist = currentRadius * 0.55;
|
|
313
|
+
if (dist > maxDist) {
|
|
314
|
+
const angle = Math.atan2(dy, dx);
|
|
315
|
+
blob.x = centerX + Math.cos(angle) * maxDist;
|
|
316
|
+
blob.y = centerY + Math.sin(angle) * maxDist;
|
|
317
|
+
blob.vx -= dx * 0.065;
|
|
318
|
+
blob.vy -= dy * 0.065;
|
|
319
|
+
}
|
|
320
|
+
// Parse color to RGB
|
|
321
|
+
const rgb = this.hexToRgb(blob.color);
|
|
322
|
+
// Draw blob with glow
|
|
323
|
+
const blurRadius = blob.radius * 1.3;
|
|
324
|
+
const outerGlow = ctx.createRadialGradient(blob.x, blob.y, 0, blob.x, blob.y, blurRadius);
|
|
325
|
+
outerGlow.addColorStop(0, `rgba(${rgb}, ${blob.opacity * 0.9})`);
|
|
326
|
+
outerGlow.addColorStop(0.2, `rgba(${rgb}, ${blob.opacity * 0.7})`);
|
|
327
|
+
outerGlow.addColorStop(0.4, `rgba(${rgb}, ${blob.opacity * 0.5})`);
|
|
328
|
+
outerGlow.addColorStop(0.6, `rgba(${rgb}, ${blob.opacity * 0.3})`);
|
|
329
|
+
outerGlow.addColorStop(0.8, `rgba(${rgb}, ${blob.opacity * 0.1})`);
|
|
330
|
+
outerGlow.addColorStop(1, `rgba(${rgb}, 0)`);
|
|
331
|
+
ctx.beginPath();
|
|
332
|
+
ctx.arc(blob.x, blob.y, blurRadius, 0, Math.PI * 2);
|
|
333
|
+
ctx.fillStyle = outerGlow;
|
|
334
|
+
ctx.fill();
|
|
335
|
+
});
|
|
336
|
+
// Highlight
|
|
337
|
+
const highlightGradient = ctx.createRadialGradient(centerX - currentRadius * 0.3, centerY - currentRadius * 0.3, 0, centerX - currentRadius * 0.3, centerY - currentRadius * 0.3, currentRadius * 0.6);
|
|
338
|
+
highlightGradient.addColorStop(0, 'rgba(255, 255, 255, 0.12)');
|
|
339
|
+
highlightGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.03)');
|
|
340
|
+
highlightGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
|
341
|
+
ctx.beginPath();
|
|
342
|
+
ctx.arc(centerX, centerY, currentRadius, 0, Math.PI * 2);
|
|
343
|
+
ctx.fillStyle = highlightGradient;
|
|
344
|
+
ctx.fill();
|
|
345
|
+
ctx.restore();
|
|
346
|
+
this.pulseAnimationId = requestAnimationFrame(this.animatePulse);
|
|
347
|
+
}
|
|
348
|
+
;
|
|
349
|
+
hexToRgb(hex) {
|
|
350
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
351
|
+
if (!result) {
|
|
352
|
+
return '0, 0, 0';
|
|
353
|
+
}
|
|
354
|
+
const r = parseInt(result[1], 16);
|
|
355
|
+
const g = parseInt(result[2], 16);
|
|
356
|
+
const b = parseInt(result[3], 16);
|
|
357
|
+
return `${r}, ${g}, ${b}`;
|
|
358
|
+
}
|
|
359
|
+
cleanup() {
|
|
360
|
+
if (this.animationFrameId) {
|
|
361
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
362
|
+
this.animationFrameId = undefined;
|
|
363
|
+
}
|
|
364
|
+
if (this.pulseAnimationId) {
|
|
365
|
+
cancelAnimationFrame(this.pulseAnimationId);
|
|
366
|
+
this.pulseAnimationId = undefined;
|
|
367
|
+
}
|
|
368
|
+
if (this.source) {
|
|
369
|
+
this.source.disconnect();
|
|
370
|
+
this.source = undefined;
|
|
371
|
+
}
|
|
372
|
+
if (this.candidateSource) {
|
|
373
|
+
this.candidateSource.disconnect();
|
|
374
|
+
this.candidateSource = undefined;
|
|
375
|
+
}
|
|
376
|
+
if (this.audioContext && this.audioContext.state !== 'closed') {
|
|
377
|
+
void this.audioContext.close();
|
|
378
|
+
this.audioContext = undefined;
|
|
379
|
+
}
|
|
380
|
+
this.analyser = undefined;
|
|
381
|
+
this.candidateAnalyser = undefined;
|
|
382
|
+
this.dataArray = undefined;
|
|
383
|
+
this.candidateDataArray = undefined;
|
|
384
|
+
}
|
|
385
|
+
ngOnDestroy() {
|
|
386
|
+
this.cleanup();
|
|
387
|
+
}
|
|
388
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: AiAudioCircleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
389
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.18", type: AiAudioCircleComponent, isStandalone: false, selector: "ui-ai-audio-circle", inputs: { audioTrack: { classPropertyName: "audioTrack", publicName: "audioTrack", isSignal: true, isRequired: false, transformFunction: null }, candidateAudioTrack: { classPropertyName: "candidateAudioTrack", publicName: "candidateAudioTrack", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, canvasSize: { classPropertyName: "canvasSize", publicName: "canvasSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { state: "stateChange" }, viewQueries: [{ propertyName: "pulseCanvasRef", first: true, predicate: ["pulseCanvas"], descendants: true }], ngImport: i0, template: "<div class=\"ai-audio-visualizer\">\n <div class=\"canvas-container\">\n <canvas #pulseCanvas></canvas>\n </div>\n</div>\n", styles: [".ai-audio-visualizer{display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;height:100%}.canvas-container{position:relative;display:flex;flex-direction:column;justify-content:center;align-items:center}canvas{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
390
|
+
}
|
|
391
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: AiAudioCircleComponent, decorators: [{
|
|
392
|
+
type: Component,
|
|
393
|
+
args: [{ selector: 'ui-ai-audio-circle', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div class=\"ai-audio-visualizer\">\n <div class=\"canvas-container\">\n <canvas #pulseCanvas></canvas>\n </div>\n</div>\n", styles: [".ai-audio-visualizer{display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;height:100%}.canvas-container{position:relative;display:flex;flex-direction:column;justify-content:center;align-items:center}canvas{display:block}\n"] }]
|
|
394
|
+
}], ctorParameters: () => [], propDecorators: { pulseCanvasRef: [{
|
|
395
|
+
type: ViewChild,
|
|
396
|
+
args: ['pulseCanvas', { static: false }]
|
|
397
|
+
}] } });
|
|
398
|
+
|
|
399
|
+
class AiAudioCircleModule {
|
|
400
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: AiAudioCircleModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
401
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.18", ngImport: i0, type: AiAudioCircleModule, declarations: [AiAudioCircleComponent], imports: [CommonModule], exports: [AiAudioCircleComponent] }); }
|
|
402
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: AiAudioCircleModule, imports: [CommonModule] }); }
|
|
403
|
+
}
|
|
404
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: AiAudioCircleModule, decorators: [{
|
|
405
|
+
type: NgModule,
|
|
406
|
+
args: [{
|
|
407
|
+
declarations: [AiAudioCircleComponent],
|
|
408
|
+
imports: [CommonModule],
|
|
409
|
+
exports: [AiAudioCircleComponent],
|
|
410
|
+
}]
|
|
411
|
+
}] });
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Generated bundle index. Do not edit.
|
|
415
|
+
*/
|
|
416
|
+
|
|
417
|
+
export { AiAudioCircleComponent, AiAudioCircleModule, InterviewState };
|
|
418
|
+
//# sourceMappingURL=testgorilla-tgo-ui-components-ai-audio-circle.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testgorilla-tgo-ui-components-ai-audio-circle.mjs","sources":["../../../components/ai-audio-circle/ai-audio-circle.component.ts","../../../components/ai-audio-circle/ai-audio-circle.component.html","../../../components/ai-audio-circle/ai-audio-circle.module.ts","../../../components/ai-audio-circle/testgorilla-tgo-ui-components-ai-audio-circle.ts"],"sourcesContent":["import {\n Component,\n input,\n model,\n OnDestroy,\n ChangeDetectionStrategy,\n WritableSignal,\n signal,\n ElementRef,\n ViewChild,\n AfterViewInit,\n effect,\n computed,\n} from '@angular/core';\nimport { Color } from '@testgorilla/tgo-ui/components/core';\n\nexport enum InterviewState {\n CandidateSpeaking = 'candidate-speaking',\n AiListening = 'ai-listening',\n AiSpeaking = 'ai-speaking',\n}\n\n@Component({\n selector: 'ui-ai-audio-circle',\n templateUrl: './ai-audio-circle.component.html',\n styleUrls: ['./ai-audio-circle.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: false,\n})\nexport class AiAudioCircleComponent implements OnDestroy, AfterViewInit {\n @ViewChild('pulseCanvas', { static: false }) pulseCanvasRef?: ElementRef<HTMLCanvasElement>;\n\n audioTrack = input<MediaStreamTrack>();\n candidateAudioTrack = input<MediaStreamTrack>();\n state = model<InterviewState>(InterviewState.AiListening);\n canvasSize = input<number>(280);\n\n isSpeaking: WritableSignal<boolean> = signal(false);\n scaleValue: WritableSignal<number> = signal(1);\n\n audioContext?: AudioContext;\n analyser?: AnalyserNode;\n candidateAnalyser?: AnalyserNode;\n dataArray?: Uint8Array<ArrayBuffer>;\n candidateDataArray?: Uint8Array<ArrayBuffer>;\n animationFrameId?: number;\n source?: MediaStreamAudioSourceNode;\n candidateSource?: MediaStreamAudioSourceNode;\n\n pulseCtx?: CanvasRenderingContext2D;\n currentAmp = 0.02;\n simTime = 0;\n baseRadius = computed(() => this.canvasSize() * 0.38);\n\n // Audio detection thresholds\n readonly AI_SPEAKING_THRESHOLD = 0.02;\n readonly CANDIDATE_SPEAKING_THRESHOLD = 0.5;\n\n readonly SHADOW_COLOR_RGB = this.hexToRgb(Color.BRAND_50);\n\n pulseTime = 0;\n pulseScale = 1;\n pulseTargetScale = 1;\n pulseAnimationId?: number;\n blobs: Array<{\n x: number;\n y: number;\n vx: number;\n vy: number;\n radius: number;\n color: string;\n opacity: number;\n }> = [];\n\n constructor() {\n effect(() => {\n const audioTrack = this.audioTrack();\n if (audioTrack) {\n this.setupAudioAnalysis();\n }\n });\n\n effect(() => {\n const candidateAudioTrack = this.candidateAudioTrack();\n if (candidateAudioTrack) {\n this.setupCandidateAudioAnalysis();\n }\n });\n }\n\n ngAfterViewInit(): void {\n this.initializeCanvas();\n this.initializeBlobs();\n this.startAnimations();\n }\n\n private initializeCanvas(): void {\n if (!this.pulseCanvasRef) {\n return;\n }\n\n const pulseCanvas = this.pulseCanvasRef.nativeElement;\n\n const dpr = window.devicePixelRatio || 1;\n\n // Setup pulse canvas\n const size = this.canvasSize();\n pulseCanvas.width = size * dpr;\n pulseCanvas.height = size * dpr;\n pulseCanvas.style.width = `${size}px`;\n pulseCanvas.style.height = `${size}px`;\n const ctx = pulseCanvas.getContext('2d');\n if (!ctx) {\n console.error('[AI_AUDIO_CIRCLE] Failed to get 2d context');\n return;\n }\n this.pulseCtx = ctx;\n this.pulseCtx.scale(dpr, dpr);\n }\n\n private initializeBlobs(): void {\n const size = this.canvasSize();\n const centerX = size / 2;\n const centerY = size / 2;\n const baseRadius = this.baseRadius();\n\n const colors = [\n { color: Color.INFORMATIVE_30, opacity: 0.8 },\n { color: Color.INFORMATIVE_20, opacity: 0.7 },\n { color: Color.BRAND_50, opacity: 0.75 },\n { color: Color.BRAND_20, opacity: 0.6 },\n { color: Color.INFORMATIVE_30, opacity: 0.65 },\n ];\n\n this.blobs = colors.map((c, i) => {\n const angle = (i / colors.length) * Math.PI * 2;\n const dist = baseRadius * 0.25;\n return {\n x: centerX + Math.cos(angle) * dist,\n y: centerY + Math.sin(angle) * dist,\n vx: (Math.random() - 0.5) * 0.4,\n vy: (Math.random() - 0.5) * 0.4,\n radius: baseRadius * (0.6 + Math.random() * 0.4),\n color: c.color,\n opacity: c.opacity,\n };\n });\n }\n\n private setupAudioAnalysis(): void {\n const audioTrack = this.audioTrack();\n if (!audioTrack) {\n return;\n }\n\n try {\n if (!this.audioContext) {\n this.audioContext = new AudioContext();\n }\n\n this.analyser = this.audioContext.createAnalyser();\n this.analyser.fftSize = 256;\n this.analyser.smoothingTimeConstant = 0.8;\n\n const bufferLength = this.analyser.frequencyBinCount;\n this.dataArray = new Uint8Array(bufferLength);\n\n const stream = new MediaStream([audioTrack]);\n this.source = this.audioContext.createMediaStreamSource(stream);\n this.source.connect(this.analyser);\n\n this.analyzeAudio();\n } catch (error) {\n console.error('[AI_AUDIO_CIRCLE] Error setting up audio analysis:', error);\n }\n }\n\n private setupCandidateAudioAnalysis(): void {\n const candidateAudioTrack = this.candidateAudioTrack();\n if (!candidateAudioTrack) {\n return;\n }\n\n try {\n if (!this.audioContext) {\n this.audioContext = new AudioContext();\n }\n\n this.candidateAnalyser = this.audioContext.createAnalyser();\n this.candidateAnalyser.fftSize = 256;\n this.candidateAnalyser.smoothingTimeConstant = 0.8;\n\n const bufferLength = this.candidateAnalyser.frequencyBinCount;\n this.candidateDataArray = new Uint8Array(bufferLength);\n\n const stream = new MediaStream([candidateAudioTrack]);\n this.candidateSource = this.audioContext.createMediaStreamSource(stream);\n this.candidateSource.connect(this.candidateAnalyser);\n } catch (error) {\n console.error('[AI_AUDIO_CIRCLE] Error setting up candidate audio analysis:', error);\n }\n }\n\n private analyzeAudio(): void {\n if (!this.analyser || !this.dataArray) {\n return;\n }\n\n const analyze = () => {\n if (!this.analyser || !this.dataArray) {\n return;\n }\n\n // Analyze AI audio\n this.analyser.getByteFrequencyData(this.dataArray);\n const sum = Array.from(this.dataArray).reduce((a, b) => a + b, 0);\n const aiVolume = sum / this.dataArray.length / 255;\n\n // Analyze candidate audio if available\n let candidateVolume = 0;\n if (this.candidateAnalyser && this.candidateDataArray) {\n this.candidateAnalyser.getByteFrequencyData(this.candidateDataArray);\n const candidateSum = Array.from(this.candidateDataArray).reduce((a, b) => a + b, 0);\n candidateVolume = candidateSum / this.candidateDataArray.length / 255;\n }\n\n const aiSpeaking = aiVolume > this.AI_SPEAKING_THRESHOLD;\n const candidateSpeaking = candidateVolume > this.CANDIDATE_SPEAKING_THRESHOLD;\n\n if (candidateSpeaking) {\n this.state.set(InterviewState.CandidateSpeaking);\n } else if (aiSpeaking) {\n this.state.set(InterviewState.AiSpeaking);\n } else {\n this.state.set(InterviewState.AiListening);\n }\n\n this.isSpeaking.set(aiSpeaking);\n\n // Update amplitude for animations\n const currentStateValue = this.state();\n if (currentStateValue === InterviewState.AiSpeaking && aiSpeaking) {\n const target = Math.pow(aiVolume, 0.8) * 1.3;\n this.currentAmp += (target - this.currentAmp) * 0.4;\n } else if (currentStateValue === InterviewState.CandidateSpeaking) {\n const target = 0.55;\n this.currentAmp += (target - this.currentAmp) * 0.1;\n } else {\n const fixedBase = 0.2;\n this.currentAmp += (fixedBase - this.currentAmp) * 0.05;\n }\n\n this.animationFrameId = requestAnimationFrame(analyze);\n };\n\n this.animationFrameId = requestAnimationFrame(analyze);\n }\n\n private startAnimations(): void {\n this.animatePulse();\n this.mainLoop();\n }\n\n private mainLoop(): void {\n this.simTime += 0.05;\n\n const currentStateValue = this.state();\n\n if (currentStateValue === InterviewState.AiSpeaking) {\n if (this.analyser && this.dataArray) {\n // Use real audio amplitude (already handled in analyzeAudio)\n } else {\n // Simulated speaking\n const rhythm = Math.sin(this.simTime * 0.8) * 0.5 + 0.5;\n const spikes = Math.random() * 0.6;\n const target = rhythm * spikes;\n this.currentAmp += (target - this.currentAmp) * 0.2;\n }\n } else if (currentStateValue === InterviewState.CandidateSpeaking) {\n const target = 0.55;\n this.currentAmp += (target - this.currentAmp) * 0.1;\n } else {\n const fixedBase = 0.2;\n this.currentAmp += (fixedBase - this.currentAmp) * 0.05;\n }\n\n requestAnimationFrame(this.mainLoop);\n }\n\n private animatePulse(): void {\n if (!this.pulseCtx) {\n return;\n }\n\n const ctx = this.pulseCtx;\n this.pulseTime += 0.016;\n const size = this.canvasSize();\n const centerX = size / 2;\n const centerY = size / 2;\n const baseRadius = this.baseRadius();\n\n ctx.clearRect(0, 0, size, size);\n\n const currentStateValue = this.state();\n const effectiveAmp = this.currentAmp;\n const isListening = currentStateValue === InterviewState.AiListening;\n const isCandidate = currentStateValue === InterviewState.CandidateSpeaking;\n\n // Scale logic\n if (isCandidate) {\n // Candidate speaking: Fixed enlarged scale (115%), no breathing effect\n this.pulseTargetScale = 1.15;\n const breathing = 0;\n this.pulseScale += (this.pulseTargetScale + breathing - this.pulseScale) * 0.05;\n } else {\n // AI speaking/listening: Scale reacts to audio amplitude, with optional breathing\n // When AI is speaking: effectiveAmp is high, scale grows with voice\n // When AI is listening: effectiveAmp is low (~0.2), scale stays near 1.0 with breathing effect\n this.pulseTargetScale = 1 + effectiveAmp * 0.3;\n const breathing = isListening ? Math.sin(this.pulseTime * 1.5) * 0.035 : 0;\n this.pulseScale += (this.pulseTargetScale + breathing - this.pulseScale) * 0.05;\n }\n\n const currentRadius = baseRadius * this.pulseScale;\n\n // Draw shadows\n for (let i = 3; i >= 1; i--) {\n const shadowRadius = currentRadius + i * 6;\n const shadowOpacity = 0.24 - i * 0.04;\n const shadowGradient = ctx.createRadialGradient(\n centerX,\n centerY,\n currentRadius * 0.98,\n centerX,\n centerY,\n shadowRadius\n );\n shadowGradient.addColorStop(0, `rgba(${this.SHADOW_COLOR_RGB}, ${shadowOpacity})`);\n shadowGradient.addColorStop(0.6, `rgba(${this.SHADOW_COLOR_RGB}, ${shadowOpacity * 0.4})`);\n shadowGradient.addColorStop(1, `rgba(${this.SHADOW_COLOR_RGB}, 0)`);\n ctx.beginPath();\n ctx.arc(centerX, centerY, shadowRadius, 0, Math.PI * 2);\n ctx.fillStyle = shadowGradient;\n ctx.fill();\n }\n\n // Clip to circle\n ctx.save();\n ctx.beginPath();\n ctx.arc(centerX, centerY, currentRadius, 0, Math.PI * 2);\n ctx.clip();\n\n // Background gradient\n const bgGradient = ctx.createLinearGradient(\n centerX - currentRadius,\n centerY - currentRadius,\n centerX + currentRadius,\n centerY + currentRadius\n );\n bgGradient.addColorStop(0, Color.BRAND_50);\n bgGradient.addColorStop(0.5, Color.INFORMATIVE_20);\n bgGradient.addColorStop(1, Color.INFORMATIVE_30);\n ctx.beginPath();\n ctx.arc(centerX, centerY, currentRadius, 0, Math.PI * 2);\n ctx.fillStyle = bgGradient;\n ctx.fill();\n\n // Animate blobs\n let speedMultiplier;\n if (isCandidate) {\n speedMultiplier = 2.0;\n } else if (isListening) {\n speedMultiplier = 1.2;\n } else {\n speedMultiplier = 0.8 + effectiveAmp * 1.5;\n }\n\n this.blobs.forEach((blob, index) => {\n // Chaos forces\n const chaos1 = Math.sin(this.pulseTime * 0.6 + index * 2.5 + blob.x * 0.02) * 0.15;\n const chaos2 = Math.cos(this.pulseTime * 0.4 + index * 1.8 + blob.y * 0.015) * 0.12;\n const chaos3 = Math.sin(this.pulseTime * 1.0 + index * 0.7) * 0.08;\n\n // Apply forces\n blob.vx += (chaos1 + chaos3 * Math.cos(this.pulseTime * 1.5 + index)) * 0.025 * speedMultiplier;\n blob.vy += (chaos2 + chaos3 * Math.sin(this.pulseTime * 1.2 + index)) * 0.025 * speedMultiplier;\n\n // Friction\n blob.vx *= 0.98;\n blob.vy *= 0.98;\n\n // Max velocity\n const maxVel = (isListening ? 1.5 : 2.2) * speedMultiplier;\n const vel = Math.sqrt(blob.vx * blob.vx + blob.vy * blob.vy);\n if (vel > maxVel) {\n blob.vx = (blob.vx / vel) * maxVel;\n blob.vy = (blob.vy / vel) * maxVel;\n }\n\n // Update position\n blob.x += blob.vx;\n blob.y += blob.vy;\n\n // Keep within bounds\n const dx = blob.x - centerX;\n const dy = blob.y - centerY;\n const dist = Math.sqrt(dx * dx + dy * dy);\n const maxDist = currentRadius * 0.55;\n if (dist > maxDist) {\n const angle = Math.atan2(dy, dx);\n blob.x = centerX + Math.cos(angle) * maxDist;\n blob.y = centerY + Math.sin(angle) * maxDist;\n blob.vx -= dx * 0.065;\n blob.vy -= dy * 0.065;\n }\n\n // Parse color to RGB\n const rgb = this.hexToRgb(blob.color);\n\n // Draw blob with glow\n const blurRadius = blob.radius * 1.3;\n const outerGlow = ctx.createRadialGradient(blob.x, blob.y, 0, blob.x, blob.y, blurRadius);\n outerGlow.addColorStop(0, `rgba(${rgb}, ${blob.opacity * 0.9})`);\n outerGlow.addColorStop(0.2, `rgba(${rgb}, ${blob.opacity * 0.7})`);\n outerGlow.addColorStop(0.4, `rgba(${rgb}, ${blob.opacity * 0.5})`);\n outerGlow.addColorStop(0.6, `rgba(${rgb}, ${blob.opacity * 0.3})`);\n outerGlow.addColorStop(0.8, `rgba(${rgb}, ${blob.opacity * 0.1})`);\n outerGlow.addColorStop(1, `rgba(${rgb}, 0)`);\n ctx.beginPath();\n ctx.arc(blob.x, blob.y, blurRadius, 0, Math.PI * 2);\n ctx.fillStyle = outerGlow;\n ctx.fill();\n });\n\n // Highlight\n const highlightGradient = ctx.createRadialGradient(\n centerX - currentRadius * 0.3,\n centerY - currentRadius * 0.3,\n 0,\n centerX - currentRadius * 0.3,\n centerY - currentRadius * 0.3,\n currentRadius * 0.6\n );\n highlightGradient.addColorStop(0, 'rgba(255, 255, 255, 0.12)');\n highlightGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.03)');\n highlightGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');\n ctx.beginPath();\n ctx.arc(centerX, centerY, currentRadius, 0, Math.PI * 2);\n ctx.fillStyle = highlightGradient;\n ctx.fill();\n\n ctx.restore();\n\n this.pulseAnimationId = requestAnimationFrame(this.animatePulse);\n };\n\n private hexToRgb(hex: string): string {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) {\n return '0, 0, 0';\n }\n const r = parseInt(result[1], 16);\n const g = parseInt(result[2], 16);\n const b = parseInt(result[3], 16);\n return `${r}, ${g}, ${b}`;\n }\n\n private cleanup(): void {\n if (this.animationFrameId) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = undefined;\n }\n if (this.pulseAnimationId) {\n cancelAnimationFrame(this.pulseAnimationId);\n this.pulseAnimationId = undefined;\n }\n if (this.source) {\n this.source.disconnect();\n this.source = undefined;\n }\n if (this.candidateSource) {\n this.candidateSource.disconnect();\n this.candidateSource = undefined;\n }\n if (this.audioContext && this.audioContext.state !== 'closed') {\n void this.audioContext.close();\n this.audioContext = undefined;\n }\n this.analyser = undefined;\n this.candidateAnalyser = undefined;\n this.dataArray = undefined;\n this.candidateDataArray = undefined;\n }\n\n ngOnDestroy(): void {\n this.cleanup();\n }\n}\n","<div class=\"ai-audio-visualizer\">\n <div class=\"canvas-container\">\n <canvas #pulseCanvas></canvas>\n </div>\n</div>\n","import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { AiAudioCircleComponent } from './ai-audio-circle.component';\n\n@NgModule({\n declarations: [AiAudioCircleComponent],\n imports: [CommonModule],\n exports: [AiAudioCircleComponent],\n})\nexport class AiAudioCircleModule {}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|