Skip to content

Instantly share code, notes, and snippets.

@CodeMyUI
Created February 17, 2020 22:22
Show Gist options
  • Save CodeMyUI/d566f6b2378fa6cc595c86cc5beb6bb4 to your computer and use it in GitHub Desktop.
Save CodeMyUI/d566f6b2378fa6cc595c86cc5beb6bb4 to your computer and use it in GitHub Desktop.
What's behind ?
<canvas id="canvas"></canvas>
<h1>What's behind the curtain ?</h1>
<p>This funny interactive curtain is made with ThreeJS, polylines and verlet constraints (adapted from verlet-js).</p>
function App() {
const conf = {
el: 'canvas',
gravity: -0.3,
nx: 60,
ny: 40,
size: 1.5,
mouseRadius: 10,
mouseStrength: 0.4
};
let renderer, scene, camera;
let width, height;
const { randFloat: rnd, randFloatSpread: rndFS } = THREE.Math;
const mouse = new THREE.Vector2(), oldMouse = new THREE.Vector2();
const verlet = new VerletJS(), polylines = [];
const uCx = { value: 0 }, uCy = { value: 0 };
init();
function init() {
renderer = new THREE.WebGLRenderer({ canvas: document.getElementById(conf.el), antialias: true, alpha: true });
camera = new THREE.PerspectiveCamera();
verlet.width = 256;
verlet.height = 256;
updateSize();
window.addEventListener('resize', updateSize, false);
initScene();
initListeners();
animate();
}
function initScene() {
scene = new THREE.Scene();
verlet.gravity = new Vec2(0, conf.gravity);
initCurtain();
}
function initCurtain() {
const material = new THREE.ShaderMaterial({
transparent: true,
uniforms: {
uCx, uCy,
uSize: { value: conf.size / conf.nx }
},
vertexShader: `
uniform float uCx;
uniform float uCy;
uniform float uSize;
attribute vec3 color;
attribute vec3 next;
attribute vec3 prev;
attribute float side;
varying vec4 vColor;
void main() {
vec3 pos = vec3(position.x * uCx, position.y * uCy, 0.0);
vec2 sprev = vec2(prev.x * uCx, prev.y * uCy);
vec2 snext = vec2(next.x * uCx, next.y * uCy);
vec2 tangent = normalize(snext - sprev);
vec2 normal = vec2(-tangent.y, tangent.x);
float dist = length(snext - sprev);
normal *= smoothstep(0.0, 0.02, dist);
vColor = vec4(color, 1.0 - smoothstep(0.5, 1.0, uv.y) * 0.5);
normal *= uSize;// * (1.0 - uv.y);
pos.xy -= normal * side;
gl_Position = vec4(pos, 1.0);
}
`,
fragmentShader: `
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
}
`
});
const dx = verlet.width / conf.nx, dy = -verlet.height / conf.ny;
const ox = -dx * (conf.nx / 2 - 0.5), oy = verlet.height / 2 - dy / 2;
// const cscale = chroma.scale([chroma.random(), chroma.random()]);
const cscale = chroma.scale([0x09256f, 0x6efec8]);
for (let i = 0; i < conf.nx; i++) {
const points = [];
const vpoints = [];
for (let j = 0; j < conf.ny; j++) {
const x = ox + i * dx, y = oy + j * dy;
points.push(new THREE.Vector3(x, y, 0));
vpoints.push(new Vec2(x, y));
}
const polyline = new Polyline({ points, color1: cscale(rnd(0, 1)), color2: cscale(rnd(0, 1)) });
polylines.push(polyline);
polyline.segment = verlet.lineSegments(vpoints, 5);
polyline.segment.pin(0);
polyline.segment.particles.forEach(p => { p.pos.x += rndFS(10); });
const mesh = new THREE.Mesh(polyline.geometry, material);
scene.add(mesh);
}
}
function updatePoints() {
polylines.forEach(line => {
for (let i = 0; i < line.points.length; i++) {
const p = line.segment.particles[i].pos;
line.points[i].x = p.x;
line.points[i].y = p.y;
}
line.updateGeometry();
});
}
function updateColors() {
const c1 = chroma.random(), c2 = chroma.random();
const cscale = chroma.scale([c1, c2]);
console.log(c1.hex(), c2.hex());
// #21a25f #a0fa42
// #09256f #6efec8
polylines.forEach(line => {
line.color1 = cscale(rnd(0, 1));
line.color2 = cscale(rnd(0, 1));
const cscale1 = chroma.scale([line.color1, line.color2]);
const colors = line.geometry.attributes.color.array;
const c = new THREE.Color();
for (let i = 0; i < line.count; i++) {
c.set(cscale1(i / line.count).hex());
c.toArray(colors, (i * 2) * 3);
c.toArray(colors, (i * 2 + 1) * 3);
}
line.geometry.attributes.color.needsUpdate = true;
});
}
function animate() {
verlet.frame(16);
updatePoints();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
function initListeners() {
if ('ontouchstart' in window) {
document.body.addEventListener('touchstart', updateMouse, false);
document.body.addEventListener('touchmove', move, false);
} else {
document.body.addEventListener('mouseenter', updateMouse, false);
document.body.addEventListener('mousemove', move, false);
document.body.addEventListener('mouseup', updateColors, false);
}
}
function move(e) {
updateMouse(e);
const v1 = new THREE.Vector2(), v2 = new THREE.Vector2();
polylines.forEach(line => {
for (let i = 0; i < line.points.length; i++) {
const p = line.segment.particles[i].pos;
const l = v1.copy(oldMouse).sub(v2.set(p.x, p.y)).length();
if (l < conf.mouseRadius) {
v1.copy(mouse).sub(oldMouse).multiplyScalar(conf.mouseStrength);
p.x += v1.x; p.y += v1.y;
}
}
});
}
function updateMouse(e) {
if (e.changedTouches && e.changedTouches.length) {
e.x = e.changedTouches[0].pageX;
e.y = e.changedTouches[0].pageY;
}
if (e.x === undefined) {
e.x = e.pageX;
e.y = e.pageY;
}
oldMouse.copy(mouse);
mouse.set(
(e.x - width / 2) * verlet.width / width,
(height / 2 - e.y) * verlet.height / height
);
}
function updateSize() {
width = window.innerWidth;
height = window.innerHeight;
uCx.value = 2 / verlet.width; uCy.value = 2 / verlet.height;
renderer.setSize(width, height);
// camera.aspect = width / height;
// camera.updateProjectionMatrix();
}
}
// adapted from https://github.com/oframe/ogl/blob/master/src/extras/Polyline.js
const Polyline = (function () {
const tmp = new THREE.Vector3();
class Polyline {
constructor(params) {
const { points, color1, color2 } = params;
this.points = points;
this.count = points.length;
this.color1 = color1;
this.color2 = color2;
this.init();
this.updateGeometry();
}
init() {
// const cscale = chroma.scale([chroma.random(), chroma.random()]);
const cscale = chroma.scale([this.color1, this.color2]);
this.geometry = new THREE.BufferGeometry();
this.position = new Float32Array(this.count * 3 * 2);
this.prev = new Float32Array(this.count * 3 * 2);
this.next = new Float32Array(this.count * 3 * 2);
const side = new Float32Array(this.count * 1 * 2);
const uv = new Float32Array(this.count * 2 * 2);
const color = new Float32Array(this.count * 3 * 2);
const index = new Uint16Array((this.count - 1) * 3 * 2);
const c = new THREE.Color();
for (let i = 0; i < this.count; i++) {
const i2 = i * 2;
side.set([-1, 1], i2);
const v = i / (this.count - 1);
uv.set([0, v, 1, v], i * 4);
c.set(cscale(v).hex());
c.toArray(color, i2 * 3);
c.toArray(color, (i2 + 1) * 3);
if (i === this.count - 1) continue;
index.set([i2 + 0, i2 + 1, i2 + 2], (i2 + 0) * 3);
index.set([i2 + 2, i2 + 1, i2 + 3], (i2 + 1) * 3);
}
this.geometry.setAttribute('position', new THREE.BufferAttribute(this.position, 3));
this.geometry.setAttribute('color', new THREE.BufferAttribute(color, 3));
this.geometry.setAttribute('prev', new THREE.BufferAttribute(this.prev, 3));
this.geometry.setAttribute('next', new THREE.BufferAttribute(this.next, 3));
this.geometry.setAttribute('side', new THREE.BufferAttribute(side, 1));
this.geometry.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
this.geometry.setIndex(new THREE.BufferAttribute(index, 1));
}
updateGeometry() {
this.points.forEach((p, i) => {
p.toArray(this.position, i * 3 * 2);
p.toArray(this.position, i * 3 * 2 + 3);
if (!i) {
tmp.copy(p).sub(this.points[i + 1]).add(p);
tmp.toArray(this.prev, i * 3 * 2);
tmp.toArray(this.prev, i * 3 * 2 + 3);
} else {
p.toArray(this.next, (i - 1) * 3 * 2);
p.toArray(this.next, (i - 1) * 3 * 2 + 3);
}
if (i === this.points.length - 1) {
tmp.copy(p).sub(this.points[i - 1]).add(p);
tmp.toArray(this.next, i * 3 * 2);
tmp.toArray(this.next, i * 3 * 2 + 3);
} else {
p.toArray(this.prev, (i + 1) * 3 * 2);
p.toArray(this.prev, (i + 1) * 3 * 2 + 3);
}
});
this.geometry.attributes.position.needsUpdate = true;
this.geometry.attributes.prev.needsUpdate = true;
this.geometry.attributes.next.needsUpdate = true;
}
}
return Polyline;
})();
App();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.0.3/chroma.min.js"></script>
<script src="https://klevron.github.io/codepen/lib/verlet-1.0.0.min.js"></script>
body {
margin: 0;
height: 100%;
font-family: 'Montserrat', sans-serif;
background-image: radial-gradient(circle, #ffffff, #aaaaaa);
}
canvas {
position: fixed;
top: 0;
bottom: 0;
z-index: 1;
}
h1 {
font-size: 50px;
text-align: center;
width: 60%;
margin: 0 auto;
padding-top: 10%;
}
p {
font-size: 30px;
text-align: center;
width: 50%;
margin: 1em auto 0;
}
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet" />
@CodeMyUI
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment