nodejs-audio-visualizer 4.3.7 → 4.3.9
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/LICENSE +21 -21
- package/README.md +75 -75
- package/dist/audio.d.ts +12 -12
- package/dist/audio.js +120 -120
- package/dist/beats.d.ts +19 -19
- package/dist/beats.js +43 -43
- package/dist/bpmEncoder.d.ts +14 -14
- package/dist/bpmEncoder.js +97 -97
- package/dist/config.d.ts +67 -67
- package/dist/config.js +356 -356
- package/dist/dsp.d.ts +16 -16
- package/dist/dsp.js +76 -76
- package/dist/highlight.d.ts +32 -32
- package/dist/highlight.js +325 -325
- package/dist/image.d.ts +79 -79
- package/dist/image.js +458 -458
- package/dist/index.d.ts +72 -72
- package/dist/index.js +539 -541
- package/dist/index.js.map +1 -1
- package/dist/subtitleConvert.d.ts +3 -3
- package/dist/subtitleConvert.js +61 -61
- package/dist/video.d.ts +77 -72
- package/dist/video.js +407 -338
- package/dist/video.js.map +1 -1
- package/dist/waitForEventLoop.d.ts +1 -1
- package/dist/waitForEventLoop.js +5 -5
- package/package.json +61 -61
- package/src/audio.ts +120 -120
- package/src/beats.ts +68 -68
- package/src/bpmEncoder.ts +97 -97
- package/src/config.ts +462 -462
- package/src/dsp.ts +95 -95
- package/src/highlight.ts +284 -284
- package/src/image.ts +602 -602
- package/src/index.ts +693 -678
- package/src/subtitleConvert.ts +63 -63
- package/src/tests/audio.test.ts +96 -96
- package/src/tests/config.test.ts +475 -475
- package/src/tests/dsp.test.ts +24 -24
- package/src/tests/highlight.test.ts +178 -178
- package/src/tests/image.test.ts +222 -222
- package/src/tests/index.test.ts +156 -63
- package/src/tests/subtitleConvert.test.ts +23 -23
- package/src/tests/video.test.ts +214 -138
- package/src/typings.d.ts +6 -6
- package/src/video.ts +517 -437
- package/src/waitForEventLoop.ts +2 -2
package/src/image.ts
CHANGED
|
@@ -1,602 +1,602 @@
|
|
|
1
|
-
import { RotationAliasName } from './index';
|
|
2
|
-
import { decode, BmpDecoder } from 'bmp-js';
|
|
3
|
-
import { EncodedBmp } from './bpmEncoder';
|
|
4
|
-
import Jimp from 'jimp';
|
|
5
|
-
import { SpectrumEffect } from './config';
|
|
6
|
-
|
|
7
|
-
export interface Color {
|
|
8
|
-
red: number;
|
|
9
|
-
green: number;
|
|
10
|
-
blue: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface Position {
|
|
14
|
-
x: number;
|
|
15
|
-
y: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface Size {
|
|
19
|
-
width: number;
|
|
20
|
-
height: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface MixColorProps {
|
|
24
|
-
color: Color;
|
|
25
|
-
colorOpacity: number;
|
|
26
|
-
backgroundColor: Color;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const mixValues = (value1: number, value1Opacity: number, value2: number) =>
|
|
30
|
-
Math.round(value2 * (1 - value1Opacity) + value1 * value1Opacity);
|
|
31
|
-
|
|
32
|
-
export const mixColors = ({
|
|
33
|
-
color,
|
|
34
|
-
colorOpacity,
|
|
35
|
-
backgroundColor,
|
|
36
|
-
}: MixColorProps): Color => {
|
|
37
|
-
return {
|
|
38
|
-
red: mixValues(color.red, colorOpacity, backgroundColor.red),
|
|
39
|
-
green: mixValues(color.green, colorOpacity, backgroundColor.green),
|
|
40
|
-
blue: mixValues(color.blue, colorOpacity, backgroundColor.blue),
|
|
41
|
-
};
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const getRectPixelColor = (params: {
|
|
45
|
-
imageDstBuffer: EncodedBmp;
|
|
46
|
-
pixelIndex: number;
|
|
47
|
-
color: Color;
|
|
48
|
-
opacity: number;
|
|
49
|
-
}) => {
|
|
50
|
-
const {
|
|
51
|
-
imageDstBuffer,
|
|
52
|
-
pixelIndex,
|
|
53
|
-
color,
|
|
54
|
-
opacity,
|
|
55
|
-
} = params;
|
|
56
|
-
if (opacity === 1) {
|
|
57
|
-
return color;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const backgroundPixelColor: Color = {
|
|
61
|
-
blue: imageDstBuffer.data[pixelIndex],
|
|
62
|
-
green: imageDstBuffer.data[pixelIndex + 1],
|
|
63
|
-
red: imageDstBuffer.data[pixelIndex + 2],
|
|
64
|
-
};
|
|
65
|
-
const resultColor = mixColors({
|
|
66
|
-
color,
|
|
67
|
-
colorOpacity: opacity,
|
|
68
|
-
backgroundColor: backgroundPixelColor,
|
|
69
|
-
});
|
|
70
|
-
return resultColor;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
interface DrawRectPorps {
|
|
74
|
-
imageDstBuffer: EncodedBmp;
|
|
75
|
-
position: Position;
|
|
76
|
-
size: Size;
|
|
77
|
-
color: Color;
|
|
78
|
-
opacity: number;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export const drawRect = ({
|
|
82
|
-
imageDstBuffer,
|
|
83
|
-
position,
|
|
84
|
-
size,
|
|
85
|
-
color,
|
|
86
|
-
opacity,
|
|
87
|
-
}: DrawRectPorps) => {
|
|
88
|
-
for (let currY = position.y; currY < position.y + size.height; currY++) {
|
|
89
|
-
for (let currX = position.x; currX < position.x + size.width; currX++) {
|
|
90
|
-
const pixelIndex = imageDstBuffer.shiftPos + currY * imageDstBuffer.rowBytes + currX * 3;
|
|
91
|
-
const pixelColor = getRectPixelColor({
|
|
92
|
-
imageDstBuffer,
|
|
93
|
-
pixelIndex,
|
|
94
|
-
color,
|
|
95
|
-
opacity,
|
|
96
|
-
});
|
|
97
|
-
imageDstBuffer.data[pixelIndex] = pixelColor.blue;
|
|
98
|
-
imageDstBuffer.data[pixelIndex + 1] = pixelColor.green;
|
|
99
|
-
imageDstBuffer.data[pixelIndex + 2] = pixelColor.red;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
interface Edge {
|
|
105
|
-
yMin: number;
|
|
106
|
-
yMax: number;
|
|
107
|
-
x: number;
|
|
108
|
-
dx: number;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
interface FillRectProps {
|
|
112
|
-
imageDstBuffer: EncodedBmp;
|
|
113
|
-
points: [Position, Position, Position, Position];
|
|
114
|
-
color: Color;
|
|
115
|
-
opacity: number;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const buildQuadEdgeTable = (points: [Position, Position, Position, Position]): Edge[] => {
|
|
119
|
-
const edges: Edge[] = [];
|
|
120
|
-
|
|
121
|
-
for (let i = 0; i < points.length; i++) {
|
|
122
|
-
const p1 = points[i];
|
|
123
|
-
const p2 = points[(i + 1) % points.length];
|
|
124
|
-
|
|
125
|
-
if (p1.y === p2.y) continue; // Skip horizontal edges
|
|
126
|
-
|
|
127
|
-
const yMin = Math.min(p1.y, p2.y);
|
|
128
|
-
const yMax = Math.max(p1.y, p2.y);
|
|
129
|
-
const x = p1.y < p2.y ? p1.x : p2.x;
|
|
130
|
-
const dx = (p2.x - p1.x) / (p2.y - p1.y);
|
|
131
|
-
|
|
132
|
-
edges.push({ yMin, yMax, x, dx });
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return edges.sort((a, b) => a.yMin - b.yMin);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Fills a quadrilateral (4-sided polygon) using scan-line algorithm
|
|
140
|
-
* Only the outer edges are anti-aliased
|
|
141
|
-
*/
|
|
142
|
-
export const fillRect = ({
|
|
143
|
-
imageDstBuffer,
|
|
144
|
-
points,
|
|
145
|
-
color,
|
|
146
|
-
opacity,
|
|
147
|
-
}: FillRectProps) => {
|
|
148
|
-
// Build edge table from all 4 vertices
|
|
149
|
-
const edgeTable = buildQuadEdgeTable(points);
|
|
150
|
-
if (edgeTable.length === 0) return;
|
|
151
|
-
|
|
152
|
-
// Find the bounding box
|
|
153
|
-
const yMin = Math.min(...points.map(p => p.y));
|
|
154
|
-
const yMax = Math.max(...points.map(p => p.y));
|
|
155
|
-
|
|
156
|
-
// Active Edge Table for scan line algorithm
|
|
157
|
-
const activeEdges: Edge[] = [];
|
|
158
|
-
|
|
159
|
-
// Scan line algorithm: process each horizontal line
|
|
160
|
-
for (let y = Math.floor(yMin); y <= Math.ceil(yMax); y++) {
|
|
161
|
-
// Add edges that start at this scanline to AET
|
|
162
|
-
for (const edge of edgeTable) {
|
|
163
|
-
if (Math.floor(edge.yMin) === y) {
|
|
164
|
-
activeEdges.push({ ...edge });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Remove edges that end at this scanline from AET
|
|
169
|
-
for (let i = activeEdges.length - 1; i >= 0; i--) {
|
|
170
|
-
if (Math.ceil(activeEdges[i].yMax) <= y) {
|
|
171
|
-
activeEdges.splice(i, 1);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Sort active edges by x coordinate
|
|
176
|
-
activeEdges.sort((a, b) => a.x - b.x);
|
|
177
|
-
|
|
178
|
-
// Fill pixels between pairs of intersections
|
|
179
|
-
for (let i = 0; i < activeEdges.length; i += 2) {
|
|
180
|
-
if (i + 1 < activeEdges.length) {
|
|
181
|
-
const xStart = activeEdges[i].x;
|
|
182
|
-
const xEnd = activeEdges[i + 1].x;
|
|
183
|
-
|
|
184
|
-
// Anti-aliased rendering with sub-pixel precision
|
|
185
|
-
const xStartFloor = Math.floor(xStart);
|
|
186
|
-
const xEndCeil = Math.ceil(xEnd);
|
|
187
|
-
|
|
188
|
-
for (let x = xStartFloor; x < xEndCeil; x++) {
|
|
189
|
-
const pixelIndex = imageDstBuffer.shiftPos + y * imageDstBuffer.rowBytes + x * 3;
|
|
190
|
-
|
|
191
|
-
// Calculate coverage for anti-aliasing
|
|
192
|
-
let coverage = 1.0;
|
|
193
|
-
if (x === xStartFloor) {
|
|
194
|
-
// Left edge pixel - calculate fractional coverage
|
|
195
|
-
coverage = Math.min(1.0, 1.0 - (xStart - xStartFloor));
|
|
196
|
-
} else if (x === xEndCeil - 1) {
|
|
197
|
-
// Right edge pixel - calculate fractional coverage
|
|
198
|
-
coverage = Math.min(1.0, xEnd - Math.floor(xEnd));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Apply coverage to opacity for smooth edges
|
|
202
|
-
const effectiveOpacity = opacity * coverage;
|
|
203
|
-
|
|
204
|
-
const pixelColor = getRectPixelColor({
|
|
205
|
-
imageDstBuffer,
|
|
206
|
-
pixelIndex,
|
|
207
|
-
color,
|
|
208
|
-
opacity: effectiveOpacity,
|
|
209
|
-
});
|
|
210
|
-
imageDstBuffer.data[pixelIndex] = pixelColor.blue;
|
|
211
|
-
imageDstBuffer.data[pixelIndex + 1] = pixelColor.green;
|
|
212
|
-
imageDstBuffer.data[pixelIndex + 2] = pixelColor.red;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Update x coordinates for next scanline (x' = x + dx)
|
|
218
|
-
for (const edge of activeEdges) {
|
|
219
|
-
edge.x += edge.dx;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
interface DrawSpectrumProps {
|
|
225
|
-
imageDstBuffer: EncodedBmp;
|
|
226
|
-
spectrum: number[];
|
|
227
|
-
size: Size;
|
|
228
|
-
position: Position;
|
|
229
|
-
rotation: RotationAliasName;
|
|
230
|
-
margin: number;
|
|
231
|
-
color: Color;
|
|
232
|
-
opacity: number;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const getRectY = (rotation: RotationAliasName, top: number, height: number, rectHeight: number) => {
|
|
236
|
-
switch (rotation) {
|
|
237
|
-
case 'up':
|
|
238
|
-
return top + height - rectHeight;
|
|
239
|
-
case 'mirror':
|
|
240
|
-
return top + Math.trunc((height - rectHeight) / 2);
|
|
241
|
-
default:
|
|
242
|
-
return top;
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const minRectHeight = 2;
|
|
247
|
-
|
|
248
|
-
const getRectHeight = (height: number, spectrumValue: number) => {
|
|
249
|
-
const rectHeight = Math.trunc(height * spectrumValue);
|
|
250
|
-
if (rectHeight < minRectHeight) {
|
|
251
|
-
return minRectHeight;
|
|
252
|
-
}
|
|
253
|
-
return rectHeight;
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const drawSpectrum = ({
|
|
257
|
-
imageDstBuffer,
|
|
258
|
-
spectrum,
|
|
259
|
-
size,
|
|
260
|
-
position,
|
|
261
|
-
rotation,
|
|
262
|
-
margin,
|
|
263
|
-
color,
|
|
264
|
-
opacity,
|
|
265
|
-
}: DrawSpectrumProps) => {
|
|
266
|
-
const busWidth = Math.trunc(size.width / spectrum.length);
|
|
267
|
-
const left = Math.trunc(position.x);
|
|
268
|
-
const top = Math.trunc(position.y);
|
|
269
|
-
const height = Math.trunc(size.height);
|
|
270
|
-
|
|
271
|
-
for (let spectrumX = 0; spectrumX < spectrum.length; spectrumX++) {
|
|
272
|
-
const spectrumValue = spectrum[spectrumX];
|
|
273
|
-
if ((spectrumValue > 1) || (spectrumValue < 0)) {
|
|
274
|
-
throw new Error('Spectrum values must be in range from 0 to 1');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const rectX = left + busWidth * spectrumX;
|
|
278
|
-
const rectHeight = getRectHeight(height, spectrumValue);
|
|
279
|
-
const rectY = getRectY(rotation, top, height, rectHeight);
|
|
280
|
-
drawRect({
|
|
281
|
-
imageDstBuffer,
|
|
282
|
-
position: { x: rectX + margin, y: rectY },
|
|
283
|
-
size: { width: busWidth - margin / 2, height: rectHeight },
|
|
284
|
-
color,
|
|
285
|
-
opacity,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const calcCirclePoint = (angleRadians: number, radius: number): Position => {
|
|
291
|
-
const circleX = radius * Math.cos(angleRadians);
|
|
292
|
-
const circleY = radius * Math.sin(angleRadians);
|
|
293
|
-
return { x: circleX, y: circleY };
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const lerp = (start: number, end: number, t: number) =>
|
|
297
|
-
start + (end - start) * t;
|
|
298
|
-
|
|
299
|
-
interface DrawPolarSpectrumProps {
|
|
300
|
-
imageDstBuffer: EncodedBmp;
|
|
301
|
-
spectrum: number[];
|
|
302
|
-
centerX: number;
|
|
303
|
-
centerY: number;
|
|
304
|
-
innerRadius: number;
|
|
305
|
-
maxBarLength: number;
|
|
306
|
-
barWidth: number;
|
|
307
|
-
color: Color;
|
|
308
|
-
opacity: number;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const drawPolarSpectrum = ({
|
|
312
|
-
imageDstBuffer,
|
|
313
|
-
spectrum,
|
|
314
|
-
centerX,
|
|
315
|
-
centerY,
|
|
316
|
-
innerRadius,
|
|
317
|
-
maxBarLength,
|
|
318
|
-
barWidth,
|
|
319
|
-
color,
|
|
320
|
-
opacity,
|
|
321
|
-
}: DrawPolarSpectrumProps) => {
|
|
322
|
-
const angleStep = (2 * Math.PI) / spectrum.length;
|
|
323
|
-
const innerLength = innerRadius * 2 * Math.PI;
|
|
324
|
-
const radiansPerPixel = 2 * Math.PI / innerLength;
|
|
325
|
-
const barWidthInRadians = radiansPerPixel * barWidth;
|
|
326
|
-
const spectrumAverage = spectrum.reduce((a, b) => a + b, 0) / spectrum.length;
|
|
327
|
-
const scaleFactor = lerp(0.75, 1, spectrumAverage);
|
|
328
|
-
const scaledMaxOuterRadius = (innerRadius + maxBarLength) * scaleFactor;
|
|
329
|
-
const scaledInnerRadius = innerRadius * scaleFactor;
|
|
330
|
-
for (let i = 0; i < spectrum.length; i++) {
|
|
331
|
-
const currentSpectrumValue = spectrum[i] || 0;
|
|
332
|
-
const busRadius = lerp(scaledInnerRadius, scaledMaxOuterRadius, currentSpectrumValue);
|
|
333
|
-
const angle = i * angleStep;
|
|
334
|
-
const angleRadiansStart = angle;
|
|
335
|
-
const angleRadiansEnd = angle + barWidthInRadians;
|
|
336
|
-
|
|
337
|
-
const point1 = calcCirclePoint(angleRadiansStart, scaledInnerRadius);
|
|
338
|
-
const point2 = calcCirclePoint(angleRadiansStart, busRadius);
|
|
339
|
-
|
|
340
|
-
const point3 = calcCirclePoint(angleRadiansEnd, scaledInnerRadius);
|
|
341
|
-
const point4 = calcCirclePoint(angleRadiansEnd, busRadius);
|
|
342
|
-
|
|
343
|
-
fillRect({
|
|
344
|
-
imageDstBuffer,
|
|
345
|
-
points: [
|
|
346
|
-
{ x: centerX + Math.round(point1.x), y: centerY + Math.round(point1.y) },
|
|
347
|
-
{ x: centerX + Math.round(point2.x), y: centerY + Math.round(point2.y) },
|
|
348
|
-
{ x: centerX + Math.round(point4.x), y: centerY + Math.round(point4.y) },
|
|
349
|
-
{ x: centerX + Math.round(point3.x), y: centerY + Math.round(point3.y) },
|
|
350
|
-
],
|
|
351
|
-
color,
|
|
352
|
-
opacity,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
export interface CommonVisualizerFrameProps {
|
|
358
|
-
backgroundImageBuffer: EncodedBmp;
|
|
359
|
-
spectrum: number[];
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export interface CreateVisualizerFrameProps {
|
|
363
|
-
size: Size;
|
|
364
|
-
position: Position;
|
|
365
|
-
rotation: RotationAliasName;
|
|
366
|
-
margin: number;
|
|
367
|
-
color: Color | string;
|
|
368
|
-
opacity: number;
|
|
369
|
-
spectrumEffect?: SpectrumEffect;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export interface CreatePolarVisualizerFrameProps {
|
|
373
|
-
centerX: number;
|
|
374
|
-
centerY: number;
|
|
375
|
-
innerRadius: number;
|
|
376
|
-
maxBarLength: number;
|
|
377
|
-
barWidth: number;
|
|
378
|
-
color: Color | string;
|
|
379
|
-
opacity: number;
|
|
380
|
-
spectrumEffect?: SpectrumEffect;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export const createSpectrumVisualizerFrameGenerator = () => {
|
|
384
|
-
let prevSpectrum: number[] | null = null;
|
|
385
|
-
|
|
386
|
-
return ({
|
|
387
|
-
backgroundImageBuffer,
|
|
388
|
-
spectrum,
|
|
389
|
-
size,
|
|
390
|
-
position,
|
|
391
|
-
rotation,
|
|
392
|
-
margin,
|
|
393
|
-
color,
|
|
394
|
-
opacity,
|
|
395
|
-
spectrumEffect,
|
|
396
|
-
}: CommonVisualizerFrameProps & CreateVisualizerFrameProps) => {
|
|
397
|
-
const imageDstBuffer = Object.assign({}, backgroundImageBuffer);
|
|
398
|
-
imageDstBuffer.data = Buffer.from(backgroundImageBuffer.data);
|
|
399
|
-
|
|
400
|
-
const rgbSpectrumColor = (typeof color === 'string') ? hexToRgb(color) : color;
|
|
401
|
-
if (spectrumEffect === 'volume') {
|
|
402
|
-
drawSpectrum({
|
|
403
|
-
imageDstBuffer,
|
|
404
|
-
spectrum,
|
|
405
|
-
size,
|
|
406
|
-
position: { x: position.x + 2, y: position.y + 2 },
|
|
407
|
-
rotation,
|
|
408
|
-
margin,
|
|
409
|
-
color: rgbSpectrumColor,
|
|
410
|
-
opacity: opacity * 0.5,
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
if (spectrumEffect === 'smooth' && prevSpectrum) {
|
|
414
|
-
drawSpectrum({
|
|
415
|
-
imageDstBuffer,
|
|
416
|
-
spectrum: prevSpectrum,
|
|
417
|
-
size,
|
|
418
|
-
position,
|
|
419
|
-
rotation,
|
|
420
|
-
margin,
|
|
421
|
-
color: rgbSpectrumColor,
|
|
422
|
-
opacity: opacity * 0.3,
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
drawSpectrum({
|
|
426
|
-
imageDstBuffer,
|
|
427
|
-
spectrum,
|
|
428
|
-
size,
|
|
429
|
-
position,
|
|
430
|
-
rotation,
|
|
431
|
-
margin,
|
|
432
|
-
color: rgbSpectrumColor,
|
|
433
|
-
opacity,
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
prevSpectrum = spectrum;
|
|
437
|
-
|
|
438
|
-
return imageDstBuffer;
|
|
439
|
-
};
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
export const createPolarVisualizerFrameGenerator = () => {
|
|
443
|
-
let prevSpectrum: number[] | null = null;
|
|
444
|
-
|
|
445
|
-
return ({
|
|
446
|
-
backgroundImageBuffer,
|
|
447
|
-
spectrum,
|
|
448
|
-
centerX,
|
|
449
|
-
centerY,
|
|
450
|
-
innerRadius,
|
|
451
|
-
maxBarLength,
|
|
452
|
-
barWidth,
|
|
453
|
-
color,
|
|
454
|
-
opacity,
|
|
455
|
-
spectrumEffect,
|
|
456
|
-
}: CommonVisualizerFrameProps & CreatePolarVisualizerFrameProps) => {
|
|
457
|
-
const imageDstBuffer = Object.assign({}, backgroundImageBuffer);
|
|
458
|
-
imageDstBuffer.data = Buffer.from(backgroundImageBuffer.data);
|
|
459
|
-
|
|
460
|
-
const rgbSpectrumColor = (typeof color === 'string') ? hexToRgb(color) : color;
|
|
461
|
-
|
|
462
|
-
if (spectrumEffect === 'volume') {
|
|
463
|
-
drawPolarSpectrum({
|
|
464
|
-
imageDstBuffer,
|
|
465
|
-
spectrum,
|
|
466
|
-
centerX: centerX + 2,
|
|
467
|
-
centerY: centerY + 2,
|
|
468
|
-
innerRadius,
|
|
469
|
-
maxBarLength,
|
|
470
|
-
barWidth,
|
|
471
|
-
color: rgbSpectrumColor,
|
|
472
|
-
opacity: opacity * 0.5,
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (spectrumEffect === 'smooth' && prevSpectrum) {
|
|
477
|
-
drawPolarSpectrum({
|
|
478
|
-
imageDstBuffer,
|
|
479
|
-
spectrum: prevSpectrum,
|
|
480
|
-
centerX,
|
|
481
|
-
centerY,
|
|
482
|
-
innerRadius,
|
|
483
|
-
maxBarLength,
|
|
484
|
-
barWidth,
|
|
485
|
-
color: rgbSpectrumColor,
|
|
486
|
-
opacity: opacity * 0.3,
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
drawPolarSpectrum({
|
|
491
|
-
imageDstBuffer,
|
|
492
|
-
spectrum,
|
|
493
|
-
centerX,
|
|
494
|
-
centerY,
|
|
495
|
-
innerRadius,
|
|
496
|
-
maxBarLength,
|
|
497
|
-
barWidth,
|
|
498
|
-
color: rgbSpectrumColor,
|
|
499
|
-
opacity,
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
prevSpectrum = spectrum;
|
|
503
|
-
|
|
504
|
-
return imageDstBuffer;
|
|
505
|
-
};
|
|
506
|
-
};
|
|
507
|
-
|
|
508
|
-
export const convertToBmp = async (filePath: string, targetWidth?: number, targetHeight?: number) =>
|
|
509
|
-
new Promise<Buffer>(async (resolve, reject) => {
|
|
510
|
-
try {
|
|
511
|
-
const image = await Jimp.read(filePath);
|
|
512
|
-
if (targetWidth && targetHeight) {
|
|
513
|
-
const sw = image.getWidth();
|
|
514
|
-
const sh = image.getHeight();
|
|
515
|
-
const isOrientationMismatch =
|
|
516
|
-
(sw > sh && targetWidth < targetHeight) ||
|
|
517
|
-
(sw < sh && targetWidth > targetHeight);
|
|
518
|
-
|
|
519
|
-
if (isOrientationMismatch) {
|
|
520
|
-
const targetAspect = targetWidth / targetHeight;
|
|
521
|
-
let cropW: number, cropH: number, cropX: number, cropY: number;
|
|
522
|
-
if (sw / sh > targetAspect) {
|
|
523
|
-
cropW = Math.round(sh * targetAspect);
|
|
524
|
-
cropH = sh;
|
|
525
|
-
cropX = Math.round((sw - cropW) / 2);
|
|
526
|
-
cropY = 0;
|
|
527
|
-
} else {
|
|
528
|
-
cropW = sw;
|
|
529
|
-
cropH = Math.round(sw / targetAspect);
|
|
530
|
-
cropX = 0;
|
|
531
|
-
cropY = Math.round((sh - cropH) / 2);
|
|
532
|
-
}
|
|
533
|
-
image.crop(cropX, cropY, cropW, cropH);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
image.resize(targetWidth, targetHeight);
|
|
537
|
-
}
|
|
538
|
-
image.getBuffer("image/bmp", (err, value) => {
|
|
539
|
-
if (err) {
|
|
540
|
-
reject(err);
|
|
541
|
-
}
|
|
542
|
-
resolve(value);
|
|
543
|
-
});
|
|
544
|
-
} catch {
|
|
545
|
-
throw new Error(`Cannot procces image file from path: ${filePath}`);
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
export const parseImage = (buffer: Buffer) => decode(buffer);
|
|
550
|
-
|
|
551
|
-
export const getImageColor = (image: BmpDecoder): Color => {
|
|
552
|
-
let blueSum = 0;
|
|
553
|
-
let greenSum = 0;
|
|
554
|
-
let redSum = 0;
|
|
555
|
-
|
|
556
|
-
for (let currY = 0; currY < image.height; currY++) {
|
|
557
|
-
for (let currX = 0; currX < image.width; currX++) {
|
|
558
|
-
const idx = (image.width * currY + currX) << 2;
|
|
559
|
-
|
|
560
|
-
blueSum += image.data[idx + 1];
|
|
561
|
-
greenSum += image.data[idx + 2];
|
|
562
|
-
redSum += image.data[idx + 3];
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const pixelsCount = image.width * image.height;
|
|
567
|
-
return {
|
|
568
|
-
red: ~~(redSum / pixelsCount),
|
|
569
|
-
green: ~~(greenSum / pixelsCount),
|
|
570
|
-
blue: ~~(blueSum / pixelsCount)
|
|
571
|
-
};
|
|
572
|
-
};
|
|
573
|
-
|
|
574
|
-
export const getVideoFrameColor = (bgrBuffer: Buffer, width: number, height: number): Color => {
|
|
575
|
-
let blueSum = 0;
|
|
576
|
-
let greenSum = 0;
|
|
577
|
-
let redSum = 0;
|
|
578
|
-
const pixelsCount = width * height;
|
|
579
|
-
|
|
580
|
-
for (let i = 0; i < bgrBuffer.length; i += 3) {
|
|
581
|
-
blueSum += bgrBuffer[i];
|
|
582
|
-
greenSum += bgrBuffer[i + 1];
|
|
583
|
-
redSum += bgrBuffer[i + 2];
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
return {
|
|
587
|
-
red: ~~(redSum / pixelsCount),
|
|
588
|
-
green: ~~(greenSum / pixelsCount),
|
|
589
|
-
blue: ~~(blueSum / pixelsCount),
|
|
590
|
-
};
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
export const invertColor = (color: Color) =>
|
|
594
|
-
({ red: 255 - color.red, green: 255 - color.green, blue: 255 - color.blue });
|
|
595
|
-
|
|
596
|
-
const hexToRgb = (hex: String) => {
|
|
597
|
-
const red = parseInt(hex.substring(1, 3), 16);
|
|
598
|
-
const green = parseInt(hex.substring(3, 5), 16);
|
|
599
|
-
const blue = parseInt(hex.substring(5, 7), 16);
|
|
600
|
-
|
|
601
|
-
return { red, green, blue };
|
|
602
|
-
};
|
|
1
|
+
import { RotationAliasName } from './index';
|
|
2
|
+
import { decode, BmpDecoder } from 'bmp-js';
|
|
3
|
+
import { EncodedBmp } from './bpmEncoder';
|
|
4
|
+
import Jimp from 'jimp';
|
|
5
|
+
import { SpectrumEffect } from './config';
|
|
6
|
+
|
|
7
|
+
export interface Color {
|
|
8
|
+
red: number;
|
|
9
|
+
green: number;
|
|
10
|
+
blue: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Position {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Size {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface MixColorProps {
|
|
24
|
+
color: Color;
|
|
25
|
+
colorOpacity: number;
|
|
26
|
+
backgroundColor: Color;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const mixValues = (value1: number, value1Opacity: number, value2: number) =>
|
|
30
|
+
Math.round(value2 * (1 - value1Opacity) + value1 * value1Opacity);
|
|
31
|
+
|
|
32
|
+
export const mixColors = ({
|
|
33
|
+
color,
|
|
34
|
+
colorOpacity,
|
|
35
|
+
backgroundColor,
|
|
36
|
+
}: MixColorProps): Color => {
|
|
37
|
+
return {
|
|
38
|
+
red: mixValues(color.red, colorOpacity, backgroundColor.red),
|
|
39
|
+
green: mixValues(color.green, colorOpacity, backgroundColor.green),
|
|
40
|
+
blue: mixValues(color.blue, colorOpacity, backgroundColor.blue),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getRectPixelColor = (params: {
|
|
45
|
+
imageDstBuffer: EncodedBmp;
|
|
46
|
+
pixelIndex: number;
|
|
47
|
+
color: Color;
|
|
48
|
+
opacity: number;
|
|
49
|
+
}) => {
|
|
50
|
+
const {
|
|
51
|
+
imageDstBuffer,
|
|
52
|
+
pixelIndex,
|
|
53
|
+
color,
|
|
54
|
+
opacity,
|
|
55
|
+
} = params;
|
|
56
|
+
if (opacity === 1) {
|
|
57
|
+
return color;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const backgroundPixelColor: Color = {
|
|
61
|
+
blue: imageDstBuffer.data[pixelIndex],
|
|
62
|
+
green: imageDstBuffer.data[pixelIndex + 1],
|
|
63
|
+
red: imageDstBuffer.data[pixelIndex + 2],
|
|
64
|
+
};
|
|
65
|
+
const resultColor = mixColors({
|
|
66
|
+
color,
|
|
67
|
+
colorOpacity: opacity,
|
|
68
|
+
backgroundColor: backgroundPixelColor,
|
|
69
|
+
});
|
|
70
|
+
return resultColor;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
interface DrawRectPorps {
|
|
74
|
+
imageDstBuffer: EncodedBmp;
|
|
75
|
+
position: Position;
|
|
76
|
+
size: Size;
|
|
77
|
+
color: Color;
|
|
78
|
+
opacity: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const drawRect = ({
|
|
82
|
+
imageDstBuffer,
|
|
83
|
+
position,
|
|
84
|
+
size,
|
|
85
|
+
color,
|
|
86
|
+
opacity,
|
|
87
|
+
}: DrawRectPorps) => {
|
|
88
|
+
for (let currY = position.y; currY < position.y + size.height; currY++) {
|
|
89
|
+
for (let currX = position.x; currX < position.x + size.width; currX++) {
|
|
90
|
+
const pixelIndex = imageDstBuffer.shiftPos + currY * imageDstBuffer.rowBytes + currX * 3;
|
|
91
|
+
const pixelColor = getRectPixelColor({
|
|
92
|
+
imageDstBuffer,
|
|
93
|
+
pixelIndex,
|
|
94
|
+
color,
|
|
95
|
+
opacity,
|
|
96
|
+
});
|
|
97
|
+
imageDstBuffer.data[pixelIndex] = pixelColor.blue;
|
|
98
|
+
imageDstBuffer.data[pixelIndex + 1] = pixelColor.green;
|
|
99
|
+
imageDstBuffer.data[pixelIndex + 2] = pixelColor.red;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
interface Edge {
|
|
105
|
+
yMin: number;
|
|
106
|
+
yMax: number;
|
|
107
|
+
x: number;
|
|
108
|
+
dx: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface FillRectProps {
|
|
112
|
+
imageDstBuffer: EncodedBmp;
|
|
113
|
+
points: [Position, Position, Position, Position];
|
|
114
|
+
color: Color;
|
|
115
|
+
opacity: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const buildQuadEdgeTable = (points: [Position, Position, Position, Position]): Edge[] => {
|
|
119
|
+
const edges: Edge[] = [];
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < points.length; i++) {
|
|
122
|
+
const p1 = points[i];
|
|
123
|
+
const p2 = points[(i + 1) % points.length];
|
|
124
|
+
|
|
125
|
+
if (p1.y === p2.y) continue; // Skip horizontal edges
|
|
126
|
+
|
|
127
|
+
const yMin = Math.min(p1.y, p2.y);
|
|
128
|
+
const yMax = Math.max(p1.y, p2.y);
|
|
129
|
+
const x = p1.y < p2.y ? p1.x : p2.x;
|
|
130
|
+
const dx = (p2.x - p1.x) / (p2.y - p1.y);
|
|
131
|
+
|
|
132
|
+
edges.push({ yMin, yMax, x, dx });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return edges.sort((a, b) => a.yMin - b.yMin);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Fills a quadrilateral (4-sided polygon) using scan-line algorithm
|
|
140
|
+
* Only the outer edges are anti-aliased
|
|
141
|
+
*/
|
|
142
|
+
export const fillRect = ({
|
|
143
|
+
imageDstBuffer,
|
|
144
|
+
points,
|
|
145
|
+
color,
|
|
146
|
+
opacity,
|
|
147
|
+
}: FillRectProps) => {
|
|
148
|
+
// Build edge table from all 4 vertices
|
|
149
|
+
const edgeTable = buildQuadEdgeTable(points);
|
|
150
|
+
if (edgeTable.length === 0) return;
|
|
151
|
+
|
|
152
|
+
// Find the bounding box
|
|
153
|
+
const yMin = Math.min(...points.map(p => p.y));
|
|
154
|
+
const yMax = Math.max(...points.map(p => p.y));
|
|
155
|
+
|
|
156
|
+
// Active Edge Table for scan line algorithm
|
|
157
|
+
const activeEdges: Edge[] = [];
|
|
158
|
+
|
|
159
|
+
// Scan line algorithm: process each horizontal line
|
|
160
|
+
for (let y = Math.floor(yMin); y <= Math.ceil(yMax); y++) {
|
|
161
|
+
// Add edges that start at this scanline to AET
|
|
162
|
+
for (const edge of edgeTable) {
|
|
163
|
+
if (Math.floor(edge.yMin) === y) {
|
|
164
|
+
activeEdges.push({ ...edge });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Remove edges that end at this scanline from AET
|
|
169
|
+
for (let i = activeEdges.length - 1; i >= 0; i--) {
|
|
170
|
+
if (Math.ceil(activeEdges[i].yMax) <= y) {
|
|
171
|
+
activeEdges.splice(i, 1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Sort active edges by x coordinate
|
|
176
|
+
activeEdges.sort((a, b) => a.x - b.x);
|
|
177
|
+
|
|
178
|
+
// Fill pixels between pairs of intersections
|
|
179
|
+
for (let i = 0; i < activeEdges.length; i += 2) {
|
|
180
|
+
if (i + 1 < activeEdges.length) {
|
|
181
|
+
const xStart = activeEdges[i].x;
|
|
182
|
+
const xEnd = activeEdges[i + 1].x;
|
|
183
|
+
|
|
184
|
+
// Anti-aliased rendering with sub-pixel precision
|
|
185
|
+
const xStartFloor = Math.floor(xStart);
|
|
186
|
+
const xEndCeil = Math.ceil(xEnd);
|
|
187
|
+
|
|
188
|
+
for (let x = xStartFloor; x < xEndCeil; x++) {
|
|
189
|
+
const pixelIndex = imageDstBuffer.shiftPos + y * imageDstBuffer.rowBytes + x * 3;
|
|
190
|
+
|
|
191
|
+
// Calculate coverage for anti-aliasing
|
|
192
|
+
let coverage = 1.0;
|
|
193
|
+
if (x === xStartFloor) {
|
|
194
|
+
// Left edge pixel - calculate fractional coverage
|
|
195
|
+
coverage = Math.min(1.0, 1.0 - (xStart - xStartFloor));
|
|
196
|
+
} else if (x === xEndCeil - 1) {
|
|
197
|
+
// Right edge pixel - calculate fractional coverage
|
|
198
|
+
coverage = Math.min(1.0, xEnd - Math.floor(xEnd));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Apply coverage to opacity for smooth edges
|
|
202
|
+
const effectiveOpacity = opacity * coverage;
|
|
203
|
+
|
|
204
|
+
const pixelColor = getRectPixelColor({
|
|
205
|
+
imageDstBuffer,
|
|
206
|
+
pixelIndex,
|
|
207
|
+
color,
|
|
208
|
+
opacity: effectiveOpacity,
|
|
209
|
+
});
|
|
210
|
+
imageDstBuffer.data[pixelIndex] = pixelColor.blue;
|
|
211
|
+
imageDstBuffer.data[pixelIndex + 1] = pixelColor.green;
|
|
212
|
+
imageDstBuffer.data[pixelIndex + 2] = pixelColor.red;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Update x coordinates for next scanline (x' = x + dx)
|
|
218
|
+
for (const edge of activeEdges) {
|
|
219
|
+
edge.x += edge.dx;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
interface DrawSpectrumProps {
|
|
225
|
+
imageDstBuffer: EncodedBmp;
|
|
226
|
+
spectrum: number[];
|
|
227
|
+
size: Size;
|
|
228
|
+
position: Position;
|
|
229
|
+
rotation: RotationAliasName;
|
|
230
|
+
margin: number;
|
|
231
|
+
color: Color;
|
|
232
|
+
opacity: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const getRectY = (rotation: RotationAliasName, top: number, height: number, rectHeight: number) => {
|
|
236
|
+
switch (rotation) {
|
|
237
|
+
case 'up':
|
|
238
|
+
return top + height - rectHeight;
|
|
239
|
+
case 'mirror':
|
|
240
|
+
return top + Math.trunc((height - rectHeight) / 2);
|
|
241
|
+
default:
|
|
242
|
+
return top;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const minRectHeight = 2;
|
|
247
|
+
|
|
248
|
+
const getRectHeight = (height: number, spectrumValue: number) => {
|
|
249
|
+
const rectHeight = Math.trunc(height * spectrumValue);
|
|
250
|
+
if (rectHeight < minRectHeight) {
|
|
251
|
+
return minRectHeight;
|
|
252
|
+
}
|
|
253
|
+
return rectHeight;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const drawSpectrum = ({
|
|
257
|
+
imageDstBuffer,
|
|
258
|
+
spectrum,
|
|
259
|
+
size,
|
|
260
|
+
position,
|
|
261
|
+
rotation,
|
|
262
|
+
margin,
|
|
263
|
+
color,
|
|
264
|
+
opacity,
|
|
265
|
+
}: DrawSpectrumProps) => {
|
|
266
|
+
const busWidth = Math.trunc(size.width / spectrum.length);
|
|
267
|
+
const left = Math.trunc(position.x);
|
|
268
|
+
const top = Math.trunc(position.y);
|
|
269
|
+
const height = Math.trunc(size.height);
|
|
270
|
+
|
|
271
|
+
for (let spectrumX = 0; spectrumX < spectrum.length; spectrumX++) {
|
|
272
|
+
const spectrumValue = spectrum[spectrumX];
|
|
273
|
+
if ((spectrumValue > 1) || (spectrumValue < 0)) {
|
|
274
|
+
throw new Error('Spectrum values must be in range from 0 to 1');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const rectX = left + busWidth * spectrumX;
|
|
278
|
+
const rectHeight = getRectHeight(height, spectrumValue);
|
|
279
|
+
const rectY = getRectY(rotation, top, height, rectHeight);
|
|
280
|
+
drawRect({
|
|
281
|
+
imageDstBuffer,
|
|
282
|
+
position: { x: rectX + margin, y: rectY },
|
|
283
|
+
size: { width: busWidth - margin / 2, height: rectHeight },
|
|
284
|
+
color,
|
|
285
|
+
opacity,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const calcCirclePoint = (angleRadians: number, radius: number): Position => {
|
|
291
|
+
const circleX = radius * Math.cos(angleRadians);
|
|
292
|
+
const circleY = radius * Math.sin(angleRadians);
|
|
293
|
+
return { x: circleX, y: circleY };
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const lerp = (start: number, end: number, t: number) =>
|
|
297
|
+
start + (end - start) * t;
|
|
298
|
+
|
|
299
|
+
interface DrawPolarSpectrumProps {
|
|
300
|
+
imageDstBuffer: EncodedBmp;
|
|
301
|
+
spectrum: number[];
|
|
302
|
+
centerX: number;
|
|
303
|
+
centerY: number;
|
|
304
|
+
innerRadius: number;
|
|
305
|
+
maxBarLength: number;
|
|
306
|
+
barWidth: number;
|
|
307
|
+
color: Color;
|
|
308
|
+
opacity: number;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const drawPolarSpectrum = ({
|
|
312
|
+
imageDstBuffer,
|
|
313
|
+
spectrum,
|
|
314
|
+
centerX,
|
|
315
|
+
centerY,
|
|
316
|
+
innerRadius,
|
|
317
|
+
maxBarLength,
|
|
318
|
+
barWidth,
|
|
319
|
+
color,
|
|
320
|
+
opacity,
|
|
321
|
+
}: DrawPolarSpectrumProps) => {
|
|
322
|
+
const angleStep = (2 * Math.PI) / spectrum.length;
|
|
323
|
+
const innerLength = innerRadius * 2 * Math.PI;
|
|
324
|
+
const radiansPerPixel = 2 * Math.PI / innerLength;
|
|
325
|
+
const barWidthInRadians = radiansPerPixel * barWidth;
|
|
326
|
+
const spectrumAverage = spectrum.reduce((a, b) => a + b, 0) / spectrum.length;
|
|
327
|
+
const scaleFactor = lerp(0.75, 1, spectrumAverage);
|
|
328
|
+
const scaledMaxOuterRadius = (innerRadius + maxBarLength) * scaleFactor;
|
|
329
|
+
const scaledInnerRadius = innerRadius * scaleFactor;
|
|
330
|
+
for (let i = 0; i < spectrum.length; i++) {
|
|
331
|
+
const currentSpectrumValue = spectrum[i] || 0;
|
|
332
|
+
const busRadius = lerp(scaledInnerRadius, scaledMaxOuterRadius, currentSpectrumValue);
|
|
333
|
+
const angle = i * angleStep;
|
|
334
|
+
const angleRadiansStart = angle;
|
|
335
|
+
const angleRadiansEnd = angle + barWidthInRadians;
|
|
336
|
+
|
|
337
|
+
const point1 = calcCirclePoint(angleRadiansStart, scaledInnerRadius);
|
|
338
|
+
const point2 = calcCirclePoint(angleRadiansStart, busRadius);
|
|
339
|
+
|
|
340
|
+
const point3 = calcCirclePoint(angleRadiansEnd, scaledInnerRadius);
|
|
341
|
+
const point4 = calcCirclePoint(angleRadiansEnd, busRadius);
|
|
342
|
+
|
|
343
|
+
fillRect({
|
|
344
|
+
imageDstBuffer,
|
|
345
|
+
points: [
|
|
346
|
+
{ x: centerX + Math.round(point1.x), y: centerY + Math.round(point1.y) },
|
|
347
|
+
{ x: centerX + Math.round(point2.x), y: centerY + Math.round(point2.y) },
|
|
348
|
+
{ x: centerX + Math.round(point4.x), y: centerY + Math.round(point4.y) },
|
|
349
|
+
{ x: centerX + Math.round(point3.x), y: centerY + Math.round(point3.y) },
|
|
350
|
+
],
|
|
351
|
+
color,
|
|
352
|
+
opacity,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
export interface CommonVisualizerFrameProps {
|
|
358
|
+
backgroundImageBuffer: EncodedBmp;
|
|
359
|
+
spectrum: number[];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface CreateVisualizerFrameProps {
|
|
363
|
+
size: Size;
|
|
364
|
+
position: Position;
|
|
365
|
+
rotation: RotationAliasName;
|
|
366
|
+
margin: number;
|
|
367
|
+
color: Color | string;
|
|
368
|
+
opacity: number;
|
|
369
|
+
spectrumEffect?: SpectrumEffect;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface CreatePolarVisualizerFrameProps {
|
|
373
|
+
centerX: number;
|
|
374
|
+
centerY: number;
|
|
375
|
+
innerRadius: number;
|
|
376
|
+
maxBarLength: number;
|
|
377
|
+
barWidth: number;
|
|
378
|
+
color: Color | string;
|
|
379
|
+
opacity: number;
|
|
380
|
+
spectrumEffect?: SpectrumEffect;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export const createSpectrumVisualizerFrameGenerator = () => {
|
|
384
|
+
let prevSpectrum: number[] | null = null;
|
|
385
|
+
|
|
386
|
+
return ({
|
|
387
|
+
backgroundImageBuffer,
|
|
388
|
+
spectrum,
|
|
389
|
+
size,
|
|
390
|
+
position,
|
|
391
|
+
rotation,
|
|
392
|
+
margin,
|
|
393
|
+
color,
|
|
394
|
+
opacity,
|
|
395
|
+
spectrumEffect,
|
|
396
|
+
}: CommonVisualizerFrameProps & CreateVisualizerFrameProps) => {
|
|
397
|
+
const imageDstBuffer = Object.assign({}, backgroundImageBuffer);
|
|
398
|
+
imageDstBuffer.data = Buffer.from(backgroundImageBuffer.data);
|
|
399
|
+
|
|
400
|
+
const rgbSpectrumColor = (typeof color === 'string') ? hexToRgb(color) : color;
|
|
401
|
+
if (spectrumEffect === 'volume') {
|
|
402
|
+
drawSpectrum({
|
|
403
|
+
imageDstBuffer,
|
|
404
|
+
spectrum,
|
|
405
|
+
size,
|
|
406
|
+
position: { x: position.x + 2, y: position.y + 2 },
|
|
407
|
+
rotation,
|
|
408
|
+
margin,
|
|
409
|
+
color: rgbSpectrumColor,
|
|
410
|
+
opacity: opacity * 0.5,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (spectrumEffect === 'smooth' && prevSpectrum) {
|
|
414
|
+
drawSpectrum({
|
|
415
|
+
imageDstBuffer,
|
|
416
|
+
spectrum: prevSpectrum,
|
|
417
|
+
size,
|
|
418
|
+
position,
|
|
419
|
+
rotation,
|
|
420
|
+
margin,
|
|
421
|
+
color: rgbSpectrumColor,
|
|
422
|
+
opacity: opacity * 0.3,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
drawSpectrum({
|
|
426
|
+
imageDstBuffer,
|
|
427
|
+
spectrum,
|
|
428
|
+
size,
|
|
429
|
+
position,
|
|
430
|
+
rotation,
|
|
431
|
+
margin,
|
|
432
|
+
color: rgbSpectrumColor,
|
|
433
|
+
opacity,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
prevSpectrum = spectrum;
|
|
437
|
+
|
|
438
|
+
return imageDstBuffer;
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
export const createPolarVisualizerFrameGenerator = () => {
|
|
443
|
+
let prevSpectrum: number[] | null = null;
|
|
444
|
+
|
|
445
|
+
return ({
|
|
446
|
+
backgroundImageBuffer,
|
|
447
|
+
spectrum,
|
|
448
|
+
centerX,
|
|
449
|
+
centerY,
|
|
450
|
+
innerRadius,
|
|
451
|
+
maxBarLength,
|
|
452
|
+
barWidth,
|
|
453
|
+
color,
|
|
454
|
+
opacity,
|
|
455
|
+
spectrumEffect,
|
|
456
|
+
}: CommonVisualizerFrameProps & CreatePolarVisualizerFrameProps) => {
|
|
457
|
+
const imageDstBuffer = Object.assign({}, backgroundImageBuffer);
|
|
458
|
+
imageDstBuffer.data = Buffer.from(backgroundImageBuffer.data);
|
|
459
|
+
|
|
460
|
+
const rgbSpectrumColor = (typeof color === 'string') ? hexToRgb(color) : color;
|
|
461
|
+
|
|
462
|
+
if (spectrumEffect === 'volume') {
|
|
463
|
+
drawPolarSpectrum({
|
|
464
|
+
imageDstBuffer,
|
|
465
|
+
spectrum,
|
|
466
|
+
centerX: centerX + 2,
|
|
467
|
+
centerY: centerY + 2,
|
|
468
|
+
innerRadius,
|
|
469
|
+
maxBarLength,
|
|
470
|
+
barWidth,
|
|
471
|
+
color: rgbSpectrumColor,
|
|
472
|
+
opacity: opacity * 0.5,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (spectrumEffect === 'smooth' && prevSpectrum) {
|
|
477
|
+
drawPolarSpectrum({
|
|
478
|
+
imageDstBuffer,
|
|
479
|
+
spectrum: prevSpectrum,
|
|
480
|
+
centerX,
|
|
481
|
+
centerY,
|
|
482
|
+
innerRadius,
|
|
483
|
+
maxBarLength,
|
|
484
|
+
barWidth,
|
|
485
|
+
color: rgbSpectrumColor,
|
|
486
|
+
opacity: opacity * 0.3,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
drawPolarSpectrum({
|
|
491
|
+
imageDstBuffer,
|
|
492
|
+
spectrum,
|
|
493
|
+
centerX,
|
|
494
|
+
centerY,
|
|
495
|
+
innerRadius,
|
|
496
|
+
maxBarLength,
|
|
497
|
+
barWidth,
|
|
498
|
+
color: rgbSpectrumColor,
|
|
499
|
+
opacity,
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
prevSpectrum = spectrum;
|
|
503
|
+
|
|
504
|
+
return imageDstBuffer;
|
|
505
|
+
};
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
export const convertToBmp = async (filePath: string, targetWidth?: number, targetHeight?: number) =>
|
|
509
|
+
new Promise<Buffer>(async (resolve, reject) => {
|
|
510
|
+
try {
|
|
511
|
+
const image = await Jimp.read(filePath);
|
|
512
|
+
if (targetWidth && targetHeight) {
|
|
513
|
+
const sw = image.getWidth();
|
|
514
|
+
const sh = image.getHeight();
|
|
515
|
+
const isOrientationMismatch =
|
|
516
|
+
(sw > sh && targetWidth < targetHeight) ||
|
|
517
|
+
(sw < sh && targetWidth > targetHeight);
|
|
518
|
+
|
|
519
|
+
if (isOrientationMismatch) {
|
|
520
|
+
const targetAspect = targetWidth / targetHeight;
|
|
521
|
+
let cropW: number, cropH: number, cropX: number, cropY: number;
|
|
522
|
+
if (sw / sh > targetAspect) {
|
|
523
|
+
cropW = Math.round(sh * targetAspect);
|
|
524
|
+
cropH = sh;
|
|
525
|
+
cropX = Math.round((sw - cropW) / 2);
|
|
526
|
+
cropY = 0;
|
|
527
|
+
} else {
|
|
528
|
+
cropW = sw;
|
|
529
|
+
cropH = Math.round(sw / targetAspect);
|
|
530
|
+
cropX = 0;
|
|
531
|
+
cropY = Math.round((sh - cropH) / 2);
|
|
532
|
+
}
|
|
533
|
+
image.crop(cropX, cropY, cropW, cropH);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
image.resize(targetWidth, targetHeight);
|
|
537
|
+
}
|
|
538
|
+
image.getBuffer("image/bmp", (err, value) => {
|
|
539
|
+
if (err) {
|
|
540
|
+
reject(err);
|
|
541
|
+
}
|
|
542
|
+
resolve(value);
|
|
543
|
+
});
|
|
544
|
+
} catch {
|
|
545
|
+
throw new Error(`Cannot procces image file from path: ${filePath}`);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
export const parseImage = (buffer: Buffer) => decode(buffer);
|
|
550
|
+
|
|
551
|
+
export const getImageColor = (image: BmpDecoder): Color => {
|
|
552
|
+
let blueSum = 0;
|
|
553
|
+
let greenSum = 0;
|
|
554
|
+
let redSum = 0;
|
|
555
|
+
|
|
556
|
+
for (let currY = 0; currY < image.height; currY++) {
|
|
557
|
+
for (let currX = 0; currX < image.width; currX++) {
|
|
558
|
+
const idx = (image.width * currY + currX) << 2;
|
|
559
|
+
|
|
560
|
+
blueSum += image.data[idx + 1];
|
|
561
|
+
greenSum += image.data[idx + 2];
|
|
562
|
+
redSum += image.data[idx + 3];
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const pixelsCount = image.width * image.height;
|
|
567
|
+
return {
|
|
568
|
+
red: ~~(redSum / pixelsCount),
|
|
569
|
+
green: ~~(greenSum / pixelsCount),
|
|
570
|
+
blue: ~~(blueSum / pixelsCount)
|
|
571
|
+
};
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
export const getVideoFrameColor = (bgrBuffer: Buffer, width: number, height: number): Color => {
|
|
575
|
+
let blueSum = 0;
|
|
576
|
+
let greenSum = 0;
|
|
577
|
+
let redSum = 0;
|
|
578
|
+
const pixelsCount = width * height;
|
|
579
|
+
|
|
580
|
+
for (let i = 0; i < bgrBuffer.length; i += 3) {
|
|
581
|
+
blueSum += bgrBuffer[i];
|
|
582
|
+
greenSum += bgrBuffer[i + 1];
|
|
583
|
+
redSum += bgrBuffer[i + 2];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
red: ~~(redSum / pixelsCount),
|
|
588
|
+
green: ~~(greenSum / pixelsCount),
|
|
589
|
+
blue: ~~(blueSum / pixelsCount),
|
|
590
|
+
};
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
export const invertColor = (color: Color) =>
|
|
594
|
+
({ red: 255 - color.red, green: 255 - color.green, blue: 255 - color.blue });
|
|
595
|
+
|
|
596
|
+
const hexToRgb = (hex: String) => {
|
|
597
|
+
const red = parseInt(hex.substring(1, 3), 16);
|
|
598
|
+
const green = parseInt(hex.substring(3, 5), 16);
|
|
599
|
+
const blue = parseInt(hex.substring(5, 7), 16);
|
|
600
|
+
|
|
601
|
+
return { red, green, blue };
|
|
602
|
+
};
|