Skip to main content
Tutorials

Draw Letters with LEDs

Overview

This tutorial will walk you through creating a reusable LedLetter component that displays any capital letter (A-Z) using LEDs on a PCB. We'll use the @tscircuit/alphabet library to get letter shape data and mathematically calculate LED positions along each letter's strokes.

The @tscircuit/alphabet Library

The @tscircuit/alphabet library provides letter shape data as line segments. Each letter is represented as an array of {x1, y1, x2, y2} objects, where coordinates are normalized to the [0, 1] range.

import { lineAlphabet } from "@tscircuit/alphabet"

const aLines = lineAlphabet["A"]
// Array of {x1, y1, x2, y2} - each object is a line segment (stroke) of the letter

The letter "A" typically has three strokes: the left diagonal, the right diagonal, and the horizontal crossbar. Each stroke is a line segment with start and end coordinates. Because the coordinates are normalized, we can scale them to any size on the PCB.

Placing LEDs Along Letter Strokes

To display a letter with LEDs, we need to convert the normalized line segment coordinates into PCB positions. For each line segment, we calculate how many LEDs to place based on the segment length, then distribute them evenly along the stroke.

The coordinate conversion flips the Y-axis because lineAlphabet uses SVG conventions (Y increases downward) while PCB coordinates use standard electronics conventions (Y increases upward).

import { lineAlphabet } from "@tscircuit/alphabet"

export default () => {
const lines = lineAlphabet["A"]
const scale = 10
const pcbX = 0
const pcbY = 0

const positions = []
for (const line of lines) {
const dx = line.x2 - line.x1
const dy = line.y2 - line.y1
const segLen = Math.sqrt(dx * dx + dy * dy)
const n = Math.max(1, Math.round(segLen * scale / 3))
for (let i = 0; i <= n; i++) {
const t = n === 0 ? 0.5 : i / n
positions.push({
x: pcbX + (line.x1 + t * dx - 0.5) * scale,
y: pcbY + (0.5 - (line.y1 + t * dy)) * scale,
})
}
}

return (
<board width="20mm" height="20mm" routingDisabled>
{positions.map((pos, i) => (
<led
key={i}
name={"LED" + i}
color="red"
footprint="0603"
pcbX={pos.x}
pcbY={pos.y}
/>
))}
</board>
)
}
PCB Circuit Preview

Here we place LEDs along each stroke of the letter "A". Longer strokes get more LEDs — the number is calculated as Math.max(1, Math.round(segmentLength * scale / 3)), where 3 is the approximate spacing between LEDs in millimeters.

The position formula centers the letter around (pcbX, pcbY):

  • X position: pcbX + (x - 0.5) * scale — offsets by -0.5 to center horizontally
  • Y position: pcbY + (0.5 - y) * scale — flips the Y-axis and centers vertically

Adding Current-Limiting Resistors

Each LED needs a current-limiting resistor in series. For a 5V supply and a typical red LED with ~2V forward voltage, a 330Ω resistor limits the current to approximately 9mA — a safe operating current for most SMD LEDs.

The wiring for each LED is: power net → resistor → LED anode → LED cathode → ground net.

import { lineAlphabet } from "@tscircuit/alphabet"

export default () => {
const lines = lineAlphabet["A"]
const scale = 10
const pcbX = 0
const pcbY = 0

const positions = []
for (const line of lines) {
const dx = line.x2 - line.x1
const dy = line.y2 - line.y1
const segLen = Math.sqrt(dx * dx + dy * dy)
const n = Math.max(1, Math.round(segLen * scale / 3))
for (let i = 0; i <= n; i++) {
const t = n === 0 ? 0.5 : i / n
positions.push({
x: pcbX + (line.x1 + t * dx - 0.5) * scale,
y: pcbY + (0.5 - (line.y1 + t * dy)) * scale,
})
}
}

return (
<board width="20mm" height="20mm" routingDisabled>
{positions.map((pos, i) => {
const ledName = "LED" + i
const resName = "R" + i
return (
<group key={i}>
<led
name={ledName}
color="red"
footprint="0603"
pcbX={pos.x}
pcbY={pos.y}
/>
<resistor
name={resName}
resistance="330"
footprint="0402"
pcbX={pos.x + 0.8}
pcbY={pos.y}
/>
<trace from={"." + resName + " .pin1"} to="net.VCC" />
<trace from={"." + resName + " .pin2"} to={"." + ledName + " .pos"} />
<trace from={"." + ledName + " .neg"} to="net.GND" />
</group>
)
})}
</board>
)
}
PCB Circuit Preview

Each resistor is placed 0.8mm to the right of its corresponding LED on the PCB. The 0402 footprint is used for resistors to keep the layout compact, while 0603 LEDs are easier to see and solder.

The Complete LedLetter Component

Now let's wrap everything into a reusable LedLetter component. It accepts a letter prop, power and ground nets, position offsets for both PCB and schematic views, a scale factor, LED color, and a name prefix to ensure unique component names when multiple letters are placed on the same board.

import { lineAlphabet } from "@tscircuit/alphabet"

