Skip to content

Instantly share code, notes, and snippets.

@tgreiser
Created July 4, 2025 23:54
Show Gist options
  • Select an option

  • Save tgreiser/d4d624a0300f1f1ba6acb2ad7b8086c0 to your computer and use it in GitHub Desktop.

Select an option

Save tgreiser/d4d624a0300f1f1ba6acb2ad7b8086c0 to your computer and use it in GitHub Desktop.
fxhash open form with kontra
<!DOCTYPE html>
<html>
<head>
<title>open-form</title>
<meta charset="utf-8" />
<script src="./fxhash.min.js"></script>
<script src="./node_modules/kontra/kontra.min.js"></script>
<style name="style">
body,
html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
#app {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 10px;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="./index.js"></script>
</body>
</html>
/**
* To install:
* npm install kontra
*
* See: https://straker.github.io/kontra/getting-started
*/
// list of hashes in the lineageof the iteration
console.log("lineage:", $fx.lineage)
// the depth in the lineage, ie the number of ancestors
console.log("depth:", $fx.depth)
// previously, you would be using $fx.rand() to get a random number [0; 1]
// console.log("previously, $fx.rand():", $fx.rand())
// now, you can use $fx.rand( depth ) to get a random number using the seed at
// the given depth
console.log("$fx.randAt(0)", $fx.randAt(0))
// General ideas to work with open-form
// - Apply randomness in sequence for each depth, so that you keep existing
// properties of ancestors
// - You may want to first define the features of your piece, and mutate these
// features for each depth
// - You also have the option to apply the same algorithm in sequence for each
// depth if that fits your project.
// - More generally, there are no hard rules :)
// You have access to a list of hashes/randomizers for each depth, you are
// free to leverage these in any way that makes sense for your art. have fun!
// This is an example where the features are defined first, and the depth
// is used to mutate these features. In this case the features are defined as
// an array of numbers, but it can be anything :)
function getFeatures(depth) {
const options = {
color: ["red", "green", "blue", "orange", "greenyellow", "darkviolet"],
size: ["small", "medium", "big"],
}
// we use depth 0 to define the first features
const features = {
color: options.color[Math.floor($fx.randAt(0) * options.color.length)],
size: options.size[Math.floor($fx.randAt(0) * options.size.length)],
complexity: 0,
}
// we iterate through each depth to mutate some features
for (let i = 0; i <= depth; i++) {
// randomly mutate color or size
if ($fx.randAt(i) < 0.5) {
features.color =
options.color[Math.floor($fx.randAt(i) * options.color.length)]
} else {
features.size =
options.size[Math.floor($fx.randAt(i) * options.size.length)]
}
// we can also mutate a number for instance
features.complexity += $fx.randAt(i) * 0.1
console.log(`Features at depth ${i}:`, { ...features })
}
return features
}
function sizeToNumber(size) {
switch (size) {
case "small": return 100
case "medium": return 200
case "big": return 300
}
}
function getSprite(features, canvas) {
// Set base positions once using fx(hash) randomness
const baseX = $fx.randAt(0) * canvas.width * 0.5 + 0.25 * canvas.width
const baseY = $fx.randAt(0) * canvas.height * 0.25 + 0.125 * canvas.height
let sprite = new kontra.Sprite({
x: baseX,
y: baseY,
width: sizeToNumber(features.size) * $fx.randAt(0),
height: sizeToNumber(features.size) * $fx.randAt(0),
color: features.color,
})
return sprite
}
// Create a canvas element and add it to the app container
const appContainer = document.getElementById("app")
const canvas = document.createElement('canvas')
canvas.width = 640
canvas.height = 640
canvas.style.width = '100%'
canvas.style.height = '100%'
appContainer.appendChild(canvas)
// Initialize kontra with the canvas
kontra.init(canvas)
let sprites = []
let seeds = []
for (let iX = 0; iX <= $fx.depth; iX++) {
sprites.push(getSprite(getFeatures(iX), canvas))
seeds.push($fx.randAt(iX))
}
// Create a simple game loop for drawing
const loop = new kontra.GameLoop({
update: () => {
// Update logic here if needed
},
render: () => {
// Clear the canvas
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
sprites.forEach((sprite, iX) => {
// modulate by sin for bouncing effect - much slower animation
sprite.y = sprite.y + Math.sin(Date.now() * 0.01 * seeds[iX]) * 5
sprite.render()
})
}
})
loop.start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment