Загрузка данных


import { CanvasRenderingTarget2D } from 'fancy-canvas';
import { IPrimitivePaneRenderer } from 'lightweight-charts';

import { getThemeStore } from '@src/theme';

import type { ParallelChannel } from './parallelChannel';

import type { Point } from '@core/Drawings/types';

const UI = {
  lineWidth: 2,
  middleLineWidth: 1,
  middleLineDash: 6,
  middleLineGap: 4,
  handleRadius: 5,
  handleBorderWidth: 2,
  textLineHeightMultiplier: 1.2,
  textOffset: 6,
};

export class ParallelChannelPaneRenderer implements IPrimitivePaneRenderer {
  private parallelChannel: ParallelChannel;

  constructor(parallelChannel: ParallelChannel) {
    this.parallelChannel = parallelChannel;
  }

  public draw(target: CanvasRenderingTarget2D): void {
    const data = this.parallelChannel.getRenderData();

    if (!data) {
      return;
    }

    target.useBitmapCoordinateSpace(
      ({
        context,
        horizontalPixelRatio,
        verticalPixelRatio,
      }) => {
        const pixelRatio = Math.max(
          horizontalPixelRatio,
          verticalPixelRatio,
        );

        const startPoint = scalePoint(
          data.startPoint,
          horizontalPixelRatio,
          verticalPixelRatio,
        );

        const mainMiddlePoint = scalePoint(
          data.mainMiddlePoint,
          horizontalPixelRatio,
          verticalPixelRatio,
        );

        const endPoint = scalePoint(
          data.endPoint,
          horizontalPixelRatio,
          verticalPixelRatio,
        );

        const parallelStartPoint = scalePoint(
          data.parallelStartPoint,
          horizontalPixelRatio,
          verticalPixelRatio,
        );

        const parallelMiddlePoint = scalePoint(
          data.parallelMiddlePoint,
          horizontalPixelRatio,
          verticalPixelRatio,
        );

        const parallelEndPoint = scalePoint(
          data.parallelEndPoint,
          horizontalPixelRatio,
          verticalPixelRatio,
        );

        const middleStartPoint = scalePoint(
          data.middleStartPoint,
          horizontalPixelRatio,
          verticalPixelRatio,
        );

        const middleEndPoint = scalePoint(
          data.middleEndPoint,
          horizontalPixelRatio,
          verticalPixelRatio,
        );

        context.save();

        drawChannelFill(
          context,
          [
            startPoint,
            endPoint,
            parallelEndPoint,
            parallelStartPoint,
          ],
          data.backgroundColor,
        );

        context.strokeStyle = data.lineColor;
        context.lineWidth = UI.lineWidth * pixelRatio;

        drawLine(
          context,
          startPoint,
          endPoint,
        );

        drawLine(
          context,
          parallelStartPoint,
          parallelEndPoint,
        );

        if (data.showMiddleLine) {
          context.save();

          context.lineWidth =
            UI.middleLineWidth *
            pixelRatio;

          context.setLineDash([
            UI.middleLineDash * pixelRatio,
            UI.middleLineGap * pixelRatio,
          ]);

          drawLine(
            context,
            middleStartPoint,
            middleEndPoint,
          );

          context.restore();
        }

        if (data.text.trim()) {
          const channelCenterPoint = getMiddlePoint(
            mainMiddlePoint,
            parallelMiddlePoint,
          );

          const isMainLineAbove =
            mainMiddlePoint.y <=
            parallelMiddlePoint.y;

          const textStartPoint = isMainLineAbove
            ? startPoint
            : parallelStartPoint;

          const textEndPoint = isMainLineAbove
            ? endPoint
            : parallelEndPoint;

          const textMiddlePoint = isMainLineAbove
            ? mainMiddlePoint
            : parallelMiddlePoint;

          drawTextAlongLine(context, {
            startPoint: textStartPoint,
            endPoint: textEndPoint,
            middlePoint: textMiddlePoint,
            channelCenterPoint,
            text: data.text,
            fontSize: data.fontSize,
            isBold: data.isBold,
            isItalic: data.isItalic,
            textColor: data.textColor,
            verticalPixelRatio,
          });
        }

        if (data.showHandles) {
          const { colors } = getThemeStore();

          context.fillStyle =
            colors.chartBackground;

          context.strokeStyle =
            colors.chartLineColor;

          context.lineWidth =
            UI.handleBorderWidth *
            pixelRatio;

          const radius =
            UI.handleRadius *
            pixelRatio;

          drawHandle(
            context,
            startPoint,
            radius,
          );

          drawHandle(
            context,
            mainMiddlePoint,
            radius,
          );

          drawHandle(
            context,
            endPoint,
            radius,
          );

          drawHandle(
            context,
            parallelStartPoint,
            radius,
          );

          drawHandle(
            context,
            parallelMiddlePoint,
            radius,
          );

          drawHandle(
            context,
            parallelEndPoint,
            radius,
          );
        }

        context.restore();
      },
    );
  }
}

function scalePoint(
  point: Point,
  horizontalPixelRatio: number,
  verticalPixelRatio: number,
): Point {
  return {
    x:
      point.x *
      horizontalPixelRatio,
    y:
      point.y *
      verticalPixelRatio,
  };
}

function drawChannelFill(
  context: CanvasRenderingContext2D,
  points: Point[],
  color: string,
): void {
  const [
    startPoint,
    endPoint,
    parallelEndPoint,
    parallelStartPoint,
  ] = points;

  context.save();
  context.fillStyle = color;

  context.beginPath();

  context.moveTo(
    startPoint.x,
    startPoint.y,
  );

  context.lineTo(
    endPoint.x,
    endPoint.y,
  );

  context.lineTo(
    parallelEndPoint.x,
    parallelEndPoint.y,
  );

  context.lineTo(
    parallelStartPoint.x,
    parallelStartPoint.y,
  );

  context.closePath();
  context.fill();

  context.restore();
}

function drawLine(
  context: CanvasRenderingContext2D,
  startPoint: Point,
  endPoint: Point,
): void {
  context.beginPath();

  context.moveTo(
    startPoint.x,
    startPoint.y,
  );

  context.lineTo(
    endPoint.x,
    endPoint.y,
  );

  context.stroke();
}

function drawHandle(
  context: CanvasRenderingContext2D,
  point: Point,
  radius: number,
): void {
  context.beginPath();

  context.arc(
    point.x,
    point.y,
    radius,
    0,
    Math.PI * 2,
  );

  context.fill();
  context.stroke();
}

function drawTextAlongLine(
  context: CanvasRenderingContext2D,
  params: {
    startPoint: Point;
    endPoint: Point;
    middlePoint: Point;
    channelCenterPoint: Point;
    text: string;
    fontSize: number;
    isBold: boolean;
    isItalic: boolean;
    textColor: string;
    verticalPixelRatio: number;
  },
): void {
  const {
    startPoint,
    endPoint,
    middlePoint,
    channelCenterPoint,
    text,
    fontSize,
    isBold,
    isItalic,
    textColor,
    verticalPixelRatio,
  } = params;

  const lines = text.split('\n');
  const safeFontSize = Math.max(1, fontSize);
  const fontSizePx =
    safeFontSize *
    verticalPixelRatio;

  const lineHeight =
    safeFontSize *
    UI.textLineHeightMultiplier *
    verticalPixelRatio;

  const directionX =
    endPoint.x -
    startPoint.x;

  const directionY =
    endPoint.y -
    startPoint.y;

  let angle = Math.atan2(
    directionY,
    directionX,
  );

  if (
    angle > Math.PI / 2 ||
    angle < -Math.PI / 2
  ) {
    angle += Math.PI;
  }

  const outwardX =
    middlePoint.x -
    channelCenterPoint.x;

  const outwardY =
    middlePoint.y -
    channelCenterPoint.y;

  const outwardLength = Math.hypot(
    outwardX,
    outwardY,
  );

  const normalX = outwardLength
    ? outwardX / outwardLength
    : 0;

  const normalY = outwardLength
    ? outwardY / outwardLength
    : -1;

  const blockHeight =
    lines.length *
    lineHeight;

  const distance =
    blockHeight / 2 +
    UI.textOffset *
      verticalPixelRatio;

  const textCenterX =
    middlePoint.x +
    normalX *
      distance;

  const textCenterY =
    middlePoint.y +
    normalY *
      distance;

  const fontWeight = isBold
    ? '700 '
    : '';

  const fontStyle = isItalic
    ? 'italic '
    : '';

  context.save();

  context.translate(
    textCenterX,
    textCenterY,
  );

  context.rotate(angle);

  context.font =
    `${fontStyle}${fontWeight}` +
    `${fontSizePx}px Inter, sans-serif`;

  context.fillStyle = textColor;
  context.textAlign = 'center';
  context.textBaseline = 'middle';

  const firstLineY =
    -(
      lines.length -
      1
    ) *
    lineHeight /
    2;

  lines.forEach(
    (
      line,
      index,
    ) => {
      context.fillText(
        line,
        0,
        firstLineY +
          index *
            lineHeight,
      );
    },
  );

  context.restore();
}

function getMiddlePoint(
  startPoint: Point,
  endPoint: Point,
): Point {
  return {
    x:
      (
        startPoint.x +
        endPoint.x
      ) /
      2,
    y:
      (
        startPoint.y +
        endPoint.y
      ) /
      2,
  };
}