diff --git a/BeefLibs/Beefy2D/src/gfx/Font.bf b/BeefLibs/Beefy2D/src/gfx/Font.bf index 227831f6..888b5601 100644 --- a/BeefLibs/Beefy2D/src/gfx/Font.bf +++ b/BeefLibs/Beefy2D/src/gfx/Font.bf @@ -36,6 +36,104 @@ namespace Beefy.gfx public float mMaxWidth; } + public class FontEffect + { + public enum Kind + { + case None; + case Outline(float thickness, uint32 color); + } + + public class EffectCharData + { + public Font.CharData mSrcCharData; + public Font.CharData mEffectCharData ~ delete _; + } + + public class FontEntry + { + public Font mFont; + public ImageAtlas mAtlas = new ImageAtlas() ~ delete _; + public List mCharData = new .() ~ delete _; + } + + public Kind mKind; + public String mEffectOptions = new .() ~ delete _; + public Dictionary mFontEntries = new .() ~ DeleteDictionaryAndValues!(_); + public Dictionary mCharData = new .() ~ DeleteDictionaryAndValues!(_); + + public this(Kind kind) + { + mKind = kind; + switch (mKind) + { + case .Outline(let thickness, let color): + mEffectOptions..Clear().AppendF( + $""" + Effect=Stroke + Size={thickness} + Color=#{color:X} + """); + default: + } + } + + public ~this() + { + + } + + public Font.CharData Apply(Font font, Font.CharData charData) + { + EffectCharData effectCharData = null; + if (mCharData.TryAdd(charData, ?, var effectCharDataPtr)) + { + effectCharData = new EffectCharData(); + *effectCharDataPtr = effectCharData; + + effectCharData.mSrcCharData = charData; + effectCharData.mEffectCharData = new .(charData); + } + else + { + return (*effectCharDataPtr).mEffectCharData; + } + + FontEntry fontEntry = null; + if (mFontEntries.TryAdd(font, ?, var fontEntryPtr)) + { + fontEntry = new .(); + *fontEntryPtr = fontEntry; + } + else + fontEntry = *fontEntryPtr; + + int32 ofsX = 0; + int32 ofsY = 0; + if (mKind case .Outline(let thickness, ?)) + { + ofsX += (.)Math.Ceiling(thickness) + 2; + ofsY += (.)Math.Ceiling(thickness) + 2; + } + + effectCharData.mEffectCharData.mXOffset -= ofsX; + effectCharData.mEffectCharData.mYOffset -= ofsY; + var effectImage = fontEntry.mAtlas.Alloc(charData.mImageSegment.mSrcWidth + ofsX * 2, charData.mImageSegment.mSrcHeight + ofsY * 2); + effectCharData.mEffectCharData.mImageSegment = effectImage; + effectCharData.mSrcCharData.mImageSegment.ApplyEffect(effectImage, mEffectOptions); + + /*uint32 color = 0xFFFFFFFF; + effectImage.SetBits(0, 0, 1, 1, 1, &color);*/ + + return effectCharData.mEffectCharData; + } + + public void Prepare(Font font, StringView str) + { + + } + } + public class Font { [CallingConvention(.Stdcall), CLink] @@ -89,6 +187,24 @@ namespace Beefy.gfx public int32 mYOffset; public int32 mXAdvance; public bool mIsCombiningMark; + + public this() + { + + } + + public this(CharData copyFrom) + { + mImageSegment = copyFrom.mImageSegment; + mX = copyFrom.mX; + mY = copyFrom.mY; + mWidth = copyFrom.mWidth; + mHeight = copyFrom.mHeight; + mXOffset = copyFrom.mXOffset; + mYOffset = copyFrom.mYOffset; + mXAdvance = copyFrom.mXAdvance; + mIsCombiningMark = copyFrom.mIsCombiningMark; + } } public class Page @@ -139,6 +255,9 @@ namespace Beefy.gfx //BitmapFont mBMFont ~ delete _; public StringView mEllipsis = "..."; + List mFontEffectStack ~ delete _; + DisposeProxy mFontEffectDisposeProxy = new .(new () => { PopFontEffect(); }) ~ delete _; + public this() { } @@ -153,6 +272,19 @@ namespace Beefy.gfx FTFont_ClearCache(); } + public DisposeProxy PushFontEffect(FontEffect fontEffect) + { + if (mFontEffectStack == null) + mFontEffectStack = new .(); + mFontEffectStack.Add(fontEffect); + return mFontEffectDisposeProxy; + } + + public void PopFontEffect() + { + mFontEffectStack.PopBack(); + } + static void BuildFontNameCache() { #if BF_PLATFORM_WINDOWS @@ -666,13 +798,23 @@ namespace Beefy.gfx mLowCharData[(int)checkChar] = charData; else mCharData[checkChar] = charData; - return charData; + break; } - + } + + if (charData == null) + { if (checkChar == (char32)'?') return null; return GetCharData((char32)'?'); } + + if (mFontEffectStack?.IsEmpty == false) + { + var fontEffect = mFontEffectStack.Back; + charData = fontEffect.Apply(this, charData); + } + return charData; } @@ -839,7 +981,10 @@ namespace Beefy.gfx uint32 color = g.mColor; - g.PushTextRenderState(); + bool usingTextRenderState = mFontEffectStack?.IsEmpty != false; + + if (usingTextRenderState) + g.PushTextRenderState(); float markTopOfs = 0; float markBotOfs = 0; @@ -943,7 +1088,8 @@ namespace Beefy.gfx prevChar = c; } - g.PopRenderState(); + if (usingTextRenderState) + g.PopRenderState(); if (fontMetrics != null) fontMetrics.mMaxX = Math.Max(fontMetrics.mMaxX, curX); diff --git a/BeefLibs/Beefy2D/src/gfx/Image.bf b/BeefLibs/Beefy2D/src/gfx/Image.bf index f46b5c72..f5db7e87 100644 --- a/BeefLibs/Beefy2D/src/gfx/Image.bf +++ b/BeefLibs/Beefy2D/src/gfx/Image.bf @@ -71,6 +71,9 @@ namespace Beefy.gfx [CallingConvention(.Stdcall), CLink] static extern int32 Gfx_Texture_GetHeight(void* textureSegment); + [CallingConvention(.Stdcall), CLink] + static extern void Gfx_ApplyEffect(void* destTextureSegment, void* srcTextureSegment, char8* options); + public this() { } @@ -189,6 +192,11 @@ namespace Beefy.gfx Gfx_ModifyTextureSegment(mNativeTextureSegment, srcImage.mNativeTextureSegment, (int32)srcX, (int32)srcY, (int32)srcWidth, (int32)srcHeight); } + public void ApplyEffect(Image destImage, StringView options) + { + Gfx_ApplyEffect(destImage.mNativeTextureSegment, mNativeTextureSegment, options.ToScopeCStr!()); + } + public void SetBits(int destX, int destY, int destWidth, int destHeight, int srcPitch, uint32* bits) { Gfx_Texture_SetBits(mNativeTextureSegment, (.)destX, (.)destY, (.)destWidth, (.)destHeight, (.)srcPitch, bits); diff --git a/BeefLibs/Beefy2D/src/gfx/ImageAtlas.bf b/BeefLibs/Beefy2D/src/gfx/ImageAtlas.bf new file mode 100644 index 00000000..f600f752 --- /dev/null +++ b/BeefLibs/Beefy2D/src/gfx/ImageAtlas.bf @@ -0,0 +1,66 @@ +using System.Collections; +using System; +namespace Beefy.gfx; + +class ImageAtlas +{ + public class Page + { + public Image mImage ~ delete _; + public int32 mCurX; + public int32 mCurY; + public int32 mMaxRowHeight; + } + + public List mPages = new .() ~ DeleteContainerAndItems!(_); + public bool mAllowMultiplePages = true; + + public int32 mImageWidth = 1024; + public int32 mImageHeight = 1024; + + public this() + { + + } + + public Image Alloc(int32 width, int32 height) + { + Page page = null; + if (!mPages.IsEmpty) + { + page = mPages.Back; + if (page.mCurX + (int)width > page.mImage.mSrcWidth) + { + // Move down to next row + page.mCurX = 0; + page.mCurY += page.mMaxRowHeight; + page.mMaxRowHeight = 0; + } + + if (page.mCurY + height > page.mImage.mSrcHeight) + { + // Doesn't fit + page = null; + } + } + + if (page == null) + { + page = new .(); + page.mImage = Image.CreateDynamic(mImageWidth, mImageHeight); + + uint32* colors = new uint32[mImageWidth*mImageHeight]*; + defer delete colors; + for (int i < mImageWidth*mImageHeight) + colors[i] = 0xFF000000 | (.)i; + + page.mImage.SetBits(0, 0, mImageWidth, mImageHeight, mImageWidth, colors); + mPages.Add(page); + } + + Image image = page.mImage.CreateImageSegment(page.mCurX, page.mCurY, width, height); + page.mCurX += width; + page.mMaxRowHeight = Math.Max(page.mMaxRowHeight, height); + return image; + } +} \ No newline at end of file diff --git a/BeefySysLib/BeefySysLib.cpp b/BeefySysLib/BeefySysLib.cpp index f15fc3e7..cd552817 100644 --- a/BeefySysLib/BeefySysLib.cpp +++ b/BeefySysLib/BeefySysLib.cpp @@ -10,6 +10,7 @@ #include "util/Vector.h" #include "util/PerfTimer.h" #include "util/TLSingleton.h" +#include "img/ImgEffects.h" #include "util/AllocDebug.h" @@ -474,12 +475,12 @@ BF_EXPORT TextureSegment* BF_CALLTYPE Gfx_LoadTexture(const char* fileName, int BF_EXPORT void BF_CALLTYPE Gfx_Texture_SetBits(TextureSegment* textureSegment, int destX, int destY, int destWidth, int destHeight, int srcPitch, uint32* bits) { - textureSegment->mTexture->SetBits(destX, destY, destWidth, destHeight, srcPitch, bits); + textureSegment->SetBits(destX, destY, destWidth, destHeight, srcPitch, bits); } BF_EXPORT void BF_CALLTYPE Gfx_Texture_GetBits(TextureSegment* textureSegment, int srcX, int srcY, int srcWidth, int srcHeight, int destPitch, uint32* bits) { - textureSegment->mTexture->GetBits(srcX, srcY, srcWidth, srcHeight, destPitch, bits); + textureSegment->GetBits(srcX, srcY, srcWidth, srcHeight, destPitch, bits); } BF_EXPORT void BF_CALLTYPE Gfx_Texture_Delete(TextureSegment* textureSegment) @@ -516,6 +517,93 @@ BF_EXPORT void BF_CALLTYPE Gfx_ModifyTextureSegment(TextureSegment* destTextureS destTextureSegment->mScaleY = (float)abs(srcHeight); } +int32 FormatI32(const StringView& str) +{ + String val = String(str); + if (val.StartsWith('#')) + { + return (int32)strtoll(val.c_str() + 1, NULL, 16); + } + if (val.StartsWith("0x")) + { + return (int32)strtoll(val.c_str() + 2, NULL, 16); + } + return atoi(val.c_str()); +} + +BF_EXPORT void BF_CALLTYPE Gfx_ApplyEffect(TextureSegment* destTextureSegment, TextureSegment* srcTextureSegment, char* optionsPtr) +{ + BaseImageEffect* effect = NULL; + defer({ + delete effect; + }); + + bool needsPremultiply = false; + + String options = optionsPtr; + for (auto line : options.Split('\n')) + { + int eqPos = (int)line.IndexOf('='); + if (eqPos != -1) + { + StringView cmd = StringView(line, 0, eqPos); + StringView value = StringView(line, eqPos + 1); + + if (cmd == "Effect") + { + if (value == "Stroke") + { + auto strokeEffect = new ImageStrokeEffect(); + effect = strokeEffect; + strokeEffect->mPosition = 'OutF'; + strokeEffect->mSize = 1; + strokeEffect->mColorFill.mColor = 0xFF000000; + } + } + if (cmd == "Size") + { + int32 size = FormatI32(value); + if (auto strokeEffect = dynamic_cast(effect)) + { + strokeEffect->mSize = size; + } + } + if (cmd == "Color") + { + uint32 color = (uint32)FormatI32(value); + if (auto strokeEffect = dynamic_cast(effect)) + { + strokeEffect->mColorFill.mColor = color; + if ((color & 0x00FFFFFF) != 0) + needsPremultiply = true; + } + } + } + } + + if (effect != NULL) + { + ImageData destImageData; + ImageData srcImageData; + + Rect srcRect = srcTextureSegment->GetRect(); + Rect destRect = destTextureSegment->GetRect(); + + destTextureSegment->GetImageData(destImageData); + srcImageData.CreateNew(destImageData.mWidth, destImageData.mHeight); + + srcTextureSegment->GetImageData(srcImageData, + (int)(destRect.mWidth - srcRect.mWidth) / 2, + (int)(destRect.mHeight - srcRect.mHeight) / 2); + + effect->Apply(NULL, &srcImageData, &destImageData); + if (needsPremultiply) + destImageData.PremultiplyAlpha(); + + destTextureSegment->SetImageData(destImageData); + } +} + BF_EXPORT TextureSegment* BF_CALLTYPE Gfx_CreateTextureSegment(TextureSegment* textureSegment, int srcX, int srcY, int srcWidth, int srcHeight) { Texture* texture = textureSegment->mTexture; diff --git a/BeefySysLib/gfx/Texture.cpp b/BeefySysLib/gfx/Texture.cpp index 7b16a4e1..a7cafa56 100644 --- a/BeefySysLib/gfx/Texture.cpp +++ b/BeefySysLib/gfx/Texture.cpp @@ -1,6 +1,7 @@ #include "Texture.h" #include "util/AllocDebug.h" +#include "img/ImageData.h" USING_NS_BF; @@ -31,3 +32,50 @@ void TextureSegment::InitFromTexture(Texture* texture) mScaleX = (float) mTexture->mWidth; mScaleY = (float) mTexture->mHeight; } + +void TextureSegment::SetBits(int destX, int destY, int destWidth, int destHeight, int srcPitch, uint32* bits) +{ + int x1 = (int)(mU1 * mTexture->mWidth + 0.5f); + int y1 = (int)(mV1 * mTexture->mHeight + 0.5f); + mTexture->SetBits(destX + x1, destY + y1, destWidth, destHeight, srcPitch, bits); +} + +void TextureSegment::GetBits(int srcX, int srcY, int srcWidth, int srcHeight, int destPitch, uint32* bits) +{ + int x1 = (int)(mU1 * mTexture->mWidth + 0.5f); + int y1 = (int)(mV1 * mTexture->mHeight + 0.5f); + mTexture->GetBits(srcX + x1, srcY + y1, srcWidth, srcHeight, destPitch, bits); +} + +void TextureSegment::GetImageData(ImageData& imageData) +{ + int x1 = (int)(mU1 * mTexture->mWidth + 0.5f); + int x2 = (int)(mU2 * mTexture->mWidth + 0.5f); + int y1 = (int)(mV1 * mTexture->mHeight + 0.5f); + int y2 = (int)(mV2 * mTexture->mHeight + 0.5f); + imageData.CreateNew(x2 - x1, y2 - y1); + mTexture->GetBits(x1, y1, x2 - x1, y2 - y1, x2 - x1, imageData.mBits); +} + +void TextureSegment::GetImageData(ImageData& imageData, int destX, int destY) +{ + int x1 = (int)(mU1 * mTexture->mWidth + 0.5f); + int x2 = (int)(mU2 * mTexture->mWidth + 0.5f); + int y1 = (int)(mV1 * mTexture->mHeight + 0.5f); + int y2 = (int)(mV2 * mTexture->mHeight + 0.5f); + mTexture->GetBits(x1, y1, x2 - x1, y2 - y1, imageData.mWidth, imageData.mBits + destX + destY * imageData.mWidth); +} + +void TextureSegment::SetImageData(ImageData& imageData) +{ + SetBits(0, 0, imageData.mWidth, imageData.mHeight, imageData.mStride, imageData.mBits); +} + +Rect TextureSegment::GetRect() +{ + float x1 = mU1 * mTexture->mWidth; + float x2 = mU2 * mTexture->mWidth; + float y1 = mV1 * mTexture->mHeight; + float y2 = mV2 * mTexture->mHeight; + return Rect(x1, y1, x2 - x1, y2 - y1); +} diff --git a/BeefySysLib/gfx/Texture.h b/BeefySysLib/gfx/Texture.h index 1c7ead24..277afe55 100644 --- a/BeefySysLib/gfx/Texture.h +++ b/BeefySysLib/gfx/Texture.h @@ -2,6 +2,7 @@ #include "Common.h" #include "RenderTarget.h" +#include "../util/Rect.h" NS_BF_BEGIN; @@ -38,6 +39,15 @@ public: public: void InitFromTexture(Texture* texture); + + virtual void SetBits(int destX, int destY, int destWidth, int destHeight, int srcPitch, uint32* bits); + virtual void GetBits(int srcX, int srcY, int srcWidth, int srcHeight, int destPitch, uint32* bits); + + void GetImageData(ImageData& imageData); + void GetImageData(ImageData& imageData, int destX, int destY); + void SetImageData(ImageData& imageData); + + Rect GetRect(); }; NS_BF_END; diff --git a/BeefySysLib/img/ImageData.cpp b/BeefySysLib/img/ImageData.cpp index 137c3e57..b9e70676 100644 --- a/BeefySysLib/img/ImageData.cpp +++ b/BeefySysLib/img/ImageData.cpp @@ -16,6 +16,7 @@ ImageData::ImageData() mY = 0; mWidth = 0; mHeight = 0; + mStride = 0; mWantsAlphaPremultiplied = true; mAlphaPremultiplied = false; mIsAdditive = false; @@ -67,7 +68,7 @@ void ImageData::CreateNew(int x, int y, int width, int height, bool clear) void ImageData::CreateNew(int width, int height, bool clear) { - mWidth = width; + mWidth = mStride = width; mHeight = height; mBits = new uint32[mWidth*mHeight]; if (clear) @@ -89,7 +90,7 @@ void ImageData::CopyFrom(ImageData* img, int x, int y) { for (int x = destStartX; x < destEndX; x++) { - mBits[x + y * mWidth] = img->mBits[(x + srcXOfs) + (y + srcYOfs)*img->mWidth]; + mBits[x + y * mStride] = img->mBits[(x + srcXOfs) + (y + srcYOfs)*img->mStride]; } } } diff --git a/BeefySysLib/img/ImageData.h b/BeefySysLib/img/ImageData.h index 000555cf..d59997b3 100644 --- a/BeefySysLib/img/ImageData.h +++ b/BeefySysLib/img/ImageData.h @@ -20,6 +20,7 @@ public: int mY; int mWidth; int mHeight; + int mStride; void* mHWBits; int mHWBitsLength; int mHWBitsType; diff --git a/BeefySysLib/img/ImgEffects.cpp b/BeefySysLib/img/ImgEffects.cpp index c01dd534..f269d5db 100644 --- a/BeefySysLib/img/ImgEffects.cpp +++ b/BeefySysLib/img/ImgEffects.cpp @@ -241,6 +241,7 @@ ImageData* ImageEffects::GetDestImage(ImageData* usingImage) return mSwapImages[1]; ImageData* anImage = new ImageData(); + anImage->mStride = usingImage->mStride; anImage->mWidth = usingImage->mWidth; anImage->mHeight = usingImage->mHeight; anImage->mBits = new uint32[anImage->mWidth*anImage->mHeight]; diff --git a/BeefySysLib/util/Rect.h b/BeefySysLib/util/Rect.h index 814ba284..48753f48 100644 --- a/BeefySysLib/util/Rect.h +++ b/BeefySysLib/util/Rect.h @@ -21,6 +21,14 @@ public: mHeight = 0; } + Rect(float x, float y, float width, float height) + { + mX = x; + mY = y; + mWidth = width; + mHeight = height; + } + bool operator!=(const Rect& r2) { return (mX != r2.mX) || (mY != r2.mY) || (mWidth != r2.mWidth) || (mHeight != r2.mHeight);