Skip to content

Commit

Permalink
add image watermark
Browse files Browse the repository at this point in the history
  • Loading branch information
SlicedSilver committed Jul 30, 2024
1 parent 4085974 commit 4e421f3
Show file tree
Hide file tree
Showing 15 changed files with 485 additions and 58 deletions.
8 changes: 8 additions & 0 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,12 @@ export default [
limit: '2.00 KB',
brotli: true,
},
{
name: 'Plugin: Image Watermark',
path: 'dist/lightweight-charts.production.mjs',
import: '{ ImageWatermark }',
ignore: ['fancy-canvas'],
limit: '2.00 KB',
brotli: true,
},
];
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { createChart, createChartEx, defaultHorzScaleBehavior } from './api/crea
Plugins
*/
export { TextWatermark } from './plugins/text-watermark/primitive';
export { ImageWatermark } from './plugins/image-watermark/primitive';

/**
* Returns the current version as a string. For example `'3.3.0'`.
Expand Down
33 changes: 33 additions & 0 deletions src/plugins/image-watermark/options.ts
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,
};
43 changes: 43 additions & 0 deletions src/plugins/image-watermark/pane-renderer.ts
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);
});
}
}
133 changes: 133 additions & 0 deletions src/plugins/image-watermark/pane-view.ts
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,
};
}
112 changes: 112 additions & 0 deletions src/plugins/image-watermark/primitive.ts
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)
);
}
}
5 changes: 3 additions & 2 deletions src/plugins/text-watermark/primitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {

import { DeepPartial } from '../../helpers/strict-type-checks';

import { Time } from '../../model/horz-scale-behavior-time/types';
import { IPanePrimitivePaneView } from '../../model/ipane-primitive';

import {
Expand Down Expand Up @@ -61,7 +62,7 @@ function mergeOptionsWithDefaults(
* chart.panes()[0].attachPrimitive(textWatermark);
* ```
*/
export class TextWatermark implements IPanePrimitive<unknown> {
export class TextWatermark<T = Time> implements IPanePrimitive<T> {
public requestUpdate?: () => void;
private _paneViews: TextWatermarkPaneView[];
private _options: TextWatermarkOptions;
Expand All @@ -81,7 +82,7 @@ export class TextWatermark implements IPanePrimitive<unknown> {
return this._paneViews;
}

public attached({ requestUpdate }: PaneAttachedParameter<unknown>): void {
public attached({ requestUpdate }: PaneAttachedParameter<T>): void {
this.requestUpdate = requestUpdate;
}

Expand Down
8 changes: 0 additions & 8 deletions tests/e2e/graphics/test-cases/initial-options/watermark.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ function generateData() {

function runTestCase(container) {
const chart = (window.chart = LightweightCharts.createChart(container, {
watermark: {
visible: true,
color: 'red',
text: 'TradingView Watermark Example',
fontSize: 24,
fontFamily: 'Roboto',
fontStyle: 'italic',
},
layout: { attributionLogo: false },
}));

Expand Down
Loading

0 comments on commit 4e421f3

Please sign in to comment.