Skip to content

Lección 33 - Motor de maquetación ​

Los navegadores implementan varios sistemas de maquetación, como Flexbox, Grid y Block, y así es fácil conseguir efectos como el «centrado» sin calcular a mano la posición de los nodos.

En las aplicaciones de tipo lienzo, que funcionan fuera del DOM, tienes que implementar la lógica de tu propio motor de maquetación. Figma ha implementado Auto Layout, donde Grid está ahora en beta, mientras que Vertical y Horizontal corresponden a la propiedad flex-direction de CSS. Para más detalles, consulta: Figma - Guía de Auto Layout

fuente: https://www.figma.com/community/file/1284819663700490015

Las herramientas centradas en pasar del diseño al código suelen ofrecer esta capacidad. Para más detalles, consulta: Maquetación en pencil.dev

Un objeto padre puede hacerse cargo del tamaño y de la posición de sus hijos con un sistema de maquetación al estilo de flexbox, mediante propiedades como layout, justifyContent y alignItems.

ts
export interface Layout {
    /** Enable flex layout. None means all children are absolutely positioned and will not be affected by layout properties. Frames default to horizontal, groups default to none. */
    layout?: 'none' | 'vertical' | 'horizontal';
    /** The gap between children in the main axis direction. Defaults to 0. */
    gap?: NumberOrVariable;
    layoutIncludeStroke?: boolean;
    /** The Inside padding along the edge of the container */
    padding?:
        | /** The inside padding to all sides */ NumberOrVariable
        | /** The inside horizontal and vertical padding */ [
              NumberOrVariable,
              NumberOrVariable,
          ]
        | /** Top, Right, Bottom, Left padding */ [
              NumberOrVariable,
              NumberOrVariable,
              NumberOrVariable,
              NumberOrVariable,
          ];
    /** Control the justify alignment of the children along the main axis. Defaults to 'start'. */
    justifyContent?:
        | 'start'
        | 'center'
        | 'end'
        | 'space_between'
        | 'space_around';
    /** Control the alignment of children along the cross axis. Defaults to 'start'. */
    alignItems?: 'start' | 'center' | 'end';
}
fuente: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Flexible_box_layout/Basic_concepts

En esta lección implementaremos maquetaciones Flexbox y admitiremos en los nodos propiedades CSS con los mismos nombres:

ts
const parent = {
    id: 'parent',
    type: 'rect',
    x: 100,
    y: 100,
    width: 200,
    height: 200,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
};
const child = {
    id: 'child',
    parentId: 'parent',
    type: 'rect',
    width: 50,
    height: 50,
};

Yoga ​

Usar el motor de maquetación Yoga en el frontend solo es posible mediante WASM. Ahora mismo hay varias implementaciones disponibles:

Conviene señalar que Yoga también se aplica al espacio 3D, siempre que se especifique un plano. Para más detalles, consulta: react-three-flex

Otra diferencia importante respecto al Flexbox del DOM es que tienes que especificar el plano del contenedor en 3D. Los elementos se colocan en el plano 2D definido por los dos ejes, con el ancho y el alto calculados a lo largo de esos dos ejes.

orientación de los ejes

pixijs/layout ​

pixijs/layout también está implementado con Yoga. Entre las implementaciones parecidas está pixi-flex-layout

ts
const container = new Container({
    layout: {
        width: '80%',
        height: '80%',
        gap: 4,
        flexWrap: 'wrap',
        justifyContent: 'center',
        alignContent: 'center',
    },
});

troika-flex-layout ​

troika-flex-layout, calculado en un WebWorker con yoga-layout-prebuilt:

ts
import { requestFlexLayout } from 'troika-flex-layout';

// Describe your layout style tree, with a unique id for each node:
const styleTree = {
    id: 'root',
    width: 100,
    height: 100,
    alignItems: 'center',
    justifyContent: 'center',
    children: [
        {
            id: 'child',
            width: '50%',
            height: '50%',
        },
    ],
};

// Initiate a layout request with a callback function:
requestFlexLayout(styleTree, (results) => {
    // The results are a mapping of node ids to layout boxes:
    // {
    //   root: { left: 0, top: 0, width: 100, height: 100 },
    //   child: { left: 25, top: 25, width: 50, height: 50 }
    // }
});

Alternativas a Yoga ​

Implementaciones en JS puro:

Implementaciones en Rust:

  • stretch implementa Flexbox y ofrece un enlace WASM stretch-layout, pero lleva mucho tiempo sin mantenimiento.
  • taffy Biblioteca de maquetación de interfaces de alto rendimiento escrita en Rust. Actualmente implementa varios algoritmos de maquetación de CSS, incluidos Flexbox, Grid y Block. Los enlaces WASM todavía no están disponibles. Para más detalles, consulta: enlaces WASM de taffy

Nuestra implementación ​

Elegimos el yoga-layout oficial. Además del Flexbox habitual, también ofrece propiedades básicas de maquetación como Margin y Padding.

ts
import { loadYoga } from 'yoga-layout/load';

class YogaSystem extends System {
    async prepare() {
        Yoga = await loadYoga();
    }
}

Hay que construir un árbol de maquetación paralelo al grafo de escena.

Primero hay que construir un árbol de maquetación paralelo al grafo de escena. La estructura de sus nodos es la siguiente:

ts
interface StyleTreeNode {
    id: string;
    top?: number | string;
    left?: number | string;
    width?: number | string;
    height?: number | string;
    children: StyleTreeNode[];
    padding?: number | number[];
    margin?: number | number[];
    gap?: number;
    rowGap?: number;
    columnGap?: number;
}

Después, en el momento adecuado, se convierte el árbol de maquetación en un árbol de Yoga, se ejecuta el cálculo de la maquetación y se aplican los resultados a los nodos del grafo de escena:

ts
const root = Yoga.Node.createWithConfig(yogaConfig);
populateNode(root, styleTree);

root.calculateLayout();
const results = Object.create(null);
walkStyleTree(styleTree, (styleNode) => {
    const { id, yogaNode } = styleNode;
    results[id] = {
        x: yogaNode.getComputedLeft(),
        y: yogaNode.getComputedTop(),
        width: yogaNode.getComputedWidth(),
        height: yogaNode.getComputedHeight(),
    };
});
root.freeRecursive();

Cuándo hacer la maquetación ​

Cuando cambia el tamaño del propio contenedor Flexbox, hay que volver a calcular la maquetación y aplicarla a la posición y al tamaño de sus hijos. Lo mismo ocurre cuando se añaden hijos nuevos al contenedor.

ts
class YogaSystem extends System {
    private readonly bounds = this.query((q) =>
        q.addedOrChanged.and.removed
            .with(ComputedBounds)
            .trackWrites.and.with(Flex),
    );

    execute() {
        this.bounds.addedOrChanged.forEach((entity) => {
            // relayout
        });
    }
}

AlignItems y JustifyContent ​

El uso más habitual es centrar el contenido con estas dos propiedades:

Para implementar botones con el texto centrado:

La etiqueta usa su ancho intrínseco (wordWrap: false), y Yoga centra dentro del botón la caja de texto medida. Un wordWrapWidth fijo con maxLines: 1 y textOverflow: 'ellipsis' puede truncar la etiqueta aunque el botón tenga espacio de sobra. Reserva esas opciones para las etiquetas que necesiten truncarse a propósito.

Espaciado ​

El ejemplo siguiente muestra el efecto de padding y gap:

ts
const parent = {
    id: 'yoga-gap-parent',
    type: 'rect',
    x: 100,
    y: 100,
    width: 200,
    height: 250,
    fill: 'grey',
    display: 'flex',
    padding: 10,
    flexWrap: 'wrap',
    gap: 10,
    zIndex: 0,
};

Base, crecimiento y contracción flex ​

Base, crecimiento y contracción flex

Flex grow acepta cualquier valor de coma flotante >= 0, y 0 es el valor predeterminado. Un contenedor reparte el espacio sobrante entre sus hijos, ponderado por el valor de flex grow de cada hijo.

Ancho y alto mínimos y máximos ​

Ancho y alto mínimos y máximos

Exportar SVG ​

El Flexbox de CSS admite elementos HTML como contenedores. Los elementos SVG quedan fuera de ese modelo, así que al exportar a SVG hay que seguir usando las posiciones absolutas que resultan de la maquetación.

ts
const layoutNodes = api.readLayoutFromECS(nodes);

(await serializeNodesToSVGElements(layoutNodes)).forEach((element) => {
    $namespace.appendChild(element);
});

Lectura adicional ​

Released under the MIT License.