interface ScreenerSlice {
aggregation: number;
camera: number;
rowCount: number;
cellHeight: number;
pricePrecision: number;
priceDivider: number;
priceStep: number;
aggregatedPriceStep: number;
quantityPrecision: number;
quantityDivider: number;
}

const initialState: ScreenerSlice = {
aggregation: 10,
camera: 0,
rowCount: 40,
cellHeight: cupOptions().cell.defaultHeight,
priceDivider: 1,
pricePrecision: 0,
priceStep: 0,
aggregatedPriceStep: 0,
quantityPrecision: 0,
quantityDivider: 1,
};


const Cup = ({workerRef, symbol, settings, setShowSettings}: CupProps) => {
const cupParams = useSelector((state: AppState) => state.cupSlice); // ScreenerSlice
const [dpiScale, setDpiScale] = useState(Math.ceil(window.devicePixelRatio));
const [canvasSize, setCanvasSize] = useState<CanvasSize>({height: 0, width: 0});
const containerRef = useRef<HTMLDivElement|null>(null);
const canvasRef = useRef<HTMLCanvasElement|null>(null);
const [zoom, setZoom] = useState(1);
const [isLoaded, setIsLoaded] = useState(false);
const size = useComponentResizeListener(canvasRef);
const darkMode = useTheme().palette.mode === “dark”;
const dispatch = useDispatch();

console.log(cupParams, dpiScale);
// console.log(containerRef, canvasRef);

const cupSubscribe = useCallback(async(pair: string, zoom: number) => {
workerRef.current?.postMessage(JSON.stringify({type: “subscribe”, pair, zoom}));
}, []);

const cupUnsubscribe = useCallback(async(pair: string) => {
workerRef.current?.postMessage(JSON.stringify({type: “unsubscribe”, pair}));
}, []);

const sendSettings = () => {
workerRef.current?.postMessage(JSON.stringify({
type: “change_max_volume”,
isFixed: settings.isFixedVolume,
maxVolume: parseInt(settings.maxVolume || “0”) < 0 ? 1 : parseInt(settings.maxVolume),
volumeAsDollars: settings.volumeAsDollars,
}));
workerRef.current?.postMessage(JSON.stringify({
type: “change_publisher_timeout”,
value: parseInt(settings.timeout || “0”) < 40 ? 40 : parseInt(settings.timeout),
}));
};

const wheelHandler = (e: WheelEvent) => {
e.preventDefault();
workerRef.current?.postMessage(JSON.stringify({type: e.deltaY < 0 ? “camera_up” : “camera_down”}));
};
const zoomAdd = () => {
let newZoom;
setZoom(newZoom);
};

const zoomSub = () => {
setZoom(newZoom);
};
useEffect(() => {
workerRef.current = new Worker(new URL(“/workers/cup-builder.ts”, import.meta.url));
canvasRef.current?.addEventListener(“wheel”, wheelHandler, {passive: false});
sendSettings();

return () => {
workerRef.current?.terminate();
canvasRef.current?.removeEventListener(“wheel”, wheelHandler);
};
}, []);

useEffect(() => {
if (!workerRef.current) return;
let animationFrameId: number|null = null;

workerRef.current.onmessage = (event: MessageEvent<{
type: string,
camera: number,
aggregation: number,
bestBidPrice: number,
bestAskPrice: number,
maxVolume: number,
pricePrecision: number,
quantityPrecision: number,
priceStep: number,
cup: {[key: number]: CupItem},
rowsCount: number,
volumeAsDollars: boolean,
}>) => {
if (event?.data?.type === “update_cup”) {
if (null !== animationFrameId) {
cancelAnimationFrame(animationFrameId);
}

animationFrameId = requestAnimationFrame(() => {
const context = canvasRef.current?.getContext(“2d”);
const zoomedTickSize = event.data.priceStep * event.data.aggregation;

if (context) {
const rowsOnScreenCount = cupTools.getRowsCountOnScreen(
canvasSize.height,
cupOptions().cell.defaultHeight * dpiScale,
);
const realCellHeight = parseInt((canvasSize.height / rowsOnScreenCount).toFixed(0));

if (event.data.rowsCount !== rowsOnScreenCount) {
workerRef.current?.postMessage(JSON.stringify({type: “change_rows_count”, value: rowsOnScreenCount}));
}

cupDrawer.clear(context, canvasSize);

if (cupParams.rowCount !== rowsOnScreenCount
|| cupParams.cellHeight !== realCellHeight
|| cupParams.aggregation !== event.data.aggregation
) {
dispatch(setCupParams({
aggregation: event.data.aggregation,
rowCount: rowsOnScreenCount,
cellHeight: realCellHeight,
pricePrecision: event.data.pricePrecision,
priceStep: event.data.priceStep,
quantityPrecision: event.data.quantityPrecision,
}));
}

if (event.data.camera === 0 && isLoaded) {
setIsLoaded(false);
}

if (event.data.camera > 0 && !isLoaded) {
setIsLoaded(true);
}

if (event.data.camera !== 0) {
cupDrawer.draw(
context,
canvasSize,
dpiScale,
event.data.bestBidPrice,
event.data.bestAskPrice,
event.data.maxVolume,
event.data.pricePrecision,
event.data.quantityPrecision,
event.data.priceStep,
event.data.aggregation,
rowsOnScreenCount,
event.data.camera,
realCellHeight,
{
buy: parseInt((Math.floor(event.data.bestBidPrice / zoomedTickSize) * zoomedTickSize).toFixed(0)),
sell: parseInt((Math.ceil(event.data.bestAskPrice / zoomedTickSize) * zoomedTickSize).toFixed(0)),
},
darkMode,
event.data.volumeAsDollars,
event.data.cup,
);
}
}
});
}
};

return () => {
if (null !== animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
}, [workerRef.current, canvasSize, darkMode, dpiScale, isLoaded]);

useEffect(() => {
setDpiScale(Math.ceil(window.devicePixelRatio));
}, [window.devicePixelRatio]);

useEffect(() => {
if (!size) {
return;
}

setCanvasSize({
width: Math.floor(size.width) * dpiScale,
height: Math.floor(size.height) * dpiScale,
});
}, [dpiScale, size]);

useEffect(() => {
cupSubscribe(symbol.toUpperCase(), zoom);

return () => {
cupUnsubscribe(symbol.toUpperCase());
};
}, [symbol, zoom]);
useEffect(() => {
if (settings.isFixedVolume) {
window.localStorage.setItem(cup-volume-${symbol}, settings.maxVolume);
} else {
window.localStorage.removeItem(cup-volume-${symbol});
}
}, [settings]);

return <div ref={containerRef} className={styles.canvasWrapper}>
<canvas
ref={canvasRef}
className={[styles.canvas, isLoaded ? “” : styles.loading].join(" “)}
width={canvasSize?.width}
height={canvasSize?.height}
/>
</div>;
};

export default Cup;


const cupDrawer = {
clear: (ctx: CanvasRenderingContext2D, size: CanvasSize) => {
ctx.clearRect(0, 0, size.width, size.height);
},
draw: (
ctx: CanvasRenderingContext2D,
size: CanvasSize,
dpiScale: number,
bestBidPrice: number,
bestAskPrice: number,
maxVolume: number,
pricePrecision: number,
quantityPrecision: number,
priceStep: number,
zoom: number,
rowCount: number,
camera: number,
cellHeight: number,
zoomedBestMicroPrice: any,
darkMode: boolean,
volumeAsDollars: boolean,
cup: {[key: number]: CupItem},
) => {
const zoomedTickSize = priceStep * zoom;
const startMicroPrice = camera + rowCount / 2 * zoomedTickSize - zoomedTickSize;
const quantityDivider = Math.pow(10, quantityPrecision);
const priceDivider = Math.pow(10, pricePrecision);

for (let index = 0; index <= rowCount; index++) {
const microPrice = startMicroPrice - index * zoomedTickSize;

if (microPrice < 0) continue;

const item = cup[microPrice] || {};
let side = “undefined” !== typeof item?.bid
? “bid”
: “undefined” !== typeof item?.ask ? “ask” : “”;

if (”" === side || (!item?.bid && !item.ask)) {
side = (microPrice >= (zoomedBestMicroPrice?.buy || zoomedBestMicroPrice?.sell || 0) ? “ask” : “bid”);
}

const yPosition = cellHeight * index;
const quantity = volumeAsDollars
? ((side === “bid” ? item?.bid || 0 : item?.ask || 0) / quantityDivider) * (microPrice / priceDivider)
: (side === “bid” ? item?.bid || 0 : item?.ask || 0) / quantityDivider;

const isBestPrice = cupTools.isBestPrice(
side,
microPrice,
zoomedBestMicroPrice,
);

if ((microPrice / zoomedTickSize) % 5 === 0) {
ctx.beginPath();
ctx.strokeStyle = darkMode ? “#232425” : “#dddedf”;
ctx.lineWidth = Math.max(1, dpiScale);
ctx.moveTo(0, yPosition);
ctx.lineTo(size.width, yPosition);
ctx.stroke();
}

cupDrawer.drawCell(
ctx,
quantity,
side,
maxVolume,
isBestPrice,
yPosition,
cellHeight,
size.width,
dpiScale,
darkMode
);

if (isBestPrice) {
cupDrawer.drawFill(
ctx,
quantity,
side,
maxVolume,
yPosition,
cellHeight,
size.width,
dpiScale,
darkMode
);
}
};

export default cupDrawer;

изменяется aggregatedPriceStep, aggregation и zoom, сжатие больше одного.
Есть центровка стакана, при сжатии больше 1, возможно расчет идет неправильный на центровку стакана.
Есть проблема. ПРОАНАЛИЗИРУЙ КОД, НАЙДИ ОШИБКУ