skip to content

How do I draw a line? (video player)

Transcript ↓

Excuse me while I draw a line onto your computer screen hnnnnnnnn. In the immortal words of Jonathan Cash, “okay so fine, I’ve drawn a line”.

A line is a kind of emaciated rectangle joining two points in space—in this case point (a) and point (c). “Where’s point (b)?” you might ask. Point (b) is somewhere over here, off screen, because point (b) forgot to bring their signed release form and cannot, therefore, take part in this video.

The question is, how did I draw this line onto your screen? How did I make all the right pixels light up, to form that line-like shape? And the answer is: none of your f**king business. See you later, yeah?

...

If you’re still there, I suppose I can tell you how I might have drawn my line.

(1) The horizontal rule:

HTML doesn’t have an “L” element because, if it did, people wouldn’t be able to tell if it was “l” for “line” or capital “I” for, er, “I Am A Line”. Instead, there’s the verbosely and officiously named “horizontal rule”.La di f**king da.

The horizontal rule is a semantic element representing a thematic break in your content. For example, you might use a horizontal rule to separate the long, rambling travelogue part of your recipe page from the actual f**king recipe. It’s arguably more helpful to use an <h2> heading with the actual text “The actual fucking recipe” but I digress.

By default, browsers give horizontal rules their lineishness [sic] using the border property. You’ll notice there’s what appears to be a shadow cast by the line which really makes it pop. But what the LAMEstream media don’t tell you is that this shadow isn’t really a shadow but the effect of using a 1 pixel border and a border-style of inset on all sides of an element with no height.

If you want to remove this thing that isn’t really a shadow there is a property for the horizontal rule curiously named noshade. The noshade property is what’s called deprecated, which means you shouldn’t use it anymore, even though you can. Like don’t... but seriously, go ahead. It’s fine. But for how long? Nah, you’re good. Or are you?

In any case, you can just restyle the border with CSS. Make the border more permissive with border-style: dashed or use pseudo-content to label the points. Shit like that.

hr::before {
  content: 'A';
}

hr::after {
  content: 'C';
}

All of this styling we can do to a horizontal rule we can do just as easily to a <div>. But there are reasons why <div> is short for <divisive>. Firstly, screen readers do not acknowledge empty <div>s, meaning screen reader users would be ushered from “bla bla sun kissed rural Umbria” into “2 tbsps of corn starch” without any kind of warning.

Secondly, <div>s are not recognized by shorthand syntax like markdown. Why should a content editor have to write <div class="horizontal-rule"></div> when a horizontal rule in markdown is just three asterisks.

(2) Scalable Vector Graphics

Scalable Vector Graphics, or sverg, are named for their incorporation of graphics that are scalable on account of them being vectors. Like HTML they are a kind of markup, and because they can be embedded into HTML, they are a kind of markup that is kind of a part of a kind of markup.

SVG offers a number of ways to draw lines. If you are interested in being obtuse, you could use a <rect> element with a height of 1. Naturally, the name rect is short for “rectangle”, from the Latin etymology rectum.

A more on-the-nose approach would be to use the <line> element with its x1, y1, x2, and y2 attributes. These lines are only ever straight, or at least that’s what they tell themselves. To make your line change direction and create corners, you want <polyline>, which takes a set of points.

By far the most powerful and versatile line thingy in SVG is the <path>. The <path>’s d attribute, which stands for “dem points”, can include special commands like M. The M command lets you start a new line, using the same path, beginning at a different position.

MONK!

Monk is a word that starts with “M”, which is pertinent here because a group of monks from the 12th Century, presumably in the early hours before they got p**sed up on monastic beer, invented a numbering system wherein every number between 0 and 9999 can be represented by a single character made of lines.

That rhymes.

Using paths, I can quickly and efficiently recreate the first 9 monk numbers in SVG. For multiples of 10, I simply take the multiples of 1, reuse the paths with a <use> element, but flip them horizontally with a transform using scale and translate.

<symbol id="10">
  <use xlink:href="#1" transform="scale(-1, 1) translate(-60, 0)" />
</symbol>

For multiples of 100, I flip vertically, and for multiples of 1000 I flip both horizontally and vertically. Now, to recreate any number between 1 and 9999 I just need to reference each component by id inside another SVG. In this case, the number is 6969.

<svg viewBox="0 0 60 90">
  <use xlink:href="#6000" />
  <use xlink:href="#900" />
  <use xlink:href="#60" />
  <use xlink:href="#9" />
</svg>

Nice. Nice.

Should you need to calculate the length of your path, you have a couple of options:

  1. Try to recall the trigonometric functions you were taught at school (which would be futile because you weren’t actually listening were you you wicked child) or…
  2. Use the built in getTotalLength method of the SVGGeometryElement interface, which does all of this for you, thereby coddling you and destroying your capacity for independent thought.
const path = document.querySelector('path');
const length = path.getTotalLength();

The value returned is not in pixels but “user units”: the units you used to draw the path relative to the viewBox. One thing this is useful for is knowing how large one of two adjacent circles can be before it starts touching another circle—step 1 (or possibly step zero) in a circle packing routine you might use to create a basic generative artwork.

Using the special commands C, S, Q, T, and A, you can curve your paths too. Exacting curves is difficult, but there are some hacks. For example, if I take this zig-zag path and stick an S before my second point, I go from zig-zagger to squiggler in a f**king second.

<svg viewBox="0 0 400 100">
  <path d="M5,50 S15,40 28,65 62,25 75,68 120,35 160,62 200,25 230,65 280,30 330,73 360,42 395,50" />
</svg>

Each curve the S command produces is a mirror of the previous curve, hence the total annihilation of pointy bits. It should be no surprise the S stands for smooth—as in operators, babies’ bottoms, and jazz.

Combine curves with fills and you can draw pretty much anything with a single <path> element. For example, this picture of Bobby Hill from King Of The Hill as the goat deity Baphomet with the inscription “That’s My Purse, I Don’t Know You” in Gothic Script.

Here is another single path, this time illustrating a skull. When you include a skull in your artwork it is a “memento mori”—a convenient reminder that you will, at some point, die. If you are not an art lover, you can, instead, set up daily notifications with a calendar app.

THE POINT IS THOUGH, RIGHT, that by converting the d string into an array, mapping it and randomizing the integers, I could use this image to create randomized glitchy artifacts, which is also fun to do.

let newPath = pathString.split('').map(bit => {
  let bitInt = parseInt(bit);
  if (Number.isInteger(bitInt)) {
    // Randomize integer between 0 and 9
  }
}).join('');

Then I just pick a random item from that array and make sure the coordinate is no bigger than 9 and no smaller than 0, or the path syntax will break and I’ll see nothing at all. The higher the value where 20 is now, the less likely that specific coordinate will change.

(3) UTF-8 Encoded SVG background images:

If you’re a pervert for HTTP requests, you can of course grab a ropy JPEG image of a line and set it as a background-image:

.element {
  background-image: url(/path/to/line.jpg);
}

The more cultured among you might prefer to use a UTF-8 encoded SVG data URL. All we need is a 1 by 1 SVG with a filled <rect> element.

.line {
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><rect width="1" height="1" /></svg>');
}

Drawing a line just requires repeating the SVG in one direction and setting the width of the line using background-size:

.line {
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><rect width="1" height="1" /></svg>');
  background-repeat: repeat-x;
  background-size: auto 0.125em;
}

We can even make it a dotted line by f**king with the viewBox. Now the “image” is twice as wide as the <rect> inside it, leaving gaps.

viewBox=" 0 0 2 1"

Repeat this SVG in both directions and—b**ger me with a pez dispenser—we have stripes.

background-repeat: repeat;

If you were to include your utf8 encoded SVG data URL in the context of a CSS-in-JS tool like Styled Components AND if you were to include an inline style on that SVG, technically you would be writing CSS-in-SVG-in-CSS-in-JS, which is 3 orders of galaxy brain higher than mere CSS-in-JS.

const Line = styled.div`
  background-image: url('data:image/svg+xml;utf8,<svg style="opacity: 0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><rect width="1" height="1" /></svg>');
`;

(4) (Repeating) Linear Gradients:

As versatile as this simple SVG is, one thing you can’t easily do is rotate the background to make diagonal stripes. But you can achieve this effect with gradients.

.line {
  repeating-linear-gradient(45deg, transparent, transparent 3px, black 3px, black 6px);
}

Unlike with data URLs, which are strings, we can now interpolate custom properties, giving us easy control over the stripe dimensions and colors.

