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/README.md +81 -2
- package/dist/index.cjs +1893 -297
- package/dist/index.d.cts +45 -3
- package/dist/index.d.ts +45 -3
- package/dist/index.js +1892 -297
- package/package.json +5 -3
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
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
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] =
|
|
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
|
|
133
|
+
const start = Math.max(0, points.length - lookback);
|
|
127
134
|
let min = Infinity;
|
|
128
135
|
let max = -Infinity;
|
|
129
|
-
for (
|
|
130
|
-
|
|
131
|
-
if (
|
|
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
|
|
136
|
-
const first =
|
|
137
|
-
const last =
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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 =
|
|
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 =
|
|
494
|
+
const outerRgb = parseColorRgb(palette.badgeOuterBg);
|
|
426
495
|
ctx.save();
|
|
427
|
-
ctx.globalAlpha =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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,
|
|
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 /
|
|
616
|
-
let interval = niceTimeInterval(
|
|
617
|
-
while (interval * targetPxPerSec < 60 && interval <
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1421
|
-
if (
|
|
1422
|
-
if (
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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
|
|
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
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
const
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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 (
|
|
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
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
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
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
-
|
|
2882
|
+
leftEdge,
|
|
2883
|
+
rightEdge,
|
|
2884
|
+
chartW,
|
|
1579
2885
|
dt
|
|
1580
2886
|
);
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
|
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
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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)
|
|
1761
|
-
else
|
|
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
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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
|
-
|
|
1771
|
-
|
|
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
|
-
|
|
1779
|
-
|
|
1780
|
-
lineHeight: "16px"
|
|
3274
|
+
display: "flex",
|
|
3275
|
+
alignItems: "center"
|
|
1781
3276
|
},
|
|
1782
|
-
children:
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
});
|