Icone

Il formato .icon

Un'icona salvata è un file JSON in chiaro, e la riga di comando lo trasforma in componenti. Non serve aprire l'editor per produrne uno: serve sapere com'è fatto.

Perché in chiaro

Il piano prevedeva di comprimerlo. Un set pesa qualche decina di kilobyte e la compressione non cambia niente di percepibile; in cambio si perde ciò che conta in un formato di scambio: poterlo aprire, correggere a mano, e vedere in un diff cosa è cambiato.

Da lì discende il resto. I tracciati hanno solo M, L, C, Z — un solo caso da generare. Le trasformazioni sono decomposte, non matrici. E gli id sono stabili fra un fotogramma e l'altro, il che vuol dire che scrivere lo stesso id in due stati è il modo di dire cosa diventa cosa.

Lo schema

JSON Schema 2020-12. È chiuso: un nome di proprietà sbagliato viene rifiutato invece che ignorato, perché un campo scartato in silenzio è il difetto più difficile da vedere.

Una prova nel repository lo confronta con il modello vero in tre direzioni: che accetti ciò che il programma salva, che rifiuti ciò che è scritto male, e che ogni esempio qui sotto si apra davvero e disegni qualcosa.

Scarica lo schema

Il minimo indispensabile

Servono id, name, almeno un fotogramma con rootId e nodes, e transitions — anche vuoto. Tutto il resto ha un valore predefinito.

Esempi

minimo.icon

Il documento più piccolo che funziona: una forma, nessuna animazione. Tutto ciò che non c'è ha un valore predefinito.

{
  "formato": "icone",
  "versione": 1,
  "set": {
    "id": "esempi",
    "name": "Esempi",
    "icons": [
      {
        "id": "spunta",
        "name": "spunta",
        "size": { "w": 24, "h": 24 },
        "states": [
          {
            "id": "s1",
            "name": "Fermo",
            "rootId": "root",
            "nodes": {
              "root": {
                "id": "root",
                "geometry": { "kind": "group" },
                "childIds": ["segno"]
              },
              "segno": {
                "id": "segno",
                "name": "Segno",
                "parentId": "root",
                "geometry": {
                  "kind": "path",
                  "commands": [
                    { "type": "M", "x": 5, "y": 12.5 },
                    { "type": "L", "x": 10, "y": 17.5 },
                    { "type": "L", "x": 19, "y": 7 }
                  ]
                },
                "style": {
                  "fill": { "kind": "none" },
                  "stroke": { "kind": "currentColor" },
                  "strokeWidth": 2
                }
              }
            }
          }
        ],
        "transitions": []
      }
    ]
  }
}

menu-chiudi.icon

Due fotogrammi che usano gli stessi id. È il punto centrale del formato: id uguali significano abbinamento esatto, e il motore non deve indovinare niente.

{
  "formato": "icone",
  "versione": 1,
  "set": {
    "id": "esempi",
    "name": "Esempi",
    "icons": [
      {
        "id": "menu",
        "name": "menu",
        "size": { "w": 24, "h": 24 },
        "states": [
          {
            "id": "aperto",
            "name": "Menu",
            "rootId": "root",
            "nodes": {
              "root": { "id": "root", "geometry": { "kind": "group" }, "childIds": ["alta", "mezza", "bassa"] },
              "alta": {
                "id": "alta",
                "name": "Barra alta",
                "parentId": "root",
                "geometry": { "kind": "line", "x1": 4, "y1": 7, "x2": 20, "y2": 7 },
                "style": { "fill": { "kind": "none" }, "stroke": { "kind": "currentColor" }, "strokeWidth": 2 }
              },
              "mezza": {
                "id": "mezza",
                "name": "Barra di mezzo",
                "parentId": "root",
                "geometry": { "kind": "line", "x1": 4, "y1": 12, "x2": 20, "y2": 12 },
                "style": { "fill": { "kind": "none" }, "stroke": { "kind": "currentColor" }, "strokeWidth": 2 }
              },
              "bassa": {
                "id": "bassa",
                "name": "Barra bassa",
                "parentId": "root",
                "geometry": { "kind": "line", "x1": 4, "y1": 17, "x2": 20, "y2": 17 },
                "style": { "fill": { "kind": "none" }, "stroke": { "kind": "currentColor" }, "strokeWidth": 2 }
              }
            }
          },
          {
            "id": "chiuso",
            "name": "Chiudi",
            "rootId": "root",
            "nodes": {
              "root": { "id": "root", "geometry": { "kind": "group" }, "childIds": ["alta", "mezza", "bassa"] },
              "alta": {
                "id": "alta",
                "name": "Barra alta",
                "parentId": "root",
                "geometry": { "kind": "line", "x1": 4, "y1": 7, "x2": 20, "y2": 7 },
                "transform": { "y": 5, "rotation": 45 },
                "style": { "fill": { "kind": "none" }, "stroke": { "kind": "currentColor" }, "strokeWidth": 2 }
              },
              "mezza": {
                "id": "mezza",
                "name": "Barra di mezzo",
                "parentId": "root",
                "geometry": { "kind": "line", "x1": 4, "y1": 12, "x2": 20, "y2": 12 },
                "style": { "fill": { "kind": "none" }, "stroke": { "kind": "currentColor" }, "strokeWidth": 2, "opacity": 0 }
              },
              "bassa": {
                "id": "bassa",
                "name": "Barra bassa",
                "parentId": "root",
                "geometry": { "kind": "line", "x1": 4, "y1": 17, "x2": 20, "y2": 17 },
                "transform": { "y": -5, "rotation": -45 },
                "style": { "fill": { "kind": "none" }, "stroke": { "kind": "currentColor" }, "strokeWidth": 2 }
              }
            }
          }
        ],
        "transitions": [
          {
            "id": "t1",
            "kind": "smart",
            "fromStateId": "aperto",
            "toStateId": "chiuso",
            "timing": { "duration": 280, "easing": { "kind": "cubic", "p1x": 0.4, "p1y": 0, "p2x": 0.2, "p2y": 1 } },
            "trigger": { "kind": "toggle" }
          }
        ]
      }
    ]
  }
}

