REVDANCATT'S GENUARY 2023

04 - Intersections

/* global preloadImagesTmr fxhash fxrand */

//
//  fxhash - Genuary 04 Intersections
//
//
//  HELLO!! Code is copyright revdancatt (that's me), so no sneaky using it for your
//  NFT projects.
//  But please feel free to unpick it, and ask me questions. A quick note, this is written
//  as an artist, which is a slightly different (and more storytelling way) of writing
//  code, than if this was an engineering project. I've tried to keep it somewhat readable
//  rather than doing clever shortcuts, that are cool, but harder for people to understand.
//
//  You can find me at...
//  https://twitter.com/revdancatt
//  https://instagram.com/revdancatt
//  https://youtube.com/revdancatt
//

const ratio = 1
// const startTime = new Date().getTime() // so we can figure out how long since the scene started
let drawn = false
let highRes = false // display high or low res
const features = {}
let resizeTmr = null
const aniFrame = null

window.$fxhashFeatures = {}

//  Work out what all our features are
const makeFeatures = () => {
  // For this project we will be setting the origin x to the middle of the
  // canvas and the origin y to the bottom of the canvas
  // We have three vanishing points, one to the right, one to the left and one above everything
  features.rightVPoint = {
    x: 1.5,
    y: 1
  }
  features.leftVPoint = {
    x: -1.5,
    y: 1
  }
  features.topVPoint = {
    x: 0,
    y: -5
  }

  // Now we are going to have an array of "blocks" that we will draw
  features.blocks = []
  // And we need to decide how many we are going to have
  // We're going to have a total volume of boxes
  const maxVolume = 0.02
  let currentVolume = 0

  let escape = 0
  // Now let's work out the size of each block
  while (currentVolume < maxVolume && escape < 200) {
    // create a starting point for the block in the x range of -0.4 to 0.4,
    // and a y from 0.1 to 0.8
    const newBlock = {
      x: fxrand() * 0.8 - 0.4,
      y: fxrand() * 0.8 + 0.1,
      height: fxrand() * 0.15 + 0.05,
      rightWidth: fxrand() * 0.15 + 0.05,
      leftWidth: fxrand() * 0.15 + 0.05
    }

    if (fxrand() < 0.1) newBlock.height *= 0.1
    if (fxrand() < 0.1) newBlock.rightWidth *= 0.1
    if (fxrand() < 0.1) newBlock.leftWidth *= 0.1
    if (fxrand() > 0.9) newBlock.height *= 2
    if (fxrand() > 0.9) newBlock.rightWidth *= 2
    if (fxrand() > 0.9) newBlock.leftWidth *= 2

    const volume = newBlock.height * newBlock.rightWidth * newBlock.leftWidth
    currentVolume += volume
    features.blocks.push(newBlock)
    escape++
  }
}

//  Call the above make features, so we'll have the window.$fxhashFeatures available
//  for fxhash
makeFeatures()

console.log(features)
console.table(window.$fxhashFeatures)

const init = async () => {
  //  I should add a timer to this, but really how often to people who aren't
  //  the developer resize stuff all the time. Stick it in a digital frame and
  //  have done with it!
  window.addEventListener('resize', async () => {
    //  If we do resize though, work out the new size...
    clearTimeout(resizeTmr)
    resizeTmr = setTimeout(async () => {
      await layoutCanvas()
    }, 100)
  })

  //  Now layout the canvas
  await layoutCanvas()
}

const layoutCanvas = async () => {
  const wWidth = window.innerWidth
  const wHeight = window.innerHeight
  let cWidth = wWidth
  let cHeight = cWidth * ratio
  if (cHeight > wHeight) {
    cHeight = wHeight
    cWidth = wHeight / ratio
  }
  const canvas = document.getElementById('target')
  if (highRes) {
    canvas.height = 8192
    canvas.width = 8192 / ratio
  } else {
    canvas.width = Math.min((8192 / 2), cWidth * 2)
    canvas.height = Math.min((8192 / ratio / 2), cHeight * 2)
    //  Minimum size to be half of the high rez cersion
    if (Math.min(canvas.width, canvas.height) < 8192 / 2) {
      if (canvas.width < canvas.height) {
        canvas.height = 8192 / 2
        canvas.width = 8192 / 2 / ratio
      } else {
        canvas.width = 8192 / 2
        canvas.height = 8192 / 2 / ratio
      }
    }
  }

  canvas.style.position = 'absolute'
  canvas.style.width = `${cWidth}px`
  canvas.style.height = `${cHeight}px`
  canvas.style.left = `${(wWidth - cWidth) / 2}px`
  canvas.style.top = `${(wHeight - cHeight) / 2}px`

  //  And draw it!!
  drawCanvas()
}

