From 3b8257638768ffa5e7d5292fc33bbfecb6103056 Mon Sep 17 00:00:00 2001 From: Narayana Swamy Date: Thu, 18 Apr 2024 00:27:51 +0530 Subject: [PATCH] Code Refactoring; Typescript changes; --- dist/heatmap.d.ts | 109 --- .../heatmap.d.ts} | 0 dist/{ => types}/main.d.ts | 4 +- dist/{ => types}/shaders.d.ts | 0 dist/{ => types}/types.d.ts | 0 dist/{ => types}/utils/shaderUtils.d.ts | 0 dist/{ => types}/utils/utils.d.ts | 0 dist/visualHeatmap.esm.js | 145 +--- dist/visualHeatmap.esm.min.js | 2 +- dist/visualHeatmap.js | 145 +--- dist/visualHeatmap.min.js | 2 +- package.json | 6 +- rollup.config.js | 41 +- src/heatmap.ts | 819 ++++++++++++++---- src/heatmapRenderer.ts | 686 --------------- src/heatmap_.ts | 178 ++++ src/main.ts | 5 +- 17 files changed, 880 insertions(+), 1262 deletions(-) delete mode 100644 dist/heatmap.d.ts rename dist/{heatmapRenderer.d.ts => types/heatmap.d.ts} (100%) rename dist/{ => types}/main.d.ts (57%) rename dist/{ => types}/shaders.d.ts (100%) rename dist/{ => types}/types.d.ts (100%) rename dist/{ => types}/utils/shaderUtils.d.ts (100%) rename dist/{ => types}/utils/utils.d.ts (100%) delete mode 100644 src/heatmapRenderer.ts create mode 100644 src/heatmap_.ts diff --git a/dist/heatmap.d.ts b/dist/heatmap.d.ts deleted file mode 100644 index 1ef1d11..0000000 --- a/dist/heatmap.d.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { HeatmapRenderer } from "./heatmapRenderer"; -import { BackgroundImageConfig, GradientElement, HeatmapConfig, Point, Translate } from "./types"; -export declare class Heatmap { - private renderer; - ctx: WebGL2RenderingContext | null; - ratio: number; - width: number; - height: number; - min: number; - max: number; - size: number; - zoom: number; - angle: number; - intensity: number; - translate: [number, number]; - opacity: number; - gradient: import("./types").MappedGradient | null; - imageConfig: BackgroundImageConfig | null; - constructor(context: string | HTMLElement, config: HeatmapConfig); - /** - * Set the maximum data value for relative gradient calculations - * @param max - number - * @returns instance - */ - setMax(max: number): HeatmapRenderer; - /** - * Set the minimum data value for relative gradient calculations - * @param min - number - * @returns instance - */ - setMin(min: number): HeatmapRenderer; - /** - * Accepts array of objects with color value and offset - * @param gradient - Color Gradient - * @returns instance - */ - setGradient(gradient: GradientElement[]): HeatmapRenderer; - /** - * Set the translate transformation on the canvas - * @param translate - Accepts array [x, y] - * @returns instance - */ - setTranslate(translate: Translate): HeatmapRenderer; - /** - * Set the zoom transformation on the canvas - * @param zoom - Accepts float value - * @returns instance - */ - setZoom(zoom: number): HeatmapRenderer; - /** - * Set the rotation transformation on the canvas - * @param angle - Accepts angle in radians - * @returns instance - */ - setRotationAngle(angle: number): HeatmapRenderer; - /** - * Set the point radius - * @param size - Accepts float value - * @returns instance - */ - setSize(size: number): HeatmapRenderer; - /** - * Set the intensity factor - * @param intensity - Accepts float value - * @returns instance - */ - setIntensity(intensity: number): HeatmapRenderer; - /** - * Set the opacity factor - * @param opacity - The opacity factor. - * @returns instance - */ - setOpacity(opacity: number): HeatmapRenderer; - /** - * Set the background image - * @param config - Accepts Object with { Url, height, width, x, and y} properties - * @returns instance - */ - setBackgroundImage(config: BackgroundImageConfig): HeatmapRenderer | undefined; - /** - * After adding data points, need to invoke .render() method to update the heatmap - * @param data - The data points with 'x', 'y' and 'value' - * @param transIntactFlag - Flag indicating whether to apply existing heatmap transformations on the newly added data points - * @returns instance - */ - addData(data: Point[], transIntactFlag: boolean): HeatmapRenderer; - /** - * @param data - Accepts an array of data points with 'x', 'y' and 'value' - * @returns instance - */ - renderData(data: Point[]): HeatmapRenderer; - /** - * Method to re-render the heatmap. This method needs to be invoked as and when configurations get changed - */ - render(): void; - /** - * Get projected co-ordinates relative to the heatmap layer - * @param data - The data point to project. - * @returns projected data point. - */ - projection(data: Point): { - x: number; - y: number; - }; - /** - * Clears canvas - */ - clear(): void; -} diff --git a/dist/heatmapRenderer.d.ts b/dist/types/heatmap.d.ts similarity index 100% rename from dist/heatmapRenderer.d.ts rename to dist/types/heatmap.d.ts diff --git a/dist/main.d.ts b/dist/types/main.d.ts similarity index 57% rename from dist/main.d.ts rename to dist/types/main.d.ts index 9331b8b..e5be85c 100644 --- a/dist/main.d.ts +++ b/dist/types/main.d.ts @@ -1,3 +1,3 @@ -import { Heatmap } from "./heatmap"; +import { HeatmapRenderer } from "./heatmap"; import { HeatmapConfig } from "./types"; -export default function (context: string | HTMLElement, config: HeatmapConfig): Heatmap; +export default function (context: string | HTMLElement, config: HeatmapConfig): HeatmapRenderer; diff --git a/dist/shaders.d.ts b/dist/types/shaders.d.ts similarity index 100% rename from dist/shaders.d.ts rename to dist/types/shaders.d.ts diff --git a/dist/types.d.ts b/dist/types/types.d.ts similarity index 100% rename from dist/types.d.ts rename to dist/types/types.d.ts diff --git a/dist/utils/shaderUtils.d.ts b/dist/types/utils/shaderUtils.d.ts similarity index 100% rename from dist/utils/shaderUtils.d.ts rename to dist/types/utils/shaderUtils.d.ts diff --git a/dist/utils/utils.d.ts b/dist/types/utils/utils.d.ts similarity index 100% rename from dist/utils/utils.d.ts rename to dist/types/utils/utils.d.ts diff --git a/dist/visualHeatmap.esm.js b/dist/visualHeatmap.esm.js index 95a8d8f..9ff55d5 100644 --- a/dist/visualHeatmap.esm.js +++ b/dist/visualHeatmap.esm.js @@ -359,7 +359,8 @@ function gradientMapper(grad) { offset: offsets, }; } -function extractData(data, self) { +function extractData(data) { + const self = this; const len = data.length; let { posVec = new Float32Array(), rVec = new Float32Array() } = (self.hearmapExData || {}); if (self.pLen !== len) { @@ -793,7 +794,7 @@ class HeatmapRenderer { if (data.constructor !== Array) { throw new Error("Expected Array type"); } - this.hearmapExData = extractData(data, this); + this.hearmapExData = extractData.call(this, data); this.heatmapData = data; this.render(); return this; @@ -830,145 +831,9 @@ class HeatmapRenderer { } } -// Internal class that encapsulates private properties and methods -class Heatmap { - constructor(context, config) { - this.renderer = new HeatmapRenderer(context, config); - this.ctx = this.renderer.ctx; - this.ratio = this.renderer.ratio; - this.width = this.renderer.width; - this.height = this.renderer.height; - this.min = this.renderer.min; - this.max = this.renderer.max; - this.size = this.renderer.size; - this.zoom = this.renderer.zoom; - this.angle = this.renderer.angle; - this.intensity = this.renderer.intensity; - this.translate = this.renderer.translate; - this.opacity = this.renderer.opacity; - this.gradient = this.renderer.gradient; - this.imageConfig = this.renderer.imageConfig; - } - /** - * Set the maximum data value for relative gradient calculations - * @param max - number - * @returns instance - */ - setMax(max) { - return this.renderer.setMax(max); - } - /** - * Set the minimum data value for relative gradient calculations - * @param min - number - * @returns instance - */ - setMin(min) { - return this.renderer.setMin(min); - } - /** - * Accepts array of objects with color value and offset - * @param gradient - Color Gradient - * @returns instance - */ - setGradient(gradient) { - return this.renderer.setGradient(gradient); - } - /** - * Set the translate transformation on the canvas - * @param translate - Accepts array [x, y] - * @returns instance - */ - setTranslate(translate) { - return this.renderer.setTranslate(translate); - } - /** - * Set the zoom transformation on the canvas - * @param zoom - Accepts float value - * @returns instance - */ - setZoom(zoom) { - return this.renderer.setZoom(zoom); - } - /** - * Set the rotation transformation on the canvas - * @param angle - Accepts angle in radians - * @returns instance - */ - setRotationAngle(angle) { - return this.renderer.setRotationAngle(angle); - } - /** - * Set the point radius - * @param size - Accepts float value - * @returns instance - */ - setSize(size) { - return this.renderer.setSize(size); - } - /** - * Set the intensity factor - * @param intensity - Accepts float value - * @returns instance - */ - setIntensity(intensity) { - return this.renderer.setIntensity(intensity); - } - /** - * Set the opacity factor - * @param opacity - The opacity factor. - * @returns instance - */ - setOpacity(opacity) { - return this.renderer.setOpacity(opacity); - } - /** - * Set the background image - * @param config - Accepts Object with { Url, height, width, x, and y} properties - * @returns instance - */ - setBackgroundImage(config) { - return this.renderer.setBackgroundImage(config); - } - /** - * After adding data points, need to invoke .render() method to update the heatmap - * @param data - The data points with 'x', 'y' and 'value' - * @param transIntactFlag - Flag indicating whether to apply existing heatmap transformations on the newly added data points - * @returns instance - */ - addData(data, transIntactFlag) { - return this.renderer.addData(data, transIntactFlag); - } - /** - * @param data - Accepts an array of data points with 'x', 'y' and 'value' - * @returns instance - */ - renderData(data) { - return this.renderer.renderData(data); - } - /** - * Method to re-render the heatmap. This method needs to be invoked as and when configurations get changed - */ - render() { - this.renderer.render(); - } - /** - * Get projected co-ordinates relative to the heatmap layer - * @param data - The data point to project. - * @returns projected data point. - */ - projection(data) { - return this.renderer.projection(data); - } - /** - * Clears canvas - */ - clear() { - this.renderer.clear(); - } -} - +// import { Heatmap } from "./heatmap"; function main (context, config) { - return new Heatmap(context, config); + return new HeatmapRenderer(context, config); } export { main as default }; diff --git a/dist/visualHeatmap.esm.min.js b/dist/visualHeatmap.esm.min.js index dde8f79..54363ce 100644 --- a/dist/visualHeatmap.esm.min.js +++ b/dist/visualHeatmap.esm.min.js @@ -3,4 +3,4 @@ * (c) 2024 Narayana Swamy (narayanaswamy14@gmail.com) * @license BSD-3-Clause */ -const t={vertex:"#version 300 es\n\t\t\t\tin vec2 a_position;\n\t\t\t\tin float a_intensity;\n\t\t\t\tuniform float u_size;\n\t\t\t\tuniform vec2 u_resolution;\n\t\t\t\tuniform vec2 u_translate; \n\t\t\t\tuniform float u_zoom; \n\t\t\t\tuniform float u_angle; \n\t\t\t\tuniform float u_density;\n\t\t\t\tout float v_i;\n\n\t\t\t\tvec2 rotation(vec2 v, float a, float aspect) {\n\t\t\t\t\tfloat s = sin(a); float c = cos(a); mat2 rotationMat = mat2(c, -s, s, c); \n\t\t\t\t\tmat2 scaleMat = mat2(aspect, 0.0, 0.0, 1.0);\n\t\t\t\t\tmat2 scaleMatInv = mat2(1.0/aspect, 0.0, 0.0, 1.0);\n\t\t\t\t\treturn scaleMatInv * rotationMat * scaleMat * v;\n\t\t\t\t}\n\n\t\t\t\tvoid main() {\n\t\t\t\t\tvec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n\t\t\t\t\tvec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n\t\t\t\t\tfloat zoomFactor = max(u_zoom, 0.1);\n\t\t\t\t\tzeroToTwo = zeroToTwo / zoomFactor;\n\t\t\t\t\tif (u_angle != 0.0) {\n\t\t\t\t\t\tzeroToTwo = rotation(zeroToTwo, u_angle, u_resolution.x / u_resolution.y);\n\t\t\t\t\t}\n\t\t\t\t\tgl_Position = vec4(zeroToTwo , 0, 1);\n\t\t\t\t\tgl_PointSize = u_size * u_density;\n\t\t\t\t\tv_i = a_intensity;\n\t\t\t\t}",fragment:"#version 300 es\n\t\t\t\tprecision mediump float;\n\t\t\t\tuniform float u_max;\n\t\t\t\tuniform float u_min;\n\t\t\t\tuniform float u_intensity;\n\t\t\t\tin float v_i;\n\t\t\t\tout vec4 fragColor;\n\t\t\t\tvoid main() {\n\t\t\t\t\tfloat r = 0.0; \n\t\t\t\t\tvec2 cxy = 2.0 * gl_PointCoord - 1.0;\n\t\t\t\t\tr = dot(cxy, cxy);\n\t\t\t\t\tfloat deno = max(u_max - u_min, 1.0);\n\t\t\t\t\tif(r <= 1.0) {\n\t\t\t\t\t\tfragColor = vec4(0, 0, 0, ((v_i - u_min) / (deno)) * u_intensity * (1.0 - sqrt(r)));\n\t\t\t\t\t}\n\t\t\t\t}"},e={vertex:"#version 300 es\n\t\t\t\tprecision highp float;\n\t\t\t\tin vec2 a_texCoord;\n\t\t\t\tout vec2 v_texCoord;\n\t\t\t\tvoid main() {\n\t\t\t\t\tvec2 clipSpace = a_texCoord * 2.0 - 1.0;\n\t\t\t\t\tgl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);\n\t\t\t\t\tv_texCoord = a_texCoord;\n\t\t\t\t}\n\t",fragment:"#version 300 es\n\t\t\t\t\tprecision mediump float;\n\t\t\t\t\tin vec2 v_texCoord;\n\t\t\t\t\tout vec4 fragColor;\n\t\t\t\t\tuniform sampler2D u_framebuffer;\n\t\t\t\t\tuniform vec4 u_colorArr[20];\n\t\t\t\t\tuniform float u_colorCount;\n\t\t\t\t\tuniform float u_opacity;\n\t\t\t\t\tuniform float u_offset[20];\n\n\t\t\t\t\tfloat remap ( float minval, float maxval, float curval ) {\n\t\t\t\t\t\treturn ( curval - minval ) / ( maxval - minval );\n\t\t\t\t\t}\n\n\t\t\t\t\tvoid main() {\n\t\t\t\t\t\tfloat alpha = texture(u_framebuffer, v_texCoord.xy).a;\n\t\t\t\t\t\tif (alpha > 0.0 && alpha <= 1.0) {\n\t\t\t\t\t\t\tvec4 color_;\n\n\t\t\t\t\t\t\tif (alpha <= u_offset[0]) {\n\t\t\t\t\t\t\t\tcolor_ = u_colorArr[0];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tfor (int i = 1; i <= 20; ++i) {\n\t\t\t\t\t\t\t\t\tif (alpha <= u_offset[i]) {\n\t\t\t\t\t\t\t\t\t\tcolor_ = mix( u_colorArr[i - 1], u_colorArr[i], remap( u_offset[i - 1], u_offset[i], alpha ) );\n\t\t\t\t\t\t\t\t\t\tcolor_ = color_ * mix( u_colorArr[i - 1][3], u_colorArr[i][3], remap( u_offset[i - 1], u_offset[i], alpha ));\n\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcolor_ = color_ * u_opacity;\n\t\t\t\t\t\t\tif (color_.a < 0.0) {\n\t\t\t\t\t\t\t\tcolor_.a = 0.0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfragColor = color_;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfragColor = vec4(0.0, 0.0, 0.0, 0.0);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t"},r={vertex:"#version 300 es\n precision highp float;\n in vec2 a_position;\n in vec2 a_texCoord;\n uniform vec2 u_resolution;\n\t\t\t\t\tuniform vec2 u_translate; \n\t\t\t\t\tuniform float u_zoom; \n\t\t\t\t\tuniform float u_angle; \n\t\t\t\t\tuniform float u_density;\n out vec2 v_texCoord;\n\n vec2 rotation(vec2 v, float a, float aspect) {\n\t\t\t\t\t\tfloat s = sin(a); float c = cos(a); mat2 m = mat2(c, -s, s, c);\n\t\t\t\t\t\tmat2 scaleMat = mat2(aspect, 0.0, 0.0, 1.0);\n\t\t\t\t\t\tmat2 scaleMatInv = mat2(1.0/aspect, 0.0, 0.0, 1.0);\n\t\t\t\t\t\treturn scaleMatInv * m * scaleMat * v;\n\t\t\t\t\t}\n\n void main() {\n \tvec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n \tzeroToOne.y = 1.0 - zeroToOne.y;\n\t\t\t\t\t\tvec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n\t\t\t\t\t\tfloat zoomFactor = u_zoom;\n\t\t\t\t\t\tif (zoomFactor == 0.0) {\n\t\t\t\t\t\t\tzoomFactor = 0.1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tzeroToTwo = zeroToTwo / zoomFactor;\n\t\t\t\t\t\tif (u_angle != 0.0) {\n\t\t\t\t\t\t\tzeroToTwo = rotation(zeroToTwo, u_angle * -1.0, u_resolution.x / u_resolution.y);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tgl_Position = vec4(zeroToTwo , 0, 1);\n\t\t\t\t\t\tv_texCoord = a_texCoord;\n }\n \t\t",fragment:"#version 300 es\n precision mediump float;\n uniform sampler2D u_image;\n in vec2 v_texCoord;\n out vec4 fragColor;\n void main() {\n fragColor = texture(u_image, v_texCoord);\n }\n "};function i(t,e,r){const i=t.createShader(t[e]);if(!i)throw new Error("Failed to create shader.");t.shaderSource(i,r),t.compileShader(i);if(!t.getShaderParameter(i,t.COMPILE_STATUS)){const e=t.getShaderInfoLog(i);throw t.deleteShader(i),new Error("*** Error compiling shader '"+i+"':"+e)}return i}function n(t,e){const r=i(t,"VERTEX_SHADER",e.vertex),n=i(t,"FRAGMENT_SHADER",e.fragment),o=t.createProgram();if(!o)throw new Error("Failed to create program.");t.attachShader(o,r),t.attachShader(o,n),t.linkProgram(o);if(t.getProgramParameter(o,t.LINK_STATUS))return o;{const e=t.getProgramInfoLog(o);throw t.deleteProgram(o),new Error("Error in program linking:"+e)}}function o(t){return null==t}function a(t){return"number"!=typeof t}function s(t){if(!Array.isArray(t)||t.length<2)throw new Error("Invalid gradient: Expected an array with at least 2 elements.");if(!function(t){for(let e=0;e1||t<0)throw this.intensity=t>1?1:0,new Error("Invalid Intensity value "+t);return this.intensity=t,this}setOpacity(t){if(o(t)||a(t))throw new Error("Invalid Opacity: Expected Number");if(t>1||t<0)throw new Error("Invalid Opacity value "+t);return this.opacity=t,this}setBackgroundImage(t){const e=this;if(!t.url)return;const r=this.ctx.getParameter(this.ctx.MAX_TEXTURE_SIZE);return this.imageTexture=this.ctx.createTexture(),this.type="TEXTURE_2D",this.imageConfig=null,this.imgWidth=t.width||this.width,this.imgHeight=t.height||this.height,this.imgWidth=this.imgWidth>r?r:this.imgWidth,this.imgHeight=this.imgHeight>r?r:this.imgHeight,function(t,e,r){const i=new Image;i.crossOrigin="anonymous",i.onload=e,i.onerror=r,i.src=t}(t.url,(function(){e.ctx.activeTexture(e.ctx.TEXTURE0),e.ctx.bindTexture(e.ctx.TEXTURE_2D,e.imageTexture),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_WRAP_S,e.ctx.CLAMP_TO_EDGE),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_WRAP_T,e.ctx.CLAMP_TO_EDGE),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_MIN_FILTER,e.ctx.LINEAR),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_MAG_FILTER,e.ctx.LINEAR),e.ctx.texImage2D(e.ctx.TEXTURE_2D,0,e.ctx.RGBA,this.naturalWidth,this.naturalHeight,0,e.ctx.RGBA,e.ctx.UNSIGNED_BYTE,this),e.imageConfig={x:t.x||0,y:t.y||0,height:e.imgHeight,width:e.imgWidth,image:this},e.render()}),(function(t){throw new Error(`Image Load Error, ${t}`)})),this}clearData(){this.heatmapData=[],this.hearmapExData={},this.render()}addData(t,e){const r=this;for(let i=0;it[e].value&&(o.min=t[e].value),o.max 0.0 && alpha <= 1.0) {\n\t\t\t\t\t\t\tvec4 color_;\n\n\t\t\t\t\t\t\tif (alpha <= u_offset[0]) {\n\t\t\t\t\t\t\t\tcolor_ = u_colorArr[0];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tfor (int i = 1; i <= 20; ++i) {\n\t\t\t\t\t\t\t\t\tif (alpha <= u_offset[i]) {\n\t\t\t\t\t\t\t\t\t\tcolor_ = mix( u_colorArr[i - 1], u_colorArr[i], remap( u_offset[i - 1], u_offset[i], alpha ) );\n\t\t\t\t\t\t\t\t\t\tcolor_ = color_ * mix( u_colorArr[i - 1][3], u_colorArr[i][3], remap( u_offset[i - 1], u_offset[i], alpha ));\n\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcolor_ = color_ * u_opacity;\n\t\t\t\t\t\t\tif (color_.a < 0.0) {\n\t\t\t\t\t\t\t\tcolor_.a = 0.0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfragColor = color_;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfragColor = vec4(0.0, 0.0, 0.0, 0.0);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t"},i={vertex:"#version 300 es\n precision highp float;\n in vec2 a_position;\n in vec2 a_texCoord;\n uniform vec2 u_resolution;\n\t\t\t\t\tuniform vec2 u_translate; \n\t\t\t\t\tuniform float u_zoom; \n\t\t\t\t\tuniform float u_angle; \n\t\t\t\t\tuniform float u_density;\n out vec2 v_texCoord;\n\n vec2 rotation(vec2 v, float a, float aspect) {\n\t\t\t\t\t\tfloat s = sin(a); float c = cos(a); mat2 m = mat2(c, -s, s, c);\n\t\t\t\t\t\tmat2 scaleMat = mat2(aspect, 0.0, 0.0, 1.0);\n\t\t\t\t\t\tmat2 scaleMatInv = mat2(1.0/aspect, 0.0, 0.0, 1.0);\n\t\t\t\t\t\treturn scaleMatInv * m * scaleMat * v;\n\t\t\t\t\t}\n\n void main() {\n \tvec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n \tzeroToOne.y = 1.0 - zeroToOne.y;\n\t\t\t\t\t\tvec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n\t\t\t\t\t\tfloat zoomFactor = u_zoom;\n\t\t\t\t\t\tif (zoomFactor == 0.0) {\n\t\t\t\t\t\t\tzoomFactor = 0.1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tzeroToTwo = zeroToTwo / zoomFactor;\n\t\t\t\t\t\tif (u_angle != 0.0) {\n\t\t\t\t\t\t\tzeroToTwo = rotation(zeroToTwo, u_angle * -1.0, u_resolution.x / u_resolution.y);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tgl_Position = vec4(zeroToTwo , 0, 1);\n\t\t\t\t\t\tv_texCoord = a_texCoord;\n }\n \t\t",fragment:"#version 300 es\n precision mediump float;\n uniform sampler2D u_image;\n in vec2 v_texCoord;\n out vec4 fragColor;\n void main() {\n fragColor = texture(u_image, v_texCoord);\n }\n "};function r(t,e,i){const r=t.createShader(t[e]);if(!r)throw new Error("Failed to create shader.");t.shaderSource(r,i),t.compileShader(r);if(!t.getShaderParameter(r,t.COMPILE_STATUS)){const e=t.getShaderInfoLog(r);throw t.deleteShader(r),new Error("*** Error compiling shader '"+r+"':"+e)}return r}function o(t,e){const i=r(t,"VERTEX_SHADER",e.vertex),o=r(t,"FRAGMENT_SHADER",e.fragment),n=t.createProgram();if(!n)throw new Error("Failed to create program.");t.attachShader(n,i),t.attachShader(n,o),t.linkProgram(n);if(t.getProgramParameter(n,t.LINK_STATUS))return n;{const e=t.getProgramInfoLog(n);throw t.deleteProgram(n),new Error("Error in program linking:"+e)}}function n(t){return null==t}function a(t){return"number"!=typeof t}function s(t){if(!Array.isArray(t)||t.length<2)throw new Error("Invalid gradient: Expected an array with at least 2 elements.");if(!function(t){for(let e=0;et[e].value&&(n.min=t[e].value),n.max1||t<0)throw this.intensity=t>1?1:0,new Error("Invalid Intensity value "+t);return this.intensity=t,this}setOpacity(t){if(n(t)||a(t))throw new Error("Invalid Opacity: Expected Number");if(t>1||t<0)throw new Error("Invalid Opacity value "+t);return this.opacity=t,this}setBackgroundImage(t){const e=this;if(!t.url)return;const i=this.ctx.getParameter(this.ctx.MAX_TEXTURE_SIZE);return this.imageTexture=this.ctx.createTexture(),this.type="TEXTURE_2D",this.imageConfig=null,this.imgWidth=t.width||this.width,this.imgHeight=t.height||this.height,this.imgWidth=this.imgWidth>i?i:this.imgWidth,this.imgHeight=this.imgHeight>i?i:this.imgHeight,function(t,e,i){const r=new Image;r.crossOrigin="anonymous",r.onload=e,r.onerror=i,r.src=t}(t.url,(function(){e.ctx.activeTexture(e.ctx.TEXTURE0),e.ctx.bindTexture(e.ctx.TEXTURE_2D,e.imageTexture),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_WRAP_S,e.ctx.CLAMP_TO_EDGE),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_WRAP_T,e.ctx.CLAMP_TO_EDGE),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_MIN_FILTER,e.ctx.LINEAR),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_MAG_FILTER,e.ctx.LINEAR),e.ctx.texImage2D(e.ctx.TEXTURE_2D,0,e.ctx.RGBA,this.naturalWidth,this.naturalHeight,0,e.ctx.RGBA,e.ctx.UNSIGNED_BYTE,this),e.imageConfig={x:t.x||0,y:t.y||0,height:e.imgHeight,width:e.imgWidth,image:this},e.render()}),(function(t){throw new Error(`Image Load Error, ${t}`)})),this}clearData(){this.heatmapData=[],this.hearmapExData={},this.render()}addData(t,e){const i=this;for(let r=0;r 0.0 && alpha <= 1.0) {\n\t\t\t\t\t\t\tvec4 color_;\n\n\t\t\t\t\t\t\tif (alpha <= u_offset[0]) {\n\t\t\t\t\t\t\t\tcolor_ = u_colorArr[0];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tfor (int i = 1; i <= 20; ++i) {\n\t\t\t\t\t\t\t\t\tif (alpha <= u_offset[i]) {\n\t\t\t\t\t\t\t\t\t\tcolor_ = mix( u_colorArr[i - 1], u_colorArr[i], remap( u_offset[i - 1], u_offset[i], alpha ) );\n\t\t\t\t\t\t\t\t\t\tcolor_ = color_ * mix( u_colorArr[i - 1][3], u_colorArr[i][3], remap( u_offset[i - 1], u_offset[i], alpha ));\n\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcolor_ = color_ * u_opacity;\n\t\t\t\t\t\t\tif (color_.a < 0.0) {\n\t\t\t\t\t\t\t\tcolor_.a = 0.0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfragColor = color_;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfragColor = vec4(0.0, 0.0, 0.0, 0.0);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t"},r={vertex:"#version 300 es\n precision highp float;\n in vec2 a_position;\n in vec2 a_texCoord;\n uniform vec2 u_resolution;\n\t\t\t\t\tuniform vec2 u_translate; \n\t\t\t\t\tuniform float u_zoom; \n\t\t\t\t\tuniform float u_angle; \n\t\t\t\t\tuniform float u_density;\n out vec2 v_texCoord;\n\n vec2 rotation(vec2 v, float a, float aspect) {\n\t\t\t\t\t\tfloat s = sin(a); float c = cos(a); mat2 m = mat2(c, -s, s, c);\n\t\t\t\t\t\tmat2 scaleMat = mat2(aspect, 0.0, 0.0, 1.0);\n\t\t\t\t\t\tmat2 scaleMatInv = mat2(1.0/aspect, 0.0, 0.0, 1.0);\n\t\t\t\t\t\treturn scaleMatInv * m * scaleMat * v;\n\t\t\t\t\t}\n\n void main() {\n \tvec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n \tzeroToOne.y = 1.0 - zeroToOne.y;\n\t\t\t\t\t\tvec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n\t\t\t\t\t\tfloat zoomFactor = u_zoom;\n\t\t\t\t\t\tif (zoomFactor == 0.0) {\n\t\t\t\t\t\t\tzoomFactor = 0.1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tzeroToTwo = zeroToTwo / zoomFactor;\n\t\t\t\t\t\tif (u_angle != 0.0) {\n\t\t\t\t\t\t\tzeroToTwo = rotation(zeroToTwo, u_angle * -1.0, u_resolution.x / u_resolution.y);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tgl_Position = vec4(zeroToTwo , 0, 1);\n\t\t\t\t\t\tv_texCoord = a_texCoord;\n }\n \t\t",fragment:"#version 300 es\n precision mediump float;\n uniform sampler2D u_image;\n in vec2 v_texCoord;\n out vec4 fragColor;\n void main() {\n fragColor = texture(u_image, v_texCoord);\n }\n "};function i(t,e,r){const i=t.createShader(t[e]);if(!i)throw new Error("Failed to create shader.");t.shaderSource(i,r),t.compileShader(i);if(!t.getShaderParameter(i,t.COMPILE_STATUS)){const e=t.getShaderInfoLog(i);throw t.deleteShader(i),new Error("*** Error compiling shader '"+i+"':"+e)}return i}function n(t,e){const r=i(t,"VERTEX_SHADER",e.vertex),n=i(t,"FRAGMENT_SHADER",e.fragment),o=t.createProgram();if(!o)throw new Error("Failed to create program.");t.attachShader(o,r),t.attachShader(o,n),t.linkProgram(o);if(t.getProgramParameter(o,t.LINK_STATUS))return o;{const e=t.getProgramInfoLog(o);throw t.deleteProgram(o),new Error("Error in program linking:"+e)}}function o(t){return null==t}function a(t){return"number"!=typeof t}function s(t){if(!Array.isArray(t)||t.length<2)throw new Error("Invalid gradient: Expected an array with at least 2 elements.");if(!function(t){for(let e=0;e1||t<0)throw this.intensity=t>1?1:0,new Error("Invalid Intensity value "+t);return this.intensity=t,this}setOpacity(t){if(o(t)||a(t))throw new Error("Invalid Opacity: Expected Number");if(t>1||t<0)throw new Error("Invalid Opacity value "+t);return this.opacity=t,this}setBackgroundImage(t){const e=this;if(!t.url)return;const r=this.ctx.getParameter(this.ctx.MAX_TEXTURE_SIZE);return this.imageTexture=this.ctx.createTexture(),this.type="TEXTURE_2D",this.imageConfig=null,this.imgWidth=t.width||this.width,this.imgHeight=t.height||this.height,this.imgWidth=this.imgWidth>r?r:this.imgWidth,this.imgHeight=this.imgHeight>r?r:this.imgHeight,function(t,e,r){const i=new Image;i.crossOrigin="anonymous",i.onload=e,i.onerror=r,i.src=t}(t.url,(function(){e.ctx.activeTexture(e.ctx.TEXTURE0),e.ctx.bindTexture(e.ctx.TEXTURE_2D,e.imageTexture),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_WRAP_S,e.ctx.CLAMP_TO_EDGE),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_WRAP_T,e.ctx.CLAMP_TO_EDGE),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_MIN_FILTER,e.ctx.LINEAR),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_MAG_FILTER,e.ctx.LINEAR),e.ctx.texImage2D(e.ctx.TEXTURE_2D,0,e.ctx.RGBA,this.naturalWidth,this.naturalHeight,0,e.ctx.RGBA,e.ctx.UNSIGNED_BYTE,this),e.imageConfig={x:t.x||0,y:t.y||0,height:e.imgHeight,width:e.imgWidth,image:this},e.render()}),(function(t){throw new Error(`Image Load Error, ${t}`)})),this}clearData(){this.heatmapData=[],this.hearmapExData={},this.render()}addData(t,e){const r=this;for(let i=0;it[e].value&&(o.min=t[e].value),o.max 0.0 && alpha <= 1.0) {\n\t\t\t\t\t\t\tvec4 color_;\n\n\t\t\t\t\t\t\tif (alpha <= u_offset[0]) {\n\t\t\t\t\t\t\t\tcolor_ = u_colorArr[0];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tfor (int i = 1; i <= 20; ++i) {\n\t\t\t\t\t\t\t\t\tif (alpha <= u_offset[i]) {\n\t\t\t\t\t\t\t\t\t\tcolor_ = mix( u_colorArr[i - 1], u_colorArr[i], remap( u_offset[i - 1], u_offset[i], alpha ) );\n\t\t\t\t\t\t\t\t\t\tcolor_ = color_ * mix( u_colorArr[i - 1][3], u_colorArr[i][3], remap( u_offset[i - 1], u_offset[i], alpha ));\n\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcolor_ = color_ * u_opacity;\n\t\t\t\t\t\t\tif (color_.a < 0.0) {\n\t\t\t\t\t\t\t\tcolor_.a = 0.0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfragColor = color_;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfragColor = vec4(0.0, 0.0, 0.0, 0.0);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t"},i={vertex:"#version 300 es\n precision highp float;\n in vec2 a_position;\n in vec2 a_texCoord;\n uniform vec2 u_resolution;\n\t\t\t\t\tuniform vec2 u_translate; \n\t\t\t\t\tuniform float u_zoom; \n\t\t\t\t\tuniform float u_angle; \n\t\t\t\t\tuniform float u_density;\n out vec2 v_texCoord;\n\n vec2 rotation(vec2 v, float a, float aspect) {\n\t\t\t\t\t\tfloat s = sin(a); float c = cos(a); mat2 m = mat2(c, -s, s, c);\n\t\t\t\t\t\tmat2 scaleMat = mat2(aspect, 0.0, 0.0, 1.0);\n\t\t\t\t\t\tmat2 scaleMatInv = mat2(1.0/aspect, 0.0, 0.0, 1.0);\n\t\t\t\t\t\treturn scaleMatInv * m * scaleMat * v;\n\t\t\t\t\t}\n\n void main() {\n \tvec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n \tzeroToOne.y = 1.0 - zeroToOne.y;\n\t\t\t\t\t\tvec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n\t\t\t\t\t\tfloat zoomFactor = u_zoom;\n\t\t\t\t\t\tif (zoomFactor == 0.0) {\n\t\t\t\t\t\t\tzoomFactor = 0.1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tzeroToTwo = zeroToTwo / zoomFactor;\n\t\t\t\t\t\tif (u_angle != 0.0) {\n\t\t\t\t\t\t\tzeroToTwo = rotation(zeroToTwo, u_angle * -1.0, u_resolution.x / u_resolution.y);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tgl_Position = vec4(zeroToTwo , 0, 1);\n\t\t\t\t\t\tv_texCoord = a_texCoord;\n }\n \t\t",fragment:"#version 300 es\n precision mediump float;\n uniform sampler2D u_image;\n in vec2 v_texCoord;\n out vec4 fragColor;\n void main() {\n fragColor = texture(u_image, v_texCoord);\n }\n "};function r(t,e,i){const r=t.createShader(t[e]);if(!r)throw new Error("Failed to create shader.");t.shaderSource(r,i),t.compileShader(r);if(!t.getShaderParameter(r,t.COMPILE_STATUS)){const e=t.getShaderInfoLog(r);throw t.deleteShader(r),new Error("*** Error compiling shader '"+r+"':"+e)}return r}function o(t,e){const i=r(t,"VERTEX_SHADER",e.vertex),o=r(t,"FRAGMENT_SHADER",e.fragment),n=t.createProgram();if(!n)throw new Error("Failed to create program.");t.attachShader(n,i),t.attachShader(n,o),t.linkProgram(n);if(t.getProgramParameter(n,t.LINK_STATUS))return n;{const e=t.getProgramInfoLog(n);throw t.deleteProgram(n),new Error("Error in program linking:"+e)}}function n(t){return null==t}function a(t){return"number"!=typeof t}function s(t){if(!Array.isArray(t)||t.length<2)throw new Error("Invalid gradient: Expected an array with at least 2 elements.");if(!function(t){for(let e=0;et[e].value&&(n.min=t[e].value),n.max1||t<0)throw this.intensity=t>1?1:0,new Error("Invalid Intensity value "+t);return this.intensity=t,this}setOpacity(t){if(n(t)||a(t))throw new Error("Invalid Opacity: Expected Number");if(t>1||t<0)throw new Error("Invalid Opacity value "+t);return this.opacity=t,this}setBackgroundImage(t){const e=this;if(!t.url)return;const i=this.ctx.getParameter(this.ctx.MAX_TEXTURE_SIZE);return this.imageTexture=this.ctx.createTexture(),this.type="TEXTURE_2D",this.imageConfig=null,this.imgWidth=t.width||this.width,this.imgHeight=t.height||this.height,this.imgWidth=this.imgWidth>i?i:this.imgWidth,this.imgHeight=this.imgHeight>i?i:this.imgHeight,function(t,e,i){const r=new Image;r.crossOrigin="anonymous",r.onload=e,r.onerror=i,r.src=t}(t.url,(function(){e.ctx.activeTexture(e.ctx.TEXTURE0),e.ctx.bindTexture(e.ctx.TEXTURE_2D,e.imageTexture),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_WRAP_S,e.ctx.CLAMP_TO_EDGE),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_WRAP_T,e.ctx.CLAMP_TO_EDGE),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_MIN_FILTER,e.ctx.LINEAR),e.ctx.texParameteri(e.ctx.TEXTURE_2D,e.ctx.TEXTURE_MAG_FILTER,e.ctx.LINEAR),e.ctx.texImage2D(e.ctx.TEXTURE_2D,0,e.ctx.RGBA,this.naturalWidth,this.naturalHeight,0,e.ctx.RGBA,e.ctx.UNSIGNED_BYTE,this),e.imageConfig={x:t.x||0,y:t.y||0,height:e.imgHeight,width:e.imgWidth,image:this},e.render()}),(function(t){throw new Error(`Image Load Error, ${t}`)})),this}clearData(){this.heatmapData=[],this.hearmapExData={},this.render()}addData(t,e){const i=this;for(let r=0;r data[i].value) { + dataMinMaxValue.min = data[i].value; + } + if (dataMinMaxValue.max < data[i].value) { + dataMinMaxValue.max = data[i].value; + } + } + + return { + posVec: posVec as Float32Array, + rVec: rVec as Float32Array, + minMax: dataMinMaxValue, + }; +} + +function transCoOr(this: HeatmapRenderer, data: Point) { + const zoomFactor = this.zoom || 0.1; + const halfWidth = this.width / 2; + const halfHeight = this.height / 2; + const { angle, translate } = this; + + // Combine operations to reduce the number of arithmetic steps + let posX = ((data.x - halfWidth) / halfWidth) * zoomFactor; + let posY = ((data.y - halfHeight) / halfHeight) * zoomFactor; + + // Rotate the point if there's an angle + if (angle !== 0.0) { + const cosAngle = Math.cos(angle); + const sinAngle = Math.sin(angle); + posY = sinAngle * posX + cosAngle * posY; + posX = cosAngle * posX - sinAngle * posY; + } + + // Scale back and adjust the position + posX = posX * halfWidth + halfWidth - translate[0]; + posY = posY * halfHeight + halfHeight - translate[1]; + + data.x = posX; + data.y = posY; + + return { x: posX, y: posY }; +} + +function renderExec(this: HeatmapRenderer) { + const ctx = this.ctx; + + if (!ctx) { + return; + } + + ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT); + + ctx.bindTexture(ctx.TEXTURE_2D, this.fbTexObj); + ctx.texImage2D( + ctx.TEXTURE_2D, + 0, + ctx.RGBA, + this.width * this.ratio, + this.height * this.ratio, + 0, + ctx.RGBA, + ctx.UNSIGNED_BYTE, + null + ); + ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_S, ctx.CLAMP_TO_EDGE); + ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_T, ctx.CLAMP_TO_EDGE); + ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MIN_FILTER, ctx.LINEAR); + + ctx.bindFramebuffer(ctx.FRAMEBUFFER, this.fbo); + ctx.framebufferTexture2D( + ctx.FRAMEBUFFER, + ctx.COLOR_ATTACHMENT0, + ctx.TEXTURE_2D, + this.fbTexObj, + 0 + ); + + if (this.hearmapExData) { + renderHeatGrad.call(this, ctx, this.hearmapExData as HearmapExData); + } + ctx.bindFramebuffer(ctx.FRAMEBUFFER, null); + if (this.imageConfig) { + renderImage.call(this, ctx, this.imageConfig); + } + renderColorGradiant.call(this, ctx); +} + +function renderHeatGrad( + this: HeatmapRenderer, + ctx: WebGL2RenderingContext, + exData: HearmapExData +) { + ctx.useProgram(this.gradShadOP.program); + + this.min = + this.configMin !== null ? this.configMin : exData?.minMax?.min ?? 0; + this.max = + this.configMax !== null ? this.configMax : exData?.minMax?.max ?? 0; + this.gradShadOP.attr[0].data = exData.posVec || []; + this.gradShadOP.attr[1].data = exData.rVec || []; + + ctx.uniform2fv( + this.gradShadOP.uniform.u_resolution, + new Float32Array([this.width * this.ratio, this.height * this.ratio]) + ); + ctx.uniform2fv( + this.gradShadOP.uniform.u_translate, + new Float32Array([this.translate[0], this.translate[1]]) + ); + ctx.uniform1f(this.gradShadOP.uniform.u_zoom, this.zoom ? this.zoom : 0.01); + ctx.uniform1f(this.gradShadOP.uniform.u_angle, this.angle); + ctx.uniform1f(this.gradShadOP.uniform.u_density, this.ratio); + ctx.uniform1f(this.gradShadOP.uniform.u_max, this.max); + ctx.uniform1f(this.gradShadOP.uniform.u_min, this.min); + ctx.uniform1f(this.gradShadOP.uniform.u_size, this.size); + ctx.uniform1f(this.gradShadOP.uniform.u_intensity, this.intensity); + + this.gradShadOP.attr.forEach(function (d) { + ctx.bindBuffer(d.bufferType, d.buffer); + ctx.bufferData(d.bufferType, d.data, d.drawType); + ctx.enableVertexAttribArray(d.attribute); + ctx.vertexAttribPointer(d.attribute, d.size, d.valueType, true, 0, 0); + }); + + ctx.drawArrays(ctx.POINTS, 0, (exData.posVec || []).length / 2); +} + +function renderImage( + this: HeatmapRenderer, + ctx: WebGL2RenderingContext, + imageConfig: BackgroundImageConfig +) { + const { x = 0, y = 0, width = 0, height = 0 } = imageConfig; + + ctx.useProgram(this.imageShaOP.program); + + ctx.uniform2fv( + this.imageShaOP.uniform.u_resolution, + new Float32Array([this.width * this.ratio, this.height * this.ratio]) + ); + ctx.uniform2fv( + this.imageShaOP.uniform.u_translate, + new Float32Array([this.translate[0], this.translate[1]]) + ); + ctx.uniform1f(this.imageShaOP.uniform.u_zoom, this.zoom ? this.zoom : 0.01); + ctx.uniform1f(this.imageShaOP.uniform.u_angle, this.angle); + ctx.uniform1f(this.imageShaOP.uniform.u_density, this.ratio); + + this.imageShaOP.attr[0].data = new Float32Array([ + x, + y, + x + width, + y, + x, + y + height, + x, + y + height, + x + width, + y, + x + width, + y + height, + ]); + + this.imageShaOP.attr.forEach(function (d) { + ctx.bindBuffer(d.bufferType, d.buffer); + ctx.bufferData(d.bufferType, d.data, d.drawType); + ctx.enableVertexAttribArray(d.attribute); + ctx.vertexAttribPointer(d.attribute, d.size, d.valueType, true, 0, 0); + }); + + ctx.uniform1i(this.imageShaOP.uniform.u_image, 0); + ctx.activeTexture(this.ctx!.TEXTURE0); + ctx.bindTexture(this.ctx!.TEXTURE_2D, this.imageTexture); + ctx.drawArrays(ctx.TRIANGLES, 0, 6); +} + +function renderColorGradiant( + this: HeatmapRenderer, + ctx: WebGL2RenderingContext +) { + ctx.useProgram(this.colorShadOP.program); + + ctx.uniform4fv(this.colorShadOP.uniform.u_colorArr, this.gradient!.value); + ctx.uniform1f(this.colorShadOP.uniform.u_colorCount, this.gradient!.length); + ctx.uniform1fv( + this.colorShadOP.uniform.u_offset, + new Float32Array(this.gradient!.offset) + ); + ctx.uniform1f(this.colorShadOP.uniform.u_opacity, this.opacity); + + this.colorShadOP.attr.forEach(function (d) { + ctx.bindBuffer(d.bufferType, d.buffer); + ctx.bufferData(d.bufferType, d.data, d.drawType); + ctx.enableVertexAttribArray(d.attribute); + ctx.vertexAttribPointer(d.attribute, d.size, d.valueType, true, 0, 0); + }); + + ctx.uniform1i(this.colorShadOP.uniform.u_framebuffer, 0); + ctx.activeTexture(ctx.TEXTURE0); + ctx.bindTexture(ctx.TEXTURE_2D, this.fbTexObj); + + ctx.drawArrays(ctx.TRIANGLES, 0, 6); +} + +function imageInstance( + url: string, + onLoad: () => void, + onError: OnErrorEventHandler +) { + const imageIns = new Image(); + imageIns.crossOrigin = "anonymous"; + imageIns.onload = onLoad; + imageIns.onerror = onError; + imageIns.src = url; + return imageIns; +} + +export class HeatmapRenderer { + ctx: WebGL2RenderingContext | null = null; + ratio: number = 1; + width: number = 0; + height: number = 0; + imageConfig: BackgroundImageConfig | null = null; + configMin: number | null = null; + configMax: number | null = null; + min: number = 0; + max: number = 0; + hearmapExData: HearmapExData | object = {}; + + gradShadOP!: ShaderProgram; + colorShadOP!: ShaderProgram; + imageShaOP!: ShaderProgram; + fbTexObj!: WebGLTexture; + fbo!: WebGLFramebuffer; + size: number = 0; + zoom: number = 0; + angle: number = 0; + intensity: number = 0; + translate: [number, number] = [0, 0]; + opacity: number = 0; + gradient: MappedGradient | null = null; + imageTexture: WebGLTexture | null = null; + pLen: number | undefined = undefined; + buffer: ArrayBuffer | undefined = undefined; + buffer2: ArrayBuffer | undefined = undefined; + + private layer!: HTMLCanvasElement; + private dom!: Element; + private imgWidth: number = 0; + private imgHeight: number = 0; + private heatmapData: Point[] = []; + private type: string = ""; + + constructor(container: string | HTMLElement, config: HeatmapConfig) { + try { + const res = + typeof container === "string" + ? document.querySelector(container) + : container instanceof HTMLElement + ? container + : null; + if (!res) { + throw new Error("Context must be either a string or an Element"); + } + const { clientHeight: height, clientWidth: width } = res; + const layer = document.createElement("canvas"); + const ctx = layer.getContext("webgl2", { + premultipliedAlpha: false, + depth: false, + antialias: true, + alpha: true, + preserveDrawingBuffer: false, + }) as WebGL2RenderingContext; + + this.ratio = getPixelRatio(ctx); + ctx.clearColor(0, 0, 0, 0); + ctx.enable(ctx.BLEND); + ctx.blendEquation(ctx.FUNC_ADD); + ctx.blendFunc(ctx.ONE, ctx.ONE_MINUS_SRC_ALPHA); + ctx.depthMask(true); + layer.setAttribute("height", (height * this.ratio).toString()); + layer.setAttribute("width", (width * this.ratio).toString()); + layer.style.height = `${height}px`; + layer.style.width = `${width}px`; + layer.style.position = "absolute"; + res.appendChild(layer); + + this.ctx = ctx; + this.width = width; + this.height = height; + this.imageConfig = null; + this.configMin = null; + this.configMax = null; + this.hearmapExData = {}; + this.layer = layer; + this.dom = res; + this.gradShadOP = createGradiantShader(this.ctx, GradShader); + this.colorShadOP = createColorShader(this.ctx, ColorShader); + this.imageShaOP = createImageShader(this.ctx, ImageShader); + this.fbTexObj = ctx.createTexture()!; + this.fbo = ctx.createFramebuffer()!; + + if (!isNullUndefined(config.size)) { + this.setSize(config.size); + } else { + this.size = 20.0; + } + + if (!isNullUndefined(config.max)) { + this.setMax(config.max); + } else { + this.configMax = null; + } + + if (!isNullUndefined(config.min)) { + this.setMin(config.min); + } else { + this.configMin = null; + } + + if (!isNullUndefined(config.intensity)) { + this.setIntensity(config.intensity); + } else { + this.intensity = 1.0; + } + + if (!isNullUndefined(config.translate)) { + this.setTranslate(config.translate); + } else { + this.translate = [0, 0]; + } + + if (!isNullUndefined(config.zoom)) { + this.setZoom(config.zoom); + } else { + this.zoom = 1.0; + } + + if (!isNullUndefined(config.angle)) { + this.setRotationAngle(config.angle); + } else { + this.angle = 0.0; + } + + if (!isNullUndefined(config.opacity)) { + this.setOpacity(config.opacity); + } else { + this.opacity = 1.0; + } + + this.gradient = gradientMapper(config.gradient); + + if (config.backgroundImage && config.backgroundImage.url) { + this.setBackgroundImage(config.backgroundImage); + } + + this.heatmapData = []; + + this.ctx.viewport(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); + } catch (error) { + console.error(error); + } + } + + resize() { + const height = this.dom.clientHeight; + const width = this.dom.clientWidth; + this.layer.setAttribute("height", (height * this.ratio).toString()); + this.layer.setAttribute("width", (width * this.ratio).toString()); + this.layer.style.height = `${height}px`; + this.layer.style.width = `${width}px`; + this.width = width; + this.height = height; + this.ctx!.viewport(0, 0, this.ctx!.canvas.width, this.ctx!.canvas.height); + /* Perform update */ + this.render(); + } + + clear() { + this.ctx!.clear(this.ctx!.COLOR_BUFFER_BIT | this.ctx!.DEPTH_BUFFER_BIT); + } + + setMax(max: number): HeatmapRenderer { + if (isNullUndefined(max) || isNotNumber(max)) { + throw new Error("Invalid max: Expected Number"); + } + + this.configMax = max; + return this; + } + + setMin(min: number): HeatmapRenderer { + if (isNullUndefined(min) || isNotNumber(min)) { + throw new Error("Invalid min: Expected Number"); + } + + this.configMin = min; + return this; + } + + setGradient(gradient: GradientElement[]): HeatmapRenderer { + this.gradient = gradientMapper(gradient); + return this; + } -export class Heatmap { - private renderer: HeatmapRenderer; - ctx; - ratio; - width; - height; - min; - max; - size; - zoom; - angle; - intensity; - translate; - opacity; - gradient; - imageConfig; - - constructor(context: string | HTMLElement, config: HeatmapConfig) { - this.renderer = new HeatmapRenderer(context, config); - this.ctx = this.renderer.ctx; - this.ratio = this.renderer.ratio; - this.width = this.renderer.width; - this.height = this.renderer.height; - this.min = this.renderer.min; - this.max = this.renderer.max; - this.size = this.renderer.size; - this.zoom = this.renderer.zoom; - this.angle = this.renderer.angle; - this.intensity = this.renderer.intensity; - this.translate = this.renderer.translate; - this.opacity = this.renderer.opacity; - this.gradient = this.renderer.gradient; - this.imageConfig = this.renderer.imageConfig; - } - - /** - * Set the maximum data value for relative gradient calculations - * @param max - number - * @returns instance - */ - setMax(max: number) { - return this.renderer.setMax(max); - } - - /** - * Set the minimum data value for relative gradient calculations - * @param min - number - * @returns instance - */ - setMin(min: number) { - return this.renderer.setMin(min); - } - - /** - * Accepts array of objects with color value and offset - * @param gradient - Color Gradient - * @returns instance - */ - setGradient(gradient: GradientElement[]) { - return this.renderer.setGradient(gradient); - } - - /** - * Set the translate transformation on the canvas - * @param translate - Accepts array [x, y] - * @returns instance - */ setTranslate(translate: Translate) { - return this.renderer.setTranslate(translate); - } - - /** - * Set the zoom transformation on the canvas - * @param zoom - Accepts float value - * @returns instance - */ - setZoom(zoom: number) { - return this.renderer.setZoom(zoom); - } - - /** - * Set the rotation transformation on the canvas - * @param angle - Accepts angle in radians - * @returns instance - */ - setRotationAngle(angle: number) { - return this.renderer.setRotationAngle(angle); - } - - /** - * Set the point radius - * @param size - Accepts float value - * @returns instance - */ - setSize(size: number) { - return this.renderer.setSize(size); - } - - /** - * Set the intensity factor - * @param intensity - Accepts float value - * @returns instance - */ - setIntensity(intensity: number) { - return this.renderer.setIntensity(intensity); - } - - /** - * Set the opacity factor - * @param opacity - The opacity factor. - * @returns instance - */ - setOpacity(opacity: number) { - return this.renderer.setOpacity(opacity); - } - - /** - * Set the background image - * @param config - Accepts Object with { Url, height, width, x, and y} properties - * @returns instance - */ + if (translate.constructor !== Array) { + throw new Error("Invalid Translate: Translate has to be of Array type"); + } + if (translate.length !== 2) { + throw new Error("Translate has to be of length 2"); + } + this.translate = translate; + return this; + } + + setZoom(zoom: number): HeatmapRenderer { + if (isNullUndefined(zoom) || isNotNumber(zoom)) { + throw new Error("Invalid zoom: Expected Number"); + } + + this.zoom = zoom; + return this; + } + + setRotationAngle(angle: number): HeatmapRenderer { + if (isNullUndefined(angle) || isNotNumber(angle)) { + throw new Error("Invalid Angle: Expected Number"); + } + + this.angle = angle; + return this; + } + + setSize(size: number): HeatmapRenderer { + if (isNullUndefined(size) || isNotNumber(size)) { + throw new Error("Invalid Size: Expected Number"); + } + + this.size = size; + return this; + } + + setIntensity(intensity: number): HeatmapRenderer { + if (isNullUndefined(intensity) || isNotNumber(intensity)) { + this.intensity = 1.0; // applying default intensity + throw new Error("Invalid Intensity: Expected Number"); + } + + if (intensity > 1 || intensity < 0) { + this.intensity = intensity > 1 ? 1 : 0; // Setting bound value + throw new Error("Invalid Intensity value " + intensity); + } + this.intensity = intensity; + return this; + } + + setOpacity(opacity: number): HeatmapRenderer { + if (isNullUndefined(opacity) || isNotNumber(opacity)) { + throw new Error("Invalid Opacity: Expected Number"); + } + + if (opacity > 1 || opacity < 0) { + throw new Error("Invalid Opacity value " + opacity); + } + this.opacity = opacity; + return this; + } + setBackgroundImage(config: BackgroundImageConfig) { - return this.renderer.setBackgroundImage(config); + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + if (!config.url) { + return; + } + + const maxTextureSize = this.ctx!.getParameter(this.ctx!.MAX_TEXTURE_SIZE); + this.imageTexture = this.ctx!.createTexture(); + this.type = "TEXTURE_2D"; + this.imageConfig = null; + + this.imgWidth = config.width || this.width; + this.imgHeight = config.height || this.height; + + this.imgWidth = + this.imgWidth > maxTextureSize ? maxTextureSize : this.imgWidth; + this.imgHeight = + this.imgHeight > maxTextureSize ? maxTextureSize : this.imgHeight; + + imageInstance( + config.url, + function onUpdateCallBack(this: HTMLImageElement) { + self.ctx!.activeTexture(self.ctx!.TEXTURE0); + self.ctx!.bindTexture(self.ctx!.TEXTURE_2D, self.imageTexture); + self.ctx!.texParameteri( + self.ctx!.TEXTURE_2D, + self.ctx!.TEXTURE_WRAP_S, + self.ctx!.CLAMP_TO_EDGE + ); + self.ctx!.texParameteri( + self.ctx!.TEXTURE_2D, + self.ctx!.TEXTURE_WRAP_T, + self.ctx!.CLAMP_TO_EDGE + ); + self.ctx!.texParameteri( + self.ctx!.TEXTURE_2D, + self.ctx!.TEXTURE_MIN_FILTER, + self.ctx!.LINEAR + ); + self.ctx!.texParameteri( + self.ctx!.TEXTURE_2D, + self.ctx!.TEXTURE_MAG_FILTER, + self.ctx!.LINEAR + ); + + self.ctx!.texImage2D( + self.ctx!.TEXTURE_2D, + 0, + self.ctx!.RGBA, + this.naturalWidth, + this.naturalHeight, + 0, + self.ctx!.RGBA, + self.ctx!.UNSIGNED_BYTE, + this + ); + + self.imageConfig = { + x: config.x || 0, + y: config.y || 0, + height: self.imgHeight, + width: self.imgWidth, + image: this, + }; + + self.render(); + }, + function onErrorCallBack(error) { + throw new Error(`Image Load Error, ${error}`); + } + ); + return this; } - /** - * After adding data points, need to invoke .render() method to update the heatmap - * @param data - The data points with 'x', 'y' and 'value' - * @param transIntactFlag - Flag indicating whether to apply existing heatmap transformations on the newly added data points - * @returns instance - */ - addData(data: Point[], transIntactFlag: boolean) { - return this.renderer.addData(data, transIntactFlag); + clearData() { + this.heatmapData = []; + this.hearmapExData = {}; + this.render(); } - /** - * @param data - Accepts an array of data points with 'x', 'y' and 'value' - * @returns instance - */ - renderData(data: Point[]) { - return this.renderer.renderData(data); + addData(data: Point[], transIntactFlag: boolean): HeatmapRenderer { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + for (let i = 0; i < data.length; i++) { + if (transIntactFlag) { + transCoOr.call(self, data[i]); + } + this.heatmapData.push(data[i]); + } + this.renderData(this.heatmapData); + return this; } - /** - * Method to re-render the heatmap. This method needs to be invoked as and when configurations get changed - */ - render() { - this.renderer.render(); + renderData(data: Point[]): HeatmapRenderer { + if (data.constructor !== Array) { + throw new Error("Expected Array type"); + } + this.hearmapExData = extractData.call(this, data); + this.heatmapData = data; + this.render(); + return this; } - /** - * Get projected co-ordinates relative to the heatmap layer - * @param data - The data point to project. - * @returns projected data point. - */ + render() { + renderExec.call(this); + } projection(data: Point) { - return this.renderer.projection(data); - } + // Pre-compute constants and repetitive calculations + const zoomFactor = this.zoom || 0.1; + const halfWidth = this.width / 2; + const halfHeight = this.height / 2; + const translateX = this.translate[0]; + const translateY = this.translate[1]; + const angle = this.angle; + const aspect = this.width / this.height; - /** - * Clears canvas - */ - clear() { - this.renderer.clear(); + // Calculate the adjusted positions + let posX = (data.x + translateX - halfWidth) / (halfWidth * zoomFactor); + let posY = (data.y + translateY - halfHeight) / (halfHeight * zoomFactor); + + posX *= aspect; + + // Rotate the point if there's an angle + if (angle !== 0.0) { + const cosAngle = Math.cos(-angle); + const sinAngle = Math.sin(-angle); + const xNew = cosAngle * posX - sinAngle * posY; + posY = sinAngle * posX + cosAngle * posY; + posX = xNew; + } + + posX *= 1.0 / aspect; + + // Scale back and adjust the position + posX = posX * halfWidth + halfWidth; + posY = posY * halfHeight + halfHeight; + + return { x: posX, y: posY }; } } diff --git a/src/heatmapRenderer.ts b/src/heatmapRenderer.ts deleted file mode 100644 index 6bbaa78..0000000 --- a/src/heatmapRenderer.ts +++ /dev/null @@ -1,686 +0,0 @@ -import { GradShader, ColorShader, ImageShader } from "./shaders"; -import { - BackgroundImageConfig, - GradientElement, - HearmapExData, - HeatmapConfig, - MappedGradient, - Point, - ShaderProgram, - Translate, -} from "./types"; -import { - createImageShader, - createGradiantShader, - createColorShader, -} from "./utils/shaderUtils"; -import { - isNullUndefined, - isNotNumber, - isSortedAscending, - getPixelRatio, -} from "./utils/utils"; - -function gradientMapper(grad: GradientElement[]): MappedGradient { - if (!Array.isArray(grad) || grad.length < 2) { - throw new Error( - "Invalid gradient: Expected an array with at least 2 elements." - ); - } - - if (!isSortedAscending(grad)) { - throw new Error("Invalid gradient: Gradient is not sorted"); - } - - const gradLength = grad.length; - const values = new Float32Array(gradLength * 4); - const offsets = new Array(gradLength); - - grad.forEach(function (d, i) { - const baseIndex = i * 4; - values[baseIndex] = d.color[0] / 255; - values[baseIndex + 1] = d.color[1] / 255; - values[baseIndex + 2] = d.color[2] / 255; - values[baseIndex + 3] = d.color[3] !== undefined ? d.color[3] : 1.0; - offsets[i] = d.offset; - }); - - return { - value: values, - length: gradLength, - offset: offsets, - }; -} - -function extractData(data: Point[], self: HeatmapRenderer): HearmapExData { - const len = data.length; - let { posVec = new Float32Array(), rVec = new Float32Array() } = - (self.hearmapExData || {}) as HearmapExData; - - if (self.pLen !== len) { - self.buffer = new ArrayBuffer(len * 8); - posVec = new Float32Array(self.buffer); - self.buffer2 = new ArrayBuffer(len * 4); - rVec = new Float32Array(self.buffer2); - self.pLen = len; - } - - const dataMinMaxValue = { - min: Infinity, - max: -Infinity, - }; - for (let i = 0; i < len; i++) { - posVec[i * 2] = data[i].x; - posVec[i * 2 + 1] = data[i].y; - rVec[i] = data[i].value; - if (dataMinMaxValue.min > data[i].value) { - dataMinMaxValue.min = data[i].value; - } - if (dataMinMaxValue.max < data[i].value) { - dataMinMaxValue.max = data[i].value; - } - } - - return { - posVec: posVec as Float32Array, - rVec: rVec as Float32Array, - minMax: dataMinMaxValue, - }; -} - -function transCoOr(this: HeatmapRenderer, data: Point) { - const zoomFactor = this.zoom || 0.1; - const halfWidth = this.width / 2; - const halfHeight = this.height / 2; - const { angle, translate } = this; - - // Combine operations to reduce the number of arithmetic steps - let posX = ((data.x - halfWidth) / halfWidth) * zoomFactor; - let posY = ((data.y - halfHeight) / halfHeight) * zoomFactor; - - // Rotate the point if there's an angle - if (angle !== 0.0) { - const cosAngle = Math.cos(angle); - const sinAngle = Math.sin(angle); - posY = sinAngle * posX + cosAngle * posY; - posX = cosAngle * posX - sinAngle * posY; - } - - // Scale back and adjust the position - posX = posX * halfWidth + halfWidth - translate[0]; - posY = posY * halfHeight + halfHeight - translate[1]; - - data.x = posX; - data.y = posY; - - return { x: posX, y: posY }; -} - -function renderExec(this: HeatmapRenderer) { - const ctx = this.ctx; - - if (!ctx) { - return; - } - - ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT); - - ctx.bindTexture(ctx.TEXTURE_2D, this.fbTexObj); - ctx.texImage2D( - ctx.TEXTURE_2D, - 0, - ctx.RGBA, - this.width * this.ratio, - this.height * this.ratio, - 0, - ctx.RGBA, - ctx.UNSIGNED_BYTE, - null - ); - ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_S, ctx.CLAMP_TO_EDGE); - ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_T, ctx.CLAMP_TO_EDGE); - ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MIN_FILTER, ctx.LINEAR); - - ctx.bindFramebuffer(ctx.FRAMEBUFFER, this.fbo); - ctx.framebufferTexture2D( - ctx.FRAMEBUFFER, - ctx.COLOR_ATTACHMENT0, - ctx.TEXTURE_2D, - this.fbTexObj, - 0 - ); - - if (this.hearmapExData) { - renderHeatGrad.call(this, ctx, this.hearmapExData as HearmapExData); - } - ctx.bindFramebuffer(ctx.FRAMEBUFFER, null); - if (this.imageConfig) { - renderImage.call(this, ctx, this.imageConfig); - } - renderColorGradiant.call(this, ctx); -} - -function renderHeatGrad( - this: HeatmapRenderer, - ctx: WebGL2RenderingContext, - exData: HearmapExData -) { - ctx.useProgram(this.gradShadOP.program); - - this.min = - this.configMin !== null ? this.configMin : exData?.minMax?.min ?? 0; - this.max = - this.configMax !== null ? this.configMax : exData?.minMax?.max ?? 0; - this.gradShadOP.attr[0].data = exData.posVec || []; - this.gradShadOP.attr[1].data = exData.rVec || []; - - ctx.uniform2fv( - this.gradShadOP.uniform.u_resolution, - new Float32Array([this.width * this.ratio, this.height * this.ratio]) - ); - ctx.uniform2fv( - this.gradShadOP.uniform.u_translate, - new Float32Array([this.translate[0], this.translate[1]]) - ); - ctx.uniform1f(this.gradShadOP.uniform.u_zoom, this.zoom ? this.zoom : 0.01); - ctx.uniform1f(this.gradShadOP.uniform.u_angle, this.angle); - ctx.uniform1f(this.gradShadOP.uniform.u_density, this.ratio); - ctx.uniform1f(this.gradShadOP.uniform.u_max, this.max); - ctx.uniform1f(this.gradShadOP.uniform.u_min, this.min); - ctx.uniform1f(this.gradShadOP.uniform.u_size, this.size); - ctx.uniform1f(this.gradShadOP.uniform.u_intensity, this.intensity); - - this.gradShadOP.attr.forEach(function (d) { - ctx.bindBuffer(d.bufferType, d.buffer); - ctx.bufferData(d.bufferType, d.data, d.drawType); - ctx.enableVertexAttribArray(d.attribute); - ctx.vertexAttribPointer(d.attribute, d.size, d.valueType, true, 0, 0); - }); - - ctx.drawArrays(ctx.POINTS, 0, (exData.posVec || []).length / 2); -} - -function renderImage( - this: HeatmapRenderer, - ctx: WebGL2RenderingContext, - imageConfig: BackgroundImageConfig -) { - const { x = 0, y = 0, width = 0, height = 0 } = imageConfig; - - ctx.useProgram(this.imageShaOP.program); - - ctx.uniform2fv( - this.imageShaOP.uniform.u_resolution, - new Float32Array([this.width * this.ratio, this.height * this.ratio]) - ); - ctx.uniform2fv( - this.imageShaOP.uniform.u_translate, - new Float32Array([this.translate[0], this.translate[1]]) - ); - ctx.uniform1f(this.imageShaOP.uniform.u_zoom, this.zoom ? this.zoom : 0.01); - ctx.uniform1f(this.imageShaOP.uniform.u_angle, this.angle); - ctx.uniform1f(this.imageShaOP.uniform.u_density, this.ratio); - - this.imageShaOP.attr[0].data = new Float32Array([ - x, - y, - x + width, - y, - x, - y + height, - x, - y + height, - x + width, - y, - x + width, - y + height, - ]); - - this.imageShaOP.attr.forEach(function (d) { - ctx.bindBuffer(d.bufferType, d.buffer); - ctx.bufferData(d.bufferType, d.data, d.drawType); - ctx.enableVertexAttribArray(d.attribute); - ctx.vertexAttribPointer(d.attribute, d.size, d.valueType, true, 0, 0); - }); - - ctx.uniform1i(this.imageShaOP.uniform.u_image, 0); - ctx.activeTexture(this.ctx!.TEXTURE0); - ctx.bindTexture(this.ctx!.TEXTURE_2D, this.imageTexture); - ctx.drawArrays(ctx.TRIANGLES, 0, 6); -} - -function renderColorGradiant( - this: HeatmapRenderer, - ctx: WebGL2RenderingContext -) { - ctx.useProgram(this.colorShadOP.program); - - ctx.uniform4fv(this.colorShadOP.uniform.u_colorArr, this.gradient!.value); - ctx.uniform1f(this.colorShadOP.uniform.u_colorCount, this.gradient!.length); - ctx.uniform1fv( - this.colorShadOP.uniform.u_offset, - new Float32Array(this.gradient!.offset) - ); - ctx.uniform1f(this.colorShadOP.uniform.u_opacity, this.opacity); - - this.colorShadOP.attr.forEach(function (d) { - ctx.bindBuffer(d.bufferType, d.buffer); - ctx.bufferData(d.bufferType, d.data, d.drawType); - ctx.enableVertexAttribArray(d.attribute); - ctx.vertexAttribPointer(d.attribute, d.size, d.valueType, true, 0, 0); - }); - - ctx.uniform1i(this.colorShadOP.uniform.u_framebuffer, 0); - ctx.activeTexture(ctx.TEXTURE0); - ctx.bindTexture(ctx.TEXTURE_2D, this.fbTexObj); - - ctx.drawArrays(ctx.TRIANGLES, 0, 6); -} - -function imageInstance( - url: string, - onLoad: () => void, - onError: OnErrorEventHandler -) { - const imageIns = new Image(); - imageIns.crossOrigin = "anonymous"; - imageIns.onload = onLoad; - imageIns.onerror = onError; - imageIns.src = url; - return imageIns; -} - -export class HeatmapRenderer { - ctx: WebGL2RenderingContext | null = null; - ratio: number = 1; - width: number = 0; - height: number = 0; - imageConfig: BackgroundImageConfig | null = null; - configMin: number | null = null; - configMax: number | null = null; - min: number = 0; - max: number = 0; - hearmapExData: HearmapExData | object = {}; - - gradShadOP!: ShaderProgram; - colorShadOP!: ShaderProgram; - imageShaOP!: ShaderProgram; - fbTexObj!: WebGLTexture; - fbo!: WebGLFramebuffer; - size: number = 0; - zoom: number = 0; - angle: number = 0; - intensity: number = 0; - translate: [number, number] = [0, 0]; - opacity: number = 0; - gradient: MappedGradient | null = null; - imageTexture: WebGLTexture | null = null; - pLen: number | undefined = undefined; - buffer: ArrayBuffer | undefined = undefined; - buffer2: ArrayBuffer | undefined = undefined; - - private layer!: HTMLCanvasElement; - private dom!: Element; - private imgWidth: number = 0; - private imgHeight: number = 0; - private heatmapData: Point[] = []; - private type: string = ""; - - constructor(container: string | HTMLElement, config: HeatmapConfig) { - try { - const res = - typeof container === "string" - ? document.querySelector(container) - : container instanceof HTMLElement - ? container - : null; - if (!res) { - throw new Error("Context must be either a string or an Element"); - } - const { clientHeight: height, clientWidth: width } = res; - const layer = document.createElement("canvas"); - const ctx = layer.getContext("webgl2", { - premultipliedAlpha: false, - depth: false, - antialias: true, - alpha: true, - preserveDrawingBuffer: false, - }) as WebGL2RenderingContext; - - this.ratio = getPixelRatio(ctx); - ctx.clearColor(0, 0, 0, 0); - ctx.enable(ctx.BLEND); - ctx.blendEquation(ctx.FUNC_ADD); - ctx.blendFunc(ctx.ONE, ctx.ONE_MINUS_SRC_ALPHA); - ctx.depthMask(true); - layer.setAttribute("height", (height * this.ratio).toString()); - layer.setAttribute("width", (width * this.ratio).toString()); - layer.style.height = `${height}px`; - layer.style.width = `${width}px`; - layer.style.position = "absolute"; - res.appendChild(layer); - - this.ctx = ctx; - this.width = width; - this.height = height; - this.imageConfig = null; - this.configMin = null; - this.configMax = null; - this.hearmapExData = {}; - this.layer = layer; - this.dom = res; - this.gradShadOP = createGradiantShader(this.ctx, GradShader); - this.colorShadOP = createColorShader(this.ctx, ColorShader); - this.imageShaOP = createImageShader(this.ctx, ImageShader); - this.fbTexObj = ctx.createTexture()!; - this.fbo = ctx.createFramebuffer()!; - - if (!isNullUndefined(config.size)) { - this.setSize(config.size); - } else { - this.size = 20.0; - } - - if (!isNullUndefined(config.max)) { - this.setMax(config.max); - } else { - this.configMax = null; - } - - if (!isNullUndefined(config.min)) { - this.setMin(config.min); - } else { - this.configMin = null; - } - - if (!isNullUndefined(config.intensity)) { - this.setIntensity(config.intensity); - } else { - this.intensity = 1.0; - } - - if (!isNullUndefined(config.translate)) { - this.setTranslate(config.translate); - } else { - this.translate = [0, 0]; - } - - if (!isNullUndefined(config.zoom)) { - this.setZoom(config.zoom); - } else { - this.zoom = 1.0; - } - - if (!isNullUndefined(config.angle)) { - this.setRotationAngle(config.angle); - } else { - this.angle = 0.0; - } - - if (!isNullUndefined(config.opacity)) { - this.setOpacity(config.opacity); - } else { - this.opacity = 1.0; - } - - this.gradient = gradientMapper(config.gradient); - - if (config.backgroundImage && config.backgroundImage.url) { - this.setBackgroundImage(config.backgroundImage); - } - - this.heatmapData = []; - - this.ctx.viewport(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); - } catch (error) { - console.error(error); - } - } - - resize() { - const height = this.dom.clientHeight; - const width = this.dom.clientWidth; - this.layer.setAttribute("height", (height * this.ratio).toString()); - this.layer.setAttribute("width", (width * this.ratio).toString()); - this.layer.style.height = `${height}px`; - this.layer.style.width = `${width}px`; - this.width = width; - this.height = height; - this.ctx!.viewport(0, 0, this.ctx!.canvas.width, this.ctx!.canvas.height); - /* Perform update */ - this.render(); - } - - clear() { - this.ctx!.clear(this.ctx!.COLOR_BUFFER_BIT | this.ctx!.DEPTH_BUFFER_BIT); - } - - setMax(max: number): HeatmapRenderer { - if (isNullUndefined(max) || isNotNumber(max)) { - throw new Error("Invalid max: Expected Number"); - } - - this.configMax = max; - return this; - } - - setMin(min: number): HeatmapRenderer { - if (isNullUndefined(min) || isNotNumber(min)) { - throw new Error("Invalid min: Expected Number"); - } - - this.configMin = min; - return this; - } - - setGradient(gradient: GradientElement[]): HeatmapRenderer { - this.gradient = gradientMapper(gradient); - return this; - } - - setTranslate(translate: Translate) { - if (translate.constructor !== Array) { - throw new Error("Invalid Translate: Translate has to be of Array type"); - } - if (translate.length !== 2) { - throw new Error("Translate has to be of length 2"); - } - this.translate = translate; - return this; - } - - setZoom(zoom: number): HeatmapRenderer { - if (isNullUndefined(zoom) || isNotNumber(zoom)) { - throw new Error("Invalid zoom: Expected Number"); - } - - this.zoom = zoom; - return this; - } - - setRotationAngle(angle: number): HeatmapRenderer { - if (isNullUndefined(angle) || isNotNumber(angle)) { - throw new Error("Invalid Angle: Expected Number"); - } - - this.angle = angle; - return this; - } - - setSize(size: number): HeatmapRenderer { - if (isNullUndefined(size) || isNotNumber(size)) { - throw new Error("Invalid Size: Expected Number"); - } - - this.size = size; - return this; - } - - setIntensity(intensity: number): HeatmapRenderer { - if (isNullUndefined(intensity) || isNotNumber(intensity)) { - this.intensity = 1.0; // applying default intensity - throw new Error("Invalid Intensity: Expected Number"); - } - - if (intensity > 1 || intensity < 0) { - this.intensity = intensity > 1 ? 1 : 0; // Setting bound value - throw new Error("Invalid Intensity value " + intensity); - } - this.intensity = intensity; - return this; - } - - setOpacity(opacity: number): HeatmapRenderer { - if (isNullUndefined(opacity) || isNotNumber(opacity)) { - throw new Error("Invalid Opacity: Expected Number"); - } - - if (opacity > 1 || opacity < 0) { - throw new Error("Invalid Opacity value " + opacity); - } - this.opacity = opacity; - return this; - } - - setBackgroundImage(config: BackgroundImageConfig) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - if (!config.url) { - return; - } - - const maxTextureSize = this.ctx!.getParameter(this.ctx!.MAX_TEXTURE_SIZE); - this.imageTexture = this.ctx!.createTexture(); - this.type = "TEXTURE_2D"; - this.imageConfig = null; - - this.imgWidth = config.width || this.width; - this.imgHeight = config.height || this.height; - - this.imgWidth = - this.imgWidth > maxTextureSize ? maxTextureSize : this.imgWidth; - this.imgHeight = - this.imgHeight > maxTextureSize ? maxTextureSize : this.imgHeight; - - imageInstance( - config.url, - function onUpdateCallBack(this: HTMLImageElement) { - self.ctx!.activeTexture(self.ctx!.TEXTURE0); - self.ctx!.bindTexture(self.ctx!.TEXTURE_2D, self.imageTexture); - self.ctx!.texParameteri( - self.ctx!.TEXTURE_2D, - self.ctx!.TEXTURE_WRAP_S, - self.ctx!.CLAMP_TO_EDGE - ); - self.ctx!.texParameteri( - self.ctx!.TEXTURE_2D, - self.ctx!.TEXTURE_WRAP_T, - self.ctx!.CLAMP_TO_EDGE - ); - self.ctx!.texParameteri( - self.ctx!.TEXTURE_2D, - self.ctx!.TEXTURE_MIN_FILTER, - self.ctx!.LINEAR - ); - self.ctx!.texParameteri( - self.ctx!.TEXTURE_2D, - self.ctx!.TEXTURE_MAG_FILTER, - self.ctx!.LINEAR - ); - - self.ctx!.texImage2D( - self.ctx!.TEXTURE_2D, - 0, - self.ctx!.RGBA, - this.naturalWidth, - this.naturalHeight, - 0, - self.ctx!.RGBA, - self.ctx!.UNSIGNED_BYTE, - this - ); - - self.imageConfig = { - x: config.x || 0, - y: config.y || 0, - height: self.imgHeight, - width: self.imgWidth, - image: this, - }; - - self.render(); - }, - function onErrorCallBack(error) { - throw new Error(`Image Load Error, ${error}`); - } - ); - return this; - } - - clearData() { - this.heatmapData = []; - this.hearmapExData = {}; - this.render(); - } - - addData(data: Point[], transIntactFlag: boolean): HeatmapRenderer { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - for (let i = 0; i < data.length; i++) { - if (transIntactFlag) { - transCoOr.call(self, data[i]); - } - this.heatmapData.push(data[i]); - } - this.renderData(this.heatmapData); - return this; - } - - renderData(data: Point[]): HeatmapRenderer { - if (data.constructor !== Array) { - throw new Error("Expected Array type"); - } - this.hearmapExData = extractData(data, this); - this.heatmapData = data; - this.render(); - return this; - } - - render() { - renderExec.call(this); - } - - projection(data: Point) { - // Pre-compute constants and repetitive calculations - const zoomFactor = this.zoom || 0.1; - const halfWidth = this.width / 2; - const halfHeight = this.height / 2; - const translateX = this.translate[0]; - const translateY = this.translate[1]; - const angle = this.angle; - const aspect = this.width / this.height; - - // Calculate the adjusted positions - let posX = (data.x + translateX - halfWidth) / (halfWidth * zoomFactor); - let posY = (data.y + translateY - halfHeight) / (halfHeight * zoomFactor); - - posX *= aspect; - - // Rotate the point if there's an angle - if (angle !== 0.0) { - const cosAngle = Math.cos(-angle); - const sinAngle = Math.sin(-angle); - const xNew = cosAngle * posX - sinAngle * posY; - posY = sinAngle * posX + cosAngle * posY; - posX = xNew; - } - - posX *= 1.0 / aspect; - - // Scale back and adjust the position - posX = posX * halfWidth + halfWidth; - posY = posY * halfHeight + halfHeight; - - return { x: posX, y: posY }; - } -} diff --git a/src/heatmap_.ts b/src/heatmap_.ts new file mode 100644 index 0000000..1f2c929 --- /dev/null +++ b/src/heatmap_.ts @@ -0,0 +1,178 @@ +// // Internal class that encapsulates private properties and methods + +// import { HeatmapRenderer } from "./heatmapR"; +// import { +// BackgroundImageConfig, +// GradientElement, +// HeatmapConfig, +// Point, +// Translate, +// } from "./types"; + +// export class Heatmap { +// private renderer: HeatmapRenderer; +// ctx; +// ratio; +// width; +// height; +// min; +// max; +// size; +// zoom; +// angle; +// intensity; +// translate; +// opacity; +// gradient; +// imageConfig; + +// constructor(context: string | HTMLElement, config: HeatmapConfig) { +// this.renderer = new HeatmapRenderer(context, config); +// this.ctx = this.renderer.ctx; +// this.ratio = this.renderer.ratio; +// this.width = this.renderer.width; +// this.height = this.renderer.height; +// this.min = this.renderer.min; +// this.max = this.renderer.max; +// this.size = this.renderer.size; +// this.zoom = this.renderer.zoom; +// this.angle = this.renderer.angle; +// this.intensity = this.renderer.intensity; +// this.translate = this.renderer.translate; +// this.opacity = this.renderer.opacity; +// this.gradient = this.renderer.gradient; +// this.imageConfig = this.renderer.imageConfig; +// } + +// /** +// * Set the maximum data value for relative gradient calculations +// * @param max - number +// * @returns instance +// */ +// setMax(max: number) { +// return this.renderer.setMax(max); +// } + +// /** +// * Set the minimum data value for relative gradient calculations +// * @param min - number +// * @returns instance +// */ +// setMin(min: number) { +// return this.renderer.setMin(min); +// } + +// /** +// * Accepts array of objects with color value and offset +// * @param gradient - Color Gradient +// * @returns instance +// */ +// setGradient(gradient: GradientElement[]) { +// return this.renderer.setGradient(gradient); +// } + +// /** +// * Set the translate transformation on the canvas +// * @param translate - Accepts array [x, y] +// * @returns instance +// */ +// setTranslate(translate: Translate) { +// return this.renderer.setTranslate(translate); +// } + +// /** +// * Set the zoom transformation on the canvas +// * @param zoom - Accepts float value +// * @returns instance +// */ +// setZoom(zoom: number) { +// return this.renderer.setZoom(zoom); +// } + +// /** +// * Set the rotation transformation on the canvas +// * @param angle - Accepts angle in radians +// * @returns instance +// */ +// setRotationAngle(angle: number) { +// return this.renderer.setRotationAngle(angle); +// } + +// /** +// * Set the point radius +// * @param size - Accepts float value +// * @returns instance +// */ +// setSize(size: number) { +// return this.renderer.setSize(size); +// } + +// /** +// * Set the intensity factor +// * @param intensity - Accepts float value +// * @returns instance +// */ +// setIntensity(intensity: number) { +// return this.renderer.setIntensity(intensity); +// } + +// /** +// * Set the opacity factor +// * @param opacity - The opacity factor. +// * @returns instance +// */ +// setOpacity(opacity: number) { +// return this.renderer.setOpacity(opacity); +// } + +// /** +// * Set the background image +// * @param config - Accepts Object with { Url, height, width, x, and y} properties +// * @returns instance +// */ +// setBackgroundImage(config: BackgroundImageConfig) { +// return this.renderer.setBackgroundImage(config); +// } + +// /** +// * After adding data points, need to invoke .render() method to update the heatmap +// * @param data - The data points with 'x', 'y' and 'value' +// * @param transIntactFlag - Flag indicating whether to apply existing heatmap transformations on the newly added data points +// * @returns instance +// */ +// addData(data: Point[], transIntactFlag: boolean) { +// return this.renderer.addData(data, transIntactFlag); +// } + +// /** +// * @param data - Accepts an array of data points with 'x', 'y' and 'value' +// * @returns instance +// */ +// renderData(data: Point[]) { +// return this.renderer.renderData(data); +// } + +// /** +// * Method to re-render the heatmap. This method needs to be invoked as and when configurations get changed +// */ +// render() { +// this.renderer.render(); +// } + +// /** +// * Get projected co-ordinates relative to the heatmap layer +// * @param data - The data point to project. +// * @returns projected data point. +// */ + +// projection(data: Point) { +// return this.renderer.projection(data); +// } + +// /** +// * Clears canvas +// */ +// clear() { +// this.renderer.clear(); +// } +// } diff --git a/src/main.ts b/src/main.ts index 74b928a..b4b9614 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ -import { Heatmap } from "./heatmap"; +// import { Heatmap } from "./heatmap"; +import { HeatmapRenderer } from "./heatmap"; import { HeatmapConfig } from "./types"; export default function (context: string | HTMLElement, config: HeatmapConfig) { - return new Heatmap(context, config); + return new HeatmapRenderer(context, config); }