Skip to main content
screenshot of different types of grid

The Math of Calligraphy Grids

A Deep Dive of When I Fell Down the Rabbit Hole of Coding an SVG Guideline Generator

It is no secret, that I am very much an advocate for always using Calligraphy Grids. I’m known for being the kind of teacher that will always answer the question of when to graduate from guidelines with “never”.

Yes, there comes a time, where you will have so much muscle memory, that you’ll have a slant built into your script that is pretty consistent, but why rely on a pretty consistent, if we aim for very consistent? Guidelines are our friends and I will always write on a grid.

I believe the main reason why people don’t want to use grids is because they can be time-consuming and annoying to draw. But I have (at least partially) a solution for that.

Table of Contents

Table of Contents

You Still Need a Great Ruler

You Still Need a Great Ruler

There will always be cases, where you will need to pull out pencils and rulers and draw lines by hand. And, don’t get me wrong, I love my giant 30cm set square, but it’s not something I want to take out every time I want to practice.

set square and pencil lying on a sheet of paper

I believe every calligrapher needs to know how to draw grids by hand and I also think every calligrapher needs a great tool to draw them.

But I don’t think every calligrapher needs to get as deep into the grids as I did last winter in the days leading up to Christmas.

On Digital Grids

On Digital Grids

I think we can all agree, that in our day-to-day, we want to be able to print out a grid and use that to write on.

And I assume most people don’t ever really think about drawing digital grids themselves, and just download some.

And then we—as in, the people that offer those downloads—painstakingly go ahead and manually draw them digitally in a program like Illustrator or Affinity.

I have previously shared my very effective method, of drawing grids with just four paths in Adobe Illustrator here.

a screenshot from the tutorial showing grids

However, since Adobes prices have absolutely skyrocketed, I have recently switched to using Affinty and thus can no longer use that method.

Because even though I am generally very happy with Affinity, the Distort & Transform > Transform effect is not available in Affinity.

There are ways to duplicate elements that produce the same kind of result, but if you want to adjust the grid, you can’t just adjust it in 4 places like with the Illustrator method. You basically have to start from scratch.

I learned that last December, when I wanted to quickly throw a grid together in a different x-Height than I usually use for a notebook and realized it’s a pain in the ass.

There’s this rule in programming, that when you find yourself repeating the same kind of code, you should turn it into a function.

So, that’s what I decided to do.

And that’s how I fell into the rabbit hole of drawing grids with code.

Types of Grids

Types of Grids

With my code, I wanted to cover the basics of all major calligraphy types, so this is what it can do.

Dot Grid

Dot Grid
© grid code.halfapx.com/guideline-generator/

The least restrictive and least helpful for calligraphy is the Dot Grid. It is great for Hand Lettering, but not so much for Calligraphy, since it does not even offer proper straight lines. It is great if you want to draw boxes on it, though.

Graph Grid

Graph Grid
© grid code.halfapx.com/guideline-generator/

The Graph Grid is probably the most common grid you’ll find pre-printed on paper. It will work fine if you are dealing with upright letters and use consistent ratios. But it’s not very helpful for slanted scripts.

Calligraphy Area

Calligraphy Area
© grid code.halfapx.com/guideline-generator/

This Area grid, is something I’d often create for myself, when I wanted to have flexibility of how big I wanted to write. I really like this type of grid, because It just features slant lines and lines with a pre-defined x-height and then you can space your letters how you please.

Calligraphy Lines

Calligraphy Lines
© grid code.halfapx.com/guideline-generator/

This is what I consider a proper calligraphy practice grid. It has a predefined x-height and ratio. It features slant lines to help with the angle of the letters and a small indicator where the baseline is and the x-height. This is the type of grid I’d offer in my workshops and teach on.

How Do You Even Draw With Code?

How Do You Even Draw With Code?

A question you might be wondering if you aren’t a coder is how would you even draw with just code. Aren’t images always made with graphical interfaces? No, actually, there is an image format, that follows quite the semantics.

SVG

SVG

SVG stands for Scalable Vector Graphics and it is a format that is used to define vector-based graphics for the Web, it is a format that Illustrator and Affinity and all other vector programs can export to, but it is also a format that features quite semantic tags that can be read and understood by humans. (At least some of them, the path tag is a bit of a mess to understand.)

JavaScript

JavaScript

Well, I guess if you were a masochist you could actually do all of the math manually, but since i wanted to end up with something generative, JavaScript became necessary.

And as someone, who always says that I am only decent at math as soon as letters are part of it, SVG Code is so much easier to understand when you are doing the math with variables instead of numbers.

Setting Parameters

Setting Parameters

It is fascinating, how much more complicated a task becomes, when it has to be solved on an abstract level, instead of just having to work with given parameters.

Drawing a grid with a ruler and a pencil, takes a few seconds. Doing it in illustrator mere minutes.

Doing this in a generic manner that allows you to adjust literally everything is a different kind of task, that took me a few days.

Believe me, I was not prepared to dig out old trigonometry memories and googling math formulas… but it is very fascinating, to dig into what actually happens mathematically, when your goal is a space-efficient, even grid.

Speaking of goals, before diving into a coding project, it’s best to define what the code is supposed to do.

So, here’s what I wanted to achieve with this code.

  1. The code should put out all types of grids I previously mentioned, with the ability to adjust all kinds of parameters.
  2. The resulting SVG should be able to be opened in Affinity and Affinity should render it like a browser (which affected the SVG features I could use).
  3. The end result should be something that I could offer my lettering students so that they could practice with different parameters.

Figuring Out Basic SVG Codes

Figuring Out Basic SVG Codes

So, this was my first time coding SVG. As a professional frontend engineer I have worked with a lot of SVG. It usually consists of cleaning them up, optimizing colors, making them responsive-capable and bundling them up to be used as Icons on a website.

But this was different. I was writing SVG.

There were a lot of questions in my head, but I had seen people do it before, so I was just going to start.

calligraphy Grids consist of lines and boxes and luckily, those are the easiest part of SVG.

SVGs are just a setup of a coordinate system. So in the container you define the size of your canvas and establish abstract units, which you then use to position your elements.

So, in my early tests, I started by setting up SVGs that had square canvases and then did a bit of math to figure out my coordinates.

Boxes

Boxes

So, I started by giving myself a few numbers and threw them into variables with speaking names and then added a rectangle with a stroke.

1
<svg
2
width={canvasWidth}
3
height={canvasHeight}
4
>
5
<rect
6
width={rectWidth}
7
height={rectHeight}
8
fill="none"
9
stroke="currentColor"
10
/>
11
</svg>

The rect attribute however, also needs an x and y attribute, otherwise, it sticks to the top left of the image.

So, if we want to center it, we do:

1
const rectX = (canvasWidth - rectWidth) / 2;
2
const rectY = (canvasHeight - rectHeight) / 2;

I also added a border-radius to the rectangle, by adding an rx attribute to the rect tag.

1
<svg
2
width={canvasWidth}
3
height={canvasHeight}
4
>
5
<rect
6
x={rectX}
7
y={rectY}
8
width={rectWidth}
9
height={rectHeight}
10
rx={borderRadius}
11
fill="none"
12
stroke="currentColor"
13
/>
14
</svg>

Which then gives us something like this:

Centered Rectangle with Border Radius
Show SVG Code
rendered-svg.html
1
<svg
2
role="img"
3
width="30rem"
4
style="max-width: 100%; border: 1px solid red"
5
viewBox="0 0 100 100"
6
xmlns="http://www.w3.org/2000/svg"
7
>
8
<title>Centered Rectangle with Border Radius</title>
9
<rect
10
x="20"
11
y="20"
12
width="60"
13
height="60"
14
fill="none"
15
stroke="currentColor"
16
rx="5"
17
></rect>
18
</svg>

Lines

Lines

Horizontal and Vertical Lines were also pretty straightforward.

<line> elements are made up of two points with x and y coordinates each.

To make horizontal lines, both y coordinates are the same and to make vertical lines, both x coordinates are the same.

So, if I want to draw a horizontal and a vertical line through the canvas, I need to know my middle point first.

So, then I can figure out my line coordinates. I like to think of it like this:

If I were to drop a line, horizontally, I’d first figure out my point on the y-axis, this is my “grid position” and then I’d figure out where my line starts on the left and where it ends on the right.

For the vertical line, I’d do the same, but the x-axis is my grid position and then I think top to bottom.

I find that easiest to visualize.

So if I wanted to draw a horizontal and a vertical line through the center, I’d do it like this:

1
const middleX = canvasWidth / 2;
2
const middleY = canvasHeight / 2;
3
const lineHorizontal = {
4
gridPos: middleY,
5
lineStart: 0,
6
lineEnd: canvasWidth,
7
};
8
const lineVertical = {
9
gridPos: middleX,
10
lineStart: 0,
11
lineEnd: canvasHeight,
12
};

and then I can put them into the SVG.

1
<svg
2
width={canvasWidth}
3
height={canvasHeight}
4
>
5
<line
6
x1={lineHorizontal.lineStart}
7
y1={lineHorizontal.gridPos}
8
x2={lineHorizontal.lineEnd}
9
y2={lineHorizontal.gridPos}
10
stroke="currentColor"
11
/>
12
<line
13
x1={lineVertical.gridPos}
14
y1={lineVertical.lineStart}
15
x2={lineVertical.gridPos}
16
y2={lineVertical.lineEnd}
17
stroke="currentColor"
18
/>
19
</svg>
Horizontal and Vertical Line through the middle of the document
Show SVG Code
rendered-svg.html
1
<svg
2
role="img"
3
width="30rem"
4
style="max-width: 100%; border: 1px solid red"
5
viewBox="0 0 421 391"
6
xmlns="http://www.w3.org/2000/svg"
7
>
8
<title>Horizontal and Vertical Line through the middle of the document</title>
9
<line
10
x1="0"
11
y1="195.5"
12
x2="421"
13
y2="195.5"
14
stroke="currentColor"
15
stroke-width="5"
16
></line>
17
<line
18
x1="210.5"
19
y1="0"
20
x2="210.5"
21
y2="391"
22
stroke="currentColor"
23
stroke-width="5"
24
></line>
25
</svg>

Circles and Dashed Lines

Circles and Dashed Lines

When it came to drawing dashed lines, my original idea was to use circles, but then I realized that that will create a lot of elements, so I decided to use dashed lines instead, which use

1
<line
2
x1={line.x1}
3
y1={line.y1}
4
x2={line.x2}
5
y2={line.y2}
6
stroke="currentColor"
7
stroke-width={dotSize}
8
stroke-dasharray={`0,${dotGap.toString()}`}
9
stroke-linecap="round"
10
/>

I did still end up going with circles for the dot Grid in the end.

Circles work a bit differently than rectangles, because they are drawn from the center and instead of a width and height they have a radius.

1
<circle
2
cx={circle.x}
3
cy={circle.y}
4
r={dotSize / 2}
5
fill="currentColor"
6
/>

Slant Lines

Slant Lines

There are different methods of making slanted lines in SVG, but the one I found most intuitive was to use the transform attribute.

