|
var faces = []; |
|
var selectedFace = null; |
|
|
|
var currentFaceIndex = 0; |
|
|
|
function setup () { |
|
createCanvas(833, 500); |
|
ellipseMode(CENTER); |
|
rectMode(CENTER); |
|
noStroke(); |
|
|
|
randomiseGrid(); |
|
|
|
frameRate(2); |
|
} |
|
|
|
function randomiseGrid() { |
|
//Generate a 5x3 grid of faces |
|
for (var x = 0; x < 5; x++) { |
|
faces[x] = [] |
|
for (var y = 0; y < 3; y++) { |
|
faces[x][y] = randomFace(); |
|
} |
|
} |
|
} |
|
|
|
//min, max, focus, mean |
|
//This is an awful name for this variable, but I can't think of a better one at the moment so this'll have to do. |
|
var focusedRandomArgs = { |
|
'quality' : [0, 1.0, 1.2, 0.7], |
|
'eyePosition' : [0, 0.08, 1.2, 0.05], |
|
'eyeWidth' : [0.1, 0.28, 1.1, 0.8], |
|
'eyeHeight' : [0.03, 0.13, 0.4, 0.07], |
|
'eyeSpacing' : [0.13, 0.22, 2.5, 0.17], |
|
'pupilScale' : [0.2, 0.8, 0.9, 0.4], |
|
'eyebrowPosition' : [0.00, 0.01, 1.5, 0.005], |
|
'eyebrowSlant' : [0.00, 0.3, 0.6, 0.05], |
|
'mouthPosition' : [0.4, 0.52, 2.5, 0.445], |
|
'mouthWidth' : [0.1, 0.52, 1.4, 0.26], |
|
'mouthHeight' : [0.04, 0.06, 3.0, 0.05], |
|
'noseWidth' : [0.10, 0.24, 4.0, 0.15], |
|
'noseHeight' : [0.16, 0.32, 1.9, 0.22], |
|
'nosePosition' : [0.21, 0.29, 1.9, 0.25], |
|
'earSpacing' : [0.33, 0.36, 3.0, 0.35], |
|
'earWidth' : [0.07, 0.17, 3.0, 0.11], |
|
'earHeight' : [0.16, 0.35, 2.0, 0.24] |
|
} |
|
|
|
//Generate and return a random face, with parameters in the range defined in variableFocusedRandomArgs. |
|
function randomFace() { |
|
|
|
var face = { }; |
|
|
|
for (variable in focusedRandomArgs) { |
|
var randomArguments = focusedRandomArgs[variable]; |
|
face[variable] = focusedRandom(randomArguments[0], randomArguments[1], randomArguments[2], randomArguments[3]); //set the value for every variable to a valid value within its range. |
|
} |
|
|
|
return face; |
|
} |
|
|
|
//Returns a new face that's based on random distributions whose means are centred around the average of the two argument faces. |
|
function blendFaces(faceA, faceB) { |
|
var face = { }; |
|
|
|
for (variable in focusedRandomArgs) { |
|
var randomArguments = focusedRandomArgs[variable]; |
|
var mean = (faceA[variable] + faceB[variable]) * 0.5; |
|
face[variable] = focusedRandom(randomArguments[0], randomArguments[1], 10.0, mean); //We want a high focus value so it's centred around a blend between the two faces, but we still want some possibility for variety. |
|
} |
|
|
|
return face; |
|
} |
|
|
|
/* |
|
function mouseClicked() { |
|
|
|
var faceClickedX = Math.floor(5.0 * mouseX / width); |
|
var faceClickedY = Math.floor(3.0 * mouseY / height); |
|
|
|
if (selectedFace != null) { |
|
|
|
faces[faceClickedX][faceClickedY] = blendFaces(faces[selectedFace.x][selectedFace.y], faces[faceClickedX][faceClickedY]); |
|
|
|
selectedFace = null; |
|
|
|
} else { |
|
selectedFace = {'x': faceClickedX, 'y': faceClickedY}; |
|
} |
|
|
|
loop(); //redraw |
|
} |
|
*/ |
|
|
|
function update() { |
|
//Every 2 frames, blend the 'currentFaceIndex'th face onto the 'currentFaceIndex + 1'th face. |
|
// |
|
|
|
if (frameCount % 2 == 0) { |
|
if (currentFaceIndex < 16) { |
|
|
|
var targetFaceX = currentFaceIndex % 5; |
|
var targetFaceY = Math.floor(currentFaceIndex / 5); |
|
var previousFaceX = (currentFaceIndex + 16 - 1) % 5; |
|
var previousFaceY = Math.floor(((currentFaceIndex + 16 - 1) % 16) / 5); |
|
|
|
faces[targetFaceX][targetFaceY] = blendFaces(faces[previousFaceX], faces[previousFaceY]); |
|
currentFaceIndex = (currentFaceIndex + 1) % 16; |
|
} |
|
} |
|
} |
|
|
|
function draw() { |
|
update(); |
|
|
|
background(255); |
|
|
|
scale(width / 5, height / 3); |
|
|
|
translate(0.5, 0.5); |
|
|
|
for (var x = 0; x < 5; x++) { |
|
for (var y = 0; y < 3; y++) { |
|
|
|
var isSelected = false; |
|
if (selectedFace != null) { |
|
isSelected = selectedFace.x == x && selectedFace.y == y; //Work out if we're about to draw the currently selected face. |
|
} |
|
|
|
push(); |
|
translate(x, y); |
|
scale(0.88, 0.9); |
|
drawFace(faces[x][y], isSelected); |
|
pop(); |
|
} |
|
} |
|
} |
|
|
|
function drawFace (face, isSelected) { |
|
|
|
push(); |
|
drawFaceOutline(face, isSelected); |
|
|
|
{ |
|
push(); |
|
|
|
translate(0, -face.eyePosition); |
|
|
|
{ |
|
push(); |
|
|
|
|
|
translate(-face.eyeSpacing, 0); |
|
drawEye(face, face.eyeWidth, face.eyeHeight, true); |
|
|
|
translate(0, -face.eyebrowPosition); |
|
rotate(-face.eyebrowSlant); |
|
drawEyebrow(); |
|
|
|
pop(); |
|
} |
|
|
|
{ |
|
push(); |
|
translate(face.eyeSpacing, 0); |
|
drawEye(face, face.eyeWidth, face.eyeHeight, false); |
|
|
|
translate(0, -face.eyebrowPosition); |
|
rotate(face.eyebrowSlant); |
|
drawEyebrow(); |
|
|
|
pop(); |
|
} |
|
|
|
|
|
pop(); |
|
} |
|
|
|
drawHair(); |
|
translate(0, -0.2); |
|
|
|
{ |
|
push(); |
|
|
|
translate(0, face.mouthPosition); |
|
|
|
drawMouth(face, face.mouthWidth, face.mouthHeight); |
|
|
|
pop(); |
|
} |
|
|
|
{ |
|
push(); |
|
|
|
translate(0, face.nosePosition); |
|
drawNose(face, face.noseWidth, face.noseHeight); |
|
pop(); |
|
} |
|
|
|
{ |
|
push(); |
|
translate(0, face.nosePosition); // We could (and probably should) use a separate earPosition value, but this is good enough for now. |
|
drawEars(face, isSelected); |
|
|
|
pop(); |
|
} |
|
|
|
|
|
pop(); |
|
|
|
// noLoop(); |
|
} |
|
|
|
/** |
|
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random |
|
* Returns a random integer between min (inclusive) and max (inclusive) |
|
* Using Math.round() will give you a non-uniform distribution! |
|
*/ |
|
function getRandomInt(min, max) { |
|
return Math.floor(random() * (max - min + 1)) + min; |
|
} |
|
|
|
function drawIris(face, radius, colour, pupilScale) { |
|
// fill(colour); |
|
noFill(); |
|
|
|
rotate(0.2); |
|
|
|
var drawFunction = face.quality > 0.5 ? ellipse : rect; //use whichever drawing function is appropriate given the quality variable; helpfully, both take the same arguments. |
|
|
|
drawFunction(0, 0, radius * 2, radius * 2); |
|
|
|
rotate(-0.1); |
|
fill(0); |
|
drawFunction(0, 0, radius * 2 * pupilScale, radius * 2 * pupilScale); |
|
} |
|
|
|
function drawEyeOutline(face, width, height, isLeft) { |
|
|
|
push(); |
|
|
|
//Eye-shaped points in the range from -0.5 to 0.5 |
|
let points = [-0.50000000000000011, -0.0056890012642223575, -0.50000000000000011, -0.0056890012642223575, -0.37955318115590098, -0.2357774968394441, -0.27416221466731427, -0.41150442477876109, -0.12846041767848465, -0.5, 0.019184069936862502, -0.5, 0.14545896066051478, -0.45575221238938052, 0.37372510927634772, -0.25221238938053075, 0.45580378824672174, -0.12073324905183323, 0.5, -0.0056890012642223575, 0.48737251092763495, 0.099241466498103212, 0.43249150072850912, 0.14854614412136533, 0.31253035454103933, 0.28508217446270551, 0.1289460903351142, 0.4342604298356505, -0.025012141816415766, 0.5, -0.13720252549781448, 0.5, -0.24065080135988345, 0.45575221238938052, -0.33341427877610497, 0.30151706700379216, -0.41112190383681407, 0.17003792667509462, -0.46600291403593985, 0.099241466498103212, -0.50000000000000011, -0.1, -0.50000000000000011, -0.1]; |
|
|
|
let pointCount = points.length / 2; |
|
|
|
let xFlip = isLeft ? 1 : -1; |
|
|
|
stroke(0); |
|
strokeWeight(0.002); |
|
fill(255); |
|
|
|
drawPoints(face, points, width * xFlip, height); |
|
|
|
pop(); |
|
} |
|
|
|
function drawEyebrow() { |
|
push(); |
|
|
|
var eyebrowColour = color(131, 107, 78); |
|
|
|
stroke(0); |
|
strokeWeight(0.02); |
|
noFill(); |
|
|
|
arc(0, 0, 0.4, 0.14, -(HALF_PI + QUARTER_PI), -QUARTER_PI); |
|
|
|
pop(); |
|
} |
|
|
|
function drawEye(face, width, height, isLeft) { |
|
drawEyeOutline(face, width, height, isLeft); |
|
drawIris(face, min(width * 0.5, height * 0.5), color(75, 74, 120), face.pupilScale); |
|
} |
|
|
|
function drawHair() { |
|
|
|
push(); |
|
|
|
randomSeed(2); //We want the hair to be the same every time, so we seed the RNG with a constant value. |
|
|
|
var hairColour = color(151, 127, 98); |
|
|
|
translate(0, -0.2); //Move the hair up to the right position |
|
var hairRed = red(hairColour); |
|
var hairGreen = green(hairColour); |
|
var hairBlue = blue(hairColour); |
|
|
|
noFill(); |
|
strokeWeight(0.001); |
|
|
|
//TODO: Drawing the hair is fairly slow and we should ideally cache the result in a separate graphics context. |
|
for (var xScale = 0.0001; xScale < 0.74; xScale += 0.001) { //Draw the hair as a series of arcs, with their y position and length jittered 'randomly'. |
|
stroke(0); |
|
arc(0, random() * 0.08, xScale, 0.6 + random() * 0.02, -HALF_PI, 0); |
|
} |
|
|
|
for (var xScale = 0.0001; xScale < 0.74; xScale += 0.001) { |
|
stroke(0); |
|
arc(0, random() * 0.08, xScale, 0.6 + random() * 0.02, PI, PI + HALF_PI); |
|
} |
|
|
|
pop(); |
|
} |
|
|
|
var faceColour = [255, 255, 255]; |
|
|
|
function drawFaceOutline(face, isSelected) { |
|
push(); |
|
translate(0, -0.1); |
|
fill(faceColour[0], faceColour[1], faceColour[2]); |
|
|
|
var strokeColour = isSelected ? color(57, 140, 245) : 0; |
|
|
|
stroke(strokeColour); |
|
strokeWeight(0.002); |
|
ellipse(0, 0, 0.75, 0.8); |
|
|
|
var points = [ -0.375, 0.00, |
|
-0.375, 0.05, |
|
-0.3, 0.3, |
|
-0.18, 0.48, |
|
-0.08, 0.54, |
|
0.08, 0.54, |
|
0.18, 0.48, |
|
0.3, 0.3, |
|
0.375, 0.05, |
|
0.375, 0.00]; |
|
|
|
|
|
drawPoints(face, points, 1.0, 1.0); |
|
|
|
pop(); |
|
} |
|
|
|
function drawMouth(face, width, height) { |
|
|
|
var topLipPoints = [-0.50000006, 0.028302524, |
|
-0.4802632, 0.047169972, |
|
-0.45394737, 0.009434175, |
|
-0.40460533, -0.10377322, |
|
-0.3421053, -0.25471643, |
|
-0.2763158, -0.330188, |
|
-0.20723687, -0.4811312, |
|
-0.15460534, -0.49999955, |
|
-0.118421085, -0.46226376, |
|
-0.069079034, -0.330188, |
|
-0.00986849, -0.27358475, |
|
0.046052586, -0.29245222, |
|
0.1085526, -0.27358475, |
|
0.18092094, -0.3679238, |
|
0.23684202, -0.27358475, |
|
0.305921, -0.10377322, |
|
0.3717104, 0.06603832, |
|
0.44736826, 0.31132147, |
|
0.49999997, 0.46226466, |
|
0.39802614, 0.5000005, |
|
0.2960525, 0.36792472, |
|
0.22697353, 0.2924531, |
|
0.16447367, 0.25471732, |
|
0.11842093, 0.38679305, |
|
0.036184095, 0.38679305, |
|
-0.042763192, 0.3301889, |
|
-0.10526321, 0.27358565, |
|
-0.15460534, 0.14150992, |
|
-0.20723687, 0.14150992, |
|
-0.25000006, 0.16037737, |
|
-0.33881584, 0.12264157, |
|
-0.39144745, 0.10377412, |
|
-0.43421057, 0.14150992]; |
|
|
|
var bottomLipPoints = [-0.49999997, -0.48461467, |
|
-0.49999997, -0.48461467, |
|
-0.4169435, -0.49999964, |
|
-0.32392025, -0.46923044, |
|
-0.22757474, -0.45384547, |
|
-0.16112953, -0.45384547, |
|
-0.078073055, -0.29999948, |
|
0.0016611676, -0.25384533, |
|
0.0946845, -0.22307612, |
|
0.17441864, -0.28461453, |
|
0.2574751, -0.28461453, |
|
0.4003322, -0.2076919, |
|
0.49335554, -0.1769227, |
|
0.50000006, -0.053845912, |
|
0.44352162, 0.023077447, |
|
0.370432, 0.20769262, |
|
0.27740866, 0.33076942, |
|
0.17774098, 0.45384693, |
|
0.08139531, 0.46923116, |
|
-0.03488365, 0.50000036, |
|
-0.11129562, 0.45384693, |
|
-0.19767435, 0.33076942, |
|
-0.30066445, 0.1615392, |
|
-0.38039866, -0.023076715, |
|
-0.4534883, -0.1769227]; |
|
|
|
push(); |
|
|
|
stroke(0); |
|
strokeWeight(0.002); |
|
|
|
noFill(); |
|
|
|
drawPoints(face, topLipPoints, width, height); |
|
translate(0, 0.03); |
|
drawPoints(face, bottomLipPoints, width, height); |
|
|
|
pop(); |
|
} |
|
|
|
function drawNose(face, width, height) { |
|
push(); |
|
|
|
var nosePoints = [0.29274616, -0.49625465, |
|
0.28756496, -0.4101123, |
|
0.2720208, -0.2528089, |
|
0.27720228, -0.09925089, |
|
0.34455985, 0.046816573, |
|
0.44300547, 0.17415737, |
|
0.5000001, 0.3089889, |
|
0.46373084, 0.41011247, |
|
0.37046644, 0.47752815, |
|
0.28756496, 0.50000006, |
|
0.1839381, 0.47378293, |
|
0.090673685, 0.45880166, |
|
0.03367879, 0.47378293, |
|
-0.049222693, 0.47752815, |
|
-0.11139881, 0.44756564, |
|
-0.20984444, 0.43258438, |
|
-0.33419678, 0.45880166, |
|
-0.40673572, 0.44382024, |
|
-0.4533678, 0.41011247, |
|
-0.49999988, 0.34644204, |
|
-0.49999988, 0.26779035, |
|
-0.46891183, 0.20411989, |
|
-0.3860102, 0.14044954, |
|
-0.33419678, 0.035580628, |
|
-0.2875647, -0.06928837, |
|
-0.23575115, -0.16666666, |
|
-0.19430041, -0.27153558, |
|
-0.1683937, -0.34644186, |
|
-0.17357504, -0.4213483, |
|
-0.17875639, -0.49999997]; |
|
|
|
|
|
noFill(); |
|
stroke(0); |
|
strokeWeight(0.002); |
|
drawPoints(face, nosePoints, width, height); |
|
|
|
pop(); |
|
} |
|
|
|
function drawEars(face, isSelected) { |
|
push(); |
|
|
|
var earPoints = [0.50000006, 0.5, |
|
0.42957756, 0.4835391, |
|
0.21831, 0.41358024, |
|
-0.077464715, 0.31069955, |
|
-0.2746478, 0.18312755, |
|
-0.33098587, 0.039094664, |
|
-0.41549292, -0.11728399, |
|
-0.49999997, -0.2860082, |
|
-0.47183096, -0.3847737, |
|
-0.38732392, -0.47119343, |
|
-0.09154921, -0.50000006, |
|
0.20422533, -0.45884776, |
|
0.28873247, -0.39711937]; |
|
|
|
var strokeColour = isSelected ? color(57, 140, 245) : 0; |
|
|
|
stroke(strokeColour); |
|
strokeWeight(0.002); |
|
fill(faceColour[0], faceColour[1], faceColour[2]); |
|
|
|
{ |
|
push(); |
|
translate(-face.earSpacing, 0); |
|
drawPoints(face, earPoints, face.earWidth, face.earHeight); |
|
pop(); |
|
} |
|
|
|
{ |
|
push(); |
|
translate(face.earSpacing, 0); |
|
drawPoints(face, earPoints, -face.earWidth, face.earHeight); |
|
pop(); |
|
} |
|
|
|
pop(); |
|
} |
|
|
|
function drawPoints(face, points, xScale, yScale) { |
|
|
|
var pointFunction = face.quality > 0.5 ? curveVertex : vertex; //Draw smooth curves if the quality variable is > 0.5 |
|
|
|
beginShape(); |
|
for (var i = 0; i < points.length/2; i += (face.quality <= 0.25) ? 2 : 1) { //Skip every second point if we're in low quality mode. |
|
pointFunction(points[2 * i] * xScale, points[2 * i + 1] * yScale); |
|
} |
|
endShape(); |
|
} |
|
|
|
function keyTyped() { |
|
switch (key) { |
|
case "!": |
|
saveBlocksImages(); |
|
break; |
|
case "r": |
|
randomise(); |
|
break; |
|
default: |
|
break; |
|
} |
|
} |