OPC.text({ name: 'input', label: 'Text', value: 'testing' })
OPC.color({ name: 'material', label: 'Fill', value: '#FFFFFF' })
["#0a1966", "#ffef0d", "#fafafa"],
["#32312e", "#795330", "#c7ae82", "#f5f2e3"],
["#ee726b", "#ffc5c7", "#fef9c6"],
["#203051", "#4464a1", "#62b6de", "#b3dce0", "#e2f0f3"],
["#584594", "#e488b7", "#d74c41", "#f0d235", "#36ad63", "#69bcea", "#fdfdfd"],
OPC.slider({ name: 'metal', label: 'Metalness', min: 0, max: 100, value: 100 })
OPC.slider({ name: 'wiggle', label: 'Wiggle', min: 0, max: 1, step: 0.001, value: 1 })
OPC.toggle({ name: 'showBG', label: 'Show Background', value: false })
getBoundaryBeziersToNext,
font = loadFont('https://fonts.gstatic.com/s/sniglet/v17/cIf4MaFLtkE3UjaJ_ImHRGEsnIJkWL4.ttf')
function rThreshold(cpNode) {
rs.push(cpNode.cp.circle.radius)
} while (cpNode !== cpStart)
function cpNodeChildren(cpNode) {
if (isTerminating(cpNode)) { return []; }
while (cp_.nextOnCircle !== cp.prevOnCircle) {
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 getCharModel(char) {
const loops = font.paths(char, 0, 0, defaultSize)
const mats = findMats(loops, 1)
const sats = mats.map((mat) => toScaleAxis(mat, 5))
for (const sat of sats) {
const cpStart = sat.cpNode
const maxR = cpStart.cp.circle.radius
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) {
for (let i = 0; i < levels; i++) {
shapes.push(currentShape)
let medialCurveLen = medialCurve ? approxCurveLen(medialCurves) : 0
let boundaryCurveLen = boundaryCurves ? approxCurveLen(boundaryCurves) : 0
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
const nextMedial = getCurveToNext(cpNode)
medialCurves.push(nextMedial)
medialCurveLen += approxCurveLen([nextMedial])
const nextBoundary = getBoundaryBeziersToNext(cpNode)
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()) {
currentShape[i].push(...interp)
currentShape[i].push(...interp.slice(0, -1))
const lastMedialCurve = medialCurves[medialCurves.length-1]
lastMedial = lastMedialCurve[lastMedialCurve.length-1]
const lastBoundaryCurve = boundaryCurves[boundaryCurves.length-1]
lastBoundary = lastBoundaryCurve[lastBoundaryCurve.length-1]
} while (cpNode !== cpStart)
models[char] = buildGeometry(() => {
const allOutlines = shapes.flatMap((shape) => shape[0])
const allNormals = shapes.flatMap((shape) => {
let outerNormals = shape[0].map((v, i) => shape[0][(i+1)%shape[0].length].copy().sub(v))
for (let i = 1; i < outerNormals.length; i++) {
const prev = outerNormals[i-1]
const next = outerNormals[i]
if (next.x === 0 && next.y === 0) {
next.set(prev.x, prev.y, 0)
for (let i = outerNormals.length-2; i >= 0; i--) {
const prev = outerNormals[i+1]
const next = outerNormals[i]
if (next.x === 0 && next.y === 0) {
next.set(prev.x, prev.y, 0)
for (const n of outerNormals) n.normalize()
outerNormals = outerNormals.map((n, i) => n.copy().add(outerNormals[(i-1)%outerNormals.length]).normalize())
for (const normal of outerNormals) {
for (const [shapeIdx, shape] of shapes.entries()) {
const maxR = maxRs[shapeIdx]
let outerNormals = shape[0].map((v, i) => shape[0][(i+1)%shape[0].length].copy().sub(v))
for (let i = 1; i < outerNormals.length; i++) {
const prev = outerNormals[i-1]
const next = outerNormals[i]
if (next.x === 0 && next.y === 0) {
next.set(prev.x, prev.y, 0)
for (let i = outerNormals.length-2; i >= 0; i--) {
const prev = outerNormals[i+1]
const next = outerNormals[i]
if (next.x === 0 && next.y === 0) {
next.set(prev.x, prev.y, 0)
for (const n of outerNormals) n.normalize()
outerNormals = outerNormals.map((n, i) => n.copy().add(outerNormals[(i-1)%outerNormals.length]).normalize())
for (const normal of outerNormals) {
outerNormals = outerNormals.map((curr, i) => {
return allNormals.map((v, j) => v.copy().mult(1/(allOutlines[j].copy().sub(shape[0][i]).magSq() + 1)))
.reduce((acc, next) => acc.add(next)).normalize()
const normals = shape.map((ring, i) => {
let ringNormals = ring.map((v, j) => {
const n = outerNormals[j].copy().lerp(createVector(0, 0, 1), t).normalize()
for (let i = 1; i < shape.length; i++) {
const prevN = normals[i-1]
for (let j = 0; j < prev.length; j++) {
const k = j % next.length
normal(nextN[k].x, nextN[k].y, nextN[k].z)
vertex(next[k].x, next[k].y, next[k].z)
normal(prevN[k].x, prevN[k].y, prevN[k].z)
vertex(prev[k].x, prev[k].y, prev[k].z)
for (const [i, shape1] of shapes.entries()) {
const loop1Start = loop1[0]
const loop1End = loop1[loop1.length-1]
for (const [j, shape2] of shapes.entries()) {
const loop2Start = loop2[0]
const loop2End = loop2[loop2.length-1]
for (const [start, end] of [[loop2End, loop1Start], [loop1End, loop2Start]]) {
if (start.dist(end) < 1e-1) {
const component = components[components.length-1]
for (const key2 in graph[key]) {
for (const key in graph) {
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))
models[char].clearColors()
createCanvas(windowWidth, windowHeight, WEBGL)
bg = createFramebuffer({ width: 800, height: 400 })
filterShader = createFilterShader(gradientShader)
distort = createWarp(({ glsl, millis, position }) => {
const amount = glsl.param('amount')
const t = millis.div(1000)
t.mult(2).add(position.y().mult(0.002)).sin().mult(15),
t.mult(0.5).add(position.y().mult(0.02/2)).sin().mult(25),
t.mult(1.5).add(position.x().mult(0.03/2)).sin().mult(25)
}, { space: 'world', defs: 'uniform float amount;' })
const key = palette.join(',')
for (let i = 0; i < 40; i++) {
filterShader.setUniform('size', [bg.width, bg.height])
filterShader.setUniform('numParticles', nParticles.length)
filterShader.setUniform('positions', nParticles.flatMap(() => [
random(-0.5, 0.5) * bg.width,
random(-0.25, 0.25) * bg.height,
filterShader.setUniform('influences', nParticles.map(() => random(0.8, 1)))
filterShader.setUniform('colors', nParticles.flatMap((i) => {
if (i < 3) return [1, 1, 1]
if (i === 3) return [0, 0, 0]
const c = random(palette)
return [red(c)/255, green(c)/255, blue(c)/255]
filterShader.setUniform('blur', 0.05)
const c = color(material)
directionalLight(100, 100, 100, 0, 1, -0.1)
shininess(map(metal, 0, 100, 50, 200))
distort({ amount: wiggle })
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 : getCharModel(char))
return { chars, widths, totalWidth, models }
const maxWidth = Math.max(0, ...lines.map(l => l.totalWidth))
translate(-maxWidth/2, -lines.length/2 * textLeading())
for (const line of lines) {
translate(0, textLeading())
for (const [i, m] of line.models.entries()) {
translate(line.widths[i], 0)