const vertProject = (p1, p2) => {
  const m = (p1.y - p2.y) / (p1.x - p2.x)
  const b = p2.y - m * p2.x
  const x1 = (100 - b) / m
  const x2 = (-100 - b) / m
  const y1 = m * x1 + b
  const y2 = m * x2 + b
  return {
    p1: {
      x: x1,
      y: y1
    },
    p2: {
      x: x2,
      y: y2
    }
  }
}

const horizProject = (p1, p2) => {
  const m = (p1.y - p2.y) / (p1.x - p2.x)
  const b = p2.y - m * p2.x
  const y1 = m * 100 + b
  const y2 = m * -100 + b
  const x1 = (y1 - b) / m
  const x2 = (y2 - b) / m
  return {
    p1: {
      x: x1,
      y: y1
    },
    p2: {
      x: x2,
      y: y2
    }
  }
}

// We need a function that gives us the intersection of two lines
const lineIntersect = (line1, line2) => {
  const x11 = line1.p1.x
  const y11 = line1.p1.y
  const x21 = line1.p2.x
  const y21 = line1.p2.y
  const x12 = line2.p1.x
  const y12 = line2.p1.y
  const x22 = line2.p2.x
  const y22 = line2.p2.y

  const m1 = (y21 - y11) / (x21 - x11)
  const b1 = y11 - m1 * x11
  const m2 = (y22 - y12) / (x22 - x12)
  const b2 = y12 - m2 * x12

  const x = (b2 - b1) / (m1 - m2)
  const y = m1 * x + b1
  return { x, y }
}

const drawCanvas = async () => {
  // Clear the animation frame
  if (aniFrame) window.cancelAnimationFrame(aniFrame)
  //  Let the preloader know that we've hit this function at least once
  drawn = true

  const canvas = document.getElementById('target')
  const ctx = canvas.getContext('2d')
  const w = canvas.width
  const h = canvas.height

  // Fill in the background in white
  ctx.fillStyle = '#ffffff'
  ctx.fillRect(0, 0, w, h)

  // Now loop throught the blocks
  for (let i = 0; i < features.blocks.length; i++) {
    const block = features.blocks[i]

    const tvPoint = features.topVPoint
    const rvPoint = features.rightVPoint
    const lvPoint = features.leftVPoint

    /*
    //
    // NOTE, SOME OF THIS INSTERSECTION STUFF IS SKETCHY, I'M DOING IT
    // TWO DIFFERENT WAYS AND I SHOULD CONDENSE IT DOWN TO ONE
    //
    */

    // We want to work out the points of the block
    // we'll start with the top point.
    // We need to project a line from the x,y to the top vanishing point
    // and then work how how far along that line the height is.
    // Start by working out the slope of the line
    const middleVerticalSlope = (tvPoint.y - block.y) / (tvPoint.x - block.x)
    // Now project along the line by the height relative to the distance of 1
    const topPoint = {
      x: block.x + block.height / Math.sqrt(1 + middleVerticalSlope * middleVerticalSlope),
      y: block.y + middleVerticalSlope * block.height / Math.sqrt(1 + middleVerticalSlope * middleVerticalSlope)
    }

    // If the topPoint x is > 0 invert the x and y
    if (topPoint.x > 0) {
      topPoint.x = block.x - (topPoint.x - block.x)
      topPoint.y = block.y - (topPoint.y - block.y)
    }

    // Now we need to work out the right point
    // We'll do this by projecting a line from the top point to the right vanishing point
    // and then work how how far along that line the rightWidth is.
    // Start by working out the slope of the line
    const foundationRightSlope = (rvPoint.y - block.y) / (rvPoint.x - block.x)
    // Now project along the line by the rightWidth relative to the distance of 1
    const rightPointBottom = {
      x: block.x + block.rightWidth / Math.sqrt(1 + foundationRightSlope * foundationRightSlope),
      y: block.y + foundationRightSlope * block.rightWidth / Math.sqrt(1 + foundationRightSlope * foundationRightSlope)
    }

    // Now we need to work out the rightPointTop
    // This is at the intersection of the line from the top point to the right vanishing point
    // and the line from the rightPointBottom to the top vanishing point
    // We'll do this by projecting a line from the top point to the right vanishing point
    // and then work how how far along that line the rightWidth is.
    // Start by working out the slope of the line
    const rightSlope = (rvPoint.y - topPoint.y) / (rvPoint.x - topPoint.x)
    // Now project along the line by the rightWidth relative to the distance of 1
    const rightPointTop = {
      x: topPoint.x + block.rightWidth / Math.sqrt(1 + rightSlope * rightSlope),
      y: topPoint.y + rightSlope * block.rightWidth / Math.sqrt(1 + rightSlope * rightSlope)
    }

    // Do the above again for the left point
    const foundationLeftSlope = (lvPoint.y - block.y) / (lvPoint.x - block.x)
    const leftPointBottom = {
      x: block.x - block.leftWidth / Math.sqrt(1 + foundationLeftSlope * foundationLeftSlope),
      y: block.y - foundationLeftSlope * block.leftWidth / Math.sqrt(1 + foundationLeftSlope * foundationLeftSlope)
    }
    const leftSlope = (lvPoint.y - topPoint.y) / (lvPoint.x - topPoint.x)
    const leftPointTop = {
      x: topPoint.x - block.leftWidth / Math.sqrt(1 + leftSlope * leftSlope),
      y: topPoint.y - leftSlope * block.leftWidth / Math.sqrt(1 + leftSlope * leftSlope)
    }

    const middleLine = vertProject(topPoint, block)
    const rightLine = vertProject(rightPointTop, rightPointBottom)
    const leftLine = vertProject(leftPointTop, leftPointBottom)
    const topRightLine = horizProject(topPoint, rightPointTop)
    const bottomRightLine = horizProject(rightPointBottom, block)
    const topLeftLine = horizProject(topPoint, leftPointTop)
    const bottomLeftLine = horizProject(leftPointBottom, block)
    const rightBackTopLine = horizProject(rightPointTop, lvPoint)
    const rightBackBottomLine = horizProject(rightPointBottom, lvPoint)
    const leftBackTopLine = horizProject(leftPointTop, rvPoint)
    const leftBackBottomLine = horizProject(leftPointBottom, rvPoint)

    // The backTopPoint is the intersection of the rightBackTopLine and the leftBackTopLine, calculated it here
    const backTopPoint = lineIntersect(leftBackTopLine, rightBackTopLine)
    const backBottomPoint = lineIntersect(leftBackBottomLine, rightBackBottomLine)

    block.fromPoints = [
      block,
      topPoint,
      rightPointBottom,
      rightPointTop,
      leftPointBottom,
      leftPointTop,
      backTopPoint,
      backBottomPoint
    ]

    block.projectionLines = [
      middleLine,
      rightLine,
      leftLine,
      topRightLine,
      bottomRightLine,
      topLeftLine,
      bottomLeftLine,
      rightBackTopLine,
      rightBackBottomLine,
      leftBackTopLine,
      leftBackBottomLine
    ]

    block.topPoint = topPoint
    block.rightPointBottom = rightPointBottom
    block.rightPointTop = rightPointTop
    block.leftPointBottom = leftPointBottom
    block.leftPointTop = leftPointTop
    block.backTopPoint = backTopPoint
    block.backBottomPoint = backBottomPoint
  }

  // we need to translate the canvas so that the origin is in the middle
  // of the canvas, and the y axis is pointing up
  ctx.save()
  ctx.translate(w / 2, h)
  ctx.scale(1, -1)

  // Draw the projection lines
  ctx.lineWidth = w / 1000
  ctx.strokeStyle = '#eeeeee'
  for (let i = 0; i < features.blocks.length; i++) {
    const block = features.blocks[i]
    block.projectionLines.forEach(line => {
      ctx.moveTo(line.p1.x * w, line.p1.y * h)
      ctx.lineTo(line.p2.x * w, line.p2.y * h)
    })
  }
  ctx.stroke()

  // Draw the ticks
  ctx.lineWidth = w / 500
  ctx.strokeStyle = 'red'
  ctx.beginPath()
  for (let i = 0; i < features.blocks.length; i++) {
    const block = features.blocks[i]
    // Draw ticks on the fromPoints
    block.fromPoints.forEach(fromPoint => {
      ctx.moveTo(fromPoint.x * w - (w / 200), fromPoint.y * h - w / 200)
      ctx.lineTo(fromPoint.x * w + (w / 200), fromPoint.y * h + w / 200)
      ctx.moveTo(fromPoint.x * w - (w / 200), fromPoint.y * h + w / 200)
      ctx.lineTo(fromPoint.x * w + (w / 200), fromPoint.y * h - w / 200)
    })
  }
  ctx.stroke()

  // Draw the boxes
  ctx.lineWidth = w / 500
  ctx.strokeStyle = 'black'
  ctx.beginPath()
  for (let i = 0; i < features.blocks.length; i++) {
    const block = features.blocks[i]
    ctx.moveTo(block.x * w, block.y * h)
    ctx.lineTo(block.topPoint.x * w, block.topPoint.y * h)
    ctx.moveTo(block.x * w, block.y * h)
    ctx.lineTo(block.rightPointBottom.x * w, block.rightPointBottom.y * h)
    ctx.moveTo(block.x * w, block.y * h)
    ctx.lineTo(block.leftPointBottom.x * w, block.leftPointBottom.y * h)
    ctx.moveTo(block.topPoint.x * w, block.topPoint.y * h)
    ctx.lineTo(block.rightPointTop.x * w, block.rightPointTop.y * h)
    ctx.moveTo(block.topPoint.x * w, block.topPoint.y * h)
    ctx.lineTo(block.leftPointTop.x * w, block.leftPointTop.y * h)
    ctx.moveTo(block.rightPointTop.x * w, block.rightPointTop.y * h)
    ctx.lineTo(block.rightPointBottom.x * w, block.rightPointBottom.y * h)
    ctx.moveTo(block.leftPointTop.x * w, block.leftPointTop.y * h)
    ctx.lineTo(block.leftPointBottom.x * w, block.leftPointBottom.y * h)
    ctx.moveTo(block.backBottomPoint.x * w, block.backBottomPoint.y * h)
    ctx.lineTo(block.rightPointBottom.x * w, block.rightPointBottom.y * h)
    ctx.moveTo(block.backBottomPoint.x * w, block.backBottomPoint.y * h)
    ctx.lineTo(block.leftPointBottom.x * w, block.leftPointBottom.y * h)
  }
  ctx.stroke()

  // restore the canvas
  ctx.restore()

  // Call the draw function again
  // aniFrame = window.requestAnimationFrame(drawCanvas)
}

const autoDownloadCanvas = async (showHash = false) => {
  const element = document.createElement('a')
  element.setAttribute('download', `Genuary_04_Intersections_${fxhash}`)
  element.style.display = 'none'
  document.body.appendChild(element)
  let imageBlob = null
  imageBlob = await new Promise(resolve => document.getElementById('target').toBlob(resolve, 'image/png'))
  element.setAttribute('href', window.URL.createObjectURL(imageBlob, {
    type: 'image/png'
  }))
  element.click()
  document.body.removeChild(element)
}

//  KEY PRESSED OF DOOM
document.addEventListener('keypress', async (e) => {
  e = e || window.event
  // Save
  if (e.key === 's') autoDownloadCanvas()

  //   Toggle highres mode
  if (e.key === 'h') {
    highRes = !highRes
    console.log('Highres mode is now', highRes)
    await layoutCanvas()
  }
})

//  This preloads the images so we can get access to them
// eslint-disable-next-line no-unused-vars
const preloadImages = () => {
  if (!drawn) {
    clearInterval(preloadImagesTmr)
    init()
  }
}

Project files


Home
Changelog
Page created in: 2ms