Cómo creamos un sitio estático que genera patrones de tartán en SVG
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 horizontalStripes
grupo crea un cuadrado de 280×280 con franjas horizontales. El verticalStripes
grupo crea el mismo cuadrado, pero girado 90 grados. Ambos cuadrados comienzan en (0,0)
coordenadas. Eso significa que horizontalStripes
está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
, width
y height
es 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 objectBoundingBox
para userSpaceOnUse
que 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 SvgDefs
componente 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: Palette
y Threadcount
podrí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 SvgTile
componente toma la tartan
matriz 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 SvgBg
componente. Toma la tartan
matriz como accesorios y devuelve un div de pantalla completa con el patrón de tartán como fondo.
Necesitamos convertir el SvgTile
objeto React en una cadena. El ReactDOMServer
objeto permite renderizar componentes con marcado estático. Su método renderToStaticMarkup
está 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 encodeURIComponent
resulta ú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 a
elemento de anclaje (). El download
atributo 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 fileName
como 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 useEffect
el 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.csv
archivo), 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: createPages
y onCreateNode
. onCreateNode
Se 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/2
tartans/a/3
tartans/a/4
La tartans/a/4
página debe apuntar tartans/b
como página siguiente y tartans/b
debe apuntar tartans/a/4
como página anterior.
Realizaremos un for of
bucle 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 previousLetterLastIndex
variable se actualizará al final de cada ciclo y almacenará el número de páginas por letra. La /tartans/b
pá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 paginateNode
funció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 TartansNavigation
componente 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.
Deja un comentario