diff --git a/.size-limit.js b/.size-limit.js index 7e4e3f3c4a..328ae38200 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -30,7 +30,7 @@ export default [ brotli: true, }, { - name: 'Standalone-ESM', + name: 'ESM Standalone', path: 'dist/lightweight-charts.standalone.production.mjs', limit: '50.00 KB', import: '*', @@ -42,4 +42,20 @@ export default [ limit: '50.00 KB', brotli: true, }, + { + name: 'Plugin: Text Watermark', + path: 'dist/lightweight-charts.production.mjs', + import: '{ TextWatermark }', + ignore: ['fancy-canvas'], + 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, + }, ]; diff --git a/src/api/options/chart-options-defaults.ts b/src/api/options/chart-options-defaults.ts index 10c003e58f..3ef44f5464 100644 --- a/src/api/options/chart-options-defaults.ts +++ b/src/api/options/chart-options-defaults.ts @@ -7,7 +7,6 @@ import { gridOptionsDefaults } from './grid-options-defaults'; import { layoutOptionsDefaults } from './layout-options-defaults'; import { priceScaleOptionsDefaults } from './price-scale-options-defaults'; import { timeScaleOptionsDefaults } from './time-scale-options-defaults'; -import { watermarkOptionsDefaults } from './watermark-options-defaults'; export function chartOptionsDefaults(): ChartOptionsInternal { return { @@ -29,7 +28,6 @@ export function chartOptionsDefaults(): ChartOptionsInternal implements IDestroyable, IChartModelBase private readonly _panes: Pane[] = []; private readonly _crosshair: Crosshair; private readonly _magnet: Magnet; - private readonly _watermark: Watermark; private _serieses: Series[] = []; @@ -486,7 +473,6 @@ export class ChartModel implements IDestroyable, IChartModelBase this._timeScale = new TimeScale(this, options.timeScale, this._options.localization, horzScaleBehavior); this._crosshair = new Crosshair(this, options.crosshair); this._magnet = new Magnet(options.crosshair); - this._watermark = new Watermark(this, options.watermark); this._getOrCreatePane(0); this._panes[0].setStretchFactor(DEFAULT_STRETCH_FACTOR * 2); @@ -604,10 +590,6 @@ export class ChartModel implements IDestroyable, IChartModelBase return this._panes; } - public watermarkSource(): Watermark { - return this._watermark; - } - public crosshairSource(): Crosshair { return this._crosshair; } @@ -887,7 +869,6 @@ export class ChartModel implements IDestroyable, IChartModelBase } public recalculateAllPanes(): void { - this._watermark.updateAllViews(); this._panes.forEach((p: Pane) => p.recalculate()); this.updateCrosshair(); } diff --git a/src/model/watermark.ts b/src/model/watermark.ts deleted file mode 100644 index 0cb88ca98e..0000000000 --- a/src/model/watermark.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { HorzAlign, VertAlign } from '../renderers/watermark-renderer'; -import { IPaneView } from '../views/pane/ipane-view'; -import { WatermarkPaneView } from '../views/pane/watermark-pane-view'; -import { IPriceAxisView } from '../views/price-axis/iprice-axis-view'; - -import { IChartModelBase } from './chart-model'; -import { DataSource } from './data-source'; - -/** Watermark options. */ -export interface WatermarkOptions { - /** - * Watermark color. - * - * @defaultValue `'rgba(0, 0, 0, 0)'` - */ - color: string; - - /** - * Display the watermark. - * - * @defaultValue `false` - */ - visible: boolean; - - /** - * Text of the watermark. Word wrapping is not supported. - * - * @defaultValue `''` - */ - text: string; - - /** - * Font size in pixels. - * - * @defaultValue `48` - */ - fontSize: number; - - /** - * Font family. - * - * @defaultValue `-apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif` - */ - fontFamily: string; - - /** - * Font style. - * - * @defaultValue `''` - */ - fontStyle: string; - - /** - * Horizontal alignment inside the chart area. - * - * @defaultValue `'center'` - */ - horzAlign: HorzAlign; - - /** - * Vertical alignment inside the chart area. - * - * @defaultValue `'center'` - */ - vertAlign: VertAlign; -} - -export class Watermark extends DataSource { - private readonly _paneView: WatermarkPaneView; - private readonly _options: WatermarkOptions; - - public constructor(model: IChartModelBase, options: WatermarkOptions) { - super(); - this._options = options; - this._paneView = new WatermarkPaneView(this); - } - - public override priceAxisViews(): readonly IPriceAxisView[] { - return []; - } - - public paneViews(): readonly IPaneView[] { - return [this._paneView]; - } - - public options(): Readonly { - return this._options; - } - - public updateAllViews(): void { - this._paneView.update(); - } -} diff --git a/src/plugins/image-watermark/options.ts b/src/plugins/image-watermark/options.ts new file mode 100644 index 0000000000..a2417bf222 --- /dev/null +++ b/src/plugins/image-watermark/options.ts @@ -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, +}; diff --git a/src/plugins/image-watermark/pane-renderer.ts b/src/plugins/image-watermark/pane-renderer.ts new file mode 100644 index 0000000000..e676e4fe6b --- /dev/null +++ b/src/plugins/image-watermark/pane-renderer.ts @@ -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); + }); + } +} diff --git a/src/plugins/image-watermark/pane-view.ts b/src/plugins/image-watermark/pane-view.ts new file mode 100644 index 0000000000..2d31b7db4c --- /dev/null +++ b/src/plugins/image-watermark/pane-view.ts @@ -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 { + image: HTMLImageElement | null; + imageWidth: number; + imageHeight: number; + chart: IChartApiBase | null; +} + +export class ImageWatermarkPaneView 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 | 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): 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, + }; +} diff --git a/src/plugins/image-watermark/primitive.ts b/src/plugins/image-watermark/primitive.ts new file mode 100644 index 0000000000..5cbdcef134 --- /dev/null +++ b/src/plugins/image-watermark/primitive.ts @@ -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 { + 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 implements IPanePrimitive { + private _requestUpdate?: () => void; + private _paneViews: ImageWatermarkPaneView[]; + private _options: ImageWatermarkOptions; + private _imgElement: HTMLImageElement | null = null; + private _imageUrl: string; + + public constructor( + imageUrl: string, + options: DeepPartial + ) { + this._imageUrl = imageUrl; + this._options = mergeOptionsWithDefaults(options); + this._paneViews = [new ImageWatermarkPaneView(this._options)]; + } + + public updateAllViews(): void { + this._paneViews.forEach((pw: ImageWatermarkPaneView) => pw.update()); + } + + public paneViews(): readonly IPanePrimitivePaneView[] { + return this._paneViews; + } + + public attached(attachedParams: PaneAttachedParameter): 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) => + 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): 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) => + pw.optionsUpdate(this._options) + ); + } +} diff --git a/src/plugins/text-watermark/options.ts b/src/plugins/text-watermark/options.ts new file mode 100644 index 0000000000..d35cc26d63 --- /dev/null +++ b/src/plugins/text-watermark/options.ts @@ -0,0 +1,93 @@ +import { defaultFontFamily } from '../../helpers/make-font'; + +import { HorzAlign, VertAlign } from '../types'; + +export interface TextWatermarkOptions { + /** + * Display the watermark. + * + * @defaultValue `true` + */ + visible: boolean; + + /** + * Horizontal alignment inside the chart area. + * + * @defaultValue `'center'` + */ + horzAlign: HorzAlign; + + /** + * Vertical alignment inside the chart area. + * + * @defaultValue `'center'` + */ + vertAlign: VertAlign; + + /** + * Text to be displayed within the watermark. Each item + * in the array is treated as new line. + * + * @defaultValue `[]` + */ + lines: TextWatermarkLineOptions[]; +} + +export interface TextWatermarkLineOptions { + /** + * Watermark color. + * + * @defaultValue `'rgba(0, 0, 0, 0.5)'` + */ + + color: string; + /** + * Text of the watermark. Word wrapping is not supported. + * + * @defaultValue `''` + */ + text: string; + + /** + * Font size in pixels. + * + * @defaultValue `48` + */ + fontSize: number; + + /** + * Line height in pixels. + * + * @defaultValue `1.2 * fontSize` + */ + lineHeight?: number; + + /** + * Font family. + * + * @defaultValue `-apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif` + */ + fontFamily: string; + + /** + * Font style. + * + * @defaultValue `''` + */ + fontStyle: string; +} + +export const textWatermarkOptionsDefaults: TextWatermarkOptions = { + visible: true, + horzAlign: 'center', + vertAlign: 'center', + lines: [], +}; + +export const textWatermarkLineOptionsDefaults: TextWatermarkLineOptions = { + color: 'rgba(0, 0, 0, 0.5)', + fontSize: 48, + fontFamily: defaultFontFamily, + fontStyle: '', + text: '', +}; diff --git a/src/plugins/text-watermark/pane-renderer.ts b/src/plugins/text-watermark/pane-renderer.ts new file mode 100644 index 0000000000..186089f5a7 --- /dev/null +++ b/src/plugins/text-watermark/pane-renderer.ts @@ -0,0 +1,118 @@ +import { + CanvasRenderingTarget2D, + MediaCoordinatesRenderingScope, +} from 'fancy-canvas'; + +import { IPrimitivePaneRenderer } from '../../model/ipane-primitive'; + +import { TextWatermarkLineOptions, TextWatermarkOptions } from './options'; + +export interface TextWatermarkLineRendererOptions + extends TextWatermarkLineOptions { + zoom: number; + vertOffset: number; + font: string; + lineHeight: number; +} + +export interface TextWatermarkRendererOptions extends TextWatermarkOptions { + lines: TextWatermarkLineRendererOptions[]; +} + +export class TextWatermarkRenderer implements IPrimitivePaneRenderer { + private _data: TextWatermarkRendererOptions; + private _metricsCache: Map> = new Map(); + + public constructor(options: TextWatermarkRendererOptions) { + this._data = options; + } + + public draw(target: CanvasRenderingTarget2D): void { + target.useMediaCoordinateSpace((scope: MediaCoordinatesRenderingScope) => { + const { context: ctx, mediaSize } = scope; + + let textHeight = 0; + for (const line of this._data.lines) { + if (line.text.length === 0) { + continue; + } + + ctx.font = line.font; + const textWidth = this._metrics(ctx, line.text); + if (textWidth > mediaSize.width) { + line.zoom = mediaSize.width / textWidth; + } else { + line.zoom = 1; + } + + textHeight += line.lineHeight * line.zoom; + } + + let vertOffset = 0; + switch (this._data.vertAlign) { + case 'top': + vertOffset = 0; + break; + + case 'center': + vertOffset = Math.max((mediaSize.height - textHeight) / 2, 0); + break; + + case 'bottom': + vertOffset = Math.max(mediaSize.height - textHeight, 0); + break; + } + + for (const line of this._data.lines) { + ctx.save(); + ctx.fillStyle = line.color; + let horzOffset = 0; + switch (this._data.horzAlign) { + case 'left': + ctx.textAlign = 'left'; + horzOffset = line.lineHeight / 2; + break; + + case 'center': + ctx.textAlign = 'center'; + horzOffset = mediaSize.width / 2; + break; + + case 'right': + ctx.textAlign = 'right'; + horzOffset = mediaSize.width - 1 - line.lineHeight / 2; + break; + } + + ctx.translate(horzOffset, vertOffset); + ctx.textBaseline = 'top'; + ctx.font = line.font; + ctx.scale(line.zoom, line.zoom); + ctx.fillText(line.text, 0, line.vertOffset); + ctx.restore(); + vertOffset += line.lineHeight * line.zoom; + } + }); + } + + private _metrics(ctx: CanvasRenderingContext2D, text: string): number { + const fontCache = this._fontCache(ctx.font); + let result = fontCache.get(text); + if (result === undefined) { + result = ctx.measureText(text).width; + fontCache.set(text, result); + } + + return result; + } + + private _fontCache(font: string): Map { + let fontCache = this._metricsCache.get(font); + if (fontCache === undefined) { + fontCache = new Map(); + this._metricsCache.set(font, fontCache); + } + + return fontCache; + } +} diff --git a/src/plugins/text-watermark/pane-view.ts b/src/plugins/text-watermark/pane-view.ts new file mode 100644 index 0000000000..46ec02e96d --- /dev/null +++ b/src/plugins/text-watermark/pane-view.ts @@ -0,0 +1,54 @@ +import { makeFont } from '../../helpers/make-font'; + +import { + IPrimitivePaneRenderer, + IPrimitivePaneView, +} from '../../model/ipane-primitive'; + +import { TextWatermarkLineOptions, TextWatermarkOptions } from './options'; +import { + TextWatermarkLineRendererOptions, + TextWatermarkRenderer, + TextWatermarkRendererOptions, +} from './pane-renderer'; + +export class TextWatermarkPaneView implements IPrimitivePaneView { + private _options: TextWatermarkRendererOptions; + + public constructor(options: TextWatermarkOptions) { + this._options = buildRendererOptions(options); + } + + public update(options: TextWatermarkOptions): void { + this._options = buildRendererOptions(options); + } + + public renderer(): IPrimitivePaneRenderer { + return new TextWatermarkRenderer(this._options); + } +} + +function buildRendererLineOptions( + lineOption: TextWatermarkLineOptions +): TextWatermarkLineRendererOptions { + return { + ...lineOption, + font: makeFont( + lineOption.fontSize, + lineOption.fontFamily, + lineOption.fontStyle + ), + lineHeight: lineOption.lineHeight || lineOption.fontSize * 1.2, + vertOffset: 0, + zoom: 0, + }; +} + +function buildRendererOptions( + options: TextWatermarkOptions +): TextWatermarkRendererOptions { + return { + ...options, + lines: options.lines.map(buildRendererLineOptions), + }; +} diff --git a/src/plugins/text-watermark/primitive.ts b/src/plugins/text-watermark/primitive.ts new file mode 100644 index 0000000000..3892b5107f --- /dev/null +++ b/src/plugins/text-watermark/primitive.ts @@ -0,0 +1,99 @@ +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 { + TextWatermarkLineOptions, + textWatermarkLineOptionsDefaults, + TextWatermarkOptions, + textWatermarkOptionsDefaults, +} from './options'; +import { TextWatermarkPaneView } from './pane-view'; + +function mergeLineOptionsWithDefaults( + options: Partial +): TextWatermarkLineOptions { + return { + ...textWatermarkLineOptionsDefaults, + ...options, + }; +} + +function mergeOptionsWithDefaults( + options: DeepPartial +): TextWatermarkOptions { + return { + ...textWatermarkOptionsDefaults, + ...options, + lines: options.lines?.map(mergeLineOptionsWithDefaults) ?? [], + }; +} + +/** + * A pane primitive for rendering a text watermark. + * + * @example + * ```js + * const textWatermark = new TextWatermark({ + * horzAlign: 'center', + * vertAlign: 'center', + * lines: [ + * { + * text: 'Hello', + * color: 'rgba(255,0,0,0.5)', + * fontSize: 100, + * fontStyle: 'bold', + * }, + * { + * text: 'This is a text watermark', + * color: 'rgba(0,0,255,0.5)', + * fontSize: 50, + * fontStyle: 'italic', + * fontFamily: 'monospace', + * }, + * ], + * }); + * chart.panes()[0].attachPrimitive(textWatermark); + * ``` + */ +export class TextWatermark implements IPanePrimitive { + public requestUpdate?: () => void; + private _paneViews: TextWatermarkPaneView[]; + private _options: TextWatermarkOptions; + + public constructor(options: DeepPartial) { + this._options = mergeOptionsWithDefaults(options); + this._paneViews = [new TextWatermarkPaneView(this._options)]; + } + + public updateAllViews(): void { + this._paneViews.forEach((pw: TextWatermarkPaneView) => + pw.update(this._options) + ); + } + + public paneViews(): readonly IPanePrimitivePaneView[] { + return this._paneViews; + } + + public attached({ requestUpdate }: PaneAttachedParameter): void { + this.requestUpdate = requestUpdate; + } + + public detached(): void { + this.requestUpdate = undefined; + } + + public applyOptions(options: DeepPartial): void { + this._options = mergeOptionsWithDefaults({ ...this._options, ...options }); + if (this.requestUpdate) { + this.requestUpdate(); + } + } +} diff --git a/src/plugins/types.ts b/src/plugins/types.ts new file mode 100644 index 0000000000..b6f88ff426 --- /dev/null +++ b/src/plugins/types.ts @@ -0,0 +1,8 @@ +/** + * Represents a horizontal alignment. + */ +export type HorzAlign = 'left' | 'center' | 'right'; +/** + * Represents a vertical alignment. + */ +export type VertAlign = 'top' | 'center' | 'bottom'; diff --git a/src/renderers/watermark-renderer.ts b/src/renderers/watermark-renderer.ts deleted file mode 100644 index 3e43a35259..0000000000 --- a/src/renderers/watermark-renderer.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { MediaCoordinatesRenderingScope } from 'fancy-canvas'; - -import { MediaCoordinatesPaneRenderer } from './media-coordinates-pane-renderer'; - -export interface WatermarkRendererLineData { - text: string; - font: string; - lineHeight: number; - vertOffset: number; - zoom: number; -} - -/** - * Represents a horizontal alignment. - */ -export type HorzAlign = 'left' | 'center' | 'right'; -/** - * Represents a vertical alignment. - */ -export type VertAlign = 'top' | 'center' | 'bottom'; - -export interface WatermarkRendererData { - lines: WatermarkRendererLineData[]; - color: string; - visible: boolean; - horzAlign: HorzAlign; - vertAlign: VertAlign; -} - -export class WatermarkRenderer extends MediaCoordinatesPaneRenderer { - private readonly _data: WatermarkRendererData; - private _metricsCache: Map> = new Map(); - - public constructor(data: WatermarkRendererData) { - super(); - this._data = data; - } - - protected _drawImpl(renderingScope: MediaCoordinatesRenderingScope): void {} - - protected override _drawBackgroundImpl(renderingScope: MediaCoordinatesRenderingScope): void { - if (!this._data.visible) { - return; - } - - const { context: ctx, mediaSize } = renderingScope; - - let textHeight = 0; - for (const line of this._data.lines) { - if (line.text.length === 0) { - continue; - } - - ctx.font = line.font; - const textWidth = this._metrics(ctx, line.text); - if (textWidth > mediaSize.width) { - line.zoom = mediaSize.width / textWidth; - } else { - line.zoom = 1; - } - - textHeight += line.lineHeight * line.zoom; - } - - let vertOffset = 0; - switch (this._data.vertAlign) { - case 'top': - vertOffset = 0; - break; - - case 'center': - vertOffset = Math.max((mediaSize.height - textHeight) / 2, 0); - break; - - case 'bottom': - vertOffset = Math.max((mediaSize.height - textHeight), 0); - break; - } - - ctx.fillStyle = this._data.color; - - for (const line of this._data.lines) { - ctx.save(); - - let horzOffset = 0; - switch (this._data.horzAlign) { - case 'left': - ctx.textAlign = 'left'; - horzOffset = line.lineHeight / 2; - break; - - case 'center': - ctx.textAlign = 'center'; - horzOffset = mediaSize.width / 2; - break; - - case 'right': - ctx.textAlign = 'right'; - horzOffset = mediaSize.width - 1 - line.lineHeight / 2; - break; - } - - ctx.translate(horzOffset, vertOffset); - ctx.textBaseline = 'top'; - ctx.font = line.font; - ctx.scale(line.zoom, line.zoom); - ctx.fillText(line.text, 0, line.vertOffset); - ctx.restore(); - vertOffset += line.lineHeight * line.zoom; - } - } - - private _metrics(ctx: CanvasRenderingContext2D, text: string): number { - const fontCache = this._fontCache(ctx.font); - let result = fontCache.get(text); - if (result === undefined) { - result = ctx.measureText(text).width; - fontCache.set(text, result); - } - - return result; - } - - private _fontCache(font: string): Map { - let fontCache = this._metricsCache.get(font); - if (fontCache === undefined) { - fontCache = new Map(); - this._metricsCache.set(font, fontCache); - } - - return fontCache; - } -} diff --git a/src/tsconfig.composite.json b/src/tsconfig.composite.json index 8ca4c042d1..4906c1a091 100644 --- a/src/tsconfig.composite.json +++ b/src/tsconfig.composite.json @@ -8,6 +8,7 @@ { "path": "./tsconfig.model.json" } ], "include": [ + "./plugins/**/*.ts", "./index.ts", "./standalone.ts" ] diff --git a/src/views/pane/watermark-pane-view.ts b/src/views/pane/watermark-pane-view.ts deleted file mode 100644 index d3d94d1ddd..0000000000 --- a/src/views/pane/watermark-pane-view.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { makeFont } from '../../helpers/make-font'; - -import { Watermark } from '../../model/watermark'; -import { IPaneRenderer } from '../../renderers/ipane-renderer'; -import { WatermarkRenderer, WatermarkRendererData } from '../../renderers/watermark-renderer'; - -import { IUpdatablePaneView } from './iupdatable-pane-view'; - -export class WatermarkPaneView implements IUpdatablePaneView { - private _source: Watermark; - private _invalidated: boolean = true; - - private readonly _rendererData: WatermarkRendererData = { - visible: false, - color: '', - lines: [], - vertAlign: 'center', - horzAlign: 'center', - }; - private readonly _renderer: WatermarkRenderer = new WatermarkRenderer(this._rendererData); - - public constructor(source: Watermark) { - this._source = source; - } - - public update(): void { - this._invalidated = true; - } - - public renderer(): IPaneRenderer { - if (this._invalidated) { - this._updateImpl(); - this._invalidated = false; - } - - return this._renderer; - } - - private _updateImpl(): void { - const options = this._source.options(); - const data = this._rendererData; - data.visible = options.visible; - - if (!data.visible) { - return; - } - - data.color = options.color; - data.horzAlign = options.horzAlign; - data.vertAlign = options.vertAlign; - - data.lines = [ - { - text: options.text, - font: makeFont(options.fontSize, options.fontFamily, options.fontStyle), - lineHeight: options.fontSize * 1.2, - vertOffset: 0, - zoom: 0, - }, - ]; - } -} diff --git a/tests/e2e/coverage/test-cases/chart/watermark.js b/tests/e2e/coverage/test-cases/chart/watermark.js deleted file mode 100644 index 17f5285bf0..0000000000 --- a/tests/e2e/coverage/test-cases/chart/watermark.js +++ /dev/null @@ -1,53 +0,0 @@ -function simpleData() { - return [ - { time: 1663740000, value: 10 }, - { time: 1663750000, value: 20 }, - { time: 1663760000, value: 30 }, - ]; -} - -function interactionsToPerform() { - return []; -} - -let chart; - -function beforeInteractions(container) { - chart = LightweightCharts.createChart(container, { - watermark: { - visible: true, - color: 'red', - text: 'Watermark', - fontSize: 24, - fontStyle: 'italic', - }, - }); - - const mainSeries = chart.addLineSeries(); - - mainSeries.setData(simpleData()); - - return Promise.resolve(); -} - -function afterInteractions() { - chart.applyOptions({ - watermark: { - fontFamily: 'Roboto', - horzAlign: 'left', - vertAlign: 'top', - }, - }); - - return new Promise(resolve => { - requestAnimationFrame(() => { - chart.applyOptions({ - watermark: { - horzAlign: 'right', - vertAlign: 'bottom', - }, - }); - }); - requestAnimationFrame(resolve); - }); -} diff --git a/tests/e2e/coverage/test-cases/plugins/text-watermark.js b/tests/e2e/coverage/test-cases/plugins/text-watermark.js new file mode 100644 index 0000000000..4c16bb78d5 --- /dev/null +++ b/tests/e2e/coverage/test-cases/plugins/text-watermark.js @@ -0,0 +1,64 @@ +function simpleData() { + return [ + { time: 1663740000, value: 10 }, + { time: 1663750000, value: 20 }, + { time: 1663760000, value: 30 }, + ]; +} + +function interactionsToPerform() { + return []; +} + +let chart; +let textWatermark; + +function beforeInteractions(container) { + chart = LightweightCharts.createChart(container); + + const mainSeries = chart.addLineSeries(); + + mainSeries.setData(simpleData()); + + textWatermark = new LightweightCharts.TextWatermark({ + horzAlign: 'center', + vertAlign: 'center', + lines: [ + { + text: 'Hello', + color: 'rgba(255,0,0,0.5)', + fontSize: 100, + fontStyle: 'bold', + }, + { + text: 'This is a text watermark', + color: 'rgba(0,0,255,0.5)', + fontSize: 50, + fontStyle: 'italic', + fontFamily: 'monospace', + }, + ], + }); + + const pane = chart.panes()[0]; + pane.attachPrimitive(textWatermark); + + return Promise.resolve(); +} + +function afterInteractions() { + textWatermark.applyOptions({ + horzAlign: 'left', + vertAlign: 'top', + }); + + return new Promise(resolve => { + requestAnimationFrame(() => { + textWatermark.applyOptions({ + horzAlign: 'right', + vertAlign: 'bottom', + }); + }); + requestAnimationFrame(resolve); + }); +} diff --git a/tests/e2e/graphics/test-cases/api/series-markers.js b/tests/e2e/graphics/test-cases/api/series-markers.js index b06dfe62ab..6880723234 100644 --- a/tests/e2e/graphics/test-cases/api/series-markers.js +++ b/tests/e2e/graphics/test-cases/api/series-markers.js @@ -40,13 +40,16 @@ function runTestCase(container) { series.setMarkers(markers); const seriesApiMarkers = series.markers(); - chart.applyOptions({ - watermark: { - color: 'red', - visible: true, - text: JSON.stringify(seriesApiMarkers[0]), - }, + const textWatermark = new LightweightCharts.TextWatermark({ + lines: [ + { + text: JSON.stringify(seriesApiMarkers[0]), + color: 'red', + }, + ], }); + const pane = chart.panes()[0]; + pane.attachPrimitive(textWatermark); console.assert(compare(markers, seriesApiMarkers), `series.markers() should return exactly the same that was provided to series.setMarkers()\n${JSON.stringify(seriesApiMarkers)}\n${JSON.stringify(markers)}`); } diff --git a/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js b/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js index 0592dd6e64..9e03dda8a5 100644 --- a/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js +++ b/tests/e2e/graphics/test-cases/api/subscribe-crosshair-move.js @@ -1,5 +1,7 @@ function runTestCase(container) { - const chart = window.chart = LightweightCharts.createChart(container, { layout: { attributionLogo: false } }); + const chart = (window.chart = LightweightCharts.createChart(container, { + layout: { attributionLogo: false }, + })); const series = chart.addAreaSeries(); series.setData([ { time: '1990-04-24', value: 0 }, @@ -12,15 +14,29 @@ function runTestCase(container) { chart.timeScale().fitContent(); + const textWatermark = new LightweightCharts.TextWatermark({ + lines: [ + { + text: '', + color: 'red', + fontSize: 12, + }, + ], + }); + const pane = chart.panes()[0]; + pane.attachPrimitive(textWatermark); + chart.subscribeCrosshairMove(param => { if (param.time) { const seriesData = param.seriesData.get(series) || {}; - chart.applyOptions({ - watermark: { - color: 'red', - visible: true, - text: `${param.time} - ${seriesData.time}`, - }, + textWatermark.applyOptions({ + lines: [ + { + text: `${param.time} - ${seriesData.time}`, + color: 'red', + fontSize: 12, + }, + ], }); } }); diff --git a/tests/e2e/graphics/test-cases/applying-options/watermark.js b/tests/e2e/graphics/test-cases/applying-options/watermark.js index 4f4dacb688..eddc3b25c5 100644 --- a/tests/e2e/graphics/test-cases/applying-options/watermark.js +++ b/tests/e2e/graphics/test-cases/applying-options/watermark.js @@ -22,26 +22,46 @@ function generateData() { return res; } +let textWatermark; + function runTestCase(container) { - const chart = window.chart = LightweightCharts.createChart(container, { layout: { attributionLogo: false } }); + const chart = (window.chart = LightweightCharts.createChart(container, { + layout: { attributionLogo: false }, + })); const mainSeries = chart.addCandlestickSeries(); mainSeries.setData(generateData()); + textWatermark = new LightweightCharts.TextWatermark({ + horzAlign: 'left', + vertAlign: 'bottom', + lines: [ + { + text: 'Watermark Before', + color: 'rgba(0, 0, 0, 0.5)', + fontSize: 12, + }, + ], + }); + const pane = chart.panes()[0]; + pane.attachPrimitive(textWatermark); + return new Promise(resolve => { setTimeout(() => { - chart.applyOptions({ - watermark: { - visible: true, - fontSize: 24, - horzAlign: 'center', - vertAlign: 'center', - color: 'rgba(171, 71, 188, 0.5)', - text: 'Watermark', - fontFamily: 'Roboto', - fontStyle: 'bold', - }, + textWatermark.applyOptions({ + visible: true, + horzAlign: 'center', + vertAlign: 'center', + lines: [ + { + text: 'Watermark', + color: 'rgba(171, 71, 188, 0.5)', + fontSize: 24, + fontFamily: 'Roboto', + fontStyle: 'bold', + }, + ], }); resolve(); }, 300); diff --git a/tests/e2e/graphics/test-cases/initial-options/watermark.js b/tests/e2e/graphics/test-cases/initial-options/watermark.js index 19c135d485..e48f1f7b7d 100644 --- a/tests/e2e/graphics/test-cases/initial-options/watermark.js +++ b/tests/e2e/graphics/test-cases/initial-options/watermark.js @@ -13,19 +13,26 @@ 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', - }, + const chart = (window.chart = LightweightCharts.createChart(container, { layout: { attributionLogo: false }, - }); + })); const mainSeries = chart.addAreaSeries(); - mainSeries.setData(generateData()); + + const textWatermark = new LightweightCharts.TextWatermark({ + visible: true, + lines: [ + { + color: 'red', + text: 'TradingView Watermark Example', + fontSize: 24, + fontFamily: 'Roboto', + fontStyle: 'italic', + }, + ], + }); + + const pane = chart.panes()[0]; + pane.attachPrimitive(textWatermark); } diff --git a/tests/e2e/graphics/test-cases/plugins/image-watermark.js b/tests/e2e/graphics/test-cases/plugins/image-watermark.js new file mode 100644 index 0000000000..0e2fd8a905 --- /dev/null +++ b/tests/e2e/graphics/test-cases/plugins/image-watermark.js @@ -0,0 +1,81 @@ +function generateData() { + const res = []; + const time = new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0)); + for (let i = 0; i < 500; ++i) { + res.push({ + time: time.getTime() / 1000, + value: i, + }); + + time.setUTCDate(time.getUTCDate() + 1); + } + return res; +} + +function svgToDataUrl(svgString) { + // Encode the SVG string + const encodedSvg = encodeURIComponent(svgString); + + // Create the data URL + const dataUrl = `data:image/svg+xml;charset=utf-8,${encodedSvg}`; + + return dataUrl; +} + +const svgString = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +const imageDataUrl = svgToDataUrl(svgString); + +function runTestCase(container) { + const chart = (window.chart = LightweightCharts.createChart(container, { + layout: { attributionLogo: false }, + })); + + const mainSeries = chart.addAreaSeries(); + mainSeries.setData(generateData()); + + const imageWatermark = new LightweightCharts.ImageWatermark(imageDataUrl, { + alpha: 0.5, + padding: 20, + }); + + const pane = chart.panes()[0]; + pane.attachPrimitive(imageWatermark); +} diff --git a/tests/type-checks/non-time-based-custom-series.ts b/tests/type-checks/non-time-based-custom-series.ts index 0453b9d212..a30145a361 100644 --- a/tests/type-checks/non-time-based-custom-series.ts +++ b/tests/type-checks/non-time-based-custom-series.ts @@ -1,4 +1,4 @@ -import { createChartEx, customSeriesDefaultOptions } from '../../src'; +import { createChartEx, customSeriesDefaultOptions, TextWatermark } from '../../src'; import { CandlestickData, WhitespaceData } from '../../src/model/data-consumer'; import { Time } from '../../src/model/horz-scale-behavior-time/types'; import { CustomData, CustomSeriesPricePlotValues, ICustomSeriesPaneRenderer, ICustomSeriesPaneView, PaneRendererCustomData } from '../../src/model/icustom-series'; @@ -106,3 +106,8 @@ if (dataSet) { // @ts-expect-error readonly array // eslint-disable-next-line @typescript-eslint/no-unsafe-call dataSet.push({ time: 12 }); + +const textWatermark = new TextWatermark({ + lines: [], +}); +chart.panes()[1].attachPrimitive(textWatermark); diff --git a/tests/type-checks/watermarks.ts b/tests/type-checks/watermarks.ts new file mode 100644 index 0000000000..2fe644860d --- /dev/null +++ b/tests/type-checks/watermarks.ts @@ -0,0 +1,28 @@ +import { createChart, ImageWatermark, TextWatermark } from '../../src'; + +const chart = createChart('anything'); + +const mainSeries = chart.addLineSeries(); +mainSeries.setData([]); + +const imageWatermark = new ImageWatermark('/debug/image.svg', { + alpha: 0.5, + padding: 50, + maxHeight: 400, + maxWidth: 400, +}); +chart.panes()[0].attachPrimitive(imageWatermark); + +const textWatermark = new TextWatermark({ + horzAlign: 'center', + vertAlign: 'center', + lines: [ + { + text: 'Hello', + color: 'rgba(255,0,0,0.5)', + fontSize: 100, + fontStyle: 'bold', + }, + ], +}); +chart.panes()[1].attachPrimitive(textWatermark); diff --git a/website/docs/migrations/from-v4-to-v5.md b/website/docs/migrations/from-v4-to-v5.md index 8b887a153b..573603f254 100644 --- a/website/docs/migrations/from-v4-to-v5.md +++ b/website/docs/migrations/from-v4-to-v5.md @@ -1,10 +1,80 @@ # From v4 to v5 -In this document you can find the migration guide from the previous version v4 to v5. +In this document you can find the migration guide from the previous version v4 +to v5. + +## Watermarks + +### Overview of Changes + +In the new version of Lightweight Charts, the watermark feature has undergone significant changes: + +1. **Extraction from Core**: The watermark functionality has been extracted from the core library. +2. **Re-implementation**: It's now re-implemented as a Pane Primitive (plugin) included within the library. +3. **Improved Tree-shaking**: This change makes the feature more tree-shakeable, potentially reducing bundle sizes for users who don't need watermarks. +4. **Added an Image Watermark Primitive**: In addition to the usual text based watermark, there is now +an image watermark feature provided by the [`ImageWatermark`](/api/classes/ImageWatermark.md) primitive. + +If you're currently using the watermark feature, you'll need to make a few adjustments to your code. + +### Accessing the New TextWatermark + +The TextWatermark primitive is now available as follows: + +- **ESM builds**: Import [`TextWatermark`](/api/classes/TextWatermark.md) directly. +- **Standalone script build**: Access via `LightweightCharts.TextWatermark`. + +### Changes in Options + +The options structure for watermarks has been revised: + +1. **Multiple Lines**: The plugin now supports multiple lines of text. +2. **Text Options**: Text-related options are now defined per line within the `lines` property of the options object. + +### Attaching the Watermark + +To use the Primitive, you need to attach it to a Pane: + +1. **Single Pane**: If you're using only one pane, you can easily fetch it using `chart.panes()[0]`. +2. **Multiple Panes**: For charts with multiple panes, you'll need to specify which pane to attach the watermark to. + +### Example: Implementing a Text Watermark + +Here's a comprehensive example demonstrating how to implement a text watermark in the new version: + +```js +const chart = createChart(container, options); +const mainSeries = chart.addLineSeries(); +mainSeries.setData(generateData()); + +const textWatermark = new TextWatermark({ + horzAlign: 'center', + vertAlign: 'center', + lines: [ + { + text: 'Hello', + color: 'rgba(255,0,0,0.5)', + fontSize: 100, + fontStyle: 'bold', + }, + { + text: 'This is a text watermark', + color: 'rgba(0,0,255,0.5)', + fontSize: 50, + fontStyle: 'italic', + fontFamily: 'monospace', + }, + ], +}); + +const firstPane = chart.panes()[0]; +firstPane.attachPrimitive(textWatermark); +``` ## Plugin Typings -Some of the plugin types and interfaces have been renamed due to the additional of Pane Primitives. +Some of the plugin types and interfaces have been renamed due to the additional +of Pane Primitives. - `ISeriesPrimitivePaneView` -> `IPrimitivePaneView` - `ISeriesPrimitivePaneRenderer` -> `IPrimitivePaneRenderer` diff --git a/website/tutorials/how_to/.eslintrc.js b/website/tutorials/how_to/.eslintrc.js index e5bae9c40c..379db16386 100644 --- a/website/tutorials/how_to/.eslintrc.js +++ b/website/tutorials/how_to/.eslintrc.js @@ -3,5 +3,7 @@ module.exports = { document: false, createChart: false, createChartEx: false, + TextWatermark: false, + ImageWatermark: false, }, }; diff --git a/website/tutorials/how_to/watermark-advanced.js b/website/tutorials/how_to/watermark-advanced.js index b709d35973..5ab395a50e 100644 --- a/website/tutorials/how_to/watermark-advanced.js +++ b/website/tutorials/how_to/watermark-advanced.js @@ -1,14 +1,12 @@ // remove-start -// Lightweight Charts™ Example: Watermark Advanced +// Lightweight Charts™ Example: Image Watermark // https://tradingview.github.io/lightweight-charts/tutorials/how_to/watermark // remove-end const chartOptions = { layout: { textColor: CHART_TEXT_COLOR, - // set chart background color to transparent so we can see the elements below - // highlight-next-line - background: { type: 'solid', color: 'transparent' }, + background: { type: 'solid', color: CHART_BACKGROUND_COLOR }, }, }; // remove-line @@ -16,18 +14,15 @@ const chartOptions = { const chart = createChart(document.getElementById('container'), chartOptions); // highlight-start -const container = document.getElementById('container'); -const background = document.createElement('div'); -// place below the chart -background.style.zIndex = -1; -background.style.position = 'absolute'; -// set size and position to match container -background.style.inset = '0px'; -background.style.backgroundImage = `url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTIiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMjkyIDEyOCI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBkPSJtMTgyLjkzIDcuNi42My0uMzdhNjQuMSA2NC4xIDAgMCAwIDIuNDMtNS4zMWw0Ljc3LTEuMzlhNjQuNjggNjQuNjggMCAwIDEtNC43MiAxMC41NGMuMzggMTAuNDUtMy45MyAyMS4xNS0xMS4xIDI5LjM3LTExLjY2IDEzLjQxLTI2Ljk4IDE1Ljk3LTQzLjU3IDEzLjc4bDEuMDctLjk4YTIxLjEgMjEuMSAwIDAgMCAzLjcyLTQuMDUgNDguMzcgNDguMzcgMCAwIDEtMTEuMDQgMi44NGMtMTAuNjUtNS41NC0yMS42NC0xNC45NC0yNC4yNy0yNy4yNyA5LjE5LTE3IDI4Ljk1LTI0LjAxIDQ3LjM5LTE5Ljk0YTIyLjU3IDIyLjU3IDAgMCAwIDUuODYgOS4wMmMtLjEyLTEuOTItLjEtMy44NC0uMS01Ljc2bC4wMS0xLjc4YzQuOCAyLjk2IDkuNjYgNS44NSAxNS41MiA1LjcgNC4wOC0uMSA4LjQtMS41MiAxMy40LTQuNFptLTIyLjU1IDIzLjI4YTguNDggOC40OCAwIDAgMC0xMi40NS0uMzNsLTcuOS03LjI2QTguNiA4LjYgMCAwIDAgMTMyIDEyYy02LjE0IDAtMTAuMjUgNi42My03LjcgMTIuMDlsLTEzLjAyIDEyLjE5Yy00LjEtNC45Ny01LjY4LTkuMy02LjE3LTEwLjk0IDguMzYtMTMuNzIgMjQuNDYtMjAuMTggNDAuMTUtMTcuMDcgMi45MyA2LjkgOC4zOCAxMC43MiAxNC43NyAxMy45NmwtLjMzLTEuMTRjLS43NC0yLjU2LTEuNDctNS4xLTEuNjItNy43OCA3LjA1IDMuNDUgMTQuNiAzLjM1IDIxLjc2LjMxLTQuNzYgNy4yNy0xMS4xMyAxNC4yMi0xOS40NiAxNy4yNlptLTIyLjU2LTQuMTkgOC4wMyA3LjM4QTguNiA4LjYgMCAwIDAgMTU0IDQ1YTguNiA4LjYgMCAwIDAgOC4yNS0xMC41NWM3Ljk5LTMuMDggMTQuMzctOS4zOCAxOS4yOC0xNi4yMy0zLjQ3IDE5LjQ3LTIxLjk2IDM0LjYxLTQxLjkgMzIuOTggMS43Ny0yLjg0IDIuNDktNi4wNiAzLjIxLTkuMjhsLjM1LTEuNTZjLTUuNDcgMy43Ny0xMC42NyA2LjM4LTE3LjM3IDcuNTJhNDkuOSA0OS45IDAgMCAxLTExLjg1LTguNjVsMTIuODMtMTJhOC41OCA4LjU4IDAgMCAwIDExLjAyLS41NFpNMTMyIDE2YTQuNSA0LjUgMCAxIDAgMCA5IDQuNSA0LjUgMCAwIDAgMC05Wm0xNy41IDIwLjVhNC41IDQuNSAwIDEgMSA5IDAgNC41IDQuNSAwIDAgMS05IDBaTTIxLjYzIDcxLjhhMi4zMyAyLjMzIDAgMCAxIDIuMzMgMi4zNCAyLjM0IDIuMzQgMCAwIDEtMi4zMyAyLjM3IDIuMzggMi4zOCAwIDAgMS0yLjM3LTIuMzcgMi4zOCAyLjM4IDAgMCAxIDIuMzctMi4zM1ptMS43NiA4LjJ2MTZoLTMuNTJWODBoMy41MlptLTYuNDYgMTZIMi43OFY3My4yOGgzLjc1djE5LjE0aDEwLjRWOTZabTI2LjM5LTEuMDlWODBIMzkuOHYyLjE0YTYuMjYgNi4yNiAwIDAgMC01LjEyLTIuNDZjLTQuMzIgMC03LjY4IDMuNTgtNy42OCA4LjEgMCA0LjU0IDMuMzYgOC4xMiA3LjY4IDguMTIgMi4yIDAgNC4xNi0xLjA4IDUuMTItMi41djEuNDhjMCAzLjIzLTIuMTggNS00LjgzIDVhNy4wMyA3LjAzIDAgMCAxLTUuMzItMi4zNGwtMi4xNCAyLjUyYzEuNTcgMS43NiA0LjM1IDIuOTUgNy40OSAyLjk1IDQuNzMgMCA4LjMyLTIuNTMgOC4zMi04LjFabS0xMi43Ny03LjEzYTQuNyA0LjcgMCAwIDEgNC43Ny00LjkgNC43IDQuNyAwIDAgMSA0Ljc3IDQuOSA0LjcgNC43IDAgMCAxLTQuNzcgNC45IDQuNyA0LjcgMCAwIDEtNC43Ny00LjlaTTUxLjU4IDk2aC0zLjUyVjcyaDMuNTJ2MTAuMThjLjk2LTEuNiAyLjc4LTIuNSA0Ljg2LTIuNSAzLjcxIDAgNi4xMSAyLjYyIDYuMTEgNi42OVY5NmgtMy41MnYtOS4wNmMwLTIuNTItMS4yOC00LjA2LTMuMzMtNC4wNi0yLjMzIDAtNC4xMiAxLjgyLTQuMTIgNS4yNVY5NlptMjQuODYtLjJ2LTMuMTNjLS41Mi4yLTEuMjIuMzItMS45LjMyLTEuODIgMC0yLjY4LS43My0yLjY4LTIuNzJ2LTcuMTNoNC41OFY4MGgtNC41OHYtNC40NWgtMy41MlY4MGgtMy4zM3YzLjE0aDMuMzN2Ny43YzAgMy42MiAyLjQgNS4zMiA1LjQ3IDUuMzIgMS4wOSAwIDEuOTItLjEzIDIuNjMtLjM1Wm0yMC4zLjJIOTMuNGwtMy41Mi0xMC4zN0w4Ni4zOSA5NmgtMy4zMmwtNS4zOC0xNmgzLjcybDMuNDUgMTEgMy42OC0xMWgyLjY5bDMuNjUgMTEgMy40OS0xMWgzLjc0bC01LjM4IDE2Wm02Ljc2LThjMCA0Ljg2IDMuNDkgOC4zMiA4LjM1IDguMzIgMy4zNiAwIDUuODYtMS40NCA3LjMtMy43MWwtMi43LTEuOTJhNS4wMyA1LjAzIDAgMCAxLTQuNTcgMi40M2MtMi42NSAwLTQuNzctMS43My00LjkzLTQuMzVoMTIuNThjLjAzLS41MS4wMy0uOC4wMy0xLjE1IDAtNS4xNi0zLjUyLTcuOTQtNy43MS03Ljk0QTguMTIgOC4xMiAwIDAgMCAxMDMuNSA4OFptOC4yMi01LjM0YzIuMDUgMCAzLjkgMS4yNCA0LjI5IDMuNTVoLTguOWMuNDgtMi4zNyAyLjU2LTMuNTUgNC42MS0zLjU1Wm0xMy4yMi0xMC44NWEyLjMzIDIuMzMgMCAwIDEgMi4zNCAyLjMzIDIuMzQgMi4zNCAwIDAgMS0yLjM0IDIuMzcgMi4zOCAyLjM4IDAgMCAxLTIuMzctMi4zNyAyLjM4IDIuMzggMCAwIDEgMi4zNy0yLjMzWm0yMS43IDIzLjFWODBoLTMuNTN2Mi4xNGE2LjI2IDYuMjYgMCAwIDAtNS4xMi0yLjQ2Yy00LjMyIDAtNy42OCAzLjU4LTcuNjggOC4xIDAgNC41NCAzLjM2IDguMTIgNy42OCA4LjEyIDIuMiAwIDQuMTYtMS4wOCA1LjEyLTIuNXYxLjQ4YzAgMy4yMy0yLjE4IDUtNC44MyA1YTcuMDMgNy4wMyAwIDAgMS01LjMxLTIuMzRsLTIuMTUgMi41MmMxLjU3IDEuNzYgNC4zNiAyLjk1IDcuNSAyLjk1IDQuNzMgMCA4LjMxLTIuNTMgOC4zMS04LjFaTTEyNi43IDk2aC0zLjUyVjgwaDMuNTJ2MTZabTcuMTYtOC4yMmE0LjcgNC43IDAgMCAxIDQuNzctNC45IDQuNyA0LjcgMCAwIDEgNC43NyA0LjkgNC43IDQuNyAwIDAgMS00Ljc3IDQuOSA0LjcgNC43IDAgMCAxLTQuNzctNC45Wk0xNTQuOSA5NmgtMy41MlY3MmgzLjUydjEwLjE4Yy45Ni0xLjYgMi43OC0yLjUgNC44Ni0yLjUgMy43MSAwIDYuMTEgMi42MiA2LjExIDYuNjlWOTZoLTMuNTJ2LTkuMDZjMC0yLjUyLTEuMjgtNC4wNi0zLjMyLTQuMDYtMi4zNCAwLTQuMTMgMS44Mi00LjEzIDUuMjVWOTZabTI0Ljg2LS4ydi0zLjEzYy0uNTEuMi0xLjIyLjMyLTEuODkuMzItMS44MiAwLTIuNjktLjczLTIuNjktMi43MnYtNy4xM2g0LjU4VjgwaC00LjU4di00LjQ1aC0zLjUyVjgwaC0zLjMzdjMuMTRoMy4zM3Y3LjdjMCAzLjYyIDIuNCA1LjMyIDUuNDcgNS4zMiAxLjEgMCAxLjkyLS4xMyAyLjYzLS4zNVptMjEuNTkuNThhMTEuNjcgMTEuNjcgMCAwIDEtMTEuNzUtMTEuNzRjMC02LjU2IDUuMjItMTEuNzQgMTEuNzUtMTEuNzQgNC40NSAwIDguMjIgMi4yNyAxMC4yNCA1Ljc2bC0zLjIzIDEuODVhNy45NCA3Ljk0IDAgMCAwLTcuMDEtNCA3Ljk2IDcuOTYgMCAwIDAtNy45NyA4LjEzIDcuOTYgNy45NiAwIDAgMCA3Ljk3IDguMTMgNy45NCA3Ljk0IDAgMCAwIDctNGwzLjI0IDEuODVhMTEuNjYgMTEuNjYgMCAwIDEtMTAuMjQgNS43NlptMTMuNC0uMzhoMy41MnYtNy44N2MwLTMuNDMgMS44LTUuMjUgNC4xMy01LjI1IDIuMDUgMCAzLjMzIDEuNTQgMy4zMyA0LjA2Vjk2aDMuNTJ2LTkuNjNjMC00LjA3LTIuNC02LjY5LTYuMTEtNi42OS0yLjA4IDAtMy45LjktNC44NyAyLjVWNzJoLTMuNTJ2MjRabTI1LjU2LjMyYy00LjM4IDAtNy43LTMuNzQtNy43LTguMzJzMy4zMi04LjMyIDcuNy04LjMyYzIuMyAwIDQuMjMgMS4xOCA1LjEyIDIuNDZWODBoMy41MnYxNmgtMy41MnYtMi4xNGE2LjM4IDYuMzggMCAwIDEtNS4xMiAyLjQ2Wm0uNjQtMy4yYzIuODUgMCA0Ljc3LTIuMjQgNC43Ny01LjEycy0xLjkyLTUuMTItNC43Ny01LjEyYy0yLjg0IDAtNC43NiAyLjI0LTQuNzYgNS4xMnMxLjkxIDUuMTIgNC43NiA1LjEyWk0yNTMuNzEgOTZoMy41MnYtNy44YzAtMy4yIDEuODMtNC45IDMuODQtNC45LjY0IDAgMS4xNS4xIDEuNzYuMjh2LTMuNjFjLS40OC0uMS0uOTMtLjEzLTEuMzctLjEzYTQuNSA0LjUgMCAwIDAtNC4yMyAzVjgwaC0zLjUydjE2Wm0yMS43My0zLjMzdjMuMTRjLS43LjIyLTEuNTQuMzUtMi42My4zNS0zLjA3IDAtNS40Ny0xLjctNS40Ny01LjMxdi03LjcxaC0zLjMzVjgwaDMuMzN2LTQuNDVoMy41MlY4MGg0LjU4djMuMTRoLTQuNTh2Ny4xM2MwIDEuOTkuODYgMi43MiAyLjY5IDIuNzIuNjcgMCAxLjM3LS4xMyAxLjg5LS4zMlptMTQuMjEtMS4zMWMwLTIuNjItMS42Ni00LjAzLTQuNDgtNC44NmwtMS42My0uNDhjLTEuNTctLjQ1LTEuOTItMS4xMi0xLjkyLTEuOSAwLS45NSAxLjA5LTEuNSAyLjE1LTEuNSAxLjMgMCAyLjMzLjY0IDMuMDQgMS42NGwyLjQzLTEuODZjLTEuMTItMS43Ni0zLjAxLTIuNzItNS40MS0yLjcyLTMuMiAwLTUuNyAxLjczLTUuNzMgNC41OC0uMDMgMi4zNiAxLjQxIDQuMTIgNC4yIDQuOWwxLjQuMzhjMS45Mi41NyAyLjQ3IDEuMTIgMi40NyAyLjA0IDAgMS4xMi0xLjA2IDEuNy0yLjMgMS43LTEuNjQgMC0zLjItLjgtMy44NS0yLjJsLTIuNTkgMS44NWMxLjE1IDIuMjcgMy41OCAzLjM5IDYuNDMgMy4zOSAzLjMgMCA1LjgtMS44OSA1LjgtNC45NlptLTE0My4zOCAyMS40YzAgLjQ2LS4zNy44NC0uODMuODRhLjg2Ljg2IDAgMCAxLS44Ny0uODVjMC0uNDYuMzktLjg1Ljg3LS44NS40NiAwIC44My4zOS44My44NVptLS4yOSAxMS4yNGgtMS4xMnYtOGgxLjEydjhabS01Mi4wMi4xNmE0LjA0IDQuMDQgMCAwIDAgMy45OC00LjE2IDQuMDQgNC4wNCAwIDAgMC0zLjk4LTQuMTZjLTEuMjQgMC0yLjM5LjY0LTIuOTYgMS41VjExMmgtMS4xMnYxMkg5MXYtMS4zNGMuNTcuODYgMS43MiAxLjUgMi45NiAxLjVabS0uMTItMS4wNGMtMS43NCAwLTIuOTQtMS40LTIuOTQtMy4xMiAwLTEuNzMgMS4yLTMuMTIgMi45NC0zLjEyIDEuNzUgMCAyLjk1IDEuNCAyLjk1IDMuMTIgMCAxLjczLTEuMiAzLjEyLTIuOTUgMy4xMlptNy45IDQuMjIgNS4zLTExLjM0aC0xLjI2bC0yLjkzIDYuMzUtMi45My02LjM1aC0xLjI0bDMuNTUgNy42LTEuNzYgMy43NGgxLjI2Wk0xMTUuMyAxMjRoLTEuMnYtMTAuMmgtMy42OHYtMS4xNmg4LjU2djEuMTVoLTMuNjhWMTI0Wm0zLjgyIDBoMS4xMnYtNC4wMmMwLTIuMDQgMS4yMy0yLjk0IDIuMjItMi45NC4yNCAwIC40NS4wMy42Ny4xMXYtMS4xN2EyLjQ0IDIuNDQgMCAwIDAtMi45IDEuNjZWMTE2aC0xLjExdjhabTExLjcyLTEuMzRhMy42NCAzLjY0IDAgMCAxLTIuOTYgMS41IDQuMDQgNC4wNCAwIDAgMS0zLjk4LTQuMTYgNC4wNCA0LjA0IDAgMCAxIDMuOTgtNC4xNmMxLjIzIDAgMi4zOS42NCAyLjk2IDEuNVYxMTZoMS4xMnY4aC0xLjEydi0xLjM0Wm0tNS44LTIuNjZjMCAxLjczIDEuMiAzLjEyIDIuOTUgMy4xMiAxLjc1IDAgMi45NS0xLjQgMi45NS0zLjEyIDAtMS43My0xLjItMy4xMi0yLjk1LTMuMTItMS43NCAwLTIuOTQgMS40LTIuOTQgMy4xMlptMTIuOTggNC4xNmMxLjIzIDAgMi4zOS0uNjQgMi45Ni0xLjVWMTI0aDEuMTJ2LTEySDE0MXY1LjM0YTMuNjQgMy42NCAwIDAgMC0yLjk2LTEuNSA0LjA0IDQuMDQgMCAwIDAtMy45OCA0LjE2IDQuMDQgNC4wNCAwIDAgMCAzLjk4IDQuMTZabS4xMS0xLjA0Yy0xLjc0IDAtMi45NC0xLjQtMi45NC0zLjEyIDAtMS43MyAxLjItMy4xMiAyLjk0LTMuMTIgMS43NSAwIDIuOTUgMS40IDIuOTUgMy4xMiAwIDEuNzMtMS4yIDMuMTItMi45NSAzLjEyWm0xMC42Ljg4aDEuMTF2LTMuOThjMC0xLjk5IDEuMS0zLjE0IDIuNS0zLjE0IDEuMTkgMCAyLjAyLjg2IDIuMDIgMi4yN1YxMjRoMS4xMnYtNWMwLTEuOTYtMS4yNy0zLjE2LTMuMDEtMy4xNi0xLjA0IDAtMi4wNS40NS0yLjYzIDEuNVYxMTZoLTEuMTF2OFptMTYuNzEtLjQyYzAgMi42MS0xLjcyIDMuOTItMy45NSAzLjkyLTEuODQgMC0zLjE3LS44My0zLjc3LTEuNzRsLjg4LS43NWEzLjQgMy40IDAgMCAwIDIuOSAxLjQ1YzEuMzcgMCAyLjgyLS44MyAyLjgyLTIuOTR2LTEuMDJjLS41Ny44Ni0xLjcgMS41LTIuOTIgMS41YTMuOTQgMy45NCAwIDAgMS0zLjk2LTQuMDggMy45NCAzLjk0IDAgMCAxIDMuOTYtNC4wOGMxLjIzIDAgMi4zNS42NCAyLjkyIDEuNVYxMTZoMS4xMnY3LjU4Wm0tNi44NC0zLjY2YzAgMS43MyAxLjE2IDMuMDQgMi45IDMuMDQgMS43NSAwIDIuOTItMS4zMSAyLjkyLTMuMDRzLTEuMTctMy4wNC0yLjkxLTMuMDRjLTEuNzUgMC0yLjkxIDEuMzEtMi45MSAzLjA0Wm0xMy41NSA0LjA4IDQuODgtMTEuMzZoLTEuMzVsLTQuMDMgOS4zOC00LjAzLTkuMzhoLTEuMzZsNC45IDExLjM2aC45OVptNy44NC0xMS4yNWMwIC40Ny0uMzcuODUtLjgzLjg1YS44Ni44NiAwIDAgMS0uODYtLjg1YzAtLjQ2LjM4LS44NS44Ni0uODUuNDcgMCAuODMuMzkuODMuODVabS0uMjggMTEuMjVoLTEuMTN2LThoMS4xM3Y4Wm02LjIuMTZhMy45IDMuOSAwIDAgMCAzLjU2LTEuOTVsLS45MS0uNmEyLjc4IDIuNzggMCAwIDEtMi42NCAxLjUxIDIuODcgMi44NyAwIDAgMS0yLjk2LTIuOTNoNi43NXYtLjNjLS4wMi0yLjU2LTEuNjgtNC4wNS0zLjc2LTQuMDVhNC4wNSA0LjA1IDAgMCAwLTQuMTUgNC4xNmMwIDIuMyAxLjYgNC4xNiA0LjEyIDQuMTZabS0uMDEtNy4yOGMxLjM0IDAgMi40NS44OCAyLjY0IDIuMzJoLTUuNDlhMi44NCAyLjg0IDAgMCAxIDIuODUtMi4zMlptMTMuNTUgNy4xMmgtLjkzbC0yLjEtNi4xLTIuMTQgNi4xaC0uOTJsLTIuNzQtOGgxLjE1bDIuMDggNi4wOCAyLjExLTYuMDhoLjg3bDIuMTEgNi4wOCAyLjA4LTYuMDhoMS4xN2wtMi43NCA4WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg+PC9zdmc+")`; -background.style.backgroundRepeat = 'no-repeat'; -background.style.backgroundPosition = 'center'; -background.style.opacity = '0.5'; -container.appendChild(background); +// imageDataUrl would usually be an url like '/images/my-image.png' +const imageDataUrl = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTIiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMjkyIDEyOCI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBkPSJtMTgyLjkzIDcuNi42My0uMzdhNjQuMSA2NC4xIDAgMCAwIDIuNDMtNS4zMWw0Ljc3LTEuMzlhNjQuNjggNjQuNjggMCAwIDEtNC43MiAxMC41NGMuMzggMTAuNDUtMy45MyAyMS4xNS0xMS4xIDI5LjM3LTExLjY2IDEzLjQxLTI2Ljk4IDE1Ljk3LTQzLjU3IDEzLjc4bDEuMDctLjk4YTIxLjEgMjEuMSAwIDAgMCAzLjcyLTQuMDUgNDguMzcgNDguMzcgMCAwIDEtMTEuMDQgMi44NGMtMTAuNjUtNS41NC0yMS42NC0xNC45NC0yNC4yNy0yNy4yNyA5LjE5LTE3IDI4Ljk1LTI0LjAxIDQ3LjM5LTE5Ljk0YTIyLjU3IDIyLjU3IDAgMCAwIDUuODYgOS4wMmMtLjEyLTEuOTItLjEtMy44NC0uMS01Ljc2bC4wMS0xLjc4YzQuOCAyLjk2IDkuNjYgNS44NSAxNS41MiA1LjcgNC4wOC0uMSA4LjQtMS41MiAxMy40LTQuNFptLTIyLjU1IDIzLjI4YTguNDggOC40OCAwIDAgMC0xMi40NS0uMzNsLTcuOS03LjI2QTguNiA4LjYgMCAwIDAgMTMyIDEyYy02LjE0IDAtMTAuMjUgNi42My03LjcgMTIuMDlsLTEzLjAyIDEyLjE5Yy00LjEtNC45Ny01LjY4LTkuMy02LjE3LTEwLjk0IDguMzYtMTMuNzIgMjQuNDYtMjAuMTggNDAuMTUtMTcuMDcgMi45MyA2LjkgOC4zOCAxMC43MiAxNC43NyAxMy45NmwtLjMzLTEuMTRjLS43NC0yLjU2LTEuNDctNS4xLTEuNjItNy43OCA3LjA1IDMuNDUgMTQuNiAzLjM1IDIxLjc2LjMxLTQuNzYgNy4yNy0xMS4xMyAxNC4yMi0xOS40NiAxNy4yNlptLTIyLjU2LTQuMTkgOC4wMyA3LjM4QTguNiA4LjYgMCAwIDAgMTU0IDQ1YTguNiA4LjYgMCAwIDAgOC4yNS0xMC41NWM3Ljk5LTMuMDggMTQuMzctOS4zOCAxOS4yOC0xNi4yMy0zLjQ3IDE5LjQ3LTIxLjk2IDM0LjYxLTQxLjkgMzIuOTggMS43Ny0yLjg0IDIuNDktNi4wNiAzLjIxLTkuMjhsLjM1LTEuNTZjLTUuNDcgMy43Ny0xMC42NyA2LjM4LTE3LjM3IDcuNTJhNDkuOSA0OS45IDAgMCAxLTExLjg1LTguNjVsMTIuODMtMTJhOC41OCA4LjU4IDAgMCAwIDExLjAyLS41NFpNMTMyIDE2YTQuNSA0LjUgMCAxIDAgMCA5IDQuNSA0LjUgMCAwIDAgMC05Wm0xNy41IDIwLjVhNC41IDQuNSAwIDEgMSA5IDAgNC41IDQuNSAwIDAgMS05IDBaTTIxLjYzIDcxLjhhMi4zMyAyLjMzIDAgMCAxIDIuMzMgMi4zNCAyLjM0IDIuMzQgMCAwIDEtMi4zMyAyLjM3IDIuMzggMi4zOCAwIDAgMS0yLjM3LTIuMzcgMi4zOCAyLjM4IDAgMCAxIDIuMzctMi4zM1ptMS43NiA4LjJ2MTZoLTMuNTJWODBoMy41MlptLTYuNDYgMTZIMi43OFY3My4yOGgzLjc1djE5LjE0aDEwLjRWOTZabTI2LjM5LTEuMDlWODBIMzkuOHYyLjE0YTYuMjYgNi4yNiAwIDAgMC01LjEyLTIuNDZjLTQuMzIgMC03LjY4IDMuNTgtNy42OCA4LjEgMCA0LjU0IDMuMzYgOC4xMiA3LjY4IDguMTIgMi4yIDAgNC4xNi0xLjA4IDUuMTItMi41djEuNDhjMCAzLjIzLTIuMTggNS00LjgzIDVhNy4wMyA3LjAzIDAgMCAxLTUuMzItMi4zNGwtMi4xNCAyLjUyYzEuNTcgMS43NiA0LjM1IDIuOTUgNy40OSAyLjk1IDQuNzMgMCA4LjMyLTIuNTMgOC4zMi04LjFabS0xMi43Ny03LjEzYTQuNyA0LjcgMCAwIDEgNC43Ny00LjkgNC43IDQuNyAwIDAgMSA0Ljc3IDQuOSA0LjcgNC43IDAgMCAxLTQuNzcgNC45IDQuNyA0LjcgMCAwIDEtNC43Ny00LjlaTTUxLjU4IDk2aC0zLjUyVjcyaDMuNTJ2MTAuMThjLjk2LTEuNiAyLjc4LTIuNSA0Ljg2LTIuNSAzLjcxIDAgNi4xMSAyLjYyIDYuMTEgNi42OVY5NmgtMy41MnYtOS4wNmMwLTIuNTItMS4yOC00LjA2LTMuMzMtNC4wNi0yLjMzIDAtNC4xMiAxLjgyLTQuMTIgNS4yNVY5NlptMjQuODYtLjJ2LTMuMTNjLS41Mi4yLTEuMjIuMzItMS45LjMyLTEuODIgMC0yLjY4LS43My0yLjY4LTIuNzJ2LTcuMTNoNC41OFY4MGgtNC41OHYtNC40NWgtMy41MlY4MGgtMy4zM3YzLjE0aDMuMzN2Ny43YzAgMy42MiAyLjQgNS4zMiA1LjQ3IDUuMzIgMS4wOSAwIDEuOTItLjEzIDIuNjMtLjM1Wm0yMC4zLjJIOTMuNGwtMy41Mi0xMC4zN0w4Ni4zOSA5NmgtMy4zMmwtNS4zOC0xNmgzLjcybDMuNDUgMTEgMy42OC0xMWgyLjY5bDMuNjUgMTEgMy40OS0xMWgzLjc0bC01LjM4IDE2Wm02Ljc2LThjMCA0Ljg2IDMuNDkgOC4zMiA4LjM1IDguMzIgMy4zNiAwIDUuODYtMS40NCA3LjMtMy43MWwtMi43LTEuOTJhNS4wMyA1LjAzIDAgMCAxLTQuNTcgMi40M2MtMi42NSAwLTQuNzctMS43My00LjkzLTQuMzVoMTIuNThjLjAzLS41MS4wMy0uOC4wMy0xLjE1IDAtNS4xNi0zLjUyLTcuOTQtNy43MS03Ljk0QTguMTIgOC4xMiAwIDAgMCAxMDMuNSA4OFptOC4yMi01LjM0YzIuMDUgMCAzLjkgMS4yNCA0LjI5IDMuNTVoLTguOWMuNDgtMi4zNyAyLjU2LTMuNTUgNC42MS0zLjU1Wm0xMy4yMi0xMC44NWEyLjMzIDIuMzMgMCAwIDEgMi4zNCAyLjMzIDIuMzQgMi4zNCAwIDAgMS0yLjM0IDIuMzcgMi4zOCAyLjM4IDAgMCAxLTIuMzctMi4zNyAyLjM4IDIuMzggMCAwIDEgMi4zNy0yLjMzWm0yMS43IDIzLjFWODBoLTMuNTN2Mi4xNGE2LjI2IDYuMjYgMCAwIDAtNS4xMi0yLjQ2Yy00LjMyIDAtNy42OCAzLjU4LTcuNjggOC4xIDAgNC41NCAzLjM2IDguMTIgNy42OCA4LjEyIDIuMiAwIDQuMTYtMS4wOCA1LjEyLTIuNXYxLjQ4YzAgMy4yMy0yLjE4IDUtNC44MyA1YTcuMDMgNy4wMyAwIDAgMS01LjMxLTIuMzRsLTIuMTUgMi41MmMxLjU3IDEuNzYgNC4zNiAyLjk1IDcuNSAyLjk1IDQuNzMgMCA4LjMxLTIuNTMgOC4zMS04LjFaTTEyNi43IDk2aC0zLjUyVjgwaDMuNTJ2MTZabTcuMTYtOC4yMmE0LjcgNC43IDAgMCAxIDQuNzctNC45IDQuNyA0LjcgMCAwIDEgNC43NyA0LjkgNC43IDQuNyAwIDAgMS00Ljc3IDQuOSA0LjcgNC43IDAgMCAxLTQuNzctNC45Wk0xNTQuOSA5NmgtMy41MlY3MmgzLjUydjEwLjE4Yy45Ni0xLjYgMi43OC0yLjUgNC44Ni0yLjUgMy43MSAwIDYuMTEgMi42MiA2LjExIDYuNjlWOTZoLTMuNTJ2LTkuMDZjMC0yLjUyLTEuMjgtNC4wNi0zLjMyLTQuMDYtMi4zNCAwLTQuMTMgMS44Mi00LjEzIDUuMjVWOTZabTI0Ljg2LS4ydi0zLjEzYy0uNTEuMi0xLjIyLjMyLTEuODkuMzItMS44MiAwLTIuNjktLjczLTIuNjktMi43MnYtNy4xM2g0LjU4VjgwaC00LjU4di00LjQ1aC0zLjUyVjgwaC0zLjMzdjMuMTRoMy4zM3Y3LjdjMCAzLjYyIDIuNCA1LjMyIDUuNDcgNS4zMiAxLjEgMCAxLjkyLS4xMyAyLjYzLS4zNVptMjEuNTkuNThhMTEuNjcgMTEuNjcgMCAwIDEtMTEuNzUtMTEuNzRjMC02LjU2IDUuMjItMTEuNzQgMTEuNzUtMTEuNzQgNC40NSAwIDguMjIgMi4yNyAxMC4yNCA1Ljc2bC0zLjIzIDEuODVhNy45NCA3Ljk0IDAgMCAwLTcuMDEtNCA3Ljk2IDcuOTYgMCAwIDAtNy45NyA4LjEzIDcuOTYgNy45NiAwIDAgMCA3Ljk3IDguMTMgNy45NCA3Ljk0IDAgMCAwIDctNGwzLjI0IDEuODVhMTEuNjYgMTEuNjYgMCAwIDEtMTAuMjQgNS43NlptMTMuNC0uMzhoMy41MnYtNy44N2MwLTMuNDMgMS44LTUuMjUgNC4xMy01LjI1IDIuMDUgMCAzLjMzIDEuNTQgMy4zMyA0LjA2Vjk2aDMuNTJ2LTkuNjNjMC00LjA3LTIuNC02LjY5LTYuMTEtNi42OS0yLjA4IDAtMy45LjktNC44NyAyLjVWNzJoLTMuNTJ2MjRabTI1LjU2LjMyYy00LjM4IDAtNy43LTMuNzQtNy43LTguMzJzMy4zMi04LjMyIDcuNy04LjMyYzIuMyAwIDQuMjMgMS4xOCA1LjEyIDIuNDZWODBoMy41MnYxNmgtMy41MnYtMi4xNGE2LjM4IDYuMzggMCAwIDEtNS4xMiAyLjQ2Wm0uNjQtMy4yYzIuODUgMCA0Ljc3LTIuMjQgNC43Ny01LjEycy0xLjkyLTUuMTItNC43Ny01LjEyYy0yLjg0IDAtNC43NiAyLjI0LTQuNzYgNS4xMnMxLjkxIDUuMTIgNC43NiA1LjEyWk0yNTMuNzEgOTZoMy41MnYtNy44YzAtMy4yIDEuODMtNC45IDMuODQtNC45LjY0IDAgMS4xNS4xIDEuNzYuMjh2LTMuNjFjLS40OC0uMS0uOTMtLjEzLTEuMzctLjEzYTQuNSA0LjUgMCAwIDAtNC4yMyAzVjgwaC0zLjUydjE2Wm0yMS43My0zLjMzdjMuMTRjLS43LjIyLTEuNTQuMzUtMi42My4zNS0zLjA3IDAtNS40Ny0xLjctNS40Ny01LjMxdi03LjcxaC0zLjMzVjgwaDMuMzN2LTQuNDVoMy41MlY4MGg0LjU4djMuMTRoLTQuNTh2Ny4xM2MwIDEuOTkuODYgMi43MiAyLjY5IDIuNzIuNjcgMCAxLjM3LS4xMyAxLjg5LS4zMlptMTQuMjEtMS4zMWMwLTIuNjItMS42Ni00LjAzLTQuNDgtNC44NmwtMS42My0uNDhjLTEuNTctLjQ1LTEuOTItMS4xMi0xLjkyLTEuOSAwLS45NSAxLjA5LTEuNSAyLjE1LTEuNSAxLjMgMCAyLjMzLjY0IDMuMDQgMS42NGwyLjQzLTEuODZjLTEuMTItMS43Ni0zLjAxLTIuNzItNS40MS0yLjcyLTMuMiAwLTUuNyAxLjczLTUuNzMgNC41OC0uMDMgMi4zNiAxLjQxIDQuMTIgNC4yIDQuOWwxLjQuMzhjMS45Mi41NyAyLjQ3IDEuMTIgMi40NyAyLjA0IDAgMS4xMi0xLjA2IDEuNy0yLjMgMS43LTEuNjQgMC0zLjItLjgtMy44NS0yLjJsLTIuNTkgMS44NWMxLjE1IDIuMjcgMy41OCAzLjM5IDYuNDMgMy4zOSAzLjMgMCA1LjgtMS44OSA1LjgtNC45NlptLTE0My4zOCAyMS40YzAgLjQ2LS4zNy44NC0uODMuODRhLjg2Ljg2IDAgMCAxLS44Ny0uODVjMC0uNDYuMzktLjg1Ljg3LS44NS40NiAwIC44My4zOS44My44NVptLS4yOSAxMS4yNGgtMS4xMnYtOGgxLjEydjhabS01Mi4wMi4xNmE0LjA0IDQuMDQgMCAwIDAgMy45OC00LjE2IDQuMDQgNC4wNCAwIDAgMC0zLjk4LTQuMTZjLTEuMjQgMC0yLjM5LjY0LTIuOTYgMS41VjExMmgtMS4xMnYxMkg5MXYtMS4zNGMuNTcuODYgMS43MiAxLjUgMi45NiAxLjVabS0uMTItMS4wNGMtMS43NCAwLTIuOTQtMS40LTIuOTQtMy4xMiAwLTEuNzMgMS4yLTMuMTIgMi45NC0zLjEyIDEuNzUgMCAyLjk1IDEuNCAyLjk1IDMuMTIgMCAxLjczLTEuMiAzLjEyLTIuOTUgMy4xMlptNy45IDQuMjIgNS4zLTExLjM0aC0xLjI2bC0yLjkzIDYuMzUtMi45My02LjM1aC0xLjI0bDMuNTUgNy42LTEuNzYgMy43NGgxLjI2Wk0xMTUuMyAxMjRoLTEuMnYtMTAuMmgtMy42OHYtMS4xNmg4LjU2djEuMTVoLTMuNjhWMTI0Wm0zLjgyIDBoMS4xMnYtNC4wMmMwLTIuMDQgMS4yMy0yLjk0IDIuMjItMi45NC4yNCAwIC40NS4wMy42Ny4xMXYtMS4xN2EyLjQ0IDIuNDQgMCAwIDAtMi45IDEuNjZWMTE2aC0xLjExdjhabTExLjcyLTEuMzRhMy42NCAzLjY0IDAgMCAxLTIuOTYgMS41IDQuMDQgNC4wNCAwIDAgMS0zLjk4LTQuMTYgNC4wNCA0LjA0IDAgMCAxIDMuOTgtNC4xNmMxLjIzIDAgMi4zOS42NCAyLjk2IDEuNVYxMTZoMS4xMnY4aC0xLjEydi0xLjM0Wm0tNS44LTIuNjZjMCAxLjczIDEuMiAzLjEyIDIuOTUgMy4xMiAxLjc1IDAgMi45NS0xLjQgMi45NS0zLjEyIDAtMS43My0xLjItMy4xMi0yLjk1LTMuMTItMS43NCAwLTIuOTQgMS40LTIuOTQgMy4xMlptMTIuOTggNC4xNmMxLjIzIDAgMi4zOS0uNjQgMi45Ni0xLjVWMTI0aDEuMTJ2LTEySDE0MXY1LjM0YTMuNjQgMy42NCAwIDAgMC0yLjk2LTEuNSA0LjA0IDQuMDQgMCAwIDAtMy45OCA0LjE2IDQuMDQgNC4wNCAwIDAgMCAzLjk4IDQuMTZabS4xMS0xLjA0Yy0xLjc0IDAtMi45NC0xLjQtMi45NC0zLjEyIDAtMS43MyAxLjItMy4xMiAyLjk0LTMuMTIgMS43NSAwIDIuOTUgMS40IDIuOTUgMy4xMiAwIDEuNzMtMS4yIDMuMTItMi45NSAzLjEyWm0xMC42Ljg4aDEuMTF2LTMuOThjMC0xLjk5IDEuMS0zLjE0IDIuNS0zLjE0IDEuMTkgMCAyLjAyLjg2IDIuMDIgMi4yN1YxMjRoMS4xMnYtNWMwLTEuOTYtMS4yNy0zLjE2LTMuMDEtMy4xNi0xLjA0IDAtMi4wNS40NS0yLjYzIDEuNVYxMTZoLTEuMTF2OFptMTYuNzEtLjQyYzAgMi42MS0xLjcyIDMuOTItMy45NSAzLjkyLTEuODQgMC0zLjE3LS44My0zLjc3LTEuNzRsLjg4LS43NWEzLjQgMy40IDAgMCAwIDIuOSAxLjQ1YzEuMzcgMCAyLjgyLS44MyAyLjgyLTIuOTR2LTEuMDJjLS41Ny44Ni0xLjcgMS41LTIuOTIgMS41YTMuOTQgMy45NCAwIDAgMS0zLjk2LTQuMDggMy45NCAzLjk0IDAgMCAxIDMuOTYtNC4wOGMxLjIzIDAgMi4zNS42NCAyLjkyIDEuNVYxMTZoMS4xMnY3LjU4Wm0tNi44NC0zLjY2YzAgMS43MyAxLjE2IDMuMDQgMi45IDMuMDQgMS43NSAwIDIuOTItMS4zMSAyLjkyLTMuMDRzLTEuMTctMy4wNC0yLjkxLTMuMDRjLTEuNzUgMC0yLjkxIDEuMzEtMi45MSAzLjA0Wm0xMy41NSA0LjA4IDQuODgtMTEuMzZoLTEuMzVsLTQuMDMgOS4zOC00LjAzLTkuMzhoLTEuMzZsNC45IDExLjM2aC45OVptNy44NC0xMS4yNWMwIC40Ny0uMzcuODUtLjgzLjg1YS44Ni44NiAwIDAgMS0uODYtLjg1YzAtLjQ2LjM4LS44NS44Ni0uODUuNDcgMCAuODMuMzkuODMuODVabS0uMjggMTEuMjVoLTEuMTN2LThoMS4xM3Y4Wm02LjIuMTZhMy45IDMuOSAwIDAgMCAzLjU2LTEuOTVsLS45MS0uNmEyLjc4IDIuNzggMCAwIDEtMi42NCAxLjUxIDIuODcgMi44NyAwIDAgMS0yLjk2LTIuOTNoNi43NXYtLjNjLS4wMi0yLjU2LTEuNjgtNC4wNS0zLjc2LTQuMDVhNC4wNSA0LjA1IDAgMCAwLTQuMTUgNC4xNmMwIDIuMyAxLjYgNC4xNiA0LjEyIDQuMTZabS0uMDEtNy4yOGMxLjM0IDAgMi40NS44OCAyLjY0IDIuMzJoLTUuNDlhMi44NCAyLjg0IDAgMCAxIDIuODUtMi4zMlptMTMuNTUgNy4xMmgtLjkzbC0yLjEtNi4xLTIuMTQgNi4xaC0uOTJsLTIuNzQtOGgxLjE1bDIuMDggNi4wOCAyLjExLTYuMDhoLjg3bDIuMTEgNi4wOCAyLjA4LTYuMDhoMS4xN2wtMi43NCA4WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg+PC9zdmc+'; +const imageWatermark = new ImageWatermark(imageDataUrl, { + alpha: 0.5, + padding: 20, +}); + +const firstPane = chart.panes()[0]; +firstPane.attachPrimitive(imageWatermark); // highlight-end const lineSeries = chart.addAreaSeries({ diff --git a/website/tutorials/how_to/watermark-simple.js b/website/tutorials/how_to/watermark-simple.js index a8bbd80fe7..73e09bfded 100644 --- a/website/tutorials/how_to/watermark-simple.js +++ b/website/tutorials/how_to/watermark-simple.js @@ -13,17 +13,22 @@ const chartOptions = { /** @type {import('lightweight-charts').IChartApi} */ const chart = createChart(document.getElementById('container'), chartOptions); +// remove-line +/** @type {import('lightweight-charts').TextWatermark} */ // highlight-start -chart.applyOptions({ - watermark: { - visible: true, - fontSize: 24, - horzAlign: 'center', - vertAlign: 'center', - color: 'rgba(171, 71, 188, 0.5)', - text: 'Watermark Example', - }, +const textWatermark = new TextWatermark({ + horzAlign: 'center', + vertAlign: 'center', + lines: [ + { + text: 'Watermark Example', + color: 'rgba(171, 71, 188, 0.5)', + fontSize: 24, + }, + ], }); +const firstPane = chart.panes()[0]; +firstPane.attachPrimitive(textWatermark); // highlight-end const lineSeries = chart.addAreaSeries({ diff --git a/website/tutorials/how_to/watermark.mdx b/website/tutorials/how_to/watermark.mdx index cda9d4f57d..6fe89b2a79 100644 --- a/website/tutorials/how_to/watermark.mdx +++ b/website/tutorials/how_to/watermark.mdx @@ -11,36 +11,41 @@ keywords: Lightweight Charts™ has a built-in feature for displaying simple text watermarks on your chart. This example shows how to configure and add this simple text watermark to your chart. -If you are looking to add a more complex watermark then have a look at the [advanced watermark example](#advanced-watermark-example) +If you are looking to add a more complex watermark then have a look at the [image watermark example](#image-watermark-example) included below. ## Short answer -A simple text watermark can be configured and added by specifying the [`watermark`](/docs/api/interfaces/ChartOptionsBase#watermark) property within -the chart options as follows: +A simple text watermark can be configured and added by using the [`TextWatermark`](/docs/next/api/classes/TextWatermark) pane primitive exported +from the library as follows: ```js -chart.applyOptions({ - watermark: { - visible: true, - fontSize: 24, - horzAlign: 'center', - vertAlign: 'center', - color: 'rgba(171, 71, 188, 0.5)', - text: 'Watermark Example', - }, +import { TextWatermark } from 'lightweight-charts'; + +const textWatermark = new TextWatermark({ + horzAlign: 'center', + vertAlign: 'center', + lines: [ + { + text: 'Watermark Example', + color: 'rgba(171, 71, 188, 0.5)', + fontSize: 24, + }, + ], }); -``` -The options available for the watermark are: [Watermark Options](/docs/api/interfaces/WatermarkOptions). +const firstPane = chart.panes()[0]; +firstPane.attachPrimitive(textWatermark); +``` -To have the watermark appear, you need to set `visible` to `true` and ensure that the `text` property isn't empty. +The options available for the watermark are: [TextWatermark Options](/docs/next/api/interfaces/TextWatermarkOptions). You can see full [working examples](#examples) below. ## Resources -- [Watermark Options](/docs/api/interfaces/WatermarkOptions) +- [`TextWatermark` pane primitive](/docs/next/api/classes/TextWatermark). +- [TextWatermark Options](/docs/next/api/interfaces/TextWatermarkOptions) ## Examples @@ -58,46 +63,38 @@ import codeSimple from "!!raw-loader!./watermark-simple.js"; {codeSimple} -### Advanced Watermark Example - -If a simple text watermark doesn't meet your requirements then you can use the following tips -to rather create your own custom watermark using `html` and `css`. +### Image Watermark Example -We will first set the `background` color of the chart to `transparent` so that we can -place our custom watermark underneath the chart and still see it. +If a simple text watermark doesn't meet your requirements then you can use the [`ImageWatermark`](/docs/next/api/classes/ImageWatermark) pane primitive exported +from the library as follows: ```js -chart.applyOptions({ - layout: { - // set chart background color to transparent so we can see the elements below - // highlight-next-line - background: { type: 'solid', color: 'transparent' }, - }, +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); ``` -Next we will create a `div` element, and attach it as a child of the `container` element which is holding the chart. +The options available for the watermark are: [ImageWatermark Options](/docs/next/api/interfaces/ImageWatermarkOptions). -By setting the `zIndex` value for this div to be negative it will appear beneath the chart. +You can see full [working examples](#examples) below. -We will position the div using `display: absolute` and by setting `inset: 0px` the div will fill the container. +## Resources -You can then style the div to meet your specific needs. +- [`ImageWatermark` pane primitive](/docs/next/api/classes/ImageWatermark). +- [ImageWatermark Options](/docs/next/api/interfaces/ImageWatermarkOptions) -```js -const container = document.getElementById('container'); -const background = document.createElement('div'); -// place below the chart -background.style.zIndex = -1; -background.style.position = 'absolute'; -// set size and position to match container -background.style.inset = '0px'; -background.style.backgroundImage = `url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTIiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMjkyIDEyOCI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBkPSJtMTgyLjkzIDcuNi42My0uMzdhNjQuMSA2NC4xIDAgMCAwIDIuNDMtNS4zMWw0Ljc3LTEuMzlhNjQuNjggNjQuNjggMCAwIDEtNC43MiAxMC41NGMuMzggMTAuNDUtMy45MyAyMS4xNS0xMS4xIDI5LjM3LTExLjY2IDEzLjQxLTI2Ljk4IDE1Ljk3LTQzLjU3IDEzLjc4bDEuMDctLjk4YTIxLjEgMjEuMSAwIDAgMCAzLjcyLTQuMDUgNDguMzcgNDguMzcgMCAwIDEtMTEuMDQgMi44NGMtMTAuNjUtNS41NC0yMS42NC0xNC45NC0yNC4yNy0yNy4yNyA5LjE5LTE3IDI4Ljk1LTI0LjAxIDQ3LjM5LTE5Ljk0YTIyLjU3IDIyLjU3IDAgMCAwIDUuODYgOS4wMmMtLjEyLTEuOTItLjEtMy44NC0uMS01Ljc2bC4wMS0xLjc4YzQuOCAyLjk2IDkuNjYgNS44NSAxNS41MiA1LjcgNC4wOC0uMSA4LjQtMS41MiAxMy40LTQuNFptLTIyLjU1IDIzLjI4YTguNDggOC40OCAwIDAgMC0xMi40NS0uMzNsLTcuOS03LjI2QTguNiA4LjYgMCAwIDAgMTMyIDEyYy02LjE0IDAtMTAuMjUgNi42My03LjcgMTIuMDlsLTEzLjAyIDEyLjE5Yy00LjEtNC45Ny01LjY4LTkuMy02LjE3LTEwLjk0IDguMzYtMTMuNzIgMjQuNDYtMjAuMTggNDAuMTUtMTcuMDcgMi45MyA2LjkgOC4zOCAxMC43MiAxNC43NyAxMy45NmwtLjMzLTEuMTRjLS43NC0yLjU2LTEuNDctNS4xLTEuNjItNy43OCA3LjA1IDMuNDUgMTQuNiAzLjM1IDIxLjc2LjMxLTQuNzYgNy4yNy0xMS4xMyAxNC4yMi0xOS40NiAxNy4yNlptLTIyLjU2LTQuMTkgOC4wMyA3LjM4QTguNiA4LjYgMCAwIDAgMTU0IDQ1YTguNiA4LjYgMCAwIDAgOC4yNS0xMC41NWM3Ljk5LTMuMDggMTQuMzctOS4zOCAxOS4yOC0xNi4yMy0zLjQ3IDE5LjQ3LTIxLjk2IDM0LjYxLTQxLjkgMzIuOTggMS43Ny0yLjg0IDIuNDktNi4wNiAzLjIxLTkuMjhsLjM1LTEuNTZjLTUuNDcgMy43Ny0xMC42NyA2LjM4LTE3LjM3IDcuNTJhNDkuOSA0OS45IDAgMCAxLTExLjg1LTguNjVsMTIuODMtMTJhOC41OCA4LjU4IDAgMCAwIDExLjAyLS41NFpNMTMyIDE2YTQuNSA0LjUgMCAxIDAgMCA5IDQuNSA0LjUgMCAwIDAgMC05Wm0xNy41IDIwLjVhNC41IDQuNSAwIDEgMSA5IDAgNC41IDQuNSAwIDAgMS05IDBaTTIxLjYzIDcxLjhhMi4zMyAyLjMzIDAgMCAxIDIuMzMgMi4zNCAyLjM0IDIuMzQgMCAwIDEtMi4zMyAyLjM3IDIuMzggMi4zOCAwIDAgMS0yLjM3LTIuMzcgMi4zOCAyLjM4IDAgMCAxIDIuMzctMi4zM1ptMS43NiA4LjJ2MTZoLTMuNTJWODBoMy41MlptLTYuNDYgMTZIMi43OFY3My4yOGgzLjc1djE5LjE0aDEwLjRWOTZabTI2LjM5LTEuMDlWODBIMzkuOHYyLjE0YTYuMjYgNi4yNiAwIDAgMC01LjEyLTIuNDZjLTQuMzIgMC03LjY4IDMuNTgtNy42OCA4LjEgMCA0LjU0IDMuMzYgOC4xMiA3LjY4IDguMTIgMi4yIDAgNC4xNi0xLjA4IDUuMTItMi41djEuNDhjMCAzLjIzLTIuMTggNS00LjgzIDVhNy4wMyA3LjAzIDAgMCAxLTUuMzItMi4zNGwtMi4xNCAyLjUyYzEuNTcgMS43NiA0LjM1IDIuOTUgNy40OSAyLjk1IDQuNzMgMCA4LjMyLTIuNTMgOC4zMi04LjFabS0xMi43Ny03LjEzYTQuNyA0LjcgMCAwIDEgNC43Ny00LjkgNC43IDQuNyAwIDAgMSA0Ljc3IDQuOSA0LjcgNC43IDAgMCAxLTQuNzcgNC45IDQuNyA0LjcgMCAwIDEtNC43Ny00LjlaTTUxLjU4IDk2aC0zLjUyVjcyaDMuNTJ2MTAuMThjLjk2LTEuNiAyLjc4LTIuNSA0Ljg2LTIuNSAzLjcxIDAgNi4xMSAyLjYyIDYuMTEgNi42OVY5NmgtMy41MnYtOS4wNmMwLTIuNTItMS4yOC00LjA2LTMuMzMtNC4wNi0yLjMzIDAtNC4xMiAxLjgyLTQuMTIgNS4yNVY5NlptMjQuODYtLjJ2LTMuMTNjLS41Mi4yLTEuMjIuMzItMS45LjMyLTEuODIgMC0yLjY4LS43My0yLjY4LTIuNzJ2LTcuMTNoNC41OFY4MGgtNC41OHYtNC40NWgtMy41MlY4MGgtMy4zM3YzLjE0aDMuMzN2Ny43YzAgMy42MiAyLjQgNS4zMiA1LjQ3IDUuMzIgMS4wOSAwIDEuOTItLjEzIDIuNjMtLjM1Wm0yMC4zLjJIOTMuNGwtMy41Mi0xMC4zN0w4Ni4zOSA5NmgtMy4zMmwtNS4zOC0xNmgzLjcybDMuNDUgMTEgMy42OC0xMWgyLjY5bDMuNjUgMTEgMy40OS0xMWgzLjc0bC01LjM4IDE2Wm02Ljc2LThjMCA0Ljg2IDMuNDkgOC4zMiA4LjM1IDguMzIgMy4zNiAwIDUuODYtMS40NCA3LjMtMy43MWwtMi43LTEuOTJhNS4wMyA1LjAzIDAgMCAxLTQuNTcgMi40M2MtMi42NSAwLTQuNzctMS43My00LjkzLTQuMzVoMTIuNThjLjAzLS41MS4wMy0uOC4wMy0xLjE1IDAtNS4xNi0zLjUyLTcuOTQtNy43MS03Ljk0QTguMTIgOC4xMiAwIDAgMCAxMDMuNSA4OFptOC4yMi01LjM0YzIuMDUgMCAzLjkgMS4yNCA0LjI5IDMuNTVoLTguOWMuNDgtMi4zNyAyLjU2LTMuNTUgNC42MS0zLjU1Wm0xMy4yMi0xMC44NWEyLjMzIDIuMzMgMCAwIDEgMi4zNCAyLjMzIDIuMzQgMi4zNCAwIDAgMS0yLjM0IDIuMzcgMi4zOCAyLjM4IDAgMCAxLTIuMzctMi4zNyAyLjM4IDIuMzggMCAwIDEgMi4zNy0yLjMzWm0yMS43IDIzLjFWODBoLTMuNTN2Mi4xNGE2LjI2IDYuMjYgMCAwIDAtNS4xMi0yLjQ2Yy00LjMyIDAtNy42OCAzLjU4LTcuNjggOC4xIDAgNC41NCAzLjM2IDguMTIgNy42OCA4LjEyIDIuMiAwIDQuMTYtMS4wOCA1LjEyLTIuNXYxLjQ4YzAgMy4yMy0yLjE4IDUtNC44MyA1YTcuMDMgNy4wMyAwIDAgMS01LjMxLTIuMzRsLTIuMTUgMi41MmMxLjU3IDEuNzYgNC4zNiAyLjk1IDcuNSAyLjk1IDQuNzMgMCA4LjMxLTIuNTMgOC4zMS04LjFaTTEyNi43IDk2aC0zLjUyVjgwaDMuNTJ2MTZabTcuMTYtOC4yMmE0LjcgNC43IDAgMCAxIDQuNzctNC45IDQuNyA0LjcgMCAwIDEgNC43NyA0LjkgNC43IDQuNyAwIDAgMS00Ljc3IDQuOSA0LjcgNC43IDAgMCAxLTQuNzctNC45Wk0xNTQuOSA5NmgtMy41MlY3MmgzLjUydjEwLjE4Yy45Ni0xLjYgMi43OC0yLjUgNC44Ni0yLjUgMy43MSAwIDYuMTEgMi42MiA2LjExIDYuNjlWOTZoLTMuNTJ2LTkuMDZjMC0yLjUyLTEuMjgtNC4wNi0zLjMyLTQuMDYtMi4zNCAwLTQuMTMgMS44Mi00LjEzIDUuMjVWOTZabTI0Ljg2LS4ydi0zLjEzYy0uNTEuMi0xLjIyLjMyLTEuODkuMzItMS44MiAwLTIuNjktLjczLTIuNjktMi43MnYtNy4xM2g0LjU4VjgwaC00LjU4di00LjQ1aC0zLjUyVjgwaC0zLjMzdjMuMTRoMy4zM3Y3LjdjMCAzLjYyIDIuNCA1LjMyIDUuNDcgNS4zMiAxLjEgMCAxLjkyLS4xMyAyLjYzLS4zNVptMjEuNTkuNThhMTEuNjcgMTEuNjcgMCAwIDEtMTEuNzUtMTEuNzRjMC02LjU2IDUuMjItMTEuNzQgMTEuNzUtMTEuNzQgNC40NSAwIDguMjIgMi4yNyAxMC4yNCA1Ljc2bC0zLjIzIDEuODVhNy45NCA3Ljk0IDAgMCAwLTcuMDEtNCA3Ljk2IDcuOTYgMCAwIDAtNy45NyA4LjEzIDcuOTYgNy45NiAwIDAgMCA3Ljk3IDguMTMgNy45NCA3Ljk0IDAgMCAwIDctNGwzLjI0IDEuODVhMTEuNjYgMTEuNjYgMCAwIDEtMTAuMjQgNS43NlptMTMuNC0uMzhoMy41MnYtNy44N2MwLTMuNDMgMS44LTUuMjUgNC4xMy01LjI1IDIuMDUgMCAzLjMzIDEuNTQgMy4zMyA0LjA2Vjk2aDMuNTJ2LTkuNjNjMC00LjA3LTIuNC02LjY5LTYuMTEtNi42OS0yLjA4IDAtMy45LjktNC44NyAyLjVWNzJoLTMuNTJ2MjRabTI1LjU2LjMyYy00LjM4IDAtNy43LTMuNzQtNy43LTguMzJzMy4zMi04LjMyIDcuNy04LjMyYzIuMyAwIDQuMjMgMS4xOCA1LjEyIDIuNDZWODBoMy41MnYxNmgtMy41MnYtMi4xNGE2LjM4IDYuMzggMCAwIDEtNS4xMiAyLjQ2Wm0uNjQtMy4yYzIuODUgMCA0Ljc3LTIuMjQgNC43Ny01LjEycy0xLjkyLTUuMTItNC43Ny01LjEyYy0yLjg0IDAtNC43NiAyLjI0LTQuNzYgNS4xMnMxLjkxIDUuMTIgNC43NiA1LjEyWk0yNTMuNzEgOTZoMy41MnYtNy44YzAtMy4yIDEuODMtNC45IDMuODQtNC45LjY0IDAgMS4xNS4xIDEuNzYuMjh2LTMuNjFjLS40OC0uMS0uOTMtLjEzLTEuMzctLjEzYTQuNSA0LjUgMCAwIDAtNC4yMyAzVjgwaC0zLjUydjE2Wm0yMS43My0zLjMzdjMuMTRjLS43LjIyLTEuNTQuMzUtMi42My4zNS0zLjA3IDAtNS40Ny0xLjctNS40Ny01LjMxdi03LjcxaC0zLjMzVjgwaDMuMzN2LTQuNDVoMy41MlY4MGg0LjU4djMuMTRoLTQuNTh2Ny4xM2MwIDEuOTkuODYgMi43MiAyLjY5IDIuNzIuNjcgMCAxLjM3LS4xMyAxLjg5LS4zMlptMTQuMjEtMS4zMWMwLTIuNjItMS42Ni00LjAzLTQuNDgtNC44NmwtMS42My0uNDhjLTEuNTctLjQ1LTEuOTItMS4xMi0xLjkyLTEuOSAwLS45NSAxLjA5LTEuNSAyLjE1LTEuNSAxLjMgMCAyLjMzLjY0IDMuMDQgMS42NGwyLjQzLTEuODZjLTEuMTItMS43Ni0zLjAxLTIuNzItNS40MS0yLjcyLTMuMiAwLTUuNyAxLjczLTUuNzMgNC41OC0uMDMgMi4zNiAxLjQxIDQuMTIgNC4yIDQuOWwxLjQuMzhjMS45Mi41NyAyLjQ3IDEuMTIgMi40NyAyLjA0IDAgMS4xMi0xLjA2IDEuNy0yLjMgMS43LTEuNjQgMC0zLjItLjgtMy44NS0yLjJsLTIuNTkgMS44NWMxLjE1IDIuMjcgMy41OCAzLjM5IDYuNDMgMy4zOSAzLjMgMCA1LjgtMS44OSA1LjgtNC45NlptLTE0My4zOCAyMS40YzAgLjQ2LS4zNy44NC0uODMuODRhLjg2Ljg2IDAgMCAxLS44Ny0uODVjMC0uNDYuMzktLjg1Ljg3LS44NS40NiAwIC44My4zOS44My44NVptLS4yOSAxMS4yNGgtMS4xMnYtOGgxLjEydjhabS01Mi4wMi4xNmE0LjA0IDQuMDQgMCAwIDAgMy45OC00LjE2IDQuMDQgNC4wNCAwIDAgMC0zLjk4LTQuMTZjLTEuMjQgMC0yLjM5LjY0LTIuOTYgMS41VjExMmgtMS4xMnYxMkg5MXYtMS4zNGMuNTcuODYgMS43MiAxLjUgMi45NiAxLjVabS0uMTItMS4wNGMtMS43NCAwLTIuOTQtMS40LTIuOTQtMy4xMiAwLTEuNzMgMS4yLTMuMTIgMi45NC0zLjEyIDEuNzUgMCAyLjk1IDEuNCAyLjk1IDMuMTIgMCAxLjczLTEuMiAzLjEyLTIuOTUgMy4xMlptNy45IDQuMjIgNS4zLTExLjM0aC0xLjI2bC0yLjkzIDYuMzUtMi45My02LjM1aC0xLjI0bDMuNTUgNy42LTEuNzYgMy43NGgxLjI2Wk0xMTUuMyAxMjRoLTEuMnYtMTAuMmgtMy42OHYtMS4xNmg4LjU2djEuMTVoLTMuNjhWMTI0Wm0zLjgyIDBoMS4xMnYtNC4wMmMwLTIuMDQgMS4yMy0yLjk0IDIuMjItMi45NC4yNCAwIC40NS4wMy42Ny4xMXYtMS4xN2EyLjQ0IDIuNDQgMCAwIDAtMi45IDEuNjZWMTE2aC0xLjExdjhabTExLjcyLTEuMzRhMy42NCAzLjY0IDAgMCAxLTIuOTYgMS41IDQuMDQgNC4wNCAwIDAgMS0zLjk4LTQuMTYgNC4wNCA0LjA0IDAgMCAxIDMuOTgtNC4xNmMxLjIzIDAgMi4zOS42NCAyLjk2IDEuNVYxMTZoMS4xMnY4aC0xLjEydi0xLjM0Wm0tNS44LTIuNjZjMCAxLjczIDEuMiAzLjEyIDIuOTUgMy4xMiAxLjc1IDAgMi45NS0xLjQgMi45NS0zLjEyIDAtMS43My0xLjItMy4xMi0yLjk1LTMuMTItMS43NCAwLTIuOTQgMS40LTIuOTQgMy4xMlptMTIuOTggNC4xNmMxLjIzIDAgMi4zOS0uNjQgMi45Ni0xLjVWMTI0aDEuMTJ2LTEySDE0MXY1LjM0YTMuNjQgMy42NCAwIDAgMC0yLjk2LTEuNSA0LjA0IDQuMDQgMCAwIDAtMy45OCA0LjE2IDQuMDQgNC4wNCAwIDAgMCAzLjk4IDQuMTZabS4xMS0xLjA0Yy0xLjc0IDAtMi45NC0xLjQtMi45NC0zLjEyIDAtMS43MyAxLjItMy4xMiAyLjk0LTMuMTIgMS43NSAwIDIuOTUgMS40IDIuOTUgMy4xMiAwIDEuNzMtMS4yIDMuMTItMi45NSAzLjEyWm0xMC42Ljg4aDEuMTF2LTMuOThjMC0xLjk5IDEuMS0zLjE0IDIuNS0zLjE0IDEuMTkgMCAyLjAyLjg2IDIuMDIgMi4yN1YxMjRoMS4xMnYtNWMwLTEuOTYtMS4yNy0zLjE2LTMuMDEtMy4xNi0xLjA0IDAtMi4wNS40NS0yLjYzIDEuNVYxMTZoLTEuMTF2OFptMTYuNzEtLjQyYzAgMi42MS0xLjcyIDMuOTItMy45NSAzLjkyLTEuODQgMC0zLjE3LS44My0zLjc3LTEuNzRsLjg4LS43NWEzLjQgMy40IDAgMCAwIDIuOSAxLjQ1YzEuMzcgMCAyLjgyLS44MyAyLjgyLTIuOTR2LTEuMDJjLS41Ny44Ni0xLjcgMS41LTIuOTIgMS41YTMuOTQgMy45NCAwIDAgMS0zLjk2LTQuMDggMy45NCAzLjk0IDAgMCAxIDMuOTYtNC4wOGMxLjIzIDAgMi4zNS42NCAyLjkyIDEuNVYxMTZoMS4xMnY3LjU4Wm0tNi44NC0zLjY2YzAgMS43MyAxLjE2IDMuMDQgMi45IDMuMDQgMS43NSAwIDIuOTItMS4zMSAyLjkyLTMuMDRzLTEuMTctMy4wNC0yLjkxLTMuMDRjLTEuNzUgMC0yLjkxIDEuMzEtMi45MSAzLjA0Wm0xMy41NSA0LjA4IDQuODgtMTEuMzZoLTEuMzVsLTQuMDMgOS4zOC00LjAzLTkuMzhoLTEuMzZsNC45IDExLjM2aC45OVptNy44NC0xMS4yNWMwIC40Ny0uMzcuODUtLjgzLjg1YS44Ni44NiAwIDAgMS0uODYtLjg1YzAtLjQ2LjM4LS44NS44Ni0uODUuNDcgMCAuODMuMzkuODMuODVabS0uMjggMTEuMjVoLTEuMTN2LThoMS4xM3Y4Wm02LjIuMTZhMy45IDMuOSAwIDAgMCAzLjU2LTEuOTVsLS45MS0uNmEyLjc4IDIuNzggMCAwIDEtMi42NCAxLjUxIDIuODcgMi44NyAwIDAgMS0yLjk2LTIuOTNoNi43NXYtLjNjLS4wMi0yLjU2LTEuNjgtNC4wNS0zLjc2LTQuMDVhNC4wNSA0LjA1IDAgMCAwLTQuMTUgNC4xNmMwIDIuMyAxLjYgNC4xNiA0LjEyIDQuMTZabS0uMDEtNy4yOGMxLjM0IDAgMi40NS44OCAyLjY0IDIuMzJoLTUuNDlhMi44NCAyLjg0IDAgMCAxIDIuODUtMi4zMlptMTMuNTUgNy4xMmgtLjkzbC0yLjEtNi4xLTIuMTQgNi4xaC0uOTJsLTIuNzQtOGgxLjE1bDIuMDggNi4wOCAyLjExLTYuMDhoLjg3bDIuMTEgNi4wOCAyLjA4LTYuMDhoMS4xN2wtMi43NCA4WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg+PC9zdmc+")`; -background.style.backgroundRepeat = 'no-repeat'; -background.style.backgroundPosition = 'center'; -background.style.opacity = '0.5'; -container.appendChild(background); -``` +:::tip + +Since the watermark image is black content with a transparent background, it may not be visible when +viewing the documentation site in dark mode. + +::: import codeAdvanced from "!!raw-loader!./watermark-advanced.js";