Skip to content

Instantly share code, notes, and snippets.

@troughton
Forked from dribnet/.block
Last active October 6, 2016 03:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save troughton/0abeed7173457128d8f4e1c45dcfd187 to your computer and use it in GitHub Desktop.
Save troughton/0abeed7173457128d8f4e1c45dcfd187 to your computer and use it in GitHub Desktop.
Parameterised Cartoon Faces
license: mit

Parameterised Cartoon Faces.

This sketch is a grid of emotive, cartoonish faces.

Initially, I'd started with a semi-realistic style for a self-portrait; I traced out ear, nose, eye, and mouth shapes from images, and tried to roughly match colours. However, I felt the result was fairly bland and lacked personality and character.

Next, I tried removing all colour and making the image entirely black strokes; this had a profound effect on the image. Suddenly, all the faces were extremely emotive and looked like sketches from a newspaper cartoon; their qualities were enhanced, making them caricatures of various emotions.

Moving forward, I tried playing with the range of the random variables. By concentrating attention on the most emotive aspects – the eyes and mouth – the expression on each face came through clearly, while other variables have less variation.

In this version of the sketch, the program automatically goes through each face and applies the previous face onto the current face as a blend. The distribution thereby evolves by itself over time, and will gradually converge to a single face.

// note: this file is poorly named - it can generally be ignored.
// helper functions below for supporting blocks/purview
function saveBlocksImages() {
// 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);
// 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);
imageData = offscreenCanvas.toDataURL('image/png');
imageData = imageData.replace('image/png', downloadMime);
p5.prototype.downloadFile(imageData, 'thumbnail.png', 'png');
}
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.2/p5.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.2/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="readme.purview_helper.js"></script>
<script language="javascript" type="text/javascript" src="sketch.js"></script>
<style> body {padding: 0; margin: 0;} </style>
</head>
<body id="body" style="background-color:white">
</body>
{
// See https://go.microsoft.com/fwlink/?LinkId=759670
// for the documentation about the jsconfig.json format
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"allowSyntheticDefaultImports": true
},
"exclude": [
"node_modules",
"bower_components",
"jspm_packages",
"tmp",
"temp"
]
}
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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment