{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://p5loom.dawnshift.co/schema/p5loom-spec-1.schema.json",
  "title": "p5loom spec, version 1",
  "description": "A picture described as data: canvas, seed, theme, controls, data tables and elements. Coordinates are normalised 0 to 1. Colours are theme tokens. A field may follow a control with { \"control\": \"<id>\" }. The validator in the library enforces the same rules and adds checks a schema cannot express (for example that a bound slider's range fits the field).",
  "type": "object",
  "required": ["version", "seed", "canvas", "elements"],
  "additionalProperties": false,
  "properties": {
    "version": { "const": 1, "description": "The spec format. Only 1 is accepted." },
    "seed": {
      "description": "Fixes every random choice. Or a binding to a whole-number slider.",
      "anyOf": [
        { "type": "integer", "minimum": 0, "maximum": 4294967295 },
        { "$ref": "#/$defs/binding" }
      ]
    },
    "theme": {
      "description": "light or dark. Or a binding to a select of light and dark.",
      "anyOf": [
        { "enum": ["light", "dark"] },
        { "$ref": "#/$defs/binding" }
      ]
    },
    "canvas": {
      "type": "object",
      "required": ["width", "height"],
      "additionalProperties": false,
      "properties": {
        "width": { "type": "integer", "minimum": 16, "maximum": 4096 },
        "height": { "type": "integer", "minimum": 16, "maximum": 4096 }
      }
    },
    "controls": {
      "type": "array",
      "maxItems": 12,
      "items": { "$ref": "#/$defs/control" }
    },
    "data": {
      "type": "object",
      "description": "Named data tables. Up to 4 tables of up to 1000 rows; each row is up to 8 fields of numbers or text.",
      "maxProperties": 4,
      "propertyNames": { "pattern": "^[a-z][a-z0-9-]{0,31}$" },
      "additionalProperties": {
        "type": "array",
        "minItems": 1,
        "maxItems": 1000,
        "items": {
          "type": "object",
          "minProperties": 1,
          "maxProperties": 8,
          "propertyNames": { "pattern": "^[a-z][a-z0-9-]{0,31}$" },
          "additionalProperties": {
            "anyOf": [
              { "type": "number" },
              { "type": "string", "minLength": 1, "maxLength": 60 }
            ]
          }
        }
      }
    },
    "elements": {
      "type": "array",
      "maxItems": 5000,
      "items": { "$ref": "#/$defs/element" }
    }
  },
  "$defs": {
    "token": { "enum": ["bg", "fg", "accent", "muted", "grid"] },
    "unit": { "type": "number", "minimum": 0, "maximum": 1 },
    "binding": {
      "type": "object",
      "required": ["control"],
      "additionalProperties": false,
      "properties": { "control": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,31}$" } }
    },
    "when": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,31}$", "description": "The id of a toggle control. The element is hidden while it is off." },
    "colour": {
      "anyOf": [{ "$ref": "#/$defs/token" }, { "$ref": "#/$defs/binding" }]
    },
    "weight": {
      "anyOf": [
        { "type": "number", "minimum": 0, "maximum": 64 },
        { "$ref": "#/$defs/binding" }
      ]
    },
    "point": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "$ref": "#/$defs/unit" } },
    "control": {
      "oneOf": [
        {
          "type": "object",
          "required": ["id", "type", "label", "min", "max", "step", "value"],
          "additionalProperties": false,
          "properties": {
            "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,31}$" },
            "type": { "const": "slider" },
            "label": { "type": "string", "minLength": 1, "maxLength": 80 },
            "min": { "type": "number" },
            "max": { "type": "number" },
            "step": { "type": "number", "exclusiveMinimum": 0 },
            "value": { "type": "number" }
          }
        },
        {
          "type": "object",
          "required": ["id", "type", "label", "value"],
          "additionalProperties": false,
          "properties": {
            "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,31}$" },
            "type": { "const": "toggle" },
            "label": { "type": "string", "minLength": 1, "maxLength": 80 },
            "value": { "type": "boolean" }
          }
        },
        {
          "type": "object",
          "required": ["id", "type", "label", "options", "value"],
          "additionalProperties": false,
          "properties": {
            "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,31}$" },
            "type": { "const": "select" },
            "label": { "type": "string", "minLength": 1, "maxLength": 80 },
            "options": { "type": "array", "minItems": 2, "maxItems": 12, "items": { "type": "string", "minLength": 1, "maxLength": 40 } },
            "value": { "type": "string" }
          }
        }
      ]
    },
    "element": {
      "oneOf": [
        {
          "type": "object",
          "required": ["type", "x", "y", "r"],
          "additionalProperties": false,
          "properties": {
            "type": { "const": "circle" },
            "when": { "$ref": "#/$defs/when" },
            "x": { "anyOf": [{ "$ref": "#/$defs/unit" }, { "$ref": "#/$defs/binding" }] },
            "y": { "anyOf": [{ "$ref": "#/$defs/unit" }, { "$ref": "#/$defs/binding" }] },
            "r": { "anyOf": [{ "$ref": "#/$defs/unit" }, { "$ref": "#/$defs/binding" }], "description": "A fraction of the smaller canvas side." },
            "fill": { "$ref": "#/$defs/colour" },
            "stroke": { "$ref": "#/$defs/colour" },
            "weight": { "$ref": "#/$defs/weight" }
          }
        },
        {
          "type": "object",
          "required": ["type", "x", "y", "w", "h"],
          "additionalProperties": false,
          "properties": {
            "type": { "const": "rect" },
            "when": { "$ref": "#/$defs/when" },
            "x": { "anyOf": [{ "$ref": "#/$defs/unit" }, { "$ref": "#/$defs/binding" }] },
            "y": { "anyOf": [{ "$ref": "#/$defs/unit" }, { "$ref": "#/$defs/binding" }] },
            "w": { "anyOf": [{ "$ref": "#/$defs/unit" }, { "$ref": "#/$defs/binding" }] },
            "h": { "anyOf": [{ "$ref": "#/$defs/unit" }, { "$ref": "#/$defs/binding" }] },
            "fill": { "$ref": "#/$defs/colour" },
            "stroke": { "$ref": "#/$defs/colour" },
            "weight": { "$ref": "#/$defs/weight" }
          }
        },
        {
          "type": "object",
          "required": ["type", "points"],
          "additionalProperties": false,
          "properties": {
            "type": { "const": "line" },
            "when": { "$ref": "#/$defs/when" },
            "points": { "type": "array", "minItems": 2, "maxItems": 100000, "items": { "$ref": "#/$defs/point" } },
            "stroke": { "$ref": "#/$defs/colour" },
            "weight": { "$ref": "#/$defs/weight" }
          }
        },
        {
          "type": "object",
          "required": ["type", "data"],
          "additionalProperties": false,
          "properties": {
            "type": { "const": "scatter" },
            "when": { "$ref": "#/$defs/when" },
            "data": { "type": "array", "minItems": 1, "maxItems": 100000, "items": { "$ref": "#/$defs/point" } },
            "r": { "type": "number", "exclusiveMinimum": 0, "maximum": 100, "description": "Pixels." },
            "fill": { "$ref": "#/$defs/colour" }
          }
        },
        {
          "type": "object",
          "required": ["type", "count"],
          "additionalProperties": false,
          "properties": {
            "type": { "const": "random-dots" },
            "when": { "$ref": "#/$defs/when" },
            "count": {
              "anyOf": [
                { "type": "integer", "minimum": 1, "maximum": 100000 },
                { "$ref": "#/$defs/binding" }
              ]
            },
            "radius": {
              "type": "array", "minItems": 2, "maxItems": 2,
              "items": { "type": "number", "exclusiveMinimum": 0, "maximum": 100 },
              "description": "[min, max] in pixels."
            },
            "fill": { "$ref": "#/$defs/colour" }
          }
        },
        {
          "type": "object",
          "required": ["type", "table", "x", "y"],
          "additionalProperties": false,
          "description": "Axes for a data table. Linear y axes start at zero unless zero is false.",
          "properties": {
            "type": { "const": "axes" },
            "when": { "$ref": "#/$defs/when" },
            "table": { "type": "string" },
            "x": { "type": "string" },
            "y": { "type": "string" },
            "xScale": { "enum": ["band", "linear", "log"] },
            "yScale": { "enum": ["linear", "log"] },
            "frame": { "type": "array", "minItems": 4, "maxItems": 4, "items": { "$ref": "#/$defs/unit" } },
            "yTicks": { "type": "integer", "minimum": 2, "maximum": 12 },
            "zero": { "type": "boolean" },
            "grid": { "type": "boolean" },
            "title": { "type": "boolean" },
            "stroke": { "$ref": "#/$defs/colour" }
          }
        },
        {
          "type": "object",
          "required": ["type", "table", "x", "y"],
          "additionalProperties": false,
          "description": "Bars: one per row, on a band x scale and a linear or log y scale.",
          "properties": {
            "type": { "const": "bars" },
            "when": { "$ref": "#/$defs/when" },
            "table": { "type": "string" },
            "x": { "type": "string" },
            "y": { "type": "string" },
            "yScale": { "enum": ["linear", "log"] },
            "frame": { "type": "array", "minItems": 4, "maxItems": 4, "items": { "$ref": "#/$defs/unit" } },
            "zero": { "type": "boolean" },
            "fill": { "$ref": "#/$defs/colour" }
          }
        },
        {
          "type": "object",
          "required": ["type", "table", "x", "y"],
          "additionalProperties": false,
          "description": "Points: one dot per row.",
          "properties": {
            "type": { "const": "points" },
            "when": { "$ref": "#/$defs/when" },
            "table": { "type": "string" },
            "x": { "type": "string" },
            "y": { "type": "string" },
            "xScale": { "enum": ["band", "linear", "log"] },
            "yScale": { "enum": ["linear", "log"] },
            "frame": { "type": "array", "minItems": 4, "maxItems": 4, "items": { "$ref": "#/$defs/unit" } },
            "zero": { "type": "boolean" },
            "r": { "type": "number", "exclusiveMinimum": 0, "maximum": 100 },
            "fill": { "$ref": "#/$defs/colour" }
          }
        },
        {
          "type": "object",
          "required": ["type", "table", "x", "y"],
          "additionalProperties": false,
          "description": "A polyline through the rows, in order (band) or sorted by x (linear).",
          "properties": {
            "type": { "const": "path" },
            "when": { "$ref": "#/$defs/when" },
            "table": { "type": "string" },
            "x": { "type": "string" },
            "y": { "type": "string" },
            "xScale": { "enum": ["band", "linear", "log"] },
            "yScale": { "enum": ["linear", "log"] },
            "frame": { "type": "array", "minItems": 4, "maxItems": 4, "items": { "$ref": "#/$defs/unit" } },
            "zero": { "type": "boolean" },
            "weight": { "type": "number", "minimum": 0, "maximum": 64 },
            "stroke": { "$ref": "#/$defs/colour" }
          }
        },
        {
          "type": "object",
          "required": ["type", "text", "x", "y"],
          "additionalProperties": false,
          "properties": {
            "type": { "const": "text" },
            "when": { "$ref": "#/$defs/when" },
            "text": { "type": "string", "minLength": 1, "maxLength": 80 },
            "x": { "$ref": "#/$defs/unit" },
            "y": { "$ref": "#/$defs/unit" },
            "size": { "type": "number", "minimum": 6, "maximum": 72 },
            "align": { "enum": ["left", "center", "right"] },
            "fill": { "$ref": "#/$defs/token" }
          }
        },
        {
          "type": "object",
          "required": ["type", "items", "x", "y"],
          "additionalProperties": false,
          "properties": {
            "type": { "const": "legend" },
            "when": { "$ref": "#/$defs/when" },
            "x": { "$ref": "#/$defs/unit" },
            "y": { "$ref": "#/$defs/unit" },
            "items": {
              "type": "array", "minItems": 1, "maxItems": 8,
              "items": {
                "type": "object", "required": ["label", "fill"], "additionalProperties": false,
                "properties": {
                  "label": { "type": "string", "minLength": 1, "maxLength": 40 },
                  "fill": { "$ref": "#/$defs/token" }
                }
              }
            }
          }
        }
      ]
    }
  }
}
