Skip to content
Ooidget

A knob

The example’s project folder, file by file: components as name.json, shared behaviours as name.behaviour.js. Run it.

fl_volume.json

{
 "widget": {
  "role": "slider",
  "label": "`Volume ${Math.round(s.volume * 100)}%`",
  "behaviour": [
   {
    "use": "controls/drag",
    "axis": "dragAxis",
    "travel": 300,
    "value": "volume",
    "steps": "ticks",
    "subdivisions": "subdivisions",
    "snap": "snap",
    "strength": [
     "snapMajor",
     "snapMinor"
    ],
    "microSnap": "microSnap"
   },
   {
    "use": "controls/wheel",
    "value": "volume",
    "steps": "ticks",
    "subdivisions": "subdivisions",
    "snap": "snap",
    "strength": [
     "snapMajor",
     "snapMinor"
    ],
    "microSnap": "microSnap"
   }
  ],
  "layers": [
   {
    "box": [
     25,
     25,
     74,
     74
    ],
    "edge": {
     "fn": "knurledEdge"
    },
    "edgeSamples": 360,
    "effects": [
     {
      "blur": 3,
      "color": "#000000",
      "distance": 1.5,
      "opacity": 0.5,
      "type": "dropShadow"
     },
     {
      "color": "$ringColor",
      "type": "fill"
     },
     {
      "linear": {
       "angle": 180,
       "relative": true,
       "stops": [
        [
         0,
         "#ffffff",
         0.55
        ],
        [
         0.45,
         "#ffffff",
         0
        ],
        [
         0.55,
         "#000000",
         0
        ],
        [
         1,
         "#000000",
         0.55
        ]
       ]
      },
      "type": "fill"
     },
     {
      "color": "#161d21",
      "opacity": 0.8,
      "position": "inside",
      "type": "stroke",
      "width": 1
     }
    ],
    "shape": "circle",
    "when": "s.ringStyle < 0.5"
   },
   {
    "box": [
     25,
     25,
     74,
     74
    ],
    "edge": {
     "fn": "knurledEdge"
    },
    "edgeSamples": 360,
    "effects": [
     {
      "blur": 4,
      "color": "#000000",
      "distance": 1.5,
      "opacity": 0.25,
      "type": "dropShadow"
     },
     {
      "color": "$ringColor",
      "type": "fill"
     },
     {
      "linear": {
       "angle": 180,
       "relative": true,
       "stops": [
        [
         0,
         "#000000",
         0
        ],
        [
         0.6,
         "#000000",
         0
        ],
        [
         1,
         "#000000",
         0.3
        ]
       ]
      },
      "type": "fill"
     },
     {
      "color": "#000000",
      "opacity": 0.45,
      "position": "outside",
      "type": "stroke",
      "width": 0.6
     }
    ],
    "opacityExpr": "s.ringOpacity",
    "shape": "circle",
    "when": "s.ringStyle >= 0.5"
   },
   {
    "box": [
     25,
     25,
     74,
     74
    ],
    "edge": {
     "fn": "knurledEdge"
    },
    "edgeSamples": 360,
    "effects": [
     {
      "linear": {
       "angle": 180,
       "relative": true,
       "stops": [
        [
         0,
         "#ffffff",
         0.42
        ],
        [
         0.22,
         "#ffffff",
         0.16
        ],
        [
         0.4,
         "#ffffff",
         0.02
        ],
        [
         0.5,
         "#ffffff",
         0
        ],
        [
         1,
         "#ffffff",
         0
        ]
       ]
      },
      "type": "fill"
     },
     {
      "blur": 3,
      "color": "#ffffff",
      "opacity": 0.18,
      "type": "innerGlow"
     },
     {
      "linear": {
       "angle": 180,
       "relative": true,
       "stops": [
        [
         0,
         "#ffffff",
         0.85
        ],
        [
         0.4,
         "#ffffff",
         0.1
        ],
        [
         1,
         "#ffffff",
         0.25
        ]
       ]
      },
      "position": "inside",
      "type": "stroke",
      "width": 0.8
     }
    ],
    "opacityExpr": "s.gloss",
    "shape": "circle",
    "when": "s.ringStyle >= 0.5"
   },
   {
    "effects": [
     {
      "radial": {
       "colors": [
        [
         "$arcColor",
         1
        ],
        [
         "$arcColorEnd",
         0
        ]
       ],
       "cx": 0.5,
       "cy": 0.5,
       "fn": {
        "fn": "arcFade"
       },
       "rx": 0.4032258064516129,
       "ry": 0.4032258064516129,
       "samples": 64
      },
      "type": "fill"
     }
    ],
    "pathExpr": {
     "fn": "valueArc"
    },
    "shape": "path"
   },
   {
    "effects": [
     {
      "color": "#e8edf0",
      "opacity": 0.85,
      "type": "fill"
     }
    ],
    "pathExpr": {
     "fn": "scaleMarks"
    },
    "shape": "path"
   },
   {
    "effects": [
     {
      "blur": 3,
      "color": "$accentGlow",
      "opacity": 0.7,
      "type": "outerGlow"
     },
     {
      "color": "$accent",
      "type": "fill"
     }
    ],
    "pathExpr": {
     "fn": "markUnderValue"
    },
    "shape": "path",
    "when": {
     "fn": "onAMark"
    }
   },
   {
    "align": "center",
    "box": [
     0,
     0,
     24,
     10
    ],
    "boxExpr": {
     "fn": "minLabelBox"
    },
    "color": "#c9d1d6",
    "font": "Bahnschrift",
    "size": 7,
    "text": "String(s.minValue)",
    "weight": 400
   },
   {
    "align": "center",
    "box": [
     0,
     0,
     24,
     10
    ],
    "boxExpr": {
     "fn": "midLabelBox"
    },
    "color": "#c9d1d6",
    "font": "Bahnschrift",
    "size": 7,
    "text": "String(+((s.minValue + s.maxValue) / 2).toFixed(2))",
    "weight": 400
   },
   {
    "align": "center",
    "box": [
     0,
     0,
     24,
     10
    ],
    "boxExpr": {
     "fn": "maxLabelBox"
    },
    "color": "#c9d1d6",
    "font": "Bahnschrift",
    "size": 7,
    "text": "String(s.maxValue)",
    "weight": 400
   },
   {
    "box": [
     34.5,
     34.5,
     55,
     55
    ],
    "effects": [
     {
      "blur": 2,
      "color": "#0b1114",
      "distance": 1,
      "opacity": 0.9,
      "type": "dropShadow"
     },
     {
      "color": "#20292e",
      "type": "fill"
     },
     {
      "color": "#20292e",
      "position": "outside",
      "type": "stroke",
      "width": 1
     }
    ],
    "shape": "circle"
   },
   {
    "box": [
     34.5,
     34.5,
     55,
     55
    ],
    "effects": [
     {
      "conic": {
       "colors": [
        "#3a4148",
        "#ffffff"
       ],
       "fn": "0.5 + 0.5 * Math.cos(4 * Math.PI * (u - 0.83))",
       "relative": true
      },
      "type": "fill"
     },
     {
      "radial": {
       "colors": [
        [
         "#000000",
         0.14
        ],
        [
         "#ffffff",
         0.12
        ]
       ],
       "fn": "0.5 + 0.5 * Math.sin(2 * Math.PI * u)",
       "repeat": 39,
       "rx": 0.5,
       "ry": 0.5
      },
      "type": "fill"
     }
    ],
    "shape": "circle",
    "when": "s.capStyle < 0.5"
   },
   {
    "box": [
     34.5,
     34.5,
     55,
     55
    ],
    "effects": [
     {
      "color": "$capTint",
      "type": "fill"
     }
    ],
    "opacityExpr": "s.capTintAmount",
    "shape": "circle",
    "when": "s.capStyle < 0.5"
   },
   {
    "box": [
     34.5,
     34.5,
     55,
     55
    ],
    "effects": [
     {
      "color": "$capColor",
      "type": "fill"
     },
     {
      "linear": {
       "angle": 180,
       "relative": true,
       "stops": [
        [
         0,
         "#000000",
         0
        ],
        [
         0.5,
         "#000000",
         0.1
        ],
        [
         1,
         "#000000",
         0.5
        ]
       ]
      },
      "type": "fill"
     }
    ],
    "shape": "circle",
    "when": "s.capStyle >= 0.5"
   },
   {
    "box": [
     34.5,
     34.5,
     55,
     55
    ],
    "effects": [
     {
      "radial": {
       "cx": 0.5,
       "cy": 0.5,
       "offset": 0.3,
       "relative": true,
       "rx": 0.7,
       "ry": 0.7,
       "stops": [
        [
         0,
         "#ffffff",
         0.22
        ],
        [
         0.5,
         "#ffffff",
         0.05
        ],
        [
         1,
         "#ffffff",
         0
        ]
       ]
      },
      "type": "fill"
     },
     {
      "linear": {
       "angle": 180,
       "relative": true,
       "stops": [
        [
         0,
         "#ffffff",
         0.38
        ],
        [
         0.18,
         "#ffffff",
         0.22
        ],
        [
         0.36,
         "#ffffff",
         0.03
        ],
        [
         0.42,
         "#ffffff",
         0
        ],
        [
         1,
         "#ffffff",
         0
        ]
       ]
      },
      "type": "fill"
     },
     {
      "radial": {
       "cx": 0.5,
       "cy": 0.5,
       "offset": 0.3,
       "relative": true,
       "rx": 0.16,
       "ry": 0.11,
       "stops": [
        [
         0,
         "#ffffff",
         0.9
        ],
        [
         0.35,
         "#ffffff",
         0.55
        ],
        [
         1,
         "#ffffff",
         0
        ]
       ]
      },
      "type": "fill"
     }
    ],
    "opacityExpr": "s.gloss",
    "shape": "circle",
    "when": "s.capStyle >= 0.5"
   },
   {
    "box": [
     34.5,
     34.5,
     55,
     55
    ],
    "effects": [
     {
      "radial": {
       "cx": 0.5,
       "cy": 0.5,
       "offset": 0.22,
       "relative": true,
       "rx": 0.62,
       "ry": 0.62,
       "stops": [
        [
         0,
         "#ffffff",
         0.3
        ],
        [
         0.55,
         "#ffffff",
         0.06
        ],
        [
         1,
         "#ffffff",
         0
        ]
       ]
      },
      "type": "fill"
     },
     {
      "radial": {
       "cx": 0.5,
       "cy": 0.5,
       "rx": 0.5,
       "ry": 0.5,
       "stops": [
        [
         0,
         "#000000",
         0
        ],
        [
         0.62,
         "#000000",
         0
        ],
        [
         0.88,
         "#000000",
         0.22
        ],
        [
         1,
         "#000000",
         0.5
        ]
       ]
      },
      "type": "fill"
     }
    ],
    "shape": "circle",
    "when": "Math.round(s.capProfile) === 1"
   },
   {
    "box": [
     34.5,
     34.5,
     55,
     55
    ],
    "effects": [
     {
      "linear": {
       "angle": 180,
       "relative": true,
       "stops": [
        [
         0,
         "#000000",
         0.42
        ],
        [
         0.35,
         "#000000",
         0.08
        ],
        [
         0.55,
         "#000000",
         0
        ],
        [
         1,
         "#000000",
         0
        ]
       ]
      },
      "type": "fill"
     },
     {
      "radial": {
       "cx": 0.5,
       "cy": 0.5,
       "offset": -0.26,
       "relative": true,
       "rx": 0.6,
       "ry": 0.6,
       "stops": [
        [
         0,
         "#ffffff",
         0.26
        ],
        [
         0.5,
         "#ffffff",
         0.06
        ],
        [
         1,
         "#ffffff",
         0
        ]
       ]
      },
      "type": "fill"
     },
     {
      "blur": 3,
      "color": "#000000",
      "distance": 1.8,
      "opacity": 0.55,
      "type": "innerShadow"
     }
    ],
    "shape": "circle",
    "when": "Math.round(s.capProfile) === 2"
   },
   {
    "box": [
     34.5,
     34.5,
     55,
     55
    ],
    "effects": [
     {
      "blur": 0.8,
      "color": "#000000",
      "opacity": 0.45,
      "spread": 0.3,
      "type": "innerShadow"
     },
     {
      "blur": 0.6,
      "color": "#000000",
      "distance": 0.6,
      "opacity": 0.35,
      "type": "innerShadow"
     },
     {
      "linear": {
       "angle": 180,
       "relative": true,
       "stops": [
        [
         0,
         "#c6ccd0",
         1
        ],
        [
         0.35,
         "#c6ccd0",
         0
        ],
        [
         1,
         "#c6ccd0",
         0
        ]
       ]
      },
      "position": "inside",
      "type": "stroke",
      "width": 1
     }
    ],
    "shape": "circle"
   },
   {
    "effects": [
     {
      "color": "$accentGlow",
      "type": "fill"
     },
     {
      "blur": 0.15,
      "color": "#001018",
      "distance": 0.7,
      "opacity": 0.85,
      "type": "innerShadow"
     }
    ],
    "pathExpr": {
     "fn": "pointerDot"
    },
    "shape": "path",
    "when": "Math.round(s.markStyle) === 0"
   },
   {
    "effects": [
     {
      "color": "$accent",
      "type": "fill"
     },
     {
      "blur": 0.15,
      "color": "#001018",
      "distance": 0.7,
      "opacity": 0.85,
      "type": "innerShadow"
     },
     {
      "blur": 0.1,
      "color": "#d8fdff",
      "distance": -0.45,
      "opacity": 0.8,
      "type": "innerShadow"
     }
    ],
    "opacityExpr": {
     "fn": "pointerShine"
    },
    "pathExpr": {
     "fn": "pointerDot"
    },
    "shape": "path",
    "when": "Math.round(s.markStyle) === 0"
   },
   {
    "effects": [
     {
      "color": "$accentGlow",
      "type": "fill"
     },
     {
      "blur": 0.15,
      "color": "#001018",
      "distance": 0.7,
      "opacity": 0.85,
      "type": "innerShadow"
     }
    ],
    "pathExpr": {
     "fn": "pointerLine"
    },
    "shape": "path",
    "when": "Math.round(s.markStyle) === 1"
   },
   {
    "effects": [
     {
      "color": "$accent",
      "type": "fill"
     },
     {
      "blur": 0.15,
      "color": "#001018",
      "distance": 0.7,
      "opacity": 0.85,
      "type": "innerShadow"
     },
     {
      "blur": 0.1,
      "color": "#d8fdff",
      "distance": -0.45,
      "opacity": 0.8,
      "type": "innerShadow"
     }
    ],
    "opacityExpr": {
     "fn": "pointerShine"
    },
    "pathExpr": {
     "fn": "pointerLine"
    },
    "shape": "path",
    "when": "Math.round(s.markStyle) === 1"
   },
   {
    "effects": [
     {
      "color": "$accentGlow",
      "type": "fill"
     },
     {
      "blur": 0.15,
      "color": "#001018",
      "distance": 0.7,
      "opacity": 0.85,
      "type": "innerShadow"
     }
    ],
    "pathExpr": {
     "fn": "pointerTip"
    },
    "shape": "path",
    "when": "Math.round(s.markStyle) === 2"
   },
   {
    "effects": [
     {
      "color": "$accent",
      "type": "fill"
     },
     {
      "blur": 0.15,
      "color": "#001018",
      "distance": 0.7,
      "opacity": 0.85,
      "type": "innerShadow"
     },
     {
      "blur": 0.1,
      "color": "#d8fdff",
      "distance": -0.45,
      "opacity": 0.8,
      "type": "innerShadow"
     }
    ],
    "opacityExpr": {
     "fn": "pointerShine"
    },
    "pathExpr": {
     "fn": "pointerTip"
    },
    "shape": "path",
    "when": "Math.round(s.markStyle) === 2"
   }
  ],
  "light": "s.light",
  "motion": {
   "k": {
    "duration": 280,
    "ease": "easeOut",
    "to": "ui.hover ? 1 : 0"
   },
   "v": {
    "duration": 260,
    "ease": "spring",
    "to": "s.volume"
   },
   "ve": {
    "duration": 50,
    "ease": "easeOut",
    "to": "s.volume"
   }
  },
  "params": {
   "accent": {
    "label": "Accent",
    "type": "color"
   },
   "accentGlow": {
    "label": "Accent glow",
    "type": "color"
   },
   "arcColor": {
    "label": "Arc colour",
    "type": "color"
   },
   "arcColorEnd": {
    "label": "Arc fades to",
    "type": "color"
   },
   "arcFill": {
    "choices": [
     "Fading",
     "Solid"
    ]
   },
   "arcRound": {
    "label": "Arc end roundness",
    "max": 1,
    "min": 0,
    "step": 0.05
   },
   "arcWidth": {
    "label": "Arc thickness",
    "max": 10,
    "min": 0.5,
    "step": 0.1
   },
   "capColor": {
    "label": "Cap colour (plastic)",
    "type": "color"
   },
   "capProfile": {
    "choices": [
     "Flat",
     "Domed",
     "Dished"
    ]
   },
   "capStyle": {
    "choices": [
     "Brushed",
     "Plain"
    ]
   },
   "capTint": {
    "label": "Cap tint (metal)",
    "type": "color"
   },
   "capTintAmount": {
    "label": "Cap tint amount",
    "max": 0.8,
    "min": 0,
    "step": 0.01
   },
   "fadePower": {
    "label": "Fade softness",
    "max": 4,
    "min": 0.5,
    "step": 0.05
   },
   "fadeStart": {
    "label": "Fade start",
    "max": 0.9,
    "min": 0,
    "step": 0.01
   },
   "gloss": {
    "label": "Glossiness",
    "max": 1,
    "min": 0,
    "step": 0.01
   },
   "knurlDepth": {
    "label": "Knurl depth",
    "max": 10,
    "min": 0,
    "step": 0.1
   },
   "knurlRound": {
    "label": "Knurl roundness",
    "max": 1,
    "min": 0,
    "step": 0.05
   },
   "knurls": {
    "label": "Knurls",
    "max": 12,
    "min": 0,
    "step": 1
   },
   "light": {
    "label": "Light angle",
    "max": 359,
    "min": 0,
    "step": 1,
    "unit": "deg"
   },
   "longEvery": {
    "label": "Long mark every",
    "max": 10,
    "min": 1,
    "step": 1
   },
   "markRadius": {
    "label": "Marks distance",
    "max": 56,
    "min": 38,
    "step": 0.1
   },
   "markStyle": {
    "choices": [
     "Dot",
     "Line",
     "Triangle"
    ]
   },
   "maxValue": {
    "label": "End value",
    "max": 1000,
    "min": 0,
    "step": 1
   },
   "minValue": {
    "label": "Start value",
    "max": 100,
    "min": -100,
    "step": 1
   },
   "ringColor": {
    "label": "Ring colour",
    "type": "color"
   },
   "ringOpacity": {
    "label": "Glass opacity",
    "max": 1,
    "min": 0.1,
    "step": 0.01
   },
   "ringStyle": {
    "choices": [
     "Solid",
     "Glass"
    ]
   },
   "snap": {
    "choices": [
     "Off",
     "On"
    ]
   },
   "snapMajor": {
    "label": "Snap strength, majors",
    "max": 1,
    "min": 0,
    "step": 0.05
   },
   "snapMinor": {
    "label": "Snap strength, subdivisions",
    "max": 1,
    "min": 0,
    "step": 0.05
   },
   "subdivisions": {
    "label": "Subdivisions",
    "max": 4,
    "min": 0,
    "step": 1
   },
   "ticks": {
    "label": "Major marks",
    "max": 40,
    "min": 2,
    "step": 1
   },
   "volume": {
    "label": "Volume",
    "max": 1,
    "min": 0,
    "step": 0.01
   },
   "microSnap": {
    "min": 0,
    "max": 0.45
   },
   "markLit": {
    "min": 0,
    "max": 0.5
   }
  },
  "size": [
   124,
   124
  ],
  "state": {
   "accent": "#67f2ff",
   "accentGlow": "#1ec8ff",
   "arcColor": "#67f2ff",
   "arcColorEnd": "#1ec8ff",
   "arcFill": 0,
   "arcRound": 1,
   "arcWidth": 2.1,
   "capColor": "#2a2e33",
   "capProfile": 0,
   "capStyle": 0,
   "capTint": "#3a7bd5",
   "capTintAmount": 0,
   "fadePower": 2,
   "fadeStart": 0,
   "gloss": 1,
   "knurlDepth": 6,
   "knurlRound": 1,
   "knurls": 5,
   "light": 270,
   "longEvery": 5,
   "markRadius": 40.5,
   "markStyle": 0,
   "maxValue": 100,
   "minValue": 0,
   "ringColor": "#4a555b",
   "ringOpacity": 0.55,
   "ringStyle": 0,
   "snap": 1,
   "snapMajor": 1,
   "snapMinor": 1,
   "subdivisions": 1,
   "ticks": 21,
   "volume": 0.8,
   "microSnap": 0.06,
   "markLit": 0.25,
   "dragAxis": "auto"
  }
 }
}

fl_volume.js

/** fl_volume's code: the functions fl_volume.json refers to by name, and its behaviour. */

export function knurledEdge(t, a, x, y, s, m) {
  return !m.k || !s.knurlDepth || !s.knurls
    ? 0
    : -m.k *
        s.knurlDepth *
        ((f) => s.knurlRound * (1 - Math.sin(Math.PI * f)) + (1 - s.knurlRound) * Math.abs(2 * f - 1))(
          (((((a -
            ((120 + 300 * Math.min(1, Math.max(0, (s.snap ? m.v : m.ve) ?? s.volume)) + 90) * Math.PI) /
              180) *
            s.knurls) /
            (2 * Math.PI) +
            0.5) %
            1) +
            1) %
            1,
        );
}

export function valueArc(s, ui, m) {
  const c = 62,
    r0 = 37.4,
    r1 = 37.4 + s.arcWidth,
    rm = (r0 + r1) / 2,
    h = (r1 - r0) / 2,
    rnd = Math.min(1, Math.max(0, s.arcRound)),
    dc = (((rnd * h) / rm) * 180) / Math.PI;
  const a0 = 120 + dc,
    a1 = 120 + 300 * Math.min(1, Math.max(0, (s.snap ? m.v : m.ve) ?? s.volume)) - dc;
  if (a1 <= a0) return "";
  const K = (th) => {
    if (!m.k || !s.knurlDepth || !s.knurls) return 0;
    const u =
        ((th - (120 + 300 * Math.min(1, Math.max(0, (s.snap ? m.v : m.ve) ?? s.volume)))) * s.knurls) / 360 +
        0.5,
      f = ((u % 1) + 1) % 1;
    return (
      m.k *
      s.knurlDepth *
      ((f) => s.knurlRound * (1 - Math.sin(Math.PI * f)) + (1 - s.knurlRound) * Math.abs(2 * f - 1))(f)
    );
  };
  const f = (v) => v.toFixed(3);
  const P = (r, a) => {
    const q = r - K(a);
    return f(c + q * Math.cos((a * Math.PI) / 180)) + " " + f(c + q * Math.sin((a * Math.PI) / 180));
  };
  const cap = (a, s) => {
    const t = (a * Math.PI) / 180,
      R = [Math.cos(t), Math.sin(t)],
      T = [-Math.sin(t), Math.cos(t)];
    const k = K(a),
      cx = c + (rm - k) * R[0],
      cy = c + (rm - k) * R[1];
    let o = "";
    for (let k = 1; k < 12; ++k) {
      const q = (Math.PI * k) / 12,
        u = s * Math.cos(q),
        v = s * Math.sin(q) * rnd;
      o += "L" + f(cx + h * (R[0] * u + T[0] * v)) + " " + f(cy + h * (R[1] * u + T[1] * v));
    }
    return o;
  };
  const n = Math.max(2, Math.ceil((a1 - a0) / 2));
  let d = "M" + P(r1, a0);
  for (let i = 1; i <= n; ++i) d += "L" + P(r1, a0 + ((a1 - a0) * i) / n);
  d += cap(a1, 1);
  for (let i = n; i >= 0; --i) d += "L" + P(r0, a0 + ((a1 - a0) * i) / n);
  d += cap(a0, -1);
  return d + "Z";
}

