const TradingCup = () => {
  const cupWorkerRef = useRef<Worker>();
  const orderFeedWorkerRef = useRef<Worker>();
  const symbol = useSelector((state: AppState) => state.screenerSlice.symbol);
  const [settings, setSettings] = useState<SettingsCup>({
    timeout: "40",
    aggregation: true,
    maxVolume: window.localStorage.getItem(`cup-volume-${symbol}`) || "1000",
    isFixedVolume: null !== window.localStorage.getItem(`cup-volume-${symbol}`),
    minQuantity: window.localStorage.getItem(`order-feed-min-qty-${symbol}`) || "0",
    dollarsVolume: null !== window.localStorage.getItem(`cup-dollars-volume-${symbol}`),
  });
  const [showSettings, setShowSettings] = useState(false);

  useEffect(() => {
    const channel = new MessageChannel();
    cupWorkerRef.current?.postMessage({type: "set_port", port: channel.port1}, [channel.port1]);
    orderFeedWorkerRef.current?.postMessage({type: "set_port", port: channel.port2}, [channel.port2]);
  }, []);

  useEffect(() => {
    setSettings({
      ...settings,
      maxVolume: window.localStorage.getItem(`cup-volume-${symbol}`) || "1000",
      isFixedVolume: null !== window.localStorage.getItem(`cup-volume-${symbol}`),
      minQuantity: window.localStorage.getItem(`order-feed-min-qty-${symbol}`) || "0",
      dollarsVolume: null !== window.localStorage.getItem(`cup-dollars-volume-${symbol}`),
    });
  }, [symbol]);

  return <Stack direction="row" height="100%">

    <div className={styles.Cup}>
      <Cup
        symbol={symbol}
        workerRef={cupWorkerRef}
        settings={settings}
        setShowSettings={setShowSettings}
      />
    </div>

    <SettingsPopupCup
      settings={settings}
      setSettings={setSettings}
      showSettings={showSettings}
      setShowSettings={setShowSettings}
    />
  </Stack>;
};

export default TradingCup;

import {useDispatch, useSelector} from "react-redux";
import {AppState} from "../../../store/store";
import React, {useCallback, useEffect, useRef, useState} from "react";
import useComponentResizeListener from "../../../hooks/componentResizeListener";
import styles from "./Cup.module.css";
import cupDrawer from "./Cup.drawer";
import cupTools from "./Cup.tools";
import cupOptions from "./Cup.options";
import {CupItem} from "../../../hooks/rustWsServer";
import {IconButton, useTheme} from "@mui/material";
import {AddRounded, RemoveRounded, SettingsRounded} from "@mui/icons-material";
import {setCupParams} from "../../../store/cupSlice";
import {CupProps} from "./Cup.props";

export interface CanvasSize {
  width: number;
  height: number;
}

const Cup = ({workerRef, symbol, settings, setShowSettings}: CupProps) => {
  const cupParams = useSelector((state: AppState) => state.cupSlice);
  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);
  

  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),
    
      maxVolume: settings.dollarsVolume
      ? parseInt(settings.maxVolume || "0") * parseFloat(settings.minQuantity || "0")
      : parseInt(settings.maxVolume || "0") < 0 ? 1 : parseInt(settings.maxVolume),
      }));
    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;

    if (zoom >= 1 && zoom < 10) {
      newZoom = zoom + 1;
    } else if (zoom >= 10 && zoom < 30) {
      newZoom = zoom + 5;
    } else if (zoom >= 30 && zoom < 100) {
      newZoom = zoom + 10;
    } else {
      newZoom = zoom;
    }

    setZoom(newZoom);
  };

  const zoomSub = () => {
    let newZoom;

    if (zoom > 1 && zoom <= 10) {
      newZoom = zoom - 1;
    } else if (zoom > 10 && zoom <= 30) {
      newZoom = zoom - 5;
    } else if (zoom > 30 && zoom <= 100) {
      newZoom = zoom - 10;
    } else {
      newZoom = zoom;
    }

    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);
    };
  }, [settings.dollarsVolume]);

  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,
    }>) => {
      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.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}`);
    }

    sendSettings();
  }, [settings]);

  return <div ref={containerRef} className={styles.canvasWrapper}>
    <div className={styles.controls}>
      <IconButton onClick={zoomSub} size="small" disabled={!isLoaded}>
        <RemoveRounded />
      </IconButton>
      <span>x{zoom}</span>
      <IconButton onClick={zoomAdd} size="small" disabled={!isLoaded}>
        <AddRounded />
      </IconButton>
      <IconButton onClick={() => setShowSettings(true)} size="small">
        <SettingsRounded
          // @ts-ignore
          fontSize="xx-small"
        />
      </IconButton>
    </div>
    <canvas
      ref={canvasRef}
      className={[styles.canvas, isLoaded ? "" : styles.loading].join(" ")}
      width={canvasSize?.width}
      height={canvasSize?.height}
    />
  </div>;
};

export default Cup;

Когда dollarsVolume true - перемножать quantity на price при отрисовке строк стакана. И в воркере максимальный объем определять тоже путем перемножения цены на кол-во