Skip to content

Instantly share code, notes, and snippets.

@martinlaxenaire
Last active May 1, 2021 15:51
Show Gist options
  • Save martinlaxenaire/de849b9d3f79fe60d17f2477f2cfa077 to your computer and use it in GitHub Desktop.
Save martinlaxenaire/de849b9d3f79fe60d17f2477f2cfa077 to your computer and use it in GitHub Desktop.
Overall concept to on how to use the TextTexture class and to render chosen planes onto a specific render target with curtains.js
/***
Really basic example of how I'm rendering the WebGL elements of my portfolio: https://www.martin-laxenaire.fr/
***/
import {
Curtains,
Plane,
Vec2,
PingPongPlane,
RenderTarget,
ShaderPass
} from 'curtainsjs';
import {TextTexture } from "./TextTexture.js"; // see https://gist.github.com/martinlaxenaire/549b3b01ff4bd9d29ce957edd8b56f16
import { ripplesVs, ripplesFs } from "./path/to/ripples.js"; // assuming you're exporting the shaders here
import { textVs, textFs } from "./path/to/text-planes.js"; // assuming you're exporting the shaders here
import { scrollFs } from "./path/to/scroll-pass.js"; // assuming you're exporting the shaders here
import { renderFs } from "./path/to/render-pass.js"; // assuming you're exporting the shaders here
export class WebGLLayer {
constructor() {
this.curtains = new Curtains({
container: "canvas",
pixelRatio: Math.min(1.5, window.devicePixelRatio),
antialias: false, // we'll be using render targets that disable WebGL built-in antialiasing
});
this.curtains.onSuccess(() => {
// used for various resolution uniforms
this.size = this.curtains.getBoundingRect();
// add the ripples effect
this.addRipples();
// add the different post processing passes
this.addRenderPasses();
// add the text planes
this.addHeaderPlanes();
this.addTextPlanes();
}).onError(() => {
// handle errors
});
}
onMouseMove(e) {
if (this.ripples) {
const mousePos = {
x: e.targetTouches ? e.targetTouches[0].clientX : e.clientX,
y: e.targetTouches ? e.targetTouches[0].clientY : e.clientY,
};
this.mouse.last.copy(this.mouse.current);
this.mouse.updateVelocity = true;
if (!this.mouse.lastTime) {
this.mouse.lastTime = (performance || Date).now();
}
if (
this.mouse.last.x === 0 &&
this.mouse.last.y === 0 &&
this.mouse.current.x === 0 &&
this.mouse.current.y === 0
) {
this.mouse.updateVelocity = false;
}
this.mouse.current.set(mousePos.x, mousePos.y);
const webglCoords = this.ripples.mouseToPlaneCoords(this.mouse.current);
this.ripples.uniforms.mousePosition.value = webglCoords;
// divided by a frame duration (roughly)
if (this.mouse.updateVelocity) {
const time = (performance || Date).now();
const delta = Math.max(14, time - this.mouse.lastTime);
this.mouse.lastTime = time;
this.mouse.velocity.set(
(this.mouse.current.x - this.mouse.last.x) / delta,
(this.mouse.current.y - this.mouse.last.y) / delta
);
}
}
}
addRipples() {
// see https://codepen.io/martinlaxenaire/pen/wvgObPj for complete implementation
this.mouse = {
last: new Vec2(),
current: new Vec2(),
velocity: new Vec2(),
updateVelocity: false,
lastTime: null,
};
this.ripples = new PingPongPlane(this.curtains,
document.getElementById("canvas"), {
vertexShader: ripplesVs,
fragmentShader: ripplesFs,
autoloadSources: false,
watchScroll: false,
sampler: "uRipples",
texturesOptions: {
floatingPoint: "half-float"
},
uniforms: {
mousePosition: {
name: "uMousePosition",
type: "2f",
value: this.mouse.current,
},
// our velocity
velocity: {
name: "uVelocity",
type: "2f",
value: this.mouse.velocity,
},
// window aspect ratio to draw a circle
resolution: {
name: "uResolution",
type: "2f",
value: new Vec2(this.size.width, this.size.height),
},
pixelRatio: {
name: "uPixelRatio",
type: "1f",
value: this.curtains.pixelRatio,
},
time: {
name: "uTime",
type: "1i",
value: -1,
},
viscosity: {
name: "uViscosity",
type: "1f",
value: 10.75,
},
speed: {
name: "uSpeed",
type: "1f",
value: 6.75,
},
size: {
name: "uSize",
type: "1f",
value: 2,
},
dissipation: {
name: "uDissipation",
type: "1f",
value: 0.9875,
}
},
}
);
this.ripples.onRender(() => {
this.mouse.velocity.set(this.curtains.lerp(this.mouse.velocity.x, 0, 0.05), this.curtains.lerp(this.mouse.velocity.y, 0, 0.1));
this.ripples.uniforms.velocity.value = this.mouse.velocity.clone();
this.ripples.uniforms.time.value++;
}).onAfterResize(() => {
// update our window aspect ratio uniform
const boundingRect = this.ripples.getBoundingRect();
this.ripples.uniforms.resolution.value.set(boundingRect.width, boundingRect.height);
});
// handle mouse move
window.addEventListener("mousemove", this.onMouseMove.bind(this));
window.addEventListener("touchmove", this.onMouseMove.bind(this));
}
addRenderPasses() {
this.scrollTarget = new RenderTarget(this.curtains);
// see https://codepen.io/martinlaxenaire/pen/QWdoRrJ for complete implementation
this.scroll = {
value: 0,
lastValue: 0,
effect: 0,
};
this.scrollPass = new ShaderPass(this.curtains, {
fragmentShader: scrollFs,
renderTarget: this.scrollTarget,
depth: false,
uniforms: {
scrollEffect: {
name: "uScrollEffect",
type: "1f",
value: this.scroll.effect,
},
scrollStrength: {
name: "uScrollStrength",
type: "1f",
value: this.isPortrait ? 3 : 1.5,
},
}
});
this.scrollPass.onRender(() => {
// when using a smooth scroll library, skip this and just use the scroll velocity for the scrollEffect uniform
this.scroll.lastValue = this.scroll.value;
this.scroll.value = this.curtains.getScrollValues().y;
this.scroll.delta = Math.max(-30, Math.min(30, this.scroll.lastValue - this.scroll.value));
this.scroll.effect = this.curtains.lerp(this.scroll.effect, this.scroll.delta, 0.05);
this.scrollPass.uniforms.scrollEffect.value = this.scroll.effect;
}).onAfterResize(() => {
const boundingRect = this.scrollPass.getBoundingRect();
this.scrollPass.uniforms.scrollStrength.value = boundingRect.width >= boundingRect.height ? 1.5 : 3;
});
const params = {
fragmentShader: renderFs,
depth: false,
uniforms: {
resolution: {
name: "uResolution",
type: "2f",
value: new Vec2(this.size.width, this.size.height),
},
},
};
// global post processing
this.renderPass = new ShaderPass(this.curtains, params);
this.renderPass.onAfterResize(() => {
// update our window aspect ratio uniform
const boundingRect = this.renderPass.getBoundingRect();
this.renderPass.uniforms.resolution.value.set(boundingRect.width, boundingRect.height);
});
// add our ripple texture to the render pass
this.renderPass.createTexture({
sampler: "uRipplesTexture",
fromTexture: this.ripples.getTexture()
});
}
// see https://codepen.io/martinlaxenaire/pen/QWdoRrJ
addHeaderPlanes() {
// add the header planes that will not get affected by the scroll effect
document.querySelectorAll("#header .header-plane").forEach(headerEl => {
const headerPlane = new Plane(this.curtains, headerEl, {
vertexShader: textVs,
fragmentShader: textVs,
watchScroll: false, // act like CSS fixed position
});
const textTexture = new TextTexture({
plane: headerPlane,
textElement: headerPlane.htmlElement,
sampler: "uTexture",
resolution: 1.5,
// assuming you've loaded all the fonts beforehand using the document.fonts API
// see https://developer.mozilla.org/en-US/docs/Web/API/Document/fonts
skipFontLoading: true,
});
// easily access the texture if needed
headerPlane.userData.textTexture = textTexture;
});
}
// see https://codepen.io/martinlaxenaire/pen/QWdoRrJ
addTextPlanes() {
document.querySelectorAll(".text-plane").forEach(textEl => {
const textPlane = new Plane(this.curtains, textEl, {
vertexShader: textVs,
fragmentShader: textVs,
});
// here we add them to the scroll effect pass!
// all the others planes (images and so forth) that'd need to be distorted on scroll
// will have to be added to that render target as well!
textPlane.setRenderTarget(this.scrollTarget);
const textTexture = new TextTexture({
plane: textPlane,
textElement: textPlane.htmlElement,
sampler: "uTexture",
resolution: 1.5,
skipFontLoading: true,
});
// easily access the texture if needed
textPlane.userData.textTexture = textTexture;
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment