react-native-vroom-chart 0.16.0 → 0.17.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/VroomChartHostObject.cpp +81 -0
- package/cpp/_core_include/vroom/vroom_chart.h +41 -0
- package/cpp/_core_src/candles.cpp +15 -14
- package/cpp/_core_src/chart.cpp +297 -28
- package/cpp/_core_src/chart.h +71 -2
- package/cpp/_core_src/chart_facade.cpp +27 -0
- package/cpp/_core_src/color_lerp.h +28 -0
- package/cpp/_core_src/loading_line.cpp +183 -0
- package/cpp/_core_src/loading_line.h +38 -0
- package/cpp/_core_src/loading_wave.h +140 -0
- package/cpp/_core_src/ma_overlay.cpp +15 -10
- package/cpp/_core_src/ma_overlay.h +7 -0
- package/cpp/_core_src/price_indicator.cpp +21 -7
- package/cpp/_core_src/price_indicator.h +11 -1
- package/cpp/_core_src/price_indicator_anim.h +81 -0
- package/cpp/_core_src/theme.cpp +5 -0
- package/cpp/_core_src/tip_geometry.h +55 -0
- package/cpp/_core_src/viewport.h +33 -0
- package/lib/index.d.mts +30 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +71 -11
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +71 -11
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +12 -0
- package/src/jsi.d.ts +42 -0
- package/src/theme.ts +1 -0
- package/src/useChartCore.ts +119 -4
|
@@ -58,6 +58,10 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
|
|
|
58
58
|
out.push_back(jsi::PropNameID::forAscii(rt, "preservePriceEnvelope"));
|
|
59
59
|
out.push_back(jsi::PropNameID::forAscii(rt, "beginIntervalMorph"));
|
|
60
60
|
out.push_back(jsi::PropNameID::forAscii(rt, "beginStreamMorph"));
|
|
61
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setLoading"));
|
|
62
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "beginLoadingMorph"));
|
|
63
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setLoadingMorph"));
|
|
64
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "beginLoadingReveal"));
|
|
61
65
|
out.push_back(jsi::PropNameID::forAscii(rt, "setIntervalMorph"));
|
|
62
66
|
out.push_back(jsi::PropNameID::forAscii(rt, "pan"));
|
|
63
67
|
out.push_back(jsi::PropNameID::forAscii(rt, "translate"));
|
|
@@ -532,6 +536,83 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
532
536
|
});
|
|
533
537
|
}
|
|
534
538
|
|
|
539
|
+
if (name == "setLoading") {
|
|
540
|
+
// setLoading(on, animate?) — show or hide the loading line. `on` must mean
|
|
541
|
+
// "loading *and* holding no data"; the core takes it at face value.
|
|
542
|
+
// `animate` defaults to true; pass false for reduced motion.
|
|
543
|
+
return jsi::Function::createFromHostFunction(
|
|
544
|
+
rt,
|
|
545
|
+
jsi::PropNameID::forAscii(rt, "setLoading"),
|
|
546
|
+
2,
|
|
547
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
548
|
+
const jsi::Value& /*thisVal*/,
|
|
549
|
+
const jsi::Value* args,
|
|
550
|
+
size_t count) -> jsi::Value {
|
|
551
|
+
if (count < 1) return jsi::Value::undefined();
|
|
552
|
+
const bool on = args[0].isBool() ? args[0].getBool()
|
|
553
|
+
: args[0].asNumber() != 0;
|
|
554
|
+
bool animate = true;
|
|
555
|
+
if (count >= 2 && !args[1].isUndefined() && !args[1].isNull()) {
|
|
556
|
+
animate = args[1].isBool() ? args[1].getBool()
|
|
557
|
+
: args[1].asNumber() != 0;
|
|
558
|
+
}
|
|
559
|
+
vroom_chart_set_loading(chart_, on ? 1 : 0, animate ? 1 : 0);
|
|
560
|
+
return jsi::Value::undefined();
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (name == "beginLoadingMorph") {
|
|
565
|
+
// beginLoadingMorph() — hand-off stage one: aim the loading line at the
|
|
566
|
+
// centres of the candles that just landed. Call *after* setCandles, then
|
|
567
|
+
// drive setLoadingMorph from 0 to 1.
|
|
568
|
+
return jsi::Function::createFromHostFunction(
|
|
569
|
+
rt,
|
|
570
|
+
jsi::PropNameID::forAscii(rt, "beginLoadingMorph"),
|
|
571
|
+
0,
|
|
572
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
573
|
+
const jsi::Value& /*thisVal*/,
|
|
574
|
+
const jsi::Value* /*args*/,
|
|
575
|
+
size_t /*count*/) -> jsi::Value {
|
|
576
|
+
vroom_chart_begin_loading_morph(chart_);
|
|
577
|
+
return jsi::Value::undefined();
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (name == "setLoadingMorph") {
|
|
582
|
+
// setLoadingMorph(t) — advance stage one. 0 = the frozen sine, 1 = the
|
|
583
|
+
// polyline through the candle centres.
|
|
584
|
+
return jsi::Function::createFromHostFunction(
|
|
585
|
+
rt,
|
|
586
|
+
jsi::PropNameID::forAscii(rt, "setLoadingMorph"),
|
|
587
|
+
1,
|
|
588
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
589
|
+
const jsi::Value& /*thisVal*/,
|
|
590
|
+
const jsi::Value* args,
|
|
591
|
+
size_t count) -> jsi::Value {
|
|
592
|
+
if (count < 1) return jsi::Value::undefined();
|
|
593
|
+
vroom_chart_set_loading_morph(
|
|
594
|
+
chart_, static_cast<float>(args[0].asNumber()));
|
|
595
|
+
return jsi::Value::undefined();
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (name == "beginLoadingReveal") {
|
|
600
|
+
// beginLoadingReveal() — hand-off stage two: collapse every candle onto the
|
|
601
|
+
// line at zero alpha so it grows outward from there. Call once stage one
|
|
602
|
+
// reaches 1, then drive setIntervalMorph from 0 to 1.
|
|
603
|
+
return jsi::Function::createFromHostFunction(
|
|
604
|
+
rt,
|
|
605
|
+
jsi::PropNameID::forAscii(rt, "beginLoadingReveal"),
|
|
606
|
+
0,
|
|
607
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
608
|
+
const jsi::Value& /*thisVal*/,
|
|
609
|
+
const jsi::Value* /*args*/,
|
|
610
|
+
size_t /*count*/) -> jsi::Value {
|
|
611
|
+
vroom_chart_begin_loading_reveal(chart_);
|
|
612
|
+
return jsi::Value::undefined();
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
535
616
|
if (name == "setIntervalMorph") {
|
|
536
617
|
// setIntervalMorph(t) — advance the capture toward the new candles. `t` is
|
|
537
618
|
// pre-eased progress: 0 = the captured frame, 1 = settled (capture freed).
|
|
@@ -438,6 +438,9 @@ typedef enum {
|
|
|
438
438
|
VROOM_COLOR_ACCENT_BULL, // generic up color: price indicator, volume, MACD
|
|
439
439
|
VROOM_COLOR_ACCENT_BEAR, // generic down color
|
|
440
440
|
VROOM_COLOR_LINE, // line-chart-mode close-price polyline
|
|
441
|
+
// The loading line. Its own alpha is honored, then scaled by the fade-in
|
|
442
|
+
// and the stage-3 fade-out, so an opaque color here is the usual choice.
|
|
443
|
+
VROOM_COLOR_SKELETON,
|
|
441
444
|
VROOM_COLOR_COUNT_
|
|
442
445
|
} VroomColorKey;
|
|
443
446
|
|
|
@@ -586,6 +589,44 @@ void vroom_chart_begin_interval_morph(VroomChart* chart, int32_t mode);
|
|
|
586
589
|
// translate.
|
|
587
590
|
void vroom_chart_begin_stream_morph(VroomChart* chart);
|
|
588
591
|
|
|
592
|
+
// Shows or hides the loading line: a single stroke drawn across the plot in a
|
|
593
|
+
// slow travelling sine, for a chart that has been laid out but has no data yet.
|
|
594
|
+
// Suppresses the axis text, price badge, crosshair and indicator panes for as
|
|
595
|
+
// long as it's up.
|
|
596
|
+
//
|
|
597
|
+
// `on` must mean "loading *and* holding no data for the series being shown";
|
|
598
|
+
// the core takes it at face value rather than checking the candle buffer,
|
|
599
|
+
// because a host mid-asset-switch can still be holding the previous asset's
|
|
600
|
+
// bars. Pass `animate` 0 for reduced motion: the line draws still, and the
|
|
601
|
+
// chart is allowed to go idle instead of pinning a redraw loop.
|
|
602
|
+
//
|
|
603
|
+
// Passing 0 abandons any hand-off in flight, which is what a failed or
|
|
604
|
+
// superseded fetch wants. To hand off to data instead, use the two stages
|
|
605
|
+
// below.
|
|
606
|
+
void vroom_chart_set_loading(VroomChart* chart, int32_t on, int32_t animate);
|
|
607
|
+
|
|
608
|
+
// Hand-off stage one: reshapes the line into the series. Freezes the sine where
|
|
609
|
+
// it is and aims each vertex at the vertical centre of the candle that will
|
|
610
|
+
// occupy its column, so the line resolves into the silhouette of the data.
|
|
611
|
+
//
|
|
612
|
+
// Call *after* set_candles — it reads them to know where to aim — then drive
|
|
613
|
+
// vroom_chart_set_loading_morph from 0 to 1. The axes and the candles stay
|
|
614
|
+
// suppressed throughout; stage two brings them in. No-op if the line isn't up.
|
|
615
|
+
void vroom_chart_begin_loading_morph(VroomChart* chart);
|
|
616
|
+
|
|
617
|
+
// Advances stage one. `t` (clamped to 0..1) is the eased progress: 0 renders the
|
|
618
|
+
// frozen sine, 1 the polyline through the candle centres.
|
|
619
|
+
void vroom_chart_set_loading_morph(VroomChart* chart, float t);
|
|
620
|
+
|
|
621
|
+
// Hand-off stage two: the candles take over. Captures each one collapsed onto
|
|
622
|
+
// its own vertical centre at zero alpha and leaves the loading state, so the
|
|
623
|
+
// bars grow outward from the line and their color fades up as it fades out.
|
|
624
|
+
//
|
|
625
|
+
// Call once stage one has reached 1, then drive vroom_chart_set_interval_morph
|
|
626
|
+
// from 0 to 1 as usual — the line's fade-out rides that same clock, so the two
|
|
627
|
+
// finish together. No-op if the line isn't up.
|
|
628
|
+
void vroom_chart_begin_loading_reveal(VroomChart* chart);
|
|
629
|
+
|
|
589
630
|
// Advances the morph started by either begin_*_morph above. `t` (clamped to
|
|
590
631
|
// 0..1) is the eased progress: 0 renders the captured geometry pixel-identically
|
|
591
632
|
// to the pre-swap frame, 1 renders the new candles and releases the capture.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
#include <algorithm>
|
|
11
11
|
|
|
12
|
+
#include "color_lerp.h"
|
|
12
13
|
#include "theme.h"
|
|
13
14
|
#include "viewport.h"
|
|
14
15
|
|
|
@@ -21,18 +22,6 @@ inline uint32_t resolve_color(uint32_t color, uint32_t fill) {
|
|
|
21
22
|
return (color >> 24) == 0 ? fill : color;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
// Channel-wise ARGB blend, for a candle that changes direction mid-morph.
|
|
25
|
-
inline uint32_t lerp_argb(uint32_t a, uint32_t b, float t) {
|
|
26
|
-
uint32_t out = 0;
|
|
27
|
-
for (int shift = 0; shift < 32; shift += 8) {
|
|
28
|
-
const float ca = static_cast<float>((a >> shift) & 0xFFu);
|
|
29
|
-
const float cb = static_cast<float>((b >> shift) & 0xFFu);
|
|
30
|
-
const auto v = static_cast<uint32_t>(ca + (cb - ca) * t + 0.5f);
|
|
31
|
-
out |= v << shift;
|
|
32
|
-
}
|
|
33
|
-
return out;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
25
|
inline uint32_t scale_alpha(uint32_t argb, float a) {
|
|
37
26
|
const auto alpha =
|
|
38
27
|
static_cast<uint32_t>(static_cast<float>((argb >> 24) & 0xFFu) * a + 0.5f);
|
|
@@ -90,6 +79,7 @@ void draw(SkCanvas* canvas,
|
|
|
90
79
|
|
|
91
80
|
const uint32_t fill_bull = theme.colors[VROOM_COLOR_BULL];
|
|
92
81
|
const uint32_t fill_bear = theme.colors[VROOM_COLOR_BEAR];
|
|
82
|
+
const uint32_t skeleton_color = theme.colors[VROOM_COLOR_SKELETON];
|
|
93
83
|
|
|
94
84
|
const float body_r = theme.floats[VROOM_FLOAT_CANDLE_RADIUS_PX];
|
|
95
85
|
const bool wick_round = theme.floats[VROOM_FLOAT_WICK_ROUND_CAP] > 0.5f;
|
|
@@ -207,10 +197,21 @@ void draw(SkCanvas* canvas,
|
|
|
207
197
|
bool draw_border = bull ? draw_border_bull : draw_border_bear;
|
|
208
198
|
|
|
209
199
|
const bool flip = frm && to && frm->bull != bull;
|
|
210
|
-
|
|
200
|
+
// A slot handed over from the loading skeleton starts grey, whatever its
|
|
201
|
+
// direction — so it takes the per-slot path from a different origin
|
|
202
|
+
// color than a flip does, and overrides it when both apply.
|
|
203
|
+
const bool from_skeleton = frm && to && frm->skeleton;
|
|
204
|
+
if (flip || from_skeleton || alpha < 0.999f) {
|
|
211
205
|
const auto blend = [&](uint32_t c_bull, uint32_t c_bear) {
|
|
212
206
|
uint32_t c = bull ? c_bull : c_bear;
|
|
213
|
-
if (
|
|
207
|
+
if (from_skeleton) {
|
|
208
|
+
// lerp_argb covers the alpha channel too, so the bar's
|
|
209
|
+
// opacity rides in on the same blend as its color.
|
|
210
|
+
c = lerp_argb(scale_alpha(skeleton_color, frm->skeleton_alpha),
|
|
211
|
+
c, morph_t);
|
|
212
|
+
} else if (flip) {
|
|
213
|
+
c = lerp_argb(bull ? c_bear : c_bull, c, morph_t);
|
|
214
|
+
}
|
|
214
215
|
return scale_alpha(c, alpha);
|
|
215
216
|
};
|
|
216
217
|
slot_wick = *wick_p;
|
package/cpp/_core_src/chart.cpp
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
#include "chart.h"
|
|
11
11
|
|
|
12
|
+
#include <algorithm>
|
|
12
13
|
#include <cmath>
|
|
13
14
|
|
|
14
15
|
#pragma clang diagnostic push
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
#include "ichimoku.h"
|
|
33
34
|
#include "labels.h"
|
|
34
35
|
#include "liquidity.h"
|
|
36
|
+
#include "loading_line.h"
|
|
37
|
+
#include "loading_wave.h"
|
|
35
38
|
#include "ma.h"
|
|
36
39
|
#include "ma_overlay.h"
|
|
37
40
|
#include "macd.h"
|
|
@@ -42,6 +45,7 @@
|
|
|
42
45
|
#include "rsi_pane.h"
|
|
43
46
|
#include "style_inherit.h"
|
|
44
47
|
#include "tip_anchor.h"
|
|
48
|
+
#include "tip_geometry.h"
|
|
45
49
|
#include "tip_pulse.h"
|
|
46
50
|
#include "volume.h"
|
|
47
51
|
#include "vwap.h"
|
|
@@ -67,13 +71,30 @@ vroom::Layout VroomChart::layout() const {
|
|
|
67
71
|
(rsi.enabled ? 1 : 0) + (macd.enabled ? 1 : 0) + (atr.enabled ? 1 : 0);
|
|
68
72
|
const float indicator_h = static_cast<float>(pane_count) * height_px *
|
|
69
73
|
theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
|
|
74
|
+
|
|
75
|
+
// The line chart's tip dot is anchored to the newest close and overhangs the
|
|
76
|
+
// plot's right edge by its own radius, so the gutter has to be at least that
|
|
77
|
+
// wide or the dot lands under the y-axis strip (see tip_geometry.h). Only
|
|
78
|
+
// line mode needs it, and interpolating on morph_fade rather than switching
|
|
79
|
+
// on chart_type lets the few pixels of reflow ride the candles→line
|
|
80
|
+
// crossfade instead of stepping the instant the mode changes.
|
|
81
|
+
const float base_pad = theme.floats[VROOM_FLOAT_RIGHT_PADDING_PX];
|
|
82
|
+
float right_pad = base_pad;
|
|
83
|
+
if (theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f) {
|
|
84
|
+
const float wanted = std::max(
|
|
85
|
+
base_pad, vroom::tip_geometry::gutter_px(
|
|
86
|
+
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX]));
|
|
87
|
+
right_pad = base_pad + (wanted - base_pad) *
|
|
88
|
+
std::clamp(morph_fade, 0.f, 1.f);
|
|
89
|
+
}
|
|
90
|
+
|
|
70
91
|
return vroom::Layout{
|
|
71
92
|
width_px,
|
|
72
93
|
height_px,
|
|
73
94
|
vroom::axis_extent(axis_w, y_axis_collapse_t),
|
|
74
95
|
vroom::axis_extent(theme.floats[VROOM_FLOAT_X_AXIS_HEIGHT_PX],
|
|
75
96
|
x_axis_collapse_t),
|
|
76
|
-
|
|
97
|
+
right_pad,
|
|
77
98
|
theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO],
|
|
78
99
|
0.05f,
|
|
79
100
|
0.05f,
|
|
@@ -544,6 +565,23 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
544
565
|
bg.setColor(theme.colors[VROOM_COLOR_BACKGROUND]);
|
|
545
566
|
canvas->drawRect(SkRect::MakeWH(width_px, height_px), bg);
|
|
546
567
|
|
|
568
|
+
// 1b. Loading line, in place of the scene (stages 1 and 2). Above the
|
|
569
|
+
// `candles.empty()` return because `loading` is authoritative: the host
|
|
570
|
+
// may still be holding the previous asset's bars while the new one
|
|
571
|
+
// loads, and the line has to win over them. Returning here is also what
|
|
572
|
+
// suppresses the axis text, price badge, crosshair and indicator panes
|
|
573
|
+
// until stage 3 releases the flag.
|
|
574
|
+
if (loading) {
|
|
575
|
+
if (loading_fade_in < 1.f) {
|
|
576
|
+
canvas->saveLayerAlphaf(nullptr, loading_fade_in);
|
|
577
|
+
vroom::loading_line::draw(canvas, *this, lay);
|
|
578
|
+
canvas->restore();
|
|
579
|
+
} else {
|
|
580
|
+
vroom::loading_line::draw(canvas, *this, lay);
|
|
581
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
547
585
|
if (candles.empty()) return;
|
|
548
586
|
|
|
549
587
|
// 2. Visible slice + bounds + window_ms + geometry
|
|
@@ -562,6 +600,10 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
562
600
|
const float candle_area_h = vroom::price_pane_bottom(lay);
|
|
563
601
|
const float candle_right =
|
|
564
602
|
width_px - lay.y_axis_width_px - lay.right_padding_px;
|
|
603
|
+
// The line tip is the one plot layer allowed past candle_right: its dot
|
|
604
|
+
// overhangs the newest candle's center, which the pinned-to-latest framing
|
|
605
|
+
// puts within a few pixels of that edge. It stops at the y-axis strip.
|
|
606
|
+
const float tip_clip_right = width_px - lay.y_axis_width_px;
|
|
565
607
|
|
|
566
608
|
// 3. Update label fade state ONCE per frame — both gridlines and labels
|
|
567
609
|
// share these opacities so their animations stay in lockstep.
|
|
@@ -606,6 +648,15 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
606
648
|
const std::size_t morph_n_draw = reshape ? morph_n : 0;
|
|
607
649
|
const float morph_t = reshape ? interval_morph_t : 1.f;
|
|
608
650
|
|
|
651
|
+
// Line-mode tip marker, resolved by whichever branch below draws the price
|
|
652
|
+
// series and painted at step 6.5. It is the only plot layer that has to
|
|
653
|
+
// outlive the axis backgrounds, so it can't be drawn in place like the rest.
|
|
654
|
+
float tip_opacity = 0.f;
|
|
655
|
+
std::size_t tip_slots = 0;
|
|
656
|
+
const vroom::CandleSnapshot* tip_from = nullptr;
|
|
657
|
+
std::size_t tip_from_n = 0;
|
|
658
|
+
float tip_morph_t = 1.f;
|
|
659
|
+
|
|
609
660
|
if (fade_out) {
|
|
610
661
|
// First half: only what was captured, at the axis envelope opacity.
|
|
611
662
|
// Layers with no snapshot (volume, liquidity, FVG, drawings) would
|
|
@@ -653,17 +704,11 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
653
704
|
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade * layer,
|
|
654
705
|
morph_from.data(), morph_n, 0.f,
|
|
655
706
|
theme.floats[VROOM_FLOAT_LINE_TENSION]);
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
theme.colors[VROOM_COLOR_BACKGROUND],
|
|
662
|
-
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade * layer,
|
|
663
|
-
theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
|
|
664
|
-
tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds,
|
|
665
|
-
morph_from.data(), morph_n, 0.f);
|
|
666
|
-
}
|
|
707
|
+
tip_opacity = fade * layer;
|
|
708
|
+
tip_slots = 0;
|
|
709
|
+
tip_from = morph_from.data();
|
|
710
|
+
tip_from_n = morph_n;
|
|
711
|
+
tip_morph_t = 0.f;
|
|
667
712
|
}
|
|
668
713
|
|
|
669
714
|
// Over the candles, and the panes below them. One layer for both:
|
|
@@ -702,9 +747,19 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
702
747
|
}
|
|
703
748
|
|
|
704
749
|
// 4.5. Volume bars — drawn under the candles so candles z-index above.
|
|
705
|
-
|
|
750
|
+
// Through a loading reveal they grow up off the axis on the
|
|
751
|
+
// same clock as the candles, borrowing the collapse they
|
|
752
|
+
// already have. They carry no morph snapshot of their own, so
|
|
753
|
+
// without this they'd snap in at full height while the price
|
|
754
|
+
// series is still a thread lying on the line. A render-time
|
|
755
|
+
// override, not a state change: the host still owns the
|
|
756
|
+
// collapse for the volume toggle.
|
|
757
|
+
const float vol_t = loading_line_revealing
|
|
758
|
+
? std::max(volume_collapse_t, 1.f - interval_morph_t)
|
|
759
|
+
: volume_collapse_t;
|
|
760
|
+
if (vol_t < 1.f) {
|
|
706
761
|
vroom::volume::draw(canvas, visible, n, lay, theme, volume,
|
|
707
|
-
|
|
762
|
+
vol_t, volume_collapse_easing,
|
|
708
763
|
window_ms, visible_start_ms,
|
|
709
764
|
candle_duration_ms);
|
|
710
765
|
}
|
|
@@ -753,20 +808,16 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
753
808
|
vroom::drawings::draw(canvas, *this, lay, bounds, candle_right,
|
|
754
809
|
candle_area_h);
|
|
755
810
|
|
|
756
|
-
// 5.8. Line-mode tip marker.
|
|
757
|
-
|
|
811
|
+
// 5.8. Line-mode tip marker. The saveLayer this branch may be
|
|
812
|
+
// wrapped in doesn't reach step 6.5, so fold its alpha in.
|
|
813
|
+
if (fade > 0.f) {
|
|
758
814
|
const auto anchor = vroom::tip_anchor::at(
|
|
759
815
|
range.start, range.end, candles.size(), reshape);
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade,
|
|
766
|
-
theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
|
|
767
|
-
tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds,
|
|
768
|
-
anchor.use_morph ? morph_src : nullptr,
|
|
769
|
-
anchor.use_morph ? morph_n_draw : 0, morph_t);
|
|
816
|
+
tip_opacity = fade * (wrap ? axis_phase.opacity : 1.f);
|
|
817
|
+
tip_slots = anchor.slot_count;
|
|
818
|
+
tip_from = anchor.use_morph ? morph_src : nullptr;
|
|
819
|
+
tip_from_n = anchor.use_morph ? morph_n_draw : 0;
|
|
820
|
+
tip_morph_t = morph_t;
|
|
770
821
|
}
|
|
771
822
|
|
|
772
823
|
// Incoming fade: indicator panes share the scene opacity (and
|
|
@@ -782,6 +833,15 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
782
833
|
if (wrap) canvas->restore();
|
|
783
834
|
}
|
|
784
835
|
|
|
836
|
+
// 5.9. Loading line, stage 3. Over the series it handed off to, on the same
|
|
837
|
+
// clock, so it reads as the bars peeling away from the line rather than
|
|
838
|
+
// two things dissolving independently. Before the axis masks below,
|
|
839
|
+
// since it belongs inside the plot.
|
|
840
|
+
if (loading_line_revealing) {
|
|
841
|
+
vroom::loading_line::draw_fading(canvas, *this, lay,
|
|
842
|
+
1.f - interval_morph_t);
|
|
843
|
+
}
|
|
844
|
+
|
|
785
845
|
// 6. Axis backgrounds (mask any candle overflow). The x-axis separator
|
|
786
846
|
// line is intentionally omitted for now. The bottom strip anchors at
|
|
787
847
|
// x_axis_top (below any indicator pane) so it never paints over it.
|
|
@@ -796,6 +856,24 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
796
856
|
SkRect::MakeXYWH(candle_right, 0, axis_block_w, height_px),
|
|
797
857
|
axis_bg);
|
|
798
858
|
|
|
859
|
+
// 6.5. Line-mode tip marker, resolved at 5.8. After the masks because the
|
|
860
|
+
// dot sits on the newest close, which a view following the latest bar
|
|
861
|
+
// parks within a few pixels of candle_right — closer than the dot's own
|
|
862
|
+
// radius — so it paints into the gutter the layout widened for it. The
|
|
863
|
+
// clip still stops at the y-axis strip, so nothing here can reach the
|
|
864
|
+
// price labels; the pulse ring simply ends there.
|
|
865
|
+
if (tip_opacity > 0.f && theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f) {
|
|
866
|
+
vroom::ma_overlay::draw_close_tip(
|
|
867
|
+
canvas, lay, bounds, visible, tip_slots, window_ms,
|
|
868
|
+
visible_start_ms, candle_duration_ms, candle_right, tip_clip_right,
|
|
869
|
+
candle_area_h, theme.colors[VROOM_COLOR_LINE],
|
|
870
|
+
theme.colors[VROOM_COLOR_BACKGROUND],
|
|
871
|
+
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], tip_opacity,
|
|
872
|
+
theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
|
|
873
|
+
tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds, tip_from,
|
|
874
|
+
tip_from_n, tip_morph_t);
|
|
875
|
+
}
|
|
876
|
+
|
|
799
877
|
// 7. Labels (read from y_fades / x_fades, no state mutation here)
|
|
800
878
|
vroom::labels::draw_y_labels(canvas, *this, lay, axis_bounds);
|
|
801
879
|
vroom::labels::draw_x_labels(canvas, *this, lay, axis_start_ms, axis_end_ms);
|
|
@@ -805,8 +883,19 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
805
883
|
// Hidden during a fade's outgoing half: the close is already the new
|
|
806
884
|
// series and there is no snapshot to fade.
|
|
807
885
|
if (!fade_out) {
|
|
808
|
-
|
|
809
|
-
|
|
886
|
+
// Eased off the same capture the candles reshape through, so the badge
|
|
887
|
+
// rides the bar's close edge instead of landing on the new price a
|
|
888
|
+
// whole animation early. The pairing rule is the tip marker's: the
|
|
889
|
+
// capture only stands for the newest candle while that candle is the
|
|
890
|
+
// one at the right edge.
|
|
891
|
+
const auto price_anchor = vroom::tip_anchor::at(
|
|
892
|
+
range.start, range.end, candles.size(), reshape);
|
|
893
|
+
const vroom::CandleSnapshot* price_from =
|
|
894
|
+
(price_anchor.use_morph && morph_src && morph_n_draw > 0)
|
|
895
|
+
? morph_src
|
|
896
|
+
: nullptr;
|
|
897
|
+
vroom::price_indicator::draw(canvas, *this, lay, bounds, candle_right,
|
|
898
|
+
candle_area_h, price_from, morph_t);
|
|
810
899
|
|
|
811
900
|
// 7.54. Fair Value Gap labels — the boxes themselves are back behind
|
|
812
901
|
// the candles, but their text has to clear the bars it sits over.
|
|
@@ -882,6 +971,182 @@ void VroomChart::begin_frame() {
|
|
|
882
971
|
// and a chart left open for hours would otherwise lose float precision on it.
|
|
883
972
|
tip_pulse_elapsed_s =
|
|
884
973
|
std::fmod(tip_pulse_elapsed_s + dt, vroom::tip_pulse::kPeriodSeconds);
|
|
974
|
+
|
|
975
|
+
if (loading) {
|
|
976
|
+
// Frozen once a capture exists: stage 2 morphs away from the phase the
|
|
977
|
+
// sine held when the data landed, and advancing it underneath would
|
|
978
|
+
// drag the morph's own start point around.
|
|
979
|
+
if (loading_animate && loading_line.empty()) {
|
|
980
|
+
loading_elapsed_s = std::fmod(loading_elapsed_s + dt,
|
|
981
|
+
vroom::loading_wave::kPeriodSeconds);
|
|
982
|
+
}
|
|
983
|
+
// Ramps regardless of `loading_animate`: reduced motion suppresses the
|
|
984
|
+
// wave's movement, not the appearance of the line itself, which would
|
|
985
|
+
// otherwise pop in at full strength.
|
|
986
|
+
constexpr float kFadeInSeconds = 0.3f;
|
|
987
|
+
loading_fade_in = std::min(1.f, loading_fade_in + dt / kFadeInSeconds);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
void VroomChart::set_loading(bool on, bool animate) {
|
|
992
|
+
loading_animate = animate;
|
|
993
|
+
if (on == loading) return; // a re-push of the same state isn't a restart
|
|
994
|
+
loading = on;
|
|
995
|
+
|
|
996
|
+
// Either direction abandons any hand-off in flight. Going false without
|
|
997
|
+
// passing through the two stages is the cancel path (fetch failed, asset
|
|
998
|
+
// switched again mid-load), and it should leave nothing behind.
|
|
999
|
+
loading_line.clear();
|
|
1000
|
+
loading_line_t = 1.f;
|
|
1001
|
+
loading_line_revealing = false;
|
|
1002
|
+
if (!on) {
|
|
1003
|
+
mark_dirty();
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
loading_elapsed_s = 0.f;
|
|
1008
|
+
loading_fade_in = 0.f;
|
|
1009
|
+
mark_dirty();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Visible slice + price bounds the two hand-off stages both need. Mirrors what
|
|
1013
|
+
// draw_chart computes in step 2; `n == 0` means there is nothing to hand off to.
|
|
1014
|
+
VroomChart::LoadingTarget VroomChart::loading_target() const {
|
|
1015
|
+
LoadingTarget t{};
|
|
1016
|
+
if (candles.empty()) return t;
|
|
1017
|
+
const auto range = vroom::visible_indices(candles.data(), candles.size(),
|
|
1018
|
+
visible_start_ms, visible_end_ms);
|
|
1019
|
+
const std::size_t n = range.end - range.start;
|
|
1020
|
+
if (n == 0) return t;
|
|
1021
|
+
t.visible = candles.data() + range.start;
|
|
1022
|
+
t.n = n;
|
|
1023
|
+
t.bounds = price_bounds_manual
|
|
1024
|
+
? price_bounds
|
|
1025
|
+
: vroom::auto_price_bounds(t.visible, n);
|
|
1026
|
+
return t;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
void VroomChart::begin_loading_morph() {
|
|
1030
|
+
if (!loading) return;
|
|
1031
|
+
|
|
1032
|
+
const auto lay = layout();
|
|
1033
|
+
const float area_w = vroom::candle_area_width(lay);
|
|
1034
|
+
const auto target = loading_target();
|
|
1035
|
+
if (target.n == 0 || area_w <= 0.f) return;
|
|
1036
|
+
|
|
1037
|
+
const int64_t window_ms = visible_end_ms - visible_start_ms;
|
|
1038
|
+
const float elapsed = loading_animate ? loading_elapsed_s : 0.f;
|
|
1039
|
+
|
|
1040
|
+
const auto push = [&](float xf, float to_y) {
|
|
1041
|
+
vroom::LinePoint p{};
|
|
1042
|
+
p.x = xf;
|
|
1043
|
+
// Sampling the sine at this vertex's own x is what makes the freeze
|
|
1044
|
+
// invisible: the curve the user was watching passes through this point
|
|
1045
|
+
// already, so stage 2 starts on the pixels stage 1 ended on.
|
|
1046
|
+
p.from_y = vroom::loading_wave::y_frac(elapsed,
|
|
1047
|
+
std::clamp(xf, 0.f, 1.f));
|
|
1048
|
+
p.to_y = to_y;
|
|
1049
|
+
loading_line.push_back(p);
|
|
1050
|
+
};
|
|
1051
|
+
// Mid-range, not mid-body: the bars grow outward symmetrically from this
|
|
1052
|
+
// line, so anchoring on the body would leave one wick reaching further
|
|
1053
|
+
// than the other.
|
|
1054
|
+
const auto center_frac = [&](const ::VroomCandle& c) {
|
|
1055
|
+
return static_cast<float>(
|
|
1056
|
+
vroom::price_fraction(target.bounds, (c.high + c.low) * 0.5));
|
|
1057
|
+
};
|
|
1058
|
+
const auto center_x_frac = [&](const ::VroomCandle& c) {
|
|
1059
|
+
return vroom::candle_center_x(lay, c.time_ms, candle_duration_ms,
|
|
1060
|
+
visible_start_ms, window_ms) / area_w;
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
// Left to right, the order a polyline wants. (The reveal below indexes from
|
|
1064
|
+
// the right instead, because that's what the morph machinery expects.)
|
|
1065
|
+
loading_line.clear();
|
|
1066
|
+
loading_line.reserve(target.n + 2);
|
|
1067
|
+
|
|
1068
|
+
// Spans the data doesn't reach — a short series in a wider window — are
|
|
1069
|
+
// sampled at the idle curve's own resolution and flattened to the nearest
|
|
1070
|
+
// candle's level. So the line still begins as the curve across the *whole*
|
|
1071
|
+
// plot and eases into a level run leading into the series; leaving those
|
|
1072
|
+
// spans out instead would snap the line's length on the first frame.
|
|
1073
|
+
const int across = vroom::loading_wave::sample_count(area_w);
|
|
1074
|
+
const float step = 1.f / static_cast<float>(across);
|
|
1075
|
+
const float first_xf = center_x_frac(target.visible[0]);
|
|
1076
|
+
const float last_xf = center_x_frac(target.visible[target.n - 1]);
|
|
1077
|
+
|
|
1078
|
+
for (int i = 0; static_cast<float>(i) * step < first_xf; ++i) {
|
|
1079
|
+
push(static_cast<float>(i) * step, center_frac(target.visible[0]));
|
|
1080
|
+
}
|
|
1081
|
+
for (std::size_t i = 0; i < target.n; ++i) {
|
|
1082
|
+
push(center_x_frac(target.visible[i]), center_frac(target.visible[i]));
|
|
1083
|
+
}
|
|
1084
|
+
for (int i = static_cast<int>(std::ceil(last_xf / step)); i <= across; ++i) {
|
|
1085
|
+
push(static_cast<float>(i) * step,
|
|
1086
|
+
center_frac(target.visible[target.n - 1]));
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
loading_line_t = 0.f;
|
|
1090
|
+
// `loading` stays set: the axes, price badge and panes have no business
|
|
1091
|
+
// appearing until the candles themselves do, in stage 3.
|
|
1092
|
+
mark_dirty();
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
void VroomChart::begin_loading_reveal() {
|
|
1096
|
+
if (!loading) return;
|
|
1097
|
+
loading = false;
|
|
1098
|
+
|
|
1099
|
+
const auto lay = layout();
|
|
1100
|
+
const float area_w = vroom::candle_area_width(lay);
|
|
1101
|
+
const auto target = loading_target();
|
|
1102
|
+
if (target.n == 0 || area_w <= 0.f) {
|
|
1103
|
+
loading_line.clear();
|
|
1104
|
+
mark_dirty();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const int64_t window_ms = visible_end_ms - visible_start_ms;
|
|
1109
|
+
|
|
1110
|
+
// Right-to-left, matching how the morph machinery indexes slots (slot 0 =
|
|
1111
|
+
// newest bar at the right edge).
|
|
1112
|
+
morph_from.clear();
|
|
1113
|
+
morph_from.reserve(target.n);
|
|
1114
|
+
for (std::size_t slot = 0; slot < target.n; ++slot) {
|
|
1115
|
+
const ::VroomCandle& c = target.visible[target.n - 1 - slot];
|
|
1116
|
+
const float center = static_cast<float>(
|
|
1117
|
+
vroom::price_fraction(target.bounds, (c.high + c.low) * 0.5));
|
|
1118
|
+
|
|
1119
|
+
vroom::CandleSnapshot s{};
|
|
1120
|
+
s.x = vroom::candle_center_x(lay, c.time_ms, candle_duration_ms,
|
|
1121
|
+
visible_start_ms, window_ms) / area_w;
|
|
1122
|
+
// Every price collapsed onto the centre, so the slot starts as a
|
|
1123
|
+
// zero-height sliver sitting on the line and the existing lerp is what
|
|
1124
|
+
// grows it out to full open/high/low/close.
|
|
1125
|
+
s.open = s.high = s.low = s.close = center;
|
|
1126
|
+
s.bull = c.close >= c.open;
|
|
1127
|
+
// Starts the colour blend from fully transparent rather than from the
|
|
1128
|
+
// skeleton grey: the bar emerges from the line, so a grey body would
|
|
1129
|
+
// just smear the line thicker on the first frames.
|
|
1130
|
+
s.skeleton = true;
|
|
1131
|
+
s.skeleton_alpha = 0.f;
|
|
1132
|
+
morph_from.push_back(s);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
morph_from_bounds = target.bounds;
|
|
1136
|
+
interval_morph_t = 0.f;
|
|
1137
|
+
// Slot-lerp, not crossfade: each bar has a counterpart to grow out of, so
|
|
1138
|
+
// pairing column to column is what makes the line *become* the data instead
|
|
1139
|
+
// of one scene dissolving into another.
|
|
1140
|
+
interval_morph_fade = false;
|
|
1141
|
+
// Keeps the axes out of the interval envelope (see labels::interval_phase),
|
|
1142
|
+
// which is what the flag actually selects. A timeframe switch runs the
|
|
1143
|
+
// outgoing ticks out and the new ones in; here there are no outgoing ticks
|
|
1144
|
+
// — stages 1 and 2 draw no axis at all — so that envelope would hold the
|
|
1145
|
+
// real labels back for the first half of the reveal. Per-label fades bring
|
|
1146
|
+
// them up from nothing instead, in step with the bars.
|
|
1147
|
+
morph_is_stream = true;
|
|
1148
|
+
loading_line_revealing = true;
|
|
1149
|
+
mark_dirty();
|
|
885
1150
|
}
|
|
886
1151
|
|
|
887
1152
|
bool VroomChart::tip_pulse_active() const {
|
|
@@ -902,6 +1167,10 @@ bool VroomChart::is_animating_now() const {
|
|
|
902
1167
|
// The pulse never finishes on its own, so this is what keeps the host loops
|
|
903
1168
|
// requeueing frames for it.
|
|
904
1169
|
if (tip_pulse_active()) return true;
|
|
1170
|
+
// Same deal for the loading line's wave, except a still line (reduced
|
|
1171
|
+
// motion, once faded in) has nothing left to redraw and may go idle. The
|
|
1172
|
+
// two hand-off stages are host-driven, so they need nothing here.
|
|
1173
|
+
if (loading && (loading_animate || loading_fade_in < 1.f)) return true;
|
|
905
1174
|
for (const auto& f : y_fades) {
|
|
906
1175
|
if (f.opacity != f.target) return true;
|
|
907
1176
|
}
|