Browse Source

Added fx; Fixes on Bitmap stride

master v0.1.1
maze 9 months ago
parent
commit
60e7abb87c
6 changed files with 366 additions and 16 deletions
  1. +9
    -7
      bitmap.go
  2. +3
    -3
      bitmap_test.go
  3. +6
    -6
      draw_test.go
  4. +272
    -0
      fx/fade.go
  5. +76
    -0
      fx/fade_test.go
  6. BIN
      fx/testdata/gopher.png

+ 9
- 7
bitmap.go View File

@ -43,15 +43,17 @@ type Bitmap struct {
}
func NewBitmap(w, h int) *Bitmap {
stride := w >> 3
if w&0b111 != 0 {
stride++
var (
area = w * h
pix = area >> 3
)
if pix<<3 != area {
pix++
}
pix := stride * h
return &Bitmap{
Rect: image.Rectangle{Max: image.Point{X: w, Y: h}},
Pix: make([]byte, pix),
Stride: stride,
Stride: w,
Format: MVLSBFormat,
}
}
@ -91,10 +93,10 @@ func (b *Bitmap) Set(x, y int, c color.Color) {
}
func (b *Bitmap) SetBit(x, y int, c Bit) {
if x < 0 || y < 0 || x >= b.Rect.Max.X || y >= b.Rect.Max.Y {
offset, mask := b.PixOffset(x, y)
if offset < 0 || offset >= len(b.Pix) {
return
}
offset, mask := b.PixOffset(x, y)
if c {
b.Pix[offset] |= byte(mask)
} else {


+ 3
- 3
bitmap_test.go View File

@ -12,9 +12,9 @@ func TestNewBitBuffer(t *testing.T) {
WantPixLen int
}{
{1, 1, 1, 1},
{3, 3, 1, 3},
{8, 8, 1, 8},
{128, 32, 16, 512},
{3, 3, 3, 2},
{8, 8, 8, 8},
{128, 32, 128, 512},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%dx%d", test.W, test.H), func(it *testing.T) {


+ 6
- 6
draw_test.go View File

@ -19,7 +19,7 @@ func TestFill(t *testing.T) {
&Bitmap{
Rect: image.Rectangle{Max: image.Point{X: 8, Y: 8}},
Pix: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
Stride: 1,
Stride: 8,
Format: MVLSBFormat,
},
},
@ -79,7 +79,7 @@ func TestFillRectangle(t *testing.T) {
0b00000000,
0b00000000,
},
Stride: 1,
Stride: 8,
Format: MVLSBFormat,
},
},
@ -139,7 +139,7 @@ func TestHLine(t *testing.T) {
0b00000100,
0b00000100,
},
Stride: 1,
Stride: 8,
Format: MVLSBFormat,
},
},
@ -201,7 +201,7 @@ func TestVLine(t *testing.T) {
0b00000000,
0b00000000,
},
Stride: 1,
Stride: 8,
Format: MVLSBFormat,
},
},
@ -263,7 +263,7 @@ func TestLine(t *testing.T) {
0b01000000,
0b10000000,
},
Stride: 1,
Stride: 8,
Format: MVLSBFormat,
},
},
@ -325,7 +325,7 @@ func TestCircle(t *testing.T) {
0b00000010,
0b00000100,
},
Stride: 1,
Stride: 8,
Format: MVLSBFormat,
},
},


+ 272
- 0
fx/fade.go View File

