react-native-vroom-chart 0.6.0 → 0.8.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.
Files changed (49) hide show
  1. package/cpp/VroomChartHostObject.cpp +281 -33
  2. package/cpp/_core_include/vroom/vroom_chart.h +231 -36
  3. package/cpp/_core_src/bollinger.h +1 -1
  4. package/cpp/_core_src/candles.cpp +138 -41
  5. package/cpp/_core_src/candles.h +11 -1
  6. package/cpp/_core_src/chart.cpp +91 -40
  7. package/cpp/_core_src/chart.h +69 -29
  8. package/cpp/_core_src/chart_facade.cpp +247 -69
  9. package/cpp/_core_src/drawings.cpp +147 -4
  10. package/cpp/_core_src/drawings.h +10 -5
  11. package/cpp/_core_src/gradient.cpp +46 -0
  12. package/cpp/_core_src/gradient.h +31 -0
  13. package/cpp/_core_src/labels.cpp +49 -16
  14. package/cpp/_core_src/labels.h +33 -4
  15. package/cpp/_core_src/liquidity.cpp +3 -37
  16. package/cpp/_core_src/ma_overlay.cpp +174 -12
  17. package/cpp/_core_src/ma_overlay.h +55 -3
  18. package/cpp/_core_src/macd.cpp +13 -42
  19. package/cpp/_core_src/macd.h +11 -8
  20. package/cpp/_core_src/macd_pane.cpp +57 -27
  21. package/cpp/_core_src/price_line_layout.h +1 -1
  22. package/cpp/_core_src/rsi.cpp +4 -18
  23. package/cpp/_core_src/rsi.h +6 -6
  24. package/cpp/_core_src/rsi_pane.cpp +34 -19
  25. package/cpp/_core_src/series_ma.cpp +64 -0
  26. package/cpp/_core_src/series_ma.h +30 -0
  27. package/cpp/_core_src/style_inherit.h +35 -0
  28. package/cpp/_core_src/theme.cpp +2 -1
  29. package/cpp/_core_src/viewport.cpp +36 -12
  30. package/cpp/_core_src/viewport.h +50 -0
  31. package/cpp/_core_src/volume.cpp +33 -8
  32. package/cpp/_core_src/volume.h +12 -1
  33. package/cpp/_core_src/volume_anim.cpp +32 -0
  34. package/cpp/_core_src/volume_anim.h +41 -0
  35. package/lib/index.d.mts +206 -31
  36. package/lib/index.d.ts +206 -31
  37. package/lib/index.js +324 -44
  38. package/lib/index.js.map +1 -1
  39. package/lib/index.mjs +329 -52
  40. package/lib/index.mjs.map +1 -1
  41. package/package.json +1 -1
  42. package/src/VroomChart.tsx +100 -17
  43. package/src/dataTransitions.ts +148 -0
  44. package/src/easing.ts +40 -0
  45. package/src/index.ts +9 -0
  46. package/src/jsi.d.ts +126 -19
  47. package/src/theme.ts +1 -0
  48. package/src/types.ts +3 -0
  49. package/src/useChartCore.ts +273 -30
