I tried it.

Brown is generated hair. The red circle is the hairbrush. The hairstyles were achieved by brushing the hair with the hairbrush.
Here's how that works:
The Hair
Hairs are like this:
- Bendy. Like a bezier curve. Quadratic ones are pretty simple.
- Finite. They start and end somewhere. They also have some fixed length.
- Coloured. They've got some RGB to them, usually pretty heavy in the R and G.
- (Weighty.) Let's ignore gravity to keep things simple -- and to allow awesome hairstyles.
This is roughly how you might translate that into a program:
oneHair = {
start = vector(100,100),
controlPoint = vector(120,70), -- For the bezier
end = vector(100,200),
length = 100,
update = function(self)
-- Make sure the END is within LENGTH of START
-- (This isn't actually quite right, since beziers aren't lines,
-- but it's a pretty good approximation...)
end,
draw = function(self)
-- Make a bezier curve using START, CONTROL and END.
-- Sample it at intervals. Draw lines between samples.
end
}
And then you just create really many. (There are 1200 in each of the pictures.)
It's useful to create them on or within some circle describing the shape of the head or the line of the fringe. (I used a filled half-circle for the bulk of the hair an edge of a different half-circle for the fringe.)
You can also vary the lengths of hairs. (I find bangs running down both sides of the face attractive, so I approximated hair length with this quadratic function (picture just below), using the horizontal distance from the centre of the aforementioned guide circle as input.)

I recommend a similar approach, obviously keeping the desired style in mind.
The Brush
The brushing mechanic is based on these (idealised) observations:
- Hair that touches the brush moves in the direction the brush is moved in, except slower.
- Brushing along hair straightens the hairs.
- Brushing across hair bends the hairs.
So my brushing code does essentially this:
- Find hairs that touch brush (using their bezier curves)
- For each hair:
- Move the endpoint of the hair by some fraction of the pointer movement vector.
- Project the movement vector of the pointer onto the perpendicular of the hair's direction. This gives a vector describing the brush motion in relation to the hair.
- If the brush motion in relation to the hair is more along than across the hair, move the control point some fraction of the way toward the midpoint of the hair. This smooths the hair.
- Else (if the brush motion is more across than along, move the control point some fraction of the pointer's movement vector.
The Code
I made it with LÖVE in Lua. Here's my main.lua
, if you just want to read:
local v = require "vector"
local graphics = love.graphics
local mouse = love.mouse
graphics.setBackgroundColor(255,255,255) --> white background
-- A function that makes bezier functions
-- Beziers have start point p0
-- control point p1
-- end point p2
local function makeBezierFunction(p0,p1,p2)
return function (t)
local pow = math.pow
return pow( (1-t),2 ) * p0
+ 2 * (1-t) * t * p1
+ pow(t,2) * p2
end
end
-- Hair class
-- Encapsulates all that needs to be known to create, draw or update a hair.
local hair = {}
function hair.draw(self)
-- Make a bezier function describing the shape of the hair
local shape = makeBezierFunction(
self.from,
self.mid,
self.to
)
-- This is the increment for one segment of a hair's bezier function
local segIncr = 1/self.resolution
for n=0,1-segIncr,segIncr do
-- This and next sample point
local x1,y1 = shape(n):unpack()
local x2,y2 = shape(n + segIncr):unpack()
graphics.setColor(unpack(self.col))
graphics.line(x1,y1, x2,y2)
end
end
function hair.update(self)
-- Beziers can be longer than lines. This is an approximation to save us
-- from some complicated maths.
local vectorAlongLength = self.to - self.from
-- Move endpoint to within the length of the hair of the start point.
if vectorAlongLength:len() > self.length then
self.to = self.from + vectorAlongLength:normalized() * self.length
end
end
function hair.new(from, to, verticalBend, col) -- Create a new hair object
return setmetatable({
length = (from - to):len(),
from = from,
to = to,
mid = from + v(0,verticalBend),
verticalBend = verticalBend,
resolution = 5,
col = col,
}, {
__index = hair
})
end
setmetatable(hair, {
__call = function(_,...) return hair.new(...) end
})
local hairs = {} -- all hairs stored here
local selectorSize = 30 -- radius of brush
local fringeOrigin = v(325,260) -- origin of the fringe guideline circle
local fringeRadius = 90 -- radius of the fringe guideline circle
-- Terrible drawing of a bald person, happy to soon receive lots of hair.
local portrait = graphics.newImage "portrait.png"
function love.load()
---[[ Bulk of hair, at the back
local numHairs = 1000
for _=1,numHairs do
-- Get a random angle within 180 degrees
local randomAngle = math.random() * math.pi + math.pi/2
-- Find random point within circle (though offset somewhat)
local point = fringeOrigin + v(0,-30) + v(math.sin(randomAngle), math.cos(randomAngle)) * (fringeRadius - 20 + math.random(50))
-- Fairly dark brown
local randomValue = math.random(50)
local randomCol = {randomValue+50, randomValue+30, randomValue}
local length = 30+math.random(50 * (math.pow(2*(point.x - fringeOrigin.x)/fringeRadius, 2)))
local horizontalBend = -15 + math.random(30)
local verticalBend = math.random(length)
local newHair = hair(v(point.x, point.y), v(point.x+horizontalBend, point.y+length), verticalBend, randomCol)
table.insert(hairs, newHair)
end
--]]
---[[ Fringe
local numHairs = 200
-- get random points on fringe circle arc
for _=1,numHairs do
-- Get a random angle within 180 degrees
local randomAngle = math.random() * math.pi + math.pi/2
-- Find random point somewhere ON (NOT WITHIN) the fringe arc
local point = fringeOrigin + v(math.sin(randomAngle), math.cos(randomAngle)) * fringeRadius
local randomValue = math.random(100)
local randomCol = {randomValue+50, randomValue+30, randomValue}
local length = 50+math.random(50 * (math.pow(2*(point.x - fringeOrigin.x)/fringeRadius, 2)))
local horizontalBend = -15 + math.random(30)
local verticalBend = math.random(length)
local newHair = hair(v(point.x, point.y), v(point.x+horizontalBend, point.y+length), verticalBend, randomCol)
table.insert(hairs, newHair)
end
--]]
end
lastMousePos = v()
mousePos = v()
mouseMovement = v() -- Movement vector of the mouse this step
function love.update()
mousePos = v(mouse.getPosition())
mouseMovement = mousePos - lastMousePos
if mouse.isDown "l" then -- Left mouse button pressed
-- Find all hairs with ends nearby the cursor
local nearbyHairs = {}
for _,hair in ipairs(hairs) do
-- Create a function describing the shape of the bezier
local shape = makeBezierFunction(
hair.from,
hair.from + v(0,hair.verticalBend),
hair.to
)
-- This is the increment for one segment of a hair's bezier function
local segIncr = 1/hair.resolution
for n=0,1,segIncr do
local pointOnShape = v(shape(n):unpack())
-- If it's close enough to the mouse
if (pointOnShape - mousePos):len() < selectorSize then
-- Select it
table.insert(nearbyHairs, hair)
end
end
end
-- Apply transforms to each hair
for _,hair in ipairs(nearbyHairs) do
-- Move endpoint along with movement vector
hair.to = hair.to + 0.5 * mouseMovement
-- Find movement in relation to hair
local movementAcross = mouseMovement:projectOn((hair.to - hair.from):perpendicular())
-- If the movement was ALONG the hair
if math.abs(movementAcross.x) < 10 then
-- Smooth the hair by moving control point closer to midpoint
local midPoint = (hair.to + hair.from) /2
hair.mid = 0.85 * hair.mid + midPoint * 0.15
else -- Movement was ACROSS the hair
-- Move the control point along with the movement vector
hair.mid.x = hair.mid.x + 0.25 * mouseMovement.x
end
end
end
-- Update hairs to make sure their lengths stay the same
for _,hair in ipairs(hairs) do hair:update() end
lastMousePos = mousePos
end
function love.draw(dt)
-- Draw the bald portrait image
graphics.draw(portrait, 200, 100)
-- This is a nice cheat for making less hair look like it has more volume
graphics.setLineWidth(2)
for _,hair in ipairs(hairs) do
hair:draw(dt)
end
-- Draw brush pointer
graphics.setColor(255,0,0)
graphics.circle("line", mousePos.x, mousePos.y, selectorSize)
-- The fringe line
--graphics.arc("line", fringeOrigin.x, fringeOrigin.y, fringeRadius, -math.pi, 0)
end
If you want to have a play with it, get LÖVE (any OS is OK) and open my love file with it.
If you want to see the source code in its entirety, just change the .love
extension to .zip
and unpack.