liveline 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20,7 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- Liveline: () => Liveline
23
+ Liveline: () => Liveline,
24
+ LivelineTransition: () => LivelineTransition
24
25
  });
25
26
  module.exports = __toCommonJS(index_exports);
26
27
 
@@ -28,16 +29,22 @@ module.exports = __toCommonJS(index_exports);
28
29
  var import_react2 = require("react");
29
30
 
30
31
  // src/theme.ts
31
- function hexToRgb(hex) {
32
- const h = hex.replace("#", "");
33
- const n = parseInt(h.length === 3 ? h.split("").map((c) => c + c).join("") : h, 16);
34
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
32
+ function parseColorRgb(color) {
33
+ const hex = color.match(/^#([0-9a-f]{3,8})$/i);
34
+ if (hex) {
35
+ let h = hex[1];
36
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
37
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
38
+ }
39
+ const rgb = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
40
+ if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
41
+ return [128, 128, 128];
35
42
  }
36
43
  function rgba(r, g, b, a) {
37
44
  return `rgba(${r}, ${g}, ${b}, ${a})`;
38
45
  }
39
46
  function resolveTheme(color, mode) {
40
- const [r, g, b] = hexToRgb(color);
47
+ const [r, g, b] = parseColorRgb(color);
41
48
  const isDark = mode === "dark";
42
49
  return {
43
50
  // Line
@@ -123,18 +130,19 @@ function computeRange(visible, currentValue, referenceValue, exaggerate) {
123
130
  // src/math/momentum.ts
124
131
  function detectMomentum(points, lookback = 20) {
125
132
  if (points.length < 5) return "flat";
126
- const recent = points.slice(-lookback);
133
+ const start = Math.max(0, points.length - lookback);
127
134
  let min = Infinity;
128
135
  let max = -Infinity;
129
- for (const p of recent) {
130
- if (p.value < min) min = p.value;
131
- if (p.value > max) max = p.value;
136
+ for (let i = start; i < points.length; i++) {
137
+ const v = points[i].value;
138
+ if (v < min) min = v;
139
+ if (v > max) max = v;
132
140
  }
133
141
  const range = max - min;
134
142
  if (range === 0) return "flat";
135
- const tail = recent.slice(-5);
136
- const first = tail[0].value;
137
- const last = tail[tail.length - 1].value;
143
+ const tailStart = Math.max(start, points.length - 5);
144
+ const first = points[tailStart].value;
145
+ const last = points[points.length - 1].value;
138
146
  const delta = last - first;
139
147
  const threshold = range * 0.12;
140
148
  if (delta > threshold) return "up";
@@ -156,7 +164,9 @@ function interpolateAtTime(points, time) {
156
164
  }
157
165
  const p1 = points[lo];
158
166
  const p2 = points[hi];
159
- const t = (time - p1.time) / (p2.time - p1.time);
167
+ const dt = p2.time - p1.time;
168
+ if (dt === 0) return p1.value;
169
+ const t = (time - p1.time) / dt;
160
170
  return p1.value + (p2.value - p1.value) * t;
161
171
  }
162
172
 
@@ -238,6 +248,7 @@ function drawGrid(ctx, layout, palette, formatValue, state, dt) {
238
248
  state.labels.set(key, target * FADE_IN);
239
249
  }
240
250
  }
251
+ const baseAlpha = ctx.globalAlpha;
241
252
  ctx.setLineDash([1, 3]);
242
253
  ctx.lineWidth = 1;
243
254
  ctx.font = palette.labelFont;
@@ -248,7 +259,7 @@ function drawGrid(ctx, layout, palette, formatValue, state, dt) {
248
259
  const y = toY(val);
249
260
  if (y < pad.top - 10 || y > h - pad.bottom + 10) continue;
250
261
  ctx.save();
251
- ctx.globalAlpha = alpha;
262
+ ctx.globalAlpha = baseAlpha * alpha;
252
263
  ctx.strokeStyle = palette.gridLine;
253
264
  ctx.beginPath();
254
265
  ctx.moveTo(pad.left, y);
@@ -313,10 +324,47 @@ function drawSpline(ctx, pts) {
313
324
  }
314
325
  }
315
326
 
327
+ // src/draw/loadingShape.ts
328
+ var LOADING_AMPLITUDE_RATIO = 0.07;
329
+ var LOADING_SCROLL_SPEED = 1e-3;
330
+ function loadingY(t, centerY, amplitude, scroll) {
331
+ return centerY + amplitude * (Math.sin(t * 9.4 + scroll) * 0.55 + Math.sin(t * 15.7 + scroll * 1.3) * 0.3 + Math.sin(t * 4.2 + scroll * 0.7) * 0.15);
332
+ }
333
+ function loadingBreath(now_ms) {
334
+ return 0.22 + 0.08 * Math.sin(now_ms / 1200 * Math.PI);
335
+ }
336
+
316
337
  // src/draw/line.ts
317
- function renderCurve(ctx, layout, palette, pts, showFill) {
338
+ function parseRgba(color) {
339
+ const hex = color.match(/^#([0-9a-f]{3,8})$/i);
340
+ if (hex) {
341
+ let h = hex[1];
342
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
343
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), 1];
344
+ }
345
+ const rgba2 = color.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)/);
346
+ if (rgba2) return [+rgba2[1], +rgba2[2], +rgba2[3], +rgba2[4]];
347
+ const rgb = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
348
+ if (rgb) return [+rgb[1], +rgb[2], +rgb[3], 1];
349
+ return [128, 128, 128, 1];
350
+ }
351
+ function blendColor(c1, c2, t) {
352
+ if (t <= 0) return c1;
353
+ if (t >= 1) return c2;
354
+ const [r1, g1, b1, a1] = parseRgba(c1);
355
+ const [r2, g2, b2, a2] = parseRgba(c2);
356
+ const r = Math.round(r1 + (r2 - r1) * t);
357
+ const g = Math.round(g1 + (g2 - g1) * t);
358
+ const b = Math.round(b1 + (b2 - b1) * t);
359
+ const a = a1 + (a2 - a1) * t;
360
+ if (a >= 0.995) return `rgb(${r},${g},${b})`;
361
+ return `rgba(${r},${g},${b},${a.toFixed(3)})`;
362
+ }
363
+ function renderCurve(ctx, layout, palette, pts, showFill, lineAlpha = 1, fillAlpha = 1, strokeColor) {
318
364
  const { h, pad } = layout;
319
- if (showFill) {
365
+ const baseAlpha = ctx.globalAlpha;
366
+ if (showFill && fillAlpha > 0.01) {
367
+ ctx.globalAlpha = baseAlpha * fillAlpha;
320
368
  const grad = ctx.createLinearGradient(0, pad.top, 0, h - pad.bottom);
321
369
  grad.addColorStop(0, palette.fillTop);
322
370
  grad.addColorStop(1, palette.fillBottom);
@@ -329,25 +377,52 @@ function renderCurve(ctx, layout, palette, pts, showFill) {
329
377
  ctx.fillStyle = grad;
330
378
  ctx.fill();
331
379
  }
380
+ ctx.globalAlpha = baseAlpha * lineAlpha;
332
381
  ctx.beginPath();
333
382
  ctx.moveTo(pts[0][0], pts[0][1]);
334
383
  drawSpline(ctx, pts);
335
- ctx.strokeStyle = palette.line;
384
+ ctx.strokeStyle = strokeColor ?? palette.line;
336
385
  ctx.lineWidth = palette.lineWidth;
337
386
  ctx.lineJoin = "round";
338
387
  ctx.lineCap = "round";
339
388
  ctx.stroke();
389
+ ctx.globalAlpha = baseAlpha;
340
390
  }
341
- function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0) {
342
- const { w, h, pad, toX, toY, chartW, chartH } = layout;
391
+ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0, chartReveal = 1, now_ms = 0, colorBlend = 1, skipDashLine = false, fillScale = 1) {
392
+ const { h, pad, toX, toY, chartW, chartH } = layout;
393
+ const incomingAlpha = ctx.globalAlpha;
343
394
  const yMin = pad.top;
344
395
  const yMax = h - pad.bottom;
345
396
  const clampY = (y) => Math.max(yMin, Math.min(yMax, y));
346
- const pts = visible.map(
347
- (p, i) => i === visible.length - 1 ? [toX(p.time), clampY(toY(smoothValue))] : [toX(p.time), clampY(toY(p.value))]
348
- );
349
- pts.push([toX(now), clampY(toY(smoothValue))]);
397
+ const centerY = pad.top + chartH / 2;
398
+ const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
399
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
400
+ const morphY = chartReveal < 1 ? (rawY, x) => {
401
+ const t = Math.max(0, Math.min(1, (x - pad.left) / chartW));
402
+ const centerDist = Math.abs(t - 0.5) * 2;
403
+ const localReveal = Math.max(0, Math.min(1, (chartReveal - centerDist * 0.4) / 0.6));
404
+ const baseY = loadingY(t, centerY, amplitude, scroll);
405
+ return baseY + (rawY - baseY) * localReveal;
406
+ } : (rawY, _x) => rawY;
407
+ const pts = visible.map((p, i) => {
408
+ const x = toX(p.time);
409
+ const y = i === visible.length - 1 ? morphY(clampY(toY(smoothValue)), x) : morphY(clampY(toY(p.value)), x);
410
+ return [x, y];
411
+ });
412
+ const liveTipX = toX(now);
413
+ const fullRightX = pad.left + chartW;
414
+ const tipX = chartReveal < 1 ? liveTipX + (fullRightX - liveTipX) * (1 - chartReveal) : liveTipX;
415
+ pts.push([tipX, morphY(clampY(toY(smoothValue)), tipX)]);
350
416
  if (pts.length < 2) return;
417
+ let lineAlpha = 1;
418
+ let fillAlpha = fillScale;
419
+ if (chartReveal < 1) {
420
+ const breath = loadingBreath(now_ms);
421
+ lineAlpha = breath + (1 - breath) * chartReveal;
422
+ fillAlpha = chartReveal * fillScale;
423
+ }
424
+ const colorT = Math.min(1, chartReveal * 3) * colorBlend;
425
+ const strokeColor = chartReveal < 1 || colorBlend < 1 ? blendColor(palette.gridLabel, palette.line, colorT) : void 0;
351
426
  const isScrubbing = scrubX !== null;
352
427
  ctx.save();
353
428
  ctx.beginPath();
@@ -358,30 +433,34 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
358
433
  ctx.beginPath();
359
434
  ctx.rect(0, 0, scrubX, h);
360
435
  ctx.clip();
361
- renderCurve(ctx, layout, palette, pts, showFill);
436
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
362
437
  ctx.restore();
363
438
  ctx.save();
364
439
  ctx.beginPath();
365
440
  ctx.rect(scrubX, 0, layout.w - scrubX, h);
366
441
  ctx.clip();
367
- ctx.globalAlpha = 1 - scrubAmount * 0.6;
368
- renderCurve(ctx, layout, palette, pts, showFill);
442
+ ctx.globalAlpha = incomingAlpha * (1 - scrubAmount * 0.6);
443
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
369
444
  ctx.restore();
370
445
  } else {
371
- renderCurve(ctx, layout, palette, pts, showFill);
446
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
372
447
  }
373
448
  ctx.restore();
374
- const currentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
375
- ctx.setLineDash([4, 4]);
376
- ctx.strokeStyle = palette.dashLine;
377
- ctx.lineWidth = 1;
378
- if (isScrubbing) ctx.globalAlpha = 1 - scrubAmount * 0.2;
379
- ctx.beginPath();
380
- ctx.moveTo(pad.left, currentY);
381
- ctx.lineTo(layout.w - pad.right, currentY);
382
- ctx.stroke();
383
- ctx.setLineDash([]);
384
- ctx.globalAlpha = 1;
449
+ if (!skipDashLine) {
450
+ const realCurrentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
451
+ const currentY = chartReveal < 1 ? centerY + (realCurrentY - centerY) * chartReveal : realCurrentY;
452
+ ctx.setLineDash([4, 4]);
453
+ ctx.strokeStyle = palette.dashLine;
454
+ ctx.lineWidth = 1;
455
+ const dashBase = isScrubbing ? 1 - scrubAmount * 0.2 : 1;
456
+ ctx.globalAlpha = incomingAlpha * (chartReveal < 1 ? dashBase * chartReveal : dashBase);
457
+ ctx.beginPath();
458
+ ctx.moveTo(pad.left, currentY);
459
+ ctx.lineTo(layout.w - pad.right, currentY);
460
+ ctx.stroke();
461
+ ctx.setLineDash([]);
462
+ }
463
+ ctx.globalAlpha = incomingAlpha;
385
464
  const last = pts[pts.length - 1];
386
465
  last[1] = Math.max(10, Math.min(h - 10, last[1]));
387
466
  return pts;
@@ -390,27 +469,17 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
390
469
  // src/draw/dot.ts
391
470
  var PULSE_INTERVAL = 1500;
392
471
  var PULSE_DURATION = 900;
393
- function parseColor(color) {
394
- const hex = color.match(/^#([0-9a-f]{3,8})$/i);
395
- if (hex) {
396
- let h = hex[1];
397
- if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
398
- return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
399
- }
400
- const rgb = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
401
- if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
402
- return null;
403
- }
404
472
  function lerpColor(a, b, t) {
405
473
  const r = Math.round(a[0] + (b[0] - a[0]) * t);
406
474
  const g = Math.round(a[1] + (b[1] - a[1]) * t);
407
475
  const bl = Math.round(a[2] + (b[2] - a[2]) * t);
408
476
  return `rgb(${r},${g},${bl})`;
409
477
  }
410
- function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
478
+ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = performance.now()) {
479
+ const baseAlpha = ctx.globalAlpha;
411
480
  const dim = scrubAmount * 0.7;
412
481
  if (pulse && dim < 0.3) {
413
- const t = Date.now() % PULSE_INTERVAL / PULSE_DURATION;
482
+ const t = now_ms % PULSE_INTERVAL / PULSE_DURATION;
414
483
  if (t < 1) {
415
484
  const radius = 9 + t * 12;
416
485
  const pulseAlpha = 0.35 * (1 - t) * (1 - dim * 3);
@@ -418,13 +487,13 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
418
487
  ctx.arc(x, y, radius, 0, Math.PI * 2);
419
488
  ctx.strokeStyle = palette.line;
420
489
  ctx.lineWidth = 1.5;
421
- ctx.globalAlpha = pulseAlpha;
490
+ ctx.globalAlpha = baseAlpha * pulseAlpha;
422
491
  ctx.stroke();
423
492
  }
424
493
  }
425
- const outerRgb = parseColor(palette.badgeOuterBg) ?? [255, 255, 255];
494
+ const outerRgb = parseColorRgb(palette.badgeOuterBg);
426
495
  ctx.save();
427
- ctx.globalAlpha = 1;
496
+ ctx.globalAlpha = baseAlpha;
428
497
  ctx.shadowColor = palette.badgeOuterShadow;
429
498
  ctx.shadowBlur = 6 * (1 - dim);
430
499
  ctx.shadowOffsetY = 1;
@@ -433,18 +502,19 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
433
502
  ctx.fillStyle = palette.badgeOuterBg;
434
503
  ctx.fill();
435
504
  ctx.restore();
436
- ctx.globalAlpha = 1;
505
+ ctx.globalAlpha = baseAlpha;
437
506
  ctx.beginPath();
438
507
  ctx.arc(x, y, 3.5, 0, Math.PI * 2);
439
508
  if (dim > 0.01) {
440
- const lineRgb = parseColor(palette.line) ?? [100, 100, 255];
509
+ const lineRgb = parseColorRgb(palette.line);
441
510
  ctx.fillStyle = lerpColor(lineRgb, outerRgb, dim);
442
511
  } else {
443
512
  ctx.fillStyle = palette.line;
444
513
  }
445
514
  ctx.fill();
446
515
  }
447
- function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
516
+ function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
517
+ const baseAlpha = ctx.globalAlpha;
448
518
  const upTarget = momentum === "up" ? 1 : 0;
449
519
  const downTarget = momentum === "down" ? 1 : 0;
450
520
  const canFadeInUp = arrows.down < 0.02;
@@ -455,7 +525,7 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
455
525
  if (arrows.down < 0.01) arrows.down = 0;
456
526
  if (arrows.up > 0.99) arrows.up = 1;
457
527
  if (arrows.down > 0.99) arrows.down = 1;
458
- const cycle = Date.now() % 1400 / 1400;
528
+ const cycle = now_ms % 1400 / 1400;
459
529
  const drawChevrons = (dir, opacity) => {
460
530
  if (opacity < 0.01) return;
461
531
  const baseX = x + 19;
@@ -471,7 +541,7 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
471
541
  const localT = cycle - start;
472
542
  const wave = localT >= 0 && localT < dur ? Math.sin(localT / dur * Math.PI) : 0;
473
543
  const pulse = 0.3 + 0.7 * wave;
474
- ctx.globalAlpha = opacity * pulse;
544
+ ctx.globalAlpha = baseAlpha * opacity * pulse;
475
545
  const nudge = dir === -1 ? -3 : 3;
476
546
  const cy = baseY + dir * (i * 8 - 4) + nudge;
477
547
  ctx.beginPath();
@@ -484,13 +554,13 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
484
554
  };
485
555
  drawChevrons(-1, arrows.up);
486
556
  drawChevrons(1, arrows.down);
487
- ctx.globalAlpha = 1;
557
+ ctx.globalAlpha = baseAlpha;
488
558
  }
489
559
 
490
560
  // src/draw/crosshair.ts
491
561
  function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, formatValue, formatTime, scrubOpacity, tooltipY, liveDotX, tooltipOutline) {
492
562
  if (scrubOpacity < 0.01) return;
493
- const { w, h, pad, toY } = layout;
563
+ const { h, pad, toY } = layout;
494
564
  const y = toY(hoverValue);
495
565
  ctx.save();
496
566
  ctx.globalAlpha = scrubOpacity * 0.5;
@@ -522,7 +592,7 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
522
592
  const totalW = valueW + sepW + timeW;
523
593
  let tx = hoverX - totalW / 2;
524
594
  const minX = pad.left + 4;
525
- const dotRightEdge = liveDotX != null ? liveDotX + 7 : w - pad.right;
595
+ const dotRightEdge = liveDotX != null ? liveDotX + 7 : layout.w - pad.right;
526
596
  const maxX = dotRightEdge - totalW;
527
597
  if (tx < minX) tx = minX;
528
598
  if (tx > maxX) tx = maxX;
@@ -597,7 +667,7 @@ function niceTimeInterval(windowSecs) {
597
667
 
598
668
  // src/draw/timeAxis.ts
599
669
  var FADE = 0.08;
600
- function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, formatTime, state, dt) {
670
+ function drawTimeAxis(ctx, layout, palette, windowSecs, targetWindowSecs, formatTime, state, dt) {
601
671
  const { h, pad, leftEdge, rightEdge, toX } = layout;
602
672
  const chartLeft = pad.left;
603
673
  const chartRight = layout.w - pad.right;
@@ -612,9 +682,9 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
612
682
  return fromEdge / fadeZone;
613
683
  };
614
684
  ctx.font = palette.labelFont;
615
- const targetPxPerSec = chartW / _targetWindowSecs;
616
- let interval = niceTimeInterval(_targetWindowSecs);
617
- while (interval * targetPxPerSec < 60 && interval < _targetWindowSecs) {
685
+ const targetPxPerSec = chartW / targetWindowSecs;
686
+ let interval = niceTimeInterval(targetWindowSecs);
687
+ while (interval * targetPxPerSec < 60 && interval < targetWindowSecs) {
618
688
  interval *= 2;
619
689
  }
620
690
  const useLocalDays = interval >= 86400;
@@ -651,6 +721,7 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
651
721
  label.alpha = next;
652
722
  }
653
723
  }
724
+ const baseAlpha = ctx.globalAlpha;
654
725
  const lineY = h - pad.bottom;
655
726
  const tickLen = 5;
656
727
  ctx.strokeStyle = palette.gridLine;
@@ -686,7 +757,7 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
686
757
  }
687
758
  for (const label of drawn) {
688
759
  ctx.save();
689
- ctx.globalAlpha = label.alpha;
760
+ ctx.globalAlpha = baseAlpha * label.alpha;
690
761
  ctx.strokeStyle = palette.gridLine;
691
762
  ctx.lineWidth = 1;
692
763
  ctx.beginPath();
@@ -807,11 +878,12 @@ function drawOrderbook(ctx, layout, palette, orderbook, dt, state, swingMagnitud
807
878
  state.labels[writeIdx++] = l;
808
879
  }
809
880
  state.labels.length = writeIdx;
881
+ const baseAlpha = ctx.globalAlpha;
810
882
  ctx.save();
811
883
  ctx.font = '600 13px "SF Mono", Menlo, monospace';
812
884
  ctx.textAlign = "left";
813
885
  ctx.textBaseline = "middle";
814
- ctx.globalAlpha = 1;
886
+ ctx.globalAlpha = baseAlpha;
815
887
  const outlineColor = `rgb(${bg[0]},${bg[1]},${bg[2]})`;
816
888
  for (let i = 0; i < state.labels.length; i++) {
817
889
  const l = state.labels[i];
@@ -905,6 +977,337 @@ function drawParticles(ctx, state, dt) {
905
977
  ctx.restore();
906
978
  }
907
979
 
980
+ // src/draw/candlestick.ts
981
+ var BULL = "#22c55e";
982
+ var BEAR = "#ef4444";
983
+ var BULL_RGB = [34, 197, 94];
984
+ var BEAR_RGB = [239, 68, 68];
985
+ function blendColor2(t) {
986
+ const r = Math.round(BEAR_RGB[0] + (BULL_RGB[0] - BEAR_RGB[0]) * t);
987
+ const g = Math.round(BEAR_RGB[1] + (BULL_RGB[1] - BEAR_RGB[1]) * t);
988
+ const b = Math.round(BEAR_RGB[2] + (BULL_RGB[2] - BEAR_RGB[2]) * t);
989
+ return `rgb(${r},${g},${b})`;
990
+ }
991
+ function parseRgb(color) {
992
+ const hex = color.match(/^#([0-9a-f]{6})$/i);
993
+ if (hex) {
994
+ const h = hex[1];
995
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
996
+ }
997
+ const rgb = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
998
+ if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
999
+ return [128, 128, 128];
1000
+ }
1001
+ function blendToAccent(candleColor, accentColor, t) {
1002
+ if (t <= 0) return candleColor;
1003
+ if (t >= 1) return accentColor;
1004
+ const [r1, g1, b1] = parseRgb(candleColor);
1005
+ const [r2, g2, b2] = parseRgb(accentColor);
1006
+ const r = Math.round(r1 + (r2 - r1) * t);
1007
+ const g = Math.round(g1 + (g2 - g1) * t);
1008
+ const b = Math.round(b1 + (b2 - b1) * t);
1009
+ return `rgb(${r},${g},${b})`;
1010
+ }
1011
+ function candleDims(layout, candleWidthSecs) {
1012
+ const pxPerSec = layout.chartW / (layout.rightEdge - layout.leftEdge);
1013
+ const candlePxW = candleWidthSecs * pxPerSec;
1014
+ const bodyW = Math.max(1, candlePxW * 0.7);
1015
+ const wickW = Math.max(0.8, Math.min(2, bodyW * 0.15));
1016
+ const radius = bodyW > 6 ? 1.5 : 0;
1017
+ return { bodyW, wickW, radius };
1018
+ }
1019
+ function roundedRect(ctx, x, y, w, h, r) {
1020
+ if (r <= 0 || h < r * 2) {
1021
+ ctx.rect(x, y, w, h);
1022
+ return;
1023
+ }
1024
+ ctx.moveTo(x + r, y);
1025
+ ctx.lineTo(x + w - r, y);
1026
+ ctx.arcTo(x + w, y, x + w, y + r, r);
1027
+ ctx.lineTo(x + w, y + h - r);
1028
+ ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
1029
+ ctx.lineTo(x + r, y + h);
1030
+ ctx.arcTo(x, y + h, x, y + h - r, r);
1031
+ ctx.lineTo(x, y + r);
1032
+ ctx.arcTo(x, y, x + r, y, r);
1033
+ ctx.closePath();
1034
+ }
1035
+ function drawCandlesticks(ctx, layout, candles, candleWidthSecs, liveTime, now_ms, scrubX, scrubDim, liveAlpha = 1, liveBullBlend = -1, accentColor, accentBlend = 0) {
1036
+ if (candles.length === 0) return;
1037
+ const { toX, toY } = layout;
1038
+ const { bodyW, wickW, radius } = candleDims(layout, candleWidthSecs);
1039
+ const halfBody = bodyW / 2;
1040
+ const padL = layout.pad.left;
1041
+ const padR = layout.pad.left + layout.chartW;
1042
+ const livePulse = 0.12 + Math.sin(now_ms * 4e-3) * 0.08;
1043
+ for (const c of candles) {
1044
+ const cx = toX(c.time + candleWidthSecs / 2);
1045
+ if (cx + halfBody < padL || cx - halfBody > padR) continue;
1046
+ const isBull = c.close >= c.open;
1047
+ const isLive = c.time === liveTime;
1048
+ let color = isLive && liveBullBlend >= 0 ? blendColor2(liveBullBlend) : isBull ? BULL : BEAR;
1049
+ if (accentColor && accentBlend > 0.01) {
1050
+ color = blendToAccent(color, accentColor, accentBlend);
1051
+ }
1052
+ let candleAlpha = isLive ? liveAlpha : 1;
1053
+ if (scrubDim > 0.01 && scrubX > 0) {
1054
+ const dist = cx - scrubX;
1055
+ if (dist > 0) {
1056
+ const fadeZone = bodyW * 1.5;
1057
+ const dimT = Math.min(dist / fadeZone, 1);
1058
+ candleAlpha *= 1 - scrubDim * 0.5 * dimT;
1059
+ }
1060
+ }
1061
+ const baseAlpha = ctx.globalAlpha;
1062
+ ctx.globalAlpha = baseAlpha * candleAlpha;
1063
+ const bodyTop = toY(Math.max(c.open, c.close));
1064
+ const bodyBottom = toY(Math.min(c.open, c.close));
1065
+ const bodyH = Math.max(1, bodyBottom - bodyTop);
1066
+ const wickTop = toY(c.high);
1067
+ const wickBottom = toY(c.low);
1068
+ ctx.lineCap = "round";
1069
+ ctx.strokeStyle = color;
1070
+ if (bodyTop - wickTop > 0.5) {
1071
+ ctx.beginPath();
1072
+ ctx.moveTo(cx, bodyTop);
1073
+ ctx.lineTo(cx, wickTop);
1074
+ ctx.lineWidth = wickW;
1075
+ ctx.stroke();
1076
+ }
1077
+ if (wickBottom - bodyBottom > 0.5) {
1078
+ ctx.beginPath();
1079
+ ctx.moveTo(cx, bodyBottom);
1080
+ ctx.lineTo(cx, wickBottom);
1081
+ ctx.lineWidth = wickW;
1082
+ ctx.stroke();
1083
+ }
1084
+ ctx.fillStyle = color;
1085
+ ctx.beginPath();
1086
+ roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
1087
+ ctx.fill();
1088
+ if (isLive) {
1089
+ ctx.save();
1090
+ ctx.globalAlpha = baseAlpha * candleAlpha * livePulse;
1091
+ ctx.shadowColor = color;
1092
+ ctx.shadowBlur = 8;
1093
+ ctx.fillStyle = color;
1094
+ ctx.beginPath();
1095
+ roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
1096
+ ctx.fill();
1097
+ ctx.restore();
1098
+ }
1099
+ ctx.globalAlpha = baseAlpha;
1100
+ }
1101
+ }
1102
+ function drawClosePrice(ctx, layout, palette, liveCandle, scrubDim, bullBlend = -1) {
1103
+ const y = layout.toY(liveCandle.close);
1104
+ if (y < layout.pad.top || y > layout.h - layout.pad.bottom) return;
1105
+ const isBull = liveCandle.close >= liveCandle.open;
1106
+ const color = bullBlend >= 0 ? blendColor2(bullBlend) : isBull ? BULL : BEAR;
1107
+ const baseAlpha = ctx.globalAlpha;
1108
+ ctx.save();
1109
+ ctx.setLineDash([4, 4]);
1110
+ ctx.strokeStyle = color;
1111
+ ctx.lineWidth = 1;
1112
+ ctx.globalAlpha = baseAlpha * (1 - scrubDim * 0.3) * 0.4;
1113
+ ctx.beginPath();
1114
+ ctx.moveTo(layout.pad.left, y);
1115
+ ctx.lineTo(layout.w - layout.pad.right, y);
1116
+ ctx.stroke();
1117
+ ctx.setLineDash([]);
1118
+ ctx.restore();
1119
+ }
1120
+ function drawCandleCrosshair(ctx, layout, palette, hoverX, candle, hoverTime, formatValue, formatTime, opacity) {
1121
+ if (opacity < 0.01) return;
1122
+ const { h, pad } = layout;
1123
+ ctx.save();
1124
+ ctx.globalAlpha = opacity * 0.5;
1125
+ ctx.strokeStyle = palette.crosshairLine;
1126
+ ctx.lineWidth = 1;
1127
+ ctx.beginPath();
1128
+ ctx.moveTo(hoverX, pad.top);
1129
+ ctx.lineTo(hoverX, h - pad.bottom);
1130
+ ctx.stroke();
1131
+ ctx.restore();
1132
+ if (opacity < 0.1 || layout.w < 200) return;
1133
+ const isBull = candle.close >= candle.open;
1134
+ const valueColor = isBull ? BULL : BEAR;
1135
+ const cl = formatValue(candle.close);
1136
+ const time = formatTime(hoverTime);
1137
+ ctx.save();
1138
+ ctx.globalAlpha = opacity;
1139
+ ctx.font = '400 13px "SF Mono", Menlo, monospace';
1140
+ ctx.textAlign = "left";
1141
+ let parts;
1142
+ if (layout.w >= 400) {
1143
+ const o = formatValue(candle.open);
1144
+ const hi = formatValue(candle.high);
1145
+ const lo = formatValue(candle.low);
1146
+ parts = [
1147
+ { text: "O ", color: palette.gridLabel },
1148
+ { text: o, color: valueColor },
1149
+ { text: " H ", color: palette.gridLabel },
1150
+ { text: hi, color: valueColor },
1151
+ { text: " L ", color: palette.gridLabel },
1152
+ { text: lo, color: valueColor },
1153
+ { text: " C ", color: palette.gridLabel },
1154
+ { text: cl, color: valueColor },
1155
+ { text: " \xB7 ", color: palette.gridLabel },
1156
+ { text: time, color: palette.gridLabel }
1157
+ ];
1158
+ } else {
1159
+ parts = [
1160
+ { text: "C ", color: palette.gridLabel },
1161
+ { text: cl, color: valueColor },
1162
+ { text: " \xB7 ", color: palette.gridLabel },
1163
+ { text: time, color: palette.gridLabel }
1164
+ ];
1165
+ }
1166
+ let totalW = 0;
1167
+ const widths = [];
1168
+ for (const p of parts) {
1169
+ const w = ctx.measureText(p.text).width;
1170
+ widths.push(w);
1171
+ totalW += w;
1172
+ }
1173
+ let tx = hoverX - totalW / 2;
1174
+ const minX = pad.left + 4;
1175
+ const maxX = layout.w - pad.right - totalW;
1176
+ if (tx < minX) tx = minX;
1177
+ if (tx > maxX) tx = maxX;
1178
+ const ty = pad.top + 24;
1179
+ ctx.strokeStyle = palette.tooltipBg;
1180
+ ctx.lineWidth = 3;
1181
+ ctx.lineJoin = "round";
1182
+ let cx = tx;
1183
+ for (let i = 0; i < parts.length; i++) {
1184
+ ctx.strokeText(parts[i].text, cx, ty);
1185
+ cx += widths[i];
1186
+ }
1187
+ cx = tx;
1188
+ for (let i = 0; i < parts.length; i++) {
1189
+ ctx.fillStyle = parts[i].color;
1190
+ ctx.fillText(parts[i].text, cx, ty);
1191
+ cx += widths[i];
1192
+ }
1193
+ ctx.restore();
1194
+ }
1195
+ function drawLineModeCrosshair(ctx, layout, palette, hoverX, value, hoverTime, formatValue, formatTime, opacity) {
1196
+ if (opacity < 0.01) return;
1197
+ const { h, pad } = layout;
1198
+ const y = layout.toY(value);
1199
+ ctx.save();
1200
+ ctx.globalAlpha = opacity * 0.5;
1201
+ ctx.strokeStyle = palette.crosshairLine;
1202
+ ctx.lineWidth = 1;
1203
+ ctx.beginPath();
1204
+ ctx.moveTo(hoverX, pad.top);
1205
+ ctx.lineTo(hoverX, h - pad.bottom);
1206
+ ctx.stroke();
1207
+ ctx.globalAlpha = opacity * 0.3;
1208
+ ctx.beginPath();
1209
+ ctx.moveTo(pad.left, y);
1210
+ ctx.lineTo(layout.w - pad.right, y);
1211
+ ctx.stroke();
1212
+ ctx.restore();
1213
+ if (opacity < 0.1 || layout.w < 200) return;
1214
+ const val = formatValue(value);
1215
+ const time = formatTime(hoverTime);
1216
+ ctx.save();
1217
+ ctx.globalAlpha = opacity;
1218
+ ctx.font = '400 13px "SF Mono", Menlo, monospace';
1219
+ ctx.textAlign = "left";
1220
+ const parts = [
1221
+ { text: val, color: palette.line },
1222
+ { text: " \xB7 ", color: palette.gridLabel },
1223
+ { text: time, color: palette.gridLabel }
1224
+ ];
1225
+ let totalW = 0;
1226
+ const widths = [];
1227
+ for (const p of parts) {
1228
+ const w = ctx.measureText(p.text).width;
1229
+ widths.push(w);
1230
+ totalW += w;
1231
+ }
1232
+ let tx = hoverX - totalW / 2;
1233
+ const minX = pad.left + 4;
1234
+ const maxX = layout.w - pad.right - totalW;
1235
+ if (tx < minX) tx = minX;
1236
+ if (tx > maxX) tx = maxX;
1237
+ const ty = pad.top + 24;
1238
+ ctx.strokeStyle = palette.tooltipBg;
1239
+ ctx.lineWidth = 3;
1240
+ ctx.lineJoin = "round";
1241
+ let lx = tx;
1242
+ for (let i = 0; i < parts.length; i++) {
1243
+ ctx.strokeText(parts[i].text, lx, ty);
1244
+ lx += widths[i];
1245
+ }
1246
+ lx = tx;
1247
+ for (let i = 0; i < parts.length; i++) {
1248
+ ctx.fillStyle = parts[i].color;
1249
+ ctx.fillText(parts[i].text, lx, ty);
1250
+ lx += widths[i];
1251
+ }
1252
+ ctx.restore();
1253
+ }
1254
+
1255
+ // src/draw/empty.ts
1256
+ function drawEmpty(ctx, w, h, pad, palette, alpha = 1, now_ms = 0, skipLine = false, emptyText) {
1257
+ const chartW = w - pad.left - pad.right;
1258
+ const chartH = h - pad.top - pad.bottom;
1259
+ const centerY = pad.top + chartH / 2;
1260
+ const cx = pad.left + chartW / 2;
1261
+ const text = emptyText ?? "No data to display";
1262
+ const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1263
+ ctx.save();
1264
+ ctx.font = "400 12px system-ui, -apple-system, sans-serif";
1265
+ const textW = ctx.measureText(text).width;
1266
+ const gapHalf = textW / 2 + 20;
1267
+ const fadeW = 30;
1268
+ if (!skipLine) {
1269
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
1270
+ const breath = loadingBreath(now_ms);
1271
+ const numPts = 32;
1272
+ const pts = [];
1273
+ for (let i = 0; i <= numPts; i++) {
1274
+ const t = i / numPts;
1275
+ const x = pad.left + t * chartW;
1276
+ const y = loadingY(t, centerY, amplitude, scroll);
1277
+ pts.push([x, y]);
1278
+ }
1279
+ ctx.beginPath();
1280
+ ctx.moveTo(pts[0][0], pts[0][1]);
1281
+ drawSpline(ctx, pts);
1282
+ ctx.strokeStyle = palette.gridLabel;
1283
+ ctx.lineWidth = palette.lineWidth;
1284
+ ctx.globalAlpha = breath * alpha;
1285
+ ctx.lineCap = "round";
1286
+ ctx.lineJoin = "round";
1287
+ ctx.stroke();
1288
+ }
1289
+ ctx.save();
1290
+ ctx.globalCompositeOperation = "destination-out";
1291
+ const gapLeft = cx - gapHalf - fadeW;
1292
+ const gapRight = cx + gapHalf + fadeW;
1293
+ const eraseGrad = ctx.createLinearGradient(gapLeft, 0, gapRight, 0);
1294
+ eraseGrad.addColorStop(0, "rgba(0,0,0,0)");
1295
+ eraseGrad.addColorStop(fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1296
+ eraseGrad.addColorStop(1 - fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1297
+ eraseGrad.addColorStop(1, "rgba(0,0,0,0)");
1298
+ ctx.fillStyle = eraseGrad;
1299
+ ctx.globalAlpha = alpha;
1300
+ const eraseH = amplitude * 2 + palette.lineWidth + 6;
1301
+ ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
1302
+ ctx.restore();
1303
+ ctx.textAlign = "center";
1304
+ ctx.textBaseline = "middle";
1305
+ ctx.globalAlpha = 0.35 * alpha;
1306
+ ctx.fillStyle = palette.gridLabel;
1307
+ ctx.fillText(text, cx, centerY);
1308
+ ctx.restore();
1309
+ }
1310
+
908
1311
  // src/draw/index.ts
909
1312
  var SHAKE_DECAY_RATE = 2e-3;
910
1313
  var SHAKE_MIN_AMPLITUDE = 0.2;
@@ -928,18 +1331,44 @@ function drawFrame(ctx, layout, palette, opts) {
928
1331
  shake.amplitude *= decayRate;
929
1332
  if (shake.amplitude < SHAKE_MIN_AMPLITUDE) shake.amplitude = 0;
930
1333
  }
931
- if (opts.referenceLine) {
1334
+ const reveal = opts.chartReveal;
1335
+ const pause = opts.pauseProgress;
1336
+ const revealRamp = (start, end) => {
1337
+ const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
1338
+ return t * t * (3 - 2 * t);
1339
+ };
1340
+ if (opts.referenceLine && reveal > 0.01) {
1341
+ ctx.save();
1342
+ if (reveal < 1) ctx.globalAlpha = reveal;
932
1343
  drawReferenceLine(ctx, layout, palette, opts.referenceLine);
1344
+ ctx.restore();
933
1345
  }
934
1346
  if (opts.showGrid) {
935
- drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
1347
+ const gridAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
1348
+ if (gridAlpha > 0.01) {
1349
+ ctx.save();
1350
+ if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
1351
+ drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
1352
+ ctx.restore();
1353
+ }
936
1354
  }
937
- if (opts.orderbookData && opts.orderbookState) {
1355
+ if (opts.orderbookData && opts.orderbookState && reveal > 0.01) {
1356
+ ctx.save();
1357
+ if (reveal < 1) ctx.globalAlpha = reveal;
938
1358
  drawOrderbook(ctx, layout, palette, opts.orderbookData, opts.dt, opts.orderbookState, opts.swingMagnitude);
1359
+ ctx.restore();
939
1360
  }
940
1361
  const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
941
- const pts = drawLine(ctx, layout, palette, opts.visible, opts.smoothValue, opts.now, opts.showFill, scrubX, opts.scrubAmount);
942
- drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
1362
+ const pts = drawLine(ctx, layout, palette, opts.visible, opts.smoothValue, opts.now, opts.showFill, scrubX, opts.scrubAmount, reveal, opts.now_ms);
1363
+ {
1364
+ const timeAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
1365
+ if (timeAlpha > 0.01) {
1366
+ ctx.save();
1367
+ if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
1368
+ drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
1369
+ ctx.restore();
1370
+ }
1371
+ }
943
1372
  if (pts && pts.length > 0) {
944
1373
  const lastPt = pts[pts.length - 1];
945
1374
  let dotScrub = opts.scrubAmount;
@@ -948,19 +1377,34 @@ function drawFrame(ctx, layout, palette, opts) {
948
1377
  const fadeStart = Math.min(80, layout.chartW * 0.3);
949
1378
  dotScrub = distToLive < CROSSHAIR_FADE_MIN_PX ? 0 : distToLive >= fadeStart ? opts.scrubAmount : (distToLive - CROSSHAIR_FADE_MIN_PX) / (fadeStart - CROSSHAIR_FADE_MIN_PX) * opts.scrubAmount;
950
1379
  }
951
- drawDot(ctx, lastPt[0], lastPt[1], palette, opts.showPulse, dotScrub);
1380
+ const dotAlpha = reveal < 0.3 ? 0 : (reveal - 0.3) / 0.7;
1381
+ const showPulse = opts.showPulse && reveal > 0.6 && pause < 0.5;
1382
+ if (dotAlpha > 0.01) {
1383
+ ctx.save();
1384
+ if (dotAlpha < 1) ctx.globalAlpha = dotAlpha;
1385
+ drawDot(ctx, lastPt[0], lastPt[1], palette, showPulse, dotScrub, opts.now_ms);
1386
+ ctx.restore();
1387
+ }
952
1388
  if (opts.showMomentum) {
953
- drawArrows(
954
- ctx,
955
- lastPt[0],
956
- lastPt[1],
957
- opts.momentum,
958
- palette,
959
- opts.arrowState,
960
- opts.dt
961
- );
1389
+ const arrowReveal = reveal < 1 ? revealRamp(0.6, 1) : 1;
1390
+ const arrowAlpha = arrowReveal * (1 - pause);
1391
+ if (arrowAlpha > 0.01) {
1392
+ ctx.save();
1393
+ if (arrowAlpha < 1) ctx.globalAlpha = arrowAlpha;
1394
+ drawArrows(
1395
+ ctx,
1396
+ lastPt[0],
1397
+ lastPt[1],
1398
+ opts.momentum,
1399
+ palette,
1400
+ opts.arrowState,
1401
+ opts.dt,
1402
+ opts.now_ms
1403
+ );
1404
+ ctx.restore();
1405
+ }
962
1406
  }
963
- if (opts.particleState) {
1407
+ if (opts.particleState && reveal > 0.9) {
964
1408
  const burstIntensity = spawnOnSwing(
965
1409
  opts.particleState,
966
1410
  opts.momentum,
@@ -1013,6 +1457,235 @@ function drawFrame(ctx, layout, palette, opts) {
1013
1457
  ctx.restore();
1014
1458
  }
1015
1459
  }
1460
+ function drawCandleFrame(ctx, layout, palette, opts) {
1461
+ const { w, h, pad, chartW, chartH } = layout;
1462
+ const reveal = opts.chartReveal;
1463
+ const fullLineMode = opts.lineModeProg >= 0.99;
1464
+ const revealLine = fullLineMode ? 1 - reveal : (1 - reveal) * (1 - reveal) * (1 - reveal);
1465
+ const lp = Math.max(opts.lineModeProg, revealLine);
1466
+ const colorBlend = lp > 1e-3 ? opts.lineModeProg / lp : 1;
1467
+ const revealRamp = (start, end) => {
1468
+ const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
1469
+ return t * t * (3 - 2 * t);
1470
+ };
1471
+ const gridAlpha = revealRamp(0.25, 0.6);
1472
+ if (opts.showGrid && gridAlpha > 0.01) {
1473
+ ctx.save();
1474
+ if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
1475
+ drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
1476
+ ctx.restore();
1477
+ }
1478
+ let linePts;
1479
+ if (lp > 0.01 && opts.lineVisible.length >= 2) {
1480
+ const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
1481
+ ctx.save();
1482
+ ctx.globalAlpha = lp;
1483
+ linePts = drawLine(
1484
+ ctx,
1485
+ layout,
1486
+ palette,
1487
+ opts.lineVisible,
1488
+ opts.lineSmoothValue,
1489
+ opts.now,
1490
+ opts.lineModeProg > 0.01,
1491
+ scrubX,
1492
+ opts.scrubAmount,
1493
+ opts.chartReveal,
1494
+ opts.now_ms,
1495
+ colorBlend,
1496
+ !fullLineMode,
1497
+ opts.lineModeProg
1498
+ // fillScale — fill fades smoothly with line mode transition
1499
+ );
1500
+ ctx.restore();
1501
+ }
1502
+ const closeAlpha = revealRamp(0.4, 0.8);
1503
+ const closeSource = opts.closePriceCandle ?? opts.liveCandle;
1504
+ if (closeSource && closeAlpha > 0.01) {
1505
+ if (lp < 0.99) {
1506
+ ctx.save();
1507
+ ctx.globalAlpha = closeAlpha * (1 - lp);
1508
+ drawClosePrice(ctx, layout, palette, closeSource, opts.scrubAmount, opts.liveBullBlend);
1509
+ ctx.restore();
1510
+ }
1511
+ if (lp > 0.01 && !fullLineMode) {
1512
+ const dashY = layout.toY(closeSource.close);
1513
+ if (dashY >= pad.top && dashY <= h - pad.bottom) {
1514
+ ctx.save();
1515
+ ctx.setLineDash([4, 4]);
1516
+ ctx.strokeStyle = palette.dashLine;
1517
+ ctx.lineWidth = 1;
1518
+ ctx.globalAlpha = closeAlpha * lp * (1 - opts.scrubAmount * 0.2);
1519
+ ctx.beginPath();
1520
+ ctx.moveTo(pad.left, dashY);
1521
+ ctx.lineTo(w - pad.right, dashY);
1522
+ ctx.stroke();
1523
+ ctx.setLineDash([]);
1524
+ ctx.restore();
1525
+ }
1526
+ }
1527
+ }
1528
+ const candleAlpha = opts.chartReveal * (1 - lp);
1529
+ if (candleAlpha > 0.01) {
1530
+ const ohlcScale = reveal * reveal * (3 - 2 * reveal);
1531
+ const collapseC = (c) => ohlcScale >= 0.99 ? c : {
1532
+ time: c.time,
1533
+ open: c.close + (c.open - c.close) * ohlcScale,
1534
+ high: c.close + (c.high - c.close) * ohlcScale,
1535
+ low: c.close + (c.low - c.close) * ohlcScale,
1536
+ close: c.close
1537
+ };
1538
+ const revealCandles = ohlcScale < 0.99 ? opts.candles.map(collapseC) : opts.candles;
1539
+ const revealOld = ohlcScale < 0.99 && opts.oldCandles.length > 0 ? opts.oldCandles.map(collapseC) : opts.oldCandles;
1540
+ ctx.save();
1541
+ ctx.beginPath();
1542
+ ctx.rect(pad.left - 1, pad.top, chartW + 2, chartH);
1543
+ ctx.clip();
1544
+ const accentCol = lp > 0.01 ? palette.line : void 0;
1545
+ if (opts.morphT >= 0 && revealOld.length > 0) {
1546
+ ctx.globalAlpha = (1 - opts.morphT) * candleAlpha;
1547
+ drawCandlesticks(
1548
+ ctx,
1549
+ layout,
1550
+ revealOld,
1551
+ opts.oldWidth,
1552
+ -1,
1553
+ opts.now_ms,
1554
+ opts.hoverX ?? 0,
1555
+ opts.scrubAmount,
1556
+ 1,
1557
+ -1,
1558
+ accentCol,
1559
+ lp
1560
+ );
1561
+ ctx.globalAlpha = opts.morphT * candleAlpha;
1562
+ drawCandlesticks(
1563
+ ctx,
1564
+ layout,
1565
+ revealCandles,
1566
+ opts.displayCandleWidth,
1567
+ opts.liveCandle?.time ?? -1,
1568
+ opts.now_ms,
1569
+ opts.hoverX ?? 0,
1570
+ opts.scrubAmount,
1571
+ opts.liveBirthAlpha,
1572
+ opts.liveBullBlend,
1573
+ accentCol,
1574
+ lp
1575
+ );
1576
+ ctx.globalAlpha = 1;
1577
+ } else {
1578
+ if (candleAlpha < 1) ctx.globalAlpha = candleAlpha;
1579
+ drawCandlesticks(
1580
+ ctx,
1581
+ layout,
1582
+ revealCandles,
1583
+ opts.displayCandleWidth,
1584
+ opts.liveCandle?.time ?? -1,
1585
+ opts.now_ms,
1586
+ opts.hoverX ?? 0,
1587
+ opts.scrubAmount,
1588
+ opts.liveBirthAlpha,
1589
+ opts.liveBullBlend,
1590
+ accentCol,
1591
+ lp
1592
+ );
1593
+ }
1594
+ ctx.restore();
1595
+ }
1596
+ if (lp > 0.5 && linePts && linePts.length > 0 && reveal > 0.3) {
1597
+ const lastPt = linePts[linePts.length - 1];
1598
+ const dotAlpha = (lp - 0.5) * 2 * ((reveal - 0.3) / 0.7);
1599
+ const showPulse = lp > 0.8 && reveal > 0.6;
1600
+ if (dotAlpha > 0.01) {
1601
+ ctx.save();
1602
+ ctx.globalAlpha = dotAlpha;
1603
+ drawDot(ctx, lastPt[0], lastPt[1], palette, showPulse, opts.scrubAmount, opts.now_ms);
1604
+ ctx.restore();
1605
+ }
1606
+ }
1607
+ const timeAlpha = revealRamp(0.25, 0.6);
1608
+ if (timeAlpha > 0.01) {
1609
+ ctx.save();
1610
+ if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
1611
+ drawTimeAxis(ctx, layout, palette, opts.targetWindowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
1612
+ ctx.restore();
1613
+ }
1614
+ ctx.save();
1615
+ ctx.globalCompositeOperation = "destination-out";
1616
+ const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
1617
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
1618
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
1619
+ ctx.fillStyle = fadeGrad;
1620
+ ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
1621
+ ctx.restore();
1622
+ if (opts.showEmptyOverlay) {
1623
+ const bgAlpha = 1 - opts.chartReveal;
1624
+ if (bgAlpha > 0.01) {
1625
+ const bgEmptyAlpha = (1 - opts.loadingAlpha) * bgAlpha;
1626
+ if (bgEmptyAlpha > 0.01) {
1627
+ drawEmpty(ctx, w, h, pad, palette, bgEmptyAlpha, opts.now_ms, true, opts.emptyText);
1628
+ }
1629
+ }
1630
+ }
1631
+ if (opts.chartReveal > 0.7 && opts.hoveredCandle && opts.hoverX !== null && opts.scrubAmount > 0.01) {
1632
+ if (opts.lineModeProg > 0.5) {
1633
+ drawLineModeCrosshair(
1634
+ ctx,
1635
+ layout,
1636
+ palette,
1637
+ opts.hoverX,
1638
+ opts.hoveredCandle.close,
1639
+ opts.hoverTime ?? 0,
1640
+ opts.formatValue,
1641
+ opts.formatTime,
1642
+ opts.scrubAmount
1643
+ );
1644
+ } else {
1645
+ drawCandleCrosshair(
1646
+ ctx,
1647
+ layout,
1648
+ palette,
1649
+ opts.hoverX,
1650
+ opts.hoveredCandle,
1651
+ opts.hoverTime ?? 0,
1652
+ opts.formatValue,
1653
+ opts.formatTime,
1654
+ opts.scrubAmount
1655
+ );
1656
+ }
1657
+ }
1658
+ }
1659
+
1660
+ // src/draw/loading.ts
1661
+ function drawLoading(ctx, w, h, pad, palette, now_ms, alpha = 1, strokeColor) {
1662
+ const chartW = w - pad.left - pad.right;
1663
+ const chartH = h - pad.top - pad.bottom;
1664
+ const centerY = pad.top + chartH / 2;
1665
+ const leftX = pad.left;
1666
+ const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1667
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
1668
+ const breath = loadingBreath(now_ms);
1669
+ const numPts = 32;
1670
+ const pts = [];
1671
+ for (let i = 0; i <= numPts; i++) {
1672
+ const t = i / numPts;
1673
+ const x = leftX + t * chartW;
1674
+ const y = loadingY(t, centerY, amplitude, scroll);
1675
+ pts.push([x, y]);
1676
+ }
1677
+ ctx.save();
1678
+ ctx.beginPath();
1679
+ ctx.moveTo(pts[0][0], pts[0][1]);
1680
+ drawSpline(ctx, pts);
1681
+ ctx.strokeStyle = strokeColor ?? palette.line;
1682
+ ctx.lineWidth = palette.lineWidth;
1683
+ ctx.globalAlpha = breath * alpha;
1684
+ ctx.lineCap = "round";
1685
+ ctx.lineJoin = "round";
1686
+ ctx.stroke();
1687
+ ctx.restore();
1688
+ }
1016
1689
 
1017
1690
  // src/draw/badge.ts
1018
1691
  function badgeSvgPath(pillW, pillH, tailLen, tailSpread) {
@@ -1060,6 +1733,23 @@ var VALUE_SNAP_THRESHOLD = 1e-3;
1060
1733
  var ADAPTIVE_SPEED_BOOST = 0.2;
1061
1734
  var MOMENTUM_GREEN = [34, 197, 94];
1062
1735
  var MOMENTUM_RED = [239, 68, 68];
1736
+ var CHART_REVEAL_SPEED = 0.14;
1737
+ var CHART_REVEAL_SPEED_FWD = 0.09;
1738
+ var PAUSE_PROGRESS_SPEED = 0.12;
1739
+ var PAUSE_CATCHUP_SPEED = 0.08;
1740
+ var PAUSE_CATCHUP_SPEED_FAST = 0.22;
1741
+ var LOADING_ALPHA_SPEED = 0.14;
1742
+ var CANDLE_LERP_SPEED = 0.25;
1743
+ var CANDLE_WIDTH_TRANS_MS = 300;
1744
+ var LINE_MORPH_MS = 500;
1745
+ var CLOSE_LINE_LERP_SPEED = 0.25;
1746
+ var LINE_DENSITY_MS = 350;
1747
+ var LINE_LERP_BASE = 0.08;
1748
+ var LINE_ADAPTIVE_BOOST = 0.2;
1749
+ var LINE_SNAP_THRESHOLD = 1e-3;
1750
+ var RANGE_LERP_SPEED = 0.15;
1751
+ var RANGE_ADAPTIVE_BOOST = 0.2;
1752
+ var CANDLE_BUFFER = 0.05;
1063
1753
  function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
1064
1754
  const valGap = Math.abs(value - displayValue);
1065
1755
  const prevRange = displayMax - displayMin || 1;
@@ -1191,12 +1881,14 @@ function updateHoverState(hoverPixelX, pad, w, layout, now, visible, scrubAmount
1191
1881
  lastHover
1192
1882
  };
1193
1883
  }
1194
- function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badgeColor, isWindowTransitioning, noMotion, ctx, dt) {
1195
- if (!cfg.showBadge) {
1884
+ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badgeColor, isWindowTransitioning, noMotion, ctx, dt, chartReveal = 1) {
1885
+ if (!cfg.showBadge || chartReveal < 0.25) {
1196
1886
  badge.container.style.display = "none";
1197
1887
  return badgeY;
1198
1888
  }
1199
1889
  badge.container.style.display = "";
1890
+ const badgeOpacity = chartReveal < 0.5 ? (chartReveal - 0.25) / 0.25 : 1;
1891
+ badge.container.style.opacity = badgeOpacity < 1 ? String(badgeOpacity) : "";
1200
1892
  const { w, h, pad } = layout;
1201
1893
  const text = cfg.formatValue(smoothValue);
1202
1894
  badge.text.textContent = text;
@@ -1219,7 +1911,9 @@ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badge
1219
1911
  badge.svg.setAttribute("height", String(pillH));
1220
1912
  badge.svg.setAttribute("viewBox", `0 0 ${totalW} ${pillH}`);
1221
1913
  badge.path.setAttribute("d", cfg.badgeTail ? badgeSvgPath(pillW, pillH, BADGE_TAIL_LEN, BADGE_TAIL_SPREAD) : badgePillOnly(pillW, pillH));
1222
- const targetBadgeY = Math.max(pad.top, Math.min(h - pad.bottom, layout.toY(smoothValue)));
1914
+ const centerY = pad.top + layout.chartH / 2;
1915
+ const realTargetY = Math.max(pad.top, Math.min(h - pad.bottom, layout.toY(smoothValue)));
1916
+ const targetBadgeY = chartReveal < 1 ? centerY + (realTargetY - centerY) * chartReveal : realTargetY;
1223
1917
  if (badgeY === null || noMotion) {
1224
1918
  badgeY = targetBadgeY;
1225
1919
  } else {
@@ -1255,6 +1949,115 @@ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badge
1255
1949
  }
1256
1950
  return badgeY;
1257
1951
  }
1952
+ function computeCandleRange(candles) {
1953
+ let min = Infinity;
1954
+ let max = -Infinity;
1955
+ for (const c of candles) {
1956
+ if (c.low < min) min = c.low;
1957
+ if (c.high > max) max = c.high;
1958
+ }
1959
+ if (!isFinite(min) || !isFinite(max)) return { min: 99, max: 101 };
1960
+ const range = max - min;
1961
+ const margin = range * 0.12;
1962
+ const minRange = range * 0.1 || 0.4;
1963
+ if (range < minRange) {
1964
+ const mid = (min + max) / 2;
1965
+ return { min: mid - minRange / 2, max: mid + minRange / 2 };
1966
+ }
1967
+ return { min: min - margin, max: max + margin };
1968
+ }
1969
+ function candleAtX(candles, hoverX, candleWidth, layout) {
1970
+ const time = layout.leftEdge + (hoverX - layout.pad.left) / layout.chartW * (layout.rightEdge - layout.leftEdge);
1971
+ let lo = 0;
1972
+ let hi = candles.length - 1;
1973
+ while (lo <= hi) {
1974
+ const mid = lo + hi >> 1;
1975
+ const c = candles[mid];
1976
+ if (time < c.time) hi = mid - 1;
1977
+ else if (time >= c.time + candleWidth) lo = mid + 1;
1978
+ else return c;
1979
+ }
1980
+ return null;
1981
+ }
1982
+ function updateCandleRange(computedRange, rangeInited, displayMin, displayMax, isTransitioning, windowTransProgress, wt, chartH, dt) {
1983
+ if (!rangeInited) {
1984
+ return {
1985
+ minVal: computedRange.min,
1986
+ maxVal: computedRange.max,
1987
+ valRange: computedRange.max - computedRange.min || 1e-3,
1988
+ displayMin: computedRange.min,
1989
+ displayMax: computedRange.max,
1990
+ rangeInited: true
1991
+ };
1992
+ }
1993
+ if (isTransitioning) {
1994
+ displayMin = wt.rangeFromMin + (wt.rangeToMin - wt.rangeFromMin) * windowTransProgress;
1995
+ displayMax = wt.rangeFromMax + (wt.rangeToMax - wt.rangeFromMax) * windowTransProgress;
1996
+ } else {
1997
+ const curRange = displayMax - displayMin || 1;
1998
+ const gapMin = Math.abs(displayMin - computedRange.min);
1999
+ const gapMax = Math.abs(displayMax - computedRange.max);
2000
+ const gapRatio = Math.min((gapMin + gapMax) / curRange, 1);
2001
+ const speed = RANGE_LERP_SPEED + (1 - gapRatio) * RANGE_ADAPTIVE_BOOST;
2002
+ displayMin = lerp(displayMin, computedRange.min, speed, dt);
2003
+ displayMax = lerp(displayMax, computedRange.max, speed, dt);
2004
+ const pxThreshold = 0.5 * curRange / chartH || 1e-3;
2005
+ if (Math.abs(displayMin - computedRange.min) < pxThreshold) displayMin = computedRange.min;
2006
+ if (Math.abs(displayMax - computedRange.max) < pxThreshold) displayMax = computedRange.max;
2007
+ }
2008
+ return {
2009
+ minVal: displayMin,
2010
+ maxVal: displayMax,
2011
+ valRange: displayMax - displayMin || 1e-3,
2012
+ displayMin,
2013
+ displayMax,
2014
+ rangeInited: true
2015
+ };
2016
+ }
2017
+ function updateCandleWindowTransition(targetWindowSecs, wt, displayWindow, displayMin, displayMax, now_ms, now, candles, liveCandle, candleWidth, buffer) {
2018
+ if (wt.to !== targetWindowSecs) {
2019
+ wt.from = displayWindow;
2020
+ wt.to = targetWindowSecs;
2021
+ wt.startMs = now_ms;
2022
+ wt.rangeFromMin = displayMin;
2023
+ wt.rangeFromMax = displayMax;
2024
+ const targetRightEdge = now + targetWindowSecs * buffer;
2025
+ const targetLeftEdge = targetRightEdge - targetWindowSecs;
2026
+ const targetVisible = [];
2027
+ for (const c of candles) {
2028
+ if (c.time + candleWidth >= targetLeftEdge && c.time <= targetRightEdge) {
2029
+ targetVisible.push(c);
2030
+ }
2031
+ }
2032
+ if (liveCandle && liveCandle.time + candleWidth >= targetLeftEdge && liveCandle.time <= targetRightEdge) {
2033
+ targetVisible.push(liveCandle);
2034
+ }
2035
+ if (targetVisible.length > 0) {
2036
+ const tr = computeCandleRange(targetVisible);
2037
+ wt.rangeToMin = tr.min;
2038
+ wt.rangeToMax = tr.max;
2039
+ }
2040
+ }
2041
+ let windowTransProgress = 0;
2042
+ let resultWindow;
2043
+ if (wt.startMs === 0) {
2044
+ resultWindow = targetWindowSecs;
2045
+ } else {
2046
+ const elapsed = now_ms - wt.startMs;
2047
+ const t = Math.min(elapsed / WINDOW_TRANSITION_MS, 1);
2048
+ const eased = (1 - Math.cos(t * Math.PI)) / 2;
2049
+ windowTransProgress = eased;
2050
+ const logFrom = Math.log(wt.from);
2051
+ const logTo = Math.log(wt.to);
2052
+ resultWindow = Math.exp(logFrom + (logTo - logFrom) * eased);
2053
+ if (t >= 1) {
2054
+ resultWindow = targetWindowSecs;
2055
+ wt.startMs = 0;
2056
+ windowTransProgress = 0;
2057
+ }
2058
+ }
2059
+ return { windowSecs: resultWindow, windowTransProgress };
2060
+ }
1258
2061
  function useLivelineEngine(canvasRef, containerRef, config) {
1259
2062
  const configRef = (0, import_react.useRef)(config);
1260
2063
  configRef.current = config;
@@ -1284,12 +2087,53 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1284
2087
  const badgeYRef = (0, import_react.useRef)(null);
1285
2088
  const reducedMotionRef = (0, import_react.useRef)(false);
1286
2089
  const sizeRef = (0, import_react.useRef)({ w: 0, h: 0 });
2090
+ const ctxRef = (0, import_react.useRef)(null);
1287
2091
  const rafRef = (0, import_react.useRef)(0);
1288
2092
  const lastFrameRef = (0, import_react.useRef)(0);
1289
2093
  const badgeRef = (0, import_react.useRef)(null);
1290
2094
  const hoverXRef = (0, import_react.useRef)(null);
1291
2095
  const scrubAmountRef = (0, import_react.useRef)(0);
1292
2096
  const lastHoverRef = (0, import_react.useRef)(null);
2097
+ const chartRevealRef = (0, import_react.useRef)(0);
2098
+ const pauseProgressRef = (0, import_react.useRef)(0);
2099
+ const timeDebtRef = (0, import_react.useRef)(0);
2100
+ const lastDataRef = (0, import_react.useRef)([]);
2101
+ const frozenNowRef = (0, import_react.useRef)(0);
2102
+ const pausedDataRef = (0, import_react.useRef)(null);
2103
+ const loadingAlphaRef = (0, import_react.useRef)(config.loading ? 1 : 0);
2104
+ const displayCandleRef = (0, import_react.useRef)(null);
2105
+ const liveBirthAlphaRef = (0, import_react.useRef)(1);
2106
+ const liveBullRef = (0, import_react.useRef)(0.5);
2107
+ const lineSmoothCloseRef = (0, import_react.useRef)(0);
2108
+ const lineSmoothInitedRef = (0, import_react.useRef)(false);
2109
+ const closeLineSmoothRef = (0, import_react.useRef)(0);
2110
+ const closeLineSmoothInitedRef = (0, import_react.useRef)(false);
2111
+ const lineModeProgRef = (0, import_react.useRef)(0);
2112
+ const lineModeTransRef = (0, import_react.useRef)({ startMs: 0, from: 0, to: 0 });
2113
+ const lineDensityProgRef = (0, import_react.useRef)(0);
2114
+ const lineDensityTransRef = (0, import_react.useRef)({ startMs: 0, from: 0, to: 0 });
2115
+ const lineTickSmoothRef = (0, import_react.useRef)(0);
2116
+ const lineTickSmoothInitedRef = (0, import_react.useRef)(false);
2117
+ const candleWidthTransRef = (0, import_react.useRef)({
2118
+ fromWidth: config.candleWidth ?? 1,
2119
+ toWidth: config.candleWidth ?? 1,
2120
+ startMs: 0,
2121
+ rangeFromMin: 0,
2122
+ rangeFromMax: 0,
2123
+ rangeToMin: 0,
2124
+ rangeToMax: 0,
2125
+ oldCandles: [],
2126
+ oldWidth: config.candleWidth ?? 1
2127
+ });
2128
+ const prevCandleDataRef = (0, import_react.useRef)({ candles: [], width: config.candleWidth ?? 1 });
2129
+ const pausedCandlesRef = (0, import_react.useRef)(null);
2130
+ const pausedLiveRef = (0, import_react.useRef)(null);
2131
+ const pausedLineDataRef = (0, import_react.useRef)(null);
2132
+ const pausedLineValueRef = (0, import_react.useRef)(null);
2133
+ const lastCandlesRef = (0, import_react.useRef)([]);
2134
+ const lastLiveRef = (0, import_react.useRef)(null);
2135
+ const lastLineDataStashRef = (0, import_react.useRef)([]);
2136
+ const lastLineValueStashRef = (0, import_react.useRef)(void 0);
1293
2137
  (0, import_react.useEffect)(() => {
1294
2138
  const container = containerRef.current;
1295
2139
  if (!container) return;
@@ -1410,183 +2254,713 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1410
2254
  canvas.style.width = `${w}px`;
1411
2255
  canvas.style.height = `${h}px`;
1412
2256
  }
1413
- const ctx = canvas.getContext("2d");
2257
+ let ctx = ctxRef.current;
2258
+ if (!ctx || ctx.canvas !== canvas) {
2259
+ ctx = canvas.getContext("2d");
2260
+ ctxRef.current = ctx;
2261
+ }
1414
2262
  if (!ctx) {
1415
2263
  rafRef.current = requestAnimationFrame(draw);
1416
2264
  return;
1417
2265
  }
1418
2266
  applyDpr(ctx, dpr, w, h);
1419
2267
  const noMotion = reducedMotionRef.current;
1420
- const points = cfg.data;
1421
- if (points.length < 2) {
1422
- if (badgeRef.current) badgeRef.current.container.style.display = "none";
1423
- rafRef.current = requestAnimationFrame(draw);
1424
- return;
1425
- }
1426
- const adaptiveSpeed = computeAdaptiveSpeed(
1427
- cfg.value,
1428
- displayValueRef.current,
1429
- displayMinRef.current,
1430
- displayMaxRef.current,
1431
- cfg.lerpSpeed,
1432
- noMotion
1433
- );
1434
- displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, dt);
1435
- const prevRange = displayMaxRef.current - displayMinRef.current || 1;
1436
- if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
1437
- displayValueRef.current = cfg.value;
2268
+ const isCandle = cfg.mode === "candle";
2269
+ if (isCandle) {
2270
+ if (cfg.paused && pausedCandlesRef.current === null && (cfg.candles?.length ?? 0) > 0) {
2271
+ pausedCandlesRef.current = cfg.candles.slice();
2272
+ pausedLiveRef.current = cfg.liveCandle ?? null;
2273
+ pausedLineDataRef.current = cfg.lineData?.slice() ?? null;
2274
+ pausedLineValueRef.current = cfg.lineValue ?? null;
2275
+ }
2276
+ if (!cfg.paused) {
2277
+ pausedCandlesRef.current = null;
2278
+ pausedLiveRef.current = null;
2279
+ pausedLineDataRef.current = null;
2280
+ pausedLineValueRef.current = null;
2281
+ }
2282
+ } else {
2283
+ if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
2284
+ pausedDataRef.current = cfg.data.slice();
2285
+ }
2286
+ if (!cfg.paused) {
2287
+ pausedDataRef.current = null;
2288
+ }
1438
2289
  }
1439
- const smoothValue = displayValueRef.current;
2290
+ const points = isCandle ? [] : pausedDataRef.current ?? cfg.data;
2291
+ const effectiveCandles = isCandle ? pausedCandlesRef.current ?? (cfg.candles ?? []) : [];
2292
+ const hasData = isCandle ? effectiveCandles.length >= 2 : points.length >= 2;
1440
2293
  const pad = cfg.padding;
1441
- const chartW = w - pad.left - pad.right;
1442
- const needsArrowRoom = cfg.showMomentum;
1443
- const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
1444
- const transition = windowTransitionRef.current;
1445
- const now = Date.now() / 1e3;
1446
- const windowResult = updateWindowTransition(
1447
- cfg,
1448
- transition,
1449
- displayWindowRef.current,
1450
- displayMinRef.current,
1451
- displayMaxRef.current,
1452
- noMotion,
1453
- now_ms,
1454
- now,
1455
- points,
1456
- smoothValue,
1457
- buffer
2294
+ const chartH = h - pad.top - pad.bottom;
2295
+ const pauseTarget = cfg.paused ? 1 : 0;
2296
+ pauseProgressRef.current = noMotion ? pauseTarget : lerp(pauseProgressRef.current, pauseTarget, PAUSE_PROGRESS_SPEED, dt);
2297
+ if (pauseProgressRef.current < 5e-3) pauseProgressRef.current = 0;
2298
+ if (pauseProgressRef.current > 0.995) pauseProgressRef.current = 1;
2299
+ const pauseProgress = pauseProgressRef.current;
2300
+ const pausedDt = dt * (1 - pauseProgress);
2301
+ const realDtSec = dt / 1e3;
2302
+ timeDebtRef.current += realDtSec * pauseProgress;
2303
+ if (!cfg.paused && timeDebtRef.current > 1e-3) {
2304
+ const catchUpSpeed = timeDebtRef.current > 10 ? PAUSE_CATCHUP_SPEED_FAST : PAUSE_CATCHUP_SPEED;
2305
+ timeDebtRef.current = lerp(timeDebtRef.current, 0, catchUpSpeed, dt);
2306
+ if (timeDebtRef.current < 0.01) timeDebtRef.current = 0;
2307
+ }
2308
+ const loadingTarget = cfg.loading ? 1 : 0;
2309
+ loadingAlphaRef.current = noMotion ? loadingTarget : lerp(loadingAlphaRef.current, loadingTarget, LOADING_ALPHA_SPEED, dt);
2310
+ if (loadingAlphaRef.current < 0.01) loadingAlphaRef.current = 0;
2311
+ if (loadingAlphaRef.current > 0.99) loadingAlphaRef.current = 1;
2312
+ const loadingAlpha = loadingAlphaRef.current;
2313
+ const revealTarget = !cfg.loading && hasData ? 1 : 0;
2314
+ chartRevealRef.current = noMotion ? revealTarget : lerp(
2315
+ chartRevealRef.current,
2316
+ revealTarget,
2317
+ revealTarget === 1 ? CHART_REVEAL_SPEED_FWD : CHART_REVEAL_SPEED,
2318
+ dt
1458
2319
  );
1459
- displayWindowRef.current = windowResult.windowSecs;
1460
- const windowSecs = windowResult.windowSecs;
1461
- const windowTransProgress = windowResult.windowTransProgress;
1462
- const rightEdge = now + windowSecs * buffer;
1463
- const leftEdge = rightEdge - windowSecs;
1464
- const visible = [];
1465
- for (const p of points) {
1466
- if (p.time >= leftEdge - 2 && p.time <= rightEdge) {
1467
- visible.push(p);
2320
+ if (Math.abs(chartRevealRef.current - revealTarget) < 5e-3) {
2321
+ chartRevealRef.current = revealTarget;
2322
+ }
2323
+ const chartReveal = chartRevealRef.current;
2324
+ if (chartReveal < 0.01) {
2325
+ rangeInitedRef.current = false;
2326
+ }
2327
+ let useStash;
2328
+ if (isCandle) {
2329
+ useStash = !hasData && chartReveal > 5e-3 && lastCandlesRef.current.length > 0;
2330
+ } else {
2331
+ useStash = !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
2332
+ if (hasData) lastDataRef.current = points;
2333
+ }
2334
+ if (isCandle) {
2335
+ const lmt = lineModeTransRef.current;
2336
+ const lineModeTarget = cfg.lineMode ? 1 : 0;
2337
+ if (lmt.to !== lineModeTarget) {
2338
+ lmt.from = lineModeProgRef.current;
2339
+ lmt.to = lineModeTarget;
2340
+ lmt.startMs = now_ms;
2341
+ }
2342
+ if (lmt.startMs > 0) {
2343
+ const elapsed = now_ms - lmt.startMs;
2344
+ const t = Math.min(elapsed / LINE_MORPH_MS, 1);
2345
+ lineModeProgRef.current = lmt.from + (lmt.to - lmt.from) * ((1 - Math.cos(t * Math.PI)) / 2);
2346
+ if (t >= 1) {
2347
+ lineModeProgRef.current = lmt.to;
2348
+ lmt.startMs = 0;
2349
+ }
2350
+ } else {
2351
+ lineModeProgRef.current = lmt.to;
1468
2352
  }
1469
2353
  }
1470
- if (visible.length < 2) {
2354
+ if (!hasData && !useStash) {
2355
+ const loadingColor = isCandle ? cfg.palette.gridLabel : void 0;
2356
+ if (loadingAlpha > 0.01) {
2357
+ drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, loadingColor);
2358
+ }
2359
+ if (1 - loadingAlpha > 0.01) {
2360
+ drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
2361
+ }
2362
+ ctx.save();
2363
+ ctx.globalCompositeOperation = "destination-out";
2364
+ const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
2365
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
2366
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
2367
+ ctx.fillStyle = fadeGrad;
2368
+ ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
2369
+ ctx.restore();
1471
2370
  if (badgeRef.current) badgeRef.current.container.style.display = "none";
1472
2371
  rafRef.current = requestAnimationFrame(draw);
1473
2372
  return;
1474
2373
  }
1475
- const chartH = h - pad.top - pad.bottom;
1476
- const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
1477
- const isWindowTransitioning = transition.startMs > 0;
1478
- const rangeResult = updateRange(
1479
- computedRange,
1480
- rangeInitedRef.current,
1481
- targetMinRef.current,
1482
- targetMaxRef.current,
1483
- displayMinRef.current,
1484
- displayMaxRef.current,
1485
- isWindowTransitioning,
1486
- windowTransProgress,
1487
- transition,
1488
- adaptiveSpeed,
1489
- chartH,
1490
- dt
1491
- );
1492
- rangeInitedRef.current = rangeResult.rangeInited;
1493
- targetMinRef.current = rangeResult.targetMin;
1494
- targetMaxRef.current = rangeResult.targetMax;
1495
- displayMinRef.current = rangeResult.displayMin;
1496
- displayMaxRef.current = rangeResult.displayMax;
1497
- const { minVal, maxVal, valRange } = rangeResult;
1498
- const layout = {
1499
- w,
1500
- h,
1501
- pad,
1502
- chartW,
1503
- chartH,
1504
- leftEdge,
1505
- rightEdge,
1506
- minVal,
1507
- maxVal,
1508
- valRange,
1509
- toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
1510
- toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
1511
- };
1512
- const momentum = cfg.momentumOverride ?? detectMomentum(visible);
1513
- const hoverResult = updateHoverState(
1514
- hoverXRef.current,
1515
- pad,
1516
- w,
1517
- layout,
1518
- now,
1519
- visible,
1520
- scrubAmountRef.current,
1521
- lastHoverRef.current,
1522
- cfg,
1523
- noMotion,
1524
- leftEdge,
1525
- rightEdge,
1526
- chartW,
1527
- dt
1528
- );
1529
- scrubAmountRef.current = hoverResult.scrubAmount;
1530
- lastHoverRef.current = hoverResult.lastHover;
1531
- const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
1532
- const lookback = Math.min(5, visible.length - 1);
1533
- const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
1534
- const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
1535
- drawFrame(ctx, layout, cfg.palette, {
1536
- visible,
1537
- smoothValue,
1538
- now,
1539
- momentum,
1540
- arrowState: arrowStateRef.current,
1541
- showGrid: cfg.showGrid,
1542
- showMomentum: cfg.showMomentum,
1543
- showPulse: cfg.showPulse,
1544
- showFill: cfg.showFill,
1545
- referenceLine: cfg.referenceLine,
1546
- hoverX: drawHoverX,
1547
- hoverValue: drawHoverValue,
1548
- hoverTime: drawHoverTime,
1549
- scrubAmount: scrubAmountRef.current,
1550
- windowSecs,
1551
- formatValue: cfg.formatValue,
1552
- formatTime: cfg.formatTime,
1553
- gridState: gridStateRef.current,
1554
- timeAxisState: timeAxisStateRef.current,
1555
- dt,
1556
- targetWindowSecs: cfg.windowSecs,
1557
- tooltipY: cfg.tooltipY,
1558
- tooltipOutline: cfg.tooltipOutline,
1559
- orderbookData: cfg.orderbookData,
1560
- orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
1561
- particleState: cfg.degenOptions ? particleStateRef.current : void 0,
1562
- particleOptions: cfg.degenOptions,
1563
- swingMagnitude,
1564
- shakeState: cfg.degenOptions ? shakeStateRef.current : void 0
1565
- });
1566
- const badge = badgeRef.current;
1567
- if (badge) {
1568
- badgeYRef.current = updateBadgeDOM(
1569
- badge,
2374
+ if (isCandle) {
2375
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
2376
+ const now = hasData || chartReveal < 5e-3 ? Date.now() / 1e3 - timeDebtRef.current : frozenNowRef.current;
2377
+ const rawLive = pausedCandlesRef.current ? pausedLiveRef.current ?? void 0 : cfg.liveCandle;
2378
+ let effectiveLineData = pausedLineDataRef.current ?? cfg.lineData;
2379
+ let effectiveLineValue = pausedLineValueRef.current ?? cfg.lineValue;
2380
+ if (hasData && effectiveLineData && effectiveLineData.length > 0) {
2381
+ lastLineDataStashRef.current = effectiveLineData;
2382
+ lastLineValueStashRef.current = effectiveLineValue;
2383
+ }
2384
+ if (useStash && lastLineDataStashRef.current.length > 0) {
2385
+ effectiveLineData = lastLineDataStashRef.current;
2386
+ effectiveLineValue = lastLineValueStashRef.current;
2387
+ }
2388
+ const candleWidthSecs = cfg.candleWidth ?? 1;
2389
+ const cwt = candleWidthTransRef.current;
2390
+ let morphT = -1;
2391
+ let displayCandleWidth;
2392
+ if (cwt.startMs > 0) {
2393
+ const elapsed = now_ms - cwt.startMs;
2394
+ const t = Math.min(elapsed / CANDLE_WIDTH_TRANS_MS, 1);
2395
+ morphT = (1 - Math.cos(t * Math.PI)) / 2;
2396
+ displayCandleWidth = Math.exp(
2397
+ Math.log(cwt.fromWidth) + (Math.log(cwt.toWidth) - Math.log(cwt.fromWidth)) * morphT
2398
+ );
2399
+ if (t >= 1) {
2400
+ displayCandleWidth = cwt.toWidth;
2401
+ cwt.startMs = 0;
2402
+ morphT = -1;
2403
+ }
2404
+ } else {
2405
+ displayCandleWidth = cwt.toWidth;
2406
+ }
2407
+ if (candleWidthSecs !== cwt.toWidth) {
2408
+ cwt.oldCandles = prevCandleDataRef.current.candles;
2409
+ cwt.oldWidth = prevCandleDataRef.current.width;
2410
+ cwt.fromWidth = displayCandleWidth;
2411
+ cwt.toWidth = candleWidthSecs;
2412
+ cwt.startMs = now_ms;
2413
+ morphT = 0;
2414
+ cwt.rangeFromMin = displayMinRef.current;
2415
+ cwt.rangeFromMax = displayMaxRef.current;
2416
+ const curWindow = displayWindowRef.current;
2417
+ const re = now + curWindow * CANDLE_BUFFER;
2418
+ const le = re - curWindow;
2419
+ const targetVis = [];
2420
+ for (const c of effectiveCandles) {
2421
+ if (c.time + candleWidthSecs >= le && c.time <= re) targetVis.push(c);
2422
+ }
2423
+ if (rawLive) targetVis.push(rawLive);
2424
+ if (targetVis.length > 0) {
2425
+ const tr = computeCandleRange(targetVis);
2426
+ cwt.rangeToMin = tr.min;
2427
+ cwt.rangeToMax = tr.max;
2428
+ } else {
2429
+ cwt.rangeToMin = displayMinRef.current;
2430
+ cwt.rangeToMax = displayMaxRef.current;
2431
+ }
2432
+ }
2433
+ prevCandleDataRef.current = { candles: cfg.candles ?? [], width: candleWidthSecs };
2434
+ const lineModeProg = lineModeProgRef.current;
2435
+ const ldt = lineDensityTransRef.current;
2436
+ const hasTickData = effectiveLineData && effectiveLineData.length > 0;
2437
+ const densityTarget = cfg.lineMode && lineModeProg >= 0.3 && hasTickData ? 1 : 0;
2438
+ if (ldt.to !== densityTarget) {
2439
+ ldt.from = lineDensityProgRef.current;
2440
+ ldt.to = densityTarget;
2441
+ ldt.startMs = now_ms;
2442
+ }
2443
+ let lineDensityProg;
2444
+ if (ldt.startMs > 0) {
2445
+ const elapsed = now_ms - ldt.startMs;
2446
+ const t = Math.min(elapsed / LINE_DENSITY_MS, 1);
2447
+ lineDensityProg = ldt.from + (ldt.to - ldt.from) * (1 - (1 - t) * (1 - t));
2448
+ if (t >= 1) {
2449
+ lineDensityProg = ldt.to;
2450
+ ldt.startMs = 0;
2451
+ }
2452
+ } else {
2453
+ lineDensityProg = ldt.to;
2454
+ }
2455
+ lineDensityProgRef.current = lineDensityProg;
2456
+ const transition = windowTransitionRef.current;
2457
+ const windowResult = updateCandleWindowTransition(
2458
+ cfg.windowSecs,
2459
+ transition,
2460
+ displayWindowRef.current,
2461
+ displayMinRef.current,
2462
+ displayMaxRef.current,
2463
+ now_ms,
2464
+ now,
2465
+ effectiveCandles,
2466
+ rawLive,
2467
+ candleWidthSecs,
2468
+ CANDLE_BUFFER
2469
+ );
2470
+ displayWindowRef.current = windowResult.windowSecs;
2471
+ const windowSecs = windowResult.windowSecs;
2472
+ const windowTransProgress = windowResult.windowTransProgress;
2473
+ const isWindowTransitioning = transition.startMs > 0;
2474
+ const rightEdge = now + windowSecs * CANDLE_BUFFER;
2475
+ const leftEdge = rightEdge - windowSecs;
2476
+ let smoothLive;
2477
+ if (rawLive) {
2478
+ const prev = displayCandleRef.current;
2479
+ if (!prev || prev.time !== rawLive.time) {
2480
+ displayCandleRef.current = {
2481
+ time: rawLive.time,
2482
+ open: rawLive.open,
2483
+ high: rawLive.open,
2484
+ low: rawLive.open,
2485
+ close: rawLive.open
2486
+ };
2487
+ liveBirthAlphaRef.current = 0;
2488
+ } else {
2489
+ const dc2 = displayCandleRef.current;
2490
+ dc2.open = lerp(dc2.open, rawLive.open, CANDLE_LERP_SPEED, pausedDt);
2491
+ dc2.high = lerp(dc2.high, rawLive.high, CANDLE_LERP_SPEED, pausedDt);
2492
+ dc2.low = lerp(dc2.low, rawLive.low, CANDLE_LERP_SPEED, pausedDt);
2493
+ dc2.close = lerp(dc2.close, rawLive.close, CANDLE_LERP_SPEED, pausedDt);
2494
+ }
2495
+ liveBirthAlphaRef.current = lerp(liveBirthAlphaRef.current, 1, 0.2, pausedDt);
2496
+ if (liveBirthAlphaRef.current > 0.99) liveBirthAlphaRef.current = 1;
2497
+ const dc = displayCandleRef.current;
2498
+ const bullTarget = dc.close >= dc.open ? 1 : 0;
2499
+ liveBullRef.current = lerp(liveBullRef.current, bullTarget, 0.12, pausedDt);
2500
+ if (liveBullRef.current > 0.99) liveBullRef.current = 1;
2501
+ if (liveBullRef.current < 0.01) liveBullRef.current = 0;
2502
+ smoothLive = dc;
2503
+ } else {
2504
+ displayCandleRef.current = null;
2505
+ liveBirthAlphaRef.current = 1;
2506
+ liveBullRef.current = 0.5;
2507
+ }
2508
+ if (rawLive) {
2509
+ if (!closeLineSmoothInitedRef.current) {
2510
+ closeLineSmoothRef.current = rawLive.close;
2511
+ closeLineSmoothInitedRef.current = true;
2512
+ } else {
2513
+ closeLineSmoothRef.current = lerp(closeLineSmoothRef.current, rawLive.close, CLOSE_LINE_LERP_SPEED, pausedDt);
2514
+ const gap = Math.abs(closeLineSmoothRef.current - rawLive.close);
2515
+ const range = displayMaxRef.current - displayMinRef.current || 1;
2516
+ if (gap < range * 5e-4) closeLineSmoothRef.current = rawLive.close;
2517
+ }
2518
+ } else if (!useStash) {
2519
+ closeLineSmoothInitedRef.current = false;
2520
+ }
2521
+ if (rawLive) {
2522
+ if (!lineSmoothInitedRef.current) {
2523
+ lineSmoothCloseRef.current = rawLive.close;
2524
+ lineSmoothInitedRef.current = true;
2525
+ } else {
2526
+ const valGap = Math.abs(rawLive.close - lineSmoothCloseRef.current);
2527
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
2528
+ const gapRatio = Math.min(valGap / prevRange, 1);
2529
+ const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
2530
+ lineSmoothCloseRef.current = lerp(lineSmoothCloseRef.current, rawLive.close, adaptiveSpeed, pausedDt);
2531
+ if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineSmoothCloseRef.current = rawLive.close;
2532
+ }
2533
+ } else if (!useStash) {
2534
+ lineSmoothInitedRef.current = false;
2535
+ }
2536
+ if (effectiveLineValue !== void 0 && hasTickData) {
2537
+ if (!lineTickSmoothInitedRef.current) {
2538
+ lineTickSmoothRef.current = effectiveLineValue;
2539
+ lineTickSmoothInitedRef.current = true;
2540
+ } else {
2541
+ const valGap = Math.abs(effectiveLineValue - lineTickSmoothRef.current);
2542
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
2543
+ const gapRatio = Math.min(valGap / prevRange, 1);
2544
+ const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
2545
+ lineTickSmoothRef.current = lerp(lineTickSmoothRef.current, effectiveLineValue, adaptiveSpeed, pausedDt);
2546
+ if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineTickSmoothRef.current = effectiveLineValue;
2547
+ }
2548
+ } else if (!useStash) {
2549
+ lineTickSmoothInitedRef.current = false;
2550
+ }
2551
+ const visible = [];
2552
+ for (const c of effectiveCandles) {
2553
+ if (c.time + candleWidthSecs >= leftEdge && c.time <= rightEdge) visible.push(c);
2554
+ }
2555
+ if (smoothLive && smoothLive.time + displayCandleWidth >= leftEdge && smoothLive.time <= rightEdge) {
2556
+ visible.push(smoothLive);
2557
+ }
2558
+ let oldVisible = [];
2559
+ if (morphT >= 0 && cwt.oldCandles.length > 0) {
2560
+ for (const c of cwt.oldCandles) {
2561
+ if (c.time + cwt.oldWidth >= leftEdge && c.time <= rightEdge) oldVisible.push(c);
2562
+ }
2563
+ }
2564
+ if (hasData) {
2565
+ lastCandlesRef.current = visible;
2566
+ lastLiveRef.current = smoothLive ?? null;
2567
+ }
2568
+ const effectiveVisible = useStash ? lastCandlesRef.current : visible;
2569
+ const effectiveLive = useStash ? lastLiveRef.current ?? void 0 : smoothLive;
2570
+ const chartW = w - pad.left - pad.right;
2571
+ const computed = effectiveVisible.length > 0 ? computeCandleRange(effectiveVisible) : { min: displayMinRef.current, max: displayMaxRef.current };
2572
+ const rangeResult = updateCandleRange(
2573
+ computed,
2574
+ rangeInitedRef.current,
2575
+ displayMinRef.current,
2576
+ displayMaxRef.current,
2577
+ isWindowTransitioning,
2578
+ windowTransProgress,
2579
+ transition,
2580
+ chartH,
2581
+ pausedDt
2582
+ );
2583
+ if (morphT >= 0) {
2584
+ rangeResult.displayMin = cwt.rangeFromMin + (cwt.rangeToMin - cwt.rangeFromMin) * morphT;
2585
+ rangeResult.displayMax = cwt.rangeFromMax + (cwt.rangeToMax - cwt.rangeFromMax) * morphT;
2586
+ rangeResult.minVal = rangeResult.displayMin;
2587
+ rangeResult.maxVal = rangeResult.displayMax;
2588
+ rangeResult.valRange = rangeResult.displayMax - rangeResult.displayMin || 1e-3;
2589
+ }
2590
+ rangeInitedRef.current = rangeResult.rangeInited;
2591
+ displayMinRef.current = rangeResult.displayMin;
2592
+ displayMaxRef.current = rangeResult.displayMax;
2593
+ const { minVal, maxVal, valRange } = rangeResult;
2594
+ const layout = {
2595
+ w,
2596
+ h,
2597
+ pad,
2598
+ chartW,
2599
+ chartH,
2600
+ leftEdge,
2601
+ rightEdge,
2602
+ minVal,
2603
+ maxVal,
2604
+ valRange,
2605
+ toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
2606
+ toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
2607
+ };
2608
+ const hoverPx = hoverXRef.current;
2609
+ let hoveredCandle = null;
2610
+ let isActiveHover = false;
2611
+ if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
2612
+ hoveredCandle = candleAtX(effectiveVisible, hoverPx, displayCandleWidth, layout);
2613
+ if (hoveredCandle) isActiveHover = true;
2614
+ }
2615
+ const scrubTarget = isActiveHover ? 1 : 0;
2616
+ scrubAmountRef.current = lerp(scrubAmountRef.current, scrubTarget, 0.12, dt);
2617
+ if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
2618
+ if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
2619
+ const scrubAmount = scrubAmountRef.current;
2620
+ let drawHoverX = hoverPx;
2621
+ let drawHoverTime = 0;
2622
+ let drawHoverCandle = hoveredCandle;
2623
+ if (!isActiveHover && scrubAmount > 0 && lastHoverRef.current) {
2624
+ drawHoverX = lastHoverRef.current.x;
2625
+ drawHoverTime = lastHoverRef.current.time;
2626
+ drawHoverCandle = candleAtX(effectiveVisible, lastHoverRef.current.x, displayCandleWidth, layout);
2627
+ } else if (isActiveHover && hoverPx !== null) {
2628
+ drawHoverTime = layout.leftEdge + (hoverPx - pad.left) / chartW * (layout.rightEdge - layout.leftEdge);
2629
+ lastHoverRef.current = { x: hoverPx, value: hoveredCandle?.close ?? 0, time: drawHoverTime };
2630
+ }
2631
+ let drawCandles = effectiveVisible;
2632
+ let drawOldCandles = oldVisible;
2633
+ let drawLive = effectiveLive;
2634
+ if (lineModeProg > 0.01 && drawLive && lineSmoothInitedRef.current) {
2635
+ const blended = drawLive.close + (lineSmoothCloseRef.current - drawLive.close) * lineModeProg;
2636
+ drawLive = { ...drawLive, close: blended };
2637
+ const li = drawCandles.length - 1;
2638
+ if (li >= 0 && drawCandles[li].time === drawLive.time) {
2639
+ drawCandles = drawCandles.slice();
2640
+ drawCandles[li] = { ...drawCandles[li], close: blended };
2641
+ }
2642
+ }
2643
+ if (lineModeProg > 0.01 && lineModeProg < 0.99) {
2644
+ const collapseOHLC = (c) => {
2645
+ const inv = 1 - lineModeProg;
2646
+ return {
2647
+ time: c.time,
2648
+ open: c.close + (c.open - c.close) * inv,
2649
+ high: c.close + (c.high - c.close) * inv,
2650
+ low: c.close + (c.low - c.close) * inv,
2651
+ close: c.close
2652
+ };
2653
+ };
2654
+ drawCandles = drawCandles.map(collapseOHLC);
2655
+ if (drawOldCandles.length > 0) drawOldCandles = drawOldCandles.map(collapseOHLC);
2656
+ if (drawLive) drawLive = collapseOHLC(drawLive);
2657
+ }
2658
+ let lineVisible;
2659
+ let lineSmoothValue;
2660
+ if (effectiveLineData && effectiveLineData.length > 0 && (lineDensityProg > 0.01 || lineModeProg > 0.05)) {
2661
+ const closeRefs = [];
2662
+ for (const c of drawCandles) {
2663
+ closeRefs.push({ t: c.time + displayCandleWidth / 2, v: c.close });
2664
+ }
2665
+ if (drawLive) closeRefs.push({ t: now, v: drawLive.close });
2666
+ lineVisible = [];
2667
+ let refIdx = 0;
2668
+ for (const pt of effectiveLineData) {
2669
+ if (pt.time < leftEdge || pt.time > rightEdge) continue;
2670
+ while (refIdx < closeRefs.length - 2 && closeRefs[refIdx + 1].t < pt.time) refIdx++;
2671
+ let interpClose;
2672
+ if (closeRefs.length === 0) {
2673
+ interpClose = pt.value;
2674
+ } else if (closeRefs.length === 1 || pt.time <= closeRefs[0].t) {
2675
+ interpClose = closeRefs[0].v;
2676
+ } else if (refIdx >= closeRefs.length - 1) {
2677
+ interpClose = closeRefs[closeRefs.length - 1].v;
2678
+ } else {
2679
+ const a = closeRefs[refIdx];
2680
+ const b = closeRefs[refIdx + 1];
2681
+ const span = b.t - a.t;
2682
+ const frac = span > 0 ? Math.max(0, Math.min(1, (pt.time - a.t) / span)) : 0;
2683
+ interpClose = a.v + (b.v - a.v) * frac;
2684
+ }
2685
+ const blended = interpClose + (pt.value - interpClose) * lineDensityProg;
2686
+ lineVisible.push({ time: pt.time, value: blended });
2687
+ }
2688
+ const smoothTick = lineTickSmoothInitedRef.current ? lineTickSmoothRef.current : effectiveLineValue ?? effectiveLineData[effectiveLineData.length - 1].value;
2689
+ lineSmoothValue = lineSmoothCloseRef.current + (smoothTick - lineSmoothCloseRef.current) * lineDensityProg;
2690
+ } else {
2691
+ lineVisible = drawCandles.map((c) => ({
2692
+ time: c.time + displayCandleWidth / 2,
2693
+ value: c.close
2694
+ }));
2695
+ lineSmoothValue = lineSmoothInitedRef.current ? lineSmoothCloseRef.current : drawLive?.close ?? drawCandles[drawCandles.length - 1]?.close ?? 0;
2696
+ }
2697
+ if (chartReveal < 1 && lineVisible.length >= 2) {
2698
+ const firstTime = lineVisible[0].time;
2699
+ const windowSpan = rightEdge - leftEdge;
2700
+ if (firstTime - leftEdge > windowSpan * 0.05) {
2701
+ const firstVal = lineVisible[0].value;
2702
+ const step = windowSpan / 32;
2703
+ const padded = [];
2704
+ for (let t = leftEdge; t < firstTime - step * 0.5; t += step) {
2705
+ padded.push({ time: t, value: firstVal });
2706
+ }
2707
+ lineVisible = [...padded, ...lineVisible];
2708
+ }
2709
+ }
2710
+ drawCandleFrame(ctx, layout, cfg.palette, {
2711
+ candles: drawCandles,
2712
+ displayCandleWidth,
2713
+ oldCandles: drawOldCandles,
2714
+ oldWidth: cwt.oldWidth,
2715
+ morphT,
2716
+ liveCandle: drawLive,
2717
+ closePriceCandle: closeLineSmoothInitedRef.current && rawLive ? { ...rawLive, close: closeLineSmoothRef.current } : rawLive,
2718
+ liveTime: effectiveLive?.time ?? -1,
2719
+ liveBirthAlpha: liveBirthAlphaRef.current,
2720
+ liveBullBlend: liveBullRef.current,
2721
+ lineModeProg,
2722
+ chartReveal,
2723
+ now_ms,
2724
+ now,
2725
+ pauseProgress,
2726
+ showGrid: cfg.showGrid,
2727
+ scrubAmount,
2728
+ hoverX: drawHoverX,
2729
+ hoverValue: drawHoverCandle?.close ?? null,
2730
+ hoverTime: drawHoverTime,
2731
+ hoveredCandle: drawHoverCandle,
2732
+ formatValue: cfg.formatValue,
2733
+ formatTime: cfg.formatTime,
2734
+ gridState: gridStateRef.current,
2735
+ timeAxisState: timeAxisStateRef.current,
2736
+ dt: pausedDt,
2737
+ targetWindowSecs: cfg.windowSecs,
2738
+ tooltipY: cfg.tooltipY,
2739
+ tooltipOutline: cfg.tooltipOutline,
2740
+ lineVisible,
2741
+ lineSmoothValue,
2742
+ emptyText: cfg.emptyText,
2743
+ loadingAlpha,
2744
+ // Show empty overlay when not loading AND loadingAlpha has fully
2745
+ // decayed. This prevents the gradient gap from flashing during
2746
+ // loading→live (where loadingAlpha starts at ~1), while still
2747
+ // allowing smooth fade-out during empty→live (loadingAlpha is 0).
2748
+ showEmptyOverlay: !(cfg.loading ?? false) && loadingAlpha < 0.01
2749
+ });
2750
+ if (badgeRef.current) {
2751
+ if (lineModeProg > 0.5 && cfg.showBadge) {
2752
+ const momentum = detectMomentum(lineVisible);
2753
+ badgeYRef.current = updateBadgeDOM(
2754
+ badgeRef.current,
2755
+ cfg,
2756
+ lineSmoothValue,
2757
+ layout,
2758
+ momentum,
2759
+ badgeYRef.current,
2760
+ badgeColorRef.current,
2761
+ isWindowTransitioning,
2762
+ noMotion,
2763
+ ctx,
2764
+ pausedDt,
2765
+ chartReveal
2766
+ );
2767
+ const badgeFade = (lineModeProg - 0.5) * 2;
2768
+ if (badgeRef.current.container.style.display !== "none") {
2769
+ const base = badgeRef.current.container.style.opacity ? parseFloat(badgeRef.current.container.style.opacity) : 1;
2770
+ badgeRef.current.container.style.opacity = String(
2771
+ base * badgeFade * (1 - pauseProgress)
2772
+ );
2773
+ }
2774
+ } else {
2775
+ badgeRef.current.container.style.display = "none";
2776
+ }
2777
+ }
2778
+ } else {
2779
+ const effectivePoints = useStash ? lastDataRef.current : points;
2780
+ const adaptiveSpeed = computeAdaptiveSpeed(
2781
+ cfg.value,
2782
+ displayValueRef.current,
2783
+ displayMinRef.current,
2784
+ displayMaxRef.current,
2785
+ cfg.lerpSpeed,
2786
+ noMotion
2787
+ );
2788
+ if (!useStash) {
2789
+ displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, pausedDt);
2790
+ if (pauseProgress < 0.5) {
2791
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
2792
+ if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
2793
+ displayValueRef.current = cfg.value;
2794
+ }
2795
+ }
2796
+ }
2797
+ const smoothValue = displayValueRef.current;
2798
+ const chartW = w - pad.left - pad.right;
2799
+ const needsArrowRoom = cfg.showMomentum;
2800
+ const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
2801
+ const transition = windowTransitionRef.current;
2802
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
2803
+ const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
2804
+ const windowResult = updateWindowTransition(
1570
2805
  cfg,
2806
+ transition,
2807
+ displayWindowRef.current,
2808
+ displayMinRef.current,
2809
+ displayMaxRef.current,
2810
+ noMotion,
2811
+ now_ms,
2812
+ now,
2813
+ effectivePoints,
1571
2814
  smoothValue,
1572
- layout,
1573
- momentum,
1574
- badgeYRef.current,
1575
- badgeColorRef.current,
2815
+ buffer
2816
+ );
2817
+ displayWindowRef.current = windowResult.windowSecs;
2818
+ const windowSecs = windowResult.windowSecs;
2819
+ const windowTransProgress = windowResult.windowTransProgress;
2820
+ const rightEdge = now + windowSecs * buffer;
2821
+ const leftEdge = rightEdge - windowSecs;
2822
+ const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
2823
+ const visible = [];
2824
+ for (const p of effectivePoints) {
2825
+ if (p.time >= leftEdge - 2 && p.time <= filterRight) {
2826
+ visible.push(p);
2827
+ }
2828
+ }
2829
+ if (visible.length < 2) {
2830
+ if (badgeRef.current) badgeRef.current.container.style.display = "none";
2831
+ rafRef.current = requestAnimationFrame(draw);
2832
+ return;
2833
+ }
2834
+ const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
2835
+ const isWindowTransitioning = transition.startMs > 0;
2836
+ const rangeResult = updateRange(
2837
+ computedRange,
2838
+ rangeInitedRef.current,
2839
+ targetMinRef.current,
2840
+ targetMaxRef.current,
2841
+ displayMinRef.current,
2842
+ displayMaxRef.current,
1576
2843
  isWindowTransitioning,
2844
+ windowTransProgress,
2845
+ transition,
2846
+ adaptiveSpeed,
2847
+ chartH,
2848
+ pausedDt
2849
+ );
2850
+ rangeInitedRef.current = rangeResult.rangeInited;
2851
+ targetMinRef.current = rangeResult.targetMin;
2852
+ targetMaxRef.current = rangeResult.targetMax;
2853
+ displayMinRef.current = rangeResult.displayMin;
2854
+ displayMaxRef.current = rangeResult.displayMax;
2855
+ const { minVal, maxVal, valRange } = rangeResult;
2856
+ const layout = {
2857
+ w,
2858
+ h,
2859
+ pad,
2860
+ chartW,
2861
+ chartH,
2862
+ leftEdge,
2863
+ rightEdge,
2864
+ minVal,
2865
+ maxVal,
2866
+ valRange,
2867
+ toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
2868
+ toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
2869
+ };
2870
+ const momentum = cfg.momentumOverride ?? detectMomentum(visible);
2871
+ const hoverResult = updateHoverState(
2872
+ hoverXRef.current,
2873
+ pad,
2874
+ w,
2875
+ layout,
2876
+ now,
2877
+ visible,
2878
+ scrubAmountRef.current,
2879
+ lastHoverRef.current,
2880
+ cfg,
1577
2881
  noMotion,
1578
- ctx,
2882
+ leftEdge,
2883
+ rightEdge,
2884
+ chartW,
1579
2885
  dt
1580
2886
  );
1581
- }
1582
- const valEl = cfg.valueDisplayRef?.current;
1583
- if (valEl) {
1584
- const displayVal = cfg.valueMomentumColor ? Math.abs(smoothValue) : smoothValue;
1585
- valEl.textContent = cfg.formatValue(displayVal);
1586
- if (cfg.valueMomentumColor) {
1587
- const mc = momentum === "up" ? "#22c55e" : momentum === "down" ? "#ef4444" : "";
1588
- if (mc) valEl.style.color = mc;
1589
- else valEl.style.removeProperty("color");
2887
+ scrubAmountRef.current = hoverResult.scrubAmount;
2888
+ lastHoverRef.current = hoverResult.lastHover;
2889
+ const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
2890
+ const lookback = Math.min(5, visible.length - 1);
2891
+ const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
2892
+ const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
2893
+ drawFrame(ctx, layout, cfg.palette, {
2894
+ visible,
2895
+ smoothValue,
2896
+ now,
2897
+ momentum,
2898
+ arrowState: arrowStateRef.current,
2899
+ showGrid: cfg.showGrid,
2900
+ showMomentum: cfg.showMomentum,
2901
+ showPulse: cfg.showPulse,
2902
+ showFill: cfg.showFill,
2903
+ referenceLine: cfg.referenceLine,
2904
+ hoverX: drawHoverX,
2905
+ hoverValue: drawHoverValue,
2906
+ hoverTime: drawHoverTime,
2907
+ scrubAmount: scrubAmountRef.current,
2908
+ windowSecs,
2909
+ formatValue: cfg.formatValue,
2910
+ formatTime: cfg.formatTime,
2911
+ gridState: gridStateRef.current,
2912
+ timeAxisState: timeAxisStateRef.current,
2913
+ dt,
2914
+ targetWindowSecs: cfg.windowSecs,
2915
+ tooltipY: cfg.tooltipY,
2916
+ tooltipOutline: cfg.tooltipOutline,
2917
+ orderbookData: cfg.orderbookData,
2918
+ orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
2919
+ particleState: cfg.degenOptions ? particleStateRef.current : void 0,
2920
+ particleOptions: cfg.degenOptions,
2921
+ swingMagnitude,
2922
+ shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
2923
+ chartReveal,
2924
+ pauseProgress,
2925
+ now_ms
2926
+ });
2927
+ const bgAlpha = 1 - chartReveal;
2928
+ if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
2929
+ const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
2930
+ if (bgEmptyAlpha > 0.01) {
2931
+ drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
2932
+ }
2933
+ }
2934
+ const badge = badgeRef.current;
2935
+ if (badge) {
2936
+ badgeYRef.current = updateBadgeDOM(
2937
+ badge,
2938
+ cfg,
2939
+ smoothValue,
2940
+ layout,
2941
+ momentum,
2942
+ badgeYRef.current,
2943
+ badgeColorRef.current,
2944
+ isWindowTransitioning,
2945
+ noMotion,
2946
+ ctx,
2947
+ pausedDt,
2948
+ chartReveal
2949
+ );
2950
+ if (pauseProgress > 0.01 && badge.container.style.display !== "none") {
2951
+ const base = badge.container.style.opacity ? parseFloat(badge.container.style.opacity) : 1;
2952
+ badge.container.style.opacity = String(base * (1 - pauseProgress));
2953
+ }
2954
+ }
2955
+ const valEl = cfg.valueDisplayRef?.current;
2956
+ if (valEl) {
2957
+ const displayVal = cfg.valueMomentumColor ? Math.abs(smoothValue) : smoothValue;
2958
+ valEl.textContent = cfg.formatValue(displayVal);
2959
+ if (cfg.valueMomentumColor) {
2960
+ const mc = momentum === "up" ? "#22c55e" : momentum === "down" ? "#ef4444" : "";
2961
+ if (mc) valEl.style.color = mc;
2962
+ else valEl.style.removeProperty("color");
2963
+ }
1590
2964
  }
1591
2965
  }
1592
2966
  rafRef.current = requestAnimationFrame(draw);
@@ -1618,6 +2992,9 @@ function Liveline({
1618
2992
  momentum = true,
1619
2993
  fill = true,
1620
2994
  scrub = true,
2995
+ loading = false,
2996
+ paused = false,
2997
+ emptyText,
1621
2998
  exaggerate = false,
1622
2999
  degen: degenProp,
1623
3000
  badgeTail = true,
@@ -1638,6 +3015,14 @@ function Liveline({
1638
3015
  onHover,
1639
3016
  cursor = "crosshair",
1640
3017
  pulse = true,
3018
+ mode = "line",
3019
+ candles,
3020
+ candleWidth,
3021
+ liveCandle,
3022
+ lineMode,
3023
+ lineData,
3024
+ lineValue,
3025
+ onModeChange,
1641
3026
  className,
1642
3027
  style
1643
3028
  }) {
@@ -1647,7 +3032,10 @@ function Liveline({
1647
3032
  const windowBarRef = (0, import_react2.useRef)(null);
1648
3033
  const windowBtnRefs = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1649
3034
  const [indicatorStyle, setIndicatorStyle] = (0, import_react2.useState)(null);
1650
- const palette = resolveTheme(color, theme);
3035
+ const modeBarRef = (0, import_react2.useRef)(null);
3036
+ const modeBtnRefs = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
3037
+ const [modeIndicatorStyle, setModeIndicatorStyle] = (0, import_react2.useState)(null);
3038
+ const palette = (0, import_react2.useMemo)(() => resolveTheme(color, theme), [color, theme]);
1651
3039
  const isDark = theme === "dark";
1652
3040
  const showMomentum = momentum !== false;
1653
3041
  const momentumOverride = typeof momentum === "string" ? momentum : void 0;
@@ -1676,6 +3064,20 @@ function Liveline({
1676
3064
  });
1677
3065
  }
1678
3066
  }, [activeWindowSecs, windows]);
3067
+ const activeMode = lineMode ? "line" : "candle";
3068
+ (0, import_react2.useLayoutEffect)(() => {
3069
+ if (!onModeChange) return;
3070
+ const btn = modeBtnRefs.current.get(activeMode);
3071
+ const bar = modeBarRef.current;
3072
+ if (btn && bar) {
3073
+ const barRect = bar.getBoundingClientRect();
3074
+ const btnRect = btn.getBoundingClientRect();
3075
+ setModeIndicatorStyle({
3076
+ left: btnRect.left - barRect.left,
3077
+ width: btnRect.width
3078
+ });
3079
+ }
3080
+ }, [activeMode, onModeChange]);
1679
3081
  const ws = windowStyle ?? "default";
1680
3082
  useLivelineEngine(canvasRef, containerRef, {
1681
3083
  data,
@@ -1703,9 +3105,21 @@ function Liveline({
1703
3105
  tooltipOutline,
1704
3106
  valueMomentumColor,
1705
3107
  valueDisplayRef: showValue ? valueDisplayRef : void 0,
1706
- orderbookData: orderbook
3108
+ orderbookData: orderbook,
3109
+ loading,
3110
+ paused,
3111
+ emptyText,
3112
+ mode,
3113
+ candles,
3114
+ candleWidth,
3115
+ liveCandle,
3116
+ lineMode,
3117
+ lineData,
3118
+ lineValue
1707
3119
  });
1708
3120
  const cursorStyle = scrub ? cursor : "default";
3121
+ const activeColor = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)";
3122
+ const inactiveColor = isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.22)";
1709
3123
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1710
3124
  showValue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1711
3125
  "span",
@@ -1725,68 +3139,193 @@ function Liveline({
1725
3139
  }
1726
3140
  }
1727
3141
  ),
1728
- windows && windows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1729
- "div",
1730
- {
1731
- ref: windowBarRef,
1732
- style: {
1733
- position: "relative",
1734
- display: "inline-flex",
1735
- gap: ws === "text" ? 4 : 2,
1736
- background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
1737
- borderRadius: ws === "rounded" ? 999 : 6,
1738
- padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2,
1739
- marginBottom: 6,
1740
- marginLeft: pad.left
1741
- },
1742
- children: [
1743
- ws !== "text" && indicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
1744
- position: "absolute",
1745
- top: ws === "rounded" ? 3 : 2,
1746
- left: indicatorStyle.left,
1747
- width: indicatorStyle.width,
1748
- height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
1749
- background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
1750
- borderRadius: ws === "rounded" ? 999 : 4,
1751
- transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
1752
- pointerEvents: "none"
1753
- } }),
1754
- windows.map((w) => {
1755
- const isActive = w.secs === activeWindowSecs;
1756
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3142
+ (windows && windows.length > 0 || onModeChange) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
3143
+ windows && windows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3144
+ "div",
3145
+ {
3146
+ ref: windowBarRef,
3147
+ style: {
3148
+ position: "relative",
3149
+ display: "inline-flex",
3150
+ gap: ws === "text" ? 4 : 2,
3151
+ background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3152
+ borderRadius: ws === "rounded" ? 999 : 6,
3153
+ padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
3154
+ },
3155
+ children: [
3156
+ ws !== "text" && indicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
3157
+ position: "absolute",
3158
+ top: ws === "rounded" ? 3 : 2,
3159
+ left: indicatorStyle.left,
3160
+ width: indicatorStyle.width,
3161
+ height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
3162
+ background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3163
+ borderRadius: ws === "rounded" ? 999 : 4,
3164
+ transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
3165
+ pointerEvents: "none"
3166
+ } }),
3167
+ windows.map((w) => {
3168
+ const isActive = w.secs === activeWindowSecs;
3169
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3170
+ "button",
3171
+ {
3172
+ ref: (el) => {
3173
+ if (el) windowBtnRefs.current.set(w.secs, el);
3174
+ else windowBtnRefs.current.delete(w.secs);
3175
+ },
3176
+ onClick: () => {
3177
+ setActiveWindowSecs(w.secs);
3178
+ onWindowChange?.(w.secs);
3179
+ },
3180
+ style: {
3181
+ position: "relative",
3182
+ zIndex: 1,
3183
+ fontSize: 11,
3184
+ padding: ws === "text" ? "2px 6px" : "3px 10px",
3185
+ borderRadius: ws === "rounded" ? 999 : 4,
3186
+ border: "none",
3187
+ cursor: "pointer",
3188
+ fontFamily: "system-ui, -apple-system, sans-serif",
3189
+ fontWeight: isActive ? 600 : 400,
3190
+ background: "transparent",
3191
+ color: isActive ? activeColor : inactiveColor,
3192
+ transition: "color 0.2s, background 0.15s",
3193
+ lineHeight: "16px"
3194
+ },
3195
+ children: w.label
3196
+ },
3197
+ w.secs
3198
+ );
3199
+ })
3200
+ ]
3201
+ }
3202
+ ),
3203
+ onModeChange && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
3204
+ "div",
3205
+ {
3206
+ ref: modeBarRef,
3207
+ style: {
3208
+ position: "relative",
3209
+ display: "inline-flex",
3210
+ gap: 2,
3211
+ background: isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3212
+ borderRadius: 6,
3213
+ padding: 2
3214
+ },
3215
+ children: [
3216
+ modeIndicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
3217
+ position: "absolute",
3218
+ top: 2,
3219
+ left: modeIndicatorStyle.left,
3220
+ width: modeIndicatorStyle.width,
3221
+ height: "calc(100% - 4px)",
3222
+ background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3223
+ borderRadius: 4,
3224
+ transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
3225
+ pointerEvents: "none"
3226
+ } }),
3227
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1757
3228
  "button",
1758
3229
  {
1759
3230
  ref: (el) => {
1760
- if (el) windowBtnRefs.current.set(w.secs, el);
1761
- else windowBtnRefs.current.delete(w.secs);
3231
+ if (el) modeBtnRefs.current.set("line", el);
3232
+ else modeBtnRefs.current.delete("line");
3233
+ },
3234
+ onClick: () => onModeChange("line"),
3235
+ style: {
3236
+ position: "relative",
3237
+ zIndex: 1,
3238
+ padding: "5px 7px",
3239
+ borderRadius: 4,
3240
+ border: "none",
3241
+ cursor: "pointer",
3242
+ background: "transparent",
3243
+ display: "flex",
3244
+ alignItems: "center"
1762
3245
  },
1763
- onClick: () => {
1764
- setActiveWindowSecs(w.secs);
1765
- onWindowChange?.(w.secs);
3246
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3247
+ "path",
3248
+ {
3249
+ d: "M1 8.5C2.5 8.5 3 4 5.5 4S7.5 7 8.5 7C9.5 7 10 3.5 11 3.5",
3250
+ stroke: activeMode === "line" ? activeColor : inactiveColor,
3251
+ strokeWidth: activeMode === "line" ? 1.5 : 1.2,
3252
+ strokeLinecap: "round",
3253
+ fill: "none"
3254
+ }
3255
+ ) })
3256
+ }
3257
+ ),
3258
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3259
+ "button",
3260
+ {
3261
+ ref: (el) => {
3262
+ if (el) modeBtnRefs.current.set("candle", el);
3263
+ else modeBtnRefs.current.delete("candle");
1766
3264
  },
3265
+ onClick: () => onModeChange("candle"),
1767
3266
  style: {
1768
3267
  position: "relative",
1769
3268
  zIndex: 1,
1770
- fontSize: 11,
1771
- padding: ws === "text" ? "2px 6px" : "3px 10px",
1772
- borderRadius: ws === "rounded" ? 999 : 4,
3269
+ padding: "5px 7px",
3270
+ borderRadius: 4,
1773
3271
  border: "none",
1774
3272
  cursor: "pointer",
1775
- fontFamily: "system-ui, -apple-system, sans-serif",
1776
- fontWeight: isActive ? 600 : 400,
1777
3273
  background: "transparent",
1778
- color: isActive ? isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)" : isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.22)",
1779
- transition: "color 0.2s, background 0.15s",
1780
- lineHeight: "16px"
3274
+ display: "flex",
3275
+ alignItems: "center"
1781
3276
  },
1782
- children: w.label
1783
- },
1784
- w.secs
1785
- );
1786
- })
1787
- ]
1788
- }
1789
- ),
3277
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: [
3278
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3279
+ "line",
3280
+ {
3281
+ x1: "3.5",
3282
+ y1: "1",
3283
+ x2: "3.5",
3284
+ y2: "11",
3285
+ stroke: activeMode === "candle" ? activeColor : inactiveColor,
3286
+ strokeWidth: "1"
3287
+ }
3288
+ ),
3289
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3290
+ "rect",
3291
+ {
3292
+ x: "2",
3293
+ y: "3",
3294
+ width: "3",
3295
+ height: "5",
3296
+ rx: "0.5",
3297
+ fill: activeMode === "candle" ? activeColor : inactiveColor
3298
+ }
3299
+ ),
3300
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3301
+ "line",
3302
+ {
3303
+ x1: "8.5",
3304
+ y1: "2",
3305
+ x2: "8.5",
3306
+ y2: "10",
3307
+ stroke: activeMode === "candle" ? activeColor : inactiveColor,
3308
+ strokeWidth: "1"
3309
+ }
3310
+ ),
3311
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3312
+ "rect",
3313
+ {
3314
+ x: "7",
3315
+ y: "4",
3316
+ width: "3",
3317
+ height: "4",
3318
+ rx: "0.5",
3319
+ fill: activeMode === "candle" ? activeColor : inactiveColor
3320
+ }
3321
+ )
3322
+ ] })
3323
+ }
3324
+ )
3325
+ ]
3326
+ }
3327
+ )
3328
+ ] }),
1790
3329
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1791
3330
  "div",
