From 33b0c5f5409d877b6fbafd4c8bdcb7ce685e08e2 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 4 Nov 2025 15:32:47 +0100 Subject: [PATCH 01/84] saving draft --- docs/src/app/(shaders)/folds/layout.tsx | 9 + docs/src/app/(shaders)/folds/page.tsx | 82 +++ docs/src/shader-defs/folds-def.ts | 107 +++ packages/shaders-react/src/index.ts | 4 + packages/shaders-react/src/shaders/folds.tsx | 153 +++++ packages/shaders/src/index.ts | 9 + packages/shaders/src/shaders/folds.ts | 659 +++++++++++++++++++ 7 files changed, 1023 insertions(+) create mode 100644 docs/src/app/(shaders)/folds/layout.tsx create mode 100644 docs/src/app/(shaders)/folds/page.tsx create mode 100644 docs/src/shader-defs/folds-def.ts create mode 100644 packages/shaders-react/src/shaders/folds.tsx create mode 100644 packages/shaders/src/shaders/folds.ts diff --git a/docs/src/app/(shaders)/folds/layout.tsx b/docs/src/app/(shaders)/folds/layout.tsx new file mode 100644 index 000000000..317e2e0d1 --- /dev/null +++ b/docs/src/app/(shaders)/folds/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Folds Filter • Paper', +}; + +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx new file mode 100644 index 000000000..1577c14bb --- /dev/null +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { Folds, foldsPresets } from '@paper-design/shaders-react'; +import { useControls, button, folder } from 'leva'; +import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; +import { usePresetHighlight } from '@/helpers/use-preset-highlight'; +import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; +import { FoldsShapes, FoldsShape } from '@paper-design/shaders'; +import { ShaderFit } from '@paper-design/shaders'; +import { levaDeleteImageButton, levaImageButton } from '@/helpers/leva-image-button'; +import { useState, Suspense } from 'react'; +import { ShaderDetails } from '@/components/shader-details'; +import { ShaderContainer } from '@/components/shader-container'; +import { useUrlParams } from '@/helpers/use-url-params'; +import { foldsDef } from '@/shader-defs/folds-def'; +import { toHsla } from '@/helpers/color-utils'; + +// Override just for the docs, we keep it transparent in the preset +// foldsPresets[0].params.colorBack = '#000000'; + +const { worldWidth, worldHeight, ...defaults } = foldsPresets[0].params; + +const FoldsWithControls = () => { + const [image, setImage] = useState(''); + + const [params, setParams] = useControls(() => { + const presets = Object.fromEntries( + foldsPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + name, + button(() => setParamsSafe(params, setParams, preset)), + ]) + ); + return { + colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorFront: { value: toHsla(defaults.colorFront), order: 101 }, + // shape: { + // value: defaults.shape, + // options: Object.keys(FoldsShapes) as FoldsShape[], + // order: 102, + // disabled: Boolean(image), + // }, + // repetition: { value: defaults.repetition, min: 1, max: 10, order: 200 }, + // softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, + // shiftRed: { value: defaults.shiftRed, min: -1, max: 1, order: 202 }, + // shiftBlue: { value: defaults.shiftBlue, min: -1, max: 1, order: 203 }, + // distortion: { value: defaults.distortion, min: 0, max: 1, order: 204 }, + // contour: { value: defaults.contour, min: 0, max: 1, order: 205 }, + // angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, + speed: { value: defaults.speed, min: 0, max: 4, order: 300 }, + scale: { value: defaults.scale, min: 0.2, max: 4, order: 301 }, + rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, + offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, + offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, + fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 305 }, + Image: folder({ + 'Upload image': levaImageButton((img?: HTMLImageElement) => setImage(img ?? '')), + ...(image && { 'Delete image': levaDeleteImageButton(() => setImage('')) }), + }), + Presets: folder(presets, { order: -1 }), + }; + }, [image]); + + // Reset to defaults on mount, so that Leva doesn't show values from other + // shaders when navigating (if two shaders have a color1 param for example) + useResetLevaParams(params, setParams, defaults); + useUrlParams(params, setParams, foldsDef); + usePresetHighlight(foldsPresets, params); + cleanUpLevaParams(params); + + return ( + <> + + + + + + + + ); +}; + +export default FoldsWithControls; diff --git a/docs/src/shader-defs/folds-def.ts b/docs/src/shader-defs/folds-def.ts new file mode 100644 index 000000000..a4733f7d5 --- /dev/null +++ b/docs/src/shader-defs/folds-def.ts @@ -0,0 +1,107 @@ +import { foldsPresets } from '@paper-design/shaders-react'; +import type { ShaderDef } from './shader-def-types'; +import { animatedCommonParams } from './common-param-def'; + +const defaultParams = foldsPresets[0].params; + +export const foldsDef: ShaderDef = { + name: 'Liquid Metal', + description: 'Futuristic liquid metal material applied to uploaded logo or abstract shape.', + params: [ + { + name: 'colorBack', + type: 'string', + defaultValue: defaultParams.colorBack, + isColor: true, + description: 'Background color', + }, + { + name: 'colorTint', + type: 'string', + defaultValue: defaultParams.colorTint, + isColor: true, + description: 'Overlay color (color burn blending used)', + }, + { + name: 'image', + type: 'HTMLImageElement | string', + description: 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', + }, + { + name: 'shape', + type: 'enum', + defaultValue: defaultParams.shape, + description: 'The predefined shape used as an effect mask when no image is provided.', + options: ['none', 'circle', 'daisy', 'metaballs'], + }, + { + name: 'repetition', + type: 'number', + min: 1, + max: 10, + defaultValue: defaultParams.repetition, + description: 'Density of pattern stripes', + }, + { + name: 'softness', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.softness, + description: 'Color transition sharpness (0 = hard edge, 1 = smooth gradient)', + }, + { + name: 'shiftRed', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.shiftRed, + description: 'R-channel dispersion', + }, + { + name: 'shiftBlue', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.shiftBlue, + description: 'B-channel dispersion', + }, + { + name: 'distortion', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.distortion, + description: 'Noise distortion over the stripes pattern', + }, + { + name: 'contour', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.contour, + description: 'Strength of the distortion on the shape edges', + }, + { + name: 'angle', + type: 'number', + defaultValue: defaultParams.angle, + min: 0, + max: 360, + description: 'The direction of pattern animation (angle relative to the shape)', + }, + // { + // name: 'isImage', + // type: 'boolean', + // description: 'TODO', + // options: ['true', 'false'], + // }, + // { + // name: 'suspendWhenProcessingImage', + // type: 'boolean', + // description: 'TODO', + // options: ['true', 'false'], + // }, + ...animatedCommonParams, + ], +}; diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 2c79e7b2d..b8e73ab5d 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -107,6 +107,10 @@ export { LiquidMetal, liquidMetalPresets } from './shaders/liquid-metal.js'; export type { LiquidMetalProps } from './shaders/liquid-metal.js'; export type { LiquidMetalUniforms, LiquidMetalParams } from '@paper-design/shaders'; +export { Folds, foldsPresets } from './shaders/folds.js'; +export type { FoldsProps } from './shaders/folds.js'; +export type { FoldsUniforms, FoldsParams } from '@paper-design/shaders'; + export { isPaperShaderElement, getShaderColorFromString } from '@paper-design/shaders'; export type { PaperShaderElement, ShaderFit, ShaderSizingParams, ShaderSizingUniforms } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx new file mode 100644 index 000000000..310978d37 --- /dev/null +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -0,0 +1,153 @@ +import { memo, useLayoutEffect, useState } from 'react'; +import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; +import { + foldsFragmentShader, + ShaderFitOptions, + defaultObjectSizing, + type FoldsUniforms, + type FoldsParams, + toProcessedFolds, + type ImageShaderPreset, + getShaderColorFromString, + FoldsShapes, +} from '@paper-design/shaders'; +import { transparentPixel } from '../transparent-pixel.js'; +import { suspend } from '../suspend.js'; + +export interface FoldsProps extends ShaderComponentProps, FoldsParams { + /** + * Suspends the component when the image is being processed. + */ + suspendWhenProcessingImage?: boolean; +} + +type FoldsPreset = ImageShaderPreset; + +export const defaultPreset: FoldsPreset = { + name: 'Default', + params: { + ...defaultObjectSizing, + scale: 0.6, + speed: 1, + frame: 0, + colorBack: '#ffffff', + colorFront: '#000000', + distortion: 0.07, + repetition: 2.0, + shiftRed: 0.3, + shiftBlue: 0.3, + contour: 0.4, + softness: 0.1, + angle: 70, + shape: 'diamond', + }, +}; +export const foldsPresets: FoldsPreset[] = [defaultPreset]; + +export const Folds: React.FC = memo(function FoldsImpl({ + // Own props + colorBack = defaultPreset.params.colorBack, + colorFront = defaultPreset.params.colorFront, + speed = defaultPreset.params.speed, + frame = defaultPreset.params.frame, + image = '', + contour = defaultPreset.params.contour, + distortion = defaultPreset.params.distortion, + softness = defaultPreset.params.softness, + repetition = defaultPreset.params.repetition, + shiftRed = defaultPreset.params.shiftRed, + shiftBlue = defaultPreset.params.shiftBlue, + angle = defaultPreset.params.angle, + shape = defaultPreset.params.shape, + suspendWhenProcessingImage = false, + + // Sizing props + fit = defaultPreset.params.fit, + scale = defaultPreset.params.scale, + rotation = defaultPreset.params.rotation, + originX = defaultPreset.params.originX, + originY = defaultPreset.params.originY, + offsetX = defaultPreset.params.offsetX, + offsetY = defaultPreset.params.offsetY, + worldWidth = defaultPreset.params.worldWidth, + worldHeight = defaultPreset.params.worldHeight, + ...props +}: FoldsProps) { + const imageUrl = typeof image === 'string' ? image : image.src; + const [processedStateImage, setProcessedStateImage] = useState(transparentPixel); + + let processedImage: string; + + if (suspendWhenProcessingImage && typeof window !== 'undefined' && imageUrl) { + processedImage = suspend( + (): Promise => toProcessedFolds(imageUrl).then((result) => URL.createObjectURL(result.pngBlob)), + [imageUrl, 'folds'] + ); + } else { + processedImage = processedStateImage; + } + + useLayoutEffect(() => { + if (suspendWhenProcessingImage) { + // Skip doing work in the effect as it's been handled by suspense. + return; + } + + if (!imageUrl) { + setProcessedStateImage(transparentPixel); + return; + } + + let url: string; + let current = true; + + toProcessedFolds(imageUrl).then((result) => { + if (current) { + url = URL.createObjectURL(result.pngBlob); + setProcessedStateImage(url); + } + }); + + return () => { + current = false; + }; + }, [imageUrl, suspendWhenProcessingImage]); + + const uniforms = { + // Own uniforms + u_colorBack: getShaderColorFromString(colorBack), + u_colorFront: getShaderColorFromString(colorFront), + + u_image: processedImage, + u_contour: contour, + u_distortion: distortion, + u_softness: softness, + u_repetition: repetition, + u_shiftRed: shiftRed, + u_shiftBlue: shiftBlue, + u_angle: angle, + u_isImage: Boolean(image), + u_shape: FoldsShapes[shape], + + // Sizing uniforms + u_fit: ShaderFitOptions[fit], + u_scale: scale, + u_rotation: rotation, + u_offsetX: offsetX, + u_offsetY: offsetY, + u_originX: originX, + u_originY: originY, + u_worldWidth: worldWidth, + u_worldHeight: worldHeight, + } satisfies FoldsUniforms; + + return ( + + ); +}); diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index bf59bfcbc..c9659ae87 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -210,6 +210,15 @@ export { type LiquidMetalUniforms, } from './shaders/liquid-metal.js'; +export { + foldsFragmentShader, + FoldsShapes, + toProcessedFolds, + type FoldsShape, + type FoldsParams, + type FoldsUniforms, +} from './shaders/folds.js'; + // ----- Utils ----- // export { getShaderColorFromString } from './get-shader-color-from-string.js'; export { getShaderNoiseTexture } from './get-shader-noise-texture.js'; diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts new file mode 100644 index 000000000..f9eddfefa --- /dev/null +++ b/packages/shaders/src/shaders/folds.ts @@ -0,0 +1,659 @@ +import type { ShaderMotionParams } from '../shader-mount.js'; +import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; +import { declarePI, rotation2, simplexNoise, colorBandingFix } from '../shader-utils.js'; + +/** + * + * Fluid motion imitation applied over user image + * (animated stripe pattern getting distorted with shape edges) + * + * Uniforms: + * - u_colorBack, u_colorFront (RGBA) + * - u_repetition: density of pattern stripes + * - u_softness: blur between stripes + * - u_shiftRed & u_shiftBlue: color dispersion between the stripes + * - u_distortion: pattern distortion on the whole canvas + * - u_contour: distortion power over the shape edges + * - u_shape (float used as integer): + * ---- 0: canvas-screen rectangle, needs u_worldWidth = u_worldHeight = 0 to be responsive (see vertex shader) + * ---- 1: static circle + * ---- 2: animated flower-like polar shape + * ---- 3: animated metaballs + * + */ + +// language=GLSL +export const foldsFragmentShader: string = `#version 300 es +precision mediump float; + +uniform sampler2D u_image; +uniform float u_imageAspectRatio; + +uniform vec2 u_resolution; +uniform float u_time; + +uniform vec4 u_colorBack; +uniform vec4 u_colorFront; + +uniform float u_softness; +uniform float u_repetition; +uniform float u_shiftRed; +uniform float u_shiftBlue; +uniform float u_distortion; +uniform float u_contour; +uniform float u_angle; + +uniform float u_shape; +uniform bool u_isImage; + +${sizingVariablesDeclaration} + +out vec4 fragColor; + +${declarePI} +${rotation2} +${simplexNoise} + +float getColorChanges(float c1, float c2, float stripe_p, vec3 w, float blur, float bump, float tint) { + + float ch = mix(c2, c1, smoothstep(.0, 2. * blur, stripe_p)); + + float border = w[0]; + ch = mix(ch, c2, smoothstep(border, border + 2. * blur, stripe_p)); + + if (u_isImage == true) { + bump = smoothstep(.2, .8, bump); + } + border = w[0] + .4 * (1. - bump) * w[1]; + ch = mix(ch, c1, smoothstep(border, border + 2. * blur, stripe_p)); + + border = w[0] + .5 * (1. - bump) * w[1]; + ch = mix(ch, c2, smoothstep(border, border + 2. * blur, stripe_p)); + + border = w[0] + w[1]; + ch = mix(ch, c1, smoothstep(border, border + 2. * blur, stripe_p)); + + float gradient_t = (stripe_p - w[0] - w[1]) / w[2]; + float gradient = mix(c1, c2, smoothstep(0., 1., gradient_t)); + ch = mix(ch, gradient, smoothstep(border, border + .5 * blur, stripe_p)); + + // Tint color is applied with color burn blending + ch = mix(ch, 1. - min(1., (1. - ch) / max(tint, 0.0001)), u_colorFront.a); + return ch; +} + +float getImgFrame(vec2 uv, float th) { + float frame = 1.; + frame *= smoothstep(0., th, uv.y); + frame *= 1.0 - smoothstep(1. - th, 1., uv.y); + frame *= smoothstep(0., th, uv.x); + frame *= 1.0 - smoothstep(1. - th, 1., uv.x); + return frame; +} + +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).r; + sum += w2 * textureGrad(tex, uv + vec2(0.0, r.y), dudx, dudy).r; + sum += w2 * textureGrad(tex, uv + vec2(-r.x, 0.0), dudx, dudy).r; + sum += w2 * textureGrad(tex, uv + vec2(r.x, 0.0), dudx, dudy).r; + + sum += w1 * textureGrad(tex, uv + vec2(-r.x, -r.y), dudx, dudy).r; + sum += w1 * textureGrad(tex, uv + vec2(r.x, -r.y), dudx, dudy).r; + sum += w1 * textureGrad(tex, uv + vec2(-r.x, r.y), dudx, dudy).r; + sum += w1 * textureGrad(tex, uv + vec2(r.x, r.y), dudx, dudy).r; + + return sum / norm; +} + +float lst(float edge0, float edge1, float x) { + return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); +} + +void main() { + + const float firstFrameOffset = 2.8; + float t = .3 * (u_time + firstFrameOffset); + + vec2 uv = v_imageUV; + vec2 dudx = dFdx(v_imageUV); + vec2 dudy = dFdy(v_imageUV); + vec4 img = textureGrad(u_image, uv, dudx, dudy); + + if (u_isImage == false) { + uv = v_objectUV + .5; + uv.y = 1. - uv.y; + } + + float edge = 0.; + + if (u_isImage == true) { + float edgeRaw = img.r; + edge = blurEdge3x3(u_image, uv, dudx, dudy, 6., edgeRaw); + } else { + // diamond + vec2 shapeUV = uv - .5; + shapeUV = rotate(shapeUV, .25 * PI); + shapeUV *= 1.42; + shapeUV += .5; + vec2 mask = min(shapeUV, 1. - shapeUV); + vec2 pixel_thickness = vec2(.15); + float maskX = smoothstep(0.0, pixel_thickness.x, mask.x); + float maskY = smoothstep(0.0, pixel_thickness.y, mask.y); + maskX = pow(maskX, .25); + maskY = pow(maskY, .25); + edge = clamp(1. - maskX * maskY, 0., 1.); + } + + vec3 color = vec3(edge); + + float opacity = 1.; + + float alpha = 0.; + if (u_isImage == true) { + alpha = img.g; + float frame = getImgFrame(v_imageUV, 0.); + alpha *= frame; + } else { + alpha = 1. - smoothstep(.9 - 2. * fwidth(edge), .9, edge); +// if (u_shape < 2.) { +// edge = 1.2 * edge; +// } else if (u_shape < 5.) { +// edge = 1.8 * pow(edge, 1.5); +// } + } + + vec2 p = uv * 14.; + +// p.x += sin(p.y + u_time) * .7; + p.y -= 3. * edge; + p.y += .3 * cos(p.x + u_time) - .6 * sin(.5 * p.y + u_time); + + float w = fwidth(p.x) + fwidth(p.y); + w = max(w, fwidth(3. * edge)); + w = max(w, .001); + + vec2 d = abs(fract(p) - .5); +// float line = min(d.x, d.y); + float line = d.y; + + float grid = 1. - smoothstep(0., w, line); + + color = mix(u_colorBack.rgb, u_colorFront.rgb, grid); + fragColor = vec4(color, alpha); +} +`; + +// Configuration for Poisson solver +export const POISSON_CONFIG_OPTIMIZED = { + measurePerformance: false, // Set to true to see performance metrics + workingSize: 512, // Size to solve Poisson at (will upscale to original size) + iterations: 40, // SOR converges ~2-20x faster than standard Gauss-Seidel +}; + +// Precomputed pixel data for sparse processing +interface SparsePixelData { + interiorPixels: Uint32Array; // Indices of interior pixels + boundaryPixels: Uint32Array; // Indices of boundary pixels + pixelCount: number; + // Neighbor indices for each interior pixel (4 neighbors per pixel) + // Layout: [east, west, north, south] for each pixel + neighborIndices: Int32Array; +} + +export function toProcessedFolds(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const isBlob = typeof file === 'string' && file.startsWith('blob:'); + + return new Promise((resolve, reject) => { + if (!file || !ctx) { + reject(new Error('Invalid file or canvas context')); + return; + } + + const blobContentTypePromise = isBlob && fetch(file).then((res) => res.headers.get('Content-Type')); + const img = new Image(); + img.crossOrigin = 'anonymous'; + const totalStartTime = performance.now(); + + img.onload = async () => { + // Force SVG to load at a high fidelity size if it's an SVG + let isSVG; + + const blobContentType = await blobContentTypePromise; + + if (blobContentType) { + isSVG = blobContentType === 'image/svg+xml'; + } else if (typeof file === 'string') { + isSVG = file.endsWith('.svg') || file.startsWith('data:image/svg+xml'); + } else { + isSVG = file.type === 'image/svg+xml'; + } + + let originalWidth = img.width || img.naturalWidth; + let originalHeight = img.height || img.naturalHeight; + + if (isSVG) { + // Scale SVG to max dimension while preserving aspect ratio + const svgMaxSize = 4096; + const aspectRatio = originalWidth / originalHeight; + + if (originalWidth > originalHeight) { + originalWidth = svgMaxSize; + originalHeight = svgMaxSize / aspectRatio; + } else { + originalHeight = svgMaxSize; + originalWidth = svgMaxSize * aspectRatio; + } + + img.width = originalWidth; + img.height = originalHeight; + } + + // Always scale to working resolution for consistency + const minDimension = Math.min(originalWidth, originalHeight); + const targetSize = POISSON_CONFIG_OPTIMIZED.workingSize; + + // Calculate scale to fit within workingSize + const scaleFactor = targetSize / minDimension; + const width = Math.round(originalWidth * scaleFactor); + const height = Math.round(originalHeight * scaleFactor); + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Processing Mode]`); + console.log(` Original: ${originalWidth}×${originalHeight}`); + console.log(` Working: ${width}×${height} (${(scaleFactor * 100).toFixed(1)}% scale)`); + if (scaleFactor < 1) { + console.log(` Speedup: ~${Math.round(1 / (scaleFactor * scaleFactor))}×`); + } + } + + canvas.width = originalWidth; + canvas.height = originalHeight; + + // Use a smaller canvas for shape detection and Poisson solving + const shapeCanvas = document.createElement('canvas'); + shapeCanvas.width = width; + shapeCanvas.height = height; + + const shapeCtx = shapeCanvas.getContext('2d')!; + shapeCtx.drawImage(img, 0, 0, width, height); + + // 1) Build optimized masks using TypedArrays + const startMask = performance.now(); + + const shapeImageData = shapeCtx.getImageData(0, 0, width, height); + const data = shapeImageData.data; + + // Use Uint8Array for masks (1 byte per pixel vs 8+ bytes for boolean array) + const shapeMask = new Uint8Array(width * height); + const boundaryMask = new Uint8Array(width * height); + + // First pass: identify shape pixels + let shapePixelCount = 0; + for (let i = 0, idx = 0; i < data.length; i += 4, idx++) { + const a = data[i + 3]; + const isShape = a === 0 ? 0 : 1; + shapeMask[idx] = isShape; + shapePixelCount += isShape; + } + + // 2) Optimized boundary detection using sparse approach + // Only check shape pixels, not all pixels + const boundaryIndices: number[] = []; + const interiorIndices: number[] = []; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + if (!shapeMask[idx]) continue; + + // Check if pixel is on boundary (optimized: early exit) + let isBoundary = false; + + // Check 4-connected neighbors first (most common case) + if (x === 0 || x === width - 1 || y === 0 || y === height - 1) { + isBoundary = true; + } else { + // Check all 8 neighbors (including diagonals) for comprehensive boundary detection + isBoundary = + !shapeMask[idx - 1] || // left + !shapeMask[idx + 1] || // right + !shapeMask[idx - width] || // top + !shapeMask[idx + width] || // bottom + !shapeMask[idx - width - 1] || // top-left + !shapeMask[idx - width + 1] || // top-right + !shapeMask[idx + width - 1] || // bottom-left + !shapeMask[idx + width + 1]; // bottom-right + } + + if (isBoundary) { + boundaryMask[idx] = 1; + boundaryIndices.push(idx); + } else { + interiorIndices.push(idx); + } + } + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Mask Building] Time: ${(performance.now() - startMask).toFixed(2)}ms`); + console.log( + ` Shape pixels: ${shapePixelCount} / ${width * height} (${((shapePixelCount / (width * height)) * 100).toFixed(1)}%)` + ); + console.log(` Interior pixels: ${interiorIndices.length}`); + console.log(` Boundary pixels: ${boundaryIndices.length}`); + } + + // 3) Precompute sparse data structure for solver + const sparseData = buildSparseData( + shapeMask, + boundaryMask, + new Uint32Array(interiorIndices), + new Uint32Array(boundaryIndices), + width, + height + ); + + // 4) Solve Poisson equation with optimized sparse solver + const startSolve = performance.now(); + const u = solvePoissonSparse(sparseData, shapeMask, boundaryMask, width, height); + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Poisson Solve] Time: ${(performance.now() - startSolve).toFixed(2)}ms`); + } + + // 5) Generate output image + let maxVal = 0; + let finalImageData: ImageData; + + // Only check shape pixels for max value + for (let i = 0; i < interiorIndices.length; i++) { + const idx = interiorIndices[i]!; + if (u[idx]! > maxVal) maxVal = u[idx]!; + } + + // Create gradient image at working resolution + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = width; + tempCanvas.height = height; + const tempCtx = tempCanvas.getContext('2d')!; + + const tempImg = tempCtx.createImageData(width, height); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + const px = idx * 4; + + if (!shapeMask[idx]) { + tempImg.data[px] = 255; + tempImg.data[px + 1] = 255; + tempImg.data[px + 2] = 255; + tempImg.data[px + 3] = 0; // Alpha = 0 for background + } else { + const poissonRatio = u[idx]! / maxVal; + const gray = 255 * (1 - poissonRatio); + tempImg.data[px] = gray; + tempImg.data[px + 1] = gray; + tempImg.data[px + 2] = gray; + tempImg.data[px + 3] = 255; // Alpha = 255 for shape + } + } + } + tempCtx.putImageData(tempImg, 0, 0); + + // Upscale to original resolution with smooth interpolation + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, originalWidth, originalHeight); + + // Now get the upscaled image data for final output + const outImg = ctx.getImageData(0, 0, originalWidth, originalHeight); + + // Re-apply edges from original resolution with anti-aliasing + // This ensures edges are pixel-perfect while gradient is smooth + const originalCanvas = document.createElement('canvas'); + originalCanvas.width = originalWidth; + originalCanvas.height = originalHeight; + const originalCtx = originalCanvas.getContext('2d')!; + // originalCtx.fillStyle = "white"; + // originalCtx.fillRect(0, 0, originalWidth, originalHeight); + originalCtx.drawImage(img, 0, 0, originalWidth, originalHeight); + const originalData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); + + // Process each pixel: Red channel = gradient, Alpha channel = original alpha + for (let i = 0; i < outImg.data.length; i += 4) { + const a = originalData.data[i + 3]!; + // Use only alpha to determine background vs shape + const upscaledAlpha = outImg.data[i + 3]!; + if (a === 0) { + // Background pixel + outImg.data[i] = 255; + outImg.data[i + 1] = 0; + } else { + // Red channel carries the gradient + // Check if upscale missed this pixel by looking at alpha channel + // If upscaled alpha is 0, the low-res version thought this was background + outImg.data[i] = upscaledAlpha === 0 ? 0 : outImg.data[i]!; // gradient or 0 + outImg.data[i + 1] = a; // original alpha + } + + // Unused channels fixed + outImg.data[i + 2] = 255; + outImg.data[i + 3] = 255; + } + + ctx.putImageData(outImg, 0, 0); + finalImageData = outImg; + canvas.toBlob((blob) => { + if (!blob) { + reject(new Error('Failed to create PNG blob')); + return; + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + const totalTime = performance.now() - totalStartTime; + console.log(`[Total Processing Time] ${totalTime.toFixed(2)}ms`); + if (scaleFactor < 1) { + const estimatedFullResTime = totalTime * Math.pow((originalWidth * originalHeight) / (width * height), 1.5); + console.log(`[Estimated time at full resolution] ~${estimatedFullResTime.toFixed(0)}ms`); + console.log( + `[Time saved] ~${(estimatedFullResTime - totalTime).toFixed(0)}ms (${Math.round(estimatedFullResTime / totalTime)}× faster)` + ); + } + } + + resolve({ + imageData: finalImageData, + pngBlob: blob, + }); + }, 'image/png'); + }; + + img.onerror = () => reject(new Error('Failed to load image')); + img.src = typeof file === 'string' ? file : URL.createObjectURL(file); + }); +} + +function buildSparseData( + shapeMask: Uint8Array, + boundaryMask: Uint8Array, + interiorPixels: Uint32Array, + boundaryPixels: Uint32Array, + width: number, + height: number +): SparsePixelData { + const pixelCount = interiorPixels.length; + + // Build neighbor indices for sparse processing + // For each interior pixel, store indices of its 4 neighbors + // Use -1 for out-of-bounds or non-shape neighbors + const neighborIndices = new Int32Array(pixelCount * 4); + + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + const x = idx % width; + const y = Math.floor(idx / width); + + // East neighbor + neighborIndices[i * 4 + 0] = x < width - 1 && shapeMask[idx + 1] ? idx + 1 : -1; + // West neighbor + neighborIndices[i * 4 + 1] = x > 0 && shapeMask[idx - 1] ? idx - 1 : -1; + // North neighbor + neighborIndices[i * 4 + 2] = y > 0 && shapeMask[idx - width] ? idx - width : -1; + // South neighbor + neighborIndices[i * 4 + 3] = y < height - 1 && shapeMask[idx + width] ? idx + width : -1; + } + + return { + interiorPixels, + boundaryPixels, + pixelCount, + neighborIndices, + }; +} + +function solvePoissonSparse( + sparseData: SparsePixelData, + shapeMask: Uint8Array, + boundaryMask: Uint8Array, + width: number, + height: number +): Float32Array { + // This controls how smooth the falloff gradient will be and extend into the shape + const ITERATIONS = POISSON_CONFIG_OPTIMIZED.iterations; + + // Keep C constant - only iterations control gradient spread + const C = 0.01; + + const u = new Float32Array(width * height); + const { interiorPixels, neighborIndices, pixelCount } = sparseData; + + // Performance tracking + const startTime = performance.now(); + + // Red-Black SOR for better symmetry with fewer iterations + // omega between 1.8-1.95 typically gives best convergence for Poisson + const omega = 1.9; + + // Pre-classify pixels as red or black for efficient processing + const redPixels: number[] = []; + const blackPixels: number[] = []; + + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + const x = idx % width; + const y = Math.floor(idx / width); + + if ((x + y) % 2 === 0) { + redPixels.push(i); + } else { + blackPixels.push(i); + } + } + + for (let iter = 0; iter < ITERATIONS; iter++) { + // Red pass: update red pixels + for (const i of redPixels) { + const idx = interiorPixels[i]!; + + // Get precomputed neighbor indices + const eastIdx = neighborIndices[i * 4 + 0]!; + const westIdx = neighborIndices[i * 4 + 1]!; + const northIdx = neighborIndices[i * 4 + 2]!; + const southIdx = neighborIndices[i * 4 + 3]!; + + // Sum neighbors (use 0 for out-of-bounds) + let sumN = 0; + if (eastIdx >= 0) sumN += u[eastIdx]!; + if (westIdx >= 0) sumN += u[westIdx]!; + if (northIdx >= 0) sumN += u[northIdx]!; + if (southIdx >= 0) sumN += u[southIdx]!; + + // SOR update: blend new value with old value + const newValue = (C + sumN) / 4; + u[idx] = omega * newValue + (1 - omega) * u[idx]!; + } + + // Black pass: update black pixels + for (const i of blackPixels) { + const idx = interiorPixels[i]!; + + // Get precomputed neighbor indices + const eastIdx = neighborIndices[i * 4 + 0]!; + const westIdx = neighborIndices[i * 4 + 1]!; + const northIdx = neighborIndices[i * 4 + 2]!; + const southIdx = neighborIndices[i * 4 + 3]!; + + // Sum neighbors (use 0 for out-of-bounds) + let sumN = 0; + if (eastIdx >= 0) sumN += u[eastIdx]!; + if (westIdx >= 0) sumN += u[westIdx]!; + if (northIdx >= 0) sumN += u[northIdx]!; + if (southIdx >= 0) sumN += u[southIdx]!; + + // SOR update: blend new value with old value + const newValue = (C + sumN) / 4; + u[idx] = omega * newValue + (1 - omega) * u[idx]!; + } + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + const elapsed = performance.now() - startTime; + + console.log(`[Optimized Poisson Solver (SOR ω=${omega})]`); + console.log(` Working size: ${width}×${height}`); + console.log(` Iterations: ${ITERATIONS}`); + console.log(` Time: ${elapsed.toFixed(2)}ms`); + console.log(` Interior pixels processed: ${pixelCount}`); + console.log(` Speed: ${((ITERATIONS * pixelCount) / (elapsed * 1000)).toFixed(2)} Mpixels/sec`); + } + + return u; +} + +export interface FoldsUniforms extends ShaderSizingUniforms { + u_colorBack: [number, number, number, number]; + u_colorFront: [number, number, number, number]; + u_image: HTMLImageElement | string | undefined; + u_repetition: number; + u_shiftRed: number; + u_shiftBlue: number; + u_contour: number; + u_softness: number; + u_distortion: number; + u_angle: number; + u_shape: (typeof FoldsShapes)[FoldsShape]; + u_isImage: boolean; +} + +export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { + colorBack?: string; + colorFront?: string; + image?: HTMLImageElement | string | undefined; + repetition?: number; + shiftRed?: number; + shiftBlue?: number; + contour?: number; + softness?: number; + distortion?: number; + angle?: number; + shape?: FoldsShape; +} + +export const FoldsShapes = { + none: 0, + circle: 1, + daisy: 2, + diamond: 3, + metaballs: 4, +} as const; + +export type FoldsShape = keyof typeof FoldsShapes; From 0cf97621640c15606373a0ab105e9aa766806cc6 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 4 Nov 2025 15:56:31 +0100 Subject: [PATCH 02/84] saving --- packages/shaders/src/shaders/folds.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index f9eddfefa..abe985016 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -171,17 +171,19 @@ void main() { vec2 p = uv * 14.; -// p.x += sin(p.y + u_time) * .7; + p.x += sin(p.y + u_time) * .7; p.y -= 3. * edge; + p.y -= 8. * pow(edge, 6.); p.y += .3 * cos(p.x + u_time) - .6 * sin(.5 * p.y + u_time); - float w = fwidth(p.x) + fwidth(p.y); + float w = fwidth(p.x); + w = max(w, fwidth(p.y)); w = max(w, fwidth(3. * edge)); w = max(w, .001); vec2 d = abs(fract(p) - .5); -// float line = min(d.x, d.y); - float line = d.y; + float line = min(d.x, d.y); +// float line = d.y; float grid = 1. - smoothstep(0., w, line); From 291701210ee9ccf3139860d36ff86f9471267d35 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 9 Nov 2025 22:41:59 +0100 Subject: [PATCH 03/84] draft --- docs/src/app/(shaders)/folds/page.tsx | 2 +- packages/shaders-react/src/shaders/folds.tsx | 2 +- packages/shaders/src/shaders/folds.ts | 919 +++++++++---------- 3 files changed, 449 insertions(+), 474 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index 1577c14bb..c7626dad3 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -21,7 +21,7 @@ import { toHsla } from '@/helpers/color-utils'; const { worldWidth, worldHeight, ...defaults } = foldsPresets[0].params; const FoldsWithControls = () => { - const [image, setImage] = useState(''); + const [image, setImage] = useState('/images/logos/apple.svg'); const [params, setParams] = useControls(() => { const presets = Object.fromEntries( diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index 310978d37..64a64b445 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -27,7 +27,7 @@ export const defaultPreset: FoldsPreset = { name: 'Default', params: { ...defaultObjectSizing, - scale: 0.6, + scale: 1, speed: 1, frame: 0, colorBack: '#ffffff', diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index abe985016..d4f252923 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -1,6 +1,6 @@ -import type { ShaderMotionParams } from '../shader-mount.js'; -import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, rotation2, simplexNoise, colorBandingFix } from '../shader-utils.js'; +import type {ShaderMotionParams} from '../shader-mount.js'; +import {sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms} from '../shader-sizing.js'; +import {declarePI, rotation2, simplexNoise, colorBandingFix} from '../shader-utils.js'; /** * @@ -46,13 +46,13 @@ uniform float u_angle; uniform float u_shape; uniform bool u_isImage; -${sizingVariablesDeclaration} +${ sizingVariablesDeclaration } out vec4 fragColor; -${declarePI} -${rotation2} -${simplexNoise} +${ declarePI } +${ rotation2 } +${ simplexNoise } float getColorChanges(float c1, float c2, float stripe_p, vec3 w, float blur, float bump, float tint) { @@ -132,530 +132,505 @@ void main() { } float edge = 0.; - - if (u_isImage == true) { - float edgeRaw = img.r; - edge = blurEdge3x3(u_image, uv, dudx, dudy, 6., edgeRaw); - } else { - // diamond - vec2 shapeUV = uv - .5; - shapeUV = rotate(shapeUV, .25 * PI); - shapeUV *= 1.42; - shapeUV += .5; - vec2 mask = min(shapeUV, 1. - shapeUV); - vec2 pixel_thickness = vec2(.15); - float maskX = smoothstep(0.0, pixel_thickness.x, mask.x); - float maskY = smoothstep(0.0, pixel_thickness.y, mask.y); - maskX = pow(maskX, .25); - maskY = pow(maskY, .25); - edge = clamp(1. - maskX * maskY, 0., 1.); - } - vec3 color = vec3(edge); + float edgeRaw = img.r; + edge = blurEdge3x3(u_image, uv, dudx, dudy, 6., edgeRaw); + + vec3 color = vec3(0.); float opacity = 1.; - - float alpha = 0.; - if (u_isImage == true) { - alpha = img.g; - float frame = getImgFrame(v_imageUV, 0.); - alpha *= frame; - } else { - alpha = 1. - smoothstep(.9 - 2. * fwidth(edge), .9, edge); -// if (u_shape < 2.) { -// edge = 1.2 * edge; -// } else if (u_shape < 5.) { -// edge = 1.8 * pow(edge, 1.5); -// } - } - vec2 p = uv * 14.; + float alpha = 0.; + alpha = img.g; + float frame = getImgFrame(v_imageUV, 0.); + alpha *= frame; - p.x += sin(p.y + u_time) * .7; - p.y -= 3. * edge; - p.y -= 8. * pow(edge, 6.); - p.y += .3 * cos(p.x + u_time) - .6 * sin(.5 * p.y + u_time); + vec2 p = uv * 30.; - float w = fwidth(p.x); - w = max(w, fwidth(p.y)); - w = max(w, fwidth(3. * edge)); - w = max(w, .001); + float test = .9; + + float wave = 2. * (.7 * cos(.3 * p.x + .1 * p.y + u_time) - 0.6 * sin(.6 * p.y + u_time)); + float addon = mix((1. - edge) * wave, 0., pow(edge, 8.)); + p.y -= addon; vec2 d = abs(fract(p) - .5); - float line = min(d.x, d.y); -// float line = d.y; + vec2 aa = 2. * fwidth(p); + float w = .2 * (1. - edge); - float grid = 1. - smoothstep(0., w, line); + vec2 gx = 1.0 - smoothstep(vec2(w) +vec2(0.), vec2(w) + aa, d); + float grid = gx.y; color = mix(u_colorBack.rgb, u_colorFront.rgb, grid); - fragColor = vec4(color, alpha); + + fragColor = vec4(color, 1.); + } `; // Configuration for Poisson solver export const POISSON_CONFIG_OPTIMIZED = { - measurePerformance: false, // Set to true to see performance metrics - workingSize: 512, // Size to solve Poisson at (will upscale to original size) - iterations: 40, // SOR converges ~2-20x faster than standard Gauss-Seidel + measurePerformance: false, // Set to true to see performance metrics + workingSize: 512, // Size to solve Poisson at (will upscale to original size) + iterations: 40, // SOR converges ~2-20x faster than standard Gauss-Seidel }; // Precomputed pixel data for sparse processing interface SparsePixelData { - interiorPixels: Uint32Array; // Indices of interior pixels - boundaryPixels: Uint32Array; // Indices of boundary pixels - pixelCount: number; - // Neighbor indices for each interior pixel (4 neighbors per pixel) - // Layout: [east, west, north, south] for each pixel - neighborIndices: Int32Array; + interiorPixels: Uint32Array; // Indices of interior pixels + boundaryPixels: Uint32Array; // Indices of boundary pixels + pixelCount: number; + // Neighbor indices for each interior pixel (4 neighbors per pixel) + // Layout: [east, west, north, south] for each pixel + neighborIndices: Int32Array; } export function toProcessedFolds(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - const isBlob = typeof file === 'string' && file.startsWith('blob:'); - - return new Promise((resolve, reject) => { - if (!file || !ctx) { - reject(new Error('Invalid file or canvas context')); - return; - } - - const blobContentTypePromise = isBlob && fetch(file).then((res) => res.headers.get('Content-Type')); - const img = new Image(); - img.crossOrigin = 'anonymous'; - const totalStartTime = performance.now(); - - img.onload = async () => { - // Force SVG to load at a high fidelity size if it's an SVG - let isSVG; - - const blobContentType = await blobContentTypePromise; - - if (blobContentType) { - isSVG = blobContentType === 'image/svg+xml'; - } else if (typeof file === 'string') { - isSVG = file.endsWith('.svg') || file.startsWith('data:image/svg+xml'); - } else { - isSVG = file.type === 'image/svg+xml'; - } - - let originalWidth = img.width || img.naturalWidth; - let originalHeight = img.height || img.naturalHeight; - - if (isSVG) { - // Scale SVG to max dimension while preserving aspect ratio - const svgMaxSize = 4096; - const aspectRatio = originalWidth / originalHeight; - - if (originalWidth > originalHeight) { - originalWidth = svgMaxSize; - originalHeight = svgMaxSize / aspectRatio; - } else { - originalHeight = svgMaxSize; - originalWidth = svgMaxSize * aspectRatio; - } - - img.width = originalWidth; - img.height = originalHeight; - } - - // Always scale to working resolution for consistency - const minDimension = Math.min(originalWidth, originalHeight); - const targetSize = POISSON_CONFIG_OPTIMIZED.workingSize; - - // Calculate scale to fit within workingSize - const scaleFactor = targetSize / minDimension; - const width = Math.round(originalWidth * scaleFactor); - const height = Math.round(originalHeight * scaleFactor); - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Processing Mode]`); - console.log(` Original: ${originalWidth}×${originalHeight}`); - console.log(` Working: ${width}×${height} (${(scaleFactor * 100).toFixed(1)}% scale)`); - if (scaleFactor < 1) { - console.log(` Speedup: ~${Math.round(1 / (scaleFactor * scaleFactor))}×`); - } - } - - canvas.width = originalWidth; - canvas.height = originalHeight; - - // Use a smaller canvas for shape detection and Poisson solving - const shapeCanvas = document.createElement('canvas'); - shapeCanvas.width = width; - shapeCanvas.height = height; - - const shapeCtx = shapeCanvas.getContext('2d')!; - shapeCtx.drawImage(img, 0, 0, width, height); - - // 1) Build optimized masks using TypedArrays - const startMask = performance.now(); - - const shapeImageData = shapeCtx.getImageData(0, 0, width, height); - const data = shapeImageData.data; - - // Use Uint8Array for masks (1 byte per pixel vs 8+ bytes for boolean array) - const shapeMask = new Uint8Array(width * height); - const boundaryMask = new Uint8Array(width * height); - - // First pass: identify shape pixels - let shapePixelCount = 0; - for (let i = 0, idx = 0; i < data.length; i += 4, idx++) { - const a = data[i + 3]; - const isShape = a === 0 ? 0 : 1; - shapeMask[idx] = isShape; - shapePixelCount += isShape; - } - - // 2) Optimized boundary detection using sparse approach - // Only check shape pixels, not all pixels - const boundaryIndices: number[] = []; - const interiorIndices: number[] = []; - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = y * width + x; - if (!shapeMask[idx]) continue; - - // Check if pixel is on boundary (optimized: early exit) - let isBoundary = false; - - // Check 4-connected neighbors first (most common case) - if (x === 0 || x === width - 1 || y === 0 || y === height - 1) { - isBoundary = true; - } else { - // Check all 8 neighbors (including diagonals) for comprehensive boundary detection - isBoundary = - !shapeMask[idx - 1] || // left - !shapeMask[idx + 1] || // right - !shapeMask[idx - width] || // top - !shapeMask[idx + width] || // bottom - !shapeMask[idx - width - 1] || // top-left - !shapeMask[idx - width + 1] || // top-right - !shapeMask[idx + width - 1] || // bottom-left - !shapeMask[idx + width + 1]; // bottom-right - } - - if (isBoundary) { - boundaryMask[idx] = 1; - boundaryIndices.push(idx); - } else { - interiorIndices.push(idx); - } - } - } - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Mask Building] Time: ${(performance.now() - startMask).toFixed(2)}ms`); - console.log( - ` Shape pixels: ${shapePixelCount} / ${width * height} (${((shapePixelCount / (width * height)) * 100).toFixed(1)}%)` - ); - console.log(` Interior pixels: ${interiorIndices.length}`); - console.log(` Boundary pixels: ${boundaryIndices.length}`); - } - - // 3) Precompute sparse data structure for solver - const sparseData = buildSparseData( - shapeMask, - boundaryMask, - new Uint32Array(interiorIndices), - new Uint32Array(boundaryIndices), - width, - height - ); - - // 4) Solve Poisson equation with optimized sparse solver - const startSolve = performance.now(); - const u = solvePoissonSparse(sparseData, shapeMask, boundaryMask, width, height); - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Poisson Solve] Time: ${(performance.now() - startSolve).toFixed(2)}ms`); - } - - // 5) Generate output image - let maxVal = 0; - let finalImageData: ImageData; - - // Only check shape pixels for max value - for (let i = 0; i < interiorIndices.length; i++) { - const idx = interiorIndices[i]!; - if (u[idx]! > maxVal) maxVal = u[idx]!; - } - - // Create gradient image at working resolution - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = width; - tempCanvas.height = height; - const tempCtx = tempCanvas.getContext('2d')!; - - const tempImg = tempCtx.createImageData(width, height); - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = y * width + x; - const px = idx * 4; - - if (!shapeMask[idx]) { - tempImg.data[px] = 255; - tempImg.data[px + 1] = 255; - tempImg.data[px + 2] = 255; - tempImg.data[px + 3] = 0; // Alpha = 0 for background - } else { - const poissonRatio = u[idx]! / maxVal; - const gray = 255 * (1 - poissonRatio); - tempImg.data[px] = gray; - tempImg.data[px + 1] = gray; - tempImg.data[px + 2] = gray; - tempImg.data[px + 3] = 255; // Alpha = 255 for shape - } - } - } - tempCtx.putImageData(tempImg, 0, 0); - - // Upscale to original resolution with smooth interpolation - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'high'; - ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, originalWidth, originalHeight); - - // Now get the upscaled image data for final output - const outImg = ctx.getImageData(0, 0, originalWidth, originalHeight); - - // Re-apply edges from original resolution with anti-aliasing - // This ensures edges are pixel-perfect while gradient is smooth - const originalCanvas = document.createElement('canvas'); - originalCanvas.width = originalWidth; - originalCanvas.height = originalHeight; - const originalCtx = originalCanvas.getContext('2d')!; - // originalCtx.fillStyle = "white"; - // originalCtx.fillRect(0, 0, originalWidth, originalHeight); - originalCtx.drawImage(img, 0, 0, originalWidth, originalHeight); - const originalData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); - - // Process each pixel: Red channel = gradient, Alpha channel = original alpha - for (let i = 0; i < outImg.data.length; i += 4) { - const a = originalData.data[i + 3]!; - // Use only alpha to determine background vs shape - const upscaledAlpha = outImg.data[i + 3]!; - if (a === 0) { - // Background pixel - outImg.data[i] = 255; - outImg.data[i + 1] = 0; - } else { - // Red channel carries the gradient - // Check if upscale missed this pixel by looking at alpha channel - // If upscaled alpha is 0, the low-res version thought this was background - outImg.data[i] = upscaledAlpha === 0 ? 0 : outImg.data[i]!; // gradient or 0 - outImg.data[i + 1] = a; // original alpha + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const isBlob = typeof file === 'string' && file.startsWith('blob:'); + + return new Promise((resolve, reject) => { + if (!file || !ctx) { + reject(new Error('Invalid file or canvas context')); + return; } - // Unused channels fixed - outImg.data[i + 2] = 255; - outImg.data[i + 3] = 255; - } - - ctx.putImageData(outImg, 0, 0); - finalImageData = outImg; - canvas.toBlob((blob) => { - if (!blob) { - reject(new Error('Failed to create PNG blob')); - return; - } - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - const totalTime = performance.now() - totalStartTime; - console.log(`[Total Processing Time] ${totalTime.toFixed(2)}ms`); - if (scaleFactor < 1) { - const estimatedFullResTime = totalTime * Math.pow((originalWidth * originalHeight) / (width * height), 1.5); - console.log(`[Estimated time at full resolution] ~${estimatedFullResTime.toFixed(0)}ms`); - console.log( - `[Time saved] ~${(estimatedFullResTime - totalTime).toFixed(0)}ms (${Math.round(estimatedFullResTime / totalTime)}× faster)` + const blobContentTypePromise = isBlob && fetch(file).then((res) => res.headers.get('Content-Type')); + const img = new Image(); + img.crossOrigin = 'anonymous'; + const totalStartTime = performance.now(); + + img.onload = async () => { + // Force SVG to load at a high fidelity size if it's an SVG + let isSVG; + + const blobContentType = await blobContentTypePromise; + + if (blobContentType) { + isSVG = blobContentType === 'image/svg+xml'; + } else if (typeof file === 'string') { + isSVG = file.endsWith('.svg') || file.startsWith('data:image/svg+xml'); + } else { + isSVG = file.type === 'image/svg+xml'; + } + + let originalWidth = img.width || img.naturalWidth; + let originalHeight = img.height || img.naturalHeight; + + if (isSVG) { + // Scale SVG to max dimension while preserving aspect ratio + const svgMaxSize = 4096; + const aspectRatio = originalWidth / originalHeight; + + if (originalWidth > originalHeight) { + originalWidth = svgMaxSize; + originalHeight = svgMaxSize / aspectRatio; + } else { + originalHeight = svgMaxSize; + originalWidth = svgMaxSize * aspectRatio; + } + + img.width = originalWidth; + img.height = originalHeight; + } + + // Always scale to working resolution for consistency + const minDimension = Math.min(originalWidth, originalHeight); + const targetSize = POISSON_CONFIG_OPTIMIZED.workingSize; + + // Calculate scale to fit within workingSize + const scaleFactor = targetSize / minDimension; + const width = Math.round(originalWidth * scaleFactor); + const height = Math.round(originalHeight * scaleFactor); + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Processing Mode]`); + console.log(` Original: ${ originalWidth }×${ originalHeight }`); + console.log(` Working: ${ width }×${ height } (${ (scaleFactor * 100).toFixed(1) }% scale)`); + if (scaleFactor < 1) { + console.log(` Speedup: ~${ Math.round(1 / (scaleFactor * scaleFactor)) }×`); + } + } + + canvas.width = originalWidth; + canvas.height = originalHeight; + + // Use a smaller canvas for shape detection and Poisson solving + const shapeCanvas = document.createElement('canvas'); + shapeCanvas.width = width; + shapeCanvas.height = height; + + const shapeCtx = shapeCanvas.getContext('2d')!; + shapeCtx.drawImage(img, 0, 0, width, height); + + // 1) Build optimized masks using TypedArrays + const startMask = performance.now(); + + const shapeImageData = shapeCtx.getImageData(0, 0, width, height); + const data = shapeImageData.data; + + // Use Uint8Array for masks (1 byte per pixel vs 8+ bytes for boolean array) + const shapeMask = new Uint8Array(width * height); + const boundaryMask = new Uint8Array(width * height); + + // First pass: identify shape pixels + let shapePixelCount = 0; + for (let i = 0, idx = 0; i < data.length; i += 4, idx++) { + const a = data[i + 3]; + const isShape = a === 0 ? 0 : 1; + shapeMask[idx] = isShape; + shapePixelCount += isShape; + } + + // 2) Optimized boundary detection using sparse approach + // Only check shape pixels, not all pixels + const boundaryIndices: number[] = []; + const interiorIndices: number[] = []; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + if (!shapeMask[idx]) continue; + + // Check if pixel is on boundary (optimized: early exit) + let isBoundary = false; + + // Check 4-connected neighbors first (most common case) + if (x === 0 || x === width - 1 || y === 0 || y === height - 1) { + isBoundary = true; + } else { + // Check all 8 neighbors (including diagonals) for comprehensive boundary detection + isBoundary = + !shapeMask[idx - 1] || // left + !shapeMask[idx + 1] || // right + !shapeMask[idx - width] || // top + !shapeMask[idx + width] || // bottom + !shapeMask[idx - width - 1] || // top-left + !shapeMask[idx - width + 1] || // top-right + !shapeMask[idx + width - 1] || // bottom-left + !shapeMask[idx + width + 1]; // bottom-right + } + + if (isBoundary) { + boundaryMask[idx] = 1; + boundaryIndices.push(idx); + } else { + interiorIndices.push(idx); + } + } + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Mask Building] Time: ${ (performance.now() - startMask).toFixed(2) }ms`); + console.log( + ` Shape pixels: ${ shapePixelCount } / ${ width * height } (${ ((shapePixelCount / (width * height)) * 100).toFixed(1) }%)` + ); + console.log(` Interior pixels: ${ interiorIndices.length }`); + console.log(` Boundary pixels: ${ boundaryIndices.length }`); + } + + // 3) Precompute sparse data structure for solver + const sparseData = buildSparseData( + shapeMask, + boundaryMask, + new Uint32Array(interiorIndices), + new Uint32Array(boundaryIndices), + width, + height ); - } - } - resolve({ - imageData: finalImageData, - pngBlob: blob, - }); - }, 'image/png'); - }; - - img.onerror = () => reject(new Error('Failed to load image')); - img.src = typeof file === 'string' ? file : URL.createObjectURL(file); - }); + // 4) Solve Poisson equation with optimized sparse solver + const startSolve = performance.now(); + const u = solvePoissonSparse(sparseData, shapeMask, boundaryMask, width, height); + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Poisson Solve] Time: ${ (performance.now() - startSolve).toFixed(2) }ms`); + } + + // 5) Generate output image + let maxVal = 0; + let finalImageData: ImageData; + + // Only check shape pixels for max value + for (let i = 0; i < interiorIndices.length; i++) { + const idx = interiorIndices[i]!; + if (u[idx]! > maxVal) maxVal = u[idx]!; + } + + // Create gradient image at working resolution + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = width; + tempCanvas.height = height; + const tempCtx = tempCanvas.getContext('2d')!; + + const tempImg = tempCtx.createImageData(width, height); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + const px = idx * 4; + + if (!shapeMask[idx]) { + tempImg.data[px] = 255; + tempImg.data[px + 1] = 255; + tempImg.data[px + 2] = 255; + tempImg.data[px + 3] = 0; // Alpha = 0 for background + } else { + const poissonRatio = u[idx]! / maxVal; + const gray = 255 * (1 - poissonRatio); + tempImg.data[px] = gray; + tempImg.data[px + 1] = gray; + tempImg.data[px + 2] = gray; + tempImg.data[px + 3] = 255; // Alpha = 255 for shape + } + } + } + tempCtx.putImageData(tempImg, 0, 0); + + // Upscale to original resolution with smooth interpolation + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, originalWidth, originalHeight); + + // Now get the upscaled image data for final output + const outImg = ctx.getImageData(0, 0, originalWidth, originalHeight); + + // Re-apply edges from original resolution with anti-aliasing + // This ensures edges are pixel-perfect while gradient is smooth + const originalCanvas = document.createElement('canvas'); + originalCanvas.width = originalWidth; + originalCanvas.height = originalHeight; + const originalCtx = originalCanvas.getContext('2d')!; + // originalCtx.fillStyle = "white"; + // originalCtx.fillRect(0, 0, originalWidth, originalHeight); + originalCtx.drawImage(img, 0, 0, originalWidth, originalHeight); + const originalData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); + + // Process each pixel: Red channel = gradient, Alpha channel = original alpha + for (let i = 0; i < outImg.data.length; i += 4) { + const a = originalData.data[i + 3]!; + // Use only alpha to determine background vs shape + const upscaledAlpha = outImg.data[i + 3]!; + if (a === 0) { + // Background pixel + outImg.data[i] = 255; + outImg.data[i + 1] = 0; + } else { + // Red channel carries the gradient + // Check if upscale missed this pixel by looking at alpha channel + // If upscaled alpha is 0, the low-res version thought this was background + outImg.data[i] = upscaledAlpha === 0 ? 0 : outImg.data[i]!; // gradient or 0 + outImg.data[i + 1] = a; // original alpha + } + + // Unused channels fixed + outImg.data[i + 2] = 255; + outImg.data[i + 3] = 255; + } + + ctx.putImageData(outImg, 0, 0); + finalImageData = outImg; + canvas.toBlob((blob) => { + if (!blob) { + reject(new Error('Failed to create PNG blob')); + return; + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + const totalTime = performance.now() - totalStartTime; + console.log(`[Total Processing Time] ${ totalTime.toFixed(2) }ms`); + if (scaleFactor < 1) { + const estimatedFullResTime = totalTime * Math.pow((originalWidth * originalHeight) / (width * height), 1.5); + console.log(`[Estimated time at full resolution] ~${ estimatedFullResTime.toFixed(0) }ms`); + console.log( + `[Time saved] ~${ (estimatedFullResTime - totalTime).toFixed(0) }ms (${ Math.round(estimatedFullResTime / totalTime) }× faster)` + ); + } + } + + resolve({ + imageData: finalImageData, + pngBlob: blob, + }); + }, 'image/png'); + }; + + img.onerror = () => reject(new Error('Failed to load image')); + img.src = typeof file === 'string' ? file : URL.createObjectURL(file); + }); } function buildSparseData( - shapeMask: Uint8Array, - boundaryMask: Uint8Array, - interiorPixels: Uint32Array, - boundaryPixels: Uint32Array, - width: number, - height: number + shapeMask: Uint8Array, + boundaryMask: Uint8Array, + interiorPixels: Uint32Array, + boundaryPixels: Uint32Array, + width: number, + height: number ): SparsePixelData { - const pixelCount = interiorPixels.length; - - // Build neighbor indices for sparse processing - // For each interior pixel, store indices of its 4 neighbors - // Use -1 for out-of-bounds or non-shape neighbors - const neighborIndices = new Int32Array(pixelCount * 4); - - for (let i = 0; i < pixelCount; i++) { - const idx = interiorPixels[i]!; - const x = idx % width; - const y = Math.floor(idx / width); - - // East neighbor - neighborIndices[i * 4 + 0] = x < width - 1 && shapeMask[idx + 1] ? idx + 1 : -1; - // West neighbor - neighborIndices[i * 4 + 1] = x > 0 && shapeMask[idx - 1] ? idx - 1 : -1; - // North neighbor - neighborIndices[i * 4 + 2] = y > 0 && shapeMask[idx - width] ? idx - width : -1; - // South neighbor - neighborIndices[i * 4 + 3] = y < height - 1 && shapeMask[idx + width] ? idx + width : -1; - } + const pixelCount = interiorPixels.length; + + // Build neighbor indices for sparse processing + // For each interior pixel, store indices of its 4 neighbors + // Use -1 for out-of-bounds or non-shape neighbors + const neighborIndices = new Int32Array(pixelCount * 4); + + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + const x = idx % width; + const y = Math.floor(idx / width); + + // East neighbor + neighborIndices[i * 4 + 0] = x < width - 1 && shapeMask[idx + 1] ? idx + 1 : -1; + // West neighbor + neighborIndices[i * 4 + 1] = x > 0 && shapeMask[idx - 1] ? idx - 1 : -1; + // North neighbor + neighborIndices[i * 4 + 2] = y > 0 && shapeMask[idx - width] ? idx - width : -1; + // South neighbor + neighborIndices[i * 4 + 3] = y < height - 1 && shapeMask[idx + width] ? idx + width : -1; + } - return { - interiorPixels, - boundaryPixels, - pixelCount, - neighborIndices, - }; + return { + interiorPixels, + boundaryPixels, + pixelCount, + neighborIndices, + }; } function solvePoissonSparse( - sparseData: SparsePixelData, - shapeMask: Uint8Array, - boundaryMask: Uint8Array, - width: number, - height: number + sparseData: SparsePixelData, + shapeMask: Uint8Array, + boundaryMask: Uint8Array, + width: number, + height: number ): Float32Array { - // This controls how smooth the falloff gradient will be and extend into the shape - const ITERATIONS = POISSON_CONFIG_OPTIMIZED.iterations; + // This controls how smooth the falloff gradient will be and extend into the shape + const ITERATIONS = POISSON_CONFIG_OPTIMIZED.iterations; - // Keep C constant - only iterations control gradient spread - const C = 0.01; + // Keep C constant - only iterations control gradient spread + const C = 0.01; - const u = new Float32Array(width * height); - const { interiorPixels, neighborIndices, pixelCount } = sparseData; + const u = new Float32Array(width * height); + const {interiorPixels, neighborIndices, pixelCount} = sparseData; - // Performance tracking - const startTime = performance.now(); + // Performance tracking + const startTime = performance.now(); - // Red-Black SOR for better symmetry with fewer iterations - // omega between 1.8-1.95 typically gives best convergence for Poisson - const omega = 1.9; + // Red-Black SOR for better symmetry with fewer iterations + // omega between 1.8-1.95 typically gives best convergence for Poisson + const omega = 1.9; - // Pre-classify pixels as red or black for efficient processing - const redPixels: number[] = []; - const blackPixels: number[] = []; + // Pre-classify pixels as red or black for efficient processing + const redPixels: number[] = []; + const blackPixels: number[] = []; - for (let i = 0; i < pixelCount; i++) { - const idx = interiorPixels[i]!; - const x = idx % width; - const y = Math.floor(idx / width); + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + const x = idx % width; + const y = Math.floor(idx / width); - if ((x + y) % 2 === 0) { - redPixels.push(i); - } else { - blackPixels.push(i); + if ((x + y) % 2 === 0) { + redPixels.push(i); + } else { + blackPixels.push(i); + } } - } - for (let iter = 0; iter < ITERATIONS; iter++) { - // Red pass: update red pixels - for (const i of redPixels) { - const idx = interiorPixels[i]!; - - // Get precomputed neighbor indices - const eastIdx = neighborIndices[i * 4 + 0]!; - const westIdx = neighborIndices[i * 4 + 1]!; - const northIdx = neighborIndices[i * 4 + 2]!; - const southIdx = neighborIndices[i * 4 + 3]!; - - // Sum neighbors (use 0 for out-of-bounds) - let sumN = 0; - if (eastIdx >= 0) sumN += u[eastIdx]!; - if (westIdx >= 0) sumN += u[westIdx]!; - if (northIdx >= 0) sumN += u[northIdx]!; - if (southIdx >= 0) sumN += u[southIdx]!; - - // SOR update: blend new value with old value - const newValue = (C + sumN) / 4; - u[idx] = omega * newValue + (1 - omega) * u[idx]!; - } + for (let iter = 0; iter < ITERATIONS; iter++) { + // Red pass: update red pixels + for (const i of redPixels) { + const idx = interiorPixels[i]!; + + // Get precomputed neighbor indices + const eastIdx = neighborIndices[i * 4 + 0]!; + const westIdx = neighborIndices[i * 4 + 1]!; + const northIdx = neighborIndices[i * 4 + 2]!; + const southIdx = neighborIndices[i * 4 + 3]!; + + // Sum neighbors (use 0 for out-of-bounds) + let sumN = 0; + if (eastIdx >= 0) sumN += u[eastIdx]!; + if (westIdx >= 0) sumN += u[westIdx]!; + if (northIdx >= 0) sumN += u[northIdx]!; + if (southIdx >= 0) sumN += u[southIdx]!; + + // SOR update: blend new value with old value + const newValue = (C + sumN) / 4; + u[idx] = omega * newValue + (1 - omega) * u[idx]!; + } - // Black pass: update black pixels - for (const i of blackPixels) { - const idx = interiorPixels[i]!; - - // Get precomputed neighbor indices - const eastIdx = neighborIndices[i * 4 + 0]!; - const westIdx = neighborIndices[i * 4 + 1]!; - const northIdx = neighborIndices[i * 4 + 2]!; - const southIdx = neighborIndices[i * 4 + 3]!; - - // Sum neighbors (use 0 for out-of-bounds) - let sumN = 0; - if (eastIdx >= 0) sumN += u[eastIdx]!; - if (westIdx >= 0) sumN += u[westIdx]!; - if (northIdx >= 0) sumN += u[northIdx]!; - if (southIdx >= 0) sumN += u[southIdx]!; - - // SOR update: blend new value with old value - const newValue = (C + sumN) / 4; - u[idx] = omega * newValue + (1 - omega) * u[idx]!; + // Black pass: update black pixels + for (const i of blackPixels) { + const idx = interiorPixels[i]!; + + // Get precomputed neighbor indices + const eastIdx = neighborIndices[i * 4 + 0]!; + const westIdx = neighborIndices[i * 4 + 1]!; + const northIdx = neighborIndices[i * 4 + 2]!; + const southIdx = neighborIndices[i * 4 + 3]!; + + // Sum neighbors (use 0 for out-of-bounds) + let sumN = 0; + if (eastIdx >= 0) sumN += u[eastIdx]!; + if (westIdx >= 0) sumN += u[westIdx]!; + if (northIdx >= 0) sumN += u[northIdx]!; + if (southIdx >= 0) sumN += u[southIdx]!; + + // SOR update: blend new value with old value + const newValue = (C + sumN) / 4; + u[idx] = omega * newValue + (1 - omega) * u[idx]!; + } } - } - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - const elapsed = performance.now() - startTime; + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + const elapsed = performance.now() - startTime; - console.log(`[Optimized Poisson Solver (SOR ω=${omega})]`); - console.log(` Working size: ${width}×${height}`); - console.log(` Iterations: ${ITERATIONS}`); - console.log(` Time: ${elapsed.toFixed(2)}ms`); - console.log(` Interior pixels processed: ${pixelCount}`); - console.log(` Speed: ${((ITERATIONS * pixelCount) / (elapsed * 1000)).toFixed(2)} Mpixels/sec`); - } + console.log(`[Optimized Poisson Solver (SOR ω=${ omega })]`); + console.log(` Working size: ${ width }×${ height }`); + console.log(` Iterations: ${ ITERATIONS }`); + console.log(` Time: ${ elapsed.toFixed(2) }ms`); + console.log(` Interior pixels processed: ${ pixelCount }`); + console.log(` Speed: ${ ((ITERATIONS * pixelCount) / (elapsed * 1000)).toFixed(2) } Mpixels/sec`); + } - return u; + return u; } export interface FoldsUniforms extends ShaderSizingUniforms { - u_colorBack: [number, number, number, number]; - u_colorFront: [number, number, number, number]; - u_image: HTMLImageElement | string | undefined; - u_repetition: number; - u_shiftRed: number; - u_shiftBlue: number; - u_contour: number; - u_softness: number; - u_distortion: number; - u_angle: number; - u_shape: (typeof FoldsShapes)[FoldsShape]; - u_isImage: boolean; + u_colorBack: [number, number, number, number]; + u_colorFront: [number, number, number, number]; + u_image: HTMLImageElement | string | undefined; + u_repetition: number; + u_shiftRed: number; + u_shiftBlue: number; + u_contour: number; + u_softness: number; + u_distortion: number; + u_angle: number; + u_shape: (typeof FoldsShapes)[FoldsShape]; + u_isImage: boolean; } export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { - colorBack?: string; - colorFront?: string; - image?: HTMLImageElement | string | undefined; - repetition?: number; - shiftRed?: number; - shiftBlue?: number; - contour?: number; - softness?: number; - distortion?: number; - angle?: number; - shape?: FoldsShape; + colorBack?: string; + colorFront?: string; + image?: HTMLImageElement | string | undefined; + repetition?: number; + shiftRed?: number; + shiftBlue?: number; + contour?: number; + softness?: number; + distortion?: number; + angle?: number; + shape?: FoldsShape; } export const FoldsShapes = { - none: 0, - circle: 1, - daisy: 2, - diamond: 3, - metaballs: 4, + none: 0, + circle: 1, + daisy: 2, + diamond: 3, + metaballs: 4, } as const; export type FoldsShape = keyof typeof FoldsShapes; From 777ade9b8f262aa2759cc2ba81e72deb210a45a1 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 10 Nov 2025 00:25:18 +0100 Subject: [PATCH 04/84] draft --- packages/shaders/src/shaders/folds.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index d4f252923..d56e2a94d 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -149,18 +149,24 @@ void main() { float test = .9; - float wave = 2. * (.7 * cos(.3 * p.x + .1 * p.y + u_time) - 0.6 * sin(.6 * p.y + u_time)); - float addon = mix((1. - edge) * wave, 0., pow(edge, 8.)); + float wave = (.3 * cos(.3 * p.x + .2 * p.y + u_time) - .6 * sin(.6 * p.y + u_time)); + wave = .5 + .5 * wave; + float addon = (1. - edge) * wave; + addon = mix(addon, 0., pow(edge, 8.)); p.y -= addon; vec2 d = abs(fract(p) - .5); vec2 aa = 2. * fwidth(p); - float w = .2 * (1. - edge); + float w = 0.; + w += (.5 - aa.y) * (1. - edge); +// aa *= alpha; - vec2 gx = 1.0 - smoothstep(vec2(w) +vec2(0.), vec2(w) + aa, d); - float grid = gx.y; + float line = d.y; + line = 1.0 - smoothstep(w, w + aa.y, line); +// line -= fract(p.y); +// line = clamp(line, 0., 1.); - color = mix(u_colorBack.rgb, u_colorFront.rgb, grid); + color = mix(u_colorBack.rgb, u_colorFront.rgb, line); fragColor = vec4(color, 1.); From 41df7b3a1185893cea12f85d8b4b640c4ce26a09 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 10 Nov 2025 14:19:18 +0100 Subject: [PATCH 05/84] test logos rotation --- docs/public/images/logos/audi.svg | 5 + docs/public/images/logos/biron.png | Bin 0 -> 6765 bytes docs/public/images/logos/chanel.svg | 4 + docs/public/images/logos/cibc.svg | 1 + docs/public/images/logos/cloudflare.svg | 5 + docs/public/images/logos/discord.svg | 4 + docs/public/images/logos/enterprise-rent.svg | 1 + docs/public/images/logos/kfc.svg | 1 + docs/public/images/logos/l-oreal.svg | 1 + docs/public/images/logos/microsoft.svg | 1 + docs/public/images/logos/nasa.svg | 6 + docs/public/images/logos/netflix.svg | 1 + docs/public/images/logos/nike.svg | 4 + docs/public/images/logos/perkins.svg | 126 +++++++++++++++++++ docs/public/images/logos/pizza-hut.svg | 1 + docs/public/images/logos/remix.svg | 5 + docs/public/images/logos/rogers.svg | 12 ++ docs/public/images/logos/vacasa.svg | 40 ++++++ docs/public/images/logos/vercel.svg | 4 + docs/public/images/logos/volkswagen.svg | 4 + docs/public/images/logos/yum.svg | 1 + docs/src/app/(shaders)/folds/page.tsx | 45 ++++++- 22 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 docs/public/images/logos/audi.svg create mode 100644 docs/public/images/logos/biron.png create mode 100644 docs/public/images/logos/chanel.svg create mode 100644 docs/public/images/logos/cibc.svg create mode 100644 docs/public/images/logos/cloudflare.svg create mode 100644 docs/public/images/logos/discord.svg create mode 100644 docs/public/images/logos/enterprise-rent.svg create mode 100644 docs/public/images/logos/kfc.svg create mode 100644 docs/public/images/logos/l-oreal.svg create mode 100644 docs/public/images/logos/microsoft.svg create mode 100644 docs/public/images/logos/nasa.svg create mode 100644 docs/public/images/logos/netflix.svg create mode 100644 docs/public/images/logos/nike.svg create mode 100644 docs/public/images/logos/perkins.svg create mode 100644 docs/public/images/logos/pizza-hut.svg create mode 100644 docs/public/images/logos/remix.svg create mode 100644 docs/public/images/logos/rogers.svg create mode 100644 docs/public/images/logos/vacasa.svg create mode 100644 docs/public/images/logos/vercel.svg create mode 100644 docs/public/images/logos/volkswagen.svg create mode 100644 docs/public/images/logos/yum.svg diff --git a/docs/public/images/logos/audi.svg b/docs/public/images/logos/audi.svg new file mode 100644 index 000000000..3d47a8950 --- /dev/null +++ b/docs/public/images/logos/audi.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/docs/public/images/logos/biron.png b/docs/public/images/logos/biron.png new file mode 100644 index 0000000000000000000000000000000000000000..33334bda16bdf45db26f0ae2a0813113e8923254 GIT binary patch literal 6765 zcmd^E_ghn2(+xe+k={YB(o3Xwf)Z4UC`D9i=v`WZbfOd?3JL;-s`L)h3`mutqC{Hg zf&_^mC82k|!@b{s@t$Au%zk!e&slS3oi%fwnVA|g(eu%RKp>`@H?H3WfvDgh5QP?) z7Fc=LK&b`%@CF%J2buf11|b~-TtHA~Ke&s?O&>=$m%A>G&X4`SxB#2@SZ-d2T7*ok z&gv%Ej_1&k2nHsJDRoBuDdH2C<+#EaTd9Zte6bx4F@B!|y&c1GA?dQRe`2(&NUz5C z8;&;v2hS+P6-70zkNm1g&4C7eBC{^9%qb4h@EB627^G!e>ToeaeuKYF-2;*gw5RSUu+P-~)lEArejz_7bg_ zYk&cUn|+2cJF$Jef}sL|pp0L6zb6|}@`6Apo(AsEa4N=RE)YnM^$DuaqpqyKxcFb= z{UA}#HHh(iKYb!AmpSKJH5e2vLQB(I{~3<#t;Yaq23#c)lWm8AUBDUm>pWEFZV=rQ z#jgMFL!2dw_Izytp7^e6jRM5llV3Yw;%}k|0*NF*)eD@rnEwq{PH0yP|DT)mI}i{k zQKR^RhF?W|&4Kjt)^mkE?U1&D1Aj!M&5v0S-~^mS6~f}Bo@pSVIW+VLVaG}Z%I7d= zmvr=tG!87)Qr8vA5|$rK&Uq2r`^<<22)i$3Z7_Lci$HG3jKq$|j0gR6p#b74pk?b+ zA@-9;Pe(iNynk}(9Fo^G?*q38*aL_cIXF8&5(KhV&I1Ir8Wg6Z6UEns_Dk^S}y&>gKVl z+5cwY3suU$V1BqSrK;^;h&?wPpa2X2*4?Lt7tTS-ugt;#bqFMBYJ~bcLx7?6$_WFC zj#s*c^mVS!Wscl`Y%KayH&lBp$a3d-rI7cYu}m<0IF~8hin=fEvv^wf5);+X|NiVb%OCdffV^529w1mMJVQ>u@aZqE zv;oZn7QWx=QJqm)OZkta&su9@BGRT8Vq59wn^5yGou}T3Tq`?(-29G? z6Kc9`^YuhjfAYluB^dt0cgU){QtxHG?R-SGj76;va$j49s=qL1JiyHT*(EZJ1vUBp zgZ5YqX(vA=L`Aq>;4@;F1pC@Fvf?XatE2D!Oedh&DZ-ie>mYFtA&R}zPk5<HhRZ94<~wgM`SZ?;rO1|81Ye%W zzPX0>Rw7T~h+wBwm_1vfZ?lEPi-3772E!Ypx#EQdaPeljHUS;q3DxOq zd0cSfJXu|k`*EpJ;InUu<~V=%NyPGir1n<$E%_6rqN=m|=;H_i%^FICmc_%SHj|_0 z&`p**^`7Qkv!=25UAo#YdL4As?_Ir@BCoEK-A+Aq@HVDGH_dd3MG^Qo!0R3j7YB%qD%wJ>mGgrXA--aam#Kalg< zwBxY0m3uGt8E3VBlwn@8dU0>QHqutpQt-x=F9{ICot%5d=GR#ZZh6Hr3`*pdVA{62 zKK>OG9N~--;z(!l%U-clZOTQ{`-rzn25>;G?w%y6kbvsfxMx0DcJCqP)dJ*`%$i`~^qp3r_tbwMauj?C9Q! z(4+~XPnOu$Y_%2Uw1rl|d-o<-3si*K10{HOh?R!;AvUmIcyUAyN~AjcAYKzZ^$5JB zRXG-0mrB1WRTJTjs4tVGKRDR?N>gav aDII>YxwpfB5uGBMneX3z*=r1BE7GZod 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 literal 0 HcmV?d00001 diff --git a/docs/public/images/logos/chanel.svg b/docs/public/images/logos/chanel.svg new file mode 100644 index 000000000..6c581b385 --- /dev/null +++ b/docs/public/images/logos/chanel.svg @@ -0,0 +1,4 @@ + + + diff --git a/docs/public/images/logos/cibc.svg b/docs/public/images/logos/cibc.svg new file mode 100644 index 000000000..52c26b08e --- /dev/null +++ b/docs/public/images/logos/cibc.svg @@ -0,0 +1 @@ +cibc logo \ 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 000000000..c66e12a4e --- /dev/null +++ b/docs/public/images/logos/cloudflare.svg @@ -0,0 +1,5 @@ + + + + diff --git a/docs/public/images/logos/discord.svg b/docs/public/images/logos/discord.svg new file mode 100644 index 000000000..055920e0f --- /dev/null +++ b/docs/public/images/logos/discord.svg @@ -0,0 +1,4 @@ + + + diff --git a/docs/public/images/logos/enterprise-rent.svg b/docs/public/images/logos/enterprise-rent.svg new file mode 100644 index 000000000..d0c9e1431 --- /dev/null +++ b/docs/public/images/logos/enterprise-rent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/images/logos/kfc.svg b/docs/public/images/logos/kfc.svg new file mode 100644 index 000000000..c6a61d7d5 --- /dev/null +++ b/docs/public/images/logos/kfc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/images/logos/l-oreal.svg b/docs/public/images/logos/l-oreal.svg new file mode 100644 index 000000000..e864706c7 --- /dev/null +++ b/docs/public/images/logos/l-oreal.svg @@ -0,0 +1 @@ + \ 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 000000000..7a3eaf1d3 --- /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/nasa.svg b/docs/public/images/logos/nasa.svg new file mode 100644 index 000000000..da3a8f8cc --- /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 000000000..e2736b838 --- /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 000000000..c3d51dac0 --- /dev/null +++ b/docs/public/images/logos/nike.svg @@ -0,0 +1,4 @@ + + + diff --git a/docs/public/images/logos/perkins.svg b/docs/public/images/logos/perkins.svg new file mode 100644 index 000000000..101b0f8ca --- /dev/null +++ b/docs/public/images/logos/perkins.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + diff --git a/docs/public/images/logos/pizza-hut.svg b/docs/public/images/logos/pizza-hut.svg new file mode 100644 index 000000000..bb2687bc9 --- /dev/null +++ b/docs/public/images/logos/pizza-hut.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/images/logos/remix.svg b/docs/public/images/logos/remix.svg new file mode 100644 index 000000000..f5bafe66d --- /dev/null +++ b/docs/public/images/logos/remix.svg @@ -0,0 +1,5 @@ + + + + diff --git a/docs/public/images/logos/rogers.svg b/docs/public/images/logos/rogers.svg new file mode 100644 index 000000000..c0971a685 --- /dev/null +++ b/docs/public/images/logos/rogers.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/docs/public/images/logos/vacasa.svg b/docs/public/images/logos/vacasa.svg new file mode 100644 index 000000000..d6b01a11b --- /dev/null +++ b/docs/public/images/logos/vacasa.svg @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/docs/public/images/logos/vercel.svg b/docs/public/images/logos/vercel.svg new file mode 100644 index 000000000..8f9d98619 --- /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 000000000..aed78eef3 --- /dev/null +++ b/docs/public/images/logos/volkswagen.svg @@ -0,0 +1,4 @@ + + + diff --git a/docs/public/images/logos/yum.svg b/docs/public/images/logos/yum.svg new file mode 100644 index 000000000..c9fcc7348 --- /dev/null +++ b/docs/public/images/logos/yum.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index c7626dad3..d33824d98 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -8,7 +8,7 @@ import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; import { FoldsShapes, FoldsShape } from '@paper-design/shaders'; import { ShaderFit } from '@paper-design/shaders'; import { levaDeleteImageButton, levaImageButton } from '@/helpers/leva-image-button'; -import { useState, Suspense } from 'react'; +import {useState, Suspense, useEffect, useCallback} from 'react'; import { ShaderDetails } from '@/components/shader-details'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; @@ -20,9 +20,50 @@ import { toHsla } from '@/helpers/color-utils'; const { worldWidth, worldHeight, ...defaults } = foldsPresets[0].params; +const imageFiles = [ + 'audi.svg', + 'biron.png', + 'chanel.svg', + 'cibc.svg', + 'cloudflare.svg', + 'diamond.svg', + 'discord.svg', + 'enterprise-rent.svg', + 'kfc.svg', + 'l-oreal.svg', + 'microsoft.svg', + 'nasa.svg', + 'netflix.svg', + 'nike.svg', + 'paper.svg', + 'perkins.svg', + 'pizza-hut.svg', + 'remix.svg', + 'rogers.svg', + 'vacasa.svg', + 'vercel.svg', + 'volkswagen.svg', + 'apple.svg', +] as const; + + const FoldsWithControls = () => { + const [imageIdx, setImageIdx] = useState(-1); const [image, setImage] = useState('/images/logos/apple.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(() => Math.floor(Math.random() * imageFiles.length)); + }, []); + const [params, setParams] = useControls(() => { const presets = Object.fromEntries( foldsPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ @@ -71,7 +112,7 @@ const FoldsWithControls = () => { <> - + From 2272dbb129db3dd45c65b9990857b3c8085a9072 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 10 Nov 2025 17:46:18 +0100 Subject: [PATCH 06/84] new params, advanced logic, basic coloring --- docs/src/app/(shaders)/folds/page.tsx | 11 +- packages/shaders-react/src/shaders/folds.tsx | 29 ++--- packages/shaders/src/shaders/folds.ts | 110 ++++++++----------- 3 files changed, 70 insertions(+), 80 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index d33824d98..a5283d37d 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -82,11 +82,12 @@ const FoldsWithControls = () => { // }, // repetition: { value: defaults.repetition, min: 1, max: 10, order: 200 }, // softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, - // shiftRed: { value: defaults.shiftRed, min: -1, max: 1, order: 202 }, - // shiftBlue: { value: defaults.shiftBlue, min: -1, max: 1, order: 203 }, - // distortion: { value: defaults.distortion, min: 0, max: 1, order: 204 }, - // contour: { value: defaults.contour, min: 0, max: 1, order: 205 }, - // angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, + alphaMask: { value: defaults.alphaMask, order: 202 }, + size: { value: defaults.size, min: 1, max: 50, order: 203 }, + wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, + noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, + outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 205 }, + angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, speed: { value: defaults.speed, min: 0, max: 4, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 4, order: 301 }, rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index 64a64b445..8b71f5bd3 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -32,13 +32,14 @@ export const defaultPreset: FoldsPreset = { frame: 0, colorBack: '#ffffff', colorFront: '#000000', - distortion: 0.07, repetition: 2.0, - shiftRed: 0.3, - shiftBlue: 0.3, - contour: 0.4, + alphaMask: false, + size: 20, + wave: 0, + noise: 1, + outerNoise: 1, softness: 0.1, - angle: 70, + angle: 0, shape: 'diamond', }, }; @@ -51,12 +52,13 @@ export const Folds: React.FC = memo(function FoldsImpl({ speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, image = '', - contour = defaultPreset.params.contour, - distortion = defaultPreset.params.distortion, + wave = defaultPreset.params.wave, + noise = defaultPreset.params.noise, + outerNoise = defaultPreset.params.outerNoise, softness = defaultPreset.params.softness, repetition = defaultPreset.params.repetition, - shiftRed = defaultPreset.params.shiftRed, - shiftBlue = defaultPreset.params.shiftBlue, + alphaMask = defaultPreset.params.alphaMask, + size = defaultPreset.params.size, angle = defaultPreset.params.angle, shape = defaultPreset.params.shape, suspendWhenProcessingImage = false, @@ -119,12 +121,13 @@ export const Folds: React.FC = memo(function FoldsImpl({ u_colorFront: getShaderColorFromString(colorFront), u_image: processedImage, - u_contour: contour, - u_distortion: distortion, + u_wave: wave, + u_noise: noise, + u_outerNoise: outerNoise, u_softness: softness, u_repetition: repetition, - u_shiftRed: shiftRed, - u_shiftBlue: shiftBlue, + u_alphaMask: alphaMask, + u_size: size, u_angle: angle, u_isImage: Boolean(image), u_shape: FoldsShapes[shape], diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index d56e2a94d..64576f4f3 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -9,16 +9,6 @@ import {declarePI, rotation2, simplexNoise, colorBandingFix} from '../shader-uti * * Uniforms: * - u_colorBack, u_colorFront (RGBA) - * - u_repetition: density of pattern stripes - * - u_softness: blur between stripes - * - u_shiftRed & u_shiftBlue: color dispersion between the stripes - * - u_distortion: pattern distortion on the whole canvas - * - u_contour: distortion power over the shape edges - * - u_shape (float used as integer): - * ---- 0: canvas-screen rectangle, needs u_worldWidth = u_worldHeight = 0 to be responsive (see vertex shader) - * ---- 1: static circle - * ---- 2: animated flower-like polar shape - * ---- 3: animated metaballs * */ @@ -37,10 +27,11 @@ uniform vec4 u_colorFront; uniform float u_softness; uniform float u_repetition; -uniform float u_shiftRed; -uniform float u_shiftBlue; -uniform float u_distortion; -uniform float u_contour; +uniform bool u_alphaMask; +uniform float u_size; +uniform float u_wave; +uniform float u_noise; +uniform float u_outerNoise; uniform float u_angle; uniform float u_shape; @@ -54,32 +45,10 @@ ${ declarePI } ${ rotation2 } ${ simplexNoise } -float getColorChanges(float c1, float c2, float stripe_p, vec3 w, float blur, float bump, float tint) { - - float ch = mix(c2, c1, smoothstep(.0, 2. * blur, stripe_p)); - - float border = w[0]; - ch = mix(ch, c2, smoothstep(border, border + 2. * blur, stripe_p)); - - if (u_isImage == true) { - bump = smoothstep(.2, .8, bump); - } - border = w[0] + .4 * (1. - bump) * w[1]; - ch = mix(ch, c1, smoothstep(border, border + 2. * blur, stripe_p)); - - border = w[0] + .5 * (1. - bump) * w[1]; - ch = mix(ch, c2, smoothstep(border, border + 2. * blur, stripe_p)); - - border = w[0] + w[1]; - ch = mix(ch, c1, smoothstep(border, border + 2. * blur, stripe_p)); - - float gradient_t = (stripe_p - w[0] - w[1]) / w[2]; - float gradient = mix(c1, c2, smoothstep(0., 1., gradient_t)); - ch = mix(ch, gradient, smoothstep(border, border + .5 * blur, stripe_p)); - - // Tint color is applied with color burn blending - ch = mix(ch, 1. - min(1., (1. - ch) / max(tint, 0.0001)), u_colorFront.a); - return ch; +float doubleSNoise(vec2 uv, float t) { + float noise = .5 * snoise(uv - vec2(0., .3 * t)); + noise += .5 * snoise(2. * uv + vec2(0., .32 * t)); + return noise; } float getImgFrame(vec2 uv, float th) { @@ -112,8 +81,9 @@ float blurEdge3x3(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, fl return sum / norm; } -float lst(float edge0, float edge1, float x) { - return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); +vec3 hsv2rgb(vec3 c){ + vec3 p = abs(fract(c.x + vec3(0., 2./3., 1./3.))*6.-3.); + return c.z * mix(vec3(1.), clamp(p-1., 0., 1.), c.y); } void main() { @@ -144,32 +114,46 @@ void main() { alpha = img.g; float frame = getImgFrame(v_imageUV, 0.); alpha *= frame; - - vec2 p = uv * 30.; - float test = .9; + uv = v_objectUV; + vec2 p = uv - vec2(.5); + float angle = -u_angle * PI / 180.; + p = rotate(p, angle); + p *= u_size; + + + float n = doubleSNoise(uv, u_time); + float edgeAtten = (1. - (1. - u_outerNoise) * edge); + p += edgeAtten * (n - .5) * 5. * u_noise; + + p += vec2(.5); float wave = (.3 * cos(.3 * p.x + .2 * p.y + u_time) - .6 * sin(.6 * p.y + u_time)); - wave = .5 + .5 * wave; + wave *= u_wave; float addon = (1. - edge) * wave; - addon = mix(addon, 0., pow(edge, 8.)); - p.y -= addon; + if (u_alphaMask == false) { + addon = mix(addon, 0., pow(edge, 6.)); + } + p.y -= wave; vec2 d = abs(fract(p) - .5); vec2 aa = 2. * fwidth(p); float w = 0.; w += (.5 - aa.y) * (1. - edge); -// aa *= alpha; + aa *= (u_alphaMask ? alpha : 1.); float line = d.y; - line = 1.0 - smoothstep(w, w + aa.y, line); -// line -= fract(p.y); -// line = clamp(line, 0., 1.); + line = 1. - smoothstep(w, w + aa.y, line); - color = mix(u_colorBack.rgb, u_colorFront.rgb, line); +// color = mix(u_colorBack.rgb, u_colorFront.rgb, line); +// fragColor = vec4(color, 1.); - fragColor = vec4(color, 1.); + float stripeId = floor(p.y); + float hue = fract(stripeId * 0.161803); + vec3 stripeColor = hsv2rgb(vec3(hue, 0.65, 0.85)); + color = mix(u_colorBack.rgb, stripeColor, line); + fragColor = vec4(color, 1.0); } `; @@ -607,11 +591,12 @@ export interface FoldsUniforms extends ShaderSizingUniforms { u_colorFront: [number, number, number, number]; u_image: HTMLImageElement | string | undefined; u_repetition: number; - u_shiftRed: number; - u_shiftBlue: number; - u_contour: number; + u_alphaMask: boolean; + u_size: number; + u_wave: number; + u_noise: number; + u_outerNoise: number; u_softness: number; - u_distortion: number; u_angle: number; u_shape: (typeof FoldsShapes)[FoldsShape]; u_isImage: boolean; @@ -622,11 +607,12 @@ export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { colorFront?: string; image?: HTMLImageElement | string | undefined; repetition?: number; - shiftRed?: number; - shiftBlue?: number; - contour?: number; + alphaMask?: boolean; + size?: number; softness?: number; - distortion?: number; + wave?: number; + noise?: number; + outerNoise?: number; angle?: number; shape?: FoldsShape; } From 852955b947f39e9cb3b932eed5303efb1f8c956f Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 10 Nov 2025 19:56:24 +0100 Subject: [PATCH 07/84] fixing build --- docs/src/shader-defs/folds-def.ts | 89 ------------------------------- 1 file changed, 89 deletions(-) diff --git a/docs/src/shader-defs/folds-def.ts b/docs/src/shader-defs/folds-def.ts index a4733f7d5..a71847fb1 100644 --- a/docs/src/shader-defs/folds-def.ts +++ b/docs/src/shader-defs/folds-def.ts @@ -8,100 +8,11 @@ export const foldsDef: ShaderDef = { name: 'Liquid Metal', description: 'Futuristic liquid metal material applied to uploaded logo or abstract shape.', params: [ - { - name: 'colorBack', - type: 'string', - defaultValue: defaultParams.colorBack, - isColor: true, - description: 'Background color', - }, - { - name: 'colorTint', - type: 'string', - defaultValue: defaultParams.colorTint, - isColor: true, - description: 'Overlay color (color burn blending used)', - }, { name: 'image', type: 'HTMLImageElement | string', description: 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', }, - { - name: 'shape', - type: 'enum', - defaultValue: defaultParams.shape, - description: 'The predefined shape used as an effect mask when no image is provided.', - options: ['none', 'circle', 'daisy', 'metaballs'], - }, - { - name: 'repetition', - type: 'number', - min: 1, - max: 10, - defaultValue: defaultParams.repetition, - description: 'Density of pattern stripes', - }, - { - name: 'softness', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.softness, - description: 'Color transition sharpness (0 = hard edge, 1 = smooth gradient)', - }, - { - name: 'shiftRed', - type: 'number', - min: -1, - max: 1, - defaultValue: defaultParams.shiftRed, - description: 'R-channel dispersion', - }, - { - name: 'shiftBlue', - type: 'number', - min: -1, - max: 1, - defaultValue: defaultParams.shiftBlue, - description: 'B-channel dispersion', - }, - { - name: 'distortion', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.distortion, - description: 'Noise distortion over the stripes pattern', - }, - { - name: 'contour', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.contour, - description: 'Strength of the distortion on the shape edges', - }, - { - name: 'angle', - type: 'number', - defaultValue: defaultParams.angle, - min: 0, - max: 360, - description: 'The direction of pattern animation (angle relative to the shape)', - }, - // { - // name: 'isImage', - // type: 'boolean', - // description: 'TODO', - // options: ['true', 'false'], - // }, - // { - // name: 'suspendWhenProcessingImage', - // type: 'boolean', - // description: 'TODO', - // options: ['true', 'false'], - // }, ...animatedCommonParams, ], }; From 4db5009c54822a04bcfe7b3fe895cfddaeaac86e Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 11 Nov 2025 13:38:02 +0100 Subject: [PATCH 08/84] u_angle origin fix --- packages/shaders/src/shaders/folds.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 64576f4f3..96456c081 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -116,7 +116,7 @@ void main() { alpha *= frame; uv = v_objectUV; - vec2 p = uv - vec2(.5); + vec2 p = uv; float angle = -u_angle * PI / 180.; p = rotate(p, angle); p *= u_size; From 8e9af6792d7e3561ad7bf137229a5c62bdea457b Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 11 Nov 2025 22:01:37 +0100 Subject: [PATCH 09/84] sample logos update --- docs/public/images/logos/apple.svg | 8 ++-- docs/public/images/logos/audi.svg | 5 --- docs/public/images/logos/biron.png | Bin 6765 -> 0 bytes docs/public/images/logos/chanel.svg | 10 +++-- docs/public/images/logos/cibc.svg | 15 ++++++- docs/public/images/logos/cloudflare.svg | 12 +++--- docs/public/images/logos/diamond.svg | 8 ++-- docs/public/images/logos/discord.svg | 9 +++-- docs/public/images/logos/enterprise-rent.svg | 9 ++++- docs/public/images/logos/nike.svg | 8 ++-- docs/public/images/logos/paper-logo-only.svg | 5 +++ docs/public/images/logos/paper.svg | 7 ++-- docs/public/images/logos/remix.svg | 11 ++--- docs/public/images/logos/vacasa.svg | 40 ------------------- docs/public/images/logos/yum.svg | 1 - docs/src/app/(shaders)/folds/page.tsx | 18 ++++----- 16 files changed, 77 insertions(+), 89 deletions(-) delete mode 100644 docs/public/images/logos/audi.svg delete mode 100644 docs/public/images/logos/biron.png create mode 100644 docs/public/images/logos/paper-logo-only.svg delete mode 100644 docs/public/images/logos/vacasa.svg delete mode 100644 docs/public/images/logos/yum.svg diff --git a/docs/public/images/logos/apple.svg b/docs/public/images/logos/apple.svg index ea852732e..752866aa3 100644 --- a/docs/public/images/logos/apple.svg +++ b/docs/public/images/logos/apple.svg @@ -1,3 +1,5 @@ - - - + + + + \ No newline at end of file diff --git a/docs/public/images/logos/audi.svg b/docs/public/images/logos/audi.svg deleted file mode 100644 index 3d47a8950..000000000 --- a/docs/public/images/logos/audi.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/docs/public/images/logos/biron.png b/docs/public/images/logos/biron.png deleted file mode 100644 index 33334bda16bdf45db26f0ae2a0813113e8923254..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6765 zcmd^E_ghn2(+xe+k={YB(o3Xwf)Z4UC`D9i=v`WZbfOd?3JL;-s`L)h3`mutqC{Hg zf&_^mC82k|!@b{s@t$Au%zk!e&slS3oi%fwnVA|g(eu%RKp>`@H?H3WfvDgh5QP?) z7Fc=LK&b`%@CF%J2buf11|b~-TtHA~Ke&s?O&>=$m%A>G&X4`SxB#2@SZ-d2T7*ok z&gv%Ej_1&k2nHsJDRoBuDdH2C<+#EaTd9Zte6bx4F@B!|y&c1GA?dQRe`2(&NUz5C z8;&;v2hS+P6-70zkNm1g&4C7eBC{^9%qb4h@EB627^G!e>ToeaeuKYF-2;*gw5RSUu+P-~)lEArejz_7bg_ zYk&cUn|+2cJF$Jef}sL|pp0L6zb6|}@`6Apo(AsEa4N=RE)YnM^$DuaqpqyKxcFb= z{UA}#HHh(iKYb!AmpSKJH5e2vLQB(I{~3<#t;Yaq23#c)lWm8AUBDUm>pWEFZV=rQ z#jgMFL!2dw_Izytp7^e6jRM5llV3Yw;%}k|0*NF*)eD@rnEwq{PH0yP|DT)mI}i{k zQKR^RhF?W|&4Kjt)^mkE?U1&D1Aj!M&5v0S-~^mS6~f}Bo@pSVIW+VLVaG}Z%I7d= zmvr=tG!87)Qr8vA5|$rK&Uq2r`^<<22)i$3Z7_Lci$HG3jKq$|j0gR6p#b74pk?b+ zA@-9;Pe(iNynk}(9Fo^G?*q38*aL_cIXF8&5(KhV&I1Ir8Wg6Z6UEns_Dk^S}y&>gKVl z+5cwY3suU$V1BqSrK;^;h&?wPpa2X2*4?Lt7tTS-ugt;#bqFMBYJ~bcLx7?6$_WFC zj#s*c^mVS!Wscl`Y%KayH&lBp$a3d-rI7cYu}m<0IF~8hin=fEvv^wf5);+X|NiVb%OCdffV^529w1mMJVQ>u@aZqE zv;oZn7QWx=QJqm)OZkta&su9@BGRT8Vq59wn^5yGou}T3Tq`?(-29G? z6Kc9`^YuhjfAYluB^dt0cgU){QtxHG?R-SGj76;va$j49s=qL1JiyHT*(EZJ1vUBp zgZ5YqX(vA=L`Aq>;4@;F1pC@Fvf?XatE2D!Oedh&DZ-ie>mYFtA&R}zPk5<HhRZ94<~wgM`SZ?;rO1|81Ye%W zzPX0>Rw7T~h+wBwm_1vfZ?lEPi-3772E!Ypx#EQdaPeljHUS;q3DxOq zd0cSfJXu|k`*EpJ;InUu<~V=%NyPGir1n<$E%_6rqN=m|=;H_i%^FICmc_%SHj|_0 z&`p**^`7Qkv!=25UAo#YdL4As?_Ir@BCoEK-A+Aq@HVDGH_dd3MG^Qo!0R3j7YB%qD%wJ>mGgrXA--aam#Kalg< zwBxY0m3uGt8E3VBlwn@8dU0>QHqutpQt-x=F9{ICot%5d=GR#ZZh6Hr3`*pdVA{62 zKK>OG9N~--;z(!l%U-clZOTQ{`-rzn25>;G?w%y6kbvsfxMx0DcJCqP)dJ*`%$i`~^qp3r_tbwMauj?C9Q! z(4+~XPnOu$Y_%2Uw1rl|d-o<-3si*K10{HOh?R!;AvUmIcyUAyN~AjcAYKzZ^$5JB zRXG-0mrB1WRTJTjs4tVGKRDR?N>gav aDII>YxwpfB5uGBMneX3z*=r1BE7GZod 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 diff --git a/docs/public/images/logos/chanel.svg b/docs/public/images/logos/chanel.svg index 6c581b385..880fd19c0 100644 --- a/docs/public/images/logos/chanel.svg +++ b/docs/public/images/logos/chanel.svg @@ -1,4 +1,6 @@ - - - + + + + \ No newline at end of file diff --git a/docs/public/images/logos/cibc.svg b/docs/public/images/logos/cibc.svg index 52c26b08e..cfd282fa5 100644 --- a/docs/public/images/logos/cibc.svg +++ b/docs/public/images/logos/cibc.svg @@ -1 +1,14 @@ -cibc logo \ No newline at end of file + + + + + + + + + \ No newline at end of file diff --git a/docs/public/images/logos/cloudflare.svg b/docs/public/images/logos/cloudflare.svg index c66e12a4e..9f84cae72 100644 --- a/docs/public/images/logos/cloudflare.svg +++ b/docs/public/images/logos/cloudflare.svg @@ -1,5 +1,7 @@ - - - - + + + + + \ No newline at end of file diff --git a/docs/public/images/logos/diamond.svg b/docs/public/images/logos/diamond.svg index cace8a979..02564ecd9 100644 --- a/docs/public/images/logos/diamond.svg +++ b/docs/public/images/logos/diamond.svg @@ -1,3 +1,5 @@ - - - + + + + \ No newline at end of file diff --git a/docs/public/images/logos/discord.svg b/docs/public/images/logos/discord.svg index 055920e0f..7cdc5ce20 100644 --- a/docs/public/images/logos/discord.svg +++ b/docs/public/images/logos/discord.svg @@ -1,4 +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 index d0c9e1431..a86262869 100644 --- a/docs/public/images/logos/enterprise-rent.svg +++ b/docs/public/images/logos/enterprise-rent.svg @@ -1 +1,8 @@ - \ No newline at end of file + + + + + + + + \ No newline at end of file diff --git a/docs/public/images/logos/nike.svg b/docs/public/images/logos/nike.svg index c3d51dac0..7021e581a 100644 --- a/docs/public/images/logos/nike.svg +++ b/docs/public/images/logos/nike.svg @@ -1,4 +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 000000000..cf4e31380 --- /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/paper.svg b/docs/public/images/logos/paper.svg index 892f3994b..3931129da 100644 --- a/docs/public/images/logos/paper.svg +++ b/docs/public/images/logos/paper.svg @@ -1,6 +1,7 @@ - + + + fill="#000000" transform="matrix(0.9999999999999999, 0, 0, 0.9999999999999999, 0, 0)"/> + fill="#000000" transform="matrix(0.9999999999999999, 0, 0, 0.9999999999999999, 0, 0)"/> \ No newline at end of file diff --git a/docs/public/images/logos/remix.svg b/docs/public/images/logos/remix.svg index f5bafe66d..ebc2a35c0 100644 --- a/docs/public/images/logos/remix.svg +++ b/docs/public/images/logos/remix.svg @@ -1,5 +1,6 @@ - - - - + + + + \ No newline at end of file diff --git a/docs/public/images/logos/vacasa.svg b/docs/public/images/logos/vacasa.svg deleted file mode 100644 index d6b01a11b..000000000 --- a/docs/public/images/logos/vacasa.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - diff --git a/docs/public/images/logos/yum.svg b/docs/public/images/logos/yum.svg deleted file mode 100644 index c9fcc7348..000000000 --- a/docs/public/images/logos/yum.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index a5283d37d..6771d60f2 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -8,7 +8,7 @@ import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; import { FoldsShapes, FoldsShape } from '@paper-design/shaders'; import { ShaderFit } from '@paper-design/shaders'; import { levaDeleteImageButton, levaImageButton } from '@/helpers/leva-image-button'; -import {useState, Suspense, useEffect, useCallback} from 'react'; +import { useState, Suspense, useEffect, useCallback } from 'react'; import { ShaderDetails } from '@/components/shader-details'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; @@ -21,13 +21,12 @@ import { toHsla } from '@/helpers/color-utils'; const { worldWidth, worldHeight, ...defaults } = foldsPresets[0].params; const imageFiles = [ - 'audi.svg', - 'biron.png', 'chanel.svg', 'cibc.svg', 'cloudflare.svg', 'diamond.svg', 'discord.svg', + 'paper-logo-only', 'enterprise-rent.svg', 'kfc.svg', 'l-oreal.svg', @@ -40,13 +39,11 @@ const imageFiles = [ 'pizza-hut.svg', 'remix.svg', 'rogers.svg', - 'vacasa.svg', 'vercel.svg', 'volkswagen.svg', 'apple.svg', ] as const; - const FoldsWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); const [image, setImage] = useState('/images/logos/apple.svg'); @@ -55,13 +52,14 @@ const FoldsWithControls = () => { if (imageIdx >= 0) { const name = imageFiles[imageIdx]; const img = new Image(); - img.src = `/images/logos/${ name }`; + img.src = `/images/logos/${name}`; img.onload = () => setImage(img); } }, [imageIdx]); const handleClick = useCallback(() => { - setImageIdx(() => Math.floor(Math.random() * imageFiles.length)); + setImageIdx((prev) => (prev + 1) % imageFiles.length); + // setImageIdx(() => Math.floor(Math.random() * imageFiles.length)); }, []); const [params, setParams] = useControls(() => { @@ -80,7 +78,7 @@ const FoldsWithControls = () => { // order: 102, // disabled: Boolean(image), // }, - // repetition: { value: defaults.repetition, min: 1, max: 10, order: 200 }, + stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, // softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, alphaMask: { value: defaults.alphaMask, order: 202 }, size: { value: defaults.size, min: 1, max: 50, order: 203 }, @@ -89,7 +87,7 @@ const FoldsWithControls = () => { outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, speed: { value: defaults.speed, min: 0, max: 4, order: 300 }, - scale: { value: defaults.scale, min: 0.2, max: 4, order: 301 }, + scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, @@ -113,7 +111,7 @@ const FoldsWithControls = () => { <> - + From 00fbb82e08d07bd6499919c86d6b3a52af968ce7 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 11 Nov 2025 22:03:21 +0100 Subject: [PATCH 10/84] halftone lines template --- .../app/(shaders)/halftone-lines/layout.tsx | 9 + .../src/app/(shaders)/halftone-lines/page.tsx | 130 ++++++ docs/src/shader-defs/halftone-lines-def.ts | 19 + packages/shaders-react/src/index.ts | 4 + .../src/shaders/halftone-lines.tsx | 122 ++++++ packages/shaders/src/index.ts | 8 + .../shaders/src/shaders/halftone-lines.ts | 403 ++++++++++++++++++ 7 files changed, 695 insertions(+) create mode 100644 docs/src/app/(shaders)/halftone-lines/layout.tsx create mode 100644 docs/src/app/(shaders)/halftone-lines/page.tsx create mode 100644 docs/src/shader-defs/halftone-lines-def.ts create mode 100644 packages/shaders-react/src/shaders/halftone-lines.tsx create mode 100644 packages/shaders/src/shaders/halftone-lines.ts diff --git a/docs/src/app/(shaders)/halftone-lines/layout.tsx b/docs/src/app/(shaders)/halftone-lines/layout.tsx new file mode 100644 index 000000000..0e9d6122b --- /dev/null +++ b/docs/src/app/(shaders)/halftone-lines/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Halftone Lines Filter • Paper', +}; + +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx new file mode 100644 index 000000000..ba270c395 --- /dev/null +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -0,0 +1,130 @@ +'use client'; + +import { HalftoneLines, halftoneLinesPresets } from '@paper-design/shaders-react'; +import { useControls, button, folder } from 'leva'; +import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; +import { usePresetHighlight } from '@/helpers/use-preset-highlight'; +import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; +import { HalftoneLinesType, HalftoneLinesTypes, ShaderFit } from '@paper-design/shaders'; +import { levaImageButton } from '@/helpers/leva-image-button'; +import { useState, useEffect, useCallback } from 'react'; +import { toHsla } from '@/helpers/color-utils'; +import { ShaderDetails } from '@/components/shader-details'; +import { halftoneLinesDef } from '@/shader-defs/halftone-lines-def'; +import { ShaderContainer } from '@/components/shader-container'; +import { useUrlParams } from '@/helpers/use-url-params'; + +const { worldWidth, worldHeight, ...defaults } = halftoneLinesPresets[0].params; + +const imageFiles = [ + '001.webp', + '002.webp', + '003.webp', + '004.webp', + '005.webp', + '006.webp', + '007.webp', + '008.webp', + '009.webp', + '0010.webp', + '0011.webp', + '0012.webp', + '0013.webp', + '0014.webp', + '0015.webp', + '0016.webp', + '0017.webp', + '0018.webp', +] as const; + +const HalftoneLinesWithControls = () => { + const [imageIdx, setImageIdx] = useState(-1); + const [image, setImage] = useState('/images/image-filters/0018.webp'); + const [status, setStatus] = useState('Click to load an image'); + + useEffect(() => { + if (imageIdx >= 0) { + const name = imageFiles[imageIdx]; + setStatus(`Displaying image: ${name}`); + const img = new Image(); + img.src = `/images/image-filters/${name}`; + img.onload = () => setImage(img); + } + }, [imageIdx]); + + const handleClick = useCallback(() => { + setImageIdx((prev) => (prev + 1) % imageFiles.length); + }, []); + + const setImageWithoutStatus = useCallback((img?: HTMLImageElement) => { + setImage(img ?? ''); + setImageIdx(-1); + setStatus(``); + }, []); + + const [params, setParams] = useControls(() => { + const presets = Object.fromEntries( + halftoneLinesPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + name, + button(() => setParamsSafe(params, setParams, preset)), + ]) + ); + return { + stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, + // softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, + alphaMask: { value: defaults.alphaMask, order: 202 }, + wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, + noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, + angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, + + colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorFront: { value: toHsla(defaults.colorFront), order: 101 }, + originalColors: { value: defaults.originalColors, order: 102 }, + type: { + value: defaults.type, + options: Object.keys(HalftoneLinesTypes) as HalftoneLinesType[], + order: 201, + }, + inverted: { value: defaults.inverted, order: 201 }, + size: { value: defaults.size, min: 0.01, max: 150, step: 0.1, order: 300 }, + contrast: { value: defaults.contrast, min: 0.01, max: 1, order: 302 }, + grainMixer: { value: defaults.grainMixer, min: 0, max: 1, order: 350 }, + grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 351 }, + scale: { value: defaults.scale, min: 0.1, max: 10, order: 400 }, + // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 401 }, + // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 402 }, + // originX: { value: defaults.originX, min: 0, max: 1, order: 411 }, + // originY: { value: defaults.originY, min: 0, max: 1, order: 412 }, + // rotation: { value: defaults.rotation, min: 0, max: 360, order: 420 }, + fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 450 }, + Image: folder( + { + 'Upload image': levaImageButton(setImageWithoutStatus), + }, + { order: 0 } + ), + Presets: folder(presets, { order: -1 }), + }; + }); + + // Reset to defaults on mount, so that Leva doesn't show values from other + // shaders when navigating (if two shaders have a color1 param for example) + useResetLevaParams(params, setParams, defaults); + useUrlParams(params, setParams, halftoneLinesDef); + usePresetHighlight(halftoneLinesPresets, params); + cleanUpLevaParams(params); + + return ( + <> + + + +
+ Click to change the sample image +
+ + + ); +}; + +export default HalftoneLinesWithControls; diff --git a/docs/src/shader-defs/halftone-lines-def.ts b/docs/src/shader-defs/halftone-lines-def.ts new file mode 100644 index 000000000..121b9fbd0 --- /dev/null +++ b/docs/src/shader-defs/halftone-lines-def.ts @@ -0,0 +1,19 @@ +import { halftoneLinesPresets } from '@paper-design/shaders-react'; +import type { ShaderDef } from './shader-def-types'; +import { animatedCommonParams } from './common-param-def'; + +const defaultParams = halftoneLinesPresets[0].params; + +export const halftoneLinesDef: ShaderDef = { + name: 'Halftone lines', + description: 'TBD', + params: [ + { + name: 'image', + type: 'HTMLImageElement | string', + description: + 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', + }, + ...animatedCommonParams, + ], +}; diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index b8e73ab5d..760e337fc 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -111,6 +111,10 @@ export { Folds, foldsPresets } from './shaders/folds.js'; export type { FoldsProps } from './shaders/folds.js'; export type { FoldsUniforms, FoldsParams } from '@paper-design/shaders'; +export { HalftoneLines, halftoneLinesPresets } from './shaders/halftone-lines.js'; +export type { HalftoneLinesProps } from './shaders/halftone-lines.js'; +export type { HalftoneLinesUniforms, HalftoneLinesParams } from '@paper-design/shaders'; + export { isPaperShaderElement, getShaderColorFromString } from '@paper-design/shaders'; export type { PaperShaderElement, ShaderFit, ShaderSizingParams, ShaderSizingUniforms } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx new file mode 100644 index 000000000..8e55632c0 --- /dev/null +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -0,0 +1,122 @@ +import { memo } from 'react'; +import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; +import { + halftoneLinesFragmentShader, + getShaderColorFromString, + ShaderFitOptions, + type HalftoneLinesUniforms, + type HalftoneLinesParams, + defaultObjectSizing, + type ImageShaderPreset, + HalftoneLinesTypes, +} from '@paper-design/shaders'; + +export interface HalftoneLinesProps extends ShaderComponentProps, HalftoneLinesParams {} +type HalftoneLinesPreset = ImageShaderPreset; + +export const defaultPreset: HalftoneLinesPreset = { + name: 'Default', + params: { + ...defaultObjectSizing, + + scale: 1, + speed: 0.02, + frame: 0, + colorBack: '#000000', + colorFront: '#ffffff', + stripeWidth: 0.48, + alphaMask: false, + size: 40, + wave: 1, + noise: 1, + softness: 0.1, + angle: 0, + + contrast: 0.25, + originalColors: false, + inverted: false, + grainMixer: 0.2, + grainOverlay: 0.2, + type: 'gooey', + }, +}; +export const halftoneLinesPresets: HalftoneLinesPreset[] = [defaultPreset]; + +export const HalftoneLines: React.FC = memo(function HalftoneLinesImpl({ + // Own props + colorBack = defaultPreset.params.colorBack, + colorFront = defaultPreset.params.colorFront, + speed = defaultPreset.params.speed, + frame = defaultPreset.params.frame, + image = '', + wave = defaultPreset.params.wave, + noise = defaultPreset.params.noise, + softness = defaultPreset.params.softness, + stripeWidth = defaultPreset.params.stripeWidth, + alphaMask = defaultPreset.params.alphaMask, + size = defaultPreset.params.size, + angle = defaultPreset.params.angle, + type = defaultPreset.params.type, + + contrast = defaultPreset.params.contrast, + originalColors = defaultPreset.params.originalColors, + inverted = defaultPreset.params.inverted, + grainMixer = defaultPreset.params.grainMixer, + grainOverlay = defaultPreset.params.grainOverlay, + + // Sizing props + fit = defaultPreset.params.fit, + scale = defaultPreset.params.scale, + rotation = defaultPreset.params.rotation, + originX = defaultPreset.params.originX, + originY = defaultPreset.params.originY, + offsetX = defaultPreset.params.offsetX, + offsetY = defaultPreset.params.offsetY, + worldWidth = defaultPreset.params.worldWidth, + worldHeight = defaultPreset.params.worldHeight, + ...props +}: HalftoneLinesProps) { + const uniforms = { + // Own uniforms + u_colorBack: getShaderColorFromString(colorBack), + u_colorFront: getShaderColorFromString(colorFront), + + u_image: image, + u_wave: wave, + u_noise: noise, + u_softness: softness, + u_stripeWidth: stripeWidth, + u_alphaMask: alphaMask, + u_size: size, + u_angle: angle, + u_type: HalftoneLinesTypes[type], + + u_contrast: contrast, + u_originalColors: originalColors, + u_inverted: inverted, + u_grainMixer: grainMixer, + u_grainOverlay: grainOverlay, + + // Sizing uniforms + u_fit: ShaderFitOptions[fit], + u_scale: scale, + u_rotation: rotation, + u_offsetX: offsetX, + u_offsetY: offsetY, + u_originX: originX, + u_originY: originY, + u_worldWidth: worldWidth, + u_worldHeight: worldHeight, + } satisfies HalftoneLinesUniforms; + + return ( + + ); +}); diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index c9659ae87..b6e4a7462 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -219,6 +219,14 @@ export { type FoldsUniforms, } from './shaders/folds.js'; +export { + halftoneLinesFragmentShader, + HalftoneLinesTypes, + type HalftoneLinesType, + type HalftoneLinesParams, + type HalftoneLinesUniforms, +} from './shaders/halftone-lines.js'; + // ----- Utils ----- // export { getShaderColorFromString } from './get-shader-color-from-string.js'; export { getShaderNoiseTexture } from './get-shader-noise-texture.js'; diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts new file mode 100644 index 000000000..20fc99cd7 --- /dev/null +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -0,0 +1,403 @@ +import type { ShaderMotionParams } from '../shader-mount.js'; +import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; +import { declarePI, rotation2, simplexNoise, proceduralHash21 } from '../shader-utils.js'; + +/** + * + * Fluid motion imitation applied over user image + * (animated stripe pattern getting distorted with shape edges) + * + * Uniforms: + * - u_colorBack, u_colorFront (RGBA) + * + */ + +// language=GLSL +export const halftoneLinesFragmentShader: string = `#version 300 es +precision mediump float; + +uniform mediump vec2 u_resolution; +uniform mediump float u_pixelRatio; +uniform mediump float u_originX; +uniform mediump float u_originY; +uniform mediump float u_fit; + +uniform mediump float u_scale; +uniform mediump float u_rotation; +uniform mediump float u_offsetX; +uniform mediump float u_offsetY; + +uniform float u_time; + +uniform vec4 u_colorFront; +uniform vec4 u_colorBack; +uniform float u_radius; +uniform float u_contrast; + +uniform sampler2D u_image; +uniform mediump float u_imageAspectRatio; + +uniform float u_size; +uniform float u_grainMixer; +uniform float u_grainOverlay; +uniform bool u_straight; +uniform bool u_originalColors; +uniform bool u_inverted; +uniform float u_type; + +uniform float u_softness; +uniform float u_stripeWidth; +uniform bool u_alphaMask; +uniform float u_wave; +uniform float u_noise; +uniform float u_angle; + + +${sizingVariablesDeclaration} + +out vec4 fragColor; + +${declarePI} +${rotation2} +${simplexNoise} +${proceduralHash21} + +float valueNoise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + float a = hash21(i); + float b = hash21(i + vec2(1.0, 0.0)); + float c = hash21(i + vec2(0.0, 1.0)); + float d = hash21(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + float x1 = mix(a, b, u.x); + float x2 = mix(c, d, u.x); + return mix(x1, x2, u.y); +} + +float lst(float edge0, float edge1, float x) { + return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); +} + +vec2 getImageUV(vec2 uv, vec2 extraScale) { + vec2 boxOrigin = vec2(.5 - u_originX, u_originY - .5); + float r = u_rotation * PI / 180.; + mat2 graphicRotation = mat2(cos(r), sin(r), -sin(r), cos(r)); + vec2 graphicOffset = vec2(-u_offsetX, u_offsetY); + + vec2 imageBoxSize; + if (u_fit == 1.) { + imageBoxSize.x = min(u_resolution.x / u_imageAspectRatio, u_resolution.y) * u_imageAspectRatio; + } else { + imageBoxSize.x = max(u_resolution.x / u_imageAspectRatio, u_resolution.y) * u_imageAspectRatio; + } + imageBoxSize.y = imageBoxSize.x / u_imageAspectRatio; + vec2 imageBoxScale = u_resolution.xy / imageBoxSize; + + vec2 imageUV = uv; + imageUV *= imageBoxScale; + imageUV += boxOrigin * (imageBoxScale - 1.); + imageUV += graphicOffset; + imageUV /= u_scale; + imageUV *= extraScale; + imageUV.x *= u_imageAspectRatio; + imageUV = graphicRotation * imageUV; + imageUV.x /= u_imageAspectRatio; + + imageUV += .5; + imageUV.y = 1. - imageUV.y; + + return imageUV; +} + +float doubleSNoise(vec2 uv, float t) { + float noise = .5 * snoise(uv - vec2(0., .3 * t)); + noise += .5 * snoise(2. * uv + vec2(0., .32 * t)); + return noise; +} + +float getImgFrame(vec2 uv, float th) { + float frame = 1.; + frame *= smoothstep(0., th, uv.y); + frame *= 1.0 - smoothstep(1. - th, 1., uv.y); + frame *= smoothstep(0., th, uv.x); + frame *= 1.0 - smoothstep(1. - th, 1., uv.x); + return frame; +} + +float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) +{ + vec2 texel = 1.0 / vec2(textureSize(tex, 0)); + vec2 r = max(radius, 0.0) * texel; + + // 1D Gaussian coefficients (Pascal row) + const float a = 1.0;// |offset| = 2 + const float b = 4.0;// |offset| = 1 + const float c = 6.0;// |offset| = 0 + + float norm = 256.0;// (a+b+c+b+a)^2 = 16^2 + float sum = 0.0; + + // y = -2 + { + float wy = a; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, -2.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, -2.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, -2.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, -2.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, -2.0*r.y)).r; + sum += wy * row; + } + + // y = -1 + { + float wy = b; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, -1.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, -1.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, -1.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, -1.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, -1.0*r.y)).r; + sum += wy * row; + } + + // y = 0 (use provided centerSample to avoid an extra fetch) + { + float wy = c; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 0.0)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 0.0)).r + + c * centerSample + + b * texture(tex, uv + vec2(1.0*r.x, 0.0)).r + + a * texture(tex, uv + vec2(2.0*r.x, 0.0)).r; + sum += wy * row; + } + + // y = +1 + { + float wy = b; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 1.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 1.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, 1.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, 1.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, 1.0*r.y)).r; + sum += wy * row; + } + + // y = +2 + { + float wy = a; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 2.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 2.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, 2.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, 2.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, 2.0*r.y)).r; + sum += wy * row; + } + + return sum / norm; +} + +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).r; + sum += w2 * textureGrad(tex, uv + vec2(0.0, r.y), dudx, dudy).r; + sum += w2 * textureGrad(tex, uv + vec2(-r.x, 0.0), dudx, dudy).r; + sum += w2 * textureGrad(tex, uv + vec2(r.x, 0.0), dudx, dudy).r; + + sum += w1 * textureGrad(tex, uv + vec2(-r.x, -r.y), dudx, dudy).r; + sum += w1 * textureGrad(tex, uv + vec2(r.x, -r.y), dudx, dudy).r; + sum += w1 * textureGrad(tex, uv + vec2(-r.x, r.y), dudx, dudy).r; + sum += w1 * textureGrad(tex, uv + vec2(r.x, r.y), dudx, dudy).r; + + return sum / norm; +} + +float blurEdge3x3_G(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; +} + +vec3 hsv2rgb(vec3 c){ + vec3 p = abs(fract(c.x + vec3(0., 2./3., 1./3.))*6.-3.); + return c.z * mix(vec3(1.), clamp(p-1., 0., 1.), c.y); +} + +float sst(float edge0, float edge1, float x) { + return smoothstep(edge0, edge1, x); +} + +float sigmoid(float x, float k) { + return 1.0 / (1.0 + exp(-k * (x - 0.5))); +} + +float getLumAtPx(vec2 uv, float contrast) { + vec4 tex = texture(u_image, uv); + vec3 color = vec3( + sigmoid(tex.r, contrast), + sigmoid(tex.g, contrast), + sigmoid(tex.b, contrast) + ); + float lum = dot(vec3(0.2126, 0.7152, 0.0722), color); + lum = mix(1., lum, tex.a); + lum = u_inverted ? (1. - lum) : lum; + return lum; +} + +float blendOverlay(float base, float blend) { + return base<0.5?(2.0*base*blend):(1.0-2.0*(1.0-base)*(1.0-blend)); +} + +vec3 blendOverlay(vec3 base, vec3 blend) { + return vec3(blendOverlay(base.r, blend.r), blendOverlay(base.g, blend.g), blendOverlay(base.b, blend.b)); +} + +vec3 blendHardLight(vec3 base, vec3 blend) { + return blendOverlay(blend, base); +} + +vec3 blendHardLight(vec3 base, vec3 blend, float opacity) { + return (blendHardLight(base, blend) * opacity + base * (1.0 - opacity)); +} + + +void main() { + + vec2 uv = gl_FragCoord.xy - .5 * u_resolution; + + vec2 uvNormalised = uv / u_resolution.xy; + vec2 uvOriginal = getImageUV(uvNormalised, vec2(1.)); + + float contrast = mix(0., 15., u_contrast); + if (u_originalColors == true) { + contrast = mix(.1, 4., pow(u_contrast, 2.)); + } + + float lum = getLumAtPx(uvOriginal, contrast); + + + float t = .3 * (u_time); + + float edge = lum; + + vec3 color = vec3(0.); + + float opacity = 1.; + + float frame = getImgFrame(v_imageUV, 0.); + edge = mix(1., edge, frame); + + uv = v_objectUV; + vec2 p = uv; + float angle = -u_angle * PI / 180.; + p = rotate(p, angle); + p *= u_size; + + float n = doubleSNoise(uv, u_time); + + float wave = (.3 * cos(.3 * p.x + .2 * p.y + u_time) - .6 * sin(.6 * p.y + u_time)); + wave *= u_wave; + + p.y += n * 10. * u_noise; + p.y -= wave; + + vec2 d = abs(fract(p) - .5); + vec2 aa = 2. * fwidth(p); + float w = 0.; + float wMax = .5 - aa.y; + w = edge; + + w = min(w, wMax); +// float lineDist = d.y; +// float sdf = w - lineDist; +// float afwidth = fwidth(sdf); +// afwidth = max(afwidth, 1e-4); +// float line = smoothstep(0., afwidth, sdf); + + float line = d.y; + line = 1. - sst(w, w + aa.y, line); + + float stripeId = floor(p.y); + float hue = fract(stripeId * 0.161803); + vec3 stripeColor = hsv2rgb(vec3(hue, .5, .7)); + +// color = mix(u_colorBack.rgb, stripeColor, line); + color = mix(u_colorBack.rgb, u_colorFront.rgb, line); + fragColor = vec4(color, 1.); +// fragColor = vec4(vec3(lum), 1.); +} +`; + +export interface HalftoneLinesUniforms extends ShaderSizingUniforms { + u_colorBack: [number, number, number, number]; + u_colorFront: [number, number, number, number]; + u_image: HTMLImageElement | string | undefined; + u_stripeWidth: number; + u_alphaMask: boolean; + u_size: number; + u_wave: number; + u_noise: number; + u_softness: number; + u_angle: number; + u_type: (typeof HalftoneLinesTypes)[HalftoneLinesType]; + + u_contrast: number; + u_originalColors: boolean; + u_inverted: boolean; + u_grainMixer: number; + u_grainOverlay: number; +} + +export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionParams { + colorBack?: string; + colorFront?: string; + image?: HTMLImageElement | string | undefined; + stripeWidth?: number; + alphaMask?: boolean; + size?: number; + softness?: number; + wave?: number; + noise?: number; + angle?: number; + type?: HalftoneLinesType; + + contrast?: number; + originalColors?: boolean; + inverted?: boolean; + grainMixer?: number; + grainOverlay?: number; +} + +export const HalftoneLinesTypes = { + classic: 0, + gooey: 1, + holes: 2, + soft: 3, +} as const; + +export type HalftoneLinesType = keyof typeof HalftoneLinesTypes; From 81c38af6aa8302004a09b932a7a311cca3c8e3d1 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 11 Nov 2025 22:36:33 +0100 Subject: [PATCH 11/84] u_colors support --- docs/src/app/(shaders)/folds/page.tsx | 52 +- docs/src/shader-defs/folds-def.ts | 3 +- packages/shaders-react/src/index.ts | 1 + packages/shaders-react/src/shaders/folds.tsx | 30 +- packages/shaders/src/index.ts | 1 + packages/shaders/src/shaders/folds.ts | 1045 ++++++++++-------- 6 files changed, 644 insertions(+), 488 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index 6771d60f2..f8bb3bc1d 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -5,15 +5,16 @@ import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { FoldsShapes, FoldsShape } from '@paper-design/shaders'; +import { FoldsShapes, FoldsShape, colorPanelsMeta, foldsMeta } from '@paper-design/shaders'; import { ShaderFit } from '@paper-design/shaders'; -import { levaDeleteImageButton, levaImageButton } from '@/helpers/leva-image-button'; +import { levaImageButton } from '@/helpers/leva-image-button'; import { useState, Suspense, useEffect, useCallback } from 'react'; import { ShaderDetails } from '@/components/shader-details'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; import { foldsDef } from '@/shader-defs/folds-def'; import { toHsla } from '@/helpers/color-utils'; +import { useColors } from "@/helpers/use-colors"; // Override just for the docs, we keep it transparent in the preset // foldsPresets[0].params.colorBack = '#000000'; @@ -62,16 +63,14 @@ const FoldsWithControls = () => { // setImageIdx(() => Math.floor(Math.random() * imageFiles.length)); }, []); + const { colors, setColors } = useColors({ + defaultColors: defaults.colors, + maxColorCount: foldsMeta.maxColorCount, + }); + const [params, setParams] = useControls(() => { - const presets = Object.fromEntries( - foldsPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ - name, - button(() => setParamsSafe(params, setParams, preset)), - ]) - ); return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorFront: { value: toHsla(defaults.colorFront), order: 101 }, // shape: { // value: defaults.shape, // options: Object.keys(FoldsShapes) as FoldsShape[], @@ -92,18 +91,35 @@ const FoldsWithControls = () => { offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 305 }, - Image: folder({ - 'Upload image': levaImageButton((img?: HTMLImageElement) => setImage(img ?? '')), - ...(image && { 'Delete image': levaDeleteImageButton(() => setImage('')) }), - }), - Presets: folder(presets, { order: -1 }), + Image: folder( + { + 'Upload image': levaImageButton((img?: HTMLImageElement) => setImage(img ?? '')), + }, + { order: -1 } + ), + }; + }, [colors.length]); + + useControls(() => { + const presets = Object.fromEntries( + foldsPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + name, + button(() => { + const { colors, ...presetParams } = preset; + setColors(colors); + setParamsSafe(params, setParams, presetParams); + }), + ]) + ); + return { + Presets: folder(presets, { order: -2 }), }; - }, [image]); + }); // Reset to defaults on mount, so that Leva doesn't show values from other // shaders when navigating (if two shaders have a color1 param for example) useResetLevaParams(params, setParams, defaults); - useUrlParams(params, setParams, foldsDef); + useUrlParams(params, setParams, foldsDef, setColors); usePresetHighlight(foldsPresets, params); cleanUpLevaParams(params); @@ -111,10 +127,10 @@ const FoldsWithControls = () => { <> - + - + ); }; diff --git a/docs/src/shader-defs/folds-def.ts b/docs/src/shader-defs/folds-def.ts index a71847fb1..0931a0f9e 100644 --- a/docs/src/shader-defs/folds-def.ts +++ b/docs/src/shader-defs/folds-def.ts @@ -11,7 +11,8 @@ export const foldsDef: ShaderDef = { { name: 'image', type: 'HTMLImageElement | string', - description: 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', + description: + 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', }, ...animatedCommonParams, ], diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 760e337fc..c12291807 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -134,4 +134,5 @@ export { heatmapMeta, staticMeshGradientMeta, staticRadialGradientMeta, + foldsMeta, } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index 8b71f5bd3..895934bcf 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -1,5 +1,6 @@ import { memo, useLayoutEffect, useState } from 'react'; import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; +import { colorPropsAreEqual } from '../color-props-are-equal.js'; import { foldsFragmentShader, ShaderFitOptions, @@ -27,17 +28,17 @@ export const defaultPreset: FoldsPreset = { name: 'Default', params: { ...defaultObjectSizing, - scale: 1, - speed: 1, + scale: 0.8, + speed: 0.02, frame: 0, - colorBack: '#ffffff', - colorFront: '#000000', - repetition: 2.0, + colorBack: '#000000', + colors: ['#ff9d00', '#fd4f30', '#809bff', '#6d2eff', '#333aff', '#f15cff', '#ffd557'], + stripeWidth: 0.48, alphaMask: false, - size: 20, - wave: 0, + size: 40, + wave: 1, noise: 1, - outerNoise: 1, + outerNoise: 0, softness: 0.1, angle: 0, shape: 'diamond', @@ -48,7 +49,7 @@ export const foldsPresets: FoldsPreset[] = [defaultPreset]; export const Folds: React.FC = memo(function FoldsImpl({ // Own props colorBack = defaultPreset.params.colorBack, - colorFront = defaultPreset.params.colorFront, + colors = defaultPreset.params.colors, speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, image = '', @@ -56,7 +57,7 @@ export const Folds: React.FC = memo(function FoldsImpl({ noise = defaultPreset.params.noise, outerNoise = defaultPreset.params.outerNoise, softness = defaultPreset.params.softness, - repetition = defaultPreset.params.repetition, + stripeWidth = defaultPreset.params.stripeWidth, alphaMask = defaultPreset.params.alphaMask, size = defaultPreset.params.size, angle = defaultPreset.params.angle, @@ -117,15 +118,15 @@ export const Folds: React.FC = memo(function FoldsImpl({ const uniforms = { // Own uniforms + u_colors: colors.map(getShaderColorFromString), + u_colorsCount: colors.length, u_colorBack: getShaderColorFromString(colorBack), - u_colorFront: getShaderColorFromString(colorFront), - u_image: processedImage, u_wave: wave, u_noise: noise, u_outerNoise: outerNoise, u_softness: softness, - u_repetition: repetition, + u_stripeWidth: stripeWidth, u_alphaMask: alphaMask, u_size: size, u_angle: angle, @@ -150,7 +151,8 @@ export const Folds: React.FC = memo(function FoldsImpl({ speed={speed} frame={frame} fragmentShader={foldsFragmentShader} + mipmaps={['u_image']} uniforms={uniforms} /> ); -}); +}, colorPropsAreEqual); diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index b6e4a7462..09d7015b0 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -211,6 +211,7 @@ export { } from './shaders/liquid-metal.js'; export { + foldsMeta, foldsFragmentShader, FoldsShapes, toProcessedFolds, diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 96456c081..4482bc74e 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -1,6 +1,11 @@ -import type {ShaderMotionParams} from '../shader-mount.js'; -import {sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms} from '../shader-sizing.js'; -import {declarePI, rotation2, simplexNoise, colorBandingFix} from '../shader-utils.js'; +import type { vec4 } from '../types.js'; +import type { ShaderMotionParams } from '../shader-mount.js'; +import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; +import { declarePI, rotation2, simplexNoise, colorBandingFix } from '../shader-utils.js'; + +export const foldsMeta = { + maxColorCount: 10, +} as const; /** * @@ -22,11 +27,11 @@ uniform float u_imageAspectRatio; uniform vec2 u_resolution; uniform float u_time; +uniform vec4 u_colors[${foldsMeta.maxColorCount}]; +uniform float u_colorsCount; uniform vec4 u_colorBack; -uniform vec4 u_colorFront; - uniform float u_softness; -uniform float u_repetition; +uniform float u_stripeWidth; uniform bool u_alphaMask; uniform float u_size; uniform float u_wave; @@ -37,13 +42,13 @@ uniform float u_angle; uniform float u_shape; uniform bool u_isImage; -${ sizingVariablesDeclaration } +${sizingVariablesDeclaration} out vec4 fragColor; -${ declarePI } -${ rotation2 } -${ simplexNoise } +${declarePI} +${rotation2} +${simplexNoise} float doubleSNoise(vec2 uv, float t) { float noise = .5 * snoise(uv - vec2(0., .3 * t)); @@ -60,6 +65,82 @@ float getImgFrame(vec2 uv, float th) { return frame; } +float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) +{ + vec2 texel = 1.0 / vec2(textureSize(tex, 0)); + vec2 r = max(radius, 0.0) * texel; + + // 1D Gaussian coefficients (Pascal row) + const float a = 1.0;// |offset| = 2 + const float b = 4.0;// |offset| = 1 + const float c = 6.0;// |offset| = 0 + + float norm = 256.0;// (a+b+c+b+a)^2 = 16^2 + float sum = 0.0; + + // y = -2 + { + float wy = a; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, -2.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, -2.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, -2.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, -2.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, -2.0*r.y)).r; + sum += wy * row; + } + + // y = -1 + { + float wy = b; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, -1.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, -1.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, -1.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, -1.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, -1.0*r.y)).r; + sum += wy * row; + } + + // y = 0 (use provided centerSample to avoid an extra fetch) + { + float wy = c; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 0.0)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 0.0)).r + + c * centerSample + + b * texture(tex, uv + vec2(1.0*r.x, 0.0)).r + + a * texture(tex, uv + vec2(2.0*r.x, 0.0)).r; + sum += wy * row; + } + + // y = +1 + { + float wy = b; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 1.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 1.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, 1.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, 1.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, 1.0*r.y)).r; + sum += wy * row; + } + + // y = +2 + { + float wy = a; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 2.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 2.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, 2.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, 2.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, 2.0*r.y)).r; + sum += wy * row; + } + + return sum / norm; +} + 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; @@ -81,11 +162,51 @@ float blurEdge3x3(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, fl return sum / norm; } +float blurEdge3x3_G(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; +} + vec3 hsv2rgb(vec3 c){ vec3 p = abs(fract(c.x + vec3(0., 2./3., 1./3.))*6.-3.); return c.z * mix(vec3(1.), clamp(p-1., 0., 1.), c.y); } +float sst(float edge0, float edge1, float x) { + return smoothstep(edge0, edge1, x); +} + +float posMod(float x, float m) { + return x - m * floor(x / m); +} +vec3 pickPaletteColor(float stripeId) { + int target = int(posMod(stripeId, u_colorsCount)); + vec3 result = u_colors[0].rgb; + + for (int i = 0; i < ${foldsMeta.maxColorCount}; i++) { + if (i >= int(u_colorsCount)) break; + float isHit = 1.0 - step(0.5, abs(float(i - target))); + result = mix(result, u_colors[i].rgb, isHit); + } + return result; +} + void main() { const float firstFrameOffset = 2.8; @@ -104,16 +225,21 @@ void main() { float edge = 0.; float edgeRaw = img.r; - edge = blurEdge3x3(u_image, uv, dudx, dudy, 6., edgeRaw); +// edge = 1. - blurEdge3x3(u_image, uv, dudx, dudy, 2., edgeRaw); + edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 4., edgeRaw); +// edge = 1. - edgeRaw; + edge = sst(0., 1., edge); vec3 color = vec3(0.); float opacity = 1.; - float alpha = 0.; - alpha = img.g; + float alpha = img.g; +// alpha = blurEdge3x3_G(u_image, uv, dudx, dudy, 2., img.g); + float frame = getImgFrame(v_imageUV, 0.); alpha *= frame; + edge *= frame; uv = v_objectUV; vec2 p = uv; @@ -121,508 +247,517 @@ void main() { p = rotate(p, angle); p *= u_size; - float n = doubleSNoise(uv, u_time); - float edgeAtten = (1. - (1. - u_outerNoise) * edge); - p += edgeAtten * (n - .5) * 5. * u_noise; - - p += vec2(.5); - + float edgeAtten = edge; + edgeAtten += u_outerNoise * (1. - edge); + float wave = (.3 * cos(.3 * p.x + .2 * p.y + u_time) - .6 * sin(.6 * p.y + u_time)); wave *= u_wave; - float addon = (1. - edge) * wave; - if (u_alphaMask == false) { - addon = mix(addon, 0., pow(edge, 6.)); - } - p.y -= wave; +// float addon = (1. - edge) * wave; +// if (u_alphaMask == false) { +// addon = mix(addon, 0., pow(edge, 6.)); +// } + + p.y += edgeAtten * n * 10. * u_noise; + p.y -= edgeAtten * wave; vec2 d = abs(fract(p) - .5); vec2 aa = 2. * fwidth(p); float w = 0.; - w += (.5 - aa.y) * (1. - edge); - aa *= (u_alphaMask ? alpha : 1.); - + float wMax = .5 - aa.y; +// w = mix(0., 3. * pow(u_stripeWidth, 2.), mix(edge, alpha, sst(.8, 1., u_stripeWidth))); + w = mix(0., 3. * pow(u_stripeWidth, 2.), edge); + + w = min(w, wMax); +// if (u_alphaMask == true) { +// w = mix(.0, wMax, alpha); +// } + +// float lineDist = d.y; +// float sdf = w - lineDist; +// float afwidth = fwidth(sdf); +// afwidth = max(afwidth, 1e-4); +// float line = smoothstep(0., afwidth, sdf); + float line = d.y; - line = 1. - smoothstep(w, w + aa.y, line); - -// color = mix(u_colorBack.rgb, u_colorFront.rgb, line); -// fragColor = vec4(color, 1.); + line = 1. - sst(w, w + aa.y, line); float stripeId = floor(p.y); float hue = fract(stripeId * 0.161803); - vec3 stripeColor = hsv2rgb(vec3(hue, 0.65, 0.85)); + vec3 stripeColor = hsv2rgb(vec3(hue, .5, .7)); - color = mix(u_colorBack.rgb, stripeColor, line); - fragColor = vec4(color, 1.0); + color = mix(u_colorBack.rgb, pickPaletteColor(stripeId), line); + fragColor = vec4(color, 1.); } `; // Configuration for Poisson solver export const POISSON_CONFIG_OPTIMIZED = { - measurePerformance: false, // Set to true to see performance metrics - workingSize: 512, // Size to solve Poisson at (will upscale to original size) - iterations: 40, // SOR converges ~2-20x faster than standard Gauss-Seidel + measurePerformance: false, // Set to true to see performance metrics + workingSize: 1024, // Size to solve Poisson at (will upscale to original size) + iterations: 32, // SOR converges ~2-20x faster than standard Gauss-Seidel }; // Precomputed pixel data for sparse processing interface SparsePixelData { - interiorPixels: Uint32Array; // Indices of interior pixels - boundaryPixels: Uint32Array; // Indices of boundary pixels - pixelCount: number; - // Neighbor indices for each interior pixel (4 neighbors per pixel) - // Layout: [east, west, north, south] for each pixel - neighborIndices: Int32Array; + interiorPixels: Uint32Array; // Indices of interior pixels + boundaryPixels: Uint32Array; // Indices of boundary pixels + pixelCount: number; + // Neighbor indices for each interior pixel (4 neighbors per pixel) + // Layout: [east, west, north, south] for each pixel + neighborIndices: Int32Array; } export function toProcessedFolds(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - const isBlob = typeof file === 'string' && file.startsWith('blob:'); - - return new Promise((resolve, reject) => { - if (!file || !ctx) { - reject(new Error('Invalid file or canvas context')); - return; + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const isBlob = typeof file === 'string' && file.startsWith('blob:'); + + return new Promise((resolve, reject) => { + if (!file || !ctx) { + reject(new Error('Invalid file or canvas context')); + return; + } + + const blobContentTypePromise = isBlob && fetch(file).then((res) => res.headers.get('Content-Type')); + const img = new Image(); + img.crossOrigin = 'anonymous'; + const totalStartTime = performance.now(); + + img.onload = async () => { + // Force SVG to load at a high fidelity size if it's an SVG + let isSVG; + + const blobContentType = await blobContentTypePromise; + + if (blobContentType) { + isSVG = blobContentType === 'image/svg+xml'; + } else if (typeof file === 'string') { + isSVG = file.endsWith('.svg') || file.startsWith('data:image/svg+xml'); + } else { + isSVG = file.type === 'image/svg+xml'; + } + + let originalWidth = img.width || img.naturalWidth; + let originalHeight = img.height || img.naturalHeight; + + if (isSVG) { + // Scale SVG to max dimension while preserving aspect ratio + const svgMaxSize = 4096; + const aspectRatio = originalWidth / originalHeight; + + if (originalWidth > originalHeight) { + originalWidth = svgMaxSize; + originalHeight = svgMaxSize / aspectRatio; + } else { + originalHeight = svgMaxSize; + originalWidth = svgMaxSize * aspectRatio; } - const blobContentTypePromise = isBlob && fetch(file).then((res) => res.headers.get('Content-Type')); - const img = new Image(); - img.crossOrigin = 'anonymous'; - const totalStartTime = performance.now(); - - img.onload = async () => { - // Force SVG to load at a high fidelity size if it's an SVG - let isSVG; - - const blobContentType = await blobContentTypePromise; - - if (blobContentType) { - isSVG = blobContentType === 'image/svg+xml'; - } else if (typeof file === 'string') { - isSVG = file.endsWith('.svg') || file.startsWith('data:image/svg+xml'); - } else { - isSVG = file.type === 'image/svg+xml'; - } - - let originalWidth = img.width || img.naturalWidth; - let originalHeight = img.height || img.naturalHeight; - - if (isSVG) { - // Scale SVG to max dimension while preserving aspect ratio - const svgMaxSize = 4096; - const aspectRatio = originalWidth / originalHeight; - - if (originalWidth > originalHeight) { - originalWidth = svgMaxSize; - originalHeight = svgMaxSize / aspectRatio; - } else { - originalHeight = svgMaxSize; - originalWidth = svgMaxSize * aspectRatio; - } - - img.width = originalWidth; - img.height = originalHeight; - } - - // Always scale to working resolution for consistency - const minDimension = Math.min(originalWidth, originalHeight); - const targetSize = POISSON_CONFIG_OPTIMIZED.workingSize; - - // Calculate scale to fit within workingSize - const scaleFactor = targetSize / minDimension; - const width = Math.round(originalWidth * scaleFactor); - const height = Math.round(originalHeight * scaleFactor); - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Processing Mode]`); - console.log(` Original: ${ originalWidth }×${ originalHeight }`); - console.log(` Working: ${ width }×${ height } (${ (scaleFactor * 100).toFixed(1) }% scale)`); - if (scaleFactor < 1) { - console.log(` Speedup: ~${ Math.round(1 / (scaleFactor * scaleFactor)) }×`); - } - } - - canvas.width = originalWidth; - canvas.height = originalHeight; - - // Use a smaller canvas for shape detection and Poisson solving - const shapeCanvas = document.createElement('canvas'); - shapeCanvas.width = width; - shapeCanvas.height = height; - - const shapeCtx = shapeCanvas.getContext('2d')!; - shapeCtx.drawImage(img, 0, 0, width, height); - - // 1) Build optimized masks using TypedArrays - const startMask = performance.now(); - - const shapeImageData = shapeCtx.getImageData(0, 0, width, height); - const data = shapeImageData.data; - - // Use Uint8Array for masks (1 byte per pixel vs 8+ bytes for boolean array) - const shapeMask = new Uint8Array(width * height); - const boundaryMask = new Uint8Array(width * height); - - // First pass: identify shape pixels - let shapePixelCount = 0; - for (let i = 0, idx = 0; i < data.length; i += 4, idx++) { - const a = data[i + 3]; - const isShape = a === 0 ? 0 : 1; - shapeMask[idx] = isShape; - shapePixelCount += isShape; - } - - // 2) Optimized boundary detection using sparse approach - // Only check shape pixels, not all pixels - const boundaryIndices: number[] = []; - const interiorIndices: number[] = []; - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = y * width + x; - if (!shapeMask[idx]) continue; - - // Check if pixel is on boundary (optimized: early exit) - let isBoundary = false; - - // Check 4-connected neighbors first (most common case) - if (x === 0 || x === width - 1 || y === 0 || y === height - 1) { - isBoundary = true; - } else { - // Check all 8 neighbors (including diagonals) for comprehensive boundary detection - isBoundary = - !shapeMask[idx - 1] || // left - !shapeMask[idx + 1] || // right - !shapeMask[idx - width] || // top - !shapeMask[idx + width] || // bottom - !shapeMask[idx - width - 1] || // top-left - !shapeMask[idx - width + 1] || // top-right - !shapeMask[idx + width - 1] || // bottom-left - !shapeMask[idx + width + 1]; // bottom-right - } - - if (isBoundary) { - boundaryMask[idx] = 1; - boundaryIndices.push(idx); - } else { - interiorIndices.push(idx); - } - } - } - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Mask Building] Time: ${ (performance.now() - startMask).toFixed(2) }ms`); - console.log( - ` Shape pixels: ${ shapePixelCount } / ${ width * height } (${ ((shapePixelCount / (width * height)) * 100).toFixed(1) }%)` - ); - console.log(` Interior pixels: ${ interiorIndices.length }`); - console.log(` Boundary pixels: ${ boundaryIndices.length }`); - } - - // 3) Precompute sparse data structure for solver - const sparseData = buildSparseData( - shapeMask, - boundaryMask, - new Uint32Array(interiorIndices), - new Uint32Array(boundaryIndices), - width, - height + img.width = originalWidth; + img.height = originalHeight; + } + + // Always scale to working resolution for consistency + const minDimension = Math.min(originalWidth, originalHeight); + const targetSize = POISSON_CONFIG_OPTIMIZED.workingSize; + + // Calculate scale to fit within workingSize + const scaleFactor = targetSize / minDimension; + const width = Math.round(originalWidth * scaleFactor); + const height = Math.round(originalHeight * scaleFactor); + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Processing Mode]`); + console.log(` Original: ${originalWidth}×${originalHeight}`); + console.log(` Working: ${width}×${height} (${(scaleFactor * 100).toFixed(1)}% scale)`); + if (scaleFactor < 1) { + console.log(` Speedup: ~${Math.round(1 / (scaleFactor * scaleFactor))}×`); + } + } + + canvas.width = originalWidth; + canvas.height = originalHeight; + + // Use a smaller canvas for shape detection and Poisson solving + const shapeCanvas = document.createElement('canvas'); + shapeCanvas.width = width; + shapeCanvas.height = height; + + const shapeCtx = shapeCanvas.getContext('2d')!; + shapeCtx.drawImage(img, 0, 0, width, height); + + // 1) Build optimized masks using TypedArrays + const startMask = performance.now(); + + const shapeImageData = shapeCtx.getImageData(0, 0, width, height); + const data = shapeImageData.data; + + // Use Uint8Array for masks (1 byte per pixel vs 8+ bytes for boolean array) + const shapeMask = new Uint8Array(width * height); + const boundaryMask = new Uint8Array(width * height); + + // First pass: identify shape pixels + let shapePixelCount = 0; + for (let i = 0, idx = 0; i < data.length; i += 4, idx++) { + const a = data[i + 3]; + const isShape = a === 0 ? 0 : 1; + shapeMask[idx] = isShape; + shapePixelCount += isShape; + } + + // 2) Optimized boundary detection using sparse approach + // Only check shape pixels, not all pixels + const boundaryIndices: number[] = []; + const interiorIndices: number[] = []; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + if (!shapeMask[idx]) continue; + + // Check if pixel is on boundary (optimized: early exit) + let isBoundary = false; + + // Check 4-connected neighbors first (most common case) + if (x === 0 || x === width - 1 || y === 0 || y === height - 1) { + isBoundary = true; + } else { + // Check all 8 neighbors (including diagonals) for comprehensive boundary detection + isBoundary = + !shapeMask[idx - 1] || // left + !shapeMask[idx + 1] || // right + !shapeMask[idx - width] || // top + !shapeMask[idx + width] || // bottom + !shapeMask[idx - width - 1] || // top-left + !shapeMask[idx - width + 1] || // top-right + !shapeMask[idx + width - 1] || // bottom-left + !shapeMask[idx + width + 1]; // bottom-right + } + + if (isBoundary) { + boundaryMask[idx] = 1; + boundaryIndices.push(idx); + } else { + interiorIndices.push(idx); + } + } + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Mask Building] Time: ${(performance.now() - startMask).toFixed(2)}ms`); + console.log( + ` Shape pixels: ${shapePixelCount} / ${width * height} (${((shapePixelCount / (width * height)) * 100).toFixed(1)}%)` + ); + console.log(` Interior pixels: ${interiorIndices.length}`); + console.log(` Boundary pixels: ${boundaryIndices.length}`); + } + + // 3) Precompute sparse data structure for solver + const sparseData = buildSparseData( + shapeMask, + boundaryMask, + new Uint32Array(interiorIndices), + new Uint32Array(boundaryIndices), + width, + height + ); + + // 4) Solve Poisson equation with optimized sparse solver + const startSolve = performance.now(); + const u = solvePoissonSparse(sparseData, shapeMask, boundaryMask, width, height); + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Poisson Solve] Time: ${(performance.now() - startSolve).toFixed(2)}ms`); + } + + // 5) Generate output image + let maxVal = 0; + let finalImageData: ImageData; + + // Only check shape pixels for max value + for (let i = 0; i < interiorIndices.length; i++) { + const idx = interiorIndices[i]!; + if (u[idx]! > maxVal) maxVal = u[idx]!; + } + + // Create gradient image at working resolution + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = width; + tempCanvas.height = height; + const tempCtx = tempCanvas.getContext('2d')!; + + const tempImg = tempCtx.createImageData(width, height); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + const px = idx * 4; + + if (!shapeMask[idx]) { + tempImg.data[px] = 255; + tempImg.data[px + 1] = 255; + tempImg.data[px + 2] = 255; + tempImg.data[px + 3] = 0; // Alpha = 0 for background + } else { + const poissonRatio = u[idx]! / maxVal; + const gray = 255 * (1 - poissonRatio); + tempImg.data[px] = gray; + tempImg.data[px + 1] = gray; + tempImg.data[px + 2] = gray; + tempImg.data[px + 3] = 255; // Alpha = 255 for shape + } + } + } + tempCtx.putImageData(tempImg, 0, 0); + + // Upscale to original resolution with smooth interpolation + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, originalWidth, originalHeight); + + // Now get the upscaled image data for final output + const outImg = ctx.getImageData(0, 0, originalWidth, originalHeight); + + // Re-apply edges from original resolution with anti-aliasing + // This ensures edges are pixel-perfect while gradient is smooth + const originalCanvas = document.createElement('canvas'); + originalCanvas.width = originalWidth; + originalCanvas.height = originalHeight; + const originalCtx = originalCanvas.getContext('2d')!; + // originalCtx.fillStyle = "white"; + // originalCtx.fillRect(0, 0, originalWidth, originalHeight); + originalCtx.drawImage(img, 0, 0, originalWidth, originalHeight); + const originalData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); + + // Process each pixel: Red channel = gradient, Alpha channel = original alpha + for (let i = 0; i < outImg.data.length; i += 4) { + const a = originalData.data[i + 3]!; + // Use only alpha to determine background vs shape + const upscaledAlpha = outImg.data[i + 3]!; + if (a === 0) { + // Background pixel + outImg.data[i] = 255; + outImg.data[i + 1] = 0; + } else { + // Red channel carries the gradient + // Check if upscale missed this pixel by looking at alpha channel + // If upscaled alpha is 0, the low-res version thought this was background + outImg.data[i] = upscaledAlpha === 0 ? 0 : outImg.data[i]!; // gradient or 0 + outImg.data[i + 1] = a; // original alpha + } + + // Unused channels fixed + outImg.data[i + 2] = 255; + outImg.data[i + 3] = 255; + } + + ctx.putImageData(outImg, 0, 0); + finalImageData = outImg; + canvas.toBlob((blob) => { + if (!blob) { + reject(new Error('Failed to create PNG blob')); + return; + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + const totalTime = performance.now() - totalStartTime; + console.log(`[Total Processing Time] ${totalTime.toFixed(2)}ms`); + if (scaleFactor < 1) { + const estimatedFullResTime = totalTime * Math.pow((originalWidth * originalHeight) / (width * height), 1.5); + console.log(`[Estimated time at full resolution] ~${estimatedFullResTime.toFixed(0)}ms`); + console.log( + `[Time saved] ~${(estimatedFullResTime - totalTime).toFixed(0)}ms (${Math.round(estimatedFullResTime / totalTime)}× faster)` ); + } + } - // 4) Solve Poisson equation with optimized sparse solver - const startSolve = performance.now(); - const u = solvePoissonSparse(sparseData, shapeMask, boundaryMask, width, height); - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Poisson Solve] Time: ${ (performance.now() - startSolve).toFixed(2) }ms`); - } - - // 5) Generate output image - let maxVal = 0; - let finalImageData: ImageData; - - // Only check shape pixels for max value - for (let i = 0; i < interiorIndices.length; i++) { - const idx = interiorIndices[i]!; - if (u[idx]! > maxVal) maxVal = u[idx]!; - } - - // Create gradient image at working resolution - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = width; - tempCanvas.height = height; - const tempCtx = tempCanvas.getContext('2d')!; - - const tempImg = tempCtx.createImageData(width, height); - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = y * width + x; - const px = idx * 4; - - if (!shapeMask[idx]) { - tempImg.data[px] = 255; - tempImg.data[px + 1] = 255; - tempImg.data[px + 2] = 255; - tempImg.data[px + 3] = 0; // Alpha = 0 for background - } else { - const poissonRatio = u[idx]! / maxVal; - const gray = 255 * (1 - poissonRatio); - tempImg.data[px] = gray; - tempImg.data[px + 1] = gray; - tempImg.data[px + 2] = gray; - tempImg.data[px + 3] = 255; // Alpha = 255 for shape - } - } - } - tempCtx.putImageData(tempImg, 0, 0); - - // Upscale to original resolution with smooth interpolation - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'high'; - ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, originalWidth, originalHeight); - - // Now get the upscaled image data for final output - const outImg = ctx.getImageData(0, 0, originalWidth, originalHeight); - - // Re-apply edges from original resolution with anti-aliasing - // This ensures edges are pixel-perfect while gradient is smooth - const originalCanvas = document.createElement('canvas'); - originalCanvas.width = originalWidth; - originalCanvas.height = originalHeight; - const originalCtx = originalCanvas.getContext('2d')!; - // originalCtx.fillStyle = "white"; - // originalCtx.fillRect(0, 0, originalWidth, originalHeight); - originalCtx.drawImage(img, 0, 0, originalWidth, originalHeight); - const originalData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); - - // Process each pixel: Red channel = gradient, Alpha channel = original alpha - for (let i = 0; i < outImg.data.length; i += 4) { - const a = originalData.data[i + 3]!; - // Use only alpha to determine background vs shape - const upscaledAlpha = outImg.data[i + 3]!; - if (a === 0) { - // Background pixel - outImg.data[i] = 255; - outImg.data[i + 1] = 0; - } else { - // Red channel carries the gradient - // Check if upscale missed this pixel by looking at alpha channel - // If upscaled alpha is 0, the low-res version thought this was background - outImg.data[i] = upscaledAlpha === 0 ? 0 : outImg.data[i]!; // gradient or 0 - outImg.data[i + 1] = a; // original alpha - } - - // Unused channels fixed - outImg.data[i + 2] = 255; - outImg.data[i + 3] = 255; - } - - ctx.putImageData(outImg, 0, 0); - finalImageData = outImg; - canvas.toBlob((blob) => { - if (!blob) { - reject(new Error('Failed to create PNG blob')); - return; - } - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - const totalTime = performance.now() - totalStartTime; - console.log(`[Total Processing Time] ${ totalTime.toFixed(2) }ms`); - if (scaleFactor < 1) { - const estimatedFullResTime = totalTime * Math.pow((originalWidth * originalHeight) / (width * height), 1.5); - console.log(`[Estimated time at full resolution] ~${ estimatedFullResTime.toFixed(0) }ms`); - console.log( - `[Time saved] ~${ (estimatedFullResTime - totalTime).toFixed(0) }ms (${ Math.round(estimatedFullResTime / totalTime) }× faster)` - ); - } - } - - resolve({ - imageData: finalImageData, - pngBlob: blob, - }); - }, 'image/png'); - }; - - img.onerror = () => reject(new Error('Failed to load image')); - img.src = typeof file === 'string' ? file : URL.createObjectURL(file); - }); + resolve({ + imageData: finalImageData, + pngBlob: blob, + }); + }, 'image/png'); + }; + + img.onerror = () => reject(new Error('Failed to load image')); + img.src = typeof file === 'string' ? file : URL.createObjectURL(file); + }); } function buildSparseData( - shapeMask: Uint8Array, - boundaryMask: Uint8Array, - interiorPixels: Uint32Array, - boundaryPixels: Uint32Array, - width: number, - height: number + shapeMask: Uint8Array, + boundaryMask: Uint8Array, + interiorPixels: Uint32Array, + boundaryPixels: Uint32Array, + width: number, + height: number ): SparsePixelData { - const pixelCount = interiorPixels.length; - - // Build neighbor indices for sparse processing - // For each interior pixel, store indices of its 4 neighbors - // Use -1 for out-of-bounds or non-shape neighbors - const neighborIndices = new Int32Array(pixelCount * 4); - - for (let i = 0; i < pixelCount; i++) { - const idx = interiorPixels[i]!; - const x = idx % width; - const y = Math.floor(idx / width); - - // East neighbor - neighborIndices[i * 4 + 0] = x < width - 1 && shapeMask[idx + 1] ? idx + 1 : -1; - // West neighbor - neighborIndices[i * 4 + 1] = x > 0 && shapeMask[idx - 1] ? idx - 1 : -1; - // North neighbor - neighborIndices[i * 4 + 2] = y > 0 && shapeMask[idx - width] ? idx - width : -1; - // South neighbor - neighborIndices[i * 4 + 3] = y < height - 1 && shapeMask[idx + width] ? idx + width : -1; - } + const pixelCount = interiorPixels.length; + + // Build neighbor indices for sparse processing + // For each interior pixel, store indices of its 4 neighbors + // Use -1 for out-of-bounds or non-shape neighbors + const neighborIndices = new Int32Array(pixelCount * 4); + + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + const x = idx % width; + const y = Math.floor(idx / width); + + // East neighbor + neighborIndices[i * 4 + 0] = x < width - 1 && shapeMask[idx + 1] ? idx + 1 : -1; + // West neighbor + neighborIndices[i * 4 + 1] = x > 0 && shapeMask[idx - 1] ? idx - 1 : -1; + // North neighbor + neighborIndices[i * 4 + 2] = y > 0 && shapeMask[idx - width] ? idx - width : -1; + // South neighbor + neighborIndices[i * 4 + 3] = y < height - 1 && shapeMask[idx + width] ? idx + width : -1; + } - return { - interiorPixels, - boundaryPixels, - pixelCount, - neighborIndices, - }; + return { + interiorPixels, + boundaryPixels, + pixelCount, + neighborIndices, + }; } function solvePoissonSparse( - sparseData: SparsePixelData, - shapeMask: Uint8Array, - boundaryMask: Uint8Array, - width: number, - height: number + sparseData: SparsePixelData, + shapeMask: Uint8Array, + boundaryMask: Uint8Array, + width: number, + height: number ): Float32Array { - // This controls how smooth the falloff gradient will be and extend into the shape - const ITERATIONS = POISSON_CONFIG_OPTIMIZED.iterations; + // This controls how smooth the falloff gradient will be and extend into the shape + const ITERATIONS = POISSON_CONFIG_OPTIMIZED.iterations; - // Keep C constant - only iterations control gradient spread - const C = 0.01; + // Keep C constant - only iterations control gradient spread + const C = 0.01; - const u = new Float32Array(width * height); - const {interiorPixels, neighborIndices, pixelCount} = sparseData; + const u = new Float32Array(width * height); + const { interiorPixels, neighborIndices, pixelCount } = sparseData; - // Performance tracking - const startTime = performance.now(); + // Performance tracking + const startTime = performance.now(); - // Red-Black SOR for better symmetry with fewer iterations - // omega between 1.8-1.95 typically gives best convergence for Poisson - const omega = 1.9; + // Red-Black SOR for better symmetry with fewer iterations + // omega between 1.8-1.95 typically gives best convergence for Poisson + const omega = 1.9; - // Pre-classify pixels as red or black for efficient processing - const redPixels: number[] = []; - const blackPixels: number[] = []; + // Pre-classify pixels as red or black for efficient processing + const redPixels: number[] = []; + const blackPixels: number[] = []; - for (let i = 0; i < pixelCount; i++) { - const idx = interiorPixels[i]!; - const x = idx % width; - const y = Math.floor(idx / width); + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + const x = idx % width; + const y = Math.floor(idx / width); - if ((x + y) % 2 === 0) { - redPixels.push(i); - } else { - blackPixels.push(i); - } + if ((x + y) % 2 === 0) { + redPixels.push(i); + } else { + blackPixels.push(i); } + } - for (let iter = 0; iter < ITERATIONS; iter++) { - // Red pass: update red pixels - for (const i of redPixels) { - const idx = interiorPixels[i]!; - - // Get precomputed neighbor indices - const eastIdx = neighborIndices[i * 4 + 0]!; - const westIdx = neighborIndices[i * 4 + 1]!; - const northIdx = neighborIndices[i * 4 + 2]!; - const southIdx = neighborIndices[i * 4 + 3]!; - - // Sum neighbors (use 0 for out-of-bounds) - let sumN = 0; - if (eastIdx >= 0) sumN += u[eastIdx]!; - if (westIdx >= 0) sumN += u[westIdx]!; - if (northIdx >= 0) sumN += u[northIdx]!; - if (southIdx >= 0) sumN += u[southIdx]!; - - // SOR update: blend new value with old value - const newValue = (C + sumN) / 4; - u[idx] = omega * newValue + (1 - omega) * u[idx]!; - } + for (let iter = 0; iter < ITERATIONS; iter++) { + // Red pass: update red pixels + for (const i of redPixels) { + const idx = interiorPixels[i]!; + + // Get precomputed neighbor indices + const eastIdx = neighborIndices[i * 4 + 0]!; + const westIdx = neighborIndices[i * 4 + 1]!; + const northIdx = neighborIndices[i * 4 + 2]!; + const southIdx = neighborIndices[i * 4 + 3]!; + + // Sum neighbors (use 0 for out-of-bounds) + let sumN = 0; + if (eastIdx >= 0) sumN += u[eastIdx]!; + if (westIdx >= 0) sumN += u[westIdx]!; + if (northIdx >= 0) sumN += u[northIdx]!; + if (southIdx >= 0) sumN += u[southIdx]!; + + // SOR update: blend new value with old value + const newValue = (C + sumN) / 4; + u[idx] = omega * newValue + (1 - omega) * u[idx]!; + } - // Black pass: update black pixels - for (const i of blackPixels) { - const idx = interiorPixels[i]!; - - // Get precomputed neighbor indices - const eastIdx = neighborIndices[i * 4 + 0]!; - const westIdx = neighborIndices[i * 4 + 1]!; - const northIdx = neighborIndices[i * 4 + 2]!; - const southIdx = neighborIndices[i * 4 + 3]!; - - // Sum neighbors (use 0 for out-of-bounds) - let sumN = 0; - if (eastIdx >= 0) sumN += u[eastIdx]!; - if (westIdx >= 0) sumN += u[westIdx]!; - if (northIdx >= 0) sumN += u[northIdx]!; - if (southIdx >= 0) sumN += u[southIdx]!; - - // SOR update: blend new value with old value - const newValue = (C + sumN) / 4; - u[idx] = omega * newValue + (1 - omega) * u[idx]!; - } + // Black pass: update black pixels + for (const i of blackPixels) { + const idx = interiorPixels[i]!; + + // Get precomputed neighbor indices + const eastIdx = neighborIndices[i * 4 + 0]!; + const westIdx = neighborIndices[i * 4 + 1]!; + const northIdx = neighborIndices[i * 4 + 2]!; + const southIdx = neighborIndices[i * 4 + 3]!; + + // Sum neighbors (use 0 for out-of-bounds) + let sumN = 0; + if (eastIdx >= 0) sumN += u[eastIdx]!; + if (westIdx >= 0) sumN += u[westIdx]!; + if (northIdx >= 0) sumN += u[northIdx]!; + if (southIdx >= 0) sumN += u[southIdx]!; + + // SOR update: blend new value with old value + const newValue = (C + sumN) / 4; + u[idx] = omega * newValue + (1 - omega) * u[idx]!; } + } - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - const elapsed = performance.now() - startTime; + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + const elapsed = performance.now() - startTime; - console.log(`[Optimized Poisson Solver (SOR ω=${ omega })]`); - console.log(` Working size: ${ width }×${ height }`); - console.log(` Iterations: ${ ITERATIONS }`); - console.log(` Time: ${ elapsed.toFixed(2) }ms`); - console.log(` Interior pixels processed: ${ pixelCount }`); - console.log(` Speed: ${ ((ITERATIONS * pixelCount) / (elapsed * 1000)).toFixed(2) } Mpixels/sec`); - } + console.log(`[Optimized Poisson Solver (SOR ω=${omega})]`); + console.log(` Working size: ${width}×${height}`); + console.log(` Iterations: ${ITERATIONS}`); + console.log(` Time: ${elapsed.toFixed(2)}ms`); + console.log(` Interior pixels processed: ${pixelCount}`); + console.log(` Speed: ${((ITERATIONS * pixelCount) / (elapsed * 1000)).toFixed(2)} Mpixels/sec`); + } - return u; + return u; } export interface FoldsUniforms extends ShaderSizingUniforms { - u_colorBack: [number, number, number, number]; - u_colorFront: [number, number, number, number]; - u_image: HTMLImageElement | string | undefined; - u_repetition: number; - u_alphaMask: boolean; - u_size: number; - u_wave: number; - u_noise: number; - u_outerNoise: number; - u_softness: number; - u_angle: number; - u_shape: (typeof FoldsShapes)[FoldsShape]; - u_isImage: boolean; + u_colorBack: [number, number, number, number]; + u_colors: vec4[]; + u_colorsCount: number; + u_image: HTMLImageElement | string | undefined; + u_stripeWidth: number; + u_alphaMask: boolean; + u_size: number; + u_wave: number; + u_noise: number; + u_outerNoise: number; + u_softness: number; + u_angle: number; + u_shape: (typeof FoldsShapes)[FoldsShape]; + u_isImage: boolean; } export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { - colorBack?: string; - colorFront?: string; - image?: HTMLImageElement | string | undefined; - repetition?: number; - alphaMask?: boolean; - size?: number; - softness?: number; - wave?: number; - noise?: number; - outerNoise?: number; - angle?: number; - shape?: FoldsShape; + colors?: string[]; + colorBack?: string; + image?: HTMLImageElement | string | undefined; + stripeWidth?: number; + alphaMask?: boolean; + size?: number; + softness?: number; + wave?: number; + noise?: number; + outerNoise?: number; + angle?: number; + shape?: FoldsShape; } export const FoldsShapes = { - none: 0, - circle: 1, - daisy: 2, - diamond: 3, - metaballs: 4, + none: 0, + circle: 1, + daisy: 2, + diamond: 3, + metaballs: 4, } as const; export type FoldsShape = keyof typeof FoldsShapes; From abc8d058ab31dea0e3d54c5d2d22e5de08bdbf4b Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 11 Nov 2025 22:39:40 +0100 Subject: [PATCH 12/84] tweaks --- packages/shaders-react/src/shaders/folds.tsx | 4 ++-- packages/shaders/src/shaders/folds.ts | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index 895934bcf..da401934d 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -29,7 +29,7 @@ export const defaultPreset: FoldsPreset = { params: { ...defaultObjectSizing, scale: 0.8, - speed: 0.02, + speed: 0.1, frame: 0, colorBack: '#000000', colors: ['#ff9d00', '#fd4f30', '#809bff', '#6d2eff', '#333aff', '#f15cff', '#ffd557'], @@ -38,7 +38,7 @@ export const defaultPreset: FoldsPreset = { size: 40, wave: 1, noise: 1, - outerNoise: 0, + outerNoise: 0.9, softness: 0.1, angle: 0, shape: 'diamond', diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 4482bc74e..b8fbe7079 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -253,10 +253,6 @@ void main() { float wave = (.3 * cos(.3 * p.x + .2 * p.y + u_time) - .6 * sin(.6 * p.y + u_time)); wave *= u_wave; -// float addon = (1. - edge) * wave; -// if (u_alphaMask == false) { -// addon = mix(addon, 0., pow(edge, 6.)); -// } p.y += edgeAtten * n * 10. * u_noise; p.y -= edgeAtten * wave; @@ -265,13 +261,12 @@ void main() { vec2 aa = 2. * fwidth(p); float w = 0.; float wMax = .5 - aa.y; -// w = mix(0., 3. * pow(u_stripeWidth, 2.), mix(edge, alpha, sst(.8, 1., u_stripeWidth))); w = mix(0., 3. * pow(u_stripeWidth, 2.), edge); w = min(w, wMax); -// if (u_alphaMask == true) { -// w = mix(.0, wMax, alpha); -// } + if (u_alphaMask == true) { + w = mix(.0, wMax, alpha); + } // float lineDist = d.y; // float sdf = w - lineDist; From 7429f5e31ed66a189499e07329c4321b9a137577 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 11 Nov 2025 22:40:56 +0100 Subject: [PATCH 13/84] alpha-mask original idea --- packages/shaders/src/shaders/folds.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index b8fbe7079..cefa365c5 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -276,6 +276,9 @@ void main() { float line = d.y; line = 1. - sst(w, w + aa.y, line); + if (u_alphaMask == true) { + line *= alpha; + } float stripeId = floor(p.y); float hue = fract(stripeId * 0.161803); From 935ac64eb6da0758594e2f2b1019e2d03ac7a605 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 11 Nov 2025 22:48:43 +0100 Subject: [PATCH 14/84] back to 512 workingSize --- packages/shaders/src/shaders/folds.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index cefa365c5..4e6480d47 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -292,7 +292,7 @@ void main() { // Configuration for Poisson solver export const POISSON_CONFIG_OPTIMIZED = { measurePerformance: false, // Set to true to see performance metrics - workingSize: 1024, // Size to solve Poisson at (will upscale to original size) + workingSize: 512, // Size to solve Poisson at (will upscale to original size) iterations: 32, // SOR converges ~2-20x faster than standard Gauss-Seidel }; From 42cea26cd4ee2da4be1754ef6b3a538a099c58b0 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 12 Nov 2025 15:51:39 +0100 Subject: [PATCH 15/84] saving progress --- docs/src/app/(shaders)/folds/page.tsx | 13 ++--- packages/shaders/src/shaders/folds.ts | 82 ++++++++++----------------- 2 files changed, 35 insertions(+), 60 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index f8bb3bc1d..0e574bef1 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -25,6 +25,8 @@ const imageFiles = [ 'chanel.svg', 'cibc.svg', 'cloudflare.svg', + 'apple.svg', + 'paper-logo-only.svg', 'diamond.svg', 'discord.svg', 'paper-logo-only', @@ -42,12 +44,11 @@ const imageFiles = [ 'rogers.svg', 'vercel.svg', 'volkswagen.svg', - 'apple.svg', ] as const; const FoldsWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); - const [image, setImage] = useState('/images/logos/apple.svg'); + const [image, setImage] = useState('/images/logos/nike.svg'); useEffect(() => { if (imageIdx >= 0) { @@ -71,17 +72,11 @@ const FoldsWithControls = () => { const [params, setParams] = useControls(() => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - // shape: { - // value: defaults.shape, - // options: Object.keys(FoldsShapes) as FoldsShape[], - // order: 102, - // disabled: Boolean(image), - // }, stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, // softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, alphaMask: { value: defaults.alphaMask, order: 202 }, size: { value: defaults.size, min: 1, max: 50, order: 203 }, - wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, + // wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 4e6480d47..f309966dc 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -183,11 +183,6 @@ float blurEdge3x3_G(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, return sum / norm; } -vec3 hsv2rgb(vec3 c){ - vec3 p = abs(fract(c.x + vec3(0., 2./3., 1./3.))*6.-3.); - return c.z * mix(vec3(1.), clamp(p-1., 0., 1.), c.y); -} - float sst(float edge0, float edge1, float x) { return smoothstep(edge0, edge1, x); } @@ -195,17 +190,6 @@ float sst(float edge0, float edge1, float x) { float posMod(float x, float m) { return x - m * floor(x / m); } -vec3 pickPaletteColor(float stripeId) { - int target = int(posMod(stripeId, u_colorsCount)); - vec3 result = u_colors[0].rgb; - - for (int i = 0; i < ${foldsMeta.maxColorCount}; i++) { - if (i >= int(u_colorsCount)) break; - float isHit = 1.0 - step(0.5, abs(float(i - target))); - result = mix(result, u_colors[i].rgb, isHit); - } - return result; -} void main() { @@ -226,19 +210,15 @@ void main() { float edgeRaw = img.r; // edge = 1. - blurEdge3x3(u_image, uv, dudx, dudy, 2., edgeRaw); - edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 4., edgeRaw); -// edge = 1. - edgeRaw; - edge = sst(0., 1., edge); + edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 5., edgeRaw); + float edgeCopy = edge; - vec3 color = vec3(0.); - float opacity = 1.; - - float alpha = img.g; -// alpha = blurEdge3x3_G(u_image, uv, dudx, dudy, 2., img.g); + float imgAlpha = img.g; +// imgAlpha = blurEdge3x3_G(u_image, uv, dudx, dudy, 2., img.g); float frame = getImgFrame(v_imageUV, 0.); - alpha *= frame; + imgAlpha *= frame; edge *= frame; uv = v_objectUV; @@ -247,45 +227,45 @@ void main() { p = rotate(p, angle); p *= u_size; - float n = doubleSNoise(uv, u_time); - float edgeAtten = edge; - edgeAtten += u_outerNoise * (1. - edge); - - float wave = (.3 * cos(.3 * p.x + .2 * p.y + u_time) - .6 * sin(.6 * p.y + u_time)); - wave *= u_wave; - + float n = doubleSNoise(uv + 100., u_time); + float edgeAtten = edge + u_outerNoise * (1. - edge); p.y += edgeAtten * n * 10. * u_noise; - p.y -= edgeAtten * wave; + + float stripeId = floor(p.y); vec2 d = abs(fract(p) - .5); vec2 aa = 2. * fwidth(p); float w = 0.; float wMax = .5 - aa.y; w = mix(0., 3. * pow(u_stripeWidth, 2.), edge); - w = min(w, wMax); - if (u_alphaMask == true) { - w = mix(.0, wMax, alpha); - } - -// float lineDist = d.y; -// float sdf = w - lineDist; -// float afwidth = fwidth(sdf); -// afwidth = max(afwidth, 1e-4); -// float line = smoothstep(0., afwidth, sdf); float line = d.y; line = 1. - sst(w, w + aa.y, line); if (u_alphaMask == true) { - line *= alpha; + line *= sst(0., 1., imgAlpha); } - float stripeId = floor(p.y); - float hue = fract(stripeId * 0.161803); - vec3 stripeColor = hsv2rgb(vec3(hue, .5, .7)); - - color = mix(u_colorBack.rgb, pickPaletteColor(stripeId), line); - fragColor = vec4(color, 1.); + int colorIdx = int(posMod(stripeId, u_colorsCount)); + vec4 stripeColor = u_colors[0]; + for (int i = 0; i < ${foldsMeta.maxColorCount}; i++) { + if (i >= int(u_colorsCount)) break; + float isHit = 1.0 - step(.5, abs(float(i - colorIdx))); + stripeColor = mix(stripeColor, u_colors[i], isHit); + } + + vec3 stripePremulRGB = stripeColor.rgb * stripeColor.a; + stripePremulRGB *= line; + float stripeA = stripeColor.a * line; + + vec3 color = stripePremulRGB; + float opacity = stripeA; + + vec3 bgColor = u_colorBack.rgb * u_colorBack.a; + color = color + bgColor * (1. - opacity); + opacity = opacity + u_colorBack.a * (1. - opacity); + + fragColor = vec4(color, opacity); } `; @@ -498,7 +478,7 @@ export function toProcessedFolds(file: File | string): Promise<{ imageData: Imag tempImg.data[px + 3] = 0; // Alpha = 0 for background } else { const poissonRatio = u[idx]! / maxVal; - const gray = 255 * (1 - poissonRatio); + let gray = 255 * (1 - poissonRatio); tempImg.data[px] = gray; tempImg.data[px + 1] = gray; tempImg.data[px + 2] = gray; From b48e0ef501cad6efab4bca11a4a0f7e6eb390b36 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 12 Nov 2025 17:15:38 +0100 Subject: [PATCH 16/84] saving progress on halftone lines --- .../src/app/(shaders)/halftone-lines/page.tsx | 15 +- .../src/shaders/halftone-lines.tsx | 9 +- .../shaders/src/shaders/halftone-lines.ts | 216 +++++------------- 3 files changed, 75 insertions(+), 165 deletions(-) diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index ba270c395..4f4e08d1d 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -71,9 +71,9 @@ const HalftoneLinesWithControls = () => { ); return { stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, - // softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, + softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, alphaMask: { value: defaults.alphaMask, order: 202 }, - wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, + // wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, @@ -90,12 +90,13 @@ const HalftoneLinesWithControls = () => { contrast: { value: defaults.contrast, min: 0.01, max: 1, order: 302 }, grainMixer: { value: defaults.grainMixer, min: 0, max: 1, order: 350 }, grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 351 }, + speed: {value: defaults.speed, min: 0, max: 4, order: 300}, scale: { value: defaults.scale, min: 0.1, max: 10, order: 400 }, - // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 401 }, - // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 402 }, - // originX: { value: defaults.originX, min: 0, max: 1, order: 411 }, - // originY: { value: defaults.originY, min: 0, max: 1, order: 412 }, - // rotation: { value: defaults.rotation, min: 0, max: 360, order: 420 }, + offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 401 }, + offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 402 }, + originX: { value: defaults.originX, min: 0, max: 1, order: 411 }, + originY: { value: defaults.originY, min: 0, max: 1, order: 412 }, + rotation: { value: defaults.rotation, min: 0, max: 360, order: 420 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 450 }, Image: folder( { diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index 8e55632c0..00bea26a7 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -20,19 +20,18 @@ export const defaultPreset: HalftoneLinesPreset = { ...defaultObjectSizing, scale: 1, - speed: 0.02, + speed: 0.1, frame: 0, colorBack: '#000000', colorFront: '#ffffff', - stripeWidth: 0.48, + stripeWidth: 1, alphaMask: false, size: 40, wave: 1, - noise: 1, + noise: 0.2, softness: 0.1, angle: 0, - - contrast: 0.25, + contrast: 0.7, originalColors: false, inverted: false, grainMixer: 0.2, diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index 20fc99cd7..637a6d358 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -125,129 +125,6 @@ float getImgFrame(vec2 uv, float th) { return frame; } -float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) -{ - vec2 texel = 1.0 / vec2(textureSize(tex, 0)); - vec2 r = max(radius, 0.0) * texel; - - // 1D Gaussian coefficients (Pascal row) - const float a = 1.0;// |offset| = 2 - const float b = 4.0;// |offset| = 1 - const float c = 6.0;// |offset| = 0 - - float norm = 256.0;// (a+b+c+b+a)^2 = 16^2 - float sum = 0.0; - - // y = -2 - { - float wy = a; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, -2.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, -2.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, -2.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, -2.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, -2.0*r.y)).r; - sum += wy * row; - } - - // y = -1 - { - float wy = b; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, -1.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, -1.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, -1.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, -1.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, -1.0*r.y)).r; - sum += wy * row; - } - - // y = 0 (use provided centerSample to avoid an extra fetch) - { - float wy = c; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 0.0)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 0.0)).r + - c * centerSample + - b * texture(tex, uv + vec2(1.0*r.x, 0.0)).r + - a * texture(tex, uv + vec2(2.0*r.x, 0.0)).r; - sum += wy * row; - } - - // y = +1 - { - float wy = b; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 1.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 1.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, 1.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, 1.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, 1.0*r.y)).r; - sum += wy * row; - } - - // y = +2 - { - float wy = a; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 2.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 2.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, 2.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, 2.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, 2.0*r.y)).r; - sum += wy * row; - } - - return sum / norm; -} - -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).r; - sum += w2 * textureGrad(tex, uv + vec2(0.0, r.y), dudx, dudy).r; - sum += w2 * textureGrad(tex, uv + vec2(-r.x, 0.0), dudx, dudy).r; - sum += w2 * textureGrad(tex, uv + vec2(r.x, 0.0), dudx, dudy).r; - - sum += w1 * textureGrad(tex, uv + vec2(-r.x, -r.y), dudx, dudy).r; - sum += w1 * textureGrad(tex, uv + vec2(r.x, -r.y), dudx, dudy).r; - sum += w1 * textureGrad(tex, uv + vec2(-r.x, r.y), dudx, dudy).r; - sum += w1 * textureGrad(tex, uv + vec2(r.x, r.y), dudx, dudy).r; - - return sum / norm; -} - -float blurEdge3x3_G(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; -} - -vec3 hsv2rgb(vec3 c){ - vec3 p = abs(fract(c.x + vec3(0., 2./3., 1./3.))*6.-3.); - return c.z * mix(vec3(1.), clamp(p-1., 0., 1.), c.y); -} - float sst(float edge0, float edge1, float x) { return smoothstep(edge0, edge1, x); } @@ -285,6 +162,44 @@ vec3 blendHardLight(vec3 base, vec3 blend, float opacity) { return (blendHardLight(base, blend) * opacity + base * (1.0 - opacity)); } +const int SAMPLES = 4; + +float sampleLumOnLine(vec2 uvNorm, float contrast, vec2 p_current) { + float halfSpanPx = mix(0., 40., u_softness); + vec2 gradPy = vec2(dFdx(p_current.y), dFdy(p_current.y)); + vec2 t = normalize(vec2(-gradPy.y, gradPy.x) + 1e-8); + + vec2 stepNorm = (t / u_resolution) * halfSpanPx / float((SAMPLES - 1) / 2); + + float sum = 0.0; + float wsum = 0.0; + int N = (SAMPLES - 1) / 2; + for (int i = -N; i <= N; ++i) { + float w = 1.0 - (abs(float(i)) / float(N + 1)); + vec2 uvTapNorm = uvNorm + stepNorm * float(i); + vec2 uvTapImg = getImageUV(uvTapNorm, vec2(1.0)); + sum += w * getLumAtPx(uvTapImg, contrast); + wsum += w; + } + return sum / max(wsum, 1e-6); +} + + +vec2 projectToCenterlineUV(vec2 uvNorm, vec2 p_val) { + vec2 gradPy = vec2(dFdx(p_val.y), dFdy(p_val.y)); + float gradLen = max(length(gradPy), 1e-6); + float dy_p = (floor(p_val.y) + 0.5) - p_val.y; + vec2 deltaPx = (gradPy / gradLen) * (dy_p / gradLen); + return uvNorm + deltaPx / u_resolution; +} + +float pixelDistToCenterline(vec2 p_val) { + vec2 gradPy = vec2(dFdx(p_val.y), dFdy(p_val.y)); + float gradLen = max(length(gradPy), 1e-6); + float dy_p = (fract(p_val.y) - 0.5);// signed p-space distance to center + return dy_p / gradLen;// pixels +} + void main() { @@ -298,19 +213,12 @@ void main() { contrast = mix(.1, 4., pow(u_contrast, 2.)); } - float lum = getLumAtPx(uvOriginal, contrast); - - - float t = .3 * (u_time); - - float edge = lum; - - vec3 color = vec3(0.); - - float opacity = 1.; +// float lum = getLumAtPx(uvOriginal, contrast); + float t = .3 * u_time; + float frame = getImgFrame(v_imageUV, 0.); - edge = mix(1., edge, frame); +// lum = mix(1., lum, frame); uv = v_objectUV; vec2 p = uv; @@ -320,36 +228,38 @@ void main() { float n = doubleSNoise(uv, u_time); - float wave = (.3 * cos(.3 * p.x + .2 * p.y + u_time) - .6 * sin(.6 * p.y + u_time)); - wave *= u_wave; - p.y += n * 10. * u_noise; - p.y -= wave; + + float dPx = pixelDistToCenterline(p); + float coverage = abs(dPx); + vec2 uvOnLineNorm = projectToCenterlineUV(uvNormalised, p); + vec2 uvOnLineImg = getImageUV(uvOnLineNorm, vec2(1.)); +// float lum = getLumAtPx(uvOnLineImg, contrast); + float lum = sampleLumOnLine(uvOnLineNorm, coverage * contrast, p); + + lum = mix(1., lum, frame); vec2 d = abs(fract(p) - .5); vec2 aa = 2. * fwidth(p); - float w = 0.; - float wMax = .5 - aa.y; - w = edge; - - w = min(w, wMax); -// float lineDist = d.y; -// float sdf = w - lineDist; -// float afwidth = fwidth(sdf); -// afwidth = max(afwidth, 1e-4); -// float line = smoothstep(0., afwidth, sdf); + float wMax = (.5 - aa.y); + float w = mix(wMax * u_stripeWidth, 0., lum); + +// w *= 6.; +// w = floor(w); +// w /= 6.; float line = d.y; - line = 1. - sst(w, w + aa.y, line); + line = sst(w, w + aa.y, line); + line = mix(1., line, frame); + + vec3 color = vec3(0.); + float opacity = 1.; float stripeId = floor(p.y); - float hue = fract(stripeId * 0.161803); - vec3 stripeColor = hsv2rgb(vec3(hue, .5, .7)); -// color = mix(u_colorBack.rgb, stripeColor, line); color = mix(u_colorBack.rgb, u_colorFront.rgb, line); + fragColor = vec4(color, 1.); -// fragColor = vec4(vec3(lum), 1.); } `; From d046bdc4bf44ffd47d34db1a1b7a73d8c39fb11b Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 13 Nov 2025 21:50:36 +0100 Subject: [PATCH 17/84] saving progress on both --- docs/public/images/logos/l-oreal.svg | 1 - docs/src/app/(shaders)/folds/page.tsx | 6 +- .../src/app/(shaders)/halftone-lines/page.tsx | 5 +- packages/shaders-react/src/shaders/folds.tsx | 17 ++-- .../src/shaders/halftone-lines.tsx | 17 ++-- packages/shaders/src/shaders/folds.ts | 64 +++++++++--- .../shaders/src/shaders/halftone-lines.ts | 98 ++++++++++++++----- 7 files changed, 146 insertions(+), 62 deletions(-) delete mode 100644 docs/public/images/logos/l-oreal.svg diff --git a/docs/public/images/logos/l-oreal.svg b/docs/public/images/logos/l-oreal.svg deleted file mode 100644 index e864706c7..000000000 --- a/docs/public/images/logos/l-oreal.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index 0e574bef1..e20b0d58a 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -32,7 +32,6 @@ const imageFiles = [ 'paper-logo-only', 'enterprise-rent.svg', 'kfc.svg', - 'l-oreal.svg', 'microsoft.svg', 'nasa.svg', 'netflix.svg', @@ -73,10 +72,11 @@ const FoldsWithControls = () => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, - // softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, + softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, alphaMask: { value: defaults.alphaMask, order: 202 }, + gap: { value: defaults.gap, order: 202 }, size: { value: defaults.size, min: 1, max: 50, order: 203 }, - // wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, + shift: { value: defaults.shift, min: 0, max: 1, order: 204 }, noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index 4f4e08d1d..c87752367 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -72,11 +72,10 @@ const HalftoneLinesWithControls = () => { return { stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, - alphaMask: { value: defaults.alphaMask, order: 202 }, - // wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, + blur: { value: defaults.blur, min: 0, max: 20, order: 202 }, + wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, - colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, originalColors: { value: defaults.originalColors, order: 102 }, diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index da401934d..1624ba604 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -28,18 +28,19 @@ export const defaultPreset: FoldsPreset = { name: 'Default', params: { ...defaultObjectSizing, - scale: 0.8, + scale: 2, speed: 0.1, frame: 0, colorBack: '#000000', colors: ['#ff9d00', '#fd4f30', '#809bff', '#6d2eff', '#333aff', '#f15cff', '#ffd557'], stripeWidth: 0.48, alphaMask: false, - size: 40, - wave: 1, - noise: 1, + gap: true, + size: 20, + shift: 0, + noise: 0, outerNoise: 0.9, - softness: 0.1, + softness: 0.4, angle: 0, shape: 'diamond', }, @@ -53,12 +54,13 @@ export const Folds: React.FC = memo(function FoldsImpl({ speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, image = '', - wave = defaultPreset.params.wave, + shift = defaultPreset.params.shift, noise = defaultPreset.params.noise, outerNoise = defaultPreset.params.outerNoise, softness = defaultPreset.params.softness, stripeWidth = defaultPreset.params.stripeWidth, alphaMask = defaultPreset.params.alphaMask, + gap = defaultPreset.params.gap, size = defaultPreset.params.size, angle = defaultPreset.params.angle, shape = defaultPreset.params.shape, @@ -122,12 +124,13 @@ export const Folds: React.FC = memo(function FoldsImpl({ u_colorsCount: colors.length, u_colorBack: getShaderColorFromString(colorBack), u_image: processedImage, - u_wave: wave, + u_shift: shift, u_noise: noise, u_outerNoise: outerNoise, u_softness: softness, u_stripeWidth: stripeWidth, u_alphaMask: alphaMask, + u_gap: gap, u_size: size, u_angle: angle, u_isImage: Boolean(image), diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index 00bea26a7..f713c0c87 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -18,18 +18,17 @@ export const defaultPreset: HalftoneLinesPreset = { name: 'Default', params: { ...defaultObjectSizing, - scale: 1, - speed: 0.1, + speed: 0, frame: 0, colorBack: '#000000', colorFront: '#ffffff', - stripeWidth: 1, - alphaMask: false, + stripeWidth: 0, + blur: 10, size: 40, - wave: 1, - noise: 0.2, - softness: 0.1, + wave: 0.4, + noise: 0, + softness: 1, angle: 0, contrast: 0.7, originalColors: false, @@ -52,7 +51,7 @@ export const HalftoneLines: React.FC = memo(function Halfton noise = defaultPreset.params.noise, softness = defaultPreset.params.softness, stripeWidth = defaultPreset.params.stripeWidth, - alphaMask = defaultPreset.params.alphaMask, + blur = defaultPreset.params.blur, size = defaultPreset.params.size, angle = defaultPreset.params.angle, type = defaultPreset.params.type, @@ -85,7 +84,7 @@ export const HalftoneLines: React.FC = memo(function Halfton u_noise: noise, u_softness: softness, u_stripeWidth: stripeWidth, - u_alphaMask: alphaMask, + u_blur: blur, u_size: size, u_angle: angle, u_type: HalftoneLinesTypes[type], diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index f309966dc..366b43c99 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -33,8 +33,9 @@ uniform vec4 u_colorBack; uniform float u_softness; uniform float u_stripeWidth; uniform bool u_alphaMask; +uniform bool u_gap; uniform float u_size; -uniform float u_wave; +uniform float u_shift; uniform float u_noise; uniform float u_outerNoise; uniform float u_angle; @@ -65,8 +66,7 @@ float getImgFrame(vec2 uv, float th) { return frame; } -float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) -{ +float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) { vec2 texel = 1.0 / vec2(textureSize(tex, 0)); vec2 r = max(radius, 0.0) * texel; @@ -233,19 +233,53 @@ void main() { float stripeId = floor(p.y); - vec2 d = abs(fract(p) - .5); - vec2 aa = 2. * fwidth(p); - float w = 0.; - float wMax = .5 - aa.y; - w = mix(0., 3. * pow(u_stripeWidth, 2.), edge); - w = min(w, wMax); - + vec2 fw = fwidth(p); + float aa = fw.y; + +// vec2 fw = fwidth(p); +// float aa = fw.y; + + // stripeMap in [-0.5, 0.5) + vec2 stripeMap = fract(p) - 0.5; + vec2 dSign = sign(stripeMap); + + float w = 0.0; + w = mix(0.0, 3.0 * pow(u_stripeWidth, 2.0), edge); + w = clamp(w, aa, 1.0); + if (u_gap == true) { + w = clamp(w, aa, 0.5 - aa); + } + + // ---- NEW: move the center depending on w ---- + + // how far from "maximum" half-width (0.5) + float availableShift = max(0.0, 0.5 - w);// 0 when w = 0.5 + + // map u_shift [0,1] -> [-1,1] + float signedPos = u_shift * 2.0 - 1.0; + + // final offset: small when w is big, large when w is small + float centerOffset = signedPos * availableShift; + + // shift only in Y, before abs() + stripeMap.y -= centerOffset; + + // recompute distance after shifting + vec2 d = abs(stripeMap); + + // ---------------------------------------------- + float line = d.y; - line = 1. - sst(w, w + aa.y, line); + float lMin = mix(w, aa, u_softness) - aa; + float lMax = w + aa; + + line = 1.0 - sst(lMin, lMax, line); + if (u_alphaMask == true) { - line *= sst(0., 1., imgAlpha); + line *= imgAlpha; } + int colorIdx = int(posMod(stripeId, u_colorsCount)); vec4 stripeColor = u_colors[0]; for (int i = 0; i < ${foldsMeta.maxColorCount}; i++) { @@ -705,8 +739,9 @@ export interface FoldsUniforms extends ShaderSizingUniforms { u_image: HTMLImageElement | string | undefined; u_stripeWidth: number; u_alphaMask: boolean; + u_gap: boolean; u_size: number; - u_wave: number; + u_shift: number; u_noise: number; u_outerNoise: number; u_softness: number; @@ -721,9 +756,10 @@ export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { image?: HTMLImageElement | string | undefined; stripeWidth?: number; alphaMask?: boolean; + gap?: boolean; size?: number; softness?: number; - wave?: number; + shift?: number; noise?: number; outerNoise?: number; angle?: number; diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index 637a6d358..099b5a7f1 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -47,7 +47,7 @@ uniform float u_type; uniform float u_softness; uniform float u_stripeWidth; -uniform bool u_alphaMask; +uniform float u_blur; uniform float u_wave; uniform float u_noise; uniform float u_angle; @@ -133,8 +133,40 @@ float sigmoid(float x, float k) { return 1.0 / (1.0 + exp(-k * (x - 0.5))); } +vec4 blurTexture(sampler2D tex, vec2 uv, vec2 texelSize, float radius) { + // clamp radius so loops have a known max + float r = clamp(radius, 0.0, 10.0); + int ir = int(r); + + vec4 acc = vec4(0.0); + float weightSum = 0.0; + + // simple Gaussian-ish weights based on distance + for (int y = -10; y <= 10; ++y) { + if (abs(y) > ir) continue; + for (int x = -10; x <= 10; ++x) { + if (abs(x) > ir) continue; + + vec2 offset = vec2(float(x), float(y)); + float dist2 = dot(offset, offset); + + // tweak sigma to taste; lower sigma = sharper falloff + float sigma = radius * 0.5 + 0.001; + float w = exp(-dist2 / (2.0 * sigma * sigma)); + + acc += texture(tex, uv + offset * texelSize) * w; + weightSum += w; + } + } + + return acc / max(weightSum, 0.00001); +} + + float getLumAtPx(vec2 uv, float contrast) { - vec4 tex = texture(u_image, uv); +// vec4 tex = texture(u_image, uv); + vec4 tex = blurTexture(u_image, uv, vec2(1. / u_resolution), u_blur); + vec3 color = vec3( sigmoid(tex.r, contrast), sigmoid(tex.g, contrast), @@ -165,7 +197,8 @@ vec3 blendHardLight(vec3 base, vec3 blend, float opacity) { const int SAMPLES = 4; float sampleLumOnLine(vec2 uvNorm, float contrast, vec2 p_current) { - float halfSpanPx = mix(0., 40., u_softness); +// float halfSpanPx = mix(0., 40., u_softness); + float halfSpanPx = 5.; vec2 gradPy = vec2(dFdx(p_current.y), dFdy(p_current.y)); vec2 t = normalize(vec2(-gradPy.y, gradPy.x) + 1e-8); @@ -213,44 +246,60 @@ void main() { contrast = mix(.1, 4., pow(u_contrast, 2.)); } -// float lum = getLumAtPx(uvOriginal, contrast); + float lumOrig = getLumAtPx(uvOriginal, contrast); float t = .3 * u_time; float frame = getImgFrame(v_imageUV, 0.); -// lum = mix(1., lum, frame); + lumOrig = mix(1., lumOrig, frame); uv = v_objectUV; vec2 p = uv; float angle = -u_angle * PI / 180.; - p = rotate(p, angle); +// p = rotate(p, angle + u_stripeWidth * lumOrig); + p = rotate(p, u_wave * (1. - lumOrig)); p *= u_size; - float n = doubleSNoise(uv, u_time); - - p.y += n * 10. * u_noise; + float n = doubleSNoise(uv + 100., u_time); + p.y += .4 * n * lumOrig * u_noise * u_size; float dPx = pixelDistToCenterline(p); float coverage = abs(dPx); vec2 uvOnLineNorm = projectToCenterlineUV(uvNormalised, p); vec2 uvOnLineImg = getImageUV(uvOnLineNorm, vec2(1.)); -// float lum = getLumAtPx(uvOnLineImg, contrast); - float lum = sampleLumOnLine(uvOnLineNorm, coverage * contrast, p); - + float lum = getLumAtPx(uvOnLineImg, contrast); +// float lum = sampleLumOnLine(uvOnLineNorm, coverage * contrast, p); + lum = lumOrig; lum = mix(1., lum, frame); - vec2 d = abs(fract(p) - .5); - vec2 aa = 2. * fwidth(p); - float wMax = (.5 - aa.y); - float w = mix(wMax * u_stripeWidth, 0., lum); + float line = 0.; + + { + vec2 stripeMap = abs(fract(p) - .5); + vec2 d = abs(stripeMap); + vec2 stripeSign = sign(stripeMap); + vec2 fw = fwidth(p); + float aa = fw.y; + float w = aa; +// float w = 0.; + w = mix(u_stripeWidth, 0., lum); +// w = clamp(w, 0., .5 - aa); + w = clamp(w, aa, .5); + line += sst(mix(w, aa, u_softness) - aa, w + aa, d.y); + } -// w *= 6.; -// w = floor(w); -// w /= 6.; - - float line = d.y; - line = sst(w, w + aa.y, line); line = mix(1., line, frame); + line = clamp(line, 0., 1.); + + + vec2 grainSize = 1000. * vec2(1., 1. / u_imageAspectRatio); + vec2 grainUV = getImageUV(uvNormalised, grainSize); + float grain = valueNoise(grainUV); + grain = smoothstep(.55, .7 + .2 * u_grainMixer, grain); + grain *= u_grainMixer; +// grain *= sst(.1, .0, w); +// line = mix(line, 0., grain); +// line += grain; vec3 color = vec3(0.); float opacity = 1.; @@ -268,14 +317,13 @@ export interface HalftoneLinesUniforms extends ShaderSizingUniforms { u_colorFront: [number, number, number, number]; u_image: HTMLImageElement | string | undefined; u_stripeWidth: number; - u_alphaMask: boolean; + u_blur: number; u_size: number; u_wave: number; u_noise: number; u_softness: number; u_angle: number; u_type: (typeof HalftoneLinesTypes)[HalftoneLinesType]; - u_contrast: number; u_originalColors: boolean; u_inverted: boolean; @@ -288,7 +336,7 @@ export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionPar colorFront?: string; image?: HTMLImageElement | string | undefined; stripeWidth?: number; - alphaMask?: boolean; + blur?: number; size?: number; softness?: number; wave?: number; From 5a783583baab5beb04e17b98fc2b09e6149f0d1c Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 13 Nov 2025 22:59:21 +0100 Subject: [PATCH 18/84] halftoneLines clean --- .../src/app/(shaders)/halftone-lines/page.tsx | 4 +- .../src/shaders/halftone-lines.tsx | 8 +- .../shaders/src/shaders/halftone-lines.ts | 240 +++++++----------- 3 files changed, 102 insertions(+), 150 deletions(-) diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index c87752367..26698c492 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -71,8 +71,8 @@ const HalftoneLinesWithControls = () => { ); return { stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, - softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, - blur: { value: defaults.blur, min: 0, max: 20, order: 202 }, + // softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, + smoothness: { value: defaults.smoothness, min: 0, max: 20, order: 202 }, wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index f713c0c87..c23af9cad 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -24,11 +24,11 @@ export const defaultPreset: HalftoneLinesPreset = { colorBack: '#000000', colorFront: '#ffffff', stripeWidth: 0, - blur: 10, + smoothness: 10, size: 40, wave: 0.4, noise: 0, - softness: 1, + softness: 0, angle: 0, contrast: 0.7, originalColors: false, @@ -51,7 +51,7 @@ export const HalftoneLines: React.FC = memo(function Halfton noise = defaultPreset.params.noise, softness = defaultPreset.params.softness, stripeWidth = defaultPreset.params.stripeWidth, - blur = defaultPreset.params.blur, + smoothness = defaultPreset.params.smoothness, size = defaultPreset.params.size, angle = defaultPreset.params.angle, type = defaultPreset.params.type, @@ -84,7 +84,7 @@ export const HalftoneLines: React.FC = memo(function Halfton u_noise: noise, u_softness: softness, u_stripeWidth: stripeWidth, - u_blur: blur, + u_smoothness: smoothness, u_size: size, u_angle: angle, u_type: HalftoneLinesTypes[type], diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index 099b5a7f1..5a069f60c 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -1,6 +1,6 @@ -import type { ShaderMotionParams } from '../shader-mount.js'; -import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, rotation2, simplexNoise, proceduralHash21 } from '../shader-utils.js'; +import type {ShaderMotionParams} from '../shader-mount.js'; +import {sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms} from '../shader-sizing.js'; +import {declarePI, rotation2, simplexNoise, proceduralHash21} from '../shader-utils.js'; /** * @@ -47,20 +47,20 @@ uniform float u_type; uniform float u_softness; uniform float u_stripeWidth; -uniform float u_blur; +uniform float u_smoothness; uniform float u_wave; uniform float u_noise; uniform float u_angle; -${sizingVariablesDeclaration} +${ sizingVariablesDeclaration } out vec4 fragColor; -${declarePI} -${rotation2} -${simplexNoise} -${proceduralHash21} +${ declarePI } +${ rotation2 } +${ simplexNoise } +${ proceduralHash21 } float valueNoise(vec2 st) { vec2 i = floor(st); @@ -134,39 +134,39 @@ float sigmoid(float x, float k) { } vec4 blurTexture(sampler2D tex, vec2 uv, vec2 texelSize, float radius) { - // clamp radius so loops have a known max - float r = clamp(radius, 0.0, 10.0); - int ir = int(r); + // clamp radius so loops have a known max + float r = clamp(radius, 0.0, 10.0); + int ir = int(r); - vec4 acc = vec4(0.0); - float weightSum = 0.0; + vec4 acc = vec4(0.0); + float weightSum = 0.0; - // simple Gaussian-ish weights based on distance - for (int y = -10; y <= 10; ++y) { - if (abs(y) > ir) continue; - for (int x = -10; x <= 10; ++x) { - if (abs(x) > ir) continue; + // simple Gaussian-ish weights based on distance + for (int y = -10; y <= 10; ++y) { + if (abs(y) > ir) continue; + for (int x = -10; x <= 10; ++x) { + if (abs(x) > ir) continue; - vec2 offset = vec2(float(x), float(y)); - float dist2 = dot(offset, offset); + vec2 offset = vec2(float(x), float(y)); + float dist2 = dot(offset, offset); - // tweak sigma to taste; lower sigma = sharper falloff - float sigma = radius * 0.5 + 0.001; - float w = exp(-dist2 / (2.0 * sigma * sigma)); + // tweak sigma to taste; lower sigma = sharper falloff + float sigma = radius * 0.5 + 0.001; + float w = exp(-dist2 / (2.0 * sigma * sigma)); - acc += texture(tex, uv + offset * texelSize) * w; - weightSum += w; - } + acc += texture(tex, uv + offset * texelSize) * w; + weightSum += w; } + } - return acc / max(weightSum, 0.00001); + return acc / max(weightSum, 0.00001); } float getLumAtPx(vec2 uv, float contrast) { -// vec4 tex = texture(u_image, uv); - vec4 tex = blurTexture(u_image, uv, vec2(1. / u_resolution), u_blur); - + // vec4 tex = texture(u_image, uv); + vec4 tex = blurTexture(u_image, uv, vec2(1. / u_resolution), u_smoothness); + vec3 color = vec3( sigmoid(tex.r, contrast), sigmoid(tex.g, contrast), @@ -194,46 +194,6 @@ vec3 blendHardLight(vec3 base, vec3 blend, float opacity) { return (blendHardLight(base, blend) * opacity + base * (1.0 - opacity)); } -const int SAMPLES = 4; - -float sampleLumOnLine(vec2 uvNorm, float contrast, vec2 p_current) { -// float halfSpanPx = mix(0., 40., u_softness); - float halfSpanPx = 5.; - vec2 gradPy = vec2(dFdx(p_current.y), dFdy(p_current.y)); - vec2 t = normalize(vec2(-gradPy.y, gradPy.x) + 1e-8); - - vec2 stepNorm = (t / u_resolution) * halfSpanPx / float((SAMPLES - 1) / 2); - - float sum = 0.0; - float wsum = 0.0; - int N = (SAMPLES - 1) / 2; - for (int i = -N; i <= N; ++i) { - float w = 1.0 - (abs(float(i)) / float(N + 1)); - vec2 uvTapNorm = uvNorm + stepNorm * float(i); - vec2 uvTapImg = getImageUV(uvTapNorm, vec2(1.0)); - sum += w * getLumAtPx(uvTapImg, contrast); - wsum += w; - } - return sum / max(wsum, 1e-6); -} - - -vec2 projectToCenterlineUV(vec2 uvNorm, vec2 p_val) { - vec2 gradPy = vec2(dFdx(p_val.y), dFdy(p_val.y)); - float gradLen = max(length(gradPy), 1e-6); - float dy_p = (floor(p_val.y) + 0.5) - p_val.y; - vec2 deltaPx = (gradPy / gradLen) * (dy_p / gradLen); - return uvNorm + deltaPx / u_resolution; -} - -float pixelDistToCenterline(vec2 p_val) { - vec2 gradPy = vec2(dFdx(p_val.y), dFdy(p_val.y)); - float gradLen = max(length(gradPy), 1e-6); - float dy_p = (fract(p_val.y) - 0.5);// signed p-space distance to center - return dy_p / gradLen;// pixels -} - - void main() { vec2 uv = gl_FragCoord.xy - .5 * u_resolution; @@ -246,60 +206,53 @@ void main() { contrast = mix(.1, 4., pow(u_contrast, 2.)); } - float lumOrig = getLumAtPx(uvOriginal, contrast); + float lum = getLumAtPx(uvOriginal, contrast); - float t = .3 * u_time; - float frame = getImgFrame(v_imageUV, 0.); - lumOrig = mix(1., lumOrig, frame); + lum = mix(1., lum, frame); uv = v_objectUV; vec2 p = uv; float angle = -u_angle * PI / 180.; -// p = rotate(p, angle + u_stripeWidth * lumOrig); - p = rotate(p, u_wave * (1. - lumOrig)); + p = rotate(p, angle + u_wave * (1. - lum)); p *= u_size; + vec2 pBase = v_objectUV * u_size; + float aaBase = fwidth(pBase.y); + float n = doubleSNoise(uv + 100., u_time); - p.y += .4 * n * lumOrig * u_noise * u_size; - - float dPx = pixelDistToCenterline(p); - float coverage = abs(dPx); - vec2 uvOnLineNorm = projectToCenterlineUV(uvNormalised, p); - vec2 uvOnLineImg = getImageUV(uvOnLineNorm, vec2(1.)); - float lum = getLumAtPx(uvOnLineImg, contrast); -// float lum = sampleLumOnLine(uvOnLineNorm, coverage * contrast, p); - lum = lumOrig; - lum = mix(1., lum, frame); + p.y += .4 * n * lum * u_noise * u_size; - float line = 0.; - - { - vec2 stripeMap = abs(fract(p) - .5); - vec2 d = abs(stripeMap); - vec2 stripeSign = sign(stripeMap); - vec2 fw = fwidth(p); - float aa = fw.y; - float w = aa; -// float w = 0.; - w = mix(u_stripeWidth, 0., lum); -// w = clamp(w, 0., .5 - aa); - w = clamp(w, aa, .5); - line += sst(mix(w, aa, u_softness) - aa, w + aa, d.y); - } + vec2 stripeMap = abs(fract(p) - .5); + vec2 d = abs(stripeMap); + vec2 stripeSign = sign(stripeMap); + + float aa = fwidth(p).y; + float distortAmount = max(aa - aaBase, 0.0); + float highDistort = clamp(distortAmount * 5.0, 0.0, 1.0); + + float w = mix(.5 * u_stripeWidth, 0., lum); + w = clamp(w, aa, .5 - aa); + + float loSharp = mix(w, aa, u_softness); + float loBlurry = 0.; + float hiSharp = w + aa; + float hiBlurry = w + aa; + + float lo = mix(loSharp, loBlurry, highDistort); + float hi = mix(hiSharp, hiBlurry, highDistort); + + float line = sst(lo, hi, d.y); line = mix(1., line, frame); line = clamp(line, 0., 1.); - vec2 grainSize = 1000. * vec2(1., 1. / u_imageAspectRatio); vec2 grainUV = getImageUV(uvNormalised, grainSize); float grain = valueNoise(grainUV); grain = smoothstep(.55, .7 + .2 * u_grainMixer, grain); grain *= u_grainMixer; -// grain *= sst(.1, .0, w); -// line = mix(line, 0., grain); -// line += grain; + // line += grain; vec3 color = vec3(0.); float opacity = 1.; @@ -307,55 +260,54 @@ void main() { float stripeId = floor(p.y); color = mix(u_colorBack.rgb, u_colorFront.rgb, line); - fragColor = vec4(color, 1.); } `; export interface HalftoneLinesUniforms extends ShaderSizingUniforms { - u_colorBack: [number, number, number, number]; - u_colorFront: [number, number, number, number]; - u_image: HTMLImageElement | string | undefined; - u_stripeWidth: number; - u_blur: number; - u_size: number; - u_wave: number; - u_noise: number; - u_softness: number; - u_angle: number; - u_type: (typeof HalftoneLinesTypes)[HalftoneLinesType]; - u_contrast: number; - u_originalColors: boolean; - u_inverted: boolean; - u_grainMixer: number; - u_grainOverlay: number; + u_colorBack: [number, number, number, number]; + u_colorFront: [number, number, number, number]; + u_image: HTMLImageElement | string | undefined; + u_stripeWidth: number; + u_smoothness: number; + u_size: number; + u_wave: number; + u_noise: number; + u_softness: number; + u_angle: number; + u_type: (typeof HalftoneLinesTypes)[HalftoneLinesType]; + u_contrast: number; + u_originalColors: boolean; + u_inverted: boolean; + u_grainMixer: number; + u_grainOverlay: number; } export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionParams { - colorBack?: string; - colorFront?: string; - image?: HTMLImageElement | string | undefined; - stripeWidth?: number; - blur?: number; - size?: number; - softness?: number; - wave?: number; - noise?: number; - angle?: number; - type?: HalftoneLinesType; - - contrast?: number; - originalColors?: boolean; - inverted?: boolean; - grainMixer?: number; - grainOverlay?: number; + colorBack?: string; + colorFront?: string; + image?: HTMLImageElement | string | undefined; + stripeWidth?: number; + smoothness?: number; + size?: number; + softness?: number; + wave?: number; + noise?: number; + angle?: number; + type?: HalftoneLinesType; + + contrast?: number; + originalColors?: boolean; + inverted?: boolean; + grainMixer?: number; + grainOverlay?: number; } export const HalftoneLinesTypes = { - classic: 0, - gooey: 1, - holes: 2, - soft: 3, + classic: 0, + gooey: 1, + holes: 2, + soft: 3, } as const; export type HalftoneLinesType = keyof typeof HalftoneLinesTypes; From 8f8b7bd76b562c5fc23bc8f4d9a541b30d3d13dd Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 15 Nov 2025 10:28:09 +0100 Subject: [PATCH 19/84] halftoneLines blur limit --- .../src/app/(shaders)/halftone-lines/page.tsx | 6 ++--- packages/shaders-react/src/index.ts | 1 + packages/shaders/src/index.ts | 1 + .../shaders/src/shaders/halftone-lines.ts | 22 +++++++++++-------- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index 26698c492..ef987969e 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -5,7 +5,7 @@ import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { HalftoneLinesType, HalftoneLinesTypes, ShaderFit } from '@paper-design/shaders'; +import { HalftoneLinesType, HalftoneLinesTypes, halftoneLinesMeta, ShaderFit } from '@paper-design/shaders'; import { levaImageButton } from '@/helpers/leva-image-button'; import { useState, useEffect, useCallback } from 'react'; import { toHsla } from '@/helpers/color-utils'; @@ -71,8 +71,8 @@ const HalftoneLinesWithControls = () => { ); return { stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, - // softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, - smoothness: { value: defaults.smoothness, min: 0, max: 20, order: 202 }, + softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, + smoothness: { value: defaults.smoothness, min: 0, max: halftoneLinesMeta.maxBlurRadius, order: 202 }, wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index c12291807..dc807626e 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -135,4 +135,5 @@ export { staticMeshGradientMeta, staticRadialGradientMeta, foldsMeta, + halftoneLinesMeta, } from '@paper-design/shaders'; diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index 09d7015b0..b76a61ddb 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -221,6 +221,7 @@ export { } from './shaders/folds.js'; export { + halftoneLinesMeta, halftoneLinesFragmentShader, HalftoneLinesTypes, type HalftoneLinesType, diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index 5a069f60c..3cb118fd8 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -2,6 +2,10 @@ import type {ShaderMotionParams} from '../shader-mount.js'; import {sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms} from '../shader-sizing.js'; import {declarePI, rotation2, simplexNoise, proceduralHash21} from '../shader-utils.js'; +export const halftoneLinesMeta = { + maxBlurRadius: 8, +} as const; + /** * * Fluid motion imitation applied over user image @@ -135,16 +139,16 @@ float sigmoid(float x, float k) { vec4 blurTexture(sampler2D tex, vec2 uv, vec2 texelSize, float radius) { // clamp radius so loops have a known max - float r = clamp(radius, 0.0, 10.0); + float r = clamp(radius, 0., float(${ halftoneLinesMeta.maxBlurRadius })); int ir = int(r); vec4 acc = vec4(0.0); float weightSum = 0.0; // simple Gaussian-ish weights based on distance - for (int y = -10; y <= 10; ++y) { + for (int y = -20; y <= ${ halftoneLinesMeta.maxBlurRadius }; ++y) { if (abs(y) > ir) continue; - for (int x = -10; x <= 10; ++x) { + for (int x = -20; x <= ${ halftoneLinesMeta.maxBlurRadius }; ++x) { if (abs(x) > ir) continue; vec2 offset = vec2(float(x), float(y)); @@ -202,9 +206,9 @@ void main() { vec2 uvOriginal = getImageUV(uvNormalised, vec2(1.)); float contrast = mix(0., 15., u_contrast); - if (u_originalColors == true) { - contrast = mix(.1, 4., pow(u_contrast, 2.)); - } +// if (u_originalColors == true) { +// contrast = mix(.1, 4., pow(u_contrast, 2.)); +// } float lum = getLumAtPx(uvOriginal, contrast); @@ -214,7 +218,7 @@ void main() { uv = v_objectUV; vec2 p = uv; float angle = -u_angle * PI / 180.; - p = rotate(p, angle + u_wave * (1. - lum)); + p = rotate(p, angle + u_wave * lum); p *= u_size; vec2 pBase = v_objectUV * u_size; @@ -224,7 +228,7 @@ void main() { p.y += .4 * n * lum * u_noise * u_size; vec2 stripeMap = abs(fract(p) - .5); - vec2 d = abs(stripeMap); + vec2 stripeDist = abs(stripeMap); vec2 stripeSign = sign(stripeMap); float aa = fwidth(p).y; @@ -242,7 +246,7 @@ void main() { float lo = mix(loSharp, loBlurry, highDistort); float hi = mix(hiSharp, hiBlurry, highDistort); - float line = sst(lo, hi, d.y); + float line = sst(lo, hi, stripeDist.y); line = mix(1., line, frame); line = clamp(line, 0., 1.); From e031f8d53f87e58125af351af13f5c3ddc63976d Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 15 Nov 2025 10:29:16 +0100 Subject: [PATCH 20/84] antialiased shift --- docs/src/app/(shaders)/folds/page.tsx | 7 +- packages/shaders-react/src/shaders/folds.tsx | 21 +++--- packages/shaders/src/shaders/folds.ts | 73 +++++++------------- 3 files changed, 42 insertions(+), 59 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index e20b0d58a..d75997bfa 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -5,7 +5,7 @@ import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { FoldsShapes, FoldsShape, colorPanelsMeta, foldsMeta } from '@paper-design/shaders'; +import { foldsMeta } from '@paper-design/shaders'; import { ShaderFit } from '@paper-design/shaders'; import { levaImageButton } from '@/helpers/leva-image-button'; import { useState, Suspense, useEffect, useCallback } from 'react'; @@ -47,7 +47,7 @@ const imageFiles = [ const FoldsWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); - const [image, setImage] = useState('/images/logos/nike.svg'); + const [image, setImage] = useState('/images/logos/apple.svg'); useEffect(() => { if (imageIdx >= 0) { @@ -73,10 +73,11 @@ const FoldsWithControls = () => { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, + wave: { value: defaults.wave, min: 0, max: 1, order: 201 }, alphaMask: { value: defaults.alphaMask, order: 202 }, gap: { value: defaults.gap, order: 202 }, size: { value: defaults.size, min: 1, max: 50, order: 203 }, - shift: { value: defaults.shift, min: 0, max: 1, order: 204 }, + shift: { value: defaults.shift, min: -.5, max: .5, order: 204 }, noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index 1624ba604..d495c1815 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -28,19 +28,20 @@ export const defaultPreset: FoldsPreset = { name: 'Default', params: { ...defaultObjectSizing, - scale: 2, - speed: 0.1, + scale: 1, + speed: 0, frame: 0, colorBack: '#000000', colors: ['#ff9d00', '#fd4f30', '#809bff', '#6d2eff', '#333aff', '#f15cff', '#ffd557'], - stripeWidth: 0.48, + stripeWidth: 0.5, alphaMask: false, - gap: true, - size: 20, - shift: 0, - noise: 0, - outerNoise: 0.9, - softness: 0.4, + gap: false, + size: 10, + shift: -.0, + noise: 0.1, + outerNoise: 1, + softness: 0, + wave: 0, angle: 0, shape: 'diamond', }, @@ -58,6 +59,7 @@ export const Folds: React.FC = memo(function FoldsImpl({ noise = defaultPreset.params.noise, outerNoise = defaultPreset.params.outerNoise, softness = defaultPreset.params.softness, + wave = defaultPreset.params.wave, stripeWidth = defaultPreset.params.stripeWidth, alphaMask = defaultPreset.params.alphaMask, gap = defaultPreset.params.gap, @@ -128,6 +130,7 @@ export const Folds: React.FC = memo(function FoldsImpl({ u_noise: noise, u_outerNoise: outerNoise, u_softness: softness, + u_wave: wave, u_stripeWidth: stripeWidth, u_alphaMask: alphaMask, u_gap: gap, diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 366b43c99..82585abd7 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -35,6 +35,7 @@ uniform float u_stripeWidth; uniform bool u_alphaMask; uniform bool u_gap; uniform float u_size; +uniform float u_wave; uniform float u_shift; uniform float u_noise; uniform float u_outerNoise; @@ -206,13 +207,9 @@ void main() { uv.y = 1. - uv.y; } - float edge = 0.; - - float edgeRaw = img.r; -// edge = 1. - blurEdge3x3(u_image, uv, dudx, dudy, 2., edgeRaw); - edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 5., edgeRaw); - float edgeCopy = edge; - + float edge = img.r; +// edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 5., edge); + edge = 1. - edge; float imgAlpha = img.g; // imgAlpha = blurEdge3x3_G(u_image, uv, dudx, dudy, 2., img.g); @@ -229,57 +226,37 @@ void main() { float n = doubleSNoise(uv + 100., u_time); float edgeAtten = edge + u_outerNoise * (1. - edge); - p.y += edgeAtten * n * 10. * u_noise; - - float stripeId = floor(p.y); - - vec2 fw = fwidth(p); - float aa = fw.y; - -// vec2 fw = fwidth(p); -// float aa = fw.y; - - // stripeMap in [-0.5, 0.5) - vec2 stripeMap = fract(p) - 0.5; - vec2 dSign = sign(stripeMap); - - float w = 0.0; - w = mix(0.0, 3.0 * pow(u_stripeWidth, 2.0), edge); - w = clamp(w, aa, 1.0); - if (u_gap == true) { - w = clamp(w, aa, 0.5 - aa); - } - - // ---- NEW: move the center depending on w ---- + float y = p.y + edgeAtten * n * 10. * u_noise; - // how far from "maximum" half-width (0.5) - float availableShift = max(0.0, 0.5 - w);// 0 when w = 0.5 + float w = u_stripeWidth * edge; - // map u_shift [0,1] -> [-1,1] - float signedPos = u_shift * 2.0 - 1.0; + y += 2. * sign(u_shift) * mix(0., w, abs(u_shift)); - // final offset: small when w is big, large when w is small - float centerOffset = signedPos * availableShift; - - // shift only in Y, before abs() - stripeMap.y -= centerOffset; - - // recompute distance after shifting - vec2 d = abs(stripeMap); + + float stripeId = floor(y); - // ---------------------------------------------- + float fy = fract(y); + + float m = clamp(.5, 0., 1.); +// float m = clamp(.5 + u_shift, 0., 1.); + float left = fy / m; + float right = (1. - fy) / (1. - m); + float stripeMap = 1. - min(left, right); + stripeMap *= .5; + + float aa = fwidth(fy); + + w = clamp(w, aa, .5 - aa); - float line = d.y; - float lMin = mix(w, aa, u_softness) - aa; + float lMin = w - aa; float lMax = w + aa; - line = 1.0 - sst(lMin, lMax, line); - + float line = 1.0 - sst(lMin, lMax, stripeMap); + if (u_alphaMask == true) { line *= imgAlpha; } - int colorIdx = int(posMod(stripeId, u_colorsCount)); vec4 stripeColor = u_colors[0]; for (int i = 0; i < ${foldsMeta.maxColorCount}; i++) { @@ -745,6 +722,7 @@ export interface FoldsUniforms extends ShaderSizingUniforms { u_noise: number; u_outerNoise: number; u_softness: number; + u_wave: number; u_angle: number; u_shape: (typeof FoldsShapes)[FoldsShape]; u_isImage: boolean; @@ -759,6 +737,7 @@ export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { gap?: boolean; size?: number; softness?: number; + wave?: number; shift?: number; noise?: number; outerNoise?: number; From a58c3c872600dc23c77785f8d2e7f4276c20f882 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 15 Nov 2025 17:26:15 +0100 Subject: [PATCH 21/84] saving progress on folds --- docs/src/app/(shaders)/folds/page.tsx | 18 +- packages/shaders-react/src/shaders/folds.tsx | 24 +-- packages/shaders/src/shaders/folds.ts | 168 +++++++++++++++++-- 3 files changed, 176 insertions(+), 34 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index d75997bfa..915dcbc07 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -71,22 +71,22 @@ const FoldsWithControls = () => { const [params, setParams] = useControls(() => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, + stripeWidth: { value: defaults.stripeWidth, min: 0, max: 0.5, order: 200 }, softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, - wave: { value: defaults.wave, min: 0, max: 1, order: 201 }, + gradient: { value: defaults.gradient, min: 0, max: 1, order: 201 }, alphaMask: { value: defaults.alphaMask, order: 202 }, gap: { value: defaults.gap, order: 202 }, - size: { value: defaults.size, min: 1, max: 50, order: 203 }, - shift: { value: defaults.shift, min: -.5, max: .5, order: 204 }, + size: { value: defaults.size, min: 6, max: 50, order: 203 }, + shift: { value: defaults.shift, min: -1, max: 1, order: 204 }, noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, - speed: { value: defaults.speed, min: 0, max: 4, order: 300 }, + speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, - rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, - offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, - offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, - fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 305 }, + // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, + // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, + // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, + // fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 305 }, Image: folder( { 'Upload image': levaImageButton((img?: HTMLImageElement) => setImage(img ?? '')), diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index d495c1815..0e69e48a2 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -28,20 +28,20 @@ export const defaultPreset: FoldsPreset = { name: 'Default', params: { ...defaultObjectSizing, - scale: 1, - speed: 0, + scale: 0.8, + speed: 0.2, frame: 0, colorBack: '#000000', - colors: ['#ff9d00', '#fd4f30', '#809bff', '#6d2eff', '#333aff', '#f15cff', '#ffd557'], - stripeWidth: 0.5, + colors: ['#ff9d00', '#fd4f30', '#809bff', '#ffffff'], + stripeWidth: 1, alphaMask: false, gap: false, - size: 10, - shift: -.0, - noise: 0.1, - outerNoise: 1, - softness: 0, - wave: 0, + size: 12, + shift: 1, + noise: 0.5, + outerNoise: 0, + softness: 0.2, + gradient: 0.8, angle: 0, shape: 'diamond', }, @@ -59,7 +59,7 @@ export const Folds: React.FC = memo(function FoldsImpl({ noise = defaultPreset.params.noise, outerNoise = defaultPreset.params.outerNoise, softness = defaultPreset.params.softness, - wave = defaultPreset.params.wave, + gradient = defaultPreset.params.gradient, stripeWidth = defaultPreset.params.stripeWidth, alphaMask = defaultPreset.params.alphaMask, gap = defaultPreset.params.gap, @@ -130,7 +130,7 @@ export const Folds: React.FC = memo(function FoldsImpl({ u_noise: noise, u_outerNoise: outerNoise, u_softness: softness, - u_wave: wave, + u_gradient: gradient, u_stripeWidth: stripeWidth, u_alphaMask: alphaMask, u_gap: gap, diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 82585abd7..95ff6c07c 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -35,7 +35,7 @@ uniform float u_stripeWidth; uniform bool u_alphaMask; uniform bool u_gap; uniform float u_size; -uniform float u_wave; +uniform float u_gradient; uniform float u_shift; uniform float u_noise; uniform float u_outerNoise; @@ -192,6 +192,17 @@ float posMod(float x, float m) { return x - m * floor(x / m); } +vec2 getPosition(int i, float t) { + float a = float(i) * .37; + float b = .6 + fract(float(i) / 3.) * .9; + float c = .8 + fract(float(i + 1) / 4.); + + float x = sin(t * b + a); + float y = cos(t * c + a * 1.5); + + return .5 + .5 * vec2(x, y); +} + void main() { const float firstFrameOffset = 2.8; @@ -208,8 +219,8 @@ void main() { } float edge = img.r; -// edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 5., edge); - edge = 1. - edge; + edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); +// edge = 1. - edge; float imgAlpha = img.g; // imgAlpha = blurEdge3x3_G(u_image, uv, dudx, dudy, 2., img.g); @@ -226,7 +237,7 @@ void main() { float n = doubleSNoise(uv + 100., u_time); float edgeAtten = edge + u_outerNoise * (1. - edge); - float y = p.y + edgeAtten * n * 10. * u_noise; + float y = p.y + edgeAtten * .5 * n * u_size * u_noise; float w = u_stripeWidth * edge; @@ -237,8 +248,7 @@ void main() { float fy = fract(y); - float m = clamp(.5, 0., 1.); -// float m = clamp(.5 + u_shift, 0., 1.); + float m = .5; float left = fy / m; float right = (1. - fy) / (1. - m); float stripeMap = 1. - min(left, right); @@ -257,13 +267,54 @@ void main() { line *= imgAlpha; } + float tst = 0.; + { +// float m = clamp(u_wave, .02, .98); + float m = 0.; + float left = fy / m; + float right = (1. - fy) / (1. - m); + tst = 1. - min(left, right); + } + + line -= sst(u_softness, 0., 1. - fy); + line = clamp(line, 0., 1.); + int colorIdx = int(posMod(stripeId, u_colorsCount)); - vec4 stripeColor = u_colors[0]; - for (int i = 0; i < ${foldsMeta.maxColorCount}; i++) { + + vec4 orderedStripeColor = u_colors[0]; + for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { + if (i >= int(u_colorsCount)) break; + float isHit = 1.0 - step(.5, abs(float(i - colorIdx))); + orderedStripeColor = mix(orderedStripeColor, u_colors[i], isHit); + } + + + vec3 ccc = vec3(0.); + float ooo = 0.; + float totalWeight = 0.; + + for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { if (i >= int(u_colorsCount)) break; - float isHit = 1.0 - step(.5, abs(float(i - colorIdx))); - stripeColor = mix(stripeColor, u_colors[i], isHit); + + vec2 pos = getPosition(i, 1.5 * t); + vec3 colorFraction = u_colors[i].rgb * u_colors[i].a; + float oooFraction = u_colors[i].a; + + float dist = .5 * length(uv + .5 - pos); + + dist = pow(dist, 3.5); + float weight = 1. / (dist + 1e-3); + ccc += colorFraction * weight; + ooo += oooFraction * weight; + totalWeight += weight; } + + ccc /= max(1e-4, totalWeight); + ooo /= max(1e-4, totalWeight); + vec4 stripeColor = vec4(ccc, ooo); + + + stripeColor = mix(orderedStripeColor, mix(stripeColor, orderedStripeColor, tst), u_gradient); vec3 stripePremulRGB = stripeColor.rgb * stripeColor.a; stripePremulRGB *= line; @@ -271,12 +322,17 @@ void main() { vec3 color = stripePremulRGB; float opacity = stripeA; - + + if (u_gap == true) { + color += .6 * (1. - edge) * imgAlpha; + } + vec3 bgColor = u_colorBack.rgb * u_colorBack.a; color = color + bgColor * (1. - opacity); opacity = opacity + u_colorBack.a * (1. - opacity); fragColor = vec4(color, opacity); +// fragColor = vec4(ccc, 1.); } `; @@ -572,6 +628,92 @@ export function toProcessedFolds(file: File | string): Promise<{ imageData: Imag }); } +function blurRedChannel( + imageData: ImageData, + width: number, + height: number, + radius = 2 +) { + const src = imageData.data; + const pixelCount = width * height; + + const tmp = new Uint8ClampedArray(pixelCount); + const dst = new Uint8ClampedArray(pixelCount); + + // --- Horizontal blur --- + for (let y = 0; y < height; y++) { + let sum = 0; + let count = 0; + + // initial window centered at x = 0 + for (let dx = -radius; dx <= radius; dx++) { + const xClamped = Math.max(0, Math.min(width - 1, dx)); + const idx = (y * width + xClamped) * 4; + sum += src[idx]!; + count++; + } + + for (let x = 0; x < width; x++) { + const outIdx = y * width + x; + tmp[outIdx] = sum / count; + + const xRemove = x - radius; + if (xRemove >= 0) { + const idxRemove = (y * width + xRemove) * 4; + sum -= src[idxRemove]!; + count--; + } + + const xAdd = x + radius + 1; + if (xAdd < width) { + const idxAdd = (y * width + xAdd) * 4; + sum += src[idxAdd]!; + count++; + } + } + } + + // --- Vertical blur --- + for (let x = 0; x < width; x++) { + let sum = 0; + let count = 0; + + // initial window centered at y = 0 + for (let dy = -radius; dy <= radius; dy++) { + const yClamped = Math.max(0, Math.min(height - 1, dy)); + const idx = yClamped * width + x; + sum += tmp[idx]!; + count++; + } + + for (let y = 0; y < height; y++) { + const outIdx = y * width + x; + dst[outIdx] = sum / count; + + const yRemove = y - radius; + if (yRemove >= 0) { + const idxRemove = yRemove * width + x; + sum -= tmp[idxRemove]!; + count--; + } + + const yAdd = y + radius + 1; + if (yAdd < height) { + const idxAdd = yAdd * width + x; + sum += tmp[idxAdd]!; + count++; + } + } + } + + // --- Write blurred red back into ImageData --- + for (let i = 0; i < pixelCount; i++) { + const px = i * 4; + src[px] = dst[i]!; + } +} + + function buildSparseData( shapeMask: Uint8Array, boundaryMask: Uint8Array, @@ -722,7 +864,7 @@ export interface FoldsUniforms extends ShaderSizingUniforms { u_noise: number; u_outerNoise: number; u_softness: number; - u_wave: number; + u_gradient: number; u_angle: number; u_shape: (typeof FoldsShapes)[FoldsShape]; u_isImage: boolean; @@ -737,7 +879,7 @@ export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { gap?: boolean; size?: number; softness?: number; - wave?: number; + gradient?: number; shift?: number; noise?: number; outerNoise?: number; From e19fa8d48ae6dd28dbe34b58e1946180b06a1760 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 16 Nov 2025 00:45:18 +0100 Subject: [PATCH 22/84] new logo-spots sketch --- .../(shaders)/mesh-gradient-logo/layout.tsx | 9 + .../app/(shaders)/mesh-gradient-logo/page.tsx | 135 +++ .../src/shader-defs/mesh-gradient-logo-def.ts | 19 + packages/shaders-react/src/index.ts | 5 + .../src/shaders/mesh-gradient-logo.tsx | 163 ++++ packages/shaders/src/index.ts | 8 + .../shaders/src/shaders/mesh-gradient-logo.ts | 809 ++++++++++++++++++ 7 files changed, 1148 insertions(+) create mode 100644 docs/src/app/(shaders)/mesh-gradient-logo/layout.tsx create mode 100644 docs/src/app/(shaders)/mesh-gradient-logo/page.tsx create mode 100644 docs/src/shader-defs/mesh-gradient-logo-def.ts create mode 100644 packages/shaders-react/src/shaders/mesh-gradient-logo.tsx create mode 100644 packages/shaders/src/shaders/mesh-gradient-logo.ts diff --git a/docs/src/app/(shaders)/mesh-gradient-logo/layout.tsx b/docs/src/app/(shaders)/mesh-gradient-logo/layout.tsx new file mode 100644 index 000000000..f0964a3f1 --- /dev/null +++ b/docs/src/app/(shaders)/mesh-gradient-logo/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Mesh Gradient Logo Filter • Paper', +}; + +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx b/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx new file mode 100644 index 000000000..4772e4449 --- /dev/null +++ b/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { MeshGradientLogo, meshGradientLogoPresets } from '@paper-design/shaders-react'; +import { useControls, button, folder } from 'leva'; +import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; +import { usePresetHighlight } from '@/helpers/use-preset-highlight'; +import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; +import { meshGradientLogoMeta } from '@paper-design/shaders'; +import { ShaderFit } from '@paper-design/shaders'; +import { levaImageButton } from '@/helpers/leva-image-button'; +import { useState, Suspense, useEffect, useCallback } from 'react'; +import { ShaderDetails } from '@/components/shader-details'; +import { ShaderContainer } from '@/components/shader-container'; +import { useUrlParams } from '@/helpers/use-url-params'; +import { meshGradientLogoDef } from '@/shader-defs/mesh-gradient-logo-def'; +import { toHsla } from '@/helpers/color-utils'; +import { useColors } from "@/helpers/use-colors"; + +// Override just for the docs, we keep it transparent in the preset +// meshGradientLogoPresets[0].params.colorBack = '#000000'; + +const { worldWidth, worldHeight, ...defaults } = meshGradientLogoPresets[0].params; + +const imageFiles = [ + 'chanel.svg', + 'cibc.svg', + 'cloudflare.svg', + 'apple.svg', + 'paper-logo-only.svg', + 'diamond.svg', + 'discord.svg', + 'paper-logo-only', + 'enterprise-rent.svg', + 'kfc.svg', + 'microsoft.svg', + 'nasa.svg', + 'netflix.svg', + 'nike.svg', + 'paper.svg', + 'perkins.svg', + 'pizza-hut.svg', + 'remix.svg', + 'rogers.svg', + 'vercel.svg', + 'volkswagen.svg', +] as const; + +const MeshGradientLogoWithControls = () => { + const [imageIdx, setImageIdx] = useState(-1); + const [image, setImage] = useState('/images/logos/apple.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: meshGradientLogoMeta.maxColorCount, + }); + + const [params, setParams] = useControls(() => { + return { + colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorInner: { value: toHsla(defaults.colorInner), order: 101 }, + // stripeWidth: { value: defaults.stripeWidth, min: 0, max: 0.5, order: 200 }, + contour: { value: defaults.contour, min: 0, max: 1, order: 201 }, + roundness: { value: defaults.roundness, min: 0, max: 1, order: 201 }, + // alphaMask: { value: defaults.alphaMask, order: 202 }, + // gap: { value: defaults.gap, order: 202 }, + // size: { value: defaults.size, min: 6, max: 50, order: 203 }, + // shift: { value: defaults.shift, min: -1, max: 1, order: 204 }, + // noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, + // outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 205 }, + // angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, + speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, + scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, + // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, + // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, + // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, + // fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 305 }, + Image: folder( + { + 'Upload image': levaImageButton((img?: HTMLImageElement) => setImage(img ?? '')), + }, + { order: -1 } + ), + }; + }, [colors.length]); + + useControls(() => { + const presets = Object.fromEntries( + meshGradientLogoPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + name, + button(() => { + const { colors, ...presetParams } = preset; + setColors(colors); + setParamsSafe(params, setParams, presetParams); + }), + ]) + ); + return { + Presets: folder(presets, { order: -2 }), + }; + }); + + // Reset to defaults on mount, so that Leva doesn't show values from other + // shaders when navigating (if two shaders have a color1 param for example) + useResetLevaParams(params, setParams, defaults); + useUrlParams(params, setParams, meshGradientLogoDef, setColors); + usePresetHighlight(meshGradientLogoPresets, params); + cleanUpLevaParams(params); + + return ( + <> + + + + + + + + ); +}; + +export default MeshGradientLogoWithControls; diff --git a/docs/src/shader-defs/mesh-gradient-logo-def.ts b/docs/src/shader-defs/mesh-gradient-logo-def.ts new file mode 100644 index 000000000..f0fc41443 --- /dev/null +++ b/docs/src/shader-defs/mesh-gradient-logo-def.ts @@ -0,0 +1,19 @@ +import { meshGradientLogoPresets } from '@paper-design/shaders-react'; +import type { ShaderDef } from './shader-def-types'; +import { animatedCommonParams } from './common-param-def'; + +const defaultParams = meshGradientLogoPresets[0].params; + +export const meshGradientLogoDef: ShaderDef = { + name: 'Liquid Metal', + description: 'Futuristic liquid metal material applied to uploaded logo or abstract shape.', + params: [ + { + name: 'image', + type: 'HTMLImageElement | string', + description: + 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', + }, + ...animatedCommonParams, + ], +}; diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index dc807626e..9e228d57c 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -115,6 +115,10 @@ export { HalftoneLines, halftoneLinesPresets } from './shaders/halftone-lines.js export type { HalftoneLinesProps } from './shaders/halftone-lines.js'; export type { HalftoneLinesUniforms, HalftoneLinesParams } from '@paper-design/shaders'; +export { MeshGradientLogo, meshGradientLogoPresets } from './shaders/mesh-gradient-logo.js'; +export type { MeshGradientLogoProps } from './shaders/mesh-gradient-logo.js'; +export type { MeshGradientLogoUniforms, MeshGradientLogoParams } from '@paper-design/shaders'; + export { isPaperShaderElement, getShaderColorFromString } from '@paper-design/shaders'; export type { PaperShaderElement, ShaderFit, ShaderSizingParams, ShaderSizingUniforms } from '@paper-design/shaders'; @@ -136,4 +140,5 @@ export { staticRadialGradientMeta, foldsMeta, halftoneLinesMeta, + meshGradientLogoMeta, } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/mesh-gradient-logo.tsx b/packages/shaders-react/src/shaders/mesh-gradient-logo.tsx new file mode 100644 index 000000000..5941523eb --- /dev/null +++ b/packages/shaders-react/src/shaders/mesh-gradient-logo.tsx @@ -0,0 +1,163 @@ +import { memo, useLayoutEffect, useState } from 'react'; +import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; +import { colorPropsAreEqual } from '../color-props-are-equal.js'; +import { + meshGradientLogoFragmentShader, + ShaderFitOptions, + defaultObjectSizing, + type MeshGradientLogoUniforms, + type MeshGradientLogoParams, + toProcessedMeshGradientLogo, + type ImageShaderPreset, + getShaderColorFromString, +} from '@paper-design/shaders'; +import { transparentPixel } from '../transparent-pixel.js'; +import { suspend } from '../suspend.js'; + +export interface MeshGradientLogoProps extends ShaderComponentProps, MeshGradientLogoParams { + /** + * Suspends the component when the image is being processed. + */ + suspendWhenProcessingImage?: boolean; +} + +type MeshGradientLogoPreset = ImageShaderPreset; + +export const defaultPreset: MeshGradientLogoPreset = { + name: 'Default', + params: { + ...defaultObjectSizing, + scale: 0.8, + speed: 0.5, + frame: 0, + colorBack: '#c8dfd900', + colorInner: '#c8dfd9', + colors: ['#ff9d00', '#fd4f30', '#809bff', '#c5bac5'], + stripeWidth: 1, + alphaMask: false, + gap: true, + size: 12, + shift: 1, + noise: 0.5, + outerNoise: 0, + contour: 1, + roundness: 0.8, + angle: 0, + }, +}; +export const meshGradientLogoPresets: MeshGradientLogoPreset[] = [defaultPreset]; + +export const MeshGradientLogo: React.FC = memo(function MeshGradientLogoImpl({ + // Own props + colorBack = defaultPreset.params.colorBack, + colorInner = defaultPreset.params.colorInner, + colors = defaultPreset.params.colors, + speed = defaultPreset.params.speed, + frame = defaultPreset.params.frame, + image = '', + shift = defaultPreset.params.shift, + noise = defaultPreset.params.noise, + outerNoise = defaultPreset.params.outerNoise, + contour = defaultPreset.params.contour, + roundness = defaultPreset.params.roundness, + stripeWidth = defaultPreset.params.stripeWidth, + alphaMask = defaultPreset.params.alphaMask, + gap = defaultPreset.params.gap, + size = defaultPreset.params.size, + angle = defaultPreset.params.angle, + suspendWhenProcessingImage = false, + + // Sizing props + fit = defaultPreset.params.fit, + scale = defaultPreset.params.scale, + rotation = defaultPreset.params.rotation, + originX = defaultPreset.params.originX, + originY = defaultPreset.params.originY, + offsetX = defaultPreset.params.offsetX, + offsetY = defaultPreset.params.offsetY, + worldWidth = defaultPreset.params.worldWidth, + worldHeight = defaultPreset.params.worldHeight, + ...props +}: MeshGradientLogoProps) { + const imageUrl = typeof image === 'string' ? image : image.src; + const [processedStateImage, setProcessedStateImage] = useState(transparentPixel); + + let processedImage: string; + + if (suspendWhenProcessingImage && typeof window !== 'undefined' && imageUrl) { + processedImage = suspend( + (): Promise => toProcessedMeshGradientLogo(imageUrl).then((result) => URL.createObjectURL(result.pngBlob)), + [imageUrl, 'meshGradientLogo'] + ); + } else { + processedImage = processedStateImage; + } + + useLayoutEffect(() => { + if (suspendWhenProcessingImage) { + // Skip doing work in the effect as it's been handled by suspense. + return; + } + + if (!imageUrl) { + setProcessedStateImage(transparentPixel); + return; + } + + let url: string; + let current = true; + + toProcessedMeshGradientLogo(imageUrl).then((result) => { + if (current) { + url = URL.createObjectURL(result.pngBlob); + setProcessedStateImage(url); + } + }); + + return () => { + current = false; + }; + }, [imageUrl, suspendWhenProcessingImage]); + + const uniforms = { + // Own uniforms + u_colors: colors.map(getShaderColorFromString), + u_colorsCount: colors.length, + u_colorBack: getShaderColorFromString(colorBack), + u_colorInner: getShaderColorFromString(colorInner), + u_image: processedImage, + u_shift: shift, + u_noise: noise, + u_outerNoise: outerNoise, + u_contour: contour, + u_roundness: roundness, + u_stripeWidth: stripeWidth, + u_alphaMask: alphaMask, + u_gap: gap, + u_size: size, + u_angle: angle, + u_isImage: Boolean(image), + + // Sizing uniforms + u_fit: ShaderFitOptions[fit], + u_scale: scale, + u_rotation: rotation, + u_offsetX: offsetX, + u_offsetY: offsetY, + u_originX: originX, + u_originY: originY, + u_worldWidth: worldWidth, + u_worldHeight: worldHeight, + } satisfies MeshGradientLogoUniforms; + + return ( + + ); +}, colorPropsAreEqual); diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index b76a61ddb..facf32b65 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -229,6 +229,14 @@ export { type HalftoneLinesUniforms, } from './shaders/halftone-lines.js'; +export { + meshGradientLogoMeta, + meshGradientLogoFragmentShader, + toProcessedMeshGradientLogo, + type MeshGradientLogoParams, + type MeshGradientLogoUniforms, +} from './shaders/mesh-gradient-logo.js'; + // ----- Utils ----- // export { getShaderColorFromString } from './get-shader-color-from-string.js'; export { getShaderNoiseTexture } from './get-shader-noise-texture.js'; diff --git a/packages/shaders/src/shaders/mesh-gradient-logo.ts b/packages/shaders/src/shaders/mesh-gradient-logo.ts new file mode 100644 index 000000000..11fba8ba0 --- /dev/null +++ b/packages/shaders/src/shaders/mesh-gradient-logo.ts @@ -0,0 +1,809 @@ +import type { vec4 } from '../types.js'; +import type { ShaderMotionParams } from '../shader-mount.js'; +import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; +import { declarePI, rotation2, proceduralHash21, colorBandingFix } from '../shader-utils.js'; + +export const meshGradientLogoMeta = { + maxColorCount: 10, +} as const; + +/** + * + * Fluid motion imitation applied over user image + * (animated stripe pattern getting distorted with shape edges) + * + * Uniforms: + * - u_colorBack, u_colorFront (RGBA) + * + */ + +// language=GLSL +export const meshGradientLogoFragmentShader: string = `#version 300 es +precision mediump float; + +uniform sampler2D u_image; +uniform float u_imageAspectRatio; + +uniform vec2 u_resolution; +uniform float u_time; + +uniform vec4 u_colors[${meshGradientLogoMeta.maxColorCount}]; +uniform float u_colorsCount; +uniform vec4 u_colorBack; +uniform vec4 u_colorInner; +uniform float u_contour; +uniform float u_stripeWidth; +uniform bool u_alphaMask; +uniform bool u_gap; +uniform float u_size; +uniform float u_roundness; +uniform float u_shift; +uniform float u_noise; +uniform float u_outerNoise; +uniform float u_angle; + +uniform float u_shape; +uniform bool u_isImage; + +${sizingVariablesDeclaration} + +out vec4 fragColor; + +${ declarePI } +${ rotation2 } + +float getImgFrame(vec2 uv, float th) { + float frame = 1.; + frame *= smoothstep(0., th, uv.y); + frame *= 1.0 - smoothstep(1. - th, 1., uv.y); + frame *= smoothstep(0., th, uv.x); + frame *= 1.0 - smoothstep(1. - th, 1., uv.x); + return frame; +} + +float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) { + vec2 texel = 1.0 / vec2(textureSize(tex, 0)); + vec2 r = max(radius, 0.0) * texel; + + // 1D Gaussian coefficients (Pascal row) + const float a = 1.0;// |offset| = 2 + const float b = 4.0;// |offset| = 1 + const float c = 6.0;// |offset| = 0 + + float norm = 256.0;// (a+b+c+b+a)^2 = 16^2 + float sum = 0.0; + + // y = -2 + { + float wy = a; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, -2.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, -2.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, -2.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, -2.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, -2.0*r.y)).r; + sum += wy * row; + } + + // y = -1 + { + float wy = b; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, -1.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, -1.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, -1.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, -1.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, -1.0*r.y)).r; + sum += wy * row; + } + + // y = 0 (use provided centerSample to avoid an extra fetch) + { + float wy = c; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 0.0)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 0.0)).r + + c * centerSample + + b * texture(tex, uv + vec2(1.0*r.x, 0.0)).r + + a * texture(tex, uv + vec2(2.0*r.x, 0.0)).r; + sum += wy * row; + } + + // y = +1 + { + float wy = b; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 1.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 1.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, 1.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, 1.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, 1.0*r.y)).r; + sum += wy * row; + } + + // y = +2 + { + float wy = a; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 2.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 2.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, 2.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, 2.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, 2.0*r.y)).r; + sum += wy * row; + } + + return sum / norm; +} + +float sst(float edge0, float edge1, float x) { + return smoothstep(edge0, edge1, x); +} + +float getPoint(vec2 dist, float p) { + float v = pow(1. - clamp(0., 1., length(dist)), 1.); + v = smoothstep(0., 1., v); + v = pow(v, p); + return v; +} + + +void main() { + + float t = .3 * u_time; + + vec2 uv = v_imageUV; + vec2 dudx = dFdx(v_imageUV); + vec2 dudy = dFdy(v_imageUV); + vec4 img = textureGrad(u_image, uv, dudx, dudy); + + float edge = img.r; + edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); + + float imgAlpha = img.g; + + float frame = getImgFrame(v_imageUV, 0.); + imgAlpha *= frame; + edge *= frame; + + uv = v_objectUV; + + float offset = 1. - edge; + float shadow = ((1. - edge) * imgAlpha); + shadow = pow(shadow, 5.); + + float shapeOffset = 2.4 * u_contour * offset; + float shaping = 2.5 - shapeOffset; + + float f1, f2, f3, f4; + t = 2. * u_time; + + { + vec2 traj = .5 * vec2(.8 * sin(-.5 * t), .2 + 2.5 * cos(.3 * t)); + float f = getPoint(uv + traj, shaping); + f1 = f; + } + + { + vec2 traj = .5 * vec2(1.7 * sin(-.5 * t), sin(.8 * t)); + float f = getPoint(uv + traj, shaping); + f2 = f; + } + + { + vec2 traj = .5 * vec2(.5 * cos(-.3 * t), cos(-.8 * t)); + float f = getPoint(uv + traj, shaping); + f3 = f; + } + + { + vec2 traj = .5 * vec2(.5 * cos(-.9 * t), .7 * sin(-.2 * t)); + float f = getPoint(uv + traj, shaping); + f4 = f; + } + + f1 -= .5 * f2; + f3 -= f2; + f2 -= f1; + f2 -= f3; + f1 -= f3; + f2 -= f4; + f3 -= f1; + f3 -= f1; + f4 -= f2; + f4 -= f1; + f4 -= f3; + f3 -= f4; + + f1 = sst(u_roundness, u_roundness + 2. * fwidth(f1), f1); + f2 = sst(u_roundness, u_roundness + 2. * fwidth(f2), f2); + f3 = sst(u_roundness, u_roundness + 2. * fwidth(f3), f3); + f4 = sst(u_roundness, u_roundness + 2. * fwidth(f4), f4); + + vec3 color1 = u_colors[0].rgb; + vec3 color2 = u_colors[1].rgb; + vec3 color3 = u_colors[2].rgb; + vec3 color4 = u_colors[3].rgb; + + float opacity = f1 + f2 + f3 + f4; + opacity = clamp(opacity, 0., 1.); + + vec3 color = vec3(0.); + color = mix(color, color4, f4); + color = mix(color, color2, f2); + color = mix(color, color3, f3); + color = mix(color, color1, f1); + + color *= imgAlpha; + opacity *= imgAlpha; + + vec3 backRgb = u_colorBack.rgb * u_colorBack.a; + float backA = u_colorBack.a; + vec3 innerRgb_raw = u_colorInner.rgb * u_colorInner.a; + float innerA_raw = u_colorInner.a; + + float innerA = innerA_raw * imgAlpha; + vec3 innerRgb = innerRgb_raw * imgAlpha; + + vec3 layerRgb = innerRgb + backRgb * (1.0 - innerA); + float layerA = innerA + backA * (1.0 - innerA); + + vec3 colorCopy = color; + color = color + layerRgb * (1.0 - opacity); + opacity = opacity + layerA * (1.0 - opacity); + + + fragColor = vec4(color, opacity); +} +`; + +// Configuration for Poisson solver +export const POISSON_CONFIG_OPTIMIZED = { + measurePerformance: false, // Set to true to see performance metrics + workingSize: 512, // Size to solve Poisson at (will upscale to original size) + iterations: 32, // SOR converges ~2-20x faster than standard Gauss-Seidel +}; + +// Precomputed pixel data for sparse processing +interface SparsePixelData { + interiorPixels: Uint32Array; // Indices of interior pixels + boundaryPixels: Uint32Array; // Indices of boundary pixels + pixelCount: number; + // Neighbor indices for each interior pixel (4 neighbors per pixel) + // Layout: [east, west, north, south] for each pixel + neighborIndices: Int32Array; +} + +export function toProcessedMeshGradientLogo(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const isBlob = typeof file === 'string' && file.startsWith('blob:'); + + return new Promise((resolve, reject) => { + if (!file || !ctx) { + reject(new Error('Invalid file or canvas context')); + return; + } + + const blobContentTypePromise = isBlob && fetch(file).then((res) => res.headers.get('Content-Type')); + const img = new Image(); + img.crossOrigin = 'anonymous'; + const totalStartTime = performance.now(); + + img.onload = async () => { + // Force SVG to load at a high fidelity size if it's an SVG + let isSVG; + + const blobContentType = await blobContentTypePromise; + + if (blobContentType) { + isSVG = blobContentType === 'image/svg+xml'; + } else if (typeof file === 'string') { + isSVG = file.endsWith('.svg') || file.startsWith('data:image/svg+xml'); + } else { + isSVG = file.type === 'image/svg+xml'; + } + + let originalWidth = img.width || img.naturalWidth; + let originalHeight = img.height || img.naturalHeight; + + if (isSVG) { + // Scale SVG to max dimension while preserving aspect ratio + const svgMaxSize = 4096; + const aspectRatio = originalWidth / originalHeight; + + if (originalWidth > originalHeight) { + originalWidth = svgMaxSize; + originalHeight = svgMaxSize / aspectRatio; + } else { + originalHeight = svgMaxSize; + originalWidth = svgMaxSize * aspectRatio; + } + + img.width = originalWidth; + img.height = originalHeight; + } + + // Always scale to working resolution for consistency + const minDimension = Math.min(originalWidth, originalHeight); + const targetSize = POISSON_CONFIG_OPTIMIZED.workingSize; + + // Calculate scale to fit within workingSize + const scaleFactor = targetSize / minDimension; + const width = Math.round(originalWidth * scaleFactor); + const height = Math.round(originalHeight * scaleFactor); + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Processing Mode]`); + console.log(` Original: ${originalWidth}×${originalHeight}`); + console.log(` Working: ${width}×${height} (${(scaleFactor * 100).toFixed(1)}% scale)`); + if (scaleFactor < 1) { + console.log(` Speedup: ~${Math.round(1 / (scaleFactor * scaleFactor))}×`); + } + } + + canvas.width = originalWidth; + canvas.height = originalHeight; + + // Use a smaller canvas for shape detection and Poisson solving + const shapeCanvas = document.createElement('canvas'); + shapeCanvas.width = width; + shapeCanvas.height = height; + + const shapeCtx = shapeCanvas.getContext('2d')!; + shapeCtx.drawImage(img, 0, 0, width, height); + + // 1) Build optimized masks using TypedArrays + const startMask = performance.now(); + + const shapeImageData = shapeCtx.getImageData(0, 0, width, height); + const data = shapeImageData.data; + + // Use Uint8Array for masks (1 byte per pixel vs 8+ bytes for boolean array) + const shapeMask = new Uint8Array(width * height); + const boundaryMask = new Uint8Array(width * height); + + // First pass: identify shape pixels + let shapePixelCount = 0; + for (let i = 0, idx = 0; i < data.length; i += 4, idx++) { + const a = data[i + 3]; + const isShape = a === 0 ? 0 : 1; + shapeMask[idx] = isShape; + shapePixelCount += isShape; + } + + // 2) Optimized boundary detection using sparse approach + // Only check shape pixels, not all pixels + const boundaryIndices: number[] = []; + const interiorIndices: number[] = []; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + if (!shapeMask[idx]) continue; + + // Check if pixel is on boundary (optimized: early exit) + let isBoundary = false; + + // Check 4-connected neighbors first (most common case) + if (x === 0 || x === width - 1 || y === 0 || y === height - 1) { + isBoundary = true; + } else { + // Check all 8 neighbors (including diagonals) for comprehensive boundary detection + isBoundary = + !shapeMask[idx - 1] || // left + !shapeMask[idx + 1] || // right + !shapeMask[idx - width] || // top + !shapeMask[idx + width] || // bottom + !shapeMask[idx - width - 1] || // top-left + !shapeMask[idx - width + 1] || // top-right + !shapeMask[idx + width - 1] || // bottom-left + !shapeMask[idx + width + 1]; // bottom-right + } + + if (isBoundary) { + boundaryMask[idx] = 1; + boundaryIndices.push(idx); + } else { + interiorIndices.push(idx); + } + } + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Mask Building] Time: ${(performance.now() - startMask).toFixed(2)}ms`); + console.log( + ` Shape pixels: ${shapePixelCount} / ${width * height} (${((shapePixelCount / (width * height)) * 100).toFixed(1)}%)` + ); + console.log(` Interior pixels: ${interiorIndices.length}`); + console.log(` Boundary pixels: ${boundaryIndices.length}`); + } + + // 3) Precompute sparse data structure for solver + const sparseData = buildSparseData( + shapeMask, + boundaryMask, + new Uint32Array(interiorIndices), + new Uint32Array(boundaryIndices), + width, + height + ); + + // 4) Solve Poisson equation with optimized sparse solver + const startSolve = performance.now(); + const u = solvePoissonSparse(sparseData, shapeMask, boundaryMask, width, height); + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Poisson Solve] Time: ${(performance.now() - startSolve).toFixed(2)}ms`); + } + + // 5) Generate output image + let maxVal = 0; + let finalImageData: ImageData; + + // Only check shape pixels for max value + for (let i = 0; i < interiorIndices.length; i++) { + const idx = interiorIndices[i]!; + if (u[idx]! > maxVal) maxVal = u[idx]!; + } + + // Create roundness image at working resolution + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = width; + tempCanvas.height = height; + const tempCtx = tempCanvas.getContext('2d')!; + + const tempImg = tempCtx.createImageData(width, height); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + const px = idx * 4; + + if (!shapeMask[idx]) { + tempImg.data[px] = 255; + tempImg.data[px + 1] = 255; + tempImg.data[px + 2] = 255; + tempImg.data[px + 3] = 0; // Alpha = 0 for background + } else { + const poissonRatio = u[idx]! / maxVal; + let gray = 255 * (1 - poissonRatio); + tempImg.data[px] = gray; + tempImg.data[px + 1] = gray; + tempImg.data[px + 2] = gray; + tempImg.data[px + 3] = 255; // Alpha = 255 for shape + } + } + } + tempCtx.putImageData(tempImg, 0, 0); + + // Upscale to original resolution with smooth interpolation + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, originalWidth, originalHeight); + + // Now get the upscaled image data for final output + const outImg = ctx.getImageData(0, 0, originalWidth, originalHeight); + + // Re-apply edges from original resolution with anti-aliasing + // This ensures edges are pixel-perfect while roundness is smooth + const originalCanvas = document.createElement('canvas'); + originalCanvas.width = originalWidth; + originalCanvas.height = originalHeight; + const originalCtx = originalCanvas.getContext('2d')!; + // originalCtx.fillStyle = "white"; + // originalCtx.fillRect(0, 0, originalWidth, originalHeight); + originalCtx.drawImage(img, 0, 0, originalWidth, originalHeight); + const originalData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); + + // Process each pixel: Red channel = roundness, Alpha channel = original alpha + for (let i = 0; i < outImg.data.length; i += 4) { + const a = originalData.data[i + 3]!; + // Use only alpha to determine background vs shape + const upscaledAlpha = outImg.data[i + 3]!; + if (a === 0) { + // Background pixel + outImg.data[i] = 255; + outImg.data[i + 1] = 0; + } else { + // Red channel carries the roundness + // Check if upscale missed this pixel by looking at alpha channel + // If upscaled alpha is 0, the low-res version thought this was background + outImg.data[i] = upscaledAlpha === 0 ? 0 : outImg.data[i]!; // roundness or 0 + outImg.data[i + 1] = a; // original alpha + } + + // Unused channels fixed + outImg.data[i + 2] = 255; + outImg.data[i + 3] = 255; + } + + ctx.putImageData(outImg, 0, 0); + finalImageData = outImg; + canvas.toBlob((blob) => { + if (!blob) { + reject(new Error('Failed to create PNG blob')); + return; + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + const totalTime = performance.now() - totalStartTime; + console.log(`[Total Processing Time] ${totalTime.toFixed(2)}ms`); + if (scaleFactor < 1) { + const estimatedFullResTime = totalTime * Math.pow((originalWidth * originalHeight) / (width * height), 1.5); + console.log(`[Estimated time at full resolution] ~${estimatedFullResTime.toFixed(0)}ms`); + console.log( + `[Time saved] ~${(estimatedFullResTime - totalTime).toFixed(0)}ms (${Math.round(estimatedFullResTime / totalTime)}× faster)` + ); + } + } + + resolve({ + imageData: finalImageData, + pngBlob: blob, + }); + }, 'image/png'); + }; + + img.onerror = () => reject(new Error('Failed to load image')); + img.src = typeof file === 'string' ? file : URL.createObjectURL(file); + }); +} + +function blurRedChannel( + imageData: ImageData, + width: number, + height: number, + radius = 2 +) { + const src = imageData.data; + const pixelCount = width * height; + + const tmp = new Uint8ClampedArray(pixelCount); + const dst = new Uint8ClampedArray(pixelCount); + + // --- Horizontal blur --- + for (let y = 0; y < height; y++) { + let sum = 0; + let count = 0; + + // initial window centered at x = 0 + for (let dx = -radius; dx <= radius; dx++) { + const xClamped = Math.max(0, Math.min(width - 1, dx)); + const idx = (y * width + xClamped) * 4; + sum += src[idx]!; + count++; + } + + for (let x = 0; x < width; x++) { + const outIdx = y * width + x; + tmp[outIdx] = sum / count; + + const xRemove = x - radius; + if (xRemove >= 0) { + const idxRemove = (y * width + xRemove) * 4; + sum -= src[idxRemove]!; + count--; + } + + const xAdd = x + radius + 1; + if (xAdd < width) { + const idxAdd = (y * width + xAdd) * 4; + sum += src[idxAdd]!; + count++; + } + } + } + + // --- Vertical blur --- + for (let x = 0; x < width; x++) { + let sum = 0; + let count = 0; + + // initial window centered at y = 0 + for (let dy = -radius; dy <= radius; dy++) { + const yClamped = Math.max(0, Math.min(height - 1, dy)); + const idx = yClamped * width + x; + sum += tmp[idx]!; + count++; + } + + for (let y = 0; y < height; y++) { + const outIdx = y * width + x; + dst[outIdx] = sum / count; + + const yRemove = y - radius; + if (yRemove >= 0) { + const idxRemove = yRemove * width + x; + sum -= tmp[idxRemove]!; + count--; + } + + const yAdd = y + radius + 1; + if (yAdd < height) { + const idxAdd = yAdd * width + x; + sum += tmp[idxAdd]!; + count++; + } + } + } + + // --- Write blurred red back into ImageData --- + for (let i = 0; i < pixelCount; i++) { + const px = i * 4; + src[px] = dst[i]!; + } +} + + +function buildSparseData( + shapeMask: Uint8Array, + boundaryMask: Uint8Array, + interiorPixels: Uint32Array, + boundaryPixels: Uint32Array, + width: number, + height: number +): SparsePixelData { + const pixelCount = interiorPixels.length; + + // Build neighbor indices for sparse processing + // For each interior pixel, store indices of its 4 neighbors + // Use -1 for out-of-bounds or non-shape neighbors + const neighborIndices = new Int32Array(pixelCount * 4); + + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + const x = idx % width; + const y = Math.floor(idx / width); + + // East neighbor + neighborIndices[i * 4 + 0] = x < width - 1 && shapeMask[idx + 1] ? idx + 1 : -1; + // West neighbor + neighborIndices[i * 4 + 1] = x > 0 && shapeMask[idx - 1] ? idx - 1 : -1; + // North neighbor + neighborIndices[i * 4 + 2] = y > 0 && shapeMask[idx - width] ? idx - width : -1; + // South neighbor + neighborIndices[i * 4 + 3] = y < height - 1 && shapeMask[idx + width] ? idx + width : -1; + } + + return { + interiorPixels, + boundaryPixels, + pixelCount, + neighborIndices, + }; +} + +function solvePoissonSparse( + sparseData: SparsePixelData, + shapeMask: Uint8Array, + boundaryMask: Uint8Array, + width: number, + height: number +): Float32Array { + // This controls how smooth the falloff roundness will be and extend into the shape + const ITERATIONS = POISSON_CONFIG_OPTIMIZED.iterations; + + // Keep C constant - only iterations control roundness spread + const C = 0.01; + + const u = new Float32Array(width * height); + const { interiorPixels, neighborIndices, pixelCount } = sparseData; + + // Performance tracking + const startTime = performance.now(); + + // Red-Black SOR for better symmetry with fewer iterations + // omega between 1.8-1.95 typically gives best convergence for Poisson + const omega = 1.9; + + // Pre-classify pixels as red or black for efficient processing + const redPixels: number[] = []; + const blackPixels: number[] = []; + + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + const x = idx % width; + const y = Math.floor(idx / width); + + if ((x + y) % 2 === 0) { + redPixels.push(i); + } else { + blackPixels.push(i); + } + } + + for (let iter = 0; iter < ITERATIONS; iter++) { + // Red pass: update red pixels + for (const i of redPixels) { + const idx = interiorPixels[i]!; + + // Get precomputed neighbor indices + const eastIdx = neighborIndices[i * 4 + 0]!; + const westIdx = neighborIndices[i * 4 + 1]!; + const northIdx = neighborIndices[i * 4 + 2]!; + const southIdx = neighborIndices[i * 4 + 3]!; + + // Sum neighbors (use 0 for out-of-bounds) + let sumN = 0; + if (eastIdx >= 0) sumN += u[eastIdx]!; + if (westIdx >= 0) sumN += u[westIdx]!; + if (northIdx >= 0) sumN += u[northIdx]!; + if (southIdx >= 0) sumN += u[southIdx]!; + + // SOR update: blend new value with old value + const newValue = (C + sumN) / 4; + u[idx] = omega * newValue + (1 - omega) * u[idx]!; + } + + // Black pass: update black pixels + for (const i of blackPixels) { + const idx = interiorPixels[i]!; + + // Get precomputed neighbor indices + const eastIdx = neighborIndices[i * 4 + 0]!; + const westIdx = neighborIndices[i * 4 + 1]!; + const northIdx = neighborIndices[i * 4 + 2]!; + const southIdx = neighborIndices[i * 4 + 3]!; + + // Sum neighbors (use 0 for out-of-bounds) + let sumN = 0; + if (eastIdx >= 0) sumN += u[eastIdx]!; + if (westIdx >= 0) sumN += u[westIdx]!; + if (northIdx >= 0) sumN += u[northIdx]!; + if (southIdx >= 0) sumN += u[southIdx]!; + + // SOR update: blend new value with old value + const newValue = (C + sumN) / 4; + u[idx] = omega * newValue + (1 - omega) * u[idx]!; + } + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + const elapsed = performance.now() - startTime; + + console.log(`[Optimized Poisson Solver (SOR ω=${omega})]`); + console.log(` Working size: ${width}×${height}`); + console.log(` Iterations: ${ITERATIONS}`); + console.log(` Time: ${elapsed.toFixed(2)}ms`); + console.log(` Interior pixels processed: ${pixelCount}`); + console.log(` Speed: ${((ITERATIONS * pixelCount) / (elapsed * 1000)).toFixed(2)} Mpixels/sec`); + } + + return u; +} + +export interface MeshGradientLogoUniforms extends ShaderSizingUniforms { + u_colorBack: [number, number, number, number]; + u_colorInner: [number, number, number, number]; + u_colors: vec4[]; + u_colorsCount: number; + u_image: HTMLImageElement | string | undefined; + u_stripeWidth: number; + u_alphaMask: boolean; + u_gap: boolean; + u_size: number; + u_shift: number; + u_noise: number; + u_outerNoise: number; + u_contour: number; + u_roundness: number; + u_angle: number; + u_isImage: boolean; +} + +export interface MeshGradientLogoParams extends ShaderSizingParams, ShaderMotionParams { + colors?: string[]; + colorBack?: string; + colorInner?: string; + image?: HTMLImageElement | string | undefined; + stripeWidth?: number; + alphaMask?: boolean; + gap?: boolean; + size?: number; + contour?: number; + roundness?: number; + shift?: number; + noise?: number; + outerNoise?: number; + angle?: number; +} From 059e3e27562038af4ccd1280401b8c59e30ae15d Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 16 Nov 2025 01:04:19 +0100 Subject: [PATCH 23/84] halftone lines cleanup + origColor support --- .../src/app/(shaders)/halftone-lines/page.tsx | 13 +--- .../src/shaders/halftone-lines.tsx | 21 ++----- packages/shaders/src/index.ts | 2 - .../shaders/src/shaders/halftone-lines.ts | 62 +++++++------------ 4 files changed, 33 insertions(+), 65 deletions(-) diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index ef987969e..864acaec0 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -5,7 +5,7 @@ import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { HalftoneLinesType, HalftoneLinesTypes, halftoneLinesMeta, ShaderFit } from '@paper-design/shaders'; +import { halftoneLinesMeta, ShaderFit } from '@paper-design/shaders'; import { levaImageButton } from '@/helpers/leva-image-button'; import { useState, useEffect, useCallback } from 'react'; import { toHsla } from '@/helpers/color-utils'; @@ -71,25 +71,18 @@ const HalftoneLinesWithControls = () => { ); return { stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, - softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, smoothness: { value: defaults.smoothness, min: 0, max: halftoneLinesMeta.maxBlurRadius, order: 202 }, - wave: { value: defaults.wave, min: 0, max: 1, order: 204 }, - noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, + angleDistortion: { value: defaults.angleDistortion, min: 0, max: 1, order: 204 }, + noiseDistortion: { value: defaults.noiseDistortion, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, originalColors: { value: defaults.originalColors, order: 102 }, - type: { - value: defaults.type, - options: Object.keys(HalftoneLinesTypes) as HalftoneLinesType[], - order: 201, - }, inverted: { value: defaults.inverted, order: 201 }, size: { value: defaults.size, min: 0.01, max: 150, step: 0.1, order: 300 }, contrast: { value: defaults.contrast, min: 0.01, max: 1, order: 302 }, grainMixer: { value: defaults.grainMixer, min: 0, max: 1, order: 350 }, grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 351 }, - speed: {value: defaults.speed, min: 0, max: 4, order: 300}, scale: { value: defaults.scale, min: 0.1, max: 10, order: 400 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 401 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 402 }, diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index c23af9cad..a660d2df0 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -8,7 +8,6 @@ import { type HalftoneLinesParams, defaultObjectSizing, type ImageShaderPreset, - HalftoneLinesTypes, } from '@paper-design/shaders'; export interface HalftoneLinesProps extends ShaderComponentProps, HalftoneLinesParams {} @@ -26,16 +25,14 @@ export const defaultPreset: HalftoneLinesPreset = { stripeWidth: 0, smoothness: 10, size: 40, - wave: 0.4, - noise: 0, - softness: 0, + angleDistortion: 0.4, + noiseDistortion: 0, angle: 0, contrast: 0.7, originalColors: false, inverted: false, grainMixer: 0.2, grainOverlay: 0.2, - type: 'gooey', }, }; export const halftoneLinesPresets: HalftoneLinesPreset[] = [defaultPreset]; @@ -47,15 +44,12 @@ export const HalftoneLines: React.FC = memo(function Halfton speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, image = '', - wave = defaultPreset.params.wave, - noise = defaultPreset.params.noise, - softness = defaultPreset.params.softness, + angleDistortion = defaultPreset.params.angleDistortion, + noiseDistortion = defaultPreset.params.noiseDistortion, stripeWidth = defaultPreset.params.stripeWidth, smoothness = defaultPreset.params.smoothness, size = defaultPreset.params.size, angle = defaultPreset.params.angle, - type = defaultPreset.params.type, - contrast = defaultPreset.params.contrast, originalColors = defaultPreset.params.originalColors, inverted = defaultPreset.params.inverted, @@ -80,15 +74,12 @@ export const HalftoneLines: React.FC = memo(function Halfton u_colorFront: getShaderColorFromString(colorFront), u_image: image, - u_wave: wave, - u_noise: noise, - u_softness: softness, + u_angleDistortion: angleDistortion, + u_noiseDistortion: noiseDistortion, u_stripeWidth: stripeWidth, u_smoothness: smoothness, u_size: size, u_angle: angle, - u_type: HalftoneLinesTypes[type], - u_contrast: contrast, u_originalColors: originalColors, u_inverted: inverted, diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index facf32b65..ee3a64aac 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -223,8 +223,6 @@ export { export { halftoneLinesMeta, halftoneLinesFragmentShader, - HalftoneLinesTypes, - type HalftoneLinesType, type HalftoneLinesParams, type HalftoneLinesUniforms, } from './shaders/halftone-lines.js'; diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index 3cb118fd8..89af82c4b 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -47,13 +47,10 @@ uniform float u_grainOverlay; uniform bool u_straight; uniform bool u_originalColors; uniform bool u_inverted; -uniform float u_type; - -uniform float u_softness; uniform float u_stripeWidth; uniform float u_smoothness; -uniform float u_wave; -uniform float u_noise; +uniform float u_angleDistortion; +uniform float u_noiseDistortion; uniform float u_angle; @@ -167,14 +164,14 @@ vec4 blurTexture(sampler2D tex, vec2 uv, vec2 texelSize, float radius) { } -float getLumAtPx(vec2 uv, float contrast) { - // vec4 tex = texture(u_image, uv); +float getLumAtPx(vec2 uv, float contrast, out vec3 origColor) { vec4 tex = blurTexture(u_image, uv, vec2(1. / u_resolution), u_smoothness); + origColor = tex.rgb; vec3 color = vec3( - sigmoid(tex.r, contrast), - sigmoid(tex.g, contrast), - sigmoid(tex.b, contrast) + sigmoid(tex.r, contrast), + sigmoid(tex.g, contrast), + sigmoid(tex.b, contrast) ); float lum = dot(vec3(0.2126, 0.7152, 0.0722), color); lum = mix(1., lum, tex.a); @@ -206,11 +203,9 @@ void main() { vec2 uvOriginal = getImageUV(uvNormalised, vec2(1.)); float contrast = mix(0., 15., u_contrast); -// if (u_originalColors == true) { -// contrast = mix(.1, 4., pow(u_contrast, 2.)); -// } - float lum = getLumAtPx(uvOriginal, contrast); + vec3 origColor = vec3(0.); + float lum = getLumAtPx(uvOriginal, contrast, origColor); float frame = getImgFrame(v_imageUV, 0.); lum = mix(1., lum, frame); @@ -218,14 +213,14 @@ void main() { uv = v_objectUV; vec2 p = uv; float angle = -u_angle * PI / 180.; - p = rotate(p, angle + u_wave * lum); + p = rotate(p, angle + u_angleDistortion * lum); p *= u_size; vec2 pBase = v_objectUV * u_size; float aaBase = fwidth(pBase.y); float n = doubleSNoise(uv + 100., u_time); - p.y += .4 * n * lum * u_noise * u_size; + p.y += .4 * n * lum * u_noiseDistortion * u_size; vec2 stripeMap = abs(fract(p) - .5); vec2 stripeDist = abs(stripeMap); @@ -238,7 +233,7 @@ void main() { float w = mix(.5 * u_stripeWidth, 0., lum); w = clamp(w, aa, .5 - aa); - float loSharp = mix(w, aa, u_softness); + float loSharp = w; float loBlurry = 0.; float hiSharp = w + aa; float hiBlurry = w + aa; @@ -256,14 +251,19 @@ void main() { float grain = valueNoise(grainUV); grain = smoothstep(.55, .7 + .2 * u_grainMixer, grain); grain *= u_grainMixer; - // line += grain; + line += grain; vec3 color = vec3(0.); float opacity = 1.; float stripeId = floor(p.y); - color = mix(u_colorBack.rgb, u_colorFront.rgb, line); + if (u_originalColors == true) { + color = mix(origColor, u_colorBack.rgb, line); + } else { + color = mix(u_colorFront.rgb, u_colorBack.rgb, line); + } + fragColor = vec4(color, 1.); } `; @@ -275,11 +275,9 @@ export interface HalftoneLinesUniforms extends ShaderSizingUniforms { u_stripeWidth: number; u_smoothness: number; u_size: number; - u_wave: number; - u_noise: number; - u_softness: number; + u_angleDistortion: number; + u_noiseDistortion: number; u_angle: number; - u_type: (typeof HalftoneLinesTypes)[HalftoneLinesType]; u_contrast: number; u_originalColors: boolean; u_inverted: boolean; @@ -294,24 +292,12 @@ export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionPar stripeWidth?: number; smoothness?: number; size?: number; - softness?: number; - wave?: number; - noise?: number; + angleDistortion?: number; + noiseDistortion?: number; angle?: number; - type?: HalftoneLinesType; - contrast?: number; originalColors?: boolean; inverted?: boolean; grainMixer?: number; grainOverlay?: number; -} - -export const HalftoneLinesTypes = { - classic: 0, - gooey: 1, - holes: 2, - soft: 3, -} as const; - -export type HalftoneLinesType = keyof typeof HalftoneLinesTypes; +} \ No newline at end of file From 6081d42691126794a61544714c025fd92b76f2fa Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 16 Nov 2025 17:44:57 +0100 Subject: [PATCH 24/84] folds fix and clean up --- docs/src/app/(shaders)/folds/page.tsx | 3 +- packages/shaders-react/src/shaders/folds.tsx | 11 +- packages/shaders/src/index.ts | 2 - packages/shaders/src/shaders/folds.ts | 188 ++++++------------- 4 files changed, 66 insertions(+), 138 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index 915dcbc07..cd7a4983d 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -71,11 +71,12 @@ const FoldsWithControls = () => { const [params, setParams] = useControls(() => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorInner: { value: toHsla(defaults.colorInner), order: 101 }, stripeWidth: { value: defaults.stripeWidth, min: 0, max: 0.5, order: 200 }, softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, gradient: { value: defaults.gradient, min: 0, max: 1, order: 201 }, alphaMask: { value: defaults.alphaMask, order: 202 }, - gap: { value: defaults.gap, order: 202 }, + // gap: { value: defaults.gap, order: 202 }, size: { value: defaults.size, min: 6, max: 50, order: 203 }, shift: { value: defaults.shift, min: -1, max: 1, order: 204 }, noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index 0e69e48a2..d01d84f6d 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -10,7 +10,6 @@ import { toProcessedFolds, type ImageShaderPreset, getShaderColorFromString, - FoldsShapes, } from '@paper-design/shaders'; import { transparentPixel } from '../transparent-pixel.js'; import { suspend } from '../suspend.js'; @@ -32,6 +31,7 @@ export const defaultPreset: FoldsPreset = { speed: 0.2, frame: 0, colorBack: '#000000', + colorInner: '#000000', colors: ['#ff9d00', '#fd4f30', '#809bff', '#ffffff'], stripeWidth: 1, alphaMask: false, @@ -40,10 +40,9 @@ export const defaultPreset: FoldsPreset = { shift: 1, noise: 0.5, outerNoise: 0, - softness: 0.2, - gradient: 0.8, + softness: 0, + gradient: 0, angle: 0, - shape: 'diamond', }, }; export const foldsPresets: FoldsPreset[] = [defaultPreset]; @@ -51,6 +50,7 @@ export const foldsPresets: FoldsPreset[] = [defaultPreset]; export const Folds: React.FC = memo(function FoldsImpl({ // Own props colorBack = defaultPreset.params.colorBack, + colorInner = defaultPreset.params.colorInner, colors = defaultPreset.params.colors, speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, @@ -65,7 +65,6 @@ export const Folds: React.FC = memo(function FoldsImpl({ gap = defaultPreset.params.gap, size = defaultPreset.params.size, angle = defaultPreset.params.angle, - shape = defaultPreset.params.shape, suspendWhenProcessingImage = false, // Sizing props @@ -125,6 +124,7 @@ export const Folds: React.FC = memo(function FoldsImpl({ u_colors: colors.map(getShaderColorFromString), u_colorsCount: colors.length, u_colorBack: getShaderColorFromString(colorBack), + u_colorInner: getShaderColorFromString(colorInner), u_image: processedImage, u_shift: shift, u_noise: noise, @@ -137,7 +137,6 @@ export const Folds: React.FC = memo(function FoldsImpl({ u_size: size, u_angle: angle, u_isImage: Boolean(image), - u_shape: FoldsShapes[shape], // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index ee3a64aac..109656378 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -213,9 +213,7 @@ export { export { foldsMeta, foldsFragmentShader, - FoldsShapes, toProcessedFolds, - type FoldsShape, type FoldsParams, type FoldsUniforms, } from './shaders/folds.js'; diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 95ff6c07c..13e8fe209 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -30,6 +30,7 @@ uniform float u_time; uniform vec4 u_colors[${foldsMeta.maxColorCount}]; uniform float u_colorsCount; uniform vec4 u_colorBack; +uniform vec4 u_colorInner; uniform float u_softness; uniform float u_stripeWidth; uniform bool u_alphaMask; @@ -41,7 +42,6 @@ uniform float u_noise; uniform float u_outerNoise; uniform float u_angle; -uniform float u_shape; uniform bool u_isImage; ${sizingVariablesDeclaration} @@ -142,48 +142,6 @@ float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, fl return sum / norm; } -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).r; - sum += w2 * textureGrad(tex, uv + vec2(0.0, r.y), dudx, dudy).r; - sum += w2 * textureGrad(tex, uv + vec2(-r.x, 0.0), dudx, dudy).r; - sum += w2 * textureGrad(tex, uv + vec2(r.x, 0.0), dudx, dudy).r; - - sum += w1 * textureGrad(tex, uv + vec2(-r.x, -r.y), dudx, dudy).r; - sum += w1 * textureGrad(tex, uv + vec2(r.x, -r.y), dudx, dudy).r; - sum += w1 * textureGrad(tex, uv + vec2(-r.x, r.y), dudx, dudy).r; - sum += w1 * textureGrad(tex, uv + vec2(r.x, r.y), dudx, dudy).r; - - return sum / norm; -} - -float blurEdge3x3_G(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; -} - float sst(float edge0, float edge1, float x) { return smoothstep(edge0, edge1, x); } @@ -213,108 +171,78 @@ void main() { vec2 dudy = dFdy(v_imageUV); vec4 img = textureGrad(u_image, uv, dudx, dudy); - if (u_isImage == false) { - uv = v_objectUV + .5; - uv.y = 1. - uv.y; - } - float edge = img.r; edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); -// edge = 1. - edge; float imgAlpha = img.g; -// imgAlpha = blurEdge3x3_G(u_image, uv, dudx, dudy, 2., img.g); float frame = getImgFrame(v_imageUV, 0.); imgAlpha *= frame; edge *= frame; - uv = v_objectUV; - vec2 p = uv; + vec2 patternsUV = v_objectUV; + vec2 stripesUV = v_objectUV; float angle = -u_angle * PI / 180.; - p = rotate(p, angle); - p *= u_size; + stripesUV = rotate(stripesUV, angle); + stripesUV *= u_size; - float n = doubleSNoise(uv + 100., u_time); + float n = doubleSNoise(patternsUV + 100., u_time); float edgeAtten = edge + u_outerNoise * (1. - edge); - float y = p.y + edgeAtten * .5 * n * u_size * u_noise; + float y = stripesUV.y + edgeAtten * .5 * n * u_size * u_noise; float w = u_stripeWidth * edge; - y += 2. * sign(u_shift) * mix(0., w, abs(u_shift)); - float stripeId = floor(y); - float fy = fract(y); - float m = .5; - float left = fy / m; - float right = (1. - fy) / (1. - m); - float stripeMap = 1. - min(left, right); - stripeMap *= .5; - - float aa = fwidth(fy); + float stripeMap = abs(fy - .5); + float aa = fwidth(y); w = clamp(w, aa, .5 - aa); float lMin = w - aa; float lMax = w + aa; - - float line = 1.0 - sst(lMin, lMax, stripeMap); + float line = 1. - sst(lMin, lMax, stripeMap); if (u_alphaMask == true) { line *= imgAlpha; } - float tst = 0.; - { -// float m = clamp(u_wave, .02, .98); - float m = 0.; - float left = fy / m; - float right = (1. - fy) / (1. - m); - tst = 1. - min(left, right); - } - - line -= sst(u_softness, 0., 1. - fy); + float softness = mix(0., u_softness, sst(aa, .5, w)); + line -= u_softness * sst(softness, 0., 1. - fy); line = clamp(line, 0., 1.); int colorIdx = int(posMod(stripeId, u_colorsCount)); - - vec4 orderedStripeColor = u_colors[0]; - for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { - if (i >= int(u_colorsCount)) break; - float isHit = 1.0 - step(.5, abs(float(i - colorIdx))); - orderedStripeColor = mix(orderedStripeColor, u_colors[i], isHit); - } - - - vec3 ccc = vec3(0.); - float ooo = 0.; - float totalWeight = 0.; - - for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { + vec4 orderedStripeColor = u_colors[0]; + for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { if (i >= int(u_colorsCount)) break; - - vec2 pos = getPosition(i, 1.5 * t); - vec3 colorFraction = u_colors[i].rgb * u_colors[i].a; - float oooFraction = u_colors[i].a; - - float dist = .5 * length(uv + .5 - pos); - - dist = pow(dist, 3.5); - float weight = 1. / (dist + 1e-3); - ccc += colorFraction * weight; - ooo += oooFraction * weight; - totalWeight += weight; + float isHit = 1.0 - step(.5, abs(float(i - colorIdx))); + orderedStripeColor = mix(orderedStripeColor, u_colors[i], isHit); } - - ccc /= max(1e-4, totalWeight); - ooo /= max(1e-4, totalWeight); - vec4 stripeColor = vec4(ccc, ooo); + vec3 gradientStripeColor = vec3(0.); + float gradientOpacity = 0.; + { + float totalWeight = 0.; + for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { + if (i >= int(u_colorsCount)) break; + vec2 pos = getPosition(i, 1.5 * t); + vec3 colorFraction = u_colors[i].rgb * u_colors[i].a; + float opacityFraction = u_colors[i].a; + float dist = .5 * length(patternsUV + .5 - pos); + dist = pow(dist, 3.5); + float weight = 1. / (dist + 1e-3); + gradientStripeColor += colorFraction * weight; + gradientOpacity += opacityFraction * weight; + totalWeight += weight; + } + gradientStripeColor /= max(1e-4, totalWeight); + gradientOpacity /= max(1e-4, totalWeight); + } - stripeColor = mix(orderedStripeColor, mix(stripeColor, orderedStripeColor, tst), u_gradient); + vec4 stripeColor = vec4(gradientStripeColor, gradientOpacity); + stripeColor = mix(orderedStripeColor, mix(stripeColor, orderedStripeColor, fy), u_gradient); vec3 stripePremulRGB = stripeColor.rgb * stripeColor.a; stripePremulRGB *= line; @@ -323,16 +251,28 @@ void main() { vec3 color = stripePremulRGB; float opacity = stripeA; - if (u_gap == true) { - color += .6 * (1. - edge) * imgAlpha; - } +// if (u_gap == true) { +// color += .6 * (1. - edge) * imgAlpha; +// } + + vec3 backRgb = u_colorBack.rgb * u_colorBack.a; + float backA = u_colorBack.a; + vec3 innerRgb = u_colorInner.rgb * u_colorInner.a; + float innerA = u_colorInner.a; + + innerRgb *= imgAlpha; + innerA *= imgAlpha; - vec3 bgColor = u_colorBack.rgb * u_colorBack.a; - color = color + bgColor * (1. - opacity); - opacity = opacity + u_colorBack.a * (1. - opacity); + vec3 underlayerRgb = innerRgb + backRgb * (1. - innerA); + float underlayerA = innerA + backA * (1. - innerA); + + color *= line; + opacity *= line; + + color = color + underlayerRgb * (1. - opacity); + opacity = opacity + underlayerA * (1. - opacity); fragColor = vec4(color, opacity); -// fragColor = vec4(ccc, 1.); } `; @@ -853,6 +793,7 @@ function solvePoissonSparse( export interface FoldsUniforms extends ShaderSizingUniforms { u_colorBack: [number, number, number, number]; + u_colorInner: [number, number, number, number]; u_colors: vec4[]; u_colorsCount: number; u_image: HTMLImageElement | string | undefined; @@ -866,13 +807,13 @@ export interface FoldsUniforms extends ShaderSizingUniforms { u_softness: number; u_gradient: number; u_angle: number; - u_shape: (typeof FoldsShapes)[FoldsShape]; u_isImage: boolean; } export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { colors?: string[]; colorBack?: string; + colorInner?: string; image?: HTMLImageElement | string | undefined; stripeWidth?: number; alphaMask?: boolean; @@ -884,15 +825,4 @@ export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { noise?: number; outerNoise?: number; angle?: number; - shape?: FoldsShape; -} - -export const FoldsShapes = { - none: 0, - circle: 1, - daisy: 2, - diamond: 3, - metaballs: 4, -} as const; - -export type FoldsShape = keyof typeof FoldsShapes; +} \ No newline at end of file From 129979615777cce6de6c0ebd775c823c4c59946e Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 17 Nov 2025 15:54:47 +0100 Subject: [PATCH 25/84] logo list update --- docs/public/images/logos/brave.svg | 24 +++++++++ docs/public/images/logos/brave2.png | Bin 0 -> 120364 bytes docs/public/images/logos/capy.svg | 7 +++ docs/public/images/logos/contra.svg | 14 ++++++ docs/public/images/logos/inbound.svg | 6 +++ docs/public/images/logos/infinite.svg | 1 + docs/public/images/logos/linear.svg | 3 ++ docs/public/images/logos/mercury.svg | 10 ++++ docs/public/images/logos/mymind.svg | 19 +++++++ docs/public/images/logos/paradigm.svg | 4 ++ docs/public/images/logos/resend.svg | 3 ++ docs/public/images/logos/shopify.svg | 17 +++++++ docs/public/images/logos/wealth-simple.svg | 3 ++ docs/src/app/(shaders)/folds/page.tsx | 47 +++++++++++------- .../app/(shaders)/mesh-gradient-logo/page.tsx | 45 +++++++++++------ 15 files changed, 170 insertions(+), 33 deletions(-) create mode 100644 docs/public/images/logos/brave.svg create mode 100644 docs/public/images/logos/brave2.png create mode 100644 docs/public/images/logos/capy.svg create mode 100644 docs/public/images/logos/contra.svg create mode 100644 docs/public/images/logos/inbound.svg create mode 100644 docs/public/images/logos/infinite.svg create mode 100644 docs/public/images/logos/linear.svg create mode 100644 docs/public/images/logos/mercury.svg create mode 100644 docs/public/images/logos/mymind.svg create mode 100644 docs/public/images/logos/paradigm.svg create mode 100644 docs/public/images/logos/resend.svg create mode 100644 docs/public/images/logos/shopify.svg create mode 100644 docs/public/images/logos/wealth-simple.svg diff --git a/docs/public/images/logos/brave.svg b/docs/public/images/logos/brave.svg new file mode 100644 index 000000000..32e1dc865 --- /dev/null +++ b/docs/public/images/logos/brave.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/public/images/logos/brave2.png b/docs/public/images/logos/brave2.png new file mode 100644 index 0000000000000000000000000000000000000000..76b59baf8c1c6dee5623ffe6bd01b1063ff5c788 GIT binary patch literal 120364 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 + + + + + + diff --git a/docs/public/images/logos/contra.svg b/docs/public/images/logos/contra.svg new file mode 100644 index 000000000..2d0c375db --- /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/inbound.svg b/docs/public/images/logos/inbound.svg new file mode 100644 index 000000000..b0209ed54 --- /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 000000000..465e8f0f8 --- /dev/null +++ b/docs/public/images/logos/infinite.svg @@ -0,0 +1 @@ + \ 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 000000000..f828c08d4 --- /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 000000000..5639357ce --- /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/mymind.svg b/docs/public/images/logos/mymind.svg new file mode 100644 index 000000000..0b965b079 --- /dev/null +++ b/docs/public/images/logos/mymind.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ 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 000000000..9191b0bb6 --- /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 000000000..67af6bf6d --- /dev/null +++ b/docs/public/images/logos/resend.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/public/images/logos/shopify.svg b/docs/public/images/logos/shopify.svg new file mode 100644 index 000000000..443af96d1 --- /dev/null +++ b/docs/public/images/logos/shopify.svg @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/docs/public/images/logos/wealth-simple.svg b/docs/public/images/logos/wealth-simple.svg new file mode 100644 index 000000000..e9b99985a --- /dev/null +++ b/docs/public/images/logos/wealth-simple.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index cd7a4983d..8443504db 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -22,32 +22,45 @@ import { useColors } from "@/helpers/use-colors"; const { worldWidth, worldHeight, ...defaults } = foldsPresets[0].params; const imageFiles = [ - 'chanel.svg', - 'cibc.svg', - 'cloudflare.svg', - 'apple.svg', + 'paradigm.svg', 'paper-logo-only.svg', + 'brave2.png', + 'capy.svg', + 'contra.svg', + 'infinite.svg', + 'linear.svg', + 'mercury.svg', + 'mymind.svg', + 'inbound.svg', + 'resend.svg', + 'shopify.svg', + 'wealth-simple.svg', 'diamond.svg', + + // 'chanel.svg', + // 'cibc.svg', + // 'cloudflare.svg', + // 'apple.svg', + // 'paper-logo-only.svg', 'discord.svg', - 'paper-logo-only', - 'enterprise-rent.svg', - 'kfc.svg', - 'microsoft.svg', - 'nasa.svg', - 'netflix.svg', + // 'paper-logo-only', + // 'enterprise-rent.svg', + // 'kfc.svg', + // 'microsoft.svg', + // 'nasa.svg', + // 'netflix.svg', 'nike.svg', - 'paper.svg', - 'perkins.svg', - 'pizza-hut.svg', - 'remix.svg', - 'rogers.svg', + // 'perkins.svg', + // 'pizza-hut.svg', + // 'remix.svg', + // 'rogers.svg', 'vercel.svg', 'volkswagen.svg', ] as const; const FoldsWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); - const [image, setImage] = useState('/images/logos/apple.svg'); + const [image, setImage] = useState('/images/logos/diamond.svg'); useEffect(() => { if (imageIdx >= 0) { @@ -72,7 +85,7 @@ const FoldsWithControls = () => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorInner: { value: toHsla(defaults.colorInner), order: 101 }, - stripeWidth: { value: defaults.stripeWidth, min: 0, max: 0.5, order: 200 }, + stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, gradient: { value: defaults.gradient, min: 0, max: 1, order: 201 }, alphaMask: { value: defaults.alphaMask, order: 202 }, diff --git a/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx b/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx index 4772e4449..06b2bb4ea 100644 --- a/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx +++ b/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx @@ -22,32 +22,45 @@ import { useColors } from "@/helpers/use-colors"; const { worldWidth, worldHeight, ...defaults } = meshGradientLogoPresets[0].params; const imageFiles = [ - 'chanel.svg', - 'cibc.svg', - 'cloudflare.svg', - 'apple.svg', + 'paradigm.svg', 'paper-logo-only.svg', + 'brave2.png', + 'capy.svg', + 'contra.svg', + 'infinite.svg', + 'linear.svg', + 'mercury.svg', + 'mymind.svg', + 'inbound.svg', + 'resend.svg', + 'shopify.svg', + 'wealth-simple.svg', 'diamond.svg', + + // 'chanel.svg', + // 'cibc.svg', + // 'cloudflare.svg', + // 'apple.svg', + // 'paper-logo-only.svg', 'discord.svg', - 'paper-logo-only', - 'enterprise-rent.svg', - 'kfc.svg', - 'microsoft.svg', - 'nasa.svg', - 'netflix.svg', + // 'paper-logo-only', + // 'enterprise-rent.svg', + // 'kfc.svg', + // 'microsoft.svg', + // 'nasa.svg', + // 'netflix.svg', 'nike.svg', - 'paper.svg', - 'perkins.svg', - 'pizza-hut.svg', - 'remix.svg', - 'rogers.svg', + // 'perkins.svg', + // 'pizza-hut.svg', + // 'remix.svg', + // 'rogers.svg', 'vercel.svg', 'volkswagen.svg', ] as const; const MeshGradientLogoWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); - const [image, setImage] = useState('/images/logos/apple.svg'); + const [image, setImage] = useState('/images/logos/diamond.svg'); useEffect(() => { if (imageIdx >= 0) { From 6faf74a4cb3f0a273cb0f7a6d663230f1d084d76 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 17 Nov 2025 16:33:15 +0100 Subject: [PATCH 26/84] folds adjusted --- docs/src/app/(shaders)/folds/page.tsx | 4 +- packages/shaders-react/src/shaders/folds.tsx | 82 ++++++++++++++++++-- packages/shaders/src/shaders/folds.ts | 5 +- 3 files changed, 81 insertions(+), 10 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index 8443504db..47249c446 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -90,8 +90,8 @@ const FoldsWithControls = () => { gradient: { value: defaults.gradient, min: 0, max: 1, order: 201 }, alphaMask: { value: defaults.alphaMask, order: 202 }, // gap: { value: defaults.gap, order: 202 }, - size: { value: defaults.size, min: 6, max: 50, order: 203 }, - shift: { value: defaults.shift, min: -1, max: 1, order: 204 }, + size: { value: defaults.size, min: 3, max: 50, order: 203 }, + shift: { value: defaults.shift, min: -0.5, max: 0.5, order: 204 }, noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index d01d84f6d..abd0f011f 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -30,22 +30,92 @@ export const defaultPreset: FoldsPreset = { scale: 0.8, speed: 0.2, frame: 0, - colorBack: '#000000', + colorBack: '#ffffff00', colorInner: '#000000', - colors: ['#ff9d00', '#fd4f30', '#809bff', '#ffffff'], + colors: ['#809399', '#9d31fc', '#ffffff', '#b3ff66'], + stripeWidth: 0.65, + alphaMask: true, + gap: false, + size: 13, + shift: 0.5, + noise: 0.5, + outerNoise: 0, + softness: 0, + gradient: 1, + angle: 0, + }, +}; + +export const fstPreset: FoldsPreset = { + name: '1', + params: { + ...defaultObjectSizing, + scale: 0.8, + speed: 0.2, + frame: 0, + colorBack: '#000000', + colorInner: '#00000000', + colors: ['#ffffff'], stripeWidth: 1, alphaMask: false, gap: false, - size: 12, - shift: 1, - noise: 0.5, + size: 16, + shift: 0.5, + noise: 0.32, outerNoise: 0, softness: 0, gradient: 0, angle: 0, }, }; -export const foldsPresets: FoldsPreset[] = [defaultPreset]; + +export const scdPreset: FoldsPreset = { + name: '2', + params: { + ...defaultObjectSizing, + scale: 0.8, + speed: 0.2, + frame: 0, + colorBack: '#000000', + colorInner: '#00000000', + colors: ['#ffffff', '#000000', '#0022ff', '#ffe500'], + stripeWidth: 0.6, + alphaMask: false, + gap: false, + size: 24, + shift: 0, + noise: 0.65, + outerNoise: 1, + softness: 0, + gradient: 1, + angle: 90, + }, +}; + +export const trdPreset: FoldsPreset = { + name: '3', + params: { + ...defaultObjectSizing, + scale: 0.8, + speed: 0.2, + frame: 0, + colorBack: '#000000', + colorInner: '#6e535354', + colors: ['#ff5ec4', '#5effc8', '#ffe45e', '#ff6b5e'], + stripeWidth: 0.3, + alphaMask: true, + gap: false, + size: 45, + shift: 0, + noise: 0.3, + outerNoise: 0, + softness: 0, + gradient: 0, + angle: 0, + }, +}; + +export const foldsPresets: FoldsPreset[] = [defaultPreset, fstPreset, scdPreset, trdPreset]; export const Folds: React.FC = memo(function FoldsImpl({ // Own props diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 13e8fe209..1b28dbf97 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -209,8 +209,8 @@ void main() { line *= imgAlpha; } - float softness = mix(0., u_softness, sst(aa, .5, w)); - line -= u_softness * sst(softness, 0., 1. - fy); + float softness = mix(0., u_softness, sst(aa, 2. * aa, w)); + line -= sst(softness, 0., 1. - fy); line = clamp(line, 0., 1.); int colorIdx = int(posMod(stripeId, u_colorsCount)); @@ -273,6 +273,7 @@ void main() { opacity = opacity + underlayerA * (1. - opacity); fragColor = vec4(color, opacity); +// fragColor = vec4(img.rgb, 1.); } `; From e7b4fc3325217baf9ccd03f42f3dc14a7c5dc565 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 17 Nov 2025 17:45:34 +0100 Subject: [PATCH 27/84] blobs dynamic colors + cleanup --- .../app/(shaders)/mesh-gradient-logo/page.tsx | 10 +- .../src/shaders/mesh-gradient-logo.tsx | 31 +---- .../shaders/src/shaders/mesh-gradient-logo.ts | 118 ++++++------------ 3 files changed, 40 insertions(+), 119 deletions(-) diff --git a/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx b/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx index 06b2bb4ea..909a221cf 100644 --- a/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx +++ b/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx @@ -85,16 +85,8 @@ const MeshGradientLogoWithControls = () => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorInner: { value: toHsla(defaults.colorInner), order: 101 }, - // stripeWidth: { value: defaults.stripeWidth, min: 0, max: 0.5, order: 200 }, contour: { value: defaults.contour, min: 0, max: 1, order: 201 }, - roundness: { value: defaults.roundness, min: 0, max: 1, order: 201 }, - // alphaMask: { value: defaults.alphaMask, order: 202 }, - // gap: { value: defaults.gap, order: 202 }, - // size: { value: defaults.size, min: 6, max: 50, order: 203 }, - // shift: { value: defaults.shift, min: -1, max: 1, order: 204 }, - // noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, - // outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 205 }, - // angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, + size: { value: defaults.size, min: 0, max: 1, order: 203 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/mesh-gradient-logo.tsx b/packages/shaders-react/src/shaders/mesh-gradient-logo.tsx index 5941523eb..67bcff505 100644 --- a/packages/shaders-react/src/shaders/mesh-gradient-logo.tsx +++ b/packages/shaders-react/src/shaders/mesh-gradient-logo.tsx @@ -28,21 +28,13 @@ export const defaultPreset: MeshGradientLogoPreset = { params: { ...defaultObjectSizing, scale: 0.8, - speed: 0.5, + speed: 1, frame: 0, colorBack: '#c8dfd900', colorInner: '#c8dfd9', colors: ['#ff9d00', '#fd4f30', '#809bff', '#c5bac5'], - stripeWidth: 1, - alphaMask: false, - gap: true, - size: 12, - shift: 1, - noise: 0.5, - outerNoise: 0, - contour: 1, - roundness: 0.8, - angle: 0, + size: 0.5, + contour: 0.7, }, }; export const meshGradientLogoPresets: MeshGradientLogoPreset[] = [defaultPreset]; @@ -55,16 +47,8 @@ export const MeshGradientLogo: React.FC = memo(function M speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, image = '', - shift = defaultPreset.params.shift, - noise = defaultPreset.params.noise, - outerNoise = defaultPreset.params.outerNoise, contour = defaultPreset.params.contour, - roundness = defaultPreset.params.roundness, - stripeWidth = defaultPreset.params.stripeWidth, - alphaMask = defaultPreset.params.alphaMask, - gap = defaultPreset.params.gap, size = defaultPreset.params.size, - angle = defaultPreset.params.angle, suspendWhenProcessingImage = false, // Sizing props @@ -126,17 +110,8 @@ export const MeshGradientLogo: React.FC = memo(function M u_colorBack: getShaderColorFromString(colorBack), u_colorInner: getShaderColorFromString(colorInner), u_image: processedImage, - u_shift: shift, - u_noise: noise, - u_outerNoise: outerNoise, u_contour: contour, - u_roundness: roundness, - u_stripeWidth: stripeWidth, - u_alphaMask: alphaMask, - u_gap: gap, u_size: size, - u_angle: angle, - u_isImage: Boolean(image), // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/mesh-gradient-logo.ts b/packages/shaders/src/shaders/mesh-gradient-logo.ts index 11fba8ba0..5a3a16bc7 100644 --- a/packages/shaders/src/shaders/mesh-gradient-logo.ts +++ b/packages/shaders/src/shaders/mesh-gradient-logo.ts @@ -4,7 +4,7 @@ import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingU import { declarePI, rotation2, proceduralHash21, colorBandingFix } from '../shader-utils.js'; export const meshGradientLogoMeta = { - maxColorCount: 10, + maxColorCount: 6, } as const; /** @@ -32,18 +32,7 @@ uniform float u_colorsCount; uniform vec4 u_colorBack; uniform vec4 u_colorInner; uniform float u_contour; -uniform float u_stripeWidth; -uniform bool u_alphaMask; -uniform bool u_gap; uniform float u_size; -uniform float u_roundness; -uniform float u_shift; -uniform float u_noise; -uniform float u_outerNoise; -uniform float u_angle; - -uniform float u_shape; -uniform bool u_isImage; ${sizingVariablesDeclaration} @@ -175,65 +164,47 @@ void main() { float shapeOffset = 2.4 * u_contour * offset; float shaping = 2.5 - shapeOffset; - float f1, f2, f3, f4; t = 2. * u_time; - - { - vec2 traj = .5 * vec2(.8 * sin(-.5 * t), .2 + 2.5 * cos(.3 * t)); - float f = getPoint(uv + traj, shaping); - f1 = f; - } - - { - vec2 traj = .5 * vec2(1.7 * sin(-.5 * t), sin(.8 * t)); - float f = getPoint(uv + traj, shaping); - f2 = f; + float f[${ meshGradientLogoMeta.maxColorCount }]; + + vec2 trajs[${ meshGradientLogoMeta.maxColorCount }]; + trajs[0] = vec2(0.8 * sin(-0.5 * t), 0.2 + 2.5 * cos(0.3 * t)); + trajs[1] = vec2(1.7 * cos(-0.5 * t + 1.), sin(0.8 * t)); + trajs[2] = vec2(0.5 * cos(0.3 * t), cos(-0.8 * t)); + trajs[3] = vec2(0.5 * cos(-0.9 * t), 0.7 * sin(-0.2 * t)); + trajs[4] = vec2(0.5 * sin(-0.34 * t), -.2 + 1.3 * sin(-0.8 * t)); + trajs[5] = vec2(0.9 * sin(0.85 * t + 1.), 0.7 * cos(0.6 * t)); + + float dist = 0.3; + for (int i = 0; i < ${ meshGradientLogoMeta.maxColorCount }; i++) { + dist += .03 * float(i); + f[i] = getPoint(uv + dist * trajs[i], shaping); } - { - vec2 traj = .5 * vec2(.5 * cos(-.3 * t), cos(-.8 * t)); - float f = getPoint(uv + traj, shaping); - f3 = f; - } + f[0] -= f[1]; + f[2] -= 1.2 * f[1]; + f[2] -= 1.6 * f[0]; + f[4] -= f[1]; + f[5] -= .4 * f[3]; + f[1] -= .5 * f[2]; + f[5] -= f[4]; + f[3] -= f[1]; + f[3] -= f[2]; + f[5] *= .3; + + float opacity = 0.; + vec3 color = vec3(0.); - { - vec2 traj = .5 * vec2(.5 * cos(-.9 * t), .7 * sin(-.2 * t)); - float f = getPoint(uv + traj, shaping); - f4 = f; + float size = .95 - .9 * u_size; + for (int i = 0; i < ${ meshGradientLogoMeta.maxColorCount }; i++) { + if (i >= int(u_colorsCount)) break; + f[i] = sst(size, size + 2. * fwidth(f[i]), f[i]); + opacity += f[i]; + color = mix(color, u_colors[i].rgb, f[i]); } - f1 -= .5 * f2; - f3 -= f2; - f2 -= f1; - f2 -= f3; - f1 -= f3; - f2 -= f4; - f3 -= f1; - f3 -= f1; - f4 -= f2; - f4 -= f1; - f4 -= f3; - f3 -= f4; - - f1 = sst(u_roundness, u_roundness + 2. * fwidth(f1), f1); - f2 = sst(u_roundness, u_roundness + 2. * fwidth(f2), f2); - f3 = sst(u_roundness, u_roundness + 2. * fwidth(f3), f3); - f4 = sst(u_roundness, u_roundness + 2. * fwidth(f4), f4); - - vec3 color1 = u_colors[0].rgb; - vec3 color2 = u_colors[1].rgb; - vec3 color3 = u_colors[2].rgb; - vec3 color4 = u_colors[3].rgb; - - float opacity = f1 + f2 + f3 + f4; opacity = clamp(opacity, 0., 1.); - vec3 color = vec3(0.); - color = mix(color, color4, f4); - color = mix(color, color2, f2); - color = mix(color, color3, f3); - color = mix(color, color1, f1); - color *= imgAlpha; opacity *= imgAlpha; @@ -778,17 +749,8 @@ export interface MeshGradientLogoUniforms extends ShaderSizingUniforms { u_colors: vec4[]; u_colorsCount: number; u_image: HTMLImageElement | string | undefined; - u_stripeWidth: number; - u_alphaMask: boolean; - u_gap: boolean; - u_size: number; - u_shift: number; - u_noise: number; - u_outerNoise: number; u_contour: number; - u_roundness: number; - u_angle: number; - u_isImage: boolean; + u_size: number; } export interface MeshGradientLogoParams extends ShaderSizingParams, ShaderMotionParams { @@ -796,14 +758,6 @@ export interface MeshGradientLogoParams extends ShaderSizingParams, ShaderMotion colorBack?: string; colorInner?: string; image?: HTMLImageElement | string | undefined; - stripeWidth?: number; - alphaMask?: boolean; - gap?: boolean; - size?: number; contour?: number; - roundness?: number; - shift?: number; - noise?: number; - outerNoise?: number; - angle?: number; + size?: number; } From ceb3e5fca6308efe152385ba28157a4d9fc519c4 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 17 Nov 2025 18:05:29 +0100 Subject: [PATCH 28/84] folds polishing --- docs/src/app/(shaders)/folds/page.tsx | 6 +-- packages/shaders-react/src/shaders/folds.tsx | 43 +++++++++++++++----- packages/shaders/src/shaders/folds.ts | 14 +++---- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index 47249c446..661e34576 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -89,12 +89,12 @@ const FoldsWithControls = () => { softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, gradient: { value: defaults.gradient, min: 0, max: 1, order: 201 }, alphaMask: { value: defaults.alphaMask, order: 202 }, - // gap: { value: defaults.gap, order: 202 }, size: { value: defaults.size, min: 3, max: 50, order: 203 }, shift: { value: defaults.shift, min: -0.5, max: 0.5, order: 204 }, noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, - outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 205 }, - angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, + noiseScale: { value: defaults.noiseScale, min: .1, max: 3, step: 0.01, order: 206 }, + outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 207 }, + angle: { value: defaults.angle, min: 0, max: 360, order: 208 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index abd0f011f..a394042be 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -32,17 +32,17 @@ export const defaultPreset: FoldsPreset = { frame: 0, colorBack: '#ffffff00', colorInner: '#000000', - colors: ['#809399', '#9d31fc', '#ffffff', '#b3ff66'], + colors: ['#ffed47', '#ffed47', '#31fcb8', '#ffffff', '#ff006a'], stripeWidth: 0.65, alphaMask: true, - gap: false, - size: 13, + noiseScale: 1, + size: 10, shift: 0.5, noise: 0.5, outerNoise: 0, softness: 0, gradient: 1, - angle: 0, + angle: 220, }, }; @@ -58,7 +58,7 @@ export const fstPreset: FoldsPreset = { colors: ['#ffffff'], stripeWidth: 1, alphaMask: false, - gap: false, + noiseScale: 0.5, size: 16, shift: 0.5, noise: 0.32, @@ -81,7 +81,7 @@ export const scdPreset: FoldsPreset = { colors: ['#ffffff', '#000000', '#0022ff', '#ffe500'], stripeWidth: 0.6, alphaMask: false, - gap: false, + noiseScale: 1.5, size: 24, shift: 0, noise: 0.65, @@ -104,7 +104,30 @@ export const trdPreset: FoldsPreset = { colors: ['#ff5ec4', '#5effc8', '#ffe45e', '#ff6b5e'], stripeWidth: 0.3, alphaMask: true, - gap: false, + noiseScale: 1, + size: 45, + shift: 0, + noise: 0.3, + outerNoise: 0, + softness: 0, + gradient: 0, + angle: 0, + }, +}; + +export const frtPreset: FoldsPreset = { + name: '4', + params: { + ...defaultObjectSizing, + scale: 0.8, + speed: 0.2, + frame: 0, + colorBack: '#ffffff', + colorInner: '#ffffff', + colors: ['#000000'], + stripeWidth: 0, + alphaMask: false, + noiseScale: 1, size: 45, shift: 0, noise: 0.3, @@ -115,7 +138,7 @@ export const trdPreset: FoldsPreset = { }, }; -export const foldsPresets: FoldsPreset[] = [defaultPreset, fstPreset, scdPreset, trdPreset]; +export const foldsPresets: FoldsPreset[] = [defaultPreset, fstPreset, scdPreset, trdPreset, frtPreset]; export const Folds: React.FC = memo(function FoldsImpl({ // Own props @@ -132,7 +155,7 @@ export const Folds: React.FC = memo(function FoldsImpl({ gradient = defaultPreset.params.gradient, stripeWidth = defaultPreset.params.stripeWidth, alphaMask = defaultPreset.params.alphaMask, - gap = defaultPreset.params.gap, + noiseScale = defaultPreset.params.noiseScale, size = defaultPreset.params.size, angle = defaultPreset.params.angle, suspendWhenProcessingImage = false, @@ -203,7 +226,7 @@ export const Folds: React.FC = memo(function FoldsImpl({ u_gradient: gradient, u_stripeWidth: stripeWidth, u_alphaMask: alphaMask, - u_gap: gap, + u_noiseScale: noiseScale, u_size: size, u_angle: angle, u_isImage: Boolean(image), diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 1b28dbf97..883447ee8 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -34,7 +34,7 @@ uniform vec4 u_colorInner; uniform float u_softness; uniform float u_stripeWidth; uniform bool u_alphaMask; -uniform bool u_gap; +uniform float u_noiseScale; uniform float u_size; uniform float u_gradient; uniform float u_shift; @@ -186,7 +186,7 @@ void main() { stripesUV = rotate(stripesUV, angle); stripesUV *= u_size; - float n = doubleSNoise(patternsUV + 100., u_time); + float n = doubleSNoise(u_noiseScale * patternsUV + 100., u_time); float edgeAtten = edge + u_outerNoise * (1. - edge); float y = stripesUV.y + edgeAtten * .5 * n * u_size * u_noise; @@ -209,7 +209,7 @@ void main() { line *= imgAlpha; } - float softness = mix(0., u_softness, sst(aa, 2. * aa, w)); + float softness = mix(0., u_softness, sst(0., aa, w)); line -= sst(softness, 0., 1. - fy); line = clamp(line, 0., 1.); @@ -250,10 +250,6 @@ void main() { vec3 color = stripePremulRGB; float opacity = stripeA; - -// if (u_gap == true) { -// color += .6 * (1. - edge) * imgAlpha; -// } vec3 backRgb = u_colorBack.rgb * u_colorBack.a; float backA = u_colorBack.a; @@ -800,7 +796,7 @@ export interface FoldsUniforms extends ShaderSizingUniforms { u_image: HTMLImageElement | string | undefined; u_stripeWidth: number; u_alphaMask: boolean; - u_gap: boolean; + u_noiseScale: number; u_size: number; u_shift: number; u_noise: number; @@ -818,7 +814,7 @@ export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { image?: HTMLImageElement | string | undefined; stripeWidth?: number; alphaMask?: boolean; - gap?: boolean; + noiseScale?: number; size?: number; softness?: number; gradient?: number; From 93a95b7f061b09e5bb863779cfddcf90f0c2f8ed Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 17 Nov 2025 18:08:53 +0100 Subject: [PATCH 29/84] halftone presets --- packages/shaders-react/src/shaders/halftone-lines.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index a660d2df0..76a3b64b2 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -20,7 +20,7 @@ export const defaultPreset: HalftoneLinesPreset = { scale: 1, speed: 0, frame: 0, - colorBack: '#000000', + colorBack: '#615681', colorFront: '#ffffff', stripeWidth: 0, smoothness: 10, From de4f70fb06cb9387c91735aa298103d798efd3da Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 17 Nov 2025 18:13:52 +0100 Subject: [PATCH 30/84] blobsLogo renaming --- .../layout.tsx | 0 .../page.tsx | 28 ++++++++--------- ...gradient-logo-def.ts => blobs-logo-def.ts} | 6 ++-- packages/shaders-react/src/index.ts | 8 ++--- ...{mesh-gradient-logo.tsx => blobs-logo.tsx} | 30 +++++++++---------- packages/shaders/src/index.ts | 12 ++++---- .../{mesh-gradient-logo.ts => blobs-logo.ts} | 20 ++++++------- 7 files changed, 52 insertions(+), 52 deletions(-) rename docs/src/app/(shaders)/{mesh-gradient-logo => blobs-logo}/layout.tsx (100%) rename docs/src/app/(shaders)/{mesh-gradient-logo => blobs-logo}/page.tsx (77%) rename docs/src/shader-defs/{mesh-gradient-logo-def.ts => blobs-logo-def.ts} (75%) rename packages/shaders-react/src/shaders/{mesh-gradient-logo.tsx => blobs-logo.tsx} (78%) rename packages/shaders/src/shaders/{mesh-gradient-logo.ts => blobs-logo.ts} (97%) diff --git a/docs/src/app/(shaders)/mesh-gradient-logo/layout.tsx b/docs/src/app/(shaders)/blobs-logo/layout.tsx similarity index 100% rename from docs/src/app/(shaders)/mesh-gradient-logo/layout.tsx rename to docs/src/app/(shaders)/blobs-logo/layout.tsx diff --git a/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx b/docs/src/app/(shaders)/blobs-logo/page.tsx similarity index 77% rename from docs/src/app/(shaders)/mesh-gradient-logo/page.tsx rename to docs/src/app/(shaders)/blobs-logo/page.tsx index 909a221cf..80e488320 100644 --- a/docs/src/app/(shaders)/mesh-gradient-logo/page.tsx +++ b/docs/src/app/(shaders)/blobs-logo/page.tsx @@ -1,25 +1,25 @@ 'use client'; -import { MeshGradientLogo, meshGradientLogoPresets } from '@paper-design/shaders-react'; +import { BlobsLogo, blobsLogoPresets } from '@paper-design/shaders-react'; import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { meshGradientLogoMeta } from '@paper-design/shaders'; +import { blobsLogoMeta } from '@paper-design/shaders'; import { ShaderFit } from '@paper-design/shaders'; import { levaImageButton } from '@/helpers/leva-image-button'; import { useState, Suspense, useEffect, useCallback } from 'react'; import { ShaderDetails } from '@/components/shader-details'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { meshGradientLogoDef } from '@/shader-defs/mesh-gradient-logo-def'; +import { blobsLogoDef } from '@/shader-defs/blobs-logo-def'; import { toHsla } from '@/helpers/color-utils'; import { useColors } from "@/helpers/use-colors"; // Override just for the docs, we keep it transparent in the preset -// meshGradientLogoPresets[0].params.colorBack = '#000000'; +// blobsLogoPresets[0].params.colorBack = '#000000'; -const { worldWidth, worldHeight, ...defaults } = meshGradientLogoPresets[0].params; +const { worldWidth, worldHeight, ...defaults } = blobsLogoPresets[0].params; const imageFiles = [ 'paradigm.svg', @@ -58,7 +58,7 @@ const imageFiles = [ 'volkswagen.svg', ] as const; -const MeshGradientLogoWithControls = () => { +const BlobsLogoWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); const [image, setImage] = useState('/images/logos/diamond.svg'); @@ -78,7 +78,7 @@ const MeshGradientLogoWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, - maxColorCount: meshGradientLogoMeta.maxColorCount, + maxColorCount: blobsLogoMeta.maxColorCount, }); const [params, setParams] = useControls(() => { @@ -104,7 +104,7 @@ const MeshGradientLogoWithControls = () => { useControls(() => { const presets = Object.fromEntries( - meshGradientLogoPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + blobsLogoPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ name, button(() => { const { colors, ...presetParams } = preset; @@ -121,20 +121,20 @@ const MeshGradientLogoWithControls = () => { // Reset to defaults on mount, so that Leva doesn't show values from other // shaders when navigating (if two shaders have a color1 param for example) useResetLevaParams(params, setParams, defaults); - useUrlParams(params, setParams, meshGradientLogoDef, setColors); - usePresetHighlight(meshGradientLogoPresets, params); + useUrlParams(params, setParams, blobsLogoDef, setColors); + usePresetHighlight(blobsLogoPresets, params); cleanUpLevaParams(params); return ( <> - + - + - + ); }; -export default MeshGradientLogoWithControls; +export default BlobsLogoWithControls; diff --git a/docs/src/shader-defs/mesh-gradient-logo-def.ts b/docs/src/shader-defs/blobs-logo-def.ts similarity index 75% rename from docs/src/shader-defs/mesh-gradient-logo-def.ts rename to docs/src/shader-defs/blobs-logo-def.ts index f0fc41443..7fb059fb4 100644 --- a/docs/src/shader-defs/mesh-gradient-logo-def.ts +++ b/docs/src/shader-defs/blobs-logo-def.ts @@ -1,10 +1,10 @@ -import { meshGradientLogoPresets } from '@paper-design/shaders-react'; +import { blobsLogoPresets } from '@paper-design/shaders-react'; import type { ShaderDef } from './shader-def-types'; import { animatedCommonParams } from './common-param-def'; -const defaultParams = meshGradientLogoPresets[0].params; +const defaultParams = blobsLogoPresets[0].params; -export const meshGradientLogoDef: ShaderDef = { +export const blobsLogoDef: ShaderDef = { name: 'Liquid Metal', description: 'Futuristic liquid metal material applied to uploaded logo or abstract shape.', params: [ diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 9e228d57c..e590aff93 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -115,9 +115,9 @@ export { HalftoneLines, halftoneLinesPresets } from './shaders/halftone-lines.js export type { HalftoneLinesProps } from './shaders/halftone-lines.js'; export type { HalftoneLinesUniforms, HalftoneLinesParams } from '@paper-design/shaders'; -export { MeshGradientLogo, meshGradientLogoPresets } from './shaders/mesh-gradient-logo.js'; -export type { MeshGradientLogoProps } from './shaders/mesh-gradient-logo.js'; -export type { MeshGradientLogoUniforms, MeshGradientLogoParams } from '@paper-design/shaders'; +export { BlobsLogo, blobsLogoPresets } from './shaders/blobs-logo.js'; +export type { BlobsLogoProps } from './shaders/blobs-logo.js'; +export type { BlobsLogoUniforms, BlobsLogoParams } from '@paper-design/shaders'; export { isPaperShaderElement, getShaderColorFromString } from '@paper-design/shaders'; export type { PaperShaderElement, ShaderFit, ShaderSizingParams, ShaderSizingUniforms } from '@paper-design/shaders'; @@ -140,5 +140,5 @@ export { staticRadialGradientMeta, foldsMeta, halftoneLinesMeta, - meshGradientLogoMeta, + blobsLogoMeta, } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/mesh-gradient-logo.tsx b/packages/shaders-react/src/shaders/blobs-logo.tsx similarity index 78% rename from packages/shaders-react/src/shaders/mesh-gradient-logo.tsx rename to packages/shaders-react/src/shaders/blobs-logo.tsx index 67bcff505..68f66ade9 100644 --- a/packages/shaders-react/src/shaders/mesh-gradient-logo.tsx +++ b/packages/shaders-react/src/shaders/blobs-logo.tsx @@ -2,28 +2,28 @@ import { memo, useLayoutEffect, useState } from 'react'; import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; import { colorPropsAreEqual } from '../color-props-are-equal.js'; import { - meshGradientLogoFragmentShader, + blobsLogoFragmentShader, ShaderFitOptions, defaultObjectSizing, - type MeshGradientLogoUniforms, - type MeshGradientLogoParams, - toProcessedMeshGradientLogo, + type BlobsLogoUniforms, + type BlobsLogoParams, + toProcessedBlobsLogo, type ImageShaderPreset, getShaderColorFromString, } from '@paper-design/shaders'; import { transparentPixel } from '../transparent-pixel.js'; import { suspend } from '../suspend.js'; -export interface MeshGradientLogoProps extends ShaderComponentProps, MeshGradientLogoParams { +export interface BlobsLogoProps extends ShaderComponentProps, BlobsLogoParams { /** * Suspends the component when the image is being processed. */ suspendWhenProcessingImage?: boolean; } -type MeshGradientLogoPreset = ImageShaderPreset; +type BlobsLogoPreset = ImageShaderPreset; -export const defaultPreset: MeshGradientLogoPreset = { +export const defaultPreset: BlobsLogoPreset = { name: 'Default', params: { ...defaultObjectSizing, @@ -37,9 +37,9 @@ export const defaultPreset: MeshGradientLogoPreset = { contour: 0.7, }, }; -export const meshGradientLogoPresets: MeshGradientLogoPreset[] = [defaultPreset]; +export const blobsLogoPresets: BlobsLogoPreset[] = [defaultPreset]; -export const MeshGradientLogo: React.FC = memo(function MeshGradientLogoImpl({ +export const BlobsLogo: React.FC = memo(function BlobsLogoImpl({ // Own props colorBack = defaultPreset.params.colorBack, colorInner = defaultPreset.params.colorInner, @@ -62,7 +62,7 @@ export const MeshGradientLogo: React.FC = memo(function M worldWidth = defaultPreset.params.worldWidth, worldHeight = defaultPreset.params.worldHeight, ...props -}: MeshGradientLogoProps) { +}: BlobsLogoProps) { const imageUrl = typeof image === 'string' ? image : image.src; const [processedStateImage, setProcessedStateImage] = useState(transparentPixel); @@ -70,8 +70,8 @@ export const MeshGradientLogo: React.FC = memo(function M if (suspendWhenProcessingImage && typeof window !== 'undefined' && imageUrl) { processedImage = suspend( - (): Promise => toProcessedMeshGradientLogo(imageUrl).then((result) => URL.createObjectURL(result.pngBlob)), - [imageUrl, 'meshGradientLogo'] + (): Promise => toProcessedBlobsLogo(imageUrl).then((result) => URL.createObjectURL(result.pngBlob)), + [imageUrl, 'blobsLogo'] ); } else { processedImage = processedStateImage; @@ -91,7 +91,7 @@ export const MeshGradientLogo: React.FC = memo(function M let url: string; let current = true; - toProcessedMeshGradientLogo(imageUrl).then((result) => { + toProcessedBlobsLogo(imageUrl).then((result) => { if (current) { url = URL.createObjectURL(result.pngBlob); setProcessedStateImage(url); @@ -123,14 +123,14 @@ export const MeshGradientLogo: React.FC = memo(function M u_originY: originY, u_worldWidth: worldWidth, u_worldHeight: worldHeight, - } satisfies MeshGradientLogoUniforms; + } satisfies BlobsLogoUniforms; return ( diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index 109656378..435be4273 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -226,12 +226,12 @@ export { } from './shaders/halftone-lines.js'; export { - meshGradientLogoMeta, - meshGradientLogoFragmentShader, - toProcessedMeshGradientLogo, - type MeshGradientLogoParams, - type MeshGradientLogoUniforms, -} from './shaders/mesh-gradient-logo.js'; + blobsLogoMeta, + blobsLogoFragmentShader, + toProcessedBlobsLogo, + type BlobsLogoParams, + type BlobsLogoUniforms, +} from './shaders/blobs-logo.js'; // ----- Utils ----- // export { getShaderColorFromString } from './get-shader-color-from-string.js'; diff --git a/packages/shaders/src/shaders/mesh-gradient-logo.ts b/packages/shaders/src/shaders/blobs-logo.ts similarity index 97% rename from packages/shaders/src/shaders/mesh-gradient-logo.ts rename to packages/shaders/src/shaders/blobs-logo.ts index 5a3a16bc7..186f65633 100644 --- a/packages/shaders/src/shaders/mesh-gradient-logo.ts +++ b/packages/shaders/src/shaders/blobs-logo.ts @@ -3,7 +3,7 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; import { declarePI, rotation2, proceduralHash21, colorBandingFix } from '../shader-utils.js'; -export const meshGradientLogoMeta = { +export const blobsLogoMeta = { maxColorCount: 6, } as const; @@ -18,7 +18,7 @@ export const meshGradientLogoMeta = { */ // language=GLSL -export const meshGradientLogoFragmentShader: string = `#version 300 es +export const blobsLogoFragmentShader: string = `#version 300 es precision mediump float; uniform sampler2D u_image; @@ -27,7 +27,7 @@ uniform float u_imageAspectRatio; uniform vec2 u_resolution; uniform float u_time; -uniform vec4 u_colors[${meshGradientLogoMeta.maxColorCount}]; +uniform vec4 u_colors[${blobsLogoMeta.maxColorCount}]; uniform float u_colorsCount; uniform vec4 u_colorBack; uniform vec4 u_colorInner; @@ -165,9 +165,9 @@ void main() { float shaping = 2.5 - shapeOffset; t = 2. * u_time; - float f[${ meshGradientLogoMeta.maxColorCount }]; + float f[${ blobsLogoMeta.maxColorCount }]; - vec2 trajs[${ meshGradientLogoMeta.maxColorCount }]; + vec2 trajs[${ blobsLogoMeta.maxColorCount }]; trajs[0] = vec2(0.8 * sin(-0.5 * t), 0.2 + 2.5 * cos(0.3 * t)); trajs[1] = vec2(1.7 * cos(-0.5 * t + 1.), sin(0.8 * t)); trajs[2] = vec2(0.5 * cos(0.3 * t), cos(-0.8 * t)); @@ -176,7 +176,7 @@ void main() { trajs[5] = vec2(0.9 * sin(0.85 * t + 1.), 0.7 * cos(0.6 * t)); float dist = 0.3; - for (int i = 0; i < ${ meshGradientLogoMeta.maxColorCount }; i++) { + for (int i = 0; i < ${ blobsLogoMeta.maxColorCount }; i++) { dist += .03 * float(i); f[i] = getPoint(uv + dist * trajs[i], shaping); } @@ -196,7 +196,7 @@ void main() { vec3 color = vec3(0.); float size = .95 - .9 * u_size; - for (int i = 0; i < ${ meshGradientLogoMeta.maxColorCount }; i++) { + for (int i = 0; i < ${ blobsLogoMeta.maxColorCount }; i++) { if (i >= int(u_colorsCount)) break; f[i] = sst(size, size + 2. * fwidth(f[i]), f[i]); opacity += f[i]; @@ -245,7 +245,7 @@ interface SparsePixelData { neighborIndices: Int32Array; } -export function toProcessedMeshGradientLogo(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { +export function toProcessedBlobsLogo(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const isBlob = typeof file === 'string' && file.startsWith('blob:'); @@ -743,7 +743,7 @@ function solvePoissonSparse( return u; } -export interface MeshGradientLogoUniforms extends ShaderSizingUniforms { +export interface BlobsLogoUniforms extends ShaderSizingUniforms { u_colorBack: [number, number, number, number]; u_colorInner: [number, number, number, number]; u_colors: vec4[]; @@ -753,7 +753,7 @@ export interface MeshGradientLogoUniforms extends ShaderSizingUniforms { u_size: number; } -export interface MeshGradientLogoParams extends ShaderSizingParams, ShaderMotionParams { +export interface BlobsLogoParams extends ShaderSizingParams, ShaderMotionParams { colors?: string[]; colorBack?: string; colorInner?: string; From 69b9b0bd8da4da30936c9be78e5b435654e73ae5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 17 Nov 2025 18:28:54 +0100 Subject: [PATCH 31/84] halftoneLines classic preset --- .../src/shaders/halftone-lines.tsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index 76a3b64b2..af4fa2fd7 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -35,7 +35,31 @@ export const defaultPreset: HalftoneLinesPreset = { grainOverlay: 0.2, }, }; -export const halftoneLinesPresets: HalftoneLinesPreset[] = [defaultPreset]; + +export const classicPreset: HalftoneLinesPreset = { + name: 'Classic', + params: { + ...defaultObjectSizing, + scale: 1, + speed: 0, + frame: 0, + colorBack: '#ffffff', + colorFront: '#000000', + stripeWidth: 1, + smoothness: 10, + size: 60, + angleDistortion: 0, + noiseDistortion: 0, + angle: 0, + contrast: 0.7, + originalColors: false, + inverted: false, + grainMixer: 0, + grainOverlay: 0, + }, +}; + +export const halftoneLinesPresets: HalftoneLinesPreset[] = [defaultPreset, classicPreset]; export const HalftoneLines: React.FC = memo(function HalftoneLinesImpl({ // Own props From 4b58c9de62ab288634ecb8e047789ffbb874e908 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 19 Nov 2025 00:55:55 +0100 Subject: [PATCH 32/84] 2 more folds presets --- packages/shaders-react/src/shaders/folds.tsx | 61 +++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index a394042be..7f66212fd 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -138,7 +138,66 @@ export const frtPreset: FoldsPreset = { }, }; -export const foldsPresets: FoldsPreset[] = [defaultPreset, fstPreset, scdPreset, trdPreset, frtPreset]; +export const ffsPreset: FoldsPreset = { + name: '5', + params: { + ...defaultObjectSizing, + frame: 0, + colors: [ + '#ffed47', + '#ffed47', + '#31fcb8', + '#ffffff', + '#ff006a', + '#3399cc', + '#3333cc', + ], + colorBack: '#ffffff00', + colorInner: '#000000', + stripeWidth: 1.0, + softness: 0.37, + gradient: 1.0, + alphaMask: true, + size: 3.0, + shift: -0.37, + noise: 0.46, + noiseScale: 1.0, + outerNoise: 0.0, + angle: 220, + speed: 0.20, + scale: 0.8, + }, +}; + +export const sixPreset: FoldsPreset = { + name: '6', + params: { + ...defaultObjectSizing, + frame: 0, + colors: [ + '#000000' + ], + colorBack: '#000000', + colorInner: '#ffffff', + + stripeWidth: 0.57, + softness: 0.0, + gradient: 0.0, + alphaMask: true, + + size: 18.0, + shift: -0.50, + noise: 0.61, + noiseScale: 1.07, + outerNoise: 0.0, + + angle: 0, + speed: 0.20, + scale: 0.8, + }, +}; + +export const foldsPresets: FoldsPreset[] = [defaultPreset, fstPreset, scdPreset, trdPreset, frtPreset, ffsPreset, sixPreset]; export const Folds: React.FC = memo(function FoldsImpl({ // Own props From 3f458c969d814d9b1f18c26144dbcab92c13afb2 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 19 Nov 2025 15:52:03 +0100 Subject: [PATCH 33/84] warp sketch to save --- docs/src/app/(shaders)/warp-logo/layout.tsx | 9 + docs/src/app/(shaders)/warp-logo/page.tsx | 140 ++++ docs/src/shader-defs/warp-logo-def.ts | 19 + packages/shaders-react/src/index.ts | 4 + .../shaders-react/src/shaders/warp-logo.tsx | 138 +++ packages/shaders/src/index.ts | 8 + packages/shaders/src/shaders/warp-logo.ts | 785 ++++++++++++++++++ 7 files changed, 1103 insertions(+) create mode 100644 docs/src/app/(shaders)/warp-logo/layout.tsx create mode 100644 docs/src/app/(shaders)/warp-logo/page.tsx create mode 100644 docs/src/shader-defs/warp-logo-def.ts create mode 100644 packages/shaders-react/src/shaders/warp-logo.tsx create mode 100644 packages/shaders/src/shaders/warp-logo.ts diff --git a/docs/src/app/(shaders)/warp-logo/layout.tsx b/docs/src/app/(shaders)/warp-logo/layout.tsx new file mode 100644 index 000000000..f0964a3f1 --- /dev/null +++ b/docs/src/app/(shaders)/warp-logo/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Mesh Gradient Logo Filter • Paper', +}; + +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/docs/src/app/(shaders)/warp-logo/page.tsx b/docs/src/app/(shaders)/warp-logo/page.tsx new file mode 100644 index 000000000..ab42483af --- /dev/null +++ b/docs/src/app/(shaders)/warp-logo/page.tsx @@ -0,0 +1,140 @@ +'use client'; + +import { WarpLogo, warpLogoPresets } from '@paper-design/shaders-react'; +import { useControls, button, folder } from 'leva'; +import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; +import { usePresetHighlight } from '@/helpers/use-preset-highlight'; +import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; +import { warpLogoMeta } from '@paper-design/shaders'; +import { ShaderFit } from '@paper-design/shaders'; +import { levaImageButton } from '@/helpers/leva-image-button'; +import { useState, Suspense, useEffect, useCallback } from 'react'; +import { ShaderDetails } from '@/components/shader-details'; +import { ShaderContainer } from '@/components/shader-container'; +import { useUrlParams } from '@/helpers/use-url-params'; +import { warpLogoDef } from '@/shader-defs/warp-logo-def'; +import { toHsla } from '@/helpers/color-utils'; +import { useColors } from "@/helpers/use-colors"; + +// Override just for the docs, we keep it transparent in the preset +// warpLogoPresets[0].params.colorBack = '#000000'; + +const { worldWidth, worldHeight, ...defaults } = warpLogoPresets[0].params; + +const imageFiles = [ + 'paradigm.svg', + 'paper-logo-only.svg', + 'brave2.png', + 'capy.svg', + 'contra.svg', + 'infinite.svg', + 'linear.svg', + 'mercury.svg', + 'mymind.svg', + 'inbound.svg', + 'resend.svg', + 'shopify.svg', + 'wealth-simple.svg', + 'diamond.svg', + + // 'chanel.svg', + // 'cibc.svg', + // 'cloudflare.svg', + // 'apple.svg', + // 'paper-logo-only.svg', + 'discord.svg', + // 'paper-logo-only', + // 'enterprise-rent.svg', + // 'kfc.svg', + // 'microsoft.svg', + // 'nasa.svg', + // 'netflix.svg', + 'nike.svg', + // 'perkins.svg', + // 'pizza-hut.svg', + // 'remix.svg', + // 'rogers.svg', + 'vercel.svg', + 'volkswagen.svg', +] as const; + +const WarpLogoWithControls = () => { + 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: warpLogoMeta.maxColorCount, + }); + + const [params, setParams] = useControls(() => { + return { + colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorInner: { value: toHsla(defaults.colorInner), order: 101 }, + contour: { value: defaults.contour, min: 0, max: 1, order: 201 }, + size: { value: defaults.size, min: 0, max: 1, order: 203 }, + speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, + scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, + // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, + // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, + // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, + // fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 305 }, + Image: folder( + { + 'Upload image': levaImageButton((img?: HTMLImageElement) => setImage(img ?? '')), + }, + { order: -1 } + ), + }; + }, [colors.length]); + + useControls(() => { + const presets = Object.fromEntries( + warpLogoPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + name, + button(() => { + const { colors, ...presetParams } = preset; + setColors(colors); + setParamsSafe(params, setParams, presetParams); + }), + ]) + ); + return { + Presets: folder(presets, { order: -2 }), + }; + }); + + // Reset to defaults on mount, so that Leva doesn't show values from other + // shaders when navigating (if two shaders have a color1 param for example) + useResetLevaParams(params, setParams, defaults); + useUrlParams(params, setParams, warpLogoDef, setColors); + usePresetHighlight(warpLogoPresets, params); + cleanUpLevaParams(params); + + return ( + <> + + + + + + + + ); +}; + +export default WarpLogoWithControls; diff --git a/docs/src/shader-defs/warp-logo-def.ts b/docs/src/shader-defs/warp-logo-def.ts new file mode 100644 index 000000000..2691b4567 --- /dev/null +++ b/docs/src/shader-defs/warp-logo-def.ts @@ -0,0 +1,19 @@ +import { warpLogoPresets } from '@paper-design/shaders-react'; +import type { ShaderDef } from './shader-def-types'; +import { animatedCommonParams } from './common-param-def'; + +const defaultParams = warpLogoPresets[0].params; + +export const warpLogoDef: ShaderDef = { + name: 'Liquid Metal', + description: 'Futuristic liquid metal material applied to uploaded logo or abstract shape.', + params: [ + { + name: 'image', + type: 'HTMLImageElement | string', + description: + 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', + }, + ...animatedCommonParams, + ], +}; diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index e590aff93..76b8a4bab 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -119,6 +119,10 @@ export { BlobsLogo, blobsLogoPresets } from './shaders/blobs-logo.js'; export type { BlobsLogoProps } from './shaders/blobs-logo.js'; export type { BlobsLogoUniforms, BlobsLogoParams } from '@paper-design/shaders'; +export { WarpLogo, warpLogoPresets } from './shaders/warp-logo.js'; +export type { WarpLogoProps } from './shaders/warp-logo.js'; +export type { WarpLogoUniforms, WarpLogoParams } from '@paper-design/shaders'; + export { isPaperShaderElement, getShaderColorFromString } from '@paper-design/shaders'; export type { PaperShaderElement, ShaderFit, ShaderSizingParams, ShaderSizingUniforms } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/warp-logo.tsx b/packages/shaders-react/src/shaders/warp-logo.tsx new file mode 100644 index 000000000..d6f6f03b5 --- /dev/null +++ b/packages/shaders-react/src/shaders/warp-logo.tsx @@ -0,0 +1,138 @@ +import { memo, useLayoutEffect, useState } from 'react'; +import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; +import { colorPropsAreEqual } from '../color-props-are-equal.js'; +import { + warpLogoFragmentShader, + ShaderFitOptions, + defaultObjectSizing, + type WarpLogoUniforms, + type WarpLogoParams, + toProcessedWarpLogo, + type ImageShaderPreset, + getShaderColorFromString, +} from '@paper-design/shaders'; +import { transparentPixel } from '../transparent-pixel.js'; +import { suspend } from '../suspend.js'; + +export interface WarpLogoProps extends ShaderComponentProps, WarpLogoParams { + /** + * Suspends the component when the image is being processed. + */ + suspendWhenProcessingImage?: boolean; +} + +type WarpLogoPreset = ImageShaderPreset; + +export const defaultPreset: WarpLogoPreset = { + name: 'Default', + params: { + ...defaultObjectSizing, + scale: 0.8, + speed: 1, + frame: 0, + colorBack: '#c8dfd900', + colorInner: '#c8dfd9', + colors: ['#ff9d00', '#fd4f30', '#809bff', '#c5bac5'], + size: 0.5, + contour: 0.7, + }, +}; +export const warpLogoPresets: WarpLogoPreset[] = [defaultPreset]; + +export const WarpLogo: React.FC = memo(function WarpLogoImpl({ + // Own props + colorBack = defaultPreset.params.colorBack, + colorInner = defaultPreset.params.colorInner, + colors = defaultPreset.params.colors, + speed = defaultPreset.params.speed, + frame = defaultPreset.params.frame, + image = '', + contour = defaultPreset.params.contour, + size = defaultPreset.params.size, + suspendWhenProcessingImage = false, + + // Sizing props + fit = defaultPreset.params.fit, + scale = defaultPreset.params.scale, + rotation = defaultPreset.params.rotation, + originX = defaultPreset.params.originX, + originY = defaultPreset.params.originY, + offsetX = defaultPreset.params.offsetX, + offsetY = defaultPreset.params.offsetY, + worldWidth = defaultPreset.params.worldWidth, + worldHeight = defaultPreset.params.worldHeight, + ...props +}: WarpLogoProps) { + const imageUrl = typeof image === 'string' ? image : image.src; + const [processedStateImage, setProcessedStateImage] = useState(transparentPixel); + + let processedImage: string; + + if (suspendWhenProcessingImage && typeof window !== 'undefined' && imageUrl) { + processedImage = suspend( + (): Promise => toProcessedWarpLogo(imageUrl).then((result) => URL.createObjectURL(result.pngBlob)), + [imageUrl, 'warpLogo'] + ); + } else { + processedImage = processedStateImage; + } + + useLayoutEffect(() => { + if (suspendWhenProcessingImage) { + // Skip doing work in the effect as it's been handled by suspense. + return; + } + + if (!imageUrl) { + setProcessedStateImage(transparentPixel); + return; + } + + let url: string; + let current = true; + + toProcessedWarpLogo(imageUrl).then((result) => { + if (current) { + url = URL.createObjectURL(result.pngBlob); + setProcessedStateImage(url); + } + }); + + return () => { + current = false; + }; + }, [imageUrl, suspendWhenProcessingImage]); + + const uniforms = { + // Own uniforms + u_colors: colors.map(getShaderColorFromString), + u_colorsCount: colors.length, + u_colorBack: getShaderColorFromString(colorBack), + u_colorInner: getShaderColorFromString(colorInner), + u_image: processedImage, + u_contour: contour, + u_size: size, + + // Sizing uniforms + u_fit: ShaderFitOptions[fit], + u_scale: scale, + u_rotation: rotation, + u_offsetX: offsetX, + u_offsetY: offsetY, + u_originX: originX, + u_originY: originY, + u_worldWidth: worldWidth, + u_worldHeight: worldHeight, + } satisfies WarpLogoUniforms; + + return ( + + ); +}, colorPropsAreEqual); diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index 435be4273..0cab6682f 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -233,6 +233,14 @@ export { type BlobsLogoUniforms, } from './shaders/blobs-logo.js'; +export { + warpLogoMeta, + warpLogoFragmentShader, + toProcessedWarpLogo, + type WarpLogoParams, + type WarpLogoUniforms, +} from './shaders/warp-logo.js'; + // ----- Utils ----- // export { getShaderColorFromString } from './get-shader-color-from-string.js'; export { getShaderNoiseTexture } from './get-shader-noise-texture.js'; diff --git a/packages/shaders/src/shaders/warp-logo.ts b/packages/shaders/src/shaders/warp-logo.ts new file mode 100644 index 000000000..7535cdb3e --- /dev/null +++ b/packages/shaders/src/shaders/warp-logo.ts @@ -0,0 +1,785 @@ +import type { vec4 } from '../types.js'; +import type { ShaderMotionParams } from '../shader-mount.js'; +import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; +import { declarePI, rotation2, proceduralHash21, colorBandingFix } from '../shader-utils.js'; + +export const warpLogoMeta = { + maxColorCount: 6, +} as const; + +/** + * + * Fluid motion imitation applied over user image + * (animated stripe pattern getting distorted with shape edges) + * + * Uniforms: + * - u_colorBack, u_colorFront (RGBA) + * + */ + +// language=GLSL +export const warpLogoFragmentShader: string = `#version 300 es +precision mediump float; + +uniform sampler2D u_image; +uniform float u_imageAspectRatio; + +uniform vec2 u_resolution; +uniform float u_time; + +uniform vec4 u_colors[${warpLogoMeta.maxColorCount}]; +uniform float u_colorsCount; +uniform vec4 u_colorBack; +uniform vec4 u_colorInner; +uniform float u_contour; +uniform float u_size; + +${sizingVariablesDeclaration} + +out vec4 fragColor; + +${ declarePI } +${ rotation2 } + +${ proceduralHash21 } + +float valueNoise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + float a = hash21(i); + float b = hash21(i + vec2(1.0, 0.0)); + float c = hash21(i + vec2(0.0, 1.0)); + float d = hash21(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + float x1 = mix(a, b, u.x); + float x2 = mix(c, d, u.x); + return mix(x1, x2, u.y); +} + +float getImgFrame(vec2 uv, float th) { + float frame = 1.; + frame *= smoothstep(0., th, uv.y); + frame *= 1.0 - smoothstep(1. - th, 1., uv.y); + frame *= smoothstep(0., th, uv.x); + frame *= 1.0 - smoothstep(1. - th, 1., uv.x); + return frame; +} + +float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) { + vec2 texel = 1.0 / vec2(textureSize(tex, 0)); + vec2 r = max(radius, 0.0) * texel; + + // 1D Gaussian coefficients (Pascal row) + const float a = 1.0;// |offset| = 2 + const float b = 4.0;// |offset| = 1 + const float c = 6.0;// |offset| = 0 + + float norm = 256.0;// (a+b+c+b+a)^2 = 16^2 + float sum = 0.0; + + // y = -2 + { + float wy = a; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, -2.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, -2.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, -2.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, -2.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, -2.0*r.y)).r; + sum += wy * row; + } + + // y = -1 + { + float wy = b; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, -1.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, -1.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, -1.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, -1.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, -1.0*r.y)).r; + sum += wy * row; + } + + // y = 0 (use provided centerSample to avoid an extra fetch) + { + float wy = c; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 0.0)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 0.0)).r + + c * centerSample + + b * texture(tex, uv + vec2(1.0*r.x, 0.0)).r + + a * texture(tex, uv + vec2(2.0*r.x, 0.0)).r; + sum += wy * row; + } + + // y = +1 + { + float wy = b; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 1.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 1.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, 1.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, 1.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, 1.0*r.y)).r; + sum += wy * row; + } + + // y = +2 + { + float wy = a; + float row = + a * texture(tex, uv + vec2(-2.0*r.x, 2.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 2.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, 2.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, 2.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, 2.0*r.y)).r; + sum += wy * row; + } + + return sum / norm; +} + +float sst(float edge0, float edge1, float x) { + return smoothstep(edge0, edge1, x); +} + +float getPoint(vec2 dist, float p) { + float v = pow(1. - clamp(0., 1., length(dist)), 1.); + v = smoothstep(0., 1., v); + v = pow(v, p); + return v; +} + + +void main() { + + float t = 1. * u_time; + + vec2 uv = v_imageUV; + vec2 dudx = dFdx(v_imageUV); + vec2 dudy = dFdy(v_imageUV); + vec4 img = textureGrad(u_image, uv, dudx, dudy); + + float edge = img.r; + edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); + + float imgAlpha = img.g; + + float frame = getImgFrame(v_imageUV, 0.); + imgAlpha *= frame; + edge *= frame; + + // float offset = 1. - edge; + float shadow = ((1. - edge) * imgAlpha); + shadow = pow(shadow, 5.); + + // + // opacity = clamp(opacity, 0., 1.); + // + // color *= imgAlpha; + // opacity *= imgAlpha; + // + // vec3 backRgb = u_colorBack.rgb * u_colorBack.a; + // float backA = u_colorBack.a; + // vec3 innerRgb_raw = u_colorInner.rgb * u_colorInner.a; + // float innerA_raw = u_colorInner.a; + // + // float innerA = innerA_raw * imgAlpha; + // vec3 innerRgb = innerRgb_raw * imgAlpha; + // + // vec3 layerRgb = innerRgb + backRgb * (1.0 - innerA); + // float layerA = innerA + backA * (1.0 - innerA); + // + // vec3 colorCopy = color; + // color = color + layerRgb * (1.0 - opacity); + // opacity = opacity + layerA * (1.0 - opacity); + // + // + // fragColor = vec4(color, opacity); + + +// uv = v_objectUV; + uv = v_imageUV - .5; + uv *= 2.; + + float u_swirl = .5 + .4 * edge; + float u_swirlIterations = 5.; + float u_proportion = .5; + float u_softness = 1.; + + for (int i = 1; i <= 20; i++) { + if (i >= int(u_swirlIterations)) break; + float iFloat = float(i); + uv.x += u_swirl / iFloat * cos(t + iFloat * 2.5 * uv.y); + uv.y += u_swirl / iFloat * cos(t + iFloat * 1.5 * uv.x); + } + + float proportion = clamp(u_proportion, 0., 1.); + + // float shape = 1. * abs(cos(t - uv.y - uv.x)); + float shape = exp(-1.5 * (uv.x * uv.x + uv.y * uv.y)); + shape = clamp(0., 1., shape); + shape *= (.1 + .9 * imgAlpha); + + + float mixer = shape * (u_colorsCount - 1.); + vec4 gradient = u_colors[0]; + gradient.rgb *= gradient.a; + float aa = fwidth(shape); + for (int i = 1; i < ${ warpLogoMeta.maxColorCount }; i++) { + if (i >= int(u_colorsCount)) break; + float m = clamp(mixer - float(i - 1), 0.0, 1.0); + + float localMixerStart = floor(m); + float softness = .5 * u_softness + fwidth(m); + float smoothed = smoothstep(max(0., .5 - softness - aa), min(1., .5 + softness + aa), m - localMixerStart); + float stepped = localMixerStart + smoothed; + + m = mix(stepped, m, u_softness); + + vec4 c = u_colors[i]; + c.rgb *= c.a; + gradient = mix(gradient, c, m); + } + + vec3 color = gradient.rgb; + float opacity = gradient.a; + + fragColor = vec4(color, opacity); +} +`; + +// Configuration for Poisson solver +export const POISSON_CONFIG_OPTIMIZED = { + measurePerformance: false, // Set to true to see performance metrics + workingSize: 512, // Size to solve Poisson at (will upscale to original size) + iterations: 32, // SOR converges ~2-20x faster than standard Gauss-Seidel +}; + +// Precomputed pixel data for sparse processing +interface SparsePixelData { + interiorPixels: Uint32Array; // Indices of interior pixels + boundaryPixels: Uint32Array; // Indices of boundary pixels + pixelCount: number; + // Neighbor indices for each interior pixel (4 neighbors per pixel) + // Layout: [east, west, north, south] for each pixel + neighborIndices: Int32Array; +} + +export function toProcessedWarpLogo(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const isBlob = typeof file === 'string' && file.startsWith('blob:'); + + return new Promise((resolve, reject) => { + if (!file || !ctx) { + reject(new Error('Invalid file or canvas context')); + return; + } + + const blobContentTypePromise = isBlob && fetch(file).then((res) => res.headers.get('Content-Type')); + const img = new Image(); + img.crossOrigin = 'anonymous'; + const totalStartTime = performance.now(); + + img.onload = async () => { + // Force SVG to load at a high fidelity size if it's an SVG + let isSVG; + + const blobContentType = await blobContentTypePromise; + + if (blobContentType) { + isSVG = blobContentType === 'image/svg+xml'; + } else if (typeof file === 'string') { + isSVG = file.endsWith('.svg') || file.startsWith('data:image/svg+xml'); + } else { + isSVG = file.type === 'image/svg+xml'; + } + + let originalWidth = img.width || img.naturalWidth; + let originalHeight = img.height || img.naturalHeight; + + if (isSVG) { + // Scale SVG to max dimension while preserving aspect ratio + const svgMaxSize = 4096; + const aspectRatio = originalWidth / originalHeight; + + if (originalWidth > originalHeight) { + originalWidth = svgMaxSize; + originalHeight = svgMaxSize / aspectRatio; + } else { + originalHeight = svgMaxSize; + originalWidth = svgMaxSize * aspectRatio; + } + + img.width = originalWidth; + img.height = originalHeight; + } + + // Always scale to working resolution for consistency + const minDimension = Math.min(originalWidth, originalHeight); + const targetSize = POISSON_CONFIG_OPTIMIZED.workingSize; + + // Calculate scale to fit within workingSize + const scaleFactor = targetSize / minDimension; + const width = Math.round(originalWidth * scaleFactor); + const height = Math.round(originalHeight * scaleFactor); + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Processing Mode]`); + console.log(` Original: ${originalWidth}×${originalHeight}`); + console.log(` Working: ${width}×${height} (${(scaleFactor * 100).toFixed(1)}% scale)`); + if (scaleFactor < 1) { + console.log(` Speedup: ~${Math.round(1 / (scaleFactor * scaleFactor))}×`); + } + } + + canvas.width = originalWidth; + canvas.height = originalHeight; + + // Use a smaller canvas for shape detection and Poisson solving + const shapeCanvas = document.createElement('canvas'); + shapeCanvas.width = width; + shapeCanvas.height = height; + + const shapeCtx = shapeCanvas.getContext('2d')!; + shapeCtx.drawImage(img, 0, 0, width, height); + + // 1) Build optimized masks using TypedArrays + const startMask = performance.now(); + + const shapeImageData = shapeCtx.getImageData(0, 0, width, height); + const data = shapeImageData.data; + + // Use Uint8Array for masks (1 byte per pixel vs 8+ bytes for boolean array) + const shapeMask = new Uint8Array(width * height); + const boundaryMask = new Uint8Array(width * height); + + // First pass: identify shape pixels + let shapePixelCount = 0; + for (let i = 0, idx = 0; i < data.length; i += 4, idx++) { + const a = data[i + 3]; + const isShape = a === 0 ? 0 : 1; + shapeMask[idx] = isShape; + shapePixelCount += isShape; + } + + // 2) Optimized boundary detection using sparse approach + // Only check shape pixels, not all pixels + const boundaryIndices: number[] = []; + const interiorIndices: number[] = []; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + if (!shapeMask[idx]) continue; + + // Check if pixel is on boundary (optimized: early exit) + let isBoundary = false; + + // Check 4-connected neighbors first (most common case) + if (x === 0 || x === width - 1 || y === 0 || y === height - 1) { + isBoundary = true; + } else { + // Check all 8 neighbors (including diagonals) for comprehensive boundary detection + isBoundary = + !shapeMask[idx - 1] || // left + !shapeMask[idx + 1] || // right + !shapeMask[idx - width] || // top + !shapeMask[idx + width] || // bottom + !shapeMask[idx - width - 1] || // top-left + !shapeMask[idx - width + 1] || // top-right + !shapeMask[idx + width - 1] || // bottom-left + !shapeMask[idx + width + 1]; // bottom-right + } + + if (isBoundary) { + boundaryMask[idx] = 1; + boundaryIndices.push(idx); + } else { + interiorIndices.push(idx); + } + } + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Mask Building] Time: ${(performance.now() - startMask).toFixed(2)}ms`); + console.log( + ` Shape pixels: ${shapePixelCount} / ${width * height} (${((shapePixelCount / (width * height)) * 100).toFixed(1)}%)` + ); + console.log(` Interior pixels: ${interiorIndices.length}`); + console.log(` Boundary pixels: ${boundaryIndices.length}`); + } + + // 3) Precompute sparse data structure for solver + const sparseData = buildSparseData( + shapeMask, + boundaryMask, + new Uint32Array(interiorIndices), + new Uint32Array(boundaryIndices), + width, + height + ); + + // 4) Solve Poisson equation with optimized sparse solver + const startSolve = performance.now(); + const u = solvePoissonSparse(sparseData, shapeMask, boundaryMask, width, height); + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + console.log(`[Poisson Solve] Time: ${(performance.now() - startSolve).toFixed(2)}ms`); + } + + // 5) Generate output image + let maxVal = 0; + let finalImageData: ImageData; + + // Only check shape pixels for max value + for (let i = 0; i < interiorIndices.length; i++) { + const idx = interiorIndices[i]!; + if (u[idx]! > maxVal) maxVal = u[idx]!; + } + + // Create roundness image at working resolution + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = width; + tempCanvas.height = height; + const tempCtx = tempCanvas.getContext('2d')!; + + const tempImg = tempCtx.createImageData(width, height); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + const px = idx * 4; + + if (!shapeMask[idx]) { + tempImg.data[px] = 255; + tempImg.data[px + 1] = 255; + tempImg.data[px + 2] = 255; + tempImg.data[px + 3] = 0; // Alpha = 0 for background + } else { + const poissonRatio = u[idx]! / maxVal; + let gray = 255 * (1 - poissonRatio); + tempImg.data[px] = gray; + tempImg.data[px + 1] = gray; + tempImg.data[px + 2] = gray; + tempImg.data[px + 3] = 255; // Alpha = 255 for shape + } + } + } + tempCtx.putImageData(tempImg, 0, 0); + + // Upscale to original resolution with smooth interpolation + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, originalWidth, originalHeight); + + // Now get the upscaled image data for final output + const outImg = ctx.getImageData(0, 0, originalWidth, originalHeight); + + // Re-apply edges from original resolution with anti-aliasing + // This ensures edges are pixel-perfect while roundness is smooth + const originalCanvas = document.createElement('canvas'); + originalCanvas.width = originalWidth; + originalCanvas.height = originalHeight; + const originalCtx = originalCanvas.getContext('2d')!; + // originalCtx.fillStyle = "white"; + // originalCtx.fillRect(0, 0, originalWidth, originalHeight); + originalCtx.drawImage(img, 0, 0, originalWidth, originalHeight); + const originalData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); + + // Process each pixel: Red channel = roundness, Alpha channel = original alpha + for (let i = 0; i < outImg.data.length; i += 4) { + const a = originalData.data[i + 3]!; + // Use only alpha to determine background vs shape + const upscaledAlpha = outImg.data[i + 3]!; + if (a === 0) { + // Background pixel + outImg.data[i] = 255; + outImg.data[i + 1] = 0; + } else { + // Red channel carries the roundness + // Check if upscale missed this pixel by looking at alpha channel + // If upscaled alpha is 0, the low-res version thought this was background + outImg.data[i] = upscaledAlpha === 0 ? 0 : outImg.data[i]!; // roundness or 0 + outImg.data[i + 1] = a; // original alpha + } + + // Unused channels fixed + outImg.data[i + 2] = 255; + outImg.data[i + 3] = 255; + } + + ctx.putImageData(outImg, 0, 0); + finalImageData = outImg; + canvas.toBlob((blob) => { + if (!blob) { + reject(new Error('Failed to create PNG blob')); + return; + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + const totalTime = performance.now() - totalStartTime; + console.log(`[Total Processing Time] ${totalTime.toFixed(2)}ms`); + if (scaleFactor < 1) { + const estimatedFullResTime = totalTime * Math.pow((originalWidth * originalHeight) / (width * height), 1.5); + console.log(`[Estimated time at full resolution] ~${estimatedFullResTime.toFixed(0)}ms`); + console.log( + `[Time saved] ~${(estimatedFullResTime - totalTime).toFixed(0)}ms (${Math.round(estimatedFullResTime / totalTime)}× faster)` + ); + } + } + + resolve({ + imageData: finalImageData, + pngBlob: blob, + }); + }, 'image/png'); + }; + + img.onerror = () => reject(new Error('Failed to load image')); + img.src = typeof file === 'string' ? file : URL.createObjectURL(file); + }); +} + +function blurRedChannel( + imageData: ImageData, + width: number, + height: number, + radius = 2 +) { + const src = imageData.data; + const pixelCount = width * height; + + const tmp = new Uint8ClampedArray(pixelCount); + const dst = new Uint8ClampedArray(pixelCount); + + // --- Horizontal blur --- + for (let y = 0; y < height; y++) { + let sum = 0; + let count = 0; + + // initial window centered at x = 0 + for (let dx = -radius; dx <= radius; dx++) { + const xClamped = Math.max(0, Math.min(width - 1, dx)); + const idx = (y * width + xClamped) * 4; + sum += src[idx]!; + count++; + } + + for (let x = 0; x < width; x++) { + const outIdx = y * width + x; + tmp[outIdx] = sum / count; + + const xRemove = x - radius; + if (xRemove >= 0) { + const idxRemove = (y * width + xRemove) * 4; + sum -= src[idxRemove]!; + count--; + } + + const xAdd = x + radius + 1; + if (xAdd < width) { + const idxAdd = (y * width + xAdd) * 4; + sum += src[idxAdd]!; + count++; + } + } + } + + // --- Vertical blur --- + for (let x = 0; x < width; x++) { + let sum = 0; + let count = 0; + + // initial window centered at y = 0 + for (let dy = -radius; dy <= radius; dy++) { + const yClamped = Math.max(0, Math.min(height - 1, dy)); + const idx = yClamped * width + x; + sum += tmp[idx]!; + count++; + } + + for (let y = 0; y < height; y++) { + const outIdx = y * width + x; + dst[outIdx] = sum / count; + + const yRemove = y - radius; + if (yRemove >= 0) { + const idxRemove = yRemove * width + x; + sum -= tmp[idxRemove]!; + count--; + } + + const yAdd = y + radius + 1; + if (yAdd < height) { + const idxAdd = yAdd * width + x; + sum += tmp[idxAdd]!; + count++; + } + } + } + + // --- Write blurred red back into ImageData --- + for (let i = 0; i < pixelCount; i++) { + const px = i * 4; + src[px] = dst[i]!; + } +} + + +function buildSparseData( + shapeMask: Uint8Array, + boundaryMask: Uint8Array, + interiorPixels: Uint32Array, + boundaryPixels: Uint32Array, + width: number, + height: number +): SparsePixelData { + const pixelCount = interiorPixels.length; + + // Build neighbor indices for sparse processing + // For each interior pixel, store indices of its 4 neighbors + // Use -1 for out-of-bounds or non-shape neighbors + const neighborIndices = new Int32Array(pixelCount * 4); + + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + const x = idx % width; + const y = Math.floor(idx / width); + + // East neighbor + neighborIndices[i * 4 + 0] = x < width - 1 && shapeMask[idx + 1] ? idx + 1 : -1; + // West neighbor + neighborIndices[i * 4 + 1] = x > 0 && shapeMask[idx - 1] ? idx - 1 : -1; + // North neighbor + neighborIndices[i * 4 + 2] = y > 0 && shapeMask[idx - width] ? idx - width : -1; + // South neighbor + neighborIndices[i * 4 + 3] = y < height - 1 && shapeMask[idx + width] ? idx + width : -1; + } + + return { + interiorPixels, + boundaryPixels, + pixelCount, + neighborIndices, + }; +} + +function solvePoissonSparse( + sparseData: SparsePixelData, + shapeMask: Uint8Array, + boundaryMask: Uint8Array, + width: number, + height: number +): Float32Array { + // This controls how smooth the falloff roundness will be and extend into the shape + const ITERATIONS = POISSON_CONFIG_OPTIMIZED.iterations; + + // Keep C constant - only iterations control roundness spread + const C = 0.01; + + const u = new Float32Array(width * height); + const { interiorPixels, neighborIndices, pixelCount } = sparseData; + + // Performance tracking + const startTime = performance.now(); + + // Red-Black SOR for better symmetry with fewer iterations + // omega between 1.8-1.95 typically gives best convergence for Poisson + const omega = 1.9; + + // Pre-classify pixels as red or black for efficient processing + const redPixels: number[] = []; + const blackPixels: number[] = []; + + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + const x = idx % width; + const y = Math.floor(idx / width); + + if ((x + y) % 2 === 0) { + redPixels.push(i); + } else { + blackPixels.push(i); + } + } + + for (let iter = 0; iter < ITERATIONS; iter++) { + // Red pass: update red pixels + for (const i of redPixels) { + const idx = interiorPixels[i]!; + + // Get precomputed neighbor indices + const eastIdx = neighborIndices[i * 4 + 0]!; + const westIdx = neighborIndices[i * 4 + 1]!; + const northIdx = neighborIndices[i * 4 + 2]!; + const southIdx = neighborIndices[i * 4 + 3]!; + + // Sum neighbors (use 0 for out-of-bounds) + let sumN = 0; + if (eastIdx >= 0) sumN += u[eastIdx]!; + if (westIdx >= 0) sumN += u[westIdx]!; + if (northIdx >= 0) sumN += u[northIdx]!; + if (southIdx >= 0) sumN += u[southIdx]!; + + // SOR update: blend new value with old value + const newValue = (C + sumN) / 4; + u[idx] = omega * newValue + (1 - omega) * u[idx]!; + } + + // Black pass: update black pixels + for (const i of blackPixels) { + const idx = interiorPixels[i]!; + + // Get precomputed neighbor indices + const eastIdx = neighborIndices[i * 4 + 0]!; + const westIdx = neighborIndices[i * 4 + 1]!; + const northIdx = neighborIndices[i * 4 + 2]!; + const southIdx = neighborIndices[i * 4 + 3]!; + + // Sum neighbors (use 0 for out-of-bounds) + let sumN = 0; + if (eastIdx >= 0) sumN += u[eastIdx]!; + if (westIdx >= 0) sumN += u[westIdx]!; + if (northIdx >= 0) sumN += u[northIdx]!; + if (southIdx >= 0) sumN += u[southIdx]!; + + // SOR update: blend new value with old value + const newValue = (C + sumN) / 4; + u[idx] = omega * newValue + (1 - omega) * u[idx]!; + } + } + + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { + const elapsed = performance.now() - startTime; + + console.log(`[Optimized Poisson Solver (SOR ω=${omega})]`); + console.log(` Working size: ${width}×${height}`); + console.log(` Iterations: ${ITERATIONS}`); + console.log(` Time: ${elapsed.toFixed(2)}ms`); + console.log(` Interior pixels processed: ${pixelCount}`); + console.log(` Speed: ${((ITERATIONS * pixelCount) / (elapsed * 1000)).toFixed(2)} Mpixels/sec`); + } + + return u; +} + +export interface WarpLogoUniforms extends ShaderSizingUniforms { + u_colorBack: [number, number, number, number]; + u_colorInner: [number, number, number, number]; + u_colors: vec4[]; + u_colorsCount: number; + u_image: HTMLImageElement | string | undefined; + u_contour: number; + u_size: number; +} + +export interface WarpLogoParams extends ShaderSizingParams, ShaderMotionParams { + colors?: string[]; + colorBack?: string; + colorInner?: string; + image?: HTMLImageElement | string | undefined; + contour?: number; + size?: number; +} From 42ee0e179dbf93dba66fb8b1a2a2696030672dab Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 24 Nov 2025 01:57:59 +0100 Subject: [PATCH 34/84] progress --- docs/src/app/(shaders)/warp-logo/page.tsx | 8 +- .../shaders-react/src/shaders/warp-logo.tsx | 20 +- packages/shaders/src/shaders/warp-logo.ts | 210 ++++-------------- 3 files changed, 58 insertions(+), 180 deletions(-) diff --git a/docs/src/app/(shaders)/warp-logo/page.tsx b/docs/src/app/(shaders)/warp-logo/page.tsx index ab42483af..ce4d59f1a 100644 --- a/docs/src/app/(shaders)/warp-logo/page.tsx +++ b/docs/src/app/(shaders)/warp-logo/page.tsx @@ -85,10 +85,12 @@ const WarpLogoWithControls = () => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorInner: { value: toHsla(defaults.colorInner), order: 101 }, - contour: { value: defaults.contour, min: 0, max: 1, order: 201 }, - size: { value: defaults.size, min: 0, max: 1, order: 203 }, - speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, + distortion: { value: defaults.distortion, min: 0, max: 1, order: 201 }, + outerVisibility: { value: defaults.outerVisibility, min: 0, max: 1, order: 203 }, + outerDistortion: { value: defaults.outerDistortion, min: 0, max: 1, order: 203 }, + speed: { value: defaults.speed, min: 0, max: 4, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, + innerFill: { value: defaults.innerFill, min: 0, max: 1, order: 302 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, diff --git a/packages/shaders-react/src/shaders/warp-logo.tsx b/packages/shaders-react/src/shaders/warp-logo.tsx index d6f6f03b5..18c7a92bc 100644 --- a/packages/shaders-react/src/shaders/warp-logo.tsx +++ b/packages/shaders-react/src/shaders/warp-logo.tsx @@ -32,9 +32,11 @@ export const defaultPreset: WarpLogoPreset = { frame: 0, colorBack: '#c8dfd900', colorInner: '#c8dfd9', - colors: ['#ff9d00', '#fd4f30', '#809bff', '#c5bac5'], - size: 0.5, - contour: 0.7, + colors: ['#00000000', '#006880', '#809bff', '#ff00bf', '#fbff00'], + outerVisibility: 0.4, + distortion: 0.7, + innerFill: 0, + outerDistortion: 0.85, }, }; export const warpLogoPresets: WarpLogoPreset[] = [defaultPreset]; @@ -47,8 +49,10 @@ export const WarpLogo: React.FC = memo(function WarpLogoImpl({ speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, image = '', - contour = defaultPreset.params.contour, - size = defaultPreset.params.size, + distortion = defaultPreset.params.distortion, + outerVisibility = defaultPreset.params.outerVisibility, + innerFill = defaultPreset.params.innerFill, + outerDistortion = defaultPreset.params.outerDistortion, suspendWhenProcessingImage = false, // Sizing props @@ -110,8 +114,10 @@ export const WarpLogo: React.FC = memo(function WarpLogoImpl({ u_colorBack: getShaderColorFromString(colorBack), u_colorInner: getShaderColorFromString(colorInner), u_image: processedImage, - u_contour: contour, - u_size: size, + u_distortion: distortion, + u_outerVisibility: outerVisibility, + u_innerFill: innerFill, + u_outerDistortion: outerDistortion, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/warp-logo.ts b/packages/shaders/src/shaders/warp-logo.ts index 7535cdb3e..a74172146 100644 --- a/packages/shaders/src/shaders/warp-logo.ts +++ b/packages/shaders/src/shaders/warp-logo.ts @@ -1,7 +1,7 @@ import type { vec4 } from '../types.js'; import type { ShaderMotionParams } from '../shader-mount.js'; import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, rotation2, proceduralHash21, colorBandingFix } from '../shader-utils.js'; +import { declarePI, rotation2, proceduralHash21 } from '../shader-utils.js'; export const warpLogoMeta = { maxColorCount: 6, @@ -31,8 +31,10 @@ uniform vec4 u_colors[${warpLogoMeta.maxColorCount}]; uniform float u_colorsCount; uniform vec4 u_colorBack; uniform vec4 u_colorInner; -uniform float u_contour; -uniform float u_size; +uniform float u_distortion; +uniform float u_outerVisibility; +uniform float u_innerFill; +uniform float u_outerDistortion; ${sizingVariablesDeclaration} @@ -41,21 +43,6 @@ out vec4 fragColor; ${ declarePI } ${ rotation2 } -${ proceduralHash21 } - -float valueNoise(vec2 st) { - vec2 i = floor(st); - vec2 f = fract(st); - float a = hash21(i); - float b = hash21(i + vec2(1.0, 0.0)); - float c = hash21(i + vec2(0.0, 1.0)); - float d = hash21(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - float x1 = mix(a, b, u.x); - float x2 = mix(c, d, u.x); - return mix(x1, x2, u.y); -} - float getImgFrame(vec2 uv, float th) { float frame = 1.; frame *= smoothstep(0., th, uv.y); @@ -65,7 +52,7 @@ float getImgFrame(vec2 uv, float th) { return frame; } -float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) { +float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius) { vec2 texel = 1.0 / vec2(textureSize(tex, 0)); vec2 r = max(radius, 0.0) * texel; @@ -107,7 +94,7 @@ float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, fl float row = a * texture(tex, uv + vec2(-2.0*r.x, 0.0)).r + b * texture(tex, uv + vec2(-1.0*r.x, 0.0)).r + - c * centerSample + + b * texture(tex, uv + vec2(0.)).r + b * texture(tex, uv + vec2(1.0*r.x, 0.0)).r + a * texture(tex, uv + vec2(2.0*r.x, 0.0)).r; sum += wy * row; @@ -153,75 +140,44 @@ float getPoint(vec2 dist, float p) { void main() { - - float t = 1. * u_time; + + float t = u_time; vec2 uv = v_imageUV; vec2 dudx = dFdx(v_imageUV); vec2 dudy = dFdy(v_imageUV); vec4 img = textureGrad(u_image, uv, dudx, dudy); - float edge = img.r; - edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); - + float edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10.); float imgAlpha = img.g; float frame = getImgFrame(v_imageUV, 0.); imgAlpha *= frame; - edge *= frame; +// edge *= frame; - // float offset = 1. - edge; - float shadow = ((1. - edge) * imgAlpha); - shadow = pow(shadow, 5.); - // - // opacity = clamp(opacity, 0., 1.); - // - // color *= imgAlpha; - // opacity *= imgAlpha; - // - // vec3 backRgb = u_colorBack.rgb * u_colorBack.a; - // float backA = u_colorBack.a; - // vec3 innerRgb_raw = u_colorInner.rgb * u_colorInner.a; - // float innerA_raw = u_colorInner.a; - // - // float innerA = innerA_raw * imgAlpha; - // vec3 innerRgb = innerRgb_raw * imgAlpha; - // - // vec3 layerRgb = innerRgb + backRgb * (1.0 - innerA); - // float layerA = innerA + backA * (1.0 - innerA); - // - // vec3 colorCopy = color; - // color = color + layerRgb * (1.0 - opacity); - // opacity = opacity + layerA * (1.0 - opacity); - // - // - // fragColor = vec4(color, opacity); - - -// uv = v_objectUV; - uv = v_imageUV - .5; + + uv = v_objectUV; +// uv = v_imageUV - .5; uv *= 2.; - float u_swirl = .5 + .4 * edge; - float u_swirlIterations = 5.; - float u_proportion = .5; - float u_softness = 1.; - - for (int i = 1; i <= 20; i++) { - if (i >= int(u_swirlIterations)) break; - float iFloat = float(i); - uv.x += u_swirl / iFloat * cos(t + iFloat * 2.5 * uv.y); - uv.y += u_swirl / iFloat * cos(t + iFloat * 1.5 * uv.x); - } - - float proportion = clamp(u_proportion, 0., 1.); - - // float shape = 1. * abs(cos(t - uv.y - uv.x)); + float swirl = mix(0., u_distortion, u_outerDistortion) * (1. - imgAlpha) + u_distortion * edge; +// swirl *= (.2 + .8 * smoothstep(1., 0., length(.5 * uv))); + +// uv = rotate(uv, 1.8); +// uv.x -= .9; + + for (int i = 1; i <= 6; i++) { + float iFloat = float(i); + uv.x += swirl / iFloat * cos(t + iFloat * 2.1 * uv.y); + uv.y += swirl / iFloat * cos(t + iFloat * 1.5 * uv.x); + } float shape = exp(-1.5 * (uv.x * uv.x + uv.y * uv.y)); + shape += u_innerFill * imgAlpha * frame; shape = clamp(0., 1., shape); - shape *= (.1 + .9 * imgAlpha); - + + float outerPower = pow(u_outerVisibility, 3.); + shape *= (outerPower + (1. - outerPower) * imgAlpha); float mixer = shape * (u_colorsCount - 1.); vec4 gradient = u_colors[0]; @@ -229,14 +185,10 @@ void main() { float aa = fwidth(shape); for (int i = 1; i < ${ warpLogoMeta.maxColorCount }; i++) { if (i >= int(u_colorsCount)) break; - float m = clamp(mixer - float(i - 1), 0.0, 1.0); + float m = clamp(mixer - float(i - 1), 0., 1.); float localMixerStart = floor(m); - float softness = .5 * u_softness + fwidth(m); - float smoothed = smoothstep(max(0., .5 - softness - aa), min(1., .5 + softness + aa), m - localMixerStart); - float stepped = localMixerStart + smoothed; - - m = mix(stepped, m, u_softness); + float smoothed = smoothstep(0., localMixerStart, m); vec4 c = u_colors[i]; c.rgb *= c.a; @@ -246,7 +198,7 @@ void main() { vec3 color = gradient.rgb; float opacity = gradient.a; - fragColor = vec4(color, opacity); + fragColor = vec4(color, opacity); } `; @@ -542,92 +494,6 @@ export function toProcessedWarpLogo(file: File | string): Promise<{ imageData: I }); } -function blurRedChannel( - imageData: ImageData, - width: number, - height: number, - radius = 2 -) { - const src = imageData.data; - const pixelCount = width * height; - - const tmp = new Uint8ClampedArray(pixelCount); - const dst = new Uint8ClampedArray(pixelCount); - - // --- Horizontal blur --- - for (let y = 0; y < height; y++) { - let sum = 0; - let count = 0; - - // initial window centered at x = 0 - for (let dx = -radius; dx <= radius; dx++) { - const xClamped = Math.max(0, Math.min(width - 1, dx)); - const idx = (y * width + xClamped) * 4; - sum += src[idx]!; - count++; - } - - for (let x = 0; x < width; x++) { - const outIdx = y * width + x; - tmp[outIdx] = sum / count; - - const xRemove = x - radius; - if (xRemove >= 0) { - const idxRemove = (y * width + xRemove) * 4; - sum -= src[idxRemove]!; - count--; - } - - const xAdd = x + radius + 1; - if (xAdd < width) { - const idxAdd = (y * width + xAdd) * 4; - sum += src[idxAdd]!; - count++; - } - } - } - - // --- Vertical blur --- - for (let x = 0; x < width; x++) { - let sum = 0; - let count = 0; - - // initial window centered at y = 0 - for (let dy = -radius; dy <= radius; dy++) { - const yClamped = Math.max(0, Math.min(height - 1, dy)); - const idx = yClamped * width + x; - sum += tmp[idx]!; - count++; - } - - for (let y = 0; y < height; y++) { - const outIdx = y * width + x; - dst[outIdx] = sum / count; - - const yRemove = y - radius; - if (yRemove >= 0) { - const idxRemove = yRemove * width + x; - sum -= tmp[idxRemove]!; - count--; - } - - const yAdd = y + radius + 1; - if (yAdd < height) { - const idxAdd = yAdd * width + x; - sum += tmp[idxAdd]!; - count++; - } - } - } - - // --- Write blurred red back into ImageData --- - for (let i = 0; i < pixelCount; i++) { - const px = i * 4; - src[px] = dst[i]!; - } -} - - function buildSparseData( shapeMask: Uint8Array, boundaryMask: Uint8Array, @@ -771,8 +637,10 @@ export interface WarpLogoUniforms extends ShaderSizingUniforms { u_colors: vec4[]; u_colorsCount: number; u_image: HTMLImageElement | string | undefined; - u_contour: number; - u_size: number; + u_distortion: number; + u_outerVisibility: number; + u_innerFill: number; + u_outerDistortion: number; } export interface WarpLogoParams extends ShaderSizingParams, ShaderMotionParams { @@ -780,6 +648,8 @@ export interface WarpLogoParams extends ShaderSizingParams, ShaderMotionParams { colorBack?: string; colorInner?: string; image?: HTMLImageElement | string | undefined; - contour?: number; - size?: number; + distortion?: number; + outerVisibility?: number; + innerFill?: number; + outerDistortion?: number; } From 99a25550f5ce77a5e1f1f89a3795d32b9eb8caff Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 24 Nov 2025 02:47:32 +0100 Subject: [PATCH 35/84] progress --- docs/src/app/(shaders)/warp-logo/page.tsx | 5 ++-- .../shaders-react/src/shaders/warp-logo.tsx | 7 +++-- packages/shaders/src/shaders/warp-logo.ts | 29 +++++++++++-------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/docs/src/app/(shaders)/warp-logo/page.tsx b/docs/src/app/(shaders)/warp-logo/page.tsx index ce4d59f1a..94fb7e759 100644 --- a/docs/src/app/(shaders)/warp-logo/page.tsx +++ b/docs/src/app/(shaders)/warp-logo/page.tsx @@ -83,14 +83,15 @@ const WarpLogoWithControls = () => { const [params, setParams] = useControls(() => { return { - colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorInner: { value: toHsla(defaults.colorInner), order: 101 }, + // colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + // colorInner: { value: toHsla(defaults.colorInner), order: 101 }, distortion: { value: defaults.distortion, min: 0, max: 1, order: 201 }, outerVisibility: { value: defaults.outerVisibility, min: 0, max: 1, order: 203 }, outerDistortion: { value: defaults.outerDistortion, min: 0, max: 1, order: 203 }, speed: { value: defaults.speed, min: 0, max: 4, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, innerFill: { value: defaults.innerFill, min: 0, max: 1, order: 302 }, + layering: { value: defaults.layering, min: 0, max: 1, order: 302 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, diff --git a/packages/shaders-react/src/shaders/warp-logo.tsx b/packages/shaders-react/src/shaders/warp-logo.tsx index 18c7a92bc..8100f3e42 100644 --- a/packages/shaders-react/src/shaders/warp-logo.tsx +++ b/packages/shaders-react/src/shaders/warp-logo.tsx @@ -27,16 +27,17 @@ export const defaultPreset: WarpLogoPreset = { name: 'Default', params: { ...defaultObjectSizing, - scale: 0.8, + scale: 0.65, speed: 1, frame: 0, colorBack: '#c8dfd900', colorInner: '#c8dfd9', - colors: ['#00000000', '#006880', '#809bff', '#ff00bf', '#fbff00'], + colors: ['#00000000', '#21abca', '#809bff', '#ff00bf', '#fbff00'], outerVisibility: 0.4, distortion: 0.7, innerFill: 0, outerDistortion: 0.85, + layering: 0, }, }; export const warpLogoPresets: WarpLogoPreset[] = [defaultPreset]; @@ -53,6 +54,7 @@ export const WarpLogo: React.FC = memo(function WarpLogoImpl({ outerVisibility = defaultPreset.params.outerVisibility, innerFill = defaultPreset.params.innerFill, outerDistortion = defaultPreset.params.outerDistortion, + layering = defaultPreset.params.layering, suspendWhenProcessingImage = false, // Sizing props @@ -118,6 +120,7 @@ export const WarpLogo: React.FC = memo(function WarpLogoImpl({ u_outerVisibility: outerVisibility, u_innerFill: innerFill, u_outerDistortion: outerDistortion, + u_layering: layering, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/warp-logo.ts b/packages/shaders/src/shaders/warp-logo.ts index a74172146..8469b4090 100644 --- a/packages/shaders/src/shaders/warp-logo.ts +++ b/packages/shaders/src/shaders/warp-logo.ts @@ -35,6 +35,7 @@ uniform float u_distortion; uniform float u_outerVisibility; uniform float u_innerFill; uniform float u_outerDistortion; +uniform float u_layering; ${sizingVariablesDeclaration} @@ -157,24 +158,26 @@ void main() { - uv = v_objectUV; -// uv = v_imageUV - .5; - uv *= 2.; + uv = 2.5 * v_objectUV; - float swirl = mix(0., u_distortion, u_outerDistortion) * (1. - imgAlpha) + u_distortion * edge; -// swirl *= (.2 + .8 * smoothstep(1., 0., length(.5 * uv))); + float distortion = .7 * u_distortion; + float swirl = mix(0., distortion, u_outerDistortion) * (1. - imgAlpha) + distortion * edge; -// uv = rotate(uv, 1.8); -// uv.x -= .9; - - for (int i = 1; i <= 6; i++) { + for (int i = 1; i < 5; i++) { float iFloat = float(i); - uv.x += swirl / iFloat * cos(t + iFloat * 2.1 * uv.y); + uv.x += swirl / iFloat * cos(t + iFloat * 2.5 * uv.y); uv.y += swirl / iFloat * cos(t + iFloat * 1.5 * uv.x); } float shape = exp(-1.5 * (uv.x * uv.x + uv.y * uv.y)); - shape += u_innerFill * imgAlpha * frame; - shape = clamp(0., 1., shape); + shape += mix(0., .15, u_innerFill) * imgAlpha * frame; + + uv = 3. * v_objectUV; + for (int i = 1; i < 5; i++) { + float iFloat = float(i); + uv.x -= swirl / iFloat * cos(-t + iFloat * 2.5 * uv.y); + uv.y -= swirl / iFloat * cos(t + iFloat * 1.5 * uv.x); + } + shape += .5 * u_layering * exp(-1.5 * (uv.x * uv.x + uv.y * uv.y)); float outerPower = pow(u_outerVisibility, 3.); shape *= (outerPower + (1. - outerPower) * imgAlpha); @@ -641,6 +644,7 @@ export interface WarpLogoUniforms extends ShaderSizingUniforms { u_outerVisibility: number; u_innerFill: number; u_outerDistortion: number; + u_layering: number; } export interface WarpLogoParams extends ShaderSizingParams, ShaderMotionParams { @@ -652,4 +656,5 @@ export interface WarpLogoParams extends ShaderSizingParams, ShaderMotionParams { outerVisibility?: number; innerFill?: number; outerDistortion?: number; + layering?: number; } From 110161802ed3be8307dbdc29bce435b78a7dcb07 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 24 Nov 2025 15:26:37 +0100 Subject: [PATCH 36/84] progress --- packages/shaders-react/src/shaders/warp-logo.tsx | 4 ++-- packages/shaders/src/shaders/warp-logo.ts | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/shaders-react/src/shaders/warp-logo.tsx b/packages/shaders-react/src/shaders/warp-logo.tsx index 8100f3e42..7fb9e424a 100644 --- a/packages/shaders-react/src/shaders/warp-logo.tsx +++ b/packages/shaders-react/src/shaders/warp-logo.tsx @@ -33,8 +33,8 @@ export const defaultPreset: WarpLogoPreset = { colorBack: '#c8dfd900', colorInner: '#c8dfd9', colors: ['#00000000', '#21abca', '#809bff', '#ff00bf', '#fbff00'], - outerVisibility: 0.4, - distortion: 0.7, + outerVisibility: 0.5, + distortion: 0.9, innerFill: 0, outerDistortion: 0.85, layering: 0, diff --git a/packages/shaders/src/shaders/warp-logo.ts b/packages/shaders/src/shaders/warp-logo.ts index 8469b4090..8984155eb 100644 --- a/packages/shaders/src/shaders/warp-logo.ts +++ b/packages/shaders/src/shaders/warp-logo.ts @@ -163,6 +163,9 @@ void main() { float distortion = .7 * u_distortion; float swirl = mix(0., distortion, u_outerDistortion) * (1. - imgAlpha) + distortion * edge; + uv.y += u_layering * (1. - smoothstep(0., 1., length(.6 * uv))); + uv.y -= u_layering * .4; + for (int i = 1; i < 5; i++) { float iFloat = float(i); uv.x += swirl / iFloat * cos(t + iFloat * 2.5 * uv.y); @@ -170,14 +173,6 @@ void main() { } float shape = exp(-1.5 * (uv.x * uv.x + uv.y * uv.y)); shape += mix(0., .15, u_innerFill) * imgAlpha * frame; - - uv = 3. * v_objectUV; - for (int i = 1; i < 5; i++) { - float iFloat = float(i); - uv.x -= swirl / iFloat * cos(-t + iFloat * 2.5 * uv.y); - uv.y -= swirl / iFloat * cos(t + iFloat * 1.5 * uv.x); - } - shape += .5 * u_layering * exp(-1.5 * (uv.x * uv.x + uv.y * uv.y)); float outerPower = pow(u_outerVisibility, 3.); shape *= (outerPower + (1. - outerPower) * imgAlpha); @@ -200,6 +195,8 @@ void main() { vec3 color = gradient.rgb; float opacity = gradient.a; + +// color.r = smoothstep(.7, 1., length((v_imageUV - .5))); fragColor = vec4(color, opacity); } From 75ccfbbd53fd7b7ed3e3aec8cae8695a9ff2c63f Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 24 Nov 2025 15:58:42 +0100 Subject: [PATCH 37/84] clean-up --- docs/src/app/(shaders)/warp-logo/page.tsx | 4 +- .../shaders-react/src/shaders/warp-logo.tsx | 10 +---- packages/shaders/src/shaders/warp-logo.ts | 40 ++++++++++--------- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/docs/src/app/(shaders)/warp-logo/page.tsx b/docs/src/app/(shaders)/warp-logo/page.tsx index 94fb7e759..00402d1db 100644 --- a/docs/src/app/(shaders)/warp-logo/page.tsx +++ b/docs/src/app/(shaders)/warp-logo/page.tsx @@ -83,15 +83,13 @@ const WarpLogoWithControls = () => { const [params, setParams] = useControls(() => { return { - // colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - // colorInner: { value: toHsla(defaults.colorInner), order: 101 }, + colorBack: { value: toHsla(defaults.colorBack), order: 100 }, distortion: { value: defaults.distortion, min: 0, max: 1, order: 201 }, outerVisibility: { value: defaults.outerVisibility, min: 0, max: 1, order: 203 }, outerDistortion: { value: defaults.outerDistortion, min: 0, max: 1, order: 203 }, speed: { value: defaults.speed, min: 0, max: 4, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, innerFill: { value: defaults.innerFill, min: 0, max: 1, order: 302 }, - layering: { value: defaults.layering, min: 0, max: 1, order: 302 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, diff --git a/packages/shaders-react/src/shaders/warp-logo.tsx b/packages/shaders-react/src/shaders/warp-logo.tsx index 7fb9e424a..c61494ad4 100644 --- a/packages/shaders-react/src/shaders/warp-logo.tsx +++ b/packages/shaders-react/src/shaders/warp-logo.tsx @@ -30,14 +30,12 @@ export const defaultPreset: WarpLogoPreset = { scale: 0.65, speed: 1, frame: 0, - colorBack: '#c8dfd900', - colorInner: '#c8dfd9', - colors: ['#00000000', '#21abca', '#809bff', '#ff00bf', '#fbff00'], + colorBack: '#00000000', + colors: ['#000000', '#ffffff', '#ffffff'], outerVisibility: 0.5, distortion: 0.9, innerFill: 0, outerDistortion: 0.85, - layering: 0, }, }; export const warpLogoPresets: WarpLogoPreset[] = [defaultPreset]; @@ -45,7 +43,6 @@ export const warpLogoPresets: WarpLogoPreset[] = [defaultPreset]; export const WarpLogo: React.FC = memo(function WarpLogoImpl({ // Own props colorBack = defaultPreset.params.colorBack, - colorInner = defaultPreset.params.colorInner, colors = defaultPreset.params.colors, speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, @@ -54,7 +51,6 @@ export const WarpLogo: React.FC = memo(function WarpLogoImpl({ outerVisibility = defaultPreset.params.outerVisibility, innerFill = defaultPreset.params.innerFill, outerDistortion = defaultPreset.params.outerDistortion, - layering = defaultPreset.params.layering, suspendWhenProcessingImage = false, // Sizing props @@ -114,13 +110,11 @@ export const WarpLogo: React.FC = memo(function WarpLogoImpl({ u_colors: colors.map(getShaderColorFromString), u_colorsCount: colors.length, u_colorBack: getShaderColorFromString(colorBack), - u_colorInner: getShaderColorFromString(colorInner), u_image: processedImage, u_distortion: distortion, u_outerVisibility: outerVisibility, u_innerFill: innerFill, u_outerDistortion: outerDistortion, - u_layering: layering, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/warp-logo.ts b/packages/shaders/src/shaders/warp-logo.ts index 8984155eb..652463a21 100644 --- a/packages/shaders/src/shaders/warp-logo.ts +++ b/packages/shaders/src/shaders/warp-logo.ts @@ -30,12 +30,10 @@ uniform float u_time; uniform vec4 u_colors[${warpLogoMeta.maxColorCount}]; uniform float u_colorsCount; uniform vec4 u_colorBack; -uniform vec4 u_colorInner; uniform float u_distortion; uniform float u_outerVisibility; uniform float u_innerFill; uniform float u_outerDistortion; -uniform float u_layering; ${sizingVariablesDeclaration} @@ -163,8 +161,9 @@ void main() { float distortion = .7 * u_distortion; float swirl = mix(0., distortion, u_outerDistortion) * (1. - imgAlpha) + distortion * edge; - uv.y += u_layering * (1. - smoothstep(0., 1., length(.6 * uv))); - uv.y -= u_layering * .4; + float midShift = distortion; + uv.y += midShift * (1. - smoothstep(0., 1., length(.4 * uv))); + uv.y -= .4 * midShift; for (int i = 1; i < 5; i++) { float iFloat = float(i); @@ -177,27 +176,34 @@ void main() { float outerPower = pow(u_outerVisibility, 3.); shape *= (outerPower + (1. - outerPower) * imgAlpha); - float mixer = shape * (u_colorsCount - 1.); + float mixer = shape * u_colorsCount; vec4 gradient = u_colors[0]; gradient.rgb *= gradient.a; - float aa = fwidth(shape); - for (int i = 1; i < ${ warpLogoMeta.maxColorCount }; i++) { - if (i >= int(u_colorsCount)) break; + + float outerShape = 0.; + for (int i = 1; i < ${ warpLogoMeta.maxColorCount + 1 }; i++) { + if (i > int(u_colorsCount)) break; + float m = clamp(mixer - float(i - 1), 0., 1.); + float aa = fwidth(m); + m = smoothstep(0., 1., m); - float localMixerStart = floor(m); - float smoothed = smoothstep(0., localMixerStart, m); + if (i == 1) { + outerShape = m; + } - vec4 c = u_colors[i]; + vec4 c = u_colors[i - 1]; c.rgb *= c.a; gradient = mix(gradient, c, m); } - vec3 color = gradient.rgb; - float opacity = gradient.a; - -// color.r = smoothstep(.7, 1., length((v_imageUV - .5))); + vec3 color = gradient.rgb * outerShape; + float opacity = gradient.a * outerShape; + vec3 bgColor = u_colorBack.rgb * u_colorBack.a; + color = color + bgColor * (1.0 - opacity); + opacity = opacity + u_colorBack.a * (1.0 - opacity); + fragColor = vec4(color, opacity); } `; @@ -633,7 +639,6 @@ function solvePoissonSparse( export interface WarpLogoUniforms extends ShaderSizingUniforms { u_colorBack: [number, number, number, number]; - u_colorInner: [number, number, number, number]; u_colors: vec4[]; u_colorsCount: number; u_image: HTMLImageElement | string | undefined; @@ -641,17 +646,14 @@ export interface WarpLogoUniforms extends ShaderSizingUniforms { u_outerVisibility: number; u_innerFill: number; u_outerDistortion: number; - u_layering: number; } export interface WarpLogoParams extends ShaderSizingParams, ShaderMotionParams { colors?: string[]; colorBack?: string; - colorInner?: string; image?: HTMLImageElement | string | undefined; distortion?: number; outerVisibility?: number; innerFill?: number; outerDistortion?: number; - layering?: number; } From 543d1be574ff6a975407aa60db91bad2baaae9f6 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 24 Nov 2025 16:30:08 +0100 Subject: [PATCH 38/84] polishing --- packages/shaders-react/src/shaders/warp-logo.tsx | 2 +- packages/shaders/src/shaders/warp-logo.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/shaders-react/src/shaders/warp-logo.tsx b/packages/shaders-react/src/shaders/warp-logo.tsx index c61494ad4..ce45e09a6 100644 --- a/packages/shaders-react/src/shaders/warp-logo.tsx +++ b/packages/shaders-react/src/shaders/warp-logo.tsx @@ -31,7 +31,7 @@ export const defaultPreset: WarpLogoPreset = { speed: 1, frame: 0, colorBack: '#00000000', - colors: ['#000000', '#ffffff', '#ffffff'], + colors: ['#b3d5cc', '#ffda75', '#ff8a9b', '#4058a5'], outerVisibility: 0.5, distortion: 0.9, innerFill: 0, diff --git a/packages/shaders/src/shaders/warp-logo.ts b/packages/shaders/src/shaders/warp-logo.ts index 652463a21..8398b5be1 100644 --- a/packages/shaders/src/shaders/warp-logo.ts +++ b/packages/shaders/src/shaders/warp-logo.ts @@ -176,6 +176,7 @@ void main() { float outerPower = pow(u_outerVisibility, 3.); shape *= (outerPower + (1. - outerPower) * imgAlpha); + shape = pow(shape, .75); float mixer = shape * u_colorsCount; vec4 gradient = u_colors[0]; gradient.rgb *= gradient.a; From c8c1b87e812da435bf9a18e71b6581b5f1ae6a53 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 24 Nov 2025 23:15:49 +0100 Subject: [PATCH 39/84] logos update --- docs/public/images/logos/brave.svg | 29 ++++--------------- docs/public/images/logos/capy.svg | 12 ++++---- docs/public/images/logos/contra.svg | 2 +- docs/public/images/logos/infinite.svg | 16 +++++++++- docs/public/images/logos/mymind.svg | 28 +++++++----------- docs/public/images/logos/resend.svg | 8 +++-- docs/public/images/logos/wealth-simple.svg | 8 +++-- docs/src/app/(shaders)/warp-logo/page.tsx | 17 +++++------ .../shaders-react/src/shaders/warp-logo.tsx | 2 +- 9 files changed, 56 insertions(+), 66 deletions(-) diff --git a/docs/public/images/logos/brave.svg b/docs/public/images/logos/brave.svg index 32e1dc865..6b8e23a12 100644 --- a/docs/public/images/logos/brave.svg +++ b/docs/public/images/logos/brave.svg @@ -1,24 +1,7 @@ - - - - - - - - - - - - - - - - \ No newline at end of file + + diff --git a/docs/public/images/logos/capy.svg b/docs/public/images/logos/capy.svg index 448533962..1b50b075a 100644 --- a/docs/public/images/logos/capy.svg +++ b/docs/public/images/logos/capy.svg @@ -1,7 +1,5 @@ - - - - - - - + + + + \ No newline at end of file diff --git a/docs/public/images/logos/contra.svg b/docs/public/images/logos/contra.svg index 2d0c375db..818a4d6d5 100644 --- a/docs/public/images/logos/contra.svg +++ b/docs/public/images/logos/contra.svg @@ -1,4 +1,4 @@ - + diff --git a/docs/public/images/logos/infinite.svg b/docs/public/images/logos/infinite.svg index 465e8f0f8..431968995 100644 --- a/docs/public/images/logos/infinite.svg +++ b/docs/public/images/logos/infinite.svg @@ -1 +1,15 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/docs/public/images/logos/mymind.svg b/docs/public/images/logos/mymind.svg index 0b965b079..a102c9dec 100644 --- a/docs/public/images/logos/mymind.svg +++ b/docs/public/images/logos/mymind.svg @@ -1,19 +1,11 @@ - - - - - - - - - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/docs/public/images/logos/resend.svg b/docs/public/images/logos/resend.svg index 67af6bf6d..ed9960bf5 100644 --- a/docs/public/images/logos/resend.svg +++ b/docs/public/images/logos/resend.svg @@ -1,3 +1,5 @@ - - - + + + + \ No newline at end of file diff --git a/docs/public/images/logos/wealth-simple.svg b/docs/public/images/logos/wealth-simple.svg index e9b99985a..1108cc22a 100644 --- a/docs/public/images/logos/wealth-simple.svg +++ b/docs/public/images/logos/wealth-simple.svg @@ -1,3 +1,5 @@ - - - + + + + \ No newline at end of file diff --git a/docs/src/app/(shaders)/warp-logo/page.tsx b/docs/src/app/(shaders)/warp-logo/page.tsx index 00402d1db..09ba27fc1 100644 --- a/docs/src/app/(shaders)/warp-logo/page.tsx +++ b/docs/src/app/(shaders)/warp-logo/page.tsx @@ -22,11 +22,11 @@ import { useColors } from "@/helpers/use-colors"; const { worldWidth, worldHeight, ...defaults } = warpLogoPresets[0].params; const imageFiles = [ + 'contra.svg', 'paradigm.svg', 'paper-logo-only.svg', - 'brave2.png', + 'brave.svg', 'capy.svg', - 'contra.svg', 'infinite.svg', 'linear.svg', 'mercury.svg', @@ -35,27 +35,26 @@ const imageFiles = [ 'resend.svg', 'shopify.svg', 'wealth-simple.svg', - 'diamond.svg', + 'vercel.svg', // 'chanel.svg', // 'cibc.svg', // 'cloudflare.svg', // 'apple.svg', - // 'paper-logo-only.svg', - 'discord.svg', - // 'paper-logo-only', + // 'discord.svg', // 'enterprise-rent.svg', // 'kfc.svg', // 'microsoft.svg', // 'nasa.svg', // 'netflix.svg', - 'nike.svg', + // 'nike.svg', // 'perkins.svg', // 'pizza-hut.svg', // 'remix.svg', // 'rogers.svg', - 'vercel.svg', - 'volkswagen.svg', + // 'volkswagen.svg', + + 'diamond.svg', ] as const; const WarpLogoWithControls = () => { diff --git a/packages/shaders-react/src/shaders/warp-logo.tsx b/packages/shaders-react/src/shaders/warp-logo.tsx index ce45e09a6..11296977c 100644 --- a/packages/shaders-react/src/shaders/warp-logo.tsx +++ b/packages/shaders-react/src/shaders/warp-logo.tsx @@ -31,7 +31,7 @@ export const defaultPreset: WarpLogoPreset = { speed: 1, frame: 0, colorBack: '#00000000', - colors: ['#b3d5cc', '#ffda75', '#ff8a9b', '#4058a5'], + colors: ['#b3d5cc', '#ffe9ad', '#ff8a9b', '#4058a5'], outerVisibility: 0.5, distortion: 0.9, innerFill: 0, From 78e776dd5eec7354ee1894a5180afcc4a98b6804 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 24 Nov 2025 23:19:47 +0100 Subject: [PATCH 40/84] new preset --- .../shaders-react/src/shaders/warp-logo.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/shaders-react/src/shaders/warp-logo.tsx b/packages/shaders-react/src/shaders/warp-logo.tsx index 11296977c..9e4f42c51 100644 --- a/packages/shaders-react/src/shaders/warp-logo.tsx +++ b/packages/shaders-react/src/shaders/warp-logo.tsx @@ -38,7 +38,24 @@ export const defaultPreset: WarpLogoPreset = { outerDistortion: 0.85, }, }; -export const warpLogoPresets: WarpLogoPreset[] = [defaultPreset]; + +export const blackPreset: WarpLogoPreset = { + name: 'Black', + params: { + ...defaultObjectSizing, + scale: 0.65, + speed: 1, + frame: 0, + colorBack: '#00000000', + colors: ['#000000', '#ffffff'], + outerVisibility: 0.6, + distortion: 1, + innerFill: 0, + outerDistortion: 0.8, + }, +}; + +export const warpLogoPresets: WarpLogoPreset[] = [defaultPreset, blackPreset]; export const WarpLogo: React.FC = memo(function WarpLogoImpl({ // Own props From 4ba32cde8e7a97359e1b601ce0a0849c4463b7c3 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 24 Nov 2025 23:24:09 +0100 Subject: [PATCH 41/84] text replacements --- docs/src/app/(shaders)/warp-logo/layout.tsx | 2 +- docs/src/shader-defs/warp-logo-def.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/app/(shaders)/warp-logo/layout.tsx b/docs/src/app/(shaders)/warp-logo/layout.tsx index f0964a3f1..813c2cabb 100644 --- a/docs/src/app/(shaders)/warp-logo/layout.tsx +++ b/docs/src/app/(shaders)/warp-logo/layout.tsx @@ -1,7 +1,7 @@ import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'Mesh Gradient Logo Filter • Paper', + title: 'TBD Logo Filter • Paper', }; export default function Layout({ children }: { children: React.ReactNode }) { diff --git a/docs/src/shader-defs/warp-logo-def.ts b/docs/src/shader-defs/warp-logo-def.ts index 2691b4567..a229b7aa2 100644 --- a/docs/src/shader-defs/warp-logo-def.ts +++ b/docs/src/shader-defs/warp-logo-def.ts @@ -5,8 +5,8 @@ import { animatedCommonParams } from './common-param-def'; const defaultParams = warpLogoPresets[0].params; export const warpLogoDef: ShaderDef = { - name: 'Liquid Metal', - description: 'Futuristic liquid metal material applied to uploaded logo or abstract shape.', + name: 'TBD', + description: 'TBD', params: [ { name: 'image', From d01314144becc6d1bae27d098fa22431141796fc Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 24 Nov 2025 23:58:26 +0100 Subject: [PATCH 42/84] allowing more distortion --- packages/shaders-react/src/shaders/warp-logo.tsx | 6 +++--- packages/shaders/src/shaders/warp-logo.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/shaders-react/src/shaders/warp-logo.tsx b/packages/shaders-react/src/shaders/warp-logo.tsx index 9e4f42c51..f5636b7c7 100644 --- a/packages/shaders-react/src/shaders/warp-logo.tsx +++ b/packages/shaders-react/src/shaders/warp-logo.tsx @@ -33,9 +33,9 @@ export const defaultPreset: WarpLogoPreset = { colorBack: '#00000000', colors: ['#b3d5cc', '#ffe9ad', '#ff8a9b', '#4058a5'], outerVisibility: 0.5, - distortion: 0.9, + distortion: 0.8, innerFill: 0, - outerDistortion: 0.85, + outerDistortion: 0.7, }, }; @@ -49,7 +49,7 @@ export const blackPreset: WarpLogoPreset = { colorBack: '#00000000', colors: ['#000000', '#ffffff'], outerVisibility: 0.6, - distortion: 1, + distortion: 0.9, innerFill: 0, outerDistortion: 0.8, }, diff --git a/packages/shaders/src/shaders/warp-logo.ts b/packages/shaders/src/shaders/warp-logo.ts index 8398b5be1..9c603cad1 100644 --- a/packages/shaders/src/shaders/warp-logo.ts +++ b/packages/shaders/src/shaders/warp-logo.ts @@ -158,7 +158,7 @@ void main() { uv = 2.5 * v_objectUV; - float distortion = .7 * u_distortion; + float distortion = .9 * u_distortion; float swirl = mix(0., distortion, u_outerDistortion) * (1. - imgAlpha) + distortion * edge; float midShift = distortion; From 6d261684ff1f70c815ff002472f9a152546a247d Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 25 Nov 2025 11:31:36 +0100 Subject: [PATCH 43/84] experimenting with 3d --- packages/shaders-react/src/shaders/folds.tsx | 6 +- packages/shaders/src/shaders/folds.ts | 232 +++++++++++++------ 2 files changed, 158 insertions(+), 80 deletions(-) diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index 7f66212fd..024893674 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -28,18 +28,18 @@ export const defaultPreset: FoldsPreset = { params: { ...defaultObjectSizing, scale: 0.8, - speed: 0.2, + speed: 1, frame: 0, colorBack: '#ffffff00', colorInner: '#000000', - colors: ['#ffed47', '#ffed47', '#31fcb8', '#ffffff', '#ff006a'], + colors: ['#000000', '#ffffff'], stripeWidth: 0.65, alphaMask: true, noiseScale: 1, size: 10, shift: 0.5, noise: 0.5, - outerNoise: 0, + outerNoise: 0.5, softness: 0, gradient: 1, angle: 220, diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 883447ee8..20a14b0fa 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -161,6 +161,36 @@ vec2 getPosition(int i, float t) { return .5 + .5 * vec2(x, y); } + + +float getHeight(vec2 uv) { + float a = texture(u_image, uv).r; + return a; +} + +vec3 computeNormal(vec2 uv) { + vec2 uTexelSize = vec2(1. / 100.); + float hC = getHeight(uv); + float hR = getHeight(uv + vec2(uTexelSize.x, 0.0)); + float hL = getHeight(uv - vec2(uTexelSize.x, 0.0)); + float hU = getHeight(uv + vec2(0.0, uTexelSize.y)); + float hD = getHeight(uv - vec2(0.0, uTexelSize.y)); + + float dX = (hR - hL) * 1.; + float dY = (hU - hD) * 1.; + + return normalize(vec3(-dX, -dY, 1.0)); +} + + +float getPoint(vec2 dist, float p) { + float v = pow(1. - clamp(0., 1., length(dist)), 1.); + v = smoothstep(0., 1., v); + v = pow(v, p); + return v; +} + + void main() { const float firstFrameOffset = 2.8; @@ -180,96 +210,144 @@ void main() { imgAlpha *= frame; edge *= frame; - vec2 patternsUV = v_objectUV; - vec2 stripesUV = v_objectUV; - float angle = -u_angle * PI / 180.; - stripesUV = rotate(stripesUV, angle); - stripesUV *= u_size; + vec2 patternsUV = v_objectUV; + vec2 stripesUV = v_objectUV; + float angle = -u_angle * PI / 180.; + stripesUV = rotate(stripesUV, angle); + stripesUV *= u_size; + + float n = doubleSNoise(u_noiseScale * patternsUV + 100., u_time); + float edgeAtten = edge + u_outerNoise * (1. - edge); + float y = stripesUV.y + edgeAtten * .5 * n * u_size * u_noise; + +// float w = u_stripeWidth * edge; + float w = 0.; + y += 2. * sign(u_shift) * mix(0., w, abs(u_shift)); + + float stripeId = floor(y); + float fy = fract(y); + + float stripeMap = abs(fy - .5); + float aa = fwidth(y); + + w = clamp(w, aa, .5 - aa); + + float lMin = w - aa; + float lMax = w + aa; + float line = 1. - sst(lMin, lMax, stripeMap); +// +// if (u_alphaMask == true) { +// line *= imgAlpha; +// } + + // float softness = mix(0., u_softness, sst(0., aa, w)); + // line -= sst(softness, 0., 1. - fy); + // line = clamp(line, 0., 1.); + // + // int colorIdx = int(posMod(stripeId, u_colorsCount)); + // vec4 orderedStripeColor = u_colors[0]; + // for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { + // if (i >= int(u_colorsCount)) break; + // float isHit = 1.0 - step(.5, abs(float(i - colorIdx))); + // orderedStripeColor = mix(orderedStripeColor, u_colors[i], isHit); + // } + // + // vec3 gradientStripeColor = vec3(0.); + // float gradientOpacity = 0.; + // { + // float totalWeight = 0.; + // for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { + // if (i >= int(u_colorsCount)) break; + // vec2 pos = getPosition(i, 1.5 * t); + // vec3 colorFraction = u_colors[i].rgb * u_colors[i].a; + // float opacityFraction = u_colors[i].a; + // float dist = .5 * length(patternsUV + .5 - pos); + // dist = pow(dist, 3.5); + // float weight = 1. / (dist + 1e-3); + // gradientStripeColor += colorFraction * weight; + // gradientOpacity += opacityFraction * weight; + // totalWeight += weight; + // } + // gradientStripeColor /= max(1e-4, totalWeight); + // gradientOpacity /= max(1e-4, totalWeight); + // } + // + // vec4 stripeColor = vec4(gradientStripeColor, gradientOpacity); + // stripeColor = mix(orderedStripeColor, mix(stripeColor, orderedStripeColor, fy), u_gradient); + + // vec3 stripePremulRGB = stripeColor.rgb * stripeColor.a; + // stripePremulRGB *= line; + // float stripeA = stripeColor.a * line; + // + // vec3 color = stripePremulRGB; + // float opacity = stripeA; + // + // vec3 backRgb = u_colorBack.rgb * u_colorBack.a; + // float backA = u_colorBack.a; + // vec3 innerRgb = u_colorInner.rgb * u_colorInner.a; + // float innerA = u_colorInner.a; + // + // innerRgb *= imgAlpha; + // innerA *= imgAlpha; + // + // vec3 underlayerRgb = innerRgb + backRgb * (1. - innerA); + // float underlayerA = innerA + backA * (1. - innerA); + // + // color *= line; + // opacity *= line; + // + // color = color + underlayerRgb * (1. - opacity); + // opacity = opacity + underlayerA * (1. - opacity); + // + // fragColor = vec4(color, opacity); - float n = doubleSNoise(u_noiseScale * patternsUV + 100., u_time); - float edgeAtten = edge + u_outerNoise * (1. - edge); - float y = stripesUV.y + edgeAtten * .5 * n * u_size * u_noise; + + vec3 uLightDir = vec3(.5 * sin(6. * t), .5 * cos(6. * t), 1.); + vec3 normal = computeNormal(uv); + vec3 viewDir = vec3(0., 0., 1.0); - float w = u_stripeWidth * edge; - y += 2. * sign(u_shift) * mix(0., w, abs(u_shift)); + vec3 halfDir = normalize(uLightDir + viewDir); + float NdotH = max(dot(normal, halfDir), 0.); - float stripeId = floor(y); - float fy = fract(y); - float stripeMap = abs(fy - .5); - float aa = fwidth(y); - w = clamp(w, aa, .5 - aa); - - float lMin = w - aa; - float lMax = w + aa; - float line = 1. - sst(lMin, lMax, stripeMap); - if (u_alphaMask == true) { - line *= imgAlpha; - } +// float shaping = .2 * edge;// * step(0., yTravel) + (.4 - .2 * edge) * step(yTravel, 0.); - float softness = mix(0., u_softness, sst(0., aa, w)); - line -= sst(softness, 0., 1. - fy); - line = clamp(line, 0., 1.); - int colorIdx = int(posMod(stripeId, u_colorsCount)); - vec4 orderedStripeColor = u_colors[0]; - for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { - if (i >= int(u_colorsCount)) break; - float isHit = 1.0 - step(.5, abs(float(i - colorIdx))); - orderedStripeColor = mix(orderedStripeColor, u_colors[i], isHit); - } - vec3 gradientStripeColor = vec3(0.); - float gradientOpacity = 0.; - { - float totalWeight = 0.; - for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { - if (i >= int(u_colorsCount)) break; - vec2 pos = getPosition(i, 1.5 * t); - vec3 colorFraction = u_colors[i].rgb * u_colors[i].a; - float opacityFraction = u_colors[i].a; - float dist = .5 * length(patternsUV + .5 - pos); - dist = pow(dist, 3.5); - float weight = 1. / (dist + 1e-3); - gradientStripeColor += colorFraction * weight; - gradientOpacity += opacityFraction * weight; - totalWeight += weight; - } - gradientStripeColor /= max(1e-4, totalWeight); - gradientOpacity /= max(1e-4, totalWeight); + + t = 2. * u_time; + float f[${ foldsMeta.maxColorCount }]; + +// float yTravel = mix(2.5, -1.5, fract(.1 * t)); + float yTravel = mix(2., -.2, fract(.1 * t)); + +// float shaping = .2 * edge * step(0., yTravel) + .2 * step(yTravel, 0.); + float shaping = .05 * edge; + + + vec2 trajs[${ foldsMeta.maxColorCount }]; + // trajs[0] = vec2(.0, 3. - fract(.1 * t)); + trajs[0] = vec2(-.5, -.5 + yTravel); + float dist = 1.; + for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { + f[i] = getPoint(uv + trajs[i], shaping); } - - vec4 stripeColor = vec4(gradientStripeColor, gradientOpacity); - stripeColor = mix(orderedStripeColor, mix(stripeColor, orderedStripeColor, fy), u_gradient); - - vec3 stripePremulRGB = stripeColor.rgb * stripeColor.a; - stripePremulRGB *= line; - float stripeA = stripeColor.a * line; - - vec3 color = stripePremulRGB; - float opacity = stripeA; - - vec3 backRgb = u_colorBack.rgb * u_colorBack.a; - float backA = u_colorBack.a; - vec3 innerRgb = u_colorInner.rgb * u_colorInner.a; - float innerA = u_colorInner.a; - innerRgb *= imgAlpha; - innerA *= imgAlpha; + vec3 color = vec3(1.); + f[0] = sst(.9, .9 + 2. * fwidth(f[0]), f[0]); +// color = mix(vec3(1.), vec3(.8, .7, 0.), f[0]); - vec3 underlayerRgb = innerRgb + backRgb * (1. - innerA); - float underlayerA = innerA + backA * (1. - innerA); +// vec3 color = mix(u_colors[0].rgb, u_colors[1].rgb, NdotH); - color *= line; - opacity *= line; - - color = color + underlayerRgb * (1. - opacity); - opacity = opacity + underlayerA * (1. - opacity); - - fragColor = vec4(color, opacity); -// fragColor = vec4(img.rgb, 1.); + float shadow = 1. - NdotH; + shadow = -.4 + pow(shadow, .2); + color = mix(color, vec3(0.), shadow); + color = mix(color, vec3(0.), imgAlpha - pow(edge, .05)); +// color = mix(color, vec3(0.), line * f[0]); + + fragColor = vec4(color, imgAlpha); } `; From be3c3c041e3b83a7d12a7ebc005686d2f9b4af91 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 25 Nov 2025 15:19:48 +0100 Subject: [PATCH 44/84] blobs to voronoi --- docs/src/app/(shaders)/folds/page.tsx | 20 +- .../shaders-react/src/shaders/blobs-logo.tsx | 4 +- packages/shaders/src/shaders/blobs-logo.ts | 307 +++++++++++++----- packages/shaders/src/shaders/folds.ts | 100 ++++-- 4 files changed, 318 insertions(+), 113 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index 661e34576..6a54cfa1f 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -22,42 +22,42 @@ import { useColors } from "@/helpers/use-colors"; const { worldWidth, worldHeight, ...defaults } = foldsPresets[0].params; const imageFiles = [ + 'contra.svg', 'paradigm.svg', 'paper-logo-only.svg', - 'brave2.png', + 'brave.svg', 'capy.svg', - 'contra.svg', 'infinite.svg', 'linear.svg', 'mercury.svg', 'mymind.svg', - 'inbound.svg', + // 'inbound.svg', 'resend.svg', 'shopify.svg', 'wealth-simple.svg', - 'diamond.svg', + 'vercel.svg', // 'chanel.svg', // 'cibc.svg', // 'cloudflare.svg', // 'apple.svg', - // 'paper-logo-only.svg', - 'discord.svg', - // 'paper-logo-only', + // 'discord.svg', // 'enterprise-rent.svg', // 'kfc.svg', // 'microsoft.svg', // 'nasa.svg', // 'netflix.svg', - 'nike.svg', + // 'nike.svg', // 'perkins.svg', // 'pizza-hut.svg', // 'remix.svg', // 'rogers.svg', - 'vercel.svg', - 'volkswagen.svg', + // 'volkswagen.svg', + + 'diamond.svg', ] as const; + const FoldsWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); const [image, setImage] = useState('/images/logos/diamond.svg'); diff --git a/packages/shaders-react/src/shaders/blobs-logo.tsx b/packages/shaders-react/src/shaders/blobs-logo.tsx index 68f66ade9..d48409027 100644 --- a/packages/shaders-react/src/shaders/blobs-logo.tsx +++ b/packages/shaders-react/src/shaders/blobs-logo.tsx @@ -10,6 +10,7 @@ import { toProcessedBlobsLogo, type ImageShaderPreset, getShaderColorFromString, + getShaderNoiseTexture, } from '@paper-design/shaders'; import { transparentPixel } from '../transparent-pixel.js'; import { suspend } from '../suspend.js'; @@ -112,6 +113,7 @@ export const BlobsLogo: React.FC = memo(function BlobsLogoImpl({ u_image: processedImage, u_contour: contour, u_size: size, + u_noiseTexture: getShaderNoiseTexture(), // Sizing uniforms u_fit: ShaderFitOptions[fit], @@ -131,7 +133,7 @@ export const BlobsLogo: React.FC = memo(function BlobsLogoImpl({ speed={speed} frame={frame} fragmentShader={blobsLogoFragmentShader} - mipmaps={['u_image']} + // mipmaps={['u_image']} uniforms={uniforms} /> ); diff --git a/packages/shaders/src/shaders/blobs-logo.ts b/packages/shaders/src/shaders/blobs-logo.ts index 186f65633..5ed24ee80 100644 --- a/packages/shaders/src/shaders/blobs-logo.ts +++ b/packages/shaders/src/shaders/blobs-logo.ts @@ -1,7 +1,7 @@ import type { vec4 } from '../types.js'; import type { ShaderMotionParams } from '../shader-mount.js'; import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, rotation2, proceduralHash21, colorBandingFix } from '../shader-utils.js'; +import { declarePI, rotation2, textureRandomizerGB, proceduralHash22, colorBandingFix } from '../shader-utils.js'; export const blobsLogoMeta = { maxColorCount: 6, @@ -24,6 +24,8 @@ precision mediump float; uniform sampler2D u_image; uniform float u_imageAspectRatio; +uniform sampler2D u_noiseTexture; + uniform vec2 u_resolution; uniform float u_time; @@ -137,94 +139,246 @@ float getPoint(vec2 dist, float p) { } -void main() { - - float t = .3 * u_time; - - vec2 uv = v_imageUV; - vec2 dudx = dFdx(v_imageUV); - vec2 dudy = dFdy(v_imageUV); - vec4 img = textureGrad(u_image, uv, dudx, dudy); +${ declarePI } +${ textureRandomizerGB } + +vec4 voronoi(vec2 x, float t, float shape, float imgAlpha) { + vec2 ip = floor(x); + vec2 fp = fract(x); + + vec2 mg, mr; + float md = 8.; + float rand = 0.; + float u_distortion = .5 * shape; + + for (int j = -1; j <= 1; j++) { + for (int i = -1; i <= 1; i++) { + vec2 g = vec2(float(i), float(j)); + vec2 o = randomGB(ip + g); + float raw_hash = o.x; + o = .5 + u_distortion * sin(t + TWO_PI * o); + vec2 r = g + o - fp; + float d = dot(r, r); + + if (d < md) { + md = d; + mr = r; + mg = g; + rand = raw_hash; + } + } + } - float edge = img.r; - edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); + md = 8.; + for (int j = -2; j <= 2; j++) { + for (int i = -2; i <= 2; i++) { + vec2 g = mg + vec2(float(i), float(j)); + vec2 o = randomGB(ip + g); + o = .5 + u_distortion * sin(t + TWO_PI * o); + vec2 r = g + o - fp; + if (dot(mr - r, mr - r) > .00001) { + md = min(md, dot(.5 * (mr + r), normalize(r - mr))); + } + } + } - float imgAlpha = img.g; + return vec4(md, mr, rand); +} - float frame = getImgFrame(v_imageUV, 0.); - imgAlpha *= frame; - edge *= frame; +void main() { - uv = v_objectUV; - float offset = 1. - edge; - float shadow = ((1. - edge) * imgAlpha); - shadow = pow(shadow, 5.); - - float shapeOffset = 2.4 * u_contour * offset; - float shaping = 2.5 - shapeOffset; - - t = 2. * u_time; - float f[${ blobsLogoMeta.maxColorCount }]; - - vec2 trajs[${ blobsLogoMeta.maxColorCount }]; - trajs[0] = vec2(0.8 * sin(-0.5 * t), 0.2 + 2.5 * cos(0.3 * t)); - trajs[1] = vec2(1.7 * cos(-0.5 * t + 1.), sin(0.8 * t)); - trajs[2] = vec2(0.5 * cos(0.3 * t), cos(-0.8 * t)); - trajs[3] = vec2(0.5 * cos(-0.9 * t), 0.7 * sin(-0.2 * t)); - trajs[4] = vec2(0.5 * sin(-0.34 * t), -.2 + 1.3 * sin(-0.8 * t)); - trajs[5] = vec2(0.9 * sin(0.85 * t + 1.), 0.7 * cos(0.6 * t)); - - float dist = 0.3; - for (int i = 0; i < ${ blobsLogoMeta.maxColorCount }; i++) { - dist += .03 * float(i); - f[i] = getPoint(uv + dist * trajs[i], shaping); - } - - f[0] -= f[1]; - f[2] -= 1.2 * f[1]; - f[2] -= 1.6 * f[0]; - f[4] -= f[1]; - f[5] -= .4 * f[3]; - f[1] -= .5 * f[2]; - f[5] -= f[4]; - f[3] -= f[1]; - f[3] -= f[2]; - f[5] *= .3; - - float opacity = 0.; - vec3 color = vec3(0.); - - float size = .95 - .9 * u_size; - for (int i = 0; i < ${ blobsLogoMeta.maxColorCount }; i++) { - if (i >= int(u_colorsCount)) break; - f[i] = sst(size, size + 2. * fwidth(f[i]), f[i]); - opacity += f[i]; - color = mix(color, u_colors[i].rgb, f[i]); - } + vec2 uv = v_imageUV; + vec2 dudx = dFdx(v_imageUV); + vec2 dudy = dFdy(v_imageUV); + vec4 img = textureGrad(u_image, uv, dudx, dudy); - opacity = clamp(opacity, 0., 1.); + float edge = img.r; + edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); - color *= imgAlpha; - opacity *= imgAlpha; + float imgAlpha = img.g; - vec3 backRgb = u_colorBack.rgb * u_colorBack.a; - float backA = u_colorBack.a; - vec3 innerRgb_raw = u_colorInner.rgb * u_colorInner.a; - float innerA_raw = u_colorInner.a; + vec2 shape_uv = v_patternUV; + shape_uv *= 1.25; - float innerA = innerA_raw * imgAlpha; - vec3 innerRgb = innerRgb_raw * imgAlpha; + float t = u_time; - vec3 layerRgb = innerRgb + backRgb * (1.0 - innerA); - float layerA = innerA + backA * (1.0 - innerA); + vec4 voronoiRes = voronoi(shape_uv, t, edge, imgAlpha); - vec3 colorCopy = color; - color = color + layerRgb * (1.0 - opacity); - opacity = opacity + layerA * (1.0 - opacity); + float shape = clamp(voronoiRes.w, 0., 1.); + float mixer = shape * (u_colorsCount - 1.); + mixer = (shape - .5 / u_colorsCount) * u_colorsCount; + float steps = 1.; + vec4 gradient = u_colors[0]; + gradient.rgb *= gradient.a; + for (int i = 1; i < ${ blobsLogoMeta.maxColorCount }; i++) { + if (i >= int(u_colorsCount)) break; + float localT = clamp(mixer - float(i - 1), 0.0, 1.0); + localT = round(localT * steps) / steps; + vec4 c = u_colors[i]; + c.rgb *= c.a; + gradient = mix(gradient, c, localT); + } + + if ((mixer < 0.) || (mixer > (u_colorsCount - 1.))) { + float localT = mixer + 1.; + if (mixer > (u_colorsCount - 1.)) { + localT = mixer - (u_colorsCount - 1.); + } + localT = round(localT * steps) / steps; + vec4 cFst = u_colors[0]; + cFst.rgb *= cFst.a; + vec4 cLast = u_colors[int(u_colorsCount - 1.)]; + cLast.rgb *= cLast.a; + gradient = mix(cLast, cFst, localT); + } - fragColor = vec4(color, opacity); + vec3 cellColor = gradient.rgb; + float cellOpacity = gradient.a; + float u_glow = 0.; + float u_gap = .1; + vec4 u_colorGlow = vec4(1.); + vec4 u_colorGap = vec4(1.); + + float glows = length(voronoiRes.yz * u_glow); + glows = pow(glows, 1.5); + + vec3 color = mix(cellColor, u_colorGlow.rgb * u_colorGlow.a, u_colorGlow.a * glows); + float opacity = cellOpacity + u_colorGlow.a * glows; + + float eee = 2. * voronoiRes.x; + float smoothEdge = fwidth(eee); + eee = smoothstep(u_gap - smoothEdge, u_gap + smoothEdge, eee); + + color = mix(u_colorGap.rgb * u_colorGap.a, color, eee); + opacity = mix(u_colorGap.a, opacity, eee); + + fragColor = vec4(color, imgAlpha); + + +// +// float frame = getImgFrame(v_imageUV, 0.); +// imgAlpha *= frame; +// edge *= frame; +// +// uv = v_objectUV; +// +// float offset = 1. - edge; +// float shadow = ((1. - edge) * imgAlpha); +// shadow = pow(shadow, 5.); +// +// // float shapeOffset = 2.4 * u_contour * offset; +// // float shaping = 2.5 - shapeOffset; +// // +// // float f[${ blobsLogoMeta.maxColorCount }]; +// // +// // vec2 trajs[${ blobsLogoMeta.maxColorCount }]; +// // trajs[0] = vec2(0.8 * sin(-0.5 * t), 0.2 + 2.5 * cos(0.3 * t)); +// // trajs[1] = vec2(1.7 * cos(-0.5 * t + 1.), sin(0.8 * t)); +// // trajs[2] = vec2(0.5 * cos(0.3 * t), cos(-0.8 * t)); +// // trajs[3] = vec2(0.5 * cos(-0.9 * t), 0.7 * sin(-0.2 * t)); +// // trajs[4] = vec2(0.5 * sin(-0.34 * t), -.2 + 1.3 * sin(-0.8 * t)); +// // trajs[5] = vec2(0.9 * sin(0.85 * t + 1.), 0.7 * cos(0.6 * t)); +// // +// // float dist = 0.3; +// // for (int i = 0; i < ${ blobsLogoMeta.maxColorCount }; i++) { +// // dist += .03 * float(i); +// // f[i] = getPoint(uv + dist * trajs[i], shaping); +// // } +// // +// // f[0] -= f[1]; +// // f[2] -= 1.2 * f[1]; +// // f[2] -= 1.6 * f[0]; +// // f[4] -= f[1]; +// // f[5] -= .4 * f[3]; +// // f[1] -= .5 * f[2]; +// // f[5] -= f[4]; +// // f[3] -= f[1]; +// // f[3] -= f[2]; +// // f[5] *= .3; +// // +// // float opacity = 0.; +// // vec3 color = vec3(0.); +// // +// // float size = .95 - .9 * u_size; +// // for (int i = 0; i < ${ blobsLogoMeta.maxColorCount }; i++) { +// // if (i >= int(u_colorsCount)) break; +// // f[i] = sst(size, size + 2. * fwidth(f[i]), f[i]); +// // opacity += f[i]; +// // color = mix(color, u_colors[i].rgb, f[i]); +// // } +// // +// // opacity = clamp(opacity, 0., 1.); +// // +// // color *= imgAlpha; +// // opacity *= imgAlpha; +// // +// // vec3 backRgb = u_colorBack.rgb * u_colorBack.a; +// // float backA = u_colorBack.a; +// // vec3 innerRgb_raw = u_colorInner.rgb * u_colorInner.a; +// // float innerA_raw = u_colorInner.a; +// // +// // float innerA = innerA_raw * imgAlpha; +// // vec3 innerRgb = innerRgb_raw * imgAlpha; +// // +// // vec3 layerRgb = innerRgb + backRgb * (1.0 - innerA); +// // float layerA = innerA + backA * (1.0 - innerA); +// // +// // vec3 colorCopy = color; +// // color = color + layerRgb * (1.0 - opacity); +// // opacity = opacity + layerA * (1.0 - opacity); +// +// +// // fragColor = vec4(color, opacity); +// +// vec2 shape_uv = v_patternUV; +// // shape_uv *= 1.25; +// +// vec4 voronoiRes = voronoi(shape_uv, t); +// +// float shape = clamp(voronoiRes.w, 0., 1.); +// float mixer = shape * (u_colorsCount - 1.); +// mixer = (shape - .5 / u_colorsCount) * u_colorsCount; +// +// vec4 gradient = u_colors[0]; +// gradient.rgb *= gradient.a; +// for (int i = 1; i < ${ blobsLogoMeta.maxColorCount }; i++) { +// if (i >= int(u_colorsCount)) break; +// float localT = clamp(mixer - float(i - 1), 0.0, 1.0); +// vec4 c = u_colors[i]; +// c.rgb *= c.a; +// gradient = mix(gradient, c, localT); +// } +// +// // if ((mixer < 0.) || (mixer > (u_colorsCount - 1.))) { +// // float localT = mixer + 1.; +// // if (mixer > (u_colorsCount - 1.)) { +// // localT = mixer - (u_colorsCount - 1.); +// // } +// // vec4 cFst = u_colors[0]; +// // cFst.rgb *= cFst.a; +// // vec4 cLast = u_colors[int(u_colorsCount - 1.)]; +// // cLast.rgb *= cLast.a; +// // gradient = mix(cLast, cFst, localT); +// // } +// +// vec3 cellColor = gradient.rgb; +// float cellOpacity = gradient.a; +// +// vec3 color = cellColor; +// float opacity = cellOpacity; +// +// // float u_gap = .1; +// // vec4 u_colorGap = vec4(0., 0., 0., 1.); +// // float eee = voronoiRes.x; +// // float smoothEdge = .001; +// // eee = smoothstep(u_gap - smoothEdge, u_gap + smoothEdge, eee); +// // +// // color = mix(u_colorGap.rgb * u_colorGap.a, color, eee); +// // opacity = mix(u_colorGap.a, opacity, eee); +// +// fragColor = vec4(color, opacity); } `; @@ -751,6 +905,7 @@ export interface BlobsLogoUniforms extends ShaderSizingUniforms { u_image: HTMLImageElement | string | undefined; u_contour: number; u_size: number; + u_noiseTexture?: HTMLImageElement; } export interface BlobsLogoParams extends ShaderSizingParams, ShaderMotionParams { diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 20a14b0fa..757fcb7c0 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -165,6 +165,7 @@ vec2 getPosition(int i, float t) { float getHeight(vec2 uv) { float a = texture(u_image, uv).r; +// a = pow(a, 2.); return a; } @@ -190,6 +191,16 @@ float getPoint(vec2 dist, float p) { return v; } +mat2 rotZ(float a) { + float c = cos(a), s = sin(a); + return mat2(c, -s, + s, c); +} +vec3 rotateAroundZ(vec3 v, float angle) { + mat2 r = rotZ(angle); + return vec3(r * v.xy, v.z); +} + void main() { @@ -216,7 +227,7 @@ void main() { stripesUV = rotate(stripesUV, angle); stripesUV *= u_size; - float n = doubleSNoise(u_noiseScale * patternsUV + 100., u_time); + float n = doubleSNoise(u_noiseScale * patternsUV + 100., .2 * u_time); float edgeAtten = edge + u_outerNoise * (1. - edge); float y = stripesUV.y + edgeAtten * .5 * n * u_size * u_noise; @@ -301,18 +312,54 @@ void main() { // // fragColor = vec4(color, opacity); - - vec3 uLightDir = vec3(.5 * sin(6. * t), .5 * cos(6. * t), 1.); - vec3 normal = computeNormal(uv); - vec3 viewDir = vec3(0., 0., 1.0); - vec3 halfDir = normalize(uLightDir + viewDir); - float NdotH = max(dot(normal, halfDir), 0.); - - - - -// float shaping = .2 * edge;// * step(0., yTravel) + (.4 - .2 * edge) * step(yTravel, 0.); + vec3 uLightDir1 = normalize(vec3(.5, .5, .7)); + vec3 uLightDir2 = normalize(vec3(-.5, -.5, .9)); + + + uLightDir1 = rotateAroundZ(uLightDir1, 5. * t); + uLightDir2 = rotateAroundZ(uLightDir2, -3. * t); + + vec3 normal = computeNormal(uv); + vec3 viewDir = vec3(0., 0., 1.); + + // --- material parameters --- + vec3 baseColor = vec3(1.); + vec3 specColor = vec3(.8); +// float shininess = 20.0; + + // light colors/intensities + vec3 lightColor1 = vec3(0.9, 0.2, 0.2); + vec3 lightColor2 = vec3(0.1, 0.4, 0.9); + + // --- lighting --- + float NdotL1 = max(dot(normal, uLightDir1), 0.); + float NdotL2 = max(dot(normal, uLightDir2), 0.); + + // diffuse from both lights + vec3 diffuse = + baseColor * (NdotL1 * lightColor1 + NdotL2 * lightColor2); + + // ambient (could also tint with a color if you like) + vec3 ambient = baseColor * 0.2; + + // --- specular highlights (per light) --- + vec3 halfDir1 = normalize(uLightDir1 + viewDir); + vec3 halfDir2 = normalize(uLightDir2 + viewDir); + + float NdotH1 = max(dot(normal, halfDir1), 0.); + float NdotH2 = max(dot(normal, halfDir2), 0.); + + vec3 specular = + specColor * (pow(NdotH1, 400.) * lightColor1 + + pow(NdotH2, 400.) * lightColor2); + + // --- final --- + vec3 color = ambient + diffuse + specular; + color = clamp(color, 0., 1.); + + + // float shaping = .2 * edge;// * step(0., yTravel) + (.4 - .2 * edge) * step(yTravel, 0.); @@ -334,20 +381,21 @@ void main() { for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { f[i] = getPoint(uv + trajs[i], shaping); } - - vec3 color = vec3(1.); - f[0] = sst(.9, .9 + 2. * fwidth(f[0]), f[0]); -// color = mix(vec3(1.), vec3(.8, .7, 0.), f[0]); - -// vec3 color = mix(u_colors[0].rgb, u_colors[1].rgb, NdotH); - - float shadow = 1. - NdotH; - shadow = -.4 + pow(shadow, .2); - color = mix(color, vec3(0.), shadow); - color = mix(color, vec3(0.), imgAlpha - pow(edge, .05)); -// color = mix(color, vec3(0.), line * f[0]); - - fragColor = vec4(color, imgAlpha); +// +// vec3 color = vec3(1.); +// f[0] = sst(.9, .9 + 2. * fwidth(f[0]), f[0]); +//// color = mix(vec3(1.), vec3(.8, .7, 0.), f[0]); +// +//// vec3 color = mix(u_colors[0].rgb, u_colors[1].rgb, NdotH); +// +// float shadow = 1. - NdotH; +// shadow = -.4 + pow(shadow, .2); +// color = mix(color, vec3(0.), shadow); +// color = mix(color, vec3(0.), imgAlpha - pow(edge, .05)); +// color = mix(color, vec3(0.), line); + +// fragColor = vec4(color, imgAlpha); + fragColor = vec4(vec3(NdotH1), imgAlpha); } `; From 1db44a9b85dd7a9f85f3b9fe8063e464ed83918a Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 25 Nov 2025 16:39:13 +0100 Subject: [PATCH 45/84] blobs to fbm --- packages/shaders/src/shaders/blobs-logo.ts | 247 +++++++-------------- 1 file changed, 78 insertions(+), 169 deletions(-) diff --git a/packages/shaders/src/shaders/blobs-logo.ts b/packages/shaders/src/shaders/blobs-logo.ts index 5ed24ee80..e9377e48c 100644 --- a/packages/shaders/src/shaders/blobs-logo.ts +++ b/packages/shaders/src/shaders/blobs-logo.ts @@ -1,7 +1,7 @@ import type { vec4 } from '../types.js'; import type { ShaderMotionParams } from '../shader-mount.js'; import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, rotation2, textureRandomizerGB, proceduralHash22, colorBandingFix } from '../shader-utils.js'; +import { declarePI, rotation2, textureRandomizerGB, textureRandomizerR, colorBandingFix } from '../shader-utils.js'; export const blobsLogoMeta = { maxColorCount: 6, @@ -40,7 +40,41 @@ ${sizingVariablesDeclaration} out vec4 fragColor; + ${ declarePI } +${ textureRandomizerR } +float valueNoise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + float a = randomR(i); + float b = randomR(i + vec2(1.0, 0.0)); + float c = randomR(i + vec2(0.0, 1.0)); + float d = randomR(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + float x1 = mix(a, b, u.x); + float x2 = mix(c, d, u.x); + return mix(x1, x2, u.y); +} +float fbm(vec2 n) { + float total = 0.0, amplitude = .4; + for (int i = 0; i < 8; i++) { + total += valueNoise(n) * amplitude; + n *= 1.99; + amplitude *= 0.65; + } + return total; +} + +float getNoise(vec2 uv, vec2 pUv, float t, float shape) { +// float noiseLeft = fbm(.9 * pUv, t); +// float noiseRight = fbm(mix(4., 2., shape) * pUv + t, t); + + float noise = fbm(1.2 * pUv * mix(1., 1.4, shape) - vec2(0., .1 * t)); +// noise *= fbm(.8 * pUv + vec2(0., .32 * t)); + + return noise; +} + ${ rotation2 } float getImgFrame(vec2 uv, float th) { @@ -197,188 +231,63 @@ void main() { edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); float imgAlpha = img.g; - - vec2 shape_uv = v_patternUV; - shape_uv *= 1.25; + + float frame = getImgFrame(v_imageUV, 0.); + imgAlpha *= frame; + + + vec2 shape_uv = v_objectUV; float t = u_time; - vec4 voronoiRes = voronoi(shape_uv, t, edge, imgAlpha); + float cycleDuration = 3.; + float period2 = 2.0 * cycleDuration; + float localTime1 = fract((0.1 * t + cycleDuration) / period2) * period2; + float localTime2 = fract((0.1 * t) / period2) * period2; + float timeBlend = .5 + .5 * sin(.1 * t * PI / cycleDuration - .5 * PI); + +// float atg = atan(shape_uv.y, shape_uv.x) + .001; +// float l = length(shape_uv); +// vec2 polar_uv1 = vec2(atg, localTime1 - (.5 * l) + 1. / pow(max(1e-4, l), .5)); +// polar_uv1 *= u_noiseScale; + float noise1 = getNoise(shape_uv, shape_uv, t, edge); + noise1 = 4. * edge * pow(noise1, 4.); + noise1 = 1. - clamp(noise1, 0., 1.); + + noise1 = mix(0., noise1, imgAlpha); - float shape = clamp(voronoiRes.w, 0., 1.); - float mixer = shape * (u_colorsCount - 1.); - mixer = (shape - .5 / u_colorsCount) * u_colorsCount; - float steps = 1.; +// vec2 polar_uv2 = vec2(atg, localTime2 - (.5 * l) + 1. / pow(max(1e-4, l), .5)); +// polar_uv2 *= u_noiseScale; +// float noise2 = getNoise(shape_uv, shape_uv, t); - vec4 gradient = u_colors[0]; +// float noise = mix(noise1, noise2, timeBlend); +// +// shape_uv *= (.8 + 1.2 * noise); + +// float ringShape = getRingShape(shape_uv); + float ringShape = 1.; + + float mixer = ringShape * ringShape * (u_colorsCount - 1.); + vec4 gradient = u_colors[int(u_colorsCount) - 1]; gradient.rgb *= gradient.a; - for (int i = 1; i < ${ blobsLogoMeta.maxColorCount }; i++) { - if (i >= int(u_colorsCount)) break; - float localT = clamp(mixer - float(i - 1), 0.0, 1.0); - localT = round(localT * steps) / steps; + for (int i = ${ blobsLogoMeta.maxColorCount } - 2; i >= 0; i--) { + float localT = clamp(mixer - float(int(u_colorsCount) - 1 - i - 1), 0., 1.); vec4 c = u_colors[i]; c.rgb *= c.a; gradient = mix(gradient, c, localT); } - if ((mixer < 0.) || (mixer > (u_colorsCount - 1.))) { - float localT = mixer + 1.; - if (mixer > (u_colorsCount - 1.)) { - localT = mixer - (u_colorsCount - 1.); - } - localT = round(localT * steps) / steps; - vec4 cFst = u_colors[0]; - cFst.rgb *= cFst.a; - vec4 cLast = u_colors[int(u_colorsCount - 1.)]; - cLast.rgb *= cLast.a; - gradient = mix(cLast, cFst, localT); - } - - vec3 cellColor = gradient.rgb; - float cellOpacity = gradient.a; - float u_glow = 0.; - float u_gap = .1; - vec4 u_colorGlow = vec4(1.); - vec4 u_colorGap = vec4(1.); - - float glows = length(voronoiRes.yz * u_glow); - glows = pow(glows, 1.5); - - vec3 color = mix(cellColor, u_colorGlow.rgb * u_colorGlow.a, u_colorGlow.a * glows); - float opacity = cellOpacity + u_colorGlow.a * glows; - - float eee = 2. * voronoiRes.x; - float smoothEdge = fwidth(eee); - eee = smoothstep(u_gap - smoothEdge, u_gap + smoothEdge, eee); + vec3 color = gradient.rgb * ringShape; + float opacity = gradient.a * ringShape; - color = mix(u_colorGap.rgb * u_colorGap.a, color, eee); - opacity = mix(u_colorGap.a, opacity, eee); + vec3 bgColor = u_colorBack.rgb * u_colorBack.a; + color = color + bgColor * (1. - opacity); + opacity = opacity + u_colorBack.a * (1. - opacity); - fragColor = vec4(color, imgAlpha); + ${ colorBandingFix } + // fragColor = vec4(color, opacity); + fragColor = vec4(vec3(noise1), 1.); - -// -// float frame = getImgFrame(v_imageUV, 0.); -// imgAlpha *= frame; -// edge *= frame; -// -// uv = v_objectUV; -// -// float offset = 1. - edge; -// float shadow = ((1. - edge) * imgAlpha); -// shadow = pow(shadow, 5.); -// -// // float shapeOffset = 2.4 * u_contour * offset; -// // float shaping = 2.5 - shapeOffset; -// // -// // float f[${ blobsLogoMeta.maxColorCount }]; -// // -// // vec2 trajs[${ blobsLogoMeta.maxColorCount }]; -// // trajs[0] = vec2(0.8 * sin(-0.5 * t), 0.2 + 2.5 * cos(0.3 * t)); -// // trajs[1] = vec2(1.7 * cos(-0.5 * t + 1.), sin(0.8 * t)); -// // trajs[2] = vec2(0.5 * cos(0.3 * t), cos(-0.8 * t)); -// // trajs[3] = vec2(0.5 * cos(-0.9 * t), 0.7 * sin(-0.2 * t)); -// // trajs[4] = vec2(0.5 * sin(-0.34 * t), -.2 + 1.3 * sin(-0.8 * t)); -// // trajs[5] = vec2(0.9 * sin(0.85 * t + 1.), 0.7 * cos(0.6 * t)); -// // -// // float dist = 0.3; -// // for (int i = 0; i < ${ blobsLogoMeta.maxColorCount }; i++) { -// // dist += .03 * float(i); -// // f[i] = getPoint(uv + dist * trajs[i], shaping); -// // } -// // -// // f[0] -= f[1]; -// // f[2] -= 1.2 * f[1]; -// // f[2] -= 1.6 * f[0]; -// // f[4] -= f[1]; -// // f[5] -= .4 * f[3]; -// // f[1] -= .5 * f[2]; -// // f[5] -= f[4]; -// // f[3] -= f[1]; -// // f[3] -= f[2]; -// // f[5] *= .3; -// // -// // float opacity = 0.; -// // vec3 color = vec3(0.); -// // -// // float size = .95 - .9 * u_size; -// // for (int i = 0; i < ${ blobsLogoMeta.maxColorCount }; i++) { -// // if (i >= int(u_colorsCount)) break; -// // f[i] = sst(size, size + 2. * fwidth(f[i]), f[i]); -// // opacity += f[i]; -// // color = mix(color, u_colors[i].rgb, f[i]); -// // } -// // -// // opacity = clamp(opacity, 0., 1.); -// // -// // color *= imgAlpha; -// // opacity *= imgAlpha; -// // -// // vec3 backRgb = u_colorBack.rgb * u_colorBack.a; -// // float backA = u_colorBack.a; -// // vec3 innerRgb_raw = u_colorInner.rgb * u_colorInner.a; -// // float innerA_raw = u_colorInner.a; -// // -// // float innerA = innerA_raw * imgAlpha; -// // vec3 innerRgb = innerRgb_raw * imgAlpha; -// // -// // vec3 layerRgb = innerRgb + backRgb * (1.0 - innerA); -// // float layerA = innerA + backA * (1.0 - innerA); -// // -// // vec3 colorCopy = color; -// // color = color + layerRgb * (1.0 - opacity); -// // opacity = opacity + layerA * (1.0 - opacity); -// -// -// // fragColor = vec4(color, opacity); -// -// vec2 shape_uv = v_patternUV; -// // shape_uv *= 1.25; -// -// vec4 voronoiRes = voronoi(shape_uv, t); -// -// float shape = clamp(voronoiRes.w, 0., 1.); -// float mixer = shape * (u_colorsCount - 1.); -// mixer = (shape - .5 / u_colorsCount) * u_colorsCount; -// -// vec4 gradient = u_colors[0]; -// gradient.rgb *= gradient.a; -// for (int i = 1; i < ${ blobsLogoMeta.maxColorCount }; i++) { -// if (i >= int(u_colorsCount)) break; -// float localT = clamp(mixer - float(i - 1), 0.0, 1.0); -// vec4 c = u_colors[i]; -// c.rgb *= c.a; -// gradient = mix(gradient, c, localT); -// } -// -// // if ((mixer < 0.) || (mixer > (u_colorsCount - 1.))) { -// // float localT = mixer + 1.; -// // if (mixer > (u_colorsCount - 1.)) { -// // localT = mixer - (u_colorsCount - 1.); -// // } -// // vec4 cFst = u_colors[0]; -// // cFst.rgb *= cFst.a; -// // vec4 cLast = u_colors[int(u_colorsCount - 1.)]; -// // cLast.rgb *= cLast.a; -// // gradient = mix(cLast, cFst, localT); -// // } -// -// vec3 cellColor = gradient.rgb; -// float cellOpacity = gradient.a; -// -// vec3 color = cellColor; -// float opacity = cellOpacity; -// -// // float u_gap = .1; -// // vec4 u_colorGap = vec4(0., 0., 0., 1.); -// // float eee = voronoiRes.x; -// // float smoothEdge = .001; -// // eee = smoothstep(u_gap - smoothEdge, u_gap + smoothEdge, eee); -// // -// // color = mix(u_colorGap.rgb * u_colorGap.a, color, eee); -// // opacity = mix(u_colorGap.a, opacity, eee); -// -// fragColor = vec4(color, opacity); } `; From 949db84ff8d9621ba673d66f45a7785c1f5af8e0 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 25 Nov 2025 23:08:59 +0100 Subject: [PATCH 46/84] more experiments on 3d look --- packages/shaders/src/shaders/folds.ts | 57 ++++++++++++++------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index 757fcb7c0..f9fa617d3 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -204,8 +204,7 @@ vec3 rotateAroundZ(vec3 v, float angle) { void main() { - const float firstFrameOffset = 2.8; - float t = .3 * (u_time + firstFrameOffset); + float t = .6 * u_time; vec2 uv = v_imageUV; vec2 dudx = dFdx(v_imageUV); @@ -312,25 +311,47 @@ void main() { // // fragColor = vec4(color, opacity); + + + float f[${ foldsMeta.maxColorCount }]; + + float yTravel = mix(2., -.2, fract(.5 * t)); + +// float shaping = .2 * edge * step(0., yTravel) + .2 * step(yTravel, 0.); + float shaping = .05 * edge; + + + vec2 trajs[${ foldsMeta.maxColorCount }]; + // trajs[0] = vec2(.0, 3. - fract(.1 * t)); + trajs[0] = vec2(-.4, -.5 + yTravel); + float dist = 1.; + for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { + f[i] = getPoint(uv + trajs[i], shaping); + } + f[0] = sst(.9, .9 + 2. * fwidth(f[0]), f[0]); + + vec3 uLightDir1 = normalize(vec3(.5, .5, .7)); vec3 uLightDir2 = normalize(vec3(-.5, -.5, .9)); - uLightDir1 = rotateAroundZ(uLightDir1, 5. * t); - uLightDir2 = rotateAroundZ(uLightDir2, -3. * t); +// uLightDir1 = rotateAroundZ(uLightDir1, 1. * t); +// uLightDir2 = rotateAroundZ(uLightDir2, -.2 * t); vec3 normal = computeNormal(uv); vec3 viewDir = vec3(0., 0., 1.); // --- material parameters --- vec3 baseColor = vec3(1.); + baseColor = mix(vec3(1.), vec3(.6), f[0]); + vec3 specColor = vec3(.8); // float shininess = 20.0; // light colors/intensities - vec3 lightColor1 = vec3(0.9, 0.2, 0.2); - vec3 lightColor2 = vec3(0.1, 0.4, 0.9); + vec3 lightColor1 = vec3(.2, .3, .3); + vec3 lightColor2 = vec3(.5, .5, .7); // --- lighting --- float NdotL1 = max(dot(normal, uLightDir1), 0.); @@ -364,28 +385,10 @@ void main() { - t = 2. * u_time; - float f[${ foldsMeta.maxColorCount }]; - -// float yTravel = mix(2.5, -1.5, fract(.1 * t)); - float yTravel = mix(2., -.2, fract(.1 * t)); - -// float shaping = .2 * edge * step(0., yTravel) + .2 * step(yTravel, 0.); - float shaping = .05 * edge; - - vec2 trajs[${ foldsMeta.maxColorCount }]; - // trajs[0] = vec2(.0, 3. - fract(.1 * t)); - trajs[0] = vec2(-.5, -.5 + yTravel); - float dist = 1.; - for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { - f[i] = getPoint(uv + trajs[i], shaping); - } // // vec3 color = vec3(1.); -// f[0] = sst(.9, .9 + 2. * fwidth(f[0]), f[0]); -//// color = mix(vec3(1.), vec3(.8, .7, 0.), f[0]); -// + //// vec3 color = mix(u_colors[0].rgb, u_colors[1].rgb, NdotH); // // float shadow = 1. - NdotH; @@ -394,8 +397,8 @@ void main() { // color = mix(color, vec3(0.), imgAlpha - pow(edge, .05)); // color = mix(color, vec3(0.), line); -// fragColor = vec4(color, imgAlpha); - fragColor = vec4(vec3(NdotH1), imgAlpha); + fragColor = vec4(color, imgAlpha); +// fragColor = vec4(vec3(NdotH1), imgAlpha); } `; From 0dfac04f96ebad08d03d20ce9d35b01e8504d5ee Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 25 Nov 2025 23:20:48 +0100 Subject: [PATCH 47/84] GemSmoke component renamed + defs + controls reordering --- .../{warp-logo => gem-smoke}/layout.tsx | 2 +- .../{warp-logo => gem-smoke}/page.tsx | 32 ++++----- docs/src/shader-defs/gem-smoke-def.ts | 65 +++++++++++++++++++ docs/src/shader-defs/warp-logo-def.ts | 19 ------ packages/shaders-react/src/index.ts | 6 +- .../shaders/{warp-logo.tsx => gem-smoke.tsx} | 32 ++++----- packages/shaders/src/index.ts | 12 ++-- .../shaders/{warp-logo.ts => gem-smoke.ts} | 14 ++-- 8 files changed, 114 insertions(+), 68 deletions(-) rename docs/src/app/(shaders)/{warp-logo => gem-smoke}/layout.tsx (80%) rename docs/src/app/(shaders)/{warp-logo => gem-smoke}/page.tsx (83%) create mode 100644 docs/src/shader-defs/gem-smoke-def.ts delete mode 100644 docs/src/shader-defs/warp-logo-def.ts rename packages/shaders-react/src/shaders/{warp-logo.tsx => gem-smoke.tsx} (83%) rename packages/shaders/src/shaders/{warp-logo.ts => gem-smoke.ts} (98%) diff --git a/docs/src/app/(shaders)/warp-logo/layout.tsx b/docs/src/app/(shaders)/gem-smoke/layout.tsx similarity index 80% rename from docs/src/app/(shaders)/warp-logo/layout.tsx rename to docs/src/app/(shaders)/gem-smoke/layout.tsx index 813c2cabb..c66437c46 100644 --- a/docs/src/app/(shaders)/warp-logo/layout.tsx +++ b/docs/src/app/(shaders)/gem-smoke/layout.tsx @@ -1,7 +1,7 @@ import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'TBD Logo Filter • Paper', + title: 'Gem Smoke Logo Filter • Paper', }; export default function Layout({ children }: { children: React.ReactNode }) { diff --git a/docs/src/app/(shaders)/warp-logo/page.tsx b/docs/src/app/(shaders)/gem-smoke/page.tsx similarity index 83% rename from docs/src/app/(shaders)/warp-logo/page.tsx rename to docs/src/app/(shaders)/gem-smoke/page.tsx index 09ba27fc1..e9a101429 100644 --- a/docs/src/app/(shaders)/warp-logo/page.tsx +++ b/docs/src/app/(shaders)/gem-smoke/page.tsx @@ -1,25 +1,25 @@ 'use client'; -import { WarpLogo, warpLogoPresets } from '@paper-design/shaders-react'; +import { GemSmoke, gemSmokePresets } from '@paper-design/shaders-react'; import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { warpLogoMeta } from '@paper-design/shaders'; +import { gemSmokeMeta } from '@paper-design/shaders'; import { ShaderFit } from '@paper-design/shaders'; import { levaImageButton } from '@/helpers/leva-image-button'; import { useState, Suspense, useEffect, useCallback } from 'react'; import { ShaderDetails } from '@/components/shader-details'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { warpLogoDef } from '@/shader-defs/warp-logo-def'; +import { gemSmokeDef } from '@/shader-defs/gem-smoke-def'; import { toHsla } from '@/helpers/color-utils'; import { useColors } from "@/helpers/use-colors"; // Override just for the docs, we keep it transparent in the preset -// warpLogoPresets[0].params.colorBack = '#000000'; +// gemSmokePresets[0].params.colorBack = '#000000'; -const { worldWidth, worldHeight, ...defaults } = warpLogoPresets[0].params; +const { worldWidth, worldHeight, ...defaults } = gemSmokePresets[0].params; const imageFiles = [ 'contra.svg', @@ -57,7 +57,7 @@ const imageFiles = [ 'diamond.svg', ] as const; -const WarpLogoWithControls = () => { +const GemSmokeWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); const [image, setImage] = useState('/images/logos/diamond.svg'); @@ -77,18 +77,18 @@ const WarpLogoWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, - maxColorCount: warpLogoMeta.maxColorCount, + maxColorCount: gemSmokeMeta.maxColorCount, }); const [params, setParams] = useControls(() => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, distortion: { value: defaults.distortion, min: 0, max: 1, order: 201 }, + outerDistortion: { value: defaults.outerDistortion, min: 0, max: 1, order: 202 }, outerVisibility: { value: defaults.outerVisibility, min: 0, max: 1, order: 203 }, - outerDistortion: { value: defaults.outerDistortion, min: 0, max: 1, order: 203 }, + innerFill: { value: defaults.innerFill, min: 0, max: 1, order: 304 }, speed: { value: defaults.speed, min: 0, max: 4, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, - innerFill: { value: defaults.innerFill, min: 0, max: 1, order: 302 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, @@ -104,7 +104,7 @@ const WarpLogoWithControls = () => { useControls(() => { const presets = Object.fromEntries( - warpLogoPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + gemSmokePresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ name, button(() => { const { colors, ...presetParams } = preset; @@ -121,20 +121,20 @@ const WarpLogoWithControls = () => { // Reset to defaults on mount, so that Leva doesn't show values from other // shaders when navigating (if two shaders have a color1 param for example) useResetLevaParams(params, setParams, defaults); - useUrlParams(params, setParams, warpLogoDef, setColors); - usePresetHighlight(warpLogoPresets, params); + useUrlParams(params, setParams, gemSmokeDef, setColors); + usePresetHighlight(gemSmokePresets, params); cleanUpLevaParams(params); return ( <> - + - + - + ); }; -export default WarpLogoWithControls; +export default GemSmokeWithControls; diff --git a/docs/src/shader-defs/gem-smoke-def.ts b/docs/src/shader-defs/gem-smoke-def.ts new file mode 100644 index 000000000..c2d1987c9 --- /dev/null +++ b/docs/src/shader-defs/gem-smoke-def.ts @@ -0,0 +1,65 @@ +import { gemSmokePresets } from '@paper-design/shaders-react'; +import type { ShaderDef } from './shader-def-types'; +import { animatedCommonParams } from './common-param-def'; + +const defaultParams = gemSmokePresets[0].params; + +export const gemSmokeDef: ShaderDef = { + name: 'Gem Smoke', + description: 'Fluid, smoke shape animating behind the input image and being distorted by shape', + params: [ + { + name: 'image', + type: 'HTMLImageElement | string', + description: + 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', + }, + { + name: 'colors', + type: 'string[]', + defaultValue: [], + isColor: true, + description: 'Up to 5 ray colors', + }, + { + name: 'colorBack', + type: 'string', + defaultValue: defaultParams.colorBack, + isColor: true, + description: 'Background color', + }, + { + name: 'distortion', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.distortion, + description: 'The power of smoke distortion', + }, + { + name: 'outerDistortion', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.outerDistortion, + description: 'The power of distortion out of the input shape (shape defined by alpha channel)', + }, + { + name: 'outerVisibility', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.outerDistortion, + description: 'The visibility of smoke shape out of the input shape (shape defined by alpha channel)', + }, + { + name: 'innerFill', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.innerFill, + description: 'Additional flat color within the input shape (shape defined by alpha channel)', + }, + ...animatedCommonParams, + ], +}; diff --git a/docs/src/shader-defs/warp-logo-def.ts b/docs/src/shader-defs/warp-logo-def.ts deleted file mode 100644 index a229b7aa2..000000000 --- a/docs/src/shader-defs/warp-logo-def.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { warpLogoPresets } from '@paper-design/shaders-react'; -import type { ShaderDef } from './shader-def-types'; -import { animatedCommonParams } from './common-param-def'; - -const defaultParams = warpLogoPresets[0].params; - -export const warpLogoDef: ShaderDef = { - name: 'TBD', - description: 'TBD', - params: [ - { - name: 'image', - type: 'HTMLImageElement | string', - description: - 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', - }, - ...animatedCommonParams, - ], -}; diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 76b8a4bab..5f1c947a3 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -119,9 +119,9 @@ export { BlobsLogo, blobsLogoPresets } from './shaders/blobs-logo.js'; export type { BlobsLogoProps } from './shaders/blobs-logo.js'; export type { BlobsLogoUniforms, BlobsLogoParams } from '@paper-design/shaders'; -export { WarpLogo, warpLogoPresets } from './shaders/warp-logo.js'; -export type { WarpLogoProps } from './shaders/warp-logo.js'; -export type { WarpLogoUniforms, WarpLogoParams } from '@paper-design/shaders'; +export { GemSmoke, gemSmokePresets } from './shaders/gem-smoke.js'; +export type { GemSmokeProps } from './shaders/gem-smoke.js'; +export type { GemSmokeUniforms, GemSmokeParams } from '@paper-design/shaders'; export { isPaperShaderElement, getShaderColorFromString } from '@paper-design/shaders'; export type { PaperShaderElement, ShaderFit, ShaderSizingParams, ShaderSizingUniforms } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/warp-logo.tsx b/packages/shaders-react/src/shaders/gem-smoke.tsx similarity index 83% rename from packages/shaders-react/src/shaders/warp-logo.tsx rename to packages/shaders-react/src/shaders/gem-smoke.tsx index f5636b7c7..1b9317046 100644 --- a/packages/shaders-react/src/shaders/warp-logo.tsx +++ b/packages/shaders-react/src/shaders/gem-smoke.tsx @@ -2,28 +2,28 @@ import { memo, useLayoutEffect, useState } from 'react'; import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; import { colorPropsAreEqual } from '../color-props-are-equal.js'; import { - warpLogoFragmentShader, + gemSmokeFragmentShader, ShaderFitOptions, defaultObjectSizing, - type WarpLogoUniforms, - type WarpLogoParams, - toProcessedWarpLogo, + type GemSmokeUniforms, + type GemSmokeParams, + toProcessedGemSmoke, type ImageShaderPreset, getShaderColorFromString, } from '@paper-design/shaders'; import { transparentPixel } from '../transparent-pixel.js'; import { suspend } from '../suspend.js'; -export interface WarpLogoProps extends ShaderComponentProps, WarpLogoParams { +export interface GemSmokeProps extends ShaderComponentProps, GemSmokeParams { /** * Suspends the component when the image is being processed. */ suspendWhenProcessingImage?: boolean; } -type WarpLogoPreset = ImageShaderPreset; +type GemSmokePreset = ImageShaderPreset; -export const defaultPreset: WarpLogoPreset = { +export const defaultPreset: GemSmokePreset = { name: 'Default', params: { ...defaultObjectSizing, @@ -39,7 +39,7 @@ export const defaultPreset: WarpLogoPreset = { }, }; -export const blackPreset: WarpLogoPreset = { +export const blackPreset: GemSmokePreset = { name: 'Black', params: { ...defaultObjectSizing, @@ -55,9 +55,9 @@ export const blackPreset: WarpLogoPreset = { }, }; -export const warpLogoPresets: WarpLogoPreset[] = [defaultPreset, blackPreset]; +export const gemSmokePresets: GemSmokePreset[] = [defaultPreset, blackPreset]; -export const WarpLogo: React.FC = memo(function WarpLogoImpl({ +export const GemSmoke: React.FC = memo(function GemSmokeImpl({ // Own props colorBack = defaultPreset.params.colorBack, colors = defaultPreset.params.colors, @@ -81,7 +81,7 @@ export const WarpLogo: React.FC = memo(function WarpLogoImpl({ worldWidth = defaultPreset.params.worldWidth, worldHeight = defaultPreset.params.worldHeight, ...props -}: WarpLogoProps) { +}: GemSmokeProps) { const imageUrl = typeof image === 'string' ? image : image.src; const [processedStateImage, setProcessedStateImage] = useState(transparentPixel); @@ -89,8 +89,8 @@ export const WarpLogo: React.FC = memo(function WarpLogoImpl({ if (suspendWhenProcessingImage && typeof window !== 'undefined' && imageUrl) { processedImage = suspend( - (): Promise => toProcessedWarpLogo(imageUrl).then((result) => URL.createObjectURL(result.pngBlob)), - [imageUrl, 'warpLogo'] + (): Promise => toProcessedGemSmoke(imageUrl).then((result) => URL.createObjectURL(result.pngBlob)), + [imageUrl, 'gemSmoke'] ); } else { processedImage = processedStateImage; @@ -110,7 +110,7 @@ export const WarpLogo: React.FC = memo(function WarpLogoImpl({ let url: string; let current = true; - toProcessedWarpLogo(imageUrl).then((result) => { + toProcessedGemSmoke(imageUrl).then((result) => { if (current) { url = URL.createObjectURL(result.pngBlob); setProcessedStateImage(url); @@ -143,14 +143,14 @@ export const WarpLogo: React.FC = memo(function WarpLogoImpl({ u_originY: originY, u_worldWidth: worldWidth, u_worldHeight: worldHeight, - } satisfies WarpLogoUniforms; + } satisfies GemSmokeUniforms; return ( diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index 0cab6682f..35f671e52 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -234,12 +234,12 @@ export { } from './shaders/blobs-logo.js'; export { - warpLogoMeta, - warpLogoFragmentShader, - toProcessedWarpLogo, - type WarpLogoParams, - type WarpLogoUniforms, -} from './shaders/warp-logo.js'; + gemSmokeMeta, + gemSmokeFragmentShader, + toProcessedGemSmoke, + type GemSmokeParams, + type GemSmokeUniforms, +} from './shaders/gem-smoke.js'; // ----- Utils ----- // export { getShaderColorFromString } from './get-shader-color-from-string.js'; diff --git a/packages/shaders/src/shaders/warp-logo.ts b/packages/shaders/src/shaders/gem-smoke.ts similarity index 98% rename from packages/shaders/src/shaders/warp-logo.ts rename to packages/shaders/src/shaders/gem-smoke.ts index 9c603cad1..d6becd88f 100644 --- a/packages/shaders/src/shaders/warp-logo.ts +++ b/packages/shaders/src/shaders/gem-smoke.ts @@ -3,7 +3,7 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; import { declarePI, rotation2, proceduralHash21 } from '../shader-utils.js'; -export const warpLogoMeta = { +export const gemSmokeMeta = { maxColorCount: 6, } as const; @@ -18,7 +18,7 @@ export const warpLogoMeta = { */ // language=GLSL -export const warpLogoFragmentShader: string = `#version 300 es +export const gemSmokeFragmentShader: string = `#version 300 es precision mediump float; uniform sampler2D u_image; @@ -27,7 +27,7 @@ uniform float u_imageAspectRatio; uniform vec2 u_resolution; uniform float u_time; -uniform vec4 u_colors[${warpLogoMeta.maxColorCount}]; +uniform vec4 u_colors[${gemSmokeMeta.maxColorCount}]; uniform float u_colorsCount; uniform vec4 u_colorBack; uniform float u_distortion; @@ -182,7 +182,7 @@ void main() { gradient.rgb *= gradient.a; float outerShape = 0.; - for (int i = 1; i < ${ warpLogoMeta.maxColorCount + 1 }; i++) { + for (int i = 1; i < ${ gemSmokeMeta.maxColorCount + 1 }; i++) { if (i > int(u_colorsCount)) break; float m = clamp(mixer - float(i - 1), 0., 1.); @@ -226,7 +226,7 @@ interface SparsePixelData { neighborIndices: Int32Array; } -export function toProcessedWarpLogo(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { +export function toProcessedGemSmoke(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const isBlob = typeof file === 'string' && file.startsWith('blob:'); @@ -638,7 +638,7 @@ function solvePoissonSparse( return u; } -export interface WarpLogoUniforms extends ShaderSizingUniforms { +export interface GemSmokeUniforms extends ShaderSizingUniforms { u_colorBack: [number, number, number, number]; u_colors: vec4[]; u_colorsCount: number; @@ -649,7 +649,7 @@ export interface WarpLogoUniforms extends ShaderSizingUniforms { u_outerDistortion: number; } -export interface WarpLogoParams extends ShaderSizingParams, ShaderMotionParams { +export interface GemSmokeParams extends ShaderSizingParams, ShaderMotionParams { colors?: string[]; colorBack?: string; image?: HTMLImageElement | string | undefined; From 6d1e6a784c9538ba0fca3834e58a8ed2edae9f7e Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 27 Nov 2025 18:15:10 +0100 Subject: [PATCH 48/84] gemSmoke defs update --- docs/src/shader-defs/gem-smoke-def.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/shader-defs/gem-smoke-def.ts b/docs/src/shader-defs/gem-smoke-def.ts index c2d1987c9..dcdcc7652 100644 --- a/docs/src/shader-defs/gem-smoke-def.ts +++ b/docs/src/shader-defs/gem-smoke-def.ts @@ -1,6 +1,6 @@ import { gemSmokePresets } from '@paper-design/shaders-react'; import type { ShaderDef } from './shader-def-types'; -import { animatedCommonParams } from './common-param-def'; +import { animatedImageCommonParams } from './common-param-def'; const defaultParams = gemSmokePresets[0].params; @@ -60,6 +60,6 @@ export const gemSmokeDef: ShaderDef = { defaultValue: defaultParams.innerFill, description: 'Additional flat color within the input shape (shape defined by alpha channel)', }, - ...animatedCommonParams, + ...animatedImageCommonParams, ], }; From ebc396db2cf3397e6bfd43484b8bbeea528e72f5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 28 Nov 2025 08:20:46 +0100 Subject: [PATCH 49/84] progress on 3d --- packages/shaders-react/src/shaders/folds.tsx | 6 +- packages/shaders/src/shaders/folds.ts | 128 +------------------ 2 files changed, 8 insertions(+), 126 deletions(-) diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index 024893674..0b8eeeddb 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -33,14 +33,14 @@ export const defaultPreset: FoldsPreset = { colorBack: '#ffffff00', colorInner: '#000000', colors: ['#000000', '#ffffff'], - stripeWidth: 0.65, + stripeWidth: 1, alphaMask: true, noiseScale: 1, size: 10, shift: 0.5, noise: 0.5, - outerNoise: 0.5, - softness: 0, + outerNoise: 0.33, + softness: 0.5, gradient: 1, angle: 220, }, diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index f9fa617d3..b675b2bf2 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -50,13 +50,6 @@ out vec4 fragColor; ${declarePI} ${rotation2} -${simplexNoise} - -float doubleSNoise(vec2 uv, float t) { - float noise = .5 * snoise(uv - vec2(0., .3 * t)); - noise += .5 * snoise(2. * uv + vec2(0., .32 * t)); - return noise; -} float getImgFrame(vec2 uv, float th) { float frame = 1.; @@ -165,7 +158,7 @@ vec2 getPosition(int i, float t) { float getHeight(vec2 uv) { float a = texture(u_image, uv).r; -// a = pow(a, 2.); + a = pow(a, mix(1., 3., u_softness)); return a; } @@ -219,99 +212,6 @@ void main() { float frame = getImgFrame(v_imageUV, 0.); imgAlpha *= frame; edge *= frame; - - vec2 patternsUV = v_objectUV; - vec2 stripesUV = v_objectUV; - float angle = -u_angle * PI / 180.; - stripesUV = rotate(stripesUV, angle); - stripesUV *= u_size; - - float n = doubleSNoise(u_noiseScale * patternsUV + 100., .2 * u_time); - float edgeAtten = edge + u_outerNoise * (1. - edge); - float y = stripesUV.y + edgeAtten * .5 * n * u_size * u_noise; - -// float w = u_stripeWidth * edge; - float w = 0.; - y += 2. * sign(u_shift) * mix(0., w, abs(u_shift)); - - float stripeId = floor(y); - float fy = fract(y); - - float stripeMap = abs(fy - .5); - float aa = fwidth(y); - - w = clamp(w, aa, .5 - aa); - - float lMin = w - aa; - float lMax = w + aa; - float line = 1. - sst(lMin, lMax, stripeMap); -// -// if (u_alphaMask == true) { -// line *= imgAlpha; -// } - - // float softness = mix(0., u_softness, sst(0., aa, w)); - // line -= sst(softness, 0., 1. - fy); - // line = clamp(line, 0., 1.); - // - // int colorIdx = int(posMod(stripeId, u_colorsCount)); - // vec4 orderedStripeColor = u_colors[0]; - // for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { - // if (i >= int(u_colorsCount)) break; - // float isHit = 1.0 - step(.5, abs(float(i - colorIdx))); - // orderedStripeColor = mix(orderedStripeColor, u_colors[i], isHit); - // } - // - // vec3 gradientStripeColor = vec3(0.); - // float gradientOpacity = 0.; - // { - // float totalWeight = 0.; - // for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { - // if (i >= int(u_colorsCount)) break; - // vec2 pos = getPosition(i, 1.5 * t); - // vec3 colorFraction = u_colors[i].rgb * u_colors[i].a; - // float opacityFraction = u_colors[i].a; - // float dist = .5 * length(patternsUV + .5 - pos); - // dist = pow(dist, 3.5); - // float weight = 1. / (dist + 1e-3); - // gradientStripeColor += colorFraction * weight; - // gradientOpacity += opacityFraction * weight; - // totalWeight += weight; - // } - // gradientStripeColor /= max(1e-4, totalWeight); - // gradientOpacity /= max(1e-4, totalWeight); - // } - // - // vec4 stripeColor = vec4(gradientStripeColor, gradientOpacity); - // stripeColor = mix(orderedStripeColor, mix(stripeColor, orderedStripeColor, fy), u_gradient); - - // vec3 stripePremulRGB = stripeColor.rgb * stripeColor.a; - // stripePremulRGB *= line; - // float stripeA = stripeColor.a * line; - // - // vec3 color = stripePremulRGB; - // float opacity = stripeA; - // - // vec3 backRgb = u_colorBack.rgb * u_colorBack.a; - // float backA = u_colorBack.a; - // vec3 innerRgb = u_colorInner.rgb * u_colorInner.a; - // float innerA = u_colorInner.a; - // - // innerRgb *= imgAlpha; - // innerA *= imgAlpha; - // - // vec3 underlayerRgb = innerRgb + backRgb * (1. - innerA); - // float underlayerA = innerA + backA * (1. - innerA); - // - // color *= line; - // opacity *= line; - // - // color = color + underlayerRgb * (1. - opacity); - // opacity = opacity + underlayerA * (1. - opacity); - // - // fragColor = vec4(color, opacity); - - float f[${ foldsMeta.maxColorCount }]; @@ -332,8 +232,8 @@ void main() { - vec3 uLightDir1 = normalize(vec3(.5, .5, .7)); - vec3 uLightDir2 = normalize(vec3(-.5, -.5, .9)); + vec3 uLightDir1 = normalize(vec3(.5, .5, u_stripeWidth)); + vec3 uLightDir2 = normalize(vec3(-.5, -.5, u_stripeWidth)); // uLightDir1 = rotateAroundZ(uLightDir1, 1. * t); @@ -378,27 +278,9 @@ void main() { // --- final --- vec3 color = ambient + diffuse + specular; color = clamp(color, 0., 1.); + color = mix(u_colorBack.rgb, color, imgAlpha); - - // float shaping = .2 * edge;// * step(0., yTravel) + (.4 - .2 * edge) * step(yTravel, 0.); - - - - - -// -// vec3 color = vec3(1.); - -//// vec3 color = mix(u_colors[0].rgb, u_colors[1].rgb, NdotH); -// -// float shadow = 1. - NdotH; -// shadow = -.4 + pow(shadow, .2); -// color = mix(color, vec3(0.), shadow); -// color = mix(color, vec3(0.), imgAlpha - pow(edge, .05)); -// color = mix(color, vec3(0.), line); - - fragColor = vec4(color, imgAlpha); -// fragColor = vec4(vec3(NdotH1), imgAlpha); + fragColor = vec4(color, 1.); } `; From 75e8b82882b44eb8082228f32c15f7d762a3bc63 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 28 Nov 2025 18:53:29 +0100 Subject: [PATCH 50/84] 3d progress --- docs/src/app/(shaders)/folds/page.tsx | 20 +-- packages/shaders-react/src/shaders/folds.tsx | 175 ++----------------- packages/shaders/src/shaders/folds.ts | 102 +++++------ 3 files changed, 73 insertions(+), 224 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index 6a54cfa1f..b5317036c 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -84,17 +84,17 @@ const FoldsWithControls = () => { const [params, setParams] = useControls(() => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorInner: { value: toHsla(defaults.colorInner), order: 101 }, + // colorInner: { value: toHsla(defaults.colorInner), order: 101 }, stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, - softness: { value: defaults.softness, min: 0, max: 1, order: 201 }, - gradient: { value: defaults.gradient, min: 0, max: 1, order: 201 }, - alphaMask: { value: defaults.alphaMask, order: 202 }, - size: { value: defaults.size, min: 3, max: 50, order: 203 }, - shift: { value: defaults.shift, min: -0.5, max: 0.5, order: 204 }, - noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, - noiseScale: { value: defaults.noiseScale, min: .1, max: 3, step: 0.01, order: 206 }, - outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 207 }, - angle: { value: defaults.angle, min: 0, max: 360, order: 208 }, + bevel: { value: defaults.bevel, min: 0, max: 1, order: 201 }, + overlayHeight: { value: defaults.overlayHeight, min: 0, max: 1, order: 201 }, + // alphaMask: { value: defaults.alphaMask, order: 202 }, + // size: { value: defaults.size, min: 3, max: 50, order: 203 }, + // shift: { value: defaults.shift, min: -0.5, max: 0.5, order: 204 }, + // noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, + // noiseScale: { value: defaults.noiseScale, min: .1, max: 3, step: 0.01, order: 206 }, + // outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 207 }, + overlayBevel: { value: defaults.overlayBevel, min: 0, max: 1, order: 208 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index 0b8eeeddb..81632375f 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -30,9 +30,9 @@ export const defaultPreset: FoldsPreset = { scale: 0.8, speed: 1, frame: 0, - colorBack: '#ffffff00', + colorBack: '#00000000', colorInner: '#000000', - colors: ['#000000', '#ffffff'], + colors: ['#334D4D', '#8080B3'], stripeWidth: 1, alphaMask: true, noiseScale: 1, @@ -40,164 +40,14 @@ export const defaultPreset: FoldsPreset = { shift: 0.5, noise: 0.5, outerNoise: 0.33, - softness: 0.5, - gradient: 1, - angle: 220, + bevel: 0.5, + overlayHeight: 0.2, + overlayBevel: 0.2, }, }; -export const fstPreset: FoldsPreset = { - name: '1', - params: { - ...defaultObjectSizing, - scale: 0.8, - speed: 0.2, - frame: 0, - colorBack: '#000000', - colorInner: '#00000000', - colors: ['#ffffff'], - stripeWidth: 1, - alphaMask: false, - noiseScale: 0.5, - size: 16, - shift: 0.5, - noise: 0.32, - outerNoise: 0, - softness: 0, - gradient: 0, - angle: 0, - }, -}; - -export const scdPreset: FoldsPreset = { - name: '2', - params: { - ...defaultObjectSizing, - scale: 0.8, - speed: 0.2, - frame: 0, - colorBack: '#000000', - colorInner: '#00000000', - colors: ['#ffffff', '#000000', '#0022ff', '#ffe500'], - stripeWidth: 0.6, - alphaMask: false, - noiseScale: 1.5, - size: 24, - shift: 0, - noise: 0.65, - outerNoise: 1, - softness: 0, - gradient: 1, - angle: 90, - }, -}; - -export const trdPreset: FoldsPreset = { - name: '3', - params: { - ...defaultObjectSizing, - scale: 0.8, - speed: 0.2, - frame: 0, - colorBack: '#000000', - colorInner: '#6e535354', - colors: ['#ff5ec4', '#5effc8', '#ffe45e', '#ff6b5e'], - stripeWidth: 0.3, - alphaMask: true, - noiseScale: 1, - size: 45, - shift: 0, - noise: 0.3, - outerNoise: 0, - softness: 0, - gradient: 0, - angle: 0, - }, -}; - -export const frtPreset: FoldsPreset = { - name: '4', - params: { - ...defaultObjectSizing, - scale: 0.8, - speed: 0.2, - frame: 0, - colorBack: '#ffffff', - colorInner: '#ffffff', - colors: ['#000000'], - stripeWidth: 0, - alphaMask: false, - noiseScale: 1, - size: 45, - shift: 0, - noise: 0.3, - outerNoise: 0, - softness: 0, - gradient: 0, - angle: 0, - }, -}; - -export const ffsPreset: FoldsPreset = { - name: '5', - params: { - ...defaultObjectSizing, - frame: 0, - colors: [ - '#ffed47', - '#ffed47', - '#31fcb8', - '#ffffff', - '#ff006a', - '#3399cc', - '#3333cc', - ], - colorBack: '#ffffff00', - colorInner: '#000000', - stripeWidth: 1.0, - softness: 0.37, - gradient: 1.0, - alphaMask: true, - size: 3.0, - shift: -0.37, - noise: 0.46, - noiseScale: 1.0, - outerNoise: 0.0, - angle: 220, - speed: 0.20, - scale: 0.8, - }, -}; - -export const sixPreset: FoldsPreset = { - name: '6', - params: { - ...defaultObjectSizing, - frame: 0, - colors: [ - '#000000' - ], - colorBack: '#000000', - colorInner: '#ffffff', - - stripeWidth: 0.57, - softness: 0.0, - gradient: 0.0, - alphaMask: true, - - size: 18.0, - shift: -0.50, - noise: 0.61, - noiseScale: 1.07, - outerNoise: 0.0, - - angle: 0, - speed: 0.20, - scale: 0.8, - }, -}; -export const foldsPresets: FoldsPreset[] = [defaultPreset, fstPreset, scdPreset, trdPreset, frtPreset, ffsPreset, sixPreset]; +export const foldsPresets: FoldsPreset[] = [defaultPreset]; export const Folds: React.FC = memo(function FoldsImpl({ // Own props @@ -210,13 +60,13 @@ export const Folds: React.FC = memo(function FoldsImpl({ shift = defaultPreset.params.shift, noise = defaultPreset.params.noise, outerNoise = defaultPreset.params.outerNoise, - softness = defaultPreset.params.softness, - gradient = defaultPreset.params.gradient, + bevel = defaultPreset.params.bevel, + overlayHeight = defaultPreset.params.overlayHeight, stripeWidth = defaultPreset.params.stripeWidth, alphaMask = defaultPreset.params.alphaMask, noiseScale = defaultPreset.params.noiseScale, size = defaultPreset.params.size, - angle = defaultPreset.params.angle, + overlayBevel = defaultPreset.params.overlayBevel, suspendWhenProcessingImage = false, // Sizing props @@ -281,14 +131,13 @@ export const Folds: React.FC = memo(function FoldsImpl({ u_shift: shift, u_noise: noise, u_outerNoise: outerNoise, - u_softness: softness, - u_gradient: gradient, + u_bevel: bevel, + u_overlayHeight: overlayHeight, u_stripeWidth: stripeWidth, u_alphaMask: alphaMask, u_noiseScale: noiseScale, u_size: size, - u_angle: angle, - u_isImage: Boolean(image), + u_overlayBevel: overlayBevel, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index b675b2bf2..b3d347a96 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -31,18 +31,16 @@ uniform vec4 u_colors[${foldsMeta.maxColorCount}]; uniform float u_colorsCount; uniform vec4 u_colorBack; uniform vec4 u_colorInner; -uniform float u_softness; +uniform float u_bevel; uniform float u_stripeWidth; uniform bool u_alphaMask; uniform float u_noiseScale; uniform float u_size; -uniform float u_gradient; +uniform float u_overlayHeight; uniform float u_shift; uniform float u_noise; uniform float u_outerNoise; -uniform float u_angle; - -uniform bool u_isImage; +uniform float u_overlayBevel; ${sizingVariablesDeclaration} @@ -155,20 +153,21 @@ vec2 getPosition(int i, float t) { } - -float getHeight(vec2 uv) { +float getHeight(vec2 uv, vec2 addon) { float a = texture(u_image, uv).r; - a = pow(a, mix(1., 3., u_softness)); + a += addon[0]; + a = pow(a, mix(1., 3., u_bevel)); + a = mix(a, 1., addon[1]); return a; } -vec3 computeNormal(vec2 uv) { +vec3 computeNormal(vec2 uv, vec2 addon) { vec2 uTexelSize = vec2(1. / 100.); - float hC = getHeight(uv); - float hR = getHeight(uv + vec2(uTexelSize.x, 0.0)); - float hL = getHeight(uv - vec2(uTexelSize.x, 0.0)); - float hU = getHeight(uv + vec2(0.0, uTexelSize.y)); - float hD = getHeight(uv - vec2(0.0, uTexelSize.y)); + float hC = getHeight(uv, addon); + float hR = getHeight(uv + vec2(uTexelSize.x, 0.0), addon); + float hL = getHeight(uv - vec2(uTexelSize.x, 0.0), addon); + float hU = getHeight(uv + vec2(0.0, uTexelSize.y), addon); + float hD = getHeight(uv - vec2(0.0, uTexelSize.y), addon); float dX = (hR - hL) * 1.; float dY = (hU - hD) * 1.; @@ -189,15 +188,15 @@ mat2 rotZ(float a) { return mat2(c, -s, s, c); } -vec3 rotateAroundZ(vec3 v, float angle) { - mat2 r = rotZ(angle); +vec3 rotateAroundZ(vec3 v, float overlayBevel) { + mat2 r = rotZ(overlayBevel); return vec3(r * v.xy, v.z); } void main() { - float t = .6 * u_time; + float t = .1 * u_time; vec2 uv = v_imageUV; vec2 dudx = dFdx(v_imageUV); @@ -215,20 +214,24 @@ void main() { float f[${ foldsMeta.maxColorCount }]; - float yTravel = mix(2., -.2, fract(.5 * t)); - -// float shaping = .2 * edge * step(0., yTravel) + .2 * step(yTravel, 0.); - float shaping = .05 * edge; - + float yTime = fract(t); +// yTime = pow(yTime, .8); + float yTravel = mix(1.5, -1.5, yTime); + float yShape = mix(.04 * edge, .1 * edge, yTime); vec2 trajs[${ foldsMeta.maxColorCount }]; - // trajs[0] = vec2(.0, 3. - fract(.1 * t)); trajs[0] = vec2(-.4, -.5 + yTravel); float dist = 1.; for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { - f[i] = getPoint(uv + trajs[i], shaping); + f[i] = getPoint(uv + trajs[i], yShape); } - f[0] = sst(.9, .9 + 2. * fwidth(f[0]), f[0]); + + float aa = fwidth(f[0]); + float overlayShadow = sst(.9, .93, f[0]); + f[0] = sst(.9, .9 + 2. * aa, f[0]); + overlayShadow = 1. - overlayShadow; + overlayShadow *= f[0]; + overlayShadow = 10. * pow(overlayShadow, 2.); @@ -239,32 +242,24 @@ void main() { // uLightDir1 = rotateAroundZ(uLightDir1, 1. * t); // uLightDir2 = rotateAroundZ(uLightDir2, -.2 * t); - vec3 normal = computeNormal(uv); - vec3 viewDir = vec3(0., 0., 1.); + vec3 normal = computeNormal(uv, vec2(u_overlayHeight * f[0], u_overlayBevel * overlayShadow)); + vec3 viewDir = vec3(0., 0., 1.); - // --- material parameters --- vec3 baseColor = vec3(1.); baseColor = mix(vec3(1.), vec3(.6), f[0]); vec3 specColor = vec3(.8); -// float shininess = 20.0; - // light colors/intensities - vec3 lightColor1 = vec3(.2, .3, .3); - vec3 lightColor2 = vec3(.5, .5, .7); + vec3 lightColor1 = u_colors[0].rgb; + vec3 lightColor2 = u_colors[1].rgb; - // --- lighting --- float NdotL1 = max(dot(normal, uLightDir1), 0.); float NdotL2 = max(dot(normal, uLightDir2), 0.); - // diffuse from both lights - vec3 diffuse = - baseColor * (NdotL1 * lightColor1 + NdotL2 * lightColor2); + vec3 diffuse = baseColor * (NdotL1 * lightColor1 + NdotL2 * lightColor2); - // ambient (could also tint with a color if you like) vec3 ambient = baseColor * 0.2; - // --- specular highlights (per light) --- vec3 halfDir1 = normalize(uLightDir1 + viewDir); vec3 halfDir2 = normalize(uLightDir2 + viewDir); @@ -272,15 +267,21 @@ void main() { float NdotH2 = max(dot(normal, halfDir2), 0.); vec3 specular = - specColor * (pow(NdotH1, 400.) * lightColor1 + - pow(NdotH2, 400.) * lightColor2); + specColor * ( + pow(NdotH1, 400.) * lightColor1 + + pow(NdotH2, 400.) * lightColor2 + ); - // --- final --- + float opacity = imgAlpha; vec3 color = ambient + diffuse + specular; - color = clamp(color, 0., 1.); - color = mix(u_colorBack.rgb, color, imgAlpha); + color = clamp(color, vec3(0.), vec3(1.)); + color *= imgAlpha; + + vec3 bgColor = u_colorBack.rgb * u_colorBack.a; + color = color + bgColor * (1.0 - opacity); + opacity = opacity + u_colorBack.a * (1.0 - opacity); - fragColor = vec4(color, 1.); + fragColor = vec4(color, opacity); } `; @@ -812,10 +813,9 @@ export interface FoldsUniforms extends ShaderSizingUniforms { u_shift: number; u_noise: number; u_outerNoise: number; - u_softness: number; - u_gradient: number; - u_angle: number; - u_isImage: boolean; + u_bevel: number; + u_overlayHeight: number; + u_overlayBevel: number; } export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { @@ -827,10 +827,10 @@ export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { alphaMask?: boolean; noiseScale?: number; size?: number; - softness?: number; - gradient?: number; + bevel?: number; + overlayHeight?: number; shift?: number; noise?: number; outerNoise?: number; - angle?: number; + overlayBevel?: number; } \ No newline at end of file From ca62a0a6cc3eb9be37816c935665bc2250c28fcf Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 28 Nov 2025 19:04:37 +0100 Subject: [PATCH 51/84] 3d progress --- docs/src/app/(shaders)/folds/page.tsx | 2 +- packages/shaders-react/src/shaders/folds.tsx | 4 ++-- packages/shaders/src/shaders/folds.ts | 12 ++++++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index b5317036c..c0962d3e7 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -85,7 +85,7 @@ const FoldsWithControls = () => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, // colorInner: { value: toHsla(defaults.colorInner), order: 101 }, - stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, + // stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, bevel: { value: defaults.bevel, min: 0, max: 1, order: 201 }, overlayHeight: { value: defaults.overlayHeight, min: 0, max: 1, order: 201 }, // alphaMask: { value: defaults.alphaMask, order: 202 }, diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index 81632375f..b90f083ad 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -32,7 +32,7 @@ export const defaultPreset: FoldsPreset = { frame: 0, colorBack: '#00000000', colorInner: '#000000', - colors: ['#334D4D', '#8080B3'], + colors: ['#14f7ff', '#ff0a0a'], stripeWidth: 1, alphaMask: true, noiseScale: 1, @@ -41,7 +41,7 @@ export const defaultPreset: FoldsPreset = { noise: 0.5, outerNoise: 0.33, bevel: 0.5, - overlayHeight: 0.2, + overlayHeight: 0.35, overlayBevel: 0.2, }, }; diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index b3d347a96..b35e6ff4f 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -133,6 +133,10 @@ float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, fl return sum / norm; } +float lst(float edge0, float edge1, float x) { + return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); +} + float sst(float edge0, float edge1, float x) { return smoothstep(edge0, edge1, x); } @@ -227,16 +231,16 @@ void main() { } float aa = fwidth(f[0]); - float overlayShadow = sst(.9, .93, f[0]); + float overlayShadow = lst(.9, .93, f[0]); f[0] = sst(.9, .9 + 2. * aa, f[0]); overlayShadow = 1. - overlayShadow; overlayShadow *= f[0]; - overlayShadow = 10. * pow(overlayShadow, 2.); + overlayShadow = 10. * pow(overlayShadow, 1.); - vec3 uLightDir1 = normalize(vec3(.5, .5, u_stripeWidth)); - vec3 uLightDir2 = normalize(vec3(-.5, -.5, u_stripeWidth)); + vec3 uLightDir1 = normalize(vec3(.5, .5, .5)); + vec3 uLightDir2 = normalize(vec3(-.5, -.5, .5)); // uLightDir1 = rotateAroundZ(uLightDir1, 1. * t); From 73a1a7bc4fb4c57eea205af882f5ba5badfb371b Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 28 Nov 2025 22:20:07 +0100 Subject: [PATCH 52/84] grains on HTD --- .../src/app/(shaders)/halftone-lines/page.tsx | 1 + .../src/shaders/halftone-lines.tsx | 6 +++- .../shaders/src/shaders/halftone-lines.ts | 33 ++++++++++++++----- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index 864acaec0..ad1a72c46 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -83,6 +83,7 @@ const HalftoneLinesWithControls = () => { contrast: { value: defaults.contrast, min: 0.01, max: 1, order: 302 }, grainMixer: { value: defaults.grainMixer, min: 0, max: 1, order: 350 }, grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 351 }, + grainSize: { value: defaults.grainSize, min: 0, max: 1, order: 352 }, scale: { value: defaults.scale, min: 0.1, max: 10, order: 400 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 401 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 402 }, diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index af4fa2fd7..7fc21a965 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -32,7 +32,8 @@ export const defaultPreset: HalftoneLinesPreset = { originalColors: false, inverted: false, grainMixer: 0.2, - grainOverlay: 0.2, + grainOverlay: 0, + grainSize: 0.5, }, }; @@ -56,6 +57,7 @@ export const classicPreset: HalftoneLinesPreset = { inverted: false, grainMixer: 0, grainOverlay: 0, + grainSize: 0.5, }, }; @@ -79,6 +81,7 @@ export const HalftoneLines: React.FC = memo(function Halfton inverted = defaultPreset.params.inverted, grainMixer = defaultPreset.params.grainMixer, grainOverlay = defaultPreset.params.grainOverlay, + grainSize = defaultPreset.params.grainSize, // Sizing props fit = defaultPreset.params.fit, @@ -109,6 +112,7 @@ export const HalftoneLines: React.FC = memo(function Halfton u_inverted: inverted, u_grainMixer: grainMixer, u_grainOverlay: grainOverlay, + u_grainSize: grainSize, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index 89af82c4b..5ffbaa623 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -44,6 +44,7 @@ uniform mediump float u_imageAspectRatio; uniform float u_size; uniform float u_grainMixer; uniform float u_grainOverlay; +uniform float u_grainSize; uniform bool u_straight; uniform bool u_originalColors; uniform bool u_inverted; @@ -240,29 +241,43 @@ void main() { float lo = mix(loSharp, loBlurry, highDistort); float hi = mix(hiSharp, hiBlurry, highDistort); + + vec2 grainSize = mix(2000., 200., u_grainSize) * vec2(1., 1. / u_imageAspectRatio); + vec2 grainUV = getImageUV(uvNormalised, grainSize); + float grain = valueNoise(grainUV) + .3 * u_grainMixer; + grain = smoothstep(.55, .9, grain); + grain *= u_grainMixer; + stripeDist.y += .5 * grain; + float line = sst(lo, hi, stripeDist.y); line = mix(1., line, frame); line = clamp(line, 0., 1.); - vec2 grainSize = 1000. * vec2(1., 1. / u_imageAspectRatio); - vec2 grainUV = getImageUV(uvNormalised, grainSize); - float grain = valueNoise(grainUV); - grain = smoothstep(.55, .7 + .2 * u_grainMixer, grain); - grain *= u_grainMixer; - line += grain; - vec3 color = vec3(0.); float opacity = 1.; float stripeId = floor(p.y); if (u_originalColors == true) { - color = mix(origColor, u_colorBack.rgb, line); + color = mix(origColor, u_colorBack.rgb, line); } else { color = mix(u_colorFront.rgb, u_colorBack.rgb, line); } + + float grainOverlay = valueNoise(rotate(grainUV, 1.) + vec2(3.)); + grainOverlay = mix(grainOverlay, valueNoise(rotate(grainUV, 2.) + vec2(-1.)), .5); + grainOverlay = pow(grainOverlay, 1.3); + + float grainOverlayV = grainOverlay * 2. - 1.; + vec3 grainOverlayColor = vec3(step(0., grainOverlayV)); + float grainOverlayStrength = u_grainOverlay * abs(grainOverlayV); + grainOverlayStrength = pow(grainOverlayStrength, .8); + color = mix(color, grainOverlayColor, .35 * grainOverlayStrength); + + opacity += .5 * grainOverlayStrength; + opacity = clamp(opacity, 0., 1.); fragColor = vec4(color, 1.); } @@ -283,6 +298,7 @@ export interface HalftoneLinesUniforms extends ShaderSizingUniforms { u_inverted: boolean; u_grainMixer: number; u_grainOverlay: number; + u_grainSize: number; } export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionParams { @@ -300,4 +316,5 @@ export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionPar inverted?: boolean; grainMixer?: number; grainOverlay?: number; + grainSize?: number; } \ No newline at end of file From 14a18bcdbe0f79f5f74b808efad283ddcc320024 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 28 Nov 2025 22:27:52 +0100 Subject: [PATCH 53/84] adj --- packages/shaders/src/shaders/halftone-lines.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index 5ffbaa623..f362bc090 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -232,7 +232,7 @@ void main() { float highDistort = clamp(distortAmount * 5.0, 0.0, 1.0); float w = mix(.5 * u_stripeWidth, 0., lum); - w = clamp(w, aa, .5 - aa); + w = clamp(w, 0., .5 - aa); float loSharp = w; float loBlurry = 0.; @@ -244,9 +244,9 @@ void main() { vec2 grainSize = mix(2000., 200., u_grainSize) * vec2(1., 1. / u_imageAspectRatio); vec2 grainUV = getImageUV(uvNormalised, grainSize); - float grain = valueNoise(grainUV) + .3 * u_grainMixer; + float grain = valueNoise(grainUV) + .3 * pow(u_grainMixer, 3.); grain = smoothstep(.55, .9, grain); - grain *= u_grainMixer; + grain *= pow(u_grainMixer, 3.); stripeDist.y += .5 * grain; From 57b6228d9ece0c0d11bbb446b41870e929708ad6 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 29 Nov 2025 23:06:04 +0200 Subject: [PATCH 54/84] start on grid types --- .../src/app/(shaders)/halftone-lines/page.tsx | 7 ++- .../src/shaders/halftone-lines.tsx | 11 +++- packages/shaders/src/index.ts | 2 + packages/shaders/src/shaders/halftone-dots.ts | 2 +- .../shaders/src/shaders/halftone-lines.ts | 57 +++++++++++++------ 5 files changed, 57 insertions(+), 22 deletions(-) diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index ad1a72c46..ebaeda5d5 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -5,7 +5,7 @@ import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { halftoneLinesMeta, ShaderFit } from '@paper-design/shaders'; +import { HalftoneLinesGrid, HalftoneLinesGrids, halftoneLinesMeta, ShaderFit} from '@paper-design/shaders'; import { levaImageButton } from '@/helpers/leva-image-button'; import { useState, useEffect, useCallback } from 'react'; import { toHsla } from '@/helpers/color-utils'; @@ -72,6 +72,11 @@ const HalftoneLinesWithControls = () => { return { stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, smoothness: { value: defaults.smoothness, min: 0, max: halftoneLinesMeta.maxBlurRadius, order: 202 }, + grid: { + value: defaults.grid, + options: Object.keys(HalftoneLinesGrids) as HalftoneLinesGrid[], + order: 201, + }, angleDistortion: { value: defaults.angleDistortion, min: 0, max: 1, order: 204 }, noiseDistortion: { value: defaults.noiseDistortion, min: 0, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index 7fc21a965..085074755 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -8,9 +8,11 @@ import { type HalftoneLinesParams, defaultObjectSizing, type ImageShaderPreset, + HalftoneLinesGrids, } from '@paper-design/shaders'; export interface HalftoneLinesProps extends ShaderComponentProps, HalftoneLinesParams {} + type HalftoneLinesPreset = ImageShaderPreset; export const defaultPreset: HalftoneLinesPreset = { @@ -22,11 +24,12 @@ export const defaultPreset: HalftoneLinesPreset = { frame: 0, colorBack: '#615681', colorFront: '#ffffff', - stripeWidth: 0, + grid: 'radial', + stripeWidth: 0.5, smoothness: 10, size: 40, angleDistortion: 0.4, - noiseDistortion: 0, + noiseDistortion: 0.1, angle: 0, contrast: 0.7, originalColors: false, @@ -46,6 +49,7 @@ export const classicPreset: HalftoneLinesPreset = { frame: 0, colorBack: '#ffffff', colorFront: '#000000', + grid: 'lines', stripeWidth: 1, smoothness: 10, size: 60, @@ -70,6 +74,7 @@ export const HalftoneLines: React.FC = memo(function Halfton speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, image = '', + grid = defaultPreset.params.grid, angleDistortion = defaultPreset.params.angleDistortion, noiseDistortion = defaultPreset.params.noiseDistortion, stripeWidth = defaultPreset.params.stripeWidth, @@ -101,6 +106,7 @@ export const HalftoneLines: React.FC = memo(function Halfton u_colorFront: getShaderColorFromString(colorFront), u_image: image, + u_grid: HalftoneLinesGrids[grid], u_angleDistortion: angleDistortion, u_noiseDistortion: noiseDistortion, u_stripeWidth: stripeWidth, @@ -132,7 +138,6 @@ export const HalftoneLines: React.FC = memo(function Halfton speed={speed} frame={frame} fragmentShader={halftoneLinesFragmentShader} - // mipmaps={ ['u_image'] } uniforms={uniforms} /> ); diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index 9efca1a3d..29d3cc75a 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -229,10 +229,12 @@ export { } from './shaders/folds.js'; export { + HalftoneLinesGrids, halftoneLinesMeta, halftoneLinesFragmentShader, type HalftoneLinesParams, type HalftoneLinesUniforms, + type HalftoneLinesGrid, } from './shaders/halftone-lines.js'; export { diff --git a/packages/shaders/src/shaders/halftone-dots.ts b/packages/shaders/src/shaders/halftone-dots.ts index d5fef02b5..d32c2cf61 100644 --- a/packages/shaders/src/shaders/halftone-dots.ts +++ b/packages/shaders/src/shaders/halftone-dots.ts @@ -340,7 +340,7 @@ void main() { float grainOverlayStrength = u_grainOverlay * abs(grainOverlayV); grainOverlayStrength = pow(grainOverlayStrength, .8); color = mix(color, grainOverlayColor, .5 * grainOverlayStrength); - + opacity += .5 * grainOverlayStrength; opacity = clamp(opacity, 0., 1.); diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index f362bc090..dba9e69cb 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -42,6 +42,7 @@ uniform sampler2D u_image; uniform mediump float u_imageAspectRatio; uniform float u_size; +uniform float u_grid; uniform float u_grainMixer; uniform float u_grainOverlay; uniform float u_grainSize; @@ -212,22 +213,34 @@ void main() { lum = mix(1., lum, frame); uv = v_objectUV; - vec2 p = uv; + vec2 uvGrid = uv; float angle = -u_angle * PI / 180.; - p = rotate(p, angle + u_angleDistortion * lum); - p *= u_size; - - vec2 pBase = v_objectUV * u_size; - float aaBase = fwidth(pBase.y); + uvGrid = rotate(uvGrid, angle + u_angleDistortion * lum); + uvGrid *= u_size; + + vec2 pBase = uv * u_size; + float baseRadius = length(pBase); + float aaBase = fwidth(length(uv * u_size)); + if (u_grid == 0.) { + aaBase = fwidth(pBase.y); + } + float radius = length(uvGrid); float n = doubleSNoise(uv + 100., u_time); - p.y += .4 * n * lum * u_noiseDistortion * u_size; - - vec2 stripeMap = abs(fract(p) - .5); - vec2 stripeDist = abs(stripeMap); - vec2 stripeSign = sign(stripeMap); + radius += .4 * n * lum * u_noiseDistortion * u_size; + uvGrid.y += .4 * n * lum * u_noiseDistortion * u_size; + + float stripeMap = abs(fract(radius) - .5); + float stripeDist = stripeMap; + if (u_grid == 0.) { + stripeMap = abs(fract(uvGrid.y) - .5); + stripeDist = abs(stripeMap); + } - float aa = fwidth(p).y; + float aa = fwidth(radius); + if (u_grid == 0.) { + aa = fwidth(uvGrid.y); + } float distortAmount = max(aa - aaBase, 0.0); float highDistort = clamp(distortAmount * 5.0, 0.0, 1.0); @@ -248,9 +261,9 @@ void main() { grain = smoothstep(.55, .9, grain); grain *= pow(u_grainMixer, 3.); - stripeDist.y += .5 * grain; - - float line = sst(lo, hi, stripeDist.y); + stripeDist += .5 * grain; + + float line = sst(lo, hi, stripeDist); line = mix(1., line, frame); line = clamp(line, 0., 1.); @@ -258,7 +271,8 @@ void main() { vec3 color = vec3(0.); float opacity = 1.; - float stripeId = floor(p.y); + float stripeId = floor(radius); +// float stripeId = floor(uvGrid.y); if (u_originalColors == true) { color = mix(origColor, u_colorBack.rgb, line); @@ -287,6 +301,7 @@ export interface HalftoneLinesUniforms extends ShaderSizingUniforms { u_colorBack: [number, number, number, number]; u_colorFront: [number, number, number, number]; u_image: HTMLImageElement | string | undefined; + u_grid: (typeof HalftoneLinesGrids)[HalftoneLinesGrid]; u_stripeWidth: number; u_smoothness: number; u_size: number; @@ -305,6 +320,7 @@ export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionPar colorBack?: string; colorFront?: string; image?: HTMLImageElement | string | undefined; + grid?: HalftoneLinesGrid; stripeWidth?: number; smoothness?: number; size?: number; @@ -317,4 +333,11 @@ export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionPar grainMixer?: number; grainOverlay?: number; grainSize?: number; -} \ No newline at end of file +} + +export const HalftoneLinesGrids = { + lines: 0, + radial: 1, +} as const; + +export type HalftoneLinesGrid = keyof typeof HalftoneLinesGrids; From 606ddf355a48fd0e15ac2c254afa8f8de5464fc5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 30 Nov 2025 16:37:14 +0400 Subject: [PATCH 55/84] new line params, polishing --- .../src/app/(shaders)/halftone-lines/page.tsx | 4 +- .../src/shaders/halftone-lines.tsx | 8 ++ .../shaders/src/shaders/halftone-lines.ts | 75 ++++++++----------- 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index ebaeda5d5..e7274b4e2 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -85,7 +85,9 @@ const HalftoneLinesWithControls = () => { originalColors: { value: defaults.originalColors, order: 102 }, inverted: { value: defaults.inverted, order: 201 }, size: { value: defaults.size, min: 0.01, max: 150, step: 0.1, order: 300 }, - contrast: { value: defaults.contrast, min: 0.01, max: 1, order: 302 }, + thinLines: { value: defaults.thinLines, order: 301 }, + allowOverflow: { value: defaults.allowOverflow, order: 302 }, + contrast: { value: defaults.contrast, min: 0.01, max: 1, order: 303 }, grainMixer: { value: defaults.grainMixer, min: 0, max: 1, order: 350 }, grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 351 }, grainSize: { value: defaults.grainSize, min: 0, max: 1, order: 352 }, diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index 085074755..b81f74515 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -28,6 +28,8 @@ export const defaultPreset: HalftoneLinesPreset = { stripeWidth: 0.5, smoothness: 10, size: 40, + thinLines: true, + allowOverflow: false, angleDistortion: 0.4, noiseDistortion: 0.1, angle: 0, @@ -53,6 +55,8 @@ export const classicPreset: HalftoneLinesPreset = { stripeWidth: 1, smoothness: 10, size: 60, + thinLines: true, + allowOverflow: false, angleDistortion: 0, noiseDistortion: 0, angle: 0, @@ -80,6 +84,8 @@ export const HalftoneLines: React.FC = memo(function Halfton stripeWidth = defaultPreset.params.stripeWidth, smoothness = defaultPreset.params.smoothness, size = defaultPreset.params.size, + thinLines = defaultPreset.params.thinLines, + allowOverflow = defaultPreset.params.allowOverflow, angle = defaultPreset.params.angle, contrast = defaultPreset.params.contrast, originalColors = defaultPreset.params.originalColors, @@ -112,6 +118,8 @@ export const HalftoneLines: React.FC = memo(function Halfton u_stripeWidth: stripeWidth, u_smoothness: smoothness, u_size: size, + u_thinLines: thinLines, + u_allowOverflow: allowOverflow, u_angle: angle, u_contrast: contrast, u_originalColors: originalColors, diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index dba9e69cb..6b39a6576 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -42,6 +42,8 @@ uniform sampler2D u_image; uniform mediump float u_imageAspectRatio; uniform float u_size; +uniform bool u_thinLines; +uniform bool u_allowOverflow; uniform float u_grid; uniform float u_grainMixer; uniform float u_grainOverlay; @@ -213,67 +215,50 @@ void main() { lum = mix(1., lum, frame); uv = v_objectUV; - vec2 uvGrid = uv; + float n = doubleSNoise(uv + 100., u_time); + + vec2 uvGrid = v_objectUV; float angle = -u_angle * PI / 180.; - uvGrid = rotate(uvGrid, angle + u_angleDistortion * lum); + uvGrid = rotate(uvGrid, angle + u_angleDistortion * (1. - lum)); + uvGrid += .4 * n * lum * u_noiseDistortion; uvGrid *= u_size; - vec2 pBase = uv * u_size; - float baseRadius = length(pBase); - float aaBase = fwidth(length(uv * u_size)); - if (u_grid == 0.) { - aaBase = fwidth(pBase.y); + float gridLine = uvGrid.y; + if (u_grid == 1.) { + gridLine = length(uvGrid); } - float radius = length(uvGrid); + float stripeMap = abs(fract(gridLine) - .5); + float aa = fwidth(gridLine); - float n = doubleSNoise(uv + 100., u_time); - radius += .4 * n * lum * u_noiseDistortion * u_size; - uvGrid.y += .4 * n * lum * u_noiseDistortion * u_size; - - float stripeMap = abs(fract(radius) - .5); - float stripeDist = stripeMap; - if (u_grid == 0.) { - stripeMap = abs(fract(uvGrid.y) - .5); - stripeDist = abs(stripeMap); + float w = mix(.5 * u_stripeWidth, 0., lum); + float wLo = .0; + float wHi = .5 + aa; + if (u_allowOverflow == false) { + wHi -= 2. * aa; } - - float aa = fwidth(radius); - if (u_grid == 0.) { - aa = fwidth(uvGrid.y); + if (u_thinLines == false) { + wLo += .5 * aa; + wHi -= .5 * aa; } - float distortAmount = max(aa - aaBase, 0.0); - float highDistort = clamp(distortAmount * 5.0, 0.0, 1.0); - - float w = mix(.5 * u_stripeWidth, 0., lum); - w = clamp(w, 0., .5 - aa); - - float loSharp = w; - float loBlurry = 0.; - float hiSharp = w + aa; - float hiBlurry = w + aa; - - float lo = mix(loSharp, loBlurry, highDistort); - float hi = mix(hiSharp, hiBlurry, highDistort); + w = clamp(w, wLo, wHi); + + float lo = w; + float hi = w + aa; vec2 grainSize = mix(2000., 200., u_grainSize) * vec2(1., 1. / u_imageAspectRatio); vec2 grainUV = getImageUV(uvNormalised, grainSize); float grain = valueNoise(grainUV) + .3 * pow(u_grainMixer, 3.); grain = smoothstep(.55, .9, grain); - grain *= pow(u_grainMixer, 3.); - - stripeDist += .5 * grain; - - float line = sst(lo, hi, stripeDist); + grain *= .5 * pow(u_grainMixer, 3.); + stripeMap += .5 * grain; + float line = sst(lo, hi, stripeMap); line = mix(1., line, frame); line = clamp(line, 0., 1.); vec3 color = vec3(0.); float opacity = 1.; - float stripeId = floor(radius); -// float stripeId = floor(uvGrid.y); - if (u_originalColors == true) { color = mix(origColor, u_colorBack.rgb, line); } else { @@ -293,7 +278,7 @@ void main() { opacity += .5 * grainOverlayStrength; opacity = clamp(opacity, 0., 1.); - fragColor = vec4(color, 1.); + fragColor = vec4(color, opacity); } `; @@ -305,6 +290,8 @@ export interface HalftoneLinesUniforms extends ShaderSizingUniforms { u_stripeWidth: number; u_smoothness: number; u_size: number; + u_thinLines: boolean; + u_allowOverflow: boolean; u_angleDistortion: number; u_noiseDistortion: number; u_angle: number; @@ -324,6 +311,8 @@ export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionPar stripeWidth?: number; smoothness?: number; size?: number; + thinLines?: boolean; + allowOverflow?: boolean; angleDistortion?: number; noiseDistortion?: number; angle?: number; From 1190fff08347546f402126f45ed6402ca8567a14 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 30 Nov 2025 21:40:09 +0400 Subject: [PATCH 56/84] consistent noise/angle distortion --- .../shaders/src/shaders/halftone-lines.ts | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index 6b39a6576..3494802de 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -213,24 +213,37 @@ void main() { float frame = getImgFrame(v_imageUV, 0.); lum = mix(1., lum, frame); + lum = 1. - lum; uv = v_objectUV; float n = doubleSNoise(uv + 100., u_time); vec2 uvGrid = v_objectUV; - float angle = -u_angle * PI / 180.; - uvGrid = rotate(uvGrid, angle + u_angleDistortion * (1. - lum)); - uvGrid += .4 * n * lum * u_noiseDistortion; + uvGrid += .15 * n * lum * u_noiseDistortion; uvGrid *= u_size; - float gridLine = uvGrid.y; + float gridLine; + + float angleOffset = u_angle * PI / 180.; + float angleDistort = u_angleDistortion * lum; + if (u_grid == 1.) { - gridLine = length(uvGrid); + float r = length(uvGrid); + float a = atan(uvGrid.y, uvGrid.x); + float freq = 4.; + float angularWarp = sin(a * freq + angleOffset) + .5 * cos(a * 1.5 * freq + angleOffset + 2.); + angleDistort *= smoothstep(0., .1, r / u_size); + gridLine = r + angleDistort * angularWarp; + + } else { + uvGrid = rotate(uvGrid, angleOffset + angleDistort); + gridLine = uvGrid.y; } + float stripeMap = abs(fract(gridLine) - .5); float aa = fwidth(gridLine); - float w = mix(.5 * u_stripeWidth, 0., lum); + float w = mix(0., .5 * u_stripeWidth, lum); float wLo = .0; float wHi = .5 + aa; if (u_allowOverflow == false) { @@ -242,9 +255,6 @@ void main() { } w = clamp(w, wLo, wHi); - float lo = w; - float hi = w + aa; - vec2 grainSize = mix(2000., 200., u_grainSize) * vec2(1., 1. / u_imageAspectRatio); vec2 grainUV = getImageUV(uvNormalised, grainSize); float grain = valueNoise(grainUV) + .3 * pow(u_grainMixer, 3.); @@ -252,6 +262,8 @@ void main() { grain *= .5 * pow(u_grainMixer, 3.); stripeMap += .5 * grain; + float lo = w; + float hi = w + aa; float line = sst(lo, hi, stripeMap); line = mix(1., line, frame); line = clamp(line, 0., 1.); From d49d0e9337fcab3ccd021ec73d3e9da9346229b8 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 30 Nov 2025 22:46:31 +0400 Subject: [PATCH 57/84] progress --- .../src/app/(shaders)/halftone-lines/page.tsx | 14 ++++--- .../src/shaders/halftone-lines.tsx | 16 ++++++-- .../shaders/src/shaders/halftone-lines.ts | 37 ++++++++++--------- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index e7274b4e2..b6f0a7d99 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -77,9 +77,11 @@ const HalftoneLinesWithControls = () => { options: Object.keys(HalftoneLinesGrids) as HalftoneLinesGrid[], order: 201, }, + gridOffsetX: { value: defaults.gridOffsetX, min: -1, max: 1, order: 204 }, + gridOffsetY: { value: defaults.gridOffsetY, min: -1, max: 1, order: 204 }, angleDistortion: { value: defaults.angleDistortion, min: 0, max: 1, order: 204 }, noiseDistortion: { value: defaults.noiseDistortion, min: 0, max: 1, order: 205 }, - angle: { value: defaults.angle, min: 0, max: 360, order: 206 }, + gridRotation: { value: defaults.gridRotation, min: 0, max: 360, order: 206 }, colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, originalColors: { value: defaults.originalColors, order: 102 }, @@ -92,11 +94,11 @@ const HalftoneLinesWithControls = () => { grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 351 }, grainSize: { value: defaults.grainSize, min: 0, max: 1, order: 352 }, scale: { value: defaults.scale, min: 0.1, max: 10, order: 400 }, - offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 401 }, - offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 402 }, - originX: { value: defaults.originX, min: 0, max: 1, order: 411 }, - originY: { value: defaults.originY, min: 0, max: 1, order: 412 }, - rotation: { value: defaults.rotation, min: 0, max: 360, order: 420 }, + // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 401 }, + // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 402 }, + // originX: { value: defaults.originX, min: 0, max: 1, order: 411 }, + // originY: { value: defaults.originY, min: 0, max: 1, order: 412 }, + // rotation: { value: defaults.rotation, min: 0, max: 360, order: 420 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 450 }, Image: folder( { diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index b81f74515..ad945c79e 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -25,6 +25,8 @@ export const defaultPreset: HalftoneLinesPreset = { colorBack: '#615681', colorFront: '#ffffff', grid: 'radial', + gridOffsetX: -.5, + gridOffsetY: -.5, stripeWidth: 0.5, smoothness: 10, size: 40, @@ -32,7 +34,7 @@ export const defaultPreset: HalftoneLinesPreset = { allowOverflow: false, angleDistortion: 0.4, noiseDistortion: 0.1, - angle: 0, + gridRotation: 0, contrast: 0.7, originalColors: false, inverted: false, @@ -52,6 +54,8 @@ export const classicPreset: HalftoneLinesPreset = { colorBack: '#ffffff', colorFront: '#000000', grid: 'lines', + gridOffsetX: -.5, + gridOffsetY: -.5, stripeWidth: 1, smoothness: 10, size: 60, @@ -59,7 +63,7 @@ export const classicPreset: HalftoneLinesPreset = { allowOverflow: false, angleDistortion: 0, noiseDistortion: 0, - angle: 0, + gridRotation: 0, contrast: 0.7, originalColors: false, inverted: false, @@ -79,6 +83,8 @@ export const HalftoneLines: React.FC = memo(function Halfton frame = defaultPreset.params.frame, image = '', grid = defaultPreset.params.grid, + gridOffsetX = defaultPreset.params.gridOffsetX, + gridOffsetY = defaultPreset.params.gridOffsetY, angleDistortion = defaultPreset.params.angleDistortion, noiseDistortion = defaultPreset.params.noiseDistortion, stripeWidth = defaultPreset.params.stripeWidth, @@ -86,7 +92,7 @@ export const HalftoneLines: React.FC = memo(function Halfton size = defaultPreset.params.size, thinLines = defaultPreset.params.thinLines, allowOverflow = defaultPreset.params.allowOverflow, - angle = defaultPreset.params.angle, + gridRotation = defaultPreset.params.gridRotation, contrast = defaultPreset.params.contrast, originalColors = defaultPreset.params.originalColors, inverted = defaultPreset.params.inverted, @@ -113,6 +119,8 @@ export const HalftoneLines: React.FC = memo(function Halfton u_image: image, u_grid: HalftoneLinesGrids[grid], + u_gridOffsetX: gridOffsetX, + u_gridOffsetY: gridOffsetY, u_angleDistortion: angleDistortion, u_noiseDistortion: noiseDistortion, u_stripeWidth: stripeWidth, @@ -120,7 +128,7 @@ export const HalftoneLines: React.FC = memo(function Halfton u_size: size, u_thinLines: thinLines, u_allowOverflow: allowOverflow, - u_angle: angle, + u_gridRotation: gridRotation, u_contrast: contrast, u_originalColors: originalColors, u_inverted: inverted, diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index 3494802de..e12ac6e33 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -45,6 +45,8 @@ uniform float u_size; uniform bool u_thinLines; uniform bool u_allowOverflow; uniform float u_grid; +uniform float u_gridOffsetX; +uniform float u_gridOffsetY; uniform float u_grainMixer; uniform float u_grainOverlay; uniform float u_grainSize; @@ -55,7 +57,7 @@ uniform float u_stripeWidth; uniform float u_smoothness; uniform float u_angleDistortion; uniform float u_noiseDistortion; -uniform float u_angle; +uniform float u_gridRotation; ${ sizingVariablesDeclaration } @@ -115,11 +117,6 @@ vec2 getImageUV(vec2 uv, vec2 extraScale) { return imageUV; } -float doubleSNoise(vec2 uv, float t) { - float noise = .5 * snoise(uv - vec2(0., .3 * t)); - noise += .5 * snoise(2. * uv + vec2(0., .32 * t)); - return noise; -} float getImgFrame(vec2 uv, float th) { float frame = 1.; @@ -216,7 +213,7 @@ void main() { lum = 1. - lum; uv = v_objectUV; - float n = doubleSNoise(uv + 100., u_time); + float n = snoise(3. * uv + 100.); vec2 uvGrid = v_objectUV; uvGrid += .15 * n * lum * u_noiseDistortion; @@ -224,19 +221,21 @@ void main() { float gridLine; - float angleOffset = u_angle * PI / 180.; + float angleOffset = u_gridRotation * PI / 180.; float angleDistort = u_angleDistortion * lum; + vec2 gridOffset = -vec2(u_gridOffsetX, u_gridOffsetY); if (u_grid == 1.) { - float r = length(uvGrid); - float a = atan(uvGrid.y, uvGrid.x); - float freq = 4.; - float angularWarp = sin(a * freq + angleOffset) + .5 * cos(a * 1.5 * freq + angleOffset + 2.); - angleDistort *= smoothstep(0., .1, r / u_size); - gridLine = r + angleDistort * angularWarp; - + uvGrid -= u_size * gridOffset; + uvGrid = rotate(uvGrid, angleOffset); + uvGrid += u_size * gridOffset; + + uvGrid = rotate(uvGrid, angleDistort); + uvGrid += u_size * gridOffset; + gridLine = length(uvGrid); } else { uvGrid = rotate(uvGrid, angleOffset + angleDistort); + uvGrid += gridOffset; gridLine = uvGrid.y; } @@ -299,6 +298,8 @@ export interface HalftoneLinesUniforms extends ShaderSizingUniforms { u_colorFront: [number, number, number, number]; u_image: HTMLImageElement | string | undefined; u_grid: (typeof HalftoneLinesGrids)[HalftoneLinesGrid]; + u_gridOffsetX: number; + u_gridOffsetY: number; u_stripeWidth: number; u_smoothness: number; u_size: number; @@ -306,7 +307,7 @@ export interface HalftoneLinesUniforms extends ShaderSizingUniforms { u_allowOverflow: boolean; u_angleDistortion: number; u_noiseDistortion: number; - u_angle: number; + u_gridRotation: number; u_contrast: number; u_originalColors: boolean; u_inverted: boolean; @@ -320,6 +321,8 @@ export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionPar colorFront?: string; image?: HTMLImageElement | string | undefined; grid?: HalftoneLinesGrid; + gridOffsetX?: number; + gridOffsetY?: number; stripeWidth?: number; smoothness?: number; size?: number; @@ -327,7 +330,7 @@ export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionPar allowOverflow?: boolean; angleDistortion?: number; noiseDistortion?: number; - angle?: number; + gridRotation?: number; contrast?: number; originalColors?: boolean; inverted?: boolean; From 978c99d8b86b94bfb23ea6a5459fc76f9fd661ee Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 1 Dec 2025 17:29:11 +0400 Subject: [PATCH 58/84] splitting grainSize --- .../src/app/(shaders)/halftone-lines/page.tsx | 3 ++- .../src/shaders/halftone-lines.tsx | 12 ++++++---- .../shaders/src/shaders/halftone-lines.ts | 24 +++++++++++-------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index b6f0a7d99..011cb5d87 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -91,8 +91,9 @@ const HalftoneLinesWithControls = () => { allowOverflow: { value: defaults.allowOverflow, order: 302 }, contrast: { value: defaults.contrast, min: 0.01, max: 1, order: 303 }, grainMixer: { value: defaults.grainMixer, min: 0, max: 1, order: 350 }, + grainMixerSize: { value: defaults.grainMixerSize, min: 0, max: 1, order: 352 }, grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 351 }, - grainSize: { value: defaults.grainSize, min: 0, max: 1, order: 352 }, + grainOverlaySize: { value: defaults.grainOverlaySize, min: 0, max: 1, order: 352 }, scale: { value: defaults.scale, min: 0.1, max: 10, order: 400 }, // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 401 }, // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 402 }, diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index ad945c79e..df5748f7d 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -39,8 +39,9 @@ export const defaultPreset: HalftoneLinesPreset = { originalColors: false, inverted: false, grainMixer: 0.2, + grainMixerSize: 1, grainOverlay: 0, - grainSize: 0.5, + grainOverlaySize: 0.5, }, }; @@ -68,8 +69,9 @@ export const classicPreset: HalftoneLinesPreset = { originalColors: false, inverted: false, grainMixer: 0, + grainMixerSize: 1, grainOverlay: 0, - grainSize: 0.5, + grainOverlaySize: 0.5, }, }; @@ -97,8 +99,9 @@ export const HalftoneLines: React.FC = memo(function Halfton originalColors = defaultPreset.params.originalColors, inverted = defaultPreset.params.inverted, grainMixer = defaultPreset.params.grainMixer, + grainMixerSize = defaultPreset.params.grainMixerSize, grainOverlay = defaultPreset.params.grainOverlay, - grainSize = defaultPreset.params.grainSize, + grainOverlaySize = defaultPreset.params.grainOverlaySize, // Sizing props fit = defaultPreset.params.fit, @@ -133,8 +136,9 @@ export const HalftoneLines: React.FC = memo(function Halfton u_originalColors: originalColors, u_inverted: inverted, u_grainMixer: grainMixer, + u_grainMixerSize: grainMixerSize, u_grainOverlay: grainOverlay, - u_grainSize: grainSize, + u_grainOverlaySize: grainOverlaySize, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index e12ac6e33..7a44b6b0d 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -48,8 +48,9 @@ uniform float u_grid; uniform float u_gridOffsetX; uniform float u_gridOffsetY; uniform float u_grainMixer; +uniform float u_grainMixerSize; uniform float u_grainOverlay; -uniform float u_grainSize; +uniform float u_grainOverlaySize; uniform bool u_straight; uniform bool u_originalColors; uniform bool u_inverted; @@ -234,8 +235,8 @@ void main() { uvGrid += u_size * gridOffset; gridLine = length(uvGrid); } else { - uvGrid = rotate(uvGrid, angleOffset + angleDistort); uvGrid += gridOffset; + uvGrid = rotate(uvGrid, angleOffset + angleDistort); gridLine = uvGrid.y; } @@ -254,9 +255,11 @@ void main() { } w = clamp(w, wLo, wHi); - vec2 grainSize = mix(2000., 200., u_grainSize) * vec2(1., 1. / u_imageAspectRatio); - vec2 grainUV = getImageUV(uvNormalised, grainSize); - float grain = valueNoise(grainUV) + .3 * pow(u_grainMixer, 3.); + vec2 grainMixerSize = mix(1000., 25., u_grainMixerSize) * vec2(1., 1. / u_imageAspectRatio); + vec2 grainOverlaySize = mix(2000., 200., u_grainOverlaySize) * vec2(1., 1. / u_imageAspectRatio); + vec2 grainMixerUV = getImageUV(uvNormalised, grainMixerSize); + vec2 grainOverlayUV = getImageUV(uvNormalised, grainOverlaySize); + float grain = valueNoise(grainMixerUV) + .3 * pow(u_grainMixer, 3.); grain = smoothstep(.55, .9, grain); grain *= .5 * pow(u_grainMixer, 3.); stripeMap += .5 * grain; @@ -276,10 +279,9 @@ void main() { color = mix(u_colorFront.rgb, u_colorBack.rgb, line); } - float grainOverlay = valueNoise(rotate(grainUV, 1.) + vec2(3.)); - grainOverlay = mix(grainOverlay, valueNoise(rotate(grainUV, 2.) + vec2(-1.)), .5); + float grainOverlay = valueNoise(rotate(grainOverlayUV, 1.) + vec2(3.)); + grainOverlay = mix(grainOverlay, valueNoise(rotate(grainOverlayUV, 2.) + vec2(-1.)), .5); grainOverlay = pow(grainOverlay, 1.3); - float grainOverlayV = grainOverlay * 2. - 1.; vec3 grainOverlayColor = vec3(step(0., grainOverlayV)); float grainOverlayStrength = u_grainOverlay * abs(grainOverlayV); @@ -312,8 +314,9 @@ export interface HalftoneLinesUniforms extends ShaderSizingUniforms { u_originalColors: boolean; u_inverted: boolean; u_grainMixer: number; + u_grainMixerSize: number; u_grainOverlay: number; - u_grainSize: number; + u_grainOverlaySize: number; } export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionParams { @@ -335,8 +338,9 @@ export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionPar originalColors?: boolean; inverted?: boolean; grainMixer?: number; + grainMixerSize?: number; grainOverlay?: number; - grainSize?: number; + grainOverlaySize?: number; } export const HalftoneLinesGrids = { From 3a0c33a5d92530e49e5d20b8dfbb869e6ea8edf1 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 1 Dec 2025 17:34:11 +0400 Subject: [PATCH 59/84] clean-up --- docs/src/app/(shaders)/blobs-logo/page.tsx | 10 +- docs/src/app/(shaders)/fluted-glass/page.tsx | 2 +- docs/src/app/(shaders)/folds/page.tsx | 11 +- docs/src/app/(shaders)/gem-smoke/page.tsx | 10 +- docs/src/app/(shaders)/halftone-dots/page.tsx | 8 +- .../src/app/(shaders)/halftone-lines/page.tsx | 2 +- docs/src/shader-defs/halftone-dots-def.ts | 2 +- packages/shaders-react/src/index.ts | 1 - packages/shaders-react/src/shaders/folds.tsx | 1 - .../src/shaders/halftone-lines.tsx | 8 +- packages/shaders/src/shaders/blobs-logo.ts | 88 +------------- packages/shaders/src/shaders/folds.ts | 16 +-- .../shaders/src/shaders/halftone-lines.ts | 108 +++++++++--------- 13 files changed, 95 insertions(+), 172 deletions(-) diff --git a/docs/src/app/(shaders)/blobs-logo/page.tsx b/docs/src/app/(shaders)/blobs-logo/page.tsx index 80e488320..edd20e692 100644 --- a/docs/src/app/(shaders)/blobs-logo/page.tsx +++ b/docs/src/app/(shaders)/blobs-logo/page.tsx @@ -14,7 +14,7 @@ import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; import { blobsLogoDef } from '@/shader-defs/blobs-logo-def'; import { toHsla } from '@/helpers/color-utils'; -import { useColors } from "@/helpers/use-colors"; +import { useColors } from '@/helpers/use-colors'; // Override just for the docs, we keep it transparent in the preset // blobsLogoPresets[0].params.colorBack = '#000000'; @@ -129,10 +129,14 @@ const BlobsLogoWithControls = () => { <> - + - + ); }; diff --git a/docs/src/app/(shaders)/fluted-glass/page.tsx b/docs/src/app/(shaders)/fluted-glass/page.tsx index aee1d30d2..1852fa7b8 100644 --- a/docs/src/app/(shaders)/fluted-glass/page.tsx +++ b/docs/src/app/(shaders)/fluted-glass/page.tsx @@ -126,7 +126,7 @@ const FlutedGlassWithControls = () => { -
+
Click to change the sample image
diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/folds/page.tsx index c0962d3e7..6060902bd 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/folds/page.tsx @@ -14,7 +14,7 @@ import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; import { foldsDef } from '@/shader-defs/folds-def'; import { toHsla } from '@/helpers/color-utils'; -import { useColors } from "@/helpers/use-colors"; +import { useColors } from '@/helpers/use-colors'; // Override just for the docs, we keep it transparent in the preset // foldsPresets[0].params.colorBack = '#000000'; @@ -57,7 +57,6 @@ const imageFiles = [ 'diamond.svg', ] as const; - const FoldsWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); const [image, setImage] = useState('/images/logos/diamond.svg'); @@ -137,10 +136,14 @@ const FoldsWithControls = () => { <> - + - + ); }; diff --git a/docs/src/app/(shaders)/gem-smoke/page.tsx b/docs/src/app/(shaders)/gem-smoke/page.tsx index e9a101429..74ad8088b 100644 --- a/docs/src/app/(shaders)/gem-smoke/page.tsx +++ b/docs/src/app/(shaders)/gem-smoke/page.tsx @@ -14,7 +14,7 @@ import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; import { gemSmokeDef } from '@/shader-defs/gem-smoke-def'; import { toHsla } from '@/helpers/color-utils'; -import { useColors } from "@/helpers/use-colors"; +import { useColors } from '@/helpers/use-colors'; // Override just for the docs, we keep it transparent in the preset // gemSmokePresets[0].params.colorBack = '#000000'; @@ -129,10 +129,14 @@ const GemSmokeWithControls = () => { <> - + - + ); }; diff --git a/docs/src/app/(shaders)/halftone-dots/page.tsx b/docs/src/app/(shaders)/halftone-dots/page.tsx index 56e86b9a7..4922f7b21 100644 --- a/docs/src/app/(shaders)/halftone-dots/page.tsx +++ b/docs/src/app/(shaders)/halftone-dots/page.tsx @@ -5,7 +5,13 @@ import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { HalftoneDotsType, HalftoneDotsTypes, HalftoneDotsGrid, HalftoneDotsGrids, ShaderFit } from '@paper-design/shaders'; +import { + HalftoneDotsType, + HalftoneDotsTypes, + HalftoneDotsGrid, + HalftoneDotsGrids, + ShaderFit, +} from '@paper-design/shaders'; import { levaImageButton } from '@/helpers/leva-image-button'; import { useState, useEffect, useCallback } from 'react'; import { toHsla } from '@/helpers/color-utils'; diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index 011cb5d87..83ef95724 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -5,7 +5,7 @@ import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { HalftoneLinesGrid, HalftoneLinesGrids, halftoneLinesMeta, ShaderFit} from '@paper-design/shaders'; +import { HalftoneLinesGrid, HalftoneLinesGrids, halftoneLinesMeta, ShaderFit } from '@paper-design/shaders'; import { levaImageButton } from '@/helpers/leva-image-button'; import { useState, useEffect, useCallback } from 'react'; import { toHsla } from '@/helpers/color-utils'; diff --git a/docs/src/shader-defs/halftone-dots-def.ts b/docs/src/shader-defs/halftone-dots-def.ts index da0e5bd56..031541ddc 100644 --- a/docs/src/shader-defs/halftone-dots-def.ts +++ b/docs/src/shader-defs/halftone-dots-def.ts @@ -1,6 +1,6 @@ import { halftoneDotsPresets } from '@paper-design/shaders-react'; import type { ShaderDef } from './shader-def-types'; -import {staticCommonParams, staticImageCommonParams} from './common-param-def'; +import { staticCommonParams, staticImageCommonParams } from './common-param-def'; const defaultParams = halftoneDotsPresets[0].params; diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 190e98a3e..6ff6e780b 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -111,7 +111,6 @@ export { HalftoneDots, halftoneDotsPresets } from './shaders/halftone-dots.js'; export type { HalftoneDotsProps } from './shaders/halftone-dots.js'; export type { HalftoneDotsUniforms, HalftoneDotsParams } from '@paper-design/shaders'; - export { Folds, foldsPresets } from './shaders/folds.js'; export type { FoldsProps } from './shaders/folds.js'; export type { FoldsUniforms, FoldsParams } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/folds.tsx index b90f083ad..73bdd55c4 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/folds.tsx @@ -46,7 +46,6 @@ export const defaultPreset: FoldsPreset = { }, }; - export const foldsPresets: FoldsPreset[] = [defaultPreset]; export const Folds: React.FC = memo(function FoldsImpl({ diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index df5748f7d..6b98d85de 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -25,8 +25,8 @@ export const defaultPreset: HalftoneLinesPreset = { colorBack: '#615681', colorFront: '#ffffff', grid: 'radial', - gridOffsetX: -.5, - gridOffsetY: -.5, + gridOffsetX: -0.5, + gridOffsetY: -0.5, stripeWidth: 0.5, smoothness: 10, size: 40, @@ -55,8 +55,8 @@ export const classicPreset: HalftoneLinesPreset = { colorBack: '#ffffff', colorFront: '#000000', grid: 'lines', - gridOffsetX: -.5, - gridOffsetY: -.5, + gridOffsetX: -0.5, + gridOffsetY: -0.5, stripeWidth: 1, smoothness: 10, size: 60, diff --git a/packages/shaders/src/shaders/blobs-logo.ts b/packages/shaders/src/shaders/blobs-logo.ts index e9377e48c..727a93d09 100644 --- a/packages/shaders/src/shaders/blobs-logo.ts +++ b/packages/shaders/src/shaders/blobs-logo.ts @@ -75,7 +75,7 @@ float getNoise(vec2 uv, vec2 pUv, float t, float shape) { return noise; } -${ rotation2 } +${rotation2} float getImgFrame(vec2 uv, float th) { float frame = 1.; @@ -583,92 +583,6 @@ export function toProcessedBlobsLogo(file: File | string): Promise<{ imageData: }); } -function blurRedChannel( - imageData: ImageData, - width: number, - height: number, - radius = 2 -) { - const src = imageData.data; - const pixelCount = width * height; - - const tmp = new Uint8ClampedArray(pixelCount); - const dst = new Uint8ClampedArray(pixelCount); - - // --- Horizontal blur --- - for (let y = 0; y < height; y++) { - let sum = 0; - let count = 0; - - // initial window centered at x = 0 - for (let dx = -radius; dx <= radius; dx++) { - const xClamped = Math.max(0, Math.min(width - 1, dx)); - const idx = (y * width + xClamped) * 4; - sum += src[idx]!; - count++; - } - - for (let x = 0; x < width; x++) { - const outIdx = y * width + x; - tmp[outIdx] = sum / count; - - const xRemove = x - radius; - if (xRemove >= 0) { - const idxRemove = (y * width + xRemove) * 4; - sum -= src[idxRemove]!; - count--; - } - - const xAdd = x + radius + 1; - if (xAdd < width) { - const idxAdd = (y * width + xAdd) * 4; - sum += src[idxAdd]!; - count++; - } - } - } - - // --- Vertical blur --- - for (let x = 0; x < width; x++) { - let sum = 0; - let count = 0; - - // initial window centered at y = 0 - for (let dy = -radius; dy <= radius; dy++) { - const yClamped = Math.max(0, Math.min(height - 1, dy)); - const idx = yClamped * width + x; - sum += tmp[idx]!; - count++; - } - - for (let y = 0; y < height; y++) { - const outIdx = y * width + x; - dst[outIdx] = sum / count; - - const yRemove = y - radius; - if (yRemove >= 0) { - const idxRemove = yRemove * width + x; - sum -= tmp[idxRemove]!; - count--; - } - - const yAdd = y + radius + 1; - if (yAdd < height) { - const idxAdd = yAdd * width + x; - sum += tmp[idxAdd]!; - count++; - } - } - } - - // --- Write blurred red back into ImageData --- - for (let i = 0; i < pixelCount; i++) { - const px = i * 4; - src[px] = dst[i]!; - } -} - - function buildSparseData( shapeMask: Uint8Array, boundaryMask: Uint8Array, diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/folds.ts index b35e6ff4f..0c2a51fea 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/folds.ts @@ -216,17 +216,17 @@ void main() { imgAlpha *= frame; edge *= frame; - float f[${ foldsMeta.maxColorCount }]; + float f[${foldsMeta.maxColorCount}]; float yTime = fract(t); // yTime = pow(yTime, .8); float yTravel = mix(1.5, -1.5, yTime); float yShape = mix(.04 * edge, .1 * edge, yTime); - vec2 trajs[${ foldsMeta.maxColorCount }]; + vec2 trajs[${foldsMeta.maxColorCount}]; trajs[0] = vec2(-.4, -.5 + yTravel); float dist = 1.; - for (int i = 0; i < ${ foldsMeta.maxColorCount }; i++) { + for (int i = 0; i < ${foldsMeta.maxColorCount}; i++) { f[i] = getPoint(uv + trajs[i], yShape); } @@ -581,12 +581,7 @@ export function toProcessedFolds(file: File | string): Promise<{ imageData: Imag }); } -function blurRedChannel( - imageData: ImageData, - width: number, - height: number, - radius = 2 -) { +function blurRedChannel(imageData: ImageData, width: number, height: number, radius = 2) { const src = imageData.data; const pixelCount = width * height; @@ -666,7 +661,6 @@ function blurRedChannel( } } - function buildSparseData( shapeMask: Uint8Array, boundaryMask: Uint8Array, @@ -837,4 +831,4 @@ export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { noise?: number; outerNoise?: number; overlayBevel?: number; -} \ No newline at end of file +} diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index 7a44b6b0d..7d00dab1f 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -1,9 +1,9 @@ -import type {ShaderMotionParams} from '../shader-mount.js'; -import {sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms} from '../shader-sizing.js'; -import {declarePI, rotation2, simplexNoise, proceduralHash21} from '../shader-utils.js'; +import type { ShaderMotionParams } from '../shader-mount.js'; +import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; +import { declarePI, rotation2, simplexNoise, proceduralHash21 } from '../shader-utils.js'; export const halftoneLinesMeta = { - maxBlurRadius: 8, + maxBlurRadius: 8, } as const; /** @@ -61,14 +61,14 @@ uniform float u_noiseDistortion; uniform float u_gridRotation; -${ sizingVariablesDeclaration } +${sizingVariablesDeclaration} out vec4 fragColor; -${ declarePI } -${ rotation2 } -${ simplexNoise } -${ proceduralHash21 } +${declarePI} +${rotation2} +${simplexNoise} +${proceduralHash21} float valueNoise(vec2 st) { vec2 i = floor(st); @@ -138,16 +138,16 @@ float sigmoid(float x, float k) { vec4 blurTexture(sampler2D tex, vec2 uv, vec2 texelSize, float radius) { // clamp radius so loops have a known max - float r = clamp(radius, 0., float(${ halftoneLinesMeta.maxBlurRadius })); + float r = clamp(radius, 0., float(${halftoneLinesMeta.maxBlurRadius})); int ir = int(r); vec4 acc = vec4(0.0); float weightSum = 0.0; // simple Gaussian-ish weights based on distance - for (int y = -20; y <= ${ halftoneLinesMeta.maxBlurRadius }; ++y) { + for (int y = -20; y <= ${halftoneLinesMeta.maxBlurRadius}; ++y) { if (abs(y) > ir) continue; - for (int x = -20; x <= ${ halftoneLinesMeta.maxBlurRadius }; ++x) { + for (int x = -20; x <= ${halftoneLinesMeta.maxBlurRadius}; ++x) { if (abs(x) > ir) continue; vec2 offset = vec2(float(x), float(y)); @@ -296,51 +296,51 @@ void main() { `; export interface HalftoneLinesUniforms extends ShaderSizingUniforms { - u_colorBack: [number, number, number, number]; - u_colorFront: [number, number, number, number]; - u_image: HTMLImageElement | string | undefined; - u_grid: (typeof HalftoneLinesGrids)[HalftoneLinesGrid]; - u_gridOffsetX: number; - u_gridOffsetY: number; - u_stripeWidth: number; - u_smoothness: number; - u_size: number; - u_thinLines: boolean; - u_allowOverflow: boolean; - u_angleDistortion: number; - u_noiseDistortion: number; - u_gridRotation: number; - u_contrast: number; - u_originalColors: boolean; - u_inverted: boolean; - u_grainMixer: number; - u_grainMixerSize: number; - u_grainOverlay: number; - u_grainOverlaySize: number; + u_colorBack: [number, number, number, number]; + u_colorFront: [number, number, number, number]; + u_image: HTMLImageElement | string | undefined; + u_grid: (typeof HalftoneLinesGrids)[HalftoneLinesGrid]; + u_gridOffsetX: number; + u_gridOffsetY: number; + u_stripeWidth: number; + u_smoothness: number; + u_size: number; + u_thinLines: boolean; + u_allowOverflow: boolean; + u_angleDistortion: number; + u_noiseDistortion: number; + u_gridRotation: number; + u_contrast: number; + u_originalColors: boolean; + u_inverted: boolean; + u_grainMixer: number; + u_grainMixerSize: number; + u_grainOverlay: number; + u_grainOverlaySize: number; } export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionParams { - colorBack?: string; - colorFront?: string; - image?: HTMLImageElement | string | undefined; - grid?: HalftoneLinesGrid; - gridOffsetX?: number; - gridOffsetY?: number; - stripeWidth?: number; - smoothness?: number; - size?: number; - thinLines?: boolean; - allowOverflow?: boolean; - angleDistortion?: number; - noiseDistortion?: number; - gridRotation?: number; - contrast?: number; - originalColors?: boolean; - inverted?: boolean; - grainMixer?: number; - grainMixerSize?: number; - grainOverlay?: number; - grainOverlaySize?: number; + colorBack?: string; + colorFront?: string; + image?: HTMLImageElement | string | undefined; + grid?: HalftoneLinesGrid; + gridOffsetX?: number; + gridOffsetY?: number; + stripeWidth?: number; + smoothness?: number; + size?: number; + thinLines?: boolean; + allowOverflow?: boolean; + angleDistortion?: number; + noiseDistortion?: number; + gridRotation?: number; + contrast?: number; + originalColors?: boolean; + inverted?: boolean; + grainMixer?: number; + grainMixerSize?: number; + grainOverlay?: number; + grainOverlaySize?: number; } export const HalftoneLinesGrids = { From 91ccdcb0e32707553efafe96d2819d0dcb2c0fa5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 1 Dec 2025 21:47:38 +0200 Subject: [PATCH 60/84] presets --- .../src/app/(shaders)/halftone-lines/page.tsx | 45 +++++----- .../src/shaders/halftone-lines.tsx | 86 ++++++++++++++++--- .../shaders/src/shaders/halftone-lines.ts | 48 +++++++---- 3 files changed, 125 insertions(+), 54 deletions(-) diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx index 83ef95724..b423b2d6d 100644 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ b/docs/src/app/(shaders)/halftone-lines/page.tsx @@ -70,36 +70,35 @@ const HalftoneLinesWithControls = () => { ]) ); return { - stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, + colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorFront: { value: toHsla(defaults.colorFront), order: 101 }, + originalColors: { value: defaults.originalColors, order: 102 }, + + contrast: { value: defaults.contrast, min: 0.01, max: 1, order: 200 }, + inverted: { value: defaults.inverted, order: 201 }, smoothness: { value: defaults.smoothness, min: 0, max: halftoneLinesMeta.maxBlurRadius, order: 202 }, + stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 203 }, + thinLines: { value: defaults.thinLines, order: 204 }, + allowOverflow: { value: defaults.allowOverflow, order: 205 }, + grid: { value: defaults.grid, options: Object.keys(HalftoneLinesGrids) as HalftoneLinesGrid[], - order: 201, + order: 250, }, - gridOffsetX: { value: defaults.gridOffsetX, min: -1, max: 1, order: 204 }, - gridOffsetY: { value: defaults.gridOffsetY, min: -1, max: 1, order: 204 }, - angleDistortion: { value: defaults.angleDistortion, min: 0, max: 1, order: 204 }, - noiseDistortion: { value: defaults.noiseDistortion, min: 0, max: 1, order: 205 }, - gridRotation: { value: defaults.gridRotation, min: 0, max: 360, order: 206 }, - colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorFront: { value: toHsla(defaults.colorFront), order: 101 }, - originalColors: { value: defaults.originalColors, order: 102 }, - inverted: { value: defaults.inverted, order: 201 }, - size: { value: defaults.size, min: 0.01, max: 150, step: 0.1, order: 300 }, - thinLines: { value: defaults.thinLines, order: 301 }, - allowOverflow: { value: defaults.allowOverflow, order: 302 }, - contrast: { value: defaults.contrast, min: 0.01, max: 1, order: 303 }, + size: { value: defaults.size, min: 0, max: 1, order: 251 }, + gridOffsetX: { value: defaults.gridOffsetX, min: -1, max: 1, order: 252 }, + gridOffsetY: { value: defaults.gridOffsetY, min: -1, max: 1, order: 253 }, + gridRotation: { value: defaults.gridRotation, min: 0, max: 360, order: 254 }, + gridAngleDistortion: { value: defaults.gridAngleDistortion, min: 0, max: 1, order: 255 }, + gridNoiseDistortion: { value: defaults.gridNoiseDistortion, min: 0, max: 1, order: 256 }, + grainMixer: { value: defaults.grainMixer, min: 0, max: 1, order: 350 }, - grainMixerSize: { value: defaults.grainMixerSize, min: 0, max: 1, order: 352 }, - grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 351 }, - grainOverlaySize: { value: defaults.grainOverlaySize, min: 0, max: 1, order: 352 }, + grainMixerSize: { value: defaults.grainMixerSize, min: 0, max: 1, order: 351 }, + grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 352 }, + grainOverlaySize: { value: defaults.grainOverlaySize, min: 0, max: 1, order: 353 }, + scale: { value: defaults.scale, min: 0.1, max: 10, order: 400 }, - // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 401 }, - // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 402 }, - // originX: { value: defaults.originX, min: 0, max: 1, order: 411 }, - // originY: { value: defaults.originY, min: 0, max: 1, order: 412 }, - // rotation: { value: defaults.rotation, min: 0, max: 360, order: 420 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 450 }, Image: folder( { diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx index 6b98d85de..ef80c6145 100644 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ b/packages/shaders-react/src/shaders/halftone-lines.tsx @@ -17,6 +17,36 @@ type HalftoneLinesPreset = ImageShaderPreset; export const defaultPreset: HalftoneLinesPreset = { name: 'Default', + params: { + ...defaultObjectSizing, + scale: 1, + speed: 0, + frame: 0, + colorBack: '#f1ffe0', + colorFront: '#ff006a', + grid: 'radial', + gridOffsetX: -0.5, + gridOffsetY: -0.2, + stripeWidth: 1, + smoothness: 10, + size: 0.75, + thinLines: false, + allowOverflow: true, + gridAngleDistortion: 0, + gridNoiseDistortion: 0, + gridRotation: 55, + contrast: 0.7, + originalColors: false, + inverted: false, + grainMixer: 0.2, + grainMixerSize: 1, + grainOverlay: 0, + grainOverlaySize: 0.5, + }, +}; + +export const noisePreset: HalftoneLinesPreset = { + name: 'Noisy', params: { ...defaultObjectSizing, scale: 1, @@ -24,16 +54,16 @@ export const defaultPreset: HalftoneLinesPreset = { frame: 0, colorBack: '#615681', colorFront: '#ffffff', - grid: 'radial', + grid: 'noise', gridOffsetX: -0.5, gridOffsetY: -0.5, stripeWidth: 0.5, smoothness: 10, - size: 40, + size: 0.3, thinLines: true, allowOverflow: false, - angleDistortion: 0.4, - noiseDistortion: 0.1, + gridAngleDistortion: 0, + gridNoiseDistortion: 0, gridRotation: 0, contrast: 0.7, originalColors: false, @@ -45,6 +75,36 @@ export const defaultPreset: HalftoneLinesPreset = { }, }; +export const strokesPreset: HalftoneLinesPreset = { + name: 'Strokes', + params: { + ...defaultObjectSizing, + scale: 1, + speed: 0, + frame: 0, + colorBack: '#b7a42a', + colorFront: '#1e1e2f', + grid: 'waves', + gridOffsetX: -0.5, + gridOffsetY: -0.5, + stripeWidth: 0.8, + smoothness: 10, + size: 0.8, + thinLines: false, + allowOverflow: true, + gridAngleDistortion: 0.3, + gridNoiseDistortion: 1, + gridRotation: 0, + contrast: 0.5, + originalColors: false, + inverted: false, + grainMixer: 0.62, + grainMixerSize: 0.9, + grainOverlay: 0, + grainOverlaySize: 0.5, + }, +}; + export const classicPreset: HalftoneLinesPreset = { name: 'Classic', params: { @@ -59,11 +119,11 @@ export const classicPreset: HalftoneLinesPreset = { gridOffsetY: -0.5, stripeWidth: 1, smoothness: 10, - size: 60, + size: 0.6, thinLines: true, - allowOverflow: false, - angleDistortion: 0, - noiseDistortion: 0, + allowOverflow: true, + gridAngleDistortion: 0, + gridNoiseDistortion: 0, gridRotation: 0, contrast: 0.7, originalColors: false, @@ -75,7 +135,7 @@ export const classicPreset: HalftoneLinesPreset = { }, }; -export const halftoneLinesPresets: HalftoneLinesPreset[] = [defaultPreset, classicPreset]; +export const halftoneLinesPresets: HalftoneLinesPreset[] = [defaultPreset, strokesPreset, noisePreset, classicPreset]; export const HalftoneLines: React.FC = memo(function HalftoneLinesImpl({ // Own props @@ -87,8 +147,8 @@ export const HalftoneLines: React.FC = memo(function Halfton grid = defaultPreset.params.grid, gridOffsetX = defaultPreset.params.gridOffsetX, gridOffsetY = defaultPreset.params.gridOffsetY, - angleDistortion = defaultPreset.params.angleDistortion, - noiseDistortion = defaultPreset.params.noiseDistortion, + gridAngleDistortion = defaultPreset.params.gridAngleDistortion, + gridNoiseDistortion = defaultPreset.params.gridNoiseDistortion, stripeWidth = defaultPreset.params.stripeWidth, smoothness = defaultPreset.params.smoothness, size = defaultPreset.params.size, @@ -124,8 +184,8 @@ export const HalftoneLines: React.FC = memo(function Halfton u_grid: HalftoneLinesGrids[grid], u_gridOffsetX: gridOffsetX, u_gridOffsetY: gridOffsetY, - u_angleDistortion: angleDistortion, - u_noiseDistortion: noiseDistortion, + u_gridAngleDistortion: gridAngleDistortion, + u_gridNoiseDistortion: gridNoiseDistortion, u_stripeWidth: stripeWidth, u_smoothness: smoothness, u_size: size, diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts index 7d00dab1f..8d42ac393 100644 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ b/packages/shaders/src/shaders/halftone-lines.ts @@ -56,8 +56,8 @@ uniform bool u_originalColors; uniform bool u_inverted; uniform float u_stripeWidth; uniform float u_smoothness; -uniform float u_angleDistortion; -uniform float u_noiseDistortion; +uniform float u_gridAngleDistortion; +uniform float u_gridNoiseDistortion; uniform float u_gridRotation; @@ -214,30 +214,40 @@ void main() { lum = 1. - lum; uv = v_objectUV; - float n = snoise(3. * uv + 100.); + float noise = snoise(2.5 * uv + 100.); vec2 uvGrid = v_objectUV; - uvGrid += .15 * n * lum * u_noiseDistortion; - uvGrid *= u_size; + uvGrid += .15 * noise * lum * u_gridNoiseDistortion; + float gridSize = mix(200., 5., u_size); + uvGrid *= gridSize; float gridLine; float angleOffset = u_gridRotation * PI / 180.; - float angleDistort = u_angleDistortion * lum; + float angleDistort = u_gridAngleDistortion * lum; vec2 gridOffset = -vec2(u_gridOffsetX, u_gridOffsetY); - if (u_grid == 1.) { - uvGrid -= u_size * gridOffset; + if (u_grid == 0.) { + uvGrid += gridOffset; + uvGrid = rotate(uvGrid, angleOffset + angleDistort); + gridLine = uvGrid.y; + } else if (u_grid == 1.) { + uvGrid += gridSize * gridOffset; + + uvGrid -= gridSize * gridOffset; uvGrid = rotate(uvGrid, angleOffset); - uvGrid += u_size * gridOffset; + uvGrid += gridSize * gridOffset; - uvGrid = rotate(uvGrid, angleDistort); - uvGrid += u_size * gridOffset; gridLine = length(uvGrid); - } else { + } else if (u_grid == 2.) { uvGrid += gridOffset; uvGrid = rotate(uvGrid, angleOffset + angleDistort); - gridLine = uvGrid.y; + gridLine = uvGrid.y + sin(.5 * uvGrid.x); + } else if (u_grid == 3.) { + uvGrid += gridOffset; + uvGrid = rotate(uvGrid, angleOffset + angleDistort); + noise = snoise(.2 * uvGrid); + gridLine = noise; } float stripeMap = abs(fract(gridLine) - .5); @@ -255,7 +265,7 @@ void main() { } w = clamp(w, wLo, wHi); - vec2 grainMixerSize = mix(1000., 25., u_grainMixerSize) * vec2(1., 1. / u_imageAspectRatio); + vec2 grainMixerSize = mix(1000., 50., u_grainMixerSize) * vec2(1., 1. / u_imageAspectRatio); vec2 grainOverlaySize = mix(2000., 200., u_grainOverlaySize) * vec2(1., 1. / u_imageAspectRatio); vec2 grainMixerUV = getImageUV(uvNormalised, grainMixerSize); vec2 grainOverlayUV = getImageUV(uvNormalised, grainOverlaySize); @@ -307,8 +317,8 @@ export interface HalftoneLinesUniforms extends ShaderSizingUniforms { u_size: number; u_thinLines: boolean; u_allowOverflow: boolean; - u_angleDistortion: number; - u_noiseDistortion: number; + u_gridAngleDistortion: number; + u_gridNoiseDistortion: number; u_gridRotation: number; u_contrast: number; u_originalColors: boolean; @@ -331,8 +341,8 @@ export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionPar size?: number; thinLines?: boolean; allowOverflow?: boolean; - angleDistortion?: number; - noiseDistortion?: number; + gridAngleDistortion?: number; + gridNoiseDistortion?: number; gridRotation?: number; contrast?: number; originalColors?: boolean; @@ -346,6 +356,8 @@ export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionPar export const HalftoneLinesGrids = { lines: 0, radial: 1, + waves: 2, + noise: 3, } as const; export type HalftoneLinesGrid = keyof typeof HalftoneLinesGrids; From d8a572f399d7f601d243d4bb3a8c027ea702135c Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 2 Dec 2025 19:38:38 +0200 Subject: [PATCH 61/84] keeping only Folds --- docs/src/app/(shaders)/blobs-logo/layout.tsx | 9 - docs/src/app/(shaders)/blobs-logo/page.tsx | 144 ---- docs/src/app/(shaders)/gem-smoke/layout.tsx | 9 - docs/src/app/(shaders)/gem-smoke/page.tsx | 144 ---- .../app/(shaders)/halftone-lines/layout.tsx | 9 - .../src/app/(shaders)/halftone-lines/page.tsx | 133 ---- docs/src/shader-defs/blobs-logo-def.ts | 19 - docs/src/shader-defs/gem-smoke-def.ts | 65 -- docs/src/shader-defs/halftone-lines-def.ts | 19 - packages/shaders-react/src/index.ts | 12 - .../shaders-react/src/shaders/blobs-logo.tsx | 140 ---- .../shaders-react/src/shaders/gem-smoke.tsx | 158 ---- .../src/shaders/halftone-lines.tsx | 224 ------ packages/shaders/src/index.ts | 24 - packages/shaders/src/shaders/blobs-logo.ts | 741 ------------------ packages/shaders/src/shaders/gem-smoke.ts | 660 ---------------- .../shaders/src/shaders/halftone-lines.ts | 363 --------- 17 files changed, 2873 deletions(-) delete mode 100644 docs/src/app/(shaders)/blobs-logo/layout.tsx delete mode 100644 docs/src/app/(shaders)/blobs-logo/page.tsx delete mode 100644 docs/src/app/(shaders)/gem-smoke/layout.tsx delete mode 100644 docs/src/app/(shaders)/gem-smoke/page.tsx delete mode 100644 docs/src/app/(shaders)/halftone-lines/layout.tsx delete mode 100644 docs/src/app/(shaders)/halftone-lines/page.tsx delete mode 100644 docs/src/shader-defs/blobs-logo-def.ts delete mode 100644 docs/src/shader-defs/gem-smoke-def.ts delete mode 100644 docs/src/shader-defs/halftone-lines-def.ts delete mode 100644 packages/shaders-react/src/shaders/blobs-logo.tsx delete mode 100644 packages/shaders-react/src/shaders/gem-smoke.tsx delete mode 100644 packages/shaders-react/src/shaders/halftone-lines.tsx delete mode 100644 packages/shaders/src/shaders/blobs-logo.ts delete mode 100644 packages/shaders/src/shaders/gem-smoke.ts delete mode 100644 packages/shaders/src/shaders/halftone-lines.ts diff --git a/docs/src/app/(shaders)/blobs-logo/layout.tsx b/docs/src/app/(shaders)/blobs-logo/layout.tsx deleted file mode 100644 index f0964a3f1..000000000 --- a/docs/src/app/(shaders)/blobs-logo/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Metadata } from 'next'; - -export const metadata: Metadata = { - title: 'Mesh Gradient Logo Filter • Paper', -}; - -export default function Layout({ children }: { children: React.ReactNode }) { - return <>{children}; -} diff --git a/docs/src/app/(shaders)/blobs-logo/page.tsx b/docs/src/app/(shaders)/blobs-logo/page.tsx deleted file mode 100644 index edd20e692..000000000 --- a/docs/src/app/(shaders)/blobs-logo/page.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client'; - -import { BlobsLogo, blobsLogoPresets } from '@paper-design/shaders-react'; -import { useControls, button, folder } from 'leva'; -import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; -import { usePresetHighlight } from '@/helpers/use-preset-highlight'; -import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { blobsLogoMeta } from '@paper-design/shaders'; -import { ShaderFit } from '@paper-design/shaders'; -import { levaImageButton } from '@/helpers/leva-image-button'; -import { useState, Suspense, useEffect, useCallback } from 'react'; -import { ShaderDetails } from '@/components/shader-details'; -import { ShaderContainer } from '@/components/shader-container'; -import { useUrlParams } from '@/helpers/use-url-params'; -import { blobsLogoDef } from '@/shader-defs/blobs-logo-def'; -import { toHsla } from '@/helpers/color-utils'; -import { useColors } from '@/helpers/use-colors'; - -// Override just for the docs, we keep it transparent in the preset -// blobsLogoPresets[0].params.colorBack = '#000000'; - -const { worldWidth, worldHeight, ...defaults } = blobsLogoPresets[0].params; - -const imageFiles = [ - 'paradigm.svg', - 'paper-logo-only.svg', - 'brave2.png', - 'capy.svg', - 'contra.svg', - 'infinite.svg', - 'linear.svg', - 'mercury.svg', - 'mymind.svg', - 'inbound.svg', - 'resend.svg', - 'shopify.svg', - 'wealth-simple.svg', - 'diamond.svg', - - // 'chanel.svg', - // 'cibc.svg', - // 'cloudflare.svg', - // 'apple.svg', - // 'paper-logo-only.svg', - 'discord.svg', - // 'paper-logo-only', - // 'enterprise-rent.svg', - // 'kfc.svg', - // 'microsoft.svg', - // 'nasa.svg', - // 'netflix.svg', - 'nike.svg', - // 'perkins.svg', - // 'pizza-hut.svg', - // 'remix.svg', - // 'rogers.svg', - 'vercel.svg', - 'volkswagen.svg', -] as const; - -const BlobsLogoWithControls = () => { - 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: blobsLogoMeta.maxColorCount, - }); - - const [params, setParams] = useControls(() => { - return { - colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorInner: { value: toHsla(defaults.colorInner), order: 101 }, - contour: { value: defaults.contour, min: 0, max: 1, order: 201 }, - size: { value: defaults.size, min: 0, max: 1, order: 203 }, - speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, - scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, - // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, - // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, - // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, - // fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 305 }, - Image: folder( - { - 'Upload image': levaImageButton((img?: HTMLImageElement) => setImage(img ?? '')), - }, - { order: -1 } - ), - }; - }, [colors.length]); - - useControls(() => { - const presets = Object.fromEntries( - blobsLogoPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ - name, - button(() => { - const { colors, ...presetParams } = preset; - setColors(colors); - setParamsSafe(params, setParams, presetParams); - }), - ]) - ); - return { - Presets: folder(presets, { order: -2 }), - }; - }); - - // Reset to defaults on mount, so that Leva doesn't show values from other - // shaders when navigating (if two shaders have a color1 param for example) - useResetLevaParams(params, setParams, defaults); - useUrlParams(params, setParams, blobsLogoDef, setColors); - usePresetHighlight(blobsLogoPresets, params); - cleanUpLevaParams(params); - - return ( - <> - - - - - - - - ); -}; - -export default BlobsLogoWithControls; diff --git a/docs/src/app/(shaders)/gem-smoke/layout.tsx b/docs/src/app/(shaders)/gem-smoke/layout.tsx deleted file mode 100644 index c66437c46..000000000 --- a/docs/src/app/(shaders)/gem-smoke/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Metadata } from 'next'; - -export const metadata: Metadata = { - title: 'Gem Smoke Logo Filter • Paper', -}; - -export default function Layout({ children }: { children: React.ReactNode }) { - return <>{children}; -} diff --git a/docs/src/app/(shaders)/gem-smoke/page.tsx b/docs/src/app/(shaders)/gem-smoke/page.tsx deleted file mode 100644 index 74ad8088b..000000000 --- a/docs/src/app/(shaders)/gem-smoke/page.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client'; - -import { GemSmoke, gemSmokePresets } from '@paper-design/shaders-react'; -import { useControls, button, folder } from 'leva'; -import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; -import { usePresetHighlight } from '@/helpers/use-preset-highlight'; -import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { gemSmokeMeta } from '@paper-design/shaders'; -import { ShaderFit } from '@paper-design/shaders'; -import { levaImageButton } from '@/helpers/leva-image-button'; -import { useState, Suspense, useEffect, useCallback } from 'react'; -import { ShaderDetails } from '@/components/shader-details'; -import { ShaderContainer } from '@/components/shader-container'; -import { useUrlParams } from '@/helpers/use-url-params'; -import { gemSmokeDef } from '@/shader-defs/gem-smoke-def'; -import { toHsla } from '@/helpers/color-utils'; -import { useColors } from '@/helpers/use-colors'; - -// Override just for the docs, we keep it transparent in the preset -// gemSmokePresets[0].params.colorBack = '#000000'; - -const { worldWidth, worldHeight, ...defaults } = gemSmokePresets[0].params; - -const imageFiles = [ - 'contra.svg', - 'paradigm.svg', - 'paper-logo-only.svg', - 'brave.svg', - 'capy.svg', - 'infinite.svg', - 'linear.svg', - 'mercury.svg', - 'mymind.svg', - 'inbound.svg', - 'resend.svg', - 'shopify.svg', - 'wealth-simple.svg', - 'vercel.svg', - - // 'chanel.svg', - // 'cibc.svg', - // 'cloudflare.svg', - // 'apple.svg', - // 'discord.svg', - // 'enterprise-rent.svg', - // 'kfc.svg', - // 'microsoft.svg', - // 'nasa.svg', - // 'netflix.svg', - // 'nike.svg', - // 'perkins.svg', - // 'pizza-hut.svg', - // 'remix.svg', - // 'rogers.svg', - // 'volkswagen.svg', - - 'diamond.svg', -] as const; - -const GemSmokeWithControls = () => { - 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: gemSmokeMeta.maxColorCount, - }); - - const [params, setParams] = useControls(() => { - return { - colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - distortion: { value: defaults.distortion, min: 0, max: 1, order: 201 }, - outerDistortion: { value: defaults.outerDistortion, min: 0, max: 1, order: 202 }, - outerVisibility: { value: defaults.outerVisibility, min: 0, max: 1, order: 203 }, - innerFill: { value: defaults.innerFill, min: 0, max: 1, order: 304 }, - speed: { value: defaults.speed, min: 0, max: 4, order: 300 }, - scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, - // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, - // offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, - // offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, - // fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 305 }, - Image: folder( - { - 'Upload image': levaImageButton((img?: HTMLImageElement) => setImage(img ?? '')), - }, - { order: -1 } - ), - }; - }, [colors.length]); - - useControls(() => { - const presets = Object.fromEntries( - gemSmokePresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ - name, - button(() => { - const { colors, ...presetParams } = preset; - setColors(colors); - setParamsSafe(params, setParams, presetParams); - }), - ]) - ); - return { - Presets: folder(presets, { order: -2 }), - }; - }); - - // Reset to defaults on mount, so that Leva doesn't show values from other - // shaders when navigating (if two shaders have a color1 param for example) - useResetLevaParams(params, setParams, defaults); - useUrlParams(params, setParams, gemSmokeDef, setColors); - usePresetHighlight(gemSmokePresets, params); - cleanUpLevaParams(params); - - return ( - <> - - - - - - - - ); -}; - -export default GemSmokeWithControls; diff --git a/docs/src/app/(shaders)/halftone-lines/layout.tsx b/docs/src/app/(shaders)/halftone-lines/layout.tsx deleted file mode 100644 index 0e9d6122b..000000000 --- a/docs/src/app/(shaders)/halftone-lines/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Metadata } from 'next'; - -export const metadata: Metadata = { - title: 'Halftone Lines Filter • Paper', -}; - -export default function Layout({ children }: { children: React.ReactNode }) { - return <>{children}; -} diff --git a/docs/src/app/(shaders)/halftone-lines/page.tsx b/docs/src/app/(shaders)/halftone-lines/page.tsx deleted file mode 100644 index b423b2d6d..000000000 --- a/docs/src/app/(shaders)/halftone-lines/page.tsx +++ /dev/null @@ -1,133 +0,0 @@ -'use client'; - -import { HalftoneLines, halftoneLinesPresets } from '@paper-design/shaders-react'; -import { useControls, button, folder } from 'leva'; -import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; -import { usePresetHighlight } from '@/helpers/use-preset-highlight'; -import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { HalftoneLinesGrid, HalftoneLinesGrids, halftoneLinesMeta, ShaderFit } from '@paper-design/shaders'; -import { levaImageButton } from '@/helpers/leva-image-button'; -import { useState, useEffect, useCallback } from 'react'; -import { toHsla } from '@/helpers/color-utils'; -import { ShaderDetails } from '@/components/shader-details'; -import { halftoneLinesDef } from '@/shader-defs/halftone-lines-def'; -import { ShaderContainer } from '@/components/shader-container'; -import { useUrlParams } from '@/helpers/use-url-params'; - -const { worldWidth, worldHeight, ...defaults } = halftoneLinesPresets[0].params; - -const imageFiles = [ - '001.webp', - '002.webp', - '003.webp', - '004.webp', - '005.webp', - '006.webp', - '007.webp', - '008.webp', - '009.webp', - '0010.webp', - '0011.webp', - '0012.webp', - '0013.webp', - '0014.webp', - '0015.webp', - '0016.webp', - '0017.webp', - '0018.webp', -] as const; - -const HalftoneLinesWithControls = () => { - const [imageIdx, setImageIdx] = useState(-1); - const [image, setImage] = useState('/images/image-filters/0018.webp'); - const [status, setStatus] = useState('Click to load an image'); - - useEffect(() => { - if (imageIdx >= 0) { - const name = imageFiles[imageIdx]; - setStatus(`Displaying image: ${name}`); - const img = new Image(); - img.src = `/images/image-filters/${name}`; - img.onload = () => setImage(img); - } - }, [imageIdx]); - - const handleClick = useCallback(() => { - setImageIdx((prev) => (prev + 1) % imageFiles.length); - }, []); - - const setImageWithoutStatus = useCallback((img?: HTMLImageElement) => { - setImage(img ?? ''); - setImageIdx(-1); - setStatus(``); - }, []); - - const [params, setParams] = useControls(() => { - const presets = Object.fromEntries( - halftoneLinesPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ - name, - button(() => setParamsSafe(params, setParams, preset)), - ]) - ); - return { - colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorFront: { value: toHsla(defaults.colorFront), order: 101 }, - originalColors: { value: defaults.originalColors, order: 102 }, - - contrast: { value: defaults.contrast, min: 0.01, max: 1, order: 200 }, - inverted: { value: defaults.inverted, order: 201 }, - smoothness: { value: defaults.smoothness, min: 0, max: halftoneLinesMeta.maxBlurRadius, order: 202 }, - stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 203 }, - thinLines: { value: defaults.thinLines, order: 204 }, - allowOverflow: { value: defaults.allowOverflow, order: 205 }, - - grid: { - value: defaults.grid, - options: Object.keys(HalftoneLinesGrids) as HalftoneLinesGrid[], - order: 250, - }, - size: { value: defaults.size, min: 0, max: 1, order: 251 }, - gridOffsetX: { value: defaults.gridOffsetX, min: -1, max: 1, order: 252 }, - gridOffsetY: { value: defaults.gridOffsetY, min: -1, max: 1, order: 253 }, - gridRotation: { value: defaults.gridRotation, min: 0, max: 360, order: 254 }, - gridAngleDistortion: { value: defaults.gridAngleDistortion, min: 0, max: 1, order: 255 }, - gridNoiseDistortion: { value: defaults.gridNoiseDistortion, min: 0, max: 1, order: 256 }, - - grainMixer: { value: defaults.grainMixer, min: 0, max: 1, order: 350 }, - grainMixerSize: { value: defaults.grainMixerSize, min: 0, max: 1, order: 351 }, - grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 352 }, - grainOverlaySize: { value: defaults.grainOverlaySize, min: 0, max: 1, order: 353 }, - - scale: { value: defaults.scale, min: 0.1, max: 10, order: 400 }, - fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 450 }, - Image: folder( - { - 'Upload image': levaImageButton(setImageWithoutStatus), - }, - { order: 0 } - ), - Presets: folder(presets, { order: -1 }), - }; - }); - - // Reset to defaults on mount, so that Leva doesn't show values from other - // shaders when navigating (if two shaders have a color1 param for example) - useResetLevaParams(params, setParams, defaults); - useUrlParams(params, setParams, halftoneLinesDef); - usePresetHighlight(halftoneLinesPresets, params); - cleanUpLevaParams(params); - - return ( - <> - - - -
- Click to change the sample image -
- - - ); -}; - -export default HalftoneLinesWithControls; diff --git a/docs/src/shader-defs/blobs-logo-def.ts b/docs/src/shader-defs/blobs-logo-def.ts deleted file mode 100644 index 7fb059fb4..000000000 --- a/docs/src/shader-defs/blobs-logo-def.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { blobsLogoPresets } from '@paper-design/shaders-react'; -import type { ShaderDef } from './shader-def-types'; -import { animatedCommonParams } from './common-param-def'; - -const defaultParams = blobsLogoPresets[0].params; - -export const blobsLogoDef: ShaderDef = { - name: 'Liquid Metal', - description: 'Futuristic liquid metal material applied to uploaded logo or abstract shape.', - params: [ - { - name: 'image', - type: 'HTMLImageElement | string', - description: - 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', - }, - ...animatedCommonParams, - ], -}; diff --git a/docs/src/shader-defs/gem-smoke-def.ts b/docs/src/shader-defs/gem-smoke-def.ts deleted file mode 100644 index dcdcc7652..000000000 --- a/docs/src/shader-defs/gem-smoke-def.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { gemSmokePresets } from '@paper-design/shaders-react'; -import type { ShaderDef } from './shader-def-types'; -import { animatedImageCommonParams } from './common-param-def'; - -const defaultParams = gemSmokePresets[0].params; - -export const gemSmokeDef: ShaderDef = { - name: 'Gem Smoke', - description: 'Fluid, smoke shape animating behind the input image and being distorted by shape', - params: [ - { - name: 'image', - type: 'HTMLImageElement | string', - description: - 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', - }, - { - name: 'colors', - type: 'string[]', - defaultValue: [], - isColor: true, - description: 'Up to 5 ray colors', - }, - { - name: 'colorBack', - type: 'string', - defaultValue: defaultParams.colorBack, - isColor: true, - description: 'Background color', - }, - { - name: 'distortion', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.distortion, - description: 'The power of smoke distortion', - }, - { - name: 'outerDistortion', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.outerDistortion, - description: 'The power of distortion out of the input shape (shape defined by alpha channel)', - }, - { - name: 'outerVisibility', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.outerDistortion, - description: 'The visibility of smoke shape out of the input shape (shape defined by alpha channel)', - }, - { - name: 'innerFill', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.innerFill, - description: 'Additional flat color within the input shape (shape defined by alpha channel)', - }, - ...animatedImageCommonParams, - ], -}; diff --git a/docs/src/shader-defs/halftone-lines-def.ts b/docs/src/shader-defs/halftone-lines-def.ts deleted file mode 100644 index 121b9fbd0..000000000 --- a/docs/src/shader-defs/halftone-lines-def.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { halftoneLinesPresets } from '@paper-design/shaders-react'; -import type { ShaderDef } from './shader-def-types'; -import { animatedCommonParams } from './common-param-def'; - -const defaultParams = halftoneLinesPresets[0].params; - -export const halftoneLinesDef: ShaderDef = { - name: 'Halftone lines', - description: 'TBD', - params: [ - { - name: 'image', - type: 'HTMLImageElement | string', - description: - 'An optional image used as an effect mask. A transparent background is required. If no image is provided, the shader defaults to one of the predefined shapes.', - }, - ...animatedCommonParams, - ], -}; diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 6ff6e780b..5d7ea0c10 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -115,18 +115,6 @@ export { Folds, foldsPresets } from './shaders/folds.js'; export type { FoldsProps } from './shaders/folds.js'; export type { FoldsUniforms, FoldsParams } from '@paper-design/shaders'; -export { HalftoneLines, halftoneLinesPresets } from './shaders/halftone-lines.js'; -export type { HalftoneLinesProps } from './shaders/halftone-lines.js'; -export type { HalftoneLinesUniforms, HalftoneLinesParams } from '@paper-design/shaders'; - -export { BlobsLogo, blobsLogoPresets } from './shaders/blobs-logo.js'; -export type { BlobsLogoProps } from './shaders/blobs-logo.js'; -export type { BlobsLogoUniforms, BlobsLogoParams } from '@paper-design/shaders'; - -export { GemSmoke, gemSmokePresets } from './shaders/gem-smoke.js'; -export type { GemSmokeProps } from './shaders/gem-smoke.js'; -export type { GemSmokeUniforms, GemSmokeParams } from '@paper-design/shaders'; - export { isPaperShaderElement, getShaderColorFromString } from '@paper-design/shaders'; export type { PaperShaderElement, ShaderFit, ShaderSizingParams, ShaderSizingUniforms } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/blobs-logo.tsx b/packages/shaders-react/src/shaders/blobs-logo.tsx deleted file mode 100644 index d48409027..000000000 --- a/packages/shaders-react/src/shaders/blobs-logo.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { memo, useLayoutEffect, useState } from 'react'; -import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; -import { colorPropsAreEqual } from '../color-props-are-equal.js'; -import { - blobsLogoFragmentShader, - ShaderFitOptions, - defaultObjectSizing, - type BlobsLogoUniforms, - type BlobsLogoParams, - toProcessedBlobsLogo, - type ImageShaderPreset, - getShaderColorFromString, - getShaderNoiseTexture, -} from '@paper-design/shaders'; -import { transparentPixel } from '../transparent-pixel.js'; -import { suspend } from '../suspend.js'; - -export interface BlobsLogoProps extends ShaderComponentProps, BlobsLogoParams { - /** - * Suspends the component when the image is being processed. - */ - suspendWhenProcessingImage?: boolean; -} - -type BlobsLogoPreset = ImageShaderPreset; - -export const defaultPreset: BlobsLogoPreset = { - name: 'Default', - params: { - ...defaultObjectSizing, - scale: 0.8, - speed: 1, - frame: 0, - colorBack: '#c8dfd900', - colorInner: '#c8dfd9', - colors: ['#ff9d00', '#fd4f30', '#809bff', '#c5bac5'], - size: 0.5, - contour: 0.7, - }, -}; -export const blobsLogoPresets: BlobsLogoPreset[] = [defaultPreset]; - -export const BlobsLogo: React.FC = memo(function BlobsLogoImpl({ - // Own props - colorBack = defaultPreset.params.colorBack, - colorInner = defaultPreset.params.colorInner, - colors = defaultPreset.params.colors, - speed = defaultPreset.params.speed, - frame = defaultPreset.params.frame, - image = '', - contour = defaultPreset.params.contour, - size = defaultPreset.params.size, - suspendWhenProcessingImage = false, - - // Sizing props - fit = defaultPreset.params.fit, - scale = defaultPreset.params.scale, - rotation = defaultPreset.params.rotation, - originX = defaultPreset.params.originX, - originY = defaultPreset.params.originY, - offsetX = defaultPreset.params.offsetX, - offsetY = defaultPreset.params.offsetY, - worldWidth = defaultPreset.params.worldWidth, - worldHeight = defaultPreset.params.worldHeight, - ...props -}: BlobsLogoProps) { - const imageUrl = typeof image === 'string' ? image : image.src; - const [processedStateImage, setProcessedStateImage] = useState(transparentPixel); - - let processedImage: string; - - if (suspendWhenProcessingImage && typeof window !== 'undefined' && imageUrl) { - processedImage = suspend( - (): Promise => toProcessedBlobsLogo(imageUrl).then((result) => URL.createObjectURL(result.pngBlob)), - [imageUrl, 'blobsLogo'] - ); - } else { - processedImage = processedStateImage; - } - - useLayoutEffect(() => { - if (suspendWhenProcessingImage) { - // Skip doing work in the effect as it's been handled by suspense. - return; - } - - if (!imageUrl) { - setProcessedStateImage(transparentPixel); - return; - } - - let url: string; - let current = true; - - toProcessedBlobsLogo(imageUrl).then((result) => { - if (current) { - url = URL.createObjectURL(result.pngBlob); - setProcessedStateImage(url); - } - }); - - return () => { - current = false; - }; - }, [imageUrl, suspendWhenProcessingImage]); - - const uniforms = { - // Own uniforms - u_colors: colors.map(getShaderColorFromString), - u_colorsCount: colors.length, - u_colorBack: getShaderColorFromString(colorBack), - u_colorInner: getShaderColorFromString(colorInner), - u_image: processedImage, - u_contour: contour, - u_size: size, - u_noiseTexture: getShaderNoiseTexture(), - - // Sizing uniforms - u_fit: ShaderFitOptions[fit], - u_scale: scale, - u_rotation: rotation, - u_offsetX: offsetX, - u_offsetY: offsetY, - u_originX: originX, - u_originY: originY, - u_worldWidth: worldWidth, - u_worldHeight: worldHeight, - } satisfies BlobsLogoUniforms; - - return ( - - ); -}, colorPropsAreEqual); diff --git a/packages/shaders-react/src/shaders/gem-smoke.tsx b/packages/shaders-react/src/shaders/gem-smoke.tsx deleted file mode 100644 index 1b9317046..000000000 --- a/packages/shaders-react/src/shaders/gem-smoke.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { memo, useLayoutEffect, useState } from 'react'; -import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; -import { colorPropsAreEqual } from '../color-props-are-equal.js'; -import { - gemSmokeFragmentShader, - ShaderFitOptions, - defaultObjectSizing, - type GemSmokeUniforms, - type GemSmokeParams, - toProcessedGemSmoke, - type ImageShaderPreset, - getShaderColorFromString, -} from '@paper-design/shaders'; -import { transparentPixel } from '../transparent-pixel.js'; -import { suspend } from '../suspend.js'; - -export interface GemSmokeProps extends ShaderComponentProps, GemSmokeParams { - /** - * Suspends the component when the image is being processed. - */ - suspendWhenProcessingImage?: boolean; -} - -type GemSmokePreset = ImageShaderPreset; - -export const defaultPreset: GemSmokePreset = { - name: 'Default', - params: { - ...defaultObjectSizing, - scale: 0.65, - speed: 1, - frame: 0, - colorBack: '#00000000', - colors: ['#b3d5cc', '#ffe9ad', '#ff8a9b', '#4058a5'], - outerVisibility: 0.5, - distortion: 0.8, - innerFill: 0, - outerDistortion: 0.7, - }, -}; - -export const blackPreset: GemSmokePreset = { - name: 'Black', - params: { - ...defaultObjectSizing, - scale: 0.65, - speed: 1, - frame: 0, - colorBack: '#00000000', - colors: ['#000000', '#ffffff'], - outerVisibility: 0.6, - distortion: 0.9, - innerFill: 0, - outerDistortion: 0.8, - }, -}; - -export const gemSmokePresets: GemSmokePreset[] = [defaultPreset, blackPreset]; - -export const GemSmoke: React.FC = memo(function GemSmokeImpl({ - // Own props - colorBack = defaultPreset.params.colorBack, - colors = defaultPreset.params.colors, - speed = defaultPreset.params.speed, - frame = defaultPreset.params.frame, - image = '', - distortion = defaultPreset.params.distortion, - outerVisibility = defaultPreset.params.outerVisibility, - innerFill = defaultPreset.params.innerFill, - outerDistortion = defaultPreset.params.outerDistortion, - suspendWhenProcessingImage = false, - - // Sizing props - fit = defaultPreset.params.fit, - scale = defaultPreset.params.scale, - rotation = defaultPreset.params.rotation, - originX = defaultPreset.params.originX, - originY = defaultPreset.params.originY, - offsetX = defaultPreset.params.offsetX, - offsetY = defaultPreset.params.offsetY, - worldWidth = defaultPreset.params.worldWidth, - worldHeight = defaultPreset.params.worldHeight, - ...props -}: GemSmokeProps) { - const imageUrl = typeof image === 'string' ? image : image.src; - const [processedStateImage, setProcessedStateImage] = useState(transparentPixel); - - let processedImage: string; - - if (suspendWhenProcessingImage && typeof window !== 'undefined' && imageUrl) { - processedImage = suspend( - (): Promise => toProcessedGemSmoke(imageUrl).then((result) => URL.createObjectURL(result.pngBlob)), - [imageUrl, 'gemSmoke'] - ); - } else { - processedImage = processedStateImage; - } - - useLayoutEffect(() => { - if (suspendWhenProcessingImage) { - // Skip doing work in the effect as it's been handled by suspense. - return; - } - - if (!imageUrl) { - setProcessedStateImage(transparentPixel); - return; - } - - let url: string; - let current = true; - - toProcessedGemSmoke(imageUrl).then((result) => { - if (current) { - url = URL.createObjectURL(result.pngBlob); - setProcessedStateImage(url); - } - }); - - return () => { - current = false; - }; - }, [imageUrl, suspendWhenProcessingImage]); - - const uniforms = { - // Own uniforms - u_colors: colors.map(getShaderColorFromString), - u_colorsCount: colors.length, - u_colorBack: getShaderColorFromString(colorBack), - u_image: processedImage, - u_distortion: distortion, - u_outerVisibility: outerVisibility, - u_innerFill: innerFill, - u_outerDistortion: outerDistortion, - - // Sizing uniforms - u_fit: ShaderFitOptions[fit], - u_scale: scale, - u_rotation: rotation, - u_offsetX: offsetX, - u_offsetY: offsetY, - u_originX: originX, - u_originY: originY, - u_worldWidth: worldWidth, - u_worldHeight: worldHeight, - } satisfies GemSmokeUniforms; - - return ( - - ); -}, colorPropsAreEqual); diff --git a/packages/shaders-react/src/shaders/halftone-lines.tsx b/packages/shaders-react/src/shaders/halftone-lines.tsx deleted file mode 100644 index ef80c6145..000000000 --- a/packages/shaders-react/src/shaders/halftone-lines.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { memo } from 'react'; -import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; -import { - halftoneLinesFragmentShader, - getShaderColorFromString, - ShaderFitOptions, - type HalftoneLinesUniforms, - type HalftoneLinesParams, - defaultObjectSizing, - type ImageShaderPreset, - HalftoneLinesGrids, -} from '@paper-design/shaders'; - -export interface HalftoneLinesProps extends ShaderComponentProps, HalftoneLinesParams {} - -type HalftoneLinesPreset = ImageShaderPreset; - -export const defaultPreset: HalftoneLinesPreset = { - name: 'Default', - params: { - ...defaultObjectSizing, - scale: 1, - speed: 0, - frame: 0, - colorBack: '#f1ffe0', - colorFront: '#ff006a', - grid: 'radial', - gridOffsetX: -0.5, - gridOffsetY: -0.2, - stripeWidth: 1, - smoothness: 10, - size: 0.75, - thinLines: false, - allowOverflow: true, - gridAngleDistortion: 0, - gridNoiseDistortion: 0, - gridRotation: 55, - contrast: 0.7, - originalColors: false, - inverted: false, - grainMixer: 0.2, - grainMixerSize: 1, - grainOverlay: 0, - grainOverlaySize: 0.5, - }, -}; - -export const noisePreset: HalftoneLinesPreset = { - name: 'Noisy', - params: { - ...defaultObjectSizing, - scale: 1, - speed: 0, - frame: 0, - colorBack: '#615681', - colorFront: '#ffffff', - grid: 'noise', - gridOffsetX: -0.5, - gridOffsetY: -0.5, - stripeWidth: 0.5, - smoothness: 10, - size: 0.3, - thinLines: true, - allowOverflow: false, - gridAngleDistortion: 0, - gridNoiseDistortion: 0, - gridRotation: 0, - contrast: 0.7, - originalColors: false, - inverted: false, - grainMixer: 0.2, - grainMixerSize: 1, - grainOverlay: 0, - grainOverlaySize: 0.5, - }, -}; - -export const strokesPreset: HalftoneLinesPreset = { - name: 'Strokes', - params: { - ...defaultObjectSizing, - scale: 1, - speed: 0, - frame: 0, - colorBack: '#b7a42a', - colorFront: '#1e1e2f', - grid: 'waves', - gridOffsetX: -0.5, - gridOffsetY: -0.5, - stripeWidth: 0.8, - smoothness: 10, - size: 0.8, - thinLines: false, - allowOverflow: true, - gridAngleDistortion: 0.3, - gridNoiseDistortion: 1, - gridRotation: 0, - contrast: 0.5, - originalColors: false, - inverted: false, - grainMixer: 0.62, - grainMixerSize: 0.9, - grainOverlay: 0, - grainOverlaySize: 0.5, - }, -}; - -export const classicPreset: HalftoneLinesPreset = { - name: 'Classic', - params: { - ...defaultObjectSizing, - scale: 1, - speed: 0, - frame: 0, - colorBack: '#ffffff', - colorFront: '#000000', - grid: 'lines', - gridOffsetX: -0.5, - gridOffsetY: -0.5, - stripeWidth: 1, - smoothness: 10, - size: 0.6, - thinLines: true, - allowOverflow: true, - gridAngleDistortion: 0, - gridNoiseDistortion: 0, - gridRotation: 0, - contrast: 0.7, - originalColors: false, - inverted: false, - grainMixer: 0, - grainMixerSize: 1, - grainOverlay: 0, - grainOverlaySize: 0.5, - }, -}; - -export const halftoneLinesPresets: HalftoneLinesPreset[] = [defaultPreset, strokesPreset, noisePreset, classicPreset]; - -export const HalftoneLines: React.FC = memo(function HalftoneLinesImpl({ - // Own props - colorBack = defaultPreset.params.colorBack, - colorFront = defaultPreset.params.colorFront, - speed = defaultPreset.params.speed, - frame = defaultPreset.params.frame, - image = '', - grid = defaultPreset.params.grid, - gridOffsetX = defaultPreset.params.gridOffsetX, - gridOffsetY = defaultPreset.params.gridOffsetY, - gridAngleDistortion = defaultPreset.params.gridAngleDistortion, - gridNoiseDistortion = defaultPreset.params.gridNoiseDistortion, - stripeWidth = defaultPreset.params.stripeWidth, - smoothness = defaultPreset.params.smoothness, - size = defaultPreset.params.size, - thinLines = defaultPreset.params.thinLines, - allowOverflow = defaultPreset.params.allowOverflow, - gridRotation = defaultPreset.params.gridRotation, - contrast = defaultPreset.params.contrast, - originalColors = defaultPreset.params.originalColors, - inverted = defaultPreset.params.inverted, - grainMixer = defaultPreset.params.grainMixer, - grainMixerSize = defaultPreset.params.grainMixerSize, - grainOverlay = defaultPreset.params.grainOverlay, - grainOverlaySize = defaultPreset.params.grainOverlaySize, - - // Sizing props - fit = defaultPreset.params.fit, - scale = defaultPreset.params.scale, - rotation = defaultPreset.params.rotation, - originX = defaultPreset.params.originX, - originY = defaultPreset.params.originY, - offsetX = defaultPreset.params.offsetX, - offsetY = defaultPreset.params.offsetY, - worldWidth = defaultPreset.params.worldWidth, - worldHeight = defaultPreset.params.worldHeight, - ...props -}: HalftoneLinesProps) { - const uniforms = { - // Own uniforms - u_colorBack: getShaderColorFromString(colorBack), - u_colorFront: getShaderColorFromString(colorFront), - - u_image: image, - u_grid: HalftoneLinesGrids[grid], - u_gridOffsetX: gridOffsetX, - u_gridOffsetY: gridOffsetY, - u_gridAngleDistortion: gridAngleDistortion, - u_gridNoiseDistortion: gridNoiseDistortion, - u_stripeWidth: stripeWidth, - u_smoothness: smoothness, - u_size: size, - u_thinLines: thinLines, - u_allowOverflow: allowOverflow, - u_gridRotation: gridRotation, - u_contrast: contrast, - u_originalColors: originalColors, - u_inverted: inverted, - u_grainMixer: grainMixer, - u_grainMixerSize: grainMixerSize, - u_grainOverlay: grainOverlay, - u_grainOverlaySize: grainOverlaySize, - - // Sizing uniforms - u_fit: ShaderFitOptions[fit], - u_scale: scale, - u_rotation: rotation, - u_offsetX: offsetX, - u_offsetY: offsetY, - u_originX: originX, - u_originY: originY, - u_worldWidth: worldWidth, - u_worldHeight: worldHeight, - } satisfies HalftoneLinesUniforms; - - return ( - - ); -}); diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index 29d3cc75a..29ebaa818 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -228,30 +228,6 @@ export { type FoldsUniforms, } from './shaders/folds.js'; -export { - HalftoneLinesGrids, - halftoneLinesMeta, - halftoneLinesFragmentShader, - type HalftoneLinesParams, - type HalftoneLinesUniforms, - type HalftoneLinesGrid, -} from './shaders/halftone-lines.js'; - -export { - blobsLogoMeta, - blobsLogoFragmentShader, - toProcessedBlobsLogo, - type BlobsLogoParams, - type BlobsLogoUniforms, -} from './shaders/blobs-logo.js'; - -export { - gemSmokeMeta, - gemSmokeFragmentShader, - toProcessedGemSmoke, - type GemSmokeParams, - type GemSmokeUniforms, -} from './shaders/gem-smoke.js'; // ----- Utils ----- // export { getShaderColorFromString } from './get-shader-color-from-string.js'; diff --git a/packages/shaders/src/shaders/blobs-logo.ts b/packages/shaders/src/shaders/blobs-logo.ts deleted file mode 100644 index 727a93d09..000000000 --- a/packages/shaders/src/shaders/blobs-logo.ts +++ /dev/null @@ -1,741 +0,0 @@ -import type { vec4 } from '../types.js'; -import type { ShaderMotionParams } from '../shader-mount.js'; -import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, rotation2, textureRandomizerGB, textureRandomizerR, colorBandingFix } from '../shader-utils.js'; - -export const blobsLogoMeta = { - maxColorCount: 6, -} as const; - -/** - * - * Fluid motion imitation applied over user image - * (animated stripe pattern getting distorted with shape edges) - * - * Uniforms: - * - u_colorBack, u_colorFront (RGBA) - * - */ - -// language=GLSL -export const blobsLogoFragmentShader: string = `#version 300 es -precision mediump float; - -uniform sampler2D u_image; -uniform float u_imageAspectRatio; - -uniform sampler2D u_noiseTexture; - -uniform vec2 u_resolution; -uniform float u_time; - -uniform vec4 u_colors[${blobsLogoMeta.maxColorCount}]; -uniform float u_colorsCount; -uniform vec4 u_colorBack; -uniform vec4 u_colorInner; -uniform float u_contour; -uniform float u_size; - -${sizingVariablesDeclaration} - -out vec4 fragColor; - - -${ declarePI } -${ textureRandomizerR } -float valueNoise(vec2 st) { - vec2 i = floor(st); - vec2 f = fract(st); - float a = randomR(i); - float b = randomR(i + vec2(1.0, 0.0)); - float c = randomR(i + vec2(0.0, 1.0)); - float d = randomR(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - float x1 = mix(a, b, u.x); - float x2 = mix(c, d, u.x); - return mix(x1, x2, u.y); -} -float fbm(vec2 n) { - float total = 0.0, amplitude = .4; - for (int i = 0; i < 8; i++) { - total += valueNoise(n) * amplitude; - n *= 1.99; - amplitude *= 0.65; - } - return total; -} - -float getNoise(vec2 uv, vec2 pUv, float t, float shape) { -// float noiseLeft = fbm(.9 * pUv, t); -// float noiseRight = fbm(mix(4., 2., shape) * pUv + t, t); - - float noise = fbm(1.2 * pUv * mix(1., 1.4, shape) - vec2(0., .1 * t)); -// noise *= fbm(.8 * pUv + vec2(0., .32 * t)); - - return noise; -} - -${rotation2} - -float getImgFrame(vec2 uv, float th) { - float frame = 1.; - frame *= smoothstep(0., th, uv.y); - frame *= 1.0 - smoothstep(1. - th, 1., uv.y); - frame *= smoothstep(0., th, uv.x); - frame *= 1.0 - smoothstep(1. - th, 1., uv.x); - return frame; -} - -float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) { - vec2 texel = 1.0 / vec2(textureSize(tex, 0)); - vec2 r = max(radius, 0.0) * texel; - - // 1D Gaussian coefficients (Pascal row) - const float a = 1.0;// |offset| = 2 - const float b = 4.0;// |offset| = 1 - const float c = 6.0;// |offset| = 0 - - float norm = 256.0;// (a+b+c+b+a)^2 = 16^2 - float sum = 0.0; - - // y = -2 - { - float wy = a; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, -2.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, -2.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, -2.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, -2.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, -2.0*r.y)).r; - sum += wy * row; - } - - // y = -1 - { - float wy = b; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, -1.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, -1.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, -1.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, -1.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, -1.0*r.y)).r; - sum += wy * row; - } - - // y = 0 (use provided centerSample to avoid an extra fetch) - { - float wy = c; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 0.0)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 0.0)).r + - c * centerSample + - b * texture(tex, uv + vec2(1.0*r.x, 0.0)).r + - a * texture(tex, uv + vec2(2.0*r.x, 0.0)).r; - sum += wy * row; - } - - // y = +1 - { - float wy = b; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 1.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 1.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, 1.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, 1.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, 1.0*r.y)).r; - sum += wy * row; - } - - // y = +2 - { - float wy = a; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 2.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 2.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, 2.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, 2.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, 2.0*r.y)).r; - sum += wy * row; - } - - return sum / norm; -} - -float sst(float edge0, float edge1, float x) { - return smoothstep(edge0, edge1, x); -} - -float getPoint(vec2 dist, float p) { - float v = pow(1. - clamp(0., 1., length(dist)), 1.); - v = smoothstep(0., 1., v); - v = pow(v, p); - return v; -} - - -${ declarePI } -${ textureRandomizerGB } - -vec4 voronoi(vec2 x, float t, float shape, float imgAlpha) { - vec2 ip = floor(x); - vec2 fp = fract(x); - - vec2 mg, mr; - float md = 8.; - float rand = 0.; - float u_distortion = .5 * shape; - - for (int j = -1; j <= 1; j++) { - for (int i = -1; i <= 1; i++) { - vec2 g = vec2(float(i), float(j)); - vec2 o = randomGB(ip + g); - float raw_hash = o.x; - o = .5 + u_distortion * sin(t + TWO_PI * o); - vec2 r = g + o - fp; - float d = dot(r, r); - - if (d < md) { - md = d; - mr = r; - mg = g; - rand = raw_hash; - } - } - } - - md = 8.; - for (int j = -2; j <= 2; j++) { - for (int i = -2; i <= 2; i++) { - vec2 g = mg + vec2(float(i), float(j)); - vec2 o = randomGB(ip + g); - o = .5 + u_distortion * sin(t + TWO_PI * o); - vec2 r = g + o - fp; - if (dot(mr - r, mr - r) > .00001) { - md = min(md, dot(.5 * (mr + r), normalize(r - mr))); - } - } - } - - return vec4(md, mr, rand); -} - -void main() { - - - vec2 uv = v_imageUV; - vec2 dudx = dFdx(v_imageUV); - vec2 dudy = dFdy(v_imageUV); - vec4 img = textureGrad(u_image, uv, dudx, dudy); - - float edge = img.r; - edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); - - float imgAlpha = img.g; - - float frame = getImgFrame(v_imageUV, 0.); - imgAlpha *= frame; - - - vec2 shape_uv = v_objectUV; - - float t = u_time; - - float cycleDuration = 3.; - float period2 = 2.0 * cycleDuration; - float localTime1 = fract((0.1 * t + cycleDuration) / period2) * period2; - float localTime2 = fract((0.1 * t) / period2) * period2; - float timeBlend = .5 + .5 * sin(.1 * t * PI / cycleDuration - .5 * PI); - -// float atg = atan(shape_uv.y, shape_uv.x) + .001; -// float l = length(shape_uv); -// vec2 polar_uv1 = vec2(atg, localTime1 - (.5 * l) + 1. / pow(max(1e-4, l), .5)); -// polar_uv1 *= u_noiseScale; - float noise1 = getNoise(shape_uv, shape_uv, t, edge); - noise1 = 4. * edge * pow(noise1, 4.); - noise1 = 1. - clamp(noise1, 0., 1.); - - noise1 = mix(0., noise1, imgAlpha); - -// vec2 polar_uv2 = vec2(atg, localTime2 - (.5 * l) + 1. / pow(max(1e-4, l), .5)); -// polar_uv2 *= u_noiseScale; -// float noise2 = getNoise(shape_uv, shape_uv, t); - -// float noise = mix(noise1, noise2, timeBlend); -// -// shape_uv *= (.8 + 1.2 * noise); - -// float ringShape = getRingShape(shape_uv); - float ringShape = 1.; - - float mixer = ringShape * ringShape * (u_colorsCount - 1.); - vec4 gradient = u_colors[int(u_colorsCount) - 1]; - gradient.rgb *= gradient.a; - for (int i = ${ blobsLogoMeta.maxColorCount } - 2; i >= 0; i--) { - float localT = clamp(mixer - float(int(u_colorsCount) - 1 - i - 1), 0., 1.); - vec4 c = u_colors[i]; - c.rgb *= c.a; - gradient = mix(gradient, c, localT); - } - - vec3 color = gradient.rgb * ringShape; - float opacity = gradient.a * ringShape; - - vec3 bgColor = u_colorBack.rgb * u_colorBack.a; - color = color + bgColor * (1. - opacity); - opacity = opacity + u_colorBack.a * (1. - opacity); - - ${ colorBandingFix } - // fragColor = vec4(color, opacity); - fragColor = vec4(vec3(noise1), 1.); - -} -`; - -// Configuration for Poisson solver -export const POISSON_CONFIG_OPTIMIZED = { - measurePerformance: false, // Set to true to see performance metrics - workingSize: 512, // Size to solve Poisson at (will upscale to original size) - iterations: 32, // SOR converges ~2-20x faster than standard Gauss-Seidel -}; - -// Precomputed pixel data for sparse processing -interface SparsePixelData { - interiorPixels: Uint32Array; // Indices of interior pixels - boundaryPixels: Uint32Array; // Indices of boundary pixels - pixelCount: number; - // Neighbor indices for each interior pixel (4 neighbors per pixel) - // Layout: [east, west, north, south] for each pixel - neighborIndices: Int32Array; -} - -export function toProcessedBlobsLogo(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - const isBlob = typeof file === 'string' && file.startsWith('blob:'); - - return new Promise((resolve, reject) => { - if (!file || !ctx) { - reject(new Error('Invalid file or canvas context')); - return; - } - - const blobContentTypePromise = isBlob && fetch(file).then((res) => res.headers.get('Content-Type')); - const img = new Image(); - img.crossOrigin = 'anonymous'; - const totalStartTime = performance.now(); - - img.onload = async () => { - // Force SVG to load at a high fidelity size if it's an SVG - let isSVG; - - const blobContentType = await blobContentTypePromise; - - if (blobContentType) { - isSVG = blobContentType === 'image/svg+xml'; - } else if (typeof file === 'string') { - isSVG = file.endsWith('.svg') || file.startsWith('data:image/svg+xml'); - } else { - isSVG = file.type === 'image/svg+xml'; - } - - let originalWidth = img.width || img.naturalWidth; - let originalHeight = img.height || img.naturalHeight; - - if (isSVG) { - // Scale SVG to max dimension while preserving aspect ratio - const svgMaxSize = 4096; - const aspectRatio = originalWidth / originalHeight; - - if (originalWidth > originalHeight) { - originalWidth = svgMaxSize; - originalHeight = svgMaxSize / aspectRatio; - } else { - originalHeight = svgMaxSize; - originalWidth = svgMaxSize * aspectRatio; - } - - img.width = originalWidth; - img.height = originalHeight; - } - - // Always scale to working resolution for consistency - const minDimension = Math.min(originalWidth, originalHeight); - const targetSize = POISSON_CONFIG_OPTIMIZED.workingSize; - - // Calculate scale to fit within workingSize - const scaleFactor = targetSize / minDimension; - const width = Math.round(originalWidth * scaleFactor); - const height = Math.round(originalHeight * scaleFactor); - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Processing Mode]`); - console.log(` Original: ${originalWidth}×${originalHeight}`); - console.log(` Working: ${width}×${height} (${(scaleFactor * 100).toFixed(1)}% scale)`); - if (scaleFactor < 1) { - console.log(` Speedup: ~${Math.round(1 / (scaleFactor * scaleFactor))}×`); - } - } - - canvas.width = originalWidth; - canvas.height = originalHeight; - - // Use a smaller canvas for shape detection and Poisson solving - const shapeCanvas = document.createElement('canvas'); - shapeCanvas.width = width; - shapeCanvas.height = height; - - const shapeCtx = shapeCanvas.getContext('2d')!; - shapeCtx.drawImage(img, 0, 0, width, height); - - // 1) Build optimized masks using TypedArrays - const startMask = performance.now(); - - const shapeImageData = shapeCtx.getImageData(0, 0, width, height); - const data = shapeImageData.data; - - // Use Uint8Array for masks (1 byte per pixel vs 8+ bytes for boolean array) - const shapeMask = new Uint8Array(width * height); - const boundaryMask = new Uint8Array(width * height); - - // First pass: identify shape pixels - let shapePixelCount = 0; - for (let i = 0, idx = 0; i < data.length; i += 4, idx++) { - const a = data[i + 3]; - const isShape = a === 0 ? 0 : 1; - shapeMask[idx] = isShape; - shapePixelCount += isShape; - } - - // 2) Optimized boundary detection using sparse approach - // Only check shape pixels, not all pixels - const boundaryIndices: number[] = []; - const interiorIndices: number[] = []; - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = y * width + x; - if (!shapeMask[idx]) continue; - - // Check if pixel is on boundary (optimized: early exit) - let isBoundary = false; - - // Check 4-connected neighbors first (most common case) - if (x === 0 || x === width - 1 || y === 0 || y === height - 1) { - isBoundary = true; - } else { - // Check all 8 neighbors (including diagonals) for comprehensive boundary detection - isBoundary = - !shapeMask[idx - 1] || // left - !shapeMask[idx + 1] || // right - !shapeMask[idx - width] || // top - !shapeMask[idx + width] || // bottom - !shapeMask[idx - width - 1] || // top-left - !shapeMask[idx - width + 1] || // top-right - !shapeMask[idx + width - 1] || // bottom-left - !shapeMask[idx + width + 1]; // bottom-right - } - - if (isBoundary) { - boundaryMask[idx] = 1; - boundaryIndices.push(idx); - } else { - interiorIndices.push(idx); - } - } - } - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Mask Building] Time: ${(performance.now() - startMask).toFixed(2)}ms`); - console.log( - ` Shape pixels: ${shapePixelCount} / ${width * height} (${((shapePixelCount / (width * height)) * 100).toFixed(1)}%)` - ); - console.log(` Interior pixels: ${interiorIndices.length}`); - console.log(` Boundary pixels: ${boundaryIndices.length}`); - } - - // 3) Precompute sparse data structure for solver - const sparseData = buildSparseData( - shapeMask, - boundaryMask, - new Uint32Array(interiorIndices), - new Uint32Array(boundaryIndices), - width, - height - ); - - // 4) Solve Poisson equation with optimized sparse solver - const startSolve = performance.now(); - const u = solvePoissonSparse(sparseData, shapeMask, boundaryMask, width, height); - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Poisson Solve] Time: ${(performance.now() - startSolve).toFixed(2)}ms`); - } - - // 5) Generate output image - let maxVal = 0; - let finalImageData: ImageData; - - // Only check shape pixels for max value - for (let i = 0; i < interiorIndices.length; i++) { - const idx = interiorIndices[i]!; - if (u[idx]! > maxVal) maxVal = u[idx]!; - } - - // Create roundness image at working resolution - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = width; - tempCanvas.height = height; - const tempCtx = tempCanvas.getContext('2d')!; - - const tempImg = tempCtx.createImageData(width, height); - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = y * width + x; - const px = idx * 4; - - if (!shapeMask[idx]) { - tempImg.data[px] = 255; - tempImg.data[px + 1] = 255; - tempImg.data[px + 2] = 255; - tempImg.data[px + 3] = 0; // Alpha = 0 for background - } else { - const poissonRatio = u[idx]! / maxVal; - let gray = 255 * (1 - poissonRatio); - tempImg.data[px] = gray; - tempImg.data[px + 1] = gray; - tempImg.data[px + 2] = gray; - tempImg.data[px + 3] = 255; // Alpha = 255 for shape - } - } - } - tempCtx.putImageData(tempImg, 0, 0); - - // Upscale to original resolution with smooth interpolation - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'high'; - ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, originalWidth, originalHeight); - - // Now get the upscaled image data for final output - const outImg = ctx.getImageData(0, 0, originalWidth, originalHeight); - - // Re-apply edges from original resolution with anti-aliasing - // This ensures edges are pixel-perfect while roundness is smooth - const originalCanvas = document.createElement('canvas'); - originalCanvas.width = originalWidth; - originalCanvas.height = originalHeight; - const originalCtx = originalCanvas.getContext('2d')!; - // originalCtx.fillStyle = "white"; - // originalCtx.fillRect(0, 0, originalWidth, originalHeight); - originalCtx.drawImage(img, 0, 0, originalWidth, originalHeight); - const originalData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); - - // Process each pixel: Red channel = roundness, Alpha channel = original alpha - for (let i = 0; i < outImg.data.length; i += 4) { - const a = originalData.data[i + 3]!; - // Use only alpha to determine background vs shape - const upscaledAlpha = outImg.data[i + 3]!; - if (a === 0) { - // Background pixel - outImg.data[i] = 255; - outImg.data[i + 1] = 0; - } else { - // Red channel carries the roundness - // Check if upscale missed this pixel by looking at alpha channel - // If upscaled alpha is 0, the low-res version thought this was background - outImg.data[i] = upscaledAlpha === 0 ? 0 : outImg.data[i]!; // roundness or 0 - outImg.data[i + 1] = a; // original alpha - } - - // Unused channels fixed - outImg.data[i + 2] = 255; - outImg.data[i + 3] = 255; - } - - ctx.putImageData(outImg, 0, 0); - finalImageData = outImg; - canvas.toBlob((blob) => { - if (!blob) { - reject(new Error('Failed to create PNG blob')); - return; - } - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - const totalTime = performance.now() - totalStartTime; - console.log(`[Total Processing Time] ${totalTime.toFixed(2)}ms`); - if (scaleFactor < 1) { - const estimatedFullResTime = totalTime * Math.pow((originalWidth * originalHeight) / (width * height), 1.5); - console.log(`[Estimated time at full resolution] ~${estimatedFullResTime.toFixed(0)}ms`); - console.log( - `[Time saved] ~${(estimatedFullResTime - totalTime).toFixed(0)}ms (${Math.round(estimatedFullResTime / totalTime)}× faster)` - ); - } - } - - resolve({ - imageData: finalImageData, - pngBlob: blob, - }); - }, 'image/png'); - }; - - img.onerror = () => reject(new Error('Failed to load image')); - img.src = typeof file === 'string' ? file : URL.createObjectURL(file); - }); -} - -function buildSparseData( - shapeMask: Uint8Array, - boundaryMask: Uint8Array, - interiorPixels: Uint32Array, - boundaryPixels: Uint32Array, - width: number, - height: number -): SparsePixelData { - const pixelCount = interiorPixels.length; - - // Build neighbor indices for sparse processing - // For each interior pixel, store indices of its 4 neighbors - // Use -1 for out-of-bounds or non-shape neighbors - const neighborIndices = new Int32Array(pixelCount * 4); - - for (let i = 0; i < pixelCount; i++) { - const idx = interiorPixels[i]!; - const x = idx % width; - const y = Math.floor(idx / width); - - // East neighbor - neighborIndices[i * 4 + 0] = x < width - 1 && shapeMask[idx + 1] ? idx + 1 : -1; - // West neighbor - neighborIndices[i * 4 + 1] = x > 0 && shapeMask[idx - 1] ? idx - 1 : -1; - // North neighbor - neighborIndices[i * 4 + 2] = y > 0 && shapeMask[idx - width] ? idx - width : -1; - // South neighbor - neighborIndices[i * 4 + 3] = y < height - 1 && shapeMask[idx + width] ? idx + width : -1; - } - - return { - interiorPixels, - boundaryPixels, - pixelCount, - neighborIndices, - }; -} - -function solvePoissonSparse( - sparseData: SparsePixelData, - shapeMask: Uint8Array, - boundaryMask: Uint8Array, - width: number, - height: number -): Float32Array { - // This controls how smooth the falloff roundness will be and extend into the shape - const ITERATIONS = POISSON_CONFIG_OPTIMIZED.iterations; - - // Keep C constant - only iterations control roundness spread - const C = 0.01; - - const u = new Float32Array(width * height); - const { interiorPixels, neighborIndices, pixelCount } = sparseData; - - // Performance tracking - const startTime = performance.now(); - - // Red-Black SOR for better symmetry with fewer iterations - // omega between 1.8-1.95 typically gives best convergence for Poisson - const omega = 1.9; - - // Pre-classify pixels as red or black for efficient processing - const redPixels: number[] = []; - const blackPixels: number[] = []; - - for (let i = 0; i < pixelCount; i++) { - const idx = interiorPixels[i]!; - const x = idx % width; - const y = Math.floor(idx / width); - - if ((x + y) % 2 === 0) { - redPixels.push(i); - } else { - blackPixels.push(i); - } - } - - for (let iter = 0; iter < ITERATIONS; iter++) { - // Red pass: update red pixels - for (const i of redPixels) { - const idx = interiorPixels[i]!; - - // Get precomputed neighbor indices - const eastIdx = neighborIndices[i * 4 + 0]!; - const westIdx = neighborIndices[i * 4 + 1]!; - const northIdx = neighborIndices[i * 4 + 2]!; - const southIdx = neighborIndices[i * 4 + 3]!; - - // Sum neighbors (use 0 for out-of-bounds) - let sumN = 0; - if (eastIdx >= 0) sumN += u[eastIdx]!; - if (westIdx >= 0) sumN += u[westIdx]!; - if (northIdx >= 0) sumN += u[northIdx]!; - if (southIdx >= 0) sumN += u[southIdx]!; - - // SOR update: blend new value with old value - const newValue = (C + sumN) / 4; - u[idx] = omega * newValue + (1 - omega) * u[idx]!; - } - - // Black pass: update black pixels - for (const i of blackPixels) { - const idx = interiorPixels[i]!; - - // Get precomputed neighbor indices - const eastIdx = neighborIndices[i * 4 + 0]!; - const westIdx = neighborIndices[i * 4 + 1]!; - const northIdx = neighborIndices[i * 4 + 2]!; - const southIdx = neighborIndices[i * 4 + 3]!; - - // Sum neighbors (use 0 for out-of-bounds) - let sumN = 0; - if (eastIdx >= 0) sumN += u[eastIdx]!; - if (westIdx >= 0) sumN += u[westIdx]!; - if (northIdx >= 0) sumN += u[northIdx]!; - if (southIdx >= 0) sumN += u[southIdx]!; - - // SOR update: blend new value with old value - const newValue = (C + sumN) / 4; - u[idx] = omega * newValue + (1 - omega) * u[idx]!; - } - } - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - const elapsed = performance.now() - startTime; - - console.log(`[Optimized Poisson Solver (SOR ω=${omega})]`); - console.log(` Working size: ${width}×${height}`); - console.log(` Iterations: ${ITERATIONS}`); - console.log(` Time: ${elapsed.toFixed(2)}ms`); - console.log(` Interior pixels processed: ${pixelCount}`); - console.log(` Speed: ${((ITERATIONS * pixelCount) / (elapsed * 1000)).toFixed(2)} Mpixels/sec`); - } - - return u; -} - -export interface BlobsLogoUniforms extends ShaderSizingUniforms { - u_colorBack: [number, number, number, number]; - u_colorInner: [number, number, number, number]; - u_colors: vec4[]; - u_colorsCount: number; - u_image: HTMLImageElement | string | undefined; - u_contour: number; - u_size: number; - u_noiseTexture?: HTMLImageElement; -} - -export interface BlobsLogoParams extends ShaderSizingParams, ShaderMotionParams { - colors?: string[]; - colorBack?: string; - colorInner?: string; - image?: HTMLImageElement | string | undefined; - contour?: number; - size?: number; -} diff --git a/packages/shaders/src/shaders/gem-smoke.ts b/packages/shaders/src/shaders/gem-smoke.ts deleted file mode 100644 index d6becd88f..000000000 --- a/packages/shaders/src/shaders/gem-smoke.ts +++ /dev/null @@ -1,660 +0,0 @@ -import type { vec4 } from '../types.js'; -import type { ShaderMotionParams } from '../shader-mount.js'; -import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, rotation2, proceduralHash21 } from '../shader-utils.js'; - -export const gemSmokeMeta = { - maxColorCount: 6, -} as const; - -/** - * - * Fluid motion imitation applied over user image - * (animated stripe pattern getting distorted with shape edges) - * - * Uniforms: - * - u_colorBack, u_colorFront (RGBA) - * - */ - -// language=GLSL -export const gemSmokeFragmentShader: string = `#version 300 es -precision mediump float; - -uniform sampler2D u_image; -uniform float u_imageAspectRatio; - -uniform vec2 u_resolution; -uniform float u_time; - -uniform vec4 u_colors[${gemSmokeMeta.maxColorCount}]; -uniform float u_colorsCount; -uniform vec4 u_colorBack; -uniform float u_distortion; -uniform float u_outerVisibility; -uniform float u_innerFill; -uniform float u_outerDistortion; - -${sizingVariablesDeclaration} - -out vec4 fragColor; - -${ declarePI } -${ rotation2 } - -float getImgFrame(vec2 uv, float th) { - float frame = 1.; - frame *= smoothstep(0., th, uv.y); - frame *= 1.0 - smoothstep(1. - th, 1., uv.y); - frame *= smoothstep(0., th, uv.x); - frame *= 1.0 - smoothstep(1. - th, 1., uv.x); - return frame; -} - -float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius) { - vec2 texel = 1.0 / vec2(textureSize(tex, 0)); - vec2 r = max(radius, 0.0) * texel; - - // 1D Gaussian coefficients (Pascal row) - const float a = 1.0;// |offset| = 2 - const float b = 4.0;// |offset| = 1 - const float c = 6.0;// |offset| = 0 - - float norm = 256.0;// (a+b+c+b+a)^2 = 16^2 - float sum = 0.0; - - // y = -2 - { - float wy = a; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, -2.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, -2.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, -2.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, -2.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, -2.0*r.y)).r; - sum += wy * row; - } - - // y = -1 - { - float wy = b; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, -1.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, -1.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, -1.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, -1.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, -1.0*r.y)).r; - sum += wy * row; - } - - // y = 0 (use provided centerSample to avoid an extra fetch) - { - float wy = c; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 0.0)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 0.0)).r + - b * texture(tex, uv + vec2(0.)).r + - b * texture(tex, uv + vec2(1.0*r.x, 0.0)).r + - a * texture(tex, uv + vec2(2.0*r.x, 0.0)).r; - sum += wy * row; - } - - // y = +1 - { - float wy = b; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 1.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 1.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, 1.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, 1.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, 1.0*r.y)).r; - sum += wy * row; - } - - // y = +2 - { - float wy = a; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 2.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 2.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, 2.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, 2.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, 2.0*r.y)).r; - sum += wy * row; - } - - return sum / norm; -} - -float sst(float edge0, float edge1, float x) { - return smoothstep(edge0, edge1, x); -} - -float getPoint(vec2 dist, float p) { - float v = pow(1. - clamp(0., 1., length(dist)), 1.); - v = smoothstep(0., 1., v); - v = pow(v, p); - return v; -} - - -void main() { - - float t = u_time; - - vec2 uv = v_imageUV; - vec2 dudx = dFdx(v_imageUV); - vec2 dudy = dFdy(v_imageUV); - vec4 img = textureGrad(u_image, uv, dudx, dudy); - - float edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10.); - float imgAlpha = img.g; - - float frame = getImgFrame(v_imageUV, 0.); - imgAlpha *= frame; -// edge *= frame; - - - - uv = 2.5 * v_objectUV; - - float distortion = .9 * u_distortion; - float swirl = mix(0., distortion, u_outerDistortion) * (1. - imgAlpha) + distortion * edge; - - float midShift = distortion; - uv.y += midShift * (1. - smoothstep(0., 1., length(.4 * uv))); - uv.y -= .4 * midShift; - - for (int i = 1; i < 5; i++) { - float iFloat = float(i); - uv.x += swirl / iFloat * cos(t + iFloat * 2.5 * uv.y); - uv.y += swirl / iFloat * cos(t + iFloat * 1.5 * uv.x); - } - float shape = exp(-1.5 * (uv.x * uv.x + uv.y * uv.y)); - shape += mix(0., .15, u_innerFill) * imgAlpha * frame; - - float outerPower = pow(u_outerVisibility, 3.); - shape *= (outerPower + (1. - outerPower) * imgAlpha); - - shape = pow(shape, .75); - float mixer = shape * u_colorsCount; - vec4 gradient = u_colors[0]; - gradient.rgb *= gradient.a; - - float outerShape = 0.; - for (int i = 1; i < ${ gemSmokeMeta.maxColorCount + 1 }; i++) { - if (i > int(u_colorsCount)) break; - - float m = clamp(mixer - float(i - 1), 0., 1.); - float aa = fwidth(m); - m = smoothstep(0., 1., m); - - if (i == 1) { - outerShape = m; - } - - vec4 c = u_colors[i - 1]; - c.rgb *= c.a; - gradient = mix(gradient, c, m); - } - - vec3 color = gradient.rgb * outerShape; - float opacity = gradient.a * outerShape; - - vec3 bgColor = u_colorBack.rgb * u_colorBack.a; - color = color + bgColor * (1.0 - opacity); - opacity = opacity + u_colorBack.a * (1.0 - opacity); - - fragColor = vec4(color, opacity); -} -`; - -// Configuration for Poisson solver -export const POISSON_CONFIG_OPTIMIZED = { - measurePerformance: false, // Set to true to see performance metrics - workingSize: 512, // Size to solve Poisson at (will upscale to original size) - iterations: 32, // SOR converges ~2-20x faster than standard Gauss-Seidel -}; - -// Precomputed pixel data for sparse processing -interface SparsePixelData { - interiorPixels: Uint32Array; // Indices of interior pixels - boundaryPixels: Uint32Array; // Indices of boundary pixels - pixelCount: number; - // Neighbor indices for each interior pixel (4 neighbors per pixel) - // Layout: [east, west, north, south] for each pixel - neighborIndices: Int32Array; -} - -export function toProcessedGemSmoke(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - const isBlob = typeof file === 'string' && file.startsWith('blob:'); - - return new Promise((resolve, reject) => { - if (!file || !ctx) { - reject(new Error('Invalid file or canvas context')); - return; - } - - const blobContentTypePromise = isBlob && fetch(file).then((res) => res.headers.get('Content-Type')); - const img = new Image(); - img.crossOrigin = 'anonymous'; - const totalStartTime = performance.now(); - - img.onload = async () => { - // Force SVG to load at a high fidelity size if it's an SVG - let isSVG; - - const blobContentType = await blobContentTypePromise; - - if (blobContentType) { - isSVG = blobContentType === 'image/svg+xml'; - } else if (typeof file === 'string') { - isSVG = file.endsWith('.svg') || file.startsWith('data:image/svg+xml'); - } else { - isSVG = file.type === 'image/svg+xml'; - } - - let originalWidth = img.width || img.naturalWidth; - let originalHeight = img.height || img.naturalHeight; - - if (isSVG) { - // Scale SVG to max dimension while preserving aspect ratio - const svgMaxSize = 4096; - const aspectRatio = originalWidth / originalHeight; - - if (originalWidth > originalHeight) { - originalWidth = svgMaxSize; - originalHeight = svgMaxSize / aspectRatio; - } else { - originalHeight = svgMaxSize; - originalWidth = svgMaxSize * aspectRatio; - } - - img.width = originalWidth; - img.height = originalHeight; - } - - // Always scale to working resolution for consistency - const minDimension = Math.min(originalWidth, originalHeight); - const targetSize = POISSON_CONFIG_OPTIMIZED.workingSize; - - // Calculate scale to fit within workingSize - const scaleFactor = targetSize / minDimension; - const width = Math.round(originalWidth * scaleFactor); - const height = Math.round(originalHeight * scaleFactor); - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Processing Mode]`); - console.log(` Original: ${originalWidth}×${originalHeight}`); - console.log(` Working: ${width}×${height} (${(scaleFactor * 100).toFixed(1)}% scale)`); - if (scaleFactor < 1) { - console.log(` Speedup: ~${Math.round(1 / (scaleFactor * scaleFactor))}×`); - } - } - - canvas.width = originalWidth; - canvas.height = originalHeight; - - // Use a smaller canvas for shape detection and Poisson solving - const shapeCanvas = document.createElement('canvas'); - shapeCanvas.width = width; - shapeCanvas.height = height; - - const shapeCtx = shapeCanvas.getContext('2d')!; - shapeCtx.drawImage(img, 0, 0, width, height); - - // 1) Build optimized masks using TypedArrays - const startMask = performance.now(); - - const shapeImageData = shapeCtx.getImageData(0, 0, width, height); - const data = shapeImageData.data; - - // Use Uint8Array for masks (1 byte per pixel vs 8+ bytes for boolean array) - const shapeMask = new Uint8Array(width * height); - const boundaryMask = new Uint8Array(width * height); - - // First pass: identify shape pixels - let shapePixelCount = 0; - for (let i = 0, idx = 0; i < data.length; i += 4, idx++) { - const a = data[i + 3]; - const isShape = a === 0 ? 0 : 1; - shapeMask[idx] = isShape; - shapePixelCount += isShape; - } - - // 2) Optimized boundary detection using sparse approach - // Only check shape pixels, not all pixels - const boundaryIndices: number[] = []; - const interiorIndices: number[] = []; - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = y * width + x; - if (!shapeMask[idx]) continue; - - // Check if pixel is on boundary (optimized: early exit) - let isBoundary = false; - - // Check 4-connected neighbors first (most common case) - if (x === 0 || x === width - 1 || y === 0 || y === height - 1) { - isBoundary = true; - } else { - // Check all 8 neighbors (including diagonals) for comprehensive boundary detection - isBoundary = - !shapeMask[idx - 1] || // left - !shapeMask[idx + 1] || // right - !shapeMask[idx - width] || // top - !shapeMask[idx + width] || // bottom - !shapeMask[idx - width - 1] || // top-left - !shapeMask[idx - width + 1] || // top-right - !shapeMask[idx + width - 1] || // bottom-left - !shapeMask[idx + width + 1]; // bottom-right - } - - if (isBoundary) { - boundaryMask[idx] = 1; - boundaryIndices.push(idx); - } else { - interiorIndices.push(idx); - } - } - } - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Mask Building] Time: ${(performance.now() - startMask).toFixed(2)}ms`); - console.log( - ` Shape pixels: ${shapePixelCount} / ${width * height} (${((shapePixelCount / (width * height)) * 100).toFixed(1)}%)` - ); - console.log(` Interior pixels: ${interiorIndices.length}`); - console.log(` Boundary pixels: ${boundaryIndices.length}`); - } - - // 3) Precompute sparse data structure for solver - const sparseData = buildSparseData( - shapeMask, - boundaryMask, - new Uint32Array(interiorIndices), - new Uint32Array(boundaryIndices), - width, - height - ); - - // 4) Solve Poisson equation with optimized sparse solver - const startSolve = performance.now(); - const u = solvePoissonSparse(sparseData, shapeMask, boundaryMask, width, height); - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - console.log(`[Poisson Solve] Time: ${(performance.now() - startSolve).toFixed(2)}ms`); - } - - // 5) Generate output image - let maxVal = 0; - let finalImageData: ImageData; - - // Only check shape pixels for max value - for (let i = 0; i < interiorIndices.length; i++) { - const idx = interiorIndices[i]!; - if (u[idx]! > maxVal) maxVal = u[idx]!; - } - - // Create roundness image at working resolution - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = width; - tempCanvas.height = height; - const tempCtx = tempCanvas.getContext('2d')!; - - const tempImg = tempCtx.createImageData(width, height); - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = y * width + x; - const px = idx * 4; - - if (!shapeMask[idx]) { - tempImg.data[px] = 255; - tempImg.data[px + 1] = 255; - tempImg.data[px + 2] = 255; - tempImg.data[px + 3] = 0; // Alpha = 0 for background - } else { - const poissonRatio = u[idx]! / maxVal; - let gray = 255 * (1 - poissonRatio); - tempImg.data[px] = gray; - tempImg.data[px + 1] = gray; - tempImg.data[px + 2] = gray; - tempImg.data[px + 3] = 255; // Alpha = 255 for shape - } - } - } - tempCtx.putImageData(tempImg, 0, 0); - - // Upscale to original resolution with smooth interpolation - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'high'; - ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, originalWidth, originalHeight); - - // Now get the upscaled image data for final output - const outImg = ctx.getImageData(0, 0, originalWidth, originalHeight); - - // Re-apply edges from original resolution with anti-aliasing - // This ensures edges are pixel-perfect while roundness is smooth - const originalCanvas = document.createElement('canvas'); - originalCanvas.width = originalWidth; - originalCanvas.height = originalHeight; - const originalCtx = originalCanvas.getContext('2d')!; - // originalCtx.fillStyle = "white"; - // originalCtx.fillRect(0, 0, originalWidth, originalHeight); - originalCtx.drawImage(img, 0, 0, originalWidth, originalHeight); - const originalData = originalCtx.getImageData(0, 0, originalWidth, originalHeight); - - // Process each pixel: Red channel = roundness, Alpha channel = original alpha - for (let i = 0; i < outImg.data.length; i += 4) { - const a = originalData.data[i + 3]!; - // Use only alpha to determine background vs shape - const upscaledAlpha = outImg.data[i + 3]!; - if (a === 0) { - // Background pixel - outImg.data[i] = 255; - outImg.data[i + 1] = 0; - } else { - // Red channel carries the roundness - // Check if upscale missed this pixel by looking at alpha channel - // If upscaled alpha is 0, the low-res version thought this was background - outImg.data[i] = upscaledAlpha === 0 ? 0 : outImg.data[i]!; // roundness or 0 - outImg.data[i + 1] = a; // original alpha - } - - // Unused channels fixed - outImg.data[i + 2] = 255; - outImg.data[i + 3] = 255; - } - - ctx.putImageData(outImg, 0, 0); - finalImageData = outImg; - canvas.toBlob((blob) => { - if (!blob) { - reject(new Error('Failed to create PNG blob')); - return; - } - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - const totalTime = performance.now() - totalStartTime; - console.log(`[Total Processing Time] ${totalTime.toFixed(2)}ms`); - if (scaleFactor < 1) { - const estimatedFullResTime = totalTime * Math.pow((originalWidth * originalHeight) / (width * height), 1.5); - console.log(`[Estimated time at full resolution] ~${estimatedFullResTime.toFixed(0)}ms`); - console.log( - `[Time saved] ~${(estimatedFullResTime - totalTime).toFixed(0)}ms (${Math.round(estimatedFullResTime / totalTime)}× faster)` - ); - } - } - - resolve({ - imageData: finalImageData, - pngBlob: blob, - }); - }, 'image/png'); - }; - - img.onerror = () => reject(new Error('Failed to load image')); - img.src = typeof file === 'string' ? file : URL.createObjectURL(file); - }); -} - -function buildSparseData( - shapeMask: Uint8Array, - boundaryMask: Uint8Array, - interiorPixels: Uint32Array, - boundaryPixels: Uint32Array, - width: number, - height: number -): SparsePixelData { - const pixelCount = interiorPixels.length; - - // Build neighbor indices for sparse processing - // For each interior pixel, store indices of its 4 neighbors - // Use -1 for out-of-bounds or non-shape neighbors - const neighborIndices = new Int32Array(pixelCount * 4); - - for (let i = 0; i < pixelCount; i++) { - const idx = interiorPixels[i]!; - const x = idx % width; - const y = Math.floor(idx / width); - - // East neighbor - neighborIndices[i * 4 + 0] = x < width - 1 && shapeMask[idx + 1] ? idx + 1 : -1; - // West neighbor - neighborIndices[i * 4 + 1] = x > 0 && shapeMask[idx - 1] ? idx - 1 : -1; - // North neighbor - neighborIndices[i * 4 + 2] = y > 0 && shapeMask[idx - width] ? idx - width : -1; - // South neighbor - neighborIndices[i * 4 + 3] = y < height - 1 && shapeMask[idx + width] ? idx + width : -1; - } - - return { - interiorPixels, - boundaryPixels, - pixelCount, - neighborIndices, - }; -} - -function solvePoissonSparse( - sparseData: SparsePixelData, - shapeMask: Uint8Array, - boundaryMask: Uint8Array, - width: number, - height: number -): Float32Array { - // This controls how smooth the falloff roundness will be and extend into the shape - const ITERATIONS = POISSON_CONFIG_OPTIMIZED.iterations; - - // Keep C constant - only iterations control roundness spread - const C = 0.01; - - const u = new Float32Array(width * height); - const { interiorPixels, neighborIndices, pixelCount } = sparseData; - - // Performance tracking - const startTime = performance.now(); - - // Red-Black SOR for better symmetry with fewer iterations - // omega between 1.8-1.95 typically gives best convergence for Poisson - const omega = 1.9; - - // Pre-classify pixels as red or black for efficient processing - const redPixels: number[] = []; - const blackPixels: number[] = []; - - for (let i = 0; i < pixelCount; i++) { - const idx = interiorPixels[i]!; - const x = idx % width; - const y = Math.floor(idx / width); - - if ((x + y) % 2 === 0) { - redPixels.push(i); - } else { - blackPixels.push(i); - } - } - - for (let iter = 0; iter < ITERATIONS; iter++) { - // Red pass: update red pixels - for (const i of redPixels) { - const idx = interiorPixels[i]!; - - // Get precomputed neighbor indices - const eastIdx = neighborIndices[i * 4 + 0]!; - const westIdx = neighborIndices[i * 4 + 1]!; - const northIdx = neighborIndices[i * 4 + 2]!; - const southIdx = neighborIndices[i * 4 + 3]!; - - // Sum neighbors (use 0 for out-of-bounds) - let sumN = 0; - if (eastIdx >= 0) sumN += u[eastIdx]!; - if (westIdx >= 0) sumN += u[westIdx]!; - if (northIdx >= 0) sumN += u[northIdx]!; - if (southIdx >= 0) sumN += u[southIdx]!; - - // SOR update: blend new value with old value - const newValue = (C + sumN) / 4; - u[idx] = omega * newValue + (1 - omega) * u[idx]!; - } - - // Black pass: update black pixels - for (const i of blackPixels) { - const idx = interiorPixels[i]!; - - // Get precomputed neighbor indices - const eastIdx = neighborIndices[i * 4 + 0]!; - const westIdx = neighborIndices[i * 4 + 1]!; - const northIdx = neighborIndices[i * 4 + 2]!; - const southIdx = neighborIndices[i * 4 + 3]!; - - // Sum neighbors (use 0 for out-of-bounds) - let sumN = 0; - if (eastIdx >= 0) sumN += u[eastIdx]!; - if (westIdx >= 0) sumN += u[westIdx]!; - if (northIdx >= 0) sumN += u[northIdx]!; - if (southIdx >= 0) sumN += u[southIdx]!; - - // SOR update: blend new value with old value - const newValue = (C + sumN) / 4; - u[idx] = omega * newValue + (1 - omega) * u[idx]!; - } - } - - if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { - const elapsed = performance.now() - startTime; - - console.log(`[Optimized Poisson Solver (SOR ω=${omega})]`); - console.log(` Working size: ${width}×${height}`); - console.log(` Iterations: ${ITERATIONS}`); - console.log(` Time: ${elapsed.toFixed(2)}ms`); - console.log(` Interior pixels processed: ${pixelCount}`); - console.log(` Speed: ${((ITERATIONS * pixelCount) / (elapsed * 1000)).toFixed(2)} Mpixels/sec`); - } - - return u; -} - -export interface GemSmokeUniforms extends ShaderSizingUniforms { - u_colorBack: [number, number, number, number]; - u_colors: vec4[]; - u_colorsCount: number; - u_image: HTMLImageElement | string | undefined; - u_distortion: number; - u_outerVisibility: number; - u_innerFill: number; - u_outerDistortion: number; -} - -export interface GemSmokeParams extends ShaderSizingParams, ShaderMotionParams { - colors?: string[]; - colorBack?: string; - image?: HTMLImageElement | string | undefined; - distortion?: number; - outerVisibility?: number; - innerFill?: number; - outerDistortion?: number; -} diff --git a/packages/shaders/src/shaders/halftone-lines.ts b/packages/shaders/src/shaders/halftone-lines.ts deleted file mode 100644 index 8d42ac393..000000000 --- a/packages/shaders/src/shaders/halftone-lines.ts +++ /dev/null @@ -1,363 +0,0 @@ -import type { ShaderMotionParams } from '../shader-mount.js'; -import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, rotation2, simplexNoise, proceduralHash21 } from '../shader-utils.js'; - -export const halftoneLinesMeta = { - maxBlurRadius: 8, -} as const; - -/** - * - * Fluid motion imitation applied over user image - * (animated stripe pattern getting distorted with shape edges) - * - * Uniforms: - * - u_colorBack, u_colorFront (RGBA) - * - */ - -// language=GLSL -export const halftoneLinesFragmentShader: string = `#version 300 es -precision mediump float; - -uniform mediump vec2 u_resolution; -uniform mediump float u_pixelRatio; -uniform mediump float u_originX; -uniform mediump float u_originY; -uniform mediump float u_fit; - -uniform mediump float u_scale; -uniform mediump float u_rotation; -uniform mediump float u_offsetX; -uniform mediump float u_offsetY; - -uniform float u_time; - -uniform vec4 u_colorFront; -uniform vec4 u_colorBack; -uniform float u_radius; -uniform float u_contrast; - -uniform sampler2D u_image; -uniform mediump float u_imageAspectRatio; - -uniform float u_size; -uniform bool u_thinLines; -uniform bool u_allowOverflow; -uniform float u_grid; -uniform float u_gridOffsetX; -uniform float u_gridOffsetY; -uniform float u_grainMixer; -uniform float u_grainMixerSize; -uniform float u_grainOverlay; -uniform float u_grainOverlaySize; -uniform bool u_straight; -uniform bool u_originalColors; -uniform bool u_inverted; -uniform float u_stripeWidth; -uniform float u_smoothness; -uniform float u_gridAngleDistortion; -uniform float u_gridNoiseDistortion; -uniform float u_gridRotation; - - -${sizingVariablesDeclaration} - -out vec4 fragColor; - -${declarePI} -${rotation2} -${simplexNoise} -${proceduralHash21} - -float valueNoise(vec2 st) { - vec2 i = floor(st); - vec2 f = fract(st); - float a = hash21(i); - float b = hash21(i + vec2(1.0, 0.0)); - float c = hash21(i + vec2(0.0, 1.0)); - float d = hash21(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - float x1 = mix(a, b, u.x); - float x2 = mix(c, d, u.x); - return mix(x1, x2, u.y); -} - -float lst(float edge0, float edge1, float x) { - return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); -} - -vec2 getImageUV(vec2 uv, vec2 extraScale) { - vec2 boxOrigin = vec2(.5 - u_originX, u_originY - .5); - float r = u_rotation * PI / 180.; - mat2 graphicRotation = mat2(cos(r), sin(r), -sin(r), cos(r)); - vec2 graphicOffset = vec2(-u_offsetX, u_offsetY); - - vec2 imageBoxSize; - if (u_fit == 1.) { - imageBoxSize.x = min(u_resolution.x / u_imageAspectRatio, u_resolution.y) * u_imageAspectRatio; - } else { - imageBoxSize.x = max(u_resolution.x / u_imageAspectRatio, u_resolution.y) * u_imageAspectRatio; - } - imageBoxSize.y = imageBoxSize.x / u_imageAspectRatio; - vec2 imageBoxScale = u_resolution.xy / imageBoxSize; - - vec2 imageUV = uv; - imageUV *= imageBoxScale; - imageUV += boxOrigin * (imageBoxScale - 1.); - imageUV += graphicOffset; - imageUV /= u_scale; - imageUV *= extraScale; - imageUV.x *= u_imageAspectRatio; - imageUV = graphicRotation * imageUV; - imageUV.x /= u_imageAspectRatio; - - imageUV += .5; - imageUV.y = 1. - imageUV.y; - - return imageUV; -} - - -float getImgFrame(vec2 uv, float th) { - float frame = 1.; - frame *= smoothstep(0., th, uv.y); - frame *= 1.0 - smoothstep(1. - th, 1., uv.y); - frame *= smoothstep(0., th, uv.x); - frame *= 1.0 - smoothstep(1. - th, 1., uv.x); - return frame; -} - -float sst(float edge0, float edge1, float x) { - return smoothstep(edge0, edge1, x); -} - -float sigmoid(float x, float k) { - return 1.0 / (1.0 + exp(-k * (x - 0.5))); -} - -vec4 blurTexture(sampler2D tex, vec2 uv, vec2 texelSize, float radius) { - // clamp radius so loops have a known max - float r = clamp(radius, 0., float(${halftoneLinesMeta.maxBlurRadius})); - int ir = int(r); - - vec4 acc = vec4(0.0); - float weightSum = 0.0; - - // simple Gaussian-ish weights based on distance - for (int y = -20; y <= ${halftoneLinesMeta.maxBlurRadius}; ++y) { - if (abs(y) > ir) continue; - for (int x = -20; x <= ${halftoneLinesMeta.maxBlurRadius}; ++x) { - if (abs(x) > ir) continue; - - vec2 offset = vec2(float(x), float(y)); - float dist2 = dot(offset, offset); - - // tweak sigma to taste; lower sigma = sharper falloff - float sigma = radius * 0.5 + 0.001; - float w = exp(-dist2 / (2.0 * sigma * sigma)); - - acc += texture(tex, uv + offset * texelSize) * w; - weightSum += w; - } - } - - return acc / max(weightSum, 0.00001); -} - - -float getLumAtPx(vec2 uv, float contrast, out vec3 origColor) { - vec4 tex = blurTexture(u_image, uv, vec2(1. / u_resolution), u_smoothness); - - origColor = tex.rgb; - vec3 color = vec3( - sigmoid(tex.r, contrast), - sigmoid(tex.g, contrast), - sigmoid(tex.b, contrast) - ); - float lum = dot(vec3(0.2126, 0.7152, 0.0722), color); - lum = mix(1., lum, tex.a); - lum = u_inverted ? (1. - lum) : lum; - return lum; -} - -float blendOverlay(float base, float blend) { - return base<0.5?(2.0*base*blend):(1.0-2.0*(1.0-base)*(1.0-blend)); -} - -vec3 blendOverlay(vec3 base, vec3 blend) { - return vec3(blendOverlay(base.r, blend.r), blendOverlay(base.g, blend.g), blendOverlay(base.b, blend.b)); -} - -vec3 blendHardLight(vec3 base, vec3 blend) { - return blendOverlay(blend, base); -} - -vec3 blendHardLight(vec3 base, vec3 blend, float opacity) { - return (blendHardLight(base, blend) * opacity + base * (1.0 - opacity)); -} - -void main() { - - vec2 uv = gl_FragCoord.xy - .5 * u_resolution; - - vec2 uvNormalised = uv / u_resolution.xy; - vec2 uvOriginal = getImageUV(uvNormalised, vec2(1.)); - - float contrast = mix(0., 15., u_contrast); - - vec3 origColor = vec3(0.); - float lum = getLumAtPx(uvOriginal, contrast, origColor); - - float frame = getImgFrame(v_imageUV, 0.); - lum = mix(1., lum, frame); - lum = 1. - lum; - - uv = v_objectUV; - float noise = snoise(2.5 * uv + 100.); - - vec2 uvGrid = v_objectUV; - uvGrid += .15 * noise * lum * u_gridNoiseDistortion; - float gridSize = mix(200., 5., u_size); - uvGrid *= gridSize; - - float gridLine; - - float angleOffset = u_gridRotation * PI / 180.; - float angleDistort = u_gridAngleDistortion * lum; - - vec2 gridOffset = -vec2(u_gridOffsetX, u_gridOffsetY); - if (u_grid == 0.) { - uvGrid += gridOffset; - uvGrid = rotate(uvGrid, angleOffset + angleDistort); - gridLine = uvGrid.y; - } else if (u_grid == 1.) { - uvGrid += gridSize * gridOffset; - - uvGrid -= gridSize * gridOffset; - uvGrid = rotate(uvGrid, angleOffset); - uvGrid += gridSize * gridOffset; - - gridLine = length(uvGrid); - } else if (u_grid == 2.) { - uvGrid += gridOffset; - uvGrid = rotate(uvGrid, angleOffset + angleDistort); - gridLine = uvGrid.y + sin(.5 * uvGrid.x); - } else if (u_grid == 3.) { - uvGrid += gridOffset; - uvGrid = rotate(uvGrid, angleOffset + angleDistort); - noise = snoise(.2 * uvGrid); - gridLine = noise; - } - - float stripeMap = abs(fract(gridLine) - .5); - float aa = fwidth(gridLine); - - float w = mix(0., .5 * u_stripeWidth, lum); - float wLo = .0; - float wHi = .5 + aa; - if (u_allowOverflow == false) { - wHi -= 2. * aa; - } - if (u_thinLines == false) { - wLo += .5 * aa; - wHi -= .5 * aa; - } - w = clamp(w, wLo, wHi); - - vec2 grainMixerSize = mix(1000., 50., u_grainMixerSize) * vec2(1., 1. / u_imageAspectRatio); - vec2 grainOverlaySize = mix(2000., 200., u_grainOverlaySize) * vec2(1., 1. / u_imageAspectRatio); - vec2 grainMixerUV = getImageUV(uvNormalised, grainMixerSize); - vec2 grainOverlayUV = getImageUV(uvNormalised, grainOverlaySize); - float grain = valueNoise(grainMixerUV) + .3 * pow(u_grainMixer, 3.); - grain = smoothstep(.55, .9, grain); - grain *= .5 * pow(u_grainMixer, 3.); - stripeMap += .5 * grain; - - float lo = w; - float hi = w + aa; - float line = sst(lo, hi, stripeMap); - line = mix(1., line, frame); - line = clamp(line, 0., 1.); - - vec3 color = vec3(0.); - float opacity = 1.; - - if (u_originalColors == true) { - color = mix(origColor, u_colorBack.rgb, line); - } else { - color = mix(u_colorFront.rgb, u_colorBack.rgb, line); - } - - float grainOverlay = valueNoise(rotate(grainOverlayUV, 1.) + vec2(3.)); - grainOverlay = mix(grainOverlay, valueNoise(rotate(grainOverlayUV, 2.) + vec2(-1.)), .5); - grainOverlay = pow(grainOverlay, 1.3); - float grainOverlayV = grainOverlay * 2. - 1.; - vec3 grainOverlayColor = vec3(step(0., grainOverlayV)); - float grainOverlayStrength = u_grainOverlay * abs(grainOverlayV); - grainOverlayStrength = pow(grainOverlayStrength, .8); - color = mix(color, grainOverlayColor, .35 * grainOverlayStrength); - - opacity += .5 * grainOverlayStrength; - opacity = clamp(opacity, 0., 1.); - - fragColor = vec4(color, opacity); -} -`; - -export interface HalftoneLinesUniforms extends ShaderSizingUniforms { - u_colorBack: [number, number, number, number]; - u_colorFront: [number, number, number, number]; - u_image: HTMLImageElement | string | undefined; - u_grid: (typeof HalftoneLinesGrids)[HalftoneLinesGrid]; - u_gridOffsetX: number; - u_gridOffsetY: number; - u_stripeWidth: number; - u_smoothness: number; - u_size: number; - u_thinLines: boolean; - u_allowOverflow: boolean; - u_gridAngleDistortion: number; - u_gridNoiseDistortion: number; - u_gridRotation: number; - u_contrast: number; - u_originalColors: boolean; - u_inverted: boolean; - u_grainMixer: number; - u_grainMixerSize: number; - u_grainOverlay: number; - u_grainOverlaySize: number; -} - -export interface HalftoneLinesParams extends ShaderSizingParams, ShaderMotionParams { - colorBack?: string; - colorFront?: string; - image?: HTMLImageElement | string | undefined; - grid?: HalftoneLinesGrid; - gridOffsetX?: number; - gridOffsetY?: number; - stripeWidth?: number; - smoothness?: number; - size?: number; - thinLines?: boolean; - allowOverflow?: boolean; - gridAngleDistortion?: number; - gridNoiseDistortion?: number; - gridRotation?: number; - contrast?: number; - originalColors?: boolean; - inverted?: boolean; - grainMixer?: number; - grainMixerSize?: number; - grainOverlay?: number; - grainOverlaySize?: number; -} - -export const HalftoneLinesGrids = { - lines: 0, - radial: 1, - waves: 2, - noise: 3, -} as const; - -export type HalftoneLinesGrid = keyof typeof HalftoneLinesGrids; From fa38cfe1026cbc63760c86bf1ac1393172f89064 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 2 Dec 2025 19:47:38 +0200 Subject: [PATCH 62/84] Folds => Logo3d renaming --- .../(shaders)/{folds => logo-3d}/layout.tsx | 2 +- .../app/(shaders)/{folds => logo-3d}/page.tsx | 28 ++++++++--------- .../{folds-def.ts => logo-3d-def.ts} | 10 +++---- packages/shaders-react/src/index.ts | 6 ++-- .../src/shaders/{folds.tsx => logo-3d.tsx} | 30 +++++++++---------- packages/shaders/src/index.ts | 12 ++++---- .../src/shaders/{folds.ts => logo-3d.ts} | 18 +++++------ 7 files changed, 53 insertions(+), 53 deletions(-) rename docs/src/app/(shaders)/{folds => logo-3d}/layout.tsx (82%) rename docs/src/app/(shaders)/{folds => logo-3d}/page.tsx (84%) rename docs/src/shader-defs/{folds-def.ts => logo-3d-def.ts} (61%) rename packages/shaders-react/src/shaders/{folds.tsx => logo-3d.tsx} (85%) rename packages/shaders/src/shaders/{folds.ts => logo-3d.ts} (97%) diff --git a/docs/src/app/(shaders)/folds/layout.tsx b/docs/src/app/(shaders)/logo-3d/layout.tsx similarity index 82% rename from docs/src/app/(shaders)/folds/layout.tsx rename to docs/src/app/(shaders)/logo-3d/layout.tsx index 317e2e0d1..47d67285f 100644 --- a/docs/src/app/(shaders)/folds/layout.tsx +++ b/docs/src/app/(shaders)/logo-3d/layout.tsx @@ -1,7 +1,7 @@ import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'Folds Filter • Paper', + title: '3d Logo Filter • Paper', }; export default function Layout({ children }: { children: React.ReactNode }) { diff --git a/docs/src/app/(shaders)/folds/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx similarity index 84% rename from docs/src/app/(shaders)/folds/page.tsx rename to docs/src/app/(shaders)/logo-3d/page.tsx index 6060902bd..5bbad929a 100644 --- a/docs/src/app/(shaders)/folds/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -1,25 +1,25 @@ 'use client'; -import { Folds, foldsPresets } from '@paper-design/shaders-react'; +import { Logo3d, logo3dPresets } from '@paper-design/shaders-react'; import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { foldsMeta } from '@paper-design/shaders'; +import { logo3dMeta } from '@paper-design/shaders'; import { ShaderFit } from '@paper-design/shaders'; import { levaImageButton } from '@/helpers/leva-image-button'; import { useState, Suspense, useEffect, useCallback } from 'react'; import { ShaderDetails } from '@/components/shader-details'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { foldsDef } from '@/shader-defs/folds-def'; +import { logo3dDef } from '@/shader-defs/logo-3d-def'; import { toHsla } from '@/helpers/color-utils'; import { useColors } from '@/helpers/use-colors'; // Override just for the docs, we keep it transparent in the preset -// foldsPresets[0].params.colorBack = '#000000'; +// logo3dPresets[0].params.colorBack = '#000000'; -const { worldWidth, worldHeight, ...defaults } = foldsPresets[0].params; +const { worldWidth, worldHeight, ...defaults } = logo3dPresets[0].params; const imageFiles = [ 'contra.svg', @@ -57,7 +57,7 @@ const imageFiles = [ 'diamond.svg', ] as const; -const FoldsWithControls = () => { +const Logo3dWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); const [image, setImage] = useState('/images/logos/diamond.svg'); @@ -77,7 +77,7 @@ const FoldsWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, - maxColorCount: foldsMeta.maxColorCount, + maxColorCount: logo3dMeta.maxColorCount, }); const [params, setParams] = useControls(() => { @@ -111,7 +111,7 @@ const FoldsWithControls = () => { useControls(() => { const presets = Object.fromEntries( - foldsPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + logo3dPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ name, button(() => { const { colors, ...presetParams } = preset; @@ -128,19 +128,19 @@ const FoldsWithControls = () => { // Reset to defaults on mount, so that Leva doesn't show values from other // shaders when navigating (if two shaders have a color1 param for example) useResetLevaParams(params, setParams, defaults); - useUrlParams(params, setParams, foldsDef, setColors); - usePresetHighlight(foldsPresets, params); + useUrlParams(params, setParams, logo3dDef, setColors); + usePresetHighlight(logo3dPresets, params); cleanUpLevaParams(params); return ( <> - + - + @@ -148,4 +148,4 @@ const FoldsWithControls = () => { ); }; -export default FoldsWithControls; +export default Logo3dWithControls; diff --git a/docs/src/shader-defs/folds-def.ts b/docs/src/shader-defs/logo-3d-def.ts similarity index 61% rename from docs/src/shader-defs/folds-def.ts rename to docs/src/shader-defs/logo-3d-def.ts index 0931a0f9e..4235020ed 100644 --- a/docs/src/shader-defs/folds-def.ts +++ b/docs/src/shader-defs/logo-3d-def.ts @@ -1,12 +1,12 @@ -import { foldsPresets } from '@paper-design/shaders-react'; +import { logo3dPresets } from '@paper-design/shaders-react'; import type { ShaderDef } from './shader-def-types'; import { animatedCommonParams } from './common-param-def'; -const defaultParams = foldsPresets[0].params; +const defaultParams = logo3dPresets[0].params; -export const foldsDef: ShaderDef = { - name: 'Liquid Metal', - description: 'Futuristic liquid metal material applied to uploaded logo or abstract shape.', +export const logo3dDef: ShaderDef = { + name: '3d Logo', + description: 'TBD', params: [ { name: 'image', diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 5d7ea0c10..271275bb2 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -111,9 +111,9 @@ export { HalftoneDots, halftoneDotsPresets } from './shaders/halftone-dots.js'; export type { HalftoneDotsProps } from './shaders/halftone-dots.js'; export type { HalftoneDotsUniforms, HalftoneDotsParams } from '@paper-design/shaders'; -export { Folds, foldsPresets } from './shaders/folds.js'; -export type { FoldsProps } from './shaders/folds.js'; -export type { FoldsUniforms, FoldsParams } from '@paper-design/shaders'; +export { Logo3d, logo3dPresets } from './shaders/logo-3d.js'; +export type { Logo3dProps } from './shaders/logo-3d.js'; +export type { Logo3dUniforms, Logo3dParams } from '@paper-design/shaders'; export { isPaperShaderElement, getShaderColorFromString } from '@paper-design/shaders'; export type { PaperShaderElement, ShaderFit, ShaderSizingParams, ShaderSizingUniforms } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/folds.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx similarity index 85% rename from packages/shaders-react/src/shaders/folds.tsx rename to packages/shaders-react/src/shaders/logo-3d.tsx index 73bdd55c4..af3ea5ef5 100644 --- a/packages/shaders-react/src/shaders/folds.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -2,28 +2,28 @@ import { memo, useLayoutEffect, useState } from 'react'; import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; import { colorPropsAreEqual } from '../color-props-are-equal.js'; import { - foldsFragmentShader, + logo3dFragmentShader, ShaderFitOptions, defaultObjectSizing, - type FoldsUniforms, - type FoldsParams, - toProcessedFolds, + type Logo3dUniforms, + type Logo3dParams, + toProcessedLogo3d, type ImageShaderPreset, getShaderColorFromString, } from '@paper-design/shaders'; import { transparentPixel } from '../transparent-pixel.js'; import { suspend } from '../suspend.js'; -export interface FoldsProps extends ShaderComponentProps, FoldsParams { +export interface Logo3dProps extends ShaderComponentProps, Logo3dParams { /** * Suspends the component when the image is being processed. */ suspendWhenProcessingImage?: boolean; } -type FoldsPreset = ImageShaderPreset; +type Logo3dPreset = ImageShaderPreset; -export const defaultPreset: FoldsPreset = { +export const defaultPreset: Logo3dPreset = { name: 'Default', params: { ...defaultObjectSizing, @@ -46,9 +46,9 @@ export const defaultPreset: FoldsPreset = { }, }; -export const foldsPresets: FoldsPreset[] = [defaultPreset]; +export const logo3dPresets: Logo3dPreset[] = [defaultPreset]; -export const Folds: React.FC = memo(function FoldsImpl({ +export const Logo3d: React.FC = memo(function Logo3dImpl({ // Own props colorBack = defaultPreset.params.colorBack, colorInner = defaultPreset.params.colorInner, @@ -79,7 +79,7 @@ export const Folds: React.FC = memo(function FoldsImpl({ worldWidth = defaultPreset.params.worldWidth, worldHeight = defaultPreset.params.worldHeight, ...props -}: FoldsProps) { +}: Logo3dProps) { const imageUrl = typeof image === 'string' ? image : image.src; const [processedStateImage, setProcessedStateImage] = useState(transparentPixel); @@ -87,8 +87,8 @@ export const Folds: React.FC = memo(function FoldsImpl({ if (suspendWhenProcessingImage && typeof window !== 'undefined' && imageUrl) { processedImage = suspend( - (): Promise => toProcessedFolds(imageUrl).then((result) => URL.createObjectURL(result.pngBlob)), - [imageUrl, 'folds'] + (): Promise => toProcessedLogo3d(imageUrl).then((result) => URL.createObjectURL(result.pngBlob)), + [imageUrl, 'logo3d'] ); } else { processedImage = processedStateImage; @@ -108,7 +108,7 @@ export const Folds: React.FC = memo(function FoldsImpl({ let url: string; let current = true; - toProcessedFolds(imageUrl).then((result) => { + toProcessedLogo3d(imageUrl).then((result) => { if (current) { url = URL.createObjectURL(result.pngBlob); setProcessedStateImage(url); @@ -148,14 +148,14 @@ export const Folds: React.FC = memo(function FoldsImpl({ u_originY: originY, u_worldWidth: worldWidth, u_worldHeight: worldHeight, - } satisfies FoldsUniforms; + } satisfies Logo3dUniforms; return ( diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index 29ebaa818..b52799e62 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -221,12 +221,12 @@ export { } from './shaders/halftone-dots.js'; export { - foldsMeta, - foldsFragmentShader, - toProcessedFolds, - type FoldsParams, - type FoldsUniforms, -} from './shaders/folds.js'; + logo3dMeta, + logo3dFragmentShader, + toProcessedLogo3d, + type Logo3dParams, + type Logo3dUniforms, +} from './shaders/logo-3d.js'; // ----- Utils ----- // diff --git a/packages/shaders/src/shaders/folds.ts b/packages/shaders/src/shaders/logo-3d.ts similarity index 97% rename from packages/shaders/src/shaders/folds.ts rename to packages/shaders/src/shaders/logo-3d.ts index 0c2a51fea..951d57db0 100644 --- a/packages/shaders/src/shaders/folds.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -3,7 +3,7 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; import { declarePI, rotation2, simplexNoise, colorBandingFix } from '../shader-utils.js'; -export const foldsMeta = { +export const logo3dMeta = { maxColorCount: 10, } as const; @@ -18,7 +18,7 @@ export const foldsMeta = { */ // language=GLSL -export const foldsFragmentShader: string = `#version 300 es +export const logo3dFragmentShader: string = `#version 300 es precision mediump float; uniform sampler2D u_image; @@ -27,7 +27,7 @@ uniform float u_imageAspectRatio; uniform vec2 u_resolution; uniform float u_time; -uniform vec4 u_colors[${foldsMeta.maxColorCount}]; +uniform vec4 u_colors[${logo3dMeta.maxColorCount}]; uniform float u_colorsCount; uniform vec4 u_colorBack; uniform vec4 u_colorInner; @@ -216,17 +216,17 @@ void main() { imgAlpha *= frame; edge *= frame; - float f[${foldsMeta.maxColorCount}]; + float f[${logo3dMeta.maxColorCount}]; float yTime = fract(t); // yTime = pow(yTime, .8); float yTravel = mix(1.5, -1.5, yTime); float yShape = mix(.04 * edge, .1 * edge, yTime); - vec2 trajs[${foldsMeta.maxColorCount}]; + vec2 trajs[${logo3dMeta.maxColorCount}]; trajs[0] = vec2(-.4, -.5 + yTravel); float dist = 1.; - for (int i = 0; i < ${foldsMeta.maxColorCount}; i++) { + for (int i = 0; i < ${logo3dMeta.maxColorCount}; i++) { f[i] = getPoint(uv + trajs[i], yShape); } @@ -306,7 +306,7 @@ interface SparsePixelData { neighborIndices: Int32Array; } -export function toProcessedFolds(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { +export function toProcessedLogo3d(file: File | string): Promise<{ imageData: ImageData; pngBlob: Blob }> { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const isBlob = typeof file === 'string' && file.startsWith('blob:'); @@ -798,7 +798,7 @@ function solvePoissonSparse( return u; } -export interface FoldsUniforms extends ShaderSizingUniforms { +export interface Logo3dUniforms extends ShaderSizingUniforms { u_colorBack: [number, number, number, number]; u_colorInner: [number, number, number, number]; u_colors: vec4[]; @@ -816,7 +816,7 @@ export interface FoldsUniforms extends ShaderSizingUniforms { u_overlayBevel: number; } -export interface FoldsParams extends ShaderSizingParams, ShaderMotionParams { +export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { colors?: string[]; colorBack?: string; colorInner?: string; From d2fac16fbf1049464389dc54bb805ad7a795efd3 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 5 Dec 2025 21:20:20 +0400 Subject: [PATCH 63/84] clean-up + 3d oberlay shadow AA --- docs/src/app/(shaders)/logo-3d/page.tsx | 6 +- .../shaders-react/src/shaders/logo-3d.tsx | 10 +- packages/shaders/src/shaders/logo-3d.ts | 139 +++--------------- 3 files changed, 28 insertions(+), 127 deletions(-) diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index 5bbad929a..e9d118de6 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -59,7 +59,7 @@ const imageFiles = [ const Logo3dWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); - const [image, setImage] = useState('/images/logos/diamond.svg'); + const [image, setImage] = useState('/images/logos/paper-logo-only.svg'); useEffect(() => { if (imageIdx >= 0) { @@ -86,13 +86,13 @@ const Logo3dWithControls = () => { // colorInner: { value: toHsla(defaults.colorInner), order: 101 }, // stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, bevel: { value: defaults.bevel, min: 0, max: 1, order: 201 }, - overlayHeight: { value: defaults.overlayHeight, min: 0, max: 1, order: 201 }, + // overlayHeight: { value: defaults.overlayHeight, min: 0, max: 1, order: 201 }, // alphaMask: { value: defaults.alphaMask, order: 202 }, // size: { value: defaults.size, min: 3, max: 50, order: 203 }, // shift: { value: defaults.shift, min: -0.5, max: 0.5, order: 204 }, // noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, // noiseScale: { value: defaults.noiseScale, min: .1, max: 3, step: 0.01, order: 206 }, - // outerNoise: { value: defaults.outerNoise, min: 0, max: 1, order: 207 }, + test: { value: defaults.test, min: 0, max: 1, order: 207 }, overlayBevel: { value: defaults.overlayBevel, min: 0, max: 1, order: 208 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index af3ea5ef5..cbece55f3 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -28,7 +28,7 @@ export const defaultPreset: Logo3dPreset = { params: { ...defaultObjectSizing, scale: 0.8, - speed: 1, + speed: 0, frame: 0, colorBack: '#00000000', colorInner: '#000000', @@ -39,8 +39,8 @@ export const defaultPreset: Logo3dPreset = { size: 10, shift: 0.5, noise: 0.5, - outerNoise: 0.33, - bevel: 0.5, + test: 0.33, + bevel: 0.1, overlayHeight: 0.35, overlayBevel: 0.2, }, @@ -58,7 +58,7 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ image = '', shift = defaultPreset.params.shift, noise = defaultPreset.params.noise, - outerNoise = defaultPreset.params.outerNoise, + test = defaultPreset.params.test, bevel = defaultPreset.params.bevel, overlayHeight = defaultPreset.params.overlayHeight, stripeWidth = defaultPreset.params.stripeWidth, @@ -129,7 +129,7 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ u_image: processedImage, u_shift: shift, u_noise: noise, - u_outerNoise: outerNoise, + u_test: test, u_bevel: bevel, u_overlayHeight: overlayHeight, u_stripeWidth: stripeWidth, diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 951d57db0..927a81a18 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -39,7 +39,7 @@ uniform float u_size; uniform float u_overlayHeight; uniform float u_shift; uniform float u_noise; -uniform float u_outerNoise; +uniform float u_test; uniform float u_overlayBevel; ${sizingVariablesDeclaration} @@ -141,31 +141,14 @@ float sst(float edge0, float edge1, float x) { return smoothstep(edge0, edge1, x); } -float posMod(float x, float m) { - return x - m * floor(x / m); -} - -vec2 getPosition(int i, float t) { - float a = float(i) * .37; - float b = .6 + fract(float(i) / 3.) * .9; - float c = .8 + fract(float(i + 1) / 4.); - - float x = sin(t * b + a); - float y = cos(t * c + a * 1.5); - - return .5 + .5 * vec2(x, y); -} - - -float getHeight(vec2 uv, vec2 addon) { +float getHeight(vec2 uv, float addon) { float a = texture(u_image, uv).r; - a += addon[0]; a = pow(a, mix(1., 3., u_bevel)); - a = mix(a, 1., addon[1]); + a = mix(a, 1., addon); return a; } -vec3 computeNormal(vec2 uv, vec2 addon) { +vec3 computeNormal(vec2 uv, float addon) { vec2 uTexelSize = vec2(1. / 100.); float hC = getHeight(uv, addon); float hR = getHeight(uv + vec2(uTexelSize.x, 0.0), addon); @@ -200,7 +183,7 @@ vec3 rotateAroundZ(vec3 v, float overlayBevel) { void main() { - float t = .1 * u_time; + float t = .3 + .1 * u_time; vec2 uv = v_imageUV; vec2 dudx = dFdx(v_imageUV); @@ -208,7 +191,8 @@ void main() { vec4 img = textureGrad(u_image, uv, dudx, dudy); float edge = img.r; - edge = 1. - blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); + float edgeBorder = blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); + edge = 1. - edgeBorder; float imgAlpha = img.g; @@ -231,29 +215,26 @@ void main() { } float aa = fwidth(f[0]); - float overlayShadow = lst(.9, .93, f[0]); + float overlayShadow = sst(.9 - 2. * aa - .1 * edgeBorder, .91 + .08 * edgeBorder, f[0]); f[0] = sst(.9, .9 + 2. * aa, f[0]); overlayShadow = 1. - overlayShadow; overlayShadow *= f[0]; - overlayShadow = 10. * pow(overlayShadow, 1.); + overlayShadow = 3. * overlayShadow; vec3 uLightDir1 = normalize(vec3(.5, .5, .5)); vec3 uLightDir2 = normalize(vec3(-.5, -.5, .5)); + uLightDir1 = rotateAroundZ(uLightDir1, 3. * t); + uLightDir2 = rotateAroundZ(uLightDir2, 3. * t); -// uLightDir1 = rotateAroundZ(uLightDir1, 1. * t); -// uLightDir2 = rotateAroundZ(uLightDir2, -.2 * t); - - vec3 normal = computeNormal(uv, vec2(u_overlayHeight * f[0], u_overlayBevel * overlayShadow)); + vec3 normal = computeNormal(uv, overlayShadow); vec3 viewDir = vec3(0., 0., 1.); vec3 baseColor = vec3(1.); baseColor = mix(vec3(1.), vec3(.6), f[0]); - - vec3 specColor = vec3(.8); - + vec3 lightColor1 = u_colors[0].rgb; vec3 lightColor2 = u_colors[1].rgb; @@ -271,9 +252,9 @@ void main() { float NdotH2 = max(dot(normal, halfDir2), 0.); vec3 specular = - specColor * ( - pow(NdotH1, 400.) * lightColor1 + - pow(NdotH2, 400.) * lightColor2 + ( + pow(NdotH1, 500.) * lightColor1 + + pow(NdotH2, 500.) * lightColor2 ); float opacity = imgAlpha; @@ -282,8 +263,8 @@ void main() { color *= imgAlpha; vec3 bgColor = u_colorBack.rgb * u_colorBack.a; - color = color + bgColor * (1.0 - opacity); - opacity = opacity + u_colorBack.a * (1.0 - opacity); + color = color + bgColor * (1. - opacity); + opacity = opacity + u_colorBack.a * (1. - opacity); fragColor = vec4(color, opacity); } @@ -581,86 +562,6 @@ export function toProcessedLogo3d(file: File | string): Promise<{ imageData: Ima }); } -function blurRedChannel(imageData: ImageData, width: number, height: number, radius = 2) { - const src = imageData.data; - const pixelCount = width * height; - - const tmp = new Uint8ClampedArray(pixelCount); - const dst = new Uint8ClampedArray(pixelCount); - - // --- Horizontal blur --- - for (let y = 0; y < height; y++) { - let sum = 0; - let count = 0; - - // initial window centered at x = 0 - for (let dx = -radius; dx <= radius; dx++) { - const xClamped = Math.max(0, Math.min(width - 1, dx)); - const idx = (y * width + xClamped) * 4; - sum += src[idx]!; - count++; - } - - for (let x = 0; x < width; x++) { - const outIdx = y * width + x; - tmp[outIdx] = sum / count; - - const xRemove = x - radius; - if (xRemove >= 0) { - const idxRemove = (y * width + xRemove) * 4; - sum -= src[idxRemove]!; - count--; - } - - const xAdd = x + radius + 1; - if (xAdd < width) { - const idxAdd = (y * width + xAdd) * 4; - sum += src[idxAdd]!; - count++; - } - } - } - - // --- Vertical blur --- - for (let x = 0; x < width; x++) { - let sum = 0; - let count = 0; - - // initial window centered at y = 0 - for (let dy = -radius; dy <= radius; dy++) { - const yClamped = Math.max(0, Math.min(height - 1, dy)); - const idx = yClamped * width + x; - sum += tmp[idx]!; - count++; - } - - for (let y = 0; y < height; y++) { - const outIdx = y * width + x; - dst[outIdx] = sum / count; - - const yRemove = y - radius; - if (yRemove >= 0) { - const idxRemove = yRemove * width + x; - sum -= tmp[idxRemove]!; - count--; - } - - const yAdd = y + radius + 1; - if (yAdd < height) { - const idxAdd = yAdd * width + x; - sum += tmp[idxAdd]!; - count++; - } - } - } - - // --- Write blurred red back into ImageData --- - for (let i = 0; i < pixelCount; i++) { - const px = i * 4; - src[px] = dst[i]!; - } -} - function buildSparseData( shapeMask: Uint8Array, boundaryMask: Uint8Array, @@ -810,7 +711,7 @@ export interface Logo3dUniforms extends ShaderSizingUniforms { u_size: number; u_shift: number; u_noise: number; - u_outerNoise: number; + u_test: number; u_bevel: number; u_overlayHeight: number; u_overlayBevel: number; @@ -829,6 +730,6 @@ export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { overlayHeight?: number; shift?: number; noise?: number; - outerNoise?: number; + test?: number; overlayBevel?: number; } From 85d0ab5c4eb49a9a5187acf5901dd6ca4074c981 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 6 Dec 2025 17:01:29 +0400 Subject: [PATCH 64/84] saving progress --- docs/src/app/(shaders)/logo-3d/page.tsx | 11 +-- .../shaders-react/src/shaders/logo-3d.tsx | 35 ++------ packages/shaders/src/shaders/logo-3d.ts | 83 ++++++++----------- 3 files changed, 43 insertions(+), 86 deletions(-) diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index e9d118de6..120349c9f 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -83,17 +83,10 @@ const Logo3dWithControls = () => { const [params, setParams] = useControls(() => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - // colorInner: { value: toHsla(defaults.colorInner), order: 101 }, - // stripeWidth: { value: defaults.stripeWidth, min: 0, max: 1, order: 200 }, + colorUnderlay: { value: toHsla(defaults.colorUnderlay), order: 101 }, + colorOverlay: { value: toHsla(defaults.colorOverlay), order: 102 }, bevel: { value: defaults.bevel, min: 0, max: 1, order: 201 }, - // overlayHeight: { value: defaults.overlayHeight, min: 0, max: 1, order: 201 }, - // alphaMask: { value: defaults.alphaMask, order: 202 }, - // size: { value: defaults.size, min: 3, max: 50, order: 203 }, - // shift: { value: defaults.shift, min: -0.5, max: 0.5, order: 204 }, - // noise: { value: defaults.noise, min: 0, max: 1, order: 205 }, - // noiseScale: { value: defaults.noiseScale, min: .1, max: 3, step: 0.01, order: 206 }, test: { value: defaults.test, min: 0, max: 1, order: 207 }, - overlayBevel: { value: defaults.overlayBevel, min: 0, max: 1, order: 208 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index cbece55f3..4a1193f25 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -31,18 +31,11 @@ export const defaultPreset: Logo3dPreset = { speed: 0, frame: 0, colorBack: '#00000000', - colorInner: '#000000', - colors: ['#14f7ff', '#ff0a0a'], - stripeWidth: 1, - alphaMask: true, - noiseScale: 1, - size: 10, - shift: 0.5, - noise: 0.5, + colorUnderlay: '#d6ffda', + colorOverlay: '#c2c4ff', + colors: ['#75faff', '#ff6666'], test: 0.33, bevel: 0.1, - overlayHeight: 0.35, - overlayBevel: 0.2, }, }; @@ -51,21 +44,14 @@ export const logo3dPresets: Logo3dPreset[] = [defaultPreset]; export const Logo3d: React.FC = memo(function Logo3dImpl({ // Own props colorBack = defaultPreset.params.colorBack, - colorInner = defaultPreset.params.colorInner, + colorUnderlay = defaultPreset.params.colorUnderlay, + colorOverlay = defaultPreset.params.colorOverlay, colors = defaultPreset.params.colors, speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, image = '', - shift = defaultPreset.params.shift, - noise = defaultPreset.params.noise, test = defaultPreset.params.test, bevel = defaultPreset.params.bevel, - overlayHeight = defaultPreset.params.overlayHeight, - stripeWidth = defaultPreset.params.stripeWidth, - alphaMask = defaultPreset.params.alphaMask, - noiseScale = defaultPreset.params.noiseScale, - size = defaultPreset.params.size, - overlayBevel = defaultPreset.params.overlayBevel, suspendWhenProcessingImage = false, // Sizing props @@ -125,18 +111,11 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ u_colors: colors.map(getShaderColorFromString), u_colorsCount: colors.length, u_colorBack: getShaderColorFromString(colorBack), - u_colorInner: getShaderColorFromString(colorInner), + u_colorUnderlay: getShaderColorFromString(colorUnderlay), + u_colorOverlay: getShaderColorFromString(colorOverlay), u_image: processedImage, - u_shift: shift, - u_noise: noise, u_test: test, u_bevel: bevel, - u_overlayHeight: overlayHeight, - u_stripeWidth: stripeWidth, - u_alphaMask: alphaMask, - u_noiseScale: noiseScale, - u_size: size, - u_overlayBevel: overlayBevel, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 927a81a18..8f00abfa9 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -1,7 +1,7 @@ import type { vec4 } from '../types.js'; import type { ShaderMotionParams } from '../shader-mount.js'; import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, rotation2, simplexNoise, colorBandingFix } from '../shader-utils.js'; +import { declarePI, rotation2 } from '../shader-utils.js'; export const logo3dMeta = { maxColorCount: 10, @@ -31,16 +31,10 @@ uniform vec4 u_colors[${logo3dMeta.maxColorCount}]; uniform float u_colorsCount; uniform vec4 u_colorBack; uniform vec4 u_colorInner; +uniform vec4 u_colorUnderlay; +uniform vec4 u_colorOverlay; uniform float u_bevel; -uniform float u_stripeWidth; -uniform bool u_alphaMask; -uniform float u_noiseScale; -uniform float u_size; -uniform float u_overlayHeight; -uniform float u_shift; -uniform float u_noise; uniform float u_test; -uniform float u_overlayBevel; ${sizingVariablesDeclaration} @@ -182,7 +176,6 @@ vec3 rotateAroundZ(vec3 v, float overlayBevel) { void main() { - float t = .3 + .1 * u_time; vec2 uv = v_imageUV; @@ -221,8 +214,6 @@ void main() { overlayShadow *= f[0]; overlayShadow = 3. * overlayShadow; - - vec3 uLightDir1 = normalize(vec3(.5, .5, .5)); vec3 uLightDir2 = normalize(vec3(-.5, -.5, .5)); @@ -232,30 +223,38 @@ void main() { vec3 normal = computeNormal(uv, overlayShadow); vec3 viewDir = vec3(0., 0., 1.); - vec3 baseColor = vec3(1.); - baseColor = mix(vec3(1.), vec3(.6), f[0]); - - vec3 lightColor1 = u_colors[0].rgb; - vec3 lightColor2 = u_colors[1].rgb; + vec3 baseColor = u_colorUnderlay.rgb; + vec3 overlayColor = u_colorOverlay.rgb; + vec3 materialColor = mix(baseColor, overlayColor, f[0]); + + vec3 diffuse = vec3(0.); + vec3 specular = vec3(0.); + vec3 ambient = materialColor * (1. - u_test); - float NdotL1 = max(dot(normal, uLightDir1), 0.); - float NdotL2 = max(dot(normal, uLightDir2), 0.); + float lightCount = max(u_colorsCount, 1.); + float invLightCount = u_test / (lightCount * .3); - vec3 diffuse = baseColor * (NdotL1 * lightColor1 + NdotL2 * lightColor2); + for (int i = 0; i < ${ logo3dMeta.maxColorCount }; i++) { + if (i >= int(u_colorsCount)) break; - vec3 ambient = baseColor * 0.2; + float fi = (float(i) + .5) / float(u_colorsCount); + float angle = fi * TWO_PI + 3. * t; - vec3 halfDir1 = normalize(uLightDir1 + viewDir); - vec3 halfDir2 = normalize(uLightDir2 + viewDir); + vec3 L = normalize(vec3( + cos(angle), + sin(angle), + .5 + )); - float NdotH1 = max(dot(normal, halfDir1), 0.); - float NdotH2 = max(dot(normal, halfDir2), 0.); + vec3 lightColor = u_colors[i].rgb; - vec3 specular = - ( - pow(NdotH1, 500.) * lightColor1 + - pow(NdotH2, 500.) * lightColor2 - ); + float NdotL = max(dot(normal, L), 0.); + diffuse += materialColor * NdotL * lightColor * invLightCount; + + vec3 halfDir = normalize(L + viewDir); + float NdotH = max(dot(normal, halfDir), 0.0); + specular += pow(NdotH, 500.) * lightColor * invLightCount; + } float opacity = imgAlpha; vec3 color = ambient + diffuse + specular; @@ -701,35 +700,21 @@ function solvePoissonSparse( export interface Logo3dUniforms extends ShaderSizingUniforms { u_colorBack: [number, number, number, number]; - u_colorInner: [number, number, number, number]; + u_colorUnderlay: [number, number, number, number]; + u_colorOverlay: [number, number, number, number]; u_colors: vec4[]; u_colorsCount: number; u_image: HTMLImageElement | string | undefined; - u_stripeWidth: number; - u_alphaMask: boolean; - u_noiseScale: number; - u_size: number; - u_shift: number; - u_noise: number; u_test: number; u_bevel: number; - u_overlayHeight: number; - u_overlayBevel: number; } export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { colors?: string[]; colorBack?: string; - colorInner?: string; + colorUnderlay?: string; + colorOverlay?: string; image?: HTMLImageElement | string | undefined; - stripeWidth?: number; - alphaMask?: boolean; - noiseScale?: number; - size?: number; - bevel?: number; - overlayHeight?: number; - shift?: number; - noise?: number; test?: number; - overlayBevel?: number; + bevel?: number; } From 37124fe4c812d5b778e8131eb132881eeb24bb93 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 6 Dec 2025 18:33:32 +0400 Subject: [PATCH 65/84] saving progress --- .../shaders-react/src/shaders/logo-3d.tsx | 6 ++-- packages/shaders/src/shaders/logo-3d.ts | 29 +++++++------------ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 4a1193f25..19201bd0e 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -28,10 +28,10 @@ export const defaultPreset: Logo3dPreset = { params: { ...defaultObjectSizing, scale: 0.8, - speed: 0, + speed: 4, frame: 0, - colorBack: '#00000000', - colorUnderlay: '#d6ffda', + colorBack: '#000000', + colorUnderlay: '#0d0d0d', colorOverlay: '#c2c4ff', colors: ['#75faff', '#ff6666'], test: 0.33, diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 8f00abfa9..a680f1c04 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -193,25 +193,18 @@ void main() { imgAlpha *= frame; edge *= frame; - float f[${logo3dMeta.maxColorCount}]; - float yTime = fract(t); // yTime = pow(yTime, .8); float yTravel = mix(1.5, -1.5, yTime); float yShape = mix(.04 * edge, .1 * edge, yTime); - vec2 trajs[${logo3dMeta.maxColorCount}]; - trajs[0] = vec2(-.4, -.5 + yTravel); - float dist = 1.; - for (int i = 0; i < ${logo3dMeta.maxColorCount}; i++) { - f[i] = getPoint(uv + trajs[i], yShape); - } + float overlayShape = getPoint(uv + vec2(-.4, -.5 + yTravel), yShape); - float aa = fwidth(f[0]); - float overlayShadow = sst(.9 - 2. * aa - .1 * edgeBorder, .91 + .08 * edgeBorder, f[0]); - f[0] = sst(.9, .9 + 2. * aa, f[0]); + float aa = fwidth(overlayShape); + float overlayShadow = sst(.9 - 2. * aa - .1 * edgeBorder, .91 + .08 * edgeBorder, overlayShape); + overlayShape = sst(.9, .9 + 2. * aa, overlayShape); overlayShadow = 1. - overlayShadow; - overlayShadow *= f[0]; + overlayShadow *= overlayShape; overlayShadow = 3. * overlayShadow; vec3 uLightDir1 = normalize(vec3(.5, .5, .5)); @@ -225,14 +218,14 @@ void main() { vec3 baseColor = u_colorUnderlay.rgb; vec3 overlayColor = u_colorOverlay.rgb; - vec3 materialColor = mix(baseColor, overlayColor, f[0]); + vec3 materialColor = mix(baseColor, overlayColor, overlayShape); - vec3 diffuse = vec3(0.); + vec3 diffuse = vec3(0.); vec3 specular = vec3(0.); - vec3 ambient = materialColor * (1. - u_test); + vec3 ambient = materialColor * (1. - u_test); float lightCount = max(u_colorsCount, 1.); - float invLightCount = u_test / (lightCount * .3); + float invLightCount = 2. * u_test / (lightCount * .3); for (int i = 0; i < ${ logo3dMeta.maxColorCount }; i++) { if (i >= int(u_colorsCount)) break; @@ -243,7 +236,7 @@ void main() { vec3 L = normalize(vec3( cos(angle), sin(angle), - .5 + .4 )); vec3 lightColor = u_colors[i].rgb; @@ -253,7 +246,7 @@ void main() { vec3 halfDir = normalize(L + viewDir); float NdotH = max(dot(normal, halfDir), 0.0); - specular += pow(NdotH, 500.) * lightColor * invLightCount; + specular += pow(NdotH, 50.) * lightColor * invLightCount; } float opacity = imgAlpha; From 2235931f76813cff8db7defe7fb4b32f40fb8b38 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 6 Dec 2025 18:40:53 +0400 Subject: [PATCH 66/84] testing bumped version build --- bun.lock | 116 ++++++++++++++++++++++------------------------ docs/package.json | 8 ++-- 2 files changed, 60 insertions(+), 64 deletions(-) diff --git a/bun.lock b/bun.lock index d47262d7d..a1e5c4217 100644 --- a/bun.lock +++ b/bun.lock @@ -22,13 +22,13 @@ "@base-ui-components/react": "1.0.0-beta.2", "@paper-design/shaders": "workspace:*", "@paper-design/shaders-react": "workspace:*", - "@vercel/analytics": "^1.5.0", + "@vercel/analytics": "^1.6.1", "clsx": "^2.1.1", "culori": "4.0.2", "leva": "0.9.34", - "next": "15.2.3", - "react": "^19.2.0", - "react-dom": "^19.2.0", + "next": "16.0.7", + "react": "^19.2.1", + "react-dom": "^19.2.1", }, "devDependencies": { "@eslint/eslintrc": "3.2.0", @@ -47,11 +47,11 @@ }, "packages/shaders": { "name": "@paper-design/shaders", - "version": "0.0.60", + "version": "0.0.67", }, "packages/shaders-react": { "name": "@paper-design/shaders-react", - "version": "0.0.60", + "version": "0.0.67", "dependencies": { "@paper-design/shaders": "workspace:*", }, @@ -133,7 +133,7 @@ "@emnapi/core": ["@emnapi/core@1.4.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.1", "tslib": "^2.4.0" } }, "sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg=="], - "@emnapi/runtime": ["@emnapi/runtime@1.4.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw=="], @@ -219,43 +219,55 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], @@ -275,25 +287,25 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.8", "", { "dependencies": { "@emnapi/core": "^1.4.0", "@emnapi/runtime": "^1.4.0", "@tybys/wasm-util": "^0.9.0" } }, "sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg=="], - "@next/env": ["@next/env@15.2.3", "", {}, "sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw=="], + "@next/env": ["@next/env@16.0.7", "", {}, "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.1.2", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-sgfw3+WdaYOGPKCvM1L+UucBmRfh8V2Ygefp7ELON0+0vY7uohQwXXnVWg3rY7mXDKharQR3o7uedpfvnU2hlQ=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-50ibWdn2RuFFkOEUmo9NCcQbbV9ViQOrUfG48zHBCONciHjaUKtHcYFiCwBVuzD08fzvzkWuuZkd4AqbvKO7UQ=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-gHYS9tc+G2W0ZC8rBL+H6RdtXIyk40uLiaos0yj5US85FNhbFEndMA2nW3z47nzOWiSvXTZ5kBClc3rD0zJg0w=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.7", "", { "os": "win32", "cpu": "x64" }, "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -355,8 +367,6 @@ "@stitches/react": ["@stitches/react@1.2.8", "", { "peerDependencies": { "react": ">= 16.3.0" } }, "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA=="], - "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.12", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.12" } }, "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ=="], @@ -461,7 +471,7 @@ "@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="], - "@vercel/analytics": ["@vercel/analytics@1.5.0", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g=="], + "@vercel/analytics": ["@vercel/analytics@1.6.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg=="], "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], @@ -531,8 +541,6 @@ "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], - "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -561,14 +569,10 @@ "code-block-writer": ["code-block-writer@12.0.0", "", {}, "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w=="], - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - "colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="], "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], @@ -983,7 +987,7 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "next": ["next@15.2.3", "", { "dependencies": { "@next/env": "15.2.3", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.3", "@next/swc-darwin-x64": "15.2.3", "@next/swc-linux-arm64-gnu": "15.2.3", "@next/swc-linux-arm64-musl": "15.2.3", "@next/swc-linux-x64-gnu": "15.2.3", "@next/swc-linux-x64-musl": "15.2.3", "@next/swc-win32-arm64-msvc": "15.2.3", "@next/swc-win32-x64-msvc": "15.2.3", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w=="], + "next": ["next@16.0.7", "", { "dependencies": { "@next/env": "16.0.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.7", "@next/swc-darwin-x64": "16.0.7", "@next/swc-linux-arm64-gnu": "16.0.7", "@next/swc-linux-arm64-musl": "16.0.7", "@next/swc-linux-x64-gnu": "16.0.7", "@next/swc-linux-x64-musl": "16.0.7", "@next/swc-win32-arm64-msvc": "16.0.7", "@next/swc-win32-x64-msvc": "16.0.7", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A=="], "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], @@ -1077,11 +1081,11 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="], "react-colorful": ["react-colorful@5.6.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw=="], - "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], "react-dropzone": ["react-dropzone@12.1.0", "", { "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.5.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8" } }, "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog=="], @@ -1139,7 +1143,7 @@ "shadcn": ["shadcn@2.4.0-canary.9", "", { "dependencies": { "@antfu/ni": "^23.2.0", "@babel/core": "^7.22.1", "@babel/parser": "^7.22.6", "@babel/plugin-transform-typescript": "^7.22.5", "commander": "^10.0.0", "cosmiconfig": "^8.1.3", "deepmerge": "^4.3.1", "diff": "^5.1.0", "execa": "^7.0.0", "fast-glob": "^3.3.2", "fs-extra": "^11.1.0", "https-proxy-agent": "^6.2.0", "kleur": "^4.1.5", "node-fetch": "^3.3.0", "ora": "^6.1.2", "postcss": "^8.4.24", "prompts": "^2.4.2", "recast": "^0.23.2", "stringify-object": "^5.0.0", "ts-morph": "^18.0.0", "tsconfig-paths": "^4.2.0", "zod": "^3.20.2" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-1uOaCi/a2+MlPbXZSX8HrTEg5nMl1wQKGpkdz+6GzizRxvpZqmVpwuEb1oefOfsqkm/fZy68kHGXR4yr64VTwQ=="], - "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], "shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], @@ -1157,8 +1161,6 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], @@ -1181,8 +1183,6 @@ "stdin-discarder": ["stdin-discarder@0.1.0", "", { "dependencies": { "bl": "^5.0.0" } }, "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ=="], - "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], - "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1327,6 +1327,8 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + "@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.4.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw=="], + "@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], "@radix-ui/popper/@babel/runtime": ["@babel/runtime@7.27.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw=="], @@ -1443,10 +1445,6 @@ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "color/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "cosmiconfig/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], @@ -1513,9 +1511,9 @@ "set-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], - "sharp/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="], + "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "simple-swizzle/is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "split-string/extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="], @@ -1547,8 +1545,6 @@ "@radix-ui/react-tooltip/react-dom/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "color/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "eslint-plugin-import/tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], diff --git a/docs/package.json b/docs/package.json index 49c9e8b47..84dfead10 100644 --- a/docs/package.json +++ b/docs/package.json @@ -14,13 +14,13 @@ "@base-ui-components/react": "1.0.0-beta.2", "@paper-design/shaders": "workspace:*", "@paper-design/shaders-react": "workspace:*", - "@vercel/analytics": "^1.5.0", + "@vercel/analytics": "^1.6.1", "clsx": "^2.1.1", "culori": "4.0.2", "leva": "0.9.34", - "next": "15.2.3", - "react": "^19.2.0", - "react-dom": "^19.2.0" + "next": "16.0.7", + "react": "^19.2.1", + "react-dom": "^19.2.1" }, "devDependencies": { "@eslint/eslintrc": "3.2.0", From ada722fa49bf06c5959a4ebe28a80fc1887d5e29 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 6 Dec 2025 19:36:34 +0400 Subject: [PATCH 67/84] saving progress --- docs/src/app/(shaders)/logo-3d/page.tsx | 3 +- .../shaders-react/src/shaders/logo-3d.tsx | 72 ++++++++++++++++--- packages/shaders/src/shaders/logo-3d.ts | 34 ++++----- 3 files changed, 83 insertions(+), 26 deletions(-) diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index 120349c9f..663c6face 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -86,7 +86,8 @@ const Logo3dWithControls = () => { colorUnderlay: { value: toHsla(defaults.colorUnderlay), order: 101 }, colorOverlay: { value: toHsla(defaults.colorOverlay), order: 102 }, bevel: { value: defaults.bevel, min: 0, max: 1, order: 201 }, - test: { value: defaults.test, min: 0, max: 1, order: 207 }, + lightsPower: { value: defaults.lightsPower, min: 0, max: 1, order: 207 }, + lightsPos: { value: defaults.lightsPos, min: 0, max: 360, order: 208 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 19201bd0e..0c6c87e5a 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -28,18 +28,70 @@ export const defaultPreset: Logo3dPreset = { params: { ...defaultObjectSizing, scale: 0.8, - speed: 4, + speed: 1, + frame: 0, + colorBack: '#00000000', + colorUnderlay: '#83c2c9', + colorOverlay: '#c36fa1', + colors: ['#e0992e', '#35bbbb', '#56006b'], + lightsPower: 0.38, + bevel: 0.05, + lightsPos: 242, + }, +}; + +export const monoPreset: Logo3dPreset = { + name: 'Mono', + params: { + ...defaultObjectSizing, + scale: 0.8, + speed: 1, + frame: 0, + colorBack: '#00000000', + colorUnderlay: '#e3e3e3', + colorOverlay: '#c2c1c1', + colors: ['#c2c2c2', '#000000', '#000000'], + lightsPower: 0.15, + bevel: 0, + lightsPos: 82, + }, +}; + +export const metalPreset: Logo3dPreset = { + name: 'Metal', + params: { + ...defaultObjectSizing, + scale: 0.8, + speed: 1, frame: 0, colorBack: '#000000', - colorUnderlay: '#0d0d0d', - colorOverlay: '#c2c4ff', - colors: ['#75faff', '#ff6666'], - test: 0.33, - bevel: 0.1, + colorUnderlay: '#0a0a0a', + colorOverlay: '#0f0e16', + colors: ['#c7c7ff', '#ffbfa3', '#8ffff2'], + lightsPower: 1.0, + bevel: 0.7, + lightsPos: 66, + }, +}; +export const flatPreset: Logo3dPreset = { + name: 'Flat', + params: { + ...defaultObjectSizing, + scale: 0.8, + speed: 1, + frame: 0, + colorBack: '#00000000', + colorUnderlay: '#ffbf00', + colorOverlay: '#35a75e', + colors: ['#ffffff'], + lightsPower: 0, + bevel: 0, + lightsPos: 62, }, }; -export const logo3dPresets: Logo3dPreset[] = [defaultPreset]; + +export const logo3dPresets: Logo3dPreset[] = [defaultPreset, monoPreset, metalPreset, flatPreset]; export const Logo3d: React.FC = memo(function Logo3dImpl({ // Own props @@ -50,8 +102,9 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, image = '', - test = defaultPreset.params.test, + lightsPower = defaultPreset.params.lightsPower, bevel = defaultPreset.params.bevel, + lightsPos = defaultPreset.params.lightsPos, suspendWhenProcessingImage = false, // Sizing props @@ -114,7 +167,8 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ u_colorUnderlay: getShaderColorFromString(colorUnderlay), u_colorOverlay: getShaderColorFromString(colorOverlay), u_image: processedImage, - u_test: test, + u_lightsPower: lightsPower, + u_lightsPos: lightsPos, u_bevel: bevel, // Sizing uniforms diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index a680f1c04..ac4b06c73 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -27,21 +27,22 @@ uniform float u_imageAspectRatio; uniform vec2 u_resolution; uniform float u_time; -uniform vec4 u_colors[${logo3dMeta.maxColorCount}]; +uniform vec4 u_colors[${ logo3dMeta.maxColorCount }]; uniform float u_colorsCount; uniform vec4 u_colorBack; uniform vec4 u_colorInner; uniform vec4 u_colorUnderlay; uniform vec4 u_colorOverlay; uniform float u_bevel; -uniform float u_test; +uniform float u_lightsPower; +uniform float u_lightsPos; -${sizingVariablesDeclaration} +${ sizingVariablesDeclaration } out vec4 fragColor; -${declarePI} -${rotation2} +${ declarePI } +${ rotation2 } float getImgFrame(vec2 uv, float th) { float frame = 1.; @@ -165,9 +166,9 @@ float getPoint(vec2 dist, float p) { } mat2 rotZ(float a) { - float c = cos(a), s = sin(a); - return mat2(c, -s, - s, c); + float c = cos(a), s = sin(a); + return mat2(c, -s, + s, c); } vec3 rotateAroundZ(vec3 v, float overlayBevel) { mat2 r = rotZ(overlayBevel); @@ -176,7 +177,7 @@ vec3 rotateAroundZ(vec3 v, float overlayBevel) { void main() { - float t = .3 + .1 * u_time; + float t = .1 * u_time; vec2 uv = v_imageUV; vec2 dudx = dFdx(v_imageUV); @@ -192,9 +193,8 @@ void main() { float frame = getImgFrame(v_imageUV, 0.); imgAlpha *= frame; edge *= frame; - + float yTime = fract(t); -// yTime = pow(yTime, .8); float yTravel = mix(1.5, -1.5, yTime); float yShape = mix(.04 * edge, .1 * edge, yTime); @@ -222,16 +222,16 @@ void main() { vec3 diffuse = vec3(0.); vec3 specular = vec3(0.); - vec3 ambient = materialColor * (1. - u_test); + vec3 ambient = materialColor * (1. - u_lightsPower); float lightCount = max(u_colorsCount, 1.); - float invLightCount = 2. * u_test / (lightCount * .3); + float invLightCount = 2. * u_lightsPower / (lightCount * .3); for (int i = 0; i < ${ logo3dMeta.maxColorCount }; i++) { if (i >= int(u_colorsCount)) break; float fi = (float(i) + .5) / float(u_colorsCount); - float angle = fi * TWO_PI + 3. * t; + float angle = fi * TWO_PI + radians(u_lightsPos); vec3 L = normalize(vec3( cos(angle), @@ -698,7 +698,8 @@ export interface Logo3dUniforms extends ShaderSizingUniforms { u_colors: vec4[]; u_colorsCount: number; u_image: HTMLImageElement | string | undefined; - u_test: number; + u_lightsPower: number; + u_lightsPos: number; u_bevel: number; } @@ -708,6 +709,7 @@ export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { colorUnderlay?: string; colorOverlay?: string; image?: HTMLImageElement | string | undefined; - test?: number; + lightsPower?: number; + lightsPos?: number; bevel?: number; } From fa5b9118913528271481d4eaa7567d880defcf06 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 7 Dec 2025 17:25:02 +0400 Subject: [PATCH 68/84] test logo set sync with main --- docs/public/images/logos/apple.svg | 8 +- docs/public/images/logos/brave2.png | Bin 120364 -> 0 bytes docs/public/images/logos/diamond.svg | 8 +- docs/public/images/logos/kfc.svg | 1 - docs/public/images/logos/paper.svg | 7 +- docs/public/images/logos/perkins.svg | 126 ------------------------ docs/public/images/logos/pizza-hut.svg | 1 - docs/public/images/logos/remix.svg | 6 -- docs/public/images/logos/rogers.svg | 12 --- docs/src/app/(shaders)/logo-3d/page.tsx | 28 ++---- 10 files changed, 17 insertions(+), 180 deletions(-) delete mode 100644 docs/public/images/logos/brave2.png delete mode 100644 docs/public/images/logos/kfc.svg delete mode 100644 docs/public/images/logos/perkins.svg delete mode 100644 docs/public/images/logos/pizza-hut.svg delete mode 100644 docs/public/images/logos/remix.svg delete mode 100644 docs/public/images/logos/rogers.svg diff --git a/docs/public/images/logos/apple.svg b/docs/public/images/logos/apple.svg index 752866aa3..ea852732e 100644 --- a/docs/public/images/logos/apple.svg +++ b/docs/public/images/logos/apple.svg @@ -1,5 +1,3 @@ - - - - \ No newline at end of file + + + diff --git a/docs/public/images/logos/brave2.png b/docs/public/images/logos/brave2.png deleted file mode 100644 index 76b59baf8c1c6dee5623ffe6bd01b1063ff5c788..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120364 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 - - - \ No newline at end of file + + + diff --git a/docs/public/images/logos/kfc.svg b/docs/public/images/logos/kfc.svg deleted file mode 100644 index c6a61d7d5..000000000 --- a/docs/public/images/logos/kfc.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/public/images/logos/paper.svg b/docs/public/images/logos/paper.svg index 3931129da..892f3994b 100644 --- a/docs/public/images/logos/paper.svg +++ b/docs/public/images/logos/paper.svg @@ -1,7 +1,6 @@ - - + + fill="#000000"> + fill="#000000"> \ No newline at end of file diff --git a/docs/public/images/logos/perkins.svg b/docs/public/images/logos/perkins.svg deleted file mode 100644 index 101b0f8ca..000000000 --- a/docs/public/images/logos/perkins.svg +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - diff --git a/docs/public/images/logos/pizza-hut.svg b/docs/public/images/logos/pizza-hut.svg deleted file mode 100644 index bb2687bc9..000000000 --- a/docs/public/images/logos/pizza-hut.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/public/images/logos/remix.svg b/docs/public/images/logos/remix.svg deleted file mode 100644 index ebc2a35c0..000000000 --- a/docs/public/images/logos/remix.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/docs/public/images/logos/rogers.svg b/docs/public/images/logos/rogers.svg deleted file mode 100644 index c0971a685..000000000 --- a/docs/public/images/logos/rogers.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index 663c6face..202123a7a 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -23,6 +23,7 @@ const { worldWidth, worldHeight, ...defaults } = logo3dPresets[0].params; const imageFiles = [ 'contra.svg', + 'apple.svg', 'paradigm.svg', 'paper-logo-only.svg', 'brave.svg', @@ -31,29 +32,16 @@ const imageFiles = [ 'linear.svg', 'mercury.svg', 'mymind.svg', - // 'inbound.svg', 'resend.svg', 'shopify.svg', 'wealth-simple.svg', - 'vercel.svg', - - // 'chanel.svg', - // 'cibc.svg', - // 'cloudflare.svg', - // 'apple.svg', - // 'discord.svg', - // 'enterprise-rent.svg', - // 'kfc.svg', - // 'microsoft.svg', - // 'nasa.svg', - // 'netflix.svg', - // 'nike.svg', - // 'perkins.svg', - // 'pizza-hut.svg', - // 'remix.svg', - // 'rogers.svg', - // 'volkswagen.svg', - + 'chanel.svg', + 'cibc.svg', + 'cloudflare.svg', + 'discord.svg', + 'nasa.svg', + 'nike.svg', + 'volkswagen.svg', 'diamond.svg', ] as const; From e3c9819e9bafdbc7ca47e28ff60a4609a0255246 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 7 Dec 2025 17:40:47 +0400 Subject: [PATCH 69/84] overlayHeight + presets improvements --- docs/src/app/(shaders)/logo-3d/page.tsx | 1 + packages/shaders-react/src/shaders/logo-3d.tsx | 16 +++++++++++----- packages/shaders/src/shaders/logo-3d.ts | 8 ++++++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index 202123a7a..3f2f929e0 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -76,6 +76,7 @@ const Logo3dWithControls = () => { bevel: { value: defaults.bevel, min: 0, max: 1, order: 201 }, lightsPower: { value: defaults.lightsPower, min: 0, max: 1, order: 207 }, lightsPos: { value: defaults.lightsPos, min: 0, max: 360, order: 208 }, + overlayHeight: { value: defaults.overlayHeight, min: 0, max: 1, order: 209 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 0c6c87e5a..6819a5dfa 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -31,12 +31,13 @@ export const defaultPreset: Logo3dPreset = { speed: 1, frame: 0, colorBack: '#00000000', - colorUnderlay: '#83c2c9', - colorOverlay: '#c36fa1', - colors: ['#e0992e', '#35bbbb', '#56006b'], + colorUnderlay: '#d7b7ca', + colorOverlay: '#a3d2ad', + colors: ['#e0992e', '#35bbbb', '#05006b'], lightsPower: 0.38, bevel: 0.05, lightsPos: 242, + overlayHeight: 0.5, }, }; @@ -47,13 +48,14 @@ export const monoPreset: Logo3dPreset = { scale: 0.8, speed: 1, frame: 0, - colorBack: '#00000000', + colorBack: '#e8e8e8', colorUnderlay: '#e3e3e3', colorOverlay: '#c2c1c1', colors: ['#c2c2c2', '#000000', '#000000'], lightsPower: 0.15, bevel: 0, lightsPos: 82, + overlayHeight: 0.5, }, }; @@ -66,11 +68,12 @@ export const metalPreset: Logo3dPreset = { frame: 0, colorBack: '#000000', colorUnderlay: '#0a0a0a', - colorOverlay: '#0f0e16', + colorOverlay: '#07060a', colors: ['#c7c7ff', '#ffbfa3', '#8ffff2'], lightsPower: 1.0, bevel: 0.7, lightsPos: 66, + overlayHeight: 1, }, }; export const flatPreset: Logo3dPreset = { @@ -87,6 +90,7 @@ export const flatPreset: Logo3dPreset = { lightsPower: 0, bevel: 0, lightsPos: 62, + overlayHeight: 0.5, }, }; @@ -105,6 +109,7 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ lightsPower = defaultPreset.params.lightsPower, bevel = defaultPreset.params.bevel, lightsPos = defaultPreset.params.lightsPos, + overlayHeight = defaultPreset.params.overlayHeight, suspendWhenProcessingImage = false, // Sizing props @@ -170,6 +175,7 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ u_lightsPower: lightsPower, u_lightsPos: lightsPos, u_bevel: bevel, + u_overlayHeight: overlayHeight, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index ac4b06c73..4388d5d17 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -36,6 +36,7 @@ uniform vec4 u_colorOverlay; uniform float u_bevel; uniform float u_lightsPower; uniform float u_lightsPos; +uniform float u_overlayHeight; ${ sizingVariablesDeclaration } @@ -205,7 +206,7 @@ void main() { overlayShape = sst(.9, .9 + 2. * aa, overlayShape); overlayShadow = 1. - overlayShadow; overlayShadow *= overlayShape; - overlayShadow = 3. * overlayShadow; + overlayShadow = 8. * u_overlayHeight * overlayShadow; vec3 uLightDir1 = normalize(vec3(.5, .5, .5)); vec3 uLightDir2 = normalize(vec3(-.5, -.5, .5)); @@ -231,7 +232,8 @@ void main() { if (i >= int(u_colorsCount)) break; float fi = (float(i) + .5) / float(u_colorsCount); - float angle = fi * TWO_PI + radians(u_lightsPos); +// float angleDir = 2. * (mod(fi, 2.) - .5); + float angle = fi * TWO_PI + radians(u_lightsPos);// + angleDir * TWO_PI * t * .25; vec3 L = normalize(vec3( cos(angle), @@ -701,6 +703,7 @@ export interface Logo3dUniforms extends ShaderSizingUniforms { u_lightsPower: number; u_lightsPos: number; u_bevel: number; + u_overlayHeight: number; } export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { @@ -712,4 +715,5 @@ export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { lightsPower?: number; lightsPos?: number; bevel?: number; + overlayHeight?: number; } From d6e931d3c757767f693caf306f7aea01eb852b0f Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 9 Dec 2025 15:34:17 +0400 Subject: [PATCH 70/84] defaults tweak --- packages/shaders-react/src/shaders/logo-3d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 6819a5dfa..c5b127e41 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -32,7 +32,7 @@ export const defaultPreset: Logo3dPreset = { frame: 0, colorBack: '#00000000', colorUnderlay: '#d7b7ca', - colorOverlay: '#a3d2ad', + colorOverlay: '#6c8eb7', colors: ['#e0992e', '#35bbbb', '#05006b'], lightsPower: 0.38, bevel: 0.05, From 81f4e2d4f13ee7176dcd84518645548f27cfed0b Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 9 Dec 2025 15:41:04 +0400 Subject: [PATCH 71/84] max 6 lights --- packages/shaders/src/shaders/logo-3d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 4388d5d17..20c8e9b42 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -4,7 +4,7 @@ import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingU import { declarePI, rotation2 } from '../shader-utils.js'; export const logo3dMeta = { - maxColorCount: 10, + maxColorCount: 6, } as const; /** From 75d91b010188e3246e08d4daaee1b32dcca6977d Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 3 Feb 2026 17:14:28 +0100 Subject: [PATCH 72/84] post-merge --- packages/shaders/src/shaders/logo-3d.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 20c8e9b42..1f341b0c6 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -1,6 +1,6 @@ import type { vec4 } from '../types.js'; import type { ShaderMotionParams } from '../shader-mount.js'; -import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; +import type { ShaderSizingParams, ShaderSizingUniforms } from '../shader-sizing.js'; import { declarePI, rotation2 } from '../shader-utils.js'; export const logo3dMeta = { @@ -8,12 +8,6 @@ export const logo3dMeta = { } as const; /** - * - * Fluid motion imitation applied over user image - * (animated stripe pattern getting distorted with shape edges) - * - * Uniforms: - * - u_colorBack, u_colorFront (RGBA) * */ @@ -21,6 +15,9 @@ export const logo3dMeta = { export const logo3dFragmentShader: string = `#version 300 es precision mediump float; +in mediump vec2 v_imageUV; +out vec4 fragColor; + uniform sampler2D u_image; uniform float u_imageAspectRatio; @@ -38,10 +35,6 @@ uniform float u_lightsPower; uniform float u_lightsPos; uniform float u_overlayHeight; -${ sizingVariablesDeclaration } - -out vec4 fragColor; - ${ declarePI } ${ rotation2 } From ca1482876e4f66a3be7b7c7483a825aa128b0bf5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 4 Feb 2026 15:30:54 +0100 Subject: [PATCH 73/84] remove underlay, hardcode some params --- docs/src/app/(shaders)/logo-3d/page.tsx | 2 -- packages/shaders-react/src/shaders/logo-3d.tsx | 12 ------------ packages/shaders/src/shaders/logo-3d.ts | 18 +++++------------- 3 files changed, 5 insertions(+), 27 deletions(-) diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index 3f2f929e0..93bfc45d8 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -73,10 +73,8 @@ const Logo3dWithControls = () => { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorUnderlay: { value: toHsla(defaults.colorUnderlay), order: 101 }, colorOverlay: { value: toHsla(defaults.colorOverlay), order: 102 }, - bevel: { value: defaults.bevel, min: 0, max: 1, order: 201 }, lightsPower: { value: defaults.lightsPower, min: 0, max: 1, order: 207 }, lightsPos: { value: defaults.lightsPos, min: 0, max: 360, order: 208 }, - overlayHeight: { value: defaults.overlayHeight, min: 0, max: 1, order: 209 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index c5b127e41..02fdfe274 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -35,9 +35,7 @@ export const defaultPreset: Logo3dPreset = { colorOverlay: '#6c8eb7', colors: ['#e0992e', '#35bbbb', '#05006b'], lightsPower: 0.38, - bevel: 0.05, lightsPos: 242, - overlayHeight: 0.5, }, }; @@ -53,9 +51,7 @@ export const monoPreset: Logo3dPreset = { colorOverlay: '#c2c1c1', colors: ['#c2c2c2', '#000000', '#000000'], lightsPower: 0.15, - bevel: 0, lightsPos: 82, - overlayHeight: 0.5, }, }; @@ -71,9 +67,7 @@ export const metalPreset: Logo3dPreset = { colorOverlay: '#07060a', colors: ['#c7c7ff', '#ffbfa3', '#8ffff2'], lightsPower: 1.0, - bevel: 0.7, lightsPos: 66, - overlayHeight: 1, }, }; export const flatPreset: Logo3dPreset = { @@ -88,9 +82,7 @@ export const flatPreset: Logo3dPreset = { colorOverlay: '#35a75e', colors: ['#ffffff'], lightsPower: 0, - bevel: 0, lightsPos: 62, - overlayHeight: 0.5, }, }; @@ -107,9 +99,7 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ frame = defaultPreset.params.frame, image = '', lightsPower = defaultPreset.params.lightsPower, - bevel = defaultPreset.params.bevel, lightsPos = defaultPreset.params.lightsPos, - overlayHeight = defaultPreset.params.overlayHeight, suspendWhenProcessingImage = false, // Sizing props @@ -174,8 +164,6 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ u_image: processedImage, u_lightsPower: lightsPower, u_lightsPos: lightsPos, - u_bevel: bevel, - u_overlayHeight: overlayHeight, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 1f341b0c6..96b072163 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -30,10 +30,8 @@ uniform vec4 u_colorBack; uniform vec4 u_colorInner; uniform vec4 u_colorUnderlay; uniform vec4 u_colorOverlay; -uniform float u_bevel; uniform float u_lightsPower; uniform float u_lightsPos; -uniform float u_overlayHeight; ${ declarePI } ${ rotation2 } @@ -132,7 +130,7 @@ float sst(float edge0, float edge1, float x) { float getHeight(vec2 uv, float addon) { float a = texture(u_image, uv).r; - a = pow(a, mix(1., 3., u_bevel)); + a = pow(a, mix(1., 3., .1)); a = mix(a, 1., addon); return a; } @@ -199,7 +197,7 @@ void main() { overlayShape = sst(.9, .9 + 2. * aa, overlayShape); overlayShadow = 1. - overlayShadow; overlayShadow *= overlayShape; - overlayShadow = 8. * u_overlayHeight * overlayShadow; + overlayShadow = 8. * 1. * overlayShadow; vec3 uLightDir1 = normalize(vec3(.5, .5, .5)); vec3 uLightDir2 = normalize(vec3(-.5, -.5, .5)); @@ -210,9 +208,7 @@ void main() { vec3 normal = computeNormal(uv, overlayShadow); vec3 viewDir = vec3(0., 0., 1.); - vec3 baseColor = u_colorUnderlay.rgb; - vec3 overlayColor = u_colorOverlay.rgb; - vec3 materialColor = mix(baseColor, overlayColor, overlayShape); + vec3 materialColor = u_colorOverlay.rgb; vec3 diffuse = vec3(0.); vec3 specular = vec3(0.); @@ -244,10 +240,10 @@ void main() { specular += pow(NdotH, 50.) * lightColor * invLightCount; } - float opacity = imgAlpha; + float opacity = imgAlpha * overlayShape; vec3 color = ambient + diffuse + specular; color = clamp(color, vec3(0.), vec3(1.)); - color *= imgAlpha; + color *= opacity; vec3 bgColor = u_colorBack.rgb * u_colorBack.a; color = color + bgColor * (1. - opacity); @@ -695,8 +691,6 @@ export interface Logo3dUniforms extends ShaderSizingUniforms { u_image: HTMLImageElement | string | undefined; u_lightsPower: number; u_lightsPos: number; - u_bevel: number; - u_overlayHeight: number; } export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { @@ -707,6 +701,4 @@ export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { image?: HTMLImageElement | string | undefined; lightsPower?: number; lightsPos?: number; - bevel?: number; - overlayHeight?: number; } From be8ce3e2e322c6302b4077e54ea9b981b0104160 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 4 Feb 2026 15:34:43 +0100 Subject: [PATCH 74/84] optimisation --- packages/shaders/src/shaders/logo-3d.ts | 104 ++++++------------------ 1 file changed, 26 insertions(+), 78 deletions(-) diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 96b072163..1824800a7 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -45,81 +45,6 @@ float getImgFrame(vec2 uv, float th) { return frame; } -float blurEdge5x5(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) { - vec2 texel = 1.0 / vec2(textureSize(tex, 0)); - vec2 r = max(radius, 0.0) * texel; - - // 1D Gaussian coefficients (Pascal row) - const float a = 1.0;// |offset| = 2 - const float b = 4.0;// |offset| = 1 - const float c = 6.0;// |offset| = 0 - - float norm = 256.0;// (a+b+c+b+a)^2 = 16^2 - float sum = 0.0; - - // y = -2 - { - float wy = a; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, -2.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, -2.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, -2.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, -2.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, -2.0*r.y)).r; - sum += wy * row; - } - - // y = -1 - { - float wy = b; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, -1.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, -1.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, -1.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, -1.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, -1.0*r.y)).r; - sum += wy * row; - } - - // y = 0 (use provided centerSample to avoid an extra fetch) - { - float wy = c; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 0.0)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 0.0)).r + - c * centerSample + - b * texture(tex, uv + vec2(1.0*r.x, 0.0)).r + - a * texture(tex, uv + vec2(2.0*r.x, 0.0)).r; - sum += wy * row; - } - - // y = +1 - { - float wy = b; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 1.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 1.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, 1.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, 1.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, 1.0*r.y)).r; - sum += wy * row; - } - - // y = +2 - { - float wy = a; - float row = - a * texture(tex, uv + vec2(-2.0*r.x, 2.0*r.y)).r + - b * texture(tex, uv + vec2(-1.0*r.x, 2.0*r.y)).r + - c * texture(tex, uv + vec2(0.0, 2.0*r.y)).r + - b * texture(tex, uv + vec2(1.0*r.x, 2.0*r.y)).r + - a * texture(tex, uv + vec2(2.0*r.x, 2.0*r.y)).r; - sum += wy * row; - } - - return sum / norm; -} - float lst(float edge0, float edge1, float x) { return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); } @@ -176,9 +101,8 @@ void main() { vec2 dudy = dFdy(v_imageUV); vec4 img = textureGrad(u_image, uv, dudx, dudy); - float edge = img.r; - float edgeBorder = blurEdge5x5(u_image, uv, dudx, dudy, 10., edge); - edge = 1. - edgeBorder; + float edgeBorder = img.r; + float edge = 1. - edgeBorder; float imgAlpha = img.g; @@ -668,6 +592,30 @@ function solvePoissonSparse( } } + // Jacobi smoothing passes to eliminate Red-Black SOR checkerboard artifacts + const temp = new Float32Array(width * height); + for (let pass = 0; pass < 2; pass++) { + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + const eastIdx = neighborIndices[i * 4 + 0]!; + const westIdx = neighborIndices[i * 4 + 1]!; + const northIdx = neighborIndices[i * 4 + 2]!; + const southIdx = neighborIndices[i * 4 + 3]!; + + let sumN = 0; + if (eastIdx >= 0) sumN += u[eastIdx]!; + if (westIdx >= 0) sumN += u[westIdx]!; + if (northIdx >= 0) sumN += u[northIdx]!; + if (southIdx >= 0) sumN += u[southIdx]!; + + temp[idx] = (C + sumN) / 4; + } + for (let i = 0; i < pixelCount; i++) { + const idx = interiorPixels[i]!; + u[idx] = temp[idx]!; + } + } + if (POISSON_CONFIG_OPTIMIZED.measurePerformance) { const elapsed = performance.now() - startTime; From 0c0c144c33369d6d82536e046253750779e6a426 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 4 Feb 2026 15:37:27 +0100 Subject: [PATCH 75/84] underlay color removed --- docs/src/app/(shaders)/logo-3d/page.tsx | 1 - packages/shaders-react/src/shaders/logo-3d.tsx | 6 ------ packages/shaders/src/shaders/logo-3d.ts | 3 --- 3 files changed, 10 deletions(-) diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index 93bfc45d8..09664d2c9 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -71,7 +71,6 @@ const Logo3dWithControls = () => { const [params, setParams] = useControls(() => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorUnderlay: { value: toHsla(defaults.colorUnderlay), order: 101 }, colorOverlay: { value: toHsla(defaults.colorOverlay), order: 102 }, lightsPower: { value: defaults.lightsPower, min: 0, max: 1, order: 207 }, lightsPos: { value: defaults.lightsPos, min: 0, max: 360, order: 208 }, diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 02fdfe274..86bf8c4db 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -31,7 +31,6 @@ export const defaultPreset: Logo3dPreset = { speed: 1, frame: 0, colorBack: '#00000000', - colorUnderlay: '#d7b7ca', colorOverlay: '#6c8eb7', colors: ['#e0992e', '#35bbbb', '#05006b'], lightsPower: 0.38, @@ -47,7 +46,6 @@ export const monoPreset: Logo3dPreset = { speed: 1, frame: 0, colorBack: '#e8e8e8', - colorUnderlay: '#e3e3e3', colorOverlay: '#c2c1c1', colors: ['#c2c2c2', '#000000', '#000000'], lightsPower: 0.15, @@ -63,7 +61,6 @@ export const metalPreset: Logo3dPreset = { speed: 1, frame: 0, colorBack: '#000000', - colorUnderlay: '#0a0a0a', colorOverlay: '#07060a', colors: ['#c7c7ff', '#ffbfa3', '#8ffff2'], lightsPower: 1.0, @@ -78,7 +75,6 @@ export const flatPreset: Logo3dPreset = { speed: 1, frame: 0, colorBack: '#00000000', - colorUnderlay: '#ffbf00', colorOverlay: '#35a75e', colors: ['#ffffff'], lightsPower: 0, @@ -92,7 +88,6 @@ export const logo3dPresets: Logo3dPreset[] = [defaultPreset, monoPreset, metalPr export const Logo3d: React.FC = memo(function Logo3dImpl({ // Own props colorBack = defaultPreset.params.colorBack, - colorUnderlay = defaultPreset.params.colorUnderlay, colorOverlay = defaultPreset.params.colorOverlay, colors = defaultPreset.params.colors, speed = defaultPreset.params.speed, @@ -159,7 +154,6 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ u_colors: colors.map(getShaderColorFromString), u_colorsCount: colors.length, u_colorBack: getShaderColorFromString(colorBack), - u_colorUnderlay: getShaderColorFromString(colorUnderlay), u_colorOverlay: getShaderColorFromString(colorOverlay), u_image: processedImage, u_lightsPower: lightsPower, diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 1824800a7..42f67b84a 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -28,7 +28,6 @@ uniform vec4 u_colors[${ logo3dMeta.maxColorCount }]; uniform float u_colorsCount; uniform vec4 u_colorBack; uniform vec4 u_colorInner; -uniform vec4 u_colorUnderlay; uniform vec4 u_colorOverlay; uniform float u_lightsPower; uniform float u_lightsPos; @@ -632,7 +631,6 @@ function solvePoissonSparse( export interface Logo3dUniforms extends ShaderSizingUniforms { u_colorBack: [number, number, number, number]; - u_colorUnderlay: [number, number, number, number]; u_colorOverlay: [number, number, number, number]; u_colors: vec4[]; u_colorsCount: number; @@ -644,7 +642,6 @@ export interface Logo3dUniforms extends ShaderSizingUniforms { export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { colors?: string[]; colorBack?: string; - colorUnderlay?: string; colorOverlay?: string; image?: HTMLImageElement | string | undefined; lightsPower?: number; From 6df37de7fca754ecdd5b5f4e49c8616054dda311 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 4 Feb 2026 15:53:27 +0100 Subject: [PATCH 76/84] renamings, presets --- docs/src/app/(shaders)/logo-3d/page.tsx | 2 +- .../shaders-react/src/shaders/logo-3d.tsx | 55 ++----------------- packages/shaders/src/shaders/logo-3d.ts | 8 +-- 3 files changed, 10 insertions(+), 55 deletions(-) diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index 09664d2c9..ae7769ad6 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -71,7 +71,7 @@ const Logo3dWithControls = () => { const [params, setParams] = useControls(() => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorOverlay: { value: toHsla(defaults.colorOverlay), order: 102 }, + colorBase: { value: toHsla(defaults.colorBase), order: 102 }, lightsPower: { value: defaults.lightsPower, min: 0, max: 1, order: 207 }, lightsPos: { value: defaults.lightsPos, min: 0, max: 360, order: 208 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 86bf8c4db..2dab2ad5d 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -30,65 +30,20 @@ export const defaultPreset: Logo3dPreset = { scale: 0.8, speed: 1, frame: 0, - colorBack: '#00000000', - colorOverlay: '#6c8eb7', + colorBack: '#000000', + colorBase: '#b46bb8', colors: ['#e0992e', '#35bbbb', '#05006b'], lightsPower: 0.38, lightsPos: 242, }, }; -export const monoPreset: Logo3dPreset = { - name: 'Mono', - params: { - ...defaultObjectSizing, - scale: 0.8, - speed: 1, - frame: 0, - colorBack: '#e8e8e8', - colorOverlay: '#c2c1c1', - colors: ['#c2c2c2', '#000000', '#000000'], - lightsPower: 0.15, - lightsPos: 82, - }, -}; - -export const metalPreset: Logo3dPreset = { - name: 'Metal', - params: { - ...defaultObjectSizing, - scale: 0.8, - speed: 1, - frame: 0, - colorBack: '#000000', - colorOverlay: '#07060a', - colors: ['#c7c7ff', '#ffbfa3', '#8ffff2'], - lightsPower: 1.0, - lightsPos: 66, - }, -}; -export const flatPreset: Logo3dPreset = { - name: 'Flat', - params: { - ...defaultObjectSizing, - scale: 0.8, - speed: 1, - frame: 0, - colorBack: '#00000000', - colorOverlay: '#35a75e', - colors: ['#ffffff'], - lightsPower: 0, - lightsPos: 62, - }, -}; - - -export const logo3dPresets: Logo3dPreset[] = [defaultPreset, monoPreset, metalPreset, flatPreset]; +export const logo3dPresets: Logo3dPreset[] = [defaultPreset]; export const Logo3d: React.FC = memo(function Logo3dImpl({ // Own props colorBack = defaultPreset.params.colorBack, - colorOverlay = defaultPreset.params.colorOverlay, + colorBase = defaultPreset.params.colorBase, colors = defaultPreset.params.colors, speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, @@ -154,7 +109,7 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ u_colors: colors.map(getShaderColorFromString), u_colorsCount: colors.length, u_colorBack: getShaderColorFromString(colorBack), - u_colorOverlay: getShaderColorFromString(colorOverlay), + u_colorBase: getShaderColorFromString(colorBase), u_image: processedImage, u_lightsPower: lightsPower, u_lightsPos: lightsPos, diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 42f67b84a..696c4095c 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -28,7 +28,7 @@ uniform vec4 u_colors[${ logo3dMeta.maxColorCount }]; uniform float u_colorsCount; uniform vec4 u_colorBack; uniform vec4 u_colorInner; -uniform vec4 u_colorOverlay; +uniform vec4 u_colorBase; uniform float u_lightsPower; uniform float u_lightsPos; @@ -131,7 +131,7 @@ void main() { vec3 normal = computeNormal(uv, overlayShadow); vec3 viewDir = vec3(0., 0., 1.); - vec3 materialColor = u_colorOverlay.rgb; + vec3 materialColor = u_colorBase.rgb; vec3 diffuse = vec3(0.); vec3 specular = vec3(0.); @@ -631,7 +631,7 @@ function solvePoissonSparse( export interface Logo3dUniforms extends ShaderSizingUniforms { u_colorBack: [number, number, number, number]; - u_colorOverlay: [number, number, number, number]; + u_colorBase: [number, number, number, number]; u_colors: vec4[]; u_colorsCount: number; u_image: HTMLImageElement | string | undefined; @@ -642,7 +642,7 @@ export interface Logo3dUniforms extends ShaderSizingUniforms { export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { colors?: string[]; colorBack?: string; - colorOverlay?: string; + colorBase?: string; image?: HTMLImageElement | string | undefined; lightsPower?: number; lightsPos?: number; From 1d5925b36418fd98b2b9d18094b333b3365ac284 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 4 Feb 2026 16:29:46 +0100 Subject: [PATCH 77/84] back to glsl blur --- .../shaders-react/src/shaders/logo-3d.tsx | 2 +- packages/shaders/src/shaders/logo-3d.ts | 62 ++++++++++++++++++- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 2dab2ad5d..89b214b3c 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -27,7 +27,7 @@ export const defaultPreset: Logo3dPreset = { name: 'Default', params: { ...defaultObjectSizing, - scale: 0.8, + scale: 2.1, speed: 1, frame: 0, colorBack: '#000000', diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 696c4095c..3d45989a4 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -44,6 +44,60 @@ float getImgFrame(vec2 uv, float th) { return frame; } +float blurEdge5x5(sampler2D tex, vec2 uv, float radius, float centerSample) { + vec2 texel = 1.0 / vec2(textureSize(tex, 0)); + vec2 r = max(radius, 0.0) * texel; + + const float a = 1.0; + const float b = 4.0; + const float c = 6.0; + + float norm = 256.0; + float sum = 0.0; + + // y = -2 + sum += a * ( + a * texture(tex, uv + vec2(-2.0*r.x, -2.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, -2.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, -2.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, -2.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, -2.0*r.y)).r); + + // y = -1 + sum += b * ( + a * texture(tex, uv + vec2(-2.0*r.x, -1.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, -1.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, -1.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, -1.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, -1.0*r.y)).r); + + // y = 0 + sum += c * ( + a * texture(tex, uv + vec2(-2.0*r.x, 0.0)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 0.0)).r + + c * centerSample + + b * texture(tex, uv + vec2(1.0*r.x, 0.0)).r + + a * texture(tex, uv + vec2(2.0*r.x, 0.0)).r); + + // y = +1 + sum += b * ( + a * texture(tex, uv + vec2(-2.0*r.x, 1.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 1.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, 1.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, 1.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, 1.0*r.y)).r); + + // y = +2 + sum += a * ( + a * texture(tex, uv + vec2(-2.0*r.x, 2.0*r.y)).r + + b * texture(tex, uv + vec2(-1.0*r.x, 2.0*r.y)).r + + c * texture(tex, uv + vec2(0.0, 2.0*r.y)).r + + b * texture(tex, uv + vec2(1.0*r.x, 2.0*r.y)).r + + a * texture(tex, uv + vec2(2.0*r.x, 2.0*r.y)).r); + + return sum / norm; +} + float lst(float edge0, float edge1, float x) { return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); } @@ -100,8 +154,9 @@ void main() { vec2 dudy = dFdy(v_imageUV); vec4 img = textureGrad(u_image, uv, dudx, dudy); - float edgeBorder = img.r; - float edge = 1. - edgeBorder; + float edge = img.r; + float edgeBorder = blurEdge5x5(u_image, uv, 10., edge); + edge = 1. - edgeBorder; float imgAlpha = img.g; @@ -109,7 +164,8 @@ void main() { imgAlpha *= frame; edge *= frame; - float yTime = fract(t); +// float yTime = fract(t); + float yTime = .3; float yTravel = mix(1.5, -1.5, yTime); float yShape = mix(.04 * edge, .1 * edge, yTime); From 15fb2be4f4c59a458c41644729a2597409a7c1d5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 4 Feb 2026 16:37:01 +0100 Subject: [PATCH 78/84] animation back; start reworking the overlay shape --- packages/shaders-react/src/shaders/logo-3d.tsx | 2 +- packages/shaders/src/shaders/logo-3d.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 89b214b3c..619ea03fc 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -27,7 +27,7 @@ export const defaultPreset: Logo3dPreset = { name: 'Default', params: { ...defaultObjectSizing, - scale: 2.1, + scale: 0.7, speed: 1, frame: 0, colorBack: '#000000', diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 3d45989a4..9c6f6f4f2 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -128,8 +128,9 @@ vec3 computeNormal(vec2 uv, float addon) { } -float getPoint(vec2 dist, float p) { - float v = pow(1. - clamp(0., 1., length(dist)), 1.); +float getOverlayBase(float y, float center, float p) { + float d = abs(y - center); + float v = 1. - clamp(d, 0., 1.); v = smoothstep(0., 1., v); v = pow(v, p); return v; @@ -164,12 +165,11 @@ void main() { imgAlpha *= frame; edge *= frame; -// float yTime = fract(t); - float yTime = .3; + float yTime = fract(t); float yTravel = mix(1.5, -1.5, yTime); float yShape = mix(.04 * edge, .1 * edge, yTime); - float overlayShape = getPoint(uv + vec2(-.4, -.5 + yTravel), yShape); + float overlayShape = getOverlayBase(uv.y, .5 - yTravel, yShape); float aa = fwidth(overlayShape); float overlayShadow = sst(.9 - 2. * aa - .1 * edgeBorder, .91 + .08 * edgeBorder, overlayShape); From 471a91d424ab05b0b634e87874b6f9893ecbf15c Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 4 Feb 2026 16:42:54 +0100 Subject: [PATCH 79/84] configurable shadow --- packages/shaders/src/shaders/logo-3d.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 9c6f6f4f2..a2c3003a8 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -171,9 +171,10 @@ void main() { float overlayShape = getOverlayBase(uv.y, .5 - yTravel, yShape); + const float shadowStart = .8; float aa = fwidth(overlayShape); - float overlayShadow = sst(.9 - 2. * aa - .1 * edgeBorder, .91 + .08 * edgeBorder, overlayShape); - overlayShape = sst(.9, .9 + 2. * aa, overlayShape); + float overlayShadow = sst(shadowStart - 2. * aa - .1 * edgeBorder, shadowStart + .11 + .08 * edgeBorder, overlayShape); + overlayShape = sst(shadowStart, shadowStart + 2. * aa, overlayShape); overlayShadow = 1. - overlayShadow; overlayShadow *= overlayShape; overlayShadow = 8. * 1. * overlayShadow; @@ -229,6 +230,8 @@ void main() { opacity = opacity + u_colorBack.a * (1. - opacity); fragColor = vec4(color, opacity); + +// fragColor = vec4(vec3(overlayShape, overlayShadow, 0.), 1.); } `; From 1efb569276d868f66b2400f19ac1277d164c2c79 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 4 Feb 2026 17:44:54 +0100 Subject: [PATCH 80/84] remove overlay --- docs/src/app/(shaders)/logo-3d/page.tsx | 1 + .../shaders-react/src/shaders/logo-3d.tsx | 5 +- packages/shaders/src/shaders/logo-3d.ts | 49 +++++-------------- 3 files changed, 16 insertions(+), 39 deletions(-) diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index ae7769ad6..15534b765 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -74,6 +74,7 @@ const Logo3dWithControls = () => { colorBase: { value: toHsla(defaults.colorBase), order: 102 }, lightsPower: { value: defaults.lightsPower, min: 0, max: 1, order: 207 }, lightsPos: { value: defaults.lightsPos, min: 0, max: 360, order: 208 }, + testTime: { value: defaults.testTime, min: 0, max: 1, order: 209 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 619ea03fc..016106861 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -33,8 +33,9 @@ export const defaultPreset: Logo3dPreset = { colorBack: '#000000', colorBase: '#b46bb8', colors: ['#e0992e', '#35bbbb', '#05006b'], - lightsPower: 0.38, + lightsPower: 1, lightsPos: 242, + testTime: .15, }, }; @@ -50,6 +51,7 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ image = '', lightsPower = defaultPreset.params.lightsPower, lightsPos = defaultPreset.params.lightsPos, + testTime = defaultPreset.params.testTime, suspendWhenProcessingImage = false, // Sizing props @@ -113,6 +115,7 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ u_image: processedImage, u_lightsPower: lightsPower, u_lightsPos: lightsPos, + u_testTime: testTime, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index a2c3003a8..5dea0ba98 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -31,6 +31,7 @@ uniform vec4 u_colorInner; uniform vec4 u_colorBase; uniform float u_lightsPower; uniform float u_lightsPos; +uniform float u_testTime; ${ declarePI } ${ rotation2 } @@ -108,8 +109,6 @@ float sst(float edge0, float edge1, float x) { float getHeight(vec2 uv, float addon) { float a = texture(u_image, uv).r; - a = pow(a, mix(1., 3., .1)); - a = mix(a, 1., addon); return a; } @@ -121,25 +120,15 @@ vec3 computeNormal(vec2 uv, float addon) { float hU = getHeight(uv + vec2(0.0, uTexelSize.y), addon); float hD = getHeight(uv - vec2(0.0, uTexelSize.y), addon); - float dX = (hR - hL) * 1.; - float dY = (hU - hD) * 1.; + float dX = (hR - hL); + float dY = (hU - hD); return normalize(vec3(-dX, -dY, 1.0)); } - -float getOverlayBase(float y, float center, float p) { - float d = abs(y - center); - float v = 1. - clamp(d, 0., 1.); - v = smoothstep(0., 1., v); - v = pow(v, p); - return v; -} - mat2 rotZ(float a) { float c = cos(a), s = sin(a); - return mat2(c, -s, - s, c); + return mat2(c, -s, s, c); } vec3 rotateAroundZ(vec3 v, float overlayBevel) { mat2 r = rotZ(overlayBevel); @@ -165,27 +154,13 @@ void main() { imgAlpha *= frame; edge *= frame; - float yTime = fract(t); - float yTravel = mix(1.5, -1.5, yTime); - float yShape = mix(.04 * edge, .1 * edge, yTime); - - float overlayShape = getOverlayBase(uv.y, .5 - yTravel, yShape); - - const float shadowStart = .8; - float aa = fwidth(overlayShape); - float overlayShadow = sst(shadowStart - 2. * aa - .1 * edgeBorder, shadowStart + .11 + .08 * edgeBorder, overlayShape); - overlayShape = sst(shadowStart, shadowStart + 2. * aa, overlayShape); - overlayShadow = 1. - overlayShadow; - overlayShadow *= overlayShape; - overlayShadow = 8. * 1. * overlayShadow; - vec3 uLightDir1 = normalize(vec3(.5, .5, .5)); vec3 uLightDir2 = normalize(vec3(-.5, -.5, .5)); uLightDir1 = rotateAroundZ(uLightDir1, 3. * t); uLightDir2 = rotateAroundZ(uLightDir2, 3. * t); - vec3 normal = computeNormal(uv, overlayShadow); + vec3 normal = computeNormal(uv, 0.); vec3 viewDir = vec3(0., 0., 1.); vec3 materialColor = u_colorBase.rgb; @@ -204,11 +179,7 @@ void main() { // float angleDir = 2. * (mod(fi, 2.) - .5); float angle = fi * TWO_PI + radians(u_lightsPos);// + angleDir * TWO_PI * t * .25; - vec3 L = normalize(vec3( - cos(angle), - sin(angle), - .4 - )); + vec3 L = normalize(vec3(cos(angle), sin(angle), .4)); vec3 lightColor = u_colors[i].rgb; @@ -220,7 +191,7 @@ void main() { specular += pow(NdotH, 50.) * lightColor * invLightCount; } - float opacity = imgAlpha * overlayShape; + float opacity = imgAlpha; vec3 color = ambient + diffuse + specular; color = clamp(color, vec3(0.), vec3(1.)); color *= opacity; @@ -230,8 +201,8 @@ void main() { opacity = opacity + u_colorBack.a * (1. - opacity); fragColor = vec4(color, opacity); - -// fragColor = vec4(vec3(overlayShape, overlayShadow, 0.), 1.); +// fragColor = vec4(normal, 1.); +// fragColor = vec4(vec3(texture(u_image, uv).r * imgAlpha * overlayShape + overlayShape * overlayShadow), 1.); } `; @@ -696,6 +667,7 @@ export interface Logo3dUniforms extends ShaderSizingUniforms { u_image: HTMLImageElement | string | undefined; u_lightsPower: number; u_lightsPos: number; + u_testTime: number; } export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { @@ -705,4 +677,5 @@ export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { image?: HTMLImageElement | string | undefined; lightsPower?: number; lightsPos?: number; + testTime?: number; } From 868bc157d9def5780c8f65c1589d51d5b6fcdf7f Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 7 Feb 2026 16:02:37 +0100 Subject: [PATCH 81/84] clean-up, no overlay --- docs/src/app/(shaders)/logo-3d/page.tsx | 1 - .../shaders-react/src/shaders/logo-3d.tsx | 3 -- packages/shaders/src/shaders/logo-3d.ts | 32 +++++++++---------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index 15534b765..ae7769ad6 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -74,7 +74,6 @@ const Logo3dWithControls = () => { colorBase: { value: toHsla(defaults.colorBase), order: 102 }, lightsPower: { value: defaults.lightsPower, min: 0, max: 1, order: 207 }, lightsPos: { value: defaults.lightsPos, min: 0, max: 360, order: 208 }, - testTime: { value: defaults.testTime, min: 0, max: 1, order: 209 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 016106861..9bfb4c429 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -35,7 +35,6 @@ export const defaultPreset: Logo3dPreset = { colors: ['#e0992e', '#35bbbb', '#05006b'], lightsPower: 1, lightsPos: 242, - testTime: .15, }, }; @@ -51,7 +50,6 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ image = '', lightsPower = defaultPreset.params.lightsPower, lightsPos = defaultPreset.params.lightsPos, - testTime = defaultPreset.params.testTime, suspendWhenProcessingImage = false, // Sizing props @@ -115,7 +113,6 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ u_image: processedImage, u_lightsPower: lightsPower, u_lightsPos: lightsPos, - u_testTime: testTime, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 5dea0ba98..c1365f4a3 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -31,7 +31,6 @@ uniform vec4 u_colorInner; uniform vec4 u_colorBase; uniform float u_lightsPower; uniform float u_lightsPos; -uniform float u_testTime; ${ declarePI } ${ rotation2 } @@ -107,18 +106,19 @@ float sst(float edge0, float edge1, float x) { return smoothstep(edge0, edge1, x); } -float getHeight(vec2 uv, float addon) { +float getHeight(vec2 uv) { float a = texture(u_image, uv).r; + a += 8. * sst(.0, .75, length(uv - .5)); return a; } -vec3 computeNormal(vec2 uv, float addon) { +vec3 computeNormal(vec2 uv) { vec2 uTexelSize = vec2(1. / 100.); - float hC = getHeight(uv, addon); - float hR = getHeight(uv + vec2(uTexelSize.x, 0.0), addon); - float hL = getHeight(uv - vec2(uTexelSize.x, 0.0), addon); - float hU = getHeight(uv + vec2(0.0, uTexelSize.y), addon); - float hD = getHeight(uv - vec2(0.0, uTexelSize.y), addon); + float hC = getHeight(uv); + float hR = getHeight(uv + vec2(uTexelSize.x, 0.0)); + float hL = getHeight(uv - vec2(uTexelSize.x, 0.0)); + float hU = getHeight(uv + vec2(0.0, uTexelSize.y)); + float hD = getHeight(uv - vec2(0.0, uTexelSize.y)); float dX = (hR - hL); float dY = (hU - hD); @@ -145,10 +145,11 @@ void main() { vec4 img = textureGrad(u_image, uv, dudx, dudy); float edge = img.r; + float imgAlpha = img.g; + float edgeBorder = blurEdge5x5(u_image, uv, 10., edge); edge = 1. - edgeBorder; - - float imgAlpha = img.g; + edge *= imgAlpha; float frame = getImgFrame(v_imageUV, 0.); imgAlpha *= frame; @@ -160,7 +161,7 @@ void main() { uLightDir1 = rotateAroundZ(uLightDir1, 3. * t); uLightDir2 = rotateAroundZ(uLightDir2, 3. * t); - vec3 normal = computeNormal(uv, 0.); + vec3 normal = computeNormal(uv); vec3 viewDir = vec3(0., 0., 1.); vec3 materialColor = u_colorBase.rgb; @@ -176,8 +177,7 @@ void main() { if (i >= int(u_colorsCount)) break; float fi = (float(i) + .5) / float(u_colorsCount); -// float angleDir = 2. * (mod(fi, 2.) - .5); - float angle = fi * TWO_PI + radians(u_lightsPos);// + angleDir * TWO_PI * t * .25; + float angle = fi * TWO_PI + radians(u_lightsPos); vec3 L = normalize(vec3(cos(angle), sin(angle), .4)); @@ -199,10 +199,12 @@ void main() { vec3 bgColor = u_colorBack.rgb * u_colorBack.a; color = color + bgColor * (1. - opacity); opacity = opacity + u_colorBack.a * (1. - opacity); + +// color.r = overlay; fragColor = vec4(color, opacity); // fragColor = vec4(normal, 1.); -// fragColor = vec4(vec3(texture(u_image, uv).r * imgAlpha * overlayShape + overlayShape * overlayShadow), 1.); +// fragColor = vec4(vec3(getHeight(uv)), 1.); } `; @@ -667,7 +669,6 @@ export interface Logo3dUniforms extends ShaderSizingUniforms { u_image: HTMLImageElement | string | undefined; u_lightsPower: number; u_lightsPos: number; - u_testTime: number; } export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { @@ -677,5 +678,4 @@ export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { image?: HTMLImageElement | string | undefined; lightsPower?: number; lightsPos?: number; - testTime?: number; } From b2c143e6d4bd5aaf69929d10d2c671fa29ad3c1c Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 7 Feb 2026 17:22:46 +0100 Subject: [PATCH 82/84] simplify --- docs/src/app/(shaders)/logo-3d/page.tsx | 4 +- .../shaders-react/src/shaders/logo-3d.tsx | 12 ++--- packages/shaders/src/shaders/logo-3d.ts | 47 ++++++++++++------- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index ae7769ad6..f784b2c94 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -72,8 +72,8 @@ const Logo3dWithControls = () => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorBase: { value: toHsla(defaults.colorBase), order: 102 }, - lightsPower: { value: defaults.lightsPower, min: 0, max: 1, order: 207 }, - lightsPos: { value: defaults.lightsPos, min: 0, max: 360, order: 208 }, + lightsSpread: { value: defaults.lightsSpread, min: 0, max: 1, order: 207 }, + lightsShininess: { value: defaults.lightsShininess, min: 0, max: 1, order: 208 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 9bfb4c429..13b3e9062 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -33,8 +33,8 @@ export const defaultPreset: Logo3dPreset = { colorBack: '#000000', colorBase: '#b46bb8', colors: ['#e0992e', '#35bbbb', '#05006b'], - lightsPower: 1, - lightsPos: 242, + lightsSpread: 0.5, + lightsShininess: 0.5, }, }; @@ -48,8 +48,8 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, image = '', - lightsPower = defaultPreset.params.lightsPower, - lightsPos = defaultPreset.params.lightsPos, + lightsSpread = defaultPreset.params.lightsSpread, + lightsShininess = defaultPreset.params.lightsShininess, suspendWhenProcessingImage = false, // Sizing props @@ -111,8 +111,8 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ u_colorBack: getShaderColorFromString(colorBack), u_colorBase: getShaderColorFromString(colorBase), u_image: processedImage, - u_lightsPower: lightsPower, - u_lightsPos: lightsPos, + u_lightsSpread: lightsSpread, + u_lightsShininess: lightsShininess, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index c1365f4a3..5813abc55 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -27,13 +27,13 @@ uniform float u_time; uniform vec4 u_colors[${ logo3dMeta.maxColorCount }]; uniform float u_colorsCount; uniform vec4 u_colorBack; -uniform vec4 u_colorInner; uniform vec4 u_colorBase; -uniform float u_lightsPower; -uniform float u_lightsPos; +uniform float u_lightsSpread; +uniform float u_lightsShininess; ${ declarePI } ${ rotation2 } +#define HALF_PI 1.5707963267949 float getImgFrame(vec2 uv, float th) { float frame = 1.; @@ -168,31 +168,46 @@ void main() { vec3 diffuse = vec3(0.); vec3 specular = vec3(0.); - vec3 ambient = materialColor * (1. - u_lightsPower); float lightCount = max(u_colorsCount, 1.); - float invLightCount = 2. * u_lightsPower / (lightCount * .3); + float intensity = 1. + 1. / lightCount; + float shininess = mix(75., 900., u_lightsShininess); for (int i = 0; i < ${ logo3dMeta.maxColorCount }; i++) { if (i >= int(u_colorsCount)) break; float fi = (float(i) + .5) / float(u_colorsCount); - float angle = fi * TWO_PI + radians(u_lightsPos); + float idx = float(i); - vec3 L = normalize(vec3(cos(angle), sin(angle), .4)); + // Chaotic angle: continuous rotation + layered sine waves + float angleNoise = sin(u_time * 1.1 + idx * 2.3) * 0.8 + + sin(u_time * 0.7 + idx * 3.7) * 0.5 + + sin(u_time * 1.9 + idx * 1.3) * 0.3; + float angle = fi * TWO_PI + u_time * (0.5 + idx * 0.1) + angleNoise * u_lightsSpread; + + // Chaotic height: continuous + layered oscillation + float baseElev = 0.33 * HALF_PI; + float elevNoise = sin(u_time * 0.8 + idx * 4.1 + u_time * 0.3) * 0.4 + + sin(u_time * 1.3 + idx * 2.7) * 0.3 + + cos(u_time * 0.6 + idx * 5.3) * 0.2; + float elevation = baseElev + elevNoise * u_lightsSpread; + elevation = clamp(elevation, 0., HALF_PI); + + float cosElev = cos(elevation); + vec3 L = normalize(vec3(cos(angle) * cosElev, sin(angle) * cosElev, sin(elevation))); vec3 lightColor = u_colors[i].rgb; float NdotL = max(dot(normal, L), 0.); - diffuse += materialColor * NdotL * lightColor * invLightCount; + diffuse += materialColor * NdotL * lightColor * intensity; vec3 halfDir = normalize(L + viewDir); float NdotH = max(dot(normal, halfDir), 0.0); - specular += pow(NdotH, 50.) * lightColor * invLightCount; + specular += pow(NdotH, shininess) * lightColor * intensity; } float opacity = imgAlpha; - vec3 color = ambient + diffuse + specular; + vec3 color = diffuse + specular; color = clamp(color, vec3(0.), vec3(1.)); color *= opacity; @@ -200,11 +215,7 @@ void main() { color = color + bgColor * (1. - opacity); opacity = opacity + u_colorBack.a * (1. - opacity); -// color.r = overlay; - fragColor = vec4(color, opacity); -// fragColor = vec4(normal, 1.); -// fragColor = vec4(vec3(getHeight(uv)), 1.); } `; @@ -667,8 +678,8 @@ export interface Logo3dUniforms extends ShaderSizingUniforms { u_colors: vec4[]; u_colorsCount: number; u_image: HTMLImageElement | string | undefined; - u_lightsPower: number; - u_lightsPos: number; + u_lightsSpread: number; + u_lightsShininess: number; } export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { @@ -676,6 +687,6 @@ export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { colorBack?: string; colorBase?: string; image?: HTMLImageElement | string | undefined; - lightsPower?: number; - lightsPos?: number; + lightsSpread?: number; + lightsShininess?: number; } From cbc906892a49a346b908db47901db425489c9cc6 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 8 Feb 2026 13:16:13 +0100 Subject: [PATCH 83/84] iterating --- .../shaders-react/src/shaders/logo-3d.tsx | 6 ++--- packages/shaders/src/shaders/logo-3d.ts | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index 13b3e9062..da65443de 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -31,10 +31,10 @@ export const defaultPreset: Logo3dPreset = { speed: 1, frame: 0, colorBack: '#000000', - colorBase: '#b46bb8', - colors: ['#e0992e', '#35bbbb', '#05006b'], + colorBase: '#871cca', + colors: ['#f2ff00', '#00ff96'], lightsSpread: 0.5, - lightsShininess: 0.5, + lightsShininess: 1, }, }; diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index 5813abc55..dabe3fb45 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -170,7 +170,7 @@ void main() { vec3 specular = vec3(0.); float lightCount = max(u_colorsCount, 1.); - float intensity = 1. + 1. / lightCount; + float intensity = 2. / (.5 + .5 * lightCount); float shininess = mix(75., 900., u_lightsShininess); for (int i = 0; i < ${ logo3dMeta.maxColorCount }; i++) { @@ -178,19 +178,22 @@ void main() { float fi = (float(i) + .5) / float(u_colorsCount); float idx = float(i); + float sectorSize = TWO_PI / lightCount; - // Chaotic angle: continuous rotation + layered sine waves - float angleNoise = sin(u_time * 1.1 + idx * 2.3) * 0.8 - + sin(u_time * 0.7 + idx * 3.7) * 0.5 - + sin(u_time * 1.9 + idx * 1.3) * 0.3; - float angle = fi * TWO_PI + u_time * (0.5 + idx * 0.1) + angleNoise * u_lightsSpread; + // Each light orbits within its sector, never overlapping + float sectorCenter = fi * TWO_PI + u_time * 0.5; + float angleNoise = sin(u_time * 1.1 + idx * 2.3) * 0.3 + + sin(u_time * 0.7 + idx * 3.7) * 0.2 + + sin(u_time * 1.9 + idx * 1.3) * 0.15; + float angle = sectorCenter + angleNoise * sectorSize * u_lightsSpread; - // Chaotic height: continuous + layered oscillation + // Staggered heights so lights are distributed in 3D float baseElev = 0.33 * HALF_PI; - float elevNoise = sin(u_time * 0.8 + idx * 4.1 + u_time * 0.3) * 0.4 - + sin(u_time * 1.3 + idx * 2.7) * 0.3 - + cos(u_time * 0.6 + idx * 5.3) * 0.2; - float elevation = baseElev + elevNoise * u_lightsSpread; + float heightOffset = sin(fi * PI) * 0.3; + float elevNoise = sin(u_time * 0.8 + idx * 4.1) * 0.25 + + sin(u_time * 1.3 + idx * 2.7) * 0.2 + + cos(u_time * 0.6 + idx * 5.3) * 0.15; + float elevation = baseElev + heightOffset + elevNoise * u_lightsSpread; elevation = clamp(elevation, 0., HALF_PI); float cosElev = cos(elevation); From bc398c2b9d4f36094da76ae93e322b77a349b49b Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 9 Feb 2026 18:44:16 +0100 Subject: [PATCH 84/84] iterating --- docs/src/app/(shaders)/logo-3d/page.tsx | 14 ++++---- .../shaders-react/src/shaders/logo-3d.tsx | 18 ++++++---- packages/shaders/src/shaders/logo-3d.ts | 35 +++++++++++++------ 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/docs/src/app/(shaders)/logo-3d/page.tsx b/docs/src/app/(shaders)/logo-3d/page.tsx index f784b2c94..c57fb2269 100644 --- a/docs/src/app/(shaders)/logo-3d/page.tsx +++ b/docs/src/app/(shaders)/logo-3d/page.tsx @@ -24,11 +24,11 @@ const { worldWidth, worldHeight, ...defaults } = logo3dPresets[0].params; const imageFiles = [ 'contra.svg', 'apple.svg', - 'paradigm.svg', + // 'paradigm.svg', 'paper-logo-only.svg', - 'brave.svg', - 'capy.svg', - 'infinite.svg', + // 'brave.svg', + // 'capy.svg', + // 'infinite.svg', 'linear.svg', 'mercury.svg', 'mymind.svg', @@ -47,7 +47,7 @@ const imageFiles = [ const Logo3dWithControls = () => { const [imageIdx, setImageIdx] = useState(-1); - const [image, setImage] = useState('/images/logos/paper-logo-only.svg'); + const [image, setImage] = useState('/images/logos/diamond.svg'); useEffect(() => { if (imageIdx >= 0) { @@ -73,7 +73,9 @@ const Logo3dWithControls = () => { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorBase: { value: toHsla(defaults.colorBase), order: 102 }, lightsSpread: { value: defaults.lightsSpread, min: 0, max: 1, order: 207 }, - lightsShininess: { value: defaults.lightsShininess, min: 0, max: 1, order: 208 }, + lightsDiffuse: { value: defaults.lightsDiffuse, min: 0, max: 1, order: 209 }, + lightsSpecular: { value: defaults.lightsSpecular, min: 0, max: 1, order: 210 }, + lightsShadow: { value: defaults.lightsShadow, min: 0, max: 1, order: 211 }, speed: { value: defaults.speed, min: 0, max: 2, order: 300 }, scale: { value: defaults.scale, min: 0.2, max: 10, order: 301 }, // rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, diff --git a/packages/shaders-react/src/shaders/logo-3d.tsx b/packages/shaders-react/src/shaders/logo-3d.tsx index da65443de..5765fa2ad 100644 --- a/packages/shaders-react/src/shaders/logo-3d.tsx +++ b/packages/shaders-react/src/shaders/logo-3d.tsx @@ -31,10 +31,12 @@ export const defaultPreset: Logo3dPreset = { speed: 1, frame: 0, colorBack: '#000000', - colorBase: '#871cca', - colors: ['#f2ff00', '#00ff96'], - lightsSpread: 0.5, - lightsShininess: 1, + colorBase: '#ea05ff', + colors: ['#ffea61', '#00ffee'], + lightsSpread: 0.4, + lightsDiffuse: 1, + lightsSpecular: 1, + lightsShadow: 0.2, }, }; @@ -49,7 +51,9 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ frame = defaultPreset.params.frame, image = '', lightsSpread = defaultPreset.params.lightsSpread, - lightsShininess = defaultPreset.params.lightsShininess, + lightsDiffuse = defaultPreset.params.lightsDiffuse, + lightsSpecular = defaultPreset.params.lightsSpecular, + lightsShadow = defaultPreset.params.lightsShadow, suspendWhenProcessingImage = false, // Sizing props @@ -112,7 +116,9 @@ export const Logo3d: React.FC = memo(function Logo3dImpl({ u_colorBase: getShaderColorFromString(colorBase), u_image: processedImage, u_lightsSpread: lightsSpread, - u_lightsShininess: lightsShininess, + u_lightsDiffuse: lightsDiffuse, + u_lightsSpecular: lightsSpecular, + u_lightsShadow: lightsShadow, // Sizing uniforms u_fit: ShaderFitOptions[fit], diff --git a/packages/shaders/src/shaders/logo-3d.ts b/packages/shaders/src/shaders/logo-3d.ts index dabe3fb45..5442ce8c2 100644 --- a/packages/shaders/src/shaders/logo-3d.ts +++ b/packages/shaders/src/shaders/logo-3d.ts @@ -4,7 +4,7 @@ import type { ShaderSizingParams, ShaderSizingUniforms } from '../shader-sizing. import { declarePI, rotation2 } from '../shader-utils.js'; export const logo3dMeta = { - maxColorCount: 6, + maxColorCount: 4, } as const; /** @@ -29,7 +29,9 @@ uniform float u_colorsCount; uniform vec4 u_colorBack; uniform vec4 u_colorBase; uniform float u_lightsSpread; -uniform float u_lightsShininess; +uniform float u_lightsDiffuse; +uniform float u_lightsSpecular; +uniform float u_lightsShadow; ${ declarePI } ${ rotation2 } @@ -170,8 +172,11 @@ void main() { vec3 specular = vec3(0.); float lightCount = max(u_colorsCount, 1.); - float intensity = 2. / (.5 + .5 * lightCount); - float shininess = mix(75., 900., u_lightsShininess); + float diffuseIntensity = u_lightsDiffuse; + float specularIntensity = 7. * u_lightsSpecular; + + diffuse = materialColor; + float totalNdotL = 0.; for (int i = 0; i < ${ logo3dMeta.maxColorCount }; i++) { if (i >= int(u_colorsCount)) break; @@ -200,17 +205,23 @@ void main() { vec3 L = normalize(vec3(cos(angle) * cosElev, sin(angle) * cosElev, sin(elevation))); vec3 lightColor = u_colors[i].rgb; + vec3 halfDir = normalize(L + viewDir); + + float NdotShadow = max(dot(normal, L), 0.); + totalNdotL += pow(NdotShadow, 20.); float NdotL = max(dot(normal, L), 0.); - diffuse += materialColor * NdotL * lightColor * intensity; + NdotL = pow(NdotL, 5.) * diffuseIntensity; + diffuse = mix(diffuse, lightColor, NdotL); - vec3 halfDir = normalize(L + viewDir); - float NdotH = max(dot(normal, halfDir), 0.0); - specular += pow(NdotH, shininess) * lightColor * intensity; + float NdotH = dot(normal, halfDir); + specular += pow(NdotH, 800.) * lightColor * specularIntensity; } float opacity = imgAlpha; vec3 color = diffuse + specular; + color -= clamp(1. - totalNdotL, 0., 1.) * u_lightsShadow; + color = clamp(color, vec3(0.), vec3(1.)); color *= opacity; @@ -682,7 +693,9 @@ export interface Logo3dUniforms extends ShaderSizingUniforms { u_colorsCount: number; u_image: HTMLImageElement | string | undefined; u_lightsSpread: number; - u_lightsShininess: number; + u_lightsDiffuse: number; + u_lightsSpecular: number; + u_lightsShadow: number; } export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { @@ -691,5 +704,7 @@ export interface Logo3dParams extends ShaderSizingParams, ShaderMotionParams { colorBase?: string; image?: HTMLImageElement | string | undefined; lightsSpread?: number; - lightsShininess?: number; + lightsDiffuse?: number; + lightsSpecular?: number; + lightsShadow?: number; }