export const LedLetter = ({
letter,
power,
gnd,
pcbX = 0,
pcbY = 0,
schX = 0,
schY = 0,
scale = 10,
color = "red",
namePrefix = "",
}) => {
const lines = lineAlphabet[letter.toUpperCase()]
if (!lines) return null

const positions = []
for (const line of lines) {
const dx = line.x2 - line.x1
const dy = line.y2 - line.y1
const segLen = Math.sqrt(dx * dx + dy * dy)
const n = Math.max(1, Math.round(segLen * scale / 3))
for (let i = 0; i <= n; i++) {
const t = n === 0 ? 0.5 : i / n
positions.push({
px: pcbX + (line.x1 + t * dx - 0.5) * scale,
py: pcbY + (0.5 - (line.y1 + t * dy)) * scale,
})
}
}

return (
<group>
{positions.map((pos, i) => {
const ledName = namePrefix + "LED" + i
const resName = namePrefix + "R" + i
const sx = schX + (i % 5) * 2
const sy = schY + Math.floor(i / 5) * 2
return (
<group key={i}>
<led
name={ledName}
color={color}
footprint="0603"
pcbX={pos.px}
pcbY={pos.py}
schX={sx}
schY={sy}
/>
<resistor
name={resName}
resistance="330"
footprint="0402"
pcbX={pos.px + 0.8}
pcbY={pos.py}
schX={sx + 0.8}
schY={sy}
/>
<trace from={"." + resName + " .pin1"} to={power} />
<trace from={"." + resName + " .pin2"} to={"." + ledName + " .pos"} />
<trace from={"." + ledName + " .neg"} to={gnd} />
</group>
)
})}
</group>
)
}

export default () => (
<board width="20mm" height="20mm" routingDisabled>
<LedLetter
letter="A"
power="net.VCC"
gnd="net.GND"
pcbX={0}
pcbY={0}
schX={-4}
schY={-4}
namePrefix="A_"
/>
</board>
)
PCB Circuit Preview

The component calculates LED positions along each stroke of the letter, places a resistor next to each LED, and wires them in series. The namePrefix prop ensures that component names don't collide when multiple LedLetter instances are used on the same board.

Component Props

PropTypeDefaultDescription
letterstringrequiredThe capital letter to display (A-Z)
powerstringrequiredPower net identifier (e.g. "net.VCC")
gndstringrequiredGround net identifier (e.g. "net.GND")
pcbXnumber0X offset on PCB (mm)
pcbYnumber0Y offset on PCB (mm)
schXnumber0X offset in schematic
schYnumber0Y offset in schematic
scalenumber10Letter size in mm
colorstring"red"LED color
namePrefixstring""Prefix for component names

Displaying Multiple Letters

To display multiple letters, place several LedLetter components on the same board with different positions and name prefixes. Each letter should have a unique namePrefix to avoid component name collisions.

import { lineAlphabet } from "@tscircuit/alphabet"

export const LedLetter = ({
letter,
power,
gnd,
pcbX = 0,
pcbY = 0,
schX = 0,
schY = 0,
scale = 10,
color = "red",
namePrefix = "",
}) => {
const lines = lineAlphabet[letter.toUpperCase()]
if (!lines) return null

const positions = []
for (const line of lines) {
const dx = line.x2 - line.x1
const dy = line.y2 - line.y1
const segLen = Math.sqrt(dx * dx + dy * dy)
const n = Math.max(1, Math.round(segLen * scale / 3))
for (let i = 0; i <= n; i++) {
const t = n === 0 ? 0.5 : i / n
positions.push({
px: pcbX + (line.x1 + t * dx - 0.5) * scale,
py: pcbY + (0.5 - (line.y1 + t * dy)) * scale,
})
}
}

return (
<group>
{positions.map((pos, i) => {
const ledName = namePrefix + "LED" + i
const resName = namePrefix + "R" + i
const sx = schX + (i % 5) * 2
const sy = schY + Math.floor(i / 5) * 2
return (
<group key={i}>
<led
name={ledName}
color={color}
footprint="0603"
pcbX={pos.px}
pcbY={pos.py}
schX={sx}
schY={sy}
/>
<resistor
name={resName}
resistance="330"
footprint="0402"
pcbX={pos.px + 0.8}
pcbY={pos.py}
schX={sx + 0.8}
schY={sy}
/>
<trace from={"." + resName + " .pin1"} to={power} />
<trace from={"." + resName + " .pin2"} to={"." + ledName + " .pos"} />
<trace from={"." + ledName + " .neg"} to={gnd} />
</group>
)
})}
</group>
)
}

export default () => (
<board width="60mm" height="30mm" routingDisabled>
<LedLetter
letter="T"
power="net.VCC"
gnd="net.GND"
pcbX={-20}
pcbY={0}
schX={-12}
schY={-4}
namePrefix="T_"
/>
<LedLetter
letter="S"
power="net.VCC"
gnd="net.GND"
pcbX={-8}
pcbY={0}
schX={-4}
schY={-4}
namePrefix="S_"
/>
<LedLetter
letter="C"
power="net.VCC"
gnd="net.GND"
pcbX={4}
pcbY={0}
schX={4}
schY={-4}
namePrefix="C_"
/>
</board>
)
PCB Circuit Preview

Customizing Your Design

Changing the LED Color

Pass a different color prop to change the LED color:

<LedLetter letter="A" color="blue" power="net.VCC" gnd="net.GND" />

Available colors include red, green, blue, yellow, and white.

Adjusting Letter Size

The scale prop controls the letter size in millimeters. Increase it for larger displays:

<LedLetter letter="A" scale={15} power="net.VCC" gnd="net.GND" />

Changing the Footprint

You can switch to 0402 LEDs for a denser layout by modifying the footprint prop in the component. Similarly, you can change the resistor footprint.

Adjusting Resistance

For different supply voltages, adjust the resistance value. For a 3.3V supply with a red LED (~2V forward voltage), a 150Ω resistor gives approximately 8.7mA:

<resistor resistance="150" footprint="0402" />