class Renderer {
  depthBuffer = new Float32Array(640 * 480)
  origin = [319, 239]
  displayPort = [0, 0, 639, 479]
  rasterMask = 0
  renderStyle = ''
  nearBrightness = 0
  farBrightness = 0
  nearDistance = 0
  farDistance = 0
  depthBias = 0

  defaultRenderState = null

  fnDrawLine = null
  fnDrawTri = null
  fnDrawQuad = null
  nearFarDistanceRange = 0

  constructor () {
    this.setRenderStyle('additive')
    this.setDepthBrightness(20, 36, 1, 0)
    this.defaultRenderState = this.saveRenderState()
  }

  saveRenderState () {
    return {
      origin: this.origin,
      displayPort: this.displayPort,
      rasterMask: this.rasterMask,
      renderStyle: this.renderStyle,
      nearDistance: this.nearDistance,
      farDistance: this.farDistance,
      nearBrightness: this.nearBrightness,
      farBrightness: this.farBrightness,
      depthBias: this.depthBias,
    }
  }

  loadRenderState (state) {
    this.setOrigin(state.origin)
    this.setDisplayPort(state.displayPort)
    this.setRasterMask(state.rasterMask)
    this.setRenderStyle(state.renderStyle)
    this.setDepthBrightness(state.nearDistance, state.farDistance, state.nearBrightness, state.farBrightness)
    this.setDepthBias(state.depthBias)
  }

  restoreDefaultState () {
    this.setRenderStyle(this.defaultRenderState)
  }

  setOrigin (origin) {
    this.origin = origin
  }

  setDisplayPort (displayPort) {
    this.displayPort = displayPort
  }

  setRenderStyle (mode) {
    this.renderStyle = mode
    if (mode === 'additive') {
      this.fnDrawLine = this.clipDrawLine_Add
      this.fnDrawTri = this.clipDrawTri_Add_NoCull
      this.fnDrawQuad = this.clipDrawQuad_Add_NoCull
    } else if (mode === 'depth') {
      this.fnDrawLine = this.clipDrawLine_Set_Cull_Depth
      this.fnDrawTri = this.clipDrawTri_Set_Cull_Depth
      this.fnDrawQuad = this.clipDrawQuad_Set_Cull_Depth
    } else {
      this.fnDrawLine = this.fnDrawTri = this.fnDrawLine = null
    }
  }

  setDepthBrightness (nearDistance, farDistance, nearBrightness, farBrightness) {
    this.nearDistance = nearDistance
    this.farDistance = farDistance
    this.nearFarDistanceRange = farDistance - nearDistance
    this.nearBrightness = nearBrightness
    this.farBrightness = farBrightness
  }

  setDepthBias (depthBias) {
    this.depthBias = depthBias
  }

  setRasterMask (rasterMask) {
    this.rasterMask = rasterMask
  }

  clearDepthBuffer (value = 1000) {
    for (let i = 0; i < 640*480; ++i) {
      this.depthBuffer[i] = 10000
    }
  }
  
  masterDraw (buffer, transformedVerts, lineList, lineBrightness, triList, triBrightness, quadList, quadBrightness) {
    lineList.forEach(line => {
      const p0 = transformedVerts[line[0]]
      const p1 = transformedVerts[line[1]]
      if (p0 && p1) {
        const writeDepth = (p0[2] + p1[2]) / 2
        let wc = Math.max(0, writeDepth - this.nearDistance)
        wc = Math.min(wc / this.nearFarDistanceRange, 1)
        const brightness = lerp(this.nearBrightness, this.farBrightness, wc) * lineBrightness
        const readDepth = writeDepth + this.depthBias

        this.fnDrawLine(buffer, p0, p1, readDepth, writeDepth, brightness)
      }
    })

    triList.forEach(tri => {
      const p0 = transformedVerts[tri[0]]
      const p1 = transformedVerts[tri[1]]
      const p2 = transformedVerts[tri[2]]
      if (p0 && p1 && p2) {
        const writeDepth = (p0[2] + p1[2] + p2[2]) / 3
        let wc = Math.max(0, writeDepth - this.nearDistance)
        wc = Math.min(wc / this.nearFarDistanceRange, 1)
        const brightness = lerp(this.nearBrightness, this.farBrightness, wc) * triBrightness
        const readDepth = writeDepth + this.depthBias

        this.fnDrawTri(buffer, p0, p1, p2, readDepth, writeDepth, brightness)
      }
    })

    /*
    const nearClip = 0.01
    triList.forEach(tri => {
      const p0 = transformedVerts[tri[0]]
      const p1 = transformedVerts[tri[1]]
      const p2 = transformedVerts[tri[2]]
      if (p0 && p1 && p2) {
        const writeDepth = (p0[2] + p1[2] + p2[2]) / 3
        let wc = Math.max(0, writeDepth - this.nearDistance)
        wc = Math.min(wc / this.nearFarDistanceRange, 1)
        const brightness = lerp(this.nearBrightness, this.farBrightness, wc) * triBrightness
        const readDepth = writeDepth + this.depthBias

        const d0 = p0[2] - nearClip
        const d1 = p1[2] - nearClip
        const d2 = p2[2] - nearClip
        const dCode = (d0>=0?4:0) + (d1>=0?2:0) + (d2>=0?1:0)

        switch (dCode) {
          // All verts behind clip
          case 0:
            // this.fnDrawTri(buffer, p0, p1, p2, readDepth, writeDepth, brightness)
            break

          // All verts in front of clip
          case 7:
            this.fnDrawTri(buffer, p0, p1, p2, readDepth, writeDepth, brightness)
            break

          // One vert in front of clip
          case 4: {
              const ca01 = d0 / (d0-d1)
              const c01 = [
                p0[0] + (p1[0] - p0[0]) * ca01,
                p0[1] + (p1[1] - p0[1]) * ca01,
                p0[2] + (p1[2] - p0[2]) * ca01,
              ]
              const ca02 = d0 / (d0-d2)
              const c02 = [
                p0[0] + (p2[0] - p0[0]) * ca02,
                p0[1] + (p2[1] - p0[1]) * ca02,
                p0[2] + (p2[2] - p0[2]) * ca02,
              ]
              this.fnDrawTri(buffer, p0, c01, c02, readDepth, writeDepth, brightness)
            }
            break
          case 2: {
              const ca10 = d1 / (d1-d0)
              const c10 = [
                p1[0] + (p0[0] - p1[0]) * ca10,
                p1[1] + (p0[1] - p1[1]) * ca10,
                p1[2] + (p0[2] - p1[2]) * ca10,
              ]
              const ca12 = d1 / (d1-d2)
              const c12 = [
                p1[0] + (p2[0] - p1[0]) * ca12,
                p1[1] + (p2[1] - p1[1]) * ca12,
                p1[2] + (p2[2] - p1[2]) * ca12,
              ]
              this.fnDrawTri(buffer, c10, p1, c12, readDepth, writeDepth, brightness)
            }
            break
          case 1: {
              const ca20 = d2 / (d2-d0)
              const c20 = [
                p2[0] + (p0[0] - p2[0]) * ca20,
                p2[1] + (p0[1] - p2[1]) * ca20,
                p2[2] + (p0[2] - p2[2]) * ca20,
              ]
              const ca21 = d2 / (d2-d1)
              const c21 = [
                p2[0] + (p1[0] - p2[0]) * ca21,
                p2[1] + (p1[1] - p2[1]) * ca21,
                p2[2] + (p1[2] - p2[2]) * ca21,
              ]
              this.fnDrawTri(buffer, c20, c21, p2, readDepth, writeDepth, brightness)
            }
            break
  
          // Two verts in front of clip
          case 6: {
            const ca02 = d0 / (d0-d2)
            const c02 = [
              p0[0] + (p2[0] - p0[0]) * ca02,
              p0[1] + (p2[1] - p0[1]) * ca02,
              p0[2] + (p2[2] - p0[2]) * ca02,
            ]
            const ca12 = d1 / (d1-d2)
            const c12 = [
              p1[0] + (p2[0] - p1[0]) * ca12,
              p1[1] + (p2[1] - p1[1]) * ca12,
              p1[2] + (p2[2] - p1[2]) * ca12,
            ]
            this.fnDrawQuad(buffer, p0, p1, c12, c02, readDepth, writeDepth, brightness)
          }
          break
        case 3: {
            const ca10 = d1 / (d1-d0)
            const c10 = [
              p1[0] + (p0[0] - p1[0]) * ca10,
              p1[1] + (p0[1] - p1[1]) * ca10,
              p1[2] + (p0[2] - p1[2]) * ca10,
            ]
            const ca20 = d2 / (d2-d0)
            const c20 = [
              p2[0] + (p0[0] - p2[0]) * ca20,
              p2[1] + (p0[1] - p2[1]) * ca20,
              p2[2] + (p0[2] - p2[2]) * ca20,
            ]
            this.fnDrawQuad(buffer, p1, p2, c20, c10, readDepth, writeDepth, brightness)
          }
          break
        case 5: {
            const ca01 = d0 / (d0-d1)
            const c01 = [
              p0[0] + (p1[0] - p0[0]) * ca01,
              p0[1] + (p1[1] - p0[1]) * ca01,
              p0[2] + (p1[2] - p0[2]) * ca01,
            ]
            const ca21 = d2 / (d2-d1)
            const c21 = [
              p2[0] + (p1[0] - p2[0]) * ca21,
              p2[1] + (p1[1] - p2[1]) * ca21,
              p2[2] + (p1[2] - p2[2]) * ca21,
            ]
            this.fnDrawQuad(buffer, p2, p0, c01, c21, readDepth, writeDepth, brightness)
          }
          break
        }
      }
    })
    */

    quadList.forEach(quad => {
      const p0 = transformedVerts[quad[0]]
      const p1 = transformedVerts[quad[1]]
      const p2 = transformedVerts[quad[2]]
      const p3 = transformedVerts[quad[3]]
      if (p0 && p1 && p2 && p3) {
        const writeDepth = (p0[2] + p1[2] + p2[2] + p3[2]) / 4
        let wc = Math.max(0, writeDepth - this.nearDistance)
        wc = Math.min(wc / this.nearFarDistanceRange, 1)
        const brightness = lerp(this.nearBrightness, this.farBrightness, wc) * quadBrightness
        const readDepth = writeDepth + this.depthBias

        this.fnDrawQuad(buffer, p0, p1, p2, p3, readDepth, writeDepth, brightness)
      }
    })
  }

  drawLists (buffer, modelMatrix, vertList, lineList, lineBrightness, triList, triBrightness, quadList, quadBrightness) {
    const wvp = m4.multiply(camViewPersp, modelMatrix)

    const offsetX = this.origin[0]
    const offsetY = this.origin[1]
    const transformedVerts = vertList.map(vert => {
      const v = m4.transformVector(wvp, [vert[0], vert[1], vert[2], 1])
      if (v[2] >= -2) {
        const w = v[3]
        return [v[0]*320/w + offsetX, v[1]*240/w + offsetY, w]
      } else {
        return null
      }
    })
    this.masterDraw (buffer, transformedVerts, lineList, lineBrightness, triList, triBrightness, quadList, quadBrightness)
  }

  drawModel (buffer, modelMatrix, model, excludeObjects = [], brightness = 1) {
    const wvp = m4.multiply(camViewPersp, modelMatrix)

    const offsetX = this.origin[0]
    const offsetY = this.origin[1]
    const transformedVerts = model.verts.map(vert => {
      const v = m4.transformVector(wvp, [vert[0], vert[1], vert[2], 1])
      if (v[2] >= -2) {
        const w = v[3]
        return [v[0]*320/w + offsetX, v[1]*240/w + offsetY, w]
      } else {
        return null
      }
    })

    model.objects.forEach(object => {
      if (!excludeObjects.includes(object.name)) {
        object.subObjects.forEach(subObject => {
          const subObjectBrightness = subObject.brightness * brightness
          this.masterDraw (buffer, transformedVerts, subObject.lines, subObjectBrightness, subObject.tris, subObjectBrightness, subObject.quads, subObjectBrightness)
        })
      }
    })
  }

  #edgeFunction(a, b, c) {
    return (c[0] - a[0]) * (b[1] - a[1]) - (c[1] - a[1]) * (b[0] - a[0])
  } 
   
  // https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/rasterization-stage
  clipDrawTri_Add_NoCull(buffer, v0, v1, v2, readDepth, writeDepth, brightness) {
    const minX = Math.max(Math.floor(Math.min(v0[0], v1[0], v2[0])), this.displayPort[0])
    const minY = Math.max(Math.floor(Math.min(v0[1], v1[1], v2[1])), this.displayPort[1])
    const maxX = Math.min(Math.floor(Math.max(v0[0], v1[0], v2[0])), this.displayPort[2])
    const maxY = Math.min(Math.floor(Math.max(v0[1], v1[1], v2[1])), this.displayPort[3])

    for (let y = minY; y <= maxY; ++y) {
      if (y & this.rasterMask) continue
      let bufferIndex = y*640 + minX
      let p = [minX + 0.5, y + 0.5]
      for (let x = minX; x <= maxX; ++x) {
        const w0 = this.#edgeFunction(v0, v1, p)
        const w1 = this.#edgeFunction(v1, v2, p)
        const w2 = this.#edgeFunction(v2, v0, p)
        if (w0 <= 0 && w1 <= 0 && w2 <= 0) {
          const edge0 = [v2[0]-v1[0], v2[1]-v1[0]]
          const edge1 = [v0[0]-v2[0], v0[1]-v2[0]]
          const edge2 = [v1[0]-v0[0], v1[1]-v0[0]]
          if ((w0 == 0 ? ((edge0[1] == 0 && edge0[0] < 0) || edge0[1] < 0) : (w0 < 0)) &&
              (w1 == 0 ? ((edge1[1] == 0 && edge1[0] < 0) || edge1[1] < 0) : (w1 < 0)) &&
              (w2 == 0 ? ((edge2[1] == 0 && edge2[0] < 0) || edge2[1] < 0) : (w2 < 0))) {
            buffer[bufferIndex] += brightness
          }
        } else if (w0 >= 0 && w1 >= 0 && w2 >= 0) {
          const edge0 = [v2[0]-v1[0], v2[1]-v1[0]]
          const edge1 = [v0[0]-v2[0], v0[1]-v2[0]]
          const edge2 = [v1[0]-v0[0], v1[1]-v0[0]]
          if ((w0 == 0 ? ((edge0[1] == 0 && edge0[0] > 0) || edge0[1] > 0) : (w0 > 0)) &&
              (w1 == 0 ? ((edge1[1] == 0 && edge1[0] > 0) || edge1[1] > 0) : (w1 > 0)) &&
              (w2 == 0 ? ((edge2[1] == 0 && edge2[0] > 0) || edge2[1] > 0) : (w2 > 0))) {
            buffer[bufferIndex] += brightness
          }
        }
        p[0]++
        bufferIndex++
      }
    }
  }

  clipDrawTri_Set_Cull_Depth(buffer, v0, v1, v2, readDepth, writeDepth, brightness) {
    if ((v1[0]-v0[0]) * (v1[1]+v0[1]) + 
        (v2[0]-v1[0]) * (v2[1]+v1[1]) +
        (v0[0]-v2[0]) * (v0[1]+v2[1]) >= 0) return

    const minX = Math.max(Math.floor(Math.min(v0[0], v1[0], v2[0])), this.displayPort[0])
    const minY = Math.max(Math.floor(Math.min(v0[1], v1[1], v2[1])), this.displayPort[1])
    const maxX = Math.min(Math.floor(Math.max(v0[0], v1[0], v2[0])), this.displayPort[2])
    const maxY = Math.min(Math.floor(Math.max(v0[1], v1[1], v2[1])), this.displayPort[3])
  
    for (let y = minY; y <= maxY; ++y) {
      if (y & this.rasterMask) continue
      let bufferIndex = y*640 + minX
      let p = [minX + 0.5, y + 0.5]
      for (let x = minX; x <= maxX; ++x) {
        const w0 = this.#edgeFunction(v0, v1, p)
        if (w0 <= 0) {
          const w1 = this.#edgeFunction(v1, v2, p)
          if (w1 <= 0) {
            const w2 = this.#edgeFunction(v2, v0, p)
            if (w2 <= 0) {
              const edge0 = [v2[0]-v1[0], v2[1]-v1[0]]
              const edge1 = [v0[0]-v2[0], v0[1]-v2[0]]
              const edge2 = [v1[0]-v0[0], v1[1]-v0[0]]
              if ((w0 == 0 ? ((edge0[1] == 0 && edge0[0] < 0) || edge0[1] < 0) : (w0 < 0)) &&
                  (w1 == 0 ? ((edge1[1] == 0 && edge1[0] < 0) || edge1[1] < 0) : (w1 < 0)) &&
                  (w2 == 0 ? ((edge2[1] == 0 && edge2[0] < 0) || edge2[1] < 0) : (w2 < 0))) {
                if (readDepth < this.depthBuffer[bufferIndex]) {
                  buffer[bufferIndex] = brightness
                  this.depthBuffer[bufferIndex] = writeDepth
                }
              }
            }
          }
        }
        p[0]++
        bufferIndex++
      }
    }
  }

  clipDrawQuad_Add_NoCull(buffer, v0, v1, v2, v3, readDepth, writeDepth, brightness) {
    const minX = Math.max(Math.floor(Math.min(v0[0], v1[0], v2[0], v3[0])), this.displayPort[0])
    const minY = Math.max(Math.floor(Math.min(v0[1], v1[1], v2[1], v3[1])), this.displayPort[1])
    const maxX = Math.min(Math.floor(Math.max(v0[0], v1[0], v2[0], v3[0])), this.displayPort[2])
    const maxY = Math.min(Math.floor(Math.max(v0[1], v1[1], v2[1], v3[1])), this.displayPort[3])

    for (let y = minY; y <= maxY; ++y) {
      if (y & this.rasterMask) continue
      let bufferIndex = y*640 + minX
      let p = [minX + 0.5, y + 0.5]
      for (let x = minX; x <= maxX; ++x) {
        const w0 = this.#edgeFunction(v0, v1, p)
        const w1 = this.#edgeFunction(v1, v2, p)
        const w2 = this.#edgeFunction(v2, v3, p)
        const w3 = this.#edgeFunction(v3, v0, p)
        if (w0 <= 0 && w1 <= 0 && w2 <= 0 && w3 <= 0) {
          const edge0 = [v2[0]-v1[0], v2[1]-v1[0]]
          const edge1 = [v3[0]-v2[0], v3[1]-v2[0]]
          const edge2 = [v0[0]-v3[0], v0[1]-v3[0]]
          const edge3 = [v1[0]-v0[0], v1[1]-v0[0]]
          if ((w0 == 0 ? ((edge0[1] == 0 && edge0[0] < 0) || edge0[1] < 0) : (w0 < 0)) &&
              (w1 == 0 ? ((edge1[1] == 0 && edge1[0] < 0) || edge1[1] < 0) : (w1 < 0)) &&
              (w2 == 0 ? ((edge2[1] == 0 && edge2[0] < 0) || edge2[1] < 0) : (w2 < 0)) &&
              (w3 == 0 ? ((edge3[1] == 0 && edge3[0] < 0) || edge3[1] < 0) : (w3 < 0))) {
            buffer[bufferIndex] += brightness
          }
        } else if (w0 >= 0 && w1 >= 0 && w2 >= 0 && w3 >= 0) {
          const edge0 = [v2[0]-v1[0], v2[1]-v1[0]]
          const edge1 = [v3[0]-v2[0], v3[1]-v2[0]]
          const edge2 = [v0[0]-v3[0], v0[1]-v3[0]]
          const edge3 = [v1[0]-v0[0], v1[1]-v0[0]]
          if ((w0 == 0 ? ((edge0[1] == 0 && edge0[0] > 0) || edge0[1] > 0) : (w0 > 0)) &&
              (w1 == 0 ? ((edge1[1] == 0 && edge1[0] > 0) || edge1[1] > 0) : (w1 > 0)) &&
              (w2 == 0 ? ((edge2[1] == 0 && edge2[0] > 0) || edge2[1] > 0) : (w2 > 0)) &&
              (w3 == 0 ? ((edge3[1] == 0 && edge3[0] > 0) || edge3[1] > 0) : (w3 > 0))) {
            buffer[bufferIndex] += brightness
          }
        }
        p[0]++
        bufferIndex++
      }
    }
  }

  clipDrawQuad_Set_Cull_Depth(buffer, v0, v1, v2, v3, readDepth, writeDepth, brightness) {
    if ((v1[0]-v0[0]) * (v1[1]+v0[1]) + 
        (v2[0]-v1[0]) * (v2[1]+v1[1]) +
        (v3[0]-v2[0]) * (v3[1]+v2[1]) +
        (v0[0]-v3[0]) * (v0[1]+v3[1]) >= 0) return
    
    const minX = Math.max(Math.floor(Math.min(v0[0], v1[0], v2[0], v3[0])), this.displayPort[0])
    const minY = Math.max(Math.floor(Math.min(v0[1], v1[1], v2[1], v3[1])), this.displayPort[1])
    const maxX = Math.min(Math.floor(Math.max(v0[0], v1[0], v2[0], v3[0])), this.displayPort[2])
    const maxY = Math.min(Math.floor(Math.max(v0[1], v1[1], v2[1], v3[1])), this.displayPort[3])

    for (let y = minY; y <= maxY; ++y) {
      if (y & this.rasterMask) continue
      let bufferIndex = y*640 + minX
      let p = [minX + 0.5, y + 0.5]
      for (let x = minX; x <= maxX; ++x) {
        const w0 = this.#edgeFunction(v0, v1, p)
        if (w0 <= 0) {
          const w1 = this.#edgeFunction(v1, v2, p)
          if (w1 <= 0) {
            const w2 = this.#edgeFunction(v2, v3, p)
            if (w2 <= 0) {
              const w3 = this.#edgeFunction(v3, v0, p)
              if (w3 <= 0) {
                const edge0 = [v2[0]-v1[0], v2[1]-v1[0]]
                const edge1 = [v3[0]-v2[0], v3[1]-v2[0]]
                const edge2 = [v0[0]-v3[0], v0[1]-v3[0]]
                const edge3 = [v1[0]-v0[0], v1[1]-v0[0]]
                if ((w0 == 0 ? ((edge0[1] == 0 && edge0[0] < 0) || edge0[1] < 0) : (w0 < 0)) &&
                    (w1 == 0 ? ((edge1[1] == 0 && edge1[0] < 0) || edge1[1] < 0) : (w1 < 0)) &&
                    (w2 == 0 ? ((edge2[1] == 0 && edge2[0] < 0) || edge2[1] < 0) : (w2 < 0)) &&
                    (w3 == 0 ? ((edge3[1] == 0 && edge3[0] < 0) || edge3[1] < 0) : (w3 < 0))) {
                  if (readDepth < this.depthBuffer[bufferIndex]) {
                    buffer[bufferIndex] = brightness
                    this.depthBuffer[bufferIndex] = writeDepth
                  }
                }
              }
            }
          }
        }
        p[0]++
        bufferIndex++
      }
    }
  }
  
  clipDrawLine_Add (buffer, p0, p1, readDepth, writeDepth, brightness) {
    let cp0 = []
    let cp1 = []
    if (this.clip(p0, p1, this.displayPort, cp0, cp1)) {
      let x0 = Math.floor(cp0[0])
      let y0 = Math.floor(cp0[1])
      const x1 = Math.floor(cp1[0])
      const y1 = Math.floor(cp1[1])

      const dx = Math.abs(x1 - x0)
      const sx = x0 < x1 ? 1 : -1
      const dy = Math.abs(y1 - y0)
      const sy = y0 < y1 ? 1 : -1 
      let err = (dx > dy ? dx : -dy) / 2

      while (true) {
        if (!(y0 & this.rasterMask)) {
          const index = x0 + y0*640
          buffer[index] += brightness
        }
        if (x0 == x1 && y0 == y1) break
        const e2 = err
        if (e2 >-dx) {
          err -= dy
          x0 += sx
        }
        if (e2 < dy) {
          err += dx
          y0 += sy
        }
      }
    }
  }

  clipDrawLine_Set_Cull_Depth (buffer, v0, v1, readDepth, writeDepth, brightness) {
    let cp0 = []
    let cp1 = []
    if (this.clip(v0, v1, this.displayPort, cp0, cp1)) {
      let x0 = Math.floor(cp0[0])
      let y0 = Math.floor(cp0[1])
      const x1 = Math.floor(cp1[0])
      const y1 = Math.floor(cp1[1])

      const dx = Math.abs(x1 - x0)
      const sx = x0 < x1 ? 1 : -1
      const dy = Math.abs(y1 - y0)
      const sy = y0 < y1 ? 1 : -1 
      let err = (dx > dy ? dx : -dy) / 2

      while (true) {
        if (!(y0 & this.rasterMask)) {
          const index = x0 + y0*640
          if (readDepth < this.depthBuffer[index]) {
            buffer[index] = brightness
            this.depthBuffer[index] = writeDepth
          }
        }
        if (x0 == x1 && y0 == y1) break
        const e2 = err
        if (e2 >-dx) {
          err -= dy
          x0 += sx
        }
        if (e2 < dy) {
          err += dx
          y0 += sy
        }
      }
    }
  }

  // https://github.com/w8r/liang-barsky/
  EPSILON = 1e-6
  clipT(num, denom, c) {
    const [tE, tL] = c
    if (Math.abs(denom) < this.EPSILON) return num < 0
    const t = num / denom
  
    if (denom > 0) {
      if (t > tL) return 0
      if (t > tE) c[0] = t
    } else {
      if (t < tE) return 0
      if (t < tL) c[1] = t
    }
    return 1
  }
  clip(a, b, box, da, db) {
    const [x1, y1] = a
    const [x2, y2] = b
    const dx = x2 - x1
    const dy = y2 - y1
  
    if (da === undefined || db === undefined) {
      da = a
      db = b
    } else {
      da[0] = a[0]
      da[1] = a[1]
      db[0] = b[0]
      db[1] = b[1]
    }
  
    if (
      Math.abs(dx) < this.EPSILON &&
      Math.abs(dy) < this.EPSILON &&
      x1 >= box[0] &&
      x1 <= box[2] &&
      y1 >= box[1] &&
      y1 <= box[3]
    ) {
      return true // this.INSIDE
    }
  
    const c = [0, 1]
    if (
      this.clipT(box[0] - x1, dx, c) &&
      this.clipT(x1 - box[2], -dx, c) &&
      this.clipT(box[1] - y1, dy, c) &&
      this.clipT(y1 - box[3], -dy, c)
    ) {
      const [tE, tL] = c
      if (tL < 1) {
        db[0] = x1 + tL * dx
        db[1] = y1 + tL * dy
      }
      if (tE > 0) {
        da[0] += tE * dx
        da[1] += tE * dy
      }
      return true // this.INSIDE
    }
    return false // this.OUTSIDE
  }
}

let renderer = new Renderer()
