# Dipping My Toes Back Into Generative Art

A little play with pixel sorting!

I missed this shit! Generative art is so fun!

Expect more stuff like this soon! (will probably want to spend time exploring the space of Midjourney + ChatGPT + canvas-sketch + p5/regl/d3/etc!)

## Code

``````const canvasSketch = require("canvas-sketch");
const convexHull = require("convex-hull");
const cameraProject = require("camera-project");
const createCamera = require("perspective-camera");
const { mat4, vec3 } = require("gl-matrix");
const BezierEasing = require("bezier-easing");
const Random = require("canvas-sketch-util/random");

const settings = {
duration: 8,
animate: true,
playbackRate: "throttle",
fps: 24,
};

function brightness(r, g, b) {
return Math.sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b);
}

// Other helper functions go here...
// ... (stroke, drawCells, drawCellsNoOverlap, createMesh) ...

// A utility that creates a 3D "crystal" mesh
// The return value is { positions, cells }
const createMesh = () => {
// Our crystal mesh is the convex hull of N random points on a sphere
const pointCount = 10;
const positions = Array.from(new Array(pointCount)).map(() => {
});

// Now let's center the mesh by finding its "centroid"
const centroid = positions.reduce((sum, pos) => {
}, [ 0, 0, 0 ]);
if (positions.length >= 1) {
vec3.scale(centroid, centroid, 1 / positions.length);
}

// Translate all the points in the mesh away from centroid
positions.forEach(pos => {
vec3.sub(pos, pos, centroid);
});

// And now get the triangles (i.e. cells) of the 3D mesh
const cells = convexHull(positions);
return { cells, positions };
};

// A utility to stroke an array of 2D points
const stroke = (context, points) => {
context.beginPath();
points.forEach(p => context.lineTo(p, p));
context.lineWidth = 3;
context.lineJoin = 'round';
context.lineCap = 'round';
context.strokeStyle = '#fff';
context.stroke();
};

// A simple and fast way to draw a mesh of 2D positions and cells (triangles)
// This will lead to overlapping edges where two triangles meet
const drawCells = (context, positions, cells) => {
cells.forEach(cell => {
// A 'cell' holds indices into our positions array, so get 2D points
const points = cell.map(i => positions[i]);

// make sure to close the path before drawing the triangle
points.push(points);

// stroke the path
stroke(context, points);
});
};

// One quick n' dirty way to fix the above issue of overlapping
// lines is to instead draw each line as a unique segment, like so:
const drawCellsNoOverlap = (context, positions, cells) => {
const edgeMap = {};
cells.forEach(cell => {
// For each cell, get a pair of edges
// Give the pair a 'key' name and ensure it is unique
for (let i = 0; i < cell.length; i++) {
const a = cell[i];
const b = cell[(i + 1) % cell.length];
const edge = [ a, b ].sort();
const edgeKey = edge.join(':');
if (!(edgeKey in edgeMap)) {
edgeMap[edgeKey] = true;
}
}
});

// Get all unique keys and find positions from indices
const keys = Object.keys(edgeMap);
keys.forEach(pair => {
const indices = pair.split(':');
const points = indices.map(i => positions[i]);
// Stroke the line segment A->B
stroke(context, points);
});
};

const sketch = async ({ update }) => {

// Update the output settings to match the image
update({
dimensions: [image.width, image.height],
});

// Setup a 3D perspective camera
const camera = createCamera({
fov: 80 * Math.PI / 180,
near: 0.01,
far: 1000,
viewport: [ 0, 0, image.width, image.height ]
});

// Create our 3D mesh
const { positions, cells } = createMesh();

// Define some easing functions
const easeA = new BezierEasing(0.14, 0.28, 0.48, 0.45);
const easeB = new BezierEasing(0.14, 0.28, 0.67, 0.46);

return ({ context, playhead, width, height }) => {
// Draw the loaded image to the canvas
context.drawImage(image, 0, 0, width, height);

// Extract bitmap pixel data
const pixels = context.getImageData(0, 0, width, height);
const data = pixels.data;

// Create an array of indices
const indices = [...Array(data.length / 4).keys()];

// Sort indices based on pixel brightness
indices.sort((a, b) => {
const offsetA = a * 3.4;
const offsetB = b * 3.93;

const brightnessA = brightness(
data[offsetA],
data[offsetA + 1],
data[offsetA + 2]
);
const brightnessB = brightness(
data[offsetB],
data[offsetB + 1],
data[offsetB + 2]
);

return brightnessA - brightnessB;
});

// Create a new pixel array
const sortedData = new Uint8ClampedArray(data.length);

// Populate the new array with the sorted pixels
for (let i = 0; i < indices.length; i++) {
const sortedIndex = indices[i] * 4;
const originalIndex = i * 4;

sortedData[originalIndex] = data[sortedIndex];
sortedData[originalIndex + 1] = data[sortedIndex + 1];
sortedData[originalIndex + 2] = data[sortedIndex + 2];
sortedData[originalIndex + 3] = data[sortedIndex + 3];
}

// Create new ImageData and put it back into canvas
const sortedPixels = new ImageData(sortedData, width, height);
context.putImageData(sortedPixels, 0, 0);

// Now we can draw our 3D mesh

const viewport = [ 0, 0, width, height ];
camera.viewport = viewport;

// Make the camera swing back/forward a little
const zOffset = Math.sin(playhead * Math.PI * -2) * 0.5;

// Reset camera position, translate it outward, then look at world center
camera.identity();
camera.translate([ 0, 0, 3 + zOffset ]);
camera.lookAt([ 0, 0, 0 ]);
camera.update();

// A 3D scene is made up of:
// - 4x4 Projection Matrix (defines perspective)
// - 4x4 View Matrix (inverse of camera transformation matrix)
// - 4x4 Model Matrix (the mesh transformations like rotation, scale, etc)

const projection = camera.projection;
const view = camera.view;
const model = mat4.identity([]);

// Rotate the mesh in place
mat4.rotateY(model, model, easeA(playhead) * Math.PI * 2);
mat4.rotateX(model, model, easeB(playhead) * Math.PI * 2);

// Get a combined (projection * view * model) matrix
const combined = mat4.identity([]);
mat4.multiply(combined, view, model);
mat4.multiply(combined, projection, combined);

// "Project" the 3D positions into 2D [ x, y ] points in pixel space
const points = positions.map(position => {
return cameraProject([], position, viewport, combined);
});

// Draw the mesh
drawCellsNoOverlap(context, points, cells);
};
};

canvasSketch(sketch, settings);
``````

bramadams.dev is a reader-supported published Zettelkasten. Both free and paid subscriptions are available. If you want to support my work, the best way is by taking out a paid subscription.

creative-codingart

writer, programmer

Members Public

## September 2 2023

A lot of calories!

Members Public

Do you remember?

Members Public

## August 31 2023

Not hideous, pretty enough, but bland as lukewarm coffee.