Public
Edited
Feb 2, 2019
Insert cell
Insert cell
Insert cell
Insert cell
setup = (scale) => {
const width = scale*400;
const height = scale*400;
// initialize constants
const cellX = width/2;
const cellY = width/2;
const cellRadius = scale*100;
const mutationRadius = scale*8;
const numMutations = scale*40;
const nucleusX = cellX;
const nucleusY = cellY-(scale*15);
const nucleusRadius = 4*cellRadius/5;
const startColor = [214, 239, 251];
const endColor = [187, 161, 140];
// initialize mutation positions
let mutationPositions = []
for(let i = 0; i < numMutations; i++) {
mutationPositions.push(getMut(nucleusX, nucleusY, nucleusRadius, mutationRadius));
}

return {
scale,
width,
height,
cellX,
cellY,
cellRadius,
mutationRadius,
numMutations,
nucleusX,
nucleusY,
nucleusRadius,
mutationPositions,
startColor,
endColor
};
}
Insert cell
Insert cell
/**
* @param {context} ctx The canvas context2d.
* @param {int} t The animation tick. Between 0 and 99.
* @param {object} The inital configuration of the animation upon setup.
*/
draw = (ctx, t, config) => {
let scale = config.scale;
let width = config.width;
let height = config.height;
let cellX = config.cellX;
let cellY = config.cellY;
let cellRadius = config.cellRadius;
let mutationRadius = config.mutationRadius;
let numMutations = config.numMutations;
let nucleusX = config.nucleusX;
let nucleusY = config.nucleusY;
let nucleusRadius = config.nucleusRadius;
let startColor = config.startColor;
let endColor = config.endColor;
// derived constants
let cellX0 = cellX - cellRadius;
let cellX1 = cellX + cellRadius;
let cellY0 = cellY - cellRadius;
let cellY1 = cellY + cellRadius;
let c1offsetX = scale*30; // left control point X offset
let c2offsetX = scale*-30; // right control point X offset
// fill background
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
if(t === 0) {
// Move registration point to the center of the canvas
ctx.translate(width/2, height/2);
// Rotate
ctx.rotate(20 * Math.PI / 180);
// Move registration point back to the top left corner of canvas
ctx.translate(-width/2, -height/2);
}
if(t > 50) {
// Turn transparency on
ctx.globalAlpha = 1.0 - (Math.min(t, 60) - 50) / 10;
}
// bottom arc
ctx.strokeStyle = "#000";
let currColor = getColor(startColor, endColor, Math.min(t*2, 99));
ctx.fillStyle = "rgb(" + currColor[0] + "," + currColor[1] + "," + currColor[2] + ")";
ctx.beginPath();
ctx.moveTo(cellX0, cellY);
ctx.bezierCurveTo(cellX0+c1offsetX, cellY+scale*190, cellX1+c2offsetX, cellY+scale*190, cellX1, cellY);
ctx.fill();
ctx.stroke();
// top arc
ctx.beginPath();
ctx.moveTo(cellX0, cellY);
ctx.bezierCurveTo(cellX0-scale*10, cellY0-scale*50, cellX1+scale*10, cellY0-scale*50, cellX1, cellY);
ctx.fill();
ctx.stroke();
// nucleus
ctx.strokeStyle = "#000";
ctx.fillStyle = "#FFF";
ctx.moveTo(cellX, cellY);
ctx.beginPath();
ctx.arc(nucleusX, nucleusY, nucleusRadius, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
// other organelles
// stack of 3 horizontal ellipses on left
ctx.beginPath();
ctx.ellipse(cellX-scale*25, cellY+scale*72, scale*36, scale*7, Math.PI*0.05, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.ellipse(cellX-scale*25, cellY+scale*(72+14+1), scale*36, scale*7, Math.PI*0.05, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.ellipse(cellX-scale*25, cellY+scale*(72+14+1+14+1), scale*36, scale*7, Math.PI*0.05, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
// double-stroked vertical ellipse on right
ctx.beginPath();
ctx.ellipse(cellX+scale*40, cellY+scale*90, scale*20, scale*30, Math.PI*0.15, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.ellipse(cellX+scale*40, cellY+scale*90, scale*14, scale*24, Math.PI*0.15, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
ctx.globalAlpha = 1.0;
// mutations
if(t <= numMutations) {
for(let i = 0; i < Math.min(t, numMutations); i++) {
ctx.fillStyle = mutationColors[config.mutationPositions[i].mutColorIndex];
ctx.beginPath();
ctx.arc(config.mutationPositions[i].mutX, config.mutationPositions[i].mutY, mutationRadius, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
}
} else if(t <= 45) {
for(let i = 0; i < numMutations; i++) {
ctx.fillStyle = mutationColors[config.mutationPositions[i].mutColorIndex];
ctx.beginPath();
ctx.arc(config.mutationPositions[i].mutX - scale*Math.random()*(t-45), config.mutationPositions[i].mutY - scale*Math.random()*(t-45), mutationRadius, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
}
} else if (t <= 60){
ctx.globalAlpha = 1.0 - (t - 60) / (80-60);
for(let i = 0; i < numMutations; i++) {
ctx.fillStyle = mutationColors[config.mutationPositions[i].mutColorIndex];
ctx.beginPath();
let mutX = config.mutationPositions[i].mutX + scale*6*(t-40) * Math.cos(config.mutationPositions[i].mutTheta);
let mutY = config.mutationPositions[i].mutY + scale*6*(t-40) * Math.sin(config.mutationPositions[i].mutTheta);
ctx.arc(mutX, mutY, mutationRadius, 0, Math.PI*2);
ctx.fill();
ctx.stroke();
}
} else {
ctx.translate(width/2, height/2);
// Rotate
ctx.rotate(-20 * Math.PI / 180);
// Move registration point back to the top left corner of canvas
ctx.translate(-width/2, -height/2);
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
}
return ctx;
}
Insert cell
getMut = (nucleusX, nucleusY, nucleusRadius, mutationRadius) => {
let r = (nucleusRadius-2*mutationRadius) * Math.sqrt(Math.random());
let mutTheta = Math.random() * 2 * Math.PI;
let mutX = nucleusX + r * Math.cos(mutTheta);
let mutY = nucleusY + r * Math.sin(mutTheta);
let mutColorIndex = Math.floor(Math.random()*6);
return { mutX, mutY, mutColorIndex, mutTheta };
};
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