In Calligraphy, our slant lines are always relative to a horizontal line, so my initial thought was to just start with a horizontal line and then rotate it. In code, we rotate clockwise, so a positive number rotates the line to the right and since we want to rotate it to the left, we use a negative number. We’ll also need to specify a point to rotate around, so we’ll use the middle of our line.

Obviously, my angle had to be 55, because Calligraphy.

1
const lineStart = 0;
2
const lineEnd = canvasWidth;
3
const lineY = canvasHeight / 2;
4
const angle = 55;
5
const lineCenterPointX = (lineEnd - lineStart) / 2;
6
const lineCenterPointY = lineY;
7
8
<svg viewBox={"0 0 ${canvasWidth} ${canvasHeight}"}>
9
<line
10
x1={lineStart}
11
y1={lineY}
12
x2={lineEnd}
13
y2={lineY}
14
stroke="currentColor"
15
/>
16
<line
17
x1={lineStart}
18
y1={lineY}
19
x2={lineEnd}
20
y2={lineY}
21
stroke="currentColor"
22
transform={`rotate(${angle * -1}, ${lineCenterPointX}, ${lineCenterPointY})`}
23
/>
24
</svg>;
slanted line and horizontal line crossing
Show SVG Code
rendered-svg.html
1
<svg
2
role="img"
3
width="30rem"
4
style="max-width: 100%; border: 1px solid red"
5
viewBox="0 0 220 230"
6
xmlns="http://www.w3.org/2000/svg"
7
>
8
<title>slanted line and horizontal line crossing</title>
9
<line
10
x1="0"
11
y1="115"
12
x2="220"
13
y2="115"
14
stroke="currentColor"
15
></line>
16
<line
17
x1="0"
18
y1="115"
19
x2="220"
20
y2="115"
21
stroke="currentColor"
22
transform="rotate(-55, 110, 115)"
23
></line>
24
</svg>

The way we draw slant lines in our classic calligraphy line though, ended up not being rotated with transform, but using some trigonometry.

But this was a problem for later.

I had gathered all of my information and was ready to get into my coding project.

Technical Setup

Technical Setup

I decided to write a collection of Typescript Classes that would help me generate the SVGs. First, I’d write a class that would set up the entire document and some shared functionality and then I’d write a subclass for each type of grid.

I went with Typescript because I wanted to be able to use Interfaces for the configuration Objects and to just make it a bit more structured.

I wanted my grids to be placed on a traditional paper size and have a document title that mentioned the grid type and some of the useful parameters like the x-height, slant angle, ratio etc, depending on each type.

And then I’d also like a box around the grid with some rounded corners for that classic look.

I started with my classic graph grid.

Which was done fairly quickly, it was just throwing a bunch of horizontal and vertical lines onto the canvas by taking my cell-Size and my drawing area into account.

Wait, You Drew Every Single Line?

Wait, You Drew Every Single Line?

Let’s do a quick interlude here from a future version of me, who had written this entire thing and was planning to refactor the website part of this.

But then I had a conversation with a coworker, who is a lot more experienced with SVG than I am and he told me about the pattern tag.

So, as soon as I heard about that, I set out, to make my code more efficient and cool and was planning to rewrite the entire thing.

I prototyped it with Dot and Graph Grids and it was soo cool.

pattern allows you to define a pattern that can be used to fill a shape. So, I could just define my lines once and then use them to fill my entire drawing area.

Graph Grid With Pattern

Graph Grid With Pattern

If you think about it, a graph is just a bunch of horizontal and vertical lines that repeat across the x and y axis.

So, pattern can help us with that. We can create a <rect> and then reference a pattern in the fill attribute of the rect.

The pattern then has it’s own width and height and viewBox which define how the pattern is repeated.

So, let’s say we want to perfectly center our graph grid in any given width or height, and we want to be able to say how big our little squares are.

1
const patternHeight = (cellSize / graphDocHeight) * 100;
2
const patternWidth = (cellSize / graphDocWidth) * 100;
3
const gridYStart = (graphDocHeight % cellSize) / 2;
4
const gridXStart = (graphDocWidth % cellSize) / 2;
5
6
<svg viewBox={"0 0 ${docWidth} ${docHeight}"}>
7
<defs>
8
<pattern
9
id="horizontal"
10
viewBox={`0 0 ${docWidth} ${strokeWidth}`}
11
width="100%"
12
height={`${patternHeight}%`}
13
>
14
<line
15
x1="0"
16
x2={docWidth}
17
y1={gridYStart}
18
y2={gridYStart}
19
stroke={strokeColor}
20
stroke-width={strokeWidth}
21
></line>
22
</pattern>
23
<pattern
24
id="vertical"
25
viewBox={`0 0 ${strokeWidth} ${docHeight}`}
26
width={`${patternWidth}%`}
27
height="100%"
28
>
29
<line
30
y1={0}
31
y2={docHeight}
32
x1={gridXStart}
33
x2={gridXStart}
34
stroke={strokeColor}
35
stroke-width={strokeWidth}
36
></line>
37
</pattern>
38
</defs>
39
<rect
40
width={docWidth}
41
height={docHeight}
42
fill="url(#horizontal)"
43
></rect>
44
<rect
45
width={docWidth}
46
height={docHeight}
47
fill="url(#vertical)"
48
></rect>
49
</svg>;

And then our grid basically happens like magic.

I was so excited about this.

