diff --git a/dist/types/heatmap.d.ts b/dist/types/heatmap.d.ts index 9a5308c..4fae85f 100644 --- a/dist/types/heatmap.d.ts +++ b/dist/types/heatmap.d.ts @@ -15,6 +15,7 @@ export declare class HeatmapRenderer { intensity: number; translate: [number, number]; opacity: number; + disableCircularFalloff: boolean; hearmapExData: HearmapExData | object; gradient: MappedGradient | null; _imageTexture: WebGLTexture | null; @@ -91,6 +92,12 @@ export declare class HeatmapRenderer { */ setOpacity(opacity: number): HeatmapRenderer; /** + * Set whether to disable circular falloff + * @param disableCircularFalloff - Boolean to disable circular falloff effect. + * @returns instance + */ + setDisableCircularFalloff(disableCircularFalloff: boolean): HeatmapRenderer; + /** * Set the background image * @param config - Accepts Object with { url, height, width, x, and y} properties * @returns instance diff --git a/dist/types/types.d.ts b/dist/types/types.d.ts index 5af0790..51f9e22 100644 --- a/dist/types/types.d.ts +++ b/dist/types/types.d.ts @@ -45,6 +45,7 @@ export type HeatmapConfig = { opacity?: number; gradient: GradientElement[]; backgroundImage?: BackgroundImageConfig; + disableCircularFalloff?: boolean; }; export type HearmapExData = { posVec: Float32Array; diff --git a/dist/visualHeatmap.esm.js b/dist/visualHeatmap.esm.js index d59ce8a..dfe8a70 100644 --- a/dist/visualHeatmap.esm.js +++ b/dist/visualHeatmap.esm.js @@ -61,6 +61,7 @@ const GradShader = { uniform float u_max; uniform float u_min; uniform float u_intensity; + uniform bool u_disableCircularFalloff; in float v_i; out vec4 fragColor; @@ -71,7 +72,15 @@ const GradShader = { float deno = max(u_max - u_min, 1e-6); // Prevent division by zero if(r <= 1.0) { - float alpha = ((v_i - u_min) / deno) * u_intensity * (1.0 - sqrt(r)); + float alpha; + if(u_disableCircularFalloff) { + // No circular falloff - uniform intensity within the point + alpha = ((v_i - u_min) / deno) * u_intensity; + } else { + // Apply circular falloff for smoother gradients + alpha = ((v_i - u_min) / deno) * u_intensity * (1.0 - sqrt(r)); + } + alpha = clamp(alpha, 0.0, 1.0); // Clamp alpha to valid range fragColor = vec4(0, 0, 0, alpha); } else { @@ -295,6 +304,7 @@ const createGradiantShader = function (ctx, shader) { u_zoom: ctx.getUniformLocation(program, "u_zoom"), u_angle: ctx.getUniformLocation(program, "u_angle"), u_density: ctx.getUniformLocation(program, "u_density"), + u_disableCircularFalloff: ctx.getUniformLocation(program, "u_disableCircularFalloff"), }, }; }; @@ -458,7 +468,7 @@ function renderExec() { function renderHeatGrad(ctx, exData) { var _a, _b, _c, _d; ctx.useProgram(this._gradShadOP.program); - const { u_resolution, u_translate, u_zoom, u_angle, u_density, u_max, u_min, u_size, u_intensity } = this._gradShadOP.uniform; + const { u_resolution, u_translate, u_zoom, u_angle, u_density, u_max, u_min, u_size, u_intensity, u_disableCircularFalloff } = this._gradShadOP.uniform; this.min = this.configMin !== null ? this.configMin : (_b = (_a = exData === null || exData === void 0 ? void 0 : exData.minMax) === null || _a === void 0 ? void 0 : _a.min) !== null && _b !== void 0 ? _b : 0; this.max = @@ -474,12 +484,16 @@ function renderHeatGrad(ctx, exData) { ctx.uniform1f(u_min, this.min); ctx.uniform1f(u_size, this.size); ctx.uniform1f(u_intensity, this.intensity); + ctx.uniform1i(u_disableCircularFalloff, this.disableCircularFalloff ? 1 : 0); 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); }); + if (this.disableCircularFalloff) { + ctx.blendEquation(ctx.MAX); + } ctx.drawArrays(ctx.POINTS, 0, (exData.posVec || []).length / 2); } function renderImage(ctx, imageConfig) { @@ -559,6 +573,7 @@ class HeatmapRenderer { this.intensity = 0; this.translate = [0, 0]; this.opacity = 0; + this.disableCircularFalloff = false; this.hearmapExData = {}; this.gradient = null; this._imageTexture = null; @@ -806,6 +821,18 @@ class HeatmapRenderer { return this; } /** + * Set whether to disable circular falloff + * @param disableCircularFalloff - Boolean to disable circular falloff effect. + * @returns instance + */ + setDisableCircularFalloff(disableCircularFalloff) { + if (typeof disableCircularFalloff !== 'boolean') { + throw new Error('Invalid disableCircularFalloff: Expected Boolean'); + } + this.disableCircularFalloff = disableCircularFalloff; + return this; + } + /** * Set the background image * @param config - Accepts Object with { url, height, width, x, and y} properties * @returns instance diff --git a/dist/visualHeatmap.esm.min.js b/dist/visualHeatmap.esm.min.js index 7dffc3e..ea8132d 100644 --- a/dist/visualHeatmap.esm.min.js +++ b/dist/visualHeatmap.esm.min.js @@ -3,4 +3,4 @@ * (c) 2025 Narayana Swamy (narayanaswamy14@gmail.com) * @license BSD-3-Clause */ -const t=.1,e="highp",i="\nvec2 rotation(vec2 v, float a, float aspect) {\n float s = sin(a);\n float c = cos(a);\n mat2 rotationMat = mat2(c, -s, s, c);\n mat2 scaleMat = mat2(aspect, 0.0, 0.0, 1.0);\n mat2 scaleMatInv = mat2(1.0/aspect, 0.0, 0.0, 1.0);\n return scaleMatInv * rotationMat * scaleMat * v;\n}",r={vertex:`#version 300 es\n precision ${e} float;\n in vec2 a_position;\n in float a_intensity;\n uniform float u_size;\n uniform vec2 u_resolution;\n uniform vec2 u_translate; \n uniform float u_zoom; \n uniform float u_angle; \n uniform float u_density;\n out float v_i;\n\n ${i}\n\n void main() {\n vec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n vec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n float zoomFactor = max(u_zoom, ${t});\n zeroToTwo = zeroToTwo / zoomFactor;\n if (u_angle != 0.0) {\n zeroToTwo = rotation(zeroToTwo, u_angle, u_resolution.x / u_resolution.y);\n }\n gl_Position = vec4(zeroToTwo, 0, 1);\n gl_PointSize = u_size * u_density;\n v_i = a_intensity;\n }`,fragment:`#version 300 es\n precision ${e} float;\n uniform float u_max;\n uniform float u_min;\n uniform float u_intensity;\n in float v_i;\n out vec4 fragColor;\n \n void main() {\n float r = 0.0;\n vec2 cxy = 2.0 * gl_PointCoord - 1.0;\n r = dot(cxy, cxy);\n float deno = max(u_max - u_min, 1e-6); // Prevent division by zero\n \n if(r <= 1.0) {\n float alpha = ((v_i - u_min) / deno) * u_intensity * (1.0 - sqrt(r));\n alpha = clamp(alpha, 0.0, 1.0); // Clamp alpha to valid range\n fragColor = vec4(0, 0, 0, alpha);\n } else {\n discard; // Don't process pixels outside the point\n }\n }`},o={vertex:"#version 300 es\n precision highp float;\n in vec2 a_texCoord;\n out vec2 v_texCoord;\n void main() {\n vec2 clipSpace = a_texCoord * 2.0 - 1.0;\n gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);\n v_texCoord = a_texCoord;\n }\n ",fragment:`#version 300 es\n #define MAX_COLORS ${20}\n precision ${e} float;\n \n in vec2 v_texCoord;\n out vec4 fragColor;\n uniform sampler2D u_framebuffer;\n uniform vec4 u_colorArr[MAX_COLORS];\n uniform float u_colorCount;\n uniform float u_opacity;\n uniform float u_offset[MAX_COLORS];\n\n void main() {\n float alpha = texture(u_framebuffer, v_texCoord.xy).a;\n \n if (alpha <= 0.0 || alpha > 1.0) {\n discard;\n return;\n }\n\n vec4 color_;\n if (alpha <= u_offset[0]) {\n color_ = u_colorArr[0];\n } else {\n for (int i = 1; i < MAX_COLORS && i < int(u_colorCount); ++i) {\n if (alpha <= u_offset[i]) {\n float t = (alpha - u_offset[i - 1]) / (u_offset[i] - u_offset[i - 1]);\n color_ = mix(u_colorArr[i - 1], u_colorArr[i], t);\n break;\n }\n }\n }\n\n color_ *= u_opacity;\n color_.a = max(0.0, color_.a);\n fragColor = color_;\n }\n `},n={vertex:`#version 300 es\n precision ${e} float;\n in vec2 a_position;\n in vec2 a_texCoord;\n uniform vec2 u_resolution;\n uniform vec2 u_translate; \n uniform float u_zoom; \n uniform float u_angle; \n uniform float u_density;\n out vec2 v_texCoord;\n\n ${i}\n\n void main() {\n vec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n zeroToOne.y = 1.0 - zeroToOne.y;\n vec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n float zoomFactor = max(u_zoom, ${t});\n zeroToTwo = zeroToTwo / zoomFactor;\n \n if (u_angle != 0.0) {\n zeroToTwo = rotation(zeroToTwo, u_angle * -1.0, u_resolution.x / u_resolution.y);\n }\n\n gl_Position = vec4(zeroToTwo, 0, 1);\n v_texCoord = a_texCoord;\n }\n `,fragment:`#version 300 es\n precision ${e} float;\n uniform sampler2D u_image;\n in vec2 v_texCoord;\n out vec4 fragColor;\n \n void main() {\n vec4 texColor = texture(u_image, v_texCoord);\n if (texColor.a < 0.01) {\n discard; // Don't process nearly transparent pixels\n }\n fragColor = texColor;\n }\n `};function a(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 s(t,e){const i=a(t,"VERTEX_SHADER",e.vertex),r=a(t,"FRAGMENT_SHADER",e.fragment),o=t.createProgram();if(!o)throw new Error("Failed to create program.");t.attachShader(o,i),t.attachShader(o,r),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 h(t){return null==t}function u(t){return"number"!=typeof t}function l(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(h(t)||u(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{try{const o=this.ctx,n=this.layer.getContext("webgl2",{premultipliedAlpha:!1,depth:!1,antialias:!0,alpha:!0,preserveDrawingBuffer:!0});if(!n)throw new Error("Failed to create WebGL2 context");this.ctx=n,this.render(),this.layer.toBlob((t=>{t?i(t):r(new Error("Failed to create blob from canvas")),this.ctx=o,this.render()}),t,e)}catch(t){r(t)}}))}}function T(t,e){return new x(t,e)}export{T as default}; \ No newline at end of file +const t=.1,e="highp",i="\nvec2 rotation(vec2 v, float a, float aspect) {\n float s = sin(a);\n float c = cos(a);\n mat2 rotationMat = mat2(c, -s, s, c);\n mat2 scaleMat = mat2(aspect, 0.0, 0.0, 1.0);\n mat2 scaleMatInv = mat2(1.0/aspect, 0.0, 0.0, 1.0);\n return scaleMatInv * rotationMat * scaleMat * v;\n}",r={vertex:`#version 300 es\n precision ${e} float;\n in vec2 a_position;\n in float a_intensity;\n uniform float u_size;\n uniform vec2 u_resolution;\n uniform vec2 u_translate; \n uniform float u_zoom; \n uniform float u_angle; \n uniform float u_density;\n out float v_i;\n\n ${i}\n\n void main() {\n vec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n vec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n float zoomFactor = max(u_zoom, ${t});\n zeroToTwo = zeroToTwo / zoomFactor;\n if (u_angle != 0.0) {\n zeroToTwo = rotation(zeroToTwo, u_angle, u_resolution.x / u_resolution.y);\n }\n gl_Position = vec4(zeroToTwo, 0, 1);\n gl_PointSize = u_size * u_density;\n v_i = a_intensity;\n }`,fragment:`#version 300 es\n precision ${e} float;\n uniform float u_max;\n uniform float u_min;\n uniform float u_intensity;\n uniform bool u_disableCircularFalloff;\n in float v_i;\n out vec4 fragColor;\n \n void main() {\n float r = 0.0;\n vec2 cxy = 2.0 * gl_PointCoord - 1.0;\n r = dot(cxy, cxy);\n float deno = max(u_max - u_min, 1e-6); // Prevent division by zero\n \n if(r <= 1.0) {\n float alpha;\n if(u_disableCircularFalloff) {\n // No circular falloff - uniform intensity within the point\n alpha = ((v_i - u_min) / deno) * u_intensity;\n } else {\n // Apply circular falloff for smoother gradients\n alpha = ((v_i - u_min) / deno) * u_intensity * (1.0 - sqrt(r));\n }\n\n alpha = clamp(alpha, 0.0, 1.0); // Clamp alpha to valid range\n fragColor = vec4(0, 0, 0, alpha);\n } else {\n discard; // Don't process pixels outside the point\n }\n }`},o={vertex:"#version 300 es\n precision highp float;\n in vec2 a_texCoord;\n out vec2 v_texCoord;\n void main() {\n vec2 clipSpace = a_texCoord * 2.0 - 1.0;\n gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);\n v_texCoord = a_texCoord;\n }\n ",fragment:`#version 300 es\n #define MAX_COLORS ${20}\n precision ${e} float;\n \n in vec2 v_texCoord;\n out vec4 fragColor;\n uniform sampler2D u_framebuffer;\n uniform vec4 u_colorArr[MAX_COLORS];\n uniform float u_colorCount;\n uniform float u_opacity;\n uniform float u_offset[MAX_COLORS];\n\n void main() {\n float alpha = texture(u_framebuffer, v_texCoord.xy).a;\n \n if (alpha <= 0.0 || alpha > 1.0) {\n discard;\n return;\n }\n\n vec4 color_;\n if (alpha <= u_offset[0]) {\n color_ = u_colorArr[0];\n } else {\n for (int i = 1; i < MAX_COLORS && i < int(u_colorCount); ++i) {\n if (alpha <= u_offset[i]) {\n float t = (alpha - u_offset[i - 1]) / (u_offset[i] - u_offset[i - 1]);\n color_ = mix(u_colorArr[i - 1], u_colorArr[i], t);\n break;\n }\n }\n }\n\n color_ *= u_opacity;\n color_.a = max(0.0, color_.a);\n fragColor = color_;\n }\n `},n={vertex:`#version 300 es\n precision ${e} float;\n in vec2 a_position;\n in vec2 a_texCoord;\n uniform vec2 u_resolution;\n uniform vec2 u_translate; \n uniform float u_zoom; \n uniform float u_angle; \n uniform float u_density;\n out vec2 v_texCoord;\n\n ${i}\n\n void main() {\n vec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n zeroToOne.y = 1.0 - zeroToOne.y;\n vec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n float zoomFactor = max(u_zoom, ${t});\n zeroToTwo = zeroToTwo / zoomFactor;\n \n if (u_angle != 0.0) {\n zeroToTwo = rotation(zeroToTwo, u_angle * -1.0, u_resolution.x / u_resolution.y);\n }\n\n gl_Position = vec4(zeroToTwo, 0, 1);\n v_texCoord = a_texCoord;\n }\n `,fragment:`#version 300 es\n precision ${e} float;\n uniform sampler2D u_image;\n in vec2 v_texCoord;\n out vec4 fragColor;\n \n void main() {\n vec4 texColor = texture(u_image, v_texCoord);\n if (texColor.a < 0.01) {\n discard; // Don't process nearly transparent pixels\n }\n fragColor = texColor;\n }\n `};function a(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 s(t,e){const i=a(t,"VERTEX_SHADER",e.vertex),r=a(t,"FRAGMENT_SHADER",e.fragment),o=t.createProgram();if(!o)throw new Error("Failed to create program.");t.attachShader(o,i),t.attachShader(o,r),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 l(t){return null==t}function h(t){return"number"!=typeof t}function u(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(l(t)||h(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}setDisableCircularFalloff(t){if("boolean"!=typeof t)throw new Error("Invalid disableCircularFalloff: Expected Boolean");return this.disableCircularFalloff=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{try{const o=this.ctx,n=this.layer.getContext("webgl2",{premultipliedAlpha:!1,depth:!1,antialias:!0,alpha:!0,preserveDrawingBuffer:!0});if(!n)throw new Error("Failed to create WebGL2 context");this.ctx=n,this.render(),this.layer.toBlob((t=>{t?i(t):r(new Error("Failed to create blob from canvas")),this.ctx=o,this.render()}),t,e)}catch(t){r(t)}}))}}function T(t,e){return new x(t,e)}export{T as default}; \ No newline at end of file diff --git a/dist/visualHeatmap.js b/dist/visualHeatmap.js index 9b8146e..f78218e 100644 --- a/dist/visualHeatmap.js +++ b/dist/visualHeatmap.js @@ -67,6 +67,7 @@ vec2 rotation(vec2 v, float a, float aspect) { uniform float u_max; uniform float u_min; uniform float u_intensity; + uniform bool u_disableCircularFalloff; in float v_i; out vec4 fragColor; @@ -77,7 +78,15 @@ vec2 rotation(vec2 v, float a, float aspect) { float deno = max(u_max - u_min, 1e-6); // Prevent division by zero if(r <= 1.0) { - float alpha = ((v_i - u_min) / deno) * u_intensity * (1.0 - sqrt(r)); + float alpha; + if(u_disableCircularFalloff) { + // No circular falloff - uniform intensity within the point + alpha = ((v_i - u_min) / deno) * u_intensity; + } else { + // Apply circular falloff for smoother gradients + alpha = ((v_i - u_min) / deno) * u_intensity * (1.0 - sqrt(r)); + } + alpha = clamp(alpha, 0.0, 1.0); // Clamp alpha to valid range fragColor = vec4(0, 0, 0, alpha); } else { @@ -301,6 +310,7 @@ vec2 rotation(vec2 v, float a, float aspect) { u_zoom: ctx.getUniformLocation(program, "u_zoom"), u_angle: ctx.getUniformLocation(program, "u_angle"), u_density: ctx.getUniformLocation(program, "u_density"), + u_disableCircularFalloff: ctx.getUniformLocation(program, "u_disableCircularFalloff"), }, }; }; @@ -464,7 +474,7 @@ vec2 rotation(vec2 v, float a, float aspect) { function renderHeatGrad(ctx, exData) { var _a, _b, _c, _d; ctx.useProgram(this._gradShadOP.program); - const { u_resolution, u_translate, u_zoom, u_angle, u_density, u_max, u_min, u_size, u_intensity } = this._gradShadOP.uniform; + const { u_resolution, u_translate, u_zoom, u_angle, u_density, u_max, u_min, u_size, u_intensity, u_disableCircularFalloff } = this._gradShadOP.uniform; this.min = this.configMin !== null ? this.configMin : (_b = (_a = exData === null || exData === void 0 ? void 0 : exData.minMax) === null || _a === void 0 ? void 0 : _a.min) !== null && _b !== void 0 ? _b : 0; this.max = @@ -480,12 +490,16 @@ vec2 rotation(vec2 v, float a, float aspect) { ctx.uniform1f(u_min, this.min); ctx.uniform1f(u_size, this.size); ctx.uniform1f(u_intensity, this.intensity); + ctx.uniform1i(u_disableCircularFalloff, this.disableCircularFalloff ? 1 : 0); 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); }); + if (this.disableCircularFalloff) { + ctx.blendEquation(ctx.MAX); + } ctx.drawArrays(ctx.POINTS, 0, (exData.posVec || []).length / 2); } function renderImage(ctx, imageConfig) { @@ -565,6 +579,7 @@ vec2 rotation(vec2 v, float a, float aspect) { this.intensity = 0; this.translate = [0, 0]; this.opacity = 0; + this.disableCircularFalloff = false; this.hearmapExData = {}; this.gradient = null; this._imageTexture = null; @@ -812,6 +827,18 @@ vec2 rotation(vec2 v, float a, float aspect) { return this; } /** + * Set whether to disable circular falloff + * @param disableCircularFalloff - Boolean to disable circular falloff effect. + * @returns instance + */ + setDisableCircularFalloff(disableCircularFalloff) { + if (typeof disableCircularFalloff !== 'boolean') { + throw new Error('Invalid disableCircularFalloff: Expected Boolean'); + } + this.disableCircularFalloff = disableCircularFalloff; + return this; + } + /** * Set the background image * @param config - Accepts Object with { url, height, width, x, and y} properties * @returns instance diff --git a/dist/visualHeatmap.min.js b/dist/visualHeatmap.min.js index 19ee678..f4bb46a 100644 --- a/dist/visualHeatmap.min.js +++ b/dist/visualHeatmap.min.js @@ -3,4 +3,4 @@ * (c) 2025 Narayana Swamy (narayanaswamy14@gmail.com) * @license BSD-3-Clause */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).visualHeatmap=e()}(this,(function(){"use strict";const t=.1,e="highp",i="\nvec2 rotation(vec2 v, float a, float aspect) {\n float s = sin(a);\n float c = cos(a);\n mat2 rotationMat = mat2(c, -s, s, c);\n mat2 scaleMat = mat2(aspect, 0.0, 0.0, 1.0);\n mat2 scaleMatInv = mat2(1.0/aspect, 0.0, 0.0, 1.0);\n return scaleMatInv * rotationMat * scaleMat * v;\n}",r={vertex:`#version 300 es\n precision ${e} float;\n in vec2 a_position;\n in float a_intensity;\n uniform float u_size;\n uniform vec2 u_resolution;\n uniform vec2 u_translate; \n uniform float u_zoom; \n uniform float u_angle; \n uniform float u_density;\n out float v_i;\n\n ${i}\n\n void main() {\n vec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n vec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n float zoomFactor = max(u_zoom, ${t});\n zeroToTwo = zeroToTwo / zoomFactor;\n if (u_angle != 0.0) {\n zeroToTwo = rotation(zeroToTwo, u_angle, u_resolution.x / u_resolution.y);\n }\n gl_Position = vec4(zeroToTwo, 0, 1);\n gl_PointSize = u_size * u_density;\n v_i = a_intensity;\n }`,fragment:`#version 300 es\n precision ${e} float;\n uniform float u_max;\n uniform float u_min;\n uniform float u_intensity;\n in float v_i;\n out vec4 fragColor;\n \n void main() {\n float r = 0.0;\n vec2 cxy = 2.0 * gl_PointCoord - 1.0;\n r = dot(cxy, cxy);\n float deno = max(u_max - u_min, 1e-6); // Prevent division by zero\n \n if(r <= 1.0) {\n float alpha = ((v_i - u_min) / deno) * u_intensity * (1.0 - sqrt(r));\n alpha = clamp(alpha, 0.0, 1.0); // Clamp alpha to valid range\n fragColor = vec4(0, 0, 0, alpha);\n } else {\n discard; // Don't process pixels outside the point\n }\n }`},o={vertex:"#version 300 es\n precision highp float;\n in vec2 a_texCoord;\n out vec2 v_texCoord;\n void main() {\n vec2 clipSpace = a_texCoord * 2.0 - 1.0;\n gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);\n v_texCoord = a_texCoord;\n }\n ",fragment:`#version 300 es\n #define MAX_COLORS ${20}\n precision ${e} float;\n \n in vec2 v_texCoord;\n out vec4 fragColor;\n uniform sampler2D u_framebuffer;\n uniform vec4 u_colorArr[MAX_COLORS];\n uniform float u_colorCount;\n uniform float u_opacity;\n uniform float u_offset[MAX_COLORS];\n\n void main() {\n float alpha = texture(u_framebuffer, v_texCoord.xy).a;\n \n if (alpha <= 0.0 || alpha > 1.0) {\n discard;\n return;\n }\n\n vec4 color_;\n if (alpha <= u_offset[0]) {\n color_ = u_colorArr[0];\n } else {\n for (int i = 1; i < MAX_COLORS && i < int(u_colorCount); ++i) {\n if (alpha <= u_offset[i]) {\n float t = (alpha - u_offset[i - 1]) / (u_offset[i] - u_offset[i - 1]);\n color_ = mix(u_colorArr[i - 1], u_colorArr[i], t);\n break;\n }\n }\n }\n\n color_ *= u_opacity;\n color_.a = max(0.0, color_.a);\n fragColor = color_;\n }\n `},n={vertex:`#version 300 es\n precision ${e} float;\n in vec2 a_position;\n in vec2 a_texCoord;\n uniform vec2 u_resolution;\n uniform vec2 u_translate; \n uniform float u_zoom; \n uniform float u_angle; \n uniform float u_density;\n out vec2 v_texCoord;\n\n ${i}\n\n void main() {\n vec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n zeroToOne.y = 1.0 - zeroToOne.y;\n vec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n float zoomFactor = max(u_zoom, ${t});\n zeroToTwo = zeroToTwo / zoomFactor;\n \n if (u_angle != 0.0) {\n zeroToTwo = rotation(zeroToTwo, u_angle * -1.0, u_resolution.x / u_resolution.y);\n }\n\n gl_Position = vec4(zeroToTwo, 0, 1);\n v_texCoord = a_texCoord;\n }\n `,fragment:`#version 300 es\n precision ${e} float;\n uniform sampler2D u_image;\n in vec2 v_texCoord;\n out vec4 fragColor;\n \n void main() {\n vec4 texColor = texture(u_image, v_texCoord);\n if (texColor.a < 0.01) {\n discard; // Don't process nearly transparent pixels\n }\n fragColor = texColor;\n }\n `};function a(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 s(t,e){const i=a(t,"VERTEX_SHADER",e.vertex),r=a(t,"FRAGMENT_SHADER",e.fragment),o=t.createProgram();if(!o)throw new Error("Failed to create program.");t.attachShader(o,i),t.attachShader(o,r),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 h(t){return null==t}function u(t){return"number"!=typeof t}function l(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(h(t)||u(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{try{const o=this.ctx,n=this.layer.getContext("webgl2",{premultipliedAlpha:!1,depth:!1,antialias:!0,alpha:!0,preserveDrawingBuffer:!0});if(!n)throw new Error("Failed to create WebGL2 context");this.ctx=n,this.render(),this.layer.toBlob((t=>{t?i(t):r(new Error("Failed to create blob from canvas")),this.ctx=o,this.render()}),t,e)}catch(t){r(t)}}))}}return function(t,e){return new x(t,e)}})); \ No newline at end of file +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).visualHeatmap=e()}(this,(function(){"use strict";const t=.1,e="highp",i="\nvec2 rotation(vec2 v, float a, float aspect) {\n float s = sin(a);\n float c = cos(a);\n mat2 rotationMat = mat2(c, -s, s, c);\n mat2 scaleMat = mat2(aspect, 0.0, 0.0, 1.0);\n mat2 scaleMatInv = mat2(1.0/aspect, 0.0, 0.0, 1.0);\n return scaleMatInv * rotationMat * scaleMat * v;\n}",r={vertex:`#version 300 es\n precision ${e} float;\n in vec2 a_position;\n in float a_intensity;\n uniform float u_size;\n uniform vec2 u_resolution;\n uniform vec2 u_translate; \n uniform float u_zoom; \n uniform float u_angle; \n uniform float u_density;\n out float v_i;\n\n ${i}\n\n void main() {\n vec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n vec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n float zoomFactor = max(u_zoom, ${t});\n zeroToTwo = zeroToTwo / zoomFactor;\n if (u_angle != 0.0) {\n zeroToTwo = rotation(zeroToTwo, u_angle, u_resolution.x / u_resolution.y);\n }\n gl_Position = vec4(zeroToTwo, 0, 1);\n gl_PointSize = u_size * u_density;\n v_i = a_intensity;\n }`,fragment:`#version 300 es\n precision ${e} float;\n uniform float u_max;\n uniform float u_min;\n uniform float u_intensity;\n uniform bool u_disableCircularFalloff;\n in float v_i;\n out vec4 fragColor;\n \n void main() {\n float r = 0.0;\n vec2 cxy = 2.0 * gl_PointCoord - 1.0;\n r = dot(cxy, cxy);\n float deno = max(u_max - u_min, 1e-6); // Prevent division by zero\n \n if(r <= 1.0) {\n float alpha;\n if(u_disableCircularFalloff) {\n // No circular falloff - uniform intensity within the point\n alpha = ((v_i - u_min) / deno) * u_intensity;\n } else {\n // Apply circular falloff for smoother gradients\n alpha = ((v_i - u_min) / deno) * u_intensity * (1.0 - sqrt(r));\n }\n\n alpha = clamp(alpha, 0.0, 1.0); // Clamp alpha to valid range\n fragColor = vec4(0, 0, 0, alpha);\n } else {\n discard; // Don't process pixels outside the point\n }\n }`},o={vertex:"#version 300 es\n precision highp float;\n in vec2 a_texCoord;\n out vec2 v_texCoord;\n void main() {\n vec2 clipSpace = a_texCoord * 2.0 - 1.0;\n gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);\n v_texCoord = a_texCoord;\n }\n ",fragment:`#version 300 es\n #define MAX_COLORS ${20}\n precision ${e} float;\n \n in vec2 v_texCoord;\n out vec4 fragColor;\n uniform sampler2D u_framebuffer;\n uniform vec4 u_colorArr[MAX_COLORS];\n uniform float u_colorCount;\n uniform float u_opacity;\n uniform float u_offset[MAX_COLORS];\n\n void main() {\n float alpha = texture(u_framebuffer, v_texCoord.xy).a;\n \n if (alpha <= 0.0 || alpha > 1.0) {\n discard;\n return;\n }\n\n vec4 color_;\n if (alpha <= u_offset[0]) {\n color_ = u_colorArr[0];\n } else {\n for (int i = 1; i < MAX_COLORS && i < int(u_colorCount); ++i) {\n if (alpha <= u_offset[i]) {\n float t = (alpha - u_offset[i - 1]) / (u_offset[i] - u_offset[i - 1]);\n color_ = mix(u_colorArr[i - 1], u_colorArr[i], t);\n break;\n }\n }\n }\n\n color_ *= u_opacity;\n color_.a = max(0.0, color_.a);\n fragColor = color_;\n }\n `},n={vertex:`#version 300 es\n precision ${e} float;\n in vec2 a_position;\n in vec2 a_texCoord;\n uniform vec2 u_resolution;\n uniform vec2 u_translate; \n uniform float u_zoom; \n uniform float u_angle; \n uniform float u_density;\n out vec2 v_texCoord;\n\n ${i}\n\n void main() {\n vec2 zeroToOne = (a_position * u_density + u_translate * u_density) / (u_resolution);\n zeroToOne.y = 1.0 - zeroToOne.y;\n vec2 zeroToTwo = zeroToOne * 2.0 - 1.0;\n float zoomFactor = max(u_zoom, ${t});\n zeroToTwo = zeroToTwo / zoomFactor;\n \n if (u_angle != 0.0) {\n zeroToTwo = rotation(zeroToTwo, u_angle * -1.0, u_resolution.x / u_resolution.y);\n }\n\n gl_Position = vec4(zeroToTwo, 0, 1);\n v_texCoord = a_texCoord;\n }\n `,fragment:`#version 300 es\n precision ${e} float;\n uniform sampler2D u_image;\n in vec2 v_texCoord;\n out vec4 fragColor;\n \n void main() {\n vec4 texColor = texture(u_image, v_texCoord);\n if (texColor.a < 0.01) {\n discard; // Don't process nearly transparent pixels\n }\n fragColor = texColor;\n }\n `};function a(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 s(t,e){const i=a(t,"VERTEX_SHADER",e.vertex),r=a(t,"FRAGMENT_SHADER",e.fragment),o=t.createProgram();if(!o)throw new Error("Failed to create program.");t.attachShader(o,i),t.attachShader(o,r),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 u(t){return null==t}function l(t){return"number"!=typeof t}function h(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(u(t)||l(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}setDisableCircularFalloff(t){if("boolean"!=typeof t)throw new Error("Invalid disableCircularFalloff: Expected Boolean");return this.disableCircularFalloff=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{try{const o=this.ctx,n=this.layer.getContext("webgl2",{premultipliedAlpha:!1,depth:!1,antialias:!0,alpha:!0,preserveDrawingBuffer:!0});if(!n)throw new Error("Failed to create WebGL2 context");this.ctx=n,this.render(),this.layer.toBlob((t=>{t?i(t):r(new Error("Failed to create blob from canvas")),this.ctx=o,this.render()}),t,e)}catch(t){r(t)}}))}}return function(t,e){return new x(t,e)}})); \ No newline at end of file diff --git a/src/heatmap.ts b/src/heatmap.ts index 6abb958..e9fc3bd 100644 --- a/src/heatmap.ts +++ b/src/heatmap.ts @@ -166,7 +166,7 @@ function renderHeatGrad( ) { ctx.useProgram(this._gradShadOP.program); - const {u_resolution, u_translate, u_zoom, u_angle, u_density, u_max, u_min, u_size, u_intensity } = this._gradShadOP.uniform; + const {u_resolution, u_translate, u_zoom, u_angle, u_density, u_max, u_min, u_size, u_intensity, u_disableCircularFalloff } = this._gradShadOP.uniform; this.min = this.configMin !== null ? this.configMin : exData?.minMax?.min ?? 0; this.max = @@ -189,6 +189,7 @@ function renderHeatGrad( ctx.uniform1f(u_min, this.min); ctx.uniform1f(u_size, this.size); ctx.uniform1f(u_intensity, this.intensity); + ctx.uniform1i(u_disableCircularFalloff, this.disableCircularFalloff ? 1 : 0); this._gradShadOP.attr.forEach(function (d) { ctx.bindBuffer(d.bufferType, d.buffer); @@ -197,6 +198,10 @@ function renderHeatGrad( ctx.vertexAttribPointer(d.attribute, d.size, d.valueType, true, 0, 0); }); + if (this.disableCircularFalloff) { + ctx.blendEquation(ctx.MAX); + } + ctx.drawArrays(ctx.POINTS, 0, (exData.posVec || []).length / 2); } @@ -307,6 +312,7 @@ export class HeatmapRenderer { intensity: number = 0; translate: [number, number] = [0, 0]; opacity: number = 0; + disableCircularFalloff: boolean = false; hearmapExData: HearmapExData | object = {}; gradient: MappedGradient | null = null; @@ -590,6 +596,19 @@ export class HeatmapRenderer { return this; } + /** + * Set whether to disable circular falloff + * @param disableCircularFalloff - Boolean to disable circular falloff effect. + * @returns instance + */ + setDisableCircularFalloff(disableCircularFalloff: boolean): HeatmapRenderer { + if (typeof disableCircularFalloff !== 'boolean') { + throw new Error('Invalid disableCircularFalloff: Expected Boolean'); + } + this.disableCircularFalloff = disableCircularFalloff; + return this; + } + /** * Set the background image * @param config - Accepts Object with { url, height, width, x, and y} properties diff --git a/src/shaders.ts b/src/shaders.ts index b5f00cc..cfc12ee 100644 --- a/src/shaders.ts +++ b/src/shaders.ts @@ -58,6 +58,7 @@ export const GradShader = { uniform float u_max; uniform float u_min; uniform float u_intensity; + uniform bool u_disableCircularFalloff; in float v_i; out vec4 fragColor; @@ -68,7 +69,15 @@ export const GradShader = { float deno = max(u_max - u_min, 1e-6); // Prevent division by zero if(r <= 1.0) { - float alpha = ((v_i - u_min) / deno) * u_intensity * (1.0 - sqrt(r)); + float alpha; + if(u_disableCircularFalloff) { + // No circular falloff - uniform intensity within the point + alpha = ((v_i - u_min) / deno) * u_intensity; + } else { + // Apply circular falloff for smoother gradients + alpha = ((v_i - u_min) / deno) * u_intensity * (1.0 - sqrt(r)); + } + alpha = clamp(alpha, 0.0, 1.0); // Clamp alpha to valid range fragColor = vec4(0, 0, 0, alpha); } else { diff --git a/src/types.ts b/src/types.ts index e89c12b..84c4b38 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,6 +58,7 @@ export type HeatmapConfig = { opacity?: number; gradient: GradientElement[]; backgroundImage?: BackgroundImageConfig; + disableCircularFalloff?: boolean; }; export type HearmapExData = { diff --git a/src/utils/shaderUtils.ts b/src/utils/shaderUtils.ts index c9302f5..ea95628 100644 --- a/src/utils/shaderUtils.ts +++ b/src/utils/shaderUtils.ts @@ -145,6 +145,7 @@ export const createGradiantShader = function ( u_zoom: ctx.getUniformLocation(program, "u_zoom"), u_angle: ctx.getUniformLocation(program, "u_angle"), u_density: ctx.getUniformLocation(program, "u_density"), + u_disableCircularFalloff: ctx.getUniformLocation(program, "u_disableCircularFalloff"), }, }; };