Published
Edited
Jun 4, 2021
3 forks
27 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const w = width
const h = 640
const scene = new THREE.Scene()
scene.fog = new THREE.FogExp2(0x1f1f1f, 0.01)
scene.background = new THREE.Color(0x1f1f1f)

const SIDES = 20
const SEGMENTS = 200
const geometry = new THREE.BufferGeometry()
const indices = []
const vertices = new Float32Array(SIDES * SEGMENTS * 3)
let ptr = 0
for (let segment = 0; segment < SEGMENTS; segment++) {
for (let side = 0; side < SIDES; side++) {
vertices[ptr] = segment
vertices[ptr + 1] = side
vertices[ptr + 2] = 0
ptr += 3
}
}
const MAX = SEGMENTS * SIDES
for (let segment = 0; segment < SEGMENTS + 1; segment++) {
for (let f = 0; f < SIDES; f++) {
const a = (segment * SIDES + ((f + 1) % SIDES)) % MAX
const b = (segment * SIDES + f) % MAX
const c = (segment * SIDES + f + SIDES) % MAX
const d = (segment * SIDES + ((f + 1) % SIDES) + SIDES) % MAX
indices.push(a, b, d)
indices.push(b, c, d)
}
}
geometry.setIndex(indices)
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3))
const loader = new THREE.TextureLoader()
const matcap1 = loader.load(matcapUrl)
const matcap2 = loader.load('https://makio135.com/matcaps/512/736655_D9D8D5_2F281F_B1AEAB-512px.png')
const matcap3 = loader.load('https://makio135.com/matcaps/512/181F1F_475057_616566_525C62-512px.png')
const g = new THREE.Group()
scene.add(g)
const TAU = 2 * Math.PI
const meshes = []
const N = 9
for (let i = 0; i < N; i++) {
const geoMat = new THREE.RawShaderMaterial({
uniforms: {
SEGMENTS: { value: SEGMENTS },
SIDES: { value: SIDES },
matCapMap: {
value: i % 3 === 0 ? matcap1 : i % 3 === 1 ? matcap2 : matcap3,
},
time: { value: 0 },
index: { value: i },
},
vertexShader: vertexShader(),
fragmentShader: fragmentShader(),
})
const angle = (i * TAU) / N
const t = new THREE.Mesh(geometry, geoMat)
g.add(t)
meshes.push(t)
}
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(w, h)
renderer.setPixelRatio(devicePixelRatio)

const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 150)
camera.position.set(0, 5, 40).multiplyScalar(1.05)
camera.lookAt(new THREE.Vector3(0, 0, 10))

const controls = new THREE.OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.maxDistance = 50
invalidation.then(() => (controls.dispose(), renderer.dispose()))
controls.addEventListener("change", () => renderer.render(scene, camera))

while (true) {
meshes.forEach(m => m.material.uniforms.time.value = 0.0005 * performance.now())
controls.update()
renderer.render(scene, camera)
renderer.domElement.style.cursor = 'pointer'
yield renderer.domElement
}
}
Insert cell
vertexShader = () => `#version 300 es
precision highp float;

in vec3 position;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform mat3 normalMatrix;
uniform float SEGMENTS;
uniform float SIDES;
uniform float time;
uniform float index;

const float PI = 3.1415926535897932384626433832795;
const float TAU = 2. * PI;

out vec3 pos;
out vec3 normal;

vec4 quat(vec3 axis, float angle) {
float halfAngle = angle / 2.;
float s = sin( halfAngle );

vec4 q = vec4(axis.x * s, axis.y * s, axis.z * s, cos( halfAngle ));
return q;
}

vec3 applyQuat( vec4 q, vec3 v ){
return v + 2.0*cross(cross(v, q.xyz ) + q.w*v, q.xyz);
}

vec3 getBasePoint(float alpha) {
float r = 17.;
vec3 p = vec3(r * cos(alpha), 0., r * sin(alpha));
vec3 dir = vec3(cos(alpha+PI/2.), 0., sin(alpha+PI/2.));

float a = 2.*alpha;
a += index/9. * TAU;
p += applyQuat(quat(dir, a), vec3(0., 6., 0.));
p.y += sin(alpha * 3.) * 4.;
return p;
}

float parabola( float x, float k ) {
return pow( 4. * x * ( 1. - x ), k );
}

void main() {
float alpha = TAU * position.x / SEGMENTS;
vec3 base = getBasePoint(alpha);
vec3 prevBase = getBasePoint(alpha - TAU / SEGMENTS);
vec3 dir = normalize(base - prevBase);

float beta = TAU * position.y / SIDES;
float animStep = mod(3.*position.x + time, SEGMENTS) / SEGMENTS;
float tubeRadius = max(0., pow(sin(alpha * 3. + (1. - time) * TAU) + 1.2, 2.)) * 0.3;

vec3 tubeDir = tubeRadius * vec3(0., 1., 0.);
tubeDir = applyQuat(quat(dir, beta), tubeDir);
vec3 newPosition = base + tubeDir;

normal = normalMatrix * normalize(tubeDir);

vec4 mvp = modelViewMatrix * vec4(newPosition, 1.);
pos = mvp.xyz;
gl_Position = projectionMatrix * mvp;
}
`
Insert cell
fragmentShader = () => `#version 300 es
precision highp float;

in vec3 pos;
in vec3 normal;

out vec4 color;

uniform sampler2D matCapMap;

void main() {
vec3 n = normalize(normal);
vec3 eye = normalize(pos.xyz);
vec3 r = reflect( eye, normal );
float m = 2. * sqrt( pow( r.x, 2. ) + pow( r.y, 2. ) + pow( r.z + 1., 2. ) );
vec2 vN = r.xy / m + .5;

vec3 mat = texture(matCapMap, vN).rgb;

color = vec4(mat, 1.);
}
`
Insert cell
matcapUrl = FileAttachment("cab84392339bf82df55117a55f56e89ffe33db30dae3b36abafa85187c077b9833a1e96dd2620574b14b18e1032190ea9e2f2d2ed3d51c57e366d68a8cdfe78f.jpeg").url()
Insert cell
Insert cell
THREE = {
const THREE = window.THREE = await require("three/build/three.min.js");
await require("three/examples/js/controls/OrbitControls.js").catch(() => {});
return THREE;
}
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more