/// /// Copyright © 2016-2020 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// import * as CanvasGauges from 'canvas-gauges'; import GenericOptions = CanvasGauges.GenericOptions; import BaseGauge = CanvasGauges.BaseGauge; import { FontStyle, FontWeight } from '@home/components/widget/lib/settings.models'; import * as tinycolor_ from 'tinycolor2'; import { ColorFormats } from 'tinycolor2'; import { isDefined, isUndefined } from '@core/utils'; const tinycolor = tinycolor_; export type GaugeType = 'arc' | 'donut' | 'horizontalBar' | 'verticalBar'; export interface DigitalGaugeColorRange { pct: number; color: ColorFormats.RGBA; rgbString: string; } export interface CanvasDigitalGaugeOptions extends GenericOptions { gaugeType?: GaugeType; gaugeWithScale?: number; dashThickness?: number; roundedLineCap?: boolean; gaugeColor?: string; levelColors?: string[]; symbol?: string; label?: string; hideValue?: boolean; hideMinMax?: boolean; fontTitle?: string; fontValue?: string; fontMinMaxSize?: number; fontMinMaxStyle?: FontStyle; fontMinMaxWeight?: FontWeight; colorMinMax?: string; fontMinMax?: string; fontLabelSize?: number; fontLabelStyle?: FontStyle; fontLabelWeight?: FontWeight; colorLabel?: string; colorValue?: string; fontLabel?: string; neonGlowBrightness?: number; isMobile?: boolean; donutStartAngle?: number; donutEndAngle?: number; colorsRange?: DigitalGaugeColorRange[]; neonColorsRange?: DigitalGaugeColorRange[]; neonColorTitle?: string; neonColorLabel?: string; neonColorValue?: string; neonColorMinMax?: string; timestamp?: number; gaugeWidthScale?: number; fontTitleHeight?: FontHeightInfo; fontLabelHeight?: FontHeightInfo; fontValueHeight?: FontHeightInfo; fontMinMaxHeight?: FontHeightInfo; showTimestamp?: boolean; } const defaultDigitalGaugeOptions: CanvasDigitalGaugeOptions = { ...GenericOptions, ...{ gaugeType: 'arc', gaugeWithScale: 0.75, dashThickness: 0, roundedLineCap: false, gaugeColor: '#777', levelColors: ['blue'], symbol: '', label: '', hideValue: false, hideMinMax: false, fontTitle: 'Roboto', fontValue: 'Roboto', fontMinMaxSize: 10, fontMinMaxStyle: 'normal', fontMinMaxWeight: '500', colorMinMax: '#eee', fontMinMax: 'Roboto', fontLabelSize: 8, fontLabelStyle: 'normal', fontLabelWeight: '500', colorLabel: '#eee', fontLabel: 'Roboto', neonGlowBrightness: 0, isMobile: false } }; BaseGauge.initialize('CanvasDigitalGauge', defaultDigitalGaugeOptions); interface HTMLCanvasElementClone extends HTMLCanvasElement { initialized?: boolean; renderedTimestamp?: number; renderedValue?: number; renderedProgress?: string; } interface DigitalGaugeCanvasRenderingContext2D extends CanvasRenderingContext2D { barDimensions?: BarDimensions; currentColor?: string; } interface BarDimensions { baseX: number; baseY: number; width: number; height: number; origBaseX?: number; origBaseY?: number; fontSizeFactor?: number; Ro?: number; Cy?: number; titleY?: number; titleBottom?: number; Ri?: number; Cx?: number; strokeWidth?: number; Rm?: number; fontValueBaseline?: CanvasTextBaseline; fontMinMaxBaseline?: CanvasTextBaseline; fontMinMaxAlign?: CanvasTextAlign; labelY?: number; valueY?: number; minY?: number; maxY?: number; minX?: number; maxX?: number; barTop?: number; barBottom?: number; barLeft?: number; barRight?: number; dashLength?: number; } interface FontHeightInfo { ascent?: number; height?: number; descent?: number; } export class Drawings { static font(options: CanvasGauges.GenericOptions, target: string, baseSize: number): string { return options['font' + target + 'Style'] + ' ' + options['font' + target + 'Weight'] + ' ' + options['font' + target + 'Size'] * baseSize + 'px ' + options['font' + target]; } static normalizedValue(options: CanvasGauges.GenericOptions): {normal: number, indented: number} { const value = options.value; const min = options.minValue; const max = options.maxValue; const dt = (max - min) * 0.01; return { normal: value < min ? min : value > max ? max : value, indented: value < min ? min - dt : value > max ? max + dt : value }; } static verifyError(err: any) { if (err instanceof DOMException && (err as any).result === 0x8053000b) { return ; // ignore it } throw err; } } export class CanvasDigitalGauge extends BaseGauge { static heightCache: {[key: string]: FontHeightInfo} = {}; private elementValueClone: HTMLCanvasElementClone; private contextValueClone: DigitalGaugeCanvasRenderingContext2D; private elementProgressClone: HTMLCanvasElementClone; private contextProgressClone: DigitalGaugeCanvasRenderingContext2D; public _value: number; constructor(options: CanvasDigitalGaugeOptions) { options = {...defaultDigitalGaugeOptions,...(options || {})}; super(CanvasDigitalGauge.configure(options)); this.initValueClone(); } static configure(options: CanvasDigitalGaugeOptions): CanvasDigitalGaugeOptions { if (options.value > options.maxValue) { options.value = options.maxValue; } if (options.value < options.minValue) { options.value = options.minValue; } if (options.gaugeType === 'donut') { if (!options.donutStartAngle) { options.donutStartAngle = 1.5 * Math.PI; } if (!options.donutEndAngle) { options.donutEndAngle = options.donutStartAngle + 2 * Math.PI; } } const colorsCount = options.levelColors.length; const inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1; options.colorsRange = []; if (options.neonGlowBrightness) { options.neonColorsRange = []; } for (let i = 0; i < options.levelColors.length; i++) { const percentage = inc * i; let tColor = tinycolor(options.levelColors[i]); options.colorsRange[i] = { pct: percentage, color: tColor.toRgb(), rgbString: tColor.toRgbString() }; if (options.neonGlowBrightness) { tColor = tinycolor(options.levelColors[i]).brighten(options.neonGlowBrightness); options.neonColorsRange[i] = { pct: percentage, color: tColor.toRgb(), rgbString: tColor.toRgbString() }; } } if (options.neonGlowBrightness) { options.neonColorTitle = tinycolor(options.colorTitle).brighten(options.neonGlowBrightness).toHexString(); options.neonColorLabel = tinycolor(options.colorLabel).brighten(options.neonGlowBrightness).toHexString(); options.neonColorValue = tinycolor(options.colorValue).brighten(options.neonGlowBrightness).toHexString(); options.neonColorMinMax = tinycolor(options.colorMinMax).brighten(options.neonGlowBrightness).toHexString(); } return options; } private initValueClone() { const canvas = this.canvas; this.elementValueClone = canvas.element.cloneNode(true) as HTMLCanvasElementClone; this.contextValueClone = this.elementValueClone.getContext('2d'); this.elementValueClone.initialized = false; this.contextValueClone.translate(canvas.drawX, canvas.drawY); this.contextValueClone.save(); this.elementProgressClone = canvas.element.cloneNode(true) as HTMLCanvasElementClone; this.contextProgressClone = this.elementProgressClone.getContext('2d'); this.elementProgressClone.initialized = false; this.contextProgressClone.translate(canvas.drawX, canvas.drawY); this.contextProgressClone.save(); } destroy() { this.contextValueClone = null; this.elementValueClone = null; this.contextProgressClone = null; this.elementProgressClone = null; super.destroy(); } update(options: GenericOptions): BaseGauge { this.canvas.onRedraw = null; const result = super.update(options); this.initValueClone(); this.canvas.onRedraw = this.draw.bind(this); this.draw(); return result; } set timestamp(timestamp: number) { (this.options as CanvasDigitalGaugeOptions).timestamp = timestamp; this.draw(); } get timestamp(): number { return (this.options as CanvasDigitalGaugeOptions).timestamp; } draw(): CanvasDigitalGauge { try { const canvas = this.canvas; if (!canvas.drawWidth || !canvas.drawHeight) { return this; } const [x, y, w, h] = [ -canvas.drawX, -canvas.drawY, canvas.drawWidth, canvas.drawHeight ]; const options = this.options as CanvasDigitalGaugeOptions; const elementClone = canvas.elementClone as HTMLCanvasElementClone; if (!elementClone.initialized) { const context = canvas.contextClone; // clear the cache context.clearRect(x, y, w, h); context.save(); const canvasContext = canvas.context as DigitalGaugeCanvasRenderingContext2D; canvasContext.barDimensions = barDimensions(context, options, x, y, w, h); this.contextValueClone.barDimensions = canvasContext.barDimensions; this.contextProgressClone.barDimensions = canvasContext.barDimensions; drawBackground(context, options); drawDigitalTitle(context, options); if (!options.showTimestamp) { drawDigitalLabel(context, options); } drawDigitalMinMax(context, options); elementClone.initialized = true; } let valueChanged = false; if (!this.elementValueClone.initialized || isDefined(this._value) && this.elementValueClone.renderedValue !== this._value || (options.showTimestamp && this.elementValueClone.renderedTimestamp !== this.timestamp)) { if (isDefined(this._value)) { this.elementValueClone.renderedValue = this._value; } if (isUndefined(this.elementValueClone.renderedValue)) { this.elementValueClone.renderedValue = this.value; } const context = this.contextValueClone; // clear the cache context.clearRect(x, y, w, h); context.save(); context.drawImage(canvas.elementClone, x, y, w, h); context.save(); drawDigitalValue(context, options, this.elementValueClone.renderedValue); if (options.showTimestamp) { drawDigitalLabel(context, options); this.elementValueClone.renderedTimestamp = this.timestamp; } this.elementValueClone.initialized = true; valueChanged = true; } const progress = (Drawings.normalizedValue(options).normal - options.minValue) / (options.maxValue - options.minValue); const fixedProgress = progress.toFixed(3); if (!this.elementProgressClone.initialized || this.elementProgressClone.renderedProgress !== fixedProgress || valueChanged) { const context = this.contextProgressClone; // clear the cache context.clearRect(x, y, w, h); context.save(); context.drawImage(this.elementValueClone, x, y, w, h); context.save(); if (Number(fixedProgress) > 0) { drawProgress(context, options, progress); } this.elementProgressClone.initialized = true; this.elementProgressClone.renderedProgress = fixedProgress; } this.canvas.commit(); // clear the canvas canvas.context.clearRect(x, y, w, h); canvas.context.save(); canvas.context.drawImage(this.elementProgressClone, x, y, w, h); canvas.context.save(); // @ts-ignore super.draw(); } catch (err) { Drawings.verifyError(err); } return this; } getValueColor() { if (this.contextProgressClone) { let color = this.contextProgressClone.currentColor; const options = this.options as CanvasDigitalGaugeOptions; if (!color) { const progress = (Drawings.normalizedValue(options).normal - options.minValue) / (options.maxValue - options.minValue); if (options.neonGlowBrightness) { color = getProgressColor(progress, options.neonColorsRange); } else { color = getProgressColor(progress, options.colorsRange); } } return color; } else { return '#000'; } } } function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, x: number, y: number, w: number, h: number): BarDimensions { context.barDimensions = { baseX: x, baseY: y, width: w, height: h }; const bd = context.barDimensions; let aspect = 1; if (options.gaugeType === 'horizontalBar') { aspect = options.title === '' ? 2.5 : 2; } else if (options.gaugeType === 'verticalBar') { aspect = options.hideMinMax ? 0.35 : 0.5; } else if (options.gaugeType === 'arc') { aspect = 1.5; } const currentAspect = w / h; if (currentAspect > aspect) { bd.width = (h * aspect); bd.height = h; } else { bd.width = w; bd.height = w / aspect; } bd.origBaseX = bd.baseX; bd.origBaseY = bd.baseY; bd.baseX += (w - bd.width) / 2; bd.baseY += (h - bd.height) / 2; if (options.gaugeType === 'donut') { bd.fontSizeFactor = Math.max(bd.width, bd.height) / 125; } else if (options.gaugeType === 'verticalBar' || (options.gaugeType === 'arc' && options.title === '')) { bd.fontSizeFactor = Math.max(bd.width, bd.height) / 150; } else { bd.fontSizeFactor = Math.max(bd.width, bd.height) / 200; } const gws = options.gaugeWidthScale; if (options.neonGlowBrightness) { options.fontTitleHeight = determineFontHeight(options, 'Title', bd.fontSizeFactor); options.fontLabelHeight = determineFontHeight(options, 'Label', bd.fontSizeFactor); options.fontValueHeight = determineFontHeight(options, 'Value', bd.fontSizeFactor); options.fontMinMaxHeight = determineFontHeight(options, 'MinMax', bd.fontSizeFactor); } if (options.gaugeType === 'donut') { bd.Ro = bd.width / 2 - bd.width / 20; bd.Cy = bd.baseY + bd.height / 2; if (options.title && typeof options.title === 'string' && options.title.length > 0) { let titleOffset = determineFontHeight(options, 'Title', bd.fontSizeFactor).height; titleOffset += bd.fontSizeFactor * 2; bd.titleY = bd.baseY + titleOffset; titleOffset += bd.fontSizeFactor * 2; bd.Cy += titleOffset/2; bd.Ro -= titleOffset/2; } bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws * 1.2; bd.Cx = bd.baseX + bd.width / 2; } else if (options.gaugeType === 'arc') { if (options.title && typeof options.title === 'string' && options.title.length > 0) { bd.Ro = bd.width / 2 - bd.width / 7; bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws; } else { bd.Ro = bd.width / 2 - bd.fontSizeFactor * 4; bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws * 1.2; } bd.Cx = bd.baseX + bd.width / 2; bd.Cy = bd.baseY + bd.height / 1.25; } else if (options.gaugeType === 'verticalBar') { bd.Ro = bd.width / 2 - bd.width / 10; bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws * (options.hideMinMax ? 4 : 2.5); } else { // horizontalBar bd.Ro = bd.width / 2 - bd.width / 10; bd.Ri = bd.Ro - bd.width / 6.666666666666667 * gws; } bd.strokeWidth = bd.Ro - bd.Ri; bd.Rm = bd.Ri + bd.strokeWidth * 0.5; bd.fontValueBaseline = 'alphabetic'; bd.fontMinMaxBaseline = 'alphabetic'; bd.fontMinMaxAlign = 'center'; if (options.gaugeType === 'donut') { bd.fontValueBaseline = 'middle'; if (options.label && options.label.length > 0) { const valueHeight = determineFontHeight(options, 'Value', bd.fontSizeFactor).height; const labelHeight = determineFontHeight(options, 'Label', bd.fontSizeFactor).height; const total = valueHeight + labelHeight; bd.labelY = bd.Cy + total/2; bd.valueY = bd.Cy - total/2 + valueHeight/2; } else { bd.valueY = bd.Cy; } } else if (options.gaugeType === 'arc') { bd.titleY = bd.Cy - bd.Ro - 12 * bd.fontSizeFactor; bd.valueY = bd.Cy; bd.labelY = bd.Cy + (8 + options.fontLabelSize) * bd.fontSizeFactor; bd.minY = bd.maxY = bd.labelY; if (options.roundedLineCap) { bd.minY += bd.strokeWidth/2; bd.maxY += bd.strokeWidth/2; } bd.minX = bd.Cx - bd.Rm; bd.maxX = bd.Cx + bd.Rm; } else if (options.gaugeType === 'horizontalBar') { bd.titleY = bd.baseY + 4 * bd.fontSizeFactor + (options.title === '' ? 0 : options.fontTitleSize * bd.fontSizeFactor); bd.titleBottom = bd.titleY + (options.title === '' ? 0 : 4) * bd.fontSizeFactor; bd.valueY = bd.titleBottom + (options.hideValue ? 0 : options.fontValueSize * bd.fontSizeFactor); bd.barTop = bd.valueY + 8 * bd.fontSizeFactor; bd.barBottom = bd.barTop + bd.strokeWidth; if (options.hideMinMax && options.label === '') { bd.labelY = bd.barBottom; bd.barLeft = bd.origBaseX + options.fontMinMaxSize/3 * bd.fontSizeFactor; bd.barRight = bd.origBaseX + w + /*bd.width*/ - options.fontMinMaxSize/3 * bd.fontSizeFactor; } else { context.font = Drawings.font(options, 'MinMax', bd.fontSizeFactor); const minTextWidth = context.measureText(options.minValue+'').width; const maxTextWidth = context.measureText(options.maxValue+'').width; const maxW = Math.max(minTextWidth, maxTextWidth); bd.minX = bd.origBaseX + maxW/2 + options.fontMinMaxSize/3 * bd.fontSizeFactor; bd.maxX = bd.origBaseX + w + /*bd.width*/ - maxW/2 - options.fontMinMaxSize/3 * bd.fontSizeFactor; bd.barLeft = bd.minX; bd.barRight = bd.maxX; bd.labelY = bd.barBottom + (8 + options.fontLabelSize) * bd.fontSizeFactor; bd.minY = bd.maxY = bd.labelY; } } else if (options.gaugeType === 'verticalBar') { bd.titleY = bd.baseY + ((options.title === '' ? 0 : options.fontTitleSize) + 8) * bd.fontSizeFactor; bd.titleBottom = bd.titleY + (options.title === '' ? 0 : 4) * bd.fontSizeFactor; bd.valueY = bd.titleBottom + (options.hideValue ? 0 : options.fontValueSize * bd.fontSizeFactor); bd.barTop = bd.valueY + 8 * bd.fontSizeFactor; bd.labelY = bd.baseY + bd.height - 16; if (options.label === '') { bd.barBottom = bd.labelY; } else { bd.barBottom = bd.labelY - (8 + options.fontLabelSize) * bd.fontSizeFactor; } bd.minX = bd.maxX = bd.baseX + bd.width/2 + bd.strokeWidth/2 + options.fontMinMaxSize/3 * bd.fontSizeFactor; bd.minY = bd.barBottom; bd.maxY = bd.barTop; bd.fontMinMaxBaseline = 'middle'; bd.fontMinMaxAlign = 'left'; } if (options.dashThickness) { let circumference; if (options.gaugeType === 'donut') { circumference = Math.PI * bd.Rm * 2; } else if (options.gaugeType === 'arc') { circumference = Math.PI * bd.Rm; } else if (options.gaugeType === 'horizontalBar') { circumference = bd.barRight - bd.barLeft; } else if (options.gaugeType === 'verticalBar') { circumference = bd.barBottom - bd.barTop; } let dashCount = Math.floor(circumference / (options.dashThickness * bd.fontSizeFactor)); if (options.gaugeType === 'donut') { // tslint:disable-next-line:no-bitwise dashCount = (dashCount | 1) - 1; } else { // tslint:disable-next-line:no-bitwise dashCount = (dashCount - 1) | 1; } bd.dashLength = Math.ceil(circumference/dashCount); } return bd; } function determineFontHeight (options: CanvasDigitalGaugeOptions, target: string, baseSize: number): FontHeightInfo { const fontStyleStr = 'font-style:' + options['font' + target + 'Style'] + ';font-weight:' + options['font' + target + 'Weight'] + ';font-size:' + options['font' + target + 'Size'] * baseSize + 'px;font-family:' + options['font' + target]; let result = CanvasDigitalGauge.heightCache[fontStyleStr]; if (!result) { const fontStyle = { fontFamily: options['font' + target], fontSize: options['font' + target + 'Size'] * baseSize + 'px', fontWeight: options['font' + target + 'Weight'], fontStyle: options['font' + target + 'Style'] }; const text = $('Hg').css(fontStyle); const block = $('
'); const div = $(''); div.append(text, block); const body = $('body'); body.append(div); try { result = {}; block.css({ verticalAlign: 'baseline' }); result.ascent = block.offset().top - text.offset().top; block.css({ verticalAlign: 'bottom' }); result.height = block.offset().top - text.offset().top; result.descent = result.height - result.ascent; } finally { div.remove(); } CanvasDigitalGauge.heightCache[fontStyleStr] = result; } return result; } function drawBackground(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { const {barLeft, barRight, barTop, barBottom, width, baseX, strokeWidth} = context.barDimensions; if (context.barDimensions.dashLength) { context.setLineDash([context.barDimensions.dashLength]); } context.beginPath(); context.strokeStyle = options.gaugeColor; context.lineWidth = strokeWidth; if (options.roundedLineCap) { context.lineCap = 'round'; } if (options.gaugeType === 'donut') { context.arc(context.barDimensions.Cx, context.barDimensions.Cy, context.barDimensions.Rm, options.donutStartAngle, options.donutEndAngle); context.stroke(); } else if (options.gaugeType === 'arc') { context.arc(context.barDimensions.Cx, context.barDimensions.Cy, context.barDimensions.Rm, Math.PI, 2*Math.PI); context.stroke(); } else if (options.gaugeType === 'horizontalBar') { context.moveTo(barLeft,barTop + strokeWidth/2); context.lineTo(barRight,barTop + strokeWidth/2); context.stroke(); } else if (options.gaugeType === 'verticalBar') { context.moveTo(baseX + width/2, barBottom); context.lineTo(baseX + width/2, barTop); context.stroke(); } } function drawText(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, target: string, text: string, textX: number, textY: number) { context.fillStyle = options[(options.neonGlowBrightness ? 'neonColor' : 'color') + target]; context.fillText(text, textX, textY); } function drawDigitalTitle(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { if (!options.title || typeof options.title !== 'string') return; const {titleY, width, baseX, fontSizeFactor} = context.barDimensions; const textX = Math.round(baseX + width / 2); const textY = titleY; context.save(); context.textAlign = 'center'; context.font = Drawings.font(options, 'Title', fontSizeFactor); context.lineWidth = 0; drawText(context, options, 'Title', options.title.toUpperCase(), textX, textY); } function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { if (!options.label || options.label === '') return; const {labelY, baseX, width, fontSizeFactor} = context.barDimensions; const textX = Math.round(baseX + width / 2); const textY = labelY; context.save(); context.textAlign = 'center'; context.font = Drawings.font(options, 'Label', fontSizeFactor); context.lineWidth = 0; drawText(context, options, 'Label', options.label.toUpperCase(), textX, textY); } function drawDigitalMinMax(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { if (options.hideMinMax || options.gaugeType === 'donut') return; const {minY, maxY, minX, maxX, fontSizeFactor, fontMinMaxAlign, fontMinMaxBaseline} = context.barDimensions; context.save(); context.textAlign = fontMinMaxAlign; context.textBaseline = fontMinMaxBaseline; context.font = Drawings.font(options, 'MinMax', fontSizeFactor); context.lineWidth = 0; drawText(context, options, 'MinMax', options.minValue+'', minX, minY); drawText(context, options, 'MinMax', options.maxValue+'', maxX, maxY); } function padValue(val: any, options: CanvasDigitalGaugeOptions): string { const dec = options.valueDec; let strVal; let n; val = parseFloat(val); n = (val < 0); val = Math.abs(val); if (dec > 0) { strVal = val.toFixed(dec).toString() } else { strVal = Math.round(val).toString(); } strVal = (n ? '-' : '') + strVal; return strVal; } function drawDigitalValue(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, value: any) { if (options.hideValue) return; const {valueY, baseX, width, fontSizeFactor, fontValueBaseline} = context.barDimensions; const textX = Math.round(baseX + width / 2); const textY = valueY; let text = options.valueText || padValue(value, options); text += options.symbol; context.save(); context.textAlign = 'center'; context.textBaseline = fontValueBaseline; context.font = Drawings.font(options, 'Value', fontSizeFactor); context.lineWidth = 0; drawText(context, options, 'Value', text, textX, textY); } function getProgressColor(progress: number, colorsRange: DigitalGaugeColorRange[]): string { if (progress === 0 || colorsRange.length === 1) { return colorsRange[0].rgbString; } for (let j = 0; j < colorsRange.length; j++) { if (progress <= colorsRange[j].pct) { const lower = colorsRange[j - 1]; const upper = colorsRange[j]; const range = upper.pct - lower.pct; const rangePct = (progress - lower.pct) / range; const pctLower = 1 - rangePct; const pctUpper = rangePct; const color = tinycolor({ r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper), g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper), b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper) }); return color.toRgbString(); } } } function drawArcGlow(context: DigitalGaugeCanvasRenderingContext2D, Cx: number, Cy: number, Ri: number, Rm: number, Ro: number, color: string, progress: number, isDonut: boolean, donutStartAngle?: number, donutEndAngle?: number) { context.setLineDash([]); const strokeWidth = Ro - Ri; const blur = 0.55; const edge = strokeWidth*blur; context.lineWidth = strokeWidth+edge; const stop = blur/(2*blur+2); const glowGradient = context.createRadialGradient(Cx,Cy,Ri-edge/2,Cx,Cy,Ro+edge/2); const color1 = tinycolor(color).setAlpha(0.5).toRgbString(); const color2 = tinycolor(color).setAlpha(0).toRgbString(); glowGradient.addColorStop(0,color2); glowGradient.addColorStop(stop,color1); glowGradient.addColorStop(1.0-stop,color1); glowGradient.addColorStop(1,color2); context.strokeStyle = glowGradient; context.beginPath(); const e = 0.01 * Math.PI; if (isDonut) { context.arc(Cx, Cy, Rm, donutStartAngle - e, donutStartAngle + (donutEndAngle - donutStartAngle) * progress + e); } else { context.arc(Cx, Cy, Rm, Math.PI - e, Math.PI + Math.PI * progress + e); } context.stroke(); } function drawBarGlow(context: DigitalGaugeCanvasRenderingContext2D, startX: number, startY: number, endX: number, endY: number, color: string, strokeWidth: number, isVertical: boolean) { context.setLineDash([]); const blur = 0.55; const edge = strokeWidth*blur; context.lineWidth = strokeWidth+edge; const stop = blur/(2*blur+2); const gradientStartX = isVertical ? startX - context.lineWidth/2 : 0; const gradientStartY = isVertical ? 0 : startY - context.lineWidth/2; const gradientStopX = isVertical ? startX + context.lineWidth/2 : 0; const gradientStopY = isVertical ? 0 : startY + context.lineWidth/2; const glowGradient = context.createLinearGradient(gradientStartX,gradientStartY,gradientStopX,gradientStopY); const color1 = tinycolor(color).setAlpha(0.5).toRgbString(); const color2 = tinycolor(color).setAlpha(0).toRgbString(); glowGradient.addColorStop(0,color2); glowGradient.addColorStop(stop,color1); glowGradient.addColorStop(1.0-stop,color1); glowGradient.addColorStop(1,color2); context.strokeStyle = glowGradient; const dx = isVertical ? 0 : 0.05 * context.lineWidth; const dy = isVertical ? 0.05 * context.lineWidth : 0; context.beginPath(); context.moveTo(startX - dx, startY + dy); context.lineTo(endX + dx, endY - dy); context.stroke(); } function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, progress: number) { let neonColor; if (options.neonGlowBrightness) { context.currentColor = neonColor = getProgressColor(progress, options.neonColorsRange); } else { context.currentColor = context.strokeStyle = getProgressColor(progress, options.colorsRange); } const {barLeft, barRight, barTop, baseX, width, barBottom, Cx, Cy, Rm, Ro, Ri, strokeWidth} = context.barDimensions; if (context.barDimensions.dashLength) { context.setLineDash([context.barDimensions.dashLength]); } context.lineWidth = strokeWidth; if (options.roundedLineCap) { context.lineCap = 'round'; } else { context.lineCap = 'butt'; } if (options.gaugeType === 'donut') { if (options.neonGlowBrightness) { context.strokeStyle = neonColor; } context.beginPath(); context.arc(Cx, Cy, Rm, options.donutStartAngle, options.donutStartAngle + (options.donutEndAngle - options.donutStartAngle) * progress); context.stroke(); if (options.neonGlowBrightness && !options.isMobile) { drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, true, options.donutStartAngle, options.donutEndAngle); } } else if (options.gaugeType === 'arc') { if (options.neonGlowBrightness) { context.strokeStyle = neonColor; } context.beginPath(); context.arc(Cx, Cy, Rm, Math.PI, Math.PI + Math.PI * progress); context.stroke(); if (options.neonGlowBrightness && !options.isMobile) { drawArcGlow(context, Cx, Cy, Ri, Rm, Ro, neonColor, progress, false); } } else if (options.gaugeType === 'horizontalBar') { if (options.neonGlowBrightness) { context.strokeStyle = neonColor; } context.beginPath(); context.moveTo(barLeft,barTop + strokeWidth/2); context.lineTo(barLeft + (barRight-barLeft)*progress, barTop + strokeWidth/2); context.stroke(); if (options.neonGlowBrightness && !options.isMobile) { drawBarGlow(context, barLeft, barTop + strokeWidth/2, barLeft + (barRight-barLeft)*progress, barTop + strokeWidth/2, neonColor, strokeWidth, false); } } else if (options.gaugeType === 'verticalBar') { if (options.neonGlowBrightness) { context.strokeStyle = neonColor; } context.beginPath(); context.moveTo(baseX + width/2, barBottom); context.lineTo(baseX + width/2, barBottom - (barBottom-barTop)*progress); context.stroke(); if (options.neonGlowBrightness && !options.isMobile) { drawBarGlow(context, baseX + width/2, barBottom, baseX + width/2, barBottom - (barBottom-barTop)*progress, neonColor, strokeWidth, true); } } }