Pen plotter font

Full alphabet, numbers & punctuation with smooth curves + auto word wrap

Log in to post a comment.

// Pen Plotter Font @turtletoy
// Full alphabet, numbers & punctuation with smooth curves + auto word wrap

const text = "The quick brown fox jumps over the lazy dog";  // type:text
const sz = 3.2;        // min=1, max=8, step=0.2
const xStart = -90;    // min=-95, max=50, step=5
const yStart = -60;    // min=-80, max=80, step=5
const xEnd = 90;       // min=0, max=95, step=5
const charGap = 0.9;   // min=0.3, max=2, step=0.1
const wordGap = 3;     // min=1, max=6, step=0.5
const lineHeight = 11; // min=6, max=18, step=1
const curveSmooth = 5; // min=1, max=12, step=1

const turtle = new Turtle();

function spline(P, n) {
    if (P.length < 3) return P;
    const r = [];
    for (let i = 0; i < P.length - 1; i++) {
        const p0 = P[Math.max(i-1,0)], p1 = P[i], p2 = P[i+1], p3 = P[Math.min(i+2,P.length-1)];
        for (let t = 0; t < n; t++) {
            const s = t/n, s2 = s*s, s3 = s2*s;
            r.push([
                .5*(2*p1[0]+(-p0[0]+p2[0])*s+(2*p0[0]-5*p1[0]+4*p2[0]-p3[0])*s2+(-p0[0]+3*p1[0]-3*p2[0]+p3[0])*s3),
                .5*(2*p1[1]+(-p0[1]+p2[1])*s+(2*p0[1]-5*p1[1]+4*p2[1]-p3[1])*s2+(-p0[1]+3*p1[1]-3*p2[1]+p3[1])*s3)
            ]);
        }
    }
    r.push(P[P.length-1]);
    return r;
}

const F = {
'A':[5,[[[0,7],[2.5,0],[5,7]],[[1.2,4.5],[3.8,4.5]]]],
'B':[5,[[[0,0],[0,7]],[[0,0],[3,0],[4.5,0.8],[4.5,2.7],[3,3.5],[0,3.5]],[[0,3.5],[3.5,3.5],[5,4.3],[5,6.2],[3.5,7],[0,7]]]],
'C':[5,[[[5,1.2],[3.8,0],[1.2,0],[0,1.5],[0,5.5],[1.2,7],[3.8,7],[5,5.8]]]],
'D':[5,[[[0,0],[0,7]],[[0,0],[3,0],[5,1.8],[5,5.2],[3,7],[0,7]]]],
'E':[4.5,[[[4.5,0],[0,0],[0,7],[4.5,7]],[[0,3.5],[3.2,3.5]]]],
'F':[4.5,[[[4.5,0],[0,0],[0,7]],[[0,3.5],[3.2,3.5]]]],
'G':[5,[[[5,1.2],[3.8,0],[1.2,0],[0,1.5],[0,5.5],[1.2,7],[3.8,7],[5,5.5],[5,3.5],[3,3.5]]]],
'H':[5,[[[0,0],[0,7]],[[5,0],[5,7]],[[0,3.5],[5,3.5]]]],
'I':[3,[[[0.5,0],[2.5,0]],[[1.5,0],[1.5,7]],[[0.5,7],[2.5,7]]]],
'J':[4.5,[[[1.5,0],[4.5,0],[4.5,5.5],[3.2,7],[1.8,7],[0.5,5.5]]]],
'K':[5,[[[0,0],[0,7]],[[5,0],[0,4.2],[5,7]]]],
'L':[4.5,[[[0,0],[0,7],[4.5,7]]]],
'M':[6,[[[0,7],[0,0],[3,4.5],[6,0],[6,7]]]],
'N':[5,[[[0,7],[0,0],[5,7],[5,0]]]],
'O':[5,[[[1.5,0],[3.5,0],[5,1.5],[5,5.5],[3.5,7],[1.5,7],[0,5.5],[0,1.5],[1.5,0]]]],
'P':[5,[[[0,7],[0,0],[3.5,0],[5,1],[5,2.7],[3.5,3.5],[0,3.5]]]],
'Q':[5,[[[1.5,0],[3.5,0],[5,1.5],[5,5.5],[3.5,7],[1.5,7],[0,5.5],[0,1.5],[1.5,0]],[[3.2,5.8],[5.5,7.8]]]],
'R':[5,[[[0,7],[0,0],[3.5,0],[5,1],[5,2.7],[3.5,3.5],[0,3.5]],[[2.5,3.5],[5,7]]]],
'S':[5,[[[5,1.2],[3.8,0],[1.2,0],[0,1.2],[0,2.3],[1.2,3.5],[3.8,3.5],[5,4.7],[5,5.8],[3.8,7],[1.2,7],[0,5.8]]]],
'T':[5,[[[0,0],[5,0]],[[2.5,0],[2.5,7]]]],
'U':[5,[[[0,0],[0,5.5],[1.5,7],[3.5,7],[5,5.5],[5,0]]]],
'V':[5,[[[0,0],[2.5,7],[5,0]]]],
'W':[6,[[[0,0],[1.5,7],[3,3.2],[4.5,7],[6,0]]]],
'X':[5,[[[0,0],[5,7]],[[5,0],[0,7]]]],
'Y':[5,[[[0,0],[2.5,3.5],[5,0]],[[2.5,3.5],[2.5,7]]]],
'Z':[5,[[[0,0],[5,0],[0,7],[5,7]]]],
'a':[4,[[[4,3.5],[3,3],[1,3],[0,4.5],[0,5.8],[1,7],[3,7],[4,5.8]],[[4,3],[4,7]]]],
'b':[4,[[[0,0],[0,7]],[[0,5],[1.2,3],[3,3],[4,4.2],[4,5.8],[3,7],[1.2,7],[0,5.8]]]],
'c':[3.5,[[[3.5,3.5],[2.5,3],[1,3],[0,4.2],[0,5.8],[1,7],[2.5,7],[3.5,6.5]]]],
'd':[4,[[[4,0],[4,7]],[[4,5],[2.8,3],[1,3],[0,4.2],[0,5.8],[1,7],[2.8,7],[4,5.8]]]],
'e':[4,[[[0,5],[4,5],[4,4],[3,3],[1,3],[0,4.2],[0,5.8],[1,7],[3,7],[4,6.5]]]],
'f':[2.8,[[[2.8,0.5],[2,0],[1.2,0],[0.6,1],[0.6,7]],[[0,3],[2.5,3]]]],
'g':[4,[[[4,3],[4,8.5],[3,9.5],[1,9.5],[0,8.5]],[[4,5],[3,3],[1,3],[0,4.2],[0,5.8],[1,7],[3,7],[4,5.8]]]],
'h':[4,[[[0,0],[0,7]],[[0,4.8],[1.5,3],[3,3],[4,4],[4,7]]]],
'i':[1,[[[0.5,1.5],[0.5,2.1]],[[0.5,3],[0.5,7]]]],
'j':[2,[[[1.5,1.5],[1.5,2.1]],[[1.5,3],[1.5,8.5],[0.5,9.5],[0,9.5]]]],
'k':[3.5,[[[0,0],[0,7]],[[3.5,3],[0,5.5],[3.5,7]]]],
'l':[1.5,[[[0,0],[0.5,0],[0.5,6.5],[1.5,7]]]],
'm':[6,[[[0,7],[0,3]],[[0,4.2],[1.5,3],[2.5,3],[3,4],[3,7]],[[3,4.2],[4.5,3],[5.5,3],[6,4],[6,7]]]],
'n':[4,[[[0,7],[0,3]],[[0,4.8],[1.5,3],[3,3],[4,4],[4,7]]]],
'o':[4,[[[1.2,3],[2.8,3],[4,4.2],[4,5.8],[2.8,7],[1.2,7],[0,5.8],[0,4.2],[1.2,3]]]],
'p':[4,[[[0,3],[0,9.5]],[[0,5],[1.2,3],[3,3],[4,4.2],[4,5.8],[3,7],[1.2,7],[0,5.8]]]],
'q':[4,[[[4,3],[4,9.5]],[[4,5],[2.8,3],[1,3],[0,4.2],[0,5.8],[1,7],[2.8,7],[4,5.8]]]],
'r':[2.8,[[[0,7],[0,3]],[[0,4.8],[1.2,3],[2.3,3],[2.8,3.5]]]],
's':[3.5,[[[3.5,3.8],[2.8,3],[0.8,3],[0,3.8],[0,4.5],[0.8,5],[2.7,5],[3.5,5.5],[3.5,6.2],[2.8,7],[0.8,7],[0,6.2]]]],
't':[3,[[[1,0.5],[1,6.5],[2.5,7],[3,6.8]],[[0,3],[2.5,3]]]],
'u':[4,[[[0,3],[0,5.8],[1.2,7],[3,7],[4,5.8]],[[4,3],[4,7]]]],
'v':[4,[[[0,3],[2,7],[4,3]]]],
'w':[5,[[[0,3],[1.2,7],[2.5,4.5],[3.8,7],[5,3]]]],
'x':[4,[[[0,3],[4,7]],[[4,3],[0,7]]]],
'y':[4,[[[0,3],[2,7]],[[4,3],[1.5,8.5],[0.5,9.5],[0,9.2]]]],
'z':[4,[[[0,3],[4,3],[0,7],[4,7]]]],
'0':[5,[[[1.5,0],[3.5,0],[5,1.5],[5,5.5],[3.5,7],[1.5,7],[0,5.5],[0,1.5],[1.5,0]]]],
'1':[3,[[[0,1.8],[1.5,0],[1.5,7]],[[0,7],[3,7]]]],
'2':[5,[[[0,1.5],[1,0],[3.5,0],[5,1.2],[5,2.5],[0,7],[5,7]]]],
'3':[5,[[[0,1],[1.5,0],[3.5,0],[5,1.2],[5,2.5],[3.5,3.5],[2,3.5]],[[3.5,3.5],[5,4.5],[5,5.8],[3.5,7],[1.5,7],[0,6]]]],
'4':[5,[[[4,7],[4,0],[0,5],[5,5]]]],
'5':[5,[[[5,0],[0,0],[0,3.2],[3.5,3.2],[5,4.2],[5,5.8],[3.5,7],[1.5,7],[0,6]]]],
'6':[5,[[[4,0.5],[3,0],[1.5,0],[0,1.5],[0,5.5],[1.5,7],[3.5,7],[5,5.5],[5,4.5],[3.5,3.5],[1.5,3.5],[0,4.5]]]],
'7':[5,[[[0,0],[5,0],[2,7]]]],
'8':[5,[[[2.5,3.5],[1,3.5],[0,2.5],[0,1],[1.2,0],[3.8,0],[5,1],[5,2.5],[3.5,3.5],[2.5,3.5]],[[2.5,3.5],[0.5,4.5],[0,5.8],[1.2,7],[3.8,7],[5,5.8],[4.5,4.5],[2.5,3.5]]]],
'9':[5,[[[1,6.5],[2,7],[3.5,7],[5,5.5],[5,1.5],[3.5,0],[1.5,0],[0,1.5],[0,2.5],[1.5,3.5],[3.5,3.5],[5,2.5]]]],
'.':[1,[[[0.5,6.5],[0.5,7]]]],
',':[1.2,[[[0.6,6.5],[0.2,8]]]],
'!':[1,[[[0.5,0],[0.5,5]],[[0.5,6.5],[0.5,7]]]],
'?':[4,[[[0,1.5],[0.8,0],[3.2,0],[4,1.2],[4,2.5],[2,4.2],[2,5]],[[2,6.5],[2,7]]]],
'-':[3,[[[0,3.5],[3,3.5]]]],
':':[1,[[[0.5,2.5],[0.5,3]],[[0.5,6.5],[0.5,7]]]],
';':[1.2,[[[0.6,2.5],[0.6,3]],[[0.6,6.5],[0.2,8]]]],
'\'':[1,[[[0.5,0],[0.5,1.8]]]],
'"':[2.5,[[[0.5,0],[0.5,1.8]],[[2,0],[2,1.8]]]],
'(':[2,[[[2,0],[0.8,1.8],[0.8,5.2],[2,7]]]],
')':[2,[[[0,0],[1.2,1.8],[1.2,5.2],[0,7]]]],
'/':[3,[[[0,7],[3,0]]]],
'&':[5,[[[5,7],[1,3],[2.5,0],[3.8,0],[4.5,1],[3.5,2.5],[0,5.5],[0,6],[1,7],[3,7],[5,5]]]],
'@':[6,[[[5,2],[3.5,1],[2,1],[1,2],[1,4],[2.5,4.5],[4,4],[4,2],[3,1.5]],[[5,4.5],[5,1.5],[4,0.5],[2,0],[0.5,1],[0,3],[0.5,5.5],[2,6.5],[4,6.5],[5.5,5.8]]]],
'#':[5,[[[1.5,0],[0.5,7]],[[3.5,0],[2.5,7]],[[0,2.5],[5,2.5]],[[0,4.5],[5,4.5]]]],
'+':[4,[[[0,3.5],[4,3.5]],[[2,1.5],[2,5.5]]]],
'=':[4,[[[0,2.5],[4,2.5]],[[0,4.5],[4,4.5]]]],
'_':[5,[[[0,7.5],[5,7.5]]]],
};

