ligmen: include frame times in image ids
This commit is contained in:
parent
c551a5f61b
commit
fe5a368249
24
ligmen/formats.go
Normal file
24
ligmen/formats.go
Normal file
@ -0,0 +1,24 @@
|
||||
package ligmen
|
||||
|
||||
import (
|
||||
"image"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Image is a potentially animated image.
|
||||
type Image[Frame image.Image] struct {
|
||||
// Frames is animation's frame images.
|
||||
// An image that is not animated has a single frame.
|
||||
Frames []Frame
|
||||
// Times is the duration of each frame.
|
||||
// An image that is not animated has a single frame duration of 0.
|
||||
Times []time.Duration
|
||||
}
|
||||
|
||||
// Static is a helper to create an Image with a single frame.
|
||||
func Static[Frame image.Image](img Frame) Image[Frame] {
|
||||
return Image[Frame]{
|
||||
Frames: []Frame{img},
|
||||
Times: []time.Duration{0},
|
||||
}
|
||||
}
|
@ -14,14 +14,14 @@ import (
|
||||
type ID [28]byte
|
||||
|
||||
// Hash computes the content ID for an image or animation.
|
||||
func Hash[Image image.Image](frames []Image) ID {
|
||||
func Hash[Frame image.Image](anim Image[Frame]) ID {
|
||||
// NOTE(zephyr): We assume animations always loop forever.
|
||||
// The behavior in browsers when they don't is that every instance shares a
|
||||
// single loop count, so they quickly stop repeating as posted.
|
||||
h := sha3.New224()
|
||||
d := make([]byte, 8, 28)
|
||||
var bounds image.Rectangle
|
||||
for _, f := range frames {
|
||||
for _, f := range anim.Frames {
|
||||
if bounds == (image.Rectangle{}) {
|
||||
bounds = f.Bounds().Canon()
|
||||
}
|
||||
@ -37,10 +37,12 @@ func Hash[Image image.Image](frames []Image) ID {
|
||||
h.Write(d)
|
||||
}
|
||||
}
|
||||
d = d[:12]
|
||||
for _, t := range anim.Times {
|
||||
binary.BigEndian.PutUint64(d, uint64(t.Nanoseconds()))
|
||||
h.Write(d)
|
||||
}
|
||||
binary.BigEndian.PutUint32(d, uint32(bounds.Dx()))
|
||||
binary.BigEndian.PutUint32(d[4:], uint32(bounds.Dy()))
|
||||
binary.BigEndian.PutUint32(d[8:], uint32(len(frames)))
|
||||
h.Write(d)
|
||||
return ID(h.Sum(d[:0]))
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ligmotes.com/ligmen"
|
||||
)
|
||||
@ -36,70 +37,133 @@ func (img *testimg) At(x int, y int) color.Color {
|
||||
func TestHash(t *testing.T) {
|
||||
// We test that hashing a number of similar images doesn't produce collisions.
|
||||
cases := []struct {
|
||||
name string
|
||||
frames []image.RGBA64Image
|
||||
name string
|
||||
anim ligmen.Image[image.RGBA64Image]
|
||||
}{
|
||||
{
|
||||
name: "clear-square",
|
||||
frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 2, h: 2},
|
||||
},
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 2, h: 2}),
|
||||
},
|
||||
{
|
||||
name: "topleft-square",
|
||||
frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{A: 1}, {}, {}, {}}, w: 2, h: 2},
|
||||
},
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{A: 1}, {}, {}, {}}, w: 2, h: 2}),
|
||||
},
|
||||
{
|
||||
name: "topright-square",
|
||||
frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}, {A: 1}, {}, {}}, w: 2, h: 2},
|
||||
},
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {A: 1}, {}, {}}, w: 2, h: 2}),
|
||||
},
|
||||
{
|
||||
name: "botleft-square",
|
||||
frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}, {}, {A: 1}, {}}, w: 2, h: 2},
|
||||
},
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {}, {A: 1}, {}}, w: 2, h: 2}),
|
||||
},
|
||||
{
|
||||
name: "botright-square",
|
||||
frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}, {}, {}, {A: 1}}, w: 2, h: 2},
|
||||
},
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {}, {}, {A: 1}}, w: 2, h: 2}),
|
||||
},
|
||||
{
|
||||
name: "all-square",
|
||||
frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{A: 1}, {A: 1}, {A: 1}, {A: 1}}, w: 2, h: 2},
|
||||
},
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{A: 1}, {A: 1}, {A: 1}, {A: 1}}, w: 2, h: 2}),
|
||||
},
|
||||
{
|
||||
name: "red-square",
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{R: 1, A: 1}, {}, {}, {}}, w: 2, h: 2}),
|
||||
},
|
||||
{
|
||||
name: "green-square",
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{G: 1, A: 1}, {}, {}, {}}, w: 2, h: 2}),
|
||||
},
|
||||
{
|
||||
name: "blue-square",
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{B: 1, A: 1}, {}, {}, {}}, w: 2, h: 2}),
|
||||
},
|
||||
{
|
||||
name: "gray-square",
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{R: 1, G: 1, B: 1, A: 1}, {}, {}, {}}, w: 2, h: 2}),
|
||||
},
|
||||
{
|
||||
name: "row",
|
||||
frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 4, h: 1},
|
||||
},
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 4, h: 1}),
|
||||
},
|
||||
{
|
||||
name: "col",
|
||||
frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 1, h: 4},
|
||||
},
|
||||
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 1, h: 4}),
|
||||
},
|
||||
{
|
||||
name: "frames",
|
||||
frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
anim: ligmen.Image[image.RGBA64Image]{
|
||||
Frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
},
|
||||
Times: []time.Duration{1, 1, 1, 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "frames-first",
|
||||
anim: ligmen.Image[image.RGBA64Image]{
|
||||
Frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
},
|
||||
Times: []time.Duration{2, 1, 1, 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "frames-second",
|
||||
anim: ligmen.Image[image.RGBA64Image]{
|
||||
Frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
},
|
||||
Times: []time.Duration{1, 2, 1, 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "frames-third",
|
||||
anim: ligmen.Image[image.RGBA64Image]{
|
||||
Frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
},
|
||||
Times: []time.Duration{1, 1, 2, 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "frames-fourth",
|
||||
anim: ligmen.Image[image.RGBA64Image]{
|
||||
Frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
},
|
||||
Times: []time.Duration{1, 1, 1, 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "frames-all",
|
||||
anim: ligmen.Image[image.RGBA64Image]{
|
||||
Frames: []image.RGBA64Image{
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
||||
},
|
||||
Times: []time.Duration{2, 2, 2, 2},
|
||||
},
|
||||
},
|
||||
}
|
||||
m := make(map[ligmen.ID]string)
|
||||
for _, c := range cases {
|
||||
id := ligmen.Hash(c.frames)
|
||||
id := ligmen.Hash(c.anim)
|
||||
if had, ok := m[id]; ok {
|
||||
t.Errorf("%s collides with %s @ %v", c.name, had, id)
|
||||
continue
|
||||
@ -109,7 +173,8 @@ func TestHash(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIDRoundTrip(t *testing.T) {
|
||||
id := ligmen.Hash([]*testimg{{px: []color.RGBA64{{}, {}, {}, {}}, w: 2, h: 2}})
|
||||
img := ligmen.Static(&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 2, h: 2})
|
||||
id := ligmen.Hash(img)
|
||||
if id == (ligmen.ID{}) {
|
||||
t.Error("empty id for image")
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user