let prevX = 0, prevY = 0;
let mouseXSmoothed = 0, mouseYSmoothed = 0;
let radiusSmoothed = 0, offsetYSmoothed = 0, offsetXSmoothed = 0, tailRadiusSmoothed = 0, layerFactorSmoothed = 0;
let rotationA, rotationB, eyeDrawn;
createCanvas(windowWidth, windowHeight);
colorMode(HSB, 360, 255, 255);
buffer = createGraphics(windowWidth, windowHeight);
for (let i = numLayers - 1; i >= 0; i--) {
layers[i] = new Layer(i);
for (let i = numLayers - 1; i >= 0; i--) {
for (let i = 0; i < numLayers; i++) {
layers[i].drawLayer(buffer);
function drawGradient() {
let topColor = color(230, 255, 20);
let bottomColor = color(230, 255, 60);
for (let y = 0; y < height; y++) {
let inter = map(y, 0, height, 0, 1);
let c = lerpColor(topColor, bottomColor, inter);
constructor(layerIndex) {
this.layerFactor = 1 - layerIndex / numLayers;
this.bodyRadius = (30 * pow(this.layerFactor, 3) - 58.5 * sq(this.layerFactor) + 28 * this.layerFactor + 0.92) * bodySize / 2;
this.offsetY = (10 * pow(this.layerFactor, 3) - 10 * sq(this.layerFactor)) * bodySize;
this.backColor = color(whaleHue, 180, 220 - 30 * this.layerFactor);
this.bellyColor = color(whaleHue, 100, 250 - 30 * this.layerFactor);
this.isHead = (layerIndex == numLayers - 1);
if (this.layerFactor > 0.775) {
this.offsetX = (22 * sq(this.layerFactor) - 30 * this.layerFactor + 10) * bodySize;
this.tailRadius = (-86.7 * sq(this.layerFactor) + 153.2 * this.layerFactor - 66) * bodySize / 2;
this.bodyRadiusSmoothed = radiusSmoothed;
this.offsetYSmoothed = offsetYSmoothed;
this.offsetXSmoothed = offsetXSmoothed;
this.tailRadiusSmoothed = tailRadiusSmoothed;
this.layerFactorSmoothed = layerFactorSmoothed;
radiusSmoothed = this.bodyRadius;
offsetYSmoothed = this.offsetY;
offsetXSmoothed = this.offsetX;
tailRadiusSmoothed = this.tailRadius;
layerFactorSmoothed = this.layerFactor;
prevX = this.currentX || 0;
prevY = this.currentY || 0;
this.currentX = (movementSpeed / numLayers * mouseXSmoothed + this.currentX || 0) / (movementSpeed / numLayers + 1);
this.currentY = (movementSpeed / numLayers * mouseYSmoothed + this.currentY || 0) / (movementSpeed / numLayers + 1);
this.currentX = this.prevX;
this.currentY = this.prevY;
this.prevMouseX = mouseXSmoothed;
this.prevMouseY = mouseYSmoothed;
mouseXSmoothed = this.currentX;
mouseYSmoothed = this.currentY;
if (this.layerFactor <= 0.775) {
buffer.fill(this.backColor);
buffer.ellipse(this.currentX || 0, this.currentY - this.offsetY || 0, this.bodyRadius * 2 || 0, this.bodyRadius * 2 || 0);
buffer.fill(this.bellyColor);
for (let i = PI / 6; i <= 5 * PI / 6; i += PI / 15) {
buffer.vertex(this.currentX + cos(i) * this.bodyRadius, this.currentY - this.offsetY + sin(i) * this.bodyRadius);
buffer.vertex(this.currentX, this.currentY - this.offsetY + this.bodyRadius / 3);
if (this.layerFactor > 0.3 && this.layerFactor <= 0.4) {
rotationA = this.currentX - this.bodyRadius;
rotationB = this.currentY + this.bodyRadius * 0.6;
buffer.ellipse(sqrt(sq(rotationA) + sq(rotationB)) * sin(PI / 4 + atan(rotationA / rotationB)),
sqrt(sq(rotationA) + sq(rotationB)) * cos(PI / 4 + atan(rotationA / rotationB)),
bodySize * (this.layerFactor - 0.3) * 15, bodySize * (this.layerFactor - 0.3) * 40);
rotationA = this.currentX + this.bodyRadius;
buffer.ellipse(sqrt(sq(rotationA) + sq(rotationB)) * sin(PI / 4 + atan(rotationA / rotationB)),
sqrt(sq(rotationA) + sq(rotationB)) * cos(PI / 4 + atan(rotationA / rotationB)),
bodySize * (this.layerFactor - 0.3) * 40, bodySize * (this.layerFactor - 0.3) * 15);
if (this.layerFactor < 0.2) {
for (let i = 1; i >= -1; i -= 2) {
buffer.ellipse(this.currentX + i * this.bodyRadius * 0.9, this.currentY - this.bodyRadius * 0.15, bodySize / 3, bodySize / 3);
buffer.ellipse(this.currentX + i * this.bodyRadius * 0.93, this.currentY - this.bodyRadius * 0.18, bodySize / 10, bodySize / 10);
for (let i = 1; i >= -1; i -= 2) {
buffer.vertex(this.currentX + i * this.offsetX, this.currentY - this.offsetY + this.bodyRadius);
buffer.vertex(this.prevMouseX + i * this.offsetXSmoothed, this.prevMouseY - this.offsetYSmoothed + this.bodyRadiusSmoothed);
buffer.vertex(this.prevMouseX + i * this.offsetXSmoothed, this.prevMouseY - this.offsetYSmoothed - this.bodyRadiusSmoothed);
buffer.vertex(this.currentX + i * this.offsetX, this.currentY - this.offsetY - this.bodyRadius);
buffer.vertex(this.currentX + i * this.offsetX, this.currentY - this.offsetY + this.bodyRadius);
buffer.vertex(this.currentX + i * this.offsetX - this.tailRadius, this.currentY - this.offsetY);
buffer.vertex(this.currentX + i * this.offsetX, this.currentY - this.offsetY - this.bodyRadius);
buffer.vertex(this.currentX + i * this.offsetX + this.tailRadius, this.currentY - this.offsetY);
buffer.vertex(this.currentX + i * this.offsetX - this.tailRadius, this.currentY - this.offsetY);
buffer.vertex(this.prevMouseX + i * this.offsetXSmoothed - this.tailRadiusSmoothed, this.prevMouseY - this.offsetYSmoothed);
buffer.vertex(this.prevMouseX + i * this.offsetXSmoothed + this.tailRadiusSmoothed, this.prevMouseY - this.offsetYSmoothed);
buffer.vertex(this.currentX + i * this.offsetX + this.tailRadius, this.currentY - this.offsetY);
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
buffer = createGraphics(windowWidth, windowHeight);