<head>
  <title>Turn Signal</title>
  <meta publisher="Mister Doctor" />
  <meta description="A sequential car turn signal. Idea by Turbohopper." />
  <meta
    property="bg_color"
    label="Background Color"
    type="color"
    min="0"
    max="360"
    default="#202020" />
  <meta property="color" label="Color" type="color" min="0" max="360" default="#ff8000" />
  <meta property="draw_colors" label="Dead Zone" type="boolean" default="1" />
  <meta property="mode" label="Mode" type="combobox" values="Right,Left,Hazards" default="Right" />
  <meta property="bars" label="Bars" type="number" min="1" max="100" default="40" />
  <meta property="speed" label="Speed" type="number" min="0" max="100" default="50" />
  <meta property="hold_on_time" label="Hold On Time" type="number" min="0" max="100" default="1" />
  <meta
    property="hold_off_time"
    label="Hold Off Time"
    type="number"
    min="0"
    max="100"
    default="1" />
  <meta
    property="fade_in_mode"
    label="Fade-In Mode"
    type="combobox"
    values="Sequential,Simultaneous"
    default="Sequential" />
  <meta property="fade_in_time" label="Fade-In Time" type="number" min="1" max="100" default="10" />
  <meta
    property="fade_in_curve"
    label="Fade-In curve"
    type="combobox"
    values="Linear,Ease,Ease In,Ease Out"
    default="Linear" />
  <meta
    property="fade_out_mode"
    label="Fade-Out Mode"
    type="combobox"
    values="Sequential,Simultaneous"
    default="Sequential" />
  <meta
    property="fade_out_time"
    label="Fade-Out Time"
    type="number"
    min="1"
    max="100"
    default="50" />
  <meta
    property="fade_out_curve"
    label="Fade-Out curve"
    type="combobox"
    values="Linear,Ease,Ease In,Ease Out"
    default="Linear" />
</head>
<body style="margin: 0; padding: 0">
  <canvas id="exCanvas" width="320" height="200"></canvas>
</body>
<script>
  /** @type{CanvasRenderingContext2D} */
  const c = document.getElementById("exCanvas");
  /** @type{CanvasRenderingContext2D} */
  const ctx = c.getContext("2d");
  const WIDTH = 320;
  const HEIGHT = 200;

  let t = 0;

  let prev_time = performance.now();
  let time = performance.now();
  let delta_time = time - prev_time;

  function update() {
    prev_time = time;
    time = performance.now();
    delta_time = time - prev_time;

    const t_on = hold_on_time / 50;
    const t_off = hold_off_time / 50;
    const t_in = fade_in_time / 50;
    const t_out = fade_out_time / 50;

    const bg_color_rgb = hex_to_rgb(bg_color);
    const color_rgb = hex_to_rgb(color);

    const dx = WIDTH / bars;
    const du = 1 / bars;

    let fade_in_linear = (u, t) => 0;
    let fade_out_linear = (u, t) => 0;
    let t_total = t_in + t_on + t_out + t_off;

    switch (fade_in_mode + fade_out_mode) {
      case "SequentialSequential":
        fade_in_linear = (u, t) => clamp((t - u) / t_in);
        fade_out_linear = (u, t) => clamp((u - t + t_in + t_on) / t_out + 1);
        t_total += 1;
        break;
      case "SequentialSimultaneous":
        fade_in_linear = (u, t) => clamp((t - u) / t_in);
        fade_out_linear = (u, t) => clamp((-t + t_in + t_on + 1) / t_out + 1);
        t_total += 1;
        break;
      case "SimultaneousSequential":
        fade_in_linear = (u, t) => clamp(t / t_in);
        fade_out_linear = (u, t) => clamp((u - t + t_in + t_on) / t_out + 1);
        t_total += 1;
        break;
      case "SimultaneousSimultaneous":
        fade_in_linear = (u, t) => clamp(t / t_in);
        fade_out_linear = (u, t) => clamp((-t + t_in + t_on) / t_out + 1);
        break;
    }

    const fade_in = apply_curve(fade_in_linear, fade_in_curve);
    const fade_out = apply_curve(fade_out_linear, fade_out_curve);
    const blend = (u, t) => Math.min(fade_in(u, t), fade_out(u, t));

    let u = (i) => 0;
    switch (mode) {
      case "Right":
        u = (i) => i * du;
        break;
      case "Left":
        u = (i) => 1 - i * du;
        break;
      case "Hazards":
        u = (i) => (i * du < 0.5 ? 1 - 2 * i * du : 2 * i * du - 1);
        break;
    }

    for (let i = 0; i < bars; i++) {
      ctx.fillStyle = rgb_to_hex(lerp_rgb(bg_color_rgb, color_rgb, blend(u(i), t)));
      ctx.fillRect(i * dx, 0, dx, HEIGHT);
    }

    if (draw_colors) {
      ctx.fillStyle = bg_color;
      ctx.fillRect(0, 0, 40, 20);
      ctx.fillStyle = color;
      ctx.fillRect(40, 0, 40, 20);
    }

    t = (t + delta_time * 0.0005 * 6 ** (speed / 50 - 0.5)) % t_total;

    window.requestAnimationFrame(update);
  }

  /**@param f {(number, number) => (number)}
   * @param curve_type {"Linear"|"Ease"|"Ease In"|"Ease Out"}
   * @return {(number, number) => number} */
  function apply_curve(f, curve_type) {
    switch (curve_type) {
      case "Linear":
        return f;
      case "Ease":
        return (u, t) => smoothstep(f(u, t));
      case "Ease In":
        return (u, t) => f(u, t) ** 2;
      case "Ease Out":
        return (u, t) => 1 - (1 - f(u, t)) ** 2;
    }
    return f;
  }

  /**@param {number} x
   * @return {number} */
  function clamp(x) {
    return x < 0 ? 0 : x > 1 ? 1 : x;
  }

  /**@param {number} a
   * @param {number} b
   * @param {number} t
   * @return {number} */
  function lerp(a, b, t) {
    return a + (b - a) * t;
  }

  /**@param {number} x
   * @return {number} */
  function smoothstep(x) {
    return x * x * (3 - 2 * x);
  }

  /**@param {string} hex
   * @return {number} */
  function hex_to_float(hex) {
    return parseInt(hex, 16) / 255.0;
  }

  /**@param {string} hex
   * @return {{ r: number, g: number, b: number }} */
  function hex_to_rgb(hex) {
    return {
      r: hex_to_float(hex.slice(1, 3)),
      g: hex_to_float(hex.slice(3, 5)),
      b: hex_to_float(hex.slice(5, 7)),
    };
  }

  /**@param {number} float
   * @return {string} */
  function float_to_hex(float) {
    return Math.round(float * 255)
      .toString(16)
      .padStart(2, "0");
  }

  /**@param {{ r: number, g: number, b: number }} rgb
   * @return {string} */
  function rgb_to_hex(rgb) {
    return "#" + float_to_hex(rgb.r) + float_to_hex(rgb.g) + float_to_hex(rgb.b);
  }

  /**@param {{ r: number, g: number, b: number }} rgb0
   * @param {{ r: number, g: number, b: number }} rgb1
   * @param {number} t
   * @return {{ r: number, g: number, b: number }} */
  function lerp_rgb(rgb0, rgb1, t) {
    return {
      r: lerp(rgb0.r, rgb1.r, t),
      g: lerp(rgb0.g, rgb1.g, t),
      b: lerp(rgb0.b, rgb1.b, t),
    };
  }

  window.requestAnimationFrame(update);
</script>
