Reverse-engineering · по бандлам

TakeProfit: архитектура кода

Разбор минифицированных бандлов: drag-and-drop, анимации, докинг/сетка, рендер-планировщик и pan/zoom — с verbatim-сниппетами из кода.

📦 215 чанков свежего билда 🧠 ядро BgH94ARf.js · 3.8 МБ 🔬 6 подсистем · параллельный разбор 📅 12 июня 2026
high подтверждено в кодеmedium косвенноlow не подтверждено
TL;DR

Технологический скелет

каркас
SvelteKit + Svelte 5
график: модель/ввод
Lightweight Charts (форк)
график: рендер
PixiJS v7 · WebGL2
виджеты/докинг
Lumino DockPanel
drag-and-drop
нативный HTML5 + тач-мост
данные
Connect/gRPC-web · Protobuf
🔁

Корректировки прежних гипотез (по факту кода): график — это форк TradingView Lightweight Charts (а не просто «похожий API»), с рендером, заменённым на Pixi/WebGL. Семьи «ease*» не существует — это была ошибка чтения подстрок (increase/release). Drag-and-drop — не @lumino/dragdrop, а нативный HTML5 + тач-мост (поэтому Playwright-drag мышью не вызывал оверлей). Раскладка хранится на бэкенде, не в браузере.

01

Чарт-движок: Lightweight Charts + WebGL

подходФорк TradingView Lightweight Charts (модель/взаимодействие) с рендером, заменённым на PixiJS v7 / WebGL2
где в кодеBgH94ARf.js (3.8 МБ — весь движок)
достоверностьhigh

Как работает

Код (verbatim)

zoom(e,t){const i=this.coordinateToFloatIndex(e),r=this.barSpacing(),n=r+t*(r/10);this.setBarSpacing(n),this.internalOptions().rightBarStaysOnScroll||this.setRightOffset(this.rightOffset()+(i-this.coordinateToFloatIndex(e)))}

Зум меняет barSpacing на 10% за единицу дельты и держит бар под курсором (rightOffset-коррекция).

02

Pan / Zoom: колесо, drag, инерция

подходLightweight Charts interaction layer (wheel + pressed-move + kinetic)
где в кодеBgH94ARf.js
достоверностьhigh

Как работает

Код (verbatim)

_onMousewheel=e=>{let t=e.deltaX/100,i=-e.deltaY/100;...switch(e.deltaMode){case e.DOM_DELTA_PAGE:t*=120,i*=120;break;case e.DOM_DELTA_LINE:t*=32}if(i!==0&&...mouseWheel){const r=Math.sign(i)*Math.min(1,Math.abs(i)),n=e.clientX-this._element.getBoundingClientRect().left;this.model().zoomTime(n,r)}t!==0&&...&&this.model().scrollChart(-80*t)}

Главный wheel-handler: вертикаль → зум времени с якорем под курсором, горизонталь → скролл ленты (×80).

getPosition(e){const t=Gt(this._animationStartPosition),i=e-t.time;return t.position+this._speedPxPerMsec*(Math.pow(this._dumpingCoeff,i)-1)/Math.log(this._dumpingCoeff)}

Инерция: путь = startPos + speed·(dumping^Δt − 1)/ln(dumping) — экспоненциальное затухание.

_initKineticScrollIfNeeded(e,...){...const r=this._chart.internalOptions().kineticScroll;(e&&r.touch||!e&&r.mouse)&&(this._scrollXAnimation=new Xue(.2,7,.997,15),...)}

Инерция включается только для touch (по дефолту); KineticAnimation с коэффициентом затухания .997.

03

Render-scheduler: on-demand рендер

подходPixiJS v7 Ticker с выключенным автоциклом + кастомный rAF с dirty-флагами
где в кодеBgH94ARf.js
достоверностьhigh

Как работает

Код (verbatim)

r.ticker.autoStart=!1,r.ticker.stop();const n=()=>{r?.scenes.some(a=>a.isNeedRedraw())&&r.ticker.update(),requestAnimationFrame(n)};return requestAnimationFrame(n),r

Автотикер Pixi выключен; свой rAF зовёт update() (рендер) только когда сцена «грязная».

isNeedRedraw(){if(this._rootCont.visible===!1)return!1;if(this._chartModel.needRedraw)return this._chartModel.needRedraw=!1,!0;... for(const t of e){...if(n.paneViews().some(a=>a.needRedraw)||...)return!0}return!1}

Gate-проверка: обходит флаги модели/crosshair/grid/пейнов; флаг модели сбрасывается при чтении (consume).

04

Docking / layout виджетов

подходLumino DockPanel (быв. PhosphorJS, @lumino/widgets) под кастомным LayoutManager
где в кодеBgH94ARf.js (ядро) · CzHe8QoV.js (бутстрап) · By1WdL_-.js (добавление) · C3wOGcEP.js (lazy, не скачан)
достоверностьhigh

