我们如何创建生成苏格兰格纹图案的静态网站

Avatar of Paulina Hetman
Paulina Hetman

DigitalOcean 为您的旅程的每个阶段提供云产品。从 免费获得 200 美元信用额度!

苏格兰格纹是一种图案织物,通常与苏格兰有关,特别是他们的时尚苏格兰短裙。在 tartanify.com 上,我们收集了 5,000 多种苏格兰格纹图案(作为 SVG 和 PNG 文件),并仔细过滤掉了任何具有明确使用限制的图案。

这个想法是在 Sylvain Guizard 在我们苏格兰的暑假期间想出来的。一开始,我们考虑在一些图形软件(如 Adobe Illustrator 或 Sketch)中手动构建图案库。但那是在我们发现苏格兰格纹图案的数量达到数千之前。我们感到不知所措,放弃了……直到我发现苏格兰格纹具有特定的解剖结构,并由简单的字符串引用,这些字符串由线数和颜色代码组成。

苏格兰格纹解剖结构和 SVG

苏格兰格纹 由互相平行且相互垂直的彩色线交织形成的交替色带构成。垂直和水平色带遵循相同的颜色和宽度图案。水平和垂直色带交叉的矩形区域通过混合原始颜色呈现出新的颜色。此外,苏格兰格纹采用称为斜纹的特定技术编织,从而产生可见的斜线。我尝试使用 SVG 矩形作为线程来重建此处的技术

让我们分析以下 SVG 结构


<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>

horizontalStripes 组创建一个带有水平条纹的 280×280 正方形。verticalStripes 组创建相同的正方形,但旋转了 90 度。两个正方形都从 (0,0) 坐标开始。这意味着 horizontalStripes 完全被 verticalStripes 覆盖;也就是说,除非我们在上面应用蒙版。

<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>

mask SVG 元素定义了一个 Alpha 蒙版。默认情况下,其 xywidthheight 属性使用的坐标系为 objectBoundingBox。将 widthheight 设置为 1(或 100%)意味着蒙版覆盖了 verticalStripes,导致蒙版内的白色部分完全可见。

我们可以用图案填充蒙版吗?是的,我们可以!让我们使用图案平铺来反映苏格兰格纹编织技术,如下所示

在图案定义中,我们将 patternUnits 从默认的 objectBoundingBox 更改为 userSpaceOnUse,以便现在 widthheight 以像素定义。

<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>

使用 React 进行苏格兰格纹编织

我们刚刚看到了如何使用 SVG 创建手动“编织”。现在让我们使用 React 自动化此过程。

SvgDefs 组件很简单,它返回 defs 标记。

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>
  )
}

我们将苏格兰格纹表示为条纹数组。每个条纹都是一个对象,具有两个属性:fill(十六进制颜色)和 size(数字)。

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 },
]

苏格兰格纹数据 通常以一对字符串的形式提供:PaletteThreadcount,看起来可能像这样

// Palette
O#FF8A00 P#E52E71 W#FFFFFF K#100E17

// Threadcount
O/40 P10 W10 P70 K/10.

我不会介绍如何将此字符串表示转换为条纹数组,但是,如果您有兴趣,您可以在 此 Gist 中找到我的方法。

SvgTile 组件将 tartan 数组作为道具,并返回一个 SVG 结构。

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>
  )
}

使用苏格兰格纹 SVG 平铺作为背景图像

在 tartanify.com 上,每个 单个苏格兰格纹 都用作全屏元素的背景图像。这需要一些额外的操作,因为我们没有将苏格兰格纹图案平铺作为 SVG 图像。我们也无法直接在 background-image 属性中使用内联 SVG。

幸运的是,将 SVG 编码为背景图像确实有效

.bg-element {
  background-image: url('data:image/svg+xml;charset=utf-8,<svg>...</svg>');
}

现在让我们创建一个 SvgBg 组件。它将 tartan 数组作为道具,并返回一个具有苏格兰格纹图案作为背景的全屏 div。

我们需要将 SvgTile React 对象转换为字符串。ReactDOMServer 对象允许我们将组件渲染为静态标记。它的方法 renderToStaticMarkup 在浏览器和 Node 服务器上都可用。后者很重要,因为稍后我们将使用 Gatsby 服务器端渲染苏格兰格纹页面。

const tartanStr = ReactDOMServer.renderToStaticMarkup(<SvgTile tartan={tartan} />)

我们的 SVG 字符串包含以 # 符号开头的十六进制颜色代码。同时,# 启动 URL 中的片段标识符。这意味着我们的代码将中断,除非我们转义所有这些实例。这就是内置 JavaScript encodeURIComponent 函数派上用场的地方。

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}")`,
      }}
    />
  )
}

使 SVG 苏格兰格纹平铺可下载

现在让我们下载我们的 SVG 图像。

SvgDownloadLink 组件将 svgData(已编码的 SVG 字符串)和 fileName 作为道具,并创建一个锚点 (<a>) 元素。download 属性提示用户保存链接的 URL 而不是导航到它。与值一起使用时,它建议目标文件的名称。

const SvgDownloadLink = ({ svgData, fileName = "file" }) => {
  return (
    <a
      download={`${fileName}.svg`}
      href={`data:image/svg+xml;utf8,${svgData}`}
    >
      Download as SVG
    </a>
  )
}

将 SVG 苏格兰格纹平铺转换为高分辨率 PNG 图像文件

那么那些更喜欢 PNG 图像格式而不是 SVG 的用户呢?我们能否为他们提供高分辨率的 PNG 文件?

PngDownloadLink 组件与 SvgDownloadLink 类似,它创建一个锚点标签,并将 tartanDatafileName 作为道具。但是,在这种情况下,我们还需要提供苏格兰格纹平铺大小,因为我们需要设置画布尺寸。

const Tile = SvgTile({tartan})
// Tartan tiles are always square
const tartanSize = Tile.props.width

在浏览器中,一旦组件准备就绪,我们就在 <canvas> 元素 上绘制苏格兰格纹平铺。我们将使用画布 toDataUrl() 方法,该方法将图像作为数据 URI 返回。最后,我们将日期 URI 设置为锚点标签的 href 属性。

请注意,我们对画布使用双倍尺寸,并将 ctx 缩放两倍。这样,我们将输出一个尺寸是原尺寸两倍的 PNG,这对于高分辨率使用非常棒。

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>
  )
}

对于那个演示,我可以跳过 React 的 useEffect 钩子,代码也能正常工作。但是,我们的代码在服务器和浏览器上都执行,这得益于 Gatsby。在我们开始创建画布之前,我们需要确保我们处于浏览器中。我们也应该确保锚元素“准备就绪”,然后再修改其属性。

使用 Gatsby 从 CSV 创建静态网站

如果您还没有听说过 Gatsby,它是一个免费的开源框架,允许您从几乎任何地方提取数据并生成由 React 驱动的静态网站。

Tartanify.com 是由我和 Sylvain 设计的 Gatsby 网站。在项目开始时,我们只有巨大的 CSV 文件(认真说,5,495 行)、将调色板和线数字符串转换为苏格兰格纹 SVG 结构的方法,以及尝试 Gatsby 的目标。

为了使用 CSV 文件作为数据源,我们需要两个 Gatsby 插件:gatsby-transformer-csvgatsby-source-filesystem。在幕后,源插件读取 /src/data 文件夹中的文件(我们将 tartans.csv 文件放在这里),然后转换器插件将 CSV 文件解析为 JSON 数组。

// gatsby-config.js
module.exports = {
  /* ... */
  plugins: [
    'gatsby-transformer-csv',
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/src/data`,
        name: 'data',
      },
    },
  ],
}

现在,让我们看看在 gatsby-node.js 文件 中发生了什么。该文件在网站构建过程中运行。在那里,我们可以使用两个 Gatsby 节点 API:createPagesonCreateNodeonCreateNode 在创建新节点时调用。我们将为苏格兰格子呢节点添加两个额外的字段:其唯一的 slug 和唯一的名称。这是必要的,因为 CSV 文件包含一些存储在相同名称下的苏格兰格子呢变体。

// 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,
    })
  }
}

接下来,我们为每个独立的苏格兰格子呢创建页面。我们希望能够访问其兄弟姐妹,以便我们可以轻松地浏览。我们将查询上一个和下一个边缘并将结果添加到苏格兰格子呢页面上下文。

// 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,
        },
      })
    }
  )
}

我们决定按字母索引苏格兰格子呢并创建分页的字母页面。这些页面列出苏格兰格子呢,并链接到其各自的页面。每个页面最多显示 60 个苏格兰格子呢,每个字母的页面数量各不相同。例如,字母“a”将有四个页面:tartans/atartans/a/2tartans/a/3tartans/a/4。由于大量传统名称以“Mac”开头,“m”的页面数量最多(15 个)。

tartans/a/4 页面应指向 tartans/b 作为其下一页,而 tartans/b 应指向 tartans/a/4 作为其上一页。

我们将通过字母数组 ["a", "b", ... , "z"] 运行一个 for of 循环,并查询以给定字母开头的所有苏格兰格子呢。这可以使用过滤器和正则表达式运算符来完成。

allTartansCsv(filter: { Name: { regex: "/^${letter}/i" } })

previousLetterLastIndex 变量将在每个循环结束时更新,并存储每个字母的页面数量。/tartans/b 页面需要知道 a 页面的数量(4),因为其上一页链接应该是 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)
  }
}

paginateNode 函数返回一个数组,其中初始元素按 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
}

现在,让我们看一下苏格兰格子呢模板。由于 Gatsby 是一个 React 应用程序,因此我们可以使用我们在本文第一部分中构建的组件。

// ./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

最后,让我们关注苏格兰格子呢索引页面(字母页面)。

// ./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

TartansNavigation 组件在索引页面之间添加了下一个和上一个导航。

// ./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

最后的想法

到此为止吧。我试图涵盖这个项目的所有关键方面。您可以在 GitHub 上找到所有 tartanify.com 代码。本文的结构反映了我的个人旅程——理解苏格兰格子呢的独特性,将其转换为 SVG,自动化流程,生成图像版本,以及发现 Gatsby 来构建一个用户友好的网站。它可能不像我们自己的苏格兰之旅那么有趣😉,但我真的很享受它。再一次,一个副项目被证明是深入了解新技术的最佳方式。