From c76b71143b63dc9ead199687431b8dbc5269baaa Mon Sep 17 00:00:00 2001 From: jaredvu Date: Sun, 26 Nov 2023 13:32:54 -0800 Subject: [PATCH] Add hoverCanvas. Optimize Clearing for canvas --- .../orderbook/useDrawOrderbookHistograms.ts | 154 ++++++++++++------ src/views/Orderbook.tsx | 76 ++++++--- src/views/Orderbook/OrderbookRow.tsx | 12 +- 3 files changed, 154 insertions(+), 88 deletions(-) diff --git a/src/hooks/orderbook/useDrawOrderbookHistograms.ts b/src/hooks/orderbook/useDrawOrderbookHistograms.ts index ed9979c..c974ec2 100644 --- a/src/hooks/orderbook/useDrawOrderbookHistograms.ts +++ b/src/hooks/orderbook/useDrawOrderbookHistograms.ts @@ -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(null); + const hoverCanvasRef = useRef(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; price?: Nullable; mine?: Nullable; - 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(); + 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 }; }; diff --git a/src/views/Orderbook.tsx b/src/views/Orderbook.tsx index 49903aa..b654197 100644 --- a/src/views/Orderbook.tsx +++ b/src/views/Orderbook.tsx @@ -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} /> + ) + )} <$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} /> + ) + )} )} diff --git a/src/views/Orderbook/OrderbookRow.tsx b/src/views/Orderbook/OrderbookRow.tsx index 0ad8189..e3cf225 100644 --- a/src/views/Orderbook/OrderbookRow.tsx +++ b/src/views/Orderbook/OrderbookRow.tsx @@ -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); - } -`;