Skip to content

Commit

Permalink
Merge pull request #818 from tradingview/fancy-canvas-0.3
Browse files Browse the repository at this point in the history
fancy-canvas 2
  • Loading branch information
ezhukovskiy committed Dec 28, 2022
2 parents 2522653 + ae04c7a commit 7a605d8
Show file tree
Hide file tree
Showing 56 changed files with 1,055 additions and 927 deletions.
10 changes: 10 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ jobs:
- run-graphics-tests:
devicePixelRatio: "1.0"

graphics-tests-dpr1_25:
executor: node16-browsers-executor
steps:
- run-graphics-tests:
devicePixelRatio: "1.25"

graphics-tests-dpr1_5:
executor: node16-browsers-executor
steps:
Expand Down Expand Up @@ -329,6 +335,10 @@ workflows:
filters: *merge-based-filters
requires:
- build
- graphics-tests-dpr1_25:
filters: *merge-based-filters
requires:
- build
- graphics-tests-dpr1_5:
filters: *merge-based-filters
requires:
Expand Down
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ module.exports = [
{
name: 'Standalone',
path: 'dist/lightweight-charts.standalone.production.js',
limit: '44.4 KB',
limit: '45.7 KB',
},
];
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"node": ">=16.16.0"
},
"dependencies": {
"fancy-canvas": "0.2.2"
"fancy-canvas": "2.1.0"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "~13.3.0",
Expand Down
9 changes: 5 additions & 4 deletions src/api/time-scale-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Size } from '../gui/canvas-utils';
import { Size } from 'fancy-canvas';

import { TimeAxisWidget } from '../gui/time-axis-widget';

import { assert } from '../helpers/assertions';
Expand Down Expand Up @@ -154,11 +155,11 @@ export class TimeScaleApi implements ITimeScaleApi, IDestroyable {
}

public width(): number {
return this._timeAxisWidget.getSize().w;
return this._timeAxisWidget.getSize().width;
}

public height(): number {
return this._timeAxisWidget.getSize().h;
return this._timeAxisWidget.getSize().height;
}

public subscribeVisibleTimeRangeChange(handler: TimeRangeChangeEventHandler): void {
Expand Down Expand Up @@ -206,6 +207,6 @@ export class TimeScaleApi implements ITimeScaleApi, IDestroyable {
}

private _onSizeChanged(size: Size): void {
this._sizeChanged.fire(size.w, size.h);
this._sizeChanged.fire(size.width, size.height);
}
}
73 changes: 16 additions & 57 deletions src/gui/canvas-utils.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,26 @@
import { Binding as CanvasCoordinateSpaceBinding, bindToDevicePixelRatio } from 'fancy-canvas/coordinate-space';
import {
bindCanvasElementBitmapSizeTo,
CanvasElementBitmapSizeBinding,
Size,
} from 'fancy-canvas';

import { ensureNotNull } from '../helpers/assertions';

export class Size {
public h: number;
public w: number;

public constructor(w: number, h: number) {
this.w = w;
this.h = h;
}

public equals(size: Size): boolean {
return (this.w === size.w) && (this.h === size.h);
}
}

export function getCanvasDevicePixelRatio(canvas: HTMLCanvasElement): number {
return canvas.ownerDocument &&
canvas.ownerDocument.defaultView &&
canvas.ownerDocument.defaultView.devicePixelRatio
|| 1;
}

export function getContext2D(canvas: HTMLCanvasElement): CanvasRenderingContext2D {
const ctx = ensureNotNull(canvas.getContext('2d'));
// sometimes (very often) ctx getContext returns the same context every time
// and there might be previous transformation
// so let's reset it to be sure that everything is ok
// do no use resetTransform to respect Edge
ctx.setTransform(1, 0, 0, 1, 0, 0);
return ctx;
}

export function createPreconfiguredCanvas(doc: Document, size: Size): HTMLCanvasElement {
const canvas = doc.createElement('canvas');

const pixelRatio = getCanvasDevicePixelRatio(canvas);
// we should keep the layout size...
canvas.style.width = `${size.w}px`;
canvas.style.height = `${size.h}px`;
// ...but multiply coordinate space dimensions to device pixel ratio
canvas.width = size.w * pixelRatio;
canvas.height = size.h * pixelRatio;
return canvas;
}

export function createBoundCanvas(parentElement: HTMLElement, size: Size): CanvasCoordinateSpaceBinding {
export function createBoundCanvas(parentElement: HTMLElement, size: Size): CanvasElementBitmapSizeBinding {
const doc = ensureNotNull(parentElement.ownerDocument);
const canvas = doc.createElement('canvas');
parentElement.appendChild(canvas);

const binding = bindToDevicePixelRatio(canvas, { allowDownsampling: false });
binding.resizeCanvas({
width: size.w,
height: size.h,
const binding = bindCanvasElementBitmapSizeTo(canvas, {
type: 'device-pixel-content-box',
options: {
allowResizeObserver: false,
},
transform: (bitmapSize: Size, canvasElementClientSize: Size) => ({
width: Math.max(bitmapSize.width, canvasElementClientSize.width),
height: Math.max(bitmapSize.height, canvasElementClientSize.height),
}),
});
binding.resizeCanvasElement(size);
return binding;
}

export function drawScaled(ctx: CanvasRenderingContext2D, ratio: number, func: () => void): void {
ctx.save();
ctx.scale(ratio, ratio);
func();
ctx.restore();
}
190 changes: 112 additions & 78 deletions src/gui/chart-widget.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Size, size } from 'fancy-canvas';

import { ensureDefined, ensureNotNull } from '../helpers/assertions';
import { isChromiumBased, isWindows } from '../helpers/browsers';
import { drawScaled } from '../helpers/canvas-helpers';
import { Delegate } from '../helpers/delegate';
import { IDestroyable } from '../helpers/idestroyable';
import { ISubscription } from '../helpers/isubscription';
Expand All @@ -20,7 +21,6 @@ import { Series } from '../model/series';
import { SeriesPlotRow } from '../model/series-data';
import { OriginalTime, TimePointIndex } from '../model/time-data';

import { createPreconfiguredCanvas, getCanvasDevicePixelRatio, getContext2D, Size } from './canvas-utils';
// import { PaneSeparator, SEPARATOR_HEIGHT } from './pane-separator';
import { PaneWidget } from './pane-widget';
import { TimeAxisWidget } from './time-axis-widget';
Expand Down Expand Up @@ -237,81 +237,16 @@ export class ChartWidget implements IDestroyable {
this._drawImpl(this._invalidateMask, performance.now());
this._invalidateMask = null;
}
// calculate target size
const firstPane = this._paneWidgets[0];
const targetCanvas = createPreconfiguredCanvas(document, new Size(this._width, this._height));
const ctx = getContext2D(targetCanvas);
const pixelRatio = getCanvasDevicePixelRatio(targetCanvas);
drawScaled(ctx, pixelRatio, () => {
let targetX = 0;
let targetY = 0;

const drawPriceAxises = (position: 'left' | 'right') => {
for (let paneIndex = 0; paneIndex < this._paneWidgets.length; paneIndex++) {
const paneWidget = this._paneWidgets[paneIndex];
const paneWidgetHeight = paneWidget.getSize().h;
const priceAxisWidget = ensureNotNull(position === 'left' ? paneWidget.leftPriceAxisWidget() : paneWidget.rightPriceAxisWidget());
const image = priceAxisWidget.getImage();
ctx.drawImage(image, targetX, targetY, priceAxisWidget.getWidth(), paneWidgetHeight);
targetY += paneWidgetHeight;
// if (paneIndex < this._paneWidgets.length - 1) {
// const separator = this._paneSeparators[paneIndex];
// const separatorSize = separator.getSize();
// const separatorImage = separator.getImage();
// ctx.drawImage(separatorImage, targetX, targetY, separatorSize.w, separatorSize.h);
// targetY += separatorSize.h;
// }
}
};
// draw left price scale if exists
if (this._isLeftAxisVisible()) {
drawPriceAxises('left');
targetX = ensureNotNull(firstPane.leftPriceAxisWidget()).getWidth();
}
targetY = 0;
for (let paneIndex = 0; paneIndex < this._paneWidgets.length; paneIndex++) {
const paneWidget = this._paneWidgets[paneIndex];
const paneWidgetSize = paneWidget.getSize();
const image = paneWidget.getImage();
ctx.drawImage(image, targetX, targetY, paneWidgetSize.w, paneWidgetSize.h);
targetY += paneWidgetSize.h;
// if (paneIndex < this._paneWidgets.length - 1) {
// const separator = this._paneSeparators[paneIndex];
// const separatorSize = separator.getSize();
// const separatorImage = separator.getImage();
// ctx.drawImage(separatorImage, targetX, targetY, separatorSize.w, separatorSize.h);
// targetY += separatorSize.h;
// }
}
targetX += firstPane.getSize().w;
if (this._isRightAxisVisible()) {
targetY = 0;
drawPriceAxises('right');
}
const drawStub = (position: 'left' | 'right') => {
const stub = ensureNotNull(position === 'left' ? this._timeAxisWidget.leftStub() : this._timeAxisWidget.rightStub());
const size = stub.getSize();
const image = stub.getImage();
ctx.drawImage(image, targetX, targetY, size.w, size.h);
};
// draw time scale
if (this._options.timeScale.visible) {
targetX = 0;
if (this._isLeftAxisVisible()) {
drawStub('left');
targetX = ensureNotNull(firstPane.leftPriceAxisWidget()).getWidth();
}
const size = this._timeAxisWidget.getSize();
const image = this._timeAxisWidget.getImage();
ctx.drawImage(image, targetX, targetY, size.w, size.h);
if (this._isRightAxisVisible()) {
targetX += firstPane.getSize().w;
drawStub('right');
ctx.restore();
}
}
});
return targetCanvas;
const screeshotBitmapSize = this._traverseLayout(null);
const screenshotCanvas = document.createElement('canvas');
screenshotCanvas.width = screeshotBitmapSize.width;
screenshotCanvas.height = screeshotBitmapSize.height;

const ctx = ensureNotNull(screenshotCanvas.getContext('2d'));
this._traverseLayout(ctx);

return screenshotCanvas;
}

public getPriceAxisWidth(position: DefaultPriceScaleId): number {
Expand All @@ -336,6 +271,105 @@ export class ChartWidget implements IDestroyable {
return ensureNotNull(priceAxisWidget).getWidth();
}

/**
* Traverses the widget's layout (pane and axis child widgets),
* draws the screenshot (if rendering context is passed) and returns the screenshot bitmap size
*
* @param ctx - if passed, used to draw the screenshot of widget
* @returns screenshot bitmap size
*/
private _traverseLayout(ctx: CanvasRenderingContext2D | null): Size {
let totalWidth = 0;
let totalHeight = 0;

const firstPane = this._paneWidgets[0];

const drawPriceAxises = (position: 'left' | 'right', targetX: number) => {
let targetY = 0;
for (let paneIndex = 0; paneIndex < this._paneWidgets.length; paneIndex++) {
const paneWidget = this._paneWidgets[paneIndex];
const priceAxisWidget = ensureNotNull(position === 'left' ? paneWidget.leftPriceAxisWidget() : paneWidget.rightPriceAxisWidget());
const bitmapSize = priceAxisWidget.getBitmapSize();
if (ctx !== null) {
priceAxisWidget.drawBitmap(ctx, targetX, targetY);
}
targetY += bitmapSize.height;
// if (paneIndex < this._paneWidgets.length - 1) {
// const separator = this._paneSeparators[paneIndex];
// const separatorBitmapSize = separator.getBitmapSize();
// if (ctx !== null) {
// separator.drawBitmap(ctx, targetX, targetY);
// }
// targetY += separatorBitmapSize.height;
// }
}
};

// draw left price scale if exists
if (this._isLeftAxisVisible()) {
drawPriceAxises('left', 0);
const leftAxisBitmapWidth = ensureNotNull(firstPane.leftPriceAxisWidget()).getBitmapSize().width;
totalWidth += leftAxisBitmapWidth;
}
for (let paneIndex = 0; paneIndex < this._paneWidgets.length; paneIndex++) {
const paneWidget = this._paneWidgets[paneIndex];
const bitmapSize = paneWidget.getBitmapSize();
if (ctx !== null) {
paneWidget.drawBitmap(ctx, totalWidth, totalHeight);
}
totalHeight += bitmapSize.height;
// if (paneIndex < this._paneWidgets.length - 1) {
// const separator = this._paneSeparators[paneIndex];
// const separatorBitmapSize = separator.getBitmapSize();
// if (ctx !== null) {
// separator.drawBitmap(ctx, totalWidth, totalHeight);
// }
// totalHeight += separatorBitmapSize.height;
// }
}
const firstPaneBitmapWidth = firstPane.getBitmapSize().width;
totalWidth += firstPaneBitmapWidth;

// draw right price scale if exists
if (this._isRightAxisVisible()) {
drawPriceAxises('right', totalWidth);
const rightAxisBitmapWidth = ensureNotNull(firstPane.rightPriceAxisWidget()).getBitmapSize().width;
totalWidth += rightAxisBitmapWidth;
}

const drawStub = (position: 'left' | 'right', targetX: number, targetY: number) => {
const stub = ensureNotNull(position === 'left' ? this._timeAxisWidget.leftStub() : this._timeAxisWidget.rightStub());
stub.drawBitmap(ensureNotNull(ctx), targetX, targetY);
};

// draw time scale and stubs
if (this._options.timeScale.visible) {
const timeAxisBitmapSize = this._timeAxisWidget.getBitmapSize();

if (ctx !== null) {
let targetX = 0;
if (this._isLeftAxisVisible()) {
drawStub('left', targetX, totalHeight);
targetX = ensureNotNull(firstPane.leftPriceAxisWidget()).getBitmapSize().width;
}

this._timeAxisWidget.drawBitmap(ctx, targetX, totalHeight);
targetX += timeAxisBitmapSize.width;

if (this._isRightAxisVisible()) {
drawStub('right', targetX, totalHeight);
}
}

totalHeight += timeAxisBitmapSize.height;
}

return size({
width: totalWidth,
height: totalHeight,
});
}

// eslint-disable-next-line complexity
private _adjustSizeImpl(): void {
let totalStretch = 0;
Expand Down Expand Up @@ -390,7 +424,7 @@ export class ChartWidget implements IDestroyable {

accumulatedHeight += paneHeight;

paneWidget.setSize(new Size(paneWidth, paneHeight));
paneWidget.setSize(size({ width: paneWidth, height: paneHeight }));
if (this._isLeftAxisVisible()) {
paneWidget.setPriceAxisSize(leftPriceAxisWidth, 'left');
}
Expand All @@ -404,7 +438,7 @@ export class ChartWidget implements IDestroyable {
}

this._timeAxisWidget.setSizes(
new Size(timeAxisVisible ? paneWidth : 0, timeAxisHeight),
size({ width: timeAxisVisible ? paneWidth : 0, height: timeAxisHeight }),
timeAxisVisible ? leftPriceAxisWidth : 0,
timeAxisVisible ? rightPriceAxisWidth : 0
);
Expand Down
Loading

0 comments on commit 7a605d8

Please sign in to comment.