react-native-vroom-chart 0.13.0 → 0.13.1

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.
@@ -124,6 +124,14 @@ target_compile_options(vroomchart PRIVATE -Wno-mismatched-tags)
124
124
 
125
125
  find_library(VROOM_LOG_LIB log)
126
126
 
127
+ # Android 15+ devices can use 16KB pages. NDK r27 and older default to 4KB
128
+ # ELF alignment (align=2**12); Play/SDK 35 then rejects libvroomchart.so.
129
+ # r28+ already aligns to 16KB, so these flags are redundant there but harmless.
130
+ target_link_options(vroomchart PRIVATE
131
+ "-Wl,-z,max-page-size=16384"
132
+ "-Wl,-z,common-page-size=16384"
133
+ )
134
+
127
135
  target_link_libraries(vroomchart
128
136
  ${VROOM_LOG_LIB}
129
137
  ReactAndroid::jsi
package/android/README.md CHANGED
@@ -52,13 +52,14 @@ different, unmerged RTTI record for "the same" class and fails with
52
52
  `Expected a Skia object of a different type`.
53
53
 
54
54
  `VroomChartHostObject.cpp`'s `wrapPicture()` works around this only on
55
- Android: instead of constructing `RNSkia::JsiSkPicture` directly (as iOS
56
- does), it serializes the `SkPicture` (`SkPicture::serialize()`) and calls
57
- RN-Skia's own public JS API, `Skia.Picture.MakePicture(bytes)`, via JSI. That
58
- runs `JsiSkPictureFactory::MakePicture` genuinely compiled inside
59
- `librnskia.so` — so the returned object's RTTI matches what
60
- `librnskia.so`'s own code expects. This costs a serialize + re-parse of the
61
- picture's draw commands on every `render()`/gesture call, which is real
62
- overhead on what's meant to be a 60fps hot path; if that shows up in
63
- profiling, revisit (e.g. a merged-`.so` build, or a lighter-weight bridge
64
- that avoids the round trip).
55
+ Android. Two copies of Skia cannot share an `sk_sp<SkPicture>` pointer, and
56
+ serializing the chart picture (the original workaround) embedded the Android
57
+ system typeface on every pan/zoom frame an OOM that killed the process after
58
+ a few seconds of gesturing.
59
+
60
+ The hot path now rasterizes the picture in vroom's Skia and hands RN-Skia raw
61
+ pixels via `Skia.Image.MakeImage`, so `<Image>` paints a framebuffer whose size
62
+ is bounded by the view. If rasterization fails, we still serialize, but with
63
+ `SkTypeface::SerializeBehavior::kDontIncludeData` so the font file is not
64
+ copied; `MakePicture` re-resolves `sans-serif` inside `librnskia.so`. iOS is
65
+ unchanged (one binary, direct `JsiSkPicture` wrap).
@@ -4,6 +4,7 @@
4
4
  #include <string>
5
5
  #include <vector>
6
6
 
7
+ #include "chart.h"
7
8
  #include "chart_internal.h"
8
9
  #include "vroom/vroom_chart.h"
9
10
 
@@ -11,6 +12,18 @@
11
12
  #include "JsiSkNativeObjects.h"
12
13
  #include "JsiSkPicture.h"
13
14
 
15
+ #if defined(__ANDROID__)
16
+ #include <algorithm>
17
+ #include <cmath>
18
+
19
+ #include "include/core/SkBitmap.h"
20
+ #include "include/core/SkCanvas.h"
21
+ #include "include/core/SkData.h"
22
+ #include "include/core/SkImageInfo.h"
23
+ #include "include/core/SkSerialProcs.h"
24
+ #include "include/core/SkTypeface.h"
25
+ #endif
26
+
14
27
  namespace vroom {
15
28
 
16
29
  namespace jsi = facebook::jsi;
@@ -74,27 +87,21 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
74
87
  }
75
88
 
76
89
  #if defined(__ANDROID__)
77
- // Android only: RN-Skia is compiled into its own librnskia.so, separate from
78
- // libvroomchart.so. Constructing a RNSkia::JsiSkPicture directly here (as iOS
79
- // does, below) gives it a vtable/typeinfo from *our* .so; when RN-Skia's own
80
- // compiled code later consumes it (e.g. Convertor.h's
81
- // `getPropertyValue<sk_sp<SkPicture>>`, which does
82
- // `getJsiObject<JsiSkPicture>(rt, value)` — a dynamic_pointer_cast
83
- // under the hood), the cast fails cross-.so with "Expected a Skia object of
84
- // a different type", because the object's *runtime* type was never actually
85
- // compiled inside librnskia.so. iOS statically links everything into one
86
- // binary, so no such mismatch exists there.
90
+ // Android only: RN-Skia lives in librnskia.so, vroom in libvroomchart.so, and
91
+ // each statically links its own libskia.a. A JsiSkPicture constructed here has
92
+ // our .so's RTTI, so RN-Skia's dynamic_pointer_cast fails with "Expected a
93
+ // Skia object of a different type". iOS statically links one binary, so the
94
+ // iOS wrap below can hand the sk_sp<SkPicture> through directly.
87
95
  //
88
- // The fix: build the picture through RN-Skia's own public JS API instead
89
- // (`Skia.Picture.MakePicture(bytes)`, the same call `require(...).png`-style
90
- // static pictures use), so the resulting JsiSkPicture is genuinely
91
- // constructed by librnskia.so's own code. This costs a serialize +
92
- // re-parse of the picture's draw ops per call — real overhead on a gesture
93
- // hot path but is the only ABI-safe option found so far. Revisit if
94
- // profiling shows this mattering (e.g. a merged-.so build, or a lighter
95
- // bridge that avoids the round trip).
96
- #include "include/core/SkData.h"
97
-
96
+ // Crossing that boundary used to serialize the chart picture and call
97
+ // Skia.Picture.MakePicture(bytes). Default SkPicture::serialize() embeds the
98
+ // Android system typeface (Noto-scale, megabytes) on every pan/zoom frame;
99
+ // Hermes never kept up and the process was OOM-killed (~3 GB RSS in ~10s).
100
+ //
101
+ // Preferred path: rasterize in *our* Skia and hand RN-Skia raw pixels via
102
+ // Skia.Image.MakeImage memory is one framebuffer, labels stay correct.
103
+ // Fallback: still serialize, but with kDontIncludeData so the font file is
104
+ // not copied; MakePicture re-resolves sans-serif on RN-Skia's side.
98
105
  namespace {
99
106
  class SkDataMutableBuffer : public facebook::jsi::MutableBuffer {
100
107
  public:
@@ -108,41 +115,119 @@ class SkDataMutableBuffer : public facebook::jsi::MutableBuffer {
108
115
  private:
109
116
  sk_sp<SkData> data_;
110
117
  };
111
- } // namespace
112
118
 
113
- static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
114
- const sk_sp<SkPicture>& pic) {
115
- if (!pic) return facebook::jsi::Value::null();
116
- sk_sp<SkData> serialized = pic->serialize();
117
- if (!serialized) return facebook::jsi::Value::null();
119
+ jsi::Value skiaApiObject(jsi::Runtime& rt, const char* name) {
120
+ auto skiaApi = rt.global().getProperty(rt, "SkiaApi");
121
+ if (!skiaApi.isObject()) return jsi::Value::undefined();
122
+ auto prop = skiaApi.asObject(rt).getProperty(rt, name);
123
+ return prop.isObject() ? std::move(prop) : jsi::Value::undefined();
124
+ }
118
125
 
119
- auto buffer = std::make_shared<SkDataMutableBuffer>(std::move(serialized));
126
+ // Uint8Array-shaped `{ buffer }` — MakePicture and Data.fromBytes both
127
+ // read the ArrayBuffer off that property.
128
+ jsi::Object bytesLike(jsi::Runtime& rt, sk_sp<SkData> data) {
129
+ auto buffer = std::make_shared<SkDataMutableBuffer>(std::move(data));
120
130
  jsi::ArrayBuffer arrayBuffer(rt, buffer);
131
+ jsi::Object arg(rt);
132
+ arg.setProperty(rt, "buffer", arrayBuffer);
133
+ return arg;
134
+ }
121
135
 
122
- auto skiaApi = rt.global().getProperty(rt, "SkiaApi");
123
- if (!skiaApi.isObject()) return facebook::jsi::Value::null();
124
- auto pictureFactory = skiaApi.asObject(rt).getProperty(rt, "Picture");
125
- if (!pictureFactory.isObject()) return facebook::jsi::Value::null();
136
+ sk_sp<const SkData> serializeTypefaceNoEmbed(SkTypeface* tf, void*) {
137
+ if (!tf) return nullptr;
138
+ return tf->serialize(SkTypeface::SerializeBehavior::kDontIncludeData);
139
+ }
140
+
141
+ jsi::Value wrapSerializedPicture(jsi::Runtime& rt, const sk_sp<SkPicture>& pic) {
142
+ SkSerialProcs procs;
143
+ procs.fTypefaceProc = serializeTypefaceNoEmbed;
144
+ sk_sp<SkData> serialized = pic->serialize(&procs);
145
+ if (!serialized) return jsi::Value::null();
146
+
147
+ auto pictureFactory = skiaApiObject(rt, "Picture");
148
+ if (pictureFactory.isUndefined()) return jsi::Value::null();
126
149
  auto pictureFactoryObj = pictureFactory.asObject(rt);
127
150
  auto makePicture =
128
151
  pictureFactoryObj.getPropertyAsFunction(rt, "MakePicture");
129
152
 
130
- // Mirrors JsiSkPictureFactory::MakePicture's expected argument shape: an
131
- // object with a `.buffer` property holding the ArrayBuffer (matching a
132
- // Uint8Array-like value; the factory ignores everything else about it).
133
- //
134
153
  // Skia 2.11's NativeState host methods recover the factory via `this`
135
154
  // (`fromThis` → "Expected PictureFactory but got non-object" if unbound).
136
- jsi::Object arg(rt);
137
- arg.setProperty(rt, "buffer", arrayBuffer);
138
- return makePicture.callWithThis(rt, pictureFactoryObj, arg);
155
+ return makePicture.callWithThis(rt, pictureFactoryObj,
156
+ bytesLike(rt, std::move(serialized)));
157
+ }
158
+
159
+ jsi::Value wrapRasterImage(jsi::Runtime& rt, const sk_sp<SkPicture>& pic,
160
+ float dpr) {
161
+ const SkRect cull = pic->cullRect();
162
+ const float scale = dpr > 0.f ? dpr : 1.f;
163
+ const int w =
164
+ std::max(1, static_cast<int>(std::ceil(cull.width() * scale)));
165
+ const int h =
166
+ std::max(1, static_cast<int>(std::ceil(cull.height() * scale)));
167
+
168
+ // Reused across frames on this JS thread so pan/zoom doesn't heap-allocate
169
+ // a new bitmap every time. Multiple charts share it; a size change reallocs.
170
+ thread_local SkBitmap raster;
171
+ const auto info = SkImageInfo::MakeN32Premul(w, h);
172
+ if ((raster.width() != w || raster.height() != h) &&
173
+ !raster.tryAllocPixels(info)) {
174
+ return jsi::Value::null();
175
+ }
176
+ raster.eraseColor(SK_ColorTRANSPARENT);
177
+ SkCanvas canvas(raster);
178
+ canvas.scale(scale, scale);
179
+ canvas.translate(-cull.left(), -cull.top());
180
+ canvas.drawPicture(pic);
181
+
182
+ SkPixmap pixmap;
183
+ if (!raster.peekPixels(&pixmap) || pixmap.addr() == nullptr) {
184
+ return jsi::Value::null();
185
+ }
186
+ sk_sp<SkData> pixels =
187
+ SkData::MakeWithCopy(pixmap.addr(), pixmap.computeByteSize());
188
+ if (!pixels) return jsi::Value::null();
189
+
190
+ auto dataFactory = skiaApiObject(rt, "Data");
191
+ auto imageFactory = skiaApiObject(rt, "Image");
192
+ if (dataFactory.isUndefined() || imageFactory.isUndefined()) {
193
+ return jsi::Value::null();
194
+ }
195
+ auto dataFactoryObj = dataFactory.asObject(rt);
196
+ auto imageFactoryObj = imageFactory.asObject(rt);
197
+ auto fromBytes = dataFactoryObj.getPropertyAsFunction(rt, "fromBytes");
198
+ auto makeImage = imageFactoryObj.getPropertyAsFunction(rt, "MakeImage");
199
+
200
+ auto skData =
201
+ fromBytes.callWithThis(rt, dataFactoryObj, bytesLike(rt, std::move(pixels)));
202
+ if (!skData.isObject()) return jsi::Value::null();
203
+
204
+ jsi::Object imageInfo(rt);
205
+ imageInfo.setProperty(rt, "width", static_cast<double>(w));
206
+ imageInfo.setProperty(rt, "height", static_cast<double>(h));
207
+ imageInfo.setProperty(rt, "colorType",
208
+ static_cast<double>(pixmap.colorType()));
209
+ imageInfo.setProperty(rt, "alphaType",
210
+ static_cast<double>(pixmap.alphaType()));
211
+ return makeImage.callWithThis(rt, imageFactoryObj, imageInfo, skData,
212
+ static_cast<double>(pixmap.rowBytes()));
213
+ }
214
+ } // namespace
215
+
216
+ static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
217
+ const sk_sp<SkPicture>& pic,
218
+ float dpr = 1.f) {
219
+ if (!pic) return facebook::jsi::Value::null();
220
+ auto image = wrapRasterImage(rt, pic, dpr);
221
+ if (!image.isNull()) return image;
222
+ return wrapSerializedPicture(rt, pic);
139
223
  }