caricamento.icon

trimEnd da 0 a 1 disegna il tratto, spin aggiunge un giro intero, loop lo ripete. Tre campi per un caricatore.

{
  "formato": "icone",
  "versione": 1,
  "set": {
    "id": "esempi",
    "name": "Esempi",
    "size": { "w": 24, "h": 24 },
    "tokens": [{ "id": "accento", "name": "Accento", "color": "#5b5bd6" }],
    "icons": [
      {
        "id": "caricamento",
        "name": "caricamento",
        "size": { "w": 24, "h": 24 },
        "states": [
          {
            "id": "vuoto",
            "name": "Vuoto",
            "rootId": "root",
            "nodes": {
              "root": { "id": "root", "geometry": { "kind": "group" }, "childIds": ["pista", "arco"] },
              "pista": {
                "id": "pista",
                "name": "Pista",
                "parentId": "root",
                "geometry": { "kind": "ellipse", "cx": 12, "cy": 12, "rx": 9, "ry": 9 },
                "style": {
                  "fill": { "kind": "none" },
                  "stroke": { "kind": "currentColor" },
                  "strokeWidth": 2,
                  "strokeOpacity": 0.2
                }
              },
              "arco": {
                "id": "arco",
                "name": "Arco",
                "parentId": "root",
                "geometry": { "kind": "ellipse", "cx": 12, "cy": 12, "rx": 9, "ry": 9 },
                "transform": { "rotation": -90, "originX": 0.5, "originY": 0.5 },
                "style": {
                  "fill": { "kind": "none" },
                  "stroke": { "kind": "token", "tokenId": "accento" },
                  "strokeWidth": 2,
                  "strokeLineCap": "round",
                  "trimStart": 0,
                  "trimEnd": 0
                }
              }
            }
          },
          {
            "id": "pieno",
            "name": "Pieno",
            "rootId": "root",
            "nodes": {
              "root": { "id": "root", "geometry": { "kind": "group" }, "childIds": ["pista", "arco"] },
              "pista": {
                "id": "pista",
                "name": "Pista",
                "parentId": "root",
                "geometry": { "kind": "ellipse", "cx": 12, "cy": 12, "rx": 9, "ry": 9 },
                "style": {
                  "fill": { "kind": "none" },
                  "stroke": { "kind": "currentColor" },
                  "strokeWidth": 2,
                  "strokeOpacity": 0.2
                }
              },
              "arco": {
                "id": "arco",
                "name": "Arco",
                "parentId": "root",
                "geometry": { "kind": "ellipse", "cx": 12, "cy": 12, "rx": 9, "ry": 9 },
                "transform": { "rotation": 270, "originX": 0.5, "originY": 0.5 },
                "style": {
                  "fill": { "kind": "none" },
                  "stroke": { "kind": "token", "tokenId": "accento" },
                  "strokeWidth": 2,
                  "strokeLineCap": "round",
                  "trimStart": 0,
                  "trimEnd": 1
                }
              }
            }
          }
        ],
        "transitions": [
          {
            "id": "t1",
            "kind": "smart",
            "fromStateId": "vuoto",
            "toStateId": "pieno",
            "timing": { "duration": 1200, "easing": { "kind": "cubic", "p1x": 0.4, "p1y": 0, "p2x": 0.2, "p2y": 1 } },
            "nodeMotion": { "arco": { "spin": 1 } },
            "loop": { "mode": "loop", "count": 0, "hold": 200 },
            "trigger": { "kind": "none" }
          }
        ]
      }
    ]
  }
}

Da file a componenti

npx @icone/cli build menu-chiudi.icon --out src/icons --package