1
0
Fork 0
mirror of https://github.com/beefytech/Beef.git synced 2025-07-08 17:25:59 +02:00

FontEffect support - outlined fonts

This commit is contained in:
Brian Fiete 2025-01-26 07:04:26 -08:00
parent 474bad09b2
commit 89bf475045
10 changed files with 385 additions and 8 deletions

View file

@ -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,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);

View file

@ -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);

View 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;
}
}