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;