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
|
type ID [28]byte
|
||||||
|
|
||||||
// Hash computes the content ID for an image or animation.
|
// 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.
|
// NOTE(zephyr): We assume animations always loop forever.
|
||||||
// The behavior in browsers when they don't is that every instance shares a
|
// The behavior in browsers when they don't is that every instance shares a
|
||||||
// single loop count, so they quickly stop repeating as posted.
|
// single loop count, so they quickly stop repeating as posted.
|
||||||
h := sha3.New224()
|
h := sha3.New224()
|
||||||
d := make([]byte, 8, 28)
|
d := make([]byte, 8, 28)
|
||||||
var bounds image.Rectangle
|
var bounds image.Rectangle
|
||||||
for _, f := range frames {
|
for _, f := range anim.Frames {
|
||||||
if bounds == (image.Rectangle{}) {
|
if bounds == (image.Rectangle{}) {
|
||||||
bounds = f.Bounds().Canon()
|
bounds = f.Bounds().Canon()
|
||||||
}
|
}
|
||||||
@ -37,10 +37,12 @@ func Hash[Image image.Image](frames []Image) ID {
|
|||||||
h.Write(d)
|
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, uint32(bounds.Dx()))
|
||||||
binary.BigEndian.PutUint32(d[4:], uint32(bounds.Dy()))
|
binary.BigEndian.PutUint32(d[4:], uint32(bounds.Dy()))
|
||||||
binary.BigEndian.PutUint32(d[8:], uint32(len(frames)))
|
|
||||||
h.Write(d)
|
h.Write(d)
|
||||||
return ID(h.Sum(d[:0]))
|
return ID(h.Sum(d[:0]))
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"ligmotes.com/ligmen"
|
"ligmotes.com/ligmen"
|
||||||
)
|
)
|
||||||
@ -36,70 +37,133 @@ func (img *testimg) At(x int, y int) color.Color {
|
|||||||
func TestHash(t *testing.T) {
|
func TestHash(t *testing.T) {
|
||||||
// We test that hashing a number of similar images doesn't produce collisions.
|
// We test that hashing a number of similar images doesn't produce collisions.
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
frames []image.RGBA64Image
|
anim ligmen.Image[image.RGBA64Image]
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "clear-square",
|
name: "clear-square",
|
||||||
frames: []image.RGBA64Image{
|
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 2, h: 2}),
|
||||||
&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 2, h: 2},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "topleft-square",
|
name: "topleft-square",
|
||||||
frames: []image.RGBA64Image{
|
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{A: 1}, {}, {}, {}}, w: 2, h: 2}),
|
||||||
&testimg{px: []color.RGBA64{{A: 1}, {}, {}, {}}, w: 2, h: 2},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "topright-square",
|
name: "topright-square",
|
||||||
frames: []image.RGBA64Image{
|
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {A: 1}, {}, {}}, w: 2, h: 2}),
|
||||||
&testimg{px: []color.RGBA64{{}, {A: 1}, {}, {}}, w: 2, h: 2},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "botleft-square",
|
name: "botleft-square",
|
||||||
frames: []image.RGBA64Image{
|
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {}, {A: 1}, {}}, w: 2, h: 2}),
|
||||||
&testimg{px: []color.RGBA64{{}, {}, {A: 1}, {}}, w: 2, h: 2},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "botright-square",
|
name: "botright-square",
|
||||||
frames: []image.RGBA64Image{
|
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {}, {}, {A: 1}}, w: 2, h: 2}),
|
||||||
&testimg{px: []color.RGBA64{{}, {}, {}, {A: 1}}, w: 2, h: 2},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "all-square",
|
name: "all-square",
|
||||||
frames: []image.RGBA64Image{
|
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{A: 1}, {A: 1}, {A: 1}, {A: 1}}, w: 2, h: 2}),
|
||||||
&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",
|
name: "row",
|
||||||
frames: []image.RGBA64Image{
|
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 4, h: 1}),
|
||||||
&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 4, h: 1},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "col",
|
name: "col",
|
||||||
frames: []image.RGBA64Image{
|
anim: ligmen.Static[image.RGBA64Image](&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 1, h: 4}),
|
||||||
&testimg{px: []color.RGBA64{{}, {}, {}, {}}, w: 1, h: 4},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "frames",
|
name: "frames",
|
||||||
frames: []image.RGBA64Image{
|
anim: ligmen.Image[image.RGBA64Image]{
|
||||||
&testimg{px: []color.RGBA64{{}}, w: 1, h: 1},
|
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},
|
||||||
&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)
|
m := make(map[ligmen.ID]string)
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
id := ligmen.Hash(c.frames)
|
id := ligmen.Hash(c.anim)
|
||||||
if had, ok := m[id]; ok {
|
if had, ok := m[id]; ok {
|
||||||
t.Errorf("%s collides with %s @ %v", c.name, had, id)
|
t.Errorf("%s collides with %s @ %v", c.name, had, id)
|
||||||
continue
|
continue
|
||||||
@ -109,7 +173,8 @@ func TestHash(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIDRoundTrip(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{}) {
|
if id == (ligmen.ID{}) {
|
||||||
t.Error("empty id for image")
|
t.Error("empty id for image")
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user