@ -0,0 +1,272 @@
package fx
import (
"image"
"image/color"
"image/draw"
"time"
"maze.io/x/bitmap"
)
const (
shiftRightRGBMaxSteps = 8
shiftRightRGB565MaxSteps = 6
shiftRightRGB888MaxSteps = 8
)
func repeatWithin(duration time.Duration, n int, f func(int)) <-chan time.Time {
signal := make(chan time.Time)
if n == 0 {
close(signal)
} else {
go func(signal chan<- time.Time, n int, f func(int)) {
ticker := time.NewTicker(duration / time.Duration(n))
for i := 0; i < n; i++ {
f(i)
signal <- <-ticker.C
}
ticker.Stop()
close(signal)
}(signal, n, f)
}
return signal
}
// FadeOutDither dims the colors by dithering, most useful for 1-bit bitmaps.
func FadeOutDither(duration time.Duration, im bitmap.Image) <-chan time.Time {
return repeatWithin(duration, len(ditherMasks), func(step int) {
DitherStep(im, step)
})
}
// FadeOutVertical draws horizontal raster lines to fade out the image.
func FadeOutHorizontal(duration time.Duration, im bitmap.Image) <-chan time.Time {
var (
bounds = im.Bounds()
width = bounds.Dx()
height = bounds.Dy()
steps = height / 2
)
if height&1 == 1 {
steps++
}
return repeatWithin(duration, steps, func(y int) {
var (
offset0 = y * 2
offset1 = height - offset0 - 1
)
bitmap.Line(im, image.Point{Y: offset0}, image.Point{X: width, Y: offset0}, color.Black)
bitmap.Line(im, image.Point{Y: offset1}, image.Point{X: width, Y: offset1}, color.Black)
})
}
// FadeOutVertical draws vertical raster lines to fade out the image.
func FadeOutVertical(duration time.Duration, im bitmap.Image) <-chan time.Time {
var (
bounds = im.Bounds()
width = bounds.Dx()
height = bounds.Dy()
steps = width / 2
)
if width&1 == 1 {
steps++
}
return repeatWithin(duration, steps, func(x int) {
var (
offset0 = x * 2
offset1 = width - offset0 - 1
)
bitmap.Line(im, image.Point{X: offset0}, image.Point{X: offset0, Y: height}, color.Black)
bitmap.Line(im, image.Point{X: offset1}, image.Point{X: offset1, Y: height}, color.Black)
})
}
func FadeOutDiagonal(duration time.Duration, im bitmap.Image) <-chan time.Time {
var (
bounds = im.Bounds()
width = bounds.Dx()
height = bounds.Dy()
max, steps int
)
if width > height {
max = width
} else {
max = height
}
steps = max / 2
if max&1 == 1 {
steps++
}
return repeatWithin(duration, steps, func(i int) {
var (
offset0 = i * 2
offset1 = max - offset0 - 1
)
bitmap.Line(im, image.Point{X: offset0}, image.Point{Y: offset0}, color.Black)
bitmap.Line(im, image.Point{X: offset1}, image.Point{Y: offset1}, color.Black)
})
}
type ditherMask [4]byte
func (m ditherMask) At(x, y int) color.Color {
if x < 0 {
x = -x
}
if y < 0 {
y = -y
}
if m[y&3]&(1<<(x&3)) == 0 {
return color.Black
}
return color.Transparent
}
func (m ditherMask) Bounds() image.Rectangle {
return image.Rectangle{
Min: image.Point{X: -1e9, Y: -1e9},
Max: image.Point{X: +1e9, Y: +1e9},
}
}
func (m ditherMask) ColorModel() color.Model {
return color.RGBAModel
}
var ditherMasks = [8]*ditherMask{
{
0b1111,
0b1110,
0b1111,
0b1011,
},
{
0b1111,
0b1010,
0b1111,
0b1010,
},
{
0b1101,
0b1010,
0b0111,
0b1010,
},
{
0b0101,
0b1010,
0b0101,
0b1010,
},
{
0b0101,
0b1000,
0b0101,
0b0010,
},
{
0b0101,
0b0000,
0b0101,
0b0000,
},
{
0b0100,
0b0000,
0b0001,
0b0000,
},
{
0b0000,
0b0000,
0b0000,
0b0000,
},
}
func DitherStep(im bitmap.Image, step int) {
if step < 0 || step >= len(ditherMasks) {
// Nothing to do here.
return
}
draw.Draw(im, im.Bounds(), ditherMasks[step], image.Point{}, draw.Over)
//draw.DrawMask(im, im.Bounds(), im, image.Point{}, ditherMasks[step], image.Point{}, draw.Src)
}
// FadeOutShift dims the colors by bit shifting. 1-bit bitmaps are ignored.
func FadeOutShift(duration time.Duration, im bitmap.Image) <-chan time.Time {
switch im := im.(type) {
case *bitmap.Bitmap:
// pointless
signal := make(chan time.Time)
close(signal)
return signal
case *bitmap.RGB565Image:
return repeatWithin(duration, shiftRightRGB565MaxSteps, func(_ int) { shiftRightRGB565(im) })
case *bitmap.RGB888Image:
return repeatWithin(duration, shiftRightRGB888MaxSteps, func(_ int) { shiftRightRGB888(im) })
default:
return repeatWithin(duration, shiftRightRGBMaxSteps, func(_ int) { shiftRightRGB(im) })
}
}
func ShiftRight(im bitmap.Image) {
switch im := im.(type) {
case *bitmap.Bitmap:
// Pointless, ignored.
case *bitmap.RGB565Image:
shiftRightRGB565(im)
case *bitmap.RGB888Image:
shiftRightRGB888(im)
default:
shiftRightRGB(im)
}
}
// Naive approach by converting the color to RGBA and shifting.
func shiftRightRGB(im bitmap.Image) {
b := im.Bounds()
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
r, g, b, a := im.At(x, y).RGBA()
im.Set(x, y, color.RGBA{
R: uint8(r>>8) >> 1,
G: uint8(g>>8) >> 1,
B: uint8(b>>8) >> 1,
A: uint8(a>>8) >> 0,
})
}
}
}
func shiftRightRGB565(im *bitmap.RGB565Image) {
for i, l := 0, len(im.Pix); i < l; i += 2 {
var (
hi = im.Pix[i+0]
lo = im.Pix[i+1]
r, g, b byte
)
r |= (hi & 0b11111000) >> 4 // RRRRR... -> ....RRRR
g |= (hi & 0b00000111) << 2 // .....GGG -> ...GGG..
g |= (lo & 0b11100000) >> 6 // GGG..... -> ......GG
b |= (lo & 0b00011111) >> 1 // ...BBBBB -> ....BBBB
im.Pix[i+0] = (r << 3) | (g >> 3) // .RRRR.GG
im.Pix[i+1] = (g << 5) | b // GGG.BBBB
}
}
func shiftRightRGB888(im *bitmap.RGB888Image) {
for i, l := 0, len(im.Pix); i < l; i += 3 {
im.Pix[i+0] >>= 1
im.Pix[i+1] >>= 1
im.Pix[i+2] >>= 1
}
}

+ 76
- 0
fx/fade_test.go View File

@ -0,0 +1,76 @@
package fx
import (
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"os"
"path/filepath"
"testing"
_ "image/png" // PNG codec
"maze.io/x/bitmap"
)
func TestDitherStep(t *testing.T) {
im := testLoadImage(t, filepath.Join("testdata", "gopher.png"))
for i := 0; i < len(ditherMasks); i++ {
DitherStep(im, i)
if os.Getenv("DEBUG_DITHERSTEP") != "" {
o, err := os.Create(filepath.Join(os.TempDir(), fmt.Sprintf("dither-step-%d.png", i)))
if err != nil {
t.Fatal(err)
}
if err = png.Encode(o, im); err != nil {
t.Fatal(err)
}
if err = o.Close(); err != nil {
t.Fatal(err)
}
t.Log("saved to", o.Name())
}
}
}
func TestFadeOutVertical(t *testing.T) {
var (
im = testLoadImage(t, filepath.Join("testdata", "gopher.png"))
bounds = im.Bounds()
width = bounds.Dx()
height = bounds.Dy()
steps = width / 2
)
if width&1 == 1 {
steps++
}
for i := 0; i < im.Bounds().Dy()/2; i++ {
offset := i * 2
bitmap.VLine(im, image.Point{X: offset}, height, color.Transparent)
bitmap.VLine(im, image.Point{X: height - offset - 1}, height, color.Transparent)
}
}
func testLoadImage(t *testing.T, name string) bitmap.Image {
f, err := os.Open(name)
if err != nil {
t.Fatal(err)
}
defer f.Close()
i, _, err := image.Decode(f)
if err != nil {
t.Fatal("error decoding", name+":", err)
}
return toRGBA(i)
}
func toRGBA(i image.Image) *image.RGBA {
if o, ok := i.(*image.RGBA); ok {
return o
}
o := image.NewRGBA(i.Bounds())
draw.Draw(o, o.Bounds(), i, image.Point{}, draw.Src)
return o
}

BIN
fx/testdata/gopher.png View File

Before After
Width: 245  |  Height: 300  |  Size: 40 KiB

Loading…
Cancel
Save