Как работает

Код (verbatim)

layout:{main:{type:"split-area",orientation:"horizontal",sizes:[.3333,.6666],children:[{type:"tab-area",widgets:[{ID:s.ID}],currentIndex:0}]}}

Дефолтная раскладка: горизонтальный сплит (доли 0.33/0.66) с tab-area внутри — нативный формат Lumino.

applyLayout=()=>{const e={},{layout:t,widgets:i}=G(this);Object.values(i).map(n=>{const{luminoWidget:a}=n;e[n.ID]=a});const r=v2e(t,e);if(!Yi.lm)throw new Error("Lumino was not initialized");Yi.lm.applyConfig(r),this.#r()}

Восстановление докинга: ID→luminoWidget, v2e гидратирует конфиг, applyConfig пересобирает DockPanel.

05

Drag-and-drop виджетов

подходНативный HTML5 Drag-and-Drop (НЕ @lumino/dragdrop) + тач-мост; drop-оверлей рисует Lumino DockPanel
где в кодеCzHe8QoV.js · By1WdL_-.js · BgH94ARf.js
достоверностьhigh (HTML5+тач) / medium (роль Lumino в оверлее)

Как работает

Код (verbatim)

fe.startWidgetDrag(),mt.dataTransfer&&(mt.dataTransfer.setData("text/plain",JSON.stringify({action:"move",ID:F.ID})),mt.dataTransfer.effectAllowed="move");const Nt=ia[F.type]?.htmlElement;Nt&&mt.dataTransfer?.setDragImage(Nt,0,0)

Старт нативного drag: action/ID в dataTransfer + drag-image из готового DOM-узла типа виджета.

const T=h.changedTouches[0],A=new DataTransfer;A.setData("text/plain",JSON.stringify({action:"move",ID:t}));const N=document.elementFromPoint(T.clientX,T.clientY);N.dispatchEvent(new DragEvent("drop",{clientX:T.clientX,clientY:T.clientY,dataTransfer:A,bubbles:!0}))

Тач-мост: вручную собирает DataTransfer и синтезирует drop в элемент под пальцем.

_handleMouseMoveInDown(e,t){const i={x:t.clientX,y:t.clientY},{manhattanDistance:r}=_1(i,e.startClientPosition);r>=zI&&(this._transitionTo({type:"mouse-drag"}),this._callbacks.firePressedPointerMove(t))}

In-chart pointer-машина: переход в drag только после смещения ≥5px (manhattan), затем pressed-move.

06

Анимации и easing

подходТри слоя: PixiJS Ticker (рендер) + kinetic-скролл (эксп. затухание) + Svelte 5 transitions (UI)
где в кодеBgH94ARf.js · C2JkWN82.js (Svelte transitions)
достоверностьhigh

Как работает

Код (verbatim)

cs(3,x,()=>Wl,()=>({duration:200,easing:Uv,y:32}))   /* Wl = fly: едет по Y на 32px */

Скомпилированный Svelte-transition: fly 200мс с easing-функцией и смещением y.

function w(t){return t<.5?4*t*t*t:.5*Math.pow(2*t-2,3)+1}

cubicInOut — дефолтный easing модуля Svelte transitions.

s[s.DefaultAnimationDuration=400]="DefaultAnimationDuration"; s[s.AnimationDurationMs=1e3]="AnimationDurationMs"

Дефолтные длительности анимаций графика: 400мс и 1000мс.

07

Grid-resize, persistence, Widget Linking

подходResize — Lumino SplitLayout; persist — remote-бэкенд (НЕ localStorage); linking — кастомные Svelte-сторы
где в кодеBgH94ARf.js · By1WdL_-.js · CzHe8QoV.js
достоверностьhigh (linking/resize) / medium-high (persist — эндпоинт не извлечён)

Как работает

Код (verbatim)

#r=()=>{const e=vo(this.#n);Yi.lm?.handleModifications(e),this.#t=()=>{Yi.lm?.unhandleModifications(e)}};#n=e=>{this.update(t=>({...t,layout:_2e(e)}))}

Подписка на изменения Lumino (вкл. ресайз ручкой): новый конфиг с пересчитанными sizes пишется в layout стора.

setChannel(e,t=!1){this.channel.update(i=>{i?.removeSubscriber(this.ID);const r=e?this.workspace.channels[e]:null;return!this.channelIndependent&&r&&(t&&r.fade(),r.addSubscriber(this.ID)),r})}

Привязка виджета к каналу-цвету: отписка от старого, подписка на новый + флэш fade().

class qte{security;dateRange;timeframe;crosshair;indicators;chartType;...}

Канальные тумблеры синка: что реплицировать связанным виджетам.