Graph Grid with pattern
Show SVG Code
rendered-svg.html
1
<svg
2
role="img"
3
style="max-width: 100%; border: 1px solid red"
4
viewBox="0 0 226 101"
5
xmlns="http://www.w3.org/2000/svg"
6
>
7
<title>Graph Grid with pattern</title>
8
<defs>
9
<pattern
10
id="horizontal"
11
viewBox="0 0 226 0.3"
12
width="100%"
13
height="4.9504950495049505%"
14
>
15
<line
16
x1="0"
17
x2="226"
18
y1="0.5"
19
y2="0.5"
20
stroke="currentColor"
21
stroke-width="0.3"
22
></line>
23
</pattern>
24
<pattern
25
id="vertical"
26
viewBox="0 0 0.3 101"
27
width="2.2123893805309733%"
28
height="100%"
29
>
30
<line
31
y1="0"
32
y2="101"
33
x1="0.5"
34
x2="0.5"
35
stroke="currentColor"
36
stroke-width="0.3"
37
></line>
38
</pattern>
39
</defs>
40
<rect
41
width="226"
42
height="101"
43
fill="url(#horizontal)"
44
></rect>
45
<rect
46
width="226"
47
height="101"
48
fill="url(#vertical)"
49
></rect>
50
</svg>

Dot Grid With Pattern

Dot Grid With Pattern

Dot Grid is even cooler. We can just take one line with a dashed line that’s made to look like dots and repeat that.

1
const gridYStart = (docHeight % cellSize) / 2;
2
const gridXStart = (docWidth % cellSize) / 2;
3
const patternHeight = (cellSize / docHeight) * 100;
4
5
<svg viewBox={"0 0 ${docWidth} ${docHeight}"}>
6
<defs>
7
<pattern
8
id="horizontal-dotted-line"
9
viewBox={`0 0 ${docWidth} ${dotSize}`}
10
width="100%"
11
height={`${patternHeight}%`}
12
>
13
<line
14
x1={gridXStart}
15
y1={gridYStart}
16
x2={docWidth}
17
y2={gridYStart}
18
stroke={strokeColor}
19
stroke-width={dotSize}
20
stroke-dasharray={`0,${cellSize}`}
21
stroke-linecap="round"
22
></line>
23
</pattern>
24
</defs>
25
<rect
26
x="0"
27
y="0"
28
width={docWidth}
29
height={docHeight}
30
fill="url(#horizontal-dotted-line)"
31
></rect>
32
</svg>;
dot Grid with pattern
Show SVG Code
rendered-svg.html
1
<svg
2
role="img"
3
style="max-width: 100%; border: 1px solid red"
4
viewBox="0 0 219 100"
5
xmlns="http://www.w3.org/2000/svg"
6
>
7
<title>dot Grid with pattern</title>
8
<defs>
9
<pattern
10
id="horizontal-dotted-line"
11
viewBox="0 0 219 0.5"
12
width="100%"
13
height="4%"
14
>
15
<line
16
x1="1.5"
17
y1="0"
18
x2="219"
19
y2="0"
20
stroke="currentColor"
21
stroke-width="0.5"
22
stroke-dasharray="0,4"
23
stroke-linecap="round"
24
></line>
25
</pattern>
26
</defs>
27
<rect
28
x="0"
29
y="0"
30
width="219"
31
height="100"
32
fill="url(#horizontal-dotted-line)"
33
></rect>
34
</svg>

However, that was already at a point where I had learned that in Affinity <mask> does not work, while <clipPath> does. And I had to adjust my code accordingly, so everything coming out beyond the rounded corners of my area got cut off.

So, I was cautious to not get too excited about this, and make every grid before testing.

And this is where I stopped and saved my svg and then opened it in Affinity.

empty affinity dot grid

Yeah, one big nothing.

Well, I guess in the end, it is good I didn’t create everything with pattern because then I’d have to rewrite it all.

Because now I just knew to save myself the effort. Because the solution I had was working.

functional dot grid in affinity

Let’s Talk Math

Let’s Talk Math

Okay, so, the pattern did not work for me, so I had to do a lot of repetition and loops.

Let’s look at the math for each of the grids and how they are constructed.

Line Drawing Function and general Class Setup

Line Drawing Function and general Class Setup

I won’t be sharing the entire code here. It’s open-source and you can dive deep into it there.

Generally, in my classes there are two drawing functions makeSVG() and makeSVGString()

And then each subclass hooks into that and adds its specific grid.

There are then helper functions to create groups (<g> tags that make it easier to have similar elements in one place) and to add lines. There was also some masking involved for the rounded area box.

I also have a function that formats coordinates, because more than two floating points make the SVG file bigger and usually the result will be exact enough with two.

You’ll see addSolidLine and addDashedLine functions in the code, but they are just wrappers around addLine that make it quicker to read whether it’s a horizontal or vertical line and if it’s solid or dashed.

