Cómo creamos un sitio estático que genera patrones de tartán en SVG

Índice
  1. Anatomía del tartán y SVG
  2. Usando React para tejer tartán
  3. Usar un mosaico SVG de tartán como imagen de fondo
  4. Cómo descargar un mosaico de tartán SVG
  5. Crear un sitio web estático a partir de CSV con Gatsby

El tartán es una tela estampada que se asocia típicamente con Escocia, en particular con sus elegantes faldas escocesas. En tartanify.com, reunimos más de 5000 patrones de tartán (como archivos SVG y PNG), teniendo cuidado de filtrar aquellos que tengan restricciones de uso explícitas.

La idea surgió de Sylvain Guizard durante nuestras vacaciones de verano en Escocia. Al principio, estábamos pensando en crear la biblioteca de patrones manualmente en algún software de gráficos, como Adobe Illustrator o Sketch. Pero eso fue antes de que descubriéramos que la cantidad de patrones de tartán es de miles. Nos sentimos abrumados y nos dimos por vencidos… hasta que descubrí que los tartán tienen una anatomía específica y están referenciados por simples cadenas compuestas por números de hilos y códigos de colores.

Anatomía del tartán y SVG

El tartán se confecciona con bandas alternas de hilos de colores tejidos en ángulos rectos y paralelos entre sí. Las bandas verticales y horizontales siguen el mismo patrón de colores y anchos. Las áreas rectangulares donde se cruzan las bandas horizontales y verticales dan la apariencia de nuevos colores al mezclar los originales. Además, los tartán se tejen con una técnica específica llamada sarga, que da como resultado líneas diagonales visibles. Intenté recrear la técnica con rectángulos SVG como hilos aquí:

Analicemos la siguiente estructura SVG:

svg viewBox="0 0 280 280" x="0"  y="0"   defs    mask x="0" y="0"      rect x="0" y="0" fill="url(#diagonalStripes)"/    /mask  /defs  g    rect fill="#FF8A00" x="0" y="0"/        rect fill="#E52E71" x="0" y="40"/    rect fill="#FFFFFF" x="0" y="50"/    rect fill="#E52E71" x="0" y="60"/       rect fill="#100E17" x="0" y="130"/        rect fill="#E52E71" x="0" y="150"/    rect fill="#FFFFFF" x="0" y="220"/    rect fill="#E52E71" x="0" y="230"/       rect fill="#FF8A00" x="0" y="240"/  /g  g mask="url(#grating)"    rect fill="#FF8A00" x="0" y="0" /      rect fill="#E52E71" x="40" y="0" /    rect fill="#FFFFFF" x="50" y="0" /    rect fill="#E52E71" x="60" y="0" /    rect fill="#100E17" x="130" y="0" /       rect fill="#E52E71" x="150" y="0" /    rect fill="#FFFFFF" x="220" y="0" /    rect fill="#E52E71" x="230" y="0" /       rect fill="#FF8A00" x="240" y="0" /  /g/svg

El horizontalStripesgrupo crea un cuadrado de 280×280 con franjas horizontales. El verticalStripesgrupo crea el mismo cuadrado, pero girado 90 grados. Ambos cuadrados comienzan en (0,0)coordenadas. Eso significa que horizontalStripesestán completamente cubiertos por verticalStripes; es decir, a menos que apliquemos una mascarilla en la superior.

defs  mask x="0" y="0"    rect x="0" y="0" fill="url(#diagonalStripes)"/  /mask/defs

El elemento SVG de máscara define una máscara alfa. De forma predeterminada, el sistema de coordenadas utilizado para sus atributos x, y, widthy heightes objectBoundingBox. Configurar width y height en 1 (o 100%) significa que la máscara cubre el verticalStripes resultado, lo que hace que solo las partes blancas dentro de la máscara sean completamente visibles.

¿Podemos llenar nuestra máscara con un patrón? ¡Si podemos! Reflejemos la técnica de tejido de tartán utilizando un patrón de mosaico, así:

En la definición del patrón cambiamos las Unidades del patrón del valor predeterminado objectBoundingBoxpara userSpaceOnUseque ahora, el ancho y el alto se definan en píxeles.

svg  defs    pattern x="0" y="0" patternUnits="userSpaceOnUse"      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

Usando React para tejer tartán

Acabamos de ver cómo podemos crear un “tejido” manual con SVG. Ahora automaticemos este proceso con React.

El SvgDefscomponente es sencillo: devuelve el marcado defs.

const SvgDefs = () = {  return (    defs      pattern               x="0"        y="0"                      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 x="0" y="0"        rect          x="0"          y="0"                            fill="url(#diagonalStripes)"        /      /mask    /defs  )}

Representaremos un tartán como una serie de rayas. Cada franja es un objeto con dos propiedades: fill(un color hexadecimal) y size(un número).

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

Los datos de Tartans suelen estar disponibles como un par de cadenas: Palettey Threadcountpodrían verse así:

// PaletteO#FF8A00 P#E52E71 W#FFFFFF K#100E17// ThreadcountO/40 P10 W10 P70 K/10.

No cubriré cómo convertir esta representación de cadena en la matriz de rayas pero, si está interesado, puede encontrar mi método en este Gist .

El SvgTilecomponente toma la tartanmatriz como accesorios y devuelve una estructura 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"                SvgDefs /      g        {tartan.map((el, index) = {          return (            rect              fill={el.fill}                           height={el.size}              x="0"              y={cumulativeSizes[index - 1] || 0}            /          )        })}      /g      g mask="url(#grating)"        {tartan.map((el, index) = {          return (            rect              fill={el.fill}              width={el.size}                           x={cumulativeSizes[index - 1] || 0}              y="0"            /          )        })}      /g    /svg  )}

Usar un mosaico SVG de tartán como imagen de fondo

En tartanify.com, cada tartán individual se utiliza como imagen de fondo en un elemento de pantalla completa. Esto requiere cierta manipulación adicional ya que no tenemos nuestro mosaico con patrón de tartán como una imagen SVG. Tampoco podemos usar un SVG en línea directamente en la propiedad de imagen de fondo.

Afortunadamente, codificar el SVG como imagen de fondo funciona:

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

Ahora creemos un SvgBgcomponente. Toma la tartanmatriz como accesorios y devuelve un div de pantalla completa con el patrón de tartán como fondo.

Necesitamos convertir el SvgTileobjeto React en una cadena. El ReactDOMServerobjeto permite renderizar componentes con marcado estático. Su método renderToStaticMarkupestá disponible tanto en el navegador como en el servidor Node. Esto último es importante ya que más adelante renderizaremos en el servidor las páginas de tartán con Gatsby .

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

Nuestra cadena SVG contiene códigos de color hexadecimales que comienzan con el #símbolo. Al mismo tiempo, #inicia un identificador de fragmento en una URL. Significa que nuestro código se romperá a menos que escapemos de todas esas instancias. Ahí es donde la función JavaScript incorporada encodeURIComponentresulta útil.

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

Cómo descargar un mosaico de tartán SVG

Descarguemos ahora nuestra imagen SVG.

El SvgDownloadLink componente toma svgData (la cadena SVG ya codificada) y fileName como accesorios y crea un aelemento de anclaje (). El downloadatributo solicita al usuario que guarde la URL vinculada en lugar de navegar hasta ella. Cuando se usa con un valor, sugiere el nombre del archivo de destino.

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

Conversión de un mosaico de tartán SVG en un archivo de imagen PNG de alta resolución

¿Qué pasa con los usuarios que prefieren el formato de imagen PNG a SVG? ¿Podemos proporcionarles archivos PNG de alta resolución?

El PngDownloadLink componente, al igual que SvgDownloadLink, crea una etiqueta de anclaje y tiene tartanData y fileNamecomo accesorios. Sin embargo, en este caso también debemos proporcionar el tamaño del mosaico de tartán, ya que necesitamos establecer las dimensiones del lienzo.

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

En el navegador, una vez que el componente está listo, dibujamos el mosaico SVG en un canvas elemento. Usaremos el toDataUrl()método canvas que devuelve la imagen como un URI de datos. Finalmente, configuramos el URI de fecha como href atributo de nuestra etiqueta de anclaje.

Tenga en cuenta que usamos dimensiones dobles para el lienzo y escalamos dos veces ctx. De esta manera, generaremos un PNG que tiene el doble de tamaño, lo cual es excelente para uso en alta resolución.

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

Para esa demostración, podría haberme saltado useEffectel gancho de React y el código habría funcionado bien. Sin embargo, nuestro código se ejecuta tanto en el servidor como en el navegador, gracias a Gatsby. Antes de comenzar a crear el lienzo, debemos asegurarnos de que estamos en un navegador. También debemos asegurarnos de que el elemento de anclaje esté "listo" antes de modificar su atributo.

Crear un sitio web estático a partir de CSV con Gatsby

Si aún no ha oído hablar de Gatsby , es un marco gratuito y de código abierto que le permite extraer datos de casi cualquier lugar y generar sitios web estáticos impulsados ​​por React.

Tartanify.com es un sitio web de Gatsby codificado por mí y diseñado por Sylvain. Al comienzo del proyecto, todo lo que teníamos era un archivo CSV enorme (en serio, 5495 filas), un método para convertir la paleta y las cadenas de número de hilos en la estructura SVG de tartán y un objetivo para darle a Gatsby una oportunidad.

Para utilizar un archivo CSV como fuente de datos, necesitamos dos complementos de Gatsby: gatsby-transformer-csv y gatsby-source-filesystem . Debajo del capó, el complemento fuente lee los archivos en la carpeta /src/data (que es donde colocamos el tartans.csvarchivo), luego el complemento transformador analiza el archivo CSV en matrices JSON.

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

Ahora, veamos qué sucede en el archivo gatsby-node.js. El archivo se ejecuta durante el proceso de creación del sitio. Ahí es donde podemos usar dos API de Gatsby Node: createPagesy onCreateNode. onCreateNodeSe llama cuando se crea un nuevo nodo. Agregaremos dos campos adicionales a un nodo de tartán: su slug único y un nombre único. Es necesario ya que el archivo CSV contiene varias variantes de tartán que se almacenan con el mismo nombre.

// gatsby-node.js// We add slugs here and use this array to check if a slug is already in uselet slugs = []// Then, if needed, we append a numberlet i = 1exports.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,    })  }}

A continuación, creamos páginas para cada tartán individual. Queremos tener acceso a sus hermanos para que podamos navegar fácilmente. Consultaremos los bordes anterior y siguiente y agregaremos el resultado al contexto de la página de tartán.

// gatsby-node.jsexports.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,        },      })    }  )}

Decidimos indexar los tartanes por letras y crear páginas de letras paginadas. Estas páginas enumeran tartanes con enlaces a sus páginas individuales. Mostramos un máximo de 60 tartanes por página y el número de páginas por carta varía. Por ejemplo, la letra “a” tendrá cuatro páginas : tartans/a, y . El mayor número de páginas (15) pertenece a “m” debido a la gran cantidad de nombres tradicionales que comienzan con “Mac”.tartans/a/2tartans/a/3tartans/a/4

La tartans/a/4página debe apuntar tartans/b como página siguiente y tartans/bdebe apuntar tartans/a/4como página anterior.

Realizaremos un for ofbucle a través de la matriz de letras ["a", "b", ... , "z"]y consultaremos todos los tartanes que comiencen con una letra determinada. Esto se puede hacer con el operador de filtro y expresión regular:

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

La previousLetterLastIndexvariable se actualizará al final de cada ciclo y almacenará el número de páginas por letra. La /tartans/bpágina necesita saber el número de páginas (4) ya que su enlace anterior debe ser tartans/a/4.

// gatsby-node.jsconst 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)  }}

La paginateNodefunción devuelve una matriz donde los elementos iniciales se agrupan por 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}

