Files

311 lines
8.5 KiB
Go

package art
import (
"fmt"
"image"
"image/color"
)
type RenderOptions struct {
Channel Channel
References bool
// Isolate limits presentation to a layer/group and its ancestors. It never
// changes serialized visibility, and normal exports leave this empty.
Isolate ID
}
func options(o RenderOptions) RenderOptions {
if o.Channel == "" {
o.Channel = Albedo
}
return o
}
func (d *Document) Render(frame ID, o RenderOptions) (*image.NRGBA, error) {
d.mu.Lock()
if d.closed {
d.mu.Unlock()
return nil, ErrClosed
}
s := d.current
d.mu.Unlock()
out := image.NewNRGBA(image.Rect(0, 0, s.Canvas.Width, s.Canvas.Height))
if err := renderRegion(s, frame, out, out.Bounds(), options(o)); err != nil {
return nil, err
}
return out, nil
}
func (e *Edit) Render(frame ID, o RenderOptions) (*image.NRGBA, error) {
if err := e.check(); err != nil {
return nil, err
}
out := image.NewNRGBA(image.Rect(0, 0, e.work.Canvas.Width, e.work.Canvas.Height))
if err := renderRegion(e.work, frame, out, out.Bounds(), options(o)); err != nil {
return nil, err
}
return out, nil
}
// RenderRegion recomposites only the damaged canvas rectangle into a caller's
// retained image. Presentation owns this image, never the canonical pixels.
func (d *Document) RenderRegion(frame ID, dst *image.NRGBA, r image.Rectangle, o RenderOptions) error {
d.mu.Lock()
if d.closed {
d.mu.Unlock()
return ErrClosed
}
s := d.current
d.mu.Unlock()
return renderRegion(s, frame, dst, r, options(o))
}
func (e *Edit) RenderRegion(frame ID, dst *image.NRGBA, r image.Rectangle, o RenderOptions) error {
if err := e.check(); err != nil {
return err
}
return renderRegion(e.work, frame, dst, r, options(o))
}
func renderRegion(s *state, frame ID, dst *image.NRGBA, r image.Rectangle, o RenderOptions) error {
if _, ok := s.frame(frame); !ok {
return fmt.Errorf("art: missing frame")
}
if !validChannel(o.Channel) {
return fmt.Errorf("art: invalid channel")
}
bounds := image.Rect(0, 0, s.Canvas.Width, s.Canvas.Height)
if dst == nil || dst.Bounds() != bounds {
return fmt.Errorf("art: composite dimensions mismatch")
}
r = r.Intersect(bounds)
if r.Empty() {
return nil
}
for y := r.Min.Y; y < r.Max.Y; y++ {
i := dst.PixOffset(r.Min.X, y)
clear(dst.Pix[i : i+r.Dx()*4])
}
cels := make(map[ID]Cel)
for _, c := range s.Cels {
if c.Frame == frame && c.Channel == o.Channel {
cels[c.Layer] = c
}
}
var isolated map[ID]bool
if o.Isolate != "" {
index, found := s.layer(o.Isolate)
if !found {
return fmt.Errorf("art: missing isolated layer")
}
isolated = map[ID]bool{o.Isolate: true}
// Include descendants of an isolated group, but not siblings of any
// ancestor. Ancestors still supply their existing opacity and blend.
for changed := true; changed; {
changed = false
for _, layer := range s.Layers {
if layer.Parent != "" && isolated[layer.Parent] && !isolated[layer.ID] {
isolated[layer.ID], changed = true, true
}
}
}
for parent := s.Layers[index].Parent; parent != ""; {
if isolated[parent] {
return fmt.Errorf("art: cyclic isolated groups")
}
isolated[parent] = true
index, found = s.layer(parent)
if !found {
return fmt.Errorf("art: missing isolated group")
}
parent = s.Layers[index].Parent
}
}
var layers func(ID, *image.NRGBA, int) error
layers = func(parent ID, target *image.NRGBA, depth int) error {
if depth > len(s.Layers) {
return fmt.Errorf("art: cyclic groups")
}
for _, l := range s.Layers {
if isolated != nil && !isolated[l.ID] {
continue
}
if l.Parent != parent || !l.Visible || l.Opacity == 0 || (l.Kind == Reference && !o.References) {
continue
}
var group *image.NRGBA
var p *pixels
var cel Cel
if l.Kind == Group {
group = image.NewNRGBA(r)
if err := layers(l.ID, group, depth+1); err != nil {
return err
}
} else {
var ok bool
cel, ok = cels[l.ID]
if !ok {
continue
}
p = s.Images[cel.Image]
}
composite := func(x, y int, src Color) {
if src.A == 0 {
// Normal, height and emission textures retain RGB independently
// of alpha. When the composite is also transparent, the
// topmost nonzero data pixel supplies its hidden RGB. This
// must not cover any pixel with alpha. Albedo retains normal
// transparent compositing semantics.
if o.Channel != Albedo && (group != nil || cel.Opacity != 0) && (src.R != 0 || src.G != 0 || src.B != 0) {
i := target.PixOffset(x, y)
if target.Pix[i+3] == 0 {
target.Pix[i], target.Pix[i+1], target.Pix[i+2] = src.R, src.G, src.B
}
}
return
}
i := target.PixOffset(x, y)
v := blendPixel(Color{target.Pix[i], target.Pix[i+1], target.Pix[i+2], target.Pix[i+3]}, src, l.Opacity, l.Blend)
target.Pix[i], target.Pix[i+1], target.Pix[i+2], target.Pix[i+3] = v.R, v.G, v.B, v.A
}
if group != nil {
for y := r.Min.Y; y < r.Max.Y; y++ {
for x := r.Min.X; x < r.Max.X; x++ {
v := group.NRGBAAt(x, y)
composite(x, y, Color{v.R, v.G, v.B, v.A})
}
}
continue
}
// Canonical pixels are sparse tiles. Tiles do not overlap, so their
// map iteration order cannot affect source-over ordering between layers.
// Empty canvas areas need neither sampling nor palette lookup.
cs := s.forChannel(cel.Channel)
for key, block := range p.Tiles {
x0, y0 := key.X*TileSize, key.Y*TileSize
area := image.Rect(x0+cel.X, y0+cel.Y,
min(x0+TileSize, p.Width)+cel.X, min(y0+TileSize, p.Height)+cel.Y).Intersect(r)
for y := area.Min.Y; y < area.Max.Y; y++ {
for x := area.Min.X; x < area.Max.X; x++ {
v := block[(y-cel.Y-y0)*TileSize+x-cel.X-x0]
src := decode(cs, v)
src.A = uint8((uint32(src.A)*uint32(cel.Opacity) + 127) / 255)
composite(x, y, src)
}
}
}
}
return nil
}
return layers("", dst, 0)
}
// FrameAt uses integer milliseconds and does not mutate the author document.
// Ping-pong does not double the endpoints (0,1,2,1 for three frames).
func (d *Document) FrameAt(tag ID, ms int64) (ID, error) {
d.mu.Lock()
defer d.mu.Unlock()
if d.closed {
return "", ErrClosed
}
return frameAt(d.current, tag, ms)
}
func frameOrder(s *state, tag ID) ([]Frame, error) {
start, end, dir := 0, len(s.Frames)-1, Forward
if tag != "" {
found := false
for _, t := range s.Tags {
if t.ID == tag {
start, _ = s.frame(t.From)
end, _ = s.frame(t.To)
dir = t.Direction
found = true
break
}
}
if !found {
return nil, fmt.Errorf("art: missing animation tag")
}
}
frames := append([]Frame(nil), s.Frames[start:end+1]...)
switch dir {
case Reverse:
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
frames[i], frames[j] = frames[j], frames[i]
}
case PingPong:
for i := len(frames) - 2; i > 0; i-- {
frames = append(frames, frames[i])
}
}
return frames, nil
}
func frameAt(s *state, tag ID, ms int64) (ID, error) {
frames, err := frameOrder(s, tag)
if err != nil {
return "", err
}
var total int64
for _, f := range frames {
total += int64(f.DurationMS)
}
if total == 0 {
return "", fmt.Errorf("art: empty animation")
}
ms %= total
if ms < 0 {
ms += total
}
for _, f := range frames {
if ms < int64(f.DurationMS) {
return f.ID, nil
}
ms -= int64(f.DurationMS)
}
return frames[0].ID, nil
}
func (d *Document) Onion(frame ID, before, after int, o RenderOptions) (*image.NRGBA, error) {
d.mu.Lock()
if d.closed {
d.mu.Unlock()
return nil, ErrClosed
}
s := d.current
d.mu.Unlock()
fi, ok := s.frame(frame)
if !ok {
return nil, fmt.Errorf("art: missing frame")
}
if before < 0 || before > 8 || after < 0 || after > 8 {
return nil, ErrLimit
}
out := image.NewNRGBA(image.Rect(0, 0, s.Canvas.Width, s.Canvas.Height))
for side := 0; side < 2; side++ {
count, step := before, -1
tint := Color{255, 100, 100, 255}
if side == 1 {
count, step = after, 1
tint = Color{100, 180, 255, 255}
}
for n := count; n >= 1; n-- {
idx := fi + step*n
if idx < 0 || idx >= len(s.Frames) {
continue
}
im := image.NewNRGBA(out.Bounds())
if err := renderRegion(s, s.Frames[idx].ID, im, im.Bounds(), options(o)); err != nil {
return nil, err
}
for y := 0; y < s.Canvas.Height; y++ {
for x := 0; x < s.Canvas.Width; x++ {
c := im.NRGBAAt(x, y)
if c.A == 0 {
continue
}
a := uint8(int(c.A) / (n + 2))
p := out.NRGBAAt(x, y)
v := blendPixel(Color{p.R, p.G, p.B, p.A}, Color{tint.R, tint.G, tint.B, a}, 255, BlendNormal)
out.SetNRGBA(x, y, color.NRGBA{R: v.R, G: v.G, B: v.B, A: v.A})
}
}
}
}
return out, nil
}