POLS / solana-clock /js /chart.js
Ethscriptions's picture
solana-clock: 动画引擎 + 15 个场景 + 播放器
d45e47b verified
Raw History Blame Contribute Delete
5.41 kB
// Real SOL price chart renderer (data snapshot from Surf API)
import { SOL_PRICES, SOL_T0 } from './data-sol.js';
import { clamp, lerp, glow, text, hexA, fmtUSD, F_MONO, GREEN, PURPLE, RED, WHITE } from './draw.js';
export const idx = (date) => clamp(Math.round((Date.parse(date + 'T00:00:00Z') / 1000 - SOL_T0) / 86400), 0, SOL_PRICES.length - 1);
export const price = (i) => SOL_PRICES[clamp(Math.round(i), 0, SOL_PRICES.length - 1)];
export const dateOf = (i) => new Date((SOL_T0 + Math.round(i) * 86400) * 1000).toISOString().slice(0, 10);
export const LAST = SOL_PRICES.length - 1;
/** interpolated price at fractional index */
export function priceF(f) {
const i = Math.floor(f), k = f - i;
return lerp(price(i), price(i + 1), k);
}
/**
* Draw a chart. rect {x,y,w,h}. from/to: day indices. prog: 0..1 drawn fraction.
* Returns head {x,y,v,i,slope}.
*/
export function drawChart(ctx, rect, o) {
const { from, to, prog = 1, log = false, color = GREEN, color2 = PURPLE, alpha = 1, grid = true, yMin, yMax, lw = 5, fill = true, head = true, headLabel = true, dates = true, gridVals } = o;
const n = to - from;
let lo = Infinity, hi = -Infinity;
for (let i = from; i <= to; i++) { const v = price(i); if (v < lo) lo = v; if (v > hi) hi = v; }
lo = yMin ?? lo * (log ? .8 : .9); hi = yMax ?? hi * 1.08;
if (!log) lo = yMin ?? Math.max(0, lo - (hi - lo) * .05);
const Y = v => log ? rect.y + rect.h - (Math.log(v) - Math.log(lo)) / (Math.log(hi) - Math.log(lo)) * rect.h : rect.y + rect.h - (v - lo) / (hi - lo) * rect.h;
const X = i => rect.x + (i - from) / n * rect.w;
ctx.save(); ctx.globalAlpha *= alpha;
if (grid) {
const vals = gridVals || (log ? [1, 10, 100] : niceTicks(lo, hi));
ctx.lineWidth = 1;
for (const v of vals) {
if (v < lo || v > hi) continue;
const y = Y(v);
ctx.strokeStyle = 'rgba(255,255,255,.09)'; ctx.setLineDash([6, 8]); ctx.beginPath(); ctx.moveTo(rect.x, y); ctx.lineTo(rect.x + rect.w, y); ctx.stroke(); ctx.setLineDash([]);
text(ctx, fmtUSD(v), rect.x - 12, y, { size: 20, fam: F_MONO, weight: 500, color: 'rgba(255,255,255,.45)', align: 'right' });
}
if (dates) {
const y0 = new Date((SOL_T0 + from * 86400) * 1000).getUTCFullYear(), y1 = new Date((SOL_T0 + to * 86400) * 1000).getUTCFullYear();
for (let yr = y0; yr <= y1 + 1; yr++) {
const i = idx(yr + '-01-01'); if (i <= from || i >= to) continue;
const x = X(i);
ctx.strokeStyle = 'rgba(255,255,255,.08)'; ctx.beginPath(); ctx.moveTo(x, rect.y); ctx.lineTo(x, rect.y + rect.h); ctx.stroke();
text(ctx, String(yr), x, rect.y + rect.h + 22, { size: 20, fam: F_MONO, weight: 600, color: 'rgba(255,255,255,.5)', align: 'center' });
}
}
}
const endF = from + n * clamp(prog);
if (endF > from) {
const step = Math.max(1, Math.floor(n / 900));
ctx.beginPath();
let first = true, lx = 0, ly = 0;
for (let i = from; i <= Math.floor(endF); i += step) { const x = X(i), y = Y(price(i)); if (first) { ctx.moveTo(x, y); first = false; } else ctx.lineTo(x, y); lx = x; ly = y; }
const hv = priceF(endF), hx = X(endF), hy = Y(hv); ctx.lineTo(hx, hy);
const g = ctx.createLinearGradient(rect.x, 0, rect.x + rect.w, 0); g.addColorStop(0, color2); g.addColorStop(1, color);
// fill
if (fill) {
ctx.save(); ctx.lineTo(hx, rect.y + rect.h); ctx.lineTo(X(from), rect.y + rect.h); ctx.closePath();
const fg = ctx.createLinearGradient(0, rect.y, 0, rect.y + rect.h); fg.addColorStop(0, hexA(color, .28)); fg.addColorStop(1, hexA(color, 0));
ctx.fillStyle = fg; ctx.fill(); ctx.restore();
// re-trace stroke path
ctx.beginPath(); first = true;
for (let i = from; i <= Math.floor(endF); i += step) { const x = X(i), y = Y(price(i)); if (first) { ctx.moveTo(x, y); first = false; } else ctx.lineTo(x, y); }
ctx.lineTo(hx, hy);
}
ctx.lineJoin = 'round'; ctx.lineCap = 'round';
ctx.strokeStyle = hexA(color, .25); ctx.lineWidth = lw * 3.2; ctx.stroke();
ctx.strokeStyle = g; ctx.lineWidth = lw; ctx.stroke();
void lx; void ly;
const sl = (Y(priceF(Math.min(endF + 3, to))) - Y(priceF(Math.max(endF - 3, from)))) / (X(Math.min(endF + 3, to)) - X(Math.max(endF - 3, from)) || 1);
if (head && prog > 0) {
glow(ctx, hx, hy, 60, color, .9);
ctx.fillStyle = WHITE; ctx.beginPath(); ctx.arc(hx, hy, 8, 0, 7); ctx.fill();
if (headLabel) {
const right = hx < rect.x + rect.w * .75;
text(ctx, fmtUSD(hv), hx + (right ? 22 : -22), hy - 34, { size: 34, fam: F_MONO, weight: 800, color: WHITE, align: right ? 'left' : 'right', stroke: 6, strokeColor: 'rgba(0,0,0,.7)' });
text(ctx, dateOf(endF), hx + (right ? 22 : -22), hy - 2, { size: 20, fam: F_MONO, weight: 500, color: 'rgba(255,255,255,.6)', align: right ? 'left' : 'right' });
}
}
ctx.restore();
return { x: hx, y: hy, v: hv, i: endF, slope: sl, X, Y };
}
ctx.restore();
return { x: X(from), y: Y(price(from)), v: price(from), i: from, slope: 0, X, Y };
}
function niceTicks(lo, hi) {
const span = hi - lo, raw = span / 4, mag = Math.pow(10, Math.floor(Math.log10(raw)));
const step = [1, 2, 2.5, 5, 10].map(k => k * mag).find(s => span / s <= 5);
const out = []; for (let v = Math.ceil(lo / step) * step; v <= hi; v += step) out.push(+v.toFixed(4));
return out;
}
export const RED_C = RED;