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


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

export interface RayStyle {
  lineColor: string;
}

export interface RayTextStyle {
  text: string;
  fontSize: number;
  isBold: boolean;
  isItalic: boolean;
  textColor: string;
}

export type RaySettings = RayStyle & RayTextStyle & SettingsValues;

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

  return {
    lineColor: colors.chartLineColor,
    text: '',
    fontSize: 14,
    isBold: false,
    isItalic: false,
    textColor: colors.chartPriceLineText,
  };
}

export function getRaySettingTabs(settings: RaySettings): SettingsTab[] {
  const styleFields: SettingField[] = [
    {
      key: 'lineColor',
      label: 'Цвет линии',
      type: 'color',
      defaultValue: settings.lineColor,
    },
  ];

  const textFields: SettingField[] = [
    {
      key: 'fontSize',
      label: 'Размер текста',
      type: 'number',
      defaultValue: settings.fontSize,
      min: 8,
      max: 24,
    },
    {
      key: 'text',
      label: 'Текст',
      type: 'textarea',
      defaultValue: settings.text,
      placeholder: 'Введите текст',
    },
    {
      key: 'isBold',
      label: 'Жирный текст',
      type: 'boolean',
      defaultValue: settings.isBold,
    },
    {
      key: 'isItalic',
      label: 'Курсив',
      type: 'boolean',
      defaultValue: settings.isItalic,
    },
    {
      key: 'textColor',
      label: 'Цвет текста',
      type: 'color',
      defaultValue: settings.textColor,
    },
  ];

  return [
    { key: 'style', label: 'Стиль', fields: styleFields },
    { key: 'text', label: 'Текст', fields: textFields },
  ];
}



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

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

import { Ray } from './ray';

const UI = {
  lineWidth: 2,
  handleRadius: 5,
  handleBorderWidth: 2,
  textLineHeightMultiplier: 1.2,
  textOffset: 8,
};

export class RayPaneRenderer implements IPrimitivePaneRenderer {
  private readonly ray: Ray;

  constructor(ray: Ray) {
    this.ray = ray;
  }

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

    if (!data) {
      return;
    }

    const { colors } = getThemeStore();

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

      const startX = data.startPoint.x * horizontalPixelRatio;
      const startY = data.startPoint.y * verticalPixelRatio;
      const directionX = data.directionPoint.x * horizontalPixelRatio;
      const directionY = data.directionPoint.y * verticalPixelRatio;
      const endX = data.rayEndPoint.x * horizontalPixelRatio;
      const endY = data.rayEndPoint.y * verticalPixelRatio;

      context.save();

      context.lineWidth = UI.lineWidth * pixelRatio;
      context.strokeStyle = data.lineColor;
      context.beginPath();
      context.moveTo(startX, startY);
      context.lineTo(endX, endY);
      context.stroke();

      drawRayText(context, {
        startX,
        startY,
        directionX,
        directionY,
        text: data.text,
        fontSize: data.fontSize,
        isBold: data.isBold,
        isItalic: data.isItalic,
        textColor: data.textColor,
        pixelRatio,
      });

      if (data.showHandles) {
        context.fillStyle = colors.chartBackground;
        context.strokeStyle = data.lineColor;
        context.lineWidth = UI.handleBorderWidth * pixelRatio;

        drawHandle(context, startX, startY, UI.handleRadius * pixelRatio);
        drawHandle(context, directionX, directionY, UI.handleRadius * pixelRatio);
      }

      context.restore();
    });
  }
}

function drawRayText(
  context: CanvasRenderingContext2D,
  params: {
    startX: number;
    startY: number;
    directionX: number;
    directionY: number;
    text: string;
    fontSize: number;
    isBold: boolean;
    isItalic: boolean;
    textColor: string;
    pixelRatio: number;
  },
): void {
  const { startX, startY, directionX, directionY, text, fontSize, isBold, isItalic, textColor, pixelRatio } = params;

  if (!text.trim()) {
    return;
  }

  const dx = directionX - startX;
  const dy = directionY - startY;

  if (dx === 0 && dy === 0) {
    return;
  }

  const lines = text.split('\n');
  const safeFontSize = Math.max(1, fontSize);
  const fontSizePx = safeFontSize * pixelRatio;
  const lineHeight = safeFontSize * UI.textLineHeightMultiplier * pixelRatio;
  const fontWeight = isBold ? '700 ' : '';
  const fontStyle = isItalic ? 'italic ' : '';

  const angle = Math.atan2(dy, dx);
  const textX = (startX + directionX) / 2;
  const textY = (startY + directionY) / 2;
  const textOffset = UI.textOffset * pixelRatio;
  const blockHeight = lines.length * lineHeight;

  context.save();
  context.translate(textX, textY);
  context.rotate(angle);

  context.font = `${fontStyle}${fontWeight}${fontSizePx}px Inter, sans-serif`;
  context.fillStyle = textColor;
  context.textAlign = 'center';
  context.textBaseline = 'middle';

  const textCenterY = -(blockHeight / 2 + textOffset);
  const startLineY = textCenterY - blockHeight / 2 + lineHeight / 2;

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

  context.restore();
}

function drawHandle(context: CanvasRenderingContext2D, x: number, y: number, radius: number): void {
  context.beginPath();
  context.arc(x, y, radius, 0, Math.PI * 2);
  context.fill();
  context.stroke();
}