diff --git a/AGENTS.md b/AGENTS.md index 410da5c..da595b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -282,3 +282,6 @@ actually advanced. layout actually changed. - A feature that needs a per-frame or per-tick loop needs a reason, and an off switch. +- Chart overlays may animate only `transform` or `opacity`. Animating + `background-position` on a full-width gradient cost ~15% CPU; the + compositor will not save you from a paint property. diff --git a/docs/implementation.md b/docs/implementation.md index f0fbb6a..44581b2 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -1401,3 +1401,9 @@ The header quote paints the DOM directly; Vue's `price` flushes at 1 Hz. Schwab still prints many forming ticks per frame. Those now coalesce to one `candles.update` on the next animation frame. A new minute flushes immediately. + +**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. diff --git a/static/style.css b/static/style.css index 242993f..d6718cc 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:3px; transform:translateY(-1.5px); overflow:hidden; opacity:0; background:repeating-linear-gradient(110deg,transparent 0 8px,color-mix(in srgb,var(--accent) 55%,white) 8px 13px,transparent 13px 19px); mask-image:linear-gradient(90deg,transparent 0,#000 8%,#000 100%); pointer-events:none; }.current-price-pulse::after { content:''; position:absolute; top:-2px; bottom:-2px; left:-24%; width:24%; background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--accent) 35%,white),transparent); opacity:.8; }.current-price-pulse.active { opacity:.9; animation:current-price-flow 1.5s linear infinite; }.current-price-pulse.active::after { animation:current-price-scan 2.8s linear infinite; }@keyframes current-price-flow { to { background-position-x:57px; } }@keyframes current-price-scan { to { transform:translateX(520%); } } +.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); } } .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 { animation:none; } } +@media (prefers-reduced-motion:reduce) { .current-price-pulse.active::after { animation:none; } } @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; }