export function arcFade(u, s) {
  return s.arcFill >= 0.5
    ? 0
    : ((r) => {
        const t = Math.min(1, Math.max(0, (r - 37.4) / s.arcWidth));
        return t < s.fadeStart ? 0 : 1 - Math.pow(1 - (t - s.fadeStart) / (1 - s.fadeStart), s.fadePower);
      })(u * 50);
}

export function scaleMarks(s, ui, m) {
  {
    const c = 62,
      sub = Math.max(0, Math.round(s.subdivisions)),
      per = sub + 1,
      n = Math.max(2, Math.round(s.ticks)),
      m = (n - 1) * per,
      le = Math.max(1, Math.round(s.longEvery)),
      r0 = s.markRadius,
      kind = (i) => (i % per !== 0 ? 0 : (i / per) % le === 0 ? 2 : 1),
      len = (k) => (k === 2 ? 7.5 : k === 1 ? 5 : 2.5),
      wid = (k) => (k === 0 ? 0.7 : 1.6),
      bar = (i, k) => {
        const a = ((120 + (300 * i) / m) * Math.PI) / 180,
          ca = Math.cos(a),
          sa = Math.sin(a),
          h = wid(k) / 2,
          R1 = r0 + len(k);
        const p = (r, o) => (c + r * ca - o * sa).toFixed(3) + " " + (c + r * sa + o * ca).toFixed(3);
        return "M" + p(r0, -h) + "L" + p(R1, -h) + "L" + p(R1, h) + "L" + p(r0, h) + "Z";
      };
    let d = "";
    for (let i = 0; i <= m; ++i) d += bar(i, kind(i));
    return d;
  }
}

export function onAMark(s, ui, m) {
  {
    const c = 62,
      sub = Math.max(0, Math.round(s.subdivisions)),
      per = sub + 1,
      n = Math.max(2, Math.round(s.ticks)),
      m = (n - 1) * per,
      le = Math.max(1, Math.round(s.longEvery)),
      r0 = s.markRadius,
      kind = (i) => (i % per !== 0 ? 0 : (i / per) % le === 0 ? 2 : 1),
      len = (k) => (k === 2 ? 7.5 : k === 1 ? 5 : 2.5),
      wid = (k) => (k === 0 ? 0.7 : 1.6),
      bar = (i, k) => {
        const a = ((120 + (300 * i) / m) * Math.PI) / 180,
          ca = Math.cos(a),
          sa = Math.sin(a),
          h = wid(k) / 2,
          R1 = r0 + len(k);
        const p = (r, o) => (c + r * ca - o * sa).toFixed(3) + " " + (c + r * sa + o * ca).toFixed(3);
        return "M" + p(r0, -h) + "L" + p(R1, -h) + "L" + p(R1, h) + "L" + p(r0, h) + "Z";
      };
    const x = Math.min(1, Math.max(0, (s.snap ? m.v : m.ve) ?? s.volume)) * m;
    return Math.abs(x - Math.round(x)) < (s.snap ? 0.02 : (s.markLit ?? 0.25));
  }
}

export function markUnderValue(s, ui, m) {
  {
    const c = 62,
      sub = Math.max(0, Math.round(s.subdivisions)),
      per = sub + 1,
      n = Math.max(2, Math.round(s.ticks)),
      m = (n - 1) * per,
      le = Math.max(1, Math.round(s.longEvery)),
      r0 = s.markRadius,
      kind = (i) => (i % per !== 0 ? 0 : (i / per) % le === 0 ? 2 : 1),
      len = (k) => (k === 2 ? 7.5 : k === 1 ? 5 : 2.5),
      wid = (k) => (k === 0 ? 0.7 : 1.6),
      bar = (i, k) => {
        const a = ((120 + (300 * i) / m) * Math.PI) / 180,
          ca = Math.cos(a),
          sa = Math.sin(a),
          h = wid(k) / 2,
          R1 = r0 + len(k);
        const p = (r, o) => (c + r * ca - o * sa).toFixed(3) + " " + (c + r * sa + o * ca).toFixed(3);
        return "M" + p(r0, -h) + "L" + p(R1, -h) + "L" + p(R1, h) + "L" + p(r0, h) + "Z";
      };
    const i = Math.round(Math.min(1, Math.max(0, (s.snap ? m.v : m.ve) ?? s.volume)) * m);
    return bar(i, kind(i));
  }
}

export function minLabelBox(s, ui, m) {
  const a = ((120 + 300 * 0) * Math.PI) / 180,
    r = s.markRadius + 14;
  return [62 + r * Math.cos(a) - 12, 62 + r * Math.sin(a) - 5, 24, 10];
}

export function midLabelBox(s, ui, m) {
  const a = ((120 + 300 * 0.5) * Math.PI) / 180,
    r = s.markRadius + 14;
  return [62 + r * Math.cos(a) - 12, 62 + r * Math.sin(a) - 5, 24, 10];
}

export function maxLabelBox(s, ui, m) {
  const a = ((120 + 300 * 1) * Math.PI) / 180,
    r = s.markRadius + 14;
  return [62 + r * Math.cos(a) - 12, 62 + r * Math.sin(a) - 5, 24, 10];
}

export function pointerDot(s, ui, m) {
  const t = ((120 + 300 * Math.min(1, Math.max(0, (s.snap ? m.v : m.ve) ?? s.volume))) * Math.PI) / 180,
    x = 62 + 32.3 * Math.cos(t),
    y = 62 + 32.3 * Math.sin(t),
    q = 1.9;
  let d = "";
  for (let i = 0; i < 24; ++i) {
    const u = (i / 24) * 2 * Math.PI;
    d += (i ? "L" : "M") + (x + q * Math.cos(u)).toFixed(3) + " " + (y + q * Math.sin(u)).toFixed(3);
  }
  return d + "Z";
}

export function pointerShine(s, ui, m) {
  return (
    0.5 +
    0.5 *
      Math.cos(
        ((120 + 300 * Math.min(1, Math.max(0, (s.snap ? m.v : m.ve) ?? s.volume)) - m.light) * Math.PI) / 180,
      )
  );
}

export function pointerLine(s, ui, m) {
  const t = ((120 + 300 * Math.min(1, Math.max(0, (s.snap ? m.v : m.ve) ?? s.volume))) * Math.PI) / 180,
    c = Math.cos(t),
    s2 = Math.sin(t),
    h = 1.5 / 2,
    k = 62;
  const p = (r, o) => (k + r * c - o * s2).toFixed(3) + " " + (k + r * s2 + o * c).toFixed(3);
  return "M" + p(29, -h) + "L" + p(35.5, -h) + "L" + p(35.5, h) + "L" + p(29, h) + "Z";
}

export function pointerTip(s, ui, m) {
  const t = ((120 + 300 * Math.min(1, Math.max(0, (s.snap ? m.v : m.ve) ?? s.volume))) * Math.PI) / 180,
    ca = Math.cos(t),
    sa = Math.sin(t),
    c = 62,
    q = 2.3,
    r = 32.3;
  const p = (rr, o) => (c + rr * ca - o * sa).toFixed(3) + " " + (c + rr * sa + o * ca).toFixed(3);
  return "M" + p(r + q, 0) + "L" + p(r - q * 0.8, -q * 0.95) + "L" + p(r - q * 0.8, q * 0.95) + "Z";
}

controls/detents.js

From the controls library, shared by the examples.

/**
 * Marks a value (0 to 1) can rest on, shared by the drag and wheel behaviours: majors (`steps` of them) with
 * `subdivisions` between, each level pulling with its own `strength`; `snap` off leaves the value free, held on a mark
 * only across `microSnap`. Each setting is a number, or the name of a state key that holds it.
 */
export function detents(o) {
  // a setting: its number, or the state's value under the name it gives
  const at = (s, k, d) => {
    const v = o[k];
    return v == null ? d : typeof v === "string" ? (s[v] ?? d) : v;
  };
  const key = o.value || "value";
  const clamp = (v) => Math.min(1, Math.max(0, v));
  // marks a major spans (its subdivisions and itself), and marks across the range
  const per = (s) => Math.max(0, Math.round(at(s, "subdivisions", 0))) + 1;
  const steps = (s) => (Math.max(2, Math.round(at(s, "steps", 11))) - 1) * per(s);
  const pull = (s, level) => {
    const v = (o.strength || [1, 1])[level];
    return clamp(typeof v === "string" ? (s[v] ?? 0) : (v ?? 0));
  };
  const snaps = (s) => !!at(s, "snap", 1);

  /** The value as felt while it moves: pulled toward the nearest mark (continuous, never backwards). */
  const detent = (v, s) => {
    const n = steps(s);
    const h = Math.round(v * n);
    const d = v * n - h;
    if (!snaps(s)) {
      // snap off: no pull; a mark held only across a hair, that much taken out of the travel, so there is no jump
      const z = Math.min(0.45, Math.max(0, at(s, "microSnap", 0)));
      if (!z) return v;
      const a = Math.abs(d);
      return (h + (a <= z ? 0 : ((Math.sign(d) * (a - z)) / (0.5 - z)) * 0.5)) / n;
    }
    const k = h % per(s) === 0 ? pull(s, 0) : pull(s, 1);
    return (h + d - (k * Math.sin(2 * Math.PI * d)) / (2 * Math.PI)) / n;
  };

  /** Where it settles: the closest mark of a level that snaps (subdivisions, else majors), else where it is. */
  const nearest = (v, s) => {
    if (!snaps(s)) return v;
    const p = per(s);
    const n = steps(s);
    if (pull(s, 1) > 0 && p > 1) return Math.round(v * n) / n;
    if (pull(s, 0) > 0) return (Math.round((v * n) / p) * p) / n;
    return v;
  };

  /** One step between marks that snap, else a fiftieth of the range. */
  const step = (s) => {
    const minor = pull(s, 1) > 0 && per(s) > 1;
    return snaps(s) && (minor || pull(s, 0) > 0) ? (minor ? 1 : per(s)) / steps(s) : 1 / 50;
  };

  return { at, key, clamp, detent, nearest, step };
}

controls/drag.behaviour.js

From the controls library, shared by the examples.

/**
 * A value dragged by the pointer, from anywhere on the widget, resting on marks (see detents.js). `axis`:
 * - "y": up is more; "x": right is more; "both": up and right both count;
 * - "angle": the value follows the pointer around the widget's centre, clockwise is more, `sweep` degrees (300) for
 *   the whole range: for those who take a knob by its mark and turn it;
 * - "auto": the knob follows the pointer ("angle") wherever it is taken - the knurl, the disk, the arc, the marks -
 *   as a real knob would; a press at its very centre (within `centre`, a share of the width: 0.15), where an angle
 *   means nothing, drags straight ("both");
 * - or the name of a state key holding one of those.
 * `travel` (for the straight axes): the pixels for the whole range, a state key's, or "width" / "height" (its own,
 * less `inset`). Let go, it settles on the nearest mark that snaps.
 */
import { detents } from "./detents.js";

const AXES = ["x", "y", "both", "angle", "auto"];

export default (o) => {
  const d = detents(o);
  const axisOf = (s) => {
    const a = o.axis ?? "y";
    return AXES.includes(a) ? a : AXES.includes(s[a]) ? s[a] : "y";
  };
  const travel = (s, ui) =>
    o.travel === "width" ? (ui?.w ?? 300) - (o.inset ?? 0)
    : o.travel === "height" ? (ui?.h ?? 300) - (o.inset ?? 0)
    : d.at(s, "travel", 300);

  // The turn so far, as a share of the range, added up move by move: each move's angle around the centre against the
  // last one's, so a turn past half a circle never wraps back. Kept in state while the drag lasts (_turn, _turnAt,
  // _turnFrom: the press's angle, which tells a new drag from the one going on), and held within the range, so turning
  // back from a stop moves at once.
  const turned = (s0, dx, dy, ui, s) => {
    if (ui?.x == null || ui?.y == null) return {};
    const cx = ui.w / 2, cy = ui.h / 2;
    const from = Math.atan2(ui.y - dy - cy, ui.x - dx - cx);
    const now = Math.atan2(ui.y - cy, ui.x - cx);
    const same = s && s._turnFrom === from;
    let step = now - (same ? s._turnAt : from);
    if (step > Math.PI) step -= 2 * Math.PI;
    if (step < -Math.PI) step += 2 * Math.PI;
    const sweep = ((o.sweep ?? 300) * Math.PI) / 180;
    const v0 = d.clamp(s0[d.key] ?? 0);
    const turn = Math.min((1 - v0) * sweep, Math.max(-v0 * sweep, (same ? s._turn : 0) + step));
    return { share: turn / sweep, keep: { _turn: turn, _turnAt: now, _turnFrom: from } };
  };
  // auto: taken anywhere but its very centre, it follows the pointer round
  const offCentre = (dx, dy, ui) => ui?.x != null && Math.hypot(ui.x - dx - ui.w / 2, ui.y - dy - ui.h / 2) > (o.centre ?? 0.15) * ui.w;
  // how far the pointer moved along the axes this drag reads (up and right are more), and what to keep meanwhile
  const along = (s0, dx, dy, ui, s, axis) => {
    if (axis === "auto") axis = offCentre(dx, dy, ui) ? "angle" : "both";
    if (axis === "angle") return turned(s0, dx, dy, ui, s);
    return { share: (axis === "x" ? dx : axis === "both" ? dx - dy : -dy) / Math.max(1, travel(s0, ui)) };
  };

  return {
    drag(s0, dx, dy, ui, s) {
      const { share = 0, keep = {} } = along(s0, dx, dy, ui, s, axisOf(s0));
      const v = d.clamp(s0[d.key] ?? 0) + share;
      return { [d.key]: d.clamp(d.detent(d.clamp(v), s0)), ...keep };
    },
    release(s) {
      return { [d.key]: d.clamp(d.nearest(s[d.key] ?? 0, s)) };
    },
  };
};

controls/wheel.behaviour.js

From the controls library, shared by the examples.

/**
 * A value turned by the mouse wheel (or a watch's crown, which falls back to the wheel): a mark per notch when its
 * marks snap, else a fiftieth of the range (see detents.js). `reverse`: true turns it the other way.
 */
import { detents } from "./detents.js";

export default (o) => {
  const d = detents(o);
  return {
    wheel(s, notches) {
      const n = (o.reverse ? -1 : 1) * Math.sign(notches) * Math.max(1, Math.round(Math.abs(notches)));
      return { [d.key]: d.clamp(d.nearest((s[d.key] ?? 0) + n * d.step(s), s)) };
    },
  };
};