After years of building web applications I’d drifted away from artistically driven code; as a former Flash engineer I missed making visuals that move. This project was a quick, focused way to relearn shader math while exploring modern WebGPU tooling.
taotajima.jp
The first time I stumbled onto taotajima.jp I remember sitting there, utterly awestruck — the transition effect felt like a magical, tactile trick on the web. That moment stuck with me, and I knew I wanted to rebuild it with TSL as a learning exercise.
The Anatomy of the Effect
After doing some research I found this excellent write-up, Taotajima.jp WebGL deconstruction. Huge thanks to the author — the breakdown gave me the roadmap I needed to recreate this effect.
Diagonal delay
// enterUniform tweens from 0 1 (vice versa)
const delayValue = clamp(
enterUniform.mul(6.5).sub(uv().y.mul(2.3)).add(uv().x.mul(1.2)).sub(1.8),
0.0,
1.0
);
This produces a diagonal sweep across the image (bottom-right → top-left). It's used in both texture translation and wavy distortion below.
Texture Translation with Acceleration
const translateValue = enterUniform.add(delayValue.mul(accelUniform));
const imageTranslate = vec2(-0.4, 1.2).mul(translateValue);
const videoTranslate = vec2(-0.4, 1.2).mul(
translateValue.sub(1.0).sub(accelUniform)
);
Both textures slide in opposite directions with acceleration, which gives the fold a satisfying 'lift and slide' feel.
Wavy Distortion
const waveTimeSpeed = uniform(config.wave.timeSpeed);
const waveAmplitude = uniform(
new THREE.Vector2(config.wave.amplitude.x, config.wave.amplitude.y)
);
const waveUvScale = uniform(
new THREE.Vector2(config.wave.uvScale.x, config.wave.uvScale.y)
);
const waveStrength = uniform(
new THREE.Vector2(config.wave.strength.x, config.wave.strength.y)
);
const distortion = sin(
sin(time.mul(waveTimeSpeed))
.mul(waveAmplitude)
.add(uv().yx.mul(waveUvScale))
).mul(waveStrength);
const xy = distortion.mul(
pingpong(enterUniform).mul(0.6).add(pingpong(delayValue).mul(0.4))
);
The distortion blends a wobble effect with a position-scaled wave so the fold looks soft instead of a flat translation.
Ripple Effect
// Circle SDF
const Circle = Fn(
([position, radius]: [THREE.ConstNode<THREE.Vector2>, number]) => {
return length(position).sub(radius);
}
);
const dist = Circle(
vec2(posFromMouse.x.mul(aspectRatioUniform), posFromMouse.y),
circleRadius
);
const wave = sin(dist.mul(rippleFreq).sub(time.mul(rippleSpeed)));
const ripple = wave.mul(boundary).mul(fade).mul(rippleAmp).mul(enterUniform);
material.positionNode = positionLocal.add(vec3(0, 0, ripple));
This applies z-axis displacement ripples centered on the hover point.
When you combine all of the above, it actually works! That said, the original still looks so much better — huge respect to the original author.
The Scroll Sync
The second challenge was syncing smooth scroll with the canvas. Since the canvas needs to scroll in sync with DOM content, using native scroll creates a janky experience, especially on mobile devices.
Lenis to the rescue
Lenis gives a reliable, eased scroll value that’s easy to pipe straight into Three.js camera transforms. Using lenis.scroll avoids jankiness and lets the canvas follow the page precisely while keeping smooth motion.
const lenis = new Lenis({
wrapper: scrollWrapper, // Scrollable container
content: scrollContent, // Content element
lerp: 0.1, // Smoothing factor
syncTouch: true // Mobile support
});
const animate = (time: number) => {
lenis.raf(time);
// Sync camera position with scroll
camera.position.y = -lenis.scroll;
renderer.render(scene, camera);
};
renderer.setAnimationLoop(animate);
Closing Thoughts
All in all, this was exactly the kind of satisfying project I needed. A weekend of tinkering with shaders, recreating something that genuinely impressed me, and getting my hands dirty with TSL.
The result isn't perfect, and I'm sure there are plenty of subtle techniques in the original that I haven't uncovered yet, but the learning process was worth every minute.
Thanks for reading through! Happy coding! ✨
Live Demo: taotajima-tsl.pages.dev
Source Code: github.com/zenoplex/taotajima-tsl
Images: pexels
Videos: grok imagine




Top comments (0)