Public
Edited
Oct 2, 2022
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
simpleAnimationCanvas = {
const width = 1200
const height = 300
const c = DOM.context2d(width, height);


/////////////////////////////////////
//////////// Create data ////////////
/////////////////////////////////////
const dataStart = _.range(0, 200).map(i => ({
id: i,
x: Math.random()*width,
y: Math.random()*height,
r: Math.random()*30,
fill: `rgb(${Math.round(Math.random()*255)}, ${Math.round(Math.random()*255)}, ${Math.round(Math.random()*255)})`
}))
const dataEnd = _.range(0, 200).map(i => ({
id: i,
x: Math.random()*width,
y: Math.random()*height,
r: Math.random()*20,
fill: `rgb(${Math.round(Math.random()*255)}, ${Math.round(Math.random()*255)}, ${Math.round(Math.random()*255)})`
}))

/////////////////////////////////////
/////////// Draw function ///////////
/////////////////////////////////////
const draw = (data) => {
c.fillStyle = "black"
c.fillRect(0, 0, width, height); // Make sure to clear or fill the rect
for (const { x, y, r, fill } of data) {
c.beginPath();
c.arc(x, y, r, 0, 2*Math.PI)
c.fillStyle = fill
c.fill()
}
}

/////////////////////////////////////
///////////// Animation /////////////
/////////////////////////////////////
const tl = gsap.to(dataStart, {
x: (index) => dataEnd[index].x,
y: (index) => dataEnd[index].y,
r: (index) => dataEnd[index].r,
fill: (index) => dataEnd[index].fill,
duration: 3,
//stagger: 0.01,
ease: 'bounce',
onUpdate: () => draw(dataStart)
})

tl.pause()

/////////////////////////////////
///// Control the animation /////
/////////////////////////////////
const play = document.querySelector("#play-test");
const pause = document.querySelector("#pause-test");
const restart = document.querySelector("#restart-test");
const reverse = document.querySelector("#reverse-test");
play.onclick = () => tl.play();
restart.onclick = () => tl.restart()
pause.onclick = () => tl.pause();
reverse.onclick = () => tl.reverse();

draw(dataStart)

yield c.canvas;
}
Insert cell
Insert cell
dataStart = dataCirclePackingByRed // Data we will animate from
Insert cell
dataEnd = dataSequentialGrid // Data will animate to
Insert cell
viewof animationDuration = Inputs.range([1, 10], {value: 2, step: 1, label: "Animation duration"})
Insert cell
Insert cell
{
const c = DOM.context2d(width, height);
c.translate(width/2, height/2);

/////////////////////////////////////
/////////// Draw function ///////////
/////////////////////////////////////
const draw = (data) => {
c.fillStyle = "black"
c.fillRect(-width / 2, -width / 2, width, height); // Make sure to clear or fill the rect
for (const { x, y, r, fill } of data) {
c.beginPath();
c.arc(x, y, r, 0, 2*Math.PI)
c.fillStyle = fill
c.fill()
}
}

/////////////////////////////////////
///////////// Animation /////////////
/////////////////////////////////////
// Animate the x,y pos and the fill s.t. x and y are moved to the position
// of the moved circles, which we can access via the index.
const tl = gsap.to(dataStart, {
x: (index) => dataEnd[index].x,
y: (index) => dataEnd[index].y,
duration: animationDuration,
//stagger: 0.1,
stagger: { amount: 2 },
ease: 'linear', //elastic
onUpdate: () => draw(dataStart)
});
tl.pause()


/////////////////////////////////
///// Control the animation /////
/////////////////////////////////
const play = document.querySelector("#play");
const pause = document.querySelector("#pause");
const restart = document.querySelector("#restart");
const reverse = document.querySelector("#reverse");
play.onclick = () => tl.play();
restart.onclick = () => tl.restart()
pause.onclick = () => tl.pause();
reverse.onclick = () => tl.reverse();

draw(dataStart)
yield c.canvas;

}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function getDataSequentialGrid(dataIn) {
const numElements = dataIn.length
const cellSize = radius*2
const numCells = numElements

// If you want the width to be controlled by the width of the container
// but the height is as much as needed to fit the data.
const numCols = Math.ceil(width / cellSize)
const numRows = Math.ceil(numCells / numCols)
const height = numRows * cellSize
// If you want the height to be the height of the container, comment out the line above.

// Use d3 band scales to position the elements in rows and cols
const xScale = d3.scaleBand()
.domain(_.range(numCols))
.range([0, width])
const yScale = d3.scaleBand()
.domain(_.range(numRows))
.range([0, height])

// Create an array of just the x and y positions where each etry will go
const cells = []
let count = 0
for (let i = 0; i < numRows; i++) {
for (let j = 0; j < numCols; j++) {
if (count <= numCells) {
const entry = {
id: count,
x: xScale(j) - width / 2,
y: yScale(i) - height / 2,
}
cells.push(entry)
}
count += 1
}
}

// Merge the array of x and y positions for each entry with the data containing the
// rest of the information, i.e. the colours
const dataSequentialGrid = []
dataIn.forEach((d, i) => {
const entry = {
name: d.name,
fill: d.fill,
red: d.red,
green: d.green,
blue: d.blue,
r: d.r,
x: cells[i].x,
y: cells[i].y
}
dataSequentialGrid.push(entry)
})

return dataSequentialGrid
}
Insert cell
Insert cell
Insert cell
function getCirclePacking(dataIn) {
const dataClone = _.cloneDeep(dataIn)
const circlePacking = d3.packSiblings(dataClone)
return circlePacking
}
Insert cell
Insert cell
function sortByCol(dataIn, col) {
// value of col has to be 'red', 'green' or 'blue'
const dataClone = _.cloneDeep(dataIn)
const dataSorted = _.sortBy(dataClone, col)
return dataSorted
}
Insert cell
Insert cell
dataTidy = tidyData(dataSample)
Insert cell
function tidyData(dataIn) {
const data = []
dataIn.forEach(frame => {
frame.colours.forEach(colour => {
data.push({
name: frame.img,
fill: colour,
red: +colour.split(', ')[0].split('(')[1],
green: +colour.split(', ')[1],
blue: +colour.split(', ')[2].split(')')[0],
r: radius
})
})
})
return data
}
Insert cell
Insert cell
height = width
Insert cell
width = 900
Insert cell
radius = 2.5 // How big the circle for each colour-entry will be
Insert cell
Insert cell
dataSample = data.filter((d, i) => i%5 === 0) // Take every 5th frame; you can change this to experiment.
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

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