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


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

import { Direction } from '@src/types';

import type { Ruler, RulerUiStyle } from './ruler';

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

interface Bounds {
  left: number;
  right: number;
  top: number;
  bottom: number;
}

export class RulerPaneRenderer implements IPrimitivePaneRenderer {
  private readonly ruler: Ruler;

  constructor(ruler: Ruler) {
    this.ruler = ruler;
  }

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

    if (data.hidden || !data.startPoint || !data.endPoint) {
      return;
    }

    const bounds = getBounds(data.startPoint, data.endPoint);

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

      const left = bounds.left * horizontalPixelRatio;
      const right = bounds.right * horizontalPixelRatio;
      const top = bounds.top * verticalPixelRatio;
      const bottom = bounds.bottom * verticalPixelRatio;

      const centerX = (left + right) / 2;
      const centerY = (top + bottom) / 2;

      context.save();

      context.fillStyle = data.fillColor;
      context.fillRect(left, top, right - left, bottom - top);

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

      drawHorizontalArrow(context, left, right, centerY, 10 * pixelRatio, data.horizontalArrowSide);

      drawVerticalArrow(context, centerX, top, bottom, 10 * pixelRatio, data.verticalArrowSide);

      drawInfoBox(
        context,
        centerX,
        top - data.style.infoOffset * pixelRatio,
        data.infoLines,
        data.style,
        data.infoBackgroundColor,
        data.infoTextColor,
        pixelRatio,
        verticalPixelRatio,
      );

      context.restore();
    });
  }
}

function getBounds(startPoint: Point, endPoint: Point): Bounds {
  return {
    left: Math.min(startPoint.x, endPoint.x),
    right: Math.max(startPoint.x, endPoint.x),
    top: Math.min(startPoint.y, endPoint.y),
    bottom: Math.max(startPoint.y, endPoint.y),
  };
}

function drawHorizontalArrow(
  context: CanvasRenderingContext2D,
  left: number,
  right: number,
  y: number,
  size: number,
  side: Direction.Left | Direction.Right | null,
): void {
  context.beginPath();
  context.moveTo(left, y);
  context.lineTo(right, y);
  context.stroke();

  if (!side) {
    return;
  }

  context.beginPath();

  if (side === Direction.Left) {
    context.moveTo(left, y);
    context.lineTo(left + size, y - size);
    context.moveTo(left, y);
    context.lineTo(left + size, y + size);
  }

  if (side === Direction.Right) {
    context.moveTo(right, y);
    context.lineTo(right - size, y - size);
    context.moveTo(right, y);
    context.lineTo(right - size, y + size);
  }

  context.stroke();
}

function drawVerticalArrow(
  context: CanvasRenderingContext2D,
  x: number,
  top: number,
  bottom: number,
  size: number,
  side: Direction.Top | Direction.Bottom | null,
): void {
  context.beginPath();
  context.moveTo(x, top);
  context.lineTo(x, bottom);
  context.stroke();

  if (!side) {
    return;
  }

  context.beginPath();

  if (side === Direction.Top) {
    context.moveTo(x, top);
    context.lineTo(x - size, top + size);
    context.moveTo(x, top);
    context.lineTo(x + size, top + size);
  }

  if (side === Direction.Bottom) {
    context.moveTo(x, bottom);
    context.lineTo(x - size, bottom - size);
    context.moveTo(x, bottom);
    context.lineTo(x + size, bottom - size);
  }

  context.stroke();
}

function drawInfoBox(
  context: CanvasRenderingContext2D,
  centerX: number,
  topY: number,
  lines: readonly string[],
  style: Required<RulerUiStyle>,
  fillColor: string,
  textColor: string,
  pixelRatio: number,
  verticalPixelRatio: number,
): void {
  context.save();
  context.font = style.infoFont;
  context.textAlign = 'center';

  const padding = style.padding * pixelRatio;
  const lineHeight = 14 * verticalPixelRatio;
  const gap = 2 * verticalPixelRatio;

  let maxWidth = 0;

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

  const boxWidth = maxWidth + padding * 2;
  const boxHeight = lines.length * lineHeight + (lines.length - 1) * gap + padding * 2;

  const boxX = centerX - boxWidth / 2;
  const boxY = topY - boxHeight;

  context.fillStyle = fillColor;
  context.beginPath();
  drawRoundedRect(context, boxX, boxY, boxWidth, boxHeight, 2 * pixelRatio);
  context.fill();

  context.fillStyle = textColor;

  let textY = boxY + padding + lineHeight * 0.8;

  for (const line of lines) {
    context.fillText(line, centerX, textY);
    textY += lineHeight + gap;
  }

  context.restore();
}

function drawRoundedRect(
  context: CanvasRenderingContext2D,
  x: number,
  y: number,
  width: number,
  height: number,
  radius: number,
): void {
  const safeRadius = Math.min(radius, width / 2, height / 2);

  context.moveTo(x + safeRadius, y);
  context.arcTo(x + width, y, x + width, y + height, safeRadius);
  context.arcTo(x + width, y + height, x, y + height, safeRadius);
  context.arcTo(x, y + height, x, y, safeRadius);
  context.arcTo(x, y, x + width, y, safeRadius);
  context.closePath();
}


import { getThemeStore } from '@src/theme';
import { SettingField, SettingsTab, SettingsValues } from '@src/types';

export interface RulerStyle {
  lineColor: string;
  fillColor: string;
  infoTextColor: string;
  infoBackgroundColor: string;
}

export type RulerSettings = SettingsValues & RulerStyle;

export function createDefaultSettings(): RulerSettings {
  const { colors } = getThemeStore();

  return {
    lineColor: colors.chartLineColor,
    fillColor: colors.rulerPositiveFill,
    infoTextColor: colors.chartPriceLineText,
    infoBackgroundColor: colors.axisMarkerLabelFill,
  };
}

export function getRulerSettingsTabs(settings: RulerSettings): SettingsTab[] {
  const fields: SettingField[] = [
    {
      key: 'lineColor',
      label: 'Цвет линии',
      type: 'color',
      defaultValue: settings.lineColor,
    },
    {
      key: 'fillColor',
      label: 'Цвет фона',
      type: 'color',
      defaultValue: settings.fillColor,
    },
    {
      key: 'infoTextColor',
      label: 'Цвет текста окна',
      type: 'color',
      defaultValue: settings.infoTextColor,
    },
    {
      key: 'infoBackgroundColor',
      label: 'Цвет фона окна',
      type: 'color',
      defaultValue: settings.infoBackgroundColor,
    },
  ];

  return [
    {
      key: 'style',
      label: 'Стиль',
      fields,
    },
  ];
}