Save/load blend mode/opacity for group layers when needed (#3225)

This commit is contained in:
David Capello 2024-06-25 13:55:42 -03:00
parent d46d791339
commit 76a8a8af7a
7 changed files with 64 additions and 23 deletions

View File

@ -75,8 +75,10 @@ A 128-byte header (same as FLC/FLI header, but with other magic number):
32 bpp = RGBA
16 bpp = Grayscale
8 bpp = Indexed
DWORD Flags:
DWORD Flags (see NOTE.6):
1 = Layer opacity has valid value
2 = Layer blend mode/opacity is valid for groups
(composite groups separately first when rendering)
WORD Speed (milliseconds between frame, like in FLC files)
DEPRECATED: You should use the frame duration field
from each frame header
@ -175,7 +177,7 @@ entire layers layout:
WORD Layer child level (see NOTE.1)
WORD Default layer width in pixels (ignored)
WORD Default layer height in pixels (ignored)
WORD Blend mode (always 0 for layer set)
WORD Blend mode (see NOTE.6)
Normal = 0
Multiply = 1
Screen = 2
@ -195,8 +197,7 @@ entire layers layout:
Addition = 16
Subtract = 17
Divide = 18
BYTE Opacity
Note: valid only if file header flags field has bit 1 set
BYTE Opacity (see NOTE.6)
BYTE[3] For future (set to zero)
STRING Layer name
+ If layer type = 2
@ -609,6 +610,14 @@ if this value is the same, we compare the specific `zIndex` value to
disambiguate some scenarios. An example of this implementation can be
found in the [RenderPlan code](https://github.com/aseprite/aseprite/blob/8e91d22b704d6d1e95e1482544318cee9f166c4d/src/doc/render_plan.cpp#L77).
### NOTE.6
The blend mode and opacity fields of Layer Chunks (0x2004) are always
valid for image and tilemap layers. The opacity field only when the
[main header](#header) "Flags" field has the bit 1 enabled. Both
fields are valid for group layers too when the same "Flags" field has
the bit 2.
## File Format Changes
1. The first change from the first release of the new .ase format,

View File

@ -145,7 +145,8 @@ public:
} // anonymous namespace
static void ase_file_prepare_header(FILE* f, dio::AsepriteHeader* header, const Sprite* sprite,
const frame_t firstFrame, const frame_t totalFrames);
frame_t firstFrame, frame_t totalFrames,
bool composeGroups);
static void ase_file_write_header(FILE* f, dio::AsepriteHeader* header);
static void ase_file_write_header_filesize(FILE* f, dio::AsepriteHeader* header);
@ -153,6 +154,7 @@ static void ase_file_prepare_frame_header(FILE* f, dio::AsepriteFrameHeader* fra
static void ase_file_write_frame_header(FILE* f, dio::AsepriteFrameHeader* frame_header);
static void ase_file_write_layers(FILE* f, FileOp* fop,
const dio::AsepriteHeader* header,
dio::AsepriteFrameHeader* frame_header,
const dio::AsepriteExternalFiles& ext_files,
const Layer* layer, int child_level);
@ -171,7 +173,10 @@ static void ase_file_write_close_chunk(FILE* f, dio::AsepriteChunk* chunk);
static void ase_file_write_color2_chunk(FILE* f, dio::AsepriteFrameHeader* frame_header, const Palette* pal);
static void ase_file_write_palette_chunk(FILE* f, dio::AsepriteFrameHeader* frame_header, const Palette* pal, int from, int to);
static void ase_file_write_layer_chunk(FILE* f, dio::AsepriteFrameHeader* frame_header, const Layer* layer, int child_level);
static void ase_file_write_layer_chunk(FILE* f, const dio::AsepriteHeader* header,
dio::AsepriteFrameHeader* frame_header,
const Layer* layer,
int child_level);
static void ase_file_write_cel_chunk(FILE* f, dio::AsepriteFrameHeader* frame_header,
const Cel* cel,
const LayerImage* layer,
@ -350,7 +355,8 @@ bool AseFormat::onSave(FileOp* fop)
dio::AsepriteHeader header;
ase_file_prepare_header(f, &header, sprite,
fop->roi().fromFrame(),
fop->roi().frames());
fop->roi().frames(),
fop->config().composeGroups);
ase_file_write_header(f, &header);
bool require_new_palette_chunk = false;
@ -427,7 +433,7 @@ bool AseFormat::onSave(FileOp* fop)
// before layers so older version don't get confused by the new
// user data chunks for tags.
for (Layer* child : sprite->root()->layers())
ase_file_write_layers(f, fop, &frame_header, ext_files, child, 0);
ase_file_write_layers(f, fop, &header, &frame_header, ext_files, child, 0);
// Write slice chunks
ase_file_write_slice_chunks(f, fop, &frame_header,
@ -468,8 +474,11 @@ bool AseFormat::onSave(FileOp* fop)
#endif // ENABLE_SAVE
static void ase_file_prepare_header(FILE* f, dio::AsepriteHeader* header, const Sprite* sprite,
const frame_t firstFrame, const frame_t totalFrames)
static void ase_file_prepare_header(FILE* f, dio::AsepriteHeader* header,
const Sprite* sprite,
const frame_t firstFrame,
const frame_t totalFrames,
const bool composeGroups)
{
header->pos = ftell(f);
@ -481,7 +490,8 @@ static void ase_file_prepare_header(FILE* f, dio::AsepriteHeader* header, const
header->depth = (sprite->pixelFormat() == IMAGE_RGB ? 32:
sprite->pixelFormat() == IMAGE_GRAYSCALE ? 16:
sprite->pixelFormat() == IMAGE_INDEXED ? 8: 0);
header->flags = ASE_FILE_FLAG_LAYER_WITH_OPACITY;
header->flags = (ASE_FILE_FLAG_LAYER_WITH_OPACITY |
(composeGroups ? ASE_FILE_FLAG_COMPOSITE_GROUPS: 0));
header->speed = sprite->frameDuration(firstFrame);
header->next = 0;
header->frit = 0;
@ -569,17 +579,18 @@ static void ase_file_write_frame_header(FILE* f, dio::AsepriteFrameHeader* frame
}
static void ase_file_write_layers(FILE* f, FileOp* fop,
const dio::AsepriteHeader* header,
dio::AsepriteFrameHeader* frame_header,
const dio::AsepriteExternalFiles& ext_files,
const Layer* layer, int child_index)
{
ase_file_write_layer_chunk(f, frame_header, layer, child_index);
ase_file_write_layer_chunk(f, header, frame_header, layer, child_index);
if (!layer->userData().isEmpty())
ase_file_write_user_data_chunk(f, fop, frame_header, ext_files, &layer->userData());
if (layer->isGroup()) {
for (const Layer* child : static_cast<const LayerGroup*>(layer)->layers())
ase_file_write_layers(f, fop, frame_header, ext_files, child, child_index+1);
ase_file_write_layers(f, fop, header, frame_header, ext_files, child, child_index+1);
}
}
@ -709,7 +720,11 @@ static void ase_file_write_palette_chunk(FILE* f, dio::AsepriteFrameHeader* fram
}
}
static void ase_file_write_layer_chunk(FILE* f, dio::AsepriteFrameHeader* frame_header, const Layer* layer, int child_level)
static void ase_file_write_layer_chunk(FILE* f,
const dio::AsepriteHeader* header,
dio::AsepriteFrameHeader* frame_header,
const Layer* layer,
const int child_level)
{
ChunkWriter chunk(f, frame_header, ASE_FILE_CHUNK_LAYER);
@ -718,13 +733,21 @@ static void ase_file_write_layer_chunk(FILE* f, dio::AsepriteFrameHeader* frame_
static_cast<int>(doc::LayerFlags::PersistentFlagsMask), f);
// Layer type
bool saveBlendInfo = false;
int layerType = ASE_FILE_LAYER_IMAGE;
if (layer->isImage()) {
saveBlendInfo = true;
if (layer->isTilemap())
layerType = ASE_FILE_LAYER_TILEMAP;
}
else if (layer->isGroup()) {
layerType = ASE_FILE_LAYER_GROUP;
// If the "composite groups" flag is not specified, group layers
// don't contain blend mode + opacity.
if ((header->flags & ASE_FILE_FLAG_COMPOSITE_GROUPS) == ASE_FILE_FLAG_COMPOSITE_GROUPS) {
saveBlendInfo = true;
}
}
fputw(layerType, f);
@ -734,8 +757,8 @@ static void ase_file_write_layer_chunk(FILE* f, dio::AsepriteFrameHeader* frame_
// Default width & height, and blend mode
fputw(0, f);
fputw(0, f);
fputw(layer->isImage() ? (int)static_cast<const LayerImage*>(layer)->blendMode(): 0, f);
fputc(layer->isImage() ? (int)static_cast<const LayerImage*>(layer)->opacity(): 0, f);
fputw(saveBlendInfo ? int(layer->blendMode()): 0, f);
fputc(saveBlendInfo ? int(layer->opacity()): 0, f);
// Padding
ase_file_write_padding(f, 3);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -26,6 +26,7 @@ void FileOpConfig::fillFromPreferences()
workingCS = get_working_rgb_space_from_preferences();
rgbMapAlgorithm = pref.quantization.rgbmapAlgorithm();
cacheCompressedTilesets = pref.tileset.cacheCompressedTilesets();
composeGroups = pref.experimental.composeGroups();
}
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -42,6 +42,11 @@ namespace app {
// compressed data that was loaded as-is).
bool cacheCompressedTilesets = true;
// True if layer groups are composed in a separate image first,
// and then composed with the rest of the sprite. In this case
// blend mode and opacity fields are valid for groups too.
bool composeGroups = false;
void fillFromPreferences();
};

View File

@ -1,4 +1,4 @@
Copyright (c) 2018-2023 Igara Studio S.A.
Copyright (c) 2018-2024 Igara Studio S.A.
Copyright (c) 2016-2018 David Capello
Permission is hereby granted, free of charge, to any person obtaining

View File

@ -18,6 +18,7 @@
#define ASE_FILE_FRAME_MAGIC 0xF1FA
#define ASE_FILE_FLAG_LAYER_WITH_OPACITY 1
#define ASE_FILE_FLAG_COMPOSITE_GROUPS 2
#define ASE_FILE_CHUNK_FLI_COLOR2 4
#define ASE_FILE_CHUNK_FLI_COLOR 11

View File

@ -1,5 +1,5 @@
// Aseprite Document IO Library
// Copyright (c) 2018-2023 Igara Studio S.A.
// Copyright (c) 2018-2024 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -600,12 +600,14 @@ doc::Layer* AsepriteDecoder::readLayerChunk(AsepriteHeader* header,
}
if (layer) {
if (layer->isImage() &&
const bool composeGroups = (header->flags & ASE_FILE_FLAG_COMPOSITE_GROUPS);
if ((layer->isImage() || (layer->isGroup() && composeGroups)) &&
// Only transparent layers can have blend mode and opacity
!(flags & int(doc::LayerFlags::Background))) {
static_cast<doc::LayerImage*>(layer)->setBlendMode((doc::BlendMode)blendmode);
layer->setBlendMode((doc::BlendMode)blendmode);
if (header->flags & ASE_FILE_FLAG_LAYER_WITH_OPACITY)
static_cast<doc::LayerImage*>(layer)->setOpacity(opacity);
layer->setOpacity(opacity);
}
// flags