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