REVDANCATT'S GENUARY 2023

03 - Glitch Art

/* global preloadImagesTmr fxhash fxrand Image */
//
//
//  Genuary 2023 - 03 Glitch
//
//
//  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 = {}
const nextFrame = null
let resizeTmr = null
let imageLoadingSetup = false
/* eslint-disable */
let sourceImagesLoaded = []
/* eslint-enable */
const textures = []

window.$fxhashFeatures = {}

//  Work out what all our features are
const makeFeatures = () => {
  // Now we pick a number of rows, something between 6 and 18
  features.rows = Math.floor(fxrand() * 4) + 6
  features.rowHeight = 1 / features.rows
  //  If we know the row height, that's the height we'll use for an equalateral triangle, so we need to work out the width
  features.triangleWidth = features.rowHeight / (Math.sqrt(3) / 2)
  //  Now we need to work out how many triangles we can fit in a row
  features.trianglesPerRow = Math.ceil(1 / features.triangleWidth) * 2

  features.indsideIndex = Math.floor(fxrand() * 24).toString().padStart(2, '0')
  features.outsideIndex = Math.floor(fxrand() * 24).toString().padStart(2, '0')
  features.face1Index = Math.floor(fxrand() * 24).toString().padStart(2, '0')
  features.face2Index = Math.floor(fxrand() * 24).toString().padStart(2, '0')

  let swapInsideOutside = false
  let swapFace = false
  // Sometimes we'll swap things
  if (fxrand() < 0.2) swapInsideOutside = true
  if (fxrand() < 0.4) swapFace = true

  // This holds the images we are going to load
  features.sourceArray = []

  // This is the inside image
  let pushThis = {
    type: 'inside',
    index: features.indsideIndex,
    swaps: []
  }
  // Now we need to work out where we are going to swap the images from and to
  let swapChance = 0.2
  if (swapFace) swapChance = 0.3
  for (let i = 0; i < features.rows; i++) {
    for (let j = 0; j < features.trianglesPerRow; j++) {
      // There is a chance we care going to swap into this spot
      if (fxrand() < swapChance) {
        const toIndex = `${i}-${j}`
        // Now grab somewhere to swap from
        const fromIndex = `${Math.floor(fxrand() * features.rows)}-${Math.floor(fxrand() * features.trianglesPerRow)}`
        pushThis.swaps.push({
          from: fromIndex,
          to: toIndex
        })
      }
    }
  }
  features.sourceArray.push(pushThis)

  // This is the outside image
  pushThis = {
    type: 'outside',
    index: features.outsideIndex,
    swaps: []
  }
  // Now we need to work out where we are going to swap the images from and to
  swapChance = 0.1
  if (swapFace) swapChance = 0.2
  for (let i = 0; i < features.rows; i++) {
    for (let j = 0; j < features.trianglesPerRow; j++) {
      // There is a chance we care going to swap into this spot
      if (fxrand() < swapChance) {
        const toIndex = `${i}-${j}`
        // Now grab somewhere to swap from
        const fromIndex = `${Math.floor(fxrand() * features.rows)}-${Math.floor(fxrand() * (features.trianglesPerRow - 2)) + 1}`
        pushThis.swaps.push({
          from: fromIndex,
          to: toIndex
        })
      }
    }
  }
  features.sourceArray.push(pushThis)

  // Now add the two faces
  pushThis = {
    type: 'face',
    index: features.face1Index,
    swaps: []
  }
  // For this we are going to do things slightly differently, we want to shuffle x% of the triangles around
  const subset1 = features.rows * features.trianglesPerRow * 0.1
  for (let i = 0; i < subset1; i++) {
    // We are going to pick a random place to take something from, but bias towards the middle
    const fromIndex = `${Math.floor((fxrand() * features.rows + fxrand() * features.rows) / 2)}-${Math.min(Math.max(2, Math.floor((fxrand() * features.trianglesPerRow + fxrand() * features.trianglesPerRow) / 2)), features.trianglesPerRow - 4)}`
    // Meanwhile to two is just randomly picked
    const toIndex = `${Math.floor(fxrand() * features.rows)}-${Math.floor(fxrand() * features.trianglesPerRow)}`
    pushThis.swaps.push({
      from: fromIndex,
      to: toIndex
    })
  }
  features.sourceArray.push(pushThis)

  pushThis = {
    type: 'face',
    index: features.face2Index,
    swaps: []
  }
  // For this we are going to do things slightly differently, we want to shuffle x% of the triangles around
  const subset2 = features.rows * features.trianglesPerRow * 0.5
  for (let i = 0; i < subset2; i++) {
    // We are going to pick a random place to take something from, but bias towards the middle
    const fromIndex = `${Math.floor((fxrand() * features.rows + fxrand() * features.rows) / 2)}-${Math.min(Math.max(2, Math.floor((fxrand() * features.trianglesPerRow + fxrand() * features.trianglesPerRow) / 2)), features.trianglesPerRow - 4)}`
    // And we're going to put it somewhere else in the middle
    const toIndex = `${Math.floor((fxrand() * features.rows + fxrand() * features.rows) / 2)}-${Math.floor((fxrand() * features.trianglesPerRow + fxrand() * features.trianglesPerRow) / 2)}`
    pushThis.swaps.push({
      from: fromIndex,
      to: toIndex
    })
  }
  features.sourceArray.push(pushThis)

  // Sometimes we'll swap the inside and outside
  if (swapInsideOutside) {
    features.sourceArray[0].type = 'outside'
    features.sourceArray[1].type = 'inside'
  }

  // Sometimes we'll set the first entry to be a face too
  if (swapFace) {
    features.sourceArray[0].type = 'face'
  }

  // Now we need a bunch of strips to copy from one place to another
  features.strips = []
  const stripCount = Math.floor(fxrand() * 100) + 100
  for (let i = 0; i < stripCount; i++) {
    const fromIndex = fxrand()
    const toIndex = fxrand()
    features.strips.push({
      from: fromIndex,
      to: toIndex,
      shift: (fxrand() - 0.5) * 0.2
    })
  }

  features.verts = []
  const vertCount = Math.floor(fxrand() * 6)
  for (let i = 0; i < vertCount; i++) {
    const fromIndex = fxrand()
    const toIndex = fxrand()
    features.verts.push({
      from: fromIndex,
      to: toIndex,
      shift: (fxrand() - 0.5) * 0.2
    })
  }

  window.$fxhashFeatures.inside = features.indsideIndex
  window.$fxhashFeatures.outside = features.outsideIndex
  window.$fxhashFeatures.face1 = features.face1Index
  window.$fxhashFeatures.face2 = features.face2Index
  window.$fxhashFeatures.Background = 'outside'
  if (swapInsideOutside) window.$fxhashFeatures.Background = 'inside'
  if (swapFace) window.$fxhashFeatures.Background = 'face'
}

//  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 () => {
    clearTimeout(resizeTmr)
    resizeTmr = setTimeout(layoutCanvas, 100)
  })

  //  Now layout the canvas
  await layoutCanvas()
}

const layoutCanvas = async () => {
  //  Kill the next animation frame
  window.cancelAnimationFrame(nextFrame)

  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
      }
    }
  }

  // Adjust it to fit the rows better
  if (!highRes) {
    const newHeight = Math.floor(canvas.height / features.rows) * features.rows
    canvas.height = newHeight
    canvas.width = newHeight / 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 drawCanvas = async () => {
  //  Let the preloader know that we've hit this function at least once
  drawn = true
  //  Make sure there's only one nextFrame to be called
  window.cancelAnimationFrame(nextFrame)

  // Grab all the canvas stuff
  const canvas = document.getElementById('target')
  const ctx = canvas.getContext('2d')
  const w = canvas.width
  const h = canvas.height

  // Copy the first image from the texture array to the canvas filling it
  ctx.drawImage(textures[0], 0, 0, w, h)

  // Set the stroke to be white
  ctx.strokeStyle = '#ffffff'
  ctx.lineWidth = w / 400

  // Build a map of all the triangles we need to draw, recording all the positions
  // so we can run through it later
  const triangleMap = {}

  const xOffset = (w - (features.triangleWidth * w * features.trianglesPerRow / 2)) / 2
  for (let i = 0; i < features.rows; i++) {
    //  Work out the y position of the row
    const y = i * features.rowHeight * h
    // we need to work out an x offset for the row so the triangles are centered in the middle

    for (let j = 0; j < features.trianglesPerRow; j++) {
      const index = `${i}-${j}`

      //  Work out the x position of the triangle
      const x = j * features.triangleWidth * w / 2

      let flipped = false
      // if j is odd, flip the triangle
      if (j % 2 === 1) {
        flipped = true
      }
      // If i is odd, flip the triangle
      if (i % 2 === 1) {
        flipped = !flipped
      }

      // Work out the bounding box of the triangle
      const bbox = {
        x: x + xOffset,
        y,
        width: features.triangleWidth * w,
        height: features.rowHeight * h
      }

      const points = []
      // Work out the positions of the points of the triangle
      if (flipped) {
        points.push({ x: x + xOffset, y: y + features.rowHeight * h })
        points.push({ x: x + features.triangleWidth * w / 2 + xOffset, y })
        points.push({ x: x + features.triangleWidth * w + xOffset, y: y + features.rowHeight * h })
      } else {
        points.push({ x: x + xOffset, y })
        points.push({ x: x + features.triangleWidth * w / 2 + xOffset, y: y + features.rowHeight * h })
        points.push({ x: x + features.triangleWidth * w + xOffset, y })
      }
      triangleMap[index] = {
        bbox,
        points,
        flipped
      }
    }
  }

  for (let sourceId = 0; sourceId < features.sourceArray.length; sourceId++) {
    if (features.sourceArray[sourceId].swaps) {
      features.sourceArray[sourceId].swaps.forEach((item, index) => {
        const fromTile = triangleMap[item.from]
        const toTile = triangleMap[item.to]
        // Now draw the image from the texture[0] image to the canvas
        if (!toTile.flipped) {
          ctx.drawImage(textures[sourceId], fromTile.bbox.x / w * textures[sourceId].width, fromTile.bbox.y / h * textures[sourceId].height, fromTile.bbox.width / w * textures[sourceId].width, fromTile.bbox.height / h * textures[sourceId].height, toTile.bbox.x, toTile.bbox.y, toTile.bbox.width, toTile.bbox.height)
        } else {
          ctx.save()
          ctx.scale(1, -1)
          ctx.drawImage(textures[sourceId], fromTile.bbox.x / w * textures[sourceId].width, fromTile.bbox.y / h * textures[sourceId].height, fromTile.bbox.width / w * textures[sourceId].width, fromTile.bbox.height / h * textures[sourceId].height, toTile.bbox.x, -toTile.bbox.y, toTile.bbox.width, -toTile.bbox.height)
          ctx.restore()
        }
        ctx.restore()
      })
    }
  }

  // Now shuffle the strips around
  const stripHeight = h / 400
  features.strips.forEach((item, index) => {
    ctx.drawImage(canvas, 0, item.from * h, w, stripHeight, item.shift * w, item.to * h, w, stripHeight)
  })
  features.strips.forEach((item, index) => {
    ctx.drawImage(canvas, 0, item.from * h * 0.8, w, stripHeight, item.shift * w, h - item.to * h, w, stripHeight / 2)
  })

  // Now shuffle the verts around
  const vertWidth = w / 10
  features.verts.forEach((item, index) => {
    ctx.drawImage(canvas, item.from * w, 0, vertWidth, h, item.to * w, item.shift * h, vertWidth, h)
  })

  features.strips.forEach((item, index) => {
    ctx.drawImage(canvas, 0, item.from * h * 0.8, w, stripHeight, -w / 2 + (w * item.shift), h - item.to * h, w, stripHeight / 4)
    ctx.drawImage(canvas, 0, item.from * h * 0.8, w, stripHeight, -w / 2 + (w * item.shift), item.to * h, w, stripHeight / 4)
  })

  features.strips.forEach((item, index) => {
    ctx.drawImage(canvas, item.from * w, 0, stripHeight / 4, h, item.to * w, h - (item.to * h / 3), stripHeight / 4, h)
  })

  const hShift = w / 5
  ctx.globalCompositeOperation = 'difference'
  features.verts.forEach((item, index) => {
    ctx.drawImage(canvas, 0, item.from * h, w, hShift, (item.shift * w), item.from * h, w, hShift)
  })
  ctx.globalCompositeOperation = 'source-over'
}

const autoDownloadCanvas = async (showHash = false) => {
  const element = document.createElement('a')
  element.setAttribute('download', `Genuary-03-Glitch_${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 = () => {
  // We need to load in four images based on the features.textureArray, we need to
  // have a load event for each of them, and then we need to check if all four have
  // loaded before we kick off the init function by setting the textureXLoaded to true
  // for each one
  if (!imageLoadingSetup) {
    features.sourceArray.forEach((texture, index) => {
      sourceImagesLoaded[index] = false
      textures[index] = new Image()
      textures[index].onload = () => {
        // eslint-disable-next-line no-eval
        eval(`sourceImagesLoaded[${index}] = true`)
      }
      textures[index].src = `./source/${texture.type}/image${texture.index}.jpg`
    })
  }
  imageLoadingSetup = true

  let allSourceImagesLoaded = true
  // Check to see if all the images have loaded
  sourceImagesLoaded.forEach((loaded) => {
    if (!loaded) allSourceImagesLoaded = false
  })

  //  If paper1 has loaded and we haven't draw anything yet, then kick it all off
  if (allSourceImagesLoaded && !drawn) {
    clearInterval(preloadImagesTmr)
    init()
  }
}

Project files


Home
Changelog
Page created in: 1ms