-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4085974
commit 4e421f3
Showing
15 changed files
with
485 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
export interface ImageWatermarkOptions { | ||
/** | ||
* Maximum width for the image watermark. | ||
* | ||
* @defaultValue undefined | ||
*/ | ||
maxWidth?: number; | ||
/** | ||
* Maximum height for the image watermark. | ||
* | ||
* @defaultValue undefined | ||
*/ | ||
maxHeight?: number; | ||
/** | ||
* Padding to maintain around the image watermark relative | ||
* to the chart pane edges. | ||
* | ||
* @defaultValue 0 | ||
*/ | ||
padding: number; | ||
/** | ||
* The alpha (opacity) for the image watermark. Where `1` is fully | ||
* opaque (visible) and `0` is fully transparent. | ||
* | ||
* @defaultValue 1 | ||
*/ | ||
alpha: number; | ||
} | ||
|
||
export const imageWatermarkOptionsDefaults: ImageWatermarkOptions = { | ||
alpha: 1, | ||
padding: 0, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { | ||
CanvasRenderingTarget2D, | ||
MediaCoordinatesRenderingScope, | ||
} from 'fancy-canvas'; | ||
|
||
import { IPrimitivePaneRenderer } from '../../model/ipane-primitive'; | ||
|
||
import { ImageWatermarkOptions } from './options'; | ||
|
||
export interface Placement { | ||
x: number; | ||
y: number; | ||
height: number; | ||
width: number; | ||
} | ||
|
||
export interface ImageWatermarkRendererOptions extends ImageWatermarkOptions { | ||
placement: Placement | null; | ||
imgElement: HTMLImageElement | null; | ||
} | ||
|
||
export class ImageWatermarkRenderer implements IPrimitivePaneRenderer { | ||
private _data: ImageWatermarkRendererOptions; | ||
|
||
public constructor(data: ImageWatermarkRendererOptions) { | ||
this._data = data; | ||
} | ||
|
||
public draw(target: CanvasRenderingTarget2D): void { | ||
target.useMediaCoordinateSpace((scope: MediaCoordinatesRenderingScope) => { | ||
const ctx = scope.context; | ||
const pos = this._data.placement; | ||
if (!pos) { | ||
return; | ||
} | ||
if (!this._data.imgElement) { | ||
throw new Error(`Image element missing.`); | ||
} | ||
ctx.globalAlpha = this._data.alpha ?? 1; | ||
ctx.drawImage(this._data.imgElement, pos.x, pos.y, pos.width, pos.height); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { IChartApiBase } from '../../api/ichart-api'; | ||
|
||
import { | ||
IPrimitivePaneRenderer, | ||
IPrimitivePaneView, | ||
PrimitivePaneViewZOrder, | ||
} from '../../model/ipane-primitive'; | ||
|
||
import { ImageWatermarkOptions } from './options'; | ||
import { | ||
ImageWatermarkRenderer, | ||
ImageWatermarkRendererOptions, | ||
Placement, | ||
} from './pane-renderer'; | ||
|
||
interface ImageWatermarkPaneViewState<T> { | ||
image: HTMLImageElement | null; | ||
imageWidth: number; | ||
imageHeight: number; | ||
chart: IChartApiBase<T> | null; | ||
} | ||
|
||
export class ImageWatermarkPaneView<T> implements IPrimitivePaneView { | ||
private _options: ImageWatermarkOptions; | ||
private _rendererOptions: ImageWatermarkRendererOptions; | ||
private _image: HTMLImageElement | null = null; | ||
private _imageWidth: number = 0; // don't draw until loaded | ||
private _imageHeight: number = 0; | ||
private _chart: IChartApiBase<T> | null = null; | ||
private _placement: Placement | null = null; | ||
|
||
public constructor(options: ImageWatermarkOptions) { | ||
this._options = options; | ||
this._rendererOptions = buildRendererOptions( | ||
this._options, | ||
this._placement, | ||
this._image | ||
); | ||
} | ||
|
||
public stateUpdate(state: ImageWatermarkPaneViewState<T>): void { | ||
if (state.chart !== undefined) { | ||
this._chart = state.chart; | ||
} | ||
if (state.imageWidth !== undefined) { | ||
this._imageWidth = state.imageWidth; | ||
} | ||
if (state.imageHeight !== undefined) { | ||
this._imageHeight = state.imageHeight; | ||
} | ||
if (state.image !== undefined) { | ||
this._image = state.image; | ||
} | ||
this.update(); | ||
} | ||
|
||
public optionsUpdate(options: ImageWatermarkOptions): void { | ||
this._options = options; | ||
this.update(); | ||
} | ||
|
||
public zOrder(): PrimitivePaneViewZOrder { | ||
return 'bottom' satisfies PrimitivePaneViewZOrder; | ||
} | ||
|
||
public update(): void { | ||
this._placement = this._determinePlacement(); | ||
this._rendererOptions = buildRendererOptions( | ||
this._options, | ||
this._placement, | ||
this._image | ||
); | ||
} | ||
|
||
public renderer(): IPrimitivePaneRenderer { | ||
return new ImageWatermarkRenderer(this._rendererOptions); | ||
} | ||
|
||
private _determinePlacement(): Placement | null { | ||
if (!this._chart || !this._imageWidth || !this._imageHeight) { | ||
return null; | ||
} | ||
const leftPriceScaleWidth = this._chart.priceScale('left').width(); | ||
const plotAreaWidth = this._chart.timeScale().width(); | ||
const startX = leftPriceScaleWidth; | ||
const plotAreaHeight = | ||
this._chart.chartElement().clientHeight - | ||
this._chart.timeScale().height(); | ||
|
||
const plotCentreX = Math.round(plotAreaWidth / 2) + startX; | ||
const plotCentreY = Math.round(plotAreaHeight / 2) + 0; | ||
|
||
const padding = this._options.padding ?? 0; | ||
let availableWidth = plotAreaWidth - 2 * padding; | ||
let availableHeight = plotAreaHeight - 2 * padding; | ||
|
||
if (this._options.maxHeight) { | ||
availableHeight = Math.min(availableHeight, this._options.maxHeight); | ||
} | ||
if (this._options.maxWidth) { | ||
availableWidth = Math.min(availableWidth, this._options.maxWidth); | ||
} | ||
|
||
const scaleX = availableWidth / this._imageWidth; | ||
const scaleY = availableHeight / this._imageHeight; | ||
const scaleToUse = Math.min(scaleX, scaleY); | ||
|
||
const drawWidth = this._imageWidth * scaleToUse; | ||
const drawHeight = this._imageHeight * scaleToUse; | ||
|
||
const x = plotCentreX - 0.5 * drawWidth; | ||
const y = plotCentreY - 0.5 * drawHeight; | ||
|
||
return { | ||
x, | ||
y, | ||
height: drawHeight, | ||
width: drawWidth, | ||
}; | ||
} | ||
} | ||
|
||
function buildRendererOptions( | ||
options: ImageWatermarkOptions, | ||
placement: Placement | null, | ||
imgElement: HTMLImageElement | null | ||
): ImageWatermarkRendererOptions { | ||
return { | ||
...options, | ||
placement, | ||
imgElement, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { | ||
IPanePrimitive, | ||
PaneAttachedParameter, | ||
} from '../../api/ipane-primitive-api'; | ||
|
||
import { DeepPartial } from '../../helpers/strict-type-checks'; | ||
|
||
import { Time } from '../../model/horz-scale-behavior-time/types'; | ||
import { IPanePrimitivePaneView } from '../../model/ipane-primitive'; | ||
|
||
import { | ||
ImageWatermarkOptions, | ||
imageWatermarkOptionsDefaults, | ||
} from './options'; | ||
import { ImageWatermarkPaneView } from './pane-view'; | ||
|
||
function mergeOptionsWithDefaults( | ||
options: DeepPartial<ImageWatermarkOptions> | ||
): ImageWatermarkOptions { | ||
return { | ||
...imageWatermarkOptionsDefaults, | ||
...options, | ||
}; | ||
} | ||
|
||
/** | ||
* A pane primitive for rendering a image watermark. | ||
* | ||
* @example | ||
* ```js | ||
* import { ImageWatermark } from 'lightweight-charts'; | ||
* | ||
* const imageWatermark = new ImageWatermark('/images/my-image.png', { | ||
* alpha: 0.5, | ||
* padding: 20, | ||
* }); | ||
* | ||
* const firstPane = chart.panes()[0]; | ||
* firstPane.attachPrimitive(imageWatermark); | ||
* ``` | ||
*/ | ||
export class ImageWatermark<T = Time> implements IPanePrimitive<T> { | ||
private _requestUpdate?: () => void; | ||
private _paneViews: ImageWatermarkPaneView<T>[]; | ||
private _options: ImageWatermarkOptions; | ||
private _imgElement: HTMLImageElement | null = null; | ||
private _imageUrl: string; | ||
|
||
public constructor( | ||
imageUrl: string, | ||
options: DeepPartial<ImageWatermarkOptions> | ||
) { | ||
this._imageUrl = imageUrl; | ||
this._options = mergeOptionsWithDefaults(options); | ||
this._paneViews = [new ImageWatermarkPaneView(this._options)]; | ||
} | ||
|
||
public updateAllViews(): void { | ||
this._paneViews.forEach((pw: ImageWatermarkPaneView<T>) => pw.update()); | ||
} | ||
|
||
public paneViews(): readonly IPanePrimitivePaneView[] { | ||
return this._paneViews; | ||
} | ||
|
||
public attached(attachedParams: PaneAttachedParameter<T>): void { | ||
const { requestUpdate, chart } = attachedParams; | ||
this._requestUpdate = requestUpdate; | ||
this._imgElement = new Image(); | ||
this._imgElement.onload = () => { | ||
const imageHeight = this._imgElement?.naturalHeight ?? 1; | ||
const imageWidth = this._imgElement?.naturalWidth ?? 1; | ||
this._paneViews.forEach((pv: ImageWatermarkPaneView<T>) => | ||
pv.stateUpdate({ | ||
imageHeight, | ||
imageWidth, | ||
image: this._imgElement, | ||
chart, | ||
}) | ||
); | ||
if (this._requestUpdate) { | ||
this._requestUpdate(); | ||
} | ||
}; | ||
this._imgElement.src = this._imageUrl; | ||
} | ||
|
||
public detached(): void { | ||
this._requestUpdate = undefined; | ||
this._imgElement = null; | ||
} | ||
|
||
public applyOptions(options: DeepPartial<ImageWatermarkOptions>): void { | ||
this._options = mergeOptionsWithDefaults({ ...this._options, ...options }); | ||
this._updateOptions(); | ||
if (this.requestUpdate) { | ||
this.requestUpdate(); | ||
} | ||
} | ||
|
||
public requestUpdate(): void { | ||
if (this._requestUpdate) { | ||
this._requestUpdate(); | ||
} | ||
} | ||
|
||
private _updateOptions(): void { | ||
this._paneViews.forEach((pw: ImageWatermarkPaneView<T>) => | ||
pw.optionsUpdate(this._options) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.