1792
3331
  {
@@ -1809,7 +3348,64 @@ function Liveline({
1809
3348
  )
1810
3349
  ] });
1811
3350
  }
3351
+
3352
+ // src/LivelineTransition.tsx
3353
+ var import_react3 = require("react");
3354
+ var import_jsx_runtime2 = require("react/jsx-runtime");
3355
+ function LivelineTransition({
3356
+ active,
3357
+ children,
3358
+ duration = 300,
3359
+ className,
3360
+ style
3361
+ }) {
3362
+ const childArray = Array.isArray(children) ? children : [children];
3363
+ const [mounted, setMounted] = (0, import_react3.useState)(() => /* @__PURE__ */ new Set([active]));
3364
+ const [visible, setVisible] = (0, import_react3.useState)(active);
3365
+ const prevRef = (0, import_react3.useRef)(active);
3366
+ (0, import_react3.useEffect)(() => {
3367
+ if (active === prevRef.current) return () => {
3368
+ };
3369
+ const oldKey = prevRef.current;
3370
+ prevRef.current = active;
3371
+ setMounted((prev) => /* @__PURE__ */ new Set([...prev, active]));
3372
+ let raf1 = requestAnimationFrame(() => {
3373
+ raf1 = requestAnimationFrame(() => setVisible(active));
3374
+ });
3375
+ const timer = setTimeout(() => {
3376
+ setMounted((prev) => {
3377
+ const next = new Set(prev);
3378
+ next.delete(oldKey);
3379
+ return next;
3380
+ });
3381
+ }, duration + 50);
3382
+ return () => {
3383
+ cancelAnimationFrame(raf1);
3384
+ clearTimeout(timer);
3385
+ };
3386
+ }, [active, duration]);
3387
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, style: { position: "relative", width: "100%", height: "100%", ...style }, children: childArray.map((child) => {
3388
+ const key = String(child.key ?? "");
3389
+ if (!mounted.has(key)) return null;
3390
+ const isActive = key === visible;
3391
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3392
+ "div",
3393
+ {
3394
+ style: {
3395
+ position: "absolute",
3396
+ inset: 0,
3397
+ opacity: isActive ? 1 : 0,
3398
+ transition: `opacity ${duration}ms ease`,
3399
+ pointerEvents: isActive ? "auto" : "none"
3400
+ },
3401
+ children: child
3402
+ },
3403
+ key
3404
+ );
3405
+ }) });
3406
+ }
1812
3407
  // Annotate the CommonJS export names for ESM import in node:
1813
3408
  0 && (module.exports = {
1814
- Liveline
3409
+ Liveline,
3410
+ LivelineTransition
1815
3411
  });