01 - Perfect loop / Infinite loop / endless GIFs
/* global preloadImagesTmr fxhash fxrand palettes noise */
//
// fxhash - Genurary 2023 - 01
//
//
// 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
let tick = 0
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
}
}
// Work out what all our features are
const makeFeatures = () => {
// Pick a random palette
features.palette = palettes[Math.floor(fxrand() * palettes.length)]
// Set how many rings we are going to have
features.rings = 32
// And how many segments in each ring
// A third of the time we'll have a handful of segments, the other half we'll have a lot
if (fxrand() < 0.33) {
// Have between 6 and 12 segments
features.segments = Math.floor(fxrand() * 6) + 6
} else {
features.segments = 64
}
// Now we need a tunnel array to hold the rings
features.tunnel = []
// We need to do some noise related stuff, we want to have an x/y offset for where we
// start in the noisefield, then a radius for the rings, and a radius for the segments
const noiseOffset = {
x: fxrand() * 2000 + 1000,
y: fxrand() * 2000 + 1000
}
// The ring radius is how far we move from the noiseOffset for each ring
const ringRadius = fxrand() * 10 + 1
// The segment radius is how far we move from the ring radius for each segment
const segmentRadius = fxrand() * 0.2 + 0.1
// We also need the radius angle, so we can work out the angle for each ring through the noise field
features.radiusAngle = 360 / features.rings
// We need to do a similar thing for modifying the radius of the tunnel
features.tunnelNoiseOffset = {
x: fxrand() * 2000 + 1000,
y: fxrand() * 2000 + 1000
}
features.tunnelNoiseRadius = fxrand() * 0.1 + 0.1
// Loop through the rings adding a new ring to the tunnel
for (let i = 0; i < features.rings; i++) {
// The ring is made up of segments, so we need an array to hold them
const ring = []
// grab the x,y position for this ring in the noise field
const ringNoisePos = {
x: noiseOffset.x + (Math.cos((features.radiusAngle * i) * (Math.PI / 180)) * ringRadius),
y: noiseOffset.y + (Math.sin((features.radiusAngle * i) * (Math.PI / 180)) * ringRadius)
}
// Loop through the segments adding a new segment to the ring
for (let j = 0; j < features.segments; j++) {
// Work out the x,y position for this segment in the noise field
const segmentNoisePos = {
x: ringNoisePos.x + (Math.cos((360 / features.segments * j) * (Math.PI / 180)) * segmentRadius),
y: ringNoisePos.y + (Math.sin((360 / features.segments * j) * (Math.PI / 180)) * segmentRadius)
}
// Get the noise value for this segment, convert it to a number between 0 and 1
const noiseVal = noise.simplex2(segmentNoisePos.x, segmentNoisePos.y) / 2 + 0.5
// The segment is made up of a position and a colour
const segment = {
// The position is a random number between 0 and 1
index: j,
// The colour is a random number between 0 and 1
colour: features.palette.colors[Math.floor(noiseVal * features.palette.colors.length)].value
}
// Add the segment to the ring
ring.push(segment)
}
const thisRing = {
index: i,
segments: ring,
position: -i
}
// Add the ring to the tunnel
features.tunnel.push(thisRing)
}
}
// 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
}
}
}
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
// Fill the canvas with black
ctx.fillStyle = '#000000'
ctx.fillRect(0, 0, w, h)
// Set the line width
ctx.lineWidth = w / 500
ctx.lineCap = 'round'
ctx.join = 'round'
// Save the canvas state
ctx.save()
// translate the canvas to the center of the screen
ctx.translate(w / 2, h / 2)
// Grab the position of the ring with index 0
let lastRing = null
for (let i = 0; i < features.tunnel.length; i++) {
if (features.tunnel[i].index === features.rings - 1) {
lastRing = features.tunnel[i]
break
}
}
const percent = 1 - (lastRing.position / -(features.rings))
const rotateRange = (percent * 90) + (tick * 90)
// Rotate the canvas the percent of 360
ctx.rotate(rotateRange * Math.PI / 180)
const wobbleOffset = Math.sin(rotateRange * 1 * Math.PI / 180)
// Loop through the rings
for (let i = features.rings - 1; i >= 0; i--) {
// Get the ring
const ring = features.tunnel[i]
// The segments are going to go around the origin in a cirlce
// so we need to work out the angle between each segment
const angle = Math.PI * 2 / features.segments
// Work out the radius of the ring, based on the position of the ring
let radiusOuter = (w / 2) * (1 - (ring.position / -features.rings))
let radiusInner = (w / 2) * (1 - ((ring.position + 1) / -features.rings))
// Now we want to add a little bit of perspective to the rings, so they bunch up the closer they are to the origin
// We do this by scaling the radius of the ring by the position of the ring
radiusOuter *= (1 + (ring.position / features.rings)) * 2
radiusInner *= (1 + ((ring.position + 1) / features.rings)) * 2
// Grab the x/y position source for the noise for this ring
// This is the starting point for the noise, and is used to offset the noise
// Loop through the segments
for (let j = 0; j < features.segments; j++) {
// Get the segment
const segment = ring.segments[j]
// Work out the four root corners of the segment, modified for the width and height
const rootCorners = {
c1: {
x: radiusInner * Math.cos(angle * segment.index) / w,
y: radiusInner * Math.sin(angle * segment.index) / h
},
c2: {
x: radiusOuter * Math.cos(angle * segment.index) / w,
y: radiusOuter * Math.sin(angle * segment.index) / h
},
c3: {
x: radiusOuter * Math.cos(angle * (segment.index + 1)) / w,
y: radiusOuter * Math.sin(angle * (segment.index + 1)) / h
},
c4: {
x: radiusInner * Math.cos(angle * (segment.index + 1)) / w,
y: radiusInner * Math.sin(angle * (segment.index + 1)) / h
}
}
// Work out the radius adjutment for each corner
const radiusAdjustment = {
c1: noise.perlin3(rootCorners.c1.x + features.tunnelNoiseOffset.x, rootCorners.c1.y + features.tunnelNoiseOffset.y, wobbleOffset) * 0.5 + 1,
c2: noise.perlin3(rootCorners.c2.x + features.tunnelNoiseOffset.x, rootCorners.c2.y + features.tunnelNoiseOffset.y, wobbleOffset) * 0.5 + 1,
c3: noise.perlin3(rootCorners.c3.x + features.tunnelNoiseOffset.x, rootCorners.c3.y + features.tunnelNoiseOffset.y, wobbleOffset) * 0.5 + 1,
c4: noise.perlin3(rootCorners.c4.x + features.tunnelNoiseOffset.x, rootCorners.c4.y + features.tunnelNoiseOffset.y, wobbleOffset) * 0.5 + 1
}
const x1 = (radiusInner * radiusAdjustment.c1) * Math.cos(angle * segment.index)
const y1 = (radiusInner * radiusAdjustment.c1) * Math.sin(angle * segment.index)
const x2 = (radiusOuter * radiusAdjustment.c2) * Math.cos(angle * segment.index)
const y2 = (radiusOuter * radiusAdjustment.c2) * Math.sin(angle * segment.index)
const x3 = (radiusOuter * radiusAdjustment.c3) * Math.cos(angle * (segment.index + 1))
const y3 = (radiusOuter * radiusAdjustment.c3) * Math.sin(angle * (segment.index + 1))
const x4 = (radiusInner * radiusAdjustment.c4) * Math.cos(angle * (segment.index + 1))
const y4 = (radiusInner * radiusAdjustment.c4) * Math.sin(angle * (segment.index + 1))
// Work out the colour of the segment, as hsl
const hsl = rgbToHsl(hexToRgb(segment.colour))
// Work out the luminosity of the segment, based on how far in the ring is
// so it tends to be darker the closer it is to the origin
const lumMod = radiusOuter / (w / 2)
// Draw the segment
ctx.beginPath()
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.lineTo(x3, y3)
ctx.lineTo(x4, y4)
ctx.closePath()
ctx.strokeStyle = `hsl(${hsl.h}, ${hsl.s * lumMod}%, ${hsl.l * lumMod}%)`
ctx.fillStyle = `hsl(${hsl.h}, ${hsl.s * lumMod}%, ${hsl.l * lumMod}%)`
ctx.fill()
ctx.stroke()
// Restore the canvas state
}
// add a little to the position of the ring
ring.position += 0.1
}
// Loop through the rings, if any of them have a position greater than 1, then we need to remove it from the array and stick it on the end
for (let i = 0; i < features.rings; i++) {
if (features.tunnel[i].position > 0) {
const ring = features.tunnel.splice(i, 1)[0]
ring.position -= features.rings
features.tunnel.push(ring)
// if that was ring0 then we need to update the tick
if (ring.index === features.rings - 1) {
tick += 1
if (tick > 3) {
console.log('Loop finished')
tick = 0
}
}
}
}
// Restore the canvas state
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_01_${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