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;

возможно при сжатии больше 1 расчет идет неправильный на центровку стакана. нужно найти проблему