Add hoverCanvas. Optimize Clearing for canvas

This commit is contained in:
jaredvu 2023-11-26 13:32:54 -08:00
parent a2dfda3481
commit c76b71143b
No known key found for this signature in database
GPG Key ID: B9FE2F3F0A5D523C
3 changed files with 154 additions and 88 deletions

View File

@ -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 };
};

View File

@ -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>
</>
)}

View File

@ -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);
}
`;