140
224
  #else
141
225
  // Shared helper: wraps a fresh picture for return to JS. Memory pressure is
142
226
  // applied inside JsiSkPicture::create (NativeObject::create reads
143
227
  // getMemoryPressure() → picture->approximateBytesUsed()).
144
228
  static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
145
- const sk_sp<SkPicture>& pic) {
229
+ const sk_sp<SkPicture>& pic,
230
+ float /*dpr*/ = 1.f) {
146
231
  if (!pic) return facebook::jsi::Value::null();
147
232
  auto host =
148
233
  std::make_shared<RNSkia::JsiSkPicture>(/*context=*/nullptr, pic);
@@ -448,7 +533,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
448
533
  const float dx = static_cast<float>(args[0].asNumber());
449
534
  const float dy = static_cast<float>(args[1].asNumber());
450
535
  vroom_chart_pan(chart_, dx, dy);
451
- return wrapPicture(rt2, render_chart_picture(chart_));
536
+ return wrapPicture(rt2, render_chart_picture(chart_),
537
+ chart_->px_ratio);
452
538
  });
453
539
  }
454
540
 
@@ -467,7 +553,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
467
553
  const float dx = static_cast<float>(args[0].asNumber());
468
554
  const float dy = static_cast<float>(args[1].asNumber());
469
555
  vroom_chart_translate(chart_, dx, dy);
470
- return wrapPicture(rt2, render_chart_picture(chart_));
556
+ return wrapPicture(rt2, render_chart_picture(chart_),
557
+ chart_->px_ratio);
471
558
  });
472
559
  }
473
560
 
@@ -489,7 +576,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
489
576
  const float fx = static_cast<float>(args[2].asNumber());
490
577
  const float fy = static_cast<float>(args[3].asNumber());
491
578
  vroom_chart_zoom(chart_, sx, sy, fx, fy);
492
- return wrapPicture(rt2, render_chart_picture(chart_));
579
+ return wrapPicture(rt2, render_chart_picture(chart_),
580
+ chart_->px_ratio);
493
581
  });
494
582
  }
495
583
 
@@ -505,7 +593,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
505
593
  if (count < 1) return jsi::Value::null();
506
594
  const float dy = static_cast<float>(args[0].asNumber());
507
595
  vroom_chart_scale_price_axis(chart_, dy);
508
- return wrapPicture(rt2, render_chart_picture(chart_));
596
+ return wrapPicture(rt2, render_chart_picture(chart_),
597
+ chart_->px_ratio);
509
598
  });
510
599
  }
511
600
 
@@ -521,7 +610,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
521
610
  if (count < 1) return jsi::Value::null();
522
611
  const float dx = static_cast<float>(args[0].asNumber());
523
612
  vroom_chart_scale_time_axis(chart_, dx);
524
- return wrapPicture(rt2, render_chart_picture(chart_));
613
+ return wrapPicture(rt2, render_chart_picture(chart_),
614
+ chart_->px_ratio);
525
615
  });
526
616
  }
527
617
 
@@ -572,7 +662,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
572
662
  const float x = static_cast<float>(args[0].asNumber());
573
663
  const float y = static_cast<float>(args[1].asNumber());
574
664
  vroom_chart_set_crosshair(chart_, x, y);
575
- return wrapPicture(rt2, render_chart_picture(chart_));
665
+ return wrapPicture(rt2, render_chart_picture(chart_),
666
+ chart_->px_ratio);
576
667
  });
577
668
  }
578
669
 
@@ -587,7 +678,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
587
678
  const jsi::Value* /*args*/,
588
679
  size_t /*count*/) -> jsi::Value {
589
680
  vroom_chart_clear_crosshair(chart_);
590
- return wrapPicture(rt2, render_chart_picture(chart_));
681
+ return wrapPicture(rt2, render_chart_picture(chart_),
682
+ chart_->px_ratio);
591
683
  });
592
684
  }
593
685
 
@@ -1098,7 +1190,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
1098
1190
  const jsi::Value& /*thisVal*/,
1099
1191
  const jsi::Value* /*args*/,
1100
1192
  size_t /*count*/) -> jsi::Value {
1101
- return wrapPicture(rt2, render_chart_picture(chart_));
1193
+ return wrapPicture(rt2, render_chart_picture(chart_),
1194
+ chart_->px_ratio);
1102
1195
  });
1103
1196
  }
1104
1197
 
@@ -6,7 +6,7 @@
6
6
  // Methods exposed to JS:
7
7
  // setCandles(arrayBuffer) — copies packed VroomCandle[] into the core
8
8
  // setSize(width, height, pxRatio)
9
- // render() -> JsiSkPicture — returns the current chart picture
9
+ // render() -> SkPicture (iOS) / SkImage (Android)
10
10
 
11
11
  #pragma once
12
12
 
@@ -5,7 +5,7 @@
5
5
  // methods on the returned object:
6
6
  // setCandles(arrayBuffer)
7
7
  // setSize(w, h, pxRatio)
8
- // render() -> JsiSkPicture
8
+ // render() -> SkPicture (iOS) / SkImage (Android)
9
9
  //
10
10
  // The host object's lifetime is managed by JS GC — when the React component
11
11
  // drops its reference, the underlying VroomChart is destroyed.
package/lib/index.js CHANGED
@@ -514,6 +514,9 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
514
514
  }
515
515
 
516
516
  // src/VroomChart.tsx
