<template>
  <div id="vignette">
    <canvas ref="vignettecanvas"></canvas>
  </div>
</template>
<script>

// OVERENGINEERED UNOPTIMIZED CANVAS BULLSH*T
// BUT IT'S OKAY SINCE IT'S BLADE RUNNER INNIT

// Some stuff left unoptimized / verbose to show the work.

// TODO:
// - optimize render loop, avoid overdraws, etc
// - smoothly fade rows in on the horizon

// Constants.  Change at own risk
import { TimelineLite } from "gsap"
import { cancelAnimationFrameHandle, requestAnimationFrameHandle } from "../engines/util/framehandle"
import { ColorPalette } from "../engines/util/color"

const CANVAS_WIDTH = 800
const CANVAS_HEIGHT = 900
const FRAME_TIME = 1000 / 16
const LIGHT_ROWS = 20
const LIGHT_ROW_DEPTH = 2
const LIGHT_SPACING = 4.6
const LIGHT_SIZE = 0.1
const LIGHT_SCATTER = 0.4
const BUILDING_ROWS = 138
const BUILDING_ROW_DEPTH = 1
const BUILDING_ROW_WIDTH = 60
const BUILDING_MIN_HEIGHT = 1.5
const BUILDING_MAX_HEIGHT = 3
const STACK_HEIGHT = 8
const STACK_THRESHOLD = 0.77
const STACK_LIGHT_CHANCE = 0.95
const STACK_LIGHT_SIZE = 0.13
const FADE_GRAY_VALUE = 25
const FADE_OFFSET = 0.35
const FX_CHANCE = 0.102

// Virtual camera. Used in perspective calculations
const CAMERA = {
  x: 0,
  y: 12,
  z: 0,
  fov: 170,
  dist: 30,
  zSpeed: 0.005
}

// Virtual vanishing point XY. Used in perspective calculations
const VP_OFS = {
  x: 0.5,
  y: 0.27
}

// Global hoisted vars for rendering contexts and timers
let c, ctx, output_c, output_ctx
let _t, _dt, _ft

function ceil(n) {
  let f = (n << 0)
  f = f === n ? f : f + 1
  return f
}

// Draw a frame of the scene.
// Uses the current timestamp and the seeded RNG to render a
// pseudorandom cityscape with lights and buildings.
// We always generate and draw a set amount of city in front of
// the camera, so it appears to be endless as we 'fly over' it.
//
// 1. Clear the whole scene
// 2. Render random rows of lights
// 3. Render random rows of buildings
// 4. Blit scene to onscreen canvas
const _$ = {
  vPointX: 0,
  vPointY: 0,
  rowScreenX: 0,
  MAX_LIGHTS: 0,
  closestLightRow: 0,
  rowZ: 0,
  rowRelativeZ: 0,
  scalingFactor: 0,
  rowScreenWidth: 0,
  rowScreenHeight: 0,
  rowScreenY: 0,
  rowScreenLightSpacing: 0,
  rowLightCount: 0,
  lightSize: 0,
  lightHalfSize: 0,
  lightScreenX: 0,
  lightScreenY: 0,
  closestBuildingRow: 0,
  rowBuildingCount: 0,
  rowBuildingScreenWidth: 0,
  rowShade: 0,
  rowStyleString: "",
  lightData: [],
  isStack: false,
  buildingHeight: 0,
  buildingScreenHeight: 0,
  buildingScreenX: 0,
  buildingScreenY: 0,
  lightColor: 0
}

const exPalette = new ColorPalette(["black", "#111", "#113", "white", "sliver", "#f88", "orange", "oldlace", "#569"])

