mirror of
https://github.com/beefytech/Beef.git
synced 2025-06-08 11:38:21 +02:00
FontEffect support - outlined fonts
This commit is contained in:
parent
474bad09b2
commit
89bf475045
10 changed files with 385 additions and 8 deletions
|
@ -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<EffectCharData> mCharData = new .() ~ delete _;
|
||||
}
|
||||
|
||||
public Kind mKind;
|
||||
public String mEffectOptions = new .() ~ delete _;
|
||||
public Dictionary<Font, FontEntry> mFontEntries = new .() ~ DeleteDictionaryAndValues!(_);
|
||||
public Dictionary<Font.CharData, EffectCharData> 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<FontEffect> 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,6 +981,9 @@ namespace Beefy.gfx
|
|||
|
||||
uint32 color = g.mColor;
|
||||
|
||||
bool usingTextRenderState = mFontEffectStack?.IsEmpty != false;
|
||||
|
||||
if (usingTextRenderState)
|
||||
g.PushTextRenderState();
|
||||
|
||||
float markTopOfs = 0;
|
||||
|
@ -943,6 +1088,7 @@ namespace Beefy.gfx
|
|||
prevChar = c;
|
||||
}
|
||||
|
||||
if (usingTextRenderState)
|
||||
g.PopRenderState();
|
||||
|
||||
if (fontMetrics != null)
|
||||
|
|
|
@ -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);
|
||||
|
|
66
BeefLibs/Beefy2D/src/gfx/ImageAtlas.bf
Normal file
66
BeefLibs/Beefy2D/src/gfx/ImageAtlas.bf
Normal file
|
@ -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<Page> 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;
|
||||
}
|
||||
}
|
|
@ -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<ImageStrokeEffect*>(effect))
|
||||
{
|
||||
strokeEffect->mSize = size;
|
||||
}
|
||||
}
|
||||
if (cmd == "Color")
|
||||
{
|
||||
uint32 color = (uint32)FormatI32(value);
|
||||
if (auto strokeEffect = dynamic_cast<ImageStrokeEffect*>(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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ public:
|
|||
int mY;
|
||||
int mWidth;
|
||||
int mHeight;
|
||||
int mStride;
|
||||
void* mHWBits;
|
||||
int mHWBitsLength;
|
||||
int mHWBitsType;
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue