@vroomchart/core-wasm 0.1.0 → 0.1.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.
Binary file
package/dist/index.d.ts CHANGED
@@ -20,6 +20,16 @@ type CrosshairCandle = {
20
20
  close: number;
21
21
  volume: number;
22
22
  };
23
+ /**
24
+ * What the crosshair currently snaps to. `timeMs` is the snapped slot's
25
+ * period-start time — set even in the empty space ahead of the most recent
26
+ * candle. `candle` holds the OHLCV when a real candle sits at that slot, and is
27
+ * null when the crosshair is parked on a future candle-aligned slot.
28
+ */
29
+ type CrosshairInfo = {
30
+ timeMs: number;
31
+ candle: CrosshairCandle | null;
32
+ };
23
33
  /** Axis region sizes in CSS px, for hit-testing gestures on the JS side. */
24
34
  type AxisMetrics = {
25
35
  yAxisWidth: number;
@@ -70,6 +80,12 @@ interface VroomChartHandle {
70
80
  clearCrosshair(): void;
71
81
  /** OHLCV of the candle the crosshair snaps to, or null when inactive. */
72
82
  getCrosshairCandle(): CrosshairCandle | null;
83
+ /**
84
+ * The slot the crosshair snaps to (real candle or future candle-aligned
85
+ * slot), or null when inactive. Unlike getCrosshairCandle this reports a
86
+ * timeMs even in the empty space ahead of the most recent candle.
87
+ */
88
+ getCrosshairInfo(): CrosshairInfo | null;
73
89
  setRSI(enabled: boolean, period: number, upperBand: number, lowerBand: number, maEnabled: boolean, maPeriod: number): void;
74
90
  setMACD(enabled: boolean, fast: number, slow: number, signal: number): void;
75
91
  setOverlays(overlays: OverlaySpec[]): void;
@@ -138,7 +154,7 @@ type VroomTheme = {
138
154
  declare const BYTES_PER_CANDLE = 48;
139
155
  /** Serialize candles into the packed little-endian buffer the core expects. */
140
156
  declare function packCandles(candles: Candle[]): ArrayBuffer;
141
- /** Decode the packed buffer back to candles (used by the Canvas2D stub core). */
157
+ /** Decode the packed buffer back to candles (inverse of packCandles). */
142
158
  declare function unpackCandles(buf: ArrayBuffer): Candle[];
143
159
 
144
160
  declare const COLOR_KEYS: Record<keyof VroomTheme, ColorKey>;
@@ -160,24 +176,19 @@ type LoadVroomOptions = {
160
176
  /**
161
177
  * Override where the Skia-WASM core is loaded from. By default the core uses
162
178
  * the WASM build bundled in this package (turnkey, no asset hosting needed);
163
- * pass `wasm` to load it from your own URLs instead. On load failure the
164
- * Canvas2D stub is used unless `fallbackToStub: false`.
179
+ * pass `wasm` to load it from your own URLs instead.
165
180
  */
166
181
  wasm?: WasmConfig;
167
- /** When wasm loading fails, fall back to the stub (default true). */
168
- fallbackToStub?: boolean;
169
- /** Force the Canvas2D stub instead of the WASM core (tests / SSR). */
170
- forceStub?: boolean;
171
182
  };
172
183
  /**
173
184
  * Load the chart core. Cached: repeated calls share one module instance.
174
185
  *
175
- * Defaults to the real Skia-WASM core bundled in this package. Pass
176
- * `forceStub` for the Canvas2D stub, or `wasm` to load the core from custom
177
- * URLs. WASM load failures fall back to the stub unless `fallbackToStub: false`.
186
+ * Resolves to the Skia-WASM core bundled in this package, or the core at the
187
+ * URLs in `opts.wasm`. The returned promise rejects if the WASM module fails to
188
+ * load (and the cache is cleared so a later call can retry).
178
189
  */
179
190
  declare function loadVroom(opts?: LoadVroomOptions): Promise<VroomModule>;
180
191
  /** Test/HMR helper: drop the cached module so the next loadVroom() re-creates it. */
181
192
  declare function resetVroomForTesting(): void;
182
193
 
183
- export { type AxisMetrics, BYTES_PER_CANDLE, COLOR_KEYS, ColorKey, type CrosshairCandle, type LoadVroomOptions, type OverlaySpec, type VroomChartHandle, type VroomModule, type WasmConfig, applyTheme, argbToCss, loadVroom, packCandles, parseColor, resetVroomForTesting, unpackCandles };
194
+ export { type AxisMetrics, BYTES_PER_CANDLE, COLOR_KEYS, ColorKey, type CrosshairCandle, type CrosshairInfo, type LoadVroomOptions, type OverlaySpec, type VroomChartHandle, type VroomModule, type WasmConfig, applyTheme, argbToCss, loadVroom, packCandles, parseColor, resetVroomForTesting, unpackCandles };
package/dist/index.js CHANGED
@@ -2,455 +2,6 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
- // src/handle.ts
6
- var ColorKey = /* @__PURE__ */ ((ColorKey2) => {
7
- ColorKey2[ColorKey2["Background"] = 0] = "Background";
8
- ColorKey2[ColorKey2["Bull"] = 1] = "Bull";
9
- ColorKey2[ColorKey2["Bear"] = 2] = "Bear";
10
- ColorKey2[ColorKey2["Wick"] = 3] = "Wick";
11
- ColorKey2[ColorKey2["Grid"] = 4] = "Grid";
12
- ColorKey2[ColorKey2["AxisText"] = 5] = "AxisText";
13
- ColorKey2[ColorKey2["Crosshair"] = 6] = "Crosshair";
14
- ColorKey2[ColorKey2["TooltipBg"] = 7] = "TooltipBg";
15
- ColorKey2[ColorKey2["TooltipText"] = 8] = "TooltipText";
16
- ColorKey2[ColorKey2["CrosshairTarget"] = 9] = "CrosshairTarget";
17
- return ColorKey2;
18
- })(ColorKey || {});
19
-
20
- // src/color.ts
21
- var COLOR_KEYS = {
22
- background: 0 /* Background */,
23
- bull: 1 /* Bull */,
24
- bear: 2 /* Bear */,
25
- grid: 4 /* Grid */,
26
- axisText: 5 /* AxisText */,
27
- crosshair: 6 /* Crosshair */,
28
- crosshairTarget: 9 /* CrosshairTarget */
29
- };
30
- function parseColor(value) {
31
- if (typeof value === "number") {
32
- return Number.isFinite(value) ? value >>> 0 : null;
33
- }
34
- let s = value.trim();
35
- if (s.startsWith("#")) s = s.slice(1);
36
- if (s.length === 6) s = `ff${s}`;
37
- if (s.length !== 8 || !/^[0-9a-fA-F]{8}$/.test(s)) return null;
38
- return parseInt(s, 16) >>> 0;
39
- }
40
- function applyTheme(handle, theme) {
41
- Object.keys(COLOR_KEYS).forEach((field) => {
42
- const value = theme[field];
43
- if (value == null) return;
44
- const argb = parseColor(value);
45
- if (argb == null) return;
46
- handle.setColor(COLOR_KEYS[field], argb);
47
- });
48
- }
49
- function argbToCss(argb) {
50
- const a = (argb >>> 24 & 255) / 255;
51
- const r = argb >>> 16 & 255;
52
- const g = argb >>> 8 & 255;
53
- const b = argb & 255;
54
- return `rgba(${r}, ${g}, ${b}, ${a})`;
55
- }
56
-
57
- // src/packCandles.ts
58
- var BYTES_PER_CANDLE = 48;
59
- function packCandles(candles) {
60
- const buf = new ArrayBuffer(candles.length * BYTES_PER_CANDLE);
61
- const view = new DataView(buf);
62
- for (let i = 0; i < candles.length; i++) {
63
- const c = candles[i];
64
- const off = i * BYTES_PER_CANDLE;
65
- view.setBigInt64(off, BigInt(Math.trunc(c.timeMs)), true);
66
- view.setFloat64(off + 8, c.open, true);
67
- view.setFloat64(off + 16, c.high, true);
68
- view.setFloat64(off + 24, c.low, true);
69
- view.setFloat64(off + 32, c.close, true);
70
- view.setFloat64(off + 40, c.volume, true);
71
- }
72
- return buf;
73
- }
74
- function unpackCandles(buf) {
75
- const view = new DataView(buf);
76
- const n = Math.floor(buf.byteLength / BYTES_PER_CANDLE);
77
- const out = new Array(n);
78
- for (let i = 0; i < n; i++) {
79
- const off = i * BYTES_PER_CANDLE;
80
- out[i] = {
81
- timeMs: Number(view.getBigInt64(off, true)),
82
- open: view.getFloat64(off + 8, true),
83
- high: view.getFloat64(off + 16, true),
84
- low: view.getFloat64(off + 24, true),
85
- close: view.getFloat64(off + 32, true),
86
- volume: view.getFloat64(off + 40, true)
87
- };
88
- }
89
- return out;
90
- }
91
-
92
- // src/stub/stubModule.ts
93
- var DEFAULT_VISIBLE = 80;
94
- var Y_AXIS_W = 58;
95
- var X_AXIS_H = 22;
96
- var RIGHT_PAD_CANDLES = 4;
97
- var DEFAULT_COLORS = {
98
- [0 /* Background */]: 4279046423,
99
- [1 /* Bull */]: 4280723098,
100
- [2 /* Bear */]: 4293874512,
101
- [3 /* Wick */]: 4286086022,
102
- [4 /* Grid */]: 4280231472,
103
- [5 /* AxisText */]: 4287337630,
104
- [6 /* Crosshair */]: 4291416537,
105
- [9 /* CrosshairTarget */]: 4291416537
106
- };
107
- function median(xs) {
108
- if (xs.length === 0) return 0;
109
- const s = [...xs].sort((a, b) => a - b);
110
- const mid = s.length >> 1;
111
- return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
112
- }
113
- function niceStep(raw) {
114
- if (raw <= 0 || !Number.isFinite(raw)) return 1;
115
- const mag = Math.pow(10, Math.floor(Math.log10(raw)));
116
- const norm = raw / mag;
117
- const step = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10;
118
- return step * mag;
119
- }
120
- var StubChart = class {
121
- constructor(canvas) {
122
- __publicField(this, "canvas", canvas);
123
- __publicField(this, "ctx");
124
- __publicField(this, "candles", []);
125
- __publicField(this, "durationMs", 6e4);
126
- __publicField(this, "wCss", 0);
127
- __publicField(this, "hCss", 0);
128
- __publicField(this, "dpr", 1);
129
- __publicField(this, "visStart", 0);
130
- __publicField(this, "visEnd", 0);
131
- __publicField(this, "priceMin", 0);
132
- __publicField(this, "priceMax", 1);
133
- __publicField(this, "rangeInit", false);
134
- __publicField(this, "crosshair", false);
135
- __publicField(this, "chX", 0);
136
- __publicField(this, "chY", 0);
137
- __publicField(this, "colors", { ...DEFAULT_COLORS });
138
- const ctx = canvas.getContext("2d");
139
- if (!ctx) throw new Error("VroomChart stub: 2D canvas context unavailable");
140
- this.ctx = ctx;
141
- }
142
- // --- geometry helpers ---------------------------------------------------
143
- get plotW() {
144
- return Math.max(0, this.wCss - Y_AXIS_W);
145
- }
146
- get plotH() {
147
- return Math.max(0, this.hCss - X_AXIS_H);
148
- }
149
- xOf(timeMs) {
150
- const span = this.visEnd - this.visStart || 1;
151
- return (timeMs - this.visStart) / span * this.plotW;
152
- }
153
- yOf(price) {
154
- const span = this.priceMax - this.priceMin || 1;
155
- return (this.priceMax - price) / span * this.plotH;
156
- }
157
- color(key) {
158
- return argbToCss(this.colors[key] ?? DEFAULT_COLORS[key] ?? 4278190080);
159
- }
160
- // --- data ---------------------------------------------------------------
161
- setCandles(packed) {
162
- this.candles = unpackCandles(packed);
163
- if (this.candles.length >= 2) {
164
- const diffs = [];
165
- for (let i = 1; i < this.candles.length; i++) {
166
- diffs.push(this.candles[i].timeMs - this.candles[i - 1].timeMs);
167
- }
168
- const m = median(diffs);
169
- if (m > 0) this.durationMs = m;
170
- }
171
- if (!this.rangeInit) this.resetWindow();
172
- this.recomputePriceBounds();
173
- }
174
- resetWindow() {
175
- const n = this.candles.length;
176
- if (n === 0) {
177
- this.visStart = 0;
178
- this.visEnd = 1;
179
- return;
180
- }
181
- const last = this.candles[n - 1].timeMs;
182
- this.visEnd = last + this.durationMs * RIGHT_PAD_CANDLES;
183
- this.visStart = this.visEnd - this.durationMs * (DEFAULT_VISIBLE + RIGHT_PAD_CANDLES);
184
- this.rangeInit = true;
185
- }
186
- recomputePriceBounds() {
187
- let lo = Infinity;
188
- let hi = -Infinity;
189
- for (const c of this.candles) {
190
- if (c.timeMs < this.visStart || c.timeMs > this.visEnd) continue;
191
- if (c.low < lo) lo = c.low;
192
- if (c.high > hi) hi = c.high;
193
- }
194
- if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi <= lo) {
195
- if (this.priceMax > this.priceMin) return;
196
- lo = 0;
197
- hi = 1;
198
- }
199
- const pad = (hi - lo) * 0.08;
200
- this.priceMin = lo - pad;
201
- this.priceMax = hi + pad;
202
- }
203
- // --- sizing -------------------------------------------------------------
204
- setSize(width, height, dpr) {
205
- this.wCss = width;
206
- this.hCss = height;
207
- this.dpr = dpr || 1;
208
- this.canvas.width = Math.max(1, Math.round(width * this.dpr));
209
- this.canvas.height = Math.max(1, Math.round(height * this.dpr));
210
- this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
211
- }
212
- setColor(key, argb) {
213
- this.colors[key] = argb >>> 0;
214
- }
215
- setVisibleRange(startMs, endMs) {
216
- if (startMs === 0 && endMs === 0) {
217
- this.rangeInit = false;
218
- this.resetWindow();
219
- } else {
220
- this.visStart = startMs;
221
- this.visEnd = endMs;
222
- this.rangeInit = true;
223
- }
224
- this.recomputePriceBounds();
225
- }
226
- // --- viewport mutators --------------------------------------------------
227
- pan(dx, dy) {
228
- this.translate(dx, dy);
229
- }
230
- translate(dx, dy) {
231
- const span = this.visEnd - this.visStart;
232
- const dt = dx / (this.plotW || 1) * span;
233
- this.visStart -= dt;
234
- this.visEnd -= dt;
235
- if (dy) {
236
- const pspan = this.priceMax - this.priceMin;
237
- const dp = dy / (this.plotH || 1) * pspan;
238
- this.priceMin += dp;
239
- this.priceMax += dp;
240
- }
241
- }
242
- zoom(scaleX, scaleY, fx, fy) {
243
- if (scaleX !== 1) {
244
- const span = this.visEnd - this.visStart;
245
- const focalT = this.visStart + fx / (this.plotW || 1) * span;
246
- const newSpan = span / scaleX;
247
- this.visStart = focalT - fx / (this.plotW || 1) * newSpan;
248
- this.visEnd = this.visStart + newSpan;
249
- }
250
- if (scaleY !== 1) {
251
- const pspan = this.priceMax - this.priceMin;
252
- const focalP = this.priceMax - fy / (this.plotH || 1) * pspan;
253
- const newSpan = pspan / scaleY;
254
- this.priceMax = focalP + fy / (this.plotH || 1) * newSpan;
255
- this.priceMin = this.priceMax - newSpan;
256
- }
257
- }
258
- scalePriceAxis(dy) {
259
- const factor = Math.max(0.1, 1 + dy / (this.plotH || 1));
260
- const center = (this.priceMin + this.priceMax) / 2;
261
- const half = (this.priceMax - this.priceMin) / 2 * factor;
262
- this.priceMin = center - half;
263
- this.priceMax = center + half;
264
- }
265
- scaleTimeAxis(dx) {
266
- const factor = Math.max(0.1, 1 + dx / (this.plotW || 1));
267
- const span = (this.visEnd - this.visStart) * factor;
268
- this.visStart = this.visEnd - span;
269
- }
270
- getAxisMetrics() {
271
- return { yAxisWidth: Y_AXIS_W, xAxisHeight: X_AXIS_H, indicatorHeight: 0 };
272
- }
273
- // --- crosshair ----------------------------------------------------------
274
- setCrosshair(x, y) {
275
- this.crosshair = true;
276
- this.chX = x;
277
- this.chY = y;
278
- }
279
- clearCrosshair() {
280
- this.crosshair = false;
281
- }
282
- snapIndex() {
283
- if (this.candles.length === 0) return -1;
284
- const span = this.visEnd - this.visStart || 1;
285
- const t = this.visStart + this.chX / (this.plotW || 1) * span;
286
- let best = -1;
287
- let bestD = Infinity;
288
- for (let i = 0; i < this.candles.length; i++) {
289
- const d = Math.abs(this.candles[i].timeMs - t);
290
- if (d < bestD) {
291
- bestD = d;
292
- best = i;
293
- }
294
- }
295
- return best;
296
- }
297
- getCrosshairCandle() {
298
- if (!this.crosshair) return null;
299
- const i = this.snapIndex();
300
- if (i < 0) return null;
301
- const c = this.candles[i];
302
- return { ...c };
303
- }
304
- // --- indicators (accepted, not drawn in the stub) -----------------------
305
- setRSI() {
306
- }
307
- setMACD() {
308
- }
309
- setOverlays(_overlays) {
310
- }
311
- setVWAP() {
312
- }
313
- isAnimating() {
314
- return false;
315
- }
316
- // --- rendering ----------------------------------------------------------
317
- present() {
318
- const ctx = this.ctx;
319
- const w = this.wCss;
320
- const h = this.hCss;
321
- if (w <= 0 || h <= 0) return;
322
- ctx.clearRect(0, 0, w, h);
323
- ctx.fillStyle = this.color(0 /* Background */);
324
- ctx.fillRect(0, 0, w, h);
325
- this.drawPriceAxis();
326
- this.drawTimeAxis();
327
- this.drawCandles();
328
- if (this.crosshair) this.drawCrosshair();
329
- }
330
- priceTicks() {
331
- const span = this.priceMax - this.priceMin;
332
- if (span <= 0) return [];
333
- const step = niceStep(span / 5);
334
- const first = Math.ceil(this.priceMin / step) * step;
335
- const out = [];
336
- for (let p = first; p <= this.priceMax; p += step) out.push(p);
337
- return out;
338
- }
339
- fmtPrice(p) {
340
- const span = this.priceMax - this.priceMin;
341
- const decimals = span < 1 ? 4 : span < 100 ? 2 : 0;
342
- return p.toFixed(decimals);
343
- }
344
- drawPriceAxis() {
345
- const ctx = this.ctx;
346
- const plotW = this.plotW;
347
- ctx.lineWidth = 1;
348
- ctx.strokeStyle = this.color(4 /* Grid */);
349
- ctx.fillStyle = this.color(5 /* AxisText */);
350
- ctx.font = '11px -apple-system, system-ui, "Segoe UI", sans-serif';
351
- ctx.textAlign = "left";
352
- ctx.textBaseline = "middle";
353
- for (const p of this.priceTicks()) {
354
- const y = this.yOf(p);
355
- if (y < 0 || y > this.plotH) continue;
356
- ctx.beginPath();
357
- ctx.moveTo(0, Math.round(y) + 0.5);
358
- ctx.lineTo(plotW, Math.round(y) + 0.5);
359
- ctx.stroke();
360
- ctx.fillText(this.fmtPrice(p), plotW + 6, y);
361
- }
362
- }
363
- fmtTime(timeMs) {
364
- const d = new Date(timeMs);
365
- const span = this.visEnd - this.visStart;
366
- const twoDays = 2 * 24 * 3600 * 1e3;
367
- const pad = (n) => String(n).padStart(2, "0");
368
- if (span < twoDays) return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
369
- return `${d.getMonth() + 1}/${d.getDate()}`;
370
- }
371
- drawTimeAxis() {
372
- const ctx = this.ctx;
373
- const span = this.visEnd - this.visStart;
374
- if (span <= 0) return;
375
- const targetPx = 80;
376
- const stepMs = niceStep(span / this.plotW * targetPx);
377
- const first = Math.ceil(this.visStart / stepMs) * stepMs;
378
- ctx.fillStyle = this.color(5 /* AxisText */);
379
- ctx.font = '11px -apple-system, system-ui, "Segoe UI", sans-serif';
380
- ctx.textAlign = "center";
381
- ctx.textBaseline = "middle";
382
- const y = this.plotH + X_AXIS_H / 2;
383
- for (let t = first; t <= this.visEnd; t += stepMs) {
384
- const x = this.xOf(t);
385
- if (x < 0 || x > this.plotW) continue;
386
- ctx.fillText(this.fmtTime(t), x, y);
387
- }
388
- }
389
- drawCandles() {
390
- const ctx = this.ctx;
391
- const span = this.visEnd - this.visStart || 1;
392
- const slotW = this.durationMs / span * this.plotW;
393
- const bodyW = Math.max(1, slotW * 0.7);
394
- const bull = this.color(1 /* Bull */);
395
- const bear = this.color(2 /* Bear */);
396
- const wick = this.color(3 /* Wick */);
397
- for (const c of this.candles) {
398
- const x = this.xOf(c.timeMs);
399
- if (x < -slotW || x > this.plotW + slotW) continue;
400
- const up = c.close >= c.open;
401
- const yHigh = this.yOf(c.high);
402
- const yLow = this.yOf(c.low);
403
- const yOpen = this.yOf(c.open);
404
- const yClose = this.yOf(c.close);
405
- ctx.strokeStyle = wick;
406
- ctx.lineWidth = 1;
407
- ctx.beginPath();
408
- ctx.moveTo(Math.round(x) + 0.5, yHigh);
409
- ctx.lineTo(Math.round(x) + 0.5, yLow);
410
- ctx.stroke();
411
- ctx.fillStyle = up ? bull : bear;
412
- const top = Math.min(yOpen, yClose);
413
- const bodyH = Math.max(1, Math.abs(yClose - yOpen));
414
- ctx.fillRect(x - bodyW / 2, top, bodyW, bodyH);
415
- }
416
- }
417
- drawCrosshair() {
418
- const ctx = this.ctx;
419
- const i = this.snapIndex();
420
- if (i < 0) return;
421
- const c = this.candles[i];
422
- const x = this.xOf(c.timeMs);
423
- const y = Math.max(0, Math.min(this.plotH, this.chY));
424
- ctx.save();
425
- ctx.strokeStyle = this.color(6 /* Crosshair */);
426
- ctx.lineWidth = 1;
427
- ctx.setLineDash([4, 4]);
428
- ctx.beginPath();
429
- ctx.moveTo(Math.round(x) + 0.5, 0);
430
- ctx.lineTo(Math.round(x) + 0.5, this.plotH);
431
- ctx.moveTo(0, Math.round(y) + 0.5);
432
- ctx.lineTo(this.plotW, Math.round(y) + 0.5);
433
- ctx.stroke();
434
- ctx.setLineDash([]);
435
- const ty = this.yOf(c.close);
436
- ctx.strokeStyle = this.color(9 /* CrosshairTarget */);
437
- ctx.beginPath();
438
- ctx.arc(x, ty, 3.5, 0, Math.PI * 2);
439
- ctx.stroke();
440
- ctx.restore();
441
- }
442
- destroy() {
443
- this.candles = [];
444
- }
445
- };
446
- function createStubModule() {
447
- return {
448
- create(canvas) {
449
- return new StubChart(canvas);
450
- }
451
- };
452
- }
453
-
454
5
  // src/wasm/adapter.ts
455
6
  var uid = 0;
456
7
  var WasmHandle = class {
@@ -496,6 +47,9 @@ var WasmHandle = class {
496
47
  getCrosshairCandle() {
497
48
  return this.wc.getCrosshairCandle();
498
49
  }
50
+ getCrosshairInfo() {
51
+ return this.wc.getCrosshairInfo();
52
+ }
499
53
  setRSI(enabled, period, upperBand, lowerBand, maEnabled, maPeriod) {
500
54
  this.wc.setRSI(enabled, period, upperBand, lowerBand, maEnabled, maPeriod);
501
55
  }
@@ -555,6 +109,93 @@ async function loadWasmCore(cfg) {
555
109
  return makeWasmModule(instance, fontBytes);
556
110
  }
557
111
 
112
+ // src/handle.ts
113
+ var ColorKey = /* @__PURE__ */ ((ColorKey2) => {
114
+ ColorKey2[ColorKey2["Background"] = 0] = "Background";
115
+ ColorKey2[ColorKey2["Bull"] = 1] = "Bull";
116
+ ColorKey2[ColorKey2["Bear"] = 2] = "Bear";
117
+ ColorKey2[ColorKey2["Wick"] = 3] = "Wick";
118
+ ColorKey2[ColorKey2["Grid"] = 4] = "Grid";
119
+ ColorKey2[ColorKey2["AxisText"] = 5] = "AxisText";
120
+ ColorKey2[ColorKey2["Crosshair"] = 6] = "Crosshair";
121
+ ColorKey2[ColorKey2["TooltipBg"] = 7] = "TooltipBg";
122
+ ColorKey2[ColorKey2["TooltipText"] = 8] = "TooltipText";
123
+ ColorKey2[ColorKey2["CrosshairTarget"] = 9] = "CrosshairTarget";
124
+ return ColorKey2;
125
+ })(ColorKey || {});
126
+
127
+ // src/packCandles.ts
128
+ var BYTES_PER_CANDLE = 48;
129
+ function packCandles(candles) {
130
+ const buf = new ArrayBuffer(candles.length * BYTES_PER_CANDLE);
131
+ const view = new DataView(buf);
132
+ for (let i = 0; i < candles.length; i++) {
133
+ const c = candles[i];
134
+ const off = i * BYTES_PER_CANDLE;
135
+ view.setBigInt64(off, BigInt(Math.trunc(c.timeMs)), true);
136
+ view.setFloat64(off + 8, c.open, true);
137
+ view.setFloat64(off + 16, c.high, true);
138
+ view.setFloat64(off + 24, c.low, true);
139
+ view.setFloat64(off + 32, c.close, true);
140
+ view.setFloat64(off + 40, c.volume, true);
141
+ }
142
+ return buf;
143
+ }
144
+ function unpackCandles(buf) {
145
+ const view = new DataView(buf);
146
+ const n = Math.floor(buf.byteLength / BYTES_PER_CANDLE);
147
+ const out = new Array(n);
148
+ for (let i = 0; i < n; i++) {
149
+ const off = i * BYTES_PER_CANDLE;
150
+ out[i] = {
151
+ timeMs: Number(view.getBigInt64(off, true)),
152
+ open: view.getFloat64(off + 8, true),
153
+ high: view.getFloat64(off + 16, true),
154
+ low: view.getFloat64(off + 24, true),
155
+ close: view.getFloat64(off + 32, true),
156
+ volume: view.getFloat64(off + 40, true)
157
+ };
158
+ }
159
+ return out;
160
+ }
161
+
162
+ // src/color.ts
163
+ var COLOR_KEYS = {
164
+ background: 0 /* Background */,
165
+ bull: 1 /* Bull */,
166
+ bear: 2 /* Bear */,
167
+ grid: 4 /* Grid */,
168
+ axisText: 5 /* AxisText */,
169
+ crosshair: 6 /* Crosshair */,
170
+ crosshairTarget: 9 /* CrosshairTarget */
171
+ };
172
+ function parseColor(value) {
173
+ if (typeof value === "number") {
174
+ return Number.isFinite(value) ? value >>> 0 : null;
175
+ }
176
+ let s = value.trim();
177
+ if (s.startsWith("#")) s = s.slice(1);
178
+ if (s.length === 6) s = `ff${s}`;
179
+ if (s.length !== 8 || !/^[0-9a-fA-F]{8}$/.test(s)) return null;
180
+ return parseInt(s, 16) >>> 0;
181
+ }
182
+ function applyTheme(handle, theme) {
183
+ Object.keys(COLOR_KEYS).forEach((field) => {
184
+ const value = theme[field];
185
+ if (value == null) return;
186
+ const argb = parseColor(value);
187
+ if (argb == null) return;
188
+ handle.setColor(COLOR_KEYS[field], argb);
189
+ });
190
+ }
191
+ function argbToCss(argb) {
192
+ const a = (argb >>> 24 & 255) / 255;
193
+ const r = argb >>> 16 & 255;
194
+ const g = argb >>> 8 & 255;
195
+ const b = argb & 255;
196
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
197
+ }
198
+
558
199
  // src/index.ts
559
200
  function bundledWasmConfig() {
560
201
  return {
@@ -566,17 +207,11 @@ function bundledWasmConfig() {
566
207
  var modulePromise = null;
567
208
  function loadVroom(opts) {
568
209
  if (modulePromise) return modulePromise;
569
- if (opts?.forceStub) {
570
- modulePromise = Promise.resolve(createStubModule());
571
- } else {
572
- const cfg = opts?.wasm ?? bundledWasmConfig();
573
- const fallback = opts?.fallbackToStub !== false;
574
- modulePromise = loadWasmCore(cfg).catch((err) => {
575
- if (!fallback) throw err;
576
- console.warn("[vroom] Skia-WASM core failed to load; using Canvas2D stub.", err);
577
- return createStubModule();
578
- });
579
- }
210
+ const cfg = opts?.wasm ?? bundledWasmConfig();
211
+ modulePromise = loadWasmCore(cfg).catch((err) => {
212
+ modulePromise = null;
213
+ throw err;
214
+ });
580
215
  return modulePromise;
581
216
  }
582
217
  function resetVroomForTesting() {