dsssp
Version:
React Library for Audio Processing and Visualization
1,795 lines (1,746 loc) • 52.5 kB
JavaScript
'use strict';
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const React = require('react');
const jsxRuntime = require('react/jsx-runtime');
const GraphGainGrid = () => {
const {
height,
scale: { minGain, maxGain, dbSteps, dbLabels },
theme: {
background: {
grid: { dotted, lineColor, lineWidth },
label: { color: labelColor, fontSize, fontFamily }
}
}
} = useGraph();
if (!dbSteps) return null;
const steps = dbSteps || maxGain;
const dBs = Array.from(
{ length: (maxGain - minGain) / steps + 1 },
(_, i) => {
return maxGain - i * steps;
}
);
const centerY = getCenterLine(minGain, maxGain, height);
const strokeDasharray = "1,2";
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
dBs.slice(0, -1).map((tick, index) => {
if (index === 0) return null;
const tickY = `${index / (dBs.length - 1) * 100}%`;
const tickLabel = tick > 0 ? `+${tick}` : tick;
return /* @__PURE__ */ jsxRuntime.jsxs(React.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx(
"line",
{
x1: "0",
x2: "100%",
y1: tickY,
y2: tickY,
stroke: lineColor,
strokeWidth: lineWidth.minor,
...dotted ? { strokeDasharray } : {}
}
),
dbLabels && index !== 0 && index !== dBs.length - 1 && /* @__PURE__ */ jsxRuntime.jsx(
"text",
{
x: 3,
y: tickY,
fill: labelColor,
fontSize,
fontFamily,
textAnchor: "start",
transform: "translate(0 -3)",
children: tickLabel
}
)
] }, tick);
}),
/* @__PURE__ */ jsxRuntime.jsx(
"line",
{
id: "centerLine",
x1: "0",
x2: "100%",
y1: centerY,
y2: centerY,
stroke: lineColor,
strokeWidth: lineWidth.center,
...dotted ? { strokeDasharray } : {}
}
),
dbLabels && /* @__PURE__ */ jsxRuntime.jsx(
"text",
{
y: 12,
x: 5,
fill: labelColor,
fontSize,
fontFamily,
children: "dB"
}
)
] });
};
const GraphFrequencyGrid = () => {
const {
height,
logScale,
scale: { octaveLabels, octaveTicks, majorTicks },
theme: {
background: {
grid: { dotted, lineColor, lineWidth },
label: { color: labelColor, fontSize, fontFamily }
}
}
} = useGraph();
const ticks = octaveTicks ? logScale.ticks(octaveTicks) : [0, 0];
const strokeDasharray = "1,2";
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
ticks.slice(1, -1).map((tick) => {
const tickX = logScale.x(tick);
const width = majorTicks.includes(tick) ? lineWidth.major : lineWidth.minor;
return /* @__PURE__ */ jsxRuntime.jsx(
"line",
{
x1: tickX,
x2: tickX,
y1: "0",
y2: "100%",
stroke: lineColor,
strokeWidth: width,
...dotted ? { strokeDasharray } : {}
},
tick
);
}),
octaveLabels.map((octave) => {
const octaveX = logScale.x(octave);
return /* @__PURE__ */ jsxRuntime.jsx(
"text",
{
y: height - 4,
x: octaveX + (octave > 1e4 ? -4 : 4),
textAnchor: octave > 1e4 ? "end" : "start",
fill: labelColor,
fontSize,
fontFamily,
children: (octave < 1e3 ? octave : `${octave / 1e3}k`) + "Hz"
},
octave
);
})
] });
};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var cjs;
var hasRequiredCjs;
function requireCjs () {
if (hasRequiredCjs) return cjs;
hasRequiredCjs = 1;
var isMergeableObject = function isMergeableObject(value) {
return isNonNullObject(value)
&& !isSpecial(value)
};
function isNonNullObject(value) {
return !!value && typeof value === 'object'
}
function isSpecial(value) {
var stringValue = Object.prototype.toString.call(value);
return stringValue === '[object RegExp]'
|| stringValue === '[object Date]'
|| isReactElement(value)
}
// see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25
var canUseSymbol = typeof Symbol === 'function' && Symbol.for;
var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7;
function isReactElement(value) {
return value.$$typeof === REACT_ELEMENT_TYPE
}
function emptyTarget(val) {
return Array.isArray(val) ? [] : {}
}
function cloneUnlessOtherwiseSpecified(value, options) {
return (options.clone !== false && options.isMergeableObject(value))
? deepmerge(emptyTarget(value), value, options)
: value
}
function defaultArrayMerge(target, source, options) {
return target.concat(source).map(function(element) {
return cloneUnlessOtherwiseSpecified(element, options)
})
}
function getMergeFunction(key, options) {
if (!options.customMerge) {
return deepmerge
}
var customMerge = options.customMerge(key);
return typeof customMerge === 'function' ? customMerge : deepmerge
}
function getEnumerableOwnPropertySymbols(target) {
return Object.getOwnPropertySymbols
? Object.getOwnPropertySymbols(target).filter(function(symbol) {
return Object.propertyIsEnumerable.call(target, symbol)
})
: []
}
function getKeys(target) {
return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))
}
function propertyIsOnObject(object, property) {
try {
return property in object
} catch(_) {
return false
}
}
// Protects from prototype poisoning and unexpected merging up the prototype chain.
function propertyIsUnsafe(target, key) {
return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet,
&& !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,
&& Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
}
function mergeObject(target, source, options) {
var destination = {};
if (options.isMergeableObject(target)) {
getKeys(target).forEach(function(key) {
destination[key] = cloneUnlessOtherwiseSpecified(target[key], options);
});
}
getKeys(source).forEach(function(key) {
if (propertyIsUnsafe(target, key)) {
return
}
if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {
destination[key] = getMergeFunction(key, options)(target[key], source[key], options);
} else {
destination[key] = cloneUnlessOtherwiseSpecified(source[key], options);
}
});
return destination
}
function deepmerge(target, source, options) {
options = options || {};
options.arrayMerge = options.arrayMerge || defaultArrayMerge;
options.isMergeableObject = options.isMergeableObject || isMergeableObject;
// cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge()
// implementations can use it. The caller may not replace it.
options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified;
var sourceIsArray = Array.isArray(source);
var targetIsArray = Array.isArray(target);
var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;
if (!sourceAndTargetTypesMatch) {
return cloneUnlessOtherwiseSpecified(source, options)
} else if (sourceIsArray) {
return options.arrayMerge(target, source, options)
} else {
return mergeObject(target, source, options)
}
}
deepmerge.all = function deepmergeAll(array, options) {
if (!Array.isArray(array)) {
throw new Error('first argument should be an array')
}
return array.reduce(function(prev, next) {
return deepmerge(prev, next, options)
}, {})
};
var deepmerge_1 = deepmerge;
cjs = deepmerge_1;
return cjs;
}
var cjsExports = requireCjs();
const merge = /*@__PURE__*/getDefaultExportFromCjs(cjsExports);
const defaultScale = {
minFreq: 20,
maxFreq: 2e4,
sampleRate: 44100,
// 48000 / 96000 / 192000
minGain: -16,
maxGain: 16,
dbSteps: 4,
// 0 to disable
dbLabels: true,
octaveTicks: 10,
// ticks per octave (0 to disable)
octaveLabels: [20, 40, 60, 100, 200, 500, 1e3, 2e3, 5e3, 1e4, 2e4],
majorTicks: [100, 1e3, 1e4]
// ticks with the major line width, same as zero gain
};
const defaultTheme = {
background: {
// background grid lines
grid: {
dotted: false,
lineColor: "#3D4C5F",
lineWidth: { minor: 0.25, major: 0.5, center: 1, border: 0.25 }
},
// background gradient
gradient: {
start: "#1E2530",
stop: "#000000",
direction: "VERTICAL"
},
// frequency and gain labels
label: {
fontSize: 10,
fontFamily: "sans-serif",
color: "#626F84"
},
// mouse tracker
tracker: {
lineWidth: 0.5,
lineColor: "#7B899D",
labelColor: "#626F84",
backgroundColor: "#070C18"
}
},
// basic frequency response and composite curves
curve: {
width: 1.5,
opacity: 1,
color: "#FFFFFF"
},
filters: {
// filter curves
curve: {
width: { normal: 1, active: 1 },
opacity: { normal: 0.5, active: 0.7 }
},
// filter points
point: {
radius: 16,
lineWidth: 2,
backgroundOpacity: {
normal: 0.2,
active: 0.6,
drag: 0.8
},
// label inside the point
// size and color applicable to filter icons as well
label: {
fontSize: 24,
fontFamily: "monospace",
color: "inherit"
}
},
// styles of the empty / zero gain filters
zeroPoint: {
color: "#626F84",
background: "#97A3B4"
},
gradientOpacity: 0.7,
fill: false,
// default color for filters, points, and curves
defaultColor: "#66FF66",
// empty placeholder of filter colors
colors: []
}
};
const FrequencyResponseGraph = React.forwardRef((props, forwardedRef) => {
const ref = React.useRef(null);
React.useImperativeHandle(forwardedRef, () => ref.current);
const {
width,
height,
scale = {},
theme = {},
style = {},
className = "",
children
} = props;
const mergedTheme = merge(defaultTheme, theme);
const mergedScale = merge(defaultScale, scale, {
arrayMerge: (_, source) => source
// overwrite arrays
});
const { minFreq, maxFreq } = mergedScale;
const logScale = getLogScaleFn(minFreq, maxFreq, width);
FrequencyResponseGraph.displayName = "FrequencyResponseGraph";
const graphId = `frequency-response-graph-${String(Math.random()).slice(2, 9)}`;
const resetStyles = `
#${graphId} * {
pointer-events: none;
}`;
return /* @__PURE__ */ jsxRuntime.jsxs(
"svg",
{
ref,
id: graphId,
className,
viewBox: `0 0 ${width} ${height}`,
style: {
width,
height,
position: "relative",
verticalAlign: "middle",
userSelect: "none",
...style
},
children: [
/* @__PURE__ */ jsxRuntime.jsx("style", { children: resetStyles }),
/* @__PURE__ */ jsxRuntime.jsxs(
GraphProvider,
{
svgRef: ref,
width,
height,
theme: mergedTheme,
scale: mergedScale,
logScale,
children: [
/* @__PURE__ */ jsxRuntime.jsx(GraphGradient, {}),
/* @__PURE__ */ jsxRuntime.jsx(GraphGainGrid, {}),
/* @__PURE__ */ jsxRuntime.jsx(GraphFrequencyGrid, {}),
children
]
}
)
]
}
);
});
const GraphContext = React.createContext(
undefined
);
const GraphProvider = ({
children,
svgRef,
scale,
logScale,
height,
width,
theme
}) => {
const memoizedTheme = React.useMemo(() => theme, [JSON.stringify(theme)]);
const memoizedScale = React.useMemo(() => scale, [JSON.stringify(scale)]);
const contextValue = React.useMemo(
() => ({
svgRef,
theme: memoizedTheme,
scale: memoizedScale,
logScale,
height,
width
}),
[svgRef, memoizedTheme, memoizedScale, logScale, height, width]
);
return /* @__PURE__ */ jsxRuntime.jsx(GraphContext.Provider, { value: contextValue, children });
};
const useGraph = () => {
const context = React.useContext(GraphContext);
if (context === undefined) {
throw new Error(
"useGraph must be used within an FrequencyResponseGraphProvider"
);
}
return context;
};
const directions = {
VERTICAL: {
x1: "0",
y1: "0",
x2: "0",
y2: "1"
},
HORIZONTAL: {
x1: "0",
y1: "0",
x2: "1",
y2: "0"
},
DIAGONAL_TL_BR: {
x1: "0",
y1: "0",
x2: "1",
y2: "1"
},
DIAGONAL_BL_TR: {
x1: "0",
y1: "1",
x2: "1",
y2: "0"
}
};
const GraphGradient = () => {
const {
theme: {
background: {
gradient: { start, stop, direction },
grid: {
lineColor,
lineWidth: { border: borderWidth }
}
}
}
} = useGraph();
const id = `gBg${Math.random().toString().substring(2, 9)}`;
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsxs(
"linearGradient",
{
id,
...directions[direction],
children: [
/* @__PURE__ */ jsxRuntime.jsx(
"stop",
{
offset: "0%",
stopColor: start
}
),
/* @__PURE__ */ jsxRuntime.jsx(
"stop",
{
offset: "100%",
stopColor: stop
}
)
]
}
),
/* @__PURE__ */ jsxRuntime.jsx(
"rect",
{
x: "0",
y: "0",
width: "100%",
height: "100%",
fill: `url(#${id})`
}
),
Boolean(borderWidth) && /* @__PURE__ */ jsxRuntime.jsx(
"rect",
{
x: borderWidth / 2,
y: borderWidth / 2,
width: `calc(100% - ${borderWidth}px)`,
height: `calc(100% - ${borderWidth}px)`,
fill: "none",
stroke: lineColor,
strokeWidth: borderWidth
}
)
] });
};
const filterTypes = {
BYPASS: 0,
PEAK: 6,
HIGHSHELF1: 1,
HIGHSHELF2: 2,
LOWSHELF1: 3,
LOWSHELF2: 4,
HIGHPASS1: 7,
HIGHPASS2: 8,
LOWPASS1: 9,
LOWPASS2: 10,
BANDPASS: 11,
NOTCH: 5,
GAIN: 12
// ONEPOLE_HP: 0x0d,
// ONEPOLE_LP: 0x0e
// COEFFICIENTS: 0x10
};
const filterTypeKeys = Object.keys(filterTypes);
const fastFloor = (x) => x >> 0;
const fastRound = (x) => x + (x > 0 ? 0.5 : -0.5) >> 0;
const stripTail = (x) => fastRound(x * 100) / 100;
const getLogScaleFn = (minFreq, maxFreq, width) => {
const logMinFreq = Math.log10(minFreq);
const logMaxFreq = Math.log10(maxFreq);
const logRange = logMaxFreq - logMinFreq;
const x = (freq) => {
const logFreq = Math.log10(freq);
const x2 = (logFreq - logMinFreq) / logRange * width;
return x2;
};
const ticks = (number) => {
const ticks2 = [];
const decades = fastFloor(logMaxFreq - logMinFreq);
for (let i = 0; i <= decades; i++) {
const decadeStart = 10 ** (fastFloor(Math.log10(minFreq)) + i);
if (decadeStart >= minFreq) ticks2.push(decadeStart);
for (let j = 2; j <= number - 1; j++) {
const tick = fastFloor(decadeStart * j);
if (tick <= maxFreq) {
ticks2.push(tick);
}
}
}
return ticks2;
};
return { x, ticks };
};
function calcBiQuadCoefficients(type, frequency, peakGain, Q = 0.707, sampleRate = 44100) {
let A0 = 0;
let A1 = 0;
let A2 = 0;
let B1 = 0;
let B2 = 0;
let norm;
sampleRate = Math.max(1, sampleRate);
frequency = Math.max(0, Math.min(frequency, sampleRate / 2));
Q = Math.max(1e-4, Q);
peakGain = Math.max(-120, Math.min(peakGain, 120));
const V = 10 ** (Math.abs(peakGain) / 20);
const K = Math.tan(Math.PI * frequency / sampleRate);
switch (type) {
case "NOTCH":
norm = 1 / (1 + K / Q + K * K);
A0 = (1 + K * K) * norm;
A1 = 2 * (K * K - 1) * norm;
A2 = A0;
B1 = A1;
B2 = (1 - K / Q + K * K) * norm;
break;
case "PEAK":
if (peakGain >= 0) {
norm = 1 / (1 + 1 / Q * K + K * K);
A0 = (1 + V / Q * K + K * K) * norm;
A1 = 2 * (K * K - 1) * norm;
A2 = (1 - V / Q * K + K * K) * norm;
B1 = A1;
B2 = (1 - 1 / Q * K + K * K) * norm;
} else {
norm = 1 / (1 + V / Q * K + K * K);
A0 = (1 + 1 / Q * K + K * K) * norm;
A1 = 2 * (K * K - 1) * norm;
A2 = (1 - 1 / Q * K + K * K) * norm;
B1 = A1;
B2 = (1 - V / Q * K + K * K) * norm;
}
break;
case "LOWSHELF1":
if (peakGain >= 0) {
norm = 1 / (K + 1);
A0 = (K * V + 1) * norm;
A1 = (K * V - 1) * norm;
A2 = 0;
B1 = (K - 1) * norm;
B2 = 0;
} else {
norm = 1 / (K * V + 1);
A0 = (K + 1) * norm;
A1 = (K - 1) * norm;
A2 = 0;
B1 = (K * V - 1) * norm;
B2 = 0;
}
break;
case "LOWSHELF2":
if (peakGain >= 0) {
norm = 1 / (1 + Math.SQRT2 * K + K * K);
A0 = (1 + Math.sqrt(2 * V) * K + V * K * K) * norm;
A1 = 2 * (V * K * K - 1) * norm;
A2 = (1 - Math.sqrt(2 * V) * K + V * K * K) * norm;
B1 = 2 * (K * K - 1) * norm;
B2 = (1 - Math.SQRT2 * K + K * K) * norm;
} else {
norm = 1 / (1 + Math.sqrt(2 * V) * K + V * K * K);
A0 = (1 + Math.SQRT2 * K + K * K) * norm;
A1 = 2 * (K * K - 1) * norm;
A2 = (1 - Math.SQRT2 * K + K * K) * norm;
B1 = 2 * (V * K * K - 1) * norm;
B2 = (1 - Math.sqrt(2 * V) * K + V * K * K) * norm;
}
break;
case "HIGHSHELF1":
if (peakGain >= 0) {
norm = 1 / (K + 1);
A0 = (K + V) * norm;
A1 = (K - V) * norm;
A2 = 0;
B1 = (K - 1) * norm;
B2 = 0;
} else {
norm = 1 / (K + V);
A0 = (K + 1) * norm;
A1 = (K - 1) * norm;
A2 = 0;
B1 = (K - V) * norm;
B2 = 0;
}
break;
case "HIGHSHELF2":
if (peakGain >= 0) {
norm = 1 / (1 + Math.SQRT2 * K + K * K);
A0 = (V + Math.sqrt(2 * V) * K + K * K) * norm;
A1 = 2 * (K * K - V) * norm;
A2 = (V - Math.sqrt(2 * V) * K + K * K) * norm;
B1 = 2 * (K * K - 1) * norm;
B2 = (1 - Math.SQRT2 * K + K * K) * norm;
} else {
norm = 1 / (V + Math.sqrt(2 * V) * K + K * K);
A0 = (1 + Math.SQRT2 * K + K * K) * norm;
A1 = 2 * (K * K - 1) * norm;
A2 = (1 - Math.SQRT2 * K + K * K) * norm;
B1 = 2 * (K * K - V) * norm;
B2 = (V - Math.sqrt(2 * V) * K + K * K) * norm;
}
break;
case "LOWPASS1":
norm = 1 / (1 / K + 1);
A0 = A1 = norm;
B1 = (1 - 1 / K) * norm;
A2 = B2 = 0;
break;
case "LOWPASS2":
norm = 1 / (1 + K / Q + K * K);
A0 = K * K * norm;
A1 = 2 * A0;
A2 = A0;
B1 = 2 * (K * K - 1) * norm;
B2 = (1 - K / Q + K * K) * norm;
break;
case "HIGHPASS1":
norm = 1 / (K + 1);
A0 = norm;
A1 = -norm;
B1 = (K - 1) * norm;
A2 = B2 = 0;
break;
case "HIGHPASS2":
norm = 1 / (1 + K / Q + K * K);
A0 = 1 * norm;
A1 = -2 * A0;
A2 = A0;
B1 = 2 * (K * K - 1) * norm;
B2 = (1 - K / Q + K * K) * norm;
break;
case "BANDPASS":
norm = 1 / (1 + K / Q + K * K);
A0 = K / Q * norm;
A1 = 0;
A2 = -A0;
B1 = 2 * (K * K - 1) * norm;
B2 = (1 - K / Q + K * K) * norm;
break;
case "GAIN":
const gain = 10 ** (peakGain / 20);
A0 = gain;
A1 = 0;
A2 = 0;
B1 = 0;
B2 = 0;
break;
case "BYPASS":
A0 = 1;
A1 = 0;
A2 = 0;
B1 = 0;
B2 = 0;
break;
// case 'ONEPOLE_LP':
// B1 = Math.exp(-2.0 * Math.PI * (frequency / sampleRate))
// A0 = 1.0 - B1
// B1 = -B1
// A1 = A2 = B2 = 0
// break
// case 'ONEPOLE_HP':
// B1 = -Math.exp(-2.0 * Math.PI * (0.5 - frequency / sampleRate))
// A0 = 1.0 + B1
// B1 = -B1
// A1 = A2 = B2 = 0
// break
default:
console.error("calcBiQuadCoefficients: unknown filter type");
}
return { A0, A1, A2, B1, B2 };
}
function calcMagnitudeForFrequency(vars, width, sampleRate = 44100) {
const { A0, A1, A2, B1, B2 } = vars;
const phi = Math.sin(2 * Math.PI * width / sampleRate / 2) ** 2;
let y = Math.log(
(A0 + A1 + A2) ** 2 - 4 * (A0 * A1 + 4 * A0 * A2 + A1 * A2) * phi + 16 * A0 * A2 * phi * phi
) - Math.log(
(1 + B1 + B2) ** 2 - 4 * (1 * B1 + 4 * 1 * B2 + B1 * B2) * phi + 16 * 1 * B2 * phi * phi
);
y = y * 10 / Math.LN10;
if (y === Number.NEGATIVE_INFINITY || isNaN(y)) y = -200;
return y;
}
function calcAmplitudeForFrequency(gain) {
const amplitude = 10 ** (gain / 20);
return amplitude;
}
function calcStandardDeviation(values) {
const mean = values.reduce((acc, val) => acc + val, 0) / values.length;
const squaredDiffs = values.map((val) => (val - mean) ** 2);
const variance = squaredDiffs.reduce((acc, val) => acc + val, 0) / values.length;
const standardDeviation = Math.sqrt(variance);
return standardDeviation;
}
const calcFrequency = (index, length, minFreq, maxFreq) => {
return 10 ** ((Math.log10(maxFreq) - Math.log10(minFreq)) * index / (length - 1) + Math.log10(minFreq));
};
function calcMagnitudes(vars, steps, minFreq, maxFreq, sampleRate = 44100) {
const magPlot = [];
for (let index = 0; index < steps; index++) {
const frequency = calcFrequency(index, steps, minFreq, maxFreq);
const magnitude = calcMagnitudeForFrequency(vars, frequency, sampleRate);
magPlot.push({
frequency,
magnitude
/*, amplitude, deviation */
});
}
return magPlot;
}
const reducePoints = (points) => {
const uniquePoints = points.slice(0, -1).reduce((acc, point, idx) => {
if (fastRound(point.y * 4) !== fastRound((points[idx - 1]?.y || 0) * 4)) {
acc.push(point);
}
return acc;
}, []);
return [...uniquePoints, points.slice(-1)[0]];
};
const getCenterLine = (minGain, maxGain, height) => {
const dbRange = maxGain - minGain;
return maxGain / dbRange * height;
};
const scaleMagnitude = (magnitude, minGain, maxGain, height) => {
const dbScale = height / (maxGain - minGain);
const dbCenterLine = getCenterLine(minGain, maxGain, height);
return dbCenterLine - magnitude * dbScale;
};
const calcMagnitude = (y, minGain, maxGain, height) => {
const dbScale = height / (maxGain - minGain);
const dbCenterLine = getCenterLine(minGain, maxGain, height);
return (dbCenterLine - y) / dbScale;
};
const scaleMagnitudes = (magnitudes, scale, width, height) => {
const { minGain, maxGain } = scale;
const length = magnitudes.length - 1;
return magnitudes.map((mag, i) => {
return {
x: fastRound(width / length * i),
y: stripTail(scaleMagnitude(mag.magnitude, minGain, maxGain, height))
};
});
};
const plotCurve = (points, scale, width, height) => {
const { minGain, maxGain } = scale;
const centerY = getCenterLine(minGain, maxGain, height);
let path = `M -200 ${centerY}`;
points.map((point) => {
path += ` L ${point.x} ${point.y > height + 2 ? height + 2 : point.y}`;
});
path += ` L ${width + 200} ${centerY}`;
return path;
};
const calcFilterCoefficients = (filter, sampleRate = 44100) => {
const { type, freq, gain, q } = filter;
return calcBiQuadCoefficients(type, freq, gain, q, sampleRate);
};
const calcFilterMagnitudes = (vars, scale, width, precisionDivider = 2) => {
const { minFreq, maxFreq, sampleRate } = scale;
const steps = width / precisionDivider;
const magnitudes = calcMagnitudes(vars, steps, minFreq, maxFreq, sampleRate);
return magnitudes;
};
const calcCompositeMagnitudes = (magnitudes) => {
const compositeMags = [];
if (!magnitudes?.length) return [];
if (!magnitudes?.[0]?.length) return [];
for (let i = 0; i < magnitudes[0].length; i++) {
const totalGain = magnitudes.reduce((sum, arr) => {
const { magnitude } = arr[i] || {};
if (!magnitude) return sum;
const filterGain = 10 ** (magnitude / 20);
return sum + 20 * Math.log10(filterGain);
}, 0);
const { frequency } = magnitudes[0][i] || {};
if (!frequency) continue;
compositeMags.push({
frequency,
magnitude: totalGain
});
}
return compositeMags;
};
const limitRange = (value, min, max) => Math.min(Math.max(value, min), max);
const getPointerPosition = (e) => {
const CTM = e.target.getScreenCTM();
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
if (!CTM) {
return { x: clientX, y: clientY };
}
return {
x: (clientX - CTM.e) / CTM.a,
y: (clientY - CTM.f) / CTM.d
};
};
const getZeroFreq = (type) => ["BYPASS", "GAIN"].includes(type) || !type;
const getZeroGain = (type) => [
"LOWPASS1",
"LOWPASS2",
"HIGHPASS1",
"HIGHPASS2",
"BANDPASS",
"BYPASS",
"NOTCH"
].includes(type) || !type;
const getZeroQ = (type) => [
"LOWSHELF1",
"LOWSHELF2",
"HIGHSHELF1",
"HIGHSHELF2",
"HIGHPASS1",
"LOWPASS1",
"BYPASS",
"GAIN"
].includes(type) || !type;
const getIconStyles = (type, gain = 0) => String(type).includes("SHELF") && gain > 0 || type === "PEAK" && gain < 0 || type === "GAIN" && gain < 0 ? {
transform: "scale(1, -1)",
transformBox: "fill-box",
// not a CSS style, but we forced return type
transformOrigin: "center"
} : {};
const getIconSymbol = (type) => {
switch (type) {
case "PEAK":
return "";
case "HIGHSHELF1":
case "HIGHSHELF2":
return "";
case "LOWSHELF1":
case "LOWSHELF2":
return "";
case "HIGHPASS1":
case "HIGHPASS2":
return "";
case "LOWPASS1":
case "LOWPASS2":
return "";
case "BANDPASS":
return "";
case "NOTCH":
return "";
case "GAIN":
return "";
case "BYPASS":
// EMPTY / VOID / NULL / UNDEFINED
default:
return "";
}
};
const getFilterKey = (filter) => `${filter.type}_${filter.freq}_${filter.q}_${filter.gain}`;
const CompositeCurve = ({
filters,
resolutionFactor = 2,
color,
dotted,
opacity,
lineWidth,
gradientId,
style,
easing,
animate,
duration,
// ms
className
}) => {
const { scale, width } = useGraph();
const { minFreq, maxFreq, sampleRate } = scale;
const [magnitudesCache, setMagnitudesCache] = React.useState({});
const memoizedGetFilterKey = React.useCallback((filter) => {
return getFilterKey(filter);
}, []);
const activeKeys = React.useMemo(() => {
return new Set(filters.map((f) => memoizedGetFilterKey(f)));
}, [filters, memoizedGetFilterKey]);
const updateCache = React.useCallback(() => {
const newCache = { ...magnitudesCache };
Object.keys(newCache).forEach((cachedKey) => {
if (!activeKeys.has(cachedKey)) {
delete newCache[cachedKey];
}
});
filters.forEach((filter) => {
const key = memoizedGetFilterKey(filter);
if (!newCache[key]) {
const { type, freq, gain, q } = filter;
const steps = width / resolutionFactor;
const vars = calcBiQuadCoefficients(type, freq, gain, q, sampleRate);
newCache[key] = calcMagnitudes(vars, steps, minFreq, maxFreq, sampleRate) || [];
}
});
setMagnitudesCache(newCache);
}, [filters]);
React.useEffect(() => {
updateCache();
}, [updateCache]);
const compositeMagnitudes = React.useMemo(() => {
const allMags = Object.values(magnitudesCache).filter((m) => m.length);
return calcCompositeMagnitudes(allMags);
}, [magnitudesCache]);
if (!compositeMagnitudes.length) return null;
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx("use", { href: "#centerLine" }),
/* @__PURE__ */ jsxRuntime.jsx(
FrequencyResponseCurve,
{
magnitudes: compositeMagnitudes,
color,
dotted,
opacity,
lineWidth,
gradientId,
style,
easing,
animate,
duration,
className
}
)
] });
};
const easingSplines = {
// Standard CSS easing values translated to SVG keySplines format
linear: "0 0 1 1",
easeIn: "0.42 0 1 1",
easeOut: "0 0 0.58 1",
easeInOut: "0.42 0 0.58 1"
};
const FilterCurve = ({
filter,
index = -1,
resolutionFactor = 2,
color,
dotted,
opacity,
lineWidth,
gradientId,
showPin = false,
showBypass = false,
active = false,
activeColor,
activeOpacity,
activeLineWidth,
style,
easing,
animate,
duration,
// ms
className,
onChange
}) => {
const {
scale,
width,
theme: {
filters: { zeroPoint, curve, defaultColor, colors }
}
} = useGraph();
const prevFilterHashRef = React.useRef("");
const vars = calcFilterCoefficients(filter, scale.sampleRate);
const magnitudes = calcFilterMagnitudes(vars, scale, width, resolutionFactor);
React.useEffect(() => {
const filterHash = JSON.stringify(filter);
if (vars && prevFilterHashRef.current !== filterHash) {
onChange?.(index, vars);
prevFilterHashRef.current = filterHash;
}
}, [filter, vars, onChange]);
if (!vars || !magnitudes?.length) return null;
const zeroValue = filter.type === "BYPASS";
if (zeroValue && !showBypass) return null;
const normalColor = color || colors?.[index]?.curve || defaultColor;
const curveColor = zeroValue ? zeroPoint.color : active ? activeColor || colors?.[index]?.active || normalColor : normalColor;
const curveOpacity = active ? activeOpacity || curve.opacity.active : opacity || curve.opacity.normal;
const curveWidth = active ? activeLineWidth || curve.width.active : lineWidth || curve.width.normal;
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
showPin && /* @__PURE__ */ jsxRuntime.jsx(
FilterPin,
{
vars,
filter,
color: curveColor,
opacity: curveOpacity,
lineWidth: curveWidth
}
),
/* @__PURE__ */ jsxRuntime.jsx(
FrequencyResponseCurve,
{
magnitudes,
dotted,
color: curveColor,
opacity: curveOpacity,
lineWidth: curveWidth,
gradientId,
style,
easing,
animate,
duration,
className
}
)
] });
};
const FilterPin = ({
filter,
vars,
opacity,
lineWidth,
color
}) => {
const { scale, height, logScale } = useGraph();
const { minGain, maxGain, sampleRate } = scale;
const { freq, type } = filter;
let { gain, q } = filter;
const zeroGain = getZeroGain(type);
const {
theme: {
filters: { point }
}
} = useGraph();
if (!["LOW", "HIGH", "NOTCH"].some((item) => type.includes(item))) return null;
const pass1FilterType = type.includes("PASS1") || type === "NOTCH";
const pass2FilterType = type.includes("PASS2");
if (pass1FilterType || pass2FilterType) gain = 0;
if (pass1FilterType) q = 0.7;
let pointRadius = gain >= 0 || zeroGain ? point.radius : -point.radius;
let pass2UpFlag = false;
if (pass2FilterType && q > 1.1) {
pointRadius = -point.radius;
pass2UpFlag = true;
}
let pointY = pointRadius || 0;
if (pass1FilterType || pass2FilterType) {
pointY += getCenterLine(minGain, maxGain, height);
} else {
pointY += scaleMagnitude(gain, minGain, maxGain, height);
}
const centerMagnitude = calcMagnitudeForFrequency(vars, freq, sampleRate);
const magnitudeY = scaleMagnitude(centerMagnitude, minGain, maxGain, height);
const deltaX = pointY > magnitudeY;
const x = logScale.x(freq);
if (gain < 0 && deltaX || gain >= 0 && !deltaX || pass2UpFlag) {
return /* @__PURE__ */ jsxRuntime.jsx(
"line",
{
x1: x,
x2: x,
y1: pointY,
y2: magnitudeY,
stroke: color,
strokeWidth: lineWidth,
strokeOpacity: opacity
}
);
}
return null;
};
const FilterGradient = ({
id,
filter,
index = 0,
opacity,
color,
fill = false,
className,
style
}) => {
const {
theme: { filters }
} = useGraph();
const stopColor = color || filters.colors?.[index]?.gradient || filters.defaultColor;
const stopOpacity = opacity || filters.gradientOpacity;
let gradientDirection;
const filterGain = filter?.gain || 1;
const filterType = filter?.type || "GAIN";
const zeroGain = React.useMemo(() => getZeroGain(filterType), [filterType]);
const startColor = fill || filters.fill ? stopColor : false;
if (zeroGain) {
gradientDirection = { y1: "140%", y2: "0%" };
} else if (filterGain <= 0) {
gradientDirection = { y1: "100%", y2: "0%" };
} else {
gradientDirection = { y1: "0%", y2: "100%" };
}
return /* @__PURE__ */ jsxRuntime.jsxs(
"linearGradient",
{
id,
x1: "0%",
x2: "0%",
...gradientDirection,
className,
style,
children: [
/* @__PURE__ */ jsxRuntime.jsx(
"stop",
{
offset: "0%",
stopColor,
stopOpacity
}
),
/* @__PURE__ */ jsxRuntime.jsx(
"stop",
{
offset: "100%",
stopColor: startColor || "transparent",
stopOpacity: startColor ? stopOpacity : 0
}
)
]
}
);
};
const FilterIcon = ({
color = "#FFFFFF",
size = 24,
gain,
type,
filter,
className = "",
...style
}) => {
const pxSize = `${size}px`;
const iconGain = gain || filter?.gain || 0;
const iconType = type || filter?.type || "BYPASS";
const iconStyles = getIconStyles(iconType, iconGain);
const iconSymbol = getIconSymbol(iconType);
return /* @__PURE__ */ jsxRuntime.jsx(
"div",
{
className,
style: {
color,
border: 0,
margin: 0,
padding: 0,
width: pxSize,
height: pxSize,
lineHeight: 1,
fontSize: pxSize,
fontFamily: "dsssp",
textAlign: "center",
display: "inline-block",
verticalAlign: "middle",
...style,
...iconStyles
},
dangerouslySetInnerHTML: { __html: iconSymbol }
}
);
};
const BypassIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
FilterIcon,
{
...props,
type: "BYPASS"
}
);
const LowPassIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
FilterIcon,
{
...props,
type: "LOWPASS2"
}
);
const HighPassIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
FilterIcon,
{
...props,
type: "HIGHPASS2"
}
);
const LowShelfIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
FilterIcon,
{
...props,
type: "LOWSHELF2"
}
);
const HighShelfIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
FilterIcon,
{
...props,
type: "HIGHSHELF2"
}
);
const BandPassIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
FilterIcon,
{
...props,
type: "BANDPASS"
}
);
const NotchIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
FilterIcon,
{
...props,
type: "NOTCH"
}
);
const PeakIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
FilterIcon,
{
...props,
type: "PEAK"
}
);
const GainIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
FilterIcon,
{
...props,
type: "GAIN"
}
);
const FilterPoint = ({
filter,
index = -1,
dragX = true,
dragY = true,
wheelQ = true,
active = false,
// manual `hovered` state
showIcon = false,
label = "",
labelFontSize,
labelFontFamily,
labelColor,
radius,
lineWidth,
color,
zeroColor,
dragColor,
activeColor,
background,
zeroBackground,
dragBackground,
activeBackground,
backgroundOpacity,
dragBackgroundOpacity,
activeBackgroundOpacity,
className,
style,
onChange,
onEnter,
onLeave,
onDrag
}) => {
const {
svgRef,
scale,
logScale,
height,
width,
theme: {
filters: { zeroPoint, colors, defaultColor, point }
}
} = useGraph();
const { minGain, maxGain, minFreq, maxFreq } = scale;
const { freq: filterFreq, gain: filterGain, q: filterQ, type } = filter;
const circleRef = React.useRef(null);
const labelRef = React.useRef(null);
const [hovered, setHovered] = React.useState(false);
const [dragging, setDragging] = React.useState(false);
const [zeroGain, passFilter] = React.useMemo(
() => [getZeroGain(type), type.includes("PASS") || type === "NOTCH"],
[type]
);
const x = logScale.x(filterFreq);
const centerY = getCenterLine(minGain, maxGain, height);
const y = !passFilter ? scaleMagnitude(filterGain, minGain, maxGain, height) : centerY;
let offset = { x: 0, y: 0 };
let cx;
let cy;
const moveFreq = React.useRef(filterFreq);
const moveGain = React.useRef(filterGain);
const dragMove = (e) => {
e.preventDefault();
e.stopPropagation();
if (!circleRef.current) return;
const svgBounds = svgRef.current?.getBoundingClientRect();
if (!svgBounds) return;
const { x: x2, y: y2 } = getPointerPosition(e);
const offsetX = x2 - (svgBounds.left ?? 0);
const offsetY = y2 - (svgBounds.top ?? 0);
if (dragX) {
cx = limitRange(offsetX - offset.x, 0, width);
circleRef.current.setAttributeNS(null, "cx", String(cx));
labelRef.current?.setAttributeNS(null, "x", String(cx));
moveFreq.current = stripTail(
limitRange(calcFrequency(cx, width, minFreq, maxFreq), minFreq, maxFreq)
);
}
if (dragY) {
if (zeroGain) {
cy = centerY;
} else {
cy = limitRange(offsetY - offset.y, 0, height);
}
circleRef.current.setAttributeNS(null, "cy", String(cy));
labelRef.current?.setAttributeNS(null, "y", String(cy));
const gain = stripTail(calcMagnitude(cy, minGain, maxGain, height));
moveGain.current = gain < 0.05 && gain > -0.05 ? 0 : gain;
}
onChange?.({
index,
...filter,
freq: moveFreq.current,
...!passFilter ? { gain: moveGain.current } : {}
});
};
const dragEnd = (e) => {
e.preventDefault();
e.stopPropagation();
const svg = svgRef.current;
const circleEl = circleRef.current;
if (!svg || !circleEl) return;
const touchEvent = "touches" in e;
circleEl.setAttribute(
"fill-opacity",
String(
touchEvent ? backgroundOpacity ?? point.backgroundOpacity.normal : activeBackgroundOpacity ?? point.backgroundOpacity.active
)
);
svg.removeEventListener("mousemove", dragMove);
svg.removeEventListener("mouseup", dragEnd);
svg.removeEventListener("mouseleave", dragEnd);
circleEl.removeEventListener("touchmove", dragMove);
circleEl.removeEventListener("touchend", dragEnd);
circleEl.removeEventListener("touchcancel", dragEnd);
setDragging(false);
onChange?.({
index,
...filter,
freq: moveFreq.current,
gain: moveGain.current,
ended: true
});
onDrag?.(false);
};
const dragStart = (e) => {
e.preventDefault();
e.stopPropagation();
const svg = svgRef.current;
const circleEl = circleRef.current;
if (!svg || !circleEl) return;
setDragging(true);
const svgBounds = svg.getBoundingClientRect();
const { x: x2, y: y2 } = getPointerPosition(e);
const { left, top } = svgBounds;
offset = {
x: x2 - left - parseFloat(circleEl.getAttributeNS(null, "cx") || "0"),
y: y2 - top - parseFloat(circleEl.getAttributeNS(null, "cy") || "0")
};
circleEl.setAttribute(
"fill-opacity",
String(dragBackgroundOpacity || point.backgroundOpacity.drag)
);
svg.addEventListener("mousemove", dragMove);
svg.addEventListener("mouseup", dragEnd);
svg.addEventListener("mouseleave", dragEnd);
circleEl.addEventListener("touchmove", dragMove);
circleEl.addEventListener("touchend", dragEnd);
circleEl.addEventListener("touchcancel", dragEnd);
onDrag?.(true);
};
const handleMouseEnter = () => {
setHovered(true);
onEnter?.({ ...filter, index });
};
const handleMouseLeave = () => {
setHovered(false);
onLeave?.({ ...filter, index });
};
const scrollQ = (e) => {
e.preventDefault();
let newQ = filterQ;
newQ += e.deltaY > 0 ? 0.1 : -0.1;
newQ = stripTail(limitRange(newQ, 0.1, 20));
onChange?.({ index, ...filter, q: newQ, ended: true });
};
if (wheelQ) circleRef.current?.addEventListener("wheel", scrollQ);
if (type === "BYPASS") return null;
const strokeWidth = lineWidth || point.lineWidth;
const pointColor = color || colors?.[index]?.point || defaultColor;
const bgColor = background || colors?.[index]?.background || pointColor;
const zeroValue = filterGain === 0 && !zeroGain;
const strokeColor = zeroValue ? zeroColor || zeroPoint.color : dragging ? dragColor || colors?.[index]?.drag || pointColor : active || hovered ? activeColor || colors?.[index]?.active || pointColor : pointColor;
const fillColor = zeroValue ? zeroBackground || zeroPoint.background : dragging ? dragBackground || colors?.[index]?.dragBackground || bgColor : active || hovered ? activeBackground || colors?.[index]?.activeBackground || bgColor : bgColor;
const fillOpacity = active || hovered ? activeBackgroundOpacity ?? point.backgroundOpacity?.active : backgroundOpacity ?? point.backgroundOpacity?.normal;
if (label || showIcon) {
labelColor ||= point.label.color;
labelFontSize ||= point.label.fontSize;
labelFontFamily ||= point.label.fontFamily;
if (labelColor === "inherit") labelColor = strokeColor;
}
let labelStyle = {};
if (showIcon) {
label = getIconSymbol(type);
labelFontFamily = "dsssp";
labelStyle = getIconStyles(type, filterGain);
}
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx(
"circle",
{
ref: circleRef,
cx: x,
cy: y,
r: radius || point.radius,
fill: fillColor,
fillOpacity,
stroke: strokeColor,
strokeWidth,
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
onMouseDown: (e) => dragStart(e),
onTouchStart: (e) => dragStart(e),
style: { cursor: "pointer", pointerEvents: "auto", ...style },
className
}
),
Boolean(label) && /* @__PURE__ */ jsxRuntime.jsx(
"text",
{
ref: labelRef,
x,
y,
textAnchor: "middle",
dominantBaseline: "central",
fill: labelColor,
fontSize: labelFontSize,
fontFamily: labelFontFamily,
style: { ...labelStyle },
dangerouslySetInnerHTML: { __html: label }
}
)
] });
};
const FrequencyResponseCurve = ({
magnitudes,
dotted = false,
color,
opacity,
lineWidth,
gradientId,
className,
style,
animate = false,
easing = "easeInOut",
duration = 300
// ms
}) => {
const {
scale,
width,
height,
theme: { curve }
} = useGraph();
const curveColor = color || curve.color;
const curveWidth = lineWidth || curve.width;
const curveOpacity = opacity || curve.opacity;
const { currentPath, initialPath } = React.useMemo(() => {
const points = scaleMagnitudes(magnitudes, scale, width, height);
const flatPoints = points.map((p) => ({ x: p.x, y: height / 2 }));
const currentPath2 = plotCurve(points, scale, width, height);
const initialPath2 = plotCurve(flatPoints, scale, width, height);
return { currentPath: currentPath2, initialPath: initialPath2 };
}, [magnitudes, scale, width, height]);
const animateRef = React.useRef(null);
const [fromPath, setFromPath] = React.useState(initialPath);
const [toPath, setToPath] = React.useState(initialPath);
React.useLayoutEffect(() => {
if (animate) {
setFromPath(toPath);
animateRef.current?.beginElement();
requestAnimationFrame(() => {
setToPath(currentPath);
});
}
}, [currentPath, animate]);
return /* @__PURE__ */ jsxRuntime.jsx(
"path",
{
d: animate ? fromPath : currentPath,
stroke: curveColor,
strokeWidth: curveWidth,
strokeOpacity: curveOpacity,
strokeLinecap: "round",
...dotted ? { strokeDasharray: "1,3" } : {},
fill: gradientId ? `url(#${gradientId})` : "none",
className,
style,
children: animate && /* @__PURE__ */ jsxRuntime.jsx(
"animate",
{
ref: animateRef,
from: fromPath,
to: toPath,
fill: "freeze",
repeatCount: "1",
attributeName: "d",
dur: `${duration}ms`,
calcMode: "spline",
keyTimes: "0;1",
keySplines: easingSplines[easing],
additive: "replace",
accumulate: "none"
}
)
}
);
};
const PointerTracker = ({
lineWidth,
lineColor,
labelColor,
backgroundColor,
gainPrecision = 1
}) => {
const {
svgRef,
width,
height,
scale: { minGain, maxGain, minFreq, maxFreq },
theme: {
background: {
tracker,
label: { fontSize, fontFamily }
}
}
} = useGraph();
const color = labelColor || tracker.labelColor;
const fillColor = backgroundColor || tracker.backgroundColor;
const strokeColor = lineColor || tracker.lineColor;
const strokeWidth = lineWidth || tracker.lineWidth;
const strokeDasharray = "1,2";
const fontSizePadding = (fontSize || 0) + 3;
const [freqWidth, setFreqWidth] = React.useState(0);
const [gainWidth, setGainWidth] = React.useState(0);
const [freqLabel, setFreqLabel] = React.useState(0);
const [gainLabel, setGainLabel] = React.useState(0);
const [trackMouse, setTrackMouse] = React.useState(false);
const [mouse, setMouse] = React.useState({ x: -50, y: -50 });
const freqLabelRef = React.useRef(null);
const gainLabelRef = React.useRef(null);
const mouseMove = (e) => {
e.preventDefault();
const { x, y } = getPointerPosition(e);
setMouse({ x, y });
const newGain = calcMagnitude(y, minGain, maxGain, height).toFixed(
gainPrecision
);
if (newGain !== String(gainLabel)) {
setGainLabel(Number(newGain));
}
const newFreq = fastFloor(calcFrequency(x, width, minFreq, maxFreq));
if (newFreq !== freqLabel) {
setFreqLabel(newFreq);
}
};
React.useEffect(() => {
if (!freqLabelRef.current) return;
const w = fastFloor(freqLabelRef.current.getBBox().width);
if (w !== freqWidth) {
setFreqWidth(w);
}
}, [freqLabel]);
React.useEffect(() => {
if (!gainLabelRef.current) return;
const w = fastFloor(gainLabelRef.current.getBBox().width);
if (w !== gainWidth) {
setGainWidth(w);
}
}, [gainLabel]);
const handleMouseEnter = () => setTrackMouse(true);
const handleMouseLeave = () => setTrackMouse(false);
const handleTouchStart = () => setTrackMouse(true);
const handleTouchEnd = () => setTrackMouse(false);
const handleTouchCancel = () => setTrackMouse(false);
React.useEffect(() => {
const svg = svgRef.current;
if (!svg) return;
svg.addEventListener("mouseenter", handleMouseEnter);
svg.addEventListener("mouseleave", handleMouseLeave);
svg.addEventListener("mousemove", mouseMove);
svg.addEventListener("touchstart", handleTouchStart);
svg.addEventListener("touchmove", mouseMove);
svg.addEventListener("touchend", handleTouchEnd);
svg.addEventListener("touchcancel", handleTouchCancel);
return () => {
svg.removeEventListener("mouseenter", handleMouseEnter);
svg.removeEventListener("mouseleave", handleMouseLeave);
svg.removeEventListener("mousemove", mouseMove);
svg.removeEventListener("touchstart", handleTouchStart);
svg.removeEventListener("touchmove", mouseMove);
svg.removeEventListener("touchend", handleTouchEnd);
svg.removeEventListener("touchcancel", handleTouchCancel);
};
}, [svgRef.current]);
React.useEffect(() => {
setTrackMouse(true);
}, []);
if (!trackMouse) return null;
return /* @__PURE__ */ jsxRuntime.jsxs(React.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx(
"rect",
{
width: freqWidth + 6,
height: fontSizePadding,
fill: fillColor,
stroke: strokeColor,
x: mouse.x - freqWidth / 2 - 3,
y: height - fontSizePadding - 1
}
),
/* @__PURE__ */ jsxRuntime.jsx(
"text",
{
ref: freqLabelRef,
x: mouse.x - freqWidth / 2,
y: height - 4,
fill: color,
fontSize,
fontFamily,
children: freqLabel
}
),
/* @__PURE__ */ jsxRuntime.jsx(
"rect",
{
width: gainWidth + 6,
height: fontSizePadding,
fill: fillColor,
stroke: strokeColor,
x: 0.5,
y: mouse.y - 7
}
),
/* @__PURE__ */ jsxRuntime.jsx(
"text",
{
ref: gainLabelRef,
x: 3,
y: mouse.y + 3,
fill: color,
fontSize,
fontFamily,
children: gainLabel > 0 ? `+${gainLabel}` : gainLabel
}
),
/* @__PURE__ */ jsxRuntime.jsx(
"line",
{
x1: gainWidth + 7,
x2: width,
y1: mouse.y,
y2: mouse.y,
stroke: strokeColor,
strokeWidth,
strokeDasharray,
strokeLinecap: "round"
}
),
/* @__PURE__ */