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()
}
}
Home
Changelog
Page created in: 0ms