Some aseprite loading work. Still need to solve decompression

This commit is contained in:
Matt Thorson 2020-05-26 20:30:16 -07:00
parent 524bb14919
commit aed77272ac
4 changed files with 666 additions and 5 deletions

View File

@ -1,8 +1,7 @@
FileVersion = 1 FileVersion = 1
Dependencies = {corlib = "*", SDL2 = "*"} Dependencies = {corlib = "*", SDL2 = "*", MiniZ = "*"}
[Project] [Project]
Name = "Strawberry" Name = "Strawberry"
TargetType = "BeefLib" TargetType = "BeefLib"
StartupObject = "Program" StartupObject = "Program"
DefaultNamespace = "Strawberry"

View File

@ -15,6 +15,7 @@ namespace Strawberry
public class Game public class Game
{ {
public readonly Dictionary<String, Sprite> Sprites;
public readonly List<VirtualInput> VirtualInputs; public readonly List<VirtualInput> VirtualInputs;
public readonly String Title; public readonly String Title;
public readonly int Width; public readonly int Width;
@ -42,7 +43,6 @@ namespace Strawberry
: base() : base()
{ {
Game = this; Game = this;
VirtualInputs = new List<VirtualInput>();
Title = windowTitle; Title = windowTitle;
Width = width; Width = width;
@ -81,8 +81,13 @@ namespace Strawberry
SDLMixer.OpenAudio(44100, SDLMixer.MIX_DEFAULT_FORMAT, 2, 4096); SDLMixer.OpenAudio(44100, SDLMixer.MIX_DEFAULT_FORMAT, 2, 4096);
SDLTTF.Init(); SDLTTF.Init();
BuildTypeLists(); VirtualInputs = new List<VirtualInput>();
Input.[Friend]Init(gamepadLimit); Input.[Friend]Init(gamepadLimit);
Sprites = new Dictionary<String, Sprite>();
//LoadSprites();
BuildTypeLists();
} }
public ~this() public ~this()
@ -104,6 +109,8 @@ namespace Strawberry
DisposeTypeLists(); DisposeTypeLists();
Input.[Friend]Dispose(); Input.[Friend]Dispose();
DisposeSprites();
Sprite.[Friend]Dispose();
Game = null; Game = null;
} }
@ -184,7 +191,7 @@ namespace Strawberry
} }
} }
public void Render() private void Render()
{ {
SDL.SetRenderDrawColor(Renderer, ClearColor.R, ClearColor.G, ClearColor.B, ClearColor.A); SDL.SetRenderDrawColor(Renderer, ClearColor.R, ClearColor.G, ClearColor.B, ClearColor.A);
SDL.RenderClear(Renderer); SDL.RenderClear(Renderer);
@ -214,6 +221,51 @@ namespace Strawberry
} }
} }
// Load Images
private void LoadSprites()
{
let root = scope String(ContentRoot);
root.Append(Path.DirectorySeparatorChar);
root.Append("Sprites");
if (Directory.Exists(root))
LoadSpritesDir(root);
else
Console.WriteLine("Content/Sprites folder does not exist!");
}
private void LoadSpritesDir(String directory)
{
for (let dir in Directory.EnumerateDirectories(directory))
{
let path = scope String();
dir.GetFilePath(path);
LoadSpritesDir(path);
}
for (let file in Directory.EnumerateFiles(directory, "*.ase"))
{
let path = scope String();
file.GetFilePath(path);
let sprite = new [Friend]Sprite(path);
path.Remove(0, ContentRoot.Length + 8);
Sprites.Add(new String(path), sprite);
}
}
private void DisposeSprites()
{
for (let kv in Sprites)
{
delete kv.key;
delete kv.value;
}
delete Sprites;
}
// Type assignable caching // Type assignable caching
private void BuildTypeLists() private void BuildTypeLists()

604
src/Core/Sprite.bf Normal file
View File

@ -0,0 +1,604 @@
using System;
using SDL2;
using System.IO;
using System.Text;
using System.Collections;
using MiniZ;
namespace Strawberry
{
public class Sprite
{
private enum Modes
{
Indexed = 1,
Grayscale = 2,
RGBA = 4,
}
private enum Chunks
{
OldPaletteA = 0x0004,
OldPaletteB = 0x0011,
Layer = 0x2004,
Cel = 0x2005,
CelExtra = 0x2006,
Mask = 0x2016,
Path = 0x2017,
FrameTags = 0x2018,
Palette = 0x2019,
UserData = 0x2020,
Slice = 0x2022
}
public readonly String Path;
private Frame[] frames;
private List<Layer> layers;
private List<Tag> tags;
private List<Slice> slices;
private int width;
private int height;
private Modes mode;
private this(String path)
{
Path = new String(path);
Load();
}
public ~this()
{
delete Path;
Unload();
}
private void Unload()
{
for (let f in frames)
delete f;
delete frames;
for (let l in layers)
delete l;
delete layers;
for (let t in tags)
delete t;
delete tags;
for (let s in slices)
delete s;
delete slices;
}
public void Reload()
{
Unload();
Load();
}
private void Load()
{
/*
Aseprite file loading based on code from Noel Berry's Foster Framework here:
https://github.com/NoelFB/Foster/blob/master/Framework/Graphics/Images/Aseprite.cs
*/
let stream = scope FileStream();
stream.Open(Path, .Read, .Read);
//Helpers to match ASE file format spec
uint8 BYTE() => stream.Read<uint8>();
uint16 WORD() => stream.Read<uint16>();
int16 SHORT() => stream.Read<int16>();
uint32 DWORD() => stream.Read<uint32>();
int32 LONG() => stream.Read<int32>();
void SEEK(int bytes) => stream.Position += bytes;
void BYTES(uint8[] into, int bytes)
{
for (let i < bytes)
into[i] = BYTE();
}
String STRING(String into)
{
let len = WORD();
let arr = scope uint8[len];
for (let i < len)
arr[i] = BYTE();
Encoding.UTF8.DecodeToUTF8(arr, into);
return into;
}
//Parse
{
//Header
{
//File Size
DWORD();
// Magic number
var magic = WORD();
if (magic != 0xA5E0)
Runtime.FatalError("File is not in .ase format");
// Frame Count / Width / Height / Color Mode
frames = new Frame[WORD()];
width = WORD();
height = WORD();
mode = (Modes)(WORD() / 8);
// Other Info, Ignored
DWORD(); // Flags
WORD(); // Speed (deprecated)
DWORD(); // Set be 0
DWORD(); // Set be 0
BYTE(); // Palette entry
SEEK(3); // Ignore these bytes
WORD(); // Number of colors (0 means 256 for old sprites)
BYTE(); // Pixel width
BYTE(); // Pixel height
SEEK(92); // For Future
}
layers = new List<Layer>();
tags = new List<Tag>();
slices = new List<Slice>();
// Body
{
var temp = scope:: uint8[width * height * (int)mode];
let palette = scope:: Color[256];
HasUserData last = null;
for (int i = 0; i < frames.Count; i++)
{
let frame = new Frame(width, height);
frames[i] = frame;
int64 frameStart, frameEnd;
int chunkCount;
// frame header
{
frameStart = stream.Position;
frameEnd = frameStart + DWORD();
WORD(); // Magic number (always 0xF1FA)
chunkCount = WORD(); // Number of "chunks" in this frame
frame.Duration = WORD(); // Frame duration (in milliseconds)
SEEK(6); // For future (set to zero)
}
// chunks
for (int j = 0; j < chunkCount; j++)
{
int64 chunkStart, chunkEnd;
Chunks chunkType;
// chunk header
{
chunkStart = stream.Position;
chunkEnd = chunkStart + DWORD();
chunkType = (Chunks)WORD();
}
// LAYER CHUNK
if (chunkType == Chunks.Layer)
{
// create layer
var layer = new Layer();
// get layer data
layer.Flag = (Layer.Flags)WORD();
layer.Type = (Layer.Types)WORD();
layer.ChildLevel = WORD();
WORD(); // width (unused)
WORD(); // height (unused)
layer.BlendMode = WORD();
layer.Alpha = (BYTE() / 255f);
SEEK(3); // for future
STRING(layer.Name);
layers.Add(layer);
}
// CEL CHUNK
else if (chunkType == Chunks.Cel) Cel:
{
var layer = layers[WORD()];
var x = SHORT();
var y = SHORT();
var alpha = BYTE() / 255f;
var celType = WORD();
var width = 0;
var height = 0;
Color[] pixels = null;
Cel link = null;
SEEK(7);
// RAW or DEFLATE
if (celType == 0 || celType == 2)
{
width = WORD();
height = WORD();
var count = width * height * (int)mode;
if (count > temp.Count)
temp = scope:: uint8[count];
// RAW
if (celType == 0)
{
BYTES(temp, count);
}
// DEFLATE
else
{
//TODO: Figure this out to enable aseprite loading
Runtime.FatalError("Decompression not yet implemented.");
/*
Noel's C#:
SEEK(2);
using var deflate = new DeflateStream(reader.BaseStream, CompressionMode.Decompress, true);
deflate.Read(temp, 0, count);
*/
}
// get pixel data
pixels = scope:Cel Color[width * height];
BytesToPixels(temp, pixels, mode, palette);
}
// REFERENCE
else if (celType == 1)
{
var linkFrame = frames[WORD()];
var linkCel = linkFrame.Cels[frame.Cels.Count];
width = linkCel.Width;
height = linkCel.Height;
pixels = linkCel.Pixels;
link = linkCel;
}
else
Runtime.FatalError("Cel type not yet implemented");
var cel = new Cel(layer, pixels);
cel.X = x;
cel.Y = y;
cel.Width = width;
cel.Height = height;
cel.Alpha = alpha;
cel.Link = link;
// draw to frame if visible
if (cel.Layer.Visible)
CelToFrame(frame, cel);
frame.Cels.Add(cel);
}
// PALETTE CHUNK
else if (chunkType == Chunks.Palette)
{
DWORD(); //size (unused)
var start = DWORD();
var end = DWORD();
SEEK(8); // for future
for (int p = 0; p < (end - start) + 1; p++)
{
var hasName = WORD();
palette[start + p] = Color(BYTE(), BYTE(), BYTE(), BYTE());
if (Calc.BitCheck(hasName, 0))
STRING(scope String());
}
}
// USERDATA
else if (chunkType == Chunks.UserData)
{
if (last != null)
{
var flags = (int)DWORD();
// has text
if (Calc.BitCheck(flags, 0))
STRING(last.UserDataText);
// has color
if (Calc.BitCheck(flags, 1))
last.UserDataColor = Color(BYTE(), BYTE(), BYTE(), BYTE());
}
}
// TAG
else if (chunkType == Chunks.FrameTags)
{
var count = WORD();
SEEK(8);
for (int t = 0; t < count; t++)
{
var tag = new Tag();
tag.From = WORD();
tag.To = WORD();
tag.LoopDirection = (Tag.LoopDirections)BYTE();
SEEK(8);
tag.Color = Color(BYTE(), BYTE(), BYTE(), (uint8)255);
SEEK(1);
STRING(tag.Name);
tags.Add(tag);
}
}
// SLICE
else if (chunkType == Chunks.Slice)
{
var count = DWORD();
var flags = (int)DWORD();
DWORD(); // reserved
var name = STRING(scope String());
for (int s = 0; s < count; s++)
{
var slice = new Slice();
slice.Name.Set(name);
slice.Frame = (int)DWORD();
slice.OriginX = (int)LONG();
slice.OriginY = (int)LONG();
slice.Width = (int)DWORD();
slice.Height = (int)DWORD();
// 9 slice (ignored atm)
if (Calc.BitCheck(flags, 0))
{
slice.NineSlice = Rect(
(int)LONG(),
(int)LONG(),
(int)DWORD(),
(int)DWORD());
}
// pivot point
if (Calc.BitCheck(flags, 1))
slice.Pivot = Point((int)DWORD(), (int)DWORD());
slices.Add(slice);
}
}
stream.Position = chunkEnd;
}
stream.Position = frameEnd;
frame.FinishLoad();
}
}
}
stream.Close();
}
private void BytesToPixels(uint8[] bytes, Color[] pixels, Modes mode, Color[] palette)
{
int len = pixels.Count;
if (mode == Modes.RGBA)
{
for (int p = 0, int b = 0; p < len; p++, b += 4)
{
pixels[p].R = (uint8)(bytes[b + 0] * bytes[b + 3] / 255);
pixels[p].G = (uint8)(bytes[b + 1] * bytes[b + 3] / 255);
pixels[p].B = (uint8)(bytes[b + 2] * bytes[b + 3] / 255);
pixels[p].A = bytes[b + 3];
}
}
else if (mode == Modes.Grayscale)
{
for (int p = 0, int b = 0; p < len; p++, b += 2)
{
pixels[p].R = pixels[p].G = pixels[p].B = (uint8)(bytes[b + 0] * bytes[b + 1] / 255);
pixels[p].A = bytes[b + 1];
}
}
else if (mode == Modes.Indexed)
{
for (int p = 0; p < len; p++)
pixels[p] = palette[bytes[p]];
}
}
private void CelToFrame(Frame frame, Cel cel)
{
let opacity = (uint8)((cel.Alpha * cel.Layer.Alpha) * 255);
var pxLen = frame.PixelsLength;
var blend = BlendModes[0];
if (cel.Layer.BlendMode < BlendModes.Count)
blend = BlendModes[cel.Layer.BlendMode];
for (int sx = Math.Max(0, -cel.X), int right = Math.Min(cel.Width, width - cel.X); sx < right; sx++)
{
int dx = cel.X + sx;
int dy = cel.Y * width;
for (int sy = Math.Max(0, -cel.Y), int bottom = Math.Min(cel.Height, height - cel.Y); sy < bottom; sy++, dy += width)
{
if (dx + dy >= 0 && dx + dy < pxLen)
blend(frame.Pixels, (dx + dy) * 4, cel.Pixels[sx + sy * cel.Width], opacity);
}
}
}
// Data
private class Frame
{
public SDL.Texture* Texture;
public float Duration;
public List<Cel> Cels = new List<Cel>() ~ delete _;
public uint8* Pixels;
public int32 PixelsLength;
public this(int w, int h)
{
Texture = SDL.CreateTexture(Game.Renderer, (uint32)SDL.PIXELFORMAT_ARGB8888, (int32)SDL.TextureAccess.Static, (int32)w, (int32)h);
void* ptr;
SDL.LockTexture(Texture, null, out ptr, out PixelsLength);
Pixels = (uint8*)ptr;
}
public ~this()
{
SDL.DestroyTexture(Texture);
}
public void FinishLoad()
{
SDL.UnlockTexture(Texture);
Pixels = null;
}
}
public class Tag
{
public enum LoopDirections
{
Forward = 0,
Reverse = 1,
PingPong = 2
}
public String Name = new String() ~ delete _;
public LoopDirections LoopDirection;
public int From;
public int To;
public Color Color;
}
public class HasUserData
{
public String UserDataText = new String("") ~ delete _;
public Color UserDataColor;
}
private class Slice : HasUserData
{
public int Frame;
public String Name = new String() ~ delete _;
public int OriginX;
public int OriginY;
public int Width;
public int Height;
public Point? Pivot;
public Rect? NineSlice;
}
private class Cel : HasUserData
{
public Layer Layer;
public Color[] Pixels;
public Cel Link;
public int X;
public int Y;
public int Width;
public int Height;
public float Alpha;
public this(Layer layer, Color[] pixels)
{
Layer = layer;
Pixels = pixels;
}
}
private class Layer : HasUserData
{
public enum Flags
{
Visible = 1,
Editable = 2,
LockMovement = 4,
Background = 8,
PreferLinkedCels = 16,
Collapsed = 32,
Reference = 64
}
public enum Types
{
Normal = 0,
Group = 1
}
public Flags Flag;
public Types Type;
public String Name = new String() ~ delete _;
public int ChildLevel;
public int BlendMode;
public float Alpha;
public bool Visible { get { return Flag.HasFlag(Flags.Visible); } }
}
// Blend Modes
// More or less copied from Aseprite's source code:
// https://github.com/aseprite/aseprite/blob/master/src/doc/blend_funcs.cpp
private delegate void Blend(uint8* dest, int index, Color src, uint8 opacity);
private static void Dispose()
{
for (let b in BlendModes)
delete b;
delete BlendModes;
}
private static readonly Blend[] BlendModes = new Blend[]
{
// 0 - NORMAL
new (dest, index, src, opacity) =>
{
if (src.A != 0)
{
uint8 a = dest[index];
uint8 r = dest[index + 1];
uint8 g = dest[index + 2];
uint8 b = dest[index + 3];
if (dest[index] == 0)
{
a = src.A;
r = src.R;
g = src.G;
b = src.B;
}
else
{
var sa = MUL_UN8(src.A, opacity);
var ra = a + sa - MUL_UN8(a, sa);
a = (uint8)ra;
r = (uint8)(r + (src.R - r) * sa / ra);
g = (uint8)(g + (src.G - g) * sa / ra);
b = (uint8)(b + (src.B - b) * sa / ra);
}
dest[index] = a;
dest[index + 1] = r;
dest[index + 2] = g;
dest[index + 3] = b;
}
}
};
[Inline]
private static int MUL_UN8(int a, int b)
{
var t = (a * b) + 0x80;
return (((t >> 8) + t) >> 8);
}
}
}

View File

@ -5,6 +5,12 @@ namespace Strawberry
{ {
static public class Calc static public class Calc
{ {
[Inline]
static public bool BitCheck(int bits, int pos)
{
return (bits & (1 << pos)) != 0;
}
//Move toward a target value without crossing it //Move toward a target value without crossing it
[Inline] [Inline]
static public float Approach(float value, float target, float maxDelta) static public float Approach(float value, float target, float maxDelta)