REVDANCATT'S GENUARY 2023

07 - Sample a color palette from your favorite movie/album cover

/* global preloadImagesTmr fxhash fxrand */

//
//  fxhash - Genuary 07 Sample a color palette - Kiss Me, Kiss Me, Kiss Me
//
//
//  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 = {}

const hexToRgb = (hex) => {
  const result = /([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
  return {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  }
}

const rgbToHsl = (rgb) => {
  rgb.r /= 255
  rgb.g /= 255
  rgb.b /= 255
  const max = Math.max(rgb.r, rgb.g, rgb.b)
  const min = Math.min(rgb.r, rgb.g, rgb.b)
  let h
  let s
  const l = (max + min) / 2

  if (max === min) {
    h = s = 0 // achromatic
  } else {
    const d = max - min
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
    switch (max) {
      case rgb.r:
        h = (rgb.g - rgb.b) / d + (rgb.g < rgb.b ? 6 : 0)
        break
      case rgb.g:
        h = (rgb.b - rgb.r) / d + 2
        break
      case rgb.b:
        h = (rgb.r - rgb.g) / d + 4
        break
    }
    h /= 6
  }

  return {
    h: h * 360,
    s: s * 100,
    l: l * 100
  }
}

// We want to good shuffle array function
const shuffleArray = (a) => {
  const array = JSON.parse(JSON.stringify(a))
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(fxrand() * (i + 1))
    const temp = array[i]
    array[i] = array[j]
    array[j] = temp
  }
  return array
}

//  Work out what all our features are
const makeFeatures = () => {
  features.palette = ['#37313F', '#515EA0', '#3E3F6D', '#3D73AE', '#C5B04E', '#AD7FAB', '#E244BF', '#ACADC2'] // Sigue Sigue Sputnik - Flaunt It
  features.palette = ['#7CABD1', '#C6D7E8', '#5896C0', '#97B9D8', '#BFA984', '#8BA9BE', '#F4F9F9', '#785D4B'] // Jean Michel Jarre - Oxygene
  features.palette = ['#160303', '#3A0F0A', '#C7C7B3', '#B7AF9B', '#63160A', '#C02809', '#1D0D08'] // Gwenno - Le Kov
  features.palette = ['#f8eae2', '#e3aa54', '#cb4f15', '#ae0113', '#eabf76', '#b40110', '#ebc29a', '#be180e', '#c4330d'] // Kiss Me, Kiss Me, Kiss Me

  // We want to have a grid between 4 and 7
  features.gridSize = Math.floor(fxrand() * 4) + 4
  // There is a slim chance to double that
  if (fxrand() < 0.15) features.gridSize *= 2

  // Loop throught the grid size on the x and y to create the grid
  features.grid = []
  for (let x = 0; x < features.gridSize; x++) {
    for (let y = 0; y < features.gridSize; y++) {
      const innerColour = features.palette[Math.floor(fxrand() * features.palette.length)]
      const outerColour = features.palette[Math.floor(fxrand() * features.palette.length)]
      const cornerOffsets = {
        tl: {
          x: fxrand(),
          y: fxrand()
        },
        tr: {
          x: fxrand(),
          y: fxrand()
        },
        bl: {
          x: fxrand(),
          y: fxrand()
        },
        br: {
          x: fxrand(),
          y: fxrand()
        }
      }
      const extraDarkChance = 0.2
      features.grid.push({
        x,
        y,
        innerColour,
        outerColour,
        cornerOffsets,
        extraDarkChance: [fxrand() < extraDarkChance, fxrand() < extraDarkChance, fxrand() < extraDarkChance, fxrand() < extraDarkChance],
        hasCircles: fxrand() < 0.45,
        circleColours: shuffleArray(features.palette)
      })
    }
  }
}

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

  // Round the canvas size to take into account the number of tiles
  if (!highRes) {
    const roundingValue = features.gridSize
    canvas.width = Math.floor(canvas.width / roundingValue) * roundingValue
    canvas.height = Math.floor(canvas.height / roundingValue) * roundingValue
  }

  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 () => {
  // 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

  // Get the size of the tiles
  const tileSize = w / features.gridSize
  // Loop through the grid and draw the tiles
  for (let i = 0; i < features.grid.length; i++) {
    const tile = features.grid[i]
    const x = tile.x * tileSize
    const y = tile.y * tileSize

    // Draw the inner colour
    ctx.fillStyle = `${tile.outerColour}`
    ctx.fillRect(x, y, tileSize, tileSize)

    // Work out the corners of the inner tile
    // The inner adjustment modifies the corners of the inner tile
    const innerAdjustment = 0.1
    const minAmount = 0.075
    const tl = {
      x: x + (tileSize * (tile.cornerOffsets.tl.x * innerAdjustment + minAmount)),
      y: y + (tileSize * (tile.cornerOffsets.tl.y * innerAdjustment + minAmount))
    }
    const tr = {
      x: x + (tileSize * (1 - (tile.cornerOffsets.tr.x * innerAdjustment + minAmount))),
      y: y + (tileSize * (tile.cornerOffsets.tr.y * innerAdjustment + minAmount))
    }
    const bl = {
      x: x + (tileSize * (tile.cornerOffsets.bl.x * innerAdjustment + minAmount)),
      y: y + (tileSize * (1 - (tile.cornerOffsets.bl.y * innerAdjustment + minAmount)))
    }
    const br = {
      x: x + (tileSize * (1 - (tile.cornerOffsets.br.x * innerAdjustment + minAmount))),
      y: y + (tileSize * (1 - (tile.cornerOffsets.br.y * innerAdjustment + minAmount)))
    }

    // Draw the inner tile
    ctx.fillStyle = `${tile.innerColour}`
    ctx.beginPath()
    ctx.moveTo(tl.x, tl.y)
    ctx.lineTo(tr.x, tr.y)
    ctx.lineTo(br.x, br.y)
    ctx.lineTo(bl.x, bl.y)
    ctx.closePath()
    ctx.fill()

    // Make a darker version of the outer colour
    const darkerColourHSL = rgbToHsl(hexToRgb(tile.outerColour))
    // Draw the top part of the outer tile, using the top two tile points, and the top two inner corner points
    ctx.fillStyle = `hsl(${darkerColourHSL.h},${darkerColourHSL.s}%,${darkerColourHSL.l * 0.9}%)`
    if (tile.extraDarkChance[0]) ctx.fillStyle = `hsl(${darkerColourHSL.h},${darkerColourHSL.s}%,${darkerColourHSL.l * 0.1}%)`
    ctx.beginPath()
    ctx.moveTo(x, y)
    ctx.lineTo(x + tileSize, y)
    ctx.lineTo(tr.x, tr.y)
    ctx.lineTo(tl.x, tl.y)
    ctx.closePath()
    ctx.fill()
    // Draw the left side
    ctx.fillStyle = `hsl(${darkerColourHSL.h},${darkerColourHSL.s}%,${darkerColourHSL.l * 0.95}%)`
    if (tile.extraDarkChance[1]) ctx.fillStyle = `hsl(${darkerColourHSL.h},${darkerColourHSL.s}%,${darkerColourHSL.l * 0.2}%)`
    ctx.beginPath()
    ctx.moveTo(x, y)
    ctx.lineTo(x, y + tileSize)
    ctx.lineTo(bl.x, bl.y)
    ctx.lineTo(tl.x, tl.y)
    ctx.closePath()
    ctx.fill()
    // Draw the right side
    ctx.fillStyle = `hsl(${darkerColourHSL.h},${darkerColourHSL.s}%,${darkerColourHSL.l}%)`
    if (tile.extraDarkChance[2]) ctx.fillStyle = `hsl(${darkerColourHSL.h},${darkerColourHSL.s}%,${darkerColourHSL.l * 0.4}%)`
    ctx.beginPath()
    ctx.moveTo(x + tileSize, y)
    ctx.lineTo(x + tileSize, y + tileSize)
    ctx.lineTo(br.x, br.y)
    ctx.lineTo(tr.x, tr.y)
    ctx.closePath()
    ctx.fill()
    // Draw the bottom side
    ctx.fillStyle = `hsl(${darkerColourHSL.h},${darkerColourHSL.s}%,${darkerColourHSL.l * 1.05}%)`
    if (tile.extraDarkChance[3]) ctx.fillStyle = `hsl(${darkerColourHSL.h},${darkerColourHSL.s}%,${darkerColourHSL.l * 0.6}%)`
    ctx.beginPath()
    ctx.moveTo(x, y + tileSize)
    ctx.lineTo(x + tileSize, y + tileSize)
    ctx.lineTo(br.x, br.y)
    ctx.lineTo(bl.x, bl.y)
    ctx.closePath()
    ctx.fill()

    // If the tiles has circles, then we need to create a clipping path from the inner tile
    // and then draw a range of decreasing circles
    if (tile.hasCircles) {
      ctx.save()
      ctx.beginPath()
      ctx.moveTo(tl.x, tl.y)
      ctx.lineTo(tr.x, tr.y)
      ctx.lineTo(br.x, br.y)
      ctx.lineTo(bl.x, bl.y)
      ctx.closePath()
      ctx.clip()
      const circleCount = features.palette.length
      for (let i = circleCount; i > 0; i--) {
        const circleX = x + (tileSize / 2)
        const circleY = y + (tileSize / 2)
        const circleRadius = i / circleCount * (tileSize / 2) * 0.6
        ctx.beginPath()
        ctx.arc(circleX, circleY, circleRadius, 0, 2 * Math.PI)
        ctx.fillStyle = tile.circleColours[i]
        ctx.fill()
      }
      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_07_${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: 0ms