RGB color mixer with per-nibble hex control and touch strip display
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/image/font"
|
||||||
"qrun/lib/streamdeck"
|
"qrun/lib/streamdeck"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,52 +33,60 @@ func main() {
|
|||||||
|
|
||||||
dev.SetBrightness(80)
|
dev.SetBrightness(80)
|
||||||
|
|
||||||
|
stripFont := streamdeck.LoadFace("fonts/AtkinsonHyperlegibleMono-Bold.ttf", 28)
|
||||||
|
|
||||||
rgb := [3]int{0, 0, 0}
|
rgb := [3]int{0, 0, 0}
|
||||||
fine := [3]bool{false, false, false}
|
hiNibble := [3]bool{false, false, false}
|
||||||
labels := [3]string{"R", "G", "B"}
|
names := [3]string{"Red", "Green", "Blue"}
|
||||||
labelColors := [3]color.RGBA{
|
chanColors := [3]color.RGBA{
|
||||||
{255, 0, 0, 255},
|
{255, 80, 80, 255},
|
||||||
{0, 255, 0, 255},
|
{80, 255, 80, 255},
|
||||||
{0, 100, 255, 255},
|
{80, 160, 255, 255},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m := dev.Model()
|
||||||
|
segW := m.LCDWidth / 4
|
||||||
|
half := m.LCDHeight / 2
|
||||||
|
|
||||||
updateLCD := func() {
|
updateLCD := func() {
|
||||||
c := color.RGBA{uint8(rgb[0]), uint8(rgb[1]), uint8(rgb[2]), 255}
|
bg := color.RGBA{uint8(rgb[0]), uint8(rgb[1]), uint8(rgb[2]), 255}
|
||||||
dev.SetLCDColor(0, 0, dev.Model().LCDWidth, dev.Model().LCDHeight, c)
|
img := image.NewRGBA(image.Rect(0, 0, m.LCDWidth, m.LCDHeight))
|
||||||
}
|
draw.Draw(img, img.Bounds(), &image.Uniform{bg}, image.Point{}, draw.Src)
|
||||||
|
|
||||||
updateKey := func(i int) {
|
|
||||||
bg := color.RGBA{labelColors[i].R / 4, labelColors[i].G / 4, labelColors[i].B / 4, 255}
|
|
||||||
sz := dev.Model().KeySize
|
|
||||||
txt := streamdeck.TextImageWithFaceSized(streamdeck.MonoBoldSmall, sz, bg, labelColors[i], labels[i], fmt.Sprintf("%d", rgb[i]))
|
|
||||||
if fine[i] {
|
|
||||||
img := image.NewRGBA(image.Rect(0, 0, sz, sz))
|
|
||||||
draw.Draw(img, img.Bounds(), txt, image.Point{}, draw.Src)
|
|
||||||
border := labelColors[i]
|
|
||||||
b := 4
|
|
||||||
for y := 0; y < sz; y++ {
|
|
||||||
for x := 0; x < sz; x++ {
|
|
||||||
if x < b || x >= sz-b || y < b || y >= sz-b {
|
|
||||||
img.Set(x, y, border)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dev.SetKeyImage(i, img)
|
|
||||||
} else {
|
|
||||||
dev.SetKeyImage(i, txt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAllKeys := func() {
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
updateKey(i)
|
x0 := i * segW
|
||||||
|
x1 := (i + 1) * segW
|
||||||
|
cc := chanColors[i]
|
||||||
|
|
||||||
|
top := img.SubImage(image.Rect(x0, 0, x1, half)).(*image.RGBA)
|
||||||
|
streamdeck.DrawOutlinedText(top, stripFont, cc, color.Black, 2, names[i])
|
||||||
|
|
||||||
|
valStr := fmt.Sprintf("%03d %02x", rgb[i], rgb[i])
|
||||||
|
bot := img.SubImage(image.Rect(x0, half, x1, m.LCDHeight)).(*image.RGBA)
|
||||||
|
streamdeck.DrawOutlinedText(bot, stripFont, cc, color.Black, 2, valStr)
|
||||||
|
|
||||||
|
charW := font.MeasureString(stripFont, "0").Ceil()
|
||||||
|
fullW := font.MeasureString(stripFont, valStr).Ceil()
|
||||||
|
metrics := stripFont.Metrics()
|
||||||
|
lineH := metrics.Height.Ceil()
|
||||||
|
botH := m.LCDHeight - half
|
||||||
|
textX := x0 + (segW-fullW)/2
|
||||||
|
baseY := half + (botH-lineH)/2 + metrics.Ascent.Ceil() + 2
|
||||||
|
|
||||||
|
nibbleIdx := 5
|
||||||
|
if hiNibble[i] {
|
||||||
|
nibbleIdx = 4
|
||||||
|
}
|
||||||
|
ux := textX + nibbleIdx*charW
|
||||||
|
for x := ux; x < ux+charW; x++ {
|
||||||
|
img.Set(x, baseY, cc)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
dev.SetLCDImage(0, 0, m.LCDWidth, m.LCDHeight, img)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLCD()
|
updateLCD()
|
||||||
updateAllKeys()
|
|
||||||
|
|
||||||
for i := 3; i < dev.Model().Keys; i++ {
|
for i := 0; i < m.Keys; i++ {
|
||||||
dev.ClearKey(i)
|
dev.ClearKey(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,17 +106,16 @@ func main() {
|
|||||||
if ev.Encoder != nil && ev.Encoder.Encoder < 3 {
|
if ev.Encoder != nil && ev.Encoder.Encoder < 3 {
|
||||||
i := ev.Encoder.Encoder
|
i := ev.Encoder.Encoder
|
||||||
if ev.Encoder.Delta != 0 {
|
if ev.Encoder.Delta != 0 {
|
||||||
delta := ev.Encoder.Delta
|
step := 1
|
||||||
if !fine[i] {
|
if hiNibble[i] {
|
||||||
delta *= 10
|
step = 16
|
||||||
}
|
}
|
||||||
rgb[i] = clamp(rgb[i] + delta)
|
rgb[i] = clamp(rgb[i] + ev.Encoder.Delta*step)
|
||||||
updateLCD()
|
updateLCD()
|
||||||
updateKey(i)
|
|
||||||
fmt.Printf("R=%d G=%d B=%d\n", rgb[0], rgb[1], rgb[2])
|
fmt.Printf("R=%d G=%d B=%d\n", rgb[0], rgb[1], rgb[2])
|
||||||
} else if ev.Encoder.Pressed {
|
} else if ev.Encoder.Pressed {
|
||||||
fine[i] = !fine[i]
|
hiNibble[i] = !hiNibble[i]
|
||||||
updateKey(i)
|
updateLCD()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case <-sig:
|
case <-sig:
|
||||||
|
|||||||
@@ -28,17 +28,17 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
MonoRegular = loadFace("fonts/AtkinsonHyperlegibleMono-Regular.ttf", 72)
|
MonoRegular = LoadFace("fonts/AtkinsonHyperlegibleMono-Regular.ttf", 72)
|
||||||
MonoMedium = loadFace("fonts/AtkinsonHyperlegibleMono-Medium.ttf", 72)
|
MonoMedium = LoadFace("fonts/AtkinsonHyperlegibleMono-Medium.ttf", 72)
|
||||||
MonoBold = loadFace("fonts/AtkinsonHyperlegibleMono-Bold.ttf", 72)
|
MonoBold = LoadFace("fonts/AtkinsonHyperlegibleMono-Bold.ttf", 72)
|
||||||
MonoRegularSmall = loadFace("fonts/AtkinsonHyperlegibleMono-Regular.ttf", 40)
|
MonoRegularSmall = LoadFace("fonts/AtkinsonHyperlegibleMono-Regular.ttf", 40)
|
||||||
MonoMediumSmall = loadFace("fonts/AtkinsonHyperlegibleMono-Medium.ttf", 40)
|
MonoMediumSmall = LoadFace("fonts/AtkinsonHyperlegibleMono-Medium.ttf", 40)
|
||||||
MonoBoldSmall = loadFace("fonts/AtkinsonHyperlegibleMono-Bold.ttf", 40)
|
MonoBoldSmall = LoadFace("fonts/AtkinsonHyperlegibleMono-Bold.ttf", 40)
|
||||||
Regular = loadFace("fonts/AtkinsonHyperlegible-Regular.ttf", 16)
|
Regular = LoadFace("fonts/AtkinsonHyperlegible-Regular.ttf", 16)
|
||||||
Bold = loadFace("fonts/AtkinsonHyperlegible-Bold.ttf", 16)
|
Bold = LoadFace("fonts/AtkinsonHyperlegible-Bold.ttf", 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadFace(path string, size float64) font.Face {
|
func LoadFace(path string, size float64) font.Face {
|
||||||
data, err := fontFS.ReadFile(path)
|
data, err := fontFS.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("streamdeck: read font %s: %v", path, err)
|
log.Fatalf("streamdeck: read font %s: %v", path, err)
|
||||||
@@ -58,7 +58,23 @@ func loadFace(path string, size float64) font.Face {
|
|||||||
return face
|
return face
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DrawOutlinedText(img *image.RGBA, face font.Face, fg color.Color, outline color.Color, thickness int, lines ...string) {
|
||||||
|
for dx := -thickness; dx <= thickness; dx++ {
|
||||||
|
for dy := -thickness; dy <= thickness; dy++ {
|
||||||
|
if dx == 0 && dy == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
drawTextOffset(img, face, outline, dx, dy, lines...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawTextOffset(img, face, fg, 0, 0, lines...)
|
||||||
|
}
|
||||||
|
|
||||||
func DrawText(img *image.RGBA, face font.Face, fg color.Color, lines ...string) {
|
func DrawText(img *image.RGBA, face font.Face, fg color.Color, lines ...string) {
|
||||||
|
drawTextOffset(img, face, fg, 0, 0, lines...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawTextOffset(img *image.RGBA, face font.Face, fg color.Color, dx, dy int, lines ...string) {
|
||||||
metrics := face.Metrics()
|
metrics := face.Metrics()
|
||||||
lineHeight := metrics.Height.Ceil()
|
lineHeight := metrics.Height.Ceil()
|
||||||
bounds := img.Bounds()
|
bounds := img.Bounds()
|
||||||
@@ -77,12 +93,78 @@ func DrawText(img *image.RGBA, face font.Face, fg color.Color, lines ...string)
|
|||||||
Dst: img,
|
Dst: img,
|
||||||
Src: &image.Uniform{fg},
|
Src: &image.Uniform{fg},
|
||||||
Face: face,
|
Face: face,
|
||||||
Dot: fixed.P(bounds.Min.X+x, bounds.Min.Y+y),
|
Dot: fixed.P(bounds.Min.X+x+dx, bounds.Min.Y+y+dy),
|
||||||
}
|
}
|
||||||
d.DrawString(line)
|
d.DrawString(line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TextSpan struct {
|
||||||
|
Text string
|
||||||
|
Color color.Color
|
||||||
|
Outline color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
func DrawOutlinedSpans(img *image.RGBA, face font.Face, defaultOutline color.Color, thickness int, lines ...[]TextSpan) {
|
||||||
|
var full []string
|
||||||
|
for _, line := range lines {
|
||||||
|
s := ""
|
||||||
|
for _, span := range line {
|
||||||
|
s += span.Text
|
||||||
|
}
|
||||||
|
full = append(full, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics := face.Metrics()
|
||||||
|
lineHeight := metrics.Height.Ceil()
|
||||||
|
bounds := img.Bounds()
|
||||||
|
w := bounds.Dx()
|
||||||
|
h := bounds.Dy()
|
||||||
|
totalHeight := lineHeight * len(lines)
|
||||||
|
startY := (h-totalHeight)/2 + metrics.Ascent.Ceil()
|
||||||
|
|
||||||
|
for dx := -thickness; dx <= thickness; dx++ {
|
||||||
|
for dy := -thickness; dy <= thickness; dy++ {
|
||||||
|
if dx == 0 && dy == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, line := range lines {
|
||||||
|
width := font.MeasureString(face, full[i]).Ceil()
|
||||||
|
x := (w - width) / 2
|
||||||
|
y := startY + i*lineHeight
|
||||||
|
d := &font.Drawer{
|
||||||
|
Dst: img,
|
||||||
|
Face: face,
|
||||||
|
Dot: fixed.P(bounds.Min.X+x+dx, bounds.Min.Y+y+dy),
|
||||||
|
}
|
||||||
|
for _, span := range line {
|
||||||
|
ol := defaultOutline
|
||||||
|
if span.Outline != nil {
|
||||||
|
ol = span.Outline
|
||||||
|
}
|
||||||
|
d.Src = &image.Uniform{ol}
|
||||||
|
d.DrawString(span.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, line := range lines {
|
||||||
|
width := font.MeasureString(face, full[i]).Ceil()
|
||||||
|
x := (w - width) / 2
|
||||||
|
y := startY + i*lineHeight
|
||||||
|
d := &font.Drawer{
|
||||||
|
Dst: img,
|
||||||
|
Face: face,
|
||||||
|
Dot: fixed.P(bounds.Min.X+x, bounds.Min.Y+y),
|
||||||
|
}
|
||||||
|
for _, span := range line {
|
||||||
|
d.Src = &image.Uniform{span.Color}
|
||||||
|
d.DrawString(span.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TextImageSized(size int, bg color.Color, fg color.Color, lines ...string) image.Image {
|
func TextImageSized(size int, bg color.Color, fg color.Color, lines ...string) image.Image {
|
||||||
return TextImageWithFaceSized(MonoMedium, size, bg, fg, lines...)
|
return TextImageWithFaceSized(MonoMedium, size, bg, fg, lines...)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user