Ahora veamos la plantilla de tartán. Dado que Gatsby es una aplicación React, podemos usar los componentes que creamos en la primera parte de este artículo.

// ./src/templates/tartan.jsimport 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

Finalmente centrémonos en las páginas de índice de tartanes (las páginas de letras).

// ./src/templates/tartans.jsimport 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

El TartansNavigationcomponente agrega navegación siguiente-anterior entre las páginas de índice.

// ./src/components/tartansnavigation.jsimport 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

Pensamientos finales

Detengámonos aquí. Intenté cubrir todos los aspectos clave de este proyecto. Puedes encontrar todo el código de tartanify.com en GitHub . La estructura de este artículo refleja mi viaje personal: comprender la especificidad de los tartanes, traducirlos a SVG, automatizar el proceso, generar versiones de imágenes y descubrir Gatsby para crear un sitio web fácil de usar. Quizás no fue tan divertido como nuestro viaje a Escocia, pero realmente lo disfruté. Una vez más, un proyecto paralelo demostró ser la mejor manera de profundizar en nuevas tecnologías.

SUSCRÍBETE A NUESTRO BOLETÍN 
No te pierdas de nuestro contenido ni de ninguna de nuestras guías para que puedas avanzar en los juegos que más te gustan.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir

Este sitio web utiliza cookies para mejorar tu experiencia mientras navegas por él. Este sitio web utiliza cookies para mejorar tu experiencia de usuario. Al continuar navegando, aceptas su uso. Mas informacion