function render() {
  // Calculate the pixel XY of the vanishing point
  // (could be done on init, but useful if we ever want to
  // dynamically move the camera)
  _$.vPointX = c.width * VP_OFS.x >> 0
  _$.vPointY = c.height * VP_OFS.y >> 0

  // If we wanted to, we could give each row an X offset
  // and include it in perspective calculations,
  // but we just use the centre alignment for each one here.
  _$.rowScreenX = CAMERA.x + _$.vPointX

  // 1. Clear the whole scene...
  // (canvases are transparent so that the CSS 'sky' gradient can be seen)
  ctx.clearRect(0, 0, c.width, c.height)
  output_ctx.clearRect(0, 0, output_c.width, output_c.height)

  // 2. Render random rows of lights...

  // Calculate the closest row to the camera so we
  // can render the required number of rows into the distance
  _$.closestLightRow = Math.floor(CAMERA.z / LIGHT_ROW_DEPTH)

  // Draw each row of lights
  for (let i = 0; i < LIGHT_ROWS; i++) {
    // Calculate this row's base Z position
    // and Z relative to camera
    _$.rowZ = (_$.closestLightRow * LIGHT_ROW_DEPTH) + (LIGHT_ROW_DEPTH * i)
    _$.rowRelativeZ = _$.rowZ - CAMERA.z

    // Don't draw the row if it's behind the camera,
    // or beyond the camera's draw distance
    if (_$.rowRelativeZ <= 0 || _$.rowRelativeZ > CAMERA.dist) {
      continue
    }

    // Get the perspective scaling factor and pixel Y position for this row
    _$.scalingFactor = CAMERA.fov / _$.rowRelativeZ
    _$.rowScreenY = CAMERA.y * _$.scalingFactor + _$.vPointY

    // Don't draw the row if it's off-canvas
    if (_$.rowScreenY > c.height) {
      continue
    }

    // Calculate the spacing and number of lights we need to render for this row
    _$.rowScreenLightSpacing = LIGHT_SPACING * _$.scalingFactor
    _$.rowLightCount = c.width / _$.rowScreenLightSpacing

    // Seed the RNG in a way that gets us decent distribution
    // for the random lights
    ColorPalette.setSeed(_$.rowZ * 0.573)

    // Render the random lights for this row
    for (let j = 0; j < _$.rowLightCount; j++) {
      // Randomize light size, with perspective
      _$.lightSize = ColorPalette.getRandomRNG() * (LIGHT_SIZE * _$.scalingFactor)
      _$.lightHalfSize = _$.lightSize * 0.5

      // Randomly offset the XY of the light, with perspective
      _$.lightScreenX = (j * _$.rowScreenLightSpacing) +
          (ColorPalette.getRandomRNG() * LIGHT_SCATTER * _$.scalingFactor) - _$.lightHalfSize
      _$.lightScreenY = (_$.rowScreenY + (ColorPalette.getRandomRNG() * LIGHT_SCATTER) * _$.scalingFactor) - _$.lightHalfSize

      // Don't render if the light is offscreen
      if (_$.lightScreenX < 0 || _$.lightScreenX > c.width ||
          _$.lightScreenY > c.height) {
        // HACK: we still need to call the RNG the same number of times
        // for every row to ensure consistency between frames. If we didn't
        // do this, the lights would jump all over the place near the edges
        // of the screen.
        exPalette.getRandomFromPalette()
        continue
      }

      // Pick a random colour for this light
      ctx.fillStyle = exPalette.getRandomFromPalette()

      // Render the light twice, mirrored either side of the centre vanishing point.
      // Saves us having to do perspective offset calculation for every light,
      // and won't be noitceable when we overlay the city buildings.
      ctx.fillRect((_$.rowScreenX + _$.lightScreenX), _$.lightScreenY, _$.lightSize, _$.lightSize)
      ctx.fillRect((_$.rowScreenX - _$.lightScreenX), _$.lightScreenY, _$.lightSize, _$.lightSize)
    }
  }

  // 3. Render random rows of buildings...

  // Calculate the closest row to the camera so we
  // can render the required number of rows into the distance
  _$.closestBuildingRow = Math.floor(CAMERA.z / BUILDING_ROW_DEPTH)

  // Draw each row of buildings
  for (let i = BUILDING_ROWS; i > 0; i--) {
    // Calculate this row's base Z position
    // and Z relative to camera
    _$.rowZ = (_$.closestBuildingRow * BUILDING_ROW_DEPTH) + (BUILDING_ROW_DEPTH * i)
    _$.rowRelativeZ = _$.rowZ - CAMERA.z

    // Don't draw the row if it's behind the camera,
    // or beyond the camera's draw distance
    if (_$.rowRelativeZ <= 0 || _$.rowRelativeZ > CAMERA.dist) {
      continue
    }

    // Get the perspective scaling factor and pixel Y position for this row
    _$.scalingFactor = CAMERA.fov / _$.rowRelativeZ

    // Calculate the perspective-scaled position and base size of our row.
    // Offset the XY so that the row's 'origin' is at centre bottom (i.e. ground-up)
    _$.rowScreenWidth = BUILDING_ROW_WIDTH * _$.scalingFactor
    _$.rowScreenHeight = BUILDING_MAX_HEIGHT * _$.scalingFactor
    _$.rowScreenX = CAMERA.x * _$.scalingFactor + _$.vPointX - (_$.rowScreenWidth * 0.5)
    _$.rowScreenY = CAMERA.y * _$.scalingFactor + _$.vPointY - _$.rowScreenHeight

    // Seed the RNG to keep rendering consistent for this row
    ColorPalette.setSeed(_$.rowZ)

    // Calculate a random number of buildings for this row
    // and get their screen width
    _$.rowBuildingCount = ColorPalette.randomRange(20, 70)
    _$.rowBuildingScreenWidth = _$.rowScreenWidth / _$.rowBuildingCount

    // Calculate the shade we want the buildings in this row to be.
    // The tint is darker nearer the camera, giving a sort of crude distance fog
    // near the horizon.
    _$.rowShade = Math.round(FADE_GRAY_VALUE * (_$.rowRelativeZ / (CAMERA.dist) - FADE_OFFSET))
    _$.rowStyleString = "rgb(" + _$.rowShade + "," + _$.rowShade + "," + _$.rowShade + ")"

    // Calclate and render each building
    _$.lightData.length = 0
    ctx.fillStyle = _$.rowStyleString
    for (let j = 0; j < _$.rowBuildingCount; j++) {
      // Buildings have a certain chance to become a 'stack' i.e. way taller than
      // everything else. We calculate a random ranged height for the building,
      // and if it exceeds a threshold, it gets turned into a stack.
      _$.isStack = false
      _$.buildingHeight = Math.max(BUILDING_MIN_HEIGHT, ColorPalette.getRandomRNG() * BUILDING_MAX_HEIGHT)

      if (_$.buildingHeight > (BUILDING_MAX_HEIGHT * STACK_THRESHOLD)) {
        _$.isStack = true
        // Stacks have 40% height variance
        _$.buildingHeight = (STACK_HEIGHT * 0.6 + (ColorPalette.getRandomRNG() * 0.4))
      }

      // Calculate the pixel size and position of this building, adjusted for perspective
      _$.buildingScreenHeight = _$.buildingHeight * _$.scalingFactor
      _$.buildingScreenX = _$.rowScreenX + (j * _$.rowBuildingScreenWidth)
      _$.buildingScreenY = _$.rowScreenY + _$.rowScreenHeight - _$.buildingScreenHeight

      // Draw the building on screen
      ctx.fillRect(_$.buildingScreenX, _$.buildingScreenY, Math.ceil(_$.rowBuildingScreenWidth), _$.buildingScreenHeight)

      // Seed the RNG for consistency when calculating stack lights (if needed)

      ColorPalette.setSeed(_$.buildingHeight + j)
      // Stacks have a chance to get lights on their top corners.
      // Generate and store light data so we can render it on top of the buildings
      if (_$.isStack && ColorPalette.getRandomRNG() < STACK_LIGHT_CHANCE) {
        // Get random light size and color.
        // Slightly higher chance of red vs white lights
        _$.lightSize = ColorPalette.getRandomRNG() * (STACK_LIGHT_SIZE * _$.scalingFactor)
        _$.lightColor = (ColorPalette.getRandomRNG() > 0.6) ? "white" : "red"
        // Save light info for rendering after we do all the buildings
        // (helps minimixe changes to ctx.fillStyle)
        _$.lightData.push(_$.buildingScreenX)
        _$.lightData.push(_$.buildingScreenY)
        _$.lightData.push(_$.lightSize)
        _$.lightData.push(_$.lightColor)
      }
    }

    // Draw any lights on stacks that need them in this row
    for (let j = 0; j < _$.lightData.length; j += 4) {
      _$.buildingScreenX = _$.lightData[j]
      _$.buildingScreenY = _$.lightData[j + 1]
      _$.lightSize = _$.lightData[j + 2]
      _$.lightHalfSize = _$.lightSize * 0.5
      _$.lightColor = _$.lightData[j + 3]

      // Draw lights centred at the top left and right corners of the stack
      ctx.fillStyle = _$.lightColor
      ctx.fillRect(_$.buildingScreenX - _$.lightHalfSize,
        _$.buildingScreenY - _$.lightHalfSize, _$.lightSize, _$.lightSize)
      ctx.fillRect(_$.buildingScreenX + _$.rowBuildingScreenWidth - _$.lightHalfSize,
        _$.buildingScreenY - _$.lightHalfSize, _$.lightSize, _$.lightSize)
    }
  }

  // 4. Blit scene to onscreen canvas.
  // Now that we've built up the scene in-memory, we just render the image to
  // our canvas in the DOM.
  output_ctx.drawImage(c, 0, 0)
}

