Image
El nodo que enseña una imagen. Su fichero viaja dentro del envelope, así que el juego no descarga nada, y su tamaño natural es el del propio original.
primitivedesde v1sin focus
Un Image dibuja una imagen dentro de un rectángulo. Usas uno para iconos,
retratos y banners: la moneda que va al lado de un precio, el arte de un objeto
en un hueco del inventario, la cabecera del panel de una tienda. El fichero no se
descarga en tiempo de ejecución: zabloo export lo lee del disco y mete sus
bytes en el envelope, así que lo que recibe el juego es
una imagen que ya tiene. El nodo no acepta hijos, porque es un rect con píxeles
dentro y todo lo demás es layout.

- hover — inactivo
- pressed — inactivo
- focused — inactivo
- selected — inactivo
- disabled — inactivo
Se lee del último frame dibujado
Parado: pulsa Ejecutar para dibujarlo en la GPU.
import { Column, Image, Row, Text } from "@zabloo/react";
import type { ReactNode } from "react";
const BOX = { width: 132, height: 84 } as const;
const FRAME = {
background: "{color.slot}",
radius: "{radius.md}",
borderWidth: "{border.hairline}",
borderColor: "{color.line}",
} as const;
function Sample({ caption, children }: { caption: string; children: ReactNode }) {
return (
<Column layout={{ gap: "{space.2}", align: "center" }}>
{children}
<Text style={{ color: "{color.faint}", fontSize: "{text.xs}" }}>{caption}</Text>
</Column>
);
}
export default function ImageFit() {
return (
<Column
layout={{ grow: 1, justify: "center", align: "center", padding: "{space.6}" }}
style={{ background: "{color.bg}" }}
>
<Row layout={{ gap: "{space.3}", align: "start" }}>
<Sample caption="contain">
{/* The whole image, undistorted, letterboxed in the box. */}
<Image src="icons/star.png" fit="contain" layout={BOX} style={FRAME} />
</Sample>
<Sample caption="cover">
{/* Fills the rect, undistorted, cropping the overflowing axis. */}
<Image src="icons/star.png" fit="cover" layout={BOX} style={FRAME} />
</Sample>
<Sample caption="stretch">
{/* Fills it exactly, aspect ratio and all. */}
<Image src="icons/star.png" fit="stretch" layout={BOX} style={FRAME} />
</Sample>
<Sample caption="style.color">
{/* The tint multiplies per channel — absent = white = the pixels as
they are, so this needs no second image. */}
<Image
src="icons/star.png"
fit="contain"
layout={BOX}
style={{ ...FRAME, color: "{color.gold}" }}
/>
</Sample>
</Row>
</Column>
);
}{
"v": 1,
"tokens": {
"color.bg": "#0b0d13",
"color.surface": "#0e1016",
"color.raised": "#14141a",
"color.line": "#ffffff1f",
"color.line-strong": "#ffffff24",
"color.text": "#ffffff",
"color.muted": "#a1a1aa",
"color.faint": "#8a8a93",
"color.on-brand": "#ffffff",
"color.brand": "#8b5cf6",
"color.brand-strong": "#7c3aed",
"color.brand-hover": "#8b5cf6",
"color.brand-pressed": "#6d28d9",
"color.brand-soft": "#8b5cf61f",
"color.gold": "#fcd34d",
"color.slot": "#ffffff08",
"color.danger": "#f87171",
"radius.sm": 6,
"radius.md": 10,
"radius.lg": 14,
"radius.pill": 999,
"border.hairline": 1,
"border.focus": 2,
"space.1": 4,
"space.2": 8,
"space.3": 12,
"space.4": 16,
"space.5": 20,
"space.6": 24,
"text.xs": 11,
"text.sm": 13,
"text.md": 15,
"text.lg": 17,
"motion.fast": 120,
"motion.loop": 900
},
"views": {
"image-fit": {
"type": "Container",
"layout": {
"direction": "column",
"grow": 1,
"justify": "center",
"align": "center",
"padding": "{space.6}"
},
"style": {
"background": "{color.bg}"
},
"children": [
{
"type": "Container",
"layout": {
"direction": "row",
"gap": "{space.3}",
"align": "start"
},
"children": [
{
"type": "Container",
"layout": {
"direction": "column",
"gap": "{space.2}",
"align": "center"
},
"children": [
{
"type": "Image",
"layout": {
"width": 132,
"height": 84
},
"style": {
"background": "{color.slot}",
"radius": "{radius.md}",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line}"
},
"src": "asset:icons/star.png",
"fit": "contain"
},
{
"type": "Text",
"style": {
"color": "{color.faint}",
"fontSize": "{text.xs}"
},
"text": "contain"
}
]
},
{
"type": "Container",
"layout": {
"direction": "column",
"gap": "{space.2}",
"align": "center"
},
"children": [
{
"type": "Image",
"layout": {
"width": 132,
"height": 84
},
"style": {
"background": "{color.slot}",
"radius": "{radius.md}",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line}"
},
"src": "asset:icons/star.png",
"fit": "cover"
},
{
"type": "Text",
"style": {
"color": "{color.faint}",
"fontSize": "{text.xs}"
},
"text": "cover"
}
]
},
{
"type": "Container",
"layout": {
"direction": "column",
"gap": "{space.2}",
"align": "center"
},
"children": [
{
"type": "Image",
"layout": {
"width": 132,
"height": 84
},
"style": {
"background": "{color.slot}",
"radius": "{radius.md}",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line}"
},
"src": "asset:icons/star.png",
"fit": "stretch"
},
{
"type": "Text",
"style": {
"color": "{color.faint}",
"fontSize": "{text.xs}"
},
"text": "stretch"
}
]
},
{
"type": "Container",
"layout": {
"direction": "column",
"gap": "{space.2}",
"align": "center"
},
"children": [
{
"type": "Image",
"layout": {
"width": 132,
"height": 84
},
"style": {
"background": "{color.slot}",
"radius": "{radius.md}",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line}",
"color": "{color.gold}"
},
"src": "asset:icons/star.png",
"fit": "contain"
},
{
"type": "Text",
"style": {
"color": "{color.faint}",
"fontSize": "{text.xs}"
},
"text": "style.color"
}
]
}
]
}
]
}
},
"assets": {
"icons/star.png": {
"hash": "02f69f8ed48938f7704d264cd732afec0762fd8fd654944876443c87ed3cb1bd",
"mime": "image/png",
"size": 689,
"width": 64,
"height": 64,
"data": "iVBORw0KGgoA… (689 bytes, base64)"
}
}
}Las dos tablas de abajo son el caso más claro de que las dos capas son cosas de
verdad distintas. La prop que escribes es un path dentro de tu proyecto; la
prop que se publica es una referencia al envelope, reescrita por zabloo export cuando reunió los bytes. El mismo nombre, dos significados, y solo uno de
ellos llega nunca a la máquina de un jugador.
Props de autoría
Lo que escribes en @zabloo/react.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| src | string | — | Path dentro del src/assets/ del proyecto — "logo.png", "icons/coin.png". |
| fit | "contain" | "cover" | "stretch" | "contain" | Cómo llena el original el rect del layout. |
Además de estas, todo componente acepta las props base del nodo —id,
visible, disabled, layout, style, states, transition, autofocus,
clip— más variant, que el theme resuelve al exportar y que nunca aparece en
la IR.
Props de IR
Lo que llega al juego, cuando ya no queda capa de autoría.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| src | AssetRef | — | "asset:<id>" — una entrada del manifest del envelope. Nunca un binding. |
| fit | "contain" | "cover" | "stretch" | "contain" | Cómo llena el original el rect del layout. |
Una referencia a un asset se recoge al exportar, cuando los bytes se reúnen dentro del envelope — así que en tiempo de ejecución no hay nada a lo que un binding pueda apuntar. Un icono por fila elegido por los datos no se puede expresar en la IR v1; una vista que lo necesite autora sus huecos de forma estática, o el juego cambia la vista entera.
El fit
Todos los modos pintan dentro del rect. cover recorta el original a través
de sus UVs en vez de desbordarse, así que el invariante que hace honesto el
hit-testing sobre los rects del layout se cumple sin ninguna maquinaria de
recorte.
| Modo | Resultado |
|---|---|
"contain" | La imagen entera, sin deformar, centrada — con franjas. |
"cover" | Llena el rect, sin deformar, recortando por igual el eje que se sale. |
"stretch" | Llena el rect exactamente, deformando la proporción. |
El pintado
No hay props de style específicas de Image. Todo es el conjunto de estilos
de siempre, que es lo que hace que un icono sea tematizable y distinto por estado
sin coste ninguno:
style.colortiñe la imagen, multiplicando canal a canal. Ausente = blanco = los píxeles tal cual. Es el mismo “color del contenido de este nodo” que colorea los glifos, así questates.*.style.colortiñe por estado sin nada nuevo.style.radiusredondea la imagen pintada, a juego con el fondo del propio nodo.style.backgroundyborderWidthson el placeholder. Una imagen no pinta nada hasta que sus bytes están decodificados, y el layout ya ha reservado el hueco a partir delwidth/heightdel manifest.
No hay estado loading: el placeholder se escribe, no es un estado de
ejecución en el que entre el SDK.
Comportamiento
- Estados
Solo
disabled, heredado — un icono se apaga con el control al que pertenece. Dentro de unButton, los estados del propio botón pueden teñirlo: el estilo es del botón, la imagen es su contenido, así que un hover ilumina los dos sin que la imagen declare nada.- Focusable
No. Una imagen no acepta input y nada de lo que haga el jugador le llega, así que no se le aplica nada más allá de
disabled.- Tamaño
Intrínseco, a partir del
width/heightdel manifest: es una hoja con tamaño natural, comoText. Dalelayout.width/heightpara dimensionarla tú, yfitpara elegir cómo llena la imagen esa caja.- Actions
Ninguna, así que el juego no oye nada de un
Image. Una imagen que reacciona a una pulsación es unImagedentro de unButton, y la pulsación es del botón.
Degradación
En un SDK más antiguo la caja conserva su tamaño exacto y el fondo que le hayas puesto — la imagen no llega a aparecer dentro.
Como un Container vacío: es una hoja, así que lo que sobrevive es su caja, su fondo y su borde — el placeholder que escribiste, para siempre. El layout no se mueve nada, porque el hueco estaba reservado desde el manifest antes de decodificar ningún byte.
Composición
Un icono delante de una etiqueta es un Row; un botón que es solo un icono es un
Button cuyo único hijo es un Image. El nodo no opina de ninguna de las dos
cosas.
- Tamaño intrínseco — cuando el arte se dibujó al tamaño al que debe verse, que es el caso normal de los iconos.
- Dimensionada y recortada — usa
fit="cover"con una caja explícita para banners y retratos, donde la forma del hueco importa más que la imagen entera. - Un original monocromo teñido — cuando la misma forma necesite varios colores: un fichero en el envelope, todos los colores que tenga el theme, y un cambio de estado no cuesta un segundo asset.
// Intrinsic size, straight from the manifest.
<Image src="icons/coin.png" />
// Sized and cropped by you.
<Image src="banners/shop.png" fit="cover" layout={{ width: 320, height: 120 }} style={{ radius: 8 }} />
// Tinted per state: the same "color of this node's content" that colors glyphs.
<Image
src="icons/star.png"
layout={{ width: 20, height: 20 }}
style={{ color: "{color.faint}" }}
states={{ hover: { style: { color: "{color.gold}" } } }}
/>