react-native-vroom-chart 0.13.0 → 0.14.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.
@@ -178,160 +178,274 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
178
178
  // 4.35. Price-series morph state, shared by the gradient fill below and the
179
179
  // series itself (5). `morph_fade` crossfades candles→line and
180
180
  // `morph_collapse` folds each candle toward its close (the line vertex);
181
- // fade 0 = pure candles, fade 1 = pure line. An interval morph
182
- // additionally reshapes each slot from the geometry it held before the
183
- // timeframe switch (see `morph_from`) candle bodies, line vertices and
184
- // the fill alike, so every layer stays in step mid-crossfade.
181
+ // fade 0 = pure candles, fade 1 = pure line.
182
+ //
183
+ // An interval *transform* reshapes each slot from the geometry it held
184
+ // before the timeframe switch (see `morph_from`). A host-chosen
185
+ // *fade* skips the pairing and uses the same envelope as the axes
186
+ // (`labels::interval_phase`): outgoing snapshot out, then new scene in.
185
187
  const float fade = morph_fade;
186
188
  const float collapse = morph_collapse;
187
189
  const bool morphing = interval_morph_t < 1.f && !morph_from.empty();
188
- const vroom::CandleSnapshot* morph_src = morphing ? morph_from.data() : nullptr;
189
190
  const std::size_t morph_n = morphing ? morph_from.size() : 0;
190
- const float morph_t = morphing ? interval_morph_t : 1.f;
191
-
192
- // 4.4. Line-mode gradient fill the wash under the close polyline. Behind the
193
- // volume bars so they stay legible on top of it, which puts it at the
194
- // back of the price pane's content.
195
- if (fade > 0.f) {
196
- vroom::ma_overlay::draw_close_gradient(
197
- canvas, lay, bounds, visible, n, window_ms,
198
- visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
199
- theme.colors[VROOM_COLOR_LINE],
200
- theme.floats[VROOM_FLOAT_LINE_GRADIENT_OPACITY],
201
- fade, morph_src, morph_n, morph_t,
202
- theme.floats[VROOM_FLOAT_LINE_TENSION]);
203
- }
191
+ const bool fade_swap = morphing && interval_morph_fade;
192
+ const bool fade_out = fade_swap && axis_phase.outgoing;
193
+ const bool fade_in = fade_swap && !axis_phase.outgoing;
194
+ const bool reshape = morphing && !fade_swap;
195
+ const vroom::CandleSnapshot* morph_src = reshape ? morph_from.data() : nullptr;
196
+ const std::size_t morph_n_draw = reshape ? morph_n : 0;
197
+ const float morph_t = reshape ? interval_morph_t : 1.f;
198
+
199
+ if (fade_out) {
200
+ // First half: only the captured series, at the axis envelope opacity.
201
+ // Layers with no snapshot (volume, overlays, indicators, drawings)
202
+ // would already be the new data, so they stay hidden until the incoming
203
+ // half. morph_t = 0 draws the capture pixel-identically.
204
+ const float layer = axis_phase.opacity;
205
+ if (layer > 0.f) {
206
+ if (fade > 0.f) {
207
+ vroom::ma_overlay::draw_close_gradient(
208
+ canvas, lay, bounds, visible, 0, window_ms,
209
+ visible_start_ms, candle_duration_ms, candle_right,
210
+ candle_area_h, theme.colors[VROOM_COLOR_LINE],
211
+ theme.floats[VROOM_FLOAT_LINE_GRADIENT_OPACITY],
212
+ fade * layer, morph_from.data(), morph_n, 0.f,
213
+ theme.floats[VROOM_FLOAT_LINE_TENSION]);
214
+ }
215
+ if (fade < 1.f) {
216
+ vroom::candles::draw(canvas, visible, 0, lay, theme, bounds,
217
+ window_ms, visible_start_ms,
218
+ candle_duration_ms, collapse,
219
+ (1.f - fade) * layer, morph_from.data(),
220
+ morph_n, 0.f);
221
+ }
222
+ if (fade > 0.f) {
223
+ vroom::ma_overlay::draw_close_line(
224
+ canvas, lay, bounds, visible, 0, window_ms,
225
+ visible_start_ms, candle_duration_ms, candle_right,
226
+ candle_area_h, theme.colors[VROOM_COLOR_LINE],
227
+ theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade * layer,
228
+ morph_from.data(), morph_n, 0.f,
229
+ theme.floats[VROOM_FLOAT_LINE_TENSION]);
230
+ if (theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f) {
231
+ vroom::ma_overlay::draw_close_tip(
232
+ canvas, lay, bounds, visible, 0, window_ms,
233
+ visible_start_ms, candle_duration_ms, candle_right,
234
+ candle_area_h, theme.colors[VROOM_COLOR_LINE],
235
+ theme.colors[VROOM_COLOR_BACKGROUND],
236
+ theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade * layer,
237
+ theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
238
+ tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds,
239
+ morph_from.data(), morph_n, 0.f);
240
+ }
241
+ }
242
+ }
243
+ } else {
244
+ // Incoming fade: the new scene at envelope opacity, no slot lerp
245
+ // (`morph_t` is already 1). Transforms use the reshape path above.
246
+ const bool wrap = fade_in && axis_phase.opacity > 0.f &&
247
+ axis_phase.opacity < 0.999f;
248
+ if (wrap) {
249
+ canvas->saveLayerAlpha(
250
+ nullptr, static_cast<U8CPU>(axis_phase.opacity * 255.f + 0.5f));
251
+ }
252
+ const bool draw_new = !fade_in || axis_phase.opacity > 0.f;
253
+
254
+ if (draw_new) {
255
+ // 4.4. Line-mode gradient fill — the wash under the close polyline.
256
+ if (fade > 0.f) {
257
+ vroom::ma_overlay::draw_close_gradient(
258
+ canvas, lay, bounds, visible, n, window_ms,
259
+ visible_start_ms, candle_duration_ms, candle_right,
260
+ candle_area_h, theme.colors[VROOM_COLOR_LINE],
261
+ theme.floats[VROOM_FLOAT_LINE_GRADIENT_OPACITY], fade,
262
+ morph_src, morph_n_draw, morph_t,
263
+ theme.floats[VROOM_FLOAT_LINE_TENSION]);
264
+ }
204
265
 
205
- // 4.5. Volume bars — drawn under the candles so candles z-index above.
206
- // A fully collapsed chart has no bars left to draw, which is also how
207
- // "volume disabled" is represented (set_volume snaps the scalar).
208
- if (volume_collapse_t < 1.f) {
209
- vroom::volume::draw(canvas, visible, n, lay, theme, volume,
210
- volume_collapse_t, volume_collapse_easing,
211
- window_ms, visible_start_ms, candle_duration_ms);
212
- }
266
+ // 4.5. Volume bars — drawn under the candles so candles z-index above.
267
+ if (volume_collapse_t < 1.f) {
268
+ vroom::volume::draw(canvas, visible, n, lay, theme, volume,
269
+ volume_collapse_t, volume_collapse_easing,
270
+ window_ms, visible_start_ms,
271
+ candle_duration_ms);
272
+ }
213
273
 
214
- // 4.6. Liquidity bands (resting-order depth) — behind the candles so the
215
- // candle bodies paint over the volume-driven tint. Anchored in price
216
- // space, so they scale with the y-axis.
217
- vroom::liquidity::draw(canvas, *this, lay, bounds, candle_right,
218
- candle_area_h);
219
-
220
- // 4.7. Bollinger Band fill — the translucent region between the upper and
221
- // lower bands, behind the candles so their bull/bear colors stay
222
- // untinted. The band lines themselves draw above the candles (5.65).
223
- if (bollinger.enabled) {
224
- ensure_bollinger();
225
- if (bollinger.fill_enabled &&
226
- bb_upper_cache.size() == candles.size() &&
227
- bb_lower_cache.size() == candles.size()) {
228
- vroom::ma_overlay::fill_between(
229
- canvas, lay, bounds, visible, n,
230
- bb_upper_cache.data() + range.start,
231
- bb_lower_cache.data() + range.start, window_ms,
232
- visible_start_ms, candle_duration_ms, candle_right,
233
- candle_area_h, bollinger.upper_color, bollinger.fill_opacity);
234
- }
235
- }
274
+ // 4.6. Liquidity bands (resting-order depth).
275
+ vroom::liquidity::draw(canvas, *this, lay, bounds, candle_right,
276
+ candle_area_h);
277
+
278
+ // 4.7. Bollinger Band fill.
279
+ if (bollinger.enabled) {
280
+ ensure_bollinger();
281
+ if (bollinger.fill_enabled &&
282
+ bb_upper_cache.size() == candles.size() &&
283
+ bb_lower_cache.size() == candles.size()) {
284
+ vroom::ma_overlay::fill_between(
285
+ canvas, lay, bounds, visible, n,
286
+ bb_upper_cache.data() + range.start,
287
+ bb_lower_cache.data() + range.start, window_ms,
288
+ visible_start_ms, candle_duration_ms, candle_right,
289
+ candle_area_h, bollinger.upper_color,
290
+ bollinger.fill_opacity);
291
+ }
292
+ }
236
293
 
237
- // 5. Price series — candles, a close-price line, or a blend of the two during
238
- // the candle↔line morph (state hoisted to 4.35 for the gradient fill). The
239
- // line is styled by theme.LINE.
240
- if (fade < 1.f) {
241
- vroom::candles::draw(canvas, visible, n, lay, theme, bounds, window_ms,
242
- visible_start_ms, candle_duration_ms, collapse,
243
- 1.f - fade, morph_src, morph_n, morph_t);
244
- }
245
- if (fade > 0.f) {
246
- vroom::ma_overlay::draw_close_line(
247
- canvas, lay, bounds, visible, n, window_ms,
248
- visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
249
- theme.colors[VROOM_COLOR_LINE], theme.floats[VROOM_FLOAT_LINE_WIDTH_PX],
250
- fade, morph_src, morph_n, morph_t,
251
- theme.floats[VROOM_FLOAT_LINE_TENSION]);
252
- }
294
+ // 5. Price series — candles, a close-price line, or a blend.
295
+ if (fade < 1.f) {
296
+ vroom::candles::draw(canvas, visible, n, lay, theme, bounds,
297
+ window_ms, visible_start_ms,
298
+ candle_duration_ms, collapse, 1.f - fade,
299
+ morph_src, morph_n_draw, morph_t);
300
+ }
301
+ if (fade > 0.f) {
302
+ vroom::ma_overlay::draw_close_line(
303
+ canvas, lay, bounds, visible, n, window_ms,
304
+ visible_start_ms, candle_duration_ms, candle_right,
305
+ candle_area_h, theme.colors[VROOM_COLOR_LINE],
306
+ theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade, morph_src,
307
+ morph_n_draw, morph_t,
308
+ theme.floats[VROOM_FLOAT_LINE_TENSION]);
309
+ }
253
310
 
254
- // 5.5. Moving-average overlay lines (SMA/EMA) on the price pane, over the
255
- // candles. They share the candle price scale and don't reserve a pane.
256
- if (!overlays.empty()) {
257
- ensure_overlays();
258
- for (std::size_t k = 0; k < overlays.size(); ++k) {
259
- if (overlay_caches[k].size() != candles.size()) continue;
260
- const double* vis = overlay_caches[k].data() + range.start;
261
- vroom::ma_overlay::draw(canvas, lay, bounds, visible, n, vis,
262
- window_ms, visible_start_ms,
263
- candle_duration_ms, candle_right,
264
- candle_area_h, overlays[k].color,
265
- overlays[k].width);
266
- }
267
- }
311
+ // 5.5. Moving-average overlay lines.
312
+ if (!overlays.empty()) {
313
+ ensure_overlays();
314
+ for (std::size_t k = 0; k < overlays.size(); ++k) {
315
+ if (overlay_caches[k].size() != candles.size()) continue;
316
+ const double* vis = overlay_caches[k].data() + range.start;
317
+ vroom::ma_overlay::draw(
318
+ canvas, lay, bounds, visible, n, vis, window_ms,
319
+ visible_start_ms, candle_duration_ms, candle_right,
320
+ candle_area_h, overlays[k].color, overlays[k].width);
321
+ }
322
+ }
268
323
 
269
- // 5.6. VWAP overlay (session, configurable reset) — a single price-pane line
270
- // that breaks at each session reset (vwap_breaks).
271
- if (vwap.enabled) {
272
- ensure_vwap();
273
- if (vwap_cache.size() == candles.size()) {
274
- const double* vis = vwap_cache.data() + range.start;
275
- const unsigned char* brk = vwap_breaks.size() == candles.size()
276
- ? vwap_breaks.data() + range.start
277
- : nullptr;
278
- vroom::ma_overlay::draw(canvas, lay, bounds, visible, n, vis,
279
- window_ms, visible_start_ms,
280
- candle_duration_ms, candle_right,
281
- candle_area_h,
282
- vroom::style::color_or(vwap.color, kVwapLine),
283
- vroom::style::width_or(vwap.width, 1.5f),
284
- brk);
285
- }
286
- }
324
+ // 5.6. VWAP overlay.
325
+ if (vwap.enabled) {
326
+ ensure_vwap();
327
+ if (vwap_cache.size() == candles.size()) {
328
+ const double* vis = vwap_cache.data() + range.start;
329
+ const unsigned char* brk =
330
+ vwap_breaks.size() == candles.size()
331
+ ? vwap_breaks.data() + range.start
332
+ : nullptr;
333
+ vroom::ma_overlay::draw(
334
+ canvas, lay, bounds, visible, n, vis, window_ms,
335
+ visible_start_ms, candle_duration_ms, candle_right,
336
+ candle_area_h,
337
+ vroom::style::color_or(vwap.color, kVwapLine),
338
+ vroom::style::width_or(vwap.width, 1.5f), brk);
339
+ }
340
+ }
287
341
 
288
- // 5.65. Bollinger Band lines — upper, lower, then the basis last so it
289
- // reads on top where the bands pinch. Same price scale as the
290
- // candles; the fill went down in 4.7.
291
- if (bollinger.enabled) {
292
- ensure_bollinger();
293
- const std::size_t sz = candles.size();
294
- if (bb_upper_cache.size() == sz && bb_lower_cache.size() == sz &&
295
- bb_middle_cache.size() == sz) {
296
- const auto stroke = [&](const std::vector<double>& cache,
297
- uint32_t color, float width) {
298
- vroom::ma_overlay::draw(canvas, lay, bounds, visible, n,
299
- cache.data() + range.start, window_ms,
300
- visible_start_ms, candle_duration_ms,
301
- candle_right, candle_area_h, color,
302
- width);
303
- };
304
- stroke(bb_upper_cache, bollinger.upper_color, bollinger.upper_width);
305
- stroke(bb_lower_cache, bollinger.lower_color, bollinger.lower_width);
306
- stroke(bb_middle_cache, bollinger.middle_color, bollinger.middle_width);
307
- }
308
- }
342
+ // 5.65. Bollinger Band lines.
343
+ if (bollinger.enabled) {
344
+ ensure_bollinger();
345
+ const std::size_t sz = candles.size();
346
+ if (bb_upper_cache.size() == sz && bb_lower_cache.size() == sz &&
347
+ bb_middle_cache.size() == sz) {
348
+ const auto stroke = [&](const std::vector<double>& cache,
349
+ uint32_t color, float width) {
350
+ vroom::ma_overlay::draw(
351
+ canvas, lay, bounds, visible, n,
352
+ cache.data() + range.start, window_ms,
353
+ visible_start_ms, candle_duration_ms, candle_right,
354
+ candle_area_h, color, width);
355
+ };
356
+ stroke(bb_upper_cache, bollinger.upper_color,
357
+ bollinger.upper_width);
358
+ stroke(bb_lower_cache, bollinger.lower_color,
359
+ bollinger.lower_width);
360
+ stroke(bb_middle_cache, bollinger.middle_color,
361
+ bollinger.middle_width);
362
+ }
363
+ }
309
364
 
310
- // 5.7. Drawing annotations (committed line tools + the in-progress draft).
311
- // On the price pane above the candles/overlays, below the axis labels.
312
- vroom::drawings::draw(canvas, *this, lay, bounds, candle_right,
313
- candle_area_h);
365
+ // 5.7. Drawing annotations.
366
+ vroom::drawings::draw(canvas, *this, lay, bounds, candle_right,
367
+ candle_area_h);
368
+
369
+ // 5.8. Line-mode tip marker.
370
+ if (fade > 0.f && theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f) {
371
+ const auto anchor = vroom::tip_anchor::at(
372
+ range.start, range.end, candles.size(), reshape);
373
+ vroom::ma_overlay::draw_close_tip(
374
+ canvas, lay, bounds, visible, anchor.slot_count, window_ms,
375
+ visible_start_ms, candle_duration_ms, candle_right,
376
+ candle_area_h, theme.colors[VROOM_COLOR_LINE],
377
+ theme.colors[VROOM_COLOR_BACKGROUND],
378
+ theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade,
379
+ theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
380
+ tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds,
381
+ anchor.use_morph ? morph_src : nullptr,
382
+ anchor.use_morph ? morph_n_draw : 0, morph_t);
383
+ }
314
384
 
315
- // 5.8. Line-mode tip marker the dot (and optional pulse) at the newest
316
- // close. Above the overlays so it stays the eye's anchor, but before the
317
- // axis masks, which trim the ring at the price scale.
318
- //
319
- // Unlike every layer above, this one anchors to the newest candle in
320
- // the series rather than the newest on screen, so panning into history
321
- // carries the dot off the right edge with its candle (tip_anchor.h).
322
- if (fade > 0.f && theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f) {
323
- const auto anchor = vroom::tip_anchor::at(range.start, range.end,
324
- candles.size(), morphing);
325
- vroom::ma_overlay::draw_close_tip(
326
- canvas, lay, bounds, visible, anchor.slot_count, window_ms,
327
- visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
328
- theme.colors[VROOM_COLOR_LINE],
329
- theme.colors[VROOM_COLOR_BACKGROUND],
330
- theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade,
331
- theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
332
- tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds,
333
- anchor.use_morph ? morph_src : nullptr,
334
- anchor.use_morph ? morph_n : 0, morph_t);
385
+ // Incoming fade: indicator panes share the scene opacity (and
386
+ // paint before axis masks so overflow still clips). Transforms
387
+ // keep the settled z-order after the price indicator.
388
+ if (fade_in && lay.indicator_area_h > 0.f) {
389
+ struct ActivePane {
390
+ int order;
391
+ int type;
392
+ };
393
+ ActivePane panes[2];
394
+ int count = 0;
395
+ if (rsi.enabled) panes[count++] = {rsi_order, 0};
396
+ if (macd.enabled) panes[count++] = {macd_order, 1};
397
+ if (count == 2 && panes[0].order > panes[1].order) {
398
+ const ActivePane tmp = panes[0];
399
+ panes[0] = panes[1];
400
+ panes[1] = tmp;
401
+ }
402
+
403
+ const float pane_h =
404
+ height_px * theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
405
+ float pane_top = candle_area_h;
406
+ for (int i = 0; i < count; ++i) {
407
+ const float pane_bottom = pane_top + pane_h;
408
+ if (panes[i].type == 0) {
409
+ ensure_rsi();
410
+ const double* rsi_vis =
411
+ rsi_cache.size() == candles.size()
412
+ ? rsi_cache.data() + range.start
413
+ : nullptr;
414
+ const double* rsi_ma_vis =
415
+ (rsi.ma_visible &&
416
+ rsi_ma_cache.size() == candles.size())
417
+ ? rsi_ma_cache.data() + range.start
418
+ : nullptr;
419
+ vroom::rsi_pane::draw(
420
+ canvas, *this, lay, visible, n, rsi_vis, rsi_ma_vis,
421
+ window_ms, visible_start_ms, candle_duration_ms,
422
+ candle_right, pane_top, pane_bottom);
423
+ } else {
424
+ ensure_macd();
425
+ const double* macd_vis =
426
+ macd_cache.size() == candles.size()
427
+ ? macd_cache.data() + range.start
428
+ : nullptr;
429
+ const double* sig_vis =
430
+ macd_signal_cache.size() == candles.size()
431
+ ? macd_signal_cache.data() + range.start
432
+ : nullptr;
433
+ const double* hist_vis =
434
+ macd_hist_cache.size() == candles.size()
435
+ ? macd_hist_cache.data() + range.start
436
+ : nullptr;
437
+ vroom::macd_pane::draw(
438
+ canvas, *this, lay, visible, n, macd_vis, sig_vis,
439
+ hist_vis, window_ms, visible_start_ms,
440
+ candle_duration_ms, candle_right, pane_top,
441
+ pane_bottom);
442
+ }
443
+ pane_top = pane_bottom;
444
+ }
445
+ }
446
+ }
447
+
448
+ if (wrap) canvas->restore();
335
449
  }
336
450
 
337
451
  // 6. Axis backgrounds (mask any candle overflow). The x-axis separator
@@ -354,20 +468,25 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
354
468
 
355
469
  // 7.5. Current-price line + box — above labels so the box covers any label
356
470
  // it overlaps; tracks the latest close as the price scale moves.
357
- vroom::price_indicator::draw(canvas, *this, lay, bounds,
358
- candle_right, candle_area_h);
359
-
360
- // 7.55. Consumer-supplied price status lines — same tier as the current-price
361
- // indicator (their badges must cover the labels underneath), but after
362
- // it so a resting order at the last close stays readable.
363
- vroom::price_lines::draw(canvas, *this, lay, bounds, candle_right,
364
- candle_area_h);
471
+ // Hidden during a fade's outgoing half: the close is already the new
472
+ // series and there is no snapshot to fade.
473
+ if (!fade_out) {
474
+ vroom::price_indicator::draw(canvas, *this, lay, bounds,
475
+ candle_right, candle_area_h);
476
+
477
+ // 7.55. Consumer-supplied price status lines — same tier as the
478
+ // current-price indicator (their badges must cover the labels
479
+ // underneath), but after it so a resting order at the last close
480
+ // stays readable.
481
+ vroom::price_lines::draw(canvas, *this, lay, bounds, candle_right,
482
+ candle_area_h);
483
+ }
365
484
 
366
485
  // 7.6. Indicator panes stacked below the candles, ordered by enable
367
486
  // sequence (most recently enabled at the bottom). Each pane is
368
487
  // INDICATOR_HEIGHT_FRAC of the height; the candle pane already shrank
369
- // to fit them (see layout()).
370
- if (lay.indicator_area_h > 0.f) {
488
+ // to fit them (see layout()). A fade already drew them with the scene.
489
+ if (!fade_swap && lay.indicator_area_h > 0.f) {
371
490
  struct ActivePane { int order; int type; }; // type: 0 = RSI, 1 = MACD
372
491
  ActivePane panes[2];
373
492
  int count = 0;
@@ -73,11 +73,14 @@ struct VroomChart {
73
73
 
74
74
  // Interval morph: the outgoing candle geometry captured when a timeframe
75
75
  // switch begins, indexed from the right of the visible slice (slot 0 =
76
- // newest) the pairing the preserved slot grid guarantees. Stored as
77
- // normalized fractions so it survives the new bounds and a resize.
78
- // Empty when not morphing.
76
+ // newest). `interval_morph_fade` is the host's choice at capture time:
77
+ // false = slot-lerp each column into its counterpart; true = fade the
78
+ // snapshot out then the new scene in (see labels::interval_phase).
79
+ // Stored as normalized fractions so it survives the new bounds and a
80
+ // resize. Empty when not morphing.
79
81
  std::vector<vroom::CandleSnapshot> morph_from;
80
82
  float interval_morph_t = 1.f; // 1 = not morphing
83
+ bool interval_morph_fade = false;
81
84
  // The pre-switch price scale and time window. The candle capture above is
82
85
  // normalized against these, and the axes keep rendering their old ticks from
83
86
  // them while fading out (see labels::interval_phase), so a label is never
@@ -299,6 +299,7 @@ extern "C" void vroom_chart_reset_view(VroomChart* chart) {
299
299
  // here — drop any capture rather than leaving it to reshape the wrong data.
300
300
  chart->morph_from.clear();
301
301
  chart->interval_morph_t = 1.f;
302
+ chart->interval_morph_fade = false;
302
303
  apply_default_framing(chart);
303
304
  vroom::labels::recompute_axis_width(*chart);
304
305
  if (chart->cb.on_viewport_changed) {
@@ -352,10 +353,11 @@ extern "C" void vroom_chart_preserve_price_envelope(VroomChart* chart,
352
353
  chart->mark_dirty();
353
354
  }
354
355
 
355
- extern "C" void vroom_chart_begin_interval_morph(VroomChart* chart) {
356
+ extern "C" void vroom_chart_begin_interval_morph(VroomChart* chart, int32_t mode) {
356
357
  if (!chart) return;
357
358
  chart->morph_from.clear();
358
359
  chart->interval_morph_t = 1.f;
360
+ chart->interval_morph_fade = false;
359
361
 
360
362
  const auto lay = chart->layout();
361
363
  const float area_w = vroom::candle_area_width(lay);
@@ -400,6 +402,7 @@ extern "C" void vroom_chart_begin_interval_morph(VroomChart* chart) {
400
402
  // Open the morph at 0 rather than leaving it at 1: the caller still has to
401
403
  // push the new candles, and any frame painted in between should show the
402
404
  // captured geometry — which is what the pre-switch frame looked like.
405
+ chart->interval_morph_fade = vroom::interval_morph_is_fade(mode);
403
406
  chart->interval_morph_t = 0.f;
404
407
  }
405
408
 
@@ -409,6 +412,7 @@ extern "C" void vroom_chart_set_interval_morph(VroomChart* chart, float t) {
409
412
  if (chart->interval_morph_t >= 1.f) {
410
413
  chart->morph_from.clear();
411
414
  chart->morph_from.shrink_to_fit();
415
+ chart->interval_morph_fade = false;
412
416
  }
413
417
  chart->mark_dirty();
414
418
  }
@@ -104,6 +104,10 @@ inline std::size_t morph_from_count(const CandleSnapshot* from,
104
104
  return (from && morph_t < 1.f) ? from_n : 0;
105
105
  }
106
106
 
107
+ // C facade `vroom_chart_begin_interval_morph` mode: 0 = slot-lerp transform,
108
+ // anything else = fade the outgoing snapshot out then the new scene in.
109
+ inline bool interval_morph_is_fade(int32_t mode) { return mode != 0; }
110
+
107
111
  // Returns the indices of candles whose time_ms falls in [start_ms, end_ms].
108
112
  // When both are 0, returns the full range (Phase 1 default-everything behavior).
109
113
  // Candles must be sorted ascending by time_ms (invariant of the public API).
package/lib/index.d.mts CHANGED
@@ -187,6 +187,13 @@ type ChartType = 'candles' | 'line';
187
187
  * Defaults to `'ease-in-out'`.
188
188
  */
189
189
  type TransitionEasing = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
190
+ /**
191
+ * How a same-asset interval switch animates. `'transform'` (default) lerps
192
+ * each visible column into its counterpart. `'fade'` fades the old scene out
193
+ * then the new one in — use when the two windows don’t share a 1:1 pairing
194
+ * (e.g. a fixed lookback).
195
+ */
196
+ type IntervalTransition = 'transform' | 'fade';
190
197
  /** Active drawing tool while in `draw` mode. `null` draws nothing. */
191
198
  type DrawTool = null | 'line' | 'box' | 'pencil' | 'path';
192
199
  /** A drawing anchor in data space, so it stays glued to the candles on pan/zoom. */
@@ -200,9 +207,12 @@ type DrawPoint = {
200
207
  type DrawingBase = {
201
208
  /** Stable unique id (the chart generates one for drawings it creates). */
202
209
  id: string;
203
- /** Stroke color (hex string or packed ARGB number). Default solid blue. */
210
+ /**
211
+ * Stroke color (hex string or packed ARGB number). New drawings take
212
+ * `drawingStyle.color` when set; otherwise `#ff2962ff`.
213
+ */
204
214
  color?: VroomColor;
205
- /** Stroke width in px. Default 2. */
215
+ /** Stroke width in px. New drawings take `drawingStyle.width` when set; otherwise 2. */
206
216
  width?: number;
207
217
  /**
208
218
  * Protect the drawing from editing. A locked drawing still renders and can
@@ -295,6 +305,29 @@ type DrawingStyle = {
295
305
  fill?: VroomColor;
296
306
  locked?: boolean;
297
307
  };
308
+ /**
309
+ * Default appearance for drawings the user creates — the live draft and the
310
+ * object handed to `onDrawingComplete`. Paste copies the source drawing's
311
+ * style instead of this default.
312
+ *
313
+ * Omitted fields keep the library defaults: a 2px `#ff2962ff` stroke, and for
314
+ * boxes a 10% tint of the stroke as fill.
315
+ *
316
+ * Prefer 6-digit hex for `color` (`'#00FFFF'`). vroom treats that as opaque,
317
+ * and CSS swatches preview it correctly. 8-digit hex is `#aarrggbb`, not CSS
318
+ * `#rrggbbaa`.
319
+ */
320
+ type DefaultDrawingStyle = {
321
+ /** Stroke color, used for the live draft and stamped onto the committed drawing. */
322
+ color?: VroomColor;
323
+ /** Stroke width in px. Default 2. */
324
+ width?: number;
325
+ /**
326
+ * Box interior fill. Omitted, new boxes keep the default 10% stroke tint.
327
+ * Ignored by line, pencil, and path.
328
+ */
329
+ fill?: VroomColor;
330
+ };
298
331
  /**
299
332
  * Storage adapter for **managed** drawing persistence. Provide it via the
300
333
  * `drawingStore` prop and the chart owns the drawings array itself — loading and
@@ -720,16 +753,23 @@ type VroomChartCoreProps = {
720
753
  chartType?: ChartType;
721
754
  /**
722
755
  * Duration (ms) of the animated transitions: the candle↔line switch when
723
- * `chartType` changes, and the candle reshape when the `candles` array is
756
+ * `chartType` changes, and the interval switch when the `candles` array is
724
757
  * swapped for a different interval of the same asset. Default ~300. `0` snaps
725
- * instantly. Ignored (snaps) when the OS requests reduced motion, which
726
- * instead uses a plain cross-fade.
758
+ * instantly. Ignored (snaps) when the OS requests reduced motion.
727
759
  */
728
760
  transitionMs?: number;
729
761
  /**
730
762
  * Easing curve applied to those transitions. Default `'ease-in-out'`.
731
763
  */
732
764
  transitionEasing?: TransitionEasing;
765
+ /**
766
+ * How a same-asset interval switch animates. `'transform'` (default) lerps
767
+ * each visible column into its counterpart. `'fade'` fades the old scene out
768
+ * then the new one in — use when the two windows don’t share a 1:1 pairing
769
+ * (e.g. a fixed lookback). Same duration/easing as `transitionMs`. Reduced
770
+ * motion still snaps.
771
+ */
772
+ intervalTransition?: IntervalTransition;
733
773
  theme?: VroomTheme;
734
774
  /** RSI indicator (pane below the candles). Omit/disable to hide it. */
735
775
  rsi?: RSIConfig;
@@ -802,6 +842,12 @@ type VroomChartCoreProps = {
802
842
  mode?: ChartMode;
803
843
  /** Active drawing tool while in `draw` mode. Default `null` (draws nothing). */
804
844
  tool?: DrawTool;
845
+ /**
846
+ * Default appearance for drawings the user creates (live draft + the object
847
+ * handed to `onDrawingComplete`). Paste copies the source drawing's style
848
+ * instead of this. Omitted fields keep the library defaults.
849
+ */
850
+ drawingStyle?: DefaultDrawingStyle;
805
851
  /**
806
852
  * Committed drawings to render, anchored to data so they track the candles on
807
853
  * pan/zoom. This is a controlled prop: append the value the chart hands you in
@@ -945,4 +991,4 @@ declare function classifyTransition(prev: Candle[] | null, next: Candle[], serie
945
991
  */
946
992
  declare function timeframeWindow(oldWindow: VisibleRange, oldStepMs: number, oldLastMs: number, newStepMs: number, newLastMs: number): VisibleRange;
947
993
 
948
- export { type BollingerBandsConfig, type Candle, type ChartType, type CrosshairEvent, type DataTransition, type MACDConfig, type MAKind, type MASource, type MovingAverageOverlay, type PriceLine, type PriceLinesStyle, type RSIConfig, type TransitionEasing, type VWAPConfig, type VisibleRange, type VolumeConfig, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme, classifyTransition, inferStepMs, timeframeWindow };
994
+ export { type BollingerBandsConfig, type Candle, type ChartType, type CrosshairEvent, type DataTransition, type DefaultDrawingStyle, type IntervalTransition, type MACDConfig, type MAKind, type MASource, type MovingAverageOverlay, type PriceLine, type PriceLinesStyle, type RSIConfig, type TransitionEasing, type VWAPConfig, type VisibleRange, type VolumeConfig, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme, classifyTransition, inferStepMs, timeframeWindow };