517
+ function isSkImage(frame) {
518
+ return typeof frame.getImageInfo === "function";
519
+ }
517
520
  function VroomChart(props) {
518
521
  const {
519
522
  candles,
@@ -565,17 +568,38 @@ function VroomChart(props) {
565
568
  rec.beginRecording(import_react_native_skia.Skia.XYWHRect(0, 0, 1, 1));
566
569
  return rec.finishRecordingAsPicture();
567
570
  }, []);
571
+ const emptyImage = (0, import_react2.useMemo)(() => {
572
+ const data = import_react_native_skia.Skia.Data.fromBytes(new Uint8Array(4));
573
+ return import_react_native_skia.Skia.Image.MakeImage(
574
+ {
575
+ width: 1,
576
+ height: 1,
577
+ colorType: import_react_native_skia.ColorType.RGBA_8888,
578
+ alphaType: import_react_native_skia.AlphaType.Premul
579
+ },
580
+ data,
581
+ 4
582
+ );
583
+ }, []);
568
584
  const pictureSV = (0, import_react_native_reanimated.useSharedValue)(emptyPicture);
585
+ const imageSV = (0, import_react_native_reanimated.useSharedValue)(emptyImage);
586
+ const applyFrame = (0, import_react2.useCallback)(
587
+ (frame) => {
588
+ if (isSkImage(frame)) imageSV.value = frame;
589
+ else pictureSV.value = frame;
590
+ },
591
+ [imageSV, pictureSV]
592
+ );
569
593
  const reduceMotion = (0, import_react_native_reanimated.useReducedMotion)();
570
594
  const onFrame = (0, import_react2.useCallback)(
571
595
  (p) => {
572
- pictureSV.value = p;
596
+ applyFrame(p);
573
597
  },
574
- [pictureSV]
598
+ [applyFrame]
575
599
  );
576
600
  const { handle, picture, volumeCollapseRef } = useChartCore(
577
601
  candles,
578
- { width, height },
602
+ { width, height, pxRatio: import_react_native2.PixelRatio.get() },
579
603
  visibleRange,
580
604
  defaultCandleWidth,
581
605
  chartType,
@@ -604,11 +628,11 @@ function VroomChart(props) {
604
628
  animRaf.current = null;
605
629
  if (!handle) return;
606
630
  const next = handle.render();
607
- if (next) pictureSV.value = next;
631
+ if (next) applyFrame(next);
608
632
  if (handle.isAnimating()) {
609
633
  animRaf.current = requestAnimationFrame(animTick);
610
634
  }
611
- }, [handle, pictureSV]);
635
+ }, [handle, applyFrame]);
612
636
  const maybeStartAnim = (0, import_react2.useCallback)(() => {
613
637
  if (animRaf.current != null) return;
614
638
  if (!handle?.isAnimating()) return;
@@ -623,9 +647,9 @@ function VroomChart(props) {
623
647
  };
624
648
  }, []);
625
649
  (0, import_react2.useEffect)(() => {
626
- if (picture) pictureSV.value = picture;
650
+ if (picture) applyFrame(picture);
627
651
  maybeStartAnim();
628
- }, [picture, pictureSV, maybeStartAnim]);
652
+ }, [picture, applyFrame, maybeStartAnim]);
629
653
  const morphRaf = (0, import_react2.useRef)(null);
630
654
  const morphFade = (0, import_react2.useRef)(null);
631
655
  const morphHandle = (0, import_react2.useRef)(null);
@@ -639,7 +663,7 @@ function VroomChart(props) {
639
663
  morphFade.current = target;
640
664
  handle.setChartType(target);
641
665
  const p = handle.render();
642
- if (p) pictureSV.value = p;
666
+ if (p) applyFrame(p);
643
667
  maybeStartAnim();
644
668
  return void 0;
645
669
  }
@@ -656,7 +680,7 @@ function VroomChart(props) {
656
680
  morphFade.current = target;
657
681
  handle.setChartType(target);
658
682
  const p = handle.render();
659
- if (p) pictureSV.value = p;
683
+ if (p) applyFrame(p);
660
684
  maybeStartAnim();
661
685
  return void 0;
662
686
  }
@@ -669,7 +693,7 @@ function VroomChart(props) {
669
693
  morphFade.current = fade;
670
694
  handle.setMorph(reduceMotion ? 0 : fade, fade);
671
695
  const p = handle.render();
672
- if (p) pictureSV.value = p;
696
+ if (p) applyFrame(p);
673
697
  if (prog < 1) {
674
698
  morphRaf.current = requestAnimationFrame(step);
675
699
  } else {
@@ -677,7 +701,7 @@ function VroomChart(props) {
677
701
  morphFade.current = target;
678
702
  handle.setChartType(target);
679
703
  const q = handle.render();
680
- if (q) pictureSV.value = q;
704
+ if (q) applyFrame(q);
681
705
  maybeStartAnim();
682
706
  }
683
707
  };
@@ -688,7 +712,7 @@ function VroomChart(props) {
688
712
  morphRaf.current = null;
689
713
  }
690
714
  };
691
- }, [handle, chartType, transitionMs, reduceMotion, pictureSV, maybeStartAnim]);
715
+ }, [handle, chartType, transitionMs, reduceMotion, applyFrame, maybeStartAnim]);
692
716
  const volumeRaf = (0, import_react2.useRef)(null);
693
717
  const volumeHandle = (0, import_react2.useRef)(null);
694
718
  (0, import_react2.useEffect)(() => {
@@ -714,7 +738,7 @@ function VroomChart(props) {
714
738
  volumeCollapseRef.current = { t: target, easing };
715
739
  handle.setVolumeCollapse(target, easing);
716
740
  const p = handle.render();
717
- if (p) pictureSV.value = p;
741
+ if (p) applyFrame(p);
718
742
  maybeStartAnim();
719
743
  return void 0;
720
744
  }
@@ -728,7 +752,7 @@ function VroomChart(props) {
728
752
  volumeCollapseRef.current = { t, easing: kind };
729
753
  handle.setVolumeCollapse(t, kind);
730
754
  const p = handle.render();
731
- if (p) pictureSV.value = p;
755
+ if (p) applyFrame(p);
732
756
  if (prog < 1) {
733
757
  volumeRaf.current = requestAnimationFrame(step);
734
758
  } else {
@@ -748,7 +772,7 @@ function VroomChart(props) {
748
772
  volume?.enabled,
749
773
  transitionMs,
750
774
  reduceMotion,
751
- pictureSV,
775
+ applyFrame,
752
776
  volumeCollapseRef,
753
777
  maybeStartAnim
754
778
  ]);
@@ -766,7 +790,7 @@ function VroomChart(props) {
766
790
  axisCollapse.current = { y: targetY, x: targetX };
767
791
  handle.setAxisCollapse(targetY, targetX);
768
792
  const p = handle.render();
769
- if (p) pictureSV.value = p;
793
+ if (p) applyFrame(p);
770
794
  maybeStartAnim();
771
795
  return void 0;
772
796
  }
@@ -785,7 +809,7 @@ function VroomChart(props) {
785
809
  axisCollapse.current = { y: targetY, x: targetX };
786
810
  handle.setAxisCollapse(targetY, targetX);
787
811
  const p = handle.render();
788
- if (p) pictureSV.value = p;
812
+ if (p) applyFrame(p);
789
813
  maybeStartAnim();
790
814
  return void 0;
791
815
  }
@@ -799,7 +823,7 @@ function VroomChart(props) {
799
823
  axisCollapse.current = { y, x };
800
824
  handle.setAxisCollapse(y, x);
801
825
  const p = handle.render();
802
- if (p) pictureSV.value = p;
826
+ if (p) applyFrame(p);
803
827
  if (prog < 1) {
804
828
  axisRaf.current = requestAnimationFrame(step);
805
829
  } else {
@@ -820,7 +844,7 @@ function VroomChart(props) {
820
844
  showXAxis,
821
845
  transitionMs,
822
846
  reduceMotion,
823
- pictureSV,
847
+ applyFrame,
824
848
  maybeStartAnim
825
849
  ]);
826
850
  const hitAxis = (0, import_react2.useCallback)(
@@ -860,7 +884,7 @@ function VroomChart(props) {
860
884
  priceDrag.current = { index: pl.index, id: pl.line.id, price: pl.line.price };
861
885
  handle.setPriceLineDrag(pl.index, pl.line.price);
862
886
  const p = handle.render();
863
- if (p) pictureSV.value = p;
887
+ if (p) applyFrame(p);
864
888
  }
865
889
  }
866
890
  }).onChange((e) => {
@@ -883,7 +907,7 @@ function VroomChart(props) {
883
907
  next = handle.render();
884
908
  } else if (crosshairActive.current) {
885
909
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
886
- if (ch) pictureSV.value = ch;
910
+ if (ch) applyFrame(ch);
887
911
  const info = handle.getCrosshairInfo();
888
912
  const t = info?.timeMs ?? null;
889
913
  if (t !== lastCrosshairTime.current) {
@@ -894,7 +918,7 @@ function VroomChart(props) {
894
918
  } else {
895
919
  next = handle.translate(e.changeX, e.changeY);
896
920
  }
897
- if (next) pictureSV.value = next;
921
+ if (next) applyFrame(next);
898
922
  maybeStartAnim();
899
923
  }).onEnd((e) => {
900
924
  if (!handle) return;
@@ -903,7 +927,7 @@ function VroomChart(props) {
903
927
  priceDrag.current = null;
904
928
  handle.setPriceLineDrag(-1, 0);
905
929
  const p = handle.render();
906
- if (p) pictureSV.value = p;
930
+ if (p) applyFrame(p);
907
931
  if (g) onPriceLineDragEnd?.(g.id, g.price);
908
932
  return;
909
933
  }
@@ -923,7 +947,7 @@ function VroomChart(props) {
923
947
  velocity *= Math.pow(0.5, dt / HALF_LIFE_S);
924
948
  const dx = velocity * dt;
925
949
  const next = handle.pan(dx, 0);
926
- if (next) pictureSV.value = next;
950
+ if (next) applyFrame(next);
927
951
  maybeStartAnim();
928
952
  if (Math.abs(velocity) > MIN_STOP) {
929
953
  decayRaf.current = requestAnimationFrame(tick);
@@ -977,7 +1001,7 @@ function VroomChart(props) {
977
1001
  }
978
1002
  if (frameX === 1 && frameY === 1) return;
979
1003
  const next = handle.zoom(frameX, frameY, focalX, focalY);
980
- if (next) pictureSV.value = next;
1004
+ if (next) applyFrame(next);
981
1005
  maybeStartAnim();
982
1006
  });
983
1007
  const longPress = import_react_native_gesture_handler.Gesture.LongPress().runOnJS(true).onStart((e) => {
@@ -987,7 +1011,7 @@ function VroomChart(props) {
987
1011
  cancelDecay();
988
1012
  crosshairActive.current = true;
989
1013
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
990
- if (ch) pictureSV.value = ch;
1014
+ if (ch) applyFrame(ch);
991
1015
  const info = handle.getCrosshairInfo();
992
1016
  lastCrosshairTime.current = info?.timeMs ?? null;
993
1017
  onCrosshair?.({
@@ -1010,7 +1034,7 @@ function VroomChart(props) {
1010
1034
  if (hitAxis(e.x, e.y) !== "chart") return;
1011
1035
  crosshairActive.current = false;
1012
1036
  const ch = handle.clearCrosshair();
1013
- if (ch) pictureSV.value = ch;
1037
+ if (ch) applyFrame(ch);
1014
1038
  lastCrosshairTime.current = null;
1015
1039
  onCrosshair?.({ active: false, candle: null, timeMs: null, price: null, reason: "hide" });
1016
1040
  });
@@ -1025,11 +1049,17 @@ function VroomChart(props) {
1025
1049
  style
1026
1050
  ]
1027
1051
  },
1028
- /* @__PURE__ */ import_react2.default.createElement(import_react_native_gesture_handler.GestureDetector, { gesture }, /* @__PURE__ */ import_react2.default.createElement(import_react_native2.View, { style: { flex: 1 } }, /* @__PURE__ */ import_react2.default.createElement(import_react_native_skia.Canvas, { style: { flex: 1 } }, width > 0 && height > 0 ? (
1029
- // pictureSV is always a valid picture (seeded empty, never null),
1030
- // so RN-Skia's UI-thread reader never sees null.
1031
- /* @__PURE__ */ import_react2.default.createElement(import_react_native_skia.Picture, { picture: pictureSV })
1032
- ) : null)))
1052
+ /* @__PURE__ */ import_react2.default.createElement(import_react_native_gesture_handler.GestureDetector, { gesture }, /* @__PURE__ */ import_react2.default.createElement(import_react_native2.View, { style: { flex: 1 } }, /* @__PURE__ */ import_react2.default.createElement(import_react_native_skia.Canvas, { style: { flex: 1 } }, width > 0 && height > 0 ? /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(import_react_native_skia.Picture, { picture: pictureSV }), /* @__PURE__ */ import_react2.default.createElement(
1053
+ import_react_native_skia.Image,
1054
+ {
1055
+ image: imageSV,
1056
+ x: 0,
1057
+ y: 0,
1058
+ width,
1059
+ height,
1060
+ fit: "fill"
1061
+ }
1062
+ )) : null)))
1033
1063
  );
1034
1064
  }
1035
1065
  // Annotate the CommonJS export names for ESM import in node: