Last active
July 10, 2026 00:01
-
-
Save rsms/d7c619a47e8382cba8cdb32ebc913c56 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Mesh gradient editor — interactive MESH_GRADIENT showcase. | |
| // | |
| // A sidebar lists up to 16 color points: click a row to select it, click the | |
| // "+" row to add a point, and DELETE/BACKSPACE removes the selected point | |
| // (the last one always stays). The picker at the bottom-left edits the | |
| // selected color: an HSV square + hue strip working in Display P3, plus | |
| // per-channel HDR gain sliders (100%..400% of SDR) for EDR displays. Points | |
| // are drawn as ring handles over the gradient; drag them to move the colors | |
| // around. Handles fade out when the mouse has been still for two seconds, | |
| // and the layout follows the window size. | |
| // | |
| // The gradient itself is one MESH_GRADIENT command re-submitted every frame: | |
| // scattered {x, y, color} points blended in OKLab. Point positions are kept | |
| // normalized so the mesh stretches with the window. | |
| #include <playbit/playbit.h> | |
| //////////////////////////////////////////////////////////////////////////////////////////////////// | |
| // layout (dp; the picker anchors to the sidebar's bottom-left corner) | |
| enum { | |
| kInitialWindowW = 880, | |
| kInitialWindowH = 720, | |
| kSidebarW = 236, // fits three 64dp gain sliders with 10dp gaps | |
| kRowH = 34, | |
| kSwatchSize = 16, | |
| kSvX = 12, | |
| kSvW = kSidebarW - 2 * kSvX, | |
| kSvH = 176, | |
| kHueH = 16, | |
| kSliderH = 16, | |
| kSliderW = 64, // 16dp per 100%; the whole bar is 400% | |
| kSliderPer100 = 16, // bar dp per 100% intensity | |
| kHandleRadius = 9, | |
| kMaxPoints = 16, | |
| }; | |
| static f32 gWinW = kInitialWindowW; // window content size (dp) | |
| static f32 gWinH = kInitialWindowH; | |
| #define SLIDER_ROW_Y (gWinH - 48) // HDR gain sliders + sun icon | |
| #define HUE_Y (gWinH - 72) | |
| #define SV_Y (gWinH - 72 - 8 - kSvH) | |
| #define CANVAS_W (gWinW - kSidebarW) | |
| static f32 SliderX(u32 channel) { | |
| return (f32)kSvX + (f32)channel * (kSliderW + 10.0f); | |
| } | |
| //////////////////////////////////////////////////////////////////////////////////////////////////// | |
| // lil draw API (see examples/draw-v2.c) | |
| typedef struct PBCanvasDrawCtx { | |
| PBSysHandle canvas; | |
| u64 cmdsStorage[2048]; | |
| PBU64Array cmds; // holds variably-sized PBSysCanvasCmd structs | |
| PBMem ma; | |
| } PBCanvasDrawCtx; | |
| static void BeginDraw(PBCanvasDrawCtx* g, PBSysHandle canvas, PBMem ma) { | |
| g->canvas = canvas; | |
| PBAnyArray_INIT_WITH_STORAGE(g, cmds, cmdsStorage); | |
| g->ma = ma; | |
| } | |
| static void CommitDraw(PBCanvasDrawCtx* g, u64 clearColor) { | |
| const PBSysCanvasCmd* cmds = (const PBSysCanvasCmd*)g->cmds.v; | |
| u32 cmdsSize = g->cmds.len * sizeof(u64); | |
| PBSysErr err = PBSysCanvasRender(g->canvas, clearColor, cmds, cmdsSize, 0); | |
| if UNLIKELY (err && err != PBSysErr_AGAIN) | |
| PanicOnErr(err); | |
| PBArrayFree(&g->cmds, g->ma); | |
| } | |
| static void* AppendRaw(PBCanvasDrawCtx* g, u16 size, u16 kind) { | |
| Expect((size & 7) == 0); | |
| u64* p = PBArrayAlloc(&g->cmds, g->ma, size / sizeof(u64)); | |
| ExpectNotNull(p); | |
| memset(p, 0, size); | |
| PBSysCanvasCmd* cmd = (PBSysCanvasCmd*)p; | |
| cmd->size = size; | |
| cmd->kind = kind; | |
| return cmd; | |
| } | |
| static f32 gDp = 1.0f; // pixels per dp | |
| #define PX(v) ((f32)(v) * gDp) | |
| static PBSysCanvasAABB Box(f32 x, f32 y, f32 w, f32 h) { | |
| return ( | |
| PBSysCanvasAABB){ PBRound(PX(x)), PBRound(PX(y)), PBRound(PX(x + w)), PBRound(PX(y + h)) }; | |
| } | |
| static void FillRect(PBCanvasDrawCtx* g, f32 x, f32 y, f32 w, f32 h, u64 color) { | |
| PBSysCanvasCmdRect* cmd = AppendRaw(g, sizeof(*cmd), PBSysCanvasCmdKind_RECT); | |
| cmd->bounds = Box(x, y, w, h); | |
| cmd->fillColor = color; | |
| } | |
| static void FillRoundedRect(PBCanvasDrawCtx* g, f32 x, f32 y, f32 w, f32 h, u64 color, f32 radius) { | |
| PBSysCanvasCmdRoundedRect* cmd = AppendRaw(g, sizeof(*cmd), PBSysCanvasCmdKind_ROUNDED_RECT); | |
| cmd->bounds = Box(x, y, w, h); | |
| cmd->fillColor = color; | |
| u16 r = (u16)PBRound(PX(radius)); | |
| for (u32 i = 0; i < 4; i++) | |
| cmd->cornerRadius[i] = r; | |
| } | |
| static void StrokeRect( | |
| PBCanvasDrawCtx* g, f32 x, f32 y, f32 w, f32 h, u64 color, f32 width, f32 radius) { | |
| PBSysCanvasCmdStrokeRect* cmd = AppendRaw(g, sizeof(*cmd), PBSysCanvasCmdKind_STROKE_RECT); | |
| cmd->bounds = Box(x, y, w, h); | |
| u16 r = (u16)PBRound(PX(radius)); | |
| for (u32 i = 0; i < 4; i++) | |
| cmd->cornerRadius[i] = r; | |
| cmd->strokeColor = color; | |
| cmd->strokeWidth = PX(width); | |
| } | |
| static void FillDisc(PBCanvasDrawCtx* g, f32 cx, f32 cy, f32 radius, u64 color) { | |
| FillRoundedRect(g, cx - radius, cy - radius, radius * 2, radius * 2, color, radius); | |
| } | |
| // circle outline (a stroked rounded rect whose radius makes it a circle) | |
| static void StrokeDisc(PBCanvasDrawCtx* g, f32 cx, f32 cy, f32 radius, f32 width, u64 color) { | |
| StrokeRect(g, cx - radius, cy - radius, radius * 2, radius * 2, color, width, radius); | |
| } | |
| static void FillGradientRect4( | |
| PBCanvasDrawCtx* g, f32 x, f32 y, f32 w, f32 h, u64 tl, u64 tr, u64 bl, u64 br) { | |
| PBSysCanvasCmdGradientRect* cmd = AppendRaw(g, sizeof(*cmd), PBSysCanvasCmdKind_GRADIENT_RECT); | |
| cmd->bounds = Box(x, y, w, h); | |
| cmd->fillColors[0] = tl; | |
| cmd->fillColors[1] = tr; | |
| cmd->fillColors[2] = bl; | |
| cmd->fillColors[3] = br; | |
| } | |
| static void FillMeshGradient( | |
| PBCanvasDrawCtx* g, | |
| f32 x, | |
| f32 y, | |
| f32 w, | |
| f32 h, | |
| f32 falloff, | |
| const PBSysCanvasMeshPoint* points, | |
| u16 pointCount) { | |
| Expect(pointCount <= PBSysCanvasMeshGradient_MAX_POINTS); | |
| u16 size = (u16)(sizeof(PBSysCanvasCmdMeshGradient) + pointCount * sizeof(*points)); | |
| PBSysCanvasCmdMeshGradient* cmd = AppendRaw(g, size, PBSysCanvasCmdKind_MESH_GRADIENT); | |
| cmd->bounds = Box(x, y, w, h); | |
| cmd->space = PBSysCanvasGradientSpace_OKLAB; | |
| cmd->pointCount = pointCount; | |
| cmd->falloff = falloff; | |
| memcpy(cmd->points, points, pointCount * sizeof(*points)); | |
| } | |
| static void DrawTextPlan(PBCanvasDrawCtx* g, PBSysHandle textPlan, f32 x, f32 y, u64 color) { | |
| if (textPlan == PBSysHandle_INVALID) | |
| return; | |
| PBSysCanvasCmdText* cmd = AppendRaw(g, sizeof(*cmd), PBSysCanvasCmdKind_TEXT); | |
| cmd->bounds = Box(x, y, 0, 0); | |
| cmd->textPlan = textPlan; | |
| cmd->flags = PBSysCanvasTextFlag_OVERRIDE_COLOR; | |
| cmd->fillColor = color; | |
| } | |
| //////////////////////////////////////////////////////////////////////////////////////////////////// | |
| // color: HSV picker semantics on encoded Display P3 values (no sRGB gamut | |
| // mapping). Wire colors are the renderer's native space, linear extended P3: | |
| // encoded components decode through the P3 transfer curve (the sRGB curve | |
| // over P3 primaries) and per-channel HDR gains scale the linear values past | |
| // the SDR ceiling. | |
| typedef struct Hsv { | |
| f32 h, s, v; // all [0, 1] | |
| } Hsv; | |
| static void HsvToEncodedP3(Hsv c, f32* r, f32* g, f32* b) { | |
| f32 h = c.h >= 1.0f ? 0.0f : c.h * 6.0f; | |
| i32 sector = (i32)h; | |
| f32 f = h - (f32)sector; | |
| f32 p = c.v * (1.0f - c.s); | |
| f32 q = c.v * (1.0f - c.s * f); | |
| f32 t = c.v * (1.0f - c.s * (1.0f - f)); | |
| switch (sector) { | |
| case 0: *r = c.v, *g = t, *b = p; break; | |
| case 1: *r = q, *g = c.v, *b = p; break; | |
| case 2: *r = p, *g = c.v, *b = t; break; | |
| case 3: *r = p, *g = q, *b = c.v; break; | |
| case 4: *r = t, *g = p, *b = c.v; break; | |
| default: *r = c.v, *g = p, *b = q; break; | |
| } | |
| } | |
| // Display P3 transfer decode | |
| static f32 DecodeP3(f32 v) { | |
| return v <= 0.04045f ? v / 12.92f : PBPowF32((v + 0.055f) / 1.055f, 2.4f); | |
| } | |
| static u64 HsvWireColorHDR(Hsv c, const f32 gain[3]) { | |
| f32 r, g, b; | |
| HsvToEncodedP3(c, &r, &g, &b); | |
| return PBColorRgbHDR(DecodeP3(r) * gain[0], DecodeP3(g) * gain[1], DecodeP3(b) * gain[2]).u64; | |
| } | |
| static u64 HsvWireColor(Hsv c) { // SDR (UI chrome: swatches, knobs, picker) | |
| f32 r, g, b; | |
| HsvToEncodedP3(c, &r, &g, &b); | |
| return PBColorRgb(DecodeP3(r), DecodeP3(g), DecodeP3(b)).u64; | |
| } | |
| static void FillHueStrip(PBCanvasDrawCtx* g, f32 x, f32 y, f32 w, f32 h, f32 radius) { | |
| PBSysCanvasGradientStop stops[7]; | |
| for (u32 i = 0; i < 7; i++) { | |
| stops[i].offset = (f32)i / 6.0f; | |
| stops[i]._reserved = 0; | |
| stops[i].color = HsvWireColor((Hsv){ (f32)i / 6.0f, 1.0f, 1.0f }); | |
| } | |
| u16 size = (u16)(sizeof(PBSysCanvasCmdGradient) + 7 * sizeof(stops[0])); | |
| PBSysCanvasCmdGradient* cmd = AppendRaw(g, size, PBSysCanvasCmdKind_GRADIENT); | |
| cmd->bounds = Box(x, y, w, h); | |
| u16 r = (u16)PBRound(PX(radius)); | |
| for (u32 i = 0; i < 4; i++) | |
| cmd->cornerRadius[i] = r; | |
| cmd->kind = PBSysCanvasGradientKind_LINEAR; | |
| cmd->space = PBSysCanvasGradientSpace_LINEAR_P3; | |
| cmd->spread = PBSysCanvasGradientSpread_PAD; | |
| cmd->stopCount = 7; | |
| cmd->p1[0] = PX(w); | |
| memcpy(cmd->stops, stops, sizeof(stops)); | |
| } | |
| //////////////////////////////////////////////////////////////////////////////////////////////////// | |
| // app state | |
| typedef struct Point { | |
| f32 x, y; // normalized [0, 1] within the canvas area | |
| Hsv hsv; // encoded Display P3 HSV | |
| f32 gain[3]; // per-channel linear HDR gain, [1, 4] (1 = SDR) | |
| } Point; | |
| typedef enum DragMode { DRAG_NONE, DRAG_HANDLE, DRAG_SV, DRAG_HUE, DRAG_SLIDER } DragMode; | |
| // cached value labels for one sidebar row (rebuilt when the color changes) | |
| typedef struct RowLabels { | |
| PBSysHandle plan[3]; | |
| f32 vals[3]; | |
| bool valid; | |
| } RowLabels; | |
| // cached "%d%%" labels for the gain sliders (showing the selected point) | |
| typedef struct GainLabels { | |
| PBSysHandle plan[3]; | |
| i32 pct[3]; | |
| bool valid; | |
| } GainLabels; | |
| static bool gHdrSupported = false; // PBSysCanvasFeature_HDR on the surface canvas | |
| static struct { | |
| Point points[kMaxPoints]; | |
| u32 count; | |
| u32 sel; | |
| DragMode drag; | |
| u32 dragChannel; // for DRAG_SLIDER | |
| f32 dragStartX; // pointer x at DRAG_SLIDER start (dp) | |
| f32 dragStartGain; // gain at DRAG_SLIDER start | |
| PBTime lastPointerMove; | |
| RowLabels rowLabels[kMaxPoints]; | |
| GainLabels gainLabels; | |
| } gApp; | |
| static PBSysHandle MakeLabelStr(const char* text, f32 sizeDp, f32 weight) { | |
| usize len = strlen(text); | |
| PBSysPositionedTextAttribute attributes[] = { | |
| { .start = 0, .attribute = { .type = PBSysTextAttribute_SIZE, .f = sizeDp * gDp } }, | |
| { .start = 0, .attribute = { .type = PBSysTextAttribute_WEIGHT, .f = weight } }, | |
| // tabular numerals so value columns don't shift as digits change | |
| { .start = 0, | |
| .attribute = { .type = PBSysTextAttribute_OTYPE_FEATURE, | |
| .feature = { .tag = ('t' << 24) | ('n' << 16) | ('u' << 8) | 'm', | |
| .value = 1 } } }, | |
| }; | |
| PBSysHandle textPlan = PBSysWindowTextplanCreate2( | |
| PBWindowMain().handle, | |
| (const u8*)text, | |
| len, | |
| attributes, | |
| sizeof(attributes[0]), | |
| PBCountOf(attributes)); | |
| Expect(textPlan > 0); | |
| PanicOnErr(PBSysTextplanLayout(textPlan, PBTextPlanLayout_ONE_LINE, 0, 0)); | |
| return textPlan; | |
| } | |
| // returns the row's cached r/g/b value labels, rebuilding them if the color | |
| // changed since last frame | |
| static const RowLabels* RowLabelsFor(u32 index) { | |
| RowLabels* labels = &gApp.rowLabels[index]; | |
| f32 rgb[3]; | |
| HsvToEncodedP3(gApp.points[index].hsv, &rgb[0], &rgb[1], &rgb[2]); | |
| if (labels->valid && labels->vals[0] == rgb[0] && labels->vals[1] == rgb[1] | |
| && labels->vals[2] == rgb[2]) | |
| { | |
| return labels; | |
| } | |
| for (u32 i = 0; i < 3; i++) { | |
| if (labels->valid) | |
| (void)PBSysHandleClose(labels->plan[i]); | |
| // "0.478", "0.62", "1.0": three decimals, trailing zeros trimmed, | |
| // but always keeping one decimal | |
| char buf[16]; | |
| u32 n = PBSnprintf(buf, sizeof(buf), "%.3f", (f64)rgb[i]); | |
| while (n > 3 && buf[n - 1] == '0') | |
| buf[--n] = 0; | |
| labels->plan[i] = MakeLabelStr(buf, 12, 500); | |
| labels->vals[i] = rgb[i]; | |
| } | |
| labels->valid = true; | |
| return labels; | |
| } | |
| static void InvalidateRowLabels(void) { | |
| for (u32 i = 0; i < kMaxPoints; i++) { | |
| if (gApp.rowLabels[i].valid) { | |
| for (u32 c = 0; c < 3; c++) | |
| (void)PBSysHandleClose(gApp.rowLabels[i].plan[c]); | |
| gApp.rowLabels[i].valid = false; | |
| } | |
| } | |
| } | |
| static const GainLabels* GainLabelsForSelected(void) { | |
| GainLabels* labels = &gApp.gainLabels; | |
| i32 pct[3]; | |
| for (u32 i = 0; i < 3; i++) | |
| pct[i] = (i32)PBRound(gApp.points[gApp.sel].gain[i] * 100.0f); | |
| if (labels->valid && labels->pct[0] == pct[0] && labels->pct[1] == pct[1] | |
| && labels->pct[2] == pct[2]) | |
| { | |
| return labels; | |
| } | |
| for (u32 i = 0; i < 3; i++) { | |
| if (labels->valid) | |
| (void)PBSysHandleClose(labels->plan[i]); | |
| char buf[8]; | |
| PBSnprintf(buf, sizeof(buf), "%d%%", pct[i]); | |
| labels->plan[i] = MakeLabelStr(buf, 11, 500); | |
| labels->pct[i] = pct[i]; | |
| } | |
| labels->valid = true; | |
| return labels; | |
| } | |
| static void AddPoint(f32 x, f32 y, Hsv hsv) { | |
| if (gApp.count >= kMaxPoints) | |
| return; | |
| gApp.points[gApp.count] = (Point){ .x = x, .y = y, .hsv = hsv, .gain = { 1, 1, 1 } }; | |
| gApp.sel = gApp.count; | |
| gApp.count++; | |
| } | |
| // removes the selected point; the last remaining point cannot be removed | |
| static void RemoveSelectedPoint(void) { | |
| if (gApp.count <= 1) | |
| return; | |
| memmove( | |
| &gApp.points[gApp.sel], | |
| &gApp.points[gApp.sel + 1], | |
| (gApp.count - gApp.sel - 1) * sizeof(Point)); | |
| gApp.count--; | |
| if (gApp.sel >= gApp.count) | |
| gApp.sel = gApp.count - 1; | |
| // indices shifted: drop all cached labels and let them rebuild lazily | |
| InvalidateRowLabels(); | |
| } | |
| //////////////////////////////////////////////////////////////////////////////////////////////////// | |
| // drawing | |
| static const u64 kInk = PB_COLOR_RGB(0.015f, 0.017f, 0.022f).u64; | |
| static const u64 kSeparator = PB_COLOR_RGB(0.78f, 0.78f, 0.79f).u64; | |
| static const u64 kSelectedBg = PB_COLOR_RGB(0.018f, 0.15f, 0.86f).u64; | |
| static const u64 kGray = PB_COLOR_RGB(0.26f, 0.27f, 0.29f).u64; | |
| // pastel channel tints for the gain slider fills (linear P3) | |
| static const u64 kTintR = PB_COLOR_RGB(0.93f, 0.30f, 0.17f).u64; | |
| static const u64 kTintG = PB_COLOR_RGB(0.27f, 0.70f, 0.25f).u64; | |
| static const u64 kTintB = PB_COLOR_RGB(0.20f, 0.38f, 0.87f).u64; | |
| static const u64 kSliderBorder = PB_COLOR_RGBA(0, 0, 0, 0.38f).u64; | |
| static void DrawSunIcon(PBCanvasDrawCtx* g, f32 cx, f32 cy) { | |
| StrokeDisc(g, cx, cy, 3.5f, 1.5f, kGray); | |
| FillRect(g, cx - 7, cy - 0.5f, 2.5f, 1, kGray); | |
| FillRect(g, cx + 4.5f, cy - 0.5f, 2.5f, 1, kGray); | |
| FillRect(g, cx - 0.5f, cy - 7, 1, 2.5f, kGray); | |
| FillRect(g, cx - 0.5f, cy + 4.5f, 1, 2.5f, kGray); | |
| } | |
| static void DrawGainSliders(PBCanvasDrawCtx* g) { | |
| const u64 tints[3] = { kTintR, kTintG, kTintB }; | |
| const Point* p = &gApp.points[gApp.sel]; | |
| const GainLabels* labels = GainLabelsForSelected(); | |
| DrawSunIcon(g, 20, SLIDER_ROW_Y + kSliderH + 13); | |
| for (u32 i = 0; i < 3; i++) { | |
| f32 x = SliderX(i); | |
| f32 y = SLIDER_ROW_Y; | |
| FillRect(g, x, y, kSliderW, kSliderH, PBColor_White.u64); | |
| // the bar directly represents intensity: 100% = 16dp, 400% = full | |
| FillRect(g, x, y, p->gain[i] * kSliderPer100, kSliderH, tints[i]); | |
| // ticks every 16dp (at each 100% step) | |
| for (u32 tk = 1; tk < 4; tk++) | |
| FillRect(g, x + (f32)(kSliderPer100 * tk), y + kSliderH - 4, 1, 4, kSliderBorder); | |
| // border last, on top of the bar | |
| StrokeRect(g, x, y, kSliderW, kSliderH, kSliderBorder, 1, 0); | |
| // right-aligned percent label (tabular digits: all values same width) | |
| DrawTextPlan(g, labels->plan[i], x + kSliderW - 32, y + kSliderH + 6, kInk); | |
| } | |
| } | |
| static void DrawSidebar(PBCanvasDrawCtx* g) { | |
| FillRect(g, 0, 0, kSidebarW, gWinH, PBColor_White.u64); | |
| // point rows | |
| for (u32 i = 0; i < gApp.count; i++) { | |
| f32 y = (f32)(i * kRowH); | |
| bool selected = i == gApp.sel; | |
| if (selected) | |
| FillRect(g, 0, y, kSidebarW, kRowH, kSelectedBg); | |
| FillRoundedRect( | |
| g, 12, y + 9, kSwatchSize, kSwatchSize, HsvWireColor(gApp.points[i].hsv), 4); | |
| const RowLabels* labels = RowLabelsFor(i); | |
| u64 textColor = selected ? PBColor_White.u64 : kInk; | |
| DrawTextPlan(g, labels->plan[0], 40, y + 9, textColor); | |
| DrawTextPlan(g, labels->plan[1], 87, y + 9, textColor); | |
| DrawTextPlan(g, labels->plan[2], 134, y + 9, textColor); | |
| if (!selected) | |
| FillRect(g, 0, y + kRowH - 1, kSidebarW, 1, kSeparator); | |
| } | |
| // "+" row (hidden once the mesh is full) | |
| if (gApp.count < kMaxPoints) { | |
| f32 y = (f32)(gApp.count * kRowH); | |
| f32 cx = kSidebarW / 2.0f; | |
| f32 cy = y + kRowH / 2.0f; | |
| FillRect(g, cx - 7, cy - 1, 14, 2, kGray); | |
| FillRect(g, cx - 1, cy - 7, 2, 14, kGray); | |
| FillRect(g, 0, y + kRowH - 1, kSidebarW, 1, kSeparator); | |
| } | |
| // HSV picker for the selected point | |
| Hsv sel = gApp.points[gApp.sel].hsv; | |
| // saturation/value square: white -> hue across, black downward | |
| u64 hue = HsvWireColor((Hsv){ sel.h, 1.0f, 1.0f }); | |
| FillGradientRect4( | |
| g, kSvX, SV_Y, kSvW, kSvH, PBColor_White.u64, hue, PBColor_Black.u64, PBColor_Black.u64); | |
| // knob | |
| f32 kx = kSvX + sel.s * kSvW; | |
| f32 ky = SV_Y + (1.0f - sel.v) * kSvH; | |
| StrokeDisc(g, kx, ky, 5, 2, PBColor_White.u64); | |
| FillDisc(g, kx, ky, 3.5f, HsvWireColor(sel)); | |
| // hue strip + knob | |
| FillHueStrip(g, kSvX, HUE_Y, kSvW, kHueH, kHueH / 2.0f); | |
| f32 hx = kSvX + kHueH / 2.0f + sel.h * (kSvW - kHueH); | |
| StrokeDisc(g, hx, HUE_Y + kHueH / 2.0f, 5, 2, PBColor_White.u64); | |
| FillDisc(g, hx, HUE_Y + kHueH / 2.0f, 3.5f, HsvWireColor((Hsv){ sel.h, 1, 1 })); | |
| if (gHdrSupported) | |
| DrawGainSliders(g); | |
| } | |
| static void Draw(PBSysHandle canvas) { | |
| // follow the window size (the picker anchors bottom-left, the mesh | |
| // stretches over the whole canvas area) | |
| PBSize size = PBWindowContentSize(PBWindowMain()); | |
| if (size.width > kSidebarW && size.height > 0) { | |
| gWinW = size.width; | |
| gWinH = size.height; | |
| } | |
| PBCanvasDrawCtx g; | |
| BeginDraw(&g, canvas, kMemGPA); | |
| // the mesh gradient fills the canvas area (point coords are normalized) | |
| PBSysCanvasMeshPoint meshPoints[kMaxPoints]; | |
| for (u32 i = 0; i < gApp.count; i++) { | |
| meshPoints[i] = (PBSysCanvasMeshPoint){ | |
| .x = PX(gApp.points[i].x * CANVAS_W), | |
| .y = PX(gApp.points[i].y * gWinH), | |
| .weight = 1.0f, | |
| .color = HsvWireColorHDR(gApp.points[i].hsv, gApp.points[i].gain), | |
| }; | |
| } | |
| FillMeshGradient(&g, kSidebarW, 0, CANVAS_W, gWinH, 2.2f, meshPoints, (u16)gApp.count); | |
| // point handles fade out when the mouse has been still for 2 seconds | |
| if (PBTimeNow() - gApp.lastPointerMove < 2 * PB_TIME_SECOND) { | |
| for (u32 i = 0; i < gApp.count; i++) { | |
| f32 cx = kSidebarW + gApp.points[i].x * CANVAS_W; | |
| f32 cy = gApp.points[i].y * gWinH; | |
| StrokeDisc(&g, cx, cy, kHandleRadius, 3, PBColor_White.u64); | |
| } | |
| } | |
| DrawSidebar(&g); | |
| CommitDraw(&g, PBColor_White.u64); | |
| } | |
| //////////////////////////////////////////////////////////////////////////////////////////////////// | |
| // interaction (pointer coordinates arrive in window dp) | |
| static void ApplyDrag(f32 x, f32 y) { | |
| Point* p = &gApp.points[gApp.sel]; | |
| switch (gApp.drag) { | |
| case DRAG_HANDLE: | |
| p->x = PBClamp((x - kSidebarW) / CANVAS_W, 0.0f, 1.0f); | |
| p->y = PBClamp(y / gWinH, 0.0f, 1.0f); | |
| break; | |
| case DRAG_SV: | |
| p->hsv.s = PBClamp((x - kSvX) / kSvW, 0.0f, 1.0f); | |
| p->hsv.v = 1.0f - PBClamp((y - SV_Y) / kSvH, 0.0f, 1.0f); | |
| break; | |
| case DRAG_HUE: | |
| p->hsv.h = PBClamp((x - (kSvX + kHueH / 2.0f)) / (kSvW - kHueH), 0.0f, 1.0f); | |
| break; | |
| case DRAG_SLIDER: | |
| // relative: the bar moves by the delta dragged (16dp per 100%) | |
| p->gain[gApp.dragChannel] = PBClamp( | |
| gApp.dragStartGain + (x - gApp.dragStartX) / (f32)kSliderPer100, 1.0f, 4.0f); | |
| break; | |
| case DRAG_NONE: break; | |
| } | |
| } | |
| static void OnPointerDown(f32 x, f32 y) { | |
| // canvas: grab the nearest handle within reach | |
| if (x >= kSidebarW) { | |
| f32 bestDist = 16.0f; // grab radius (dp) | |
| i32 best = -1; | |
| for (u32 i = 0; i < gApp.count; i++) { | |
| f32 dx = x - (kSidebarW + gApp.points[i].x * CANVAS_W); | |
| f32 dy = y - gApp.points[i].y * gWinH; | |
| f32 d = PBSqrtF32(dx * dx + dy * dy); | |
| if (d < bestDist) { | |
| bestDist = d; | |
| best = (i32)i; | |
| } | |
| } | |
| if (best >= 0) { | |
| gApp.sel = (u32)best; | |
| gApp.drag = DRAG_HANDLE; | |
| ApplyDrag(x, y); | |
| } | |
| return; | |
| } | |
| // picker | |
| if (x >= kSvX && x < kSvX + kSvW && y >= SV_Y && y < SV_Y + kSvH) { | |
| gApp.drag = DRAG_SV; | |
| ApplyDrag(x, y); | |
| return; | |
| } | |
| if (x >= kSvX && x < kSvX + kSvW && y >= HUE_Y - 4 && y < HUE_Y + kHueH + 4) { | |
| gApp.drag = DRAG_HUE; | |
| ApplyDrag(x, y); | |
| return; | |
| } | |
| if (gHdrSupported && y >= SLIDER_ROW_Y - 2 && y < SLIDER_ROW_Y + kSliderH + 2) { | |
| for (u32 i = 0; i < 3; i++) { | |
| if (x >= SliderX(i) - 2 && x < SliderX(i) + kSliderW + 2) { | |
| gApp.drag = DRAG_SLIDER; | |
| gApp.dragChannel = i; | |
| gApp.dragStartX = x; | |
| gApp.dragStartGain = gApp.points[gApp.sel].gain[i]; | |
| return; | |
| } | |
| } | |
| } | |
| // rows | |
| u32 row = (u32)(y / kRowH); | |
| if (row < gApp.count) { | |
| gApp.sel = row; | |
| } else if (row == gApp.count && gApp.count < kMaxPoints) { | |
| // "+": add a point in the middle of the canvas with a fresh hue | |
| Hsv hsv = { .h = (f32)(gApp.count * 5 % 16) / 16.0f, .s = 0.75f, .v = 0.95f }; | |
| AddPoint(0.5f, 0.5f, hsv); | |
| } | |
| } | |
| void main(void) { | |
| PBSysHandle window = PBWindowMain().handle; | |
| Expect(window != PBSysHandle_INVALID); | |
| PBObserve(window, U32_MAX); | |
| PanicOnErr(PBSysWindowFrameSyncEnable(window, 1)); | |
| PBWindowSetContentSizeCentered(PBWindowMain(), kInitialWindowW, kInitialWindowH); | |
| gDp = PBWindowScaleFactor(PBWindowMain()); | |
| PBSysHandle canvas = PBSysWindowOpenSurfaceCanvas(window, 0); | |
| Expect(canvas != PBSysHandle_INVALID); | |
| // the HDR gain sliders only appear when the display can actually show | |
| // extended-range colors | |
| gHdrSupported = (PBSysObjectFeatures(canvas) & (1llu << PBSysCanvasFeature_HDR)) != 0; | |
| // the four starter points from the design mock | |
| AddPoint(0.85f, 0.06f, (Hsv){ 0.594f, 0.522f, 1.0f }); // periwinkle | |
| AddPoint(0.29f, 0.22f, (Hsv){ 0.424f, 0.816f, 0.255f }); // dark green | |
| AddPoint(0.87f, 0.56f, (Hsv){ 0.097f, 1.0f, 1.0f }); // orange | |
| AddPoint(0.28f, 0.96f, (Hsv){ 0.900f, 0.148f, 0.98f }); // pink | |
| gApp.sel = 2; | |
| gApp.lastPointerMove = PBTimeNow(); | |
| Draw(canvas); // render the initial frame explicitly (reveals the window) | |
| for (PBEventAny ev; PBEventPoll(&ev.event, sizeof(ev), 1);) { | |
| switch (ev.type) { | |
| case PBEventType_POINTER_DOWN: | |
| gApp.lastPointerMove = PBTimeNow(); | |
| OnPointerDown(ev.pointer.x, ev.pointer.y); | |
| break; | |
| case PBEventType_POINTER_MOVE: | |
| gApp.lastPointerMove = PBTimeNow(); | |
| if (gApp.drag != DRAG_NONE) | |
| ApplyDrag(ev.pointer.x, ev.pointer.y); | |
| break; | |
| case PBEventType_POINTER_UP: | |
| case PBEventType_POINTER_CANCEL: gApp.drag = DRAG_NONE; break; | |
| case PBEventType_KEY_DOWN: | |
| if (ev.keyboard.keyCode == PBKeyboardKey_Delete | |
| || ev.keyboard.keyCode == PBKeyboardKey_Backspace) | |
| { | |
| RemoveSelectedPoint(); | |
| } | |
| break; | |
| case PBEventType_SIGNAL: | |
| if (ev.signal.handle == window | |
| && (ev.signal.signals | |
| & (PBSysWindowSignal_FRAME_SYNC | PBSysWindowSignal_RESIZE | |
| | PBSysWindowSignal_REPAINT))) | |
| { | |
| Draw(canvas); | |
| } | |
| break; | |
| default: break; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment