Cherry Blossom

type BlossomSceneConfig = {
 id: string;
 petalsTypes: Petal[];
 numPetals?: number;
 gravity?: number; //0~1
 windMaxSpeed?: number;
};

interface PetalConfig {
 customClass?: string;
 x?: number;
 y?: number;
 z?: number;
 xSpeedVariation?: number;
 ySpeed?: number;
 rotation?: PetalRotation;
}

type PetalRotation = {
 axis: "X" | "Y" | "Z";
 value: number;
 speed: number;
 x: number;
};

class Petal implements PetalConfig {
 el: HTMLElement;

 constructor(config: PetalConfig) {
  this.customClass = config.customClass || "";
  this.x = config.x || 0;
  this.y = config.y || 0;
  this.z = config.z || 0;
  this.xSpeedVariation = config.xSpeedVariation || 0;
  this.ySpeed = config.ySpeed || 0;
  this.rotation = {
   axis: "X",
   value: 0,
   speed: 0,
   x: 0
  };

  if (config.rotation && typeof config.rotation === "object") {
   this.rotation.axis = config.rotation.axis || this.rotation.axis;
   this.rotation.value = config.rotation.value || this.rotation.value;
   this.rotation.speed = config.rotation.speed || this.rotation.speed;
   this.rotation.x = config.rotation.x || this.rotation.x;
  }

  this.el = document.createElement("div");
  this.el.className = "petal  " + this.customClass;
  this.el.style.position = "absolute";
  this.el.style.backfaceVisibility = "visible";
 }
}

class BlossomScene {
 container: HTMLElement;
 numPetals: number;
 petalsTypes: Petal[];
 gravity: number;
 windMaxSpeed: number;
 private windMagnitude: number;
 private placeholder: HTMLElement;
 private petals: Petal[];
 private windDuration: number;
 private width: number;
 private height: number;
 private timer: number;

 constructor(config: BlossomSceneConfig) {
  let container = document.getElementById(config.id);
  if (container === null) {
   throw new Error("[id] provided was not found in document");
  }
  this.container = container;
  this.placeholder = document.createElement("div");
  this.petals = [];
  this.numPetals = config.numPetals || 10;
  this.petalsTypes = config.petalsTypes;
  this.gravity = config.gravity || 0.8;
  this.windMaxSpeed = config.windMaxSpeed || 4;
  this.windMagnitude = 0.2;
  this.windDuration = 0;
  this.width = this.container.offsetWidth;
  this.height = this.container.offsetHeight;
  this.timer = 0;

  this.container.style.overflow = "hidden";
  this.placeholder.style.transformStyle = "preserve-3d";
  this.placeholder.style.width = this.container.offsetWidth + "px";
  this.placeholder.style.height = this.container.offsetHeight + "px";
  this.container.appendChild(this.placeholder);
  this.createPetals();
  requestAnimationFrame(this.updateFrame.bind(this));
 }

 /**
  * Reset the petal position when it goes out of container
  */
 resetPetal(petal: Petal) {
  petal.x = this.width * 2 - Math.random() * this.width * 1.75;
  petal.y = petal.el.offsetHeight * -1;
  petal.z = Math.random() * 200;

  if (petal.x > this.width) {
   petal.x = this.width + petal.el.offsetWidth;
   petal.y = (Math.random() * this.height) / 2;
  }

  // Rotation
  petal.rotation.speed = Math.random() * 10;
  let randomAxis = Math.random();
  if (randomAxis > 0.5) {
   petal.rotation.axis = "X";
  } else if (randomAxis > 0.25) {
   petal.rotation.axis = "Y";
   petal.rotation.x = Math.random() * 180 + 90;
  } else {
   petal.rotation.axis = "Z";
   petal.rotation.x = Math.random() * 360 - 180;
   // looks weird if the rotation is too fast around this axis
   petal.rotation.speed = Math.random() * 3;
  }

  // random speed
  petal.xSpeedVariation = Math.random() * 0.8 - 0.4;
  petal.ySpeed = Math.random() + this.gravity;

  return petal;
 }

 /**
  * Calculate wind speed
  */
 calculateWindSpeed(t: number, y: number) {
  let a =
   ((this.windMagnitude / 2) * (this.height - (2 * y) / 3)) / this.height;
  return (
   a * Math.sin(((2 * Math.PI) / this.windDuration) * t + (3 * Math.PI) / 2) + a
  );
 }

 /**
  * Update petal position
  */
 updatePetal(petal: Petal) {
  let petalWindSpeed = this.calculateWindSpeed(this.timer, petal.y);
  let xSpeed = petalWindSpeed + petal.xSpeedVariation;

  petal.x -= xSpeed;
  petal.y += petal.ySpeed;
  petal.rotation.value += petal.rotation.speed;

  let t =
   "translateX( " +
   petal.x +
   "px ) translateY( " +
   petal.y +
   "px ) translateZ( " +
   petal.z +
   "px )  rotate" +
   petal.rotation.axis +
   "( " +
   petal.rotation.value +
   "deg )";
  if (petal.rotation.axis !== "X") {
   t += " rotateX(" + petal.rotation.x + "deg)";
  }
  petal.el.style.transform = t;

  // reset if out of view
  if (petal.x < -10 || petal.y > this.height + 10) {
   this.resetPetal(petal);
  }
 }

 /**
  * Change the wind speed
  */
 updateWind() {
  // wind duration should be related to wind magnitude, e.g. higher windspeed means longer gust duration
  this.windMagnitude = Math.random() * this.windMaxSpeed;
  this.windDuration = this.windMagnitude * 50 + (Math.random() * 20 - 10);
 }

 /**
  * Create the petals elements
  */
 createPetals() {
  for (let i = 0; i < this.numPetals; i++) {
   let tmpPetalType = this.petalsTypes[
    Math.floor(Math.random() * (this.petalsTypes.length - 1))
   ];
   let tmpPetal = new Petal({ customClass: tmpPetalType.customClass });

   this.resetPetal(tmpPetal);
   this.petals.push(tmpPetal);
   this.placeholder.appendChild(tmpPetal.el);
  }
 }

 /**
  * Update the animation frame
  */
 updateFrame() {
  if (this.timer === this.windDuration) {
   this.updateWind();
   this.timer = 0;
  }

  let petalsLen = this.petals.length;
  for (let i = 0; i < petalsLen; i++) {
   this.updatePetal(this.petals[i]);
  }

  this.timer++;
  requestAnimationFrame(this.updateFrame.bind(this));
 }
}

const petalsTypes = [
 new Petal({ customClass: "petal-style1" }),
 new Petal({ customClass: "petal-style2" }),
 new Petal({ customClass: "petal-style3" }),
 new Petal({ customClass: "petal-style4" })
];

const myBlossomSceneConfig: BlossomSceneConfig = {
 id: "blossom_container",
 petalsTypes
};

const myBlossomScene = new BlossomScene(myBlossomSceneConfig);