Public
Edited
Apr 16
Insert cell
Insert cell
Insert cell
width=960
Insert cell
height = 600
Insert cell
dotColor = "gray" // You can change this to any color you prefer

Insert cell
initialDots = d3.range(2000).map(() => ({
x: Math.random() * width,
y: Math.random() * height,
originalRadius: 1.2 + Math.random() * 0.3 // Slightly smaller radius for better glow effect
}))


Insert cell
hifive_inverted = FileAttachment("hiFive_inverted.svg").image()
Insert cell
imageCheck = {
const img = await hifive_inverted;
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
return canvas;
}
Insert cell
positions = {
const img = await hifive_inverted;
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
// Center the image
const scale = Math.min(width / img.width, height / img.height);
const x = (width - img.width * scale) / 2;
const y = (height - img.height * scale) / 2;
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
const imageData = ctx.getImageData(0, 0, width, height).data;

const endPositions = [];
const threshold = 200; // Threshold for white detection
// Sample the image to find white dots
for (let y = 0; y < height; y += 3) {
for (let x = 0; x < width; x += 3) {
const i = (y * width + x) * 4;
const brightness = (imageData[i] + imageData[i + 1] + imageData[i + 2]) / 3;
if (brightness > threshold) {
endPositions.push({
x,
y,
originalRadius: 1.2
});
}
}
}

console.log("Detected white dots:", endPositions.length);

// Ensure we have enough end positions
while (endPositions.length < initialDots.length) {
const index = Math.floor(Math.random() * endPositions.length);
endPositions.push(endPositions[index]);
}

// If we have too many, trim the list
if (endPositions.length > initialDots.length) {
endPositions.length = initialDots.length;
}

return {
start: initialDots,
end: endPositions
};
}


Insert cell
animation = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
canvas.style.background = 'black';
canvas.style.cursor = 'pointer';

function drawDots(dots, color) {
ctx.clearRect(0, 0, width, height);
// First layer: larger, very soft glow
ctx.shadowBlur = 15;
ctx.shadowColor = color;
ctx.fillStyle = color;
ctx.globalAlpha = 0.3;
dots.forEach(d => {
ctx.beginPath();
ctx.arc(d.x, d.y, d.originalRadius * 2.5, 0, Math.PI * 2);
ctx.fill();
});

// Second layer: medium glow
ctx.shadowBlur = 5;
ctx.globalAlpha = 0.3;
dots.forEach(d => {
ctx.beginPath();
ctx.arc(d.x, d.y, d.originalRadius * 1.5, 0, Math.PI * 2);
ctx.fill();
});

// Third layer: core dots
ctx.shadowBlur = 5;
ctx.globalAlpha = 1;
dots.forEach(d => {
ctx.beginPath();
ctx.arc(d.x, d.y, d.originalRadius, 0, Math.PI * 2);
ctx.fill();
});
}

// Initial state with first color (#74b5d6)
drawDots(initialDots, '#74b5d6');

let isTransitioning = false;
let isForward = true; // Track direction of animation

function animateTransition(startPositions, endPositions, startColor, endColor, onComplete) {
const startTime = performance.now();
const duration = 1600; // 1.6 seconds

function animate(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(1, elapsed / duration);

// Add easing for smoother transition
const easedProgress = d3.easeCubicInOut(progress);

const currentDots = startPositions.map((d, i) => ({
x: d.x + (endPositions[i].x - d.x) * easedProgress,
y: d.y + (endPositions[i].y - d.y) * easedProgress,
originalRadius: d.originalRadius
}));

// Color transition
const color = d3.interpolateRgb(startColor, endColor)(easedProgress);
drawDots(currentDots, color);

if (progress < 1) {
requestAnimationFrame(animate);
} else {
onComplete();
}
}

requestAnimationFrame(animate);
}

canvas.onclick = () => {
if (!isTransitioning) {
isTransitioning = true;

if (isForward) {
// Forward animation: scattered to hiFive
animateTransition(
initialDots,
positions.end,
'#74b5d6',
'#beffff',
() => {
isForward = false;
// Wait a moment before reversing
setTimeout(() => {
// Reverse animation: hiFive to scattered
animateTransition(
positions.end,
initialDots,
'#beffff',
'#74b5d6',
() => {
isTransitioning = false;
isForward = true;
}
);
}, 500); // 500ms pause before reversing
}
);
}
}
};

canvas.ontouchstart = (event) => {
event.preventDefault();
canvas.onclick();
};

return canvas;
}



Insert cell
animation
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