let ground, wallLeft, wallRight
inter = loadFont('https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfMZhrib2Bg-4.ttf')
const outlineFrag = `precision highp float;
uniform float outlineThickness;
uniform vec4 outlineColor;
const int MAX_SAMPLES = 70;
vec4 baseColor = texture2D(tex0, vTexCoord);
float d = ${2 * Math.PI} * outlineThickness;
int samples = int(min(ceil(d * 1.5), float(MAX_SAMPLES)));
for (int i = 0; i < MAX_SAMPLES; i++) {
float t = float(i) / float(samples);
float angle = 3. * t * ${2 * Math.PI};
float r = clamp(t * 3./2., 0., 1.);
) * r * outlineThickness / canvasSize
for (int x = -MAX_OFF; x <= MAX_OFF; x++) {
if (abs(float(x)) > outlineThickness) continue;
for (int y = -MAX_OFF; y <= MAX_OFF; y++) {
if (abs(float(y)) > outlineThickness) continue;
gl_FragColor.rgb = mix(outlineColor.rgb, baseColor.rgb, baseColor.a);
gl_FragColor *= max(smoothstep(0., 1., coverage), baseColor.a);
createCanvas(windowWidth, windowHeight, WEBGL)
horseDrawing = createFramebuffer({ width: 300, height: 300 })
outlineFilter = createFilterShader(outlineFrag)
for (let i = 0; i < 6; i++) {
type: random(['square', 'triangle']),
rotation: random(TWO_PI),
x: random(-width/2, width/2),
y: random(-height/2, height/2)
thickness = createSlider(5, 50, 20, 0.1)
thickness.position(width/2 - 20, height/2 + horseDrawing.height/2 + 25)
inputHorse = createInput()
inputHorse.position(width/2 - horseDrawing.width/2, height/2 + horseDrawing.height/2 + 100)
inputHorse.size(horseDrawing.width)
const img = loadImage(inputHorse.value(), () => {
horseDrawing.draw(() => {
makePhysics(horseDrawing)
}, () => console.error('Error reading data!'))
function mainToFbo(x, y) {
return [x - width/2, y - height/2]
function mousePressed() {
horseDrawing.draw(() => {
circle(...mainToFbo(mouseX, mouseY), thickness.value())
function mouseDragged() {
horseDrawing.draw(() => {
strokeWeight(thickness.value())
...mainToFbo(pmouseX, pmouseY),
...mainToFbo(mouseX, mouseY)
horseDrawing.draw(() => {
outlineFilter.setUniform('outlineColor', [0, 0, 0, 1])
outlineFilter.setUniform('outlineThickness', 6)
outlineFilter.setUniform('method', 0)
makePhysics(horseDrawing)
horseDrawing.get().canvas.toDataURL()
rect(0, 0, horseDrawing.width, horseDrawing.height)
image(horseDrawing, 0, 0)
textAlign(CENTER, BOTTOM)
text('Draw your horse below (fill him in!) then hit enter', 0, -horseDrawing.height/2 - 20)
text('Thickness:', -horseDrawing.width/2, horseDrawing.height/2 + 50)
text('...or paste someone else\'s horse:', -horseDrawing.width/2, horseDrawing.height/2 + 90)
if (!inputHorse.value()) {
text('Share your horse in the comments:', -width/2 + 50, -height/2 + 45)
Matter.Engine.update(engine, 1000 / 60)
if (bg.type === 'square') {
triangle(-20, 25, 8, -30, 36, 25);
const r = random(20, 100)
const circ = Matter.Bodies.circle(random(-width/2, width/2), -height/2, r, {
Matter.Body.setVelocity(circ, {
Matter.World.add(engine.world, [circ])
for (let t = 0; t < delaunay.triangles.length/3; t++) {
for (let off = 0; off < 3; off++) {
const idx = delaunay.triangles[t*3 + off]
const { x, y } = verts[idx].position
vertex(x, y, 0, 1.1*(u - 0.5)+0.5, 1.1*(v - 0.5)+0.5)
for (const ball of balls) {
circle(ball.position.x, ball.position.y, ball.r * 2)
function triangulate(img) {
for (let i = 0; i < 1000; i++) {
const x = floor(random(img.width))
const y = floor(random(img.height))
const a = img.pixels[4 * (y * img.width + x) + 3]
coords.push([x/img.width, y/img.height])
return [coords, Delaunator.from(coords)]
function edgesOfTriangle(t) { return [3 * t, 3 * t + 1, 3 * t + 2]; }
function triangleOfEdge(e) { return Math.floor(e / 3); }
function nextHalfedge(e) { return (e % 3 === 2) ? e - 2 : e + 1; }
function prevHalfedge(e) { return (e % 3 === 0) ? e + 2 : e - 1; }
function forEachTriangleEdge(points, delaunay, callback) {
for (let e = 0; e < delaunay.triangles.length; e++) {
if (e > delaunay.halfedges[e]) {
const p = delaunay.triangles[e]
const q = delaunay.triangles[nextHalfedge(e)]
if (delaunay.halfedges[e] >= 0) {
function makePhysics(img) {
engine = Matter.Engine.create()
const res = triangulate(img)
for (let i = 0; i < pts.length; i++) {
const x = pts[i][0] * side - side/2
const y = pts[i][1] * side - side/2
const vert = Matter.Bodies.circle(x, y, side/70, { inertia: Infinity })
Matter.World.add(engine.world, verts)
forEachTriangleEdge(pts, delaunay, (_e, p, q) => {
springs.push(Matter.Constraint.create({
Matter.World.add(engine.world, springs)
ground = Matter.Bodies.rectangle(0, height / 2 + 30, width, 60, {
wallLeft = Matter.Bodies.rectangle(-width/2 - 30, 0, 60, 3 * height, {
wallRight = Matter.Bodies.rectangle(width/2 + 30, 0, 60, 3 * height, {
Matter.World.add(engine.world, [ground, wallLeft, wallRight])