UNPKG

@thi.ng/imgui

Version:

Immediate mode GUI with flexible state handling & data only shape output

154 lines (153 loc) 4.05 kB
import { pointInRect } from "@thi.ng/geom-isec/point"; import { polygon } from "@thi.ng/geom/polygon"; import { isLayout } from "@thi.ng/layout/checks"; import { HALF_PI, PI, TAU } from "@thi.ng/math/api"; import { fitClamped, norm } from "@thi.ng/math/fit"; import { clamp } from "@thi.ng/math/interval"; import { mix } from "@thi.ng/math/mix"; import { map } from "@thi.ng/transducers/map"; import { normRange } from "@thi.ng/transducers/norm-range"; import { cartesian2 } from "@thi.ng/vectors/cartesian"; import { hash } from "@thi.ng/vectors/hash"; import { dialVal } from "../behaviors/dial.js"; import { handleSlider1Keys } from "../behaviors/slider.js"; import { dialValueLabel } from "./textlabel.js"; import { tooltipRaw } from "./tooltip.js"; const ring = ({ gui, layout, id, min, max, step, value, gap = Math.PI, rscale = 0.5, label, info, fmt }) => { let h; let box; if (isLayout(layout)) { h = __ringHeight(layout.cellW, gap); box = layout.next([1, layout.rowsForHeight(h) + 1]); } else { h = __ringHeight(layout.cw, gap); box = layout; } return ringRaw( gui, id, box.x, box.y, box.w, h, min, max, step, value, gap, rscale, 0, h + box.ch / 2 + gui.theme.baseLine, label, fmt, info ); }; const ringGroup = (opts) => { const { layout, id, value, label, info } = opts; const n = value.length; const nested = opts.horizontal !== false ? layout.nest(n, [n, 1]) : layout.nest(1, [ 1, (layout.rowsForHeight( __ringHeight(layout.cellW, opts.gap ?? Math.PI) ) + 1) * n ]); let res; let idx = -1; for (let i = 0; i < n; i++) { const v = ring({ ...opts, layout: nested, id: `${id}-${i}`, value: value[i], label: label[i], info: info?.[i] }); if (v !== void 0) { res = v; idx = i; } } return res !== void 0 ? [idx, res] : void 0; }; const ringRaw = (gui, id, x, y, w, h, min, max, step, value, thetaGap, rscale, labelX, labelY, label, fmt, info) => { const r = w / 2; const key = hash([x, y, r]); gui.registerID(id, key); const pos = [x + r, y + r]; const startTheta = HALF_PI + thetaGap / 2; const endTheta = HALF_PI + TAU - thetaGap / 2; const draw = gui.draw; const aid = gui.activeID; const hover = !gui.disabled && (aid === id || aid === "" && pointInRect(gui.mouse, [x, y], [w, h])); let v = clamp(value, min, max); let res; if (hover) { gui.setCursor("pointer"); gui.hotID = id; if (gui.isMouseDown()) { gui.activeID = id; res = dialVal(gui.mouse, pos, startTheta, thetaGap, min, max, step); } info && draw && tooltipRaw(gui, info); } const focused = gui.requestFocus(id); if (draw) { const valTheta = startTheta + (TAU - thetaGap) * norm(v, min, max); const r2 = r * rscale; const numV = fitClamped(r, 15, 80, 12, 30); const shape = (max2) => () => polygon( [ ...__arcVerts(pos, r, startTheta, max2, numV), ...__arcVerts(pos, r2, max2, startTheta, numV) ], {} ); const bgShape = gui.resource(id, key, shape(endTheta)); const valShape = gui.resource(id, v, shape(valTheta)); const valLabel = dialValueLabel( gui, id, key, v, x + labelX, y + labelY, label, fmt ); bgShape.attribs.fill = gui.bgColor(hover || focused); bgShape.attribs.stroke = gui.focusColor(id); valShape.attribs.fill = gui.fgColor(hover); gui.add(bgShape, valShape, valLabel); } if (focused && (v = handleSlider1Keys(gui, min, max, step, v)) !== void 0) { return v; } gui.lastID = id; return res; }; const __ringHeight = (w, thetaGap) => w / 2 * (1 + Math.sin(HALF_PI + thetaGap / 2)); const __arcVerts = (o, r, start, end, thetaRes = 12) => r > 1 ? map( (t) => cartesian2(null, [r, mix(start, end, t)], o), normRange( Math.max(1, Math.abs(end - start) / (PI / thetaRes)) | 0 ) ) : [o]; export { ring, ringGroup, ringRaw };