export default {
  name: "BgMovingcity",
  data() {
    return {
      dzSpeed: 0,
      framehandle: 0
    }
  },
  mounted() {
    this.$GSLite = new TimelineLite()
    this.$nextTick(() => {
      this.start()
    })
  },
  destroyed() {
    cancelAnimationFrameHandle(this.framehandle)
  },
  methods: {
    // Update method of main loop
    frameUpdate() {
      // Update our global timestamp (used in rendering)
      _t = Date.now() * 0.001
      // Move the camera slowly 'forward'
      CAMERA.z += this.dzSpeed
    },
    // Main loop.
    // Maintains a consistent update rate, but draws the screen as often
    // as the browser will allow.
    frame() {
      this.framehandle = requestAnimationFrameHandle(this.frame.bind(this))
      _ft = Date.now()
      this.frameUpdate()
      if (_ft - _dt > FRAME_TIME) {
        render()
        _dt = _ft
      }
    },
    start() {
      const vignettecanvas = this.$refs.vignettecanvas
      console.log(vignettecanvas)
      // Init frame timers (see frame())
      _dt = _ft = Date.now()
      this.dzSpeed = CAMERA.zSpeed
      // Create two canvases - one for in-memory compositing,
      // and another to go in the DOM for our final render.
      // Make them the same size as each other.
      // c = document.createElement ('canvas');
      c = vignettecanvas
      ctx = c.getContext("2d")
      // c.style.display = "none";
      output_c = document.createElement("canvas")
      //  output_ctx = output_c.getContext ('2d');
      // output_c = vignettecanvas;
      output_ctx = output_c.getContext("2d")
      output_c.width = c.width = CANVAS_WIDTH
      output_c.height = c.height = CANVAS_HEIGHT
      // document.body.appendChild (output_c);
      // Start the main loop.
      this.frame()
    },
    MoveSpeed(g) {
      const s = parseFloat(g)
      if (!isNaN(s)) {
        const v = Math.abs(s)
        this.dzSpeed = CAMERA.zSpeed + v / 100

        if (v > 1.1 && v < 2.1) {
          if (Math.random() < FX_CHANCE) {
            this.$emit("LAUNCH", this.dzSpeed)
          }
        }

        if (v > 2.2 && v < 3.0) {
          if (Math.random() < FX_CHANCE) {
            this.$emit("LAUNCH", this.dzSpeed)
          }
        }

        if (v > 6.0 && v < 8.0) {
          if (Math.random() < FX_CHANCE) {
            this.$emit("LAUNCH", this.dzSpeed)
          }
        }

        if (v > 12.1 && v < 22.1) {
          if (Math.random() < FX_CHANCE) {
            this.$emit("LAUNCH", this.dzSpeed)
          }
        }
      }
    },
    MoveExplode() {
      // this.dzSpeed = (CAMERA.zSpeed - this.dzSpeed) * 0.2;
      this.$GSLite.to(this, 2, { dzSpeed: CAMERA.zSpeed, ease: Power1.easeOut })
      // this.$GSLite.eventCallback ("onComplete", this.onSFXComplete, [target]);
    }
  }
}
</script>

<style scoped lang="scss">

#vignette {
  //  background-image: linear-gradient(right, black 0%, transparent 10%, transparent 90%, black 100%);
  position: absolute;
  top: 0;
  left: 50%;
  width: 100%;
  height: 1000px;
  max-width: 1800px;
  transform: translateX(-50%);
  z-index: 50;

  canvas {
    background: transparent;
    background-image: linear-gradient(black 20%,
        #101 30%,
        #211 40%,
        #070702 52%,
        #000 90%,
        #000 100%);
    background-repeat: no-repeat;
    display: block;
    margin: 0 auto;
    width: 100%;
    max-width: 1800px;
    height: 1000px;
    //image-rendering: pixelated;
  }
}
</style>
