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


import { CanvasRenderingTarget2D } from 'fancy-canvas';

import { drawRoundedRect } from '@core/Drawings/utils';
import { removeAlphaFromHex } from '@src/utils/removeAlphaFromHex';

import type { IPrimitivePaneRenderer } from 'lightweight-charts';
import type { SliderPositionStyle } from './settings';
import type { SliderPosition } from './sliderPosition';

const UI = {
  lineHeightMultiplier: 1.2,
  mainBoxHeight: 28,
  sideBoxHeight: 16,
  padding: 4,
  boxRadius: 4,
  labelOffset: 10,
};

export class SliderPaneRenderer implements IPrimitivePaneRenderer {
  private readonly slider: SliderPosition;

  constructor(slider: SliderPosition) {
    this.slider = slider;
  }

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

    if (!data) {
      return;
    }

    target.useBitmapCoordinateSpace(
      ({ context, horizontalPixelRatio, verticalPixelRatio, bitmapSize }) => {
        const left = data.leftX * horizontalPixelRatio;
        const right = data.rightX * horizontalPixelRatio;
        const entryY = data.entryY * verticalPixelRatio;
        const stopY = data.stopY * verticalPixelRatio;
        const targetY = data.targetY * verticalPixelRatio;
        const profitTop = data.profitTop * verticalPixelRatio;
        const profitBottom = data.profitBottom * verticalPixelRatio;
        const lossTop = data.lossTop * verticalPixelRatio;
        const lossBottom = data.lossBottom * verticalPixelRatio;
        const centerX = (left + right) / 2;
        const labelOffset = UI.labelOffset * verticalPixelRatio;

        const targetBoxHeight = getTextBoxHeight(
          data.targetText,
          data,
          UI.sideBoxHeight,
          verticalPixelRatio,
        );

        const centerBoxHeight = getTextBoxHeight(
          data.centerText,
          data,
          UI.mainBoxHeight,
          verticalPixelRatio,
        );

        const stopBoxHeight = getTextBoxHeight(
          data.stopText,
          data,
          UI.sideBoxHeight,
          verticalPixelRatio,
        );

        const targetLabelY = getOuterLabelCenterY(
          targetY,
          data.targetLabelDirection,
          targetBoxHeight,
          labelOffset,
        );

        const stopLabelY = getOuterLabelCenterY(
          stopY,
          data.stopLabelDirection,
          stopBoxHeight,
          labelOffset,
        );

        const centerLabelY = getCenterLabelCenterY(
          entryY,
          targetY,
          stopY,
          centerBoxHeight,
          labelOffset,
          bitmapSize.height,
        );

        context.save();

        context.fillStyle = data.positiveFillColor;
        context.fillRect(left, profitTop, right - left, profitBottom - profitTop);

        context.fillStyle = data.negativeFillColor;
        context.fillRect(left, lossTop, right - left, lossBottom - lossTop);

        context.lineWidth =
          data.lineWidth * Math.max(horizontalPixelRatio, verticalPixelRatio);

        drawHorizontalLine(
          context,
          left,
          right,
          targetY,
          removeAlphaFromHex(data.positiveFillColor),
        );

        drawHorizontalLine(context, left, right, entryY, data.lineColor);

        drawHorizontalLine(
          context,
          left,
          right,
          stopY,
          removeAlphaFromHex(data.negativeFillColor),
        );

        if (data.showLabels) {
          drawTextBox(
            context,
            centerX,
            targetLabelY,
            data.targetText,
            data.positiveFillColor,
            data,
            horizontalPixelRatio,
            verticalPixelRatio,
            UI.sideBoxHeight,
          );

          drawTextBox(
            context,
            centerX,
            centerLabelY,
            data.centerText,
            data.centerBoxColor,
            data,
            horizontalPixelRatio,
            verticalPixelRatio,
            UI.mainBoxHeight,
          );

          drawTextBox(
            context,
            centerX,
            stopLabelY,
            data.stopText,
            data.negativeFillColor,
            data,
            horizontalPixelRatio,
            verticalPixelRatio,
            UI.sideBoxHeight,
          );
        }

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

function getOuterLabelCenterY(
  coordinate: number,
  direction: 'up' | 'down',
  boxHeight: number,
  labelOffset: number,
): number {
  const offset = labelOffset + boxHeight / 2;

  return direction === 'up' ? coordinate - offset : coordinate + offset;
}

function getCenterLabelCenterY(
  entryY: number,
  targetY: number,
  stopY: number,
  boxHeight: number,
  labelOffset: number,
  viewportHeight: number,
): number {
  const targetSpace = Math.abs(targetY - entryY);
  const stopSpace = Math.abs(stopY - entryY);

  const direction =
    targetSpace < stopSpace
      ? stopY < entryY
        ? -1
        : 1
      : targetY < entryY
        ? -1
        : 1;

  return clampTextBoxCenter(
    entryY + direction * (labelOffset + boxHeight / 2),
    boxHeight,
    viewportHeight,
  );
}

function clampTextBoxCenter(
  coordinate: number,
  boxHeight: number,
  viewportHeight: number,
): number {
  if (viewportHeight <= boxHeight) {
    return viewportHeight / 2;
  }

  const halfHeight = boxHeight / 2;

  return Math.max(halfHeight, Math.min(coordinate, viewportHeight - halfHeight));
}

function drawHorizontalLine(
  context: CanvasRenderingContext2D,
  left: number,
  right: number,
  y: number,
  color: string,
): void {
  context.strokeStyle = color;
  context.beginPath();
  context.moveTo(left, y);
  context.lineTo(right, y);
  context.stroke();
}

function drawTextBox(
  context: CanvasRenderingContext2D,
  centerX: number,
  centerY: number,
  text: string,
  fillColor: string,
  style: SliderPositionStyle,
  horizontalPixelRatio: number,
  verticalPixelRatio: number,
  fixedHeight: number,
): void {
  context.save();

  const lines = text.split('\n');
  const fontSize = style.fontSize * verticalPixelRatio;
  const lineHeight =
    Math.round(style.fontSize * UI.lineHeightMultiplier) * verticalPixelRatio;
  const paddingX = UI.padding * horizontalPixelRatio;
  const boxHeight = getTextBoxHeight(text, style, fixedHeight, verticalPixelRatio);
  const radius = UI.boxRadius * Math.max(horizontalPixelRatio, verticalPixelRatio);

  context.font = `${fontSize}px Inter, sans-serif`;
  context.textAlign = 'center';
  context.textBaseline = 'middle';

  let maxTextWidth = 0;

  for (const line of lines) {
    maxTextWidth = Math.max(maxTextWidth, context.measureText(line).width);
  }

  const width = maxTextWidth + paddingX * 2;
  const x = centerX - width / 2;
  const y = centerY - boxHeight / 2;

  context.fillStyle = fillColor;
  context.beginPath();
  drawRoundedRect(context, x, y, width, boxHeight, radius);
  context.fill();

  context.fillStyle = style.textColor;

  if (lines.length === 1) {
    context.fillText(lines[0], centerX, centerY);
    context.restore();
    return;
  }

  const textBlockHeight = lines.length * lineHeight;
  const firstLineY = centerY - textBlockHeight / 2 + lineHeight / 2;

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

  context.restore();
}

function getTextBoxHeight(
  text: string,
  style: SliderPositionStyle,
  fixedHeight: number,
  verticalPixelRatio: number,
): number {
  const linesCount = text.split('\n').length;
  const lineHeight =
    Math.round(style.fontSize * UI.lineHeightMultiplier) * verticalPixelRatio;
  const paddingY = UI.padding * verticalPixelRatio;
  const minHeight = fixedHeight * verticalPixelRatio;

  return Math.max(minHeight, linesCount * lineHeight + paddingY * 2);
}