diff --git a/.prettierrc b/.prettierrc
index b60474d9..3708ec6b 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -4,9 +4,7 @@
"singleQuote": true,
"printWidth": 120,
"useTabs": false,
- "plugins": [
- "prettier-plugin-tailwindcss"
- ],
+ "plugins": ["prettier-plugin-tailwindcss"],
"quoteProps": "consistent",
"tailwindStylesheet": "./docs/src/index.css"
}
diff --git a/docs/public/images/logos/brave.svg b/docs/public/images/logos/brave.svg
new file mode 100644
index 00000000..6b8e23a1
--- /dev/null
+++ b/docs/public/images/logos/brave.svg
@@ -0,0 +1,7 @@
+
diff --git a/docs/public/images/logos/capy.svg b/docs/public/images/logos/capy.svg
new file mode 100644
index 00000000..1b50b075
--- /dev/null
+++ b/docs/public/images/logos/capy.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/docs/public/images/logos/chanel.svg b/docs/public/images/logos/chanel.svg
new file mode 100644
index 00000000..c85fe265
--- /dev/null
+++ b/docs/public/images/logos/chanel.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/docs/public/images/logos/cibc.svg b/docs/public/images/logos/cibc.svg
new file mode 100644
index 00000000..fd2ffe96
--- /dev/null
+++ b/docs/public/images/logos/cibc.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/docs/public/images/logos/cloudflare.svg b/docs/public/images/logos/cloudflare.svg
new file mode 100644
index 00000000..1dbaf089
--- /dev/null
+++ b/docs/public/images/logos/cloudflare.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/docs/public/images/logos/contra.svg b/docs/public/images/logos/contra.svg
new file mode 100644
index 00000000..818a4d6d
--- /dev/null
+++ b/docs/public/images/logos/contra.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/docs/public/images/logos/discord.svg b/docs/public/images/logos/discord.svg
new file mode 100644
index 00000000..7f2233ed
--- /dev/null
+++ b/docs/public/images/logos/discord.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/docs/public/images/logos/enterprise-rent.svg b/docs/public/images/logos/enterprise-rent.svg
new file mode 100644
index 00000000..a8626286
--- /dev/null
+++ b/docs/public/images/logos/enterprise-rent.svg
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/docs/public/images/logos/inbound.svg b/docs/public/images/logos/inbound.svg
new file mode 100644
index 00000000..6b195c0e
--- /dev/null
+++ b/docs/public/images/logos/inbound.svg
@@ -0,0 +1,6 @@
+
diff --git a/docs/public/images/logos/infinite.svg b/docs/public/images/logos/infinite.svg
new file mode 100644
index 00000000..654e8643
--- /dev/null
+++ b/docs/public/images/logos/infinite.svg
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/docs/public/images/logos/linear.svg b/docs/public/images/logos/linear.svg
new file mode 100644
index 00000000..f828c08d
--- /dev/null
+++ b/docs/public/images/logos/linear.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/docs/public/images/logos/mercury.svg b/docs/public/images/logos/mercury.svg
new file mode 100644
index 00000000..5639357c
--- /dev/null
+++ b/docs/public/images/logos/mercury.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/docs/public/images/logos/microsoft.svg b/docs/public/images/logos/microsoft.svg
new file mode 100644
index 00000000..7a3eaf1d
--- /dev/null
+++ b/docs/public/images/logos/microsoft.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/images/logos/mymind.svg b/docs/public/images/logos/mymind.svg
new file mode 100644
index 00000000..a102c9de
--- /dev/null
+++ b/docs/public/images/logos/mymind.svg
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/docs/public/images/logos/nasa.svg b/docs/public/images/logos/nasa.svg
new file mode 100644
index 00000000..e9d47171
--- /dev/null
+++ b/docs/public/images/logos/nasa.svg
@@ -0,0 +1,6 @@
+
diff --git a/docs/public/images/logos/netflix.svg b/docs/public/images/logos/netflix.svg
new file mode 100644
index 00000000..e2736b83
--- /dev/null
+++ b/docs/public/images/logos/netflix.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/images/logos/nike.svg b/docs/public/images/logos/nike.svg
new file mode 100644
index 00000000..7021e581
--- /dev/null
+++ b/docs/public/images/logos/nike.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/docs/public/images/logos/paper-logo-only.svg b/docs/public/images/logos/paper-logo-only.svg
new file mode 100644
index 00000000..cf4e3138
--- /dev/null
+++ b/docs/public/images/logos/paper-logo-only.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/docs/public/images/logos/paradigm.svg b/docs/public/images/logos/paradigm.svg
new file mode 100644
index 00000000..9191b0bb
--- /dev/null
+++ b/docs/public/images/logos/paradigm.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/docs/public/images/logos/resend.svg b/docs/public/images/logos/resend.svg
new file mode 100644
index 00000000..ed9960bf
--- /dev/null
+++ b/docs/public/images/logos/resend.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/docs/public/images/logos/shopify.svg b/docs/public/images/logos/shopify.svg
new file mode 100644
index 00000000..443af96d
--- /dev/null
+++ b/docs/public/images/logos/shopify.svg
@@ -0,0 +1,17 @@
+
+
+
diff --git a/docs/public/images/logos/vercel.svg b/docs/public/images/logos/vercel.svg
new file mode 100644
index 00000000..6d5e695e
--- /dev/null
+++ b/docs/public/images/logos/vercel.svg
@@ -0,0 +1,4 @@
+
diff --git a/docs/public/images/logos/volkswagen.svg b/docs/public/images/logos/volkswagen.svg
new file mode 100644
index 00000000..4ff393b3
--- /dev/null
+++ b/docs/public/images/logos/volkswagen.svg
@@ -0,0 +1,4 @@
+
diff --git a/docs/public/images/logos/wealth-simple.svg b/docs/public/images/logos/wealth-simple.svg
new file mode 100644
index 00000000..1108cc22
--- /dev/null
+++ b/docs/public/images/logos/wealth-simple.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/docs/src/app/(shaders)/heatmap/page.tsx b/docs/src/app/(shaders)/heatmap/page.tsx
index b6e974c3..61ed8557 100644
--- a/docs/src/app/(shaders)/heatmap/page.tsx
+++ b/docs/src/app/(shaders)/heatmap/page.tsx
@@ -2,7 +2,7 @@
import { Heatmap, heatmapMeta, heatmapPresets } from '@paper-design/shaders-react';
import { useControls, button, folder } from 'leva';
-import { Suspense, useState } from 'react';
+import { Suspense, useState, useEffect, useCallback } from 'react';
import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params';
import { usePresetHighlight } from '@/helpers/use-preset-highlight';
import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params';
@@ -16,9 +16,48 @@ import { levaImageButton } from '@/helpers/leva-image-button';
const { worldWidth, worldHeight, ...defaults } = heatmapPresets[0].params;
+const imageFiles = [
+ 'contra.svg',
+ 'apple.svg',
+ 'paradigm.svg',
+ 'paper-logo-only.svg',
+ 'brave.svg',
+ 'capy.svg',
+ 'infinite.svg',
+ 'linear.svg',
+ 'mercury.svg',
+ 'mymind.svg',
+ 'resend.svg',
+ 'shopify.svg',
+ 'wealth-simple.svg',
+ 'chanel.svg',
+ 'cibc.svg',
+ 'cloudflare.svg',
+ 'discord.svg',
+ 'nasa.svg',
+ 'nike.svg',
+ 'volkswagen.svg',
+ 'diamond.svg',
+] as const;
+
const HeatmapWithControls = () => {
+ const [imageIdx, setImageIdx] = useState(-1);
const [image, setImage] = useState('/images/logos/diamond.svg');
+ useEffect(() => {
+ if (imageIdx >= 0) {
+ const name = imageFiles[imageIdx];
+ const img = new Image();
+ img.src = `/images/logos/${name}`;
+ img.onload = () => setImage(img);
+ }
+ }, [imageIdx]);
+
+ const handleClick = useCallback(() => {
+ setImageIdx((prev) => (prev + 1) % imageFiles.length);
+ // setImageIdx(() => Math.floor(Math.random() * imageFiles.length));
+ }, []);
+
const { colors, setColors } = useColors({
defaultColors: defaults.colors,
maxColorCount: heatmapMeta.maxColorCount,
@@ -73,10 +112,14 @@ const HeatmapWithControls = () => {
<>
-
+
-
+
>
);
};
diff --git a/docs/src/shader-defs/heatmap-def.ts b/docs/src/shader-defs/heatmap-def.ts
index 7fc84cbe..a603c552 100644
--- a/docs/src/shader-defs/heatmap-def.ts
+++ b/docs/src/shader-defs/heatmap-def.ts
@@ -1,6 +1,6 @@
import { heatmapPresets } from '@paper-design/shaders-react';
import type { ShaderDef } from './shader-def-types';
-import {animatedCommonParams, animatedImageCommonParams} from './common-param-def';
+import { animatedCommonParams, animatedImageCommonParams } from './common-param-def';
const defaultParams = heatmapPresets[0].params;
diff --git a/packages/shaders/src/shader-sizing.ts b/packages/shaders/src/shader-sizing.ts
index 53986f57..ce292694 100644
--- a/packages/shaders/src/shader-sizing.ts
+++ b/packages/shaders/src/shader-sizing.ts
@@ -58,7 +58,6 @@ uniform float u_offsetY;`;
except for the `USE_PIXELIZATION` part we insert at start.
*/
-
export const sizingUV = `
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
diff --git a/packages/shaders/src/shaders/heatmap.ts b/packages/shaders/src/shaders/heatmap.ts
index 262f3baa..49bfa473 100644
--- a/packages/shaders/src/shaders/heatmap.ts
+++ b/packages/shaders/src/shaders/heatmap.ts
@@ -128,6 +128,26 @@ float shadowShape(vec2 uv, float t, float contour) {
return s;
}
+float blurEdge3x3(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) {
+ vec2 texel = 1.0 / vec2(textureSize(tex, 0));
+ vec2 r = radius * texel;
+
+ float w1 = 1.0, w2 = 2.0, w4 = 4.0;
+ float norm = 16.0;
+ float sum = w4 * centerSample;
+
+ sum += w2 * textureGrad(tex, uv + vec2(0.0, -r.y), dudx, dudy).g;
+ sum += w2 * textureGrad(tex, uv + vec2(0.0, r.y), dudx, dudy).g;
+ sum += w2 * textureGrad(tex, uv + vec2(-r.x, 0.0), dudx, dudy).g;
+ sum += w2 * textureGrad(tex, uv + vec2(r.x, 0.0), dudx, dudy).g;
+
+ sum += w1 * textureGrad(tex, uv + vec2(-r.x, -r.y), dudx, dudy).g;
+ sum += w1 * textureGrad(tex, uv + vec2(r.x, -r.y), dudx, dudy).g;
+ sum += w1 * textureGrad(tex, uv + vec2(-r.x, r.y), dudx, dudy).g;
+ sum += w1 * textureGrad(tex, uv + vec2(r.x, r.y), dudx, dudy).g;
+
+ return sum / norm;
+}
void main() {
vec2 uv = v_objectUV + .5;
@@ -140,6 +160,9 @@ void main() {
float imgSoftFrame = getImgFrame(imgUV, .03);
vec4 img = texture(u_image, imgUV);
+ vec2 dudx = dFdx(imgUV);
+ vec2 dudy = dFdy(imgUV);
+
if (img.a == 0.) {
fragColor = u_colorBack;
return;
@@ -165,6 +188,9 @@ void main() {
) + vec2(.5);
float shape = img[0];
+
+ img[1] = blurEdge3x3(u_image, imgUV, dudx, dudy, 8., img[1]);
+
float outerBlur = 1. - mix(1., img[1], shape);
float innerBlur = mix(img[1], 0., shape);
float contour = mix(img[2], 0., shape);
@@ -268,31 +294,48 @@ export function toProcessedHeatmap(file: File | string): Promise<{ blob: Blob }>
throw new Error('Failed to get canvas 2d context');
}
+ // 1) Draw original image once, no filters
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.filter = 'grayscale(100%) blur(' + maxBlur + 'px)';
ctx.drawImage(image, padding, padding, imgWidth, imgHeight);
- const bigBlurData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.filter = 'grayscale(100%) blur(' + Math.round(0.12 * maxBlur) + 'px)';
- ctx.drawImage(image, padding, padding, imgWidth, imgHeight);
- const innerBlurSmallData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+ const { width, height } = canvas;
+ const srcImageData = ctx.getImageData(0, 0, width, height);
+ const src = srcImageData.data; // RGBA
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.filter = 'grayscale(100%) blur(5px)';
- ctx.drawImage(image, padding, padding, imgWidth, imgHeight);
- const contourData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+ // 2) Build grayscale array (luma)
+ const totalPixels = width * height;
+ const gray = new Uint8ClampedArray(totalPixels);
+ for (let i = 0; i < totalPixels; i++) {
+ const px = i * 4;
+ const r = src[px] ?? 0;
+ const g = src[px + 1] ?? 0;
+ const b = src[px + 2] ?? 0;
+ // Standard luma conversion
+ gray[i] = (0.299 * r + 0.587 * g + 0.114 * b) | 0;
+ }
+
+ // 3) Blur grayscale for each "filter" you previously used
+ const bigBlurRadius = maxBlur;
+ const innerBlurRadius = Math.max(1, Math.round(0.12 * maxBlur));
+ const contourRadius = 5;
+
+ const bigBlurGray = multiPassBlurGray(gray, width, height, bigBlurRadius, 3);
+ const innerBlurGray = multiPassBlurGray(gray, width, height, innerBlurRadius, 3);
+ const contourGray = multiPassBlurGray(gray, width, height, contourRadius, 1);
+
+ // 4) Combine into final ImageData
+ const processedImageData = ctx.createImageData(width, height);
+ const dst = processedImageData.data;
- let processedImageData = ctx.createImageData(canvas.width, canvas.height);
- const totalPixels = canvas.width * canvas.height;
for (let i = 0; i < totalPixels; i++) {
const px = i * 4;
- processedImageData.data[px] = contourData[px]!;
- processedImageData.data[px + 1] = bigBlurData[px]!;
- processedImageData.data[px + 2] = innerBlurSmallData[px]!;
- processedImageData.data[px + 3] = 255;
+ dst[px] = contourGray[i] ?? 0;
+ dst[px + 1] = bigBlurGray[i] ?? 0;
+ dst[px + 2] = innerBlurGray[i] ?? 0;
+ dst[px + 3] = 255;
}
+
ctx.putImageData(processedImageData, 0, 0);
canvas.toBlob((blob) => {
@@ -300,7 +343,6 @@ export function toProcessedHeatmap(file: File | string): Promise<{ blob: Blob }>
reject(new Error('Failed to create PNG blob'));
return;
}
-
resolve({ blob });
}, 'image/png');
});
@@ -313,6 +355,79 @@ export function toProcessedHeatmap(file: File | string): Promise<{ blob: Blob }>
});
}
+/**
+ * Fast box blur for grayscale images using an integral image.
+ * gray: Uint8ClampedArray of length width * height
+ * radius: blur radius in pixels
+ */
+function blurGray(gray: Uint8ClampedArray, width: number, height: number, radius: number): Uint8ClampedArray {
+ if (radius <= 0) {
+ return gray.slice();
+ }
+
+ const out = new Uint8ClampedArray(width * height);
+ const integral = new Uint32Array(width * height);
+
+ // Build integral image
+ for (let y = 0; y < height; y++) {
+ let rowSum = 0;
+ for (let x = 0; x < width; x++) {
+ const idx = y * width + x;
+ const v = gray[idx] ?? 0;
+ rowSum += v;
+ integral[idx] = rowSum + (y > 0 ? (integral[idx - width] ?? 0) : 0);
+ }
+ }
+
+ // Blur using integral image
+ for (let y = 0; y < height; y++) {
+ const y1 = Math.max(0, y - radius);
+ const y2 = Math.min(height - 1, y + radius);
+ for (let x = 0; x < width; x++) {
+ const x1 = Math.max(0, x - radius);
+ const x2 = Math.min(width - 1, x + radius);
+
+ const idxA = y2 * width + x2;
+ const idxB = y2 * width + (x1 - 1);
+ const idxC = (y1 - 1) * width + x2;
+ const idxD = (y1 - 1) * width + (x1 - 1);
+
+ const A = integral[idxA] ?? 0;
+ const B = x1 > 0 ? (integral[idxB] ?? 0) : 0;
+ const C = y1 > 0 ? (integral[idxC] ?? 0) : 0;
+ const D = x1 > 0 && y1 > 0 ? (integral[idxD] ?? 0) : 0;
+
+ const sum = A - B - C + D;
+ const area = (x2 - x1 + 1) * (y2 - y1 + 1);
+ out[y * width + x] = Math.round(sum / area);
+ }
+ }
+
+ return out;
+}
+
+function multiPassBlurGray(
+ gray: Uint8ClampedArray,
+ width: number,
+ height: number,
+ radius: number,
+ passes: number
+): Uint8ClampedArray {
+ if (radius <= 0 || passes <= 1) {
+ return blurGray(gray, width, height, radius);
+ }
+
+ let input = gray;
+ let tmp: Uint8ClampedArray = gray;
+
+ for (let p = 0; p < passes; p++) {
+ tmp = blurGray(input, width, height, radius);
+ input = tmp;
+ }
+
+ return tmp;
+}
+
export interface HeatmapUniforms extends ShaderSizingUniforms {
u_image: HTMLImageElement | string;
u_contour: number;