.line {
  --lineWidth: 3px;
  --lineGap: 12px;
  --lineEnd: calc(var(--lineWidth) + var(--lineGap));
  --lineAngle: -45deg;
  background:
    repeating-linear-gradient(var(--lineAngle), transparent, transparent var(--lineGap), #000 var(--lineGap), #000 var(--lineEnd));
}

Note that --lineEnd is a kind of reactive property. When you change either --lineGap or --lineWidth its value is automatically updated. JavaScript doesn’t do this reactive thing out-of-the-box—you need a library like React instead. That’s because, unlike CSS, it is not really a grown-up programming language #shade #tea #hashtag #number.

(5) Houdini Paint Worklets:

Harry Houdini was a famous magician and illusionist who tragically died when he cast a spell on his internal organs and made them disappear. Jerry Houdini (no relation) was a famous technologist who created a number of methods for writing CSS using low-level browser APIs… who tragically died when he simultaneously choked on a honeydew melon and spontaneously combusted.

Houdini worklets are little JavaScript bambinos that let you write styles directly in JavaScript. A paint worklet turns any HTML element into your canvas, letting you draw on it like you would a <canvas> element.

/* Register in JS */
CSS.paintWorklet.addModule(line.js);

/* Use in CSS */
background: paint(line);

Drawing a line across the middle of an element using a paint worklet is as simple as defining the line’s width and stroke color, finding the element’s middle, then procedurally joining two points. We move to the middle of the left-hand side, draw a line to the right-hand side (equal to size.width) then apply the stroke.

paint(ctx, size, props) {
  ctx.lineWidth = 1;
  ctx.strokeStyle = 'black';
  const middle = size.height / 2;
  ctx.beginPath();
  ctx.moveTo(0, middle);
  ctx.lineTo(size.width, middle);
  ctx.stroke();
}

If you use a Houdini paint worklet to draw a simple line like this, not only will melon adverse Jerry Houdini start turning rapidly in his grave but I will write you a very strongly worded letter that will read something like this:


“Dear Mr Poopy Pants McWrong-Brain, It has come to my attention that you have enlisted a paint worklet, and with it a JavaScript dependency, in order to elicit a simple line artifact in your visual design, when you could have deferred to any number of much more efficient and well-supported CSS techniques. You are a complete and utter parsnip.

Best wishes for the future,

Heydon

P.S. Bye-bye, bye, bye-bye-bye mwah mwah


Before you use Houdini, you have to ask yourself, “What can Houdini do that CSS can’t do otherwise?” One thing is having access to JavaScript’s Math object, which means we can use Math.random to generate randomized line paths.

What’s more, by registering the custom properties --amplitude and --frequency, I can control the parameters within which that randomization takes place.

@property --amplitude {
  syntax: '<number>';
  initial-value: 0.5;
  inherits: true;
}

@property --frequency {
  syntax: '<length>';
  initial-value: 16px;
  inherits: true;
}

Once these properties are incorporated into the worklet using the inputProperties method, you can start tweaking the values live, in your browser’s dev tools.

static get inputProperties() {
  return ['--amplitude','--frequency',];
}

paint(ctx, size, props) {
  const amplitude = props.get('--amplitude').value;
  const frequency = props.get('--frequency').value;
  // Do something with these values
}

In practice, I can use these scrawlings to stylize my lo-fi mockup’s lorem ipsum text. Which not only looks kind of neat but also saves having to explain to my client why the word “cum” keeps appearing in their copy.

(6) WebGL:

Arguably, the pinnacle of over-engineering would be to create this simple line using WebGL which, even just to draw a line, still requires things like vertexes and matrixes, shader algorithms, and the necessity to commit shameful, depraved acts like dynamically generating Float32Arrays and stuffing them into buffers.

But whatever, this line exists in simulated three dimensional space now. You just can’t tell because you’re looking at it directly side on.

In summary:

  1. Lines are rectangles that are really thin, unless they are curved lines, in which case they are not rectangles at all but “squigglies”, “wibblers”, or “doodly dos”.
  2. You can make browsers draw lines in all sorts of different ways. Always choose the simplest and most efficient way to draw the line for the given purpose or “use case”.
  3. The Baphomet does not, in isolation, represent evil. It is a composition of supposed opposites: good and evil, man and woman, light and dark. Eliphas Lévi, the creator of this depiction of Baphomet, referred to it as the “Sabbatic Goat”. He did not invent Levi jeans.
  4. Some lines are decorative and others are meaningful. If your line means something make sure that meaning is accessible to everyone, including those who hear rather than see what the line represents.
  5. The leading cause of death among males in the United Kingdom is chocking on an unchewed melon. The second most common cause of death is spontaneous combustion and the leading cause of spontaneous combustion is informing someone CSS is a programming language.