@@ -51,7 +51,7 @@ typedef struct VroomOverlay {
51
51
 
52
52
  // Bollinger Bands overlay drawn on the price pane: a basis MA of `source` over
53
53
  // `period`, banded at ± `mult` × population standard deviation of the same
54
- // window. Per TradingView semantics the stdev always uses the window's
54
+ // window. Per the standard semantics the stdev always uses the window's
55
55
  // arithmetic mean, even when `basis_kind` selects an EMA basis line.
56
56
  typedef struct VroomBollinger {
57
57
  int32_t enabled; // 0/1
@@ -69,6 +69,97 @@ typedef struct VroomBollinger {
69
69
  float fill_opacity; // 0..1, multiplied into upper_color's alpha
70
70
  } VroomBollinger;
71
71
 
72
+ // MACD, drawn in its own pane below the candles: the difference between a fast
73
+ // and a slow moving average of `source`, a signal line smoothing that
74
+ // difference, and a histogram of the gap between the two.
75
+ //
76
+ // The style fields carry an inherit sentinel so an untouched config renders the
77
+ // stock look: a fully transparent color falls back to the theme (histogram) or
78
+ // to the built-in default (lines), and a non-positive width falls back to 1.5.
79
+ // The two `fading` histogram colors — used when a bar moves back toward zero,
80
+ // i.e. momentum is easing — fall back to their base color at half alpha.
81
+ typedef struct VroomMACD {
82
+ int32_t enabled; // 0/1
83
+ int32_t fast; // fast MA length (clamped >= 1; default 12)
84
+ int32_t slow; // slow MA length (forced > fast; default 26)
85
+ int32_t signal; // signal MA length (clamped >= 1; default 9)
86
+ int32_t source; // 0=close,1=open,2=high,3=low,4=hl2,5=hlc3,6=ohlc4
87
+ int32_t ma_kind; // fast/slow legs: 0 = SMA, 1 = EMA
88
+ int32_t signal_ma_kind; // signal line: 0 = SMA, 1 = EMA
89
+
90
+ uint32_t line_color; // MACD line; 0xAARRGGBB, 0 inherits the default blue
91
+ float line_width; // stroke px; <= 0 inherits 1.5
92
+ int32_t line_visible; // 0/1
93
+ uint32_t signal_color; // 0 inherits the default orange
94
+ float signal_width;
95
+ int32_t signal_visible;
96
+
97
+ int32_t hist_visible;
98
+ uint32_t hist_up_color; // 0 inherits VROOM_COLOR_ACCENT_BULL
99
+ uint32_t hist_up_fading_color; // 0 inherits hist_up_color at half alpha
100
+ uint32_t hist_down_color; // 0 inherits VROOM_COLOR_ACCENT_BEAR
101
+ uint32_t hist_down_fading_color; // 0 inherits hist_down_color at half alpha
102
+
103
+ uint32_t zero_color; // 0 inherits the default gray
104
+ int32_t zero_visible; // 0/1
105
+ } VroomMACD;
106
+
107
+ // RSI, drawn in its own pane below the candles: the index line, an optional
108
+ // moving-average trendline over it, and two dashed rules at the overbought and
109
+ // oversold levels. Close-only by design (Wilder's definition), so unlike the
110
+ // other averaged indicators it takes no `source`.
111
+ //
112
+ // The style fields carry the same inherit sentinel as VroomMACD: a fully
113
+ // transparent color falls back to the built-in default and a non-positive width
114
+ // falls back to 1.5.
115
+ typedef struct VroomRSI {
116
+ int32_t enabled; // 0/1
117
+ int32_t period; // lookback in candles (clamped >= 2; default 14)
118
+ double upper_band; // overbought level (0..100; default 70)
119
+ double lower_band; // oversold level (0..100; default 30)
120
+ int32_t ma_period; // trendline length (clamped >= 1; default 14)
121
+ int32_t ma_kind; // trendline: 0 = SMA, 1 = EMA
122
+ int32_t ma_visible; // 0/1
123
+
124
+ uint32_t line_color; // 0 inherits the default violet
125
+ float line_width; // stroke px; <= 0 inherits 1.5
126
+ int32_t line_visible; // 0/1
127
+ uint32_t ma_color; // 0 inherits the default amber
128
+ float ma_width;
129
+ uint32_t band_color; // both dashed rules; 0 inherits the default gray
130
+ int32_t bands_visible; // 0/1
131
+ } VroomRSI;
132
+
133
+ // Session VWAP, drawn as a single line on the price pane. The session resets
134
+ // each UTC day shifted by `reset_offset_min` minutes, and the line lifts its pen
135
+ // at each reset. Color 0 inherits the default cyan; a non-positive width
136
+ // inherits 1.5.
137
+ typedef struct VroomVWAP {
138
+ int32_t enabled; // 0/1
139
+ int32_t reset_offset_min; // session boundary offset from UTC midnight
140
+ uint32_t color; // 0xAARRGGBB; 0 inherits the default
141
+ float width; // stroke px; <= 0 inherits 1.5
142
+ } VroomVWAP;
143
+
144
+ // Volume bars on the price pane, one per candle, drawn under the candles.
145
+ //
146
+ // `height_frac` is a ceiling, not a reservation: raising it lets the tallest bar
147
+ // reach further up over the candles rather than compressing them (matching the
148
+ // conventional volume overlay). Bar heights always auto-fit the loudest volume
149
+ // in view, so the tallest bar sits exactly at the ceiling.
150
+ //
151
+ // The style fields carry an inherit sentinel so the theme keeps supplying them
152
+ // when the consumer doesn't: a negative float or a fully transparent color falls
153
+ // back to the corresponding theme key.
154
+ typedef struct VroomVolume {
155
+ int32_t enabled; // 0/1 — default 1: bars draw unless turned off
156
+ float height_frac; // tallest bar as a fraction of the price pane; < 0 inherits 0.2
157
+ float opacity; // 0..1 (1 = opaque); < 0 inherits VROOM_FLOAT_VOLUME_OPACITY
158
+ float radius_px; // top-corner radius; < 0 inherits VROOM_FLOAT_VOLUME_RADIUS_PX
159
+ uint32_t up_color; // 0xAARRGGBB; 0 inherits VROOM_COLOR_ACCENT_BULL
160
+ uint32_t down_color; // 0xAARRGGBB; 0 inherits VROOM_COLOR_ACCENT_BEAR
161
+ } VroomVolume;
162
+
72
163
  // A drawing anchor in data space (so a drawing tracks the candles on pan/zoom).
73
164
  typedef struct VroomDrawPoint {
74
165
  int64_t time_ms; // epoch milliseconds (not snapped to a candle slot)
@@ -81,17 +172,20 @@ typedef struct VroomDrawPoint {
81
172
  // and `b`; the other two corners (a.x,b.y) and (b.x,a.y) derive.
82
173
  // 2 = pencil — a freehand path through `points` (in draw order). `a`/`b` mirror
83
174
  // the first/last point so bounds and handle code stay uniform.
175
+ // 3 = path — straight segments through `points` (in draw order), ending in
176
+ // an arrowhead on the last one. Same storage as a pencil, but
177
+ // rendered unsmoothed and with every vertex a grab handle.
84
178
  //
85
- // `points`/`point_count` are only read for kind 2; line and box leave them
86
- // null/0. Like the rest of this API the points are copied internally, so the
87
- // caller may free them as soon as the call returns.
179
+ // `points`/`point_count` are only read for kinds 2 and 3; line and box leave
180
+ // them null/0. Like the rest of this API the points are copied internally, so
181
+ // the caller may free them as soon as the call returns.
88
182
  typedef struct VroomDrawing {
89
183
  VroomDrawPoint a;
90
184
  VroomDrawPoint b;
91
185
  uint32_t color; // 0xAARRGGBB
92
186
  float width; // stroke width in px
93
- int32_t kind; // 0 = line, 1 = box, 2 = pencil
94
- const VroomDrawPoint* points; // pencil path (kind 2), else null
187
+ int32_t kind; // 0 = line, 1 = box, 2 = pencil, 3 = path
188
+ const VroomDrawPoint* points; // pencil/path points (kind 2/3), else null
95
189
  int32_t point_count; // number of `points`, else 0
96
190
  } VroomDrawing;
97
191
 
@@ -199,9 +293,25 @@ typedef enum {
199
293
  VROOM_FLOAT_WICK_ROUND_CAP, // 0/1: round the wick end caps
200
294
  VROOM_FLOAT_VOLUME_RADIUS_PX, // volume bar top-corner radius px (0 = square)
201
295
  VROOM_FLOAT_LINE_WIDTH_PX, // line-chart-mode polyline stroke width px
296
+ VROOM_FLOAT_LINE_GRADIENT_OPACITY, // fill under the line at its strongest (0 disables)
202
297
  VROOM_FLOAT_COUNT_
203
298
  } VroomFloatKey;
204
299
 
300
+ // ---- Animation ------------------------------------------------------------
301
+
302
+ // Easing curves for the animations the core paces itself. Mirrors the
303
+ // TransitionEasing union in @vroomchart/types, index for index.
304
+ //
305
+ // Most animations are eased by the host before it hands the core a progress
306
+ // value; this enum exists for the ones the core has to ease internally because
307
+ // each element runs on its own slice of the timeline.
308
+ typedef enum {
309
+ VROOM_EASING_LINEAR = 0,
310
+ VROOM_EASING_IN,
311
+ VROOM_EASING_OUT,
312
+ VROOM_EASING_IN_OUT, // smoothstep; the default for unknown values
313
+ } VroomEasing;
314
+
205
315
  // ---- Callbacks ------------------------------------------------------------
206
316
 
207
317
  typedef struct {
@@ -263,6 +373,45 @@ void vroom_chart_reset_view(VroomChart* chart);
263
373
  // switch) so the price scale re-fits the newly visible candles.
264
374
  void vroom_chart_reset_price_scale(VroomChart* chart);
265
375
 
376
+ // Reads the visible price *envelope* — the min low / max high across the
377
+ // currently visible candles. This is the extent the candles actually occupy, not
378
+ // the axis range (which is wider: see vroom_chart_preserve_price_envelope).
379
+ // Returns false (out params untouched) when no candles are visible. Either out
380
+ // pointer may be null.
381
+ bool vroom_chart_get_visible_price_envelope(VroomChart* chart,
382
+ double* out_low, double* out_high);
383
+
384
+ // "Scale lock" for a same-asset data swap that re-buckets the same price action
385
+ // into a different high-low span (a timeframe switch). Rescales a *manual* price
386
+ // range so the visible envelope keeps the exact pixel height and position that
387
+ // the [prev_low, prev_high] envelope had before the swap — so candles don't
388
+ // suddenly shrink or grow when the interval changes.
389
+ //
390
+ // Call after set_candles + set_visible_range, passing the envelope read by
391
+ // vroom_chart_get_visible_price_envelope *before* the swap.
392
+ //
393
+ // No-op in auto mode: auto-fit already widens the envelope by a fixed factor, so
394
+ // its pixel height is invariant. Falls back to reset_price_scale semantics when
395
+ // either envelope is degenerate.
396
+ void vroom_chart_preserve_price_envelope(VroomChart* chart,
397
+ double prev_low, double prev_high);
398
+
399
+ // Captures the currently visible candle geometry so the next data swap can be
400
+ // animated as a reshape rather than a jump: each candle's wick and body slide
401
+ // and stretch into the shape of its counterpart in the new data.
402
+ //
403
+ // Candles are paired by *slot* — position counting back from the right edge of
404
+ // the visible window, which a timeframe switch preserves. Call before
405
+ // set_candles, then drive vroom_chart_set_interval_morph from 0 to 1.
406
+ // No-op when nothing is visible.
407
+ void vroom_chart_begin_interval_morph(VroomChart* chart);
408
+
409
+ // Advances the interval morph started by vroom_chart_begin_interval_morph. `t`
410
+ // (clamped to 0..1) is the eased progress: 0 renders the captured geometry
411
+ // pixel-identically to the pre-swap frame, 1 renders the new candles and
412
+ // releases the capture. Driven per-frame by the host animation loop.
413
+ void vroom_chart_set_interval_morph(VroomChart* chart, float t);
414
+
266
415
  void vroom_chart_pan(VroomChart* chart, float dx_px, float dy_px);
267
416
  // Directional zoom. scale_x scales the time window around focus_x_px (>1 =
268
417
  // narrower window, wider candles); scale_y scales the price range around
@@ -351,39 +500,51 @@ bool vroom_chart_get_crosshair_info(VroomChart* chart, VroomCrosshairInfo* out);
351
500
 
352
501
  // ---- Indicators -----------------------------------------------------------
353
502
 
354
- // Configures the RSI indicator (rendered in a pane below the candles). `period`
355
- // is the RSI lookback in candle counts (clamped >= 2). `upper_band`/`lower_band`
356
- // are the overbought/oversold reference levels (0..100; default 70/30).
357
- // `ma_enabled` toggles the RSI-based moving-average trendline and `ma_period`
358
- // is its length (clamped >= 1). When enabled, the candle pane shrinks by
359
- // VROOM_FLOAT_INDICATOR_HEIGHT_FRAC.
360
- void vroom_chart_set_rsi(VroomChart* chart, bool enabled, int period,
361
- double upper_band, double lower_band,
362
- bool ma_enabled, int ma_period);
363
-
364
- // Configures the MACD indicator (its own pane below the candles). `fast`/`slow`
365
- // are the EMA lengths (clamped >= 1, slow forced > fast; default 12/26) and
366
- // `signal` is the signal-line EMA length (clamped >= 1; default 9). Panes stack
367
- // in enable order, most recently enabled at the bottom.
368
- void vroom_chart_set_macd(VroomChart* chart, bool enabled, int fast, int slow,
369
- int signal);
503
+ // Configures the RSI indicator (rendered in a pane below the candles). When
504
+ // enabled, the candle pane shrinks by VROOM_FLOAT_INDICATOR_HEIGHT_FRAC. Length
505
+ // and MA-kind changes recompute the series; band levels, colors, widths, and
506
+ // visibility changes only re-render.
507
+ void vroom_chart_set_rsi(VroomChart* chart, const VroomRSI* cfg);
508
+
509
+ // Configures the MACD indicator (its own pane below the candles). Panes stack
510
+ // in enable order, most recently enabled at the bottom. Length, source, and MA
511
+ // kind changes recompute the series; color, width, and visibility changes only
512
+ // re-render.
513
+ void vroom_chart_set_macd(VroomChart* chart, const VroomMACD* cfg);
370
514
 
371
515
  // Replaces the full set of moving-average overlay lines (SMA/EMA) drawn on the
372
516
  // price pane. Pass count 0 to clear them. Overlays don't reserve a pane.
373
517
  void vroom_chart_set_overlays(VroomChart* chart, const VroomOverlay* overlays,
374
518
  size_t count);
375
519
 
376
- // Configures the session VWAP overlay (a single price-pane line). The session
377
- // resets each UTC day shifted by `reset_offset_min` minutes (the configurable
378
- // reset time). `color` is 0xAARRGGBB; `width` is the stroke px.
379
- void vroom_chart_set_vwap(VroomChart* chart, bool enabled, int reset_offset_min,
380
- uint32_t color, float width);
520
+ // Configures the session VWAP overlay (a single price-pane line). Enabled and
521
+ // reset-time changes recompute the series; color and width changes only
522
+ // re-render.
523
+ void vroom_chart_set_vwap(VroomChart* chart, const VroomVWAP* cfg);
381
524
 
382
525
  // Configures the Bollinger Bands overlay (three price-pane lines + an optional
383
526
  // translucent fill between the bands; no pane is reserved). Color/width/fill
384
527
  // changes only re-render; enabled/period/mult/source/basis changes recompute.
385
528
  void vroom_chart_set_bollinger(VroomChart* chart, const VroomBollinger* cfg);
386
529
 
530
+ // Configures the volume bars. Render-only — the bars come straight off each
531
+ // candle's volume, so nothing is recomputed. Bars are enabled by default; pass
532
+ // a config with `enabled` 0 to hide them.
533
+ void vroom_chart_set_volume(VroomChart* chart, const VroomVolume* cfg);
534
+
535
+ // Advances the staggered volume-bar collapse: 0 leaves every bar at full height,
536
+ // 1 has them all flat. Driven per-frame by the host animation loop.
537
+ //
538
+ // `t` is *linear* progress, unlike the other morph setters. Each bar falls over
539
+ // its own slice of the timeline — the tallest starting first and every bar
540
+ // landing at 1 — so `easing` (a VroomEasing) is applied per bar inside the core;
541
+ // pre-easing here would compound the two curves.
542
+ //
543
+ // Symmetric, so driving 1 -> 0 reveals the bars: the shortest rises first and
544
+ // the tallest arrives last. vroom_chart_set_volume snaps this to match its
545
+ // `enabled`, so the host only needs it while animating.
546
+ void vroom_chart_set_volume_collapse(VroomChart* chart, float t, int32_t easing);
547
+
387
548
  // ---- Drawings (line annotations) ------------------------------------------
388
549
 
389
550
  // Replaces the full set of committed line drawings (data-anchored, so they track
@@ -393,18 +554,31 @@ void vroom_chart_set_drawings(VroomChart* chart, const VroomDrawing* drawings,
393
554
  size_t count);
394
555
 
395
556
  // Hit-tests pixel (x_px, y_px) against the committed drawings. On a hit, fills
396
- // *out_index with the drawing index, *out_part with 0 (endpoint A), 1
397
- // (endpoint B), or 2 (line body), *out_t with the 0..1 grab position along the
398
- // segment (A→B; 0/1 for handle hits), and returns true. Endpoint hits are only
399
- // reported for the currently selected drawing (whose handles are visible).
400
- // Returns false on a miss (out params untouched). Any out pointer may be null.
557
+ // *out_index with the drawing index, *out_part with the sub-part that was hit,
558
+ // *out_t with the 0..1 grab position along the segment (A→B; 0/1 for handle
559
+ // hits, 0 for the shapes that have no meaningful t), and returns true. Handle
560
+ // hits are only reported for the currently selected drawing (whose handles are
561
+ // visible). Returns false on a miss (out params untouched). Any out pointer may
562
+ // be null.
563
+ //
564
+ // `out_part` is per-kind (see VroomDrawing.kind):
565
+ // line — 0 (endpoint A), 1 (endpoint B), 2 (body).
566
+ // box — 0..3 (corners, in the order a, (b.x,a.y), b, (a.x,b.y)), 4 (body).
567
+ // pencil — 5 (body; a stroke has no handles).
568
+ // path — 6 (body), or VROOM_DRAW_PART_VERTEX + i for vertex i. The offset
569
+ // keeps a vertex index from colliding with the small part numbers
570
+ // above, and is why a path is capped at VROOM_PATH_MAX_POINTS.
571
+ #define VROOM_DRAW_PART_VERTEX 100
572
+ #define VROOM_PATH_MAX_POINTS 64
401
573
  bool vroom_chart_hit_test_drawing(VroomChart* chart, float x_px, float y_px,
402
574
  int32_t* out_index, int32_t* out_part,
403
575
  float* out_t);
404
576
 
405
- // Selects a committed drawing (renders its endpoint handles). `index` -1 clears
406
- // the selection. `grabbed_endpoint` 0/1 renders that handle 50% larger while it's
407
- // being dragged; -1 for none. An out-of-range index clears the selection.
577
+ // Selects a committed drawing (renders its handles). `index` -1 clears the
578
+ // selection. `grabbed_endpoint` renders that handle 50% larger while it's being
579
+ // dragged (0/1 for a line, 0 for the box corner the host normalized to, the
580
+ // vertex index for a path); -1 for none. An out-of-range index clears the
581
+ // selection.
408
582
  void vroom_chart_set_selected_drawing(VroomChart* chart, int32_t index,
409
583
  int32_t grabbed_endpoint);
410
584
 
@@ -414,8 +588,16 @@ void vroom_chart_move_drawing_endpoint(VroomChart* chart, int32_t index,
414
588
  int32_t endpoint, int64_t time_ms,
415
589
  double price);
416
590
 
591
+ // Moves one vertex of a committed path (kind 3) to a new data-space anchor, for
592
+ // live handle dragging. `vertex` indexes `points`; a/b are re-mirrored onto the
593
+ // first/last point afterwards. No-op for an out-of-range drawing/vertex index or
594
+ // a drawing that isn't a path.
595
+ void vroom_chart_move_drawing_vertex(VroomChart* chart, int32_t index,
596
+ int32_t vertex, int64_t time_ms,
597
+ double price);
598
+
417
599
  // Shifts a whole committed drawing by a *relative* data-space delta — `a`, `b`,
418
- // and (for a pencil) every path point. Used for live body dragging of shapes
600
+ // and (for a pencil or path) every point. Used for live body dragging of shapes
419
601
  // whose points can't be restated cheaply. No-op for an out-of-range index.
420
602
  void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
421
603
  int64_t d_time_ms, double d_price);
@@ -484,6 +666,19 @@ void vroom_chart_start_draft_stroke(VroomChart* chart, uint32_t color,
484
666
  void vroom_chart_append_draft_point(VroomChart* chart, int64_t time_ms,
485
667
  double price);
486
668
 
669
+ // Restates the whole in-progress path (kind 3) draft: `count` vertices placed so
670
+ // far, plus — when `has_cursor` — a rubber-band segment from the last vertex to
671
+ // (cursor_time, cursor_price) carrying the arrow tip. Each placed vertex renders
672
+ // a node dot. A path is placed one deliberate click at a time (so `count` stays
673
+ // small, see VROOM_PATH_MAX_POINTS), which is why this restates rather than
674
+ // appending: the host stays the sole owner of the vertex list, and undoing a
675
+ // point is just a shorter array. Pass count 0 with has_cursor false to show
676
+ // nothing; the points are copied, so the caller may free them on return.
677
+ void vroom_chart_set_draft_path(VroomChart* chart, const VroomDrawPoint* points,
678
+ int32_t count, bool has_cursor,
679
+ int64_t cursor_time, double cursor_price,
680
+ uint32_t color, float width);
681
+
487
682
  // Clears the draft (hides the in-progress node dots / guideline / stroke).
488
683
  void vroom_chart_clear_draft(VroomChart* chart);
489
684
 
@@ -19,7 +19,7 @@ namespace vroom::bollinger {
19
19
  // period window
20
20
  //
21
21
  // The stdev always uses the window's arithmetic mean, even when basis_kind
22
- // selects an EMA basis line (TradingView semantics).
22
+ // selects an EMA basis line (the standard semantics).
23
23
  //
24
24
  // Each output is resized to n; values are NaN for i < period-1 and when
25
25
  // n < period.
@@ -20,6 +20,31 @@ namespace {
20
20
  inline uint32_t resolve_color(uint32_t color, uint32_t fill) {
21
21
  return (color >> 24) == 0 ? fill : color;
22
22
  }
23
+
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
+ inline uint32_t scale_alpha(uint32_t argb, float a) {
37
+ const auto alpha =
38
+ static_cast<uint32_t>(static_cast<float>((argb >> 24) & 0xFFu) * a + 0.5f);
39
+ return (alpha << 24) | (argb & 0x00FFFFFFu);
40
+ }
41
+
42
+ inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
43
+
44
+ // One slot's wick/body extents in pixels.
45
+ struct SlotGeom {
46
+ float x, open, high, low, close;
47
+ };
23
48
  } // namespace
24
49
 
25
50
  void draw(SkCanvas* canvas,
@@ -32,13 +57,23 @@ void draw(SkCanvas* canvas,
32
57
  int64_t visible_start_ms,
33
58
  int64_t candle_duration_ms,
34
59
  float collapse,
35
- float opacity) {
36
- if (!canvas || n == 0) return;
60
+ float opacity,
61
+ const CandleSnapshot* from,
62
+ std::size_t from_n,
63
+ float morph_t) {
64
+ if (!canvas) return;
37
65
 
38
66
  collapse = std::clamp(collapse, 0.f, 1.f);
39
67
  opacity = std::clamp(opacity, 0.f, 1.f);
68
+ morph_t = std::clamp(morph_t, 0.f, 1.f);
40
69
  if (opacity <= 0.f) return;
41
70
 
71
+ // A finished morph drops the capture, reducing the loop below to the plain
72
+ // candle path.
73
+ const std::size_t from_count = vroom::morph_from_count(from, from_n, morph_t);
74
+ const std::size_t slots = std::max(n, from_count);
75
+ if (slots == 0) return;
76
+
42
77
  // During the candle→line morph, fade the entire candle layer as one unit so
43
78
  // overlapping bars/wicks composite cleanly instead of stacking alpha.
44
79
  const bool fade_layer = opacity < 0.999f;
@@ -60,13 +95,20 @@ void draw(SkCanvas* canvas,
60
95
  const bool wick_round = theme.floats[VROOM_FLOAT_WICK_ROUND_CAP] > 0.5f;
61
96
 
62
97
  constexpr float kBorderWidthPx = 1.f;
98
+ const uint32_t wick_color_bull =
99
+ resolve_color(theme.colors[VROOM_COLOR_WICK_BULL], fill_bull);
100
+ const uint32_t wick_color_bear =
101
+ resolve_color(theme.colors[VROOM_COLOR_WICK_BEAR], fill_bear);
102
+ const uint32_t border_color_bull =
103
+ resolve_color(theme.colors[VROOM_COLOR_BORDER_BULL], fill_bull);
104
+ const uint32_t border_color_bear =
105
+ resolve_color(theme.colors[VROOM_COLOR_BORDER_BEAR], fill_bear);
106
+
63
107
  // Draw the border only when it differs from the fill; an inherited/transparent
64
108
  // border color (the default, and the "hide" path) renders nothing — so a hidden
65
109
  // border costs no pixels and never affects candle width.
66
- const bool draw_border_bull =
67
- resolve_color(theme.colors[VROOM_COLOR_BORDER_BULL], fill_bull) != fill_bull;
68
- const bool draw_border_bear =
69
- resolve_color(theme.colors[VROOM_COLOR_BORDER_BEAR], fill_bear) != fill_bear;
110
+ const bool draw_border_bull = border_color_bull != fill_bull;
111
+ const bool draw_border_bear = border_color_bear != fill_bear;
70
112
 
71
113
  SkPaint bull_paint;
72
114
  bull_paint.setAntiAlias(true);
@@ -78,15 +120,13 @@ void draw(SkCanvas* canvas,
78
120
 
79
121
  SkPaint wick_bull;
80
122
  wick_bull.setAntiAlias(true);
81
- wick_bull.setColor(
82
- resolve_color(theme.colors[VROOM_COLOR_WICK_BULL], fill_bull));
123
+ wick_bull.setColor(wick_color_bull);
83
124
  wick_bull.setStrokeWidth(theme.floats[VROOM_FLOAT_WICK_WIDTH_PX]);
84
125
  wick_bull.setStyle(SkPaint::kStroke_Style);
85
126
  wick_bull.setStrokeCap(wick_round ? SkPaint::kRound_Cap : SkPaint::kButt_Cap);
86
127
 
87
128
  SkPaint wick_bear = wick_bull;
88
- wick_bear.setColor(
89
- resolve_color(theme.colors[VROOM_COLOR_WICK_BEAR], fill_bear));
129
+ wick_bear.setColor(wick_color_bear);
90
130
 
91
131
  // 1px body outlines, drawn *inside* the body (see the inset at draw time) so
92
132
  // a contrasting border never widens the candle. Default sentinel resolves to
@@ -95,52 +135,110 @@ void draw(SkCanvas* canvas,
95
135
  border_bull.setAntiAlias(true);
96
136
  border_bull.setStyle(SkPaint::kStroke_Style);
97
137
  border_bull.setStrokeWidth(kBorderWidthPx);
98
- border_bull.setColor(
99
- resolve_color(theme.colors[VROOM_COLOR_BORDER_BULL], fill_bull));
138
+ border_bull.setColor(border_color_bull);
100
139
 
101
140
  SkPaint border_bear = border_bull;
102
- border_bear.setColor(
103
- resolve_color(theme.colors[VROOM_COLOR_BORDER_BEAR], fill_bear));
141
+ border_bear.setColor(border_color_bear);
142
+
143
+ // Scratch paints for the slots that need a per-slot color (see below).
144
+ SkPaint slot_wick, slot_fill, slot_border;
104
145
 
105
146
  const float half_body = body_w * 0.5f;
147
+ const float area_w = vroom::candle_area_width(lay);
106
148
 
107
- for (std::size_t i = 0; i < n; ++i) {
108
- const auto& c = visible[i];
109
- const bool bull = c.close >= c.open;
149
+ // Iterate slots counting back from the right edge — the pairing a timeframe
150
+ // switch preserves descending so the paint order stays oldest-first and
151
+ // touching wicks/borders composite exactly as they do without a morph.
152
+ for (std::size_t k = slots; k-- > 0;) {
153
+ const ::VroomCandle* to = (k < n) ? &visible[n - 1 - k] : nullptr;
154
+ const CandleSnapshot* frm = (k < from_count) ? &from[k] : nullptr;
155
+
156
+ SlotGeom g{};
157
+ bool bull;
158
+ float alpha = 1.f;
159
+
160
+ if (to) {
161
+ const SlotGeom t{
162
+ vroom::candle_center_x(lay, to->time_ms, candle_duration_ms,
163
+ visible_start_ms, window_ms),
164
+ vroom::price_to_y(lay, bounds, to->open),
165
+ vroom::price_to_y(lay, bounds, to->high),
166
+ vroom::price_to_y(lay, bounds, to->low),
167
+ vroom::price_to_y(lay, bounds, to->close),
168
+ };
169
+ bull = to->close >= to->open;
170
+ if (frm) {
171
+ // The snapshot is in band fractions, so it lands on the same
172
+ // pixels it occupied pre-switch even though the bounds changed.
173
+ g.x = lerp(frm->x * area_w, t.x, morph_t);
174
+ g.open = lerp(vroom::y_at_fraction(lay, frm->open), t.open, morph_t);
175
+ g.high = lerp(vroom::y_at_fraction(lay, frm->high), t.high, morph_t);
176
+ g.low = lerp(vroom::y_at_fraction(lay, frm->low), t.low, morph_t);
177
+ g.close =
178
+ lerp(vroom::y_at_fraction(lay, frm->close), t.close, morph_t);
179
+ } else {
180
+ g = t;
181
+ // A slot with no counterpart: fade in rather than pop.
182
+ if (from_count > 0) alpha = morph_t;
183
+ }
184
+ } else {
185
+ g = {frm->x * area_w,
186
+ vroom::y_at_fraction(lay, frm->open),
187
+ vroom::y_at_fraction(lay, frm->high),
188
+ vroom::y_at_fraction(lay, frm->low),
189
+ vroom::y_at_fraction(lay, frm->close)};
190
+ bull = frm->bull;
191
+ alpha = 1.f - morph_t; // the new data dropped this slot
192
+ }
193
+ if (alpha <= 0.f) continue;
110
194
 
111
- const float cx = vroom::candle_center_x(
112
- lay, c.time_ms, candle_duration_ms,
113
- visible_start_ms, window_ms);
114
- const float y_close = vroom::price_to_y(lay, bounds, c.close);
115
195
  // Collapse high/low/open toward the close so the candle folds into its
116
196
  // close point (the line vertex) as `collapse` → 1.
117
- const float y_high =
118
- vroom::price_to_y(lay, bounds, c.high) * (1.f - collapse) +
119
- y_close * collapse;
120
- const float y_low =
121
- vroom::price_to_y(lay, bounds, c.low) * (1.f - collapse) +
122
- y_close * collapse;
123
- const float y_open =
124
- vroom::price_to_y(lay, bounds, c.open) * (1.f - collapse) +
125
- y_close * collapse;
126
-
127
- canvas->drawLine(cx, y_high, cx, y_low,
128
- bull ? wick_bull : wick_bear);
197
+ const float y_close = g.close;
198
+ const float y_high = g.high * (1.f - collapse) + y_close * collapse;
199
+ const float y_low = g.low * (1.f - collapse) + y_close * collapse;
200
+ const float y_open = g.open * (1.f - collapse) + y_close * collapse;
201
+
202
+ // Fast path: the prebuilt bull/bear paints. A faded slot and a candle
203
+ // that changes direction mid-morph both need per-slot colors.
204
+ const SkPaint* wick_p = bull ? &wick_bull : &wick_bear;
205
+ const SkPaint* fill_p = bull ? &bull_paint : &bear_paint;
206
+ const SkPaint* border_p = bull ? &border_bull : &border_bear;
207
+ bool draw_border = bull ? draw_border_bull : draw_border_bear;
208
+
209
+ const bool flip = frm && to && frm->bull != bull;
210
+ if (flip || alpha < 0.999f) {
211
+ const auto blend = [&](uint32_t c_bull, uint32_t c_bear) {
212
+ uint32_t c = bull ? c_bull : c_bear;
213
+ if (flip) c = lerp_argb(bull ? c_bear : c_bull, c, morph_t);
214
+ return scale_alpha(c, alpha);
215
+ };
216
+ slot_wick = *wick_p;
217
+ slot_wick.setColor(blend(wick_color_bull, wick_color_bear));
218
+ slot_fill = *fill_p;
219
+ slot_fill.setColor(blend(fill_bull, fill_bear));
220
+ slot_border = *border_p;
221
+ slot_border.setColor(blend(border_color_bull, border_color_bear));
222
+ wick_p = &slot_wick;
223
+ fill_p = &slot_fill;
224
+ border_p = &slot_border;
225
+ if (flip) draw_border = draw_border_bull || draw_border_bear;
226
+ }
227
+
228
+ canvas->drawLine(g.x, y_high, g.x, y_low, *wick_p);
129
229
 
130
230
  const float y_top = std::min(y_open, y_close);
131
231
  const float y_bot = std::max(y_open, y_close);
132
232
  const float h = std::max(1.f, y_bot - y_top);
133
- const SkRect body = SkRect::MakeXYWH(cx - half_body, y_top, body_w, h);
233
+ const SkRect body = SkRect::MakeXYWH(g.x - half_body, y_top, body_w, h);
134
234
  // Corner radius clamped so thin candles / short bodies don't over-round.
135
235
  const float r = std::min({body_r, body_w * 0.5f, h * 0.5f});
136
- const SkPaint& fill = bull ? bull_paint : bear_paint;
137
236
  if (r > 0.f) {
138
- canvas->drawRoundRect(body, r, r, fill);
237
+ canvas->drawRoundRect(body, r, r, *fill_p);
139
238
  } else {
140
- canvas->drawRect(body, fill);
239
+ canvas->drawRect(body, *fill_p);
141
240
  }
142
241
 
143
- const bool draw_border = bull ? draw_border_bull : draw_border_bear;
144
242
  // Inset by half the stroke so the centered stroke falls fully within the
145
243
  // body: the border overlaps the fill's outer edge instead of straddling
146
244
  // it, keeping the candle's footprint exactly body_w. Skip when the body is
@@ -148,13 +246,12 @@ void draw(SkCanvas* canvas,
148
246
  if (draw_border && body_w > kBorderWidthPx && h > kBorderWidthPx) {
149
247
  const SkRect inner =
150
248
  body.makeInset(kBorderWidthPx * 0.5f, kBorderWidthPx * 0.5f);
151
- const SkPaint& bp = bull ? border_bull : border_bear;
152
249
  // Concentric radius so the inside border follows the rounded body.
153
250
  const float ir = std::max(0.f, r - kBorderWidthPx * 0.5f);
154
251
  if (ir > 0.f) {
155
- canvas->drawRoundRect(inner, ir, ir, bp);
252
+ canvas->drawRoundRect(inner, ir, ir, *border_p);
156
253
  } else {
157
- canvas->drawRect(inner, bp);
254
+ canvas->drawRect(inner, *border_p);
158
255
  }
159
256
  }
160
257
  }
@@ -12,6 +12,7 @@
12
12
  class SkCanvas;
13
13
 
14
14
  namespace vroom {
15
+ struct CandleSnapshot;
15
16
  struct Layout;
16
17
  struct PriceBounds;
17
18
  struct Theme;
@@ -26,6 +27,12 @@ namespace vroom::candles {
26
27
  // candle→line transition: 0 = normal candle, 1 = a flat point at the close (the
27
28
  // body/wick heights and width shrink to the line). `opacity` (0..1) fades the
28
29
  // whole candle layer out as the line fades in. Both default to a no-op.
30
+ //
31
+ // `from` / `from_n` is the outgoing geometry of an interval morph, indexed from
32
+ // the right of the visible slice (slot 0 = newest), and `morph_t` (0..1) is the
33
+ // eased progress toward `visible`. Each slot's wick and body interpolate between
34
+ // the two; slots present on only one side fade in or out. `morph_t == 1` (the
35
+ // default) draws `visible` alone.
29
36
  void draw(SkCanvas* canvas,
30
37
  const ::VroomCandle* visible,
31
38
  std::size_t n,
@@ -36,6 +43,9 @@ void draw(SkCanvas* canvas,
36
43
  int64_t visible_start_ms,
37
44
  int64_t candle_duration_ms,
38
45
  float collapse = 0.f,
39
- float opacity = 1.f);
46
+ float opacity = 1.f,
47
+ const CandleSnapshot* from = nullptr,
48
+ std::size_t from_n = 0,
49
+ float morph_t = 1.f);
40
50
 
41
51
  } // namespace vroom::candles