Current section
Files
Jump to
Current section
Files
examples/widgets/color_picker.ex
defmodule ColorPickerWidget do
@moduledoc """
Canvas-based HSV color picker widget.
Renders a hue ring surrounding a saturation/value square. The ring is built
from path segments covering the hue spectrum. The SV square uses overlapping
linear gradients (hue-to-white horizontal, transparent-to-black vertical).
Cursor circles mark the current hue position on the ring and the current
SV position in the square.
ColorPickerWidget.render("picker", model.hue, model.saturation, model.value)
Events: `Plushie.Event.Canvas` with `type: :press`, `:move`, and `:release`.
The canvas reports absolute x/y coordinates so the consuming app can compute
hue angles and SV positions from the geometry constants.
"""
import Plushie.Canvas.Shape, only: [move_to: 2, line_to: 2, close: 0]
# -- Geometry constants ------------------------------------------------------
@canvas_size 400
@cx div(@canvas_size, 2)
@cy div(@canvas_size, 2)
@outer_r 190
@inner_r 150
@mid_r div(@inner_r + @outer_r, 2)
@sq_origin 100
@sq_size 200
@segments 72
@cursor_r 7
@doc """
Renders the color picker canvas.
Returns a canvas node with interactive hue ring and SV square.
## Options
- No options currently defined; reserved for future use.
"""
def render(id, hue, saturation, value, _opts \\ []) do
import Plushie.UI
canvas id,
width: @canvas_size,
height: @canvas_size,
on_press: true,
on_release: true,
on_move: true do
layer "a_ring" do
ring_shapes()
end
layer "b_sv_hue" do
sv_hue_shapes(hue)
end
layer "c_sv_dark" do
sv_dark_shapes()
end
layer "d_cursors" do
cursor_shapes(hue, saturation, value)
end
end
end
# -- Geometry accessors (for consumers doing hit testing) --------------------
@doc "Canvas size in pixels (square)."
def canvas_size, do: @canvas_size
@doc "Centre x coordinate."
def cx, do: @cx
@doc "Centre y coordinate."
def cy, do: @cy
@doc "Inner radius of the hue ring."
def inner_r, do: @inner_r
@doc "Outer radius of the hue ring."
def outer_r, do: @outer_r
@doc "Origin (top-left x and y) of the SV square."
def sq_origin, do: @sq_origin
@doc "Side length of the SV square."
def sq_size, do: @sq_size
# -- Ring layer --------------------------------------------------------------
defp ring_shapes do
alias Plushie.Canvas.Shape
deg_per_segment = 360 / @segments
for i <- 0..(@segments - 1) do
hue_deg = i * deg_per_segment
a1 = (hue_deg - 90) * :math.pi() / 180
a2 = (hue_deg + deg_per_segment - 90) * :math.pi() / 180
Shape.path(
[
move_to(@cx + @inner_r * :math.cos(a1), @cy + @inner_r * :math.sin(a1)),
line_to(@cx + @outer_r * :math.cos(a1), @cy + @outer_r * :math.sin(a1)),
line_to(@cx + @outer_r * :math.cos(a2), @cy + @outer_r * :math.sin(a2)),
line_to(@cx + @inner_r * :math.cos(a2), @cy + @inner_r * :math.sin(a2)),
close()
],
fill: hsv_to_hex(hue_deg, 1.0, 1.0)
)
end
end
# -- SV layers ---------------------------------------------------------------
defp sv_hue_shapes(hue) do
alias Plushie.Canvas.Shape
hue_color = hsv_to_hex(hue, 1.0, 1.0)
[
Shape.rect(@sq_origin, @sq_origin, @sq_size, @sq_size,
fill:
Shape.linear_gradient(
{@sq_origin, @sq_origin},
{@sq_origin + @sq_size, @sq_origin},
[{0.0, "#ffffff"}, {1.0, hue_color}]
)
)
]
end
defp sv_dark_shapes do
alias Plushie.Canvas.Shape
[
Shape.rect(@sq_origin, @sq_origin, @sq_size, @sq_size,
fill:
Shape.linear_gradient(
{@sq_origin, @sq_origin},
{@sq_origin, @sq_origin + @sq_size},
[{0.0, "#00000000"}, {1.0, "#000000ff"}]
)
)
]
end
# -- Cursors -----------------------------------------------------------------
defp cursor_shapes(hue, saturation, value) do
alias Plushie.Canvas.Shape
angle = (hue - 90) * :math.pi() / 180
ring_x = @cx + @mid_r * :math.cos(angle)
ring_y = @cy + @mid_r * :math.sin(angle)
sv_x = @sq_origin + saturation * @sq_size
sv_y = @sq_origin + (1.0 - value) * @sq_size
cursor_stroke = Shape.stroke("#333333", 2)
[
Shape.circle(ring_x, ring_y, @cursor_r, fill: "#ffffff", stroke: cursor_stroke),
Shape.circle(sv_x, sv_y, @cursor_r, fill: "#ffffff", stroke: cursor_stroke)
]
end
# -- Color conversion --------------------------------------------------------
defp hsv_to_hex(h, s, v) do
h = fmod(h, 360.0)
h = if h < 0, do: h + 360.0, else: h
c = v * s
h_sector = h / 60.0
x = c * (1.0 - abs(fmod(h_sector, 2.0) - 1.0))
m = v - c
{r1, g1, b1} =
cond do
h_sector < 1 -> {c, x, 0.0}
h_sector < 2 -> {x, c, 0.0}
h_sector < 3 -> {0.0, c, x}
h_sector < 4 -> {0.0, x, c}
h_sector < 5 -> {x, 0.0, c}
true -> {c, 0.0, x}
end
r = round((r1 + m) * 255)
g = round((g1 + m) * 255)
b = round((b1 + m) * 255)
"##{hex_byte(r)}#{hex_byte(g)}#{hex_byte(b)}"
end
defp hex_byte(n) do
n
|> max(0)
|> min(255)
|> Integer.to_string(16)
|> String.pad_leading(2, "0")
|> String.downcase()
end
defp fmod(a, b), do: a - b * Float.floor(a / b)
end