Public
Edited
Apr 6, 2024
9 stars
Insert cell
Insert cell
Insert cell
Insert cell
{
// Canvas
const canvas = document.querySelector("canvas.webgl");

const parameters = {};
parameters.width = width;
parameters.height = 1000;
parameters.count = 200000;
parameters.size = 0.005;
parameters.radius = 5;
parameters.branches = 3;
parameters.spin = 1;
parameters.randomness = 0.2;
parameters.randomnessPower = 3;
parameters.insideColor = "#ff6030";
parameters.outsideColor = "#1b3984";

const scene = new THREE.Scene();

const renderer = new THREE.WebGLRenderer({
canvas: canvas
});
renderer.setSize(parameters.width, parameters.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

let geometry = null;
let material = null;
let points = null;

const generateGalaxy = () => {
if (points !== null) {
geometry.dispose();
material.dispose();
scene.remove(points);
}

geometry = new THREE.BufferGeometry();

const positions = new Float32Array(parameters.count * 3);
const randomness = new Float32Array(parameters.count * 3);
const colors = new Float32Array(parameters.count * 3);
const scales = new Float32Array(parameters.count * 1);

const insideColor = new THREE.Color(parameters.insideColor);
const outsideColor = new THREE.Color(parameters.outsideColor);

for (let i = 0; i < parameters.count; i++) {
const i3 = i * 3;

// Position
const radius = Math.random() * parameters.radius;

const branchAngle =
((i % parameters.branches) / parameters.branches) * Math.PI * 2;

const gRandom = () => {
return (
Math.pow(Math.random(), parameters.randomnessPower) *
(Math.random() < 0.5 ? 1 : -1) *
parameters.randomness *
radius
);
};
const randomX = gRandom();
const randomY = gRandom();
const randomZ = gRandom();

positions[i3] = Math.cos(branchAngle) * radius;
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(branchAngle) * radius;

randomness[i3] = randomX;
randomness[i3 + 1] = randomY;
randomness[i3 + 2] = randomZ;

// Color
const mixedColor = insideColor.clone();
mixedColor.lerp(outsideColor, radius / parameters.radius);

colors[i3] = mixedColor.r;
colors[i3 + 1] = mixedColor.g;
colors[i3 + 2] = mixedColor.b;

// Scale
scales[i] = Math.random();
}

geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute(
"aRandomness",
new THREE.BufferAttribute(randomness, 3)
);
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
geometry.setAttribute("aScale", new THREE.BufferAttribute(scales, 1));

material = new THREE.ShaderMaterial({
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
uniforms: {
uTime: { value: 0 },
uSize: { value: 30 * renderer.getPixelRatio() }
},
vertexShader: vertexShader,
fragmentShader: fragmentShader
});

points = new THREE.Points(geometry, material);
scene.add(points);
};

// Base camera
const camera = new THREE.PerspectiveCamera(
75,
parameters.width / parameters.height,
0.1,
100
);
camera.position.x = 3;
camera.position.y = 3;
camera.position.z = 3;
scene.add(camera);

// Controls
const controls = new THREE.OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.autoRotate = true;

/**
* Generate the first galaxy
*/
generateGalaxy();

/**
* Animate
*/
const clock = new THREE.Clock();

const tick = () => {
const elapsedTime = clock.getElapsedTime();

// Update material
material.uniforms.uTime.value = elapsedTime;

// Update controls
controls.update();

// Render
renderer.render(scene, camera);

// Call tick again on the next frame
window.requestAnimationFrame(tick);
};

tick();
return html``;
}
Insert cell
vertexShader = `
uniform float uTime;
uniform float uSize;

attribute vec3 aRandomness;
attribute float aScale;

varying vec3 vColor;

void main()
{
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
// Rotate
float angle = atan(modelPosition.x, modelPosition.z);
float distanceToCenter = length(modelPosition.xz);
float angleOffset = (1.0 / distanceToCenter) * uTime;
angle += angleOffset;
modelPosition.x = cos(angle) * distanceToCenter;
modelPosition.z = sin(angle) * distanceToCenter;

// Randomness
modelPosition.xyz += aRandomness;

vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;

// size
gl_PointSize = uSize * aScale;
gl_PointSize *= (1.0 / - viewPosition.z);

vColor = color;
}
`
Insert cell
fragmentShader = `
varying vec3 vColor;

void main()
{
// // Disc
// float strength = distance(gl_PointCoord, vec2(0.5));
// strength = step(0.5, strength);
// strength = 1.0 - strength;

// // Diffuse point
// float strength = distance(gl_PointCoord, vec2(0.5));
// strength *= 2.0;
// strength = 1.0 - strength;

// Light point
float strength = distance(gl_PointCoord, vec2(0.5));
strength = 1.0 - strength;
strength = pow(strength, 10.0);

// Final color
vec3 color = mix(vec3(0.0), vColor, strength);
gl_FragColor = vec4(color, 1.0);

}
`
Insert cell
THREE = {
const THREE = (window.THREE =
await require(`three@${T_VERSION}/build/three.min.js`));
await require(`three@${T_VERSION}/examples/js/controls/OrbitControls.js`).catch(
() => {}
);
return THREE;
}
Insert cell
T_VERSION = "0.146.0"
Insert cell
//
// Adapted from https://observablehq.com/@mourner/canvas-recorder
// Claudio's version
//
function canvasRecord(canvas, options = {}) {
if (canvas.__recordInterface) {
// maintain state for canvases updated with the generator pattern
return canvas.__recordInterface;
}
console.log("redefined");
let {
type = "webm",
maxchunks = 600,
timeslice = 100,
fps = 60,
generator = false
} = options;
let chunks = [];

let MediaRecorder = window.MediaRecorder;
if (!MediaRecorder || !MediaRecorder.isTypeSupported(`video/${type}`)) {
return html`<span style="color:red">Your browser doesn't support MediaRecorder/${type}.</span>`;
}

const downloadButton = htl.html`<button>download`;
const startPauseButton = htl.html`<button>record`;
const stopButton = htl.html`<button>stop`;
const message = htl.html`<span>${status}`;
const recordInterface = htl.html`${startPauseButton}${stopButton}${downloadButton}<br>${message}`;

let recorder = new MediaRecorder(canvas.captureStream(fps), {
mimeType: `video/${type}`
});
recordInterface._recorder = recorder;

const setButtonState = () => {
downloadButton.disabled = chunks.length == 0;
stopButton.disabled = recorder.state == "inactive";
};

setButtonState();

const stopRecording = () => {
if (recorder.state == "paused" || recorder.state == "recording") {
console.log("Stopping");
recorder.stop();
}
startPauseButton.innerHTML = "record";
message.innerHTML = `${recorder.state} chunks:${chunks.length}`;
setButtonState();
};

stopButton.onclick = stopRecording;

downloadButton.onclick = () => {
if (chunks.length == 0) {
message.innerHTML = `No recording to save`;
setButtonState();
return;
}
stopRecording();
var blob = new Blob(chunks, { type: `video/${type}` });
var link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = `canvas.${type}`;
link.click();
message.innerHTML = "file saved";
setButtonState();
};

startPauseButton.onclick = () => {
if (recorder.state == "inactive") {
chunks = [];
recorder.ondataavailable = (e) => {
chunks.push(e.data);
message.innerHTML = "recorded chunk " + chunks.length;
if (chunks.length > maxchunks) stopRecording();
};
recorder.start(timeslice);
startPauseButton.innerHTML = "pause";
} else if (recorder.state == "paused") {
recorder.resume();
startPauseButton.innerHTML = "pause";
} else if (recorder.state == "recording") {
recorder.pause();
startPauseButton.innerHTML = "resume";
}
message.innerHTML = `${recorder.state} chunks:${chunks.length}`;
setButtonState();
};
if (generator) canvas.__recordInterface = recordInterface;
return recordInterface;
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more