// Get character width
function charW(ch) {
    if (ch === ' ') return wordGap;
    const d = F[ch];
    return d ? d[0] + charGap : 3 + charGap;
}

// Measure word width in scaled units
function wordWidth(word) {
    let w = 0;
    for (const ch of word) w += charW(ch) * sz;
    return w - charGap * sz; // remove trailing gap
}

// Word-wrap text into lines that fit between xStart and xEnd
function wrapText(txt) {
    const maxW = xEnd - xStart;
    const manualLines = txt.split('\\n');
    const wrapped = [];
    
    for (const mLine of manualLines) {
        const words = mLine.split(' ');
        let currentLine = '';
        let currentW = 0;
        
        for (let i = 0; i < words.length; i++) {
            const word = words[i];
            const ww = wordWidth(word);
            const spaceW = currentLine.length > 0 ? wordGap * sz : 0;
            
            if (currentLine.length > 0 && currentW + spaceW + ww > maxW) {
                wrapped.push(currentLine);
                currentLine = word;
                currentW = ww;
            } else {
                currentLine += (currentLine.length > 0 ? ' ' : '') + word;
                currentW += spaceW + ww;
            }
        }
        wrapped.push(currentLine);
    }
    return wrapped;
}

function drawChar(ch, ox, oy, s) {
    const d = F[ch];
    if (!d) return 3;
    const [w, strokes] = d;
    strokes.forEach(st => {
        const pts = st.map(([x,y]) => [ox + x*s, oy + y*s]);
        const sm = pts.length > 2 ? spline(pts, curveSmooth) : pts;
        turtle.penup();
        turtle.goto(sm[0]);
        turtle.pendown();
        for (let i = 1; i < sm.length; i++) turtle.goto(sm[i]);
    });
    return w;
}

function walk(i) {
    if (i > 0) return false;
    const lines = wrapText(text);
    for (let ln = 0; ln < lines.length; ln++) {
        let x = xStart;
        const y = yStart + ln * lineHeight * sz;
        for (const ch of lines[ln]) {
            if (ch === ' ') { x += wordGap * sz; continue; }
            const w = drawChar(ch, x, y, sz);
            x += (w + charGap) * sz;
        }
    }
    return false;
}