xxxxxxxxxx
/*
I've wanted to make any font be able to draw itself in. This involves *parameterizing*
the shape: giving every point on the path a parameter value, between 0 and 1, indicating
how far along the path it is, so that you can transition it in based on that value.
The medial axis transform is a way to break an arbitrary shape up into a "skeleton". I thought
that skeleton might be able to give me a way to accomplish this. It probably still can, but
this is as far as I got in an hour, and discovered some problems:
- the nodes of the skeleton are double-sided, so the left half of a curve animates in, and
only later does the right half animate in
- the parameterization goes along the edge of the shape. Ideally it would follow continuous
looking paths first, and branches later.
At least in the mean time it still looks kinda cool!
*/
let font
let letterGeoms = {}
let drawShader
OPC.text({ name: 'input', label: 'Text', value: 'testing' })
function preload() {
// Sniglet 800
// font = loadFont('https://fonts.gstatic.com/s/sniglet/v17/cIf4MaFLtkE3UjaJ_ImHRGEsnIJkWL4.ttf')
// Tac One
// font = loadFont('https://fonts.gstatic.com/s/tacone/v2/ahcZv8Cj3zw7qDr8fO4hU-FwnU0.ttf')
// Imbue
// font = loadFont('https://fonts.gstatic.com/s/imbue/v27/RLpXK5P16Ki3fXhj5cvGrqjocPk4n-gVX3M93TnrnvhoPxaQfOsNNK-Q4xY.ttf')
// Open Sans
font = loadFont('https://fonts.gstatic.com/s/opensans/v40/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0C4nY1M2xLER.ttf')
}
function setup() {
createCanvas(windowWidth, windowHeight, WEBGL)
textureMode(NORMAL)
drawShader = baseMaterialShader().modify({
fragmentDeclarations: 'uniform float progress;',
'Inputs getPixelInputs': `(Inputs inputs) {
// inputs.color *= smoothstep(progress, progress+0.01, inputs.texCoord.y);
// inputs.color *= inputs.texCoord.y;
if (inputs.texCoord.y > progress) {
inputs.color = vec4(0.);
}
return inputs;
}`
})
}
const defaultSize = 20
const {
findMats,
toScaleAxis,
traverseEdges,
getCurveToNext,
getBoundaryBeziersToNext,
getCpNodesOnCircle,
isTerminating,
isSharp,
getBranches,
getCurveBetween,
simplifyMat,
} = MAT
function rThreshold(cpNode) {
const rs = []
const cpStart = cpNode
do {
rs.push(cpNode.cp.circle.radius)
cpNode = cpNode.next
} while (cpNode !== cpStart)
rs.sort((a, b) => a - b)
// return rs[floor(0.99 * rs.length)]
return rs[rs.length-1]
}
function cpNodeChildren(cpNode) {
let cp = cpNode.next;
if (isTerminating(cpNode)) { return []; }
let children = [cp];
let cp_ = cp;
while (cp_.nextOnCircle !== cp.prevOnCircle) {
cp_ = cp_.nextOnCircle;
children.push(cp_);
}
return children;
}
function treeChildrenLength(cpNode) {
if (cpNode.treeChildrenLength === undefined) {
const children = cpNodeChildren(cpNode)
cpNode.treeChildrenLength = 0
if (children.length === 1) {
for (const child of cpNodeChildren(cpNode)) {
const distToChild = Math.hypot(
cpNode.cp.circle.center[0] - child.cp.circle.center[0],
cpNode.cp.circle.center[1] - child.cp.circle.center[1],
)
cpNode.treeChildrenLength += distToChild + treeChildrenLength(child)
}
}
}
return cpNode.treeChildrenLength
}
function getLetter(letter) {
if (letterGeoms[letter]) return letterGeoms[letter]
letterGeoms[letter] = buildGeometry(() => {
const loops = font.paths(letter, 0, 0, defaultSize)
const mats = findMats(loops, 1) //.map(mat => simplifyMat(mat, 20, 5))
const sats = mats.map((mat) => toScaleAxis(mat, 5))
const levels = 2
let shapes = []
const maxRs = []
let currentShape
let currentLoop
let loopStart
let lastBoundary
let lastMedial
let totalLength = 0
for (const sat of sats) {
// Start node to iterate from - it also represents the maximum radius
// maximal disk.
const cpStart = sat.cpNode
const maxR = cpStart.cp.circle.radius
const minR = maxR * 0.5 //rThreshold(cpStart)
// Iterate through all boundary piece curves
let cpNode = cpStart
do {
// Get the loop that this boundary piece belongs to
let loop = cpNode.cp.pointOnShape.curve.loop;
const medialCurve = getCurveToNext(cpNode)
const medialCurves = medialCurve ? [medialCurve] : []
const boundaryCurves = getBoundaryBeziersToNext(cpNode) || []
const includedNodes = [cpNode]
if (loop !== currentLoop) {
currentLoop = loop
loopStart = cpNode
currentShape = []
for (let i = 0; i < levels; i++) {
currentShape.push([])
}
shapes.push(currentShape)
maxRs.push(maxR)
}
let medialCurveLen = medialCurve ? approxCurveLen(medialCurves) : 0
let boundaryCurveLen = boundaryCurves ? approxCurveLen(boundaryCurves) : 0
while (
cpNode.next !== cpStart &&
cpNodeChildren(cpNode.next).length === 1 &&
(
cpNode.next.cp.circle.radius < minR ||
(medialCurveLen < 2 && boundaryCurveLen < 2)
) &&
cpNode.next.cp.pointOnShape.curve.loop === loop // &&
) {
cpNode = cpNode.next
const nextMedial = getCurveToNext(cpNode)
if (nextMedial) {
medialCurves.push(nextMedial)
medialCurveLen += approxCurveLen([nextMedial])
}
const nextBoundary = getBoundaryBeziersToNext(cpNode)
if (nextBoundary) {
boundaryCurves.push(nextBoundary)
boundaryCurveLen += approxCurveLen(nextBoundary)
}
includedNodes.push(cpNode)
}
const prevR = includedNodes[0].cp.circle.radius
const nextR = cpNode.next.cp.circle.radius
if (medialCurves.length === 0) {
medialCurves.push([lastMedial, lastMedial])
}
if (boundaryCurves.length === 0) {
boundaryCurves.push([lastBoundary, lastBoundary])
}
const interps = getInterpolatedCurves(medialCurves, boundaryCurves, prevR, nextR, maxR, levels)
const last = cpNode.next === cpStart || cpNode.next.cp.pointOnShape.curve.loop !== loop || isSharp(cpNode.next) || cpNode.next.isHoleClosing || isTerminating(cpNode.next)
for (const [i, interp] of interps.entries()) {
if (last) {
currentShape[i].push(interp)
} else {
currentShape[i].push(interp.slice(0, -1))
}
}
if (!currentShape[2]) currentShape[2] = []
const lengths = currentShape[2]
let prev
for (let i = lengths.length; i < currentShape[1].length; i++) {
const next = currentShape[1][i]
if (prev) {
const dist = Math.hypot(prev.x - next.x, prev.y - next.y)
if (!isNaN(dist)) {
totalLength += dist
}
}
lengths.push(totalLength)
prev = next
}
currentShape.push(lengths)
const lastMedialCurve = medialCurves[medialCurves.length-1]
lastMedial = lastMedialCurve[lastMedialCurve.length-1]
const lastBoundaryCurve = boundaryCurves[boundaryCurves.length-1]
lastBoundary = lastBoundaryCurve[lastBoundaryCurve.length-1]
// Go to next boundary piece
cpNode = cpNode.next
} while (cpNode !== cpStart)
}
for (const [shapeIdx, shape] of shapes.entries()) {
const maxR = maxRs[shapeIdx]
const prev = shape[0]
const next = shape[1]
const lengths = shape[2]
// console.log(lengths)
// console.log(totalLength)
beginShape(QUAD_STRIP)
for (let j = 0; j < prev.length; j++) {
const k = j % next.length
vertex(next[k].x, next[k].y, next[k].z, 0, (lengths[k] || 0)/totalLength)
vertex(prev[k].x, prev[k].y, prev[k].z, 1, (lengths[k] || 0)/totalLength)
}
endShape()
}
const graph = {}
for (const [i, shape1] of shapes.entries()) {
const loop1 = shape1[0]
const loop1Start = loop1[0]
const loop1End = loop1[loop1.length-1]
const connections = {}
for (const [j, shape2] of shapes.entries()) {
const loop2 = shape2[0]
const loop2Start = loop2[0]
const loop2End = loop2[loop2.length-1]
let found = false
for (const [start, end] of [[loop2End, loop1Start], [loop1End, loop2Start]]) {
if (start.dist(end) < 1e-1) {
found = true
connections[j] = true
break
}
}
}
graph[i] = connections
}
const components = []
const seen = {}
const visit = (key) => {
seen[key] = true
const component = components[components.length-1]
component.push(key)
for (const key2 in graph[key]) {
if (seen[key2]) continue
visit(key2)
break
}
}
for (const key in graph) {
if (seen[key]) continue
components.push([])
visit(key)
}
/*beginShape()
let first = true
for (const component of components) {
if (!first) beginContour()
for (const index of component) {
const shape = shapes[parseInt(index)]
const loop = shape[shape.length-1]
loop.forEach((v) => vertex(v.x, v.y, v.z))
}
if (!first) endContour()
first = false
}
endShape()*/
})
letterGeoms[letter].clearColors()
return letterGeoms[letter]
}
let lastInput = ''
let startTime = -1
function draw() {
background(255)
orbitControl()
push()
textFont(font)
textSize(defaultSize)
const lines = input.split('\n').map((line) => {
const chars = line.split('')
const widths = chars.map((char) => textWidth(char))
const totalWidth = widths.reduce((acc, next) => acc + next, 0)
const models = chars.map((char) => char === ' ' ? undefined : getLetter(char))
return { chars, widths, totalWidth, models }
})
if (lastInput !== input) {
startTime = millis()
lastInput = input
}
t = millis() - startTime
const maxWidth = Math.max(0, lines.map(l => l.totalWidth))
scale(15)
translate(-maxWidth/2, -lines.length/2 * textLeading())
noStroke()
fill(0)
let j = 0
for (const line of lines) {
translate(0, textLeading())
push()
for (const [i, m] of line.models.entries()) {
if (m) {
shader(drawShader)
drawShader.setUniform('progress', map(t, j*100, j*100 + 1000, 0, 1, true))
model(m)
}
translate(line.widths[i], 0)
j++
}
pop()
}
pop()
}