Загрузка данных
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,
},
];
}