diff --git a/docs/registry.json b/docs/registry.json index f984641d..0dc24449 100644 --- a/docs/registry.json +++ b/docs/registry.json @@ -275,6 +275,19 @@ "type": "registry:component" } ] + }, + { + "name": "blobs-grid", + "type": "registry:component", + "title": "Blobs Grid Example", + "description": "Blobs Grid shader example.", + "dependencies": ["@paper-design/shaders-react"], + "files": [ + { + "path": "registry/blobs-grid-example.tsx", + "type": "registry:component" + } + ] } ] } diff --git a/docs/registry/blobs-grid-example.tsx b/docs/registry/blobs-grid-example.tsx new file mode 100644 index 00000000..87e9f9ba --- /dev/null +++ b/docs/registry/blobs-grid-example.tsx @@ -0,0 +1,5 @@ +import { BlobsGrid, type BlobsGridProps } from '@paper-design/shaders-react'; + +export function BlobsGridExample(props: BlobsGridProps) { + return ; +} diff --git a/docs/src/app/blobs-grid/layout.tsx b/docs/src/app/blobs-grid/layout.tsx new file mode 100644 index 00000000..9f44d330 --- /dev/null +++ b/docs/src/app/blobs-grid/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Blobs Grid Shader | Paper', +}; + +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/docs/src/app/blobs-grid/page.tsx b/docs/src/app/blobs-grid/page.tsx new file mode 100644 index 00000000..3bafc2e6 --- /dev/null +++ b/docs/src/app/blobs-grid/page.tsx @@ -0,0 +1,112 @@ +'use client'; + +import { BlobsGrid, type BlobsGridParams, blobsGridPresets } 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 Link from 'next/link'; +import { BackButton } from '@/components/back-button'; +import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; +import { ShaderFit, ShaderFitOptions, blobsGridMeta } from '@paper-design/shaders'; +import { useColors } from '@/helpers/use-colors'; +import { toHsla } from '@/helpers/to-hsla'; + +/** + * You can copy/paste this example to use BlobsGrid in your app + */ +const BlobsGridExample = () => { + return ; +}; + +/** + * This example has controls added so you can play with settings in the example app + */ + +const { worldWidth, worldHeight, ...defaults } = blobsGridPresets[0].params; + +const BlobsGridWithControls = () => { + const { colors, setColors } = useColors({ + defaultColors: defaults.colors, + maxColorCount: blobsGridMeta.maxColorCount, + }); + + const [params, setParams] = useControls(() => { + return { + Parameters: folder( + { + stepsPerColor: { value: defaults.stepsPerColor, min: 1, max: 3, step: 1, order: 0 }, + colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorShade: { value: toHsla(defaults.colorShade), order: 101 }, + colorSpecular: { value: toHsla(defaults.colorSpecular), order: 101 }, + colorInnerShadow: { value: toHsla(defaults.colorInnerShadow), order: 102 }, + distortion: { value: defaults.distortion, min: 0, max: 20, order: 300 }, + size: { value: defaults.size, min: 0, max: 1, order: 301 }, + specular: { value: defaults.specular, min: 0, max: 1, order: 302 }, + specularNormal: { value: defaults.specularNormal, min: 0, max: 1, order: 303 }, + shade: { value: defaults.shade, min: 0, max: 1, order: 304 }, + innerShadow: { value: defaults.innerShadow, min: 0, max: 1, order: 305 }, + speed: { value: defaults.speed, min: 0, max: 2, order: 400 }, + }, + { order: 1 } + ), + Transform: folder( + { + scale: { value: defaults.scale, min: 0.01, max: 4, order: 400 }, + rotation: { value: defaults.rotation, min: 0, max: 360, order: 401 }, + offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 402 }, + offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 403 }, + }, + { + order: 2, + collapsed: false, + } + ), + Fit: folder( + { + fit: { value: defaults.fit, options: Object.keys(ShaderFitOptions) as ShaderFit[], order: 404 }, + worldWidth: { value: 1000, min: 1, max: 5120, order: 405 }, + worldHeight: { value: 500, min: 1, max: 5120, order: 406 }, + originX: { value: defaults.originX, min: 0, max: 1, order: 407 }, + originY: { value: defaults.originY, min: 0, max: 1, order: 408 }, + }, + { + order: 3, + collapsed: true, + } + ), + }; + }, [colors.length]); + + useControls(() => { + const presets = Object.fromEntries( + blobsGridPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + name, + button(() => { + const { colors, ...presetParams } = preset; + setColors(colors); + setParamsSafe(params, setParams, presetParams); + }), + ]) + ); + return { + 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); + usePresetHighlight(blobsGridPresets, params); + cleanUpLevaParams(params); + + return ( + <> + + + + + + ); +}; + +export default BlobsGridWithControls; diff --git a/docs/src/home-shaders.ts b/docs/src/home-shaders.ts index 895a1ab1..fb6a0477 100644 --- a/docs/src/home-shaders.ts +++ b/docs/src/home-shaders.ts @@ -63,6 +63,8 @@ import { staticMeshGradientPresets, StaticRadialGradient, staticRadialGradientPresets, + BlobsGrid, + blobsGridPresets, } from '@paper-design/shaders-react'; import { StaticImageData } from 'next/image'; import TextureTest from './app/texture-test/page'; @@ -84,150 +86,148 @@ export const homeShaders = [ // shaderConfig: {}, // }, { - name: 'grain gradient', - url: '/grain-gradient', - ShaderComponent: GrainGradient, - image: grainGradientImg, - shaderConfig: { ...grainGradientPresets[0].params, frame: 7000, speed: 2 }, + name: 'simplex noise', + image: simplexNoiseImg, + url: '/simplex-noise', + ShaderComponent: SimplexNoise, + shaderConfig: { ...simplexNoisePresets[0].params, scale: 0.35 }, }, { name: 'mesh gradient', image: meshGradientImg, url: '/mesh-gradient', ShaderComponent: MeshGradient, - shaderConfig: { ...meshGradientPresets[0].params, frame: 41500 }, + shaderConfig: { ...meshGradientPresets[0].params }, }, { - name: 'static mesh gradient', - url: '/static-mesh-gradient', - ShaderComponent: StaticMeshGradient, - image: staticMeshGradientImg, - shaderConfig: { ...staticMeshGradientPresets[0].params, rotation: 270 }, - }, - { - name: 'static radial gradient', - url: '/static-radial-gradient', - ShaderComponent: StaticRadialGradient, - image: staticRadialGradientImg, - shaderConfig: { ...staticRadialGradientPresets[0].params, scale: 0.7 }, - }, - { - name: 'dithering', - url: '/dithering', - ShaderComponent: Dithering, - image: ditheringImg, - shaderConfig: { ...ditheringPresets[0].params, scale: 0.45 }, + name: 'neuro noise', + image: neuroNoiseImg, + url: '/neuro-noise', + ShaderComponent: NeuroNoise, + shaderConfig: { ...neuroNoisePresets[0].params }, }, { name: 'dot orbit', image: dotOrbitImg, url: '/dot-orbit', ShaderComponent: DotOrbit, - shaderConfig: { ...dotOrbitPresets[0].params, scale: 0.45 }, + shaderConfig: { ...dotOrbitPresets[0].params, scale: 0.35 }, + }, + { + name: 'smoke ring', + image: smokeRingImg, + url: '/smoke-ring', + ShaderComponent: SmokeRing, + shaderConfig: { ...smokeRingPresets[3].params }, + }, + { + name: 'metaballs', + image: metaballsImg, + url: '/metaballs', + ShaderComponent: Metaballs, + shaderConfig: { ...metaballsPresets[0].params }, }, { name: 'dot grid', url: '/dot-grid', ShaderComponent: DotGrid, image: dotGridImg, - shaderConfig: { ...dotGridPresets[0].params, scale: 1.1, size: 2 }, + shaderConfig: { ...dotGridPresets[0].params, scale: 0.4 }, + }, + { + name: 'perlin', + url: '/perlin-noise', + ShaderComponent: PerlinNoise, + image: perlinNoiseImg, + shaderConfig: { ...perlinNoisePresets[1].params, scale: 0.8 }, + }, + { + name: 'voronoi', + url: '/voronoi', + ShaderComponent: Voronoi, + image: voronoiImg, + shaderConfig: { ...voronoiPresets[0].params, scale: 0.5 }, + }, + { + name: 'waves', + url: '/waves', + ShaderComponent: Waves, + image: wavesImg, + shaderConfig: { ...wavesPresets[0].params, shape: 1 }, }, { name: 'warp', url: '/warp', ShaderComponent: Warp, image: warpImg, - shaderConfig: { ...warpPresets[0].params, speed: 2, offsetX: -0.2, scale: 0.6, frame: 20000 }, + shaderConfig: { ...warpPresets[2].params, scale: 0.25 }, + }, + { + name: 'god rays', + url: '/god-rays', + ShaderComponent: GodRays, + image: godRaysImg, + shaderConfig: { ...godRaysPresets[0].params, offsetX: -1.1, midSize: 1 }, }, { name: 'spiral', url: '/spiral', ShaderComponent: Spiral, image: spiralImg, - shaderConfig: { ...spiralPresets[0].params, scale: 0.5, speed: 2 }, + shaderConfig: { ...spiralPresets[1].params }, }, { name: 'swirl', url: '/swirl', ShaderComponent: Swirl, - image: swirlImg, - shaderConfig: { ...swirlPresets[0].params, speed: 0.7 }, - }, - { - name: 'waves', - url: '/waves', - ShaderComponent: Waves, - image: wavesImg, - shaderConfig: { ...wavesPresets[0].params, scale: 0.9 }, + shaderConfig: { ...swirlPresets[0].params }, }, { - name: 'neuro noise', - image: neuroNoiseImg, - url: '/neuro-noise', - ShaderComponent: NeuroNoise, - shaderConfig: { ...neuroNoisePresets[0].params, scale: 0.6, frame: 1500, offsetX: -0.17 }, - }, - { - name: 'perlin', - url: '/perlin-noise', - ShaderComponent: PerlinNoise, - image: perlinNoiseImg, - shaderConfig: { ...perlinNoisePresets[0].params, scale: 0.8, speed: 0.2 }, + name: 'dithering', + url: '/dithering', + ShaderComponent: Dithering, + shaderConfig: { ...ditheringPresets[0].params }, }, { - name: 'simplex noise', - image: simplexNoiseImg, - url: '/simplex-noise', - ShaderComponent: SimplexNoise, - shaderConfig: { ...simplexNoisePresets[0].params, scale: 0.4 }, + name: 'liquid metal', + url: '/liquid-metal', + ShaderComponent: LiquidMetal, + shaderConfig: { ...liquidMetalPresets[0].params }, }, { - name: 'voronoi', - url: '/voronoi', - ShaderComponent: Voronoi, - image: voronoiImg, - shaderConfig: { ...voronoiPresets[0].params, scale: 0.35 }, + name: 'grain gradient', + url: '/grain-gradient', + ShaderComponent: GrainGradient, + shaderConfig: { ...grainGradientPresets[0].params }, }, { name: 'pulsing border', url: '/pulsing-border', ShaderComponent: PulsingBorder, - image: pulsingBorderImg, - shaderConfig: { ...pulsingBorderPresets[0].params, scale: 0.45 }, - }, - { - name: 'metaballs', - image: metaballsImg, - url: '/metaballs', - ShaderComponent: Metaballs, - shaderConfig: { ...metaballsPresets[0].params, scale: 0.7, frame: 21300, offsetY: -0.01 }, + shaderConfig: { ...pulsingBorderPresets[0].params }, }, { name: 'color panels', url: '/color-panels', ShaderComponent: ColorPanels, - image: colorPanelsImg, - shaderConfig: { ...colorPanelsPresets[0].params, speed: 2, scale: 0.6 }, + shaderConfig: { ...colorPanelsPresets[0].params }, }, { - name: 'smoke ring', - image: smokeRingImg, - url: '/smoke-ring', - ShaderComponent: SmokeRing, - shaderConfig: { ...smokeRingPresets[0].params, scale: 0.6, speed: 2 }, + name: 'static mesh gradient', + url: '/static-mesh-gradient', + ShaderComponent: StaticMeshGradient, + shaderConfig: { ...staticMeshGradientPresets[0].params }, }, { - name: 'liquid metal', - url: '/liquid-metal', - ShaderComponent: LiquidMetal, - image: liquidMetalImg, - shaderConfig: { ...liquidMetalPresets[0].params, scale: 0.45 }, + name: 'static radial gradient', + url: '/static-radial-gradient', + ShaderComponent: StaticRadialGradient, + shaderConfig: { ...staticRadialGradientPresets[0].params }, }, { - name: 'god rays', - url: '/god-rays', - ShaderComponent: GodRays, - image: godRaysImg, - shaderConfig: { ...godRaysPresets[0].params, speed: 2, scale: 0.5, offsetY: -0.5 }, + name: 'blobs grid', + url: '/blobs-grid', + ShaderComponent: BlobsGrid, + shaderConfig: { ...blobsGridPresets[0].params }, }, ] satisfies HomeShaderConfig[]; diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 9b4d7130..2895fc28 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -87,6 +87,10 @@ export { StaticRadialGradient, staticRadialGradientPresets } from './shaders/sta export type { StaticRadialGradientProps } from './shaders/static-radial-gradient.js'; export type { StaticRadialGradientUniforms, StaticRadialGradientParams } from '@paper-design/shaders'; +export { BlobsGrid, blobsGridPresets } from './shaders/blobs-grid.js'; +export type { BlobsGridProps } from './shaders/blobs-grid.js'; +export type { BlobsGridUniforms, BlobsGridParams } 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-grid.tsx b/packages/shaders-react/src/shaders/blobs-grid.tsx new file mode 100644 index 00000000..f5dd0e6a --- /dev/null +++ b/packages/shaders-react/src/shaders/blobs-grid.tsx @@ -0,0 +1,170 @@ +import { memo } from 'react'; +import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; +import { colorPropsAreEqual } from '../color-props-are-equal.js'; +import { + defaultPatternSizing, + getShaderColorFromString, + blobsGridFragmentShader, + ShaderFitOptions, + type BlobsGridParams, + type BlobsGridUniforms, + type ShaderPreset, +} from '@paper-design/shaders'; + +export interface BlobsGridProps extends ShaderComponentProps, BlobsGridParams {} + +type BlobsGridPreset = ShaderPreset; + +export const defaultPreset: BlobsGridPreset = { + name: 'Default', + params: { + ...defaultPatternSizing, + scale: 0.45, + speed: 0.5, + frame: 0, + colors: ['#e4ab1b', '#33b1ff', '#db579b'], + stepsPerColor: 2, + colorBack: '#000000', + colorShade: '#ffffff', + colorSpecular: '#ffffffa1', + colorInnerShadow: '#000000', + distortion: 8.6, + shade: 0.0, + specular: 0, + specularNormal: 0, + size: 0.49, + innerShadow: 0, + }, +}; + +export const dropsPreset: BlobsGridPreset = { + name: 'Drops', + params: { + ...defaultPatternSizing, + scale: 0.5, + speed: 2, + frame: 0, + colors: ['#ff00aa'], + stepsPerColor: 3, + colorBack: '#ffaa00', + colorShade: '#ffffff', + colorSpecular: '#ffffffa1', + colorInnerShadow: '#00ffd0', + distortion: 9.2, + size: 0.04, + specular: 1.0, + specularNormal: 1.0, + shade: 1.0, + innerShadow: 0.06, + offsetX: 0, + offsetY: 0, + rotation: 0, + }, +}; + +export const shadowsPreset: BlobsGridPreset = { + name: 'Shadows', + params: { + ...defaultPatternSizing, + scale: 0.5, + speed: 2, + frame: 0, + colors: ['#c8e9c8', '#037c6e', '#ff9100'], + stepsPerColor: 3, + colorBack: '#ffffff', + colorShade: '#ffffff', + colorSpecular: '#ffffffa1', + colorInnerShadow: '#000000', + distortion: 5.6, + shade: 1.0, + specular: 0.0, + specularNormal: 0.82, + size: 0.51, + innerShadow: 0, + }, +}; + +export const specularPreset: BlobsGridPreset = { + name: 'Specular', + params: { + ...defaultPatternSizing, + speed: 1, + frame: 0, + colors: ['#ff7b00', '#ff29f1', '#00b2ff'], + stepsPerColor: 2, + colorBack: '#000000', + colorShade: '#5b2f3d', + colorSpecular: '#ffffff80', + colorInnerShadow: '#000000', + distortion: 7, + shade: 0.3, + specular: 0.75, + specularNormal: 0.75, + size: 0.75, + innerShadow: 0.27, + }, +}; + +export const blobsGridPresets: BlobsGridPreset[] = [defaultPreset, shadowsPreset, dropsPreset, specularPreset] as const; + +export const BlobsGrid: React.FC = memo(function BlobsGridImpl({ + // Own props + speed = defaultPreset.params.speed, + frame = defaultPreset.params.frame, + colors = defaultPreset.params.colors, + stepsPerColor = defaultPreset.params.stepsPerColor, + colorBack = defaultPreset.params.colorBack, + colorShade = defaultPreset.params.colorShade, + colorSpecular = defaultPreset.params.colorSpecular, + colorInnerShadow = defaultPreset.params.colorInnerShadow, + distortion = defaultPreset.params.distortion, + shade = defaultPreset.params.shade, + specular = defaultPreset.params.specular, + specularNormal = defaultPreset.params.specularNormal, + size = defaultPreset.params.size, + innerShadow = defaultPreset.params.innerShadow, + + // 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 +}: BlobsGridProps) { + const uniforms = { + // Own uniforms + u_colors: colors.map(getShaderColorFromString), + u_colorsCount: colors.length, + u_stepsPerColor: stepsPerColor, + u_colorBack: getShaderColorFromString(colorBack), + u_colorShade: getShaderColorFromString(colorShade), + u_colorSpecular: getShaderColorFromString(colorSpecular), + u_colorInnerShadow: getShaderColorFromString(colorInnerShadow), + u_distortion: distortion, + u_shade: shade, + u_specular: specular, + u_specularNormal: specularNormal, + u_size: size, + u_innerShadow: innerShadow, + + // 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 BlobsGridUniforms; + + return ( + + ); +}, colorPropsAreEqual); diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index baa8fa37..28ab86f3 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -171,6 +171,13 @@ export { type StaticRadialGradientUniforms, } from './shaders/static-radial-gradient.js'; +export { + blobsGridMeta, + blobsGridFragmentShader, + type BlobsGridParams, + type BlobsGridUniforms, +} from './shaders/blobs-grid.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/blobs-grid.ts b/packages/shaders/src/shaders/blobs-grid.ts new file mode 100644 index 00000000..687fa83e --- /dev/null +++ b/packages/shaders/src/shaders/blobs-grid.ts @@ -0,0 +1,200 @@ +import type { vec4 } from '../types.js'; +import type { ShaderMotionParams } from '../shader-mount.js'; +import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; +import { declareRandom, declarePI, declareRotate, declareSimplexNoise } from '../shader-utils.js'; + +export const blobsGridMeta = { + maxColorCount: 4, +} as const; + +/** + * A dot grid with distortion + 3d-simulating overlays (shades, outlines, highlights) + * + * Uniforms: + * - u_colorBack, u_colorShade, u_colorSpecular, u_colorInnerShadow (RGBA) + * - u_colors (vec4[]), u_colorsCount (float used as integer) + * - u_stepsPerColor: discrete color steps between u_colors + * - u_size, u_distortion: blob size & shape + * - u_specular, u_specularNormal: highlight size & shape (on each cell) + * - u_innerShadow, u_shade: 2 additional color overlays (on each cell) + * + */ + +// language=GLSL +export const blobsGridFragmentShader: string = `#version 300 es +precision mediump float; + +uniform float u_time; + +uniform vec4 u_colors[${blobsGridMeta.maxColorCount}]; +uniform float u_colorsCount; +uniform float u_stepsPerColor; + +uniform vec4 u_colorBack; +uniform vec4 u_colorShade; +uniform vec4 u_colorSpecular; +uniform vec4 u_colorInnerShadow; +uniform float u_distortion; +uniform float u_specular; +uniform float u_specularNormal; +uniform float u_size; +uniform float u_innerShadow; +uniform float u_shade; + +uniform float u_scale; + +${sizingVariablesDeclaration} + +out vec4 fragColor; + +${declarePI} +${declareSimplexNoise} + +vec2 rand2(vec2 c) { + mat2 m = mat2(12.9898, .16180, 78.233, .31415); + return fract(sin(m * c) * vec2(43758.5453, 14142.1)); +} + +vec2 noise(vec2 p) { + vec2 co = floor(p); + vec2 mu = fract(p); + mu = 3. * mu * mu - 2. * mu * mu * mu; + vec2 a = rand2((co + vec2(0., 0.))); + vec2 b = rand2((co + vec2(1., 0.))); + vec2 c = rand2((co + vec2(0., 1.))); + vec2 d = rand2((co + vec2(1., 1.))); + return mix(mix(a, b, mu.x), mix(c, d, mu.x), mu.y); +} + +${declareRandom} +${declareRotate} + +void main() { + + vec2 uv = v_patternUV * .1; + + float t = .1 * u_time; + + vec3 lightDir = normalize(vec3(-.5, .5, -.65)); + + vec2 dropDistortion = noise(uv * u_distortion + t); + vec2 grid = TWO_PI * uv + dropDistortion; + + vec2 cellUV = fract(grid); + vec2 cellIdx = floor(grid); + + + vec2 pos = cellUV - .5; + float aax = 2. * fwidth(grid.x); + float aay = 2. * fwidth(grid.y); + float cellInnerShadow = smoothstep(0., aax, cellUV.x); + cellInnerShadow *= smoothstep(0., aay, cellUV.y); + cellInnerShadow *= (smoothstep(1., 1. - aax, cellUV.x)); + cellInnerShadow *= (smoothstep(1., 1. - aay, cellUV.y)); + cellInnerShadow = clamp(cellInnerShadow, 0., 1.); + + vec2 posNorm = normalize(pos); + float l = length(pos); + float dist = 1. - l; + + dist *= (.5 + .7 * u_size); + + const float shapeOuter = .43; + float aa = fwidth(l); + float contour = smoothstep(shapeOuter, shapeOuter + 2. * aa, dist); + contour *= (.5 + .5 * cellInnerShadow); + + float colorCode = random(noise(cellIdx)); + float mixer = colorCode * (u_colorsCount - 1.); + mixer = (colorCode - .5 / u_colorsCount) * u_colorsCount; + float steps = max(1., u_stepsPerColor); + + vec4 gradient = u_colors[0]; + gradient.rgb *= gradient.a; + for (int i = 1; i < ${blobsGridMeta.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); + } + + + vec3 color = gradient.rgb; + float opacity = gradient.a; + + vec3 shadeNormal = normalize(vec3(posNorm * sin(l * 10. * u_shade), -2.)); + float shade = 1. - dot(shadeNormal, lightDir); + shade *= smoothstep(.0, .4, u_shade); + shade *= u_colorShade.a; + color = mix(color, u_colorShade.rgb * u_colorShade.a, shade); + + float innerShadow = (1. - smoothstep(shapeOuter, shapeOuter + .5 * u_innerShadow, dist)); + innerShadow *= u_colorInnerShadow.a; + color = mix(color, u_colorInnerShadow.rgb * u_colorInnerShadow.a, innerShadow); + + posNorm = normalize(pos - .08); + vec3 specularNormal = normalize(vec3(posNorm * sin(l * (3. + 20. * u_specularNormal)), -2.)); + float specular = smoothstep(1. - .25 * u_specular, 1. + aa - .25 * u_specular, dot(specularNormal, lightDir)); + specular *= u_colorSpecular.a; + specular -= innerShadow; + specular -= smoothstep(.3, .5, shade); + specular = clamp(specular, 0., 1.); + color = mix(color, u_colorSpecular.rgb, specular); + + color *= contour; + opacity *= contour; + + vec3 bgColor = u_colorBack.rgb * u_colorBack.a; + color = color + bgColor * (1. - opacity); + opacity = opacity + u_colorBack.a * (1. - opacity); + + fragColor = vec4(color, opacity); +} + +`; + +export interface BlobsGridUniforms extends ShaderSizingUniforms { + u_colors: vec4[]; + u_colorsCount: number; + u_stepsPerColor: number; + u_colorBack: [number, number, number, number]; + u_colorShade: [number, number, number, number]; + u_colorSpecular: [number, number, number, number]; + u_colorInnerShadow: [number, number, number, number]; + u_distortion: number; + u_specular: number; + u_specularNormal: number; + u_shade: number; + u_size: number; + u_innerShadow: number; +} + +export interface BlobsGridParams extends ShaderSizingParams, ShaderMotionParams { + colors?: string[]; + stepsPerColor?: number; + colorBack?: string; + colorShade?: string; + colorSpecular?: string; + colorInnerShadow?: string; + shade?: number; + distortion?: number; + specular?: number; + specularNormal?: number; + size?: number; + innerShadow?: number; +}