react-native-vroom-chart 0.16.0 → 0.18.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/README.md +1 -1
- package/android/CMakeLists.txt +12 -2
- package/android/README.md +16 -6
- package/android/build.gradle +10 -0
- package/android/src/main/cpp/OnLoad.cpp +10 -0
- package/android/src/main/cpp/VroomChartJsiBindings.cpp +35 -0
- package/android/src/main/cpp/VroomChartJsiBindings.h +27 -0
- package/android/src/main/java/com/vroom/chart/VroomChartModule.kt +15 -11
- package/cpp/VroomChartHostObject.cpp +81 -0
- package/cpp/VroomJsiInstaller.cpp +6 -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/ios/README.md +17 -5
- package/ios/VroomChartModule.h +3 -6
- package/ios/VroomChartModule.mm +17 -23
- package/lib/index.d.mts +30 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +75 -14
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +75 -14
- package/lib/index.mjs.map +1 -1
- package/package.json +9 -9
- package/react-native-vroom-chart.podspec +1 -1
- package/src/NativeVroomChart.ts +7 -4
- package/src/VroomChart.tsx +12 -0
- package/src/jsi.d.ts +42 -0
- package/src/theme.ts +1 -0
- package/src/useChartCore.ts +129 -7
- package/android/src/main/cpp/VroomChartJni.cpp +0 -24
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#include "loading_line.h"
|
|
2
|
+
|
|
3
|
+
#pragma clang diagnostic push
|
|
4
|
+
#pragma clang diagnostic ignored "-Wdocumentation"
|
|
5
|
+
#include "include/core/SkCanvas.h"
|
|
6
|
+
#include "include/core/SkColor.h"
|
|
7
|
+
#include "include/core/SkPaint.h"
|
|
8
|
+
#include "include/core/SkPath.h"
|
|
9
|
+
#include "include/core/SkPathBuilder.h"
|
|
10
|
+
#include "include/core/SkPoint.h"
|
|
11
|
+
#pragma clang diagnostic pop
|
|
12
|
+
|
|
13
|
+
#include <algorithm>
|
|
14
|
+
#include <cstddef>
|
|
15
|
+
#include <vector>
|
|
16
|
+
|
|
17
|
+
#include "chart.h"
|
|
18
|
+
#include "curve.h"
|
|
19
|
+
#include "loading_wave.h"
|
|
20
|
+
#include "theme.h"
|
|
21
|
+
#include "viewport.h"
|
|
22
|
+
|
|
23
|
+
namespace vroom::loading_line {
|
|
24
|
+
|
|
25
|
+
namespace {
|
|
26
|
+
|
|
27
|
+
constexpr float kStrokeWidthPx = 2.f;
|
|
28
|
+
|
|
29
|
+
// Multiplies into whatever alpha the resolved color already carries.
|
|
30
|
+
SkColor faded(SkColor c, float a) {
|
|
31
|
+
const float scaled = static_cast<float>(SkColorGetA(c)) *
|
|
32
|
+
std::clamp(a, 0.f, 1.f);
|
|
33
|
+
return SkColorSetA(c, static_cast<U8CPU>(scaled + 0.5f));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The loading line's color. A transparent `skeleton` inherits the gridline
|
|
37
|
+
// color, the same sentinel convention the candle border and wick colors use.
|
|
38
|
+
// The gridlines are the chart's existing vocabulary for "structure, not data",
|
|
39
|
+
// which is exactly what the line is: drawing it in that tone keeps it from
|
|
40
|
+
// reading as a series, without needing to be scaled down to stay quiet.
|
|
41
|
+
SkColor stroke_color(const VroomChart& chart) {
|
|
42
|
+
const SkColor c = chart.theme.colors[VROOM_COLOR_SKELETON];
|
|
43
|
+
return SkColorGetA(c) == 0 ? chart.theme.colors[VROOM_COLOR_GRID] : c;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Emits `pts` as a monotone cubic — the same curve the line chart draws (see
|
|
47
|
+
// curve.h), so the loading line and the series it hands off to are shaped by
|
|
48
|
+
// the same math and the transition has no renderer seam to hide.
|
|
49
|
+
//
|
|
50
|
+
// Monotone rather than a plain spline matters even here: by the end of the
|
|
51
|
+
// morph these vertices *are* real price data, and an unconstrained curve would
|
|
52
|
+
// overshoot a sharp reversal and bow outside the candle it is supposed to pass
|
|
53
|
+
// through.
|
|
54
|
+
SkPath spline(const std::vector<SkPoint>& pts) {
|
|
55
|
+
SkPathBuilder path;
|
|
56
|
+
if (pts.empty()) return path.detach();
|
|
57
|
+
path.moveTo(pts[0]);
|
|
58
|
+
// Below three points there is no interior vertex to fit a tangent to.
|
|
59
|
+
if (pts.size() < 3) {
|
|
60
|
+
for (std::size_t i = 1; i < pts.size(); ++i) path.lineTo(pts[i]);
|
|
61
|
+
return path.detach();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const auto secant_at = [&](std::size_t i) {
|
|
65
|
+
return curve::secant(pts[i].fX, pts[i].fY,
|
|
66
|
+
pts[i + 1].fX, pts[i + 1].fY);
|
|
67
|
+
};
|
|
68
|
+
const auto tangent_at = [&](std::size_t i) {
|
|
69
|
+
if (i == 0) return secant_at(0);
|
|
70
|
+
if (i + 1 == pts.size()) return secant_at(pts.size() - 2);
|
|
71
|
+
return curve::monotone_tangent(secant_at(i - 1), secant_at(i));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
for (std::size_t i = 0; i + 1 < pts.size(); ++i) {
|
|
75
|
+
// Flat or backwards spacing has no meaningful tangent, and curving
|
|
76
|
+
// through it would fold the line back on itself.
|
|
77
|
+
if (!(pts[i + 1].fX > pts[i].fX)) {
|
|
78
|
+
path.lineTo(pts[i + 1]);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const curve::Controls c = curve::segment_controls(
|
|
82
|
+
pts[i].fX, pts[i].fY, pts[i + 1].fX, pts[i + 1].fY,
|
|
83
|
+
tangent_at(i), tangent_at(i + 1), 1.f);
|
|
84
|
+
path.cubicTo(c.c1x, c.c1y, c.c2x, c.c2y, pts[i + 1].fX, pts[i + 1].fY);
|
|
85
|
+
}
|
|
86
|
+
return path.detach();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
void stroke_path(SkCanvas* canvas,
|
|
90
|
+
const VroomChart& chart,
|
|
91
|
+
const SkPath& path,
|
|
92
|
+
float alpha) {
|
|
93
|
+
SkPaint p;
|
|
94
|
+
p.setStyle(SkPaint::kStroke_Style);
|
|
95
|
+
p.setStrokeWidth(kStrokeWidthPx);
|
|
96
|
+
p.setStrokeCap(SkPaint::kRound_Cap);
|
|
97
|
+
p.setStrokeJoin(SkPaint::kRound_Join);
|
|
98
|
+
p.setAntiAlias(true);
|
|
99
|
+
p.setColor(faded(stroke_color(chart), alpha));
|
|
100
|
+
canvas->drawPath(path, p);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Pinned to phase 0 under reduced motion, which leaves a still curve at
|
|
104
|
+
// mid-breath rather than nothing — the placeholder is still worth showing.
|
|
105
|
+
float phase(const VroomChart& chart) {
|
|
106
|
+
return chart.loading_animate ? chart.loading_elapsed_s : 0.f;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Stage 1: the curve alone, sampled across the whole plot.
|
|
110
|
+
SkPath idle_path(const VroomChart& chart, float area_w, float pane_h) {
|
|
111
|
+
const int samples = loading_wave::sample_count(area_w);
|
|
112
|
+
const float elapsed = phase(chart);
|
|
113
|
+
std::vector<SkPoint> pts;
|
|
114
|
+
pts.reserve(static_cast<std::size_t>(samples) + 1);
|
|
115
|
+
for (int i = 0; i <= samples; ++i) {
|
|
116
|
+
const float xf = static_cast<float>(i) / static_cast<float>(samples);
|
|
117
|
+
pts.push_back(SkPoint{xf * area_w,
|
|
118
|
+
loading_wave::y_frac(elapsed, xf) * pane_h});
|
|
119
|
+
}
|
|
120
|
+
return spline(pts);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Stages 2 and 3: the captured vertices, eased from the frozen curve toward the
|
|
124
|
+
// candle centres. `t` of 1 is the shape the candles grow out of.
|
|
125
|
+
SkPath morph_path(const VroomChart& chart,
|
|
126
|
+
const Layout& lay,
|
|
127
|
+
float area_w,
|
|
128
|
+
float pane_h,
|
|
129
|
+
float t) {
|
|
130
|
+
std::vector<SkPoint> pts;
|
|
131
|
+
pts.reserve(chart.loading_line.size());
|
|
132
|
+
for (const LinePoint& pt : chart.loading_line) {
|
|
133
|
+
const float from_y = pt.from_y * pane_h;
|
|
134
|
+
const float to_y = y_at_fraction(lay, pt.to_y);
|
|
135
|
+
pts.push_back(SkPoint{pt.x * area_w, from_y + (to_y - from_y) * t});
|
|
136
|
+
}
|
|
137
|
+
return spline(pts);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
} // namespace
|
|
141
|
+
|
|
142
|
+
void draw(SkCanvas* canvas, const VroomChart& chart, const Layout& lay) {
|
|
143
|
+
if (!canvas) return;
|
|
144
|
+
const float area_w = candle_area_width(lay);
|
|
145
|
+
const float pane_h = price_pane_bottom(lay);
|
|
146
|
+
if (area_w <= 0.f || pane_h <= 0.f) return;
|
|
147
|
+
|
|
148
|
+
const float b = loading_wave::breath(phase(chart));
|
|
149
|
+
|
|
150
|
+
// A capture present means the data has landed and the line is on its way to
|
|
151
|
+
// the candle centres; absent means nothing has arrived yet.
|
|
152
|
+
if (chart.loading_line.empty()) {
|
|
153
|
+
stroke_path(canvas, chart, idle_path(chart, area_w, pane_h), b);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Settle out of the breath as the line resolves, so it holds still at full
|
|
157
|
+
// strength by the time it reaches the candle centres — subtle at the
|
|
158
|
+
// default gridline tone, but it stops a bright custom color from pulsing
|
|
159
|
+
// while the data lands. Stage 3 then fades from full, where draw_fading
|
|
160
|
+
// picks up.
|
|
161
|
+
const float alpha = b + (1.f - b) * chart.loading_line_t;
|
|
162
|
+
stroke_path(canvas, chart,
|
|
163
|
+
morph_path(chart, lay, area_w, pane_h, chart.loading_line_t),
|
|
164
|
+
alpha);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
void draw_fading(SkCanvas* canvas,
|
|
168
|
+
const VroomChart& chart,
|
|
169
|
+
const Layout& lay,
|
|
170
|
+
float alpha) {
|
|
171
|
+
if (!canvas || alpha <= 0.f) return;
|
|
172
|
+
if (chart.loading_line.empty()) return;
|
|
173
|
+
const float area_w = candle_area_width(lay);
|
|
174
|
+
const float pane_h = price_pane_bottom(lay);
|
|
175
|
+
if (area_w <= 0.f || pane_h <= 0.f) return;
|
|
176
|
+
|
|
177
|
+
// Held at the morph's end state: the line has already reached the centres,
|
|
178
|
+
// and the candles are growing out of exactly these pixels.
|
|
179
|
+
stroke_path(canvas, chart, morph_path(chart, lay, area_w, pane_h, 1.f),
|
|
180
|
+
alpha);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
} // namespace vroom::loading_line
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The loading line: a single stroke across the plot standing in for a series
|
|
2
|
+
// that hasn't arrived yet, and the bridge the real candles grow out of.
|
|
3
|
+
//
|
|
4
|
+
// Three stages, all drawn by the one function here:
|
|
5
|
+
//
|
|
6
|
+
// 1. idle — a travelling sine across the full width (see loading_wave.h)
|
|
7
|
+
// 2. morph — that sine, frozen, easing into a polyline through the vertical
|
|
8
|
+
// centre of every candle about to be drawn
|
|
9
|
+
// 3. reveal — the polyline holding still at those centres while it fades
|
|
10
|
+
// out and the candles grow outward from it
|
|
11
|
+
//
|
|
12
|
+
// Stages 1 and 2 run instead of the chart, so the axis text, price badge,
|
|
13
|
+
// crosshair and indicator panes stay suppressed until the data is really
|
|
14
|
+
// there. Stage 3 runs over the top of the normal scene.
|
|
15
|
+
|
|
16
|
+
#pragma once
|
|
17
|
+
|
|
18
|
+
class SkCanvas;
|
|
19
|
+
struct VroomChart;
|
|
20
|
+
|
|
21
|
+
namespace vroom {
|
|
22
|
+
struct Layout;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
namespace vroom::loading_line {
|
|
26
|
+
|
|
27
|
+
// Stages 1 and 2, in place of the chart. Which one it draws depends on whether
|
|
28
|
+
// the chart holds a capture (see VroomChart::begin_loading_morph).
|
|
29
|
+
void draw(SkCanvas* canvas, const VroomChart& chart, const Layout& lay);
|
|
30
|
+
|
|
31
|
+
// Stage 3, over the settled scene. `alpha` is the line's remaining opacity, so
|
|
32
|
+
// the caller can ride it on the same clock as the candles' growth.
|
|
33
|
+
void draw_fading(SkCanvas* canvas,
|
|
34
|
+
const VroomChart& chart,
|
|
35
|
+
const Layout& lay,
|
|
36
|
+
float alpha);
|
|
37
|
+
|
|
38
|
+
} // namespace vroom::loading_line
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// The idle animation the loading line rides while a series is being fetched.
|
|
2
|
+
// A position across the plot and an elapsed time in; a vertical offset and a
|
|
3
|
+
// brightness out.
|
|
4
|
+
//
|
|
5
|
+
// Three sines, not one. A single sine is too regular to read as anything but a
|
|
6
|
+
// test pattern — the eye finds the repeat immediately and the line stops
|
|
7
|
+
// looking like it's waiting for something. Summing three frequencies that
|
|
8
|
+
// aren't multiples of each other, each drifting at its own rate, gives a curve
|
|
9
|
+
// that keeps rearranging itself.
|
|
10
|
+
//
|
|
11
|
+
// The other half of the effect is restraint: a small amplitude and a faint,
|
|
12
|
+
// slowly breathing opacity. The line is a hint that the chart is alive, not a
|
|
13
|
+
// feature competing with the data about to replace it.
|
|
14
|
+
//
|
|
15
|
+
// Approach adapted from Liveline (github.com/benjitaylor/liveline, MIT), whose
|
|
16
|
+
// loading state solves the same problem well.
|
|
17
|
+
//
|
|
18
|
+
// Skia-free and header-only so the unit tests can cover it; see
|
|
19
|
+
// tests/test_loading_wave.cpp.
|
|
20
|
+
|
|
21
|
+
#pragma once
|
|
22
|
+
|
|
23
|
+
#include <cmath>
|
|
24
|
+
|
|
25
|
+
namespace vroom::loading_wave {
|
|
26
|
+
|
|
27
|
+
constexpr float kTwoPi = 6.283185307179586f;
|
|
28
|
+
|
|
29
|
+
// One component of the curve.
|
|
30
|
+
struct Harmonic {
|
|
31
|
+
float freq; // radians across the full plot width
|
|
32
|
+
float weight; // share of the amplitude
|
|
33
|
+
float drift; // multiple of the base phase speed
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Frequencies deliberately not integer multiples of one another: harmonics
|
|
37
|
+
// would lock into one repeating shape, which is the thing being avoided. The
|
|
38
|
+
// drifts are all multiples of a quarter only so the whole curve has an exact
|
|
39
|
+
// period to wrap the phase clock on — see kPeriodSeconds.
|
|
40
|
+
//
|
|
41
|
+
// Four components, weighted toward the middle of the range, because the curve
|
|
42
|
+
// has to sit convincingly next to candlesticks. A gentler two-or-three-crest
|
|
43
|
+
// undulation suits a smooth live line chart, but against minute-resolution
|
|
44
|
+
// price action it reads as decorative, and it makes the hand-off a real change
|
|
45
|
+
// of shape rather than the same curve coming into focus.
|
|
46
|
+
constexpr Harmonic kHarmonics[] = {
|
|
47
|
+
{14.0f, 0.40f, 1.00f}, // carrier — a little over two crests per screen
|
|
48
|
+
{25.0f, 0.28f, 1.25f}, // ripple, running ahead
|
|
49
|
+
{45.0f, 0.16f, 1.50f}, // fine texture, ahead again
|
|
50
|
+
{6.0f, 0.16f, 0.75f}, // a slow swell, running behind
|
|
51
|
+
};
|
|
52
|
+
constexpr int kHarmonicCount = 4;
|
|
53
|
+
|
|
54
|
+
// Base phase speed. Slow on purpose: this plays under a chart that has nothing
|
|
55
|
+
// to say yet, and anything quicker reads as activity.
|
|
56
|
+
constexpr float kDriftRadPerSecond = 1.0f;
|
|
57
|
+
|
|
58
|
+
// One full cycle of the *whole* curve. The drifts differ by quarters, so every
|
|
59
|
+
// component returns to its starting phase only after four base cycles — which
|
|
60
|
+
// is also why the shape takes ~25s to come back around. Callers wrap elapsed
|
|
61
|
+
// time by this so a chart left loading doesn't lose float precision on the
|
|
62
|
+
// phase, and wrapping here is seamless because it is a true period.
|
|
63
|
+
constexpr float kPeriodSeconds = 4.f * kTwoPi / kDriftRadPerSecond;
|
|
64
|
+
|
|
65
|
+
// Peak displacement from the centreline, as a fraction of the pane height. The
|
|
66
|
+
// components rarely peak together, so the curve typically occupies well under
|
|
67
|
+
// this — it is a bound, not the height it looks.
|
|
68
|
+
constexpr float kAmplitudeFrac = 0.10f;
|
|
69
|
+
|
|
70
|
+
// Breathing opacity. A pulse around full strength, not a dimmer: how quiet the
|
|
71
|
+
// line reads is the *color's* job — it inherits the gridline tone by default —
|
|
72
|
+
// and scaling an already-recessive grey down by a third again would leave the
|
|
73
|
+
// curve indistinguishable from the background.
|
|
74
|
+
constexpr float kBreathMin = 0.75f;
|
|
75
|
+
constexpr float kBreathMax = 1.00f;
|
|
76
|
+
// Ten breaths per curve cycle, so the breath wraps with the phase (~2.5s each)
|
|
77
|
+
// and never syncs with the drift into one combined pulse.
|
|
78
|
+
constexpr float kBreathCyclesPerPeriod = 10.f;
|
|
79
|
+
|
|
80
|
+
// Offset at `x_frac` (0 = left edge of the plot, 1 = right) and `elapsed_s`, in
|
|
81
|
+
// -1 .. 1. The weights sum to 1, so that range is tight. Phase runs as
|
|
82
|
+
// (k*x - w*t), which walks the crests rightward — the direction the series
|
|
83
|
+
// grows. Time outside one period is fine; sin wraps on its own.
|
|
84
|
+
inline float at(float elapsed_s, float x_frac) {
|
|
85
|
+
const float base = kDriftRadPerSecond * elapsed_s;
|
|
86
|
+
float sum = 0.f;
|
|
87
|
+
for (int i = 0; i < kHarmonicCount; ++i) {
|
|
88
|
+
const Harmonic& h = kHarmonics[i];
|
|
89
|
+
sum += h.weight * std::sin(h.freq * x_frac - h.drift * base);
|
|
90
|
+
}
|
|
91
|
+
return sum;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The curve's y as a fraction of the pane height (0 = pane top, 1 = bottom),
|
|
95
|
+
// centred on the pane. This is the form the line capture stores, so a resize
|
|
96
|
+
// mid-morph rescales instead of stranding the curve at old pixels.
|
|
97
|
+
inline float y_frac(float elapsed_s, float x_frac) {
|
|
98
|
+
return 0.5f + kAmplitudeFrac * at(elapsed_s, x_frac);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The line's opacity at `elapsed_s`, in kBreathMin .. kBreathMax.
|
|
102
|
+
inline float breath(float elapsed_s) {
|
|
103
|
+
const float mid = (kBreathMin + kBreathMax) * 0.5f;
|
|
104
|
+
const float half = (kBreathMax - kBreathMin) * 0.5f;
|
|
105
|
+
return mid + half * std::sin(kTwoPi * kBreathCyclesPerPeriod * elapsed_s /
|
|
106
|
+
kPeriodSeconds);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The tightest component in the table above.
|
|
110
|
+
constexpr float max_freq() {
|
|
111
|
+
float m = 0.f;
|
|
112
|
+
for (int i = 0; i < kHarmonicCount; ++i) {
|
|
113
|
+
if (kHarmonics[i].freq > m) m = kHarmonics[i].freq;
|
|
114
|
+
}
|
|
115
|
+
return m;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Vertices per cycle of that component needed to keep it from aliasing into a
|
|
119
|
+
// shape the curve doesn't have. Ten is comfortably past the point where the
|
|
120
|
+
// spline through the samples is indistinguishable from the real curve.
|
|
121
|
+
constexpr float kSamplesPerCycle = 10.f;
|
|
122
|
+
|
|
123
|
+
// How many vertices to sample the curve at across a plot `width_px` wide.
|
|
124
|
+
//
|
|
125
|
+
// The floor comes from the harmonic table rather than a literal, so retuning
|
|
126
|
+
// the curve can't silently undersample it — a narrow chart still gets enough
|
|
127
|
+
// vertices to resolve the finest component, it just spaces them more tightly.
|
|
128
|
+
//
|
|
129
|
+
// Both the idle curve and the morph capture size themselves through here, so
|
|
130
|
+
// the two are always sampled the same way and the hand-off has no shape
|
|
131
|
+
// discontinuity to hide.
|
|
132
|
+
inline int sample_count(float width_px) {
|
|
133
|
+
const int floor_n =
|
|
134
|
+
static_cast<int>(max_freq() / kTwoPi * kSamplesPerCycle) + 1;
|
|
135
|
+
const int n = static_cast<int>(width_px / 8.f);
|
|
136
|
+
const int ceil_n = 4 * floor_n;
|
|
137
|
+
return n < floor_n ? floor_n : (n > ceil_n ? ceil_n : n);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
} // namespace vroom::loading_wave
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
#include "curve.h"
|
|
20
20
|
#include "gradient.h"
|
|
21
21
|
#include "line_morph.h"
|
|
22
|
+
#include "tip_geometry.h"
|
|
22
23
|
#include "tip_pulse.h"
|
|
23
24
|
#include "viewport.h"
|
|
24
25
|
|
|
@@ -129,10 +130,6 @@ inline std::size_t series_slots(std::size_t n, const LineMorph* from,
|
|
|
129
130
|
return std::max(n, vroom::morph_line_count(from, morph_t));
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
// Width of the background-colored ring that separates the tip dot from the line
|
|
133
|
-
// and from the pulse expanding out behind it.
|
|
134
|
-
constexpr float kTipBorderPx = 2.f;
|
|
135
|
-
|
|
136
133
|
// One vertex of the close polyline, in screen space. Slot `k` counts back from
|
|
137
134
|
// the right edge (0 = the newest close), the pairing a timeframe switch
|
|
138
135
|
// preserves.
|
|
@@ -419,6 +416,7 @@ void draw_close_tip(SkCanvas* canvas,
|
|
|
419
416
|
int64_t visible_start_ms,
|
|
420
417
|
int64_t candle_duration_ms,
|
|
421
418
|
float candle_right,
|
|
419
|
+
float clip_right,
|
|
422
420
|
float candle_area_h,
|
|
423
421
|
uint32_t line_color,
|
|
424
422
|
uint32_t bg_color,
|
|
@@ -447,14 +445,21 @@ void draw_close_tip(SkCanvas* canvas,
|
|
|
447
445
|
if (tip.fY < 0.f || tip.fY > candle_area_h) return;
|
|
448
446
|
if (tip.fX < 0.f || tip.fX > candle_right) return;
|
|
449
447
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const float
|
|
453
|
-
const float dot_r = std::max(2.f, w * 1.5f);
|
|
454
|
-
const float border_r = dot_r + kTipBorderPx;
|
|
448
|
+
const auto geo = vroom::tip_geometry::of(line_width);
|
|
449
|
+
const float dot_r = geo.dot_r;
|
|
450
|
+
const float border_r = geo.border_r;
|
|
455
451
|
|
|
456
452
|
canvas->save();
|
|
457
|
-
|
|
453
|
+
// Wider than the on-pane test above: the dot is anchored at the newest
|
|
454
|
+
// candle's center, which on a view pinned to the latest bar is closer to
|
|
455
|
+
// candle_right than the dot's own radius, so it has to reach into the gutter
|
|
456
|
+
// to draw in full. `clip_right` is the gutter's far side — the y-axis
|
|
457
|
+
// strip's edge — which the layout keeps wide enough for the dot (see
|
|
458
|
+
// tip_geometry::gutter_px). The pulse ring is much wider still and clips
|
|
459
|
+
// there; that is intended.
|
|
460
|
+
canvas->clipRect(
|
|
461
|
+
SkRect::MakeLTRB(0.f, 0.f, std::max(candle_right, clip_right),
|
|
462
|
+
candle_area_h));
|
|
458
463
|
|
|
459
464
|
// Ring first: the border paints over its inner edge, so it reads as
|
|
460
465
|
// expanding out from underneath the dot rather than around it.
|
|
@@ -131,6 +131,12 @@ void draw_close_gradient(SkCanvas* canvas,
|
|
|
131
131
|
//
|
|
132
132
|
// `pulse_phase` is in cycles and wraps, so the caller can hand over elapsed time
|
|
133
133
|
// divided by tip_pulse::kPeriodSeconds. Ignored unless `pulse`.
|
|
134
|
+
//
|
|
135
|
+
// `candle_right` bounds where the marked candle may be for the marker to draw at
|
|
136
|
+
// all; `clip_right` bounds the paint, and is wider — the dot overhangs the plot
|
|
137
|
+
// when the newest candle is at the right edge, so it paints into the gutter the
|
|
138
|
+
// layout reserves for it. Callers must therefore draw the tip *after* the axis
|
|
139
|
+
// backgrounds, which mask that gutter.
|
|
134
140
|
void draw_close_tip(SkCanvas* canvas,
|
|
135
141
|
const Layout& lay,
|
|
136
142
|
const PriceBounds& bounds,
|
|
@@ -140,6 +146,7 @@ void draw_close_tip(SkCanvas* canvas,
|
|
|
140
146
|
int64_t visible_start_ms,
|
|
141
147
|
int64_t candle_duration_ms,
|
|
142
148
|
float candle_right,
|
|
149
|
+
float clip_right,
|
|
143
150
|
float candle_area_h,
|
|
144
151
|
uint32_t line_color,
|
|
145
152
|
uint32_t bg_color,
|
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
#include <cstring>
|
|
18
18
|
|
|
19
19
|
#include "chart.h"
|
|
20
|
+
#include "color_lerp.h"
|
|
20
21
|
#include "fonts.h"
|
|
21
22
|
#include "price_format.h"
|
|
23
|
+
#include "price_indicator_anim.h"
|
|
22
24
|
#include "theme.h"
|
|
23
25
|
#include "ticks.h"
|
|
24
26
|
#include "viewport.h"
|
|
@@ -37,17 +39,29 @@ void draw(SkCanvas* canvas,
|
|
|
37
39
|
const Layout& lay,
|
|
38
40
|
const PriceBounds& bounds,
|
|
39
41
|
float candle_right,
|
|
40
|
-
float candle_area_h
|
|
42
|
+
float candle_area_h,
|
|
43
|
+
const vroom::CandleSnapshot* morph_from,
|
|
44
|
+
float morph_t) {
|
|
41
45
|
if (!canvas || chart.candles.empty()) return;
|
|
42
46
|
|
|
43
47
|
// The "current price" is the latest period's close, regardless of whether
|
|
44
|
-
// that candle is horizontally in view.
|
|
48
|
+
// that candle is horizontally in view. Mid-morph it is wherever that close
|
|
49
|
+
// has eased to, so the badge stays on the candle's close edge.
|
|
45
50
|
const ::VroomCandle& last = chart.candles.back();
|
|
46
51
|
const bool bull = last.close >= last.open;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
const auto level = vroom::price_indicator_anim::level_at(
|
|
53
|
+
lay, bounds, last.close, bull, morph_from, chart.morph_from_bounds,
|
|
54
|
+
morph_t);
|
|
55
|
+
|
|
56
|
+
// Blended rather than switched, so a candle that changes direction under a
|
|
57
|
+
// tick carries the indicator's color with it instead of snapping — the same
|
|
58
|
+
// cross-fade the body itself does.
|
|
59
|
+
const SkColor color = static_cast<SkColor>(vroom::lerp_argb(
|
|
60
|
+
chart.theme.colors[bull ? VROOM_COLOR_ACCENT_BEAR : VROOM_COLOR_ACCENT_BULL],
|
|
61
|
+
chart.theme.colors[bull ? VROOM_COLOR_ACCENT_BULL : VROOM_COLOR_ACCENT_BEAR],
|
|
62
|
+
level.bull_t));
|
|
63
|
+
|
|
64
|
+
const float y = level.y;
|
|
51
65
|
if (y < 0.f || y > candle_area_h) return; // price scrolled off-range
|
|
52
66
|
|
|
53
67
|
// Dotted line from the left edge to the y-axis separator.
|
|
@@ -73,7 +87,7 @@ void draw(SkCanvas* canvas,
|
|
|
73
87
|
const vroom::PriceFormat fmt = vroom::with_tick_guard(
|
|
74
88
|
chart.price_fmt,
|
|
75
89
|
vroom::pick_price_interval(bounds.max - bounds.min, candle_area_h));
|
|
76
|
-
vroom::format_price(buf, sizeof(buf),
|
|
90
|
+
vroom::format_price(buf, sizeof(buf), level.price, fmt);
|
|
77
91
|
const size_t len = std::strlen(buf);
|
|
78
92
|
|
|
79
93
|
// Measure the tight glyph bounds (origin at the baseline) so we can center
|
|
@@ -10,6 +10,7 @@ class SkCanvas;
|
|
|
10
10
|
struct VroomChart;
|
|
11
11
|
|
|
12
12
|
namespace vroom {
|
|
13
|
+
struct CandleSnapshot;
|
|
13
14
|
struct Layout;
|
|
14
15
|
struct PriceBounds;
|
|
15
16
|
} // namespace vroom
|
|
@@ -19,11 +20,20 @@ namespace vroom::price_indicator {
|
|
|
19
20
|
// Draws the line across [0, candle_right] at the latest close's y, plus the
|
|
20
21
|
// price box in the y-axis strip. `candle_area_h` is the y of the x-axis
|
|
21
22
|
// separator; the indicator is skipped if the close maps outside [0, candle_area_h].
|
|
23
|
+
//
|
|
24
|
+
// `morph_from` is the newest candle's outgoing capture (slot 0 of
|
|
25
|
+
// VroomChart::morph_from), or null to draw the settled close. When present the
|
|
26
|
+
// level eases across `morph_t` on the same clock as the candle it marks, so the
|
|
27
|
+
// badge tracks the bar instead of jumping ahead of it — see
|
|
28
|
+
// price_indicator_anim.h. Callers must only pass a capture that pairs with the
|
|
29
|
+
// newest candle (tip_anchor.h).
|
|
22
30
|
void draw(SkCanvas* canvas,
|
|
23
31
|
const VroomChart& chart,
|
|
24
32
|
const Layout& lay,
|
|
25
33
|
const PriceBounds& bounds,
|
|
26
34
|
float candle_right,
|
|
27
|
-
float candle_area_h
|
|
35
|
+
float candle_area_h,
|
|
36
|
+
const CandleSnapshot* morph_from = nullptr,
|
|
37
|
+
float morph_t = 1.f);
|
|
28
38
|
|
|
29
39
|
} // namespace vroom::price_indicator
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Where the current-price indicator sits mid-morph (see price_indicator.cpp).
|
|
2
|
+
//
|
|
3
|
+
// The indicator marks the latest close, so every tick moves it. Drawing it
|
|
4
|
+
// straight from that close makes it jump while the candle it belongs to eases
|
|
5
|
+
// into place, which reads as the badge coming loose from the chart. Instead it
|
|
6
|
+
// rides the same capture the candles do: morph_from[0] is the newest candle's
|
|
7
|
+
// outgoing geometry and interval_morph_t is the host's eased clock, so the
|
|
8
|
+
// indicator lands on the candle's close edge on every frame rather than only at
|
|
9
|
+
// the two ends.
|
|
10
|
+
//
|
|
11
|
+
// The y is deliberately the same expression close_vertex uses for slot 0
|
|
12
|
+
// (ma_overlay.cpp). Matching it by construction, rather than by giving the two
|
|
13
|
+
// the same duration, is what keeps the badge glued to the line chart's tip.
|
|
14
|
+
//
|
|
15
|
+
// Skia-free and header-only so the unit tests can cover it; see
|
|
16
|
+
// tests/test_price_indicator_anim.cpp.
|
|
17
|
+
|
|
18
|
+
#pragma once
|
|
19
|
+
|
|
20
|
+
#include <algorithm>
|
|
21
|
+
|
|
22
|
+
#include "viewport.h"
|
|
23
|
+
|
|
24
|
+
namespace vroom::price_indicator_anim {
|
|
25
|
+
|
|
26
|
+
// One frame of the indicator.
|
|
27
|
+
struct Level {
|
|
28
|
+
float y; // pixels, on the candle's close edge
|
|
29
|
+
double price; // what the badge should read
|
|
30
|
+
float bull_t; // 0 = the captured direction's color, 1 = the new one
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
namespace detail {
|
|
34
|
+
inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
|
|
35
|
+
|
|
36
|
+
// The price a captured band fraction stood for. The capture stores fractions so
|
|
37
|
+
// it survives a resize or a rescale, so recovering the price it came from needs
|
|
38
|
+
// the band it was measured against — which is why morph_from_bounds is kept.
|
|
39
|
+
inline double price_at_fraction(const PriceBounds& b, double frac) {
|
|
40
|
+
return b.min + frac * (b.max - b.min);
|
|
41
|
+
}
|
|
42
|
+
} // namespace detail
|
|
43
|
+
|
|
44
|
+
// `from` is the newest candle's capture, or null when nothing is morphing or
|
|
45
|
+
// the capture doesn't pair with the newest candle (panned into history — see
|
|
46
|
+
// tip_anchor.h, which the caller shares the pairing rule with). A null capture
|
|
47
|
+
// gives the settled values, which is what the indicator drew before it
|
|
48
|
+
// animated at all.
|
|
49
|
+
//
|
|
50
|
+
// `from_bounds` is the price band `from` was captured against
|
|
51
|
+
// (VroomChart::morph_from_bounds).
|
|
52
|
+
inline Level level_at(const Layout& lay,
|
|
53
|
+
const PriceBounds& bounds,
|
|
54
|
+
double close_new,
|
|
55
|
+
bool bull_new,
|
|
56
|
+
const CandleSnapshot* from,
|
|
57
|
+
const PriceBounds& from_bounds,
|
|
58
|
+
float morph_t) {
|
|
59
|
+
const float to_y = vroom::price_to_y(lay, bounds, close_new);
|
|
60
|
+
if (!from) return Level{to_y, close_new, 1.f};
|
|
61
|
+
|
|
62
|
+
const float t = std::clamp(morph_t, 0.f, 1.f);
|
|
63
|
+
|
|
64
|
+
// Two different spaces on purpose. The y interpolates in band fractions,
|
|
65
|
+
// because that is the space the capture is in and the space the candles
|
|
66
|
+
// move through — anything else would start the indicator off the pixel the
|
|
67
|
+
// close occupied last frame. The price interpolates in price space, so both
|
|
68
|
+
// ends read exactly the close they belong to even when this tick set a new
|
|
69
|
+
// extreme and rescaled the band underneath. The two agree whenever the band
|
|
70
|
+
// holds still, which is the overwhelmingly common case: y_at_fraction is
|
|
71
|
+
// affine, so a fraction lerp and a price lerp are then the same function.
|
|
72
|
+
const float y =
|
|
73
|
+
detail::lerp(vroom::y_at_fraction(lay, from->close), to_y, t);
|
|
74
|
+
const double close_old =
|
|
75
|
+
detail::price_at_fraction(from_bounds, from->close);
|
|
76
|
+
|
|
77
|
+
return Level{y, close_old + (close_new - close_old) * static_cast<double>(t),
|
|
78
|
+
from->bull == bull_new ? 1.f : t};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
} // namespace vroom::price_indicator_anim
|
package/cpp/_core_src/theme.cpp
CHANGED
|
@@ -23,6 +23,11 @@ constexpr uint32_t kDefaultColors[VROOM_COLOR_COUNT_] = {
|
|
|
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
|
+
// SKELETON — transparent sentinel: inherit GRID, the way BORDER_BULL and
|
|
27
|
+
// the wick colors inherit their fills. The gridlines are already the
|
|
28
|
+
// chart's tone for structure rather than data, which is what the loading
|
|
29
|
+
// line is, so matching them keeps it from being read as a series.
|
|
30
|
+
0x00000000,
|
|
26
31
|
};
|
|
27
32
|
|
|
28
33
|
constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// How big the line chart's tip marker is, and how much gutter it needs.
|
|
2
|
+
//
|
|
3
|
+
// The dot marks the newest close, which on a view pinned to the latest bar sits
|
|
4
|
+
// half a slot from the right edge of the plot — a few pixels. The dot's own
|
|
5
|
+
// radius is larger than that, so it can only be drawn in full if it is allowed
|
|
6
|
+
// to spill into the gutter between the plot and the y-axis strip. That makes the
|
|
7
|
+
// marker's size a layout input, not just a paint detail, so the renderer
|
|
8
|
+
// (draw_close_tip in ma_overlay.cpp) and the layout (VroomChart::layout) read it
|
|
9
|
+
// from here rather than each carrying its own copy.
|
|
10
|
+
//
|
|
11
|
+
// The pulse ring is deliberately not part of this. At its widest it is 3.5x the
|
|
12
|
+
// border radius, and reserving that much gutter would eat the plot; it is
|
|
13
|
+
// allowed to clip against the axis.
|
|
14
|
+
//
|
|
15
|
+
// Skia-free and header-only so the unit tests can cover it; see
|
|
16
|
+
// tests/test_tip_geometry.cpp.
|
|
17
|
+
|
|
18
|
+
#pragma once
|
|
19
|
+
|
|
20
|
+
#include <algorithm>
|
|
21
|
+
|
|
22
|
+
namespace vroom::tip_geometry {
|
|
23
|
+
|
|
24
|
+
// Width of the background-colored ring that separates the tip dot from the line
|
|
25
|
+
// and from the pulse expanding out behind it.
|
|
26
|
+
constexpr float kBorderPx = 2.f;
|
|
27
|
+
|
|
28
|
+
// Gap left between the dot's outer edge and the y-axis strip. The dot touching
|
|
29
|
+
// the price labels reads as a rendering fault even when nothing is clipped.
|
|
30
|
+
constexpr float kClearPx = 4.f;
|
|
31
|
+
|
|
32
|
+
struct Geometry {
|
|
33
|
+
float dot_r; // the filled dot in the line's color
|
|
34
|
+
float border_r; // the dot plus its background-colored halo
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Scaling off the stroke keeps the marker proportionate at any line width; the
|
|
38
|
+
// floor stops a hairline chart from getting an invisible dot.
|
|
39
|
+
inline Geometry of(float line_width) {
|
|
40
|
+
const float w = line_width > 0.f ? line_width : 1.5f;
|
|
41
|
+
const float dot_r = std::max(2.f, w * 1.5f);
|
|
42
|
+
return Geometry{dot_r, dot_r + kBorderPx};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Gutter width that lets the dot draw in full with `kClearPx` to spare.
|
|
46
|
+
//
|
|
47
|
+
// draw_close_tip drops the marker once its center passes the plot's right edge,
|
|
48
|
+
// so the center is at worst flush with that edge and the dot overhangs it by
|
|
49
|
+
// exactly `border_r`. A gutter of that plus the clearance therefore makes "the
|
|
50
|
+
// dot never touches the axis strip" true by construction, whatever the window.
|
|
51
|
+
inline float gutter_px(float line_width) {
|
|
52
|
+
return of(line_width).border_r + kClearPx;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
} // namespace vroom::tip_geometry
|