mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 04:30:54 +02:00
add spectrum to graphic eq mode
This commit is contained in:
@@ -100,6 +100,11 @@ class AstraScopeModule : Module() {
|
|||||||
Prop("analysisFrameMs") { view, value: Double -> view.analysisFrameMs = value }
|
Prop("analysisFrameMs") { view, value: Double -> view.analysisFrameMs = value }
|
||||||
Prop("smoothing") { view, value: Double -> view.smoothing = value.toFloat() }
|
Prop("smoothing") { view, value: Double -> view.smoothing = value.toFloat() }
|
||||||
Prop("pointCount") { view, value: Int -> view.pointCount = value }
|
Prop("pointCount") { view, value: Int -> view.pointCount = value }
|
||||||
|
Prop("frequencyAnchors") { view, value: List<Double>? ->
|
||||||
|
view.frequencyAnchors = value?.let { frequencies ->
|
||||||
|
FloatArray(frequencies.size) { index -> frequencies[index].toFloat() }
|
||||||
|
}
|
||||||
|
}
|
||||||
Prop("dbMin") { view, value: Double -> view.dbMin = value.toFloat() }
|
Prop("dbMin") { view, value: Double -> view.dbMin = value.toFloat() }
|
||||||
Prop("dbMax") { view, value: Double -> view.dbMax = value.toFloat() }
|
Prop("dbMax") { view, value: Double -> view.dbMax = value.toFloat() }
|
||||||
Prop("tiltDbPerOctave") { view, value: Double -> view.tiltDbPerOctave = value.toFloat() }
|
Prop("tiltDbPerOctave") { view, value: Double -> view.tiltDbPerOctave = value.toFloat() }
|
||||||
|
|||||||
+70
-4
@@ -1,5 +1,6 @@
|
|||||||
package expo.modules.astrascope
|
package expo.modules.astrascope
|
||||||
|
|
||||||
|
import kotlin.math.floor
|
||||||
import kotlin.math.ln
|
import kotlin.math.ln
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -83,6 +84,7 @@ internal object AstraScopeProjection {
|
|||||||
dbMin: Float,
|
dbMin: Float,
|
||||||
dbMax: Float,
|
dbMax: Float,
|
||||||
tiltDbPerOctave: Float,
|
tiltDbPerOctave: Float,
|
||||||
|
frequencyAnchors: FloatArray? = null,
|
||||||
sampleRate: Float = 48_000f
|
sampleRate: Float = 48_000f
|
||||||
) {
|
) {
|
||||||
val bins = min(rawCount, raw.capacity())
|
val bins = min(rawCount, raw.capacity())
|
||||||
@@ -97,12 +99,13 @@ internal object AstraScopeProjection {
|
|||||||
val maxFrequency = max(minFrequency + 1.0, min(MAX_FREQUENCY, nyquist))
|
val maxFrequency = max(minFrequency + 1.0, min(MAX_FREQUENCY, nyquist))
|
||||||
val binWidth = nyquist / bins.toDouble()
|
val binWidth = nyquist / bins.toDouble()
|
||||||
val range = max(1f, dbMax - dbMin)
|
val range = max(1f, dbMax - dbMin)
|
||||||
|
val anchors = validFrequencyAnchors(frequencyAnchors, minFrequency, maxFrequency)
|
||||||
|
|
||||||
for (point in 0 until points) {
|
for (point in 0 until points) {
|
||||||
val t0 = point.toDouble() / (points - 1).toDouble()
|
val t0 = point.toDouble() / (points - 1).toDouble()
|
||||||
val t1 = min(1.0, (point + 1).toDouble() / (points - 1).toDouble())
|
val t1 = min(1.0, (point + 1).toDouble() / (points - 1).toDouble())
|
||||||
val frequency0 = frequencyAt(t0, minFrequency, maxFrequency)
|
val frequency0 = frequencyAt(t0, minFrequency, maxFrequency, anchors)
|
||||||
val frequency1 = frequencyAt(t1, minFrequency, maxFrequency)
|
val frequency1 = frequencyAt(t1, minFrequency, maxFrequency, anchors)
|
||||||
val centerFrequency = (frequency0 + frequency1) * 0.5
|
val centerFrequency = (frequency0 + frequency1) * 0.5
|
||||||
val bin0 = frequency0 / binWidth
|
val bin0 = frequency0 / binWidth
|
||||||
val bin1 = frequency1 / binWidth
|
val bin1 = frequency1 / binWidth
|
||||||
@@ -119,6 +122,20 @@ internal object AstraScopeProjection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps normalized x to frequency. With anchors, each frequency is pinned to
|
||||||
|
* the center of an equal-width column and the intervals remain logarithmic.
|
||||||
|
*/
|
||||||
|
internal fun spectrumFrequencyAt(
|
||||||
|
t: Double,
|
||||||
|
minFrequency: Double = MIN_FREQUENCY,
|
||||||
|
maxFrequency: Double = MAX_FREQUENCY,
|
||||||
|
frequencyAnchors: FloatArray? = null
|
||||||
|
): Double {
|
||||||
|
val anchors = validFrequencyAnchors(frequencyAnchors, minFrequency, maxFrequency)
|
||||||
|
return frequencyAt(t, minFrequency, maxFrequency, anchors)
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the largest absolute value left after one decay step. */
|
/** Returns the largest absolute value left after one decay step. */
|
||||||
fun decay(values: FloatArray, count: Int, factor: Float = DECAY_PER_FRAME): Float {
|
fun decay(values: FloatArray, count: Int, factor: Float = DECAY_PER_FRAME): Float {
|
||||||
var peak = 0f
|
var peak = 0f
|
||||||
@@ -131,8 +148,57 @@ internal object AstraScopeProjection {
|
|||||||
return peak
|
return peak
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun frequencyAt(t: Double, minFrequency: Double, maxFrequency: Double): Double =
|
private fun frequencyAt(
|
||||||
minFrequency * (maxFrequency / minFrequency).pow(t)
|
t: Double,
|
||||||
|
minFrequency: Double,
|
||||||
|
maxFrequency: Double,
|
||||||
|
anchors: FloatArray?
|
||||||
|
): Double {
|
||||||
|
val position = t.coerceIn(0.0, 1.0)
|
||||||
|
if (anchors == null) return logLerp(minFrequency, maxFrequency, position)
|
||||||
|
|
||||||
|
val count = anchors.size
|
||||||
|
val firstCenter = 0.5 / count
|
||||||
|
val lastCenter = (count - 0.5) / count
|
||||||
|
if (position <= firstCenter) {
|
||||||
|
return logLerp(minFrequency, anchors.first().toDouble(), position / firstCenter)
|
||||||
|
}
|
||||||
|
if (position >= lastCenter) {
|
||||||
|
return logLerp(
|
||||||
|
anchors.last().toDouble(),
|
||||||
|
maxFrequency,
|
||||||
|
(position - lastCenter) / (1.0 - lastCenter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val lower = floor(position * count - 0.5).toInt().coerceIn(0, count - 2)
|
||||||
|
val lowerCenter = (lower + 0.5) / count
|
||||||
|
val upperCenter = (lower + 1.5) / count
|
||||||
|
return logLerp(
|
||||||
|
anchors[lower].toDouble(),
|
||||||
|
anchors[lower + 1].toDouble(),
|
||||||
|
(position - lowerCenter) / (upperCenter - lowerCenter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validFrequencyAnchors(
|
||||||
|
anchors: FloatArray?,
|
||||||
|
minFrequency: Double,
|
||||||
|
maxFrequency: Double
|
||||||
|
): FloatArray? {
|
||||||
|
val frequencies = anchors ?: return null
|
||||||
|
if (frequencies.isEmpty()) return null
|
||||||
|
var previous = minFrequency
|
||||||
|
for (anchor in frequencies) {
|
||||||
|
val frequency = anchor.toDouble()
|
||||||
|
if (!frequency.isFinite() || frequency <= previous || frequency >= maxFrequency) return null
|
||||||
|
previous = frequency
|
||||||
|
}
|
||||||
|
return frequencies
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logLerp(start: Double, end: Double, t: Double): Double =
|
||||||
|
start * (end / start).pow(t.coerceIn(0.0, 1.0))
|
||||||
|
|
||||||
private fun safeRefreshRate(refreshRate: Float): Float =
|
private fun safeRefreshRate(refreshRate: Float): Float =
|
||||||
if (refreshRate.isFinite() && refreshRate >= 30f) refreshRate else 60f
|
if (refreshRate.isFinite() && refreshRate >= 30f) refreshRate else 60f
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ internal class AstraScopeView(
|
|||||||
var analysisFrameMs = 32.0
|
var analysisFrameMs = 32.0
|
||||||
var smoothing = 0.92f
|
var smoothing = 0.92f
|
||||||
var pointCount = 120
|
var pointCount = 120
|
||||||
|
var frequencyAnchors: FloatArray? = null
|
||||||
var dbMin = -90f
|
var dbMin = -90f
|
||||||
var dbMax = -10f
|
var dbMax = -10f
|
||||||
var tiltDbPerOctave = 3.5f
|
var tiltDbPerOctave = 3.5f
|
||||||
@@ -332,7 +333,8 @@ internal class AstraScopeView(
|
|||||||
renderedPointCount,
|
renderedPointCount,
|
||||||
dbMin,
|
dbMin,
|
||||||
dbMax,
|
dbMax,
|
||||||
tiltDbPerOctave
|
tiltDbPerOctave,
|
||||||
|
frequencyAnchors
|
||||||
)
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
+39
@@ -22,6 +22,45 @@ class AstraScopeProjectionTest {
|
|||||||
for (index in 1 until out.size) assertTrue(out[index] >= out[index - 1])
|
for (index in 1 until out.size) assertTrue(out[index] >= out[index - 1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun graphicEqFrequenciesAlignToEvenColumnCenters() {
|
||||||
|
val anchors = floatArrayOf(60f, 250f, 1_000f, 4_000f, 12_000f)
|
||||||
|
|
||||||
|
assertEquals(20.0, AstraScopeProjection.spectrumFrequencyAt(0.0, frequencyAnchors = anchors), 0.001)
|
||||||
|
anchors.forEachIndexed { index, frequency ->
|
||||||
|
val center = (index + 0.5) / anchors.size
|
||||||
|
assertEquals(
|
||||||
|
frequency.toDouble(),
|
||||||
|
AstraScopeProjection.spectrumFrequencyAt(center, frequencyAnchors = anchors),
|
||||||
|
0.001
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertEquals(
|
||||||
|
20_000.0,
|
||||||
|
AstraScopeProjection.spectrumFrequencyAt(1.0, frequencyAnchors = anchors),
|
||||||
|
0.001
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun missingOrInvalidFrequencyAnchorsFallBackToLogProjection() {
|
||||||
|
val expectedMidpoint = kotlin.math.sqrt(20.0 * 20_000.0)
|
||||||
|
val nonIncreasing = floatArrayOf(60f, 1_000f, 250f)
|
||||||
|
val outOfRange = floatArrayOf(60f, 25_000f)
|
||||||
|
|
||||||
|
assertEquals(expectedMidpoint, AstraScopeProjection.spectrumFrequencyAt(0.5), 0.001)
|
||||||
|
assertEquals(
|
||||||
|
expectedMidpoint,
|
||||||
|
AstraScopeProjection.spectrumFrequencyAt(0.5, frequencyAnchors = nonIncreasing),
|
||||||
|
0.001
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
expectedMidpoint,
|
||||||
|
AstraScopeProjection.spectrumFrequencyAt(0.5, frequencyAnchors = outOfRange),
|
||||||
|
0.001
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun clampRejectsInvalidAndOutOfRangeValues() {
|
fun clampRejectsInvalidAndOutOfRangeValues() {
|
||||||
assertEquals(0f, AstraScopeProjection.clamp01(Float.NaN), 0f)
|
assertEquals(0f, AstraScopeProjection.clamp01(Float.NaN), 0f)
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ export interface AstraScopeViewProps extends ViewProps {
|
|||||||
analysisFrameMs?: number;
|
analysisFrameMs?: number;
|
||||||
smoothing?: number;
|
smoothing?: number;
|
||||||
pointCount?: number;
|
pointCount?: number;
|
||||||
|
/** Optional frequencies pinned to evenly spaced horizontal centers. */
|
||||||
|
frequencyAnchors?: number[];
|
||||||
dbMin?: number;
|
dbMin?: number;
|
||||||
dbMax?: number;
|
dbMax?: number;
|
||||||
tiltDbPerOctave?: number;
|
tiltDbPerOctave?: number;
|
||||||
|
|||||||
@@ -329,7 +329,12 @@ export default function EQScreen() {
|
|||||||
// in the tracks' own coordinate space, so it stays glued to the thumbs.
|
// in the tracks' own coordinate space, so it stays glued to the thumbs.
|
||||||
const graphicEditorEl = renderEqGraphics ? (
|
const graphicEditorEl = renderEqGraphics ? (
|
||||||
<View style={styles.graphicEditor}>
|
<View style={styles.graphicEditor}>
|
||||||
<GraphicEQPanel gains={eq.graphicGains} enabled={eq.enabled} onChangeGain={eq.setGraphicGain} />
|
<GraphicEQPanel
|
||||||
|
gains={eq.graphicGains}
|
||||||
|
enabled={eq.enabled}
|
||||||
|
spectrumActive={scopeActive && focused}
|
||||||
|
onChangeGain={eq.setGraphicGain}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ interface SpectrumCurveProps {
|
|||||||
source?: 'pre' | 'post';
|
source?: 'pre' | 'post';
|
||||||
/** Number of log-frequency render points. */
|
/** Number of log-frequency render points. */
|
||||||
pointCount?: number;
|
pointCount?: number;
|
||||||
|
/** Optional frequencies pinned to evenly spaced horizontal centers. */
|
||||||
|
frequencyAnchors?: number[];
|
||||||
/** Active render cadence. 0 means display-sync; 32 keeps the mini-player battery-friendly. */
|
/** Active render cadence. 0 means display-sync; 32 keeps the mini-player battery-friendly. */
|
||||||
frameMs?: number;
|
frameMs?: number;
|
||||||
/** Native analysis cadence. Defaults to frameMs. */
|
/** Native analysis cadence. Defaults to frameMs. */
|
||||||
@@ -52,6 +54,7 @@ export function SpectrumCurve({
|
|||||||
active = false,
|
active = false,
|
||||||
source = 'pre',
|
source = 'pre',
|
||||||
pointCount,
|
pointCount,
|
||||||
|
frequencyAnchors,
|
||||||
frameMs = MINI_FRAME_MS,
|
frameMs = MINI_FRAME_MS,
|
||||||
analysisFrameMs,
|
analysisFrameMs,
|
||||||
smoothing = DEFAULT_SMOOTHING,
|
smoothing = DEFAULT_SMOOTHING,
|
||||||
@@ -89,6 +92,7 @@ export function SpectrumCurve({
|
|||||||
analysisFrameMs={analysisFrameMs ?? frameMs}
|
analysisFrameMs={analysisFrameMs ?? frameMs}
|
||||||
smoothing={smoothing}
|
smoothing={smoothing}
|
||||||
pointCount={resolvedPointCount}
|
pointCount={resolvedPointCount}
|
||||||
|
frequencyAnchors={frequencyAnchors}
|
||||||
dbMin={dbMin}
|
dbMin={dbMin}
|
||||||
dbMax={dbMax}
|
dbMax={dbMax}
|
||||||
tiltDbPerOctave={tiltDbPerOctave}
|
tiltDbPerOctave={tiltDbPerOctave}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
interface GraphicEQPanelProps {
|
interface GraphicEQPanelProps {
|
||||||
gains: number[];
|
gains: number[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
spectrumActive: boolean;
|
||||||
onChangeGain: (index: number, gainDb: number) => void;
|
onChangeGain: (index: number, gainDb: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +26,12 @@ interface GraphicEQPanelProps {
|
|||||||
* curve's scale. All cells are gap-less flex:1 so column centers match the
|
* curve's scale. All cells are gap-less flex:1 so column centers match the
|
||||||
* curve's evenly spaced band positions.
|
* curve's evenly spaced band positions.
|
||||||
*/
|
*/
|
||||||
export function GraphicEQPanel({ gains, enabled, onChangeGain }: GraphicEQPanelProps) {
|
export function GraphicEQPanel({
|
||||||
|
gains,
|
||||||
|
enabled,
|
||||||
|
spectrumActive,
|
||||||
|
onChangeGain,
|
||||||
|
}: GraphicEQPanelProps) {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const colors = useColors();
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
@@ -44,7 +50,11 @@ export function GraphicEQPanel({ gains, enabled, onChangeGain }: GraphicEQPanelP
|
|||||||
|
|
||||||
<View style={styles.trackRow}>
|
<View style={styles.trackRow}>
|
||||||
<View style={StyleSheet.absoluteFill}>
|
<View style={StyleSheet.absoluteFill}>
|
||||||
<GraphicResponseCurve gains={gains} enabled={enabled} />
|
<GraphicResponseCurve
|
||||||
|
gains={gains}
|
||||||
|
enabled={enabled}
|
||||||
|
spectrumActive={spectrumActive}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
{GRAPHIC_BANDS.map((def, i) => (
|
{GRAPHIC_BANDS.map((def, i) => (
|
||||||
<VerticalEQSlider
|
<VerticalEQSlider
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Skia,
|
Skia,
|
||||||
type SkPath
|
type SkPath
|
||||||
} from '@shopify/react-native-skia';
|
} from '@shopify/react-native-skia';
|
||||||
|
import { SpectrumCurve } from '@/components/SpectrumCurve';
|
||||||
import { useColors } from '@/theme/themed';
|
import { useColors } from '@/theme/themed';
|
||||||
import type { EQBand } from '@/types/audio';
|
import type { EQBand } from '@/types/audio';
|
||||||
import {
|
import {
|
||||||
@@ -24,10 +25,12 @@ import { GRAPHIC_BANDS, buildGraphicBands } from '@/audio/graphicEq';
|
|||||||
import { GRAPH_SAMPLE_RATE, buildResponseFill } from './eqGraphMath';
|
import { GRAPH_SAMPLE_RATE, buildResponseFill } from './eqGraphMath';
|
||||||
|
|
||||||
const SAMPLES = 96;
|
const SAMPLES = 96;
|
||||||
|
const GRAPHIC_SPECTRUM_ANCHORS = GRAPHIC_BANDS.map((band) => band.frequency);
|
||||||
|
|
||||||
interface GraphicResponseCurveProps {
|
interface GraphicResponseCurveProps {
|
||||||
gains: number[];
|
gains: number[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
spectrumActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +41,11 @@ interface GraphicResponseCurveProps {
|
|||||||
* Rendered behind the sliders; transparent background (the editor card owns
|
* Rendered behind the sliders; transparent background (the editor card owns
|
||||||
* the chrome).
|
* the chrome).
|
||||||
*/
|
*/
|
||||||
export function GraphicResponseCurve({ gains, enabled }: GraphicResponseCurveProps) {
|
export function GraphicResponseCurve({
|
||||||
|
gains,
|
||||||
|
enabled,
|
||||||
|
spectrumActive,
|
||||||
|
}: GraphicResponseCurveProps) {
|
||||||
const colors = useColors();
|
const colors = useColors();
|
||||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
const width = size.width;
|
const width = size.width;
|
||||||
@@ -61,36 +68,53 @@ export function GraphicResponseCurve({ gains, enabled }: GraphicResponseCurvePro
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container} onLayout={onLayout} pointerEvents="none">
|
<View style={styles.container} onLayout={onLayout} pointerEvents="none">
|
||||||
{width > 0 && height > 0 ? (
|
{width > 0 && height > 0 ? (
|
||||||
<Canvas style={StyleSheet.absoluteFill}>
|
<>
|
||||||
{/* Grid: ±6 dB lines + dashed 0 dB midline (track coordinates). */}
|
<View style={StyleSheet.absoluteFill}>
|
||||||
<Group>
|
<SpectrumCurve
|
||||||
<Path
|
source="post"
|
||||||
path={hLine(gainToTrackY(6, height), width)}
|
active={spectrumActive}
|
||||||
color={colors.glassBorder}
|
width={width}
|
||||||
style="stroke"
|
height={height}
|
||||||
strokeWidth={1}
|
frameMs={16}
|
||||||
|
frequencyAnchors={GRAPHIC_SPECTRUM_ANCHORS}
|
||||||
|
color={colors.accent}
|
||||||
|
lineOpacity={0.22}
|
||||||
|
fillOpacity={0.5}
|
||||||
|
glow={false}
|
||||||
/>
|
/>
|
||||||
<Path
|
</View>
|
||||||
path={hLine(gainToTrackY(-6, height), width)}
|
|
||||||
color={colors.glassBorder}
|
|
||||||
style="stroke"
|
|
||||||
strokeWidth={1}
|
|
||||||
/>
|
|
||||||
<Path path={hLine(height / 2, width)} color={colors.glassBorder} style="stroke" strokeWidth={1}>
|
|
||||||
<DashPathEffect intervals={[3, 5]} />
|
|
||||||
</Path>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Path path={fillPath} color={withAlpha(curveColor, 0.1)} style="fill" />
|
<Canvas style={StyleSheet.absoluteFill}>
|
||||||
<Path
|
{/* Grid: ±6 dB lines + dashed 0 dB midline (track coordinates). */}
|
||||||
path={linePath}
|
<Group>
|
||||||
color={curveColor}
|
<Path
|
||||||
style="stroke"
|
path={hLine(gainToTrackY(6, height), width)}
|
||||||
strokeWidth={2}
|
color={colors.glassBorder}
|
||||||
strokeJoin="round"
|
style="stroke"
|
||||||
strokeCap="round"
|
strokeWidth={1}
|
||||||
/>
|
/>
|
||||||
</Canvas>
|
<Path
|
||||||
|
path={hLine(gainToTrackY(-6, height), width)}
|
||||||
|
color={colors.glassBorder}
|
||||||
|
style="stroke"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
<Path path={hLine(height / 2, width)} color={colors.glassBorder} style="stroke" strokeWidth={1}>
|
||||||
|
<DashPathEffect intervals={[3, 5]} />
|
||||||
|
</Path>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Path path={fillPath} color={withAlpha(curveColor, 0.1)} style="fill" />
|
||||||
|
<Path
|
||||||
|
path={linePath}
|
||||||
|
color={curveColor}
|
||||||
|
style="stroke"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeJoin="round"
|
||||||
|
strokeCap="round"
|
||||||
|
/>
|
||||||
|
</Canvas>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user