react-native-vroom-chart 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/CMakeLists.txt +143 -0
- package/android/README.md +60 -5
- package/android/build.gradle +115 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/VroomChartJni.cpp +24 -0
- package/android/src/main/java/com/vroom/chart/VroomChartModule.kt +31 -0
- package/android/src/main/java/com/vroom/chart/VroomChartPackage.kt +31 -0
- package/cpp/VroomChartHostObject.cpp +368 -33
- package/cpp/VroomJsiInstaller.cpp +11 -1
- package/cpp/VroomSkiaContext.cpp +15 -7
- package/cpp/_core_include/vroom/vroom_chart.h +250 -22
- package/cpp/_core_src/bollinger.h +1 -1
- package/cpp/_core_src/candles.cpp +138 -41
- package/cpp/_core_src/candles.h +11 -1
- package/cpp/_core_src/chart.cpp +98 -40
- package/cpp/_core_src/chart.h +82 -20
- package/cpp/_core_src/chart_facade.cpp +297 -66
- package/cpp/_core_src/gradient.cpp +46 -0
- package/cpp/_core_src/gradient.h +31 -0
- package/cpp/_core_src/labels.cpp +49 -16
- package/cpp/_core_src/labels.h +33 -4
- package/cpp/_core_src/liquidity.cpp +3 -37
- package/cpp/_core_src/ma_overlay.cpp +174 -12
- package/cpp/_core_src/ma_overlay.h +55 -3
- package/cpp/_core_src/macd.cpp +13 -42
- package/cpp/_core_src/macd.h +11 -8
- package/cpp/_core_src/macd_pane.cpp +57 -27
- package/cpp/_core_src/price_line_layout.cpp +87 -0
- package/cpp/_core_src/price_line_layout.h +80 -0
- package/cpp/_core_src/price_lines.cpp +428 -0
- package/cpp/_core_src/price_lines.h +56 -0
- package/cpp/_core_src/rsi.cpp +4 -18
- package/cpp/_core_src/rsi.h +6 -6
- package/cpp/_core_src/rsi_pane.cpp +34 -19
- package/cpp/_core_src/series_ma.cpp +64 -0
- package/cpp/_core_src/series_ma.h +30 -0
- package/cpp/_core_src/style_inherit.h +35 -0
- package/cpp/_core_src/theme.cpp +2 -1
- package/cpp/_core_src/viewport.cpp +36 -12
- package/cpp/_core_src/viewport.h +50 -0
- package/cpp/_core_src/volume.cpp +33 -8
- package/cpp/_core_src/volume.h +12 -1
- package/cpp/_core_src/volume_anim.cpp +32 -0
- package/cpp/_core_src/volume_anim.h +41 -0
- package/lib/index.d.mts +261 -25
- package/lib/index.d.ts +261 -25
- package/lib/index.js +252 -36
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +252 -36
- package/lib/index.mjs.map +1 -1
- package/package.json +5 -4
- package/src/VroomChart.tsx +162 -11
- package/src/easing.ts +40 -0
- package/src/index.ts +5 -0
- package/src/jsi.d.ts +124 -20
- package/src/theme.ts +1 -0
- package/src/types.ts +5 -0
- package/src/useChartCore.ts +166 -28
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#include "series_ma.h"
|
|
2
|
+
|
|
3
|
+
#include <cmath> // std::nan, std::isfinite
|
|
4
|
+
#include <cstddef>
|
|
5
|
+
|
|
6
|
+
#include "ma.h"
|
|
7
|
+
|
|
8
|
+
namespace vroom::series_ma {
|
|
9
|
+
|
|
10
|
+
void ema_seeded(const std::vector<double>& src, int period,
|
|
11
|
+
std::vector<double>& out) {
|
|
12
|
+
const std::size_t n = src.size();
|
|
13
|
+
out.assign(n, std::nan(""));
|
|
14
|
+
if (period < 1) return;
|
|
15
|
+
const std::size_t P = static_cast<std::size_t>(period);
|
|
16
|
+
|
|
17
|
+
std::size_t f = 0;
|
|
18
|
+
while (f < n && !std::isfinite(src[f])) ++f;
|
|
19
|
+
if (f >= n || f + P > n) return; // not enough finite values to seed
|
|
20
|
+
|
|
21
|
+
const std::size_t seed = f + P - 1;
|
|
22
|
+
double sum = 0.0;
|
|
23
|
+
for (std::size_t k = f; k <= seed; ++k) sum += src[k];
|
|
24
|
+
double prev = sum / static_cast<double>(P);
|
|
25
|
+
out[seed] = prev;
|
|
26
|
+
|
|
27
|
+
const double alpha = 2.0 / (static_cast<double>(P) + 1.0);
|
|
28
|
+
for (std::size_t i = seed + 1; i < n; ++i) {
|
|
29
|
+
if (!std::isfinite(src[i])) break; // series are contiguous after f
|
|
30
|
+
prev = alpha * src[i] + (1.0 - alpha) * prev;
|
|
31
|
+
out[i] = prev;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
void sma_seeded(const std::vector<double>& src, int period,
|
|
36
|
+
std::vector<double>& out) {
|
|
37
|
+
const std::size_t n = src.size();
|
|
38
|
+
out.assign(n, std::nan(""));
|
|
39
|
+
if (period < 1) return;
|
|
40
|
+
const std::size_t P = static_cast<std::size_t>(period);
|
|
41
|
+
|
|
42
|
+
std::size_t f = 0;
|
|
43
|
+
while (f < n && !std::isfinite(src[f])) ++f;
|
|
44
|
+
if (f >= n || f + P > n) return;
|
|
45
|
+
|
|
46
|
+
double sum = 0.0;
|
|
47
|
+
for (std::size_t i = f; i < n; ++i) {
|
|
48
|
+
if (!std::isfinite(src[i])) break; // series are contiguous after f
|
|
49
|
+
sum += src[i];
|
|
50
|
+
if (i >= f + P) sum -= src[i - P];
|
|
51
|
+
if (i >= f + P - 1) out[i] = sum / static_cast<double>(P);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
void smooth(const std::vector<double>& src, int kind, int period,
|
|
56
|
+
std::vector<double>& out) {
|
|
57
|
+
if (kind == vroom::ma::KIND_SMA) {
|
|
58
|
+
sma_seeded(src, period, out);
|
|
59
|
+
} else {
|
|
60
|
+
ema_seeded(src, period, out);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
} // namespace vroom::series_ma
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Moving averages over an already-computed series — pure computation, no Skia.
|
|
2
|
+
//
|
|
3
|
+
// Distinct from ma.h, which averages a *price* source off the candles. These
|
|
4
|
+
// smooth a derived series that may open with a run of NaN warmup (the MACD
|
|
5
|
+
// difference, an RSI line), so they skip that run and start counting from the
|
|
6
|
+
// first finite value. Both smoothers therefore produce their first value at the
|
|
7
|
+
// same index, which is what lets the MA kind be a config toggle.
|
|
8
|
+
|
|
9
|
+
#pragma once
|
|
10
|
+
|
|
11
|
+
#include <vector>
|
|
12
|
+
|
|
13
|
+
namespace vroom::series_ma {
|
|
14
|
+
|
|
15
|
+
// SMA-seeded EMA over `src`. Fills `out` (resized to src.size()): NaN until the
|
|
16
|
+
// seed index, the seed = SMA of the first `period` finite values, then
|
|
17
|
+
// ema = alpha*src + (1-alpha)*prev.
|
|
18
|
+
void ema_seeded(const std::vector<double>& src, int period,
|
|
19
|
+
std::vector<double>& out);
|
|
20
|
+
|
|
21
|
+
// Rolling SMA over `src`, skipping the same leading NaN run as ema_seeded.
|
|
22
|
+
void sma_seeded(const std::vector<double>& src, int period,
|
|
23
|
+
std::vector<double>& out);
|
|
24
|
+
|
|
25
|
+
// Dispatches on a vroom::ma KIND_* value: KIND_SMA picks sma_seeded, anything
|
|
26
|
+
// else ema_seeded.
|
|
27
|
+
void smooth(const std::vector<double>& src, int kind, int period,
|
|
28
|
+
std::vector<double>& out);
|
|
29
|
+
|
|
30
|
+
} // namespace vroom::series_ma
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Resolution of the indicator style "inherit sentinels".
|
|
2
|
+
//
|
|
3
|
+
// Every indicator style field is optional at the public API, so the marshalled
|
|
4
|
+
// struct needs a value that means "the host didn't set this". A fully
|
|
5
|
+
// transparent color and a non-positive width are those sentinels: they can't
|
|
6
|
+
// name anything drawable, so they're free to carry the meaning. The renderer
|
|
7
|
+
// swaps them for the theme value or the built-in default at draw time, which
|
|
8
|
+
// keeps the fallback in one place instead of freezing it into every bridge.
|
|
9
|
+
|
|
10
|
+
#pragma once
|
|
11
|
+
|
|
12
|
+
#pragma clang diagnostic push
|
|
13
|
+
#pragma clang diagnostic ignored "-Wdocumentation"
|
|
14
|
+
#include "include/core/SkColor.h"
|
|
15
|
+
#pragma clang diagnostic pop
|
|
16
|
+
|
|
17
|
+
namespace vroom::style {
|
|
18
|
+
|
|
19
|
+
inline SkColor color_or(uint32_t configured, SkColor fallback) {
|
|
20
|
+
return SkColorGetA(configured) == 0 ? fallback : configured;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// For an element whose default is another element's color dimmed — the easing
|
|
24
|
+
// MACD histogram bars, say.
|
|
25
|
+
inline SkColor faded_or(uint32_t configured, SkColor base, float alpha_frac) {
|
|
26
|
+
if (SkColorGetA(configured) != 0) return configured;
|
|
27
|
+
return SkColorSetA(base,
|
|
28
|
+
static_cast<U8CPU>(SkColorGetA(base) * alpha_frac));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
inline float width_or(float configured, float fallback) {
|
|
32
|
+
return configured > 0.f ? configured : fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
} // namespace vroom::style
|
package/cpp/_core_src/theme.cpp
CHANGED
|
@@ -22,7 +22,7 @@ constexpr uint32_t kDefaultColors[VROOM_COLOR_COUNT_] = {
|
|
|
22
22
|
0x00000000, // WICK_BEAR — transparent sentinel: inherit BEAR fill
|
|
23
23
|
0xff26a69a, // ACCENT_BULL — classic teal-green (price indicator, volume, MACD)
|
|
24
24
|
0xffef5350, // ACCENT_BEAR — classic red
|
|
25
|
-
|
|
25
|
+
0xff8957e5, // LINE — line-chart close polyline; violet, matching the RSI line
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
|
|
@@ -38,6 +38,7 @@ constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
|
|
|
38
38
|
0.f, // WICK_ROUND_CAP — butt caps by default
|
|
39
39
|
0.f, // VOLUME_RADIUS_PX — square by default
|
|
40
40
|
1.5f, // LINE_WIDTH_PX — line-chart polyline stroke width
|
|
41
|
+
0.28f, // LINE_GRADIENT_OPACITY — fill under the line, ramped to 0 at the pane bottom
|
|
41
42
|
};
|
|
42
43
|
|
|
43
44
|
} // namespace
|
|
@@ -10,8 +10,7 @@ float candle_body_width(const Layout& layout,
|
|
|
10
10
|
int64_t window_ms,
|
|
11
11
|
int64_t candle_duration_ms) {
|
|
12
12
|
if (window_ms <= 0 || candle_duration_ms <= 0) return 0.f;
|
|
13
|
-
const float usable =
|
|
14
|
-
layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
|
|
13
|
+
const float usable = candle_area_width(layout);
|
|
15
14
|
const double slot = static_cast<double>(usable) *
|
|
16
15
|
(static_cast<double>(candle_duration_ms) /
|
|
17
16
|
static_cast<double>(window_ms));
|
|
@@ -24,8 +23,7 @@ float candle_center_x(const Layout& layout,
|
|
|
24
23
|
int64_t visible_start_ms,
|
|
25
24
|
int64_t window_ms) {
|
|
26
25
|
if (window_ms <= 0) return 0.f;
|
|
27
|
-
const float usable =
|
|
28
|
-
layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
|
|
26
|
+
const float usable = candle_area_width(layout);
|
|
29
27
|
const double center_time =
|
|
30
28
|
static_cast<double>(time_ms) +
|
|
31
29
|
static_cast<double>(candle_duration_ms) * 0.5;
|
|
@@ -213,19 +211,45 @@ PriceBounds auto_price_bounds(const ::VroomCandle* candles, size_t count) {
|
|
|
213
211
|
return {mid - half, mid + half};
|
|
214
212
|
}
|
|
215
213
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
214
|
+
PriceBounds preserve_envelope_bounds(const PriceBounds& old_axis,
|
|
215
|
+
const PriceBounds& old_env,
|
|
216
|
+
const PriceBounds& new_env) {
|
|
217
|
+
const double axis_range = old_axis.max - old_axis.min;
|
|
218
|
+
const double old_span = old_env.max - old_env.min;
|
|
219
|
+
const double new_span = new_env.max - new_env.min;
|
|
220
|
+
if (axis_range <= 0.0 || old_span <= 0.0 || new_span <= 0.0) return old_axis;
|
|
221
|
+
|
|
222
|
+
// Same span/range ratio => same pixel height for the envelope.
|
|
223
|
+
const double new_range = axis_range * (new_span / old_span);
|
|
224
|
+
// Fraction of the axis (from the bottom) where the old envelope's midpoint
|
|
225
|
+
// sat; putting the new midpoint at the same fraction pins the envelope box.
|
|
226
|
+
const double t =
|
|
227
|
+
((old_env.min + old_env.max) * 0.5 - old_axis.min) / axis_range;
|
|
228
|
+
const double new_mid = (new_env.min + new_env.max) * 0.5;
|
|
229
|
+
return {new_mid - t * new_range, new_mid + (1.0 - t) * new_range};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
double price_fraction(const PriceBounds& bounds, double price) {
|
|
233
|
+
const double range = bounds.max - bounds.min;
|
|
234
|
+
// Degenerate range: everything sits at the band midpoint, which keeps
|
|
235
|
+
// price_to_y's flat-series fallback intact.
|
|
236
|
+
if (range <= 0.0) return 0.5;
|
|
237
|
+
return (price - bounds.min) / range; // 0 at min, 1 at max
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
float y_at_fraction(const Layout& layout, double frac) {
|
|
219
241
|
// The candle drawing area is the full height minus the x-axis strip and
|
|
220
242
|
// any below-chart indicator pane.
|
|
221
243
|
const float candle_area_h = price_pane_bottom(layout);
|
|
222
244
|
const float top = candle_area_h * layout.top_padding_frac;
|
|
223
245
|
const float bot = candle_area_h * (1.f - layout.bottom_padding_frac);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
246
|
+
return bot - static_cast<float>(frac) * (bot - top); // invert: high → low y
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
float price_to_y(const Layout& layout,
|
|
250
|
+
const PriceBounds& bounds,
|
|
251
|
+
double price) {
|
|
252
|
+
return y_at_fraction(layout, price_fraction(bounds, price));
|
|
229
253
|
}
|
|
230
254
|
|
|
231
255
|
double y_to_price(const Layout& layout,
|
package/cpp/_core_src/viewport.h
CHANGED
|
@@ -34,6 +34,12 @@ inline float x_axis_top(const Layout& l) {
|
|
|
34
34
|
return l.height_px - l.x_axis_height_px;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Width available to candles: the full width minus the y-axis strip and the
|
|
38
|
+
// right-hand gutter.
|
|
39
|
+
inline float candle_area_width(const Layout& l) {
|
|
40
|
+
return l.width_px - l.y_axis_width_px - l.right_padding_px;
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
struct PriceBounds {
|
|
38
44
|
double min;
|
|
39
45
|
double max;
|
|
@@ -45,6 +51,27 @@ struct IndexRange {
|
|
|
45
51
|
size_t end;
|
|
46
52
|
};
|
|
47
53
|
|
|
54
|
+
// One candle's geometry captured in normalized form for the interval morph:
|
|
55
|
+
// x as a fraction of the candle-area width, prices as fractions of the price
|
|
56
|
+
// band. Resolution-independent, so a resize mid-morph stays correct — and
|
|
57
|
+
// independent of the price bounds, so the captured geometry keeps its pixel
|
|
58
|
+
// position after a timeframe switch re-scales the y-axis.
|
|
59
|
+
struct CandleSnapshot {
|
|
60
|
+
float x;
|
|
61
|
+
float open, high, low, close;
|
|
62
|
+
bool bull; // close >= open; selects the fill / wick / border color
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// How many captured slots still contribute to a frame — 0 once the morph is
|
|
66
|
+
// done, which releases the capture. Drawing routines take max(n, this) as their
|
|
67
|
+
// slot count, pairing the new candle at slot k (visible[n - 1 - k]) with the
|
|
68
|
+
// captured one (from[k]), both counting back from the right edge.
|
|
69
|
+
inline std::size_t morph_from_count(const CandleSnapshot* from,
|
|
70
|
+
std::size_t from_n,
|
|
71
|
+
float morph_t) {
|
|
72
|
+
return (from && morph_t < 1.f) ? from_n : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
48
75
|
// Returns the indices of candles whose time_ms falls in [start_ms, end_ms].
|
|
49
76
|
// When both are 0, returns the full range (Phase 1 default-everything behavior).
|
|
50
77
|
// Candles must be sorted ascending by time_ms (invariant of the public API).
|
|
@@ -140,9 +167,32 @@ constexpr double kAutoYZoom = 1.5;
|
|
|
140
167
|
// {0, 1} sentinel when count == 0 (callers keep their previous bounds).
|
|
141
168
|
PriceBounds auto_price_bounds(const ::VroomCandle* candles, size_t count);
|
|
142
169
|
|
|
170
|
+
// Rescales an axis range so a data envelope keeps the pixel height *and* the
|
|
171
|
+
// pixel position it had before a data swap — the "scale lock" applied on a
|
|
172
|
+
// timeframe switch, where the same price action re-buckets into a smaller or
|
|
173
|
+
// larger high-low span.
|
|
174
|
+
//
|
|
175
|
+
// `old_axis` is the axis range that was in effect; `old_env` / `new_env` are the
|
|
176
|
+
// visible high-low envelopes (as returned by price_bounds) before and after the
|
|
177
|
+
// swap. Resolution-independent: both envelopes share the same draw band, so the
|
|
178
|
+
// band height cancels out. Returns `old_axis` unchanged when any span is
|
|
179
|
+
// degenerate.
|
|
180
|
+
PriceBounds preserve_envelope_bounds(const PriceBounds& old_axis,
|
|
181
|
+
const PriceBounds& old_env,
|
|
182
|
+
const PriceBounds& new_env);
|
|
183
|
+
|
|
143
184
|
// Map a price to y in pixels. y=0 is top of the chart.
|
|
144
185
|
float price_to_y(const Layout& layout, const PriceBounds& bounds, double price);
|
|
145
186
|
|
|
187
|
+
// Fraction of the price band a price sits at: 0 = bounds.min, 1 = bounds.max.
|
|
188
|
+
// Returns 0.5 for a degenerate range, matching price_to_y's midpoint fallback.
|
|
189
|
+
double price_fraction(const PriceBounds& bounds, double price);
|
|
190
|
+
|
|
191
|
+
// The y of a band fraction. Splits price_to_y in two so geometry can be stored
|
|
192
|
+
// independently of the price bounds (see CandleSnapshot):
|
|
193
|
+
// price_to_y(l, b, p) == y_at_fraction(l, price_fraction(b, p))
|
|
194
|
+
float y_at_fraction(const Layout& layout, double frac);
|
|
195
|
+
|
|
146
196
|
// Inverse of price_to_y: map a pixel y in the price pane back to a price.
|
|
147
197
|
// Returns bounds.min for a degenerate range or draw band.
|
|
148
198
|
double y_to_price(const Layout& layout, const PriceBounds& bounds, float y);
|
package/cpp/_core_src/volume.cpp
CHANGED
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
#include "theme.h"
|
|
14
14
|
#include "viewport.h"
|
|
15
|
+
#include "volume_anim.h"
|
|
15
16
|
|
|
16
17
|
namespace vroom::volume {
|
|
17
18
|
|
|
18
19
|
namespace {
|
|
19
|
-
|
|
20
|
+
// Ceiling for the tallest bar when the config inherits it.
|
|
21
|
+
constexpr float kDefaultHeightFrac = 0.20f;
|
|
20
22
|
} // namespace
|
|
21
23
|
|
|
22
24
|
void draw(SkCanvas* canvas,
|
|
@@ -24,6 +26,9 @@ void draw(SkCanvas* canvas,
|
|
|
24
26
|
std::size_t n,
|
|
25
27
|
const Layout& lay,
|
|
26
28
|
const Theme& theme,
|
|
29
|
+
const ::VroomVolume& cfg,
|
|
30
|
+
float collapse_t,
|
|
31
|
+
int32_t collapse_easing,
|
|
27
32
|
int64_t window_ms,
|
|
28
33
|
int64_t visible_start_ms,
|
|
29
34
|
int64_t candle_duration_ms) {
|
|
@@ -34,23 +39,38 @@ void draw(SkCanvas* canvas,
|
|
|
34
39
|
for (std::size_t i = 0; i < n; ++i) max_vol = std::max(max_vol, visible[i].volume);
|
|
35
40
|
if (max_vol <= 0.0) return;
|
|
36
41
|
|
|
42
|
+
// Resolve every inherit sentinel up front so the loop reads plain values.
|
|
43
|
+
const float height_frac =
|
|
44
|
+
cfg.height_frac >= 0.f ? cfg.height_frac : kDefaultHeightFrac;
|
|
45
|
+
const float opacity = cfg.opacity >= 0.f
|
|
46
|
+
? cfg.opacity
|
|
47
|
+
: theme.floats[VROOM_FLOAT_VOLUME_OPACITY];
|
|
48
|
+
const float vol_r = cfg.radius_px >= 0.f
|
|
49
|
+
? cfg.radius_px
|
|
50
|
+
: theme.floats[VROOM_FLOAT_VOLUME_RADIUS_PX];
|
|
51
|
+
const SkColor up_color = cfg.up_color != 0
|
|
52
|
+
? static_cast<SkColor>(cfg.up_color)
|
|
53
|
+
: theme.colors[VROOM_COLOR_ACCENT_BULL];
|
|
54
|
+
const SkColor down_color = cfg.down_color != 0
|
|
55
|
+
? static_cast<SkColor>(cfg.down_color)
|
|
56
|
+
: theme.colors[VROOM_COLOR_ACCENT_BEAR];
|
|
57
|
+
|
|
37
58
|
const float candle_area_h = vroom::price_pane_bottom(lay);
|
|
38
|
-
const float region_h = candle_area_h *
|
|
59
|
+
const float region_h = candle_area_h * height_frac;
|
|
60
|
+
if (region_h <= 0.f) return;
|
|
39
61
|
|
|
40
62
|
const float body_w = vroom::candle_body_width(
|
|
41
63
|
lay, window_ms, candle_duration_ms);
|
|
42
64
|
const float half_body = body_w * 0.5f;
|
|
43
65
|
|
|
44
|
-
const float opacity = theme.floats[VROOM_FLOAT_VOLUME_OPACITY];
|
|
45
|
-
const float vol_r = theme.floats[VROOM_FLOAT_VOLUME_RADIUS_PX];
|
|
46
66
|
SkPaint bull_paint;
|
|
47
67
|
bull_paint.setAntiAlias(true);
|
|
48
|
-
bull_paint.setColor(
|
|
68
|
+
bull_paint.setColor(up_color);
|
|
49
69
|
bull_paint.setAlphaf(opacity);
|
|
50
70
|
|
|
51
71
|
SkPaint bear_paint;
|
|
52
72
|
bear_paint.setAntiAlias(true);
|
|
53
|
-
bear_paint.setColor(
|
|
73
|
+
bear_paint.setColor(down_color);
|
|
54
74
|
bear_paint.setAlphaf(opacity);
|
|
55
75
|
|
|
56
76
|
for (std::size_t i = 0; i < n; ++i) {
|
|
@@ -61,8 +81,13 @@ void draw(SkCanvas* canvas,
|
|
|
61
81
|
const float cx = vroom::candle_center_x(
|
|
62
82
|
lay, c.time_ms, candle_duration_ms,
|
|
63
83
|
visible_start_ms, window_ms);
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
// The 1px floor applies to the settled height, not the animated one, so
|
|
85
|
+
// a collapsing bar can still reach zero.
|
|
86
|
+
const float frac = static_cast<float>(c.volume / max_vol);
|
|
87
|
+
const float full_h = std::max(1.f, frac * region_h);
|
|
88
|
+
const float h = full_h * (1.f - vroom::volume_anim::bar_collapse(
|
|
89
|
+
frac, collapse_t, collapse_easing));
|
|
90
|
+
if (h < 0.5f) continue; // collapsed past the point of drawing anything
|
|
66
91
|
const SkRect rect =
|
|
67
92
|
SkRect::MakeXYWH(cx - half_body, candle_area_h - h, body_w, h);
|
|
68
93
|
const SkPaint& paint = bull ? bull_paint : bear_paint;
|
package/cpp/_core_src/volume.h
CHANGED
|
@@ -21,12 +21,23 @@ namespace vroom::volume {
|
|
|
21
21
|
|
|
22
22
|
// Draws a volume bar for each candle in [visible, visible + n). Bars share the
|
|
23
23
|
// candles' x/width (via candle_center_x / candle_body_width) so they scroll and
|
|
24
|
-
// resize with the candles. Heights auto-fit to the max volume in the slice
|
|
24
|
+
// resize with the candles. Heights auto-fit to the max volume in the slice, so
|
|
25
|
+
// the loudest bar lands on cfg.height_frac of the pane.
|
|
26
|
+
//
|
|
27
|
+
// `cfg` supplies height/opacity/radius/colors; each of its style fields falls
|
|
28
|
+
// back to `theme` when left on its inherit sentinel.
|
|
29
|
+
//
|
|
30
|
+
// `collapse_t` (0 = full height, 1 = flat) staggers the bars down out of view,
|
|
31
|
+
// tallest first, all landing together — see volume_anim.h. It's also the
|
|
32
|
+
// visibility gate: callers skip this entirely at 1, since nothing would draw.
|
|
25
33
|
void draw(SkCanvas* canvas,
|
|
26
34
|
const ::VroomCandle* visible,
|
|
27
35
|
std::size_t n,
|
|
28
36
|
const Layout& lay,
|
|
29
37
|
const Theme& theme,
|
|
38
|
+
const ::VroomVolume& cfg,
|
|
39
|
+
float collapse_t,
|
|
40
|
+
int32_t collapse_easing,
|
|
30
41
|
int64_t window_ms,
|
|
31
42
|
int64_t visible_start_ms,
|
|
32
43
|
int64_t candle_duration_ms);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#include "volume_anim.h"
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
|
|
5
|
+
#include "vroom/vroom_chart.h"
|
|
6
|
+
|
|
7
|
+
namespace vroom::volume_anim {
|
|
8
|
+
|
|
9
|
+
float ease(int32_t kind, float p) {
|
|
10
|
+
const float t = std::clamp(p, 0.f, 1.f);
|
|
11
|
+
switch (kind) {
|
|
12
|
+
case VROOM_EASING_LINEAR:
|
|
13
|
+
return t;
|
|
14
|
+
case VROOM_EASING_IN:
|
|
15
|
+
return t * t;
|
|
16
|
+
case VROOM_EASING_OUT:
|
|
17
|
+
return t * (2.f - t);
|
|
18
|
+
default:
|
|
19
|
+
return t * t * (3.f - 2.f * t); // smoothstep
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
float bar_collapse(float frac, float t, int32_t easing) {
|
|
24
|
+
const float progress = std::clamp(t, 0.f, 1.f);
|
|
25
|
+
const float window = std::clamp(frac, kMinWindow, 1.f);
|
|
26
|
+
// The window occupies the tail of the timeline, so a taller bar (bigger
|
|
27
|
+
// window) opens earlier. Every window ends at 1.
|
|
28
|
+
const float start = 1.f - window;
|
|
29
|
+
return ease(easing, (progress - start) / window);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
} // namespace vroom::volume_anim
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Volume-bar collapse timing — pure functions, no Skia, no state.
|
|
2
|
+
//
|
|
3
|
+
// Split out from volume.cpp so the stagger arithmetic unit-tests without a Skia
|
|
4
|
+
// checkout, and so the one piece of easing the core owns lives somewhere
|
|
5
|
+
// obvious.
|
|
6
|
+
//
|
|
7
|
+
// Unlike the candle↔line and interval morphs, the host can't pre-ease this
|
|
8
|
+
// animation: every bar runs on its own window, so easing has to be applied per
|
|
9
|
+
// bar, after the window split. Feed bar_collapse the raw linear progress.
|
|
10
|
+
|
|
11
|
+
#pragma once
|
|
12
|
+
|
|
13
|
+
#include <cstdint>
|
|
14
|
+
|
|
15
|
+
namespace vroom::volume_anim {
|
|
16
|
+
|
|
17
|
+
// Shortest window any bar gets, as a fraction of the timeline. Without a floor a
|
|
18
|
+
// near-zero bar would collapse inside a single frame and read as a pop rather
|
|
19
|
+
// than a fall. Clamping only moves a start earlier, so every bar still lands
|
|
20
|
+
// together.
|
|
21
|
+
constexpr float kMinWindow = 0.2f;
|
|
22
|
+
|
|
23
|
+
// Linear progress (0..1) mapped through a VroomEasing curve. Mirrors ease() in
|
|
24
|
+
// packages/react/src/easing.ts; unknown kinds fall back to smoothstep, matching
|
|
25
|
+
// it. Progress outside 0..1 is clamped.
|
|
26
|
+
float ease(int32_t kind, float p);
|
|
27
|
+
|
|
28
|
+
// How far a single bar has collapsed at global linear progress `t`, from 0
|
|
29
|
+
// (untouched, full height) to 1 (flat).
|
|
30
|
+
//
|
|
31
|
+
// `frac` is the bar's height as a fraction of the tallest bar in view, and
|
|
32
|
+
// doubles as the length of its window: a bar falls over the last `frac` of the
|
|
33
|
+
// timeline. So the tallest bar starts immediately and takes the whole
|
|
34
|
+
// transition, shorter bars start progressively later, and everything reaches
|
|
35
|
+
// zero at t = 1 — the "tallest first, all land together" cascade.
|
|
36
|
+
//
|
|
37
|
+
// Symmetric in `t`, so driving it backwards (1 -> 0) plays the reveal: the
|
|
38
|
+
// shortest bar rises first and the tallest arrives last.
|
|
39
|
+
float bar_collapse(float frac, float t, int32_t easing);
|
|
40
|
+
|
|
41
|
+
} // namespace vroom::volume_anim
|