feat(diseño): los diez arreglos de la revisión visual

Revisada la página con Chromium a 1440x900 y 390x844 en nueve alturas de
scroll, y verificado cada cambio contra capturas nuevas.

- El ≡ deja de leerse como icono de menú. lib/simbolo.ts se anunciaba como
  single source of truth pero no coincidía con Wordmark.tsx, que traza la
  marca con las proporciones medidas del Manual: barra de largo 5.919× su
  grosor, hueco de 2×, extremos de píldora. Ahora se derivan de ahí. De
  paso, las líneas se meten medio grosor por cada extremo: el remate
  redondo alargaba el trazo un grosor entero sobre la barra que debía
  reproducir.
- Banda: 12px en escritorio y 7px en móvil vía CSS (14px era el 3,6% de
  una pantalla de 390px y partía los párrafos), opacidad al 18%, y muelle
  de 90/30 a 210/40 — sobreamortiguado tardaba ~2s en asentar y la punta
  se arrastraba a media pantalla. El punto suelto que aparecía centrado
  arriba al cargar era el remate redondo pintándose en el origen con
  longitud cero: ahora la opacidad sigue al avance.
- Ritmo vertical un 25-30% más apretado en las diez secciones. La página
  pasa de 8.061 a 7.457px sin tocar contenido.
- Números 01-06: de marca de agua de 48px al 6% (que chocaba con títulos
  y reglas) a índice dorado sentado sobre la regla.
- Hero: los dos CTA al mismo ancho en móvil, el patrón de puntos fuera
  del logotipo, y la secuencia visible en todos los tamaños — oculta bajo
  lg dejaba el hero del móvil sin ninguna pieza gráfica. La convergencia
  de la variante "hero" se cierra más para que se lea como líneas
  buscando su eje y no como un garabato tachado.
- Estratega: fuera la retícula de puntos del retrato, que parecía el
  recuadro de imagen rota del navegador; la etiqueta del 30+ cabía en
  menos de lo que mide "construyendo" y salía en tres líneas.
- El nudo de Dolores, más pequeño y con algo más de contraste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
erikghandClaude Opus 5 committed 2026-07-31 16:34:07 -06:00
1 parent 174fcb855a
commit fa86605b39
15 files changed
+135 -52

No files matched your search

+21 -9
View File
@@ -3,7 +3,7 @@
import { useState } from 'react';
import { motion, useMotionValueEvent, useScroll, useSpring, useTransform } from 'framer-motion';
import { usePrefersReducedMotion } from '@/lib/motion';
import { BARRAS_SIMBOLO, VIEWBOX_SIMBOLO } from '@/lib/simbolo';
import { BARRAS_SIMBOLO, RADIO_BARRA, VIEWBOX_SIMBOLO } from '@/lib/simbolo';
// The gold spine of the page: one continuous line down the vertical centre,
// drawing itself as the reader scrolls and resolving into the ≡ at the close.
@@ -23,8 +23,12 @@ const ALTO = 1200;
const PASO = ALTO / TRAMOS;
// A band, not a thread. Held in real pixels by vector-effect below, so it never
// again collapses to a hairline when the viewBox scale changes.
const GROSOR = 14;
// again collapses to a hairline when the viewBox scale changes. The attribute
// is the desktop weight and the SSR fallback; GROSORES overrides it in CSS,
// because 14px is 3.6% of a 390px screen and cut straight through the body
// copy there. CSS stroke-width wins over the presentation attribute.
const GROSOR = 12;
const GROSORES = '[stroke-width:7px] md:[stroke-width:12px]';
// Four long swells rather than one page-long curve or ten short ones. Stretched
// over the whole document with preserveAspectRatio="none", a single swell reads
@@ -48,7 +52,13 @@ export function HiloConductor({ className = '' }: { className?: string }) {
const reduced = usePrefersReducedMotion();
const { scrollYProgress } = useScroll();
const recorrido = useTransform(scrollYProgress, [0, FIN], [0, 1], { clamp: true });
const avance = useSpring(recorrido, { stiffness: 90, damping: 30, restDelta: 0.001 });
// Stiff enough to keep up with the reader. At 90/30 the spring is heavily
// overdamped and takes some two seconds to settle, so the leading edge
// visibly trailed behind the scroll and read as an amputated stub.
const avance = useSpring(recorrido, { stiffness: 210, damping: 40, restDelta: 0.001 });
// At rest the round cap still paints its own dot at the path's origin, which
// landed as a stray blob dead centre at the top of the page on first load.
const aparicion = useTransform(avance, [0, 0.015], [0, 1]);
const [dibujado, setDibujado] = useState(false);
useMotionValueEvent(avance, 'change', (v) => {
@@ -65,6 +75,7 @@ export function HiloConductor({ className = '' }: { className?: string }) {
const trazo = reduced ? (
<path
d={TRAZO}
className={GROSORES}
fill="none"
stroke="currentColor"
strokeWidth={GROSOR}
@@ -74,12 +85,13 @@ export function HiloConductor({ className = '' }: { className?: string }) {
) : (
<motion.path
d={TRAZO}
className={GROSORES}
fill="none"
stroke="currentColor"
strokeWidth={GROSOR}
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
style={{ pathLength: avance }}
style={{ pathLength: avance, opacity: aparicion }}
/>
);
@@ -93,7 +105,7 @@ export function HiloConductor({ className = '' }: { className?: string }) {
auto` resolves from the width (1152 → 13824px) and silently wins
over `bottom`, adding ~6000px of blank scroll below the footer. */}
<svg
className="absolute top-0 left-1/2 h-[calc(100%-5rem)] w-full -translate-x-1/2 text-dorado/25"
className="absolute top-0 left-1/2 h-[calc(100%-5rem)] w-full -translate-x-1/2 text-dorado/[0.18]"
viewBox={`0 0 100 ${ALTO}`}
preserveAspectRatio="none"
aria-hidden="true"
@@ -105,19 +117,19 @@ export function HiloConductor({ className = '' }: { className?: string }) {
the wordmark's ≡, never a lookalike. Uniformly scaled in its own
svg, because the band's stretch would flatten the bars. */}
<svg
className="absolute bottom-4 left-1/2 w-24 -translate-x-1/2 text-dorado"
className="absolute bottom-4 left-1/2 w-20 -translate-x-1/2 text-dorado"
viewBox={VIEWBOX_SIMBOLO}
aria-hidden="true"
focusable="false"
>
{BARRAS_SIMBOLO.map((barra, i) =>
reduced ? (
<rect key={i} {...barra} rx={1.5} fill="currentColor" />
<rect key={i} {...barra} rx={RADIO_BARRA} fill="currentColor" />
) : (
<motion.rect
key={i}
{...barra}
rx={1.5}
rx={RADIO_BARRA}
fill="currentColor"
initial={{ opacity: 0 }}
animate={{ opacity: resuelto ? 1 : 0 }}
+8 -5
View File
@@ -63,12 +63,15 @@ export function SecuenciaClaridad({
if (!completa) {
const eje = linea.y1;
const d = DISPERSAS[i];
// Pulled a good way further in than it used to be: at a third of the way
// the tangle still read as a scratched-out scribble instead of as lines
// finding their axis. It must arrive close to aligned and stop short.
return {
x1: d.x1 + (26 - d.x1) * 0.35,
y1: d.y1 + (eje - d.y1) * 0.55,
x2: d.x2 + (38 - d.x2) * 0.35,
y2: d.y2 + (eje - d.y2) * 0.55,
opacity: sobrevive ? 0.95 : 0.3,
x1: d.x1 + (26 - d.x1) * 0.5,
y1: d.y1 + (eje - d.y1) * 0.7,
x2: d.x2 + (38 - d.x2) * 0.5,
y2: d.y2 + (eje - d.y2) * 0.7,
opacity: sobrevive ? 0.95 : 0.28,
};
}
return {
@@ -108,11 +108,14 @@ describe('<HiloConductor />', () => {
it('reads as a band, not as a hairline', () => {
const { container } = render(<HiloConductor />);
const grosor = Number(container.querySelector('path')?.getAttribute('stroke-width'));
const path = container.querySelector('path');
// Twice regressed into a thread nobody could see. The band is the page's
// spine and has to carry weight; the text stays legible because it sits a
// stacking level above, not because the band is faint.
expect(grosor).toBeGreaterThanOrEqual(10);
// stacking level above, not because the band is faint. The attribute is the
// desktop weight, the classes narrow it on phones where 12px is 3% of the
// screen — CSS stroke-width beats the presentation attribute.
expect(Number(path?.getAttribute('stroke-width'))).toBeGreaterThanOrEqual(10);
expect(path?.getAttribute('class') ?? '').toMatch(/md:\[stroke-width:/);
});
it('sweeps wide enough across the column to read as a route', () => {
@@ -13,7 +13,30 @@ describe('geometría del ≡', () => {
expect(izquierdas.size).toBe(1);
});
it('derives stroke lines through each bar centre', () => {
expect(LINEAS_SIMBOLO.map((l) => l.y1)).toEqual([17, 32, 47]);
it('holds the Manual proportions that make the mark read as an E', () => {
const [b1, b2, b3] = BARRAS_SIMBOLO;
// Measured off the Manual and traced in Wordmark.tsx. Stubbier bars or
// tighter gaps than these and the ≡ reads as a hamburger menu icon.
expect(b1.width / b1.height).toBeCloseTo(5.919, 3);
expect((b2.y - (b1.y + b1.height)) / b1.height).toBeCloseTo(2, 6);
expect(b3.y - b2.y).toBeCloseTo(b2.y - b1.y, 6);
});
it('centres the mark in its own viewBox', () => {
const [b1, , b3] = BARRAS_SIMBOLO;
expect(b1.y).toBeCloseTo(64 - (b3.y + b3.height), 6);
expect(b1.x).toBeCloseTo(64 - (b1.x + b1.width), 6);
});
it('reproduces each bar exactly when drawn as a round-capped stroke', () => {
BARRAS_SIMBOLO.forEach((barra, i) => {
const linea = LINEAS_SIMBOLO[i];
// A round cap reaches half the stroke width past its endpoint, so an
// un-inset line would draw a pill a full thickness longer than the bar.
expect(linea.x1 - linea.grosor / 2).toBeCloseTo(barra.x, 6);
expect(linea.x2 + linea.grosor / 2).toBeCloseTo(barra.x + barra.width, 6);
expect(linea.y1).toBeCloseTo(barra.y + barra.height / 2, 6);
expect(linea.y2).toBeCloseTo(linea.y1, 6);
});
});
});
+1 -1
View File
@@ -13,7 +13,7 @@ export function Cierre() {
return (
<section id="contacto" className="bg-azul-profundo text-white scroll-mt-24 relative overflow-hidden">
<FondoPuntos className="top-12 -left-16 w-72 md:w-[30rem] text-dorado/25" distance={36} />
<Container className="relative py-24 md:py-32 text-center">
<Container className="relative py-16 md:py-24 text-center">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="flex flex-col items-center">
<motion.div variants={fadeInUp}><Wordmark size="lg" onDark showTagline /></motion.div>
+1 -1
View File
@@ -19,7 +19,7 @@ export function ComoEmpezar() {
const desplazamiento = useTransform(scrollYProgress, [0, 1], [20, -20]);
return (
<section className="bg-white">
<Container className="py-24 md:py-32 grid gap-12 md:grid-cols-[1fr_auto] md:items-center">
<Container className="py-16 md:py-24 grid gap-12 md:grid-cols-[1fr_auto] md:items-center">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}>
<motion.p variants={fadeInUp} className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{comoEmpezar.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} className="mt-4 font-sans font-black text-seccion text-azul-profundo max-w-xl">{comoEmpezar.title}</motion.h2>
+1 -1
View File
@@ -13,7 +13,7 @@ export function Disciplinas() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-white">
<Container className="py-24 md:py-32">
<Container className="py-16 md:py-24">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{disciplinas.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
+5 -2
View File
@@ -11,8 +11,11 @@ export function Dolores() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-white relative overflow-hidden">
<IlustracionNudo className="pointer-events-none select-none absolute top-0 left-1/2 -translate-x-1/2 w-64 md:w-[28rem] text-azul-profundo/[0.08]" />
<Container className="relative py-24 md:py-32">
{/* Smaller and a touch stronger than it was: sprawling at 8% across the
whole title it read as a smudge over the headline rather than as a
knot. Stays centred so the conductor thread runs through it. */}
<IlustracionNudo className="pointer-events-none select-none absolute top-0 left-1/2 -translate-x-1/2 w-56 md:w-80 text-azul-profundo/[0.11]" />
<Container className="relative py-16 md:py-24">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{dolores.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
+10 -5
View File
@@ -1,7 +1,6 @@
'use client';
import { motion } from 'framer-motion';
import { Container } from '@/components/primitives/Container';
import { PatronPuntos } from '@/components/primitives/PatronPuntos';
import { Wordmark } from '@/components/primitives/Wordmark';
import { ContadorNumero } from '@/components/primitives/ContadorNumero';
import { MarcoRetrato } from '@/components/primitives/MarcoRetrato';
@@ -12,16 +11,20 @@ export function Estratega() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-gris-claro">
<Container className="py-24 md:py-32">
<Container className="py-16 md:py-24">
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="grid gap-12 md:grid-cols-[auto_1fr] md:items-center">
{/* TODO dueña: real portrait. The frame is built to take it as-is —
drop an <Image fill /> in place of the placeholder below. */}
<motion.div variants={fadeInUp}>
<MarcoRetrato>
{/* Reads as a brand plate, not as a broken image. The dot grid
that used to sit behind the wordmark looked like the
checkerboard a browser shows for a picture that failed to
load; the ≡ alone, meanwhile, reads as a hamburger menu once
it stands by itself in a circle. The wordmark is neither. */}
<div className="w-full h-full bg-azul-profundo grid place-items-center" role="img" aria-label="Retrato de la estratega — pendiente">
<PatronPuntos rows={4} cols={7} className="absolute inset-0 m-auto w-[150%] h-auto text-white/15" />
<span className="relative"><Wordmark size="sm" onDark /></span>
<Wordmark size="md" onDark />
</div>
</MarcoRetrato>
</motion.div>
@@ -32,7 +35,9 @@ export function Estratega() {
<motion.h2 variants={fadeInUp} className="mt-4 font-sans font-black text-seccion text-azul-profundo">{estratega.title}</motion.h2>
<motion.div variants={fadeInUp} className="mt-6 flex items-baseline gap-3">
<ContadorNumero hasta={30} sufijo="+" className="font-sans font-black text-5xl md:text-6xl text-dorado leading-none" />
<p className="text-xs uppercase tracking-[0.2em] text-gris-medio font-semibold max-w-[8rem] leading-tight">años construyendo marcas</p>
{/* 8rem was narrower than the word "construyendo" itself, which
broke the label into three ragged lines beside the number. */}
<p className="text-xs uppercase tracking-[0.14em] text-gris-medio font-semibold max-w-[12rem] leading-snug">años construyendo marcas</p>
</motion.div>
<motion.p variants={fadeInUp} className="mt-6 text-lg text-gris-medio leading-relaxed max-w-2xl">{estratega.bio}</motion.p>
<motion.blockquote variants={fadeInUp} className="mt-6 font-sans italic text-xl md:text-2xl text-azul-profundo border-l-4 border-dorado pl-4">
+1 -1
View File
@@ -11,7 +11,7 @@ export function FraseAncla() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-azul-profundo relative overflow-hidden">
<Container className="relative py-28 md:py-40 text-center max-w-5xl">
<Container className="relative py-20 md:py-28 text-center max-w-5xl">
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="flex justify-center">
{/* Payoff for the tension the hero leaves open: here the lines do
+17 -9
View File
@@ -23,10 +23,13 @@ export function Hero({ variant = 'editorial' }: { variant?: Variant }) {
return (
<section className="bg-white relative overflow-hidden">
<FondoPuntos className="top-24 -right-16 w-72 md:w-[28rem] text-azul-profundo/10" interactivo />
{/* Kept clear of the wordmark on narrow screens: at 390px the md field
reached back across the logotype and the tagline, so the pattern read
as collision rather than texture. */}
<FondoPuntos className="top-80 -right-20 w-56 md:top-24 md:-right-16 md:w-[28rem] text-azul-profundo/10" interactivo />
<FondoPuntos className="bottom-16 -left-20 w-64 md:w-96 text-dorado/20" distance={28} rows={2} />
<Container className="relative min-h-[88vh] flex items-center py-24">
<div className="grid gap-14 lg:grid-cols-[1.1fr_0.9fr] lg:items-center w-full">
<Container className="relative min-h-[80vh] lg:min-h-[88vh] flex items-center py-16 md:py-24">
<div className="grid gap-8 lg:gap-14 lg:grid-cols-[1.1fr_0.9fr] lg:items-center w-full">
<motion.div variants={staggerContainer} initial="hidden" animate="visible" {...motionProps(reduced)}
className="min-w-0 flex flex-col items-center text-center lg:items-start lg:text-left">
<motion.div variants={fadeInUp}><Wordmark size="xl" showTagline /></motion.div>
@@ -44,16 +47,21 @@ export function Hero({ variant = 'editorial' }: { variant?: Variant }) {
]}
/>
</motion.h1>
<motion.div variants={fadeInUp} className="mt-10 flex flex-col md:flex-row items-center gap-4">
<CTA href="#contacto" label={hero.cta} variant="light" className="md:whitespace-nowrap" />
<CTA href="#mecanismo" label={hero.ctaSecundario} variant="ghost" className="md:whitespace-nowrap" />
{/* Full width while stacked: at their natural widths the two CTAs
came out ragged and unequal, which read as an accident rather
than as hierarchy. */}
<motion.div variants={fadeInUp} className="mt-10 w-full flex flex-col md:flex-row items-stretch md:items-center md:w-auto gap-4">
<CTA href="#contacto" label={hero.cta} variant="light" className="text-center md:whitespace-nowrap" />
<CTA href="#mecanismo" label={hero.ctaSecundario} variant="ghost" className="text-center md:whitespace-nowrap" />
</motion.div>
</motion.div>
{/* Complexity → focus, left unresolved: the hero poses the tension
and FraseAncla pays it off by resolving into the ≡. */}
and FraseAncla pays it off by resolving into the ≡. Shown on every
breakpoint — hidden below lg it left the phone hero with no piece
of the brand's own graphic language at all. */}
<motion.div variants={fadeInUp} initial="hidden" animate="visible" {...motionProps(reduced)}
className="hidden lg:block min-w-0">
<SecuenciaClaridad variant="hero" className="w-full max-w-md mx-auto text-azul-profundo" />
className="min-w-0">
<SecuenciaClaridad variant="hero" className="w-32 lg:w-full max-w-md mx-auto text-azul-profundo" />
</motion.div>
</div>
</Container>
+1 -1
View File
@@ -24,7 +24,7 @@ export function Mecanismo() {
>
<FondoFoto src="/images/mecanismo-bg.jpg" className="inset-0 opacity-25" wash="bg-azul-profundo/80" />
</motion.div>
<Container className="relative py-24 md:py-32">
<Container className="relative py-16 md:py-24">
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="text-xs tracking-[0.3em] uppercase font-black text-dorado">{mecanismo.eyebrow}</motion.p>
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
+1 -1
View File
@@ -10,7 +10,7 @@ export function ParaQuien() {
const reduced = usePrefersReducedMotion();
return (
<section className="bg-white relative overflow-hidden">
<Container className="relative pt-24 md:pt-32 pb-12 md:pb-16 md:flex md:items-center md:justify-between md:gap-12">
<Container className="relative pt-16 md:pt-24 pb-8 md:pb-12 md:flex md:items-center md:justify-between md:gap-12">
<motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
className="font-sans font-black text-seccion text-azul-profundo max-w-3xl">{paraQuien.title}</motion.h2>
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
+5 -2
View File
@@ -12,7 +12,10 @@ import { fadeInUp, staggerContainer, viewportOnce, usePrefersReducedMotion, moti
function Tarjeta({ n, name, desc }: { n: number; name: string; desc: string }) {
return (
<TarjetaSpotlight className="relative h-full rounded-md border-t-2 border-dorado pt-5 pl-1 pr-1 pb-1 overflow-visible">
<span aria-hidden="true" className="pointer-events-none absolute -top-2 right-0 font-sans font-black text-5xl text-azul-profundo/[0.06] leading-none select-none">
{/* An index, not a watermark. At 48px and 6% opacity these collided with
the titles and with the gold rule while carrying no contrast of their
own; sat on the rule they encode the sequence and stay out of the way. */}
<span aria-hidden="true" className="pointer-events-none absolute -top-1.5 right-0 bg-gris-claro pl-2 font-sans font-black text-xs tracking-[0.2em] text-dorado leading-none select-none">
{String(n).padStart(2, '0')}
</span>
<h3 className="relative font-sans font-bold text-xl text-azul-profundo">{name}</h3>
@@ -26,7 +29,7 @@ export function PuntosInflexion() {
return (
<section className="bg-gris-claro relative overflow-hidden">
<FondoPuntos className="-bottom-6 -right-12 w-80 md:w-[32rem] text-azul-profundo/10" distance={32} />
<Container className="relative py-24 md:py-32">
<Container className="relative py-16 md:py-24">
<div className="flex flex-col md:flex-row md:items-center md:justify-between md:gap-12">
<div>
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
+32 -9
View File
@@ -2,21 +2,44 @@
// mark reads from here — HiloConductor and SecuenciaClaridad resolve into it,
// the former with <rect>, the latter with <line> — so the symbol they produce
// is provably the same one and not a lookalike.
//
// The proportions are the Manual de Identidad Visual's, already measured and
// traced in Wordmark.tsx: bar length = 5.919 × thickness, gap = 2 × thickness,
// ends fully rounded. Three bars and two gaps make the mark exactly seven
// thicknesses tall. These are not cosmetic: with stubby bars and tight gaps the
// mark stops reading as an E — the owner's 28/jul instruction — and starts
// reading as a hamburger menu icon.
export const VIEWBOX_SIMBOLO = '0 0 64 64';
export const BARRAS_SIMBOLO = [
{ x: 13, y: 13, width: 38, height: 8 },
{ x: 13, y: 28, width: 38, height: 8 },
{ x: 13, y: 43, width: 38, height: 8 },
] as const;
const LADO = 64;
const LARGO_POR_GROSOR = 5.919;
const HUECO_POR_GROSOR = 2;
// Same three bars as strokes: a horizontal line through each bar's centre,
// with the bar's height as stroke width.
export const GROSOR_BARRA = 8;
export const RADIO_BARRA = GROSOR_BARRA / 2;
const LARGO_BARRA = GROSOR_BARRA * LARGO_POR_GROSOR;
const HUECO = GROSOR_BARRA * HUECO_POR_GROSOR;
const PASO = GROSOR_BARRA + HUECO;
const IZQUIERDA = (LADO - LARGO_BARRA) / 2;
const ARRIBA = (LADO - (3 * GROSOR_BARRA + 2 * HUECO)) / 2;
export const BARRAS_SIMBOLO = [0, 1, 2].map((i) => ({
x: IZQUIERDA,
y: ARRIBA + i * PASO,
width: LARGO_BARRA,
height: GROSOR_BARRA,
}));
// The same three bars as strokes. A round cap reaches half the stroke width
// past its endpoint, so the ends are inset by exactly that much — otherwise the
// drawn line overshoots by a full thickness the rect it has to reproduce.
export const LINEAS_SIMBOLO = BARRAS_SIMBOLO.map((b) => ({
x1: b.x,
x1: b.x + b.height / 2,
y1: b.y + b.height / 2,
x2: b.x + b.width,
x2: b.x + b.width - b.height / 2,
y2: b.y + b.height / 2,
grosor: b.height,
}));