Skip to content

Instantly share code, notes, and snippets.

@jpdenford
Forked from dribnet/.block
Last active October 13, 2016 04:48
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 jpdenford/fbee9f7afce0223a8e0d79888c5d8714 to your computer and use it in GitHub Desktop.
Save jpdenford/fbee9f7afce0223a8e0d79888c5d8714 to your computer and use it in GitHub Desktop.
Sketchy Characters
license: mit

PS1 MDDN 342 2016

This sketch generates a random set of sketchy style faces. Each face is different, using a set of parameters for generation, some of which are roughly: Sketchyness Size Placement of features Hair direction / length

The interactive factor is a mouseover which pops the face out and gives it an animated expression.

If I were to take this project further, I would like to explore the interaction and animation aspects further as I enjoy seeing the characters come to life.

I was inspired to go for the sketchy aesthetic from various examples I had seen online as well as it using a minimal look which I enjoy. I think it's quite enjoyable to try to capture a hand drawn style using the computer.

To use: Click 'Randomise' to create a new grid of faces Mouseover to interract

// 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 Face(position, dimensions, colour, featureOffset){
//constrain parameters + usefull variables
featureOffset = constrain(featureOffset, 0, 2.0);
//set positions and dimensions
this.pos = position;
var avgDims = (dimensions.min + dimensions.max) / 2;
// this.dims = createVector(avgDims + (featureOffset * random(-dimensions.min/2, dimensions.max/2)), avgDims + (featureOffset * random(-dimensions.min/2, dimensions.max/2)));//dimensions;
var xDim = avgDims + (featureOffset * focusedRandom(-dimensions.min/2, dimensions.max/2,1));
var yDim = avgDims + (featureOffset * focusedRandom(-dimensions.min/2, dimensions.max/2,1));
this.dims = createVector(xDim, yDim);//dimensions;
//average of face size for setting feature sizes
var faceSize = (this.dims.x + this.dims.y) / 2;
//calculate mouth positions
var mX = (featureOffset * random(-1,1) * (faceSize / 10));
var mY = (this.dims.y / 3) + (featureOffset * random(-1,1) * (faceSize / 10));
this.mouth = new Mouth(mX, mY, faceSize/5, random(0,1));
//left eye
var lEX = - (this.dims.x / 3) + (featureOffset * random(1) * (this.dims.x / 10));
var lEY = - (this.dims.y / 7) + (featureOffset * random(-1,1) * (faceSize / 10));
this.lEyePos = createVector(lEX, lEY);
this.lEyeDims = createVector(faceSize/8, random(faceSize/15,faceSize/8));
//right eye
var rEX = (this.dims.x / 3) - (featureOffset * random(1) * (this.dims.x / 10));
var rEY = this.lEyePos.y + (featureOffset * random(-1,1) * (faceSize / 20));
this.rEyePos = createVector(rEX, rEY);
this.rEyeDims = createVector(faceSize/8, random(faceSize/15,faceSize/8));
//nosef
var nX = (featureOffset * random(-1,1) * (faceSize / 30));
var nY = (featureOffset * random(-1,1) * (faceSize / 30));
this.nosePos = createVector(nX, nY);
var noseWid = faceSize/8 + random(-1,1) * (faceSize/30);
var noseHei = faceSize/8 + random(-1,1) * (faceSize/30);
this.noseDims = createVector(noseWid, noseHei);
//left ear
var lEarX = - (this.dims.x / 2);
var lEarY = - (featureOffset * random(-1,1) * (faceSize / 15));
this.lEarPos = createVector(lEarX, lEarY);
this.lEarDims = createVector(faceSize/10, faceSize/6);
//right ear
var rEarX = (this.dims.x / 2);
var rEarY = - (featureOffset * random(-1,1) * (faceSize / 15));
this.rEarPos = createVector(rEarX, rEarY);
this.rEarDims = createVector(faceSize/10, faceSize/6);
//hair -> numhairs, dir, area
var hairArea = {
min: createVector(-random(faceSize*0.3),-this.dims.y/2.5 - random(faceSize*0.1)),
max: createVector(random(faceSize*0.3), -this.dims.y/2.5 + random(faceSize*0.1))
};
this.hair = createHair(random(MIN_NUM_HAIRS, MAX_NUM_HAIRS), createVector(random(-6,6),random(4,-8)), hairArea, MAX_HAIR_FUNK);
//funk
this.funk = random(0.3,1.0);
this.tilt = focusedRandom(-QUARTER_PI, QUARTER_PI,5);
this.seed = (this.pos.x - this.pos.y) * random(200);
this.strokes = int(random(MIN_STROKES, MAX_STROKES+1));
this.sketchyness = random(0,0.4);//focusedRandom(0, 0.4, 0, 0.2);
this.colour = colour;
this.popCalled = false;
this.mouseOn = false;
}
Face.prototype.pop = function(){
this.start = millis();
this.popCalled = true;
}
/* A draw function for each face */
Face.prototype.draw = function(tilt){
funk = this.sketchyness;
strokes = this.strokes;
strokes = (strokes < 1) ? int(1) : int(strokes);
//as numStrokes increase, reduce opacity
stroke(this.colour);
push();
translate(this.pos.x,this.pos.y);
rotate(this.tilt * tilt);
if(this.mouseOn){
scale(easeOutElastic(0,millis() - this.start, 1, .5, 2000));
}
// head
noFill();
sketchyEllipse(
0,
0,
this.dims.x,
this.dims.y,
funk,
strokes,
this.seed
);
// mouth
if(this.mouseOn){
noiseSeed(this.pos.x - this.pos.y);
this.mouth.smile = noise(millis()/900)+0.5;
}
this.mouth.draw();
// left eye
sketchyEllipse(
this.lEyePos.x,
this.lEyePos.y,
this.lEyeDims.x,
this.lEyeDims.y * (1 - this.mouth.smile),
funk,
strokes > 1? 2 : 1,
this.seed + 50
);
fill(this.colour);
var mousePos = createVector(mouseX, mouseY);
var lPupil = createVector(this.lEyePos.x, this.lEyePos.y);
var rPupil = createVector(this.rEyePos.x, this.rEyePos.y);
ellipse(lPupil.x,lPupil.y, 2, 2);
ellipse(this.rEyePos.x, this.rEyePos.y, 2, 2);
noFill();
// right eye
sketchyEllipse(
this.rEyePos.x,
this.rEyePos.y,
this.rEyeDims.x,
this.rEyeDims.y * (1 - this.mouth.smile),
funk,
strokes > 1? 2 : 1,
this.seed + 80
);
//nose
sketchyEllipse(
this.nosePos.x,
this.nosePos.y,
this.noseDims.x,
this.noseDims.y,
funk,
strokes > 1? 2 : 1,
this.seed + 90
);
//l ear
sketchyEllipse(
this.lEarPos.x,
this.lEarPos.y,
this.lEarDims.x,
this.lEarDims.y,
funk,
strokes > 1? 2 : 1,
this.pos.x - this.pos.y
);
//r ear
sketchyEllipse(
this.rEarPos.x,
this.rEarPos.y,
this.rEarDims.x,
this.rEarDims.y,
funk,
strokes > 1? 2 : 1,
this.pos.x - this.pos.y
);
this.hair();
pop();
}
Face.prototype.on = function (x,y) {
var minX = this.pos.x - this.dims.x/2;
var maxX = this.pos.x + this.dims.x/2;
var minY = this.pos.y - this.dims.y/2;
var maxY = this.pos.y + this.dims.y/2;
if(x < minX || x > maxX || y < minY || y > maxY)return false;
return true;
};
function Mouth(x, y, width, smile){
this.smile = smile || 1;
var height = width / 3;
this.draw = function(sketchy){
noiseSeed(x - y);
beginShape();
//smile vert l
curveVertex(x - width/2, y - height * this.smile);
curveVertex(x - width/5, y - height);
curveVertex(x + width/5, y - height);
//smile vert 2
curveVertex(x + width/2, y - height * this.smile);
curveVertex(x + width/5, y);
curveVertex(x - width/5, y);
//smile vert 1
curveVertex(x - width/2, y - height * this.smile);
curveVertex(x - width/5, y - height);
curveVertex(x + width/5, y - height);
endShape();
}
}
function createHair(numHairs, roughDir, area, bend){
var hairs = [];
for (var i = 0; i < numHairs; i++){
var hairPoints = [];
var pos = createVector(random(area.min.x, area.max.x), random(area.min.y, area.max.y));
hairs.push(pos);
var dir = roughDir.copy();
var vel = createVector(random(-1,1),random(-1,1));
for(var j = 0; j < 5; j++){
vel = createVector(random(-bend/2,bend/2), random(-bend/2,bend/2));
dir.add(vel);
pos.add(dir);
hairPoints.push(pos.copy());
}
hairs.push(hairPoints);
}
return function draw(){
hairs.forEach(function(h){
var lastP = h[0];
for(var i = 1; i < h.length; i++){
var nextP = h[i];
line(lastP.x, lastP.y, nextP.x, nextP.y);
lastP = nextP;
}
})
}
}
function resetFocusedRandom() {
return Math.seedrandom(arguments);
}
function focusedRandom(min, max, focus, mean) {
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)();
}
// Courtesy of http://www.joshondesign.com/2013/03/01/improvedEasingEquations
function easeOutElastic(x,t,b,c,d){
var s=1.70158;
var p=0;
var a=c;
if(t==0)return b;
if((t/=d)==1)return b+c;if(!p)p=d*.3;
if(a<Math.abs(c)){ a=c; var s=p/4;}
else var s=p/(2*Math.PI)*Math.asin(c/a);
return a*Math.pow(2,-10*t)*Math.sin((t*d-s)*(2*Math.PI)/p)+c+b;
}
<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="readme.purview_helper.js"></script>
<script language="javascript" type="text/javascript" src="face.js"></script>
<script language="javascript" type="text/javascript" src="sketch.js"></script>
<style> body {padding: 0; margin: 0;} </style>
</head>
<body style="background-color:white">
<div id="canvasContainer"></div><br>
</body>
var WIDTH = 960,
HEIGHT = 500;
var colors;
var currentColor;
var faces = [];
//sliders
var main_canvas;
var tilt_slider;
var randomGrid_button;
var hair;
function setup () {
main_canvas = createCanvas(WIDTH, HEIGHT);
// position each element on the page
main_canvas.parent('canvasContainer');
randomGrid_button = createButton('randomise');
randomGrid_button.position(10, 10);
randomGrid_button.mousePressed(generateGrid);
//cover the background
fill(0);
rect(0,0,WIDTH,HEIGHT);
//stroke(255);
generateGrid();
}
var lastMouseX;
var lastMouseY;
var centerPressed = false;
//constants for randoms
var MIN_HAIR_FUNK = 0; //not used / implicit
var MAX_HAIR_FUNK = 5;
var MIN_FACE_VARIABILITY = 3;
var MAX_FACE_VARIABILITY = 0.7;
var MIN_STROKES = 2;
var MAX_STROKES = 3;
var MIN_FACE_SIZE = 50.5;
var MAX_FACE_SIZE = 100;
var MIN_NUM_HAIRS = 30;
var MAX_NUM_HAIRS = 70;
function draw() {
background(0);
var funkAmt = mouseX / WIDTH;
var pos = createVector(mouseX, mouseY);
//draw things on top
if(mouseIsPressed){
if(mouseButton == LEFT){
if(lastMouseX != 0 || lastMouseY != 0){
line(lastMouseX,lastMouseY,mouseX,mouseY);
}
lastMouseX = mouseX;
lastMouseY = mouseY;
}
} else {
lastMouseX = lastMouseY = 0;
centerPressed = false;
}
faces.forEach(function(elem){
if(elem.on(mouseX,mouseY)){
elem.mouseOn = true;
if(!elem.popCalled){
elem.pop();
}
}else{
elem.popCalled = false;
elem.mouseOn = false;
}
});
//draw faces
faces.forEach(function(elem){
if(!elem.isDrawn){
elem.draw(1.0);
}
});
}
function generateGrid(){
var numX = 5;
var numY = 3;
faces = [];
for(var i = 0; i < numX; i++){
for (var j = 0; j < numY; j++) {
var xSpace = (width / numX);
var ySpace = (height / numY);
var f = new Face(createVector(i * xSpace + xSpace/2 , j * ySpace + ySpace/2) , { min:MIN_FACE_SIZE, max:MAX_FACE_SIZE }, color(random(254,255),150), random(MIN_FACE_VARIABILITY, MAX_FACE_VARIABILITY));
faces.push(f);
}
}
}
function keyTyped() {
if (key == '!') {
saveBlocksImages();
} else if (key == '1'){
currentColor = color.black;
} else if (key == '2'){
currentColor = color.blue;
} else if (key == '3'){
currentColor = color.green;
} else if (key == 'r'){
rect(0,0,WIDTH,HEIGHT);
faces = [];
}
}
function sketchyEllipse(xOrig, yOrig, wid, hei, funkAmt, rotations, seed){
var frequency = 3; //jaggedness
seed = seed | 70.8;
wid = wid * 0.5;
hei = hei * 0.5;
//noise seed based on position
noiseSeed(seed);
//set the max funkyness of the ellipse based on size & funk amt
var avgSizeOffset = ((wid + hei) * (constrain(funkAmt, 0, 1.0) * 0.7));
var numPoints = 50;
for(var i = 0; i < rotations; i++){
noiseSeed((i + seed) * 10);
beginShape();
for(var k = 0; k <= numPoints; k++){
var amt = (k/numPoints) * TWO_PI;
var noiseX = (cos((k/numPoints)*TWO_PI) + 1)*frequency;
var noiseY = (sin((k/numPoints)*TWO_PI) + 1)*frequency;
var noiseAmt = noise(noiseX, noiseY) - 0.5;
var nextX = xOrig + sin(amt) * (wid + noiseAmt * avgSizeOffset);
var nextY = yOrig + cos(amt) * (hei + noiseAmt * avgSizeOffset);
vertex(nextX, nextY);
}
endShape(CLOSE);
}
}
/*Maps values 0 -> 1 to 0 -> 1 -> 0 in a zigzag pattern*/
function saw(val){
val = abs(val) % 1.0;
val = val * 2;
if(val > 1.0){
val = 2.0 - val;
}
console.log(val);
return val;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment