Icone

The .icon format

A saved icon is a plain JSON file, and the command line turns it into components. You do not need to open the editor to produce one — you need to know how it is shaped.

Why plain

The plan was to compress it. A set weighs a few dozen kilobytes and compression changes nothing you would notice; in exchange you lose what matters in an interchange format: being able to open it, fix it by hand, and see in a diff what changed.

The rest follows from that. Paths have only M, L, C, Z — one case to generate. Transforms are decomposed, not matrices. And ids are stable across frames, which means writing the same id in two states is how you say what becomes what.

The schema

JSON Schema 2020-12. It is closed: a misspelled property name is rejected rather than ignored, because a field dropped in silence is the hardest defect to see.

A test in the repository checks it against the real model in three directions: that it accepts what the program saves, that it rejects what is written wrong, and that every example below actually opens and draws something.

Download the schema

The bare minimum

You need id, name, at least one frame with rootId and nodes, and transitions — empty is fine. Everything else has a default.

Examples

minimo.icon

The smallest document that works: one shape, no animation. Everything absent has a default.

{
  "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

Two frames using the same ids. This is the heart of the format: equal ids mean exact matching, and the engine has nothing to guess.

{
  "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 from 0 to 1 draws the stroke, spin adds a full turn, loop repeats it. Three fields for a spinner.

{
  "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" }
          }
        ]
      }
    ]
  }
}

From file to components

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