Skip to content

Instantly share code, notes, and snippets.

@nusu
Created April 24, 2026 18:06
Show Gist options
  • Select an option

  • Save nusu/bd9f76adeaf35c089b9e6867564e624c to your computer and use it in GitHub Desktop.

Select an option

Save nusu/bd9f76adeaf35c089b9e6867564e624c to your computer and use it in GitHub Desktop.
import React, { useEffect, useRef } from 'react'
import { useDialKit, DialRoot } from 'dialkit'
import 'dialkit/styles.css'
const IMG = "https://images.unsplash.com/photo-1774332080727-2a5da87d0104?q=80&w=2187&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
const VS = `
attribute vec2 p;
varying vec2 v;
void main(){ v = p*0.5+0.5; v.y = 1.0-v.y; gl_Position = vec4(p,0,1); }
`
// Radial jelly wave expanding from a click point.
// uOrigin - the click point (uv space)
// uTravel - current radius of the ring
// uBandHeight - ring thickness (gaussian sigma-ish)
// uPullAmount - peak radial displacement
// uOvalW - aspect ratio for the oval
// d - spring displacement signal (can go negative)
const FS = `
precision highp float;
varying vec2 v;
uniform sampler2D tex;
uniform float d;
uniform float uTravel;
uniform float uBandHeight;
uniform float uPullAmount;
uniform float uOvalW;
uniform float uCA;
uniform vec2 uOrigin;
void main(){
vec2 uv = v;
vec2 rel = uv - uOrigin;
// Slight horizontal squash so the ring reads as a soft oval, not perfect circle
rel.x /= uOvalW;
float dist = length(rel);
// Gaussian ring centered at uTravel
float ring = exp(-pow((dist - uTravel) / uBandHeight, 2.0) * 4.0);
// Outward radial direction from origin
vec2 dir = normalize(uv - uOrigin + 1e-6);
// Pixels in the ring get pulled outward; bounce sign comes from d.
vec2 pull = dir * d * uPullAmount * ring;
vec2 uvSample = uv - pull;
// Chromatic aberration along the ring's leading edge
float edge = ring * (1.0 - ring) * 4.0;
vec2 ca = dir * uCA * edge * abs(d);
float r = texture2D(tex, uvSample + ca).r;
float g = texture2D(tex, uvSample).g;
float b = texture2D(tex, uvSample - ca).b;
gl_FragColor = vec4(r, g, b, 1.0);
}
`
function compile(gl, type, src) {
const s = gl.createShader(type)
gl.shaderSource(s, src)
gl.compileShader(s)
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) console.error(gl.getShaderInfoLog(s))
return s
}
export default function JellyInteractive() {
const params = useDialKit('Jelly Interactive', {
stiffness: [391, 20, 800],
damping: [7, 1, 60],
pullAmount: [0.03, 0, 0.3],
bandHeight: [0.36, 0.05, 1.0],
ovalWidth: [0.93, 0.1, 2.0],
chromaticAberration: [0.01, 0, 0.05],
})
const canvasRef = useRef(null)
const drawRef = useRef(() => { })
const paramsRef = useRef(params)
paramsRef.current = params
const animTokenRef = useRef(0)
useEffect(() => {
const canvas = canvasRef.current
const gl = canvas.getContext('webgl', { premultipliedAlpha: false })
const prog = gl.createProgram()
gl.attachShader(prog, compile(gl, gl.VERTEX_SHADER, VS))
gl.attachShader(prog, compile(gl, gl.FRAGMENT_SHADER, FS))
gl.linkProgram(prog)
gl.useProgram(prog)
const buf = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buf)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW)
const pLoc = gl.getAttribLocation(prog, 'p')
gl.enableVertexAttribArray(pLoc)
gl.vertexAttribPointer(pLoc, 2, gl.FLOAT, false, 0, 0)
const u = {
d: gl.getUniformLocation(prog, 'd'),
travel: gl.getUniformLocation(prog, 'uTravel'),
band: gl.getUniformLocation(prog, 'uBandHeight'),
pull: gl.getUniformLocation(prog, 'uPullAmount'),
oval: gl.getUniformLocation(prog, 'uOvalW'),
ca: gl.getUniformLocation(prog, 'uCA'),
origin: gl.getUniformLocation(prog, 'uOrigin'),
}
const tex = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, tex)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
drawRef.current = (d, travel, origin) => {
const p = paramsRef.current
gl.uniform1f(u.d, d)
gl.uniform1f(u.travel, travel)
gl.uniform1f(u.band, p.bandHeight)
gl.uniform1f(u.pull, p.pullAmount)
gl.uniform1f(u.oval, p.ovalWidth)
gl.uniform1f(u.ca, p.chromaticAberration)
gl.uniform2f(u.origin, origin[0], origin[1])
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
const W = 560
const H = Math.round(W * img.height / img.width)
canvas.width = W
canvas.height = H
gl.viewport(0, 0, W, H)
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)
drawRef.current(0, -10, [0.5, 0.5])
}
img.src = IMG
}, [])
useEffect(() => { drawRef.current(0, -10, [0.5, 0.5]) },
[params.bandHeight, params.pullAmount, params.ovalWidth, params.chromaticAberration])
const onCanvasClick = (e) => {
const myToken = ++animTokenRef.current
const rect = canvasRef.current.getBoundingClientRect()
const x = (e.clientX - rect.left) / rect.width
const y = (e.clientY - rect.top) / rect.height
const origin = [x, y]
// Ring radius needs to grow large enough to leave the canvas from the
// farthest corner relative to the click point.
const farthest = Math.max(
Math.hypot(x, y),
Math.hypot(1 - x, y),
Math.hypot(x, 1 - y),
Math.hypot(1 - x, 1 - y),
)
const totalDist = farthest + 0.1
let xs = 0, vs = 0
let last = performance.now()
const startTime = last
const travelMs = 700
const pullStiffness = 600
const pullDamping = 28
let phase = 'pull'
const step = (now) => {
if (animTokenRef.current !== myToken) return // superseded by a newer click
const dt = Math.min(0.032, (now - last) / 1000)
last = now
const { stiffness, damping } = paramsRef.current
const travelT = Math.min(1, (now - startTime) / travelMs)
const travelDist = travelT * totalDist
const sub = 4
const h = dt / sub
if (phase === 'pull') {
for (let i = 0; i < sub; i++) {
const f = -pullStiffness * (xs - 1) - pullDamping * vs
vs += f * h
xs += vs * h
}
if (xs >= 0.98) phase = 'release'
} else {
for (let i = 0; i < sub; i++) {
const f = -stiffness * xs - damping * vs
vs += f * h
xs += vs * h
}
if (Math.abs(xs) < 0.002 && Math.abs(vs) < 0.02 && travelT >= 1) {
drawRef.current(0, -10, origin)
return
}
}
drawRef.current(xs, travelDist, origin)
requestAnimationFrame(step)
}
requestAnimationFrame(step)
}
return (
<div style={{
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
}}>
<canvas
ref={canvasRef}
onClick={onCanvasClick}
style={{ width: 420, borderRadius: 8, display: 'block', cursor: 'crosshair' }}
/>
<div style={{ color: '#666', fontSize: 12 }}>click anywhere on the photo</div>
<a href="/" style={{ color: '#888', fontSize: 12 }}>← back</a>
<DialRoot />
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment