Add hoverCanvas. Optimize Clearing for canvas
This commit is contained in:
parent
a2dfda3481
commit
c76b71143b
@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import type { Nullable } from '@/constants/abacus';
|
||||
@ -6,7 +6,7 @@ import { SMALL_USD_DECIMALS, TOKEN_DECIMALS } from '@/constants/numbers';
|
||||
|
||||
import { getAppTheme } from '@/state/configsSelectors';
|
||||
|
||||
import type { RowData } from '@/views/Orderbook/OrderbookRow';
|
||||
import { ROW_HEIGHT, ROW_PADDING_RIGHT, type RowData } from '@/views/Orderbook/OrderbookRow';
|
||||
|
||||
import { MustBigNumber } from '@/lib/numbers';
|
||||
|
||||
@ -27,7 +27,9 @@ export const useDrawOrderbookHistograms = ({
|
||||
}) => {
|
||||
const selectedTheme = useSelector(getAppTheme);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const hoverCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const canvas = canvasRef.current;
|
||||
const hoverCanvas = hoverCanvasRef.current;
|
||||
|
||||
/**
|
||||
* to === 'right' to === 'left'
|
||||
@ -49,15 +51,16 @@ export const useDrawOrderbookHistograms = ({
|
||||
gradientMultiplier: number;
|
||||
}) => {
|
||||
const gradient = {
|
||||
x1: to === 'right' ? barWidth : canvasWidth - barWidth,
|
||||
x2:
|
||||
x1: Math.floor(to === 'right' ? barWidth : canvasWidth - barWidth),
|
||||
x2: Math.floor(
|
||||
to === 'right'
|
||||
? 0 - (canvasWidth * gradientMultiplier - canvasWidth)
|
||||
: canvasWidth * gradientMultiplier,
|
||||
: canvasWidth * gradientMultiplier
|
||||
),
|
||||
};
|
||||
const bar = {
|
||||
x1: to === 'right' ? 0 : canvasWidth - barWidth,
|
||||
x2: to === 'right' ? Math.min(barWidth, canvasWidth - 2) : canvasWidth - 2,
|
||||
x1: Math.floor(to === 'right' ? 0 : canvasWidth - barWidth),
|
||||
x2: Math.floor(to === 'right' ? Math.min(barWidth, canvasWidth - 2) : canvasWidth - 2),
|
||||
};
|
||||
|
||||
return {
|
||||
@ -66,6 +69,22 @@ export const useDrawOrderbookHistograms = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getYFromIndex = (idx: number, type: 'text' | 'bar' | 'rect') => {
|
||||
return (
|
||||
idx * ROW_HEIGHT +
|
||||
{
|
||||
text: ROW_HEIGHT / 2, // center of the row
|
||||
bar: 1, // 1px off the top and bottom so the histograms do not touch
|
||||
rect: 0,
|
||||
}[type]
|
||||
);
|
||||
};
|
||||
|
||||
// Get X value by column, colIdx starts at 0
|
||||
const getXByColumn = ({ canvasWidth, colIdx }: { canvasWidth: number; colIdx: number }) => {
|
||||
return Math.floor(((colIdx + 1) * canvasWidth) / 3);
|
||||
};
|
||||
|
||||
const drawBars = ({
|
||||
value,
|
||||
canvasWidth,
|
||||
@ -83,11 +102,16 @@ export const useDrawOrderbookHistograms = ({
|
||||
idx: number;
|
||||
to: 'left' | 'right';
|
||||
}) => {
|
||||
const histogramBarWidth = ctx.canvas.width - 2;
|
||||
const histogramBarWidth = canvasWidth - 2;
|
||||
const barWidth = value ? (value / histogramRange) * histogramBarWidth : 0;
|
||||
const { gradient, bar } = getXYValues({ barWidth, canvasWidth, gradientMultiplier });
|
||||
|
||||
const linearGradient = ctx.createLinearGradient(gradient.x1, 10, gradient.x2, 10);
|
||||
const linearGradient = ctx.createLinearGradient(
|
||||
gradient.x1,
|
||||
ROW_HEIGHT / 2,
|
||||
gradient.x2,
|
||||
ROW_HEIGHT / 2
|
||||
);
|
||||
linearGradient.addColorStop(0, histogramAccentColor);
|
||||
linearGradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
ctx.fillStyle = linearGradient;
|
||||
@ -97,42 +121,39 @@ export const useDrawOrderbookHistograms = ({
|
||||
ctx.roundRect
|
||||
? ctx.roundRect?.(
|
||||
bar.x1,
|
||||
idx * 20 + 1,
|
||||
getYFromIndex(idx, 'bar'),
|
||||
bar.x2,
|
||||
18,
|
||||
ROW_HEIGHT - 2,
|
||||
to === 'left' ? [2, 0, 0, 2] : [0, 2, 2, 0]
|
||||
)
|
||||
: ctx.rect(bar.x1, idx * 20 + 1, bar.x2, 18);
|
||||
: ctx.rect(bar.x1, getYFromIndex(idx, 'bar'), bar.x2, ROW_HEIGHT - 2);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
const drawText = ({
|
||||
ctx,
|
||||
canvasWidth,
|
||||
idx,
|
||||
size,
|
||||
price,
|
||||
mine,
|
||||
x,
|
||||
}: {
|
||||
ctx: CanvasRenderingContext2D;
|
||||
canvasWidth: number;
|
||||
idx: number;
|
||||
size?: Nullable<number>;
|
||||
price?: Nullable<number>;
|
||||
mine?: Nullable<number>;
|
||||
x: number;
|
||||
}) => {
|
||||
ctx.font = `12.5px Satoshi`;
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
const y = getYFromIndex(idx, 'text');
|
||||
|
||||
// Size text
|
||||
if (size) {
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillText(
|
||||
MustBigNumber(size).toFixed(stepSizeDecimals ?? TOKEN_DECIMALS),
|
||||
x - 8,
|
||||
idx * 20 + 10
|
||||
getXByColumn({ canvasWidth, colIdx: 0 }) - ROW_PADDING_RIGHT,
|
||||
y
|
||||
);
|
||||
}
|
||||
|
||||
@ -141,8 +162,8 @@ export const useDrawOrderbookHistograms = ({
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillText(
|
||||
MustBigNumber(price).toFixed(tickSizeDecimals ?? SMALL_USD_DECIMALS),
|
||||
x * 2 - 8,
|
||||
idx * 20 + 10
|
||||
getXByColumn({ canvasWidth, colIdx: 1 }) - ROW_PADDING_RIGHT,
|
||||
y
|
||||
);
|
||||
}
|
||||
|
||||
@ -151,19 +172,24 @@ export const useDrawOrderbookHistograms = ({
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillText(
|
||||
MustBigNumber(mine).toFixed(stepSizeDecimals ?? TOKEN_DECIMALS),
|
||||
x * 3 - 8,
|
||||
idx * 20 + 10
|
||||
getXByColumn({ canvasWidth, colIdx: 2 }) - ROW_PADDING_RIGHT,
|
||||
y
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const ctx = canvas?.getContext('2d');
|
||||
|
||||
if (!canvas || !ctx) return;
|
||||
|
||||
/**
|
||||
* Scale canvas using device pixel ratio to unblur drawn text
|
||||
* @returns adjusted canvas width used in coordinates for drawing
|
||||
**/
|
||||
const canvasWidth = useMemo(() => {
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
|
||||
if (!canvas || !hoverCanvas) return 300 / devicePixelRatio;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const hoverCtx = hoverCanvas.getContext('2d');
|
||||
|
||||
const backingStoreRatio =
|
||||
// @ts-ignore
|
||||
ctx.webkitBackingStorePixelRatio ||
|
||||
@ -178,40 +204,68 @@ export const useDrawOrderbookHistograms = ({
|
||||
1;
|
||||
|
||||
const ratio = devicePixelRatio / backingStoreRatio;
|
||||
|
||||
canvas.width = canvas.offsetWidth * ratio;
|
||||
canvas.height = canvas.offsetHeight * ratio;
|
||||
hoverCanvas.width = hoverCanvas.offsetWidth * ratio;
|
||||
hoverCanvas.height = hoverCanvas.offsetHeight * ratio;
|
||||
|
||||
// Scale the context
|
||||
ctx.scale(ratio, ratio);
|
||||
if (hoverCtx) {
|
||||
hoverCtx.scale(ratio, ratio);
|
||||
}
|
||||
|
||||
if (ctx) {
|
||||
ctx.scale(ratio, ratio);
|
||||
ctx.font = `12.5px Satoshi`;
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
}
|
||||
|
||||
return canvas.width / ratio;
|
||||
}, [canvas, hoverCanvas]);
|
||||
|
||||
/**
|
||||
* Handle Row Hover
|
||||
*/
|
||||
const lastHoveredRowRef = useRef<number>();
|
||||
const lastHoveredRow = lastHoveredRowRef.current;
|
||||
useEffect(() => {
|
||||
const hoverCtx = hoverCanvas?.getContext('2d');
|
||||
|
||||
if (!!!hoverCtx) return;
|
||||
|
||||
if (hoveredRow !== lastHoveredRow) {
|
||||
const y = getYFromIndex(lastHoveredRow ?? 0, 'rect');
|
||||
hoverCtx.clearRect(0, y, canvasWidth, ROW_HEIGHT);
|
||||
lastHoveredRowRef.current = hoveredRow;
|
||||
}
|
||||
|
||||
if (hoveredRow !== undefined) {
|
||||
hoverCtx.fillStyle = 'rgba(255, 255, 255, 0.02)';
|
||||
hoverCtx.fillRect(0, hoveredRow * 20, canvasWidth, 20);
|
||||
}
|
||||
}, [lastHoveredRow, hoveredRow]);
|
||||
|
||||
// Update histograms and row contents on data change
|
||||
useEffect(() => {
|
||||
const ctx = canvas?.getContext('2d');
|
||||
|
||||
if (!canvas || !ctx) return;
|
||||
|
||||
// Clear canvas before redraw
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
const canvasWidth = canvas.width / ratio;
|
||||
const columnX = canvasWidth / 3;
|
||||
|
||||
// Draw histograms
|
||||
data.forEach(({ depth, mine, price, size, side }, idx) => {
|
||||
const isHovered = hoveredRow === idx && size !== undefined;
|
||||
let histogramAlpha = 0.15;
|
||||
|
||||
if (isHovered) {
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.02)';
|
||||
ctx.fillRect(0, idx * 20, canvasWidth, 20);
|
||||
histogramAlpha = 0.75;
|
||||
}
|
||||
|
||||
const histogramAccentColor =
|
||||
side === 'bid'
|
||||
? `hsla(159, 67%, 39%, ${histogramAlpha})`
|
||||
: `hsla(360, 73%, 61%, ${histogramAlpha})`;
|
||||
side === 'bid' ? `hsla(159, 67%, 39%, 0.15)` : `hsla(360, 73%, 61%, 0.15)`;
|
||||
|
||||
// Depth Bar
|
||||
drawBars({
|
||||
value: depth,
|
||||
canvasWidth,
|
||||
ctx,
|
||||
gradientMultiplier: 2,
|
||||
gradientMultiplier: 1.3,
|
||||
histogramAccentColor,
|
||||
idx,
|
||||
to,
|
||||
@ -231,14 +285,14 @@ export const useDrawOrderbookHistograms = ({
|
||||
// Size, Price, Mine
|
||||
drawText({
|
||||
ctx,
|
||||
canvasWidth,
|
||||
idx,
|
||||
size,
|
||||
price,
|
||||
mine,
|
||||
x: columnX,
|
||||
});
|
||||
});
|
||||
}, [data, histogramRange, hoveredRow, selectedTheme, stepSizeDecimals, tickSizeDecimals, to]);
|
||||
}, [canvasWidth, data, histogramRange, selectedTheme, stepSizeDecimals, tickSizeDecimals, to]);
|
||||
|
||||
return { canvasRef };
|
||||
return { canvasRef, hoverCanvasRef };
|
||||
};
|
||||
|
||||
@ -23,7 +23,7 @@ import { Canvas } from '@/components/Canvas';
|
||||
import { LoadingSpace } from '@/components/Loading/LoadingSpinner';
|
||||
import { Tag } from '@/components/Tag';
|
||||
|
||||
import { Row, type RowData } from './Orderbook/OrderbookRow';
|
||||
import { ROW_HEIGHT, Row, type RowData } from './Orderbook/OrderbookRow';
|
||||
import { SpreadRow } from './Orderbook/SpreadRow';
|
||||
|
||||
const ORDERBOOK_MAX_ROWS_PER_SIDE = 35;
|
||||
@ -115,7 +115,7 @@ export const Orderbook = ({
|
||||
side: 'bid' | 'ask';
|
||||
}>();
|
||||
|
||||
const { canvasRef: asksCanvasRef } = useDrawOrderbookHistograms({
|
||||
const { canvasRef: asksCanvasRef, hoverCanvasRef: asksHoverRef } = useDrawOrderbookHistograms({
|
||||
data: asksSlice.reverse(),
|
||||
histogramRange,
|
||||
stepSizeDecimals,
|
||||
@ -123,7 +123,7 @@ export const Orderbook = ({
|
||||
hoveredRow: hoveredRow?.side === 'ask' ? hoveredRow.idx : undefined,
|
||||
});
|
||||
|
||||
const { canvasRef: bidsCanvasRef } = useDrawOrderbookHistograms({
|
||||
const { canvasRef: bidsCanvasRef, hoverCanvasRef: bidsHoverRef } = useDrawOrderbookHistograms({
|
||||
data: bidsSlice.reverse(),
|
||||
histogramRange,
|
||||
stepSizeDecimals,
|
||||
@ -163,20 +163,31 @@ export const Orderbook = ({
|
||||
>
|
||||
<$HistogramCanvas
|
||||
ref={asksCanvasRef}
|
||||
width={asksCanvasRef.current?.clientWidth ?? 0}
|
||||
height={asksCanvasRef.current?.clientHeight ?? 0}
|
||||
width={300}
|
||||
height={maxRowsPerSide * ROW_HEIGHT}
|
||||
// width={asksCanvasRef.current?.clientWidth ?? 0}
|
||||
// height={asksCanvasRef.current?.clientHeight ?? 0}
|
||||
/>
|
||||
<$HistogramCanvas
|
||||
ref={asksHoverRef}
|
||||
width={300}
|
||||
height={maxRowsPerSide * ROW_HEIGHT}
|
||||
// width={asksCanvasRef.current?.clientWidth ?? 0}
|
||||
// height={asksCanvasRef.current?.clientHeight ?? 0}
|
||||
/>
|
||||
|
||||
{asksSlice.map((row: RowData, idx) => (
|
||||
<$Row
|
||||
key={idx}
|
||||
title={row.price ? `${row.price}` : undefined}
|
||||
onClick={() => (row.price ? onRowAction(row.price) : {})}
|
||||
onMouseOver={(e) => {
|
||||
setHoveredRow({ idx, side: 'ask' });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{asksSlice.map((row: RowData, idx) =>
|
||||
row.price ? (
|
||||
<$Row
|
||||
key={idx}
|
||||
title={`${row.price}`}
|
||||
onClick={() => onRowAction(row.price)}
|
||||
onMouseOver={(e) => setHoveredRow({ idx, side: 'ask' })}
|
||||
/>
|
||||
) : (
|
||||
<$Row key={idx} />
|
||||
)
|
||||
)}
|
||||
</$OrderbookSideContainer>
|
||||
|
||||
<SpreadRow
|
||||
@ -193,20 +204,31 @@ export const Orderbook = ({
|
||||
>
|
||||
<$HistogramCanvas
|
||||
ref={bidsCanvasRef}
|
||||
width={bidsCanvasRef.current?.clientWidth ?? 0}
|
||||
height={bidsCanvasRef.current?.clientHeight ?? 0}
|
||||
width={300}
|
||||
height={maxRowsPerSide * ROW_HEIGHT}
|
||||
// width={bidsCanvasRef.current?.clientWidth ?? 0}
|
||||
// height={bidsCanvasRef.current?.clientHeight ?? 0}
|
||||
/>
|
||||
<$HistogramCanvas
|
||||
ref={bidsHoverRef}
|
||||
width={300}
|
||||
height={maxRowsPerSide * ROW_HEIGHT}
|
||||
// width={bidsCanvasRef.current?.clientWidth ?? 0}
|
||||
// height={bidsCanvasRef.current?.clientHeight ?? 0}
|
||||
/>
|
||||
|
||||
{bidsSlice.map((row: RowData, idx) => (
|
||||
<$Row
|
||||
key={idx}
|
||||
title={row.price ? `${row.price}` : undefined}
|
||||
onClick={() => (row.price ? onRowAction(row.price) : {})}
|
||||
onMouseOver={(e) => {
|
||||
setHoveredRow({ idx, side: 'bid' });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{bidsSlice.map((row: RowData, idx) =>
|
||||
row.price ? (
|
||||
<$Row
|
||||
key={idx}
|
||||
title={`${row.price}`}
|
||||
onClick={() => onRowAction(row.price)}
|
||||
onMouseOver={(e) => setHoveredRow({ idx, side: 'bid' })}
|
||||
/>
|
||||
) : (
|
||||
<$Row key={idx} />
|
||||
)
|
||||
)}
|
||||
</$OrderbookSideContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { OrderbookLine } from '@/constants/abacus';
|
||||
|
||||
import { Output, OutputType } from '@/components/Output';
|
||||
|
||||
export type RowData = OrderbookLine & { side: 'bid' | 'ask'; mine?: number };
|
||||
|
||||
export const ROW_HEIGHT = 20;
|
||||
export const ROW_PADDING_RIGHT = 8;
|
||||
|
||||
export const Row = styled.div`
|
||||
display: flex;
|
||||
@ -27,10 +24,3 @@ export const Row = styled.div`
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
const $OrderbookRow = styled(Row)`
|
||||
&:hover {
|
||||
// color: var(--color-text-2);
|
||||
background-color: var(--color-layer-3);
|
||||
}
|
||||
`;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user