mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-17 05:13:23 +00:00
pixel: add support for Monochrome types such as the SSD1306 display
Signed-off-by: deadprogram <ron@hybridgroup.com>
This commit is contained in:
+49
-8
@@ -22,12 +22,13 @@ func NewImage[T Color](width, height int) Image[T] {
|
|||||||
}
|
}
|
||||||
var zeroColor T
|
var zeroColor T
|
||||||
var data unsafe.Pointer
|
var data unsafe.Pointer
|
||||||
if zeroColor.BitsPerPixel()%8 == 0 {
|
switch {
|
||||||
|
case zeroColor.BitsPerPixel()%8 == 0:
|
||||||
// Typical formats like RGB888 and RGB565.
|
// Typical formats like RGB888 and RGB565.
|
||||||
// Each color starts at a whole byte offset from the start.
|
// Each color starts at a whole byte offset from the start.
|
||||||
buf := make([]T, width*height)
|
buf := make([]T, width*height)
|
||||||
data = unsafe.Pointer(&buf[0])
|
data = unsafe.Pointer(&buf[0])
|
||||||
} else {
|
default:
|
||||||
// Formats like RGB444 that have 12 bits per pixel.
|
// Formats like RGB444 that have 12 bits per pixel.
|
||||||
// We access these as bytes, so allocate the buffer as a byte slice.
|
// We access these as bytes, so allocate the buffer as a byte slice.
|
||||||
bufBits := width * height * zeroColor.BitsPerPixel()
|
bufBits := width * height * zeroColor.BitsPerPixel()
|
||||||
@@ -80,10 +81,11 @@ func (img Image[T]) Len() int {
|
|||||||
func (img Image[T]) RawBuffer() []uint8 {
|
func (img Image[T]) RawBuffer() []uint8 {
|
||||||
var zeroColor T
|
var zeroColor T
|
||||||
var numBytes int
|
var numBytes int
|
||||||
if zeroColor.BitsPerPixel()%8 == 0 {
|
switch {
|
||||||
|
case zeroColor.BitsPerPixel()%8 == 0:
|
||||||
// Each color starts at a whole byte offset.
|
// Each color starts at a whole byte offset.
|
||||||
numBytes = int(unsafe.Sizeof(zeroColor)) * int(img.width) * int(img.height)
|
numBytes = int(unsafe.Sizeof(zeroColor)) * int(img.width) * int(img.height)
|
||||||
} else {
|
default:
|
||||||
// Formats like RGB444 that aren't a whole number of bytes.
|
// Formats like RGB444 that aren't a whole number of bytes.
|
||||||
numBits := zeroColor.BitsPerPixel() * int(img.width) * int(img.height)
|
numBits := zeroColor.BitsPerPixel() * int(img.width) * int(img.height)
|
||||||
numBytes = (numBits + 7) / 8 // round up (see NewImage)
|
numBytes = (numBits + 7) / 8 // round up (see NewImage)
|
||||||
@@ -99,7 +101,20 @@ func (img Image[T]) Size() (int, int) {
|
|||||||
func (img Image[T]) setPixel(index int, c T) {
|
func (img Image[T]) setPixel(index int, c T) {
|
||||||
var zeroColor T
|
var zeroColor T
|
||||||
|
|
||||||
if zeroColor.BitsPerPixel()%8 == 0 {
|
switch {
|
||||||
|
case zeroColor.BitsPerPixel() == 1:
|
||||||
|
// Monochrome.
|
||||||
|
x := int16(index) % img.width
|
||||||
|
y := int16(index) / img.width
|
||||||
|
offset := x + (y/8)*img.width
|
||||||
|
ptr := (*byte)(unsafe.Add(img.data, offset))
|
||||||
|
if c != zeroColor {
|
||||||
|
*((*byte)(ptr)) |= 1 << uint8(y%8)
|
||||||
|
} else {
|
||||||
|
*((*byte)(ptr)) &^= 1 << uint8(y%8)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case zeroColor.BitsPerPixel()%8 == 0:
|
||||||
// Each color starts at a whole byte offset.
|
// Each color starts at a whole byte offset.
|
||||||
// This is the easy case.
|
// This is the easy case.
|
||||||
offset := index * int(unsafe.Sizeof(zeroColor))
|
offset := index * int(unsafe.Sizeof(zeroColor))
|
||||||
@@ -147,7 +162,15 @@ func (img Image[T]) Get(x, y int) T {
|
|||||||
var zeroColor T
|
var zeroColor T
|
||||||
index := y*int(img.width) + x // index into img.data
|
index := y*int(img.width) + x // index into img.data
|
||||||
|
|
||||||
if zeroColor.BitsPerPixel()%8 == 0 {
|
switch {
|
||||||
|
case zeroColor.BitsPerPixel() == 1:
|
||||||
|
// Monochrome.
|
||||||
|
var c Monochrome
|
||||||
|
offset := x + (y/8)*int(img.width)
|
||||||
|
ptr := (*byte)(unsafe.Add(img.data, offset))
|
||||||
|
c = (*ptr >> uint8(y%8) & 0x1) == 1
|
||||||
|
return any(c).(T)
|
||||||
|
case zeroColor.BitsPerPixel()%8 == 0:
|
||||||
// Colors like RGB565, RGB888, etc.
|
// Colors like RGB565, RGB888, etc.
|
||||||
offset := index * int(unsafe.Sizeof(zeroColor))
|
offset := index * int(unsafe.Sizeof(zeroColor))
|
||||||
ptr := unsafe.Add(img.data, offset)
|
ptr := unsafe.Add(img.data, offset)
|
||||||
@@ -181,8 +204,26 @@ func (img Image[T]) Get(x, y int) T {
|
|||||||
func (img Image[T]) FillSolidColor(color T) {
|
func (img Image[T]) FillSolidColor(color T) {
|
||||||
var zeroColor T
|
var zeroColor T
|
||||||
|
|
||||||
// Fast pass for colors of 8, 16, 24, etc bytes in size.
|
switch {
|
||||||
if zeroColor.BitsPerPixel()%8 == 0 {
|
case zeroColor.BitsPerPixel() == 1:
|
||||||
|
// Monochrome.
|
||||||
|
var colorByte uint8
|
||||||
|
if color != zeroColor {
|
||||||
|
colorByte = 0xff
|
||||||
|
}
|
||||||
|
numBytes := int(img.width) * int(img.height) / 8
|
||||||
|
for i := 0; i < numBytes; i++ {
|
||||||
|
// TODO: this can be optimized a lot.
|
||||||
|
// - The store can be done as a 32-bit integer, after checking for
|
||||||
|
// alignment.
|
||||||
|
// - Perhaps the loop can be unrolled to improve copy performance.
|
||||||
|
ptr := (*byte)(unsafe.Add(img.data, i))
|
||||||
|
*((*byte)(ptr)) = colorByte
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case zeroColor.BitsPerPixel()%8 == 0:
|
||||||
|
// Fast pass for colors of 8, 16, 24, etc bytes in size.
|
||||||
ptr := img.data
|
ptr := img.data
|
||||||
for i := 0; i < img.Len(); i++ {
|
for i := 0; i < img.Len(); i++ {
|
||||||
// TODO: this can be optimized a lot.
|
// TODO: this can be optimized a lot.
|
||||||
|
|||||||
@@ -62,3 +62,35 @@ func TestImageRGB444BE(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImageMonochrome(t *testing.T) {
|
||||||
|
image := pixel.NewImage[pixel.Monochrome](5, 3)
|
||||||
|
if width, height := image.Size(); width != 5 && height != 3 {
|
||||||
|
t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height)
|
||||||
|
}
|
||||||
|
for _, expected := range []color.RGBA{
|
||||||
|
{R: 0xff, G: 0xff, B: 0xff},
|
||||||
|
{G: 0xff},
|
||||||
|
{R: 0xff, G: 0xff},
|
||||||
|
{G: 0xff, B: 0xff},
|
||||||
|
{R: 0x00},
|
||||||
|
{G: 0x00, A: 0xff},
|
||||||
|
{B: 0x00, A: 0xff},
|
||||||
|
} {
|
||||||
|
encoded := pixel.NewColor[pixel.Monochrome](expected.R, expected.G, expected.B)
|
||||||
|
image.Set(4, 2, encoded)
|
||||||
|
actual := image.Get(4, 2).RGBA()
|
||||||
|
switch {
|
||||||
|
case expected.R == 0 && expected.G == 0 && expected.B == 0:
|
||||||
|
// should be false eg black
|
||||||
|
if actual.R != 0 || actual.G != 0 || actual.B != 0 {
|
||||||
|
t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual)
|
||||||
|
}
|
||||||
|
case int(expected.R)+int(expected.G)+int(expected.B) > 128*3:
|
||||||
|
// should be true eg white
|
||||||
|
if actual.R == 0 || actual.G == 0 || actual.B == 0 {
|
||||||
|
t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+33
-2
@@ -14,9 +14,9 @@ import (
|
|||||||
|
|
||||||
// Pixel with a particular color, matching the underlying hardware of a
|
// Pixel with a particular color, matching the underlying hardware of a
|
||||||
// particular display. Each pixel is at least 1 byte in size.
|
// particular display. Each pixel is at least 1 byte in size.
|
||||||
// The color format is sRGB (or close to it) in all cases.
|
// The color format is sRGB (or close to it) in all cases except for 1-bit.
|
||||||
type Color interface {
|
type Color interface {
|
||||||
RGB888 | RGB565BE | RGB555 | RGB444BE
|
RGB888 | RGB565BE | RGB555 | RGB444BE | Monochrome
|
||||||
|
|
||||||
BaseColor
|
BaseColor
|
||||||
}
|
}
|
||||||
@@ -50,6 +50,8 @@ func NewColor[T Color](r, g, b uint8) T {
|
|||||||
return any(NewRGB555(r, g, b)).(T)
|
return any(NewRGB555(r, g, b)).(T)
|
||||||
case RGB444BE:
|
case RGB444BE:
|
||||||
return any(NewRGB444BE(r, g, b)).(T)
|
return any(NewRGB444BE(r, g, b)).(T)
|
||||||
|
case Monochrome:
|
||||||
|
return any(NewMonochrome(r, g, b)).(T)
|
||||||
default:
|
default:
|
||||||
panic("unknown color format")
|
panic("unknown color format")
|
||||||
}
|
}
|
||||||
@@ -202,6 +204,35 @@ func (c RGB444BE) RGBA() color.RGBA {
|
|||||||
return color
|
return color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Monochrome bool
|
||||||
|
|
||||||
|
func NewMonochrome(r, g, b uint8) Monochrome {
|
||||||
|
// Very simple black/white split.
|
||||||
|
// This isn't very accurate (especially for sRGB colors) but is close enough.
|
||||||
|
if int(r)+int(g)+int(b) > 128*3 { // light, convert to white
|
||||||
|
return Monochrome(true)
|
||||||
|
}
|
||||||
|
// dark, convert to black
|
||||||
|
return Monochrome(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Monochrome) BitsPerPixel() int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Monochrome) RGBA() color.RGBA {
|
||||||
|
value := uint8(0)
|
||||||
|
if c {
|
||||||
|
value = 255
|
||||||
|
}
|
||||||
|
return color.RGBA{
|
||||||
|
R: value,
|
||||||
|
G: value,
|
||||||
|
B: value,
|
||||||
|
A: 255,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Gamma brightness lookup table:
|
// Gamma brightness lookup table:
|
||||||
// https://victornpb.github.io/gamma-table-generator
|
// https://victornpb.github.io/gamma-table-generator
|
||||||
// gamma = 0.45 steps = 256 range = 0-255
|
// gamma = 0.45 steps = 256 range = 0-255
|
||||||
|
|||||||
Reference in New Issue
Block a user