Skip to content

Motor del diseñador

Pablo Martínez López edited this page Oct 26, 2026 · 12 revisions

Motor del diseñador

English · Español

El motor del diseñador es la parte de proxy-core::diseno que convierte un diseño efectivo en píxeles: el modelo de datos, el resolvedor del diseño efectivo y la huella de renderizado, la composición del texto con parley, el pintado de glifos con tiny-skia, los símbolos y el pintor por cara de render.rs. Las cartas de varias piezas (partidas, flip, meld…) se montan encima y se tratan en Composición del diseñador.

Para los conceptos (CardDesign, Collection, carta > colección > fábrica) lee primero Visión general del diseñador.

Superficie pública

Ruta del crate: src-tauri/crates/proxy-core/src/diseno/.

Elemento Dónde Significado
efectivo(d, coleccion) -> DisenoEfectivo efectivo.rs Resuelve carta > colección > fábrica. En las cartas de dos caras impresas (caras_impresas > 1), cada cara con FaceDesign.ajustes tiene además su propio DisenoEfectivo en caras[i]: CardDesign::de_la_cara(i) pone los AjustesCara de la cara (estilo, estilo y variante del pack, marco, emblema, opciones) en lugar de los de la carta y lo resuelve igual, así cada campo sigue cayendo a la colección y a la fábrica. DisenoEfectivo::de_cara(i) lo devuelve (o el de la carta). render y render_capa lo llaman justo tras acotar cara; huella calcula solo el diseño efectivo de esa cara (sin los ajustes de las demás, así que cambiar uno no repinta la otra, y los diseños sin ajustes propios conservan sus huellas de antes: los dos campos no se escriben si están vacíos). La app (pintar.rs) toma también el pack de de_cara(cara).frame.pack. Las maquetaciones compuestas (partidas, Secuela, flip…) son una sola cara impresa y lo ignoran.
huella(e, lang, cara, recursos) -> String efectivo.rs Clave de renderizado: SHA-256 en hexadecimal.
Motor::de_fabrica() render.rs Estado de composición tipográfica por hilo: Letras (letras) más un Maquetador. Carga las letras integradas.
Recursos { pack, simbolos, imagenes } render.rs Lo que toma prestado un renderizado: el Pack de marcos, los Simbolos y un proveedor de Imagenes.
trait Imagenes { fn imagen(&self, blob) -> Option<Arc<RgbaImage>> } render.rs El core nunca lee del disco; quien llama aporta las imágenes ya decodificadas. SinImagenes es la implementación vacía.
render(motor, e, cara, lang, rec, card, ancho_px, salida) -> Result<Render, DisenoError> componer.rs (reexportada desde render.rs) Pinta una cara impresa. Render { imagen: RgbaImage, avisos: Vec<Aviso> }.
render_capa(...) -> Option<CapaVista> componer.rs La carta sin su ilustración, sobre transparencia, más el lugar donde va la ilustración. Para arrastrar la ilustración en vivo.
Salida::{Pantalla, Impresion} render.rs Pantalla: esquinas redondeadas y transparentes. Impresión: opaca, esquinas rectas (la guillotina las redondea; el sangrado repite el borde).
reducida(img, lado) render.rs Reduce una ilustración a un máximo de lado px en el lado largo (la imagen que mueve la vista previa en vivo).
Letras::{nuevas, cargar, cargar_emoji, respaldos, cargar_cjk, tiene, tiene_escritura} letras.rs Catálogo de letras; cargar_emoji registra la letra de emoji en color como la familia genérica de emoji; respaldos y cargar_cjk registran respaldos por escritura (véase más abajo).
textos_motor, tipos_idiomas textos_motor.rs, tipos_idiomas.rs Palabras fijas que pinta el motor y líneas de tipo sin red, por idioma de diseño (véase la sección «Idiomas de diseño»).
Simbolos, SimbolosPropios simbolos.rs SVG de símbolos de Scryfall y símbolos propios del pack.

Dependencias (src-tauri/crates/proxy-core/Cargo.toml): parley (maquetación/composición), skrifa (contornos de glifos), tiny-skia (rasterizador), resvg (SVG: marcos y símbolos), fast_image_resize (Lanczos3 y reescalado de píxeles), image, regex, sha2.

La tubería de un vistazo

flowchart TD
  A["efectivo(): DisenoEfectivo"] --> B["componer::render: elegir Maquetacion + Cuerpo"]
  B --> C["pintar_cara (render.rs): una cara en un Lienzo"]
  C --> C1["elección de color: colores_marco + Tonos + vars"]
  C1 --> C2["ajuste del texto de reglas: cuerpos::ajustar, caja dinámica"]
  C2 --> C3["capas fondo, ilustración, ventana_svg"]
  C3 --> C4["capas del pack: capas con condiciones si"]
  C4 --> C5["textos: coste, título, tipo, etiqueta, emblema, reglas, F/R, pie"]
  C5 --> D["máscara de esquinas, de_pixmap: RgbaImage + avisos"]
Loading

modelo.rs: el contrato

Véase Visión general del diseñador para la tabla de tipos. Detalles relevantes para el motor:

  • FineTuning::limitado() lo limita todo: desplazamientos a +-AJUSTE_MAX_MM (3 mm); rules_box_opacity a 0..1; art_radius a 0..20 mm; card_radius a 0..6 mm; art_fade a 0..DIFUMINADO_MAX_MM (25 mm); art_fade_extension a 0..EXTENSION_DIFUMINADO_MAX_MM (35 mm); rules_box_height a +-ALTO_CAJA_MAX_MM (10 mm); tamaños de texto (y cost_size, los símbolos de maná, que comparten el rango) a TAMANO_TEXTO_MIN..MAX (0,5..1,6); interlineado a INTERLINEADO_MIN..MAX (0,7..1,6). efectivo lo aplica siempre.
  • Constantes del estrechado del título: ESTRECHADO_DEFECTO 0,65, rango ESTRECHADO_MIN 0,55 a ESTRECHADO_MAX 0,75, ESTRECHADO_BASE_DEFECTO 1,0.
  • Variantes de Aviso: SinTextoOficial, TextoNoCabe { hueco, sobra }, TituloReducido { hueco }, ReglasEditadas, FaltaNombre, LetraNoEncontrada, FaltanSimbolos, PackSinLayout, EstiloNoEnElPack, LayoutDesconocido, ColeccionPerdida, SinCinta, FaltanLetras { lang }.
  • Los valores por idioma (DesignLang, ByLang<T>, FaceData.printed_names, CardData.sin_texto) se describen en la sección «Idiomas de diseño».
  • Tests: modelo.rs hace ida y vuelta de un diseño de dos caras, comprueba que el mazo de 108 cartas se queda por debajo de 2 MB de JSON (peso_de_un_deck_disenado), comprueba que un JSON de DeckCard anterior a la 2.0 se sigue leyendo sin la clave design (una_carta_1x_se_lee_igual_y_no_escribe_design) y lleva los tests de compatibilidad de idiomas listados más abajo.

borrador.rs (core): el texto antes de pintar

No confundir con src-tauri/src/diseno/borrador.rs (el constructor de la app que pasa de Scryfall a CardData, véase Capa de aplicación del diseñador). El del core es lógica de texto pura:

  • reglas_efectivas(d, cara, lang): el texto de reglas que se pinta. Un rules_override escrito a mano manda, pero el nombre original de la carta se sigue sustituyendo por el temático.
  • sustituir_nombre(texto, originales, propio, espanol): sustituye los nombres originales (primero los nombres completos, del más largo al más corto, luego sus formas cortas, como la parte anterior a la coma) por el nombre temático. Solo palabras enteras (cambiar_palabra). Usa caracteres marcadores de uso privado para que un nombre ya sustituido nunca se sustituya dos veces. En español, quitar_articulos quita el artículo delante del nombre sustituido (del pasa a de, al pasa a a, se eliminan el/la/los/las) porque el nombre temático suele ser el de un personaje, no el de un objeto.
  • reglas_efectivas busca los nombres originales que sustituir en el nombre de la cara, en el de cada cara y en el impreso en ese idioma (FaceData::printed_name(lang)); la regla de los artículos solo se aplica con DesignLang::Es.
  • nombre_efectivo: el nombre temático de ese idioma (FaceDesign::textos(lang)); si no, el impreso en ese idioma (FaceData::printed_name(lang)); si no, el inglés. nombre_original: todos los nombres de las caras unidos con //.
  • tipo_en_espanol(en) (con la gramática española) y tipo_traducido(en, lang) (primero tipos_idiomas::tipo_en, que también cubre el español, y después la gramática española; None en inglés o si no se saben los tipos) montan una línea de tipo sin red. Un subtipo que no se sabe se queda en inglés («Creature — Eldrazi» pasa a «Criatura — Eldrazi»), porque una línea casi traducida es mejor que una entera en inglés. arreglar_tipos solo escribe una línea que difiere de la inglesa. arreglar_tipos(card, lang) rellena el tipo de las caras que aún lo traen en inglés (diseños guardados antes de saber traducirlo); arreglar_tipos_es es el atajo en español.
  • siguiente_numero / numero: numeración del pie, 0001... para cartas y T001... para fichas, por encima del más alto que ya haya en el mazo.

letras.rs: letras sin el sistema

Letras envuelve un parley::FontContext construido con CollectionOptions { shared: false, system_fonts: false }. Solo existen las letras que se pasan a cargar(Vec<u8>) (TTF/OTF, incluidas las variables). Motivo: una carta debe verse igual en todas las máquinas, y una letra del sistema que falte nunca debe cambiar la maquetación en silencio.

  • Letras integradas (LETRAS_DE_FABRICA en render.rs, con include_bytes! desde assets/fuentes/): Spectral Regular/Italic/SemiBold/Bold y Montserrat (variable). Sus licencias están al lado (Spectral-LICENSE.txt, Montserrat-LICENSE.txt).
  • Letra de emoji (LETRA_DE_EMOJI en render.rs): Noto Color Emoji en COLRv1 (assets/fuentes/Noto-COLRv1.ttf, el fichero 2D/fonts/Noto-COLRv1.ttf de googlefonts/noto-emoji en un commit fijo cuyo SHA-256 se anota junto a la constante, unos 5 MB; OFL 1.1, licencia en Noto-COLRv1-LICENSE.txt). Motor::de_fabrica la carga con Letras::cargar_emoji, que la deja como GenericFamily::Emoji. parley ya pide esa familia cuando un clúster es un emoji (secuencias ZWJ, tonos de piel, banderas, U+FE0F), después de la familia del texto, así que ninguna letra del texto tiene que traer emoji y una carta nunca muestra un recuadro vacío en su lugar. Va incluida (no se descarga) porque un emoji puede aparecer en cualquier texto de cualquier carta. Caladea está en assets/fuentes/ solo como fixture de tests (tests de letras.rs y recursos.rs).
  • Respaldo de latín y cirílico. Motor::de_fabrica llama a Letras::respaldos(&["Spectral", "Montserrat"]), que añade esas familias como respaldos de fontique para las escrituras Latn y Cyrl. Una letra de un pack o importada sin «ș», «ł» o el alfabeto cirílico toma solo esos caracteres de las letras de la casa en vez de pintar cuadrados; lo que la letra pedida sí tiene no se toca. Las de la casa cubren el latín de todos los idiomas de la app y el cirílico ruso y ucraniano.
  • Letras CJK. El japonés, el coreano y el chino necesitan letras demasiado grandes para incluirlas (Noto Sans JP/KR/SC/TC, que baja la app; véase Capa de aplicación del diseñador). Letras::cargar_cjk(datos, escritura) registra una como respaldo de sus escrituras con su idioma (ja, ko, zh-Hans, zh-Hant) y también sin idioma, así el mismo ideograma sale con la forma japonesa o china según el locale del texto, y un ideograma suelto en una carta de escritura latina sigue teniendo letra. tiene_escritura(e) es siempre cierto para Escritura::Latina/Cirilica y para una escritura CJK en cuanto su letra está cargada; si es falso, el pintor da Aviso::FaltanLetras { lang }.
  • Las letras importadas y descargadas las carga la app en el Motor de cada hilo (Recursos::con_motor en src-tauri/src/diseno/recursos.rs, que se reconstruye cuando cambia el contador de «generación» de letras). Los ids de letra son el SHA-256 del fichero. Véase Packs de marcos para el catálogo OFL descargable.
  • Una colección puede pedir otra familia para el título o las reglas (font_title, font_rules). Si no está cargada, Carta::letra_sin_peso recurre a la letra del pack y registra Aviso::LetraNoEncontrada.

texto.rs: texto enriquecido y maquetación

  1. Análisis a Parrafos (Vec<Trozo>, Trozo::Texto { texto, cursiva } o Trozo::Simbolo):
    • reglas(texto): un párrafo por línea. El texto entre paréntesis va en cursiva (texto recordatorio); {X} se convierte en símbolo; [[word]] marca un tramo resaltado; la «palabra de habilidad» corta antes de — al principio de una línea (como mucho 4 palabras, sin { : ( . , •) pasa a cursiva y con color (palabra_de_habilidad).
    • sabor(texto): texto de ambientación, todo en cursiva.
    • con_simbolos(texto): solo símbolos (línea de tipo, pie). llano(texto): texto plano.
    • Los tramos de resaltado y de palabra de habilidad usan marcadores de uso privado (RESALTE, HABILIDAD), que se quitan en maquetar y se pintan con los colores coloresTexto del pack si los tiene; si no, los marcadores desaparecen y el texto sale del color normal.
  2. Maquetador::maquetar(letras, parrafo, estilo, ancho) maqueta un párrafo con parley: familia/tamaño/grosor/altura de línea por defecto, numerales alineados forzados ("lnum", más "smcp" para versalitas), cursivas como rangos, tramos de color, luego break_all_lines(ancho) y align. ancho = None significa una sola línea.
    • Los símbolos son cajas en línea de lado estilo.tam * LADO_SIMBOLO (0,80), con hueco HUECO_SIMBOLO (0,05), bajadas BAJA_SIMBOLO (0,09) por debajo de la línea base. Los símbolos consecutivos ({2}{U}{U}:) forman una sola caja con la puntuación final (: , . ; )) unida como un Bloque anidado (Grupo.cola), de modo que un coste nunca se parte entre líneas.
    • Maquetador.locale (Option<parley::Language>) se pasa a parley como idioma del texto: elige entre las formas japonesas o chinas de los glifos y las formas locales. componer::preparar lo pone desde DesignLang::locale() solo en los idiomas que no son el español ni el inglés; en esos dos se queda en None, así su maquetación (y sus píxeles) siguen exactamente como antes de haber más idiomas.
    • Maquetador.sustituir es una lista de pares (carácter, reemplazo) tomada del mapa sustituir del pack (componer::preparar), para los caracteres que a la letra de un pack le faltan.
  3. Estilo (familia, grosor, tamaño en px, interlinea, color, Alinear, versalitas, resalte, habilidad) es el estilo listo para pintar. Bloque es un párrafo ya maquetado (ancho, alto, base, más ultima_linea_ancho()).
  4. Ajuste:
    • linea_ajustada_con_base (título y línea de tipo): véase Estrechado del título y de la línea de tipo más abajo.
    • caja_de_reglas: apila los párrafos de reglas y el texto de ambientación con Separaciones { parrafo: 0.32, sabor: 0.75 } (múltiplos del tamaño de letra) y anota dónde va la línea divisoria entre reglas y ambientación.
    • ajustar_reglas: si la caja es demasiado alta, reduce el tamaño de letra con búsqueda binaria en pasos de 0,1 pt (paso) hasta tam_min; en el mínimo aprieta la separación (parrafo 0.12, sabor 0.45) y, si aun así no cabe, devuelve el exceso en Ajuste.sobra.

pintar.rs: de glifos a píxeles

pintar_bloque(lienzo, bloque, x, y, pincel, simbolos) recorre las líneas de parley. Para cada tramo de glifos obtiene el contorno con skrifa sin hinting (funciona con letras variables mediante coordenadas normalizadas), acumula un camino de tiny_skia por tramo con el lápiz de contornos Lapiz y lo rellena con antialiasing. Detalles:

  • Pincel { escala_x, color }: escala_x es el factor de estrechado horizontal (el texto se condensa escalando x, no cambiando de letra); color sustituye el color del texto (se usa para la sombra).
  • La cursiva sintética (synthesis().skew()) se aplica como una inclinación en el lápiz; la negrita sintética dibuja un trazo extra de 0.035 * size.
  • Las cajas en línea pintan sus símbolos mediante Simbolos::pintar; en modo sombra un símbolo se convierte en un círculo del color de la sombra. Los símbolos que faltan se devuelven como códigos (pintados en gris) y salen como Aviso::FaltanSimbolos.
  • pintar_caja pinta una caja de reglas y el divisor de la ambientación; tiene_glifo(datos, c) comprueba si una letra tiene un carácter.
  • Glifos en color (emoji): antes de tomar un contorno, cada glifo se ofrece a emoji::pintar_glifo, que pide a skrifa sus capas de color (color_glyphs) y las pinta en su sitio con un ColorPainter (Pintor en emoji.rs). Usa la misma línea base, tamaño y escala_x que el texto. En modo sombra (Pincel.color) todas las capas toman el color de la sombra.
    • Pinceles: colores sólidos de la paleta 0 de CPAL (0xFFFF = el color del texto) y degradados lineales, radiales y cónicos como shaders LinearGradient / RadialGradient / SweepGradient de tiny_skia. Casi todos los emoji de Noto llevan degradado. Un degradado que tiny-skia no puede construir (degenerado, radio negativo) cae al color medio de sus paradas (medio).
    • Capas compuestas: push_layer abre un Pixmap aparte del tamaño del lienzo; los rellenos van a la de arriba; pop_layer la funde sobre la de debajo (o el lienzo) con el BlendMode que corresponde al CompositeMode de COLR (fusion).
    • Transformación del recorte y del pincel: un recorte de glifo (Recorte::Glifo) guarda la transformación que había al abrirlo. Noto mete después otra transformación para el degradado, así que el relleno pinta el contorno con la transformación del recorte y al shader solo le da la diferencia (la actual respecto a la del recorte). Usar la actual para el contorno lo movería.
    • es_emoji(c): si la letra de emoji tiene c (y c >= U+2190; por debajo, cualquier letra de texto lo trae). Lo usa la huella del render (véase la sección «Huella del render y emoji»).

simbolos.rs: símbolos de maná

Simbolos guarda usvg::Trees ya analizados por código ("T", "2/U", "W/P"), con la clave codigo() (sin llaves, en mayúsculas). Los SVG de Scryfall los descarga y los guarda la app (comando symbols_status, se refrescan tras 30 días, CADUCA_SIMBOLOS); véase Capa de aplicación del diseñador. Simbolos::nuevos() incluye siempre dos iconos de pie integrados de assets/iconos/: {PINCEL} (pincel) y {ESTRELLA} (estrella). El texto del pie asigna los caracteres ★ y ✎ a ellos.

SimbolosPropios es la capa que aporta el pack (SVG o PNG por código, con el indicador pixel para escalar por vecino más próximo). Simbolos::con_propios(Arc<SimbolosPropios>) devuelve una vista donde los símbolos propios tapan a los de Scryfall; los códigos que el pack no tiene pasan a Scryfall. Los símbolos PNG se ajustan sin deformarse, centrados en el cuadrado.

render.rs: pintar una cara

pintar_cara(motor, e, lang, rec, estilo, avisos, opciones) -> Pieza pinta una cara en un Lienzo (un tiny_skia::Pixmap más los factores mm a px kx, ky y el tamaño del lienzo en mm). Todo se especifica en milímetros sobre una carta de 63 x 88 (o el tamano propio del estilo); el lienzo convierte. Otro tamaño de carta del mazo (63,5 x 88,9 o 65 x 90 mm) solo cambia el lienzo en píxeles, así que la geometría de 63 x 88 se estira a él (kx y ky difieren en menos de un 1 %). La misma función pinta una carta normal, la mitad de una carta partida, la tira de secuela, un plano apaisado… porque lo que dibujar lo dicen Opciones (cara, cuerpo, transparente, ancho_px, alto_px, indicador, otra_cara, sin_pie, sin_arte, aventura) y el EstiloDef que se le pasa.

Paso a paso

  1. Lienzo. Lienzo::nuevo(w_mm, h_mm, ancho_px, alto_px). Se rellena de negro opaco salvo que opciones.transparente.
  2. Elección de color. colores_marco → eleccion_marco:
    • FrameColor::Palette / Gradient / Custom se toman tal cual.
    • Auto: un color de carta da su paleta; dos dan un degradado de los dos (en orden); tres o más dan gold; ninguno da artifact (el tipo contiene Artifact) o colorless. Una tierra incolora usa su identidad de color si la tiene (así una tierra dual sale coloreada); si no, la paleta land. Los colores de la cara mandan sobre los de la carta.
    • Las paletas (PaletaDef) se convierten en Tonos { claro, medio, oscuro, tinte, caja, tinta }; un color propio se convierte en tonos aclarándolo/oscureciéndolo; un degradado mezcla los dos (Tonos::mezcla). Una paleta que falta en el pack se renderiza gris.
  3. Modo de la caja de reglas. Según FrameOptions.rules_box (Auto usa la cajaPorDefecto del estilo: Tinte = tinte del color al 0,80 de opacidad, Clara = caja clara al 1,0; Dark = casi negro 0,85; Light = crema 0,92; Tint{hex} = 0,80 con un hex de 6 dígitos; un #rrggbbaa de 8 dígitos usa su alfa como opacidad). El ajuste fino rules_box_opacity empuja la opacidad hacia 1. La tinta del texto (tintaCaja) es blanca o la tinta de la paleta según la luminancia percibida de la caja.
  4. Variables SVG. Un BTreeMap<String,String> (vars) con defs (los degradados) y los colores listados en Packs de marcos, ampliado después con la geometría de la caja dinámica.
  5. Primero el texto de reglas. El cuerpo se maqueta antes de pintar nada porque la caja de reglas, al crecer, mueve la barra del título y el borde de la ilustración:
    • contenido_de_la_cara → un Contenido (texto plano, secciones con insignias o columnas de aventura; véase Composición del diseñador).
    • Caja dinámica (el estilo tiene caja): se prueba la altura «cómoda» (alto_comodo) dejando que la letra se reduzca hasta el 75 % (LETRA_COMODA) salvo que el usuario haya fijado un tamaño de reglas; si no cabe, se usa alto_max con la letra completa y se reduce lo necesario hasta un suelo de 6 pt. Si la última línea chocaría con la caja de fuerza/resistencia, se sube el relleno inferior y se rehace el ajuste. La geometría resultante pasa a ser variables numéricas para las expresiones del pack (yReglas, altoReglas, yTipo) y variables SVG (yReglas, altoReglas, yTipo, yTipo04). Trampa conocida: yReglas / altoReglas significan el área de texto en las expresiones de hueco (nums) pero la caja entera en las vars de SVG.
    • Caja fija (sin caja): se ajusta dentro del hueco reglas (menos la tira de MDFC); el texto se centra verticalmente (alinearV puede cambiarlo).
    • Un desbordamiento en el suelo lanza Aviso::TextoNoCabe { hueco: "reglas", sobra } (mm).
  6. Capas de fondo (EstiloDef.fondo): se pintan bajo la ilustración. Se omiten con sin_arte (capa de arrastre en vivo), porque taparían la ilustración que la interfaz coloca debajo.
  7. Ilustración. pintar_arte(l, img, hueco, framing):
    • Sin rotación: un recorte en un paso más reescalado Lanczos3 (reescalar con ResizeOptions::crop). Zoom >= 1; los desplazamientos en [-1, 1] mueven dentro del margen sobrante de cada eje.
    • Con rotación: se escala hasta cubrir el rectángulo rotado y luego se dibuja con una transformación de rotación bicúbica.
    • Sin imagen: un degradado oscuro neutro (sin_arte), salvo que el estilo tenga capas fondo. El hueco puede usar variables de la caja para que la ilustración llegue hasta la barra de tipo.
    • El resultado se devuelve a quien llama como Pieza.arte (fracciones del lienzo) para render_capa.
  8. Ventana de la ilustración (ventanaArte): ventana_svg pinta de negro fuera de la ventana, con esquinas redondeadas (radio del estilo, o FineTuning.art_radius). Justo antes, el difuminado de la ilustración (FineTuning.art_fade o el difuminadoArte del estilo, más la extensión) se pinta encima de la ilustración.
  9. Capas del pack (Carta::capas). Cada Capa se omite salvo que su condición si se cumpla para los Hechos (corona/fr/cinta/emblema se fijan aquí, el resto sale de hechos_de). Después:
    • Las capas cajaReglas pintan el tinte de caja pedido en su zona (tinte_caja) y nada más;
    • Las capas PNG (capa_png) resuelven {color}/{color1}/{color2} en la ruta (Capa::png_para), evalúan las expresiones de zona con las variables de la caja (una zona que referencia una variable que la carta no tiene se omite) y pintan con png_trozo (estirado completo; con desde, solo ese trozo del PNG, en su mismo sitio salvo que en diga otro; con mascara, recortado por el alfa de ese PNG) o png_en_tramos (tramos: las franjas superior e inferior conservan las proporciones y solo se estira la del medio); la mezcla (mezcla) y la opacidad se aplican mediante pintura_con. El resultado a su medida lo guarda imagenes_png::trozo (clave: huella del pack, ruta, trozo, máscara, medida y escalado), así que arrastrar el arte no repite el recorte ni la máscara; los efectos de color del marco (degradado, tinte propio) se aplican igual al trozo;
    • Las capas SVG se rellenan (rellenar) con las variables y se renderizan con resvg sobre todo el lienzo (Lienzo::svg). Una capa que no se encuentra en el pack recurre a los ficheros del pack integrado.
    • Si el estilo tiene ventanaArte.repintar y el usuario fijó art_radius, las capas se dividen en repintarTras: tras el primer grupo, repintar_ventana vuelve a pintar la ilustración (o negro) sobre las zonas donde la imagen del marco dibujó una curva ya cocida, y luego siguen las demás capas. Las zonas se recortan para empezar bajo la barra de tipo (la variable yReglas), porque con una caja dinámica corta la barra queda más abajo que la y fija de las zonas.
  10. Textos, en este orden: coste (coste: símbolos alineados a la derecha, con un círculo oscuro detrás de cada uno salvo que el pack tenga símbolos propios; el ancho del título se reduce para quedarse a la izquierda del coste cuando ambos están en la misma fila), indicador de cara (sol/luna/triángulos para transformables y MDFC, empuja el título a la derecha), título, cinta con el nombre original (cinta), línea de tipo (ancho reducido por la etiqueta), etiqueta (etiqueta: rareza/legendaria/tipo de tierra básica), emblema (imagen, ajustado sin deformarse), símbolo de color de fullArt, contenido de reglas (pintar_contenido) o salas de mazmorra (pintar_mazmorra), mano/vida de Vanguard, tira de la otra cara del MDFC (franja_otra_cara), fuerza/resistencia (o lealtad, o defensa; fr_texto), pie (pie).
  11. Avisos (falta el nombre, reglas editadas a mano, faltan las letras de la escritura, sin texto oficial en el idioma de diseño) se añaden y se devuelve la Pieza.

Después, componer::render elimina los avisos duplicados, añade FaltanSimbolos (excluyendo los iconos integrados) y aplica la máscara de esquinas: FineTuning.card_radius cuando está fijado (pantalla e impresión), si no el radio del estilo en pantalla y recto en impresión. de_pixmap des-premultiplica a RgbaImage.

Estrechado del título y de la línea de tipo

Carta::linea(nombre, parrafo, ancho_max, squeeze) → linea_ajustada_con_base:

  1. Se maqueta el texto en una línea. La línea se estrecha siempre al menos hasta squeeze_base (el «estrechado mínimo» de la colección/carta, 1,0 = nada, solo para los huecos titulo y tipo).
  2. Si aun así no cabe, se estrecha horizontalmente (Pincel.escala_x) hasta squeeze_min (el límite, 55-75 %).
  3. Si ni así cabe, se reduce el tamaño de letra lo justo y se informa Aviso::TituloReducido { hueco }.

Motivo de la decisión: condensar un poco los glifos conserva el tamaño de letra del pack y el aspecto de la carta; reducir es el último recurso. Los textos auxiliares (texto_centrado, texto_izquierda, usados por las insignias y la tira de MDFC) usan un límite fijo de 0,6.

Ajustes finos (FineTuning)

Se aplican dentro de Carta::hueco, escala_texto, estilo_texto, pintar_cara y pie:

Campo Efecto
title, cost, type_line, emblem, pt, footer (mm) Se suman a la y del hueco (Carta::hueco).
rules (mm) Se suma cuando se pinta el contenido de reglas (el hueco también mide la caja, así que no se mueve en hueco).
title_size, cost_size, type_size, rules_size, pt_size, footer_size Multiplican el tamaño de letra de ese rol (escala_texto; la cinta sigue a title_size). cost_size escala el lado de los símbolos de maná en Carta::coste: siguen pegados a la derecha y centrados en la altura del hueco, y el título gana el sitio que liberan (fin_coste). Con rules_size fijado se omite la reducción de altura cómoda: la caja crece lo que haga falta y la letra solo baja si ni la caja máxima basta.
rules_line_spacing, footer_line_spacing Multiplican la altura de línea / el paso de fila del pie.
rules_box_opacity Véase el paso 3.
art_radius, card_radius Radios de esquina (mm), véanse el paso 8 y la máscara de esquinas.
art_fade (mm) Funde con negro el borde de abajo de la ilustración (difuminar_abajo, pintado encima de la ilustración y debajo de las capas del pack: suave al empezar y al acabar, y negro del todo 1,5 mm antes del final porque el marco suele tapar ese filo). None usa el difuminadoArte del estilo (el extendido de la casa: 12 mm con 7 mm de extensión); 0 lo apaga.
art_fade_extension (mm) Cuánto sigue la ilustración por debajo de su ventana, perdiéndose (la variable extensionArte, así que el negro del marco empieza ese tanto más abajo; el difuminado acaba ahí). Con un art_fade propio, la extensión es la propia (o ninguna); sin él, la propia si está fijada y, si no, la extensionDifuminado del estilo.
rules_box_height (mm) Sube (positivo) o baja (negativo) el borde de arriba de la caja de reglas; llega a los packs como la variable subeCaja. Caja dinámica: alto_max y alto_comodo crecen lo positivo, la altura pedida es la natural más el desplazamiento y, con un valor negativo, el texto se vuelve a encajar en la caja más baja (con la letra más pequeña si hace falta, nunca por debajo del relleno + 3 mm). Los marcos fijos (extendido y clásico de la casa) mueven con subeCaja su ilustración, su línea de tipo, su emblema y el borde de su caja. Los marcos de imagen lo ignoran, salvo los que declaran caja (el pack Original, ver Packs-de-marcos); Balatro lo ignora.
rules_box_dynamic None = como el marco: dinámica en el despejado de la casa, fija en el extendido y el clásico de la casa y en los tres estilos del Original (CajaDinamica.fija_de_serie). Some(true): dinámica en todos; en un marco fijo que usa subeCaja la caja se acorta sola cuando el texto es corto (recorte, como mucho ALTO_CAJA_MAX_MM, 10 mm) escribiendo un subeCaja negativo, reevaluando el hueco reglas y reencajando el texto (se deshace si la caja no encogió de verdad). Some(false): la caja se queda siempre en alto_max (se salta el ajuste a la altura natural y se ignora alto_comodo) y el texto de reglas se centra en vertical en ella, como en las cajas fijas (caja_creciente_fija en pintar_cara; una caja que crece a su altura natural deja el texto arriba). Balatro no cambia.

Teñido de color para marcos de imagen (ExtraMarco)

Los packs basados en imágenes (Original, Balatro) tienen un PNG por color en lugar de un SVG con degradado. Solo para las capas cuya ruta contiene {color}:

  • ExtraMarco::Degradado(b): se pinta el PNG de la primera paleta y después el de la segunda con fundir_a_la_derecha (rampa alfa del 35 % al 65 % del ancho de la carta).
  • ExtraMarco::Tinte(color): se toma el PNG neutro (colorless) y se le aplica tenir, una mezcla tipo overlay guiada por la luminancia que conserva luces y sombras.
  • ExtraMarco::Nada: el PNG de la paleta elegida. Las cartas de dos colores en Auto se quedan en gold (clave_marco).

Cachés de PNG

render::imagenes_png mantiene dos listas LRU de todo el proceso (ORIGINALES: PNG decodificados; A_MEDIDA: reescalados al tamaño exacto en píxeles), cada una con un tope de 160 MiB de RGBA. Las claves incluyen Pack.huella, así que un pack modificado nunca reutiliza píxeles viejos. Repintar el mismo marco mientras se arrastra la ilustración cuesta, por tanto, solo la ilustración.

Caja de reglas y ayudas de texto secundarias

Carta expone texto_centrado, texto_izquierda, simbolos_en_fila y forma (un fragmento SVG en milímetros que comparte los degradados de la carta). cuerpos.rs los usa para dibujar las insignias (Composición del diseñador).

Avisos que produce el motor

Aviso Lo lanza
TextoNoCabe el texto de reglas desborda en el suelo de 6 pt
TituloReducido el título/tipo tuvo que reducirse tras el estrechado
FaltaNombre no hay nombre temático para la cara y el idioma
ReglasEditadas hay rules_override
SinTextoOficial el idioma de diseño no tiene impresión (CardData::sin_texto_en(lang): el español por sin_texto_es, los demás por sin_texto; siempre en rumano, polaco, neerlandés y ucraniano, que Scryfall no imprime); las reglas son el Oracle en inglés
FaltanLetras { lang } la escritura del idioma no tiene letra cargada (Letras::tiene_escritura falso: japonés, coreano o chino antes de bajar su letra)
LetraNoEncontrada la letra de la colección no está cargada
FaltanSimbolos código de símbolo sin SVG/PNG
SinCinta se pidió el nombre original en la cinta y el estilo no tiene hueco cinta
PackSinLayout, EstiloNoEnElPack, LayoutDesconocido véanse Composición del diseñador y Packs de marcos
ColeccionPerdida lo añade la app, no el motor

Idiomas de diseño

Un diseño se puede pintar en cualquiera de 15 idiomas. Los códigos son los de Scryfall y los mismos que los de la interfaz (src/lib/idiomas.ts), así el idioma de la pantalla, el de diseño y el de un mazo comparten claves.

Elemento Dónde Significado
DesignLang modelo.rs Es En Fr De It Pt Ro Pl Nl Ru Uk Ja Ko Zhs Zht. TODOS los lista en ese orden, que también decide el orden de las claves al guardar un ByLang y no se cambia. codigo() / de_codigo() (código de Scryfall), impresas() (falso en Ro, Pl, Nl, Uk: Scryfall no tiene cartas en ellos y las reglas salen del Oracle en inglés), escritura() → Escritura (Latina, Cirilica, Japonesa, Coreana, ChinaSimplificada, ChinaTradicional: qué letras hacen falta), locale() (BCP-47, p. ej. zh-Hant-TW).
ByLang<T> modelo.rs es y en siempre, más otros: BTreeMap<DesignLang, T> aplanado en el mismo objeto. Se guarda como { "es": …, "en": …, "fr": … }: es y en primero y siempre (la 2.5.x los exige e ignora el resto), los demás solo si existen, así un diseño que solo usa español e inglés se guarda byte a byte como antes. El Deserialize está escrito a mano y es tolerante: sin es/en, valores por defecto; un código desconocido (de una versión más nueva) se ignora. get(lang) cae al en; get_mut(lang) crea la entrada; tiene(lang) dice si tiene valor propio. ts-rs no sabe aplanar un mapa genérico, así que otros va con #[ts(skip)] y la pantalla lee y escribe las claves de más con src/lib/por-idioma.ts.
FaceDesign::textos(lang) modelo.rs Los textos de la cara en lang. Los idiomas que no son el español ni el inglés no caen a los textos ingleses: sin textos propios, el nombre es el impreso y las reglas las oficiales (la app copia el nombre temático a un idioma nuevo al elegirlo). reglas_a_mano(): algún idioma tiene rules_override.
FaceData.printed_names modelo.rs Nombre impreso por código en los demás idiomas (printed_name_es se queda para el español, así lo sigue leyendo la 2.5.x; el inglés es name). Se lee y escribe con printed_name(lang) / poner_printed_name(lang, nombre). Vacío no se guarda.
CardData.sin_texto modelo.rs Códigos de los demás idiomas sin impresión (sin_texto_es se queda para el español). sin_texto_en(lang), poner_sin_texto(lang, sin) (ordenado), tiene_idioma(lang) (es/en siempre; si no, se sabe que no hay impresión o todas las caras tienen reglas en ese idioma).

Textos del motor que no son datos de carta:

  • textos_motor.rs: palabra_nivel(lang) (la palabra de los niveles de un leveler, «NIVEL», «STUFE», «等级»…), palabras_nivel() (todas, para reconocer los rangos en cualquier idioma), recordatorio_fusionar(lang) / palabra_fusionar(lang) / es_linea_fusionar(linea) (la palabra clave Fusionar y su recordatorio) y codigo_pie(lang) (el código {lang} de la plantilla del pie). En los idiomas con cartas impresas, las palabras salen de cartas reales en Scryfall (anotadas al lado); el rumano, el polaco, el neerlandés, el ucraniano y el Fusionar coreano no tienen fuente oficial. Los códigos del pie se comprobaron en cartas reales (FR, DE, IT, JP, KR, CS, CT, RU); el español sigue con ES aunque las cartas españolas imprimen SP, porque cambiarlo cambiaría todas las cartas españolas ya hechas.
  • tipos_idiomas.rs: líneas de tipo sin red para todos los idiomas de diseño menos el inglés (tipo_en(en, lang)). La línea en inglés siempre tiene un vocabulario cerrado (los catálogos de Scryfall: 7 supertipos, 19 tipos de carta y unos 500 subtipos), así que los datos son una tabla fija, no una petición. Tiene dos capas. (1) assets/tipos-impresos.json, generado por scripts/tipos-impresos.mjs: por cada idioma con cartas impresas, la parte de tipos entera en inglés → cómo la imprimió Scryfall («Legendary Artifact Creature» → «Criatura artefacto legendaria»; frases enteras, porque el orden, el género y el caso cambian por idioma), y cada subtipo suelto («Elf» → «Elfo»). Se lee una vez (OnceLock) y va dentro del binario; la app nunca llama a Scryfall para esto. Los subtipos que ninguna carta trae sueltos («Druid») se deducen de las líneas con varios («Elf Druid»). Hay que repetir el script cuando un set añada tipos: mezcla con el fichero que hay y guarda al acabar cada idioma. (2) Las tablas escritas a mano (FR, DE, …) con el separador y la unión de subtipos de cada idioma, y cada frase anota la carta real de la que sale; son la única fuente del rumano, el polaco, el neerlandés y el ucraniano (sin cartas impresas ni fuente oficial) y de los tipos que ninguna carta extranjera muestra (Token, Emblem, Dungeon, Conspiracy, Vanguard…). Orden: frase del JSON y luego la de la tabla; por subtipo, tabla, luego JSON y luego la palabra inglesa. El italiano cambia el orden con varios subtipos, así que los que no se saben van en el orden inglés (union: None). Si los propios tipos no se saben, da None (manda la impresión de Scryfall, o el inglés). El español no tiene tabla (ES solo da la unión de subtipos); borrador::tipo_en_espanol queda como último recurso.
  • estructura.rs: los rangos de nivel (palabras_nivel), los niveles de clase y las cabeceras de caso («To solve», «À résoudre», «解明条件»…, de Case of the Shattered Pact) se reconocen en todos los idiomas impresos, así las secciones e insignias funcionan sea cual sea el idioma de diseño.

Huella del render y emoji

efectivo::huella calcula la huella de solo_su_idioma(e, lang): el diseño sin los textos de los idiomas que no se pintan, así editar un idioma nunca repinta otro. En español e inglés el diseño reducido es exactamente el de antes de haber más idiomas (también se quitan los demás), así que sus huellas, y sus renders en caché, no cambiaron. Si el diseño serializado lleva algún emoji (emoji::es_emoji), se añade |emoji-noto a la huella: cambiar de letra de emoji solo repinta las cartas con emoji, en vez de todas, como pasaría subiendo VERSION_MOTOR.

Tests de compatibilidad

  • efectivo::tests::las_huellas_en_espanol_e_ingles_son_las_de_antes: huellas fijas sacadas de la 2.5.9 en español e inglés.
  • modelo::tests::un_diseno_de_antes_se_reescribe_igual: tests/compatibilidad/diseno-2.5.9.json se lee y se vuelve a escribir idéntico byte a byte.
  • modelo::tests::otro_idioma_se_guarda_tras_es_y_en_y_lo_lee_la_version_de_antes: un diseño con otro idioma lo guarda tras es y en, la estructura antigua de dos campos lo sigue leyendo y una clave de idioma desconocida se ignora.

Tests dorados

Directorio: src-tauri/crates/proxy-core/tests/ (dorados.rs, layouts.rs, comun/mod.rs) e imágenes de referencia en tests/dorados/.

  • dorados.rs → dorados_de_los_casos_base: 13 casos base (criatura legendaria, variante con cinta, legendaria extended, muro bicolor, título largo, conjuro, criatura en classic, artefacto en borderless y classic, tierra extended, básica full-art, fichas extended y classic) renderizados a ANCHO = 420 px con el pack integrado, letras integradas, ilustración sintética y símbolos sintéticos (comun: dibujados a mano para no tener problemas de derechos), salida de impresión. Cada imagen se compara con tests/dorados/<name>.png; un caso falla cuando el 0,5 % de los píxeles difiere en más de 8/255 en algún canal (comun::diferencia). El test también comprueba que no haya ningún aviso FaltanSimbolos.
  • dos_renders_iguales_dan_los_mismos_bytes: entradas idénticas dan bytes idénticos; la salida de impresión es opaca; la de pantalla tiene un píxel de esquina transparente; el tamaño póker se renderiza con la proporción correcta.
  • cambiar_un_color_del_pack_cambia_el_dorado: editar una paleta cambia Pack.huella y la imagen.
  • dorados_en_otros_idiomas: la misma carta en francés, ruso y rumano contra tests/dorados/idioma-fr.png, idioma-ru.png e idioma-ro.png; comprueba que no hay FaltanLetras y que SinTextoOficial solo sale en rumano (sin impresión). dorado_en_japones (ignorado) compara idioma-ja.png; necesita la letra japonesa bajada: PB_CJK=<NotoSansJP[wght].ttf> cargo test -p proxy-core --test dorados -- --ignored dorado_en_japones.
  • tests/layouts.rs → dorados_de_los_layouts, las_cartas_grandes_salen_a_su_medida (véase Composición del diseñador).
  • Ignorados, se ejecutan a demanda con variables de entorno: tiempos (tiempos), original_con_sus_imagenes (PB_ORIGINAL, carpeta de marcos de Card Conjurer; pinta clásico, extendido y despejado), original_colores (los mismos tres estilos con cada color de marco y caja de reglas), balatro_con_sus_imagenes y balatro_contra_el_autor (PB_BALATRO, PB_AUTOR, PB_SALIDA), letras_libres_a_ojo (PB_GF), balatro_matriz en layouts.rs. Necesitan recursos externos y son para revisar a ojo, no para el CI.
  • Actualizar los dorados: PB_ACTUALIZAR_DORADOS=1 cargo test -p proxy-core --test dorados (y --test layouts) reescribe las imágenes. Hazlo solo para cambios visuales intencionados y revisa la diferencia; sube también VERSION_MOTOR cuando los píxeles cambian a propósito para que las cachés se refresquen.
  • Los tests unitarios viven junto al código: render.rs (paletas, degradados, estilos, layouts, pies, teñido, tamaños), componer.rs, cuerpos.rs, texto.rs, pintar.rs, pack.rs, efectivo.rs, textos_motor.rs, tipos_idiomas.rs, estructura.rs.

También src-tauri/crates/proxy-core/examples/disenar.rs renderiza cartas de muestra a disco para revisarlas a ojo, y examples/prototipo.rs es el prototipo inicial que se conserva como referencia (no es código de la app).

Modos de fallo

Fallo Comportamiento
Lienzo de tamaño imposible DisenoError::Lienzo (el ancho en píxeles se limita a un mínimo de 16 en render; aun así puede fallar la creación del pixmap de tiny-skia)
Fichero de letra no válido DisenoError::LetraNoValida
Símbolo corrupto DisenoError::SimboloNoValido
Falta el fichero de una capa del pack al pintar la capa se omite en silencio (los packs se validan al cargar, así que esto significa un caso de caída al pack integrado)
Falta el estilo en el pack se usa el estilo del pack integrado; luego el borderless del pack; por último DisenoError::Lienzo

Véase también

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Frontend · Frontend

Reference · Referencia

Clone this wiki locally