03 - Glitch Art
/* global preloadImagesTmr fxhash fxrand Image */
//
//
// Genuary 2023 - 03 Glitch
//
//
// 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 = {}
const nextFrame = null
let resizeTmr = null
let imageLoadingSetup = false
/* eslint-disable */
let sourceImagesLoaded = []
/* eslint-enable */
const textures = []
window.$fxhashFeatures = {}
// Work out what all our features are
const makeFeatures = () => {
// Now we pick a number of rows, something between 6 and 18
features.rows = Math.floor(fxrand() * 4) + 6
features.rowHeight = 1 / features.rows
// If we know the row height, that's the height we'll use for an equalateral triangle, so we need to work out the width
features.triangleWidth = features.rowHeight / (Math.sqrt(3) / 2)
// Now we need to work out how many triangles we can fit in a row
features.trianglesPerRow = Math.ceil(1 / features.triangleWidth) * 2
features.indsideIndex = Math.floor(fxrand() * 24).toString().padStart(2, '0')
features.outsideIndex = Math.floor(fxrand() * 24).toString().padStart(2, '0')
features.face1Index = Math.floor(fxrand() * 24).toString().padStart(2, '0')
features.face2Index = Math.floor(fxrand() * 24).toString().padStart(2, '0')
let swapInsideOutside = false
let swapFace = false
// Sometimes we'll swap things
if (fxrand() < 0.2) swapInsideOutside = true
if (fxrand() < 0.4) swapFace = true
// This holds the images we are going to load
features.sourceArray = []
// This is the inside image
let pushThis = {
type: 'inside',
index: features.indsideIndex,
swaps: []
}
// Now we need to work out where we are going to swap the images from and to
let swapChance = 0.2
if (swapFace) swapChance = 0.3
for (let i = 0; i < features.rows; i++) {
for (let j = 0; j < features.trianglesPerRow; j++) {
// There is a chance we care going to swap into this spot
if (fxrand() < swapChance) {
const toIndex = `${i}-${j}`
// Now grab somewhere to swap from
const fromIndex = `${Math.floor(fxrand() * features.rows)}-${Math.floor(fxrand() * features.trianglesPerRow)}`
pushThis.swaps.push({
from: fromIndex,
to: toIndex
})
}
}
}
features.sourceArray.push(pushThis)
// This is the outside image
pushThis = {
type: 'outside',
index: features.outsideIndex,
swaps: []
}
// Now we need to work out where we are going to swap the images from and to
swapChance = 0.1
if (swapFace) swapChance = 0.2
for (let i = 0; i < features.rows; i++) {
for (let j = 0; j < features.trianglesPerRow; j++) {
// There is a chance we care going to swap into this spot
if (fxrand() < swapChance) {
const toIndex = `${i}-${j}`
// Now grab somewhere to swap from
const fromIndex = `${Math.floor(fxrand() * features.rows)}-${Math.floor(fxrand() * (features.trianglesPerRow - 2)) + 1}`
pushThis.swaps.push({
from: fromIndex,
to: toIndex
})
}
}
}
features.sourceArray.push(pushThis)
// Now add the two faces
pushThis = {
type: 'face',
index: features.face1Index,
swaps: []
}
// For this we are going to do things slightly differently, we want to shuffle x% of the triangles around
const subset1 = features.rows * features.trianglesPerRow * 0.1
for (let i = 0; i < subset1; i++) {
// We are going to pick a random place to take something from, but bias towards the middle
const fromIndex = `${Math.floor((fxrand() * features.rows + fxrand() * features.rows) / 2)}-${Math.min(Math.max(2, Math.floor((fxrand() * features.trianglesPerRow + fxrand() * features.trianglesPerRow) / 2)), features.trianglesPerRow - 4)}`
// Meanwhile to two is just randomly picked
const toIndex = `${Math.floor(fxrand() * features.rows)}-${Math.floor(fxrand() * features.trianglesPerRow)}`
pushThis.swaps.push({
from: fromIndex,
to: toIndex
})
}
features.sourceArray.push(pushThis)
pushThis = {
type: 'face',
index: features.face2Index,
swaps: []
}
// For this we are going to do things slightly differently, we want to shuffle x% of the triangles around
const subset2 = features.rows * features.trianglesPerRow * 0.5
for (let i = 0; i < subset2; i++) {
// We are going to pick a random place to take something from, but bias towards the middle
const fromIndex = `${Math.floor((fxrand() * features.rows + fxrand() * features.rows) / 2)}-${Math.min(Math.max(2, Math.floor((fxrand() * features.trianglesPerRow + fxrand() * features.trianglesPerRow) / 2)), features.trianglesPerRow - 4)}`
// And we're going to put it somewhere else in the middle
const toIndex = `${Math.floor((fxrand() * features.rows + fxrand() * features.rows) / 2)}-${Math.floor((fxrand() * features.trianglesPerRow + fxrand() * features.trianglesPerRow) / 2)}`
pushThis.swaps.push({
from: fromIndex,
to: toIndex
})
}
features.sourceArray.push(pushThis)
// Sometimes we'll swap the inside and outside
if (swapInsideOutside) {
features.sourceArray[0].type = 'outside'
features.sourceArray[1].type = 'inside'
}
// Sometimes we'll set the first entry to be a face too
if (swapFace) {
features.sourceArray[0].type = 'face'
}
// Now we need a bunch of strips to copy from one place to another
features.strips = []
const stripCount = Math.floor(fxrand() * 100) + 100
for (let i = 0; i < stripCount; i++) {
const fromIndex = fxrand()
const toIndex = fxrand()
features.strips.push({
from: fromIndex,
to: toIndex,
shift: (fxrand() - 0.5) * 0.2
})
}
features.verts = []
const vertCount = Math.floor(fxrand() * 6)
for (let i = 0; i < vertCount; i++) {
const fromIndex = fxrand()
const toIndex = fxrand()
features.verts.push({
from: fromIndex,
to: toIndex,
shift: (fxrand() - 0.5) * 0.2
})
}
window.$fxhashFeatures.inside = features.indsideIndex
window.$fxhashFeatures.outside = features.outsideIndex
window.$fxhashFeatures.face1 = features.face1Index
window.$fxhashFeatures.face2 = features.face2Index
window.$fxhashFeatures.Background = 'outside'
if (swapInsideOutside) window.$fxhashFeatures.Background = 'inside'
if (swapFace) window.$fxhashFeatures.Background = 'face'
}
// 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 () => {
clearTimeout(resizeTmr)
resizeTmr = setTimeout(layoutCanvas, 100)
})
// Now layout the canvas
await layoutCanvas()
}
const layoutCanvas = async () => {
// Kill the next animation frame
window.cancelAnimationFrame(nextFrame)
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
}
}
}
// Adjust it to fit the rows better
if (!highRes) {
const newHeight = Math.floor(canvas.height / features.rows) * features.rows
canvas.height = newHeight
canvas.width = newHeight / 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 () => {
// Let the preloader know that we've hit this function at least once
drawn = true
// Make sure there's only one nextFrame to be called
window.cancelAnimationFrame(nextFrame)
// Grab all the canvas stuff
const canvas = document.getElementById('target')
const ctx = canvas.getContext('2d')
const w = canvas.width
const h = canvas.height
// Copy the first image from the texture array to the canvas filling it
ctx.drawImage(textures[0], 0, 0, w, h)
// Set the stroke to be white
ctx.strokeStyle = '#ffffff'
ctx.lineWidth = w / 400
// Build a map of all the triangles we need to draw, recording all the positions
// so we can run through it later
const triangleMap = {}
const xOffset = (w - (features.triangleWidth * w * features.trianglesPerRow / 2)) / 2
for (let i = 0; i < features.rows; i++) {
// Work out the y position of the row
const y = i * features.rowHeight * h
// we need to work out an x offset for the row so the triangles are centered in the middle
for (let j = 0; j < features.trianglesPerRow; j++) {
const index = `${i}-${j}`
// Work out the x position of the triangle
const x = j * features.triangleWidth * w / 2
let flipped = false
// if j is odd, flip the triangle
if (j % 2 === 1) {
flipped = true
}
// If i is odd, flip the triangle
if (i % 2 === 1) {
flipped = !flipped
}
// Work out the bounding box of the triangle
const bbox = {
x: x + xOffset,
y,
width: features.triangleWidth * w,
height: features.rowHeight * h
}
const points = []
// Work out the positions of the points of the triangle
if (flipped) {
points.push({ x: x + xOffset, y: y + features.rowHeight * h })
points.push({ x: x + features.triangleWidth * w / 2 + xOffset, y })
points.push({ x: x + features.triangleWidth * w + xOffset, y: y + features.rowHeight * h })
} else {
points.push({ x: x + xOffset, y })
points.push({ x: x + features.triangleWidth * w / 2 + xOffset, y: y + features.rowHeight * h })
points.push({ x: x + features.triangleWidth * w + xOffset, y })
}
triangleMap[index] = {
bbox,
points,
flipped
}
}
}
for (let sourceId = 0; sourceId < features.sourceArray.length; sourceId++) {
if (features.sourceArray[sourceId].swaps) {
features.sourceArray[sourceId].swaps.forEach((item, index) => {
const fromTile = triangleMap[item.from]
const toTile = triangleMap[item.to]
// Now draw the image from the texture[0] image to the canvas
if (!toTile.flipped) {
ctx.drawImage(textures[sourceId], fromTile.bbox.x / w * textures[sourceId].width, fromTile.bbox.y / h * textures[sourceId].height, fromTile.bbox.width / w * textures[sourceId].width, fromTile.bbox.height / h * textures[sourceId].height, toTile.bbox.x, toTile.bbox.y, toTile.bbox.width, toTile.bbox.height)
} else {
ctx.save()
ctx.scale(1, -1)
ctx.drawImage(textures[sourceId], fromTile.bbox.x / w * textures[sourceId].width, fromTile.bbox.y / h * textures[sourceId].height, fromTile.bbox.width / w * textures[sourceId].width, fromTile.bbox.height / h * textures[sourceId].height, toTile.bbox.x, -toTile.bbox.y, toTile.bbox.width, -toTile.bbox.height)
ctx.restore()
}
ctx.restore()
})
}
}
// Now shuffle the strips around
const stripHeight = h / 400
features.strips.forEach((item, index) => {
ctx.drawImage(canvas, 0, item.from * h, w, stripHeight, item.shift * w, item.to * h, w, stripHeight)
})
features.strips.forEach((item, index) => {
ctx.drawImage(canvas, 0, item.from * h * 0.8, w, stripHeight, item.shift * w, h - item.to * h, w, stripHeight / 2)
})
// Now shuffle the verts around
const vertWidth = w / 10
features.verts.forEach((item, index) => {
ctx.drawImage(canvas, item.from * w, 0, vertWidth, h, item.to * w, item.shift * h, vertWidth, h)
})
features.strips.forEach((item, index) => {
ctx.drawImage(canvas, 0, item.from * h * 0.8, w, stripHeight, -w / 2 + (w * item.shift), h - item.to * h, w, stripHeight / 4)
ctx.drawImage(canvas, 0, item.from * h * 0.8, w, stripHeight, -w / 2 + (w * item.shift), item.to * h, w, stripHeight / 4)
})
features.strips.forEach((item, index) => {
ctx.drawImage(canvas, item.from * w, 0, stripHeight / 4, h, item.to * w, h - (item.to * h / 3), stripHeight / 4, h)
})
const hShift = w / 5
ctx.globalCompositeOperation = 'difference'
features.verts.forEach((item, index) => {
ctx.drawImage(canvas, 0, item.from * h, w, hShift, (item.shift * w), item.from * h, w, hShift)
})
ctx.globalCompositeOperation = 'source-over'
}
const autoDownloadCanvas = async (showHash = false) => {
const element = document.createElement('a')
element.setAttribute('download', `Genuary-03-Glitch_${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 = () => {
// We need to load in four images based on the features.textureArray, we need to
// have a load event for each of them, and then we need to check if all four have
// loaded before we kick off the init function by setting the textureXLoaded to true
// for each one
if (!imageLoadingSetup) {
features.sourceArray.forEach((texture, index) => {
sourceImagesLoaded[index] = false
textures[index] = new Image()
textures[index].onload = () => {
// eslint-disable-next-line no-eval
eval(`sourceImagesLoaded[${index}] = true`)
}
textures[index].src = `./source/${texture.type}/image${texture.index}.jpg`
})
}
imageLoadingSetup = true
let allSourceImagesLoaded = true
// Check to see if all the images have loaded
sourceImagesLoaded.forEach((loaded) => {
if (!loaded) allSourceImagesLoaded = false
})
// If paper1 has loaded and we haven't draw anything yet, then kick it all off
if (allSourceImagesLoaded && !drawn) {
clearInterval(preloadImagesTmr)
init()
}
}
Home
Changelog
Page created in: 1ms