1
export class GridMaker {
2
// [...]
3
4
makeSVG(): SVGElement {
5
const svg = this.createDocument('dom') as SVGElement;
6
svg.innerHTML = this.addSVGContent();
7
return svg;
8
}
9
10
makeSVGString(addCloseTag: boolean = true): string {
11
let svgString = this.createDocument('string') as string;
12
svgString += this.addSVGContent();
13
if (addCloseTag) {
14
svgString += '</svg>';
15
}
16
return svgString;
17
}
18
19
drawSolidLine(orientation: 'horizontal' | 'vertical', gridPos: number, lineStart: number, lineEnd: number, color: string, stroke: number): string {
20
let x1,x2,y1,y2;
21
if (orientation === 'horizontal') {
22
x1 = lineStart;
23
x2 = lineEnd;
24
y1 = y2 = gridPos;
25
}
26
if (orientation === 'vertical') {
27
y1 = lineStart;
28
y2 = lineEnd;
29
x1 = x2 = gridPos;
30
}
31
return this.addLine(x1, y1, x2, y2, color, stroke);
32
}
33
34
drawDashedLine(orientation: 'horizontal' | 'vertical',gridPos: number,lineStart: number,lineEnd: number,dotRadius: number,dotColor: string): string {
35
const dotSize = dotRadius * 2;
36
const dotGap = dotRadius * 4;
37
let x1, x2, y1,y2;
38
if (orientation === 'horizontal') {
39
x1 = lineStart;
40
x2 = lineEnd;
41
y1 = y2 = gridPos;
42
}
43
if (orientation === 'vertical') {
44
y1 = lineStart;
45
y2 = lineEnd;
46
x1 = x2 = gridPos;
47
}
48
return this.addLine(x1,y1,x2,y2,dotColor,dotSize,`0,${dotGap.toString()}`,'round');
49
}
50
51
drawSlantLine(): string {
52
// we'll get into this later
53
}
54
55
addLine(x1: number, y1: number, x2: number, y2: number, color: string, stroke: number, srokeDashArray?: string, strokeLineCap?: 'round' | 'square' | 'butt'): string {
56
return /*html*/ `
57
<line
58
x1="${this.formatCoordinate(x1)}"
59
y1="${this.formatCoordinate(y1)}"
60
x2="${this.formatCoordinate(x2)}"
61
y2="${this.formatCoordinate(y2)}"
62
stroke="${color}"
63
stroke-width="${stroke}"
64
${srokeDashArray ? `stroke-dasharray="${srokeDashArray}"` : ''}
65
${strokeLineCap ? `stroke-linecap="${strokeLineCap}"` : ''}
66
/>
67
`;
68
}
69
}

Graph

Graph

So, in the graph grid, we have a bunch of horizontal and vertical lines that are all the same distance apart.

And then we can just start a loop for the number of repetitions we got and draw a line for each.

1
export class GraphGridPage extends GridMaker {
2
// [...]
3
private addGraphGrid(): string {
4
const cellSize = this.#config.cellSize!;
5
const xEnd = this.width - this.marginRight;
6
const yEnd = this.height - this.marginBottom;
7
const horizontalReps = this.gridHeight / cellSize;
8
const horizontalRemainder = this.gridHeight % cellSize;
9
const verticalReps = this.gridWidth / cellSize;
10
const verticalRemainder = this.gridWidth % cellSize;
11
12
let gridParent = this.createGroup('grid','calli-grid',this.maskId ? this.maskId : undefined,);
13
14
let yLineStart = this.marginTop + horizontalRemainder / 2;
15
for (let i = 0; i <= horizontalReps; i++) {
16
const line = this.drawSolidLine('horizontal',yLineStart,this.marginLeft,xEnd,this.#config.lineColor,this.#config.gridStrokeWidth);
17
yLineStart += cellSize;
18
gridParent += line;
19
}
20
21
let xLineStart = this.marginLeft + verticalRemainder / 2;
22
for (let i = 0; i <= verticalReps; i++) {
23
const line = this.drawSolidLine('vertical',xLineStart,this.marginTop,yEnd,this.#config.lineColor,this.#config.gridStrokeWidth);
24
xLineStart += cellSize;
25
gridParent += line;
26
}
27
28
gridParent += '</g>';
29
30
return gridParent;
31
}
32
}

Dot Grid

Dot Grid

The Dot grid is setup similarly, but here, we’re not drawing lines in the loops, but instead, we’re adding x and y coordinates into arrays, which then gives us the coordinates of where the horizontal and vertical lines intersect. This is where we then draw the dots using circle elements.

1
export class DotGridPage extends GridMaker {
2
// [...]
3
private addDotGrid(): string {
4
const cellSize = this.#config.cellSize!;
5
const horizontalReps = this.gridHeight / cellSize;
6
const horizontalRemainder = this.gridHeight % cellSize;
7
const verticalReps = this.gridWidth / cellSize;
8
const verticalRemainder = this.gridWidth % cellSize;
9
const horizontalIntersections: number[] = [];
10
const verticalIntersections: number[] = [];
11
12
let yLineStart = this.marginTop + horizontalRemainder / 2;
13
for (let i = 0; i <= horizontalReps; i++) {
14
horizontalIntersections.push(yLineStart);
15
yLineStart += cellSize;
16
}
17
18
let xLineStart = this.marginLeft + verticalRemainder / 2;
19
for (let i = 0; i <= verticalReps; i++) {
20
verticalIntersections.push(xLineStart);
21
xLineStart += cellSize;
22
}
23
24
let gridParent = this.createGroup('grid','calli-grid',this.maskId ? this.maskId : undefined);
25
26
for (const horizontalPoint of horizontalIntersections) {
27
for (const verticalPoint of verticalIntersections) {
28
const dot = `<circle cx="${this.formatCoordinate(verticalPoint)}" cy="${this.formatCoordinate(
29
horizontalPoint,
30
)}" r="${this.#config.dotSize! / 2}" fill="${this.#config.lineColor!}" />`;
31
gridParent += dot;
32
}
33
}
34
gridParent += '</g>';
35
return gridParent;
36
}
37
}

Let’s Talk About Slant Lines

Let’s Talk About Slant Lines

Okay, let’s go back to the slant lines and the problems with rotations I mentioned.

At that point, I had not yet learned that clipPath could solve my issue of mask not working in Affinity and my PDF export. I was living with the issue of lines being outside of my rounded corners, but I could not have slant lines protruding out of my area everywhere.

So, what I usually had was a start point of my line, and then usually the length of the line, as well as the angle of it. So, then I could calculate the end point of the line using trigonometry.

So, the drawSlantLine function is not one that implements a transform, but instead calculates the end point of the line and then draws a line via coordinates.

1
drawSlantLine(lineHeight: number,angle: number, xStart: number, yStart: number, color: string,stroke: number): string {
2
const xEnd = xStart + lineHeight / Math.tan((angle * Math.PI) / 180);
3
const yEnd = yStart - lineHeight;
4
const line = this.addLine(xStart, yStart, xEnd, yEnd, color, stroke);
5
return line;
6
}

I also have a few limitations for slant lines right now, with the current methods. So right now, my angle needs to fall below 90 and above 25degrees for optimal results, but I think since we’re in calligraphy world, that is fine.

There is always a graph grid for 90 degrees and I don’t think people write that steep ever, or slanted backwards.

Calligraphy Area

Calligraphy Area

Okay, let’s look at the Calligraphy Area grid.

In addition to just having horizontal lines at the x-height distance, I also wanted to have the option to add separator lines for half of the x-height, since I like to reference these in my letters, like t, p, e etc.

And then in the Area Context, I wanted to have one main slant line spanning from the bottom left corner to the top left, and then being able to specify how far apart the slant lines should be, and also how long they should be at minimum, since I felt like having tiny lines all the way to the corners was a waste of space and parameters.

So, first it was drawing horizontal lines and separator lines and then it was about figuring out the slant lines.

We start by drawing the longest line, through the middle, and then adding lines to the left and right of it, at the specified distance. And then we have to jump through extra hoops because I didn’t know about clip Path and had to make sure the lines didn’t go outside of the area, by calculating intersection points with the area and then trimming the lines to those points.

In the end, I am not going to rewrite this and use the mask, because this makes the resulting svg cleaner, so I’m keeping it like this, but I could’ve saved myself a lot of headache, because trigonometry is not my strong suit.

1
export class CalligraphyAreaPage extends GridMaker {
2
// [...]
3
private addCalligraphyArea(): string {
4
const xHeight = this.#config.xHeight;
5
const horizontalReps = this.gridHeight / xHeight;
6
const horizontalRemainder = this.gridHeight % xHeight;
7
const lineStart = this.marginLeft;
8
const lineEnd = this.width - this.marginRight;
9
const color = this.#config.lineColor;
10
const stroke = this.#config.gridStrokeWidth;
11
const dotSize = this.#config.gridStrokeWidth;
12
const rectCenterX = this.marginLeft + this.gridWidth / 2;
13
const rectCenterY = this.marginTop + this.gridHeight / 2;
14
const rectDiagonal = Math.sqrt(
15
Math.pow(this.gridWidth, 2) + Math.pow(this.gridHeight, 2),
16
);
17
const lineAngle = 180 - this.#config.slantAngle;
18
const slantSpacing = this.#config.slantAngleGap;
19
// we're using the diagonal length as a basis to ensure we cover the entire area with our function
20
const slantReps = rectDiagonal / slantSpacing;
21
22
let gridParent = this.createGroup('grid','calli-grid',this.maskId ? this.maskId : undefined);
23
24
let horizontalLines = this.createGroup('horizontal-lines');
25
let yLineStart = this.marginTop + horizontalRemainder / 2;
26
for (let i = 0; i <= horizontalReps; i++) {
27
const line = this.drawSolidLine('horizontal',yLineStart,lineStart,lineEnd,color,stroke);
28
if (this.#config.addDividerLines) {
29
if (i < horizontalReps) {
30
const gridPos = yLineStart + xHeight / 2;
31
const line = this.drawDashedLine('horizontal',gridPos,lineStart,lineEnd,dotSize,color);
32
horizontalLines += line;
33
}
34
}
35
yLineStart += xHeight;
36
horizontalLines += line;
37
}
38
gridParent += horizontalLines;
39
40
let slantLines = this.createGroup('slant-lines');
41
42
const angleRad = (lineAngle * Math.PI) / 180;
43
const centerLineLength = rectDiagonal / 2;
44
const lineStartX = rectCenterX - centerLineLength * Math.cos(angleRad);
45
const lineStartY = rectCenterY - centerLineLength * Math.sin(angleRad);
46
const lineEndX = rectCenterX + centerLineLength * Math.cos(angleRad);
47
const lineEndY = rectCenterY + centerLineLength * Math.sin(angleRad);
48
49
const line = this.drawLineWithinArea(lineStartX,lineStartY,lineEndX,lineEndY,color,stroke);
50
51
slantLines += line;
52
53
let distance = slantSpacing;
54
for (let i = 0; i < slantReps; i++) {
55
// draw lines to the left and right of center line
56
const linesLeft = this.createParallelLine(lineStartX,lineStartY,lineEndX,lineEndY,distance,color,stroke,this.#config.slantLineMinLength);
57
const linesRight = this.createParallelLine(lineStartX,lineStartY,lineEndX,lineEndY,distance * -1,color,stroke,this.#config.slantLineMinLength);
58
slantLines += linesLeft;
59
slantLines += linesRight;
60
distance += slantSpacing;
61
}
62
63
gridParent += slantLines;
64
gridParent += '</g>';
65
return gridParent;
66
}
67
68
private createParallelLine(x1: number,y1: number,x2: number,y2: number,distance: number,color: string,stroke: number,maxLength: number | undefined,): string {
69
const {x1: newX1,y1: newY1,x2: newX2,y2: newY2} = this.generateParallelCoordinates(x1, y1, x2, y2, distance);
70
71
return this.drawLineWithinArea(newX1,newY1,newX2,newY2,color,stroke,maxLength);
72
}
73
74
private generateParallelCoordinates(x1: number,y1: number,x2: number,y2: number,distance: number,): { x1: number; y1: number; x2: number; y2: number } {
75
// Calculate offsets for the new line based on the distance provided
76
const xOffset = distance;
77
78
// Calculate new coordinates for the parallel line along the X-axis
79
const newX1 = x1 + xOffset;
80
const newY1 = y1;
81
const newX2 = x2 + xOffset;
82
const newY2 = y2;
83
84
return { x1: newX1, y1: newY1, x2: newX2, y2: newY2 };
85
}
86
87
private drawLineWithinArea(x1: number,y1: number,x2: number,y2: number,color: string,stroke: number,maxLength?: number,): string {
88
const intersectionPoints = this.calculateIntersectionPoints(x1, y1, x2, y2); // Corrected parameter order
89
let line = '';
90
91
if (intersectionPoints.length > 0) {
92
// Trim the line to start and end at the intersection points
93
let trimmedX1 = x1;
94
let trimmedY1 = y1;
95
let trimmedX2 = x2;
96
let trimmedY2 = y2;
97
98
if (intersectionPoints.length >= 2) {
99
trimmedX1 = intersectionPoints[0]?.x ?? x1;
100
trimmedY1 = intersectionPoints[0]?.y ?? y1;
101
trimmedX2 = intersectionPoints[1]?.x ?? x2;
102
trimmedY2 = intersectionPoints[1]?.y ?? y2;
103
}
104
105
const trimmedLineLength = Math.sqrt((trimmedX2 - trimmedX1) ** 2 + (trimmedY2 - trimmedY1) ** 2,);
106
107
const lineColor = color;
108
if (maxLength) {
109
if (trimmedLineLength > maxLength) {
110
line = this.addLine(trimmedX1,trimmedY1,trimmedX2,trimmedY2,lineColor,stroke);
111
}
112
} else {
113
line = this.addLine(trimmedX1,trimmedY1,trimmedX2,trimmedY2,lineColor,stroke);
114
}
115
}
116
return line;
117
}
118
119
private calculateIntersectionPoints(lineX1: number,lineY1: number,lineX2: number,lineY2: number): Point[] {
120
const rectX = this.marginLeft;
121
const rectY = this.marginTop;
122
const rectWidth = this.gridWidth;
123
const rectHeight = this.gridHeight;
124
125
const slope = (lineY2 - lineY1) / (lineX2 - lineX1);
126
const yIntercept = lineY1 - slope * lineX1;
127
const isInsideRectangle = (x: number, y: number) =>
128
x >= rectX &&
129
x <= rectX + rectWidth &&
130
y >= rectY &&
131
y <= rectY + rectHeight;
132
const topIntersectionX = (rectY - yIntercept) / slope;
133
const bottomIntersectionX = (rectY + rectHeight - yIntercept) / slope;
134
const leftIntersectionY = slope * rectX + yIntercept;
135
const rightIntersectionY = slope * (rectX + rectWidth) + yIntercept;
136
137
const intersectionPoints: Point[] = [];
138
139
if (isInsideRectangle(topIntersectionX, rectY)) {
140
intersectionPoints.push({ x: topIntersectionX, y: rectY });
141
}
142
if (isInsideRectangle(bottomIntersectionX, rectY + rectHeight)) {
143
intersectionPoints.push({
144
x: bottomIntersectionX,
145
y: rectY + rectHeight,
146
});
147
}
148
if (isInsideRectangle(rectX, leftIntersectionY)) {
149
intersectionPoints.push({ x: rectX, y: leftIntersectionY });
150
}
151
if (isInsideRectangle(rectX + rectWidth, rightIntersectionY)) {
152
intersectionPoints.push({ x: rectX + rectWidth, y: rightIntersectionY });
153
}
154
return intersectionPoints;
155
}
156
}

Calligraphy Lines

Calligraphy Lines

The piece de resistance, my workhorse, my beloved, the Calligraphy Lines grid.

Here, I wanted a few more parameters to be adjustable. Not only the slant angle, but also the x-height and the ratio.

And in the calligraphy line grid, I wanted to draw the slant lines differently, Here I wanted the first line to start at the bottom left corner of the descender line and the last line to end at the top right corner of the ascender line.

So, specifying a gap is not really going to work here, because there might be something over if I’m placing lines between two lines. So instead, I opted to have a value of how many lines are drawn between and then they are evenly spaced.

I also wanted the baseline to be bolder than the others, and the option to add an indicator on the left for the baseline.

When using the area Box, I also wanted to be able to specify a buffer, so that the lines don’t start right at the top and are cut off by the rounded corners. That is very much a stylistic choice though, so it was configurable again.

The main grid is drawn like the graph Grid horizontal lines, and then there is a separate drawing function, to take care of drawing ascender, descender, base line, slant lines and all the other optional lines.

1
export class CalligraphyLinePage extends GridMaker {
2
get lineHeight(): number {
3
const { ascender, base, descender } = this.normalizedRatio;
4
return this.xHeight * (ascender + base + descender);
5
}
6
7
get ratio(): { ascender: number; base: number; descender: number } {
8
return {
9
ascender: this.#config.ratioAscender,
10
base: this.#config.ratioBase,
11
descender: this.#config.ratioDescender,
12
};
13
}
14
15
get normalizedRatio(): { ascender: number; base: number; descender: number } {
16
const normalizationFactor = 1 / this.#config.ratioBase;
17
return {
18
ascender: this.#config.ratioAscender * normalizationFactor,
19
base: 1,
20
descender: this.#config.ratioDescender * normalizationFactor,
21
};
22
}
23
24
// [...]
25
private addCalligraphyLines(): string {
26
let gridParent = this.createGroup('grid','calli-grid',this.maskId ? this.maskId : undefined,);
27
const buffer = this.#config.addAreaBox ? this.#config.areaBlockBuffer : 0;
28
const height = this.gridHeight - buffer * 2;
29
const lineReps = Math.floor(height / this.lineHeight);
30
const lineGap = (height - lineReps * this.lineHeight) / (lineReps - 1);
31
let yLineStart = this.marginTop + buffer;
32
33
for (let i = 0; i < lineReps; i++) {
34
const line = this.addCalligraphyLine(yLineStart,this.marginLeft,this.width - this.marginRight);
35
gridParent += line;
36
yLineStart += this.lineHeight + lineGap;
37
}
38
39
gridParent += '</g>';
40
41
return gridParent;
42
}
43
44
private addCalligraphyLine(gridPos: number,lineStart: number,lineEnd: number): Element | string {
45
const {
46
ascender: normalizedAscender,
47
base: normalizedBase,
48
descender: normalizedDescender,
49
} = this.normalizedRatio;
50
const gridPosAscenderLine = gridPos;
51
const gridPosXHeightLine = gridPosAscenderLine + this.xHeight * normalizedAscender;
52
const gridPosBaseLine = gridPosXHeightLine + this.xHeight * normalizedBase;
53
const gridPosDescenderLine = gridPosBaseLine + this.xHeight * normalizedDescender;
54
55
let lineGroup = this.createGroup('line');
56
const ascender = this.addLineSection('ascender',gridPosAscenderLine,lineStart,lineEnd,'down');
57
lineGroup += ascender;
58
const base = this.addLineSection('base',gridPosBaseLine,lineStart,lineEnd,'up');
59
lineGroup += base;
60
const descender = this.addLineSection('descender',gridPosDescenderLine,lineStart,lineEnd,'up');
61
lineGroup += descender;
62
if (this.#config.slantAngle > 0) {
63
const slantLines = this.addSlantLines(gridPosDescenderLine, lineStart);
64
lineGroup += slantLines;
65
}
66
lineGroup += '</g>';
67
return lineGroup;
68
}
69
70
private addSlantLines(gridPos: number, lineStart: number): string {
71
let slantGroup = this.createGroup('slant-lines');
72
const reps = this.#config.slantLinesPerLine;
73
const endPosXFinalLine = this.gridWidth;
74
const startPosXFinalLine = endPosXFinalLine - this.lineHeight / Math.tan((this.#config.slantAngle * Math.PI) / 180);
75
const totalWidth = startPosXFinalLine;
76
const spaceBetweenRepetitions = totalWidth / (reps - 1);
77
let startPosX = lineStart;
78
for (let i = 0; i < reps; i++) {
79
const slantLine = this.drawSlantLine(this.lineHeight,this.#config.slantAngle,startPosX,gridPos,this.#config.lineColor,this.#config.gridStrokeWidth);
80
slantGroup += slantLine;
81
startPosX += spaceBetweenRepetitions;
82
}
83
slantGroup += '</g>';
84
return slantGroup;
85
}
86
87
private addLineSection(section: 'ascender' | 'base' | 'descender',gridPosLine: number,lineStart: number,lineEnd: number,dividerDrawingDirection: 'down' | 'up'): Element | string {
88
const ratios = this.ratio;
89
const dividerGap = this.xHeight / ratios['base'];
90
const ratio = ratios[section];
91
const color = this.#config.lineColor;
92
const stroke = this.#config.gridStrokeWidth;
93
let group = this.createGroup(section);
94
const gridPosXHeightLine = gridPosLine - this.xHeight;
95
const gridPos = section !== 'base' ? gridPosLine : gridPosXHeightLine;
96
const line1 = this.drawSolidLine('horizontal',gridPos,lineStart,lineEnd,color,stroke);
97
group += line1;
98
/**
99
* the base section includes the x-Height Line (first solid line, and then additionally a thicker baseline)
100
* and optionally a vertical xHeight Indicator
101
*/
102
if (section === 'base') {
103
const baseLine = this.drawSolidLine('horizontal',gridPosLine,lineStart,lineEnd,color,this.#config.gridBaseLineStrokeWidth);
104
group += baseLine;
105
if (this.#config.showXHeightIndicator) {
106
const xHeightIndicator = this.drawSolidLine('vertical',lineStart + this.#config.xHeightIndicatorStrokeWidth * 0.5,gridPosXHeightLine,gridPosLine,color,this.#config.xHeightIndicatorStrokeWidth);
107
group += xHeightIndicator;
108
}
109
}
110
if (this.#config.addDividerLines) {
111
for (let i = 1; i < ratio; i++) {
112
const gap = dividerDrawingDirection == 'down' ? dividerGap : dividerGap * -1;
113
const gridPos = gridPosLine + i * gap;
114
const dotRadius = this.#config.gridStrokeWidth;
115
const divider = this.drawDashedLine('horizontal',gridPos,lineStart,lineEnd,dotRadius,color);
116
group += divider;
117
}
118
}
119
group += '</g>';
120
return group;
121
}
122
}

Variations

Variations

To see how flexible it is, here are a few examples that show off the different settings.

Spencerian Style grid

Spencerian Style grid

Calligraphy Line Grid

3:1:3 ratio, 52 degree slant, 8 slant lines per line

© grid code.halfapx.com/guideline-generator/

For the Crazy People

For the Crazy People

Yes, you can do grids with no slant lines too…

© grid code.halfapx.com/guideline-generator/

Try It Out

Try It Out

In the end, I also made a generator, because I wanted to be able to make this accessible to non-coders too.

You can play with it in the preview below, of go to the site and use it there.

Bookmark it, use it, tell me if you like it. I’m open to feedback and suggestions if anything is missing.

Wrap Up

Wrap Up

I had a lot of fun with this project and I learned a lot about SVG and how to make it more efficient and how to make it work with Affinity Designer.

It was a bit of a math odyssey at times, but I am very happy with the end result.

Search

Results will appear as you type