Skip to content

Instantly share code, notes, and snippets.

@dribnet
Last active October 23, 2019 21:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dribnet/374e9004f80b623328c2f373b9f3158d to your computer and use it in GitHub Desktop.
Save dribnet/374e9004f80b623328c2f373b9f3158d to your computer and use it in GitHub Desktop.
Marble Bot
license: mit
// note: this file is poorly named - it can generally be ignored.
// helper functions below for supporting blocks/purview
function saveBlocksImages(doZoom) {
if(doZoom == null) {
doZoom = false;
}
// generate 960x500 preview.jpg of entire canvas
// TODO: should this be recycled?
var offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = 960;
offscreenCanvas.height = 500;
var context = offscreenCanvas.getContext('2d');
// background is flat white
context.fillStyle="#FFFFFF";
context.fillRect(0, 0, 960, 500);
context.drawImage(this.canvas, 0, 0, 960, 500);
// save to browser
var downloadMime = 'image/octet-stream';
var imageData = offscreenCanvas.toDataURL('image/jpeg');
imageData = imageData.replace('image/jpeg', downloadMime);
p5.prototype.downloadFile(imageData, 'preview.jpg', 'jpg');
// generate 230x120 thumbnail.png centered on mouse
offscreenCanvas.width = 230;
offscreenCanvas.height = 120;
// background is flat white
context = offscreenCanvas.getContext('2d');
context.fillStyle="#FFFFFF";
context.fillRect(0, 0, 230, 120);
if(doZoom) {
// pixelDensity does the right thing on retina displays
var pd = this._pixelDensity;
var sx = pd * mouseX - pd * 230/2;
var sy = pd * mouseY - pd * 120/2;
var sw = pd * 230;
var sh = pd * 120;
// bounds checking - just displace if necessary
if (sx < 0) {
sx = 0;
}
if (sx > this.canvas.width - sw) {
sx = this.canvas.width - sw;
}
if (sy < 0) {
sy = 0;
}
if (sy > this.canvas.height - sh) {
sy = this.canvas.height - sh;
}
// save to browser
context.drawImage(this.canvas, sx, sy, sw, sh, 0, 0, 230, 120);
}
else {
// now scaledown
var full_width = this.canvas.width;
var full_height = this.canvas.height;
context.drawImage(this.canvas, 0, 0, full_width, full_height, 0, 0, 230, 120);
}
imageData = offscreenCanvas.toDataURL('image/png');
imageData = imageData.replace('image/png', downloadMime);
p5.prototype.downloadFile(imageData, 'thumbnail.png', 'png');
}

Twitter bot

This is part of a Twitter bot which generates marble images. It's the creative component which creates the image and message which the bot posts. It takes a sample from a marble image, gets a colour palette from the COLOURlovers API, and applies the colours to the marble image. Every output is unique with a different image and colour palette every time.

The message is generated with two components. Firstly, the coverage of each colour is tracked. The dominant colour is compared to a list of Crayola colours and the closest matching Crayola colour is found. The name of the Crayola colour is the first part of the message. The second component is a randomly chosen name which describes the form of art.

What did I make?

The artefact of my generator is a uniquely coloured marble image. The concrete properties (good qualities) of each output are a unique beautiful colour palette and an interesting marble-like look (swirly with bands of colour).

The constraints (bad artefacts) are blank/empty images, a boring or ugly colour palette, smudgy large areas of colour, and images which don't look like marble.

To ensure my generator produces a wide range of artefacts, I sample my marble from large images which has a mix of areas that are interesting and areas that are blank, boring, or ugly. To ensure it produces artefacts with more good properties than bad, the majority of the marble images it sample are interesting and are marble-like.

To ensure I get a good colour palette, I find a random popular colour palette in their 'top' category. There is enough results that it would take a long time (or luck) to get repeating colour palettes. Even if the palette repeats, the marble should be different.

Making the closed bot a green bot

In order to make the closed bot a green bot, I needed to get a colour palette from an API. I found COLOURlovers offered a nice API and implemented that. For my closed bot, I was extracting a colour palette from an image which was tedious. It also gave me problems with Cross Origin security errors when I tried to get images from an API. So I changed the way I get colour palettes to directly getting them online. This is a much more elegant solution. It also guarantees I will get nice colours because they are curated.

{
"names": [
"swirl",
"Ebru",
"marble",
"Suminagashi",
"ink",
"art"
]
}
// Third party resources:
// COLOURlovers API - A colour palette API by the folks at COLOURlovers.com http://www.colourlovers.com/api
// crayola.json - A list of Crayola colours by Darius Kazemi https://github.com/dariusk/corpora/blob/master/data/colors/crayola.json
function bot() {
this.marbleImage = new MarbleImage();
// make this true once image has been drawn
this.have_drawn = false;
// return true if image has been drawn
this.isDone = function() {
return this.have_drawn;
}
this.preload = function() {
this.marbleImage.loadColorPalette();
this.marbleImage.loadMarble();
this.marbleImage.loadJson();
}
this.setup = function() {
noSmooth();
this.marbleImage.sampleMarble();
}
this.respond = function() {
if (this.marbleImage.finished) {
image(this.marbleImage.marble);
this.marbleImage.applyColourPaletteToMarble();
this.marbleImage.getColourName();
// set have_drawn to true since we have completed
this.have_drawn = true;
}
// return the message
return this.marbleImage.message();
}
}
function MarbleImage() {
// Marble images to sample from
this.marbleImages = [
'z_1.png',
'z_2.png',
'z_3.png',
'z_4.png'
];
// The colour table (not including background) of each marbleImage (ordered darkest to lightest)
this.marblePalettes = {
'z_1.png': [
[150, 141, 136].toString(),
[190, 181, 179].toString(),
[216, 210, 210].toString(),
[235, 232, 234].toString()
],
'z_2.png': [
[136, 133, 129].toString(),
[178, 173, 169].toString(),
[201, 196, 193].toString(),
[224, 221, 221].toString()
],
'z_3.png': [
[12, 12, 10].toString(),
[147, 165, 162].toString(),
[119, 229, 226].toString(),
[183, 229, 232].toString()
],
'z_4.png': [
[67, 140, 167].toString(),
[134, 168, 184].toString(),
[10, 93, 138].toString(),
[192, 203, 212].toString()
]
};
this.marblePalette = [];
this.colourPaletteUrl = 'http://www.colourlovers.com/api/palettes/top?format=json&numResults=1&resultOffset=';
this.colourPalette = [];
this.crayolaColours;
this.artNames;
this.dominantColour;
this.closestColourName;
this.marble;
this.finished = false;
var self = this;
// Send JSONP request to src URL
// p5.js loadJSON() doesn't work with COLOURlovers API
this.jsonp = function(src, options) {
var callback_name = options.callbackName || 'callback';
var on_success = options.onSuccess || function(){};
var on_timeout = options.onTimeout || function(){};
var timeout = options.timeout || 10; // sec
var timeout_trigger = window.setTimeout(function(){
window[callback_name] = function(){};
on_timeout();
}, timeout * 1000);
window[callback_name] = function(data){
window.clearTimeout(timeout_trigger);
on_success(data);
}
var script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = src;
document.getElementsByTagName('head')[0].appendChild(script);
}
// Extract colour palette from API data
this.handleColourPaletteData = function(data) {
// Set colourPalette with the 5 colours
var colours = data[0].colors;
for (var i = 0; i < colours.length; i++) {
this.colourPalette[i] = colours[i];
}
this.sortColourPalette();
}
// Get a random popular colour palette from COLOURlovers API
this.loadColorPalette = function() {
// Add a random results page offset (1-50) to get different results every time
var rand = ceil(random(50));
this.colourPaletteUrl += rand;
// Add callback for JSONP
var callbackName = 'callback';
this.colourPaletteUrl += '&jsonCallback=' + callbackName;
var timeout = 5;
// Send GET request to load JSONP
this.jsonp(this.colourPaletteUrl, {
callbackName: callbackName,
timeout: timeout,
onSuccess: function(data) {
console.log('Successfully retreived API data');
self.handleColourPaletteData(data);
},
onTimeout: function() {
console.info('Timeout: failed to retrieve data from API after ' + timeout + ' seconds. Using local colour palette');
self.handleColourPaletteData(data);
}
});
}
// Sort colour palette from (perceptual) darkest to lightest colours
this.sortColourPalette = function() {
this.colourPalette.sort(function (a, b) {
var rgbA = self.hexToRgb(a);
rgbA = [rgbA['r'], rgbA['g'], rgbA['b']];
var rgbB = self.hexToRgb(b);
rgbB = [rgbB['r'], rgbB['g'], rgbB['b']];
return self.sumColour(rgbA) > self.sumColour(rgbB);
});
// MarbleImage is done and it's image can be used now
this.finished = true;
}
// Colour sorting code by Bas Dirks from
// http://stackoverflow.com/questions/27960722/sort-array-with-rgb-color-on-javascript
this.sumColour = function(rgb) {
// To calculate relative luminance under sRGB and RGB colorspaces that use Rec. 709:
return 0.2126*rgb[0] + 0.7152*rgb[1] + 0.0722*rgb[2];
}
// Load a random marble image
this.loadMarble = function() {
var image = random(this.marbleImages);
this.marblePalette = this.marblePalettes[image];
this.marble = loadImage(image);
}
// Load all JSON files
this.loadJson = function() {
this.crayolaColours = loadJSON('crayola.json');
this.artNames = loadJSON('art.json');
}
// Sample a random section of marble image
this.sampleMarble = function() {
var sampleWidth = width * 1.5;
var sampleHeight = height * 1.5;
var sampleX = random(this.marble.width - sampleWidth);
var sampleY = random(this.marble.height - sampleHeight);
image(this.marble, sampleX, sampleY, sampleWidth, sampleHeight, 0, 0, width, height);
var sampled = get();
this.marble = sampled;
}
// Convert a hex colour to RGB as an object
this.hexToRgb = function(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
// Replace colours of marble image from colours in colourPalette
// Also set the dominant colour as a hex value
this.applyColourPaletteToMarble = function() {
var colourCount = [];
for (var i = 0; i < this.colourPalette.length; i++) {
colourCount[i] = { 'colour': this.colourPalette[i], 'count': 0 };
}
loadPixels();
var d = pixelDensity();
var imageRes = 4 * (width * d) * (height * d);
for (var i = 0; i < imageRes; i += 4) {
var rgb = [pixels[i], pixels[i+1], pixels[i+2]].toString();
switch (rgb) {
case this.marblePalette[0]:
var colour = this.hexToRgb(this.colourPalette[0]);
pixels[i] = colour['r'];
pixels[i+1] = colour['g'];
pixels[i+2] = colour['b'];
colourCount[0]['count']++;
break;
case this.marblePalette[1]:
var colour = this.hexToRgb(this.colourPalette[1]);
pixels[i] = colour['r'];
pixels[i+1] = colour['g'];
pixels[i+2] = colour['b'];
colourCount[1]['count']++;
break;
case this.marblePalette[2]:
var colour = this.hexToRgb(this.colourPalette[2]);
pixels[i] = colour['r'];
pixels[i+1] = colour['g'];
pixels[i+2] = colour['b'];
colourCount[2]['count']++;
break;
case this.marblePalette[3]:
var colour = this.hexToRgb(this.colourPalette[3]);
pixels[i] = colour['r'];
pixels[i+1] = colour['g'];
pixels[i+2] = colour['b'];
colourCount[3]['count']++;
break;
default:
break;
}
}
updatePixels();
var highestCountColour = colourCount[0];
for (var i = 1; i < colourCount.length; i++) {
if (colourCount[i]['count'] > highestCountColour['count']) {
highestCountColour = colourCount[i];
}
}
this.dominantColour = highestCountColour['colour'];
}
// Compare colours from an array and return the closest matching colour
// (Modified) closest hex color compare code by Andrew Clark
// http://stackoverflow.com/questions/17175664/get-the-closest-color-name-depending-on-an-hex-color
this.closestColour = (function () {
function dist(s, t) {
if (!s.length || !t.length) return 0;
return dist(s.slice(2), t.slice(2)) +
Math.abs(parseInt(s.slice(0, 2), 16) - parseInt(t.slice(0, 2), 16));
}
return function (arr, str) {
var min = 0xffffff;
var best, current, i;
for (i = 0; i < arr.length; i++) {
var crayolaHex = arr[i]['hex'].replace('#', '');
var compareHex = str.replace('#', '');
current = dist(crayolaHex, compareHex);
if (current < min) {
min = current
best = arr[i];
}
}
return best;
};
}());
// Find the closest colour in crayola.json to the dominant colour
this.getColourName = function() {
var closestColour = this.closestColour(this.crayolaColours['colors'], this.dominantColour);
this.closestColourName = closestColour['color'];
}
// Combine the closest dominant colour name and an art description name as the message
this.message = function() {
if (!this.finished) {
return 'Loading colour palette';
}
return this.closestColourName + ' ' + random(this.artNames['names']);
}
}
{
"description": "List of Crayola crayon standard colors",
"colors": [
{
"color": "Almond",
"hex": "#EFDECD"
},
{
"color": "Antique Brass",
"hex": "#CD9575"
},
{
"color": "Apricot",
"hex": "#FDD9B5"
},
{
"color": "Aquamarine",
"hex": "#78DBE2"
},
{
"color": "Asparagus",
"hex": "#87A96B"
},
{
"color": "Atomic Tangerine",
"hex": "#FFA474"
},
{
"color": "Banana Mania",
"hex": "#FAE7B5"
},
{
"color": "Beaver",
"hex": "#9F8170"
},
{
"color": "Bittersweet",
"hex": "#FD7C6E"
},
{
"color": "Black",
"hex": "#000000"
},
{
"color": "Blue",
"hex": "#1F75FE"
},
{
"color": "Blue Bell",
"hex": "#A2A2D0"
},
{
"color": "Blue Green",
"hex": "#0D98BA"
},
{
"color": "Blue Violet",
"hex": "#7366BD"
},
{
"color": "Blush",
"hex": "#DE5D83"
},
{
"color": "Brick Red",
"hex": "#CB4154"
},
{
"color": "Brown",
"hex": "#B4674D"
},
{
"color": "Burnt Orange",
"hex": "#FF7F49"
},
{
"color": "Burnt Sienna",
"hex": "#EA7E5D"
},
{
"color": "Cadet Blue",
"hex": "#B0B7C6"
},
{
"color": "Canary",
"hex": "#FFFF99"
},
{
"color": "Caribbean Green",
"hex": "#00CC99"
},
{
"color": "Carnation Pink",
"hex": "#FFAACC"
},
{
"color": "Cerise",
"hex": "#DD4492"
},
{
"color": "Cerulean",
"hex": "#1DACD6"
},
{
"color": "Chestnut",
"hex": "#BC5D58"
},
{
"color": "Copper",
"hex": "#DD9475"
},
{
"color": "Cornflower",
"hex": "#9ACEEB"
},
{
"color": "Cotton Candy",
"hex": "#FFBCD9"
},
{
"color": "Dandelion",
"hex": "#FDDB6D"
},
{
"color": "Denim",
"hex": "#2B6CC4"
},
{
"color": "Desert Sand",
"hex": "#EFCDB8"
},
{
"color": "Eggplant",
"hex": "#6E5160"
},
{
"color": "Electric Lime",
"hex": "#CEFF1D"
},
{
"color": "Fern",
"hex": "#71BC78"
},
{
"color": "Forest Green",
"hex": "#6DAE81"
},
{
"color": "Fuchsia",
"hex": "#C364C5"
},
{
"color": "Fuzzy Wuzzy",
"hex": "#CC6666"
},
{
"color": "Gold",
"hex": "#E7C697"
},
{
"color": "Goldenrod",
"hex": "#FCD975"
},
{
"color": "Granny Smith Apple",
"hex": "#A8E4A0"
},
{
"color": "Gray",
"hex": "#95918C"
},
{
"color": "Green",
"hex": "#1CAC78"
},
{
"color": "Green Yellow",
"hex": "#F0E891"
},
{
"color": "Hot Magenta",
"hex": "#FF1DCE"
},
{
"color": "Inchworm",
"hex": "#B2EC5D"
},
{
"color": "Indigo",
"hex": "#5D76CB"
},
{
"color": "Jazzberry Jam",
"hex": "#CA3767"
},
{
"color": "Jungle Green",
"hex": "#3BB08F"
},
{
"color": "Laser Lemon",
"hex": "#FEFE22"
},
{
"color": "Lavender",
"hex": "#FCB4D5"
},
{
"color": "Macaroni and Cheese",
"hex": "#FFBD88"
},
{
"color": "Magenta",
"hex": "#F664AF"
},
{
"color": "Mahogany",
"hex": "#CD4A4C"
},
{
"color": "Manatee",
"hex": "#979AAA"
},
{
"color": "Mango Tango",
"hex": "#FF8243"
},
{
"color": "Maroon",
"hex": "#C8385A"
},
{
"color": "Mauvelous",
"hex": "#EF98AA"
},
{
"color": "Melon",
"hex": "#FDBCB4"
},
{
"color": "Midnight Blue",
"hex": "#1A4876"
},
{
"color": "Mountain Meadow",
"hex": "#30BA8F"
},
{
"color": "Navy Blue",
"hex": "#1974D2"
},
{
"color": "Neon Carrot",
"hex": "#FFA343"
},
{
"color": "Olive Green",
"hex": "#BAB86C"
},
{
"color": "Orange",
"hex": "#FF7538"
},
{
"color": "Orchid",
"hex": "#E6A8D7"
},
{
"color": "Outer Space",
"hex": "#414A4C"
},
{
"color": "Outrageous Orange",
"hex": "#FF6E4A"
},
{
"color": "Pacific Blue",
"hex": "#1CA9C9"
},
{
"color": "Peach",
"hex": "#FFCFAB"
},
{
"color": "Periwinkle",
"hex": "#C5D0E6"
},
{
"color": "Piggy Pink",
"hex": "#FDDDE6"
},
{
"color": "Pine Green",
"hex": "#158078"
},
{
"color": "Pink Flamingo",
"hex": "#FC74FD"
},
{
"color": "Pink Sherbert",
"hex": "#F78FA7"
},
{
"color": "Plum",
"hex": "#8E4585"
},
{
"color": "Purple Heart",
"hex": "#7442C8"
},
{
"color": "Purple Mountain's Majesty",
"hex": "#9D81BA"
},
{
"color": "Purple Pizzazz",
"hex": "#FE4EDA"
},
{
"color": "Radical Red",
"hex": "#FF496C"
},
{
"color": "Raw Sienna",
"hex": "#D68A59"
},
{
"color": "Razzle Dazzle Rose",
"hex": "#FF48D0"
},
{
"color": "Razzmatazz",
"hex": "#E3256B"
},
{
"color": "Red",
"hex": "#EE204D"
},
{
"color": "Red Orange",
"hex": "#FF5349"
},
{
"color": "Red Violet",
"hex": "#C0448F"
},
{
"color": "Robin's Egg Blue",
"hex": "#1FCECB"
},
{
"color": "Royal Purple",
"hex": "#7851A9"
},
{
"color": "Salmon",
"hex": "#FF9BAA"
},
{
"color": "Scarlet",
"hex": "#FC2847"
},
{
"color": "Screamin' Green",
"hex": "#76FF7A"
},
{
"color": "Sea Green",
"hex": "#93DFB8"
},
{
"color": "Sepia",
"hex": "#A5694F"
},
{
"color": "Shadow",
"hex": "#8A795D"
},
{
"color": "Shamrock",
"hex": "#45CEA2"
},
{
"color": "Shocking Pink",
"hex": "#FB7EFD"
},
{
"color": "Silver",
"hex": "#CDC5C2"
},
{
"color": "Sky Blue",
"hex": "#80DAEB"
},
{
"color": "Spring Green",
"hex": "#ECEABE"
},
{
"color": "Sunglow",
"hex": "#FFCF48"
},
{
"color": "Sunset Orange",
"hex": "#FD5E53"
},
{
"color": "Tan",
"hex": "#FAA76C"
},
{
"color": "Tickle Me Pink",
"hex": "#FC89AC"
},
{
"color": "Timberwolf",
"hex": "#DBD7D2"
},
{
"color": "Tropical Rain Forest",
"hex": "#17806D"
},
{
"color": "Tumbleweed",
"hex": "#DEAA88"
},
{
"color": "Turquoise Blue",
"hex": "#77DDE7"
},
{
"color": "Unmellow Yellow",
"hex": "#FFFF66"
},
{
"color": "Violet",
"hex": "#926EAE"
},
{
"color": "Violet Red",
"hex": "#F75394"
},
{
"color": "Vivid Tangerine",
"hex": "#FFA089"
},
{
"color": "Vivid Violet",
"hex": "#8F509D"
},
{
"color": "White",
"hex": "#FFFFFF"
},
{
"color": "Wild Blue Yonder",
"hex": "#A2ADD0"
},
{
"color": "Wild Strawberry",
"hex": "#FF43A4"
},
{
"color": "Wild Watermelon",
"hex": "#FC6C85"
},
{
"color": "Wisteria",
"hex": "#CDA4DE"
},
{
"color": "Yellow",
"hex": "#FCE883"
},
{
"color": "Yellow Green",
"hex": "#C5E384"
},
{
"color": "Yellow Orange",
"hex": "#FFAE42"
}
]
}
function resetFocusedRandom() {
return Math.seedrandom(arguments);
}
function focusedRandom(min, max, focus, mean) {
// console.log("hello")
if(max === undefined) {
max = min;
min = 0;
}
if(focus === undefined) {
focus = 1.0;
}
if(mean === undefined) {
mean = (min + max) / 2.0;
}
if(focus == 0) {
return d3.randomUniform(min, max)();
}
else if(focus < 0) {
focus = -1 / focus;
}
sigma = (max - mean) / focus;
val = d3.randomNormal(mean, sigma)();
if (val > min && val < max) {
return val;
}
return d3.randomUniform(min, max)();
}
<head>
<script src="http://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.3/p5.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.3/addons/p5.dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.0/seedrandom.min.js"></script>
<script src="https://d3js.org/d3-random.v1.min.js"></script>
<script language="javascript" type="text/javascript" src="focusedRandom.js"></script>
<script language="javascript" type="text/javascript" src=".purview_helper.js"></script>
<script language="javascript" type="text/javascript" src="bot.js"></script>
<script language="javascript" type="text/javascript" src="sketch.js"></script>
<style>
body {padding: 0; margin: 0;}
ul li {
list-style:none;
overflow:hidden;
border:1px solid #dedede;
margin:5px;
padding:5px;
}
.media img {
max-width:440px;
max-height:220px;
}
</style>
</head>
<body style="background-color:white">
<div id="canvasContainer"></div>
<pre>
<p id="tweet_text">
</p>
</pre>
<hr>
<div id="tweetExamples"></div>
</body>
var rndSeed;
var bot;
var renderReady = false;
function preload() {
bot = new bot();
bot.preload();
}
function setup () {
var main_canvas = createCanvas(440, 220);
main_canvas.parent('canvasContainer');
rndSeed = random(1024);
bot.setup();
}
function keyTyped() {
if (key == '!') {
saveBlocksImages();
}
else if (key == '@') {
saveBlocksImages(true);
}
}
function reportRenderReady() {
finalDiv = createDiv('(render ready)');
finalDiv.id("render_ready")
}
function draw() {
background(204);
// randomSeed(0);
resetFocusedRandom(rndSeed);
message = bot.respond();
var text = select('#tweet_text');
text.html(message);
if(renderReady == false) {
if(bot.isDone()) {
reportRenderReady();
renderReady = true;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment