Files

479 lines
14 KiB
Go

// Package art owns CPU pixel documents and reversible authoring operations.
// It has no dependency on an editor, graphics API, browser, or file system.
package art
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"image"
"sync"
)
const TileSize = 16
type ID string
type Mode string
type Channel string
type LayerKind string
type Blend string
type Direction string
const (
RGBA Mode = "rgba"
Grayscale Mode = "grayscale"
Indexed Mode = "indexed"
Albedo Channel = "albedo"
Normal Channel = "normal"
Height Channel = "height"
Emission Channel = "emission"
Raster LayerKind = "raster"
Group LayerKind = "group"
Reference LayerKind = "reference"
BlendNormal Blend = "normal"
BlendMultiply Blend = "multiply"
BlendScreen Blend = "screen"
BlendAdd Blend = "add"
Forward Direction = "forward"
Reverse Direction = "reverse"
PingPong Direction = "ping-pong"
)
var (
ErrConflict = errors.New("art: document revision changed")
ErrClosed = errors.New("art: document is closed")
ErrLocked = errors.New("art: layer is locked")
ErrLimit = errors.New("art: document limit exceeded")
ErrDone = errors.New("art: operation is already finished")
)
type Color struct{ R, G, B, A uint8 }
func (c Color) packed() uint32 {
return uint32(c.R) | uint32(c.G)<<8 | uint32(c.B)<<16 | uint32(c.A)<<24
}
func unpack(v uint32) Color { return Color{uint8(v), uint8(v >> 8), uint8(v >> 16), uint8(v >> 24)} }
type Swatch struct {
Color Color `json:"color"`
Name string `json:"name,omitempty"`
}
type Canvas struct {
Width int `json:"width"`
Height int `json:"height"`
Mode Mode `json:"mode"`
Profile string `json:"profile"`
}
type Layer struct {
ID ID `json:"id"`
Parent ID `json:"parent,omitempty"`
Name string `json:"name"`
Kind LayerKind `json:"kind"`
Visible bool `json:"visible"`
Locked bool `json:"locked"`
AlphaLock bool `json:"alphaLock"`
Opacity uint8 `json:"opacity"`
Blend Blend `json:"blend"`
}
type Frame struct {
ID ID `json:"id"`
DurationMS int `json:"durationMS"`
}
type Cel struct {
ID ID `json:"id"`
Layer ID `json:"layer"`
Frame ID `json:"frame"`
Image ID `json:"image"`
Channel Channel `json:"channel"`
X int `json:"x"`
Y int `json:"y"`
Opacity uint8 `json:"opacity"`
}
type Tag struct {
ID ID `json:"id"`
Name string `json:"name"`
From ID `json:"from"`
To ID `json:"to"`
Direction Direction `json:"direction"`
}
type Slice struct {
ID ID `json:"id"`
Name string `json:"name"`
Frame ID `json:"frame,omitempty"`
Rect image.Rectangle `json:"rect"`
Pivot image.Point `json:"pivot"`
}
type Limits struct {
MaxDimension int
MaxCanvasPixels int
MaxLayers int
MaxFrames int
MaxCels int
MaxPixelBytes int64
MaxFileBytes int64
MaxHistoryBytes int64
MaxHistorySteps int
}
func DefaultLimits() Limits {
return Limits{4096, 4194304, 256, 1024, 262144, 256 << 20, 64 << 20, 64 << 20, 256}
}
func limitsOrDefault(l Limits) Limits {
d := DefaultLimits()
if l.MaxDimension > 0 {
d.MaxDimension = l.MaxDimension
}
if l.MaxCanvasPixels > 0 {
d.MaxCanvasPixels = l.MaxCanvasPixels
}
if l.MaxLayers > 0 {
d.MaxLayers = l.MaxLayers
}
if l.MaxFrames > 0 {
d.MaxFrames = l.MaxFrames
}
if l.MaxCels > 0 {
d.MaxCels = l.MaxCels
}
if l.MaxPixelBytes > 0 {
d.MaxPixelBytes = l.MaxPixelBytes
}
if l.MaxFileBytes > 0 {
d.MaxFileBytes = l.MaxFileBytes
}
if l.MaxHistoryBytes > 0 {
d.MaxHistoryBytes = l.MaxHistoryBytes
}
if l.MaxHistorySteps > 0 {
d.MaxHistorySteps = l.MaxHistorySteps
}
return d
}
type tile [TileSize * TileSize]uint32
type tileKey struct{ X, Y int }
type pixels struct {
Width, Height int
Tiles map[tileKey]*tile
}
func newPixels(w, h int) *pixels { return &pixels{w, h, make(map[tileKey]*tile)} }
func (p *pixels) at(x, y int) uint32 {
if x < 0 || y < 0 || x >= p.Width || y >= p.Height {
return 0
}
t := p.Tiles[tileKey{x / TileSize, y / TileSize}]
if t == nil {
return 0
}
return t[(y%TileSize)*TileSize+x%TileSize]
}
func (p *pixels) clone() *pixels {
q := newPixels(p.Width, p.Height)
for k, t := range p.Tiles {
q.Tiles[k] = t
}
return q
}
type state struct {
Canvas Canvas
Palette []Swatch
Layers []Layer
Frames []Frame
Cels []Cel
Tags []Tag
Slices []Slice
Images map[ID]*pixels
Properties map[string]string
}
func (s *state) clone() *state {
q := *s
q.Palette = append([]Swatch(nil), s.Palette...)
q.Layers = append([]Layer(nil), s.Layers...)
q.Frames = append([]Frame(nil), s.Frames...)
q.Cels = append([]Cel(nil), s.Cels...)
q.Tags = append([]Tag(nil), s.Tags...)
q.Slices = append([]Slice(nil), s.Slices...)
q.Images = make(map[ID]*pixels, len(s.Images))
for id, p := range s.Images {
q.Images[id] = p
}
q.Properties = make(map[string]string, len(s.Properties))
for k, v := range s.Properties {
q.Properties[k] = v
}
return &q
}
// Info is a detached metadata snapshot. IDs are strings, so the JS bridge cannot
// round an entity ID through an IEEE-754 number.
type Info struct {
ID string `json:"id"`
Revision uint64 `json:"revision"`
Canvas Canvas `json:"canvas"`
Palette []Swatch `json:"palette"`
Layers []Layer `json:"layers"`
Frames []Frame `json:"frames"`
Cels []Cel `json:"cels"`
Tags []Tag `json:"tags"`
Slices []Slice `json:"slices"`
Properties map[string]string `json:"properties"`
}
// FramePivot resolves the existing named pivot slices. A frame-specific pivot
// overrides the document-wide pivot regardless of their serialized order.
func (i Info) FramePivot(frame ID) (image.Point, bool) {
var global, specific image.Point
hasGlobal, hasSpecific := false, false
for _, slice := range i.Slices {
if slice.Name != "pivot" {
continue
}
if slice.Frame == "" {
global, hasGlobal = slice.Pivot, true
} else if slice.Frame == frame {
specific, hasSpecific = slice.Pivot, true
}
}
if hasSpecific {
return specific, true
}
return global, hasGlobal
}
type Document struct {
mu sync.Mutex
id string
revision uint64
next uint64
closed bool
limits Limits
current *state
}
func New(w, h int, mode Mode, lim Limits) (*Document, error) {
lim = limitsOrDefault(lim)
if w < 1 || h < 1 || w > lim.MaxDimension || h > lim.MaxDimension || int64(w)*int64(h) > int64(lim.MaxCanvasPixels) {
return nil, ErrLimit
}
if mode != RGBA && mode != Grayscale && mode != Indexed {
return nil, fmt.Errorf("art: unsupported color mode %q", mode)
}
var id [16]byte
if _, err := rand.Read(id[:]); err != nil {
return nil, err
}
s := &state{Canvas: Canvas{w, h, mode, "srgb"}, Palette: DefaultPalette(), Images: make(map[ID]*pixels), Properties: make(map[string]string)}
d := &Document{id: hex.EncodeToString(id[:]), revision: 1, limits: lim, current: s}
layer, frame, im, cel := d.allocate(), d.allocate(), d.allocate(), d.allocate()
s.Layers = []Layer{{ID: layer, Name: "Layer 1", Kind: Raster, Visible: true, Opacity: 255, Blend: BlendNormal}}
s.Frames = []Frame{{frame, 100}}
s.Images[im] = newPixels(w, h)
s.Cels = []Cel{{ID: cel, Layer: layer, Frame: frame, Image: im, Channel: Albedo, Opacity: 255}}
return d, nil
}
func DefaultPalette() []Swatch {
colors := []Color{{0, 0, 0, 0}, {24, 28, 42, 255}, {52, 62, 85, 255}, {98, 111, 137, 255}, {174, 187, 204, 255}, {240, 244, 248, 255}, {255, 105, 110, 255}, {232, 57, 81, 255}, {255, 174, 91, 255}, {255, 219, 110, 255}, {160, 220, 130, 255}, {64, 166, 131, 255}, {77, 195, 209, 255}, {70, 120, 208, 255}, {139, 116, 221, 255}, {224, 137, 207, 255}}
out := make([]Swatch, len(colors))
for i, c := range colors {
out[i] = Swatch{Color: c}
}
return out
}
func (d *Document) allocate() ID { d.next++; return ID(fmt.Sprintf("%016x", d.next)) }
func (d *Document) newID() ID { d.mu.Lock(); defer d.mu.Unlock(); return d.allocate() }
func (d *Document) Revision() uint64 { d.mu.Lock(); defer d.mu.Unlock(); return d.revision }
func info(id string, rev uint64, s *state) Info {
properties := make(map[string]string, len(s.Properties))
for key, value := range s.Properties {
properties[key] = value
}
return Info{id, rev, s.Canvas, append([]Swatch{}, s.Palette...),
append([]Layer{}, s.Layers...), append([]Frame{}, s.Frames...),
append([]Cel{}, s.Cels...), append([]Tag{}, s.Tags...),
append([]Slice{}, s.Slices...), properties}
}
func (d *Document) Info() Info {
d.mu.Lock()
defer d.mu.Unlock()
return info(d.id, d.revision, d.current)
}
func (d *Document) Close() { d.mu.Lock(); defer d.mu.Unlock(); d.closed = true; d.revision++ }
func (s *state) layer(id ID) (int, bool) {
for i := range s.Layers {
if s.Layers[i].ID == id {
return i, true
}
}
return 0, false
}
func (s *state) frame(id ID) (int, bool) {
for i := range s.Frames {
if s.Frames[i].ID == id {
return i, true
}
}
return 0, false
}
func (s *state) cel(id ID) (int, bool) {
for i := range s.Cels {
if s.Cels[i].ID == id {
return i, true
}
}
return 0, false
}
func (s *state) findCel(layer, frame ID, ch Channel) (int, bool) {
for i, c := range s.Cels {
if c.Layer == layer && c.Frame == frame && c.Channel == ch {
return i, true
}
}
return 0, false
}
func (s *state) isLocked(layer ID) bool {
for n := 0; layer != "" && n <= len(s.Layers); n++ {
i, ok := s.layer(layer)
if !ok {
return true
}
if s.Layers[i].Locked {
return true
}
layer = s.Layers[i].Parent
}
return layer != ""
}
func validChannel(ch Channel) bool {
return ch == Albedo || ch == Normal || ch == Height || ch == Emission
}
func validBlend(b Blend) bool {
return b == BlendNormal || b == BlendMultiply || b == BlendScreen || b == BlendAdd
}
func (s *state) validate(l Limits) error {
c := s.Canvas
if c.Width < 1 || c.Height < 1 || c.Width > l.MaxDimension || c.Height > l.MaxDimension || int64(c.Width)*int64(c.Height) > int64(l.MaxCanvasPixels) {
return ErrLimit
}
if c.Mode != RGBA && c.Mode != Grayscale && c.Mode != Indexed {
return fmt.Errorf("art: invalid color mode")
}
if c.Profile != "srgb" {
return fmt.Errorf("art: unsupported color profile %q", c.Profile)
}
if len(s.Layers) < 1 || len(s.Layers) > l.MaxLayers || len(s.Frames) < 1 || len(s.Frames) > l.MaxFrames || len(s.Cels) > l.MaxCels {
return ErrLimit
}
if len(s.Palette) < 1 || len(s.Palette) > 256 {
return fmt.Errorf("art: palette needs 1..256 entries")
}
if c.Mode == Indexed && s.Palette[0].Color.A != 0 {
return fmt.Errorf("art: indexed entry zero is reserved for transparency")
}
ids := make(map[ID]bool)
add := func(id ID) bool {
if id == "" || ids[id] {
return false
}
ids[id] = true
return true
}
for _, v := range s.Layers {
if !add(v.ID) || len(v.Name) > 1024 || !validBlend(v.Blend) || (v.Kind != Raster && v.Kind != Group && v.Kind != Reference) {
return fmt.Errorf("art: invalid layer")
}
p := v.Parent
for n := 0; p != ""; n++ {
if n >= len(s.Layers) || p == v.ID {
return fmt.Errorf("art: cyclic layer group")
}
i, ok := s.layer(p)
if !ok || s.Layers[i].Kind != Group {
return fmt.Errorf("art: invalid layer parent")
}
p = s.Layers[i].Parent
}
}
for _, v := range s.Frames {
if !add(v.ID) || v.DurationMS < 1 || v.DurationMS > 600000 {
return fmt.Errorf("art: invalid frame")
}
}
unique := make(map[[3]string]bool)
used := make(map[ID]bool)
imageChannels := make(map[ID]Channel)
for _, v := range s.Cels {
li, ok := s.layer(v.Layer)
if !ok || s.Layers[li].Kind == Group {
return fmt.Errorf("art: invalid cel layer")
}
if _, ok = s.frame(v.Frame); !ok {
return fmt.Errorf("art: invalid cel frame")
}
key := [3]string{string(v.Layer), string(v.Frame), string(v.Channel)}
if !add(v.ID) || !validChannel(v.Channel) || unique[key] || s.Images[v.Image] == nil {
return fmt.Errorf("art: invalid or duplicate cel")
}
unique[key] = true
if old, ok := imageChannels[v.Image]; ok && old != v.Channel {
return fmt.Errorf("art: one image cannot link different surface channels")
}
imageChannels[v.Image] = v.Channel
used[v.Image] = true
if v.X < -l.MaxDimension || v.X > l.MaxDimension || v.Y < -l.MaxDimension || v.Y > l.MaxDimension {
return ErrLimit
}
}
for _, v := range s.Tags {
a, ok := s.frame(v.From)
b, ok2 := s.frame(v.To)
if !add(v.ID) || !ok || !ok2 || a > b || len(v.Name) == 0 || len(v.Name) > 1024 || (v.Direction != Forward && v.Direction != Reverse && v.Direction != PingPong) {
return fmt.Errorf("art: invalid animation tag")
}
}
for _, v := range s.Slices {
if !add(v.ID) || len(v.Name) > 1024 {
return fmt.Errorf("art: invalid slice")
}
if v.Frame != "" {
if _, ok := s.frame(v.Frame); !ok {
return fmt.Errorf("art: invalid slice frame")
}
}
}
seenTiles := make(map[*tile]bool)
var bytes int64
for id, p := range s.Images {
if !add(id) || !used[id] {
return fmt.Errorf("art: invalid or orphan image")
}
if p.Width < 1 || p.Height < 1 || p.Width > l.MaxDimension || p.Height > l.MaxDimension || int64(p.Width)*int64(p.Height) > int64(l.MaxCanvasPixels) {
return ErrLimit
}
for key, t := range p.Tiles {
if t == nil || key.X < 0 || key.Y < 0 || key.X*TileSize >= p.Width || key.Y*TileSize >= p.Height {
return fmt.Errorf("art: invalid pixel tile")
}
if !seenTiles[t] {
seenTiles[t] = true
bytes += TileSize * TileSize * 4
if bytes > l.MaxPixelBytes {
return ErrLimit
}
}
}
}
if len(s.Properties) > 256 {
return ErrLimit
}
for k, v := range s.Properties {
if len(k) > 1024 || len(v) > 65536 {
return ErrLimit
}
}
return nil
}