139 lines
4.1 KiB
Go
139 lines
4.1 KiB
Go
package art
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
)
|
|
|
|
// MergeDown bakes two adjacent normal-blend raster (or reference) siblings for
|
|
// every frame and surface channel. Their parent, frame IDs, lower layer ID and
|
|
// existing lower cel IDs survive. A repeated pair of linked images remains
|
|
// linked, while images shared with other layers are never overwritten.
|
|
//
|
|
// Backdrop-dependent blend modes are rejected instead of silently changing
|
|
// their appearance over unrelated lower layers. Indexed art is accepted only
|
|
// when the exact merged colors are representable in its current palette.
|
|
func (e *Edit) MergeDown(ctx context.Context, upper ID) (ID, error) {
|
|
if err := e.check(); err != nil {
|
|
return "", err
|
|
}
|
|
ui, ok := e.work.layer(upper)
|
|
if !ok {
|
|
return "", fmt.Errorf("art: missing layer")
|
|
}
|
|
top := e.work.Layers[ui]
|
|
li := -1
|
|
for i := ui - 1; i >= 0; i-- {
|
|
if e.work.Layers[i].Parent == top.Parent {
|
|
li = i
|
|
break
|
|
}
|
|
}
|
|
if li < 0 {
|
|
return "", fmt.Errorf("art: no layer below in the same group")
|
|
}
|
|
lower := e.work.Layers[li]
|
|
if top.Kind == Group || lower.Kind != top.Kind {
|
|
return "", fmt.Errorf("art: merge requires two raster or two reference layers")
|
|
}
|
|
if e.work.isLocked(upper) || e.work.isLocked(lower.ID) {
|
|
return "", ErrLocked
|
|
}
|
|
if !top.Visible || !lower.Visible {
|
|
return "", fmt.Errorf("art: show both layers before merging")
|
|
}
|
|
if top.Blend != BlendNormal || lower.Blend != BlendNormal {
|
|
return "", fmt.Errorf("art: merge requires Normal blend to preserve the existing backdrop")
|
|
}
|
|
before := e.work.clone()
|
|
pair := before.clone()
|
|
lower.Parent, top.Parent = "", ""
|
|
pair.Layers = []Layer{lower, top}
|
|
type source struct {
|
|
Image ID
|
|
X, Y int
|
|
Opacity uint8
|
|
}
|
|
type identity struct {
|
|
Lower, Upper source
|
|
Channel Channel
|
|
}
|
|
cache := make(map[identity]ID)
|
|
colorIndices := make(map[Color]uint32)
|
|
for i, swatch := range before.Palette {
|
|
if _, exists := colorIndices[swatch.Color]; !exists {
|
|
colorIndices[swatch.Color] = uint32(i)
|
|
}
|
|
}
|
|
for _, frame := range before.Frames {
|
|
for _, channel := range []Channel{Albedo, Normal, Height, Emission} {
|
|
if err := ctx.Err(); err != nil {
|
|
return "", err
|
|
}
|
|
key := identity{Channel: channel}
|
|
present := false
|
|
for _, cel := range before.Cels {
|
|
if cel.Frame != frame.ID || cel.Channel != channel {
|
|
continue
|
|
}
|
|
value := source{cel.Image, cel.X, cel.Y, cel.Opacity}
|
|
if cel.Layer == lower.ID {
|
|
key.Lower, present = value, true
|
|
} else if cel.Layer == upper {
|
|
key.Upper, present = value, true
|
|
}
|
|
}
|
|
if !present {
|
|
continue
|
|
}
|
|
merged := cache[key]
|
|
if merged == "" {
|
|
im := image.NewNRGBA(image.Rect(0, 0, before.Canvas.Width, before.Canvas.Height))
|
|
if err := renderRegion(pair, frame.ID, im, im.Bounds(), RenderOptions{Channel: channel, References: true}); err != nil {
|
|
return "", err
|
|
}
|
|
merged = e.doc.newID()
|
|
e.work.Images[merged] = newPixels(im.Bounds().Dx(), im.Bounds().Dy())
|
|
for y := 0; y < im.Bounds().Dy(); y++ {
|
|
if err := ctx.Err(); err != nil {
|
|
return "", err
|
|
}
|
|
for x := 0; x < im.Bounds().Dx(); x++ {
|
|
p := im.NRGBAAt(x, y)
|
|
color := Color{p.R, p.G, p.B, p.A}
|
|
var pixel uint32
|
|
if channel == Albedo && before.Canvas.Mode == Indexed {
|
|
if color.A != 0 {
|
|
var exact bool
|
|
pixel, exact = colorIndices[color]
|
|
if !exact {
|
|
return "", fmt.Errorf("art: merged color is outside the indexed palette; convert to RGBA explicitly")
|
|
}
|
|
}
|
|
} else {
|
|
pixel = encode(before.forChannel(channel), color)
|
|
}
|
|
if err := e.raw(merged, x, y, pixel); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
}
|
|
cache[key] = merged
|
|
}
|
|
if _, err := e.EnsureCel(lower.ID, frame.ID, channel); err != nil {
|
|
return "", err
|
|
}
|
|
ci, _ := e.work.findCel(lower.ID, frame.ID, channel)
|
|
e.work.Cels[ci].Image = merged
|
|
e.work.Cels[ci].X, e.work.Cels[ci].Y, e.work.Cels[ci].Opacity = 0, 0, 255
|
|
}
|
|
}
|
|
e.work.Layers[li].Opacity = 255
|
|
e.touch(image.Rect(0, 0, before.Canvas.Width, before.Canvas.Height))
|
|
if err := e.DeleteLayer(upper); err != nil {
|
|
return "", err
|
|
}
|
|
return lower.ID, nil
|
|
}
|