Public
Edited
Jun 8, 2023
3 forks
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
fields = new Map([
["left to right", (x, y) => [x, 0]], // Horizontal flow from left to right.
["diagonal", (x, y) => [x, x]], // Diagonal flow from top-left to bottom-right.
["circle", (x, y) => [y - 0.5, 0.5 - x]], // Circular flow.
["repel", (x, y) => [x - 0.5, y - 0.5]], // Repelling flow from the center.
])
Insert cell
Insert cell
exampleVector = fields.get("left to right")(0.5, 0.5) // Sample the field.
Insert cell
Insert cell
Insert cell
Insert cell
function dirToColor(dir) {
// Normalize the vector (value range from -1 to 1).
const normal = getNormalized(dir);
// Map values to be between 0 and 1.
normal[0] = (normal[0] + 1) * 0.5;
normal[1] = (normal[1] + 1) * 0.5;
// Convert to array of color values.
return [ Math.floor(normal[0] * 255), Math.floor(normal[1] * 255), 0, 255 ];
}
Insert cell
mutable animatedVector = [ 0, 0 ]; // Example vector (same as in the animation above).
Insert cell
animatedVectorColor = dirToColor(animatedVector) // Animated vector converted to color.
Insert cell
Insert cell
async function createImage(ctx, width, height, field, alphaScale = 1.0) {
// Create the image data.
let image = ctx.createImageData(width, height);

// Loop through the image pixels.
for (let y = 0, i = 0; y < height; y++) {
// Convert pixel-y to field-y (value between 0 and 1).
const fy = y / (height - 1);
for (let x = 0; x < width; x++, i += 4) {
// Convert pixel-x to field-x (value between 0 and 1).
const fx = x / (width - 1);
// Retrieve the vector from the field for the current pixel.
const dir = field(fx, fy);
// Convert the vector to a color.
const color = dirToColor(dir);

// Assign the color values to the pixel color.
image.data[i] = color[0]; // Red
image.data[i + 1] = color[1]; // Green
image.data[i + 2] = color[2]; // Blue
image.data[i + 3] = color[3] * alphaScale; // Alpha
}
}

// Create and return a bitmap image from the image data.
return createImageBitmap(image);
}
Insert cell
Insert cell
{
// Create a canvas.
const size = Math.min(width, 450);
const ctx = DOM.context2d(size, size);
ctx.canvas.style.border = "solid 1px black";
// Get a field.
const field = fields.get("circle");
// Create and draw the image.
const image = await createImage(ctx, size, size, field);
ctx.drawImage(image, 0, 0, size, size);

// Return the canvas.
return ctx.canvas;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function drawArrow(ctx, cx, cy, dir, size, lineWidth = 1, strokeColor = "#000") {
// Calculate arrow properties.
const mag = getMagnitute(dir);
const normal = getNormalized(dir);
const angle = Math.atan2(dir[1], dir[0]);

// Use the vector magnitude to scale the length of the arrow.
const length = Math.max(1, size * mag);

// Calculate start and end position of arrow.
const x0 = cx - normal[0] * length;
const y0 = cy - normal[1] * length;
const x1 = cx + normal[0] * length;
const y1 = cy + normal[1] * length;

// Set the render properties.
ctx.strokeStyle = strokeColor;
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.beginPath();

// Set the center line of the arrow.
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);

// Set the lines of the arrow head.
ctx.moveTo(x1, y1);
ctx.lineTo(x1 - length * Math.cos(angle - Math.PI / 6), y1 - length * Math.sin(angle - Math.PI / 6));
ctx.moveTo(x1, y1);
ctx.lineTo(x1 - length * Math.cos(angle + Math.PI / 6), y1 - length * Math.sin(angle + Math.PI / 6));

// Draw the arrow.
ctx.stroke();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function createParticle(x, y, size = 3, time = 20, color = "#a6cee3") {
// Return a particle object that stores the particle positions and its properties.
return { x: x, y: y, size: size, color: color, time: time }
}
Insert cell
Insert cell
function createParticles(count = particleProps.count, size = particleProps.size, time = particleProps.lifetime) {
// Create an array of particles with random positions.
let particles = [];
for (let i = 0; i < count; ++i) {
particles.push(createParticle(Math.random(), Math.random(), size, time));
}
return particles;
}
Insert cell
Insert cell
function updateParticle(particle, field, dt = 1, speed = 0.01) {
// Get the particle's x and y coordinate.
let { x, y } = particle;
// Lower the particle's lifetime based on the elapsed time.
particle.time -= dt;
// Check if the particle is inside boundaries and alive.
if (x >= 0 && x <= 1 && y >= 0 && y <= 1 && particle.time > 0) {
// Sample the field at the current particle position.
const dir = field(x, y);
// Move the particle based on the sampled field vector, scaled by the elapsed time and velocity factor.
particle.x = x + dir[0] * dt * speed;
particle.y = y + dir[1] * dt * speed;
} else {
// Reset the particle (outside boundary or beyond lifetime).
resetParticle(particle);
}
}
Insert cell
Insert cell
function resetParticle(particle, time = particleProps.lifetime) {
// Reset the particle position.
particle.x = Math.random();
particle.y = Math.random();
// Reset the particle lifetime.
particle.time = time;
}
Insert cell
Insert cell
function drawParticle(ctx, particle) {
// Get the width and height of the canvas.
const width = ctx.canvas.width / window.devicePixelRatio;
const height = ctx.canvas.height / window.devicePixelRatio;
// Construct a circle to represent the particle.
ctx.beginPath();
ctx.arc(
particle.x * width, // Convert x to canvas coordinates.
particle.y * height, // Convert y to canvas coordinates.
particle.size,
0,
2 * Math.PI
);

// Fill the circle.
ctx.fillStyle = particle.color;
ctx.fill();

// Draw the circle outline.
ctx.strokeStyle = "#333333";
ctx.stroke();
}
Insert cell
Insert cell
{
await visibility();
// Create a canvas.
const size = Math.min(width, 350);
const ctx = DOM.context2d(size, size);
ctx.canvas.style.border = "solid 1px black";

// Get a field and create a background image.
const field = fields.get("repel");
const image = await createImage(ctx, size, size, field);

// Create the particles.
const particles = createParticles(15);
// Animation loop.
let time = performance.now();
while (true) {
// Clear the canvas.
ctx.clearRect(0, 0, size, size);

// Draw the background image.
ctx.drawImage(image, 0, 0, size, size);

// Calculate the time delta.
const now = performance.now();
const dt = (now - time) / 1000;
time = now;

// Update and draw the particles.
particles.forEach((particle) => {
updateParticle(particle, field, dt, 100 / size);
drawParticle(ctx, particle);
});
// Return the canvas.
yield ctx.canvas;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
particles = createParticles(particleProps.count, particleProps.size, particleProps.lifetime)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {flowVis} from "@mroehlig/flow-visualization-of-2d-vector-fields-solution"
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