Загрузка данных
import { getThemeStore } from '@src/theme';
import { SettingField, SettingsTab, SettingsValues } from '@src/types';
export interface SliderPositionStyle {
lineColor: string;
positiveFillColor: string;
negativeFillColor: string;
}
export interface SliderPositionTextStyle {
textColor: string;
fontSize: number;
isBold: boolean;
isItalic: boolean;
}
export type SliderPositionSettings = SettingsValues & SliderPositionStyle & SliderPositionTextStyle;
export function createDefaultSettings(): SliderPositionSettings {
const { colors } = getThemeStore();
return {
lineColor: colors.chartCrosshairLine,
positiveFillColor: colors.sliderPositiveFill,
negativeFillColor: colors.sliderNegativeFill,
textColor: colors.chartPriceLineText,
fontSize: 10,
isBold: false,
isItalic: false,
};
}
export function getSliderPositionSettingsTabs(settings: SliderPositionSettings): SettingsTab[] {
const styleFields: SettingField[] = [
{
key: 'lineColor',
label: 'Цвет линии',
type: 'color',
defaultValue: settings.lineColor,
},
{
key: 'positiveFillColor',
label: 'Цвет зоны прибыли',
type: 'color',
defaultValue: settings.positiveFillColor,
},
{
key: 'negativeFillColor',
label: 'Цвет зоны убытка',
type: 'color',
defaultValue: settings.negativeFillColor,
},
];
const textFields: SettingField[] = [
{
key: 'textColor',
label: 'Цвет текста',
type: 'color',
defaultValue: settings.textColor,
},
{
key: 'fontSize',
label: 'Размер текста',
type: 'number',
defaultValue: settings.fontSize,
min: 8,
max: 24,
},
{
key: 'isBold',
label: 'Жирный текст',
type: 'boolean',
defaultValue: settings.isBold,
},
{
key: 'isItalic',
label: 'Курсив',
type: 'boolean',
defaultValue: settings.isItalic,
},
];
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 type { SliderPosition } from './sliderPosition';
import type { SliderPositionTextStyle } from './settings';
const UI = {
lineWidth: 1,
lineHeightMultiplier: 1.2,
mainBoxHeight: 28,
sideBoxHeight: 16,
padding: 4,
boxRadius: 4,
labelOffset: 10,
handleSize: 10,
handleRadius: 3,
handleBorderWidth: 1,
};
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 }) => {
const startX = data.startX * horizontalPixelRatio;
const endX = data.endX * horizontalPixelRatio;
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 sideBoxHeightPx = UI.sideBoxHeight * verticalPixelRatio;
const mainBoxHeightPx = UI.mainBoxHeight * verticalPixelRatio;
const labelOffsetPx = UI.labelOffset * verticalPixelRatio;
const targetLabelCenterY =
targetY < entryY
? targetY - labelOffsetPx - sideBoxHeightPx / 2
: targetY + labelOffsetPx + sideBoxHeightPx / 2;
const stopLabelCenterY =
stopY < entryY ? stopY - labelOffsetPx - sideBoxHeightPx / 2 : stopY + labelOffsetPx + sideBoxHeightPx / 2;
context.save();
if (data.showFill) {
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 = UI.lineWidth * Math.max(horizontalPixelRatio, verticalPixelRatio);
context.strokeStyle = data.lineColor;
drawHorizontalLine(context, left, right, entryY);
if (data.showHandles) {
drawHandle(context, startX, entryY, horizontalPixelRatio, verticalPixelRatio, 'circle');
drawHandle(context, endX, entryY, horizontalPixelRatio, verticalPixelRatio, 'rounded');
drawHandle(context, startX, targetY, horizontalPixelRatio, verticalPixelRatio, 'rounded');
drawHandle(context, startX, stopY, horizontalPixelRatio, verticalPixelRatio, 'rounded');
}
if (data.showLabels) {
drawTextBox(
context,
centerX,
targetLabelCenterY,
data.targetText,
data.positiveFillColor,
data,
horizontalPixelRatio,
verticalPixelRatio,
UI.sideBoxHeight,
);
drawTextBox(
context,
centerX,
entryY + labelOffsetPx + mainBoxHeightPx / 2,
data.centerText,
data.centerBoxColor,
data,
horizontalPixelRatio,
verticalPixelRatio,
UI.mainBoxHeight,
);
drawTextBox(
context,
centerX,
stopLabelCenterY,
data.stopText,
data.negativeFillColor,
data,
horizontalPixelRatio,
verticalPixelRatio,
UI.sideBoxHeight,
);
}
context.restore();
});
}
}
function drawHorizontalLine(context: CanvasRenderingContext2D, left: number, right: number, y: number): void {
context.beginPath();
context.moveTo(left, y);
context.lineTo(right, y);
context.stroke();
}
function drawHandle(
context: CanvasRenderingContext2D,
x: number,
y: number,
horizontalPixelRatio: number,
verticalPixelRatio: number,
shape: 'circle' | 'rounded',
): void {
const width = UI.handleSize * horizontalPixelRatio;
const height = UI.handleSize * verticalPixelRatio;
const left = x - width / 2;
const top = y - height / 2;
const { colors } = getThemeStore();
context.save();
context.fillStyle = colors.chartBackground;
context.strokeStyle = colors.chartLineColor;
context.lineWidth = UI.handleBorderWidth * Math.max(horizontalPixelRatio, verticalPixelRatio);
context.beginPath();
if (shape === 'circle') {
context.arc(x, y, Math.min(width, height) / 2, 0, Math.PI * 2);
} else {
drawRoundedRect(
context,
left,
top,
width,
height,
UI.handleRadius * Math.max(horizontalPixelRatio, verticalPixelRatio),
);
}
context.fill();
context.stroke();
context.restore();
}
function drawTextBox(
context: CanvasRenderingContext2D,
centerX: number,
centerY: number,
text: string,
fillColor: string,
textStyle: SliderPositionTextStyle,
horizontalPixelRatio: number,
verticalPixelRatio: number,
fixedHeight: number,
): void {
context.save();
const lines = text.split('\n');
const fontSize = textStyle.fontSize * verticalPixelRatio;
const lineHeight = Math.round(textStyle.fontSize * UI.lineHeightMultiplier) * verticalPixelRatio;
const paddingX = UI.padding * horizontalPixelRatio;
const paddingY = UI.padding * verticalPixelRatio;
const minBoxHeight = fixedHeight * verticalPixelRatio;
const boxHeight = Math.max(minBoxHeight, lines.length * lineHeight + paddingY * 2);
const radius = UI.boxRadius * Math.max(horizontalPixelRatio, verticalPixelRatio);
context.font = getTextFont(textStyle, fontSize);
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 = textStyle.textColor;
if (lines.length === 1) {
context.fillText(lines[0], centerX, centerY);
context.restore();
return;
}
const textBlockHeight = lines.length * lineHeight;
const firstLineCenterY = centerY - textBlockHeight / 2 + lineHeight / 2;
lines.forEach((line, index) => {
context.fillText(line, centerX, firstLineCenterY + index * lineHeight);
});
context.restore();
}
function getTextFont(style: SliderPositionTextStyle, fontSize: number): string {
const italic = style.isItalic ? 'italic ' : '';
const bold = style.isBold ? '700 ' : '';
return `${italic}${bold}${fontSize}px Inter, sans-serif`;
}
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();
}