How We Created a Static Site That Generates Tartan Patterns in SVG
Publikováno: 4.3.2020
Tartan is a patterned cloth that’s typically associated with Scotland, particularly their fashionable kilts. On tartanify.com, we gathered over 5,000 tartan patterns (as SVG and PNG files), taking care to filter out any that have explicit usage restrictions.
The idea was cooked up by Sylvain Guizard during our summer holidays in Scotland. At the very beginning, we were thinking of building the pattern library manually in some graphics software, like Adobe Illustrator or Sketch. But that was before we discovered … Read article
The post How We Created a Static Site That Generates Tartan Patterns in SVG appeared first on CSS-Tricks.
Tartan is a patterned cloth that’s typically associated with Scotland, particularly their fashionable kilts. On tartanify.com, we gathered over 5,000 tartan patterns (as SVG and PNG files), taking care to filter out any that have explicit usage restrictions.
The idea was cooked up by Sylvain Guizard during our summer holidays in Scotland. At the very beginning, we were thinking of building the pattern library manually in some graphics software, like Adobe Illustrator or Sketch. But that was before we discovered that the number of tartan patterns comes in thousands. We felt overwhelmed and gave up… until I found out that tartans have a specific anatomy and are referenced by simple strings composed of the numbers of threads and color codes.
Tartan anatomy and SVG
Tartan is made with alternating bands of colored threads woven at right angles that are parallel to each other. The vertical and horizontal bands follow the same pattern of colors and widths. The rectangular areas where the horizontal and vertical bands cross give the appearance of new colors by blending the original ones. Moreover, tartans are woven with a specific technique called twill, which results in visible diagonal lines. I tried to recreate the technique with SVG rectangles as threads here:
Let’s analyze the following SVG structure:
<svg viewBox="0 0 280 280" width="280" height="280" x="0" y="0" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="grating" x="0" y="0" width="1" height="1">
<rect x="0" y="0" width="100%" height="100%" fill="url(#diagonalStripes)"/>
</mask>
</defs>
<g id="horizontalStripes">
<rect fill="#FF8A00" height="40" width="100%" x="0" y="0"/>
<rect fill="#E52E71" height="10" width="100%" x="0" y="40"/>
<rect fill="#FFFFFF" height="10" width="100%" x="0" y="50"/>
<rect fill="#E52E71" height="70" width="100%" x="0" y="60"/>
<rect fill="#100E17" height="20" width="100%" x="0" y="130"/>
<rect fill="#E52E71" height="70" width="100%" x="0" y="150"/>
<rect fill="#FFFFFF" height="10" width="100%" x="0" y="220"/>
<rect fill="#E52E71" height="10" width="100%" x="0" y="230"/>
<rect fill="#FF8A00" height="40" width="100%" x="0" y="240"/>
</g>
<g id="verticalStripes" mask="url(#grating)">
<rect fill="#FF8A00" width="40" height="100%" x="0" y="0" />
<rect fill="#E52E71" width="10" height="100%" x="40" y="0" />
<rect fill="#FFFFFF" width="10" height="100%" x="50" y="0" />
<rect fill="#E52E71" width="70" height="100%" x="60" y="0" />
<rect fill="#100E17" width="20" height="100%" x="130" y="0" />
<rect fill="#E52E71" width="70" height="100%" x="150" y="0" />
<rect fill="#FFFFFF" width="10" height="100%" x="220" y="0" />
<rect fill="#E52E71" width="10" height="100%" x="230" y="0" />
<rect fill="#FF8A00" width="40" height="100%" x="240" y="0" />
</g>
</svg>
The horizontalStripes
group creates a 280x280 square with horizontal stripes. The verticalStripes
group creates the same square, but rotated by 90 degrees. Both squares start at (0,0)
coordinates. That means the horizontalStripes
are completely covered by the verticalStripes
; that is, unless we apply a mask on the upper one.
<defs>
<mask id="grating" x="0" y="0" width="1" height="1">
<rect x="0" y="0" width="100%" height="100%" fill="url(#diagonalStripes)"/>
</mask>
</defs>
The mask SVG element defines an alpha mask. By default, the coordinate system used for its x
, y
, width
, and height
attributes is the objectBoundingBox
. Setting width
and height
to 1
(or 100%) means that the mask covers the verticalStripes
resulting in just the white parts within the mask being full visible.
Can we fill our mask with a pattern? Yes, we can! Let’s reflect the tartan weaving technique using a pattern tile, like this:
In the pattern definition we change the patternUnits from the default objectBoundingBox
to userSpaceOnUse
so that now, width and height are defined in pixels.
<svg width="0" height="0">
<defs>
<pattern id="diagonalStripes" x="0" y="0" patternUnits="userSpaceOnUse" width="8" height="8">
<polygon points="0,4 0,8 8,0 4,0" fill="white"/>
<polygon points="4,8 8,8 8,4" fill="white"/>
</pattern>
</defs>
</svg>
Using React for tartan weaving
We just saw how we can create a manual “weave” with SVG. Now let’s automatize this process with React.
The SvgDefs
component is straightforward — it returns the defs markup.
const SvgDefs = () => {
return (
<defs>
<pattern
id="diagonalStripes"
x="0"
y="0"
width="8"
height="8"
patternUnits="userSpaceOnUse"
>
<polygon points="0,4 0,8 8,0 4,0" fill="#ffffff" />
<polygon points="4,8 8,8 8,4" fill="#ffffff" />
</pattern>
<mask id="grating" x="0" y="0" width="1" height="1">
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="url(#diagonalStripes)"
/>
</mask>
</defs>
)
}
We will represent a tartan as an array of stripes. Each stripe is an object with two properties: fill
(a hex color) and size
(a number).
const tartan = [
{ fill: "#FF8A00", size: 40 },
{ fill: "#E52E71", size: 10 },
{ fill: "#FFFFFF", size: 10 },
{ fill: "#E52E71", size: 70 },
{ fill: "#100E17", size: 20 },
{ fill: "#E52E71", size: 70 },
{ fill: "#FFFFFF", size: 10 },
{ fill: "#E52E71", size: 10 },
{ fill: "#FF8A00", size: 40 },
]
Tartans data is often available as a pair of strings: Palette
and Threadcount
that could look like this:
// Palette
O#FF8A00 P#E52E71 W#FFFFFF K#100E17
// Threadcount
O/40 P10 W10 P70 K/10.
I won’t cover how to convert this string representation into the stripes array but, if you are interested, you can find my method in this Gist.
The SvgTile
component takes the tartan
array as props and returns an SVG structure.
const SvgTile = ({ tartan }) => {
// We need to calculate the starting position of each stripe and the total size of the tile
const cumulativeSizes = tartan
.map(el => el.size)
.reduce(function(r, a) {
if (r.length > 0) a += r[r.length - 1]
r.push(a)
return r
}, [])
// The tile size
const size = cumulativeSizes[cumulativeSizes.length - 1]
return (
<svg
viewBox={`0 0 ${size} ${size}`}
width={size}
height={size}
x="0"
y="0"
xmlns="http://www.w3.org/2000/svg"
>
<SvgDefs />
<g id="horizontalStripes">
{tartan.map((el, index) => {
return (
<rect
fill={el.fill}
width="100%"
height={el.size}
x="0"
y={cumulativeSizes[index - 1] || 0}
/>
)
})}
</g>
<g id="verticalStripes" mask="url(#grating)">
{tartan.map((el, index) => {
return (
<rect
fill={el.fill}
width={el.size}
height="100%"
x={cumulativeSizes[index - 1] || 0}
y="0"
/>
)
})}
</g>
</svg>
)
}
Using a tartan SVG tile as a background image
On tartanify.com, each individual tartan is used as a background image on a full-screen element. This requires some extra manipulation since we don’t have our tartan pattern tile as an SVG image. We're also unable to use an inline SVG directly in the background-image property.
Fortunately, encoding the SVG as a background image does work:
.bg-element {
background-image: url('data:image/svg+xml;charset=utf-8,<svg>...</svg>');
}
Let’s now create an SvgBg
component. It takes the tartan
array as props and returns a full-screen div with the tartan pattern as background.
We need to convert the SvgTile
React object into a string. The ReactDOMServer
object allows us to render components to static markup. Its method renderToStaticMarkup
is available both in the browser and on the Node server. The latter is important since later we will server render the tartan pages with Gatsby.
const tartanStr = ReactDOMServer.renderToStaticMarkup(<SvgTile tartan={tartan} />)
Our SVG string contains hex color codes starting with the #
symbol. At the same time, #
starts a fragment identifier in a URL. It means our code will break unless we escape all of those instances. That’s where the built-in JavaScript encodeURIComponent
function comes in handy.
const SvgBg = ({ tartan }) => {
const tartanStr = ReactDOMServer.renderToStaticMarkup(<SvgTile tartan={tartan} />)
const tartanData = encodeURIComponent(tartanStr)
return (
<div
style={{
width: "100%",
height: "100vh",
backgroundImage: `url("data:image/svg+xml;utf8,${tartanData}")`,
}}
/>
)
}
Making an SVG tartan tile downloadable
Let’s now download our SVG image.
The SvgDownloadLink
component takes svgData
(the already encoded SVG string) and fileName
as props and creates an anchor (<a>
) element. The download
attribute prompts the user to save the linked URL instead of navigating to it. When used with a value, it suggests the name of the destination file.
const SvgDownloadLink = ({ svgData, fileName = "file" }) => {
return (
<a
download={`${fileName}.svg`}
href={`data:image/svg+xml;utf8,${svgData}`}
>
Download as SVG
</a>
)
}
Converting an SVG tartan tile to a high-res PNG image file
What about users that prefer the PNG image format over SVG? Can we provide them with high resolution PNGs?
The PngDownloadLink
component, just like SvgDownloadLink
, creates an anchor tag and has the tartanData
and fileName
as props. In this case however, we also need to provide the tartan tile size since we need to set the canvas dimensions.
const Tile = SvgTile({tartan})
// Tartan tiles are always square
const tartanSize = Tile.props.width
In the browser, once the component is ready, we draw the SVG tile on a <canvas>
element. We’ll use the canvas toDataUrl()
method that returns the image as a data URI. Finally, we set the date URI as the href
attribute of our anchor tag.
Notice that we use double dimensions for the canvas and double scale the ctx
. This way, we will output a PNG that’s double the size, which is great for high-resolution usage.
const PngDownloadLink = ({ svgData, width, height, fileName = "file" }) => {
const aEl = React.createRef()
React.useEffect(() => {
const canvas = document.createElement("canvas")
canvas.width = 2 * width
canvas.height = 2 * height
const ctx = canvas.getContext("2d")
ctx.scale(2, 2)
let img = new Image()
img.src = `data:image/svg+xml, ${svgData}`
img.onload = () => {
ctx.drawImage(img, 0, 0)
const href = canvas.toDataURL("image/png")
aEl.current.setAttribute("href", href)
}
}, [])
return (
<a
ref={aEl}
download={`${fileName}.png`}
>
Download as PNG
</a>
)
}
For that demo, I could have skipped React's useEffect
hook and the code would worked fine. Nevertheless, our code is executed both on the server and in the browser, thanks to Gatsby. Before we start creating the canvas, we need to be sure that we are in a browser. We should also make sure the anchor element is ”ready” before we modify its attribute.
Making a static website out of CSV with Gatsby
If you haven’t already heard of Gatsby, it’s a free and open source framework that allows you to pull data from almost anywhere and generate static websites that are powered by React.
Tartanify.com is a Gatsby website coded by myself and designed by Sylvain. At the beginning of the project, all we had was a huge CSV file (seriously, 5,495 rows), a method to convert the palette and threadcount strings into the tartan SVG structure, and an objective to give Gatsby a try.
In order to use a CSV file as the data source, we need two Gatsby plugins: gatsby-transformer-csv and gatsby-source-filesystem. Under the hood, the source plugin reads the files in the /src/data folder (which is where we put the tartans.csv
file), then the transformer plugin parses the CSV file into JSON arrays.
// gatsby-config.js
module.exports = {
/* ... */
plugins: [
'gatsby-transformer-csv',
{
resolve: 'gatsby-source-filesystem',
options: {
path: `${__dirname}/src/data`,
name: 'data',
},
},
],
}
Now, let’s see what happens in the gatsby-node.js file. The file is run during the site-building process. That’s where we can use two Gatsby Node APIs: createPages
and onCreateNode
. onCreateNode
is called when a new node is created. We will add two additional fields to a tartan node: its unique slug and a unique name. It is necessary since the CSV file contains a number of tartan variants that are stored under the same name.
// gatsby-node.js
// We add slugs here and use this array to check if a slug is already in use
let slugs = []
// Then, if needed, we append a number
let i = 1
exports.onCreateNode = ({ node, actions }) => {
if (node.internal.type === 'TartansCsv') {
// This transforms any string into slug
let slug = slugify(node.Name)
let uniqueName = node.Name
// If the slug is already in use, we will attach a number to it and the uniqueName
if (slugs.indexOf(slug) !== -1) {
slug += `-${i}`
uniqueName += ` ${i}`
i++
} else {
i = 1
}
slugs.push(slug)
// Adding fields to the node happen here
actions.createNodeField({
name: 'slug',
node,
value: slug,
})
actions.createNodeField({
name: 'Unique_Name',
node,
value: uniqueName,
})
}
}
Next, we create pages for each individual tartan. We want to have access to its siblings so that we can navigate easily. We will query the previous and next edges and add the result to the tartan page context.
// gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const allTartans = await graphql(`
query {
allTartansCsv {
edges {
node {
id
fields {
slug
}
}
previous {
fields {
slug
Unique_Name
}
}
next {
fields {
slug
Unique_Name
}
}
}
}
}
`)
if (allTartans.errors) {
throw allTartans.errors
}
allTartans.data.allTartansCsv.edges.forEach(
({ node, next, previous }) => {
createPage({
path: `/tartan/${node.fields.slug}`,
component: path.resolve(`./src/templates/tartan.js`),
context: {
id: node.id,
previous,
next,
},
})
}
)
}
We decided to index tartans by letters and create paginated letter pages. These pages list tartans with links to their individual pages. We display a maximum of 60 tartans per page, and the number of pages per letter varies. For example, the letter “a” will have have four pages: tartans/a
, tartans/a/2
, tartans/a/3
and tartans/a/4
. The highest number of pages (15) belongs to “m” due to a high number of traditional names starting with “Mac.”
The tartans/a/4
page should point to tartans/b
as its next page and tartans/b
should point to tartans/a/4
as its previous page.
We will run a for of
loop through the letters array ["a", "b", ... , "z"]
and query all tartans that start with a given letter. This can be done with filter and regex operator:
allTartansCsv(filter: { Name: { regex: "/^${letter}/i" } })
The previousLetterLastIndex
variable will be updated at the end of each loop and store the number of pages per letter. The /tartans/b
page need to know the number of a pages (4) since its previous link should be tartans/a/4
.
// gatsby-node.js
const letters = "abcdefghijklmnopqrstuvwxyz".split("")
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
// etc.
let previousLetterLastIndex = 1
for (const letter of letters) {
const allTartansByLetter = await graphql(`
query {
allTartansCsv(filter: {Name: {regex: "/^${letter}/i"}}) {
nodes {
Palette
fields {
slug
Unique_Name
}
}
totalCount
}
}
`)
if (allTartansByLetter.errors) {
throw allTartansByLetter.errors
}
const nodes = allTartansByLetter.data.allTartansCsv.nodes
const totalCountByLetter = allTartansByLetter.data.allTartansCsv.totalCount
const paginatedNodes = paginateNodes(nodes, pageLength)
paginatedNodes.forEach((group, index, groups) => {
createPage({
path:
index > 0 ? `/tartans/${letter}/${index + 1}` : `/tartans/${letter}`,
component: path.resolve(`./src/templates/tartans.js`),
context: {
group,
index,
last: index === groups.length - 1,
pageCount: groups.length,
letter,
previousLetterLastIndex,
},
})
})
previousLetterLastIndex = Math.ceil(totalCountByLetter / pageLength)
}
}
The paginateNode
function returns an array where initial elements are grouped by pageLength
.
const paginateNodes = (array, pageLength) => {
const result = Array()
for (let i = 0; i < Math.ceil(array.length / pageLength); i++) {
result.push(array.slice(i * pageLength, (i + 1) * pageLength))
}
return result
}
Now let’s look into the tartan template. Since Gatsby is a React application, we can use the components that we were building in the first part of this article.
// ./src/templates/tartan.js
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
import SvgTile from "../components/svgtile"
import SvgBg from "../components/svgbg"
import svgAsString from "../components/svgasstring"
import SvgDownloadLink from "../components/svgdownloadlink"
import PngDownloadLink from "../components/pngdownloadlink"
export const query = graphql`
query($id: String!) {
tartansCsv(id: { eq: $id }) {
Palette
Threadcount
Origin_URL
fields {
slug
Unique_Name
}
}
}
`
const TartanTemplate = props => {
const { fields, Palette, Threadcount } = props.data.tartansCsv
const {slug} = fields
const svg = SvgTile({
palette: Palette,
threadcount: Threadcount,
})
const svgData = svgAsString(svg)
const svgSize = svg.props.width
return (
<Layout>
<SvgBg svg={svg} />
{/* title and navigation component comes here */}
<div className="downloads">
<SvgDownloadLink svgData={svgData} fileName={slug} />
<PngDownloadLink svgData={svgData} size={svgSize} fileName={slug} />
</div>
</Layout>
)
}
export default TartanTemplate
Finally let’s focus on the tartans index pages (the letter pages).
// ./src/templates/tartans.js
import React from "react"
import Layout from "../components/layout"
import {Link} from "gatsby"
import TartansNavigation from "../components/tartansnavigation"
const TartansTemplate = ({ pageContext }) => {
const {
group,
index,
last,
pageCount,
letter,
previousLetterLastIndex,
} = pageContext
return (
<Layout>
<header>
<h1>{letter}</h1>
</header>
<ul>
{group.map(node => {
return (
<li key={node.fields.slug}>
<Link to={`/tartan/${node.fields.slug}`}>
<span>{node.fields.Unique_Name}</span>
</Link>
</li>
)
})}
</ul>
<TartansNavigation
letter={letter}
index={index}
last={last}
previousLetterLastIndex={previousLetterLastIndex}
/>
</Layout>
)
}
export default TartansTemplate
The TartansNavigation
component adds next-previous navigation between the index pages.
// ./src/components/tartansnavigation.js
import React from "react"
import {Link} from "gatsby"
const letters = "abcdefghijklmnopqrstuvwxyz".split("")
const TartansNavigation = ({
className,
letter,
index,
last,
previousLetterLastIndex,
}) => {
const first = index === 0
const letterIndex = letters.indexOf(letter)
const previousLetter = letterIndex > 0 ? letters[letterIndex - 1] : ""
const nextLetter =
letterIndex < letters.length - 1 ? letters[letterIndex + 1] : ""
let previousUrl = null, nextUrl = null
// Check if previousUrl exists and create it
if (index === 0 && previousLetter) {
// First page of each new letter except "a"
// If the previous letter had more than one page we need to attach the number
const linkFragment =
previousLetterLastIndex === 1 ? "" : `/${previousLetterLastIndex}`
previousUrl = `/tartans/${previousLetter}${linkFragment}`
} else if (index === 1) {
// The second page for a letter
previousUrl = `/tartans/${letter}`
} else if (index > 1) {
// Third and beyond
previousUrl = `/tartans/${letter}/${index}`
}
// Check if `nextUrl` exists and create it
if (last && nextLetter) {
// Last page of any letter except "z"
nextUrl = `/tartans/${nextLetter}`
} else if (!last) {
nextUrl = `/tartans/${letter}/${(index + 2).toString()}`
}
return (
<nav>
{previousUrl && (
<Link to={previousUrl} aria-label="Go to Previous Page" />
)}
{nextUrl && (
<Link to={nextUrl} aria-label="Go to Next Page" />
)}
</nav>
)
}
export default TartansNavigation
Final thoughts
Let’s stop here. I tried to cover all of the key aspects of this project. You can find all the tartanify.com code on GitHub. The structure of this article reflects my personal journey — understanding the specificity of tartans, translating them into SVG, automating the process, generating image versions, and discovering Gatsby to build a user-friendly website. It was maybe not as fun as our Scottish journey itself 😉, but I truly enjoyed it. Once again, a side project proved to be the best way to dig into new technology.
The post How We Created a Static Site That Generates Tartan Patterns in SVG appeared first on CSS-Tricks.