REVDANCATT'S GENUARY 2023

08 - Signed Distance Functions

/* global preloadImagesTmr fxhash fxrand palettes */

//
//  fxhash - Genuary 08 SDF
//
//
//  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
let aniFrame = null

window.$fxhashFeatures = {}

//  Work out what all our features are
const makeFeatures = () => {
  // Grab two different palettes
  const inPaletteIndex = Math.floor(fxrand() * palettes.length)
  let outPaletteIndex = inPaletteIndex
  while (outPaletteIndex === inPaletteIndex) outPaletteIndex = Math.floor(fxrand() * palettes.length)
  features.inColour = palettes[inPaletteIndex].colors
  features.outColour = palettes[outPaletteIndex].colors
  //   Don't always use the colours
  features.useColour = fxrand() < 0.9
  // How big do our stacks start
  features.orignalStartSize = 0.8
  features.startSize = 0.8
  features.density = fxrand() * 100 + 1
  if (fxrand() < 0.75) {
    Math.ceil(features.density *= 0.1)
    if (fxrand() < 0.5) {
      Math.ceil(features.density *= 0.1)
      if (fxrand() < 0.25) Math.ceil(features.density *= 0.1)
    }
  }

  // We now want 100,000 random numbers in an array, so we can pull them
  // and use them when we need. We can reset the pointer to 0 when we need
  // to start again
  features.rndPointer = 0
  features.rndArray = []
  for (let i = 0; i < 10000000; i++) features.rndArray.push(fxrand())
}

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

console.log(features)
console.log(features.density)
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`

  // Reset things
  drawn = false
  features.startSize = features.orignalStartSize
  features.rndPointer = 0
  //  And draw it!!
  drawCanvas()
}

// Get the distance between two points
const getDistance = (x1, y1, x2, y2) => {
  const a = x1 - x2
  const b = y1 - y2
  return (a * a + b * b) ** 0.5
}

const sdf = (x, y, cx, cy, r) => {
  return getDistance(x, y, cx, cy) - r
}

const sdfRepeat = (x, r) => {
  x /= r
  x -= Math.floor(x)
  return Math.min(x, 1.0 - x) * r
}

const drawCanvas = async () => {
  // Clear the animation frame
  if (aniFrame) window.cancelAnimationFrame(aniFrame)

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

  // If we haven't drawn yet then fill the canvas
  if (!drawn) {
    ctx.fillStyle = 'white'
    ctx.fillRect(0, 0, canvas.width, canvas.height)
  }
  //  Let the preloader know that we've hit this function at least once
  drawn = true

  // Translate the canvas to the center
  ctx.save()
  ctx.translate(w / 2, h / 2)

  ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)'
  ctx.lineWidth = w / 1000

  // do this 100 times
  for (let i = 0; i < features.density; i++) {
    // Create a random point between -0.5 and 0.5
    const point = {
      x: features.rndArray[features.rndPointer] - 0.5,
      y: features.rndArray[features.rndPointer + 1] - 0.5
    }
    features.rndPointer += 2
    if (features.rndPointer >= features.rndArray.length) features.rndPointer = 0

    // use the sdf function to get the value
    let value = sdf(point.x, point.y, 0, 0, 0.4)
    value = Math.abs(sdfRepeat(value, 0.5)) - 0.1
    let pickPalette = null
    if (value < -0.05) pickPalette = features.inColour
    if (value > 0.05) pickPalette = features.outColour

    if (Math.abs(value) > 0.05) {
      // Draw a square centred on the point
      // Work out the four corners
      const cornerMod = 0.125
      const corners = {
        tl: {
          x: point.x - features.startSize * cornerMod,
          y: point.y - features.startSize * cornerMod
        },
        tr: {
          x: point.x + features.startSize * cornerMod,
          y: point.y - features.startSize * cornerMod
        },
        bl: {
          x: point.x - features.startSize * cornerMod,
          y: point.y + features.startSize * cornerMod
        },
        br: {
          x: point.x + features.startSize * cornerMod,
          y: point.y + features.startSize * cornerMod
        }
      }

      // Grab the colours
      ctx.fillStyle = pickPalette[Math.floor(features.rndArray[features.rndPointer] * pickPalette.length)].value
      if (!features.useColour) {
        ctx.fillStyle = '#EEEEEE'
        if (value < -0.05) ctx.fillStyle = '#BBBBBB'
      }

      features.rndPointer++

      // Now draw the square
      ctx.beginPath()
      ctx.moveTo(corners.tl.x * w, corners.tl.y * h)
      ctx.lineTo(corners.tr.x * w, corners.tr.y * h)
      ctx.lineTo(corners.br.x * w, corners.br.y * h)
      ctx.lineTo(corners.bl.x * w, corners.bl.y * h)
      ctx.lineTo(corners.tl.x * w, corners.tl.y * h)
      ctx.fill()
      ctx.stroke()

      // Now draw the shadow
      const shadowDist = 0.01
      ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'
      ctx.beginPath()
      ctx.lineTo(corners.tr.x * w, corners.tr.y * h)
      ctx.lineTo(corners.tr.x * w + w * shadowDist / 2, corners.tr.y * h + h * shadowDist)
      ctx.lineTo(corners.br.x * w + w * shadowDist / 2, corners.br.y * h + h * shadowDist)
      ctx.lineTo(corners.bl.x * w + w * shadowDist / 2, corners.bl.y * h + h * shadowDist)
      ctx.lineTo(corners.bl.x * w, corners.bl.y * h)
      ctx.lineTo(corners.br.x * w, corners.br.y * h)
      ctx.lineTo(corners.tr.x * w, corners.tr.y * h)
      ctx.fill()
    }
  }
  // restore the context to its untranslated/unrotated state
  ctx.restore()
  // Call the draw function again
  features.startSize *= 0.99
  if (features.startSize > 0.01) {
    aniFrame = window.requestAnimationFrame(drawCanvas)
  }
}

const autoDownloadCanvas = async (showHash = false) => {
  const element = document.createElement('a')
  element.setAttribute('download', `Genuary_08_${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: 1ms