react-native-vroom-chart 0.8.0 → 0.9.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/cpp/_core_include/vroom/vroom_chart.h +3 -0
- package/cpp/_core_src/chart.cpp +36 -2
- package/cpp/_core_src/chart.h +15 -3
- package/cpp/_core_src/chart_facade.cpp +31 -7
- package/cpp/_core_src/curve.h +67 -0
- package/cpp/_core_src/ma_overlay.cpp +200 -34
- package/cpp/_core_src/ma_overlay.h +41 -4
- package/cpp/_core_src/theme.cpp +3 -0
- package/cpp/_core_src/tip_pulse.h +74 -0
- package/lib/index.d.mts +27 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.js +15 -5
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +15 -5
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +13 -7
- package/src/theme.ts +7 -0
- package/src/useChartCore.ts +8 -1
|
@@ -294,6 +294,9 @@ typedef enum {
|
|
|
294
294
|
VROOM_FLOAT_VOLUME_RADIUS_PX, // volume bar top-corner radius px (0 = square)
|
|
295
295
|
VROOM_FLOAT_LINE_WIDTH_PX, // line-chart-mode polyline stroke width px
|
|
296
296
|
VROOM_FLOAT_LINE_GRADIENT_OPACITY, // fill under the line at its strongest (0 disables)
|
|
297
|
+
VROOM_FLOAT_LINE_TENSION, // 0..1 line-chart corner smoothing (0 = straight)
|
|
298
|
+
VROOM_FLOAT_LINE_TIP_DOT, // 0/1: dot at the line's newest end (default on)
|
|
299
|
+
VROOM_FLOAT_LINE_TIP_PULSE, // 0/1: expanding ring around that dot
|
|
297
300
|
VROOM_FLOAT_COUNT_
|
|
298
301
|
} VroomFloatKey;
|
|
299
302
|
|
package/cpp/_core_src/chart.cpp
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
#include "chart.h"
|
|
11
11
|
|
|
12
|
+
#include <cmath>
|
|
13
|
+
|
|
12
14
|
#pragma clang diagnostic push
|
|
13
15
|
#pragma clang diagnostic ignored "-Wdocumentation"
|
|
14
16
|
#include "include/core/SkCanvas.h"
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
#include "rsi.h"
|
|
35
37
|
#include "rsi_pane.h"
|
|
36
38
|
#include "style_inherit.h"
|
|
39
|
+
#include "tip_pulse.h"
|
|
37
40
|
#include "volume.h"
|
|
38
41
|
#include "vwap.h"
|
|
39
42
|
#include "theme.h"
|
|
@@ -191,7 +194,8 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
191
194
|
visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
|
|
192
195
|
theme.colors[VROOM_COLOR_LINE],
|
|
193
196
|
theme.floats[VROOM_FLOAT_LINE_GRADIENT_OPACITY],
|
|
194
|
-
fade, morph_src, morph_n, morph_t
|
|
197
|
+
fade, morph_src, morph_n, morph_t,
|
|
198
|
+
theme.floats[VROOM_FLOAT_LINE_TENSION]);
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
// 4.5. Volume bars — drawn under the candles so candles z-index above.
|
|
@@ -239,7 +243,8 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
239
243
|
canvas, lay, bounds, visible, n, window_ms,
|
|
240
244
|
visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
|
|
241
245
|
theme.colors[VROOM_COLOR_LINE], theme.floats[VROOM_FLOAT_LINE_WIDTH_PX],
|
|
242
|
-
fade, morph_src, morph_n, morph_t
|
|
246
|
+
fade, morph_src, morph_n, morph_t,
|
|
247
|
+
theme.floats[VROOM_FLOAT_LINE_TENSION]);
|
|
243
248
|
}
|
|
244
249
|
|
|
245
250
|
// 5.5. Moving-average overlay lines (SMA/EMA) on the price pane, over the
|
|
@@ -303,6 +308,21 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
303
308
|
vroom::drawings::draw(canvas, *this, lay, bounds, candle_right,
|
|
304
309
|
candle_area_h);
|
|
305
310
|
|
|
311
|
+
// 5.8. Line-mode tip marker — the dot (and optional pulse) at the newest
|
|
312
|
+
// close. Above the overlays so it stays the eye's anchor, but before the
|
|
313
|
+
// axis masks, which trim the ring at the price scale.
|
|
314
|
+
if (fade > 0.f && theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f) {
|
|
315
|
+
vroom::ma_overlay::draw_close_tip(
|
|
316
|
+
canvas, lay, bounds, visible, n, window_ms, visible_start_ms,
|
|
317
|
+
candle_duration_ms, candle_right, candle_area_h,
|
|
318
|
+
theme.colors[VROOM_COLOR_LINE],
|
|
319
|
+
theme.colors[VROOM_COLOR_BACKGROUND],
|
|
320
|
+
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade,
|
|
321
|
+
theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
|
|
322
|
+
tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds,
|
|
323
|
+
morph_src, morph_n, morph_t);
|
|
324
|
+
}
|
|
325
|
+
|
|
306
326
|
// 6. Axis backgrounds (mask any candle overflow). The x-axis separator
|
|
307
327
|
// line is intentionally omitted for now. The bottom strip anchors at
|
|
308
328
|
// x_axis_top (below any indicator pane) so it never paints over it.
|
|
@@ -422,6 +442,17 @@ void VroomChart::begin_frame() {
|
|
|
422
442
|
last_anim_tick = now;
|
|
423
443
|
anim_started = true;
|
|
424
444
|
last_dt_seconds = dt;
|
|
445
|
+
|
|
446
|
+
// Wrapped rather than accumulated: the phase is the only thing anyone reads,
|
|
447
|
+
// and a chart left open for hours would otherwise lose float precision on it.
|
|
448
|
+
tip_pulse_elapsed_s =
|
|
449
|
+
std::fmod(tip_pulse_elapsed_s + dt, vroom::tip_pulse::kPeriodSeconds);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
bool VroomChart::tip_pulse_active() const {
|
|
453
|
+
return morph_fade > 0.f && !candles.empty() &&
|
|
454
|
+
theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f &&
|
|
455
|
+
theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f;
|
|
425
456
|
}
|
|
426
457
|
|
|
427
458
|
void VroomChart::rebuild_chart_picture() {
|
|
@@ -433,6 +464,9 @@ void VroomChart::rebuild_chart_picture() {
|
|
|
433
464
|
}
|
|
434
465
|
|
|
435
466
|
bool VroomChart::is_animating_now() const {
|
|
467
|
+
// The pulse never finishes on its own, so this is what keeps the host loops
|
|
468
|
+
// requeueing frames for it.
|
|
469
|
+
if (tip_pulse_active()) return true;
|
|
436
470
|
for (const auto& f : y_fades) {
|
|
437
471
|
if (f.opacity != f.target) return true;
|
|
438
472
|
}
|
package/cpp/_core_src/chart.h
CHANGED
|
@@ -64,6 +64,12 @@ struct VroomChart {
|
|
|
64
64
|
float morph_collapse = 0.f;
|
|
65
65
|
float morph_fade = 0.f;
|
|
66
66
|
|
|
67
|
+
// Phase of the line-tip pulse, in seconds into its cycle. Unlike every other
|
|
68
|
+
// animation here this one is core-driven: it loops with no endpoint for the
|
|
69
|
+
// host to ease toward, so begin_frame advances it from the frame clock and
|
|
70
|
+
// is_animating_now keeps the host's redraw loop alive (see tip_pulse.h).
|
|
71
|
+
float tip_pulse_elapsed_s = 0.f;
|
|
72
|
+
|
|
67
73
|
// Interval morph: the outgoing candle geometry captured when a timeframe
|
|
68
74
|
// switch begins, indexed from the right of the visible slice (slot 0 =
|
|
69
75
|
// newest) — the pairing the preserved slot grid guarantees. Stored as
|
|
@@ -307,14 +313,20 @@ struct VroomChart {
|
|
|
307
313
|
void draw_chart(SkCanvas* canvas);
|
|
308
314
|
|
|
309
315
|
// Per-frame animation bookkeeping: computes `last_dt_seconds`, which paces
|
|
310
|
-
// the label fades
|
|
316
|
+
// the label fades, and advances `tip_pulse_elapsed_s`. Called at the top of
|
|
317
|
+
// draw_chart.
|
|
311
318
|
void begin_frame();
|
|
312
319
|
|
|
320
|
+
// True when the line tip's pulse ring is on screen and looping. Gated on
|
|
321
|
+
// line mode and on having data, so a chart that isn't showing the ring can
|
|
322
|
+
// still go idle.
|
|
323
|
+
bool tip_pulse_active() const;
|
|
324
|
+
|
|
313
325
|
// Re-records `chart_picture` by invoking draw_chart on a fresh
|
|
314
326
|
// SkPictureRecorder.
|
|
315
327
|
void rebuild_chart_picture();
|
|
316
328
|
|
|
317
|
-
// True if any axis label is mid-fade
|
|
318
|
-
// to know when to keep ticking.
|
|
329
|
+
// True if any axis label is mid-fade, or the tip pulse is running. Used by
|
|
330
|
+
// the JS-side animation loop to know when to keep ticking.
|
|
319
331
|
bool is_animating_now() const;
|
|
320
332
|
};
|
|
@@ -32,6 +32,17 @@ constexpr double kAxisDragSensitivity = 300.0;
|
|
|
32
32
|
constexpr double kMinCandleBodyPx = 1.5;
|
|
33
33
|
constexpr double kMaxCandleBodyPx = 32.0;
|
|
34
34
|
|
|
35
|
+
// Trailing gap the default framing leaves between the newest candle and the
|
|
36
|
+
// price axis. The newest bar flush against the axis reads as cramped, and in line
|
|
37
|
+
// mode the tip marker's pulse needs somewhere to expand into — the axis mask
|
|
38
|
+
// (draw_chart step 6) paints over the gutter, so clearance has to come from the
|
|
39
|
+
// time window rather than from right_padding_px.
|
|
40
|
+
//
|
|
41
|
+
// A gap this small is far under the 3/4-window cap, so it neither triggers the
|
|
42
|
+
// pan rubber-band nor gets clamped away; a timeframe switch carries it over in
|
|
43
|
+
// slots (see timeframeWindow in the hosts), which preserves it in pixels.
|
|
44
|
+
constexpr double kRightGapPx = 12.0;
|
|
45
|
+
|
|
35
46
|
// Hard cap on the future gap (visible_end - last_candle): 3/4 of the window so
|
|
36
47
|
// at least 25% of the chart still shows candles. Never forces an existing larger
|
|
37
48
|
// gap (e.g. from the x-axis drag zoom) to snap back — only limits new growth.
|
|
@@ -70,14 +81,29 @@ void apply_default_framing(VroomChart* chart) {
|
|
|
70
81
|
const int64_t right_edge_ms =
|
|
71
82
|
chart->candles.back().time_ms + chart->candle_duration_ms;
|
|
72
83
|
|
|
84
|
+
const auto lay = chart->layout();
|
|
85
|
+
const double usable =
|
|
86
|
+
lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
|
|
87
|
+
|
|
88
|
+
// Places a window of the given width with kRightGapPx of air past the newest
|
|
89
|
+
// candle. Shifts the window forward rather than widening it, so the framing
|
|
90
|
+
// keeps whatever candle width it just computed. Falls back to a flush right
|
|
91
|
+
// edge when the layout isn't measured yet and px can't be converted to time.
|
|
92
|
+
const auto frame = [&](int64_t window_ms) {
|
|
93
|
+
const int64_t gap_ms =
|
|
94
|
+
usable > 0.0 && window_ms > 0
|
|
95
|
+
? static_cast<int64_t>((kRightGapPx * static_cast<double>(window_ms)) /
|
|
96
|
+
usable)
|
|
97
|
+
: 0;
|
|
98
|
+
chart->visible_end_ms = right_edge_ms + gap_ms;
|
|
99
|
+
chart->visible_start_ms = chart->visible_end_ms - window_ms;
|
|
100
|
+
};
|
|
101
|
+
|
|
73
102
|
// Px-driven framing. Reuses the inversion from vroom_chart_scale_time_axis:
|
|
74
103
|
// body_w = usable × (dur / window) × ratio → window = usable × dur × ratio / body_w.
|
|
75
104
|
// Needs a valid layout (width_px); until size is known it falls through to
|
|
76
105
|
// the candle-count default below.
|
|
77
106
|
if (chart->default_candle_px > 0.f) {
|
|
78
|
-
const auto lay = chart->layout();
|
|
79
|
-
const double usable =
|
|
80
|
-
lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
|
|
81
107
|
const double ratio = chart->theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO];
|
|
82
108
|
const double dur = static_cast<double>(chart->candle_duration_ms);
|
|
83
109
|
const double body = std::clamp(static_cast<double>(chart->default_candle_px),
|
|
@@ -88,8 +114,7 @@ void apply_default_framing(VroomChart* chart) {
|
|
|
88
114
|
chart->candles.back().time_ms - chart->candles.front().time_ms;
|
|
89
115
|
window_ms = std::clamp<int64_t>(window_ms, chart->candle_duration_ms,
|
|
90
116
|
span > 0 ? span : window_ms);
|
|
91
|
-
|
|
92
|
-
chart->visible_start_ms = chart->visible_end_ms - window_ms;
|
|
117
|
+
frame(window_ms);
|
|
93
118
|
return;
|
|
94
119
|
}
|
|
95
120
|
}
|
|
@@ -98,8 +123,7 @@ void apply_default_framing(VroomChart* chart) {
|
|
|
98
123
|
const size_t start_idx = chart->candles.size() > kDefaultVisible
|
|
99
124
|
? chart->candles.size() - kDefaultVisible
|
|
100
125
|
: 0;
|
|
101
|
-
|
|
102
|
-
chart->visible_end_ms = right_edge_ms;
|
|
126
|
+
frame(right_edge_ms - chart->candles[start_idx].time_ms);
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
// On the first manual-y gesture, adopt the on-screen auto-fit bounds so the
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Monotone cubic interpolation — the math behind the line chart's optional
|
|
2
|
+
// corner smoothing (VROOM_FLOAT_LINE_TENSION).
|
|
3
|
+
//
|
|
4
|
+
// Deliberately monotone (Fritsch-Carlson) rather than a plain Catmull-Rom or
|
|
5
|
+
// cardinal spline. An unconstrained spline through the closes overshoots on a
|
|
6
|
+
// sharp reversal, and on a price chart that isn't just ugly — the curve dips
|
|
7
|
+
// below the period's low and draws a price that never traded, next to candles
|
|
8
|
+
// that show the real one during the candle-to-line crossfade. Limiting the
|
|
9
|
+
// tangents keeps every local extreme on an actual data point.
|
|
10
|
+
//
|
|
11
|
+
// Skia-free and header-only so the unit tests can cover it (see
|
|
12
|
+
// tests/test_curve.cpp); ma_overlay.cpp owns the walk and the path emission.
|
|
13
|
+
|
|
14
|
+
#pragma once
|
|
15
|
+
|
|
16
|
+
#include <algorithm>
|
|
17
|
+
#include <cmath>
|
|
18
|
+
|
|
19
|
+
namespace vroom::curve {
|
|
20
|
+
|
|
21
|
+
// The two interior control points of a cubic Bezier segment, in whatever space
|
|
22
|
+
// the endpoints were given in (pixels, at every call site here).
|
|
23
|
+
struct Controls {
|
|
24
|
+
float c1x, c1y;
|
|
25
|
+
float c2x, c2y;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Slope of the straight line between two points. Non-increasing x yields 0,
|
|
29
|
+
// which keeps a degenerate span (duplicate timestamps, or the compressed
|
|
30
|
+
// spacing an interval morph can pass through) from producing infinities.
|
|
31
|
+
inline float secant(float x0, float y0, float x1, float y1) {
|
|
32
|
+
const float h = x1 - x0;
|
|
33
|
+
return h > 0.f ? (y1 - y0) / h : 0.f;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Tangent at a point with neighboring secants `d_prev` and `d_next`.
|
|
37
|
+
inline float monotone_tangent(float d_prev, float d_next) {
|
|
38
|
+
// Opposite signs (or a flat neighbor) mean this point is a local extreme.
|
|
39
|
+
// A zero tangent is what pins the curve to it instead of sailing past.
|
|
40
|
+
if (d_prev * d_next <= 0.f) return 0.f;
|
|
41
|
+
// Fritsch-Carlson: holding the tangent within three times the smaller
|
|
42
|
+
// neighboring secant is what makes each segment monotone, and is also why
|
|
43
|
+
// the control points below can never leave the endpoints' y range.
|
|
44
|
+
const float limit = 3.f * std::min(std::fabs(d_prev), std::fabs(d_next));
|
|
45
|
+
return std::clamp((d_prev + d_next) * 0.5f, -limit, limit);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Control points for the segment p0 -> p1, given the tangents chosen for each
|
|
49
|
+
// endpoint and how far to lean into them.
|
|
50
|
+
//
|
|
51
|
+
// `tension` 0 puts both tangents back on the segment's own secant, which lands
|
|
52
|
+
// the controls exactly on its thirds — the cubic *is* the straight line. So the
|
|
53
|
+
// knob is continuous at zero rather than snapping into a curve, and callers can
|
|
54
|
+
// interpolate it without a visible discontinuity.
|
|
55
|
+
inline Controls segment_controls(float x0, float y0,
|
|
56
|
+
float x1, float y1,
|
|
57
|
+
float m0, float m1,
|
|
58
|
+
float tension) {
|
|
59
|
+
const float d = secant(x0, y0, x1, y1);
|
|
60
|
+
const float a = d + (m0 - d) * tension;
|
|
61
|
+
const float b = d + (m1 - d) * tension;
|
|
62
|
+
const float third = (x1 - x0) / 3.f;
|
|
63
|
+
return Controls{x0 + third, y0 + a * third,
|
|
64
|
+
x1 - third, y1 - b * third};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
} // namespace vroom::curve
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
#include <algorithm>
|
|
16
16
|
#include <cmath>
|
|
17
|
+
#include <vector>
|
|
17
18
|
|
|
19
|
+
#include "curve.h"
|
|
18
20
|
#include "gradient.h"
|
|
21
|
+
#include "tip_pulse.h"
|
|
19
22
|
#include "viewport.h"
|
|
20
23
|
|
|
21
24
|
namespace vroom::ma_overlay {
|
|
@@ -48,6 +51,48 @@ void stroke_path(SkCanvas* canvas,
|
|
|
48
51
|
|
|
49
52
|
inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
|
|
50
53
|
|
|
54
|
+
// Width of the background-colored ring that separates the tip dot from the line
|
|
55
|
+
// and from the pulse expanding out behind it.
|
|
56
|
+
constexpr float kTipBorderPx = 2.f;
|
|
57
|
+
|
|
58
|
+
// One vertex of the close polyline, in screen space. Slot `k` counts back from
|
|
59
|
+
// the right edge (0 = the newest close), the pairing a timeframe switch
|
|
60
|
+
// preserves.
|
|
61
|
+
//
|
|
62
|
+
// Shared by the path builder and the tip marker: the dot has to sit on the exact
|
|
63
|
+
// pixel the line ends at, including mid-interval-morph where both coordinates
|
|
64
|
+
// are interpolated against the outgoing capture.
|
|
65
|
+
SkPoint close_vertex(const Layout& lay,
|
|
66
|
+
const PriceBounds& bounds,
|
|
67
|
+
const ::VroomCandle* visible,
|
|
68
|
+
std::size_t n,
|
|
69
|
+
int64_t window_ms,
|
|
70
|
+
int64_t visible_start_ms,
|
|
71
|
+
int64_t candle_duration_ms,
|
|
72
|
+
const CandleSnapshot* from,
|
|
73
|
+
std::size_t from_count,
|
|
74
|
+
float morph_t,
|
|
75
|
+
std::size_t k) {
|
|
76
|
+
const ::VroomCandle* to = (k < n) ? &visible[n - 1 - k] : nullptr;
|
|
77
|
+
const CandleSnapshot* frm = (k < from_count) ? &from[k] : nullptr;
|
|
78
|
+
if (to) {
|
|
79
|
+
const float tx = vroom::candle_center_x(
|
|
80
|
+
lay, to->time_ms, candle_duration_ms, visible_start_ms, window_ms);
|
|
81
|
+
const float ty = vroom::price_to_y(lay, bounds, to->close);
|
|
82
|
+
if (!frm) return SkPoint{tx, ty};
|
|
83
|
+
// The capture is in band fractions, so it lands on the same pixels it
|
|
84
|
+
// occupied pre-switch even though the bounds changed.
|
|
85
|
+
const float area_w = vroom::candle_area_width(lay);
|
|
86
|
+
return SkPoint{
|
|
87
|
+
lerp(frm->x * area_w, tx, morph_t),
|
|
88
|
+
lerp(vroom::y_at_fraction(lay, frm->close), ty, morph_t)};
|
|
89
|
+
}
|
|
90
|
+
// A slot only the outgoing data had. Counts match in practice, so this just
|
|
91
|
+
// keeps gappy or short-history data from breaking the line.
|
|
92
|
+
return SkPoint{frm->x * vroom::candle_area_width(lay),
|
|
93
|
+
vroom::y_at_fraction(lay, frm->close)};
|
|
94
|
+
}
|
|
95
|
+
|
|
51
96
|
// The close-price polyline, walked left to right. Shared by the stroke and the
|
|
52
97
|
// gradient fill so both derive from identical geometry, mid-morph included.
|
|
53
98
|
//
|
|
@@ -55,6 +100,10 @@ inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
|
|
|
55
100
|
// timeframe switch preserves — which walks the line left to right, so the vertex
|
|
56
101
|
// order matches draw()'s at morph_t == 1. Every candle has a close, so there are
|
|
57
102
|
// no gaps to break the subpath on.
|
|
103
|
+
//
|
|
104
|
+
// `tension` above 0 rounds the corners (see curve.h). It smooths the *screen*
|
|
105
|
+
// positions rather than the closes, which is what keeps the line glued to the
|
|
106
|
+
// candles mid-interval-morph, where those positions are themselves interpolated.
|
|
58
107
|
SkPath build_close_path(const Layout& lay,
|
|
59
108
|
const PriceBounds& bounds,
|
|
60
109
|
const ::VroomCandle* visible,
|
|
@@ -64,45 +113,72 @@ SkPath build_close_path(const Layout& lay,
|
|
|
64
113
|
int64_t candle_duration_ms,
|
|
65
114
|
const CandleSnapshot* from,
|
|
66
115
|
std::size_t from_n,
|
|
67
|
-
float morph_t
|
|
116
|
+
float morph_t,
|
|
117
|
+
float tension) {
|
|
68
118
|
const std::size_t from_count = vroom::morph_from_count(from, from_n, morph_t);
|
|
69
119
|
const std::size_t slots = std::max(n, from_count);
|
|
70
120
|
if (slots == 0) return SkPath();
|
|
71
|
-
const float area_w = vroom::candle_area_width(lay);
|
|
72
121
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
122
|
+
// Vertex `i` of the left-to-right walk, so the smoothing pass below can look
|
|
123
|
+
// ahead without a second copy of the morph interpolation.
|
|
124
|
+
const auto vertex = [&](std::size_t i) -> SkPoint {
|
|
125
|
+
return close_vertex(lay, bounds, visible, n, window_ms, visible_start_ms,
|
|
126
|
+
candle_duration_ms, from, from_count, morph_t,
|
|
127
|
+
slots - 1 - i);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Straight segments by default, and below three points there's no interior
|
|
131
|
+
// vertex to round. Kept as its own pass so the common case emits exactly the
|
|
132
|
+
// path it always has.
|
|
133
|
+
if (!(tension > 0.f) || slots < 3) {
|
|
134
|
+
SkPathBuilder path;
|
|
135
|
+
for (std::size_t i = 0; i < slots; ++i) {
|
|
136
|
+
const SkPoint p = vertex(i);
|
|
137
|
+
if (i == 0) {
|
|
138
|
+
path.moveTo(p);
|
|
89
139
|
} else {
|
|
90
|
-
|
|
91
|
-
y = ty;
|
|
140
|
+
path.lineTo(p);
|
|
92
141
|
}
|
|
93
|
-
} else {
|
|
94
|
-
// A slot only the outgoing data had. Counts match in practice, so this
|
|
95
|
-
// just keeps gappy or short-history data from breaking the line.
|
|
96
|
-
x = frm->x * area_w;
|
|
97
|
-
y = vroom::y_at_fraction(lay, frm->close);
|
|
98
142
|
}
|
|
143
|
+
return path.detach();
|
|
144
|
+
}
|
|
99
145
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
146
|
+
// A tangent needs a neighbor on both sides, so smoothing can't stay
|
|
147
|
+
// streaming. Reused across frames — rendering is single-threaded per chart
|
|
148
|
+
// and the buffer is only live for this call — so it stops allocating after
|
|
149
|
+
// the first frame at a given zoom.
|
|
150
|
+
static thread_local std::vector<SkPoint> pts;
|
|
151
|
+
pts.clear();
|
|
152
|
+
pts.reserve(slots);
|
|
153
|
+
for (std::size_t i = 0; i < slots; ++i) pts.push_back(vertex(i));
|
|
154
|
+
|
|
155
|
+
const auto secant_at = [&](std::size_t i) {
|
|
156
|
+
return vroom::curve::secant(pts[i].fX, pts[i].fY,
|
|
157
|
+
pts[i + 1].fX, pts[i + 1].fY);
|
|
158
|
+
};
|
|
159
|
+
// Endpoints have only one neighboring secant to lean on; interiors get the
|
|
160
|
+
// monotone-limited blend of both. Recomputing a secant or two beats carrying
|
|
161
|
+
// a second buffer.
|
|
162
|
+
const auto tangent_at = [&](std::size_t i) {
|
|
163
|
+
if (i == 0) return secant_at(0);
|
|
164
|
+
if (i + 1 == pts.size()) return secant_at(pts.size() - 2);
|
|
165
|
+
return vroom::curve::monotone_tangent(secant_at(i - 1), secant_at(i));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
SkPathBuilder path;
|
|
169
|
+
path.moveTo(pts[0]);
|
|
170
|
+
for (std::size_t i = 0; i + 1 < pts.size(); ++i) {
|
|
171
|
+
// Flat or backwards spacing — duplicate timestamps, or the compressed x
|
|
172
|
+
// a morph can pass through — has no meaningful tangent, and curving
|
|
173
|
+
// through it would fold the line back on itself.
|
|
174
|
+
if (!(pts[i + 1].fX > pts[i].fX)) {
|
|
175
|
+
path.lineTo(pts[i + 1]);
|
|
176
|
+
continue;
|
|
105
177
|
}
|
|
178
|
+
const vroom::curve::Controls c = vroom::curve::segment_controls(
|
|
179
|
+
pts[i].fX, pts[i].fY, pts[i + 1].fX, pts[i + 1].fY,
|
|
180
|
+
tangent_at(i), tangent_at(i + 1), tension);
|
|
181
|
+
path.cubicTo(c.c1x, c.c1y, c.c2x, c.c2y, pts[i + 1].fX, pts[i + 1].fY);
|
|
106
182
|
}
|
|
107
183
|
return path.detach();
|
|
108
184
|
}
|
|
@@ -170,15 +246,17 @@ void draw_close_line(SkCanvas* canvas,
|
|
|
170
246
|
float opacity,
|
|
171
247
|
const CandleSnapshot* from,
|
|
172
248
|
std::size_t from_n,
|
|
173
|
-
float morph_t
|
|
249
|
+
float morph_t,
|
|
250
|
+
float tension) {
|
|
174
251
|
if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
|
|
175
252
|
opacity = std::clamp(opacity, 0.f, 1.f);
|
|
176
253
|
if (opacity <= 0.f) return;
|
|
177
254
|
morph_t = std::clamp(morph_t, 0.f, 1.f);
|
|
255
|
+
tension = std::clamp(tension, 0.f, 1.f);
|
|
178
256
|
|
|
179
257
|
const SkPath path = build_close_path(lay, bounds, visible, n, window_ms,
|
|
180
258
|
visible_start_ms, candle_duration_ms,
|
|
181
|
-
from, from_n, morph_t);
|
|
259
|
+
from, from_n, morph_t, tension);
|
|
182
260
|
if (path.isEmpty()) return;
|
|
183
261
|
|
|
184
262
|
stroke_path(canvas, path, candle_right, candle_area_h, color, width, opacity);
|
|
@@ -199,17 +277,19 @@ void draw_close_gradient(SkCanvas* canvas,
|
|
|
199
277
|
float opacity,
|
|
200
278
|
const CandleSnapshot* from,
|
|
201
279
|
std::size_t from_n,
|
|
202
|
-
float morph_t
|
|
280
|
+
float morph_t,
|
|
281
|
+
float tension) {
|
|
203
282
|
if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
|
|
204
283
|
gradient_opacity = std::clamp(gradient_opacity, 0.f, 1.f);
|
|
205
284
|
opacity = std::clamp(opacity, 0.f, 1.f);
|
|
206
285
|
const float alpha = gradient_opacity * opacity;
|
|
207
286
|
if (alpha <= 0.f) return;
|
|
208
287
|
morph_t = std::clamp(morph_t, 0.f, 1.f);
|
|
288
|
+
tension = std::clamp(tension, 0.f, 1.f);
|
|
209
289
|
|
|
210
290
|
const SkPath line = build_close_path(lay, bounds, visible, n, window_ms,
|
|
211
291
|
visible_start_ms, candle_duration_ms,
|
|
212
|
-
from, from_n, morph_t);
|
|
292
|
+
from, from_n, morph_t, tension);
|
|
213
293
|
if (line.isEmpty()) return;
|
|
214
294
|
|
|
215
295
|
// The polyline runs strictly left to right, so its bounds give the first and
|
|
@@ -240,6 +320,92 @@ void draw_close_gradient(SkCanvas* canvas,
|
|
|
240
320
|
canvas->restore();
|
|
241
321
|
}
|
|
242
322
|
|
|
323
|
+
void draw_close_tip(SkCanvas* canvas,
|
|
324
|
+
const Layout& lay,
|
|
325
|
+
const PriceBounds& bounds,
|
|
326
|
+
const ::VroomCandle* visible,
|
|
327
|
+
std::size_t n,
|
|
328
|
+
int64_t window_ms,
|
|
329
|
+
int64_t visible_start_ms,
|
|
330
|
+
int64_t candle_duration_ms,
|
|
331
|
+
float candle_right,
|
|
332
|
+
float candle_area_h,
|
|
333
|
+
uint32_t line_color,
|
|
334
|
+
uint32_t bg_color,
|
|
335
|
+
float line_width,
|
|
336
|
+
float opacity,
|
|
337
|
+
bool pulse,
|
|
338
|
+
float pulse_phase,
|
|
339
|
+
const CandleSnapshot* from,
|
|
340
|
+
std::size_t from_n,
|
|
341
|
+
float morph_t) {
|
|
342
|
+
if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
|
|
343
|
+
opacity = std::clamp(opacity, 0.f, 1.f);
|
|
344
|
+
if (opacity <= 0.f) return;
|
|
345
|
+
morph_t = std::clamp(morph_t, 0.f, 1.f);
|
|
346
|
+
|
|
347
|
+
const std::size_t from_count = vroom::morph_from_count(from, from_n, morph_t);
|
|
348
|
+
const std::size_t slots = std::max(n, from_count);
|
|
349
|
+
if (slots == 0) return;
|
|
350
|
+
|
|
351
|
+
const SkPoint tip =
|
|
352
|
+
close_vertex(lay, bounds, visible, n, window_ms, visible_start_ms,
|
|
353
|
+
candle_duration_ms, from, from_count, morph_t, 0);
|
|
354
|
+
// Panned or scaled out of the pane: the line's end isn't on screen, so
|
|
355
|
+
// there's nothing to mark. The clip below would hide it anyway, but the ring
|
|
356
|
+
// is wide enough that a tip just off-pane would still bleed in.
|
|
357
|
+
if (tip.fY < 0.f || tip.fY > candle_area_h) return;
|
|
358
|
+
if (tip.fX < 0.f || tip.fX > candle_right) return;
|
|
359
|
+
|
|
360
|
+
// Scaling off the stroke keeps the marker proportionate at any line width;
|
|
361
|
+
// the floor stops a hairline chart from getting an invisible dot.
|
|
362
|
+
const float w = line_width > 0.f ? line_width : 1.5f;
|
|
363
|
+
const float dot_r = std::max(2.f, w * 1.5f);
|
|
364
|
+
const float border_r = dot_r + kTipBorderPx;
|
|
365
|
+
|
|
366
|
+
canvas->save();
|
|
367
|
+
canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
|
|
368
|
+
|
|
369
|
+
// Ring first: the border paints over its inner edge, so it reads as
|
|
370
|
+
// expanding out from underneath the dot rather than around it.
|
|
371
|
+
if (pulse) {
|
|
372
|
+
const vroom::tip_pulse::Frame f = vroom::tip_pulse::at(pulse_phase);
|
|
373
|
+
const float r = border_r * f.radius_mul;
|
|
374
|
+
if (f.fill_alpha > 0.f) {
|
|
375
|
+
SkPaint fill;
|
|
376
|
+
fill.setAntiAlias(true);
|
|
377
|
+
fill.setColor(static_cast<SkColor>(line_color));
|
|
378
|
+
fill.setAlphaf(fill.getAlphaf() * f.fill_alpha * opacity);
|
|
379
|
+
canvas->drawCircle(tip.fX, tip.fY, r, fill);
|
|
380
|
+
}
|
|
381
|
+
if (f.stroke_alpha > 0.f) {
|
|
382
|
+
SkPaint edge;
|
|
383
|
+
edge.setAntiAlias(true);
|
|
384
|
+
edge.setColor(static_cast<SkColor>(line_color));
|
|
385
|
+
edge.setAlphaf(edge.getAlphaf() * f.stroke_alpha * opacity);
|
|
386
|
+
edge.setStyle(SkPaint::kStroke_Style);
|
|
387
|
+
edge.setStrokeWidth(1.f);
|
|
388
|
+
canvas->drawCircle(tip.fX, tip.fY, r, edge);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Filled rather than stroked: an annulus stroked around the dot leaves an
|
|
393
|
+
// antialiased seam where the two meet, which shows up as a dark hairline.
|
|
394
|
+
SkPaint border;
|
|
395
|
+
border.setAntiAlias(true);
|
|
396
|
+
border.setColor(static_cast<SkColor>(bg_color));
|
|
397
|
+
border.setAlphaf(border.getAlphaf() * opacity);
|
|
398
|
+
canvas->drawCircle(tip.fX, tip.fY, border_r, border);
|
|
399
|
+
|
|
400
|
+
SkPaint dot;
|
|
401
|
+
dot.setAntiAlias(true);
|
|
402
|
+
dot.setColor(static_cast<SkColor>(line_color));
|
|
403
|
+
dot.setAlphaf(dot.getAlphaf() * opacity);
|
|
404
|
+
canvas->drawCircle(tip.fX, tip.fY, dot_r, dot);
|
|
405
|
+
|
|
406
|
+
canvas->restore();
|
|
407
|
+
}
|
|
408
|
+
|
|
243
409
|
void fill_between(SkCanvas* canvas,
|
|
244
410
|
const Layout& lay,
|
|
245
411
|
const PriceBounds& bounds,
|
|
@@ -48,6 +48,9 @@ void draw(SkCanvas* canvas,
|
|
|
48
48
|
// had before the timeframe switch to its new one. `morph_t == 1` (the default)
|
|
49
49
|
// draws `visible` alone.
|
|
50
50
|
//
|
|
51
|
+
// `tension` (0..1) rounds the corners, monotone-limited so the curve can't
|
|
52
|
+
// overshoot into a price that never traded (see curve.h). 0 is straight segments.
|
|
53
|
+
//
|
|
51
54
|
// Separate from draw() because the capture holds candle closes — it can't stand
|
|
52
55
|
// in for an indicator's values.
|
|
53
56
|
void draw_close_line(SkCanvas* canvas,
|
|
@@ -65,12 +68,14 @@ void draw_close_line(SkCanvas* canvas,
|
|
|
65
68
|
float opacity = 1.f,
|
|
66
69
|
const CandleSnapshot* from = nullptr,
|
|
67
70
|
std::size_t from_n = 0,
|
|
68
|
-
float morph_t = 1.f
|
|
71
|
+
float morph_t = 1.f,
|
|
72
|
+
float tension = 0.f);
|
|
69
73
|
|
|
70
74
|
// The area beneath the close-price polyline, filled with `color` ramping from
|
|
71
75
|
// `gradient_opacity` at the line's peak to fully transparent at the pane bottom.
|
|
72
|
-
// Takes the same geometry and
|
|
73
|
-
// identical
|
|
76
|
+
// Takes the same geometry, morph and tension arguments as draw_close_line and
|
|
77
|
+
// builds the identical path, so the fill tracks the line through a timeframe
|
|
78
|
+
// switch and follows the same rounded corners.
|
|
74
79
|
//
|
|
75
80
|
// `opacity` multiplies the ramp, which is what fades the fill in alongside the
|
|
76
81
|
// line during the candle→line morph. Draws nothing when either opacity is 0.
|
|
@@ -91,7 +96,39 @@ void draw_close_gradient(SkCanvas* canvas,
|
|
|
91
96
|
float opacity = 1.f,
|
|
92
97
|
const CandleSnapshot* from = nullptr,
|
|
93
98
|
std::size_t from_n = 0,
|
|
94
|
-
float morph_t = 1.f
|
|
99
|
+
float morph_t = 1.f,
|
|
100
|
+
float tension = 0.f);
|
|
101
|
+
|
|
102
|
+
// The marker at the line's newest end: a dot in `line_color` sized off
|
|
103
|
+
// `line_width`, ringed by a 2px disc of `bg_color` that separates it from the
|
|
104
|
+
// line's stroke cap, and optionally a pulse expanding out from behind it.
|
|
105
|
+
//
|
|
106
|
+
// Takes the same geometry and morph arguments as draw_close_line and reads the
|
|
107
|
+
// same vertex, so the dot stays on the line's end pixel through a timeframe
|
|
108
|
+
// switch. `opacity` scales everything, fading the marker in with the line during
|
|
109
|
+
// the candle→line morph.
|
|
110
|
+
//
|
|
111
|
+
// `pulse_phase` is in cycles and wraps, so the caller can hand over elapsed time
|
|
112
|
+
// divided by tip_pulse::kPeriodSeconds. Ignored unless `pulse`.
|
|
113
|
+
void draw_close_tip(SkCanvas* canvas,
|
|
114
|
+
const Layout& lay,
|
|
115
|
+
const PriceBounds& bounds,
|
|
116
|
+
const ::VroomCandle* visible,
|
|
117
|
+
std::size_t n,
|
|
118
|
+
int64_t window_ms,
|
|
119
|
+
int64_t visible_start_ms,
|
|
120
|
+
int64_t candle_duration_ms,
|
|
121
|
+
float candle_right,
|
|
122
|
+
float candle_area_h,
|
|
123
|
+
uint32_t line_color,
|
|
124
|
+
uint32_t bg_color,
|
|
125
|
+
float line_width,
|
|
126
|
+
float opacity = 1.f,
|
|
127
|
+
bool pulse = false,
|
|
128
|
+
float pulse_phase = 0.f,
|
|
129
|
+
const CandleSnapshot* from = nullptr,
|
|
130
|
+
std::size_t from_n = 0,
|
|
131
|
+
float morph_t = 1.f);
|
|
95
132
|
|
|
96
133
|
// Fills the closed region between two aligned series (NaN where undefined)
|
|
97
134
|
// with `color` at its alpha × `opacity` — used for the Bollinger Band fill.
|
package/cpp/_core_src/theme.cpp
CHANGED
|
@@ -39,6 +39,9 @@ constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
|
|
|
39
39
|
0.f, // VOLUME_RADIUS_PX — square by default
|
|
40
40
|
1.5f, // LINE_WIDTH_PX — line-chart polyline stroke width
|
|
41
41
|
0.28f, // LINE_GRADIENT_OPACITY — fill under the line, ramped to 0 at the pane bottom
|
|
42
|
+
0.f, // LINE_TENSION — straight segments by default
|
|
43
|
+
1.f, // LINE_TIP_DOT — the line's leading end is marked by default
|
|
44
|
+
0.f, // LINE_TIP_PULSE — the ring is opt-in; it never lets the chart idle
|
|
42
45
|
};
|
|
43
46
|
|
|
44
47
|
} // namespace
|