diff --git a/docs/implementation.md b/docs/implementation.md index 44581b2..cf994bc 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -1405,5 +1405,5 @@ Schwab still prints many forming ticks per frame. Those now coalesce to one **Animate current price** was two infinite CSS animations across the full plot: `background-position` on a masked `repeating-linear-gradient`, plus a `color-mix` shine. That cannot run on the compositor. Turning it off dropped -tab CPU from ~20% to ~5%. The cue is now a static dash and one `transform` -shine. +tab CPU from ~20% to ~5%. The cue is now a 20px frame around the right-hand +price label, opacity only. diff --git a/static/chart.js b/static/chart.js index b5e204d..9913cf0 100644 --- a/static/chart.js +++ b/static/chart.js @@ -495,7 +495,7 @@ class ConfluenceChart { this.currentPricePulse = document.createElement('div'); this.currentPricePulse.className = 'current-price-pulse'; this.currentPricePulse.hidden = true; - this.overlayLayer.appendChild(this.currentPricePulse); + el.appendChild(this.currentPricePulse); this.projectionDiagnostic = document.createElement('pre'); this.projectionDiagnostic.className = 'projection-diagnostic'; this.projectionDiagnostic.hidden = true; @@ -943,9 +943,15 @@ class ConfluenceChart { const show = this.animateCurrentPrice && this.lastCurrentPrice != null; this.currentPricePulse.hidden = !show; this.currentPricePulse.classList.toggle('active', show); - if (!show || !this.candles || this.lastCurrentPrice == null) return; + if (!show || !this.candles || this.lastCurrentPrice == null || !this.chartEl) return; const y = this.candles.priceToCoordinate(this.lastCurrentPrice); - if (y != null) this.currentPricePulse.style.top = `${Math.round(y)}px`; + const canvas = this.plotCanvas(); + if (y == null || !canvas) return; + const chart = this.chartEl.getBoundingClientRect(); + const plot = canvas.getBoundingClientRect(); + this.currentPricePulse.style.left = `${Math.round(plot.right - chart.left)}px`; + this.currentPricePulse.style.top = `${Math.round(plot.top - chart.top + y - 10)}px`; + this.currentPricePulse.style.width = `${Math.max(40, Math.round(chart.right - plot.right))}px`; } setCommentHandlers({ onToggle, onMove, onSelect }) { diff --git a/static/style.css b/static/style.css index d6718cc..7b538fa 100644 --- a/static/style.css +++ b/static/style.css @@ -14,10 +14,10 @@ button { border:1px solid var(--line); background:transparent; color:var(--muted .chart-head-tools { display:flex; align-items:center; gap:8px; }.undo-btn { display:grid; place-items:center; width:32px; height:28px; padding:0; flex:none; }.undo-btn:disabled { opacity:.35; cursor:default; }.timeframes { display:flex; flex-wrap:wrap; justify-content:flex-end; }.timeframes button+button { border-left:0; } .drawing-tools { min-height:38px; padding:5px 12px; display:flex; align-items:center; gap:9px; border-bottom:1px solid var(--line); color:var(--muted); font-size:10px; }.drawing-tools button,.drawing-tools select,.drawing-tools .line-name { padding:4px 8px; font-size:10px; }.drawing-tools select,.drawing-tools .line-name { background:var(--panel); color:var(--fg); border:1px solid var(--line); }.drawing-tools .line-name { width:130px; font:inherit; }.drawing-tools label { display:flex; gap:4px; align-items:center; }.drawing-tools input { accent-color:var(--accent); } #chart { position:relative; height:calc(100vh - 190px); min-height:420px; touch-action:pan-y; }.chart-preview,.chart-line-bridges,.chart-handles { position:absolute; inset:0; width:100%; height:100%; overflow:hidden; pointer-events:none; }.chart-preview,.chart-line-bridges { z-index:4; }.chart-line-bridges line { stroke-linecap:round; }.chart-handles { z-index:6; }.chart-preview line[hidden],.chart-anchor[hidden],.chart-line-hit[hidden],.chart-line-focus[hidden],.chart-fib-focus[hidden] { display:none; }.chart-line-focus { stroke-linecap:round; pointer-events:none; animation:line-focus .75s ease-out forwards; }.chart-line-hit { fill:none; stroke:transparent; stroke-width:16px; pointer-events:stroke; cursor:move; touch-action:none; }.chart-anchor { stroke:var(--panel); stroke-width:2px; cursor:grab; pointer-events:all; touch-action:none; }.chart-anchor:active { cursor:grabbing; }.chart-tooltip { position:absolute; z-index:5; padding:4px 7px; border:1px solid var(--line); background:var(--panel); color:var(--fg); font-size:10px; white-space:pre-line; pointer-events:none; }.chart-tooltip[hidden] { display:none; } -.current-price-pulse { position:absolute; left:0; right:0; z-index:5; height:2px; transform:translateY(-1px); overflow:hidden; opacity:0; background:repeating-linear-gradient(90deg,var(--accent) 0 5px,transparent 5px 11px); pointer-events:none; }.current-price-pulse::after { content:''; position:absolute; top:-3px; height:8px; width:72px; left:0; background:linear-gradient(90deg,transparent,var(--accent),transparent); opacity:.5; transform:translateX(-72px); }.current-price-pulse.active { opacity:.65; }.current-price-pulse.active::after { animation:current-price-scan 5s linear infinite; }@keyframes current-price-scan { to { transform:translateX(2400px); } } +.current-price-pulse { position:absolute; z-index:5; height:20px; box-sizing:border-box; border:1px solid var(--accent); opacity:0; pointer-events:none; }.current-price-pulse.active { animation:current-price-frame 1.8s ease-in-out infinite; }@keyframes current-price-frame { 50% { opacity:.3; } } .chart-ohlc { position:absolute; top:6px; left:8px; z-index:5; padding:2px 6px; border:1px solid var(--line); background:color-mix(in srgb,var(--panel) 90%,transparent); color:var(--fg); font-size:10px; font-variant-numeric:tabular-nums; pointer-events:none; white-space:nowrap; }.chart-ohlc.up { color:var(--green); }.chart-ohlc.down { color:var(--red); }.chart-ohlc[hidden] { display:none; } .projection-diagnostic { position:absolute; top:8px; left:8px; z-index:9; margin:0; padding:5px 7px; border:1px solid var(--line); background:color-mix(in srgb,var(--panel) 92%,transparent); color:var(--fg); font:9px/1.35 "IBM Plex Mono", monospace; white-space:pre; } -@media (prefers-reduced-motion:reduce) { .current-price-pulse.active::after { animation:none; } } +@media (prefers-reduced-motion:reduce) { .current-price-pulse.active { animation:none; opacity:.85; } } @keyframes line-focus { 0% { opacity:.85; stroke-width:var(--line-focus-start-width,12px); } 80% { opacity:.85; } 100% { opacity:0; stroke-width:var(--line-focus-width,2px); } } .chart-context-menu { position:absolute; z-index:8; width:165px; padding:4px; border:1px solid var(--line); background:var(--panel); box-shadow:0 5px 18px color-mix(in srgb,var(--fg) 15%,transparent); pointer-events:auto; }.chart-context-menu[hidden] { display:none; }.chart-context-menu button { width:100%; padding:6px 8px; text-align:left; color:var(--fg); font-size:10px; } .statusbar { min-height:34px; display:flex; align-items:center; gap:24px; padding:6px 13px; border-top:1px solid var(--line); color:var(--muted); font-size:10px; }.statusbar b { color:var(--fg); text-transform:uppercase; }.data-freshness { display:flex; gap:24px; margin-left:auto; white-space:nowrap; }.diag-capture { padding:3px 7px; border-color:var(--accent); color:var(--accent); font-size:9px; white-space:nowrap; }.diag-capture:disabled { opacity:.55; cursor:wait; }