diff --git a/.github/README.md b/.github/README.md
new file mode 100644
index 0000000..6594946
--- /dev/null
+++ b/.github/README.md
@@ -0,0 +1,57 @@
+# libmsdf
+
+[](https://github.com/KAJARLABORATORIES/libmsdf/actions/workflows/release.yml)
+[](https://www.nuget.org/packages/kajarlabs.osu.Framework.MsdfTextRendering)
+[](https://github.com/KAJARLABORATORIES/libmsdf/releases/latest)
+
+MSDF text rendering for [osu!framework](https://github.com/ppy/osu-framework).
+
+## Using libmsdf in your game
+
+If you are interested in **using** the library, install the package and start from the [setting up MSDF fonts](https://github.com/KAJARLABORATORIES/libmsdf/wiki/Setting-Up-MSDF-Fonts) wiki page, which walks through generating an atlas, registering it and drawing text with it.
+
+```
+dotnet add package kajarlabs.osu.Framework.MsdfTextRendering --prerelease
+```
+
+The library is pre-`1.0.0`. The public API may change between `0.x` releases, so pin an exact version if you depend on it.
+
+The rest of the information on this page is related to working *on* the library, not *using* it!
+
+## Objectives
+
+osu!framework renders text from pre-rasterised bitmap atlases. That is fast and predictable, but a glyph baked at one size degrades once it is drawn far beyond it, which shows up in any UI that zooms, animates scale, or shares a typeface across a wide range of sizes.
+
+`libmsdf` provides a second rendering path built on [multi-channel signed distance fields](https://github.com/Chlumsky/msdfgen), where glyph coverage is reconstructed in the fragment shader instead of sampled from a fixed-resolution texture.
+
+- Stay a **drop-in alternative**, not a replacement. `MsdfSpriteText` mirrors `SpriteText`, and MSDF glyphs implement the framework's own glyph abstractions so the existing text builder, layout and localisation keep working.
+- Keep the atlas pipeline **external and standard**. Atlases are produced by [`msdf-atlas-gen`](https://github.com/Chlumsky/msdf-atlas-gen), so nothing here is a bespoke font format.
+- Cover the same text features users already expect: kerning, multiline, truncation, fixed width, fallback glyphs and shadows.
+MSDF is not a free upgrade for every case. At small, static sizes a hinted bitmap font is cheaper and often looks better; this library is aimed at the cases where one atlas has to serve many sizes.
+
+## Requirements
+
+- A desktop platform with the [.NET 10.0 SDK](https://dotnet.microsoft.com/download).
+- [`msdf-atlas-gen`](https://github.com/Chlumsky/msdf-atlas-gen) for generating font atlases.
+- When working with the codebase, we recommend an IDE with intellisense and syntax highlighting, such as [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) extensions installed.
+
+### Building
+
+```
+dotnet build
+dotnet test
+```
+
+Visual test scenes live in `tests`, and are the recommended place to develop and debug rendering changes: a distance-field bug is something you see, not something an assertion catches.
+
+## Contributing
+
+Contributions can be made via pull requests to this repository. If you are unsure where to start, check the [list of open issues](https://github.com/KAJARLABORATORIES/libmsdf/issues).
+
+When reporting a rendering bug, include a minimal reproduction and the atlas and JSON metadata that trigger it. "The glyph looks wrong" is impossible to act on without the distance range it was generated with.
+
+## License
+
+This library is licensed under the [MIT license](https://opensource.org/licenses/MIT). Please see [the license file](../COPYING) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.
+
+The MSDF technique itself comes from [msdfgen](https://github.com/Chlumsky/msdfgen) by Viktor Chlumský, also MIT licensed. Atlases generated with `msdf-atlas-gen` carry the license of the font they were generated from; check it before shipping one.
diff --git a/.github/README.nuget.md b/.github/README.nuget.md
new file mode 100644
index 0000000..6594946
--- /dev/null
+++ b/.github/README.nuget.md
@@ -0,0 +1,57 @@
+# libmsdf
+
+[](https://github.com/KAJARLABORATORIES/libmsdf/actions/workflows/release.yml)
+[](https://www.nuget.org/packages/kajarlabs.osu.Framework.MsdfTextRendering)
+[](https://github.com/KAJARLABORATORIES/libmsdf/releases/latest)
+
+MSDF text rendering for [osu!framework](https://github.com/ppy/osu-framework).
+
+## Using libmsdf in your game
+
+If you are interested in **using** the library, install the package and start from the [setting up MSDF fonts](https://github.com/KAJARLABORATORIES/libmsdf/wiki/Setting-Up-MSDF-Fonts) wiki page, which walks through generating an atlas, registering it and drawing text with it.
+
+```
+dotnet add package kajarlabs.osu.Framework.MsdfTextRendering --prerelease
+```
+
+The library is pre-`1.0.0`. The public API may change between `0.x` releases, so pin an exact version if you depend on it.
+
+The rest of the information on this page is related to working *on* the library, not *using* it!
+
+## Objectives
+
+osu!framework renders text from pre-rasterised bitmap atlases. That is fast and predictable, but a glyph baked at one size degrades once it is drawn far beyond it, which shows up in any UI that zooms, animates scale, or shares a typeface across a wide range of sizes.
+
+`libmsdf` provides a second rendering path built on [multi-channel signed distance fields](https://github.com/Chlumsky/msdfgen), where glyph coverage is reconstructed in the fragment shader instead of sampled from a fixed-resolution texture.
+
+- Stay a **drop-in alternative**, not a replacement. `MsdfSpriteText` mirrors `SpriteText`, and MSDF glyphs implement the framework's own glyph abstractions so the existing text builder, layout and localisation keep working.
+- Keep the atlas pipeline **external and standard**. Atlases are produced by [`msdf-atlas-gen`](https://github.com/Chlumsky/msdf-atlas-gen), so nothing here is a bespoke font format.
+- Cover the same text features users already expect: kerning, multiline, truncation, fixed width, fallback glyphs and shadows.
+MSDF is not a free upgrade for every case. At small, static sizes a hinted bitmap font is cheaper and often looks better; this library is aimed at the cases where one atlas has to serve many sizes.
+
+## Requirements
+
+- A desktop platform with the [.NET 10.0 SDK](https://dotnet.microsoft.com/download).
+- [`msdf-atlas-gen`](https://github.com/Chlumsky/msdf-atlas-gen) for generating font atlases.
+- When working with the codebase, we recommend an IDE with intellisense and syntax highlighting, such as [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) extensions installed.
+
+### Building
+
+```
+dotnet build
+dotnet test
+```
+
+Visual test scenes live in `tests`, and are the recommended place to develop and debug rendering changes: a distance-field bug is something you see, not something an assertion catches.
+
+## Contributing
+
+Contributions can be made via pull requests to this repository. If you are unsure where to start, check the [list of open issues](https://github.com/KAJARLABORATORIES/libmsdf/issues).
+
+When reporting a rendering bug, include a minimal reproduction and the atlas and JSON metadata that trigger it. "The glyph looks wrong" is impossible to act on without the distance range it was generated with.
+
+## License
+
+This library is licensed under the [MIT license](https://opensource.org/licenses/MIT). Please see [the license file](../COPYING) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source.
+
+The MSDF technique itself comes from [msdfgen](https://github.com/Chlumsky/msdfgen) by Viktor Chlumský, also MIT licensed. Atlases generated with `msdf-atlas-gen` carry the license of the font they were generated from; check it before shipping one.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..e462ab9
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,58 @@
+name: Continuous Integration
+
+on:
+ push:
+ branches: [develop]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Test
+ runs-on: ${{ matrix.os.fullname }}
+ timeout-minutes: 60
+ env:
+ OSU_EXECUTION_MODE: ${{ matrix.threadingMode }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - { prettyname: Windows, fullname: windows-latest, configuration: Debug }
+ - { prettyname: macOS, fullname: macos-latest, configuration: Debug } # ARM: veja o item 3 abaixo
+ - { prettyname: Linux, fullname: ubuntu-latest, configuration: Debug }
+ - { prettyname: Linux, fullname: ubuntu-latest, configuration: Release }
+ threadingMode: ["SingleThread", "MultiThreaded"]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - name: Install .NET 10.0.x
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "10.0.x"
+
+ - name: Compile
+ run: dotnet build -c ${{ matrix.os.configuration }} osu-framework-msdf.slnx
+
+ - name: Test
+ run: >
+ dotnet test osu-framework-msdf.slnx
+ -c ${{ matrix.os.configuration }}
+ --no-build
+ --results-directory ${{ github.workspace }}/TestResults
+ --logger "trx;LogFileName=TestResults-${{ matrix.os.prettyname }}-${{ matrix.threadingMode }}-${{ matrix.os.configuration }}.trx"
+ -- NUnit.ConsoleOut=0
+
+ # Sobe os resultados mesmo se o teste falhar.
+ - name: Upload Test Results
+ uses: actions/upload-artifact@v4
+ if: ${{ always() }}
+ with:
+ name: libmsdf-test-results-${{ matrix.os.prettyname }}-${{ matrix.threadingMode }}-${{ matrix.os.configuration }}
+ path: ${{ github.workspace }}/TestResults/TestResults-${{ matrix.os.prettyname }}-${{ matrix.threadingMode }}-${{ matrix.os.configuration }}.trx
+ if-no-files-found: error
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..e9a8c35
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,144 @@
+name: Package and NuGet
+
+on:
+ push:
+ tags:
+ - "[0-9]+.[0-9]+.[0-9]+*"
+
+permissions:
+ contents: read
+
+jobs:
+ notify_process_deploy:
+ name: Notify start
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ env:
+ WEBHOOK_URL: ${{ secrets.DISCORD_INFRA_WEBHOOK_URL }}
+ steps:
+ - name: Notify Deployment
+ run: |
+ export TITLE="🚀 Deployment in Progress: libmsdf ($GITHUB_REF_NAME)"
+ export URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
+ export DESCRIPTION="A new deployment for **libmsdf** has started."$'\n'"Stay tuned for updates!"$'\n'"➜ [View Workflow Run]($URL)"
+ export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID"
+
+ BODY="$(jq --null-input '{
+ embeds: [{
+ title: env.TITLE,
+ color: 15098112,
+ description: env.DESCRIPTION,
+ url: env.URL,
+ author: { name: env.GITHUB_ACTOR, icon_url: env.ACTOR_ICON }
+ }]
+ }')"
+
+ curl --fail -H "Content-Type: application/json" -d "$BODY" "$WEBHOOK_URL"
+
+ test:
+ name: Test (${{ matrix.threadingMode }})
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ fail-fast: true
+ matrix:
+ threadingMode: ["SingleThread", "MultiThreaded"]
+ env:
+ OSU_EXECUTION_MODE: ${{ matrix.threadingMode }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - name: Install .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "10.0.x"
+
+ - name: Test
+ run: dotnet test -c Release
+
+ pack:
+ name: Pack (libmsdf)
+ needs: [test]
+ runs-on: ubuntu-latest
+ environment: production
+ timeout-minutes: 15
+ permissions:
+ contents: read
+ id-token: write
+ env:
+ ARTIFACTS: ${{ github.workspace }}/artifacts
+ VERSION: ${{ github.ref_name }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - name: Install .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "10.0.x"
+
+ - name: Pack (libmsdf)
+ run: dotnet pack lib -c Release /p:Version="${VERSION#v}" -o "$ARTIFACTS"
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: libmsdf
+ path: ${{ github.workspace }}/artifacts/*.nupkg
+ if-no-files-found: error
+
+ - name: NuGet login (OIDC)
+ id: login
+ uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841
+ with:
+ user: ${{ vars.NUGET_USER }}
+
+ - name: Publish packages to NuGet
+ env:
+ NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }}
+ run: >
+ dotnet nuget push "$ARTIFACTS"/*.nupkg
+ --api-key "$NUGET_API_KEY"
+ --source https://api.nuget.org/v3/index.json
+ --skip-duplicate
+
+ notify_status_deploy:
+ name: Notify result
+ needs: [test, pack]
+ if: always()
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ env:
+ PACK_RESULT: ${{ needs.pack.result }}
+ WEBHOOK_URL: ${{ secrets.DISCORD_INFRA_WEBHOOK_URL }}
+ steps:
+ - name: Notify Deployment Status
+ run: |
+ export TITLE="🚀 Deployment Completed: libmsdf ($GITHUB_REF_NAME)"
+ export URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
+
+ if [ "$PACK_RESULT" = "success" ]; then
+ STATUS="✅ Deployment successful!"; export COLOR=3066993
+ else
+ STATUS="❌ Deployment failed. Check logs for details."; export COLOR=15158332
+ fi
+
+ export DESCRIPTION="Deployment for **libmsdf** has finished."$'\n'"$STATUS"$'\n'"➜ [View Workflow Run]($URL)"
+ export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID"
+
+ BODY="$(jq --null-input '{
+ embeds: [{
+ title: env.TITLE,
+ color: (env.COLOR | tonumber),
+ description: env.DESCRIPTION,
+ url: env.URL,
+ author: { name: env.GITHUB_ACTOR, icon_url: env.ACTOR_ICON }
+ }]
+ }')"
+
+ curl --fail -H "Content-Type: application/json" -d "$BODY" "$WEBHOOK_URL"
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
new file mode 100644
index 0000000..52aaba7
--- /dev/null
+++ b/.github/workflows/report-nunit.yml
@@ -0,0 +1,42 @@
+name: Annotate CI run with Test Results
+
+on:
+ workflow_run:
+ workflows: ["Continuous Integration"]
+ types: [completed]
+
+permissions:
+ contents: read
+ actions: read
+ checks: write
+
+jobs:
+ annotate:
+ name: Annotate CI run with Test Results
+ runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
+ timeout-minutes: 5
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: ${{ github.event.workflow_run.repository.full_name }}
+ ref: ${{ github.event.workflow_run.head_sha }}
+ persist-credentials: false
+
+ - name: Download results
+ uses: actions/download-artifact@v4
+ with:
+ pattern: libmsdf-test-results-*
+ merge-multiple: true
+ run-id: ${{ github.event.workflow_run.id }}
+ github-token: ${{ github.token }}
+
+ - name: Annotate CI run with Test Results
+ uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2
+ with:
+ name: Results
+ path: "*.trx"
+ reporter: dotnet-trx
+ list-suites: "failed"
+ list-tests: "failed"
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..e222d25
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 KAJAR LABORATORIES
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Directory.Build.props b/Directory.Build.props
index 6b853ec..b53025a 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -18,11 +18,15 @@
-
-
+
+
-
-
+
+
+
+ True
+ README.md
+
Default
@@ -44,6 +48,7 @@
KAJAR LABORATORIES MSDF Text Rendering for osu!framework
Automated release.
MIT
+ README.md
https://github.com/KAJARLABORATORIES/libmsdf
https://github.com/KAJARLABORATORIES/libmsdf
diff --git a/analysis/banned-symbols.txt b/analysis/banned-symbols.txt
index de25143..5e088f8 100644
--- a/analysis/banned-symbols.txt
+++ b/analysis/banned-symbols.txt
@@ -3,14 +3,9 @@ M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Us
M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable or EqualityComparer.Default instead.
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
-T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable instead.
-M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
-M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection,NotificationCallbackDelegate) instead.
M:System.Guid.#ctor;Probably meaning to use Guid.CreateVersion7() instead. If actually wanting empty, use Guid.Empty.
-M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable,NotificationCallbackDelegate) instead.
-M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList,NotificationCallbackDelegate) instead.
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
diff --git a/lib/Graphics/Containers/IMsdfTextPart.cs b/lib/Graphics/Containers/IMsdfTextPart.cs
new file mode 100644
index 0000000..3d827b3
--- /dev/null
+++ b/lib/Graphics/Containers/IMsdfTextPart.cs
@@ -0,0 +1,12 @@
+using osu.Framework.Graphics;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers;
+
+public interface IMsdfTextPart
+{
+ IEnumerable Drawables { get; }
+
+ event Action>? DrawablePartsRecreated;
+
+ void RecreateDrawablesFor(MsdfTextFlowContainer textFlowContainer);
+}
diff --git a/lib/Graphics/Containers/MsdfTextChunk.cs b/lib/Graphics/Containers/MsdfTextChunk.cs
new file mode 100644
index 0000000..7927b55
--- /dev/null
+++ b/lib/Graphics/Containers/MsdfTextChunk.cs
@@ -0,0 +1,115 @@
+using System.Globalization;
+using System.Text;
+using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers;
+
+public class MsdfTextChunk : MsdfTextPart
+ where TMsdfSpriteText : MsdfSpriteText, new()
+{
+ private readonly LocalisableString text;
+ private readonly bool newLineIsParagraph;
+ private readonly Func creationFunc;
+ private readonly Action? creationParameters;
+
+ public MsdfTextChunk(LocalisableString text, bool newLineIsParagraph, Func creationFunc, Action? creationParameters = null)
+ {
+ this.text = text;
+ this.newLineIsParagraph = newLineIsParagraph;
+ this.creationFunc = creationFunc;
+ this.creationParameters = creationParameters;
+ }
+
+ protected override IEnumerable CreateDrawablesFor(MsdfTextFlowContainer textFlowContainer)
+ {
+ var currentContent = textFlowContainer.Localisation?.GetLocalisedString(text) ?? text.ToString();
+
+ var drawables = new List();
+
+ if (!newLineIsParagraph)
+ {
+ var newLine = new MsdfTextNewLine(true);
+
+ newLine.RecreateDrawablesFor(textFlowContainer);
+ drawables.AddRange(newLine.Drawables);
+ }
+
+ drawables.AddRange(CreateDrawablesFor(currentContent, textFlowContainer));
+ return drawables;
+ }
+
+ protected virtual IEnumerable CreateDrawablesFor(string text, MsdfTextFlowContainer textFlowContainer)
+ {
+ var first = true;
+ var sprites = new List();
+
+ foreach (var l in text.Split('\n'))
+ {
+ if (!first)
+ {
+ var lastChild = sprites.LastOrDefault() ?? textFlowContainer.Children.LastOrDefault();
+
+ if (lastChild != null)
+ {
+ var newLine = new MsdfTextFlowContainer.NewLineContainer(newLineIsParagraph);
+
+ sprites.Add(newLine);
+ }
+ }
+
+ foreach (var word in SplitWords(l))
+ {
+ if (string.IsNullOrEmpty(word)) continue;
+
+ var textSprite = CreateSpriteText(textFlowContainer);
+
+ textSprite.Text = word;
+ sprites.Add(textSprite);
+ }
+
+ first = false;
+ }
+
+ return sprites;
+ }
+
+ protected string[] SplitWords(string text)
+ {
+ var words = new List();
+ var builder = new StringBuilder();
+
+ for (var i = 0; i < text.Length; i++)
+ {
+ if (i == 0
+ || char.IsSeparator(text[i - 1])
+ || char.IsControl(text[i - 1])
+ || char.GetUnicodeCategory(text[i - 1]) == UnicodeCategory.DashPunctuation
+ || text[i - 1] == '/'
+ || text[i - 1] == '\\'
+ || (isCjkCharacter(text[i - 1]) && !char.IsPunctuation(text[i])))
+ {
+ words.Add(builder.ToString());
+ builder.Clear();
+ }
+
+ builder.Append(text[i]);
+ }
+
+ if (builder.Length > 0)
+ words.Add(builder.ToString());
+
+ return words.ToArray();
+
+ static bool isCjkCharacter(char c) => c >= '\x2E80' && c <= '\x9FFF';
+ }
+
+ protected virtual TMsdfSpriteText CreateSpriteText(MsdfTextFlowContainer textFlowContainer)
+ {
+ var spriteText = creationFunc.Invoke();
+ textFlowContainer.ApplyDefaultCreationParameters(spriteText);
+ creationParameters?.Invoke(spriteText);
+ return spriteText;
+ }
+}
diff --git a/lib/Graphics/Containers/MsdfTextFlowContainer.cs b/lib/Graphics/Containers/MsdfTextFlowContainer.cs
new file mode 100644
index 0000000..2425043
--- /dev/null
+++ b/lib/Graphics/Containers/MsdfTextFlowContainer.cs
@@ -0,0 +1,503 @@
+using System.Buffers;
+using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Caching;
+using osu.Framework.Extensions.EnumExtensions;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Framework.Utils;
+using osuTK;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers;
+
+public partial class MsdfTextFlowContainer : CompositeDrawable
+{
+ private readonly Action? defaultCreationParameters;
+
+ private readonly List parts = [];
+
+ private readonly Cached partsCache = new();
+
+ public float FirstLineIndent
+ {
+ get => Flow.FirstLineIndent;
+ set => Flow.FirstLineIndent = value;
+ }
+
+ public float ContentIndent
+ {
+ get => Flow.ContentIndent;
+ set => Flow.ContentIndent = value;
+ }
+
+ public float ParagraphSpacing
+ {
+ get => Flow.ParagraphSpacing;
+ set => Flow.ParagraphSpacing = value;
+ }
+
+ public float LineSpacing
+ {
+ get => Flow.LineSpacing;
+ set => Flow.LineSpacing = value;
+ }
+
+ public Anchor TextAnchor
+ {
+ get => Flow.TextAnchor;
+ set => Flow.TextAnchor = value;
+ }
+
+ public LocalisableString Text
+ {
+ set
+ {
+ Flow.Clear();
+ parts.Clear();
+
+ AddText(value);
+ }
+ }
+
+ public new Axes RelativeSizeAxes
+ {
+ get => base.RelativeSizeAxes;
+ set
+ {
+ base.RelativeSizeAxes = value;
+ setFlowSizing();
+ }
+ }
+
+ public new Axes AutoSizeAxes
+ {
+ get => base.AutoSizeAxes;
+ set
+ {
+ base.AutoSizeAxes = value;
+ setFlowSizing();
+ }
+ }
+
+ public override float Width
+ {
+ get => base.Width;
+ set
+ {
+ base.Width = value;
+ setFlowSizing();
+ }
+ }
+
+ private void setFlowSizing()
+ {
+ if (AutoSizeAxes.HasFlagFast(Axes.X))
+ {
+ Flow.RelativeSizeAxes = Axes.None;
+ Flow.AutoSizeAxes = Axes.Both;
+ }
+ else
+ {
+ Flow.AutoSizeAxes = Axes.Y;
+ Flow.RelativeSizeAxes = Axes.X;
+ }
+ }
+
+ public new MarginPadding Padding
+ {
+ get => base.Padding;
+ set => base.Padding = value;
+ }
+
+ public Vector2 Spacing
+ {
+ get => Flow.Spacing;
+ set => Flow.Spacing = value;
+ }
+
+ public Vector2 MaximumSize
+ {
+ get => Flow.MaximumSize;
+ set => Flow.MaximumSize = value;
+ }
+
+ public new bool Masking
+ {
+ get => base.Masking;
+ set => base.Masking = value;
+ }
+
+ public FillDirection Direction
+ {
+ get => Flow.Direction;
+ set => Flow.Direction = value;
+ }
+
+ public IEnumerable Children => Flow.Children;
+
+ [Resolved]
+ internal LocalisationManager? Localisation { get; private set; }
+
+ protected readonly MsdfInnerFlow Flow;
+ private readonly Bindable localisationParameters = new();
+
+ public MsdfTextFlowContainer(Action? defaultCreationParameters = null)
+ {
+ this.defaultCreationParameters = defaultCreationParameters;
+
+ InternalChild = Flow = CreateFlow().With(f => f.AutoSizeAxes = Axes.Both);
+ }
+
+ protected virtual MsdfInnerFlow CreateFlow() => new();
+
+ protected override void LoadAsyncComplete()
+ {
+ base.LoadAsyncComplete();
+
+ if (Localisation != null)
+ localisationParameters.Value = Localisation.CurrentParameters.Value;
+
+ RecreateAllParts();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (Localisation != null)
+ {
+ localisationParameters.BindValueChanged(_ => partsCache.Invalidate());
+ ((IBindable)localisationParameters).BindTo(Localisation.CurrentParameters);
+ }
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!partsCache.IsValid)
+ RecreateAllParts();
+ }
+
+ protected override int Compare(Drawable x, Drawable y)
+ {
+ if (TextAnchor.HasFlagFast(Anchor.x2))
+ return base.Compare(y, x);
+
+ return base.Compare(x, y);
+ }
+
+ public IMsdfTextPart AddText(LocalisableString text, Action? creationParameters = null)
+ where TMsdfSpriteText : MsdfSpriteText, new()
+ => AddPart(CreateChunkFor(text, true, () => new TMsdfSpriteText(), creationParameters));
+
+ public IMsdfTextPart AddText(LocalisableString text, Action? creationParameters = null)
+ => AddPart(CreateChunkFor(text, true, CreateSpriteText, creationParameters));
+
+ public void AddText(TMsdfSpriteText text, Action? creationParameters = null)
+ where TMsdfSpriteText : MsdfSpriteText
+ {
+ defaultCreationParameters?.Invoke(text);
+ creationParameters?.Invoke(text);
+ AddPart(new MsdfTextPartManual(text.Yield()));
+ }
+
+ public IMsdfTextPart AddParagraph(LocalisableString paragraph, Action? creationParameters = null)
+ where TMsdfSpriteText : MsdfSpriteText, new()
+ => AddPart(CreateChunkFor(paragraph, false, () => new TMsdfSpriteText(), creationParameters));
+
+ ///
+ public IMsdfTextPart AddParagraph(LocalisableString paragraph, Action? creationParameters = null)
+ => AddPart(CreateChunkFor(paragraph, false, CreateSpriteText, creationParameters));
+
+ protected internal virtual MsdfTextChunk CreateChunkFor(LocalisableString text, bool newLineIsParagraph, Func creationFunc, Action? creationParameters = null)
+ where TMsdfSpriteText : MsdfSpriteText, new()
+ => new(text, newLineIsParagraph, creationFunc, creationParameters);
+
+ public void NewLine() => AddPart(new MsdfTextNewLine(false));
+
+ public void NewParagraph() => AddPart(new MsdfTextNewLine(true));
+
+ protected internal virtual MsdfSpriteText CreateSpriteText() => new();
+
+ internal void ApplyDefaultCreationParameters(MsdfSpriteText spriteText) => defaultCreationParameters?.Invoke(spriteText);
+
+ public void Clear(bool disposeChildren = true)
+ {
+ Flow.Clear(disposeChildren);
+ parts.Clear();
+ }
+
+ protected internal IMsdfTextPart AddPart(IMsdfTextPart part)
+ {
+ parts.Add(part);
+
+ if (partsCache.IsValid)
+ recreatePart(part);
+
+ return part;
+ }
+
+ public bool RemovePart(IMsdfTextPart partToRemove)
+ {
+ if (!parts.Remove(partToRemove))
+ return false;
+
+ partsCache.Invalidate();
+ return true;
+ }
+
+ protected virtual void RecreateAllParts()
+ {
+ foreach (var manualPart in parts.OfType())
+ Flow.RemoveRange(manualPart.Drawables, false);
+
+ Flow.Clear(true);
+
+ foreach (var part in parts)
+ recreatePart(part);
+
+ partsCache.Validate();
+ }
+
+ private void recreatePart(IMsdfTextPart part)
+ {
+ part.RecreateDrawablesFor(this);
+ foreach (var drawable in part.Drawables)
+ Flow.Add(drawable);
+ }
+
+ protected partial class MsdfInnerFlow : FillFlowContainer
+ {
+ public float FirstLineIndent
+ {
+ get;
+ set
+ {
+ if (field == value) return;
+
+ field = value;
+
+ InvalidateLayout();
+ }
+ }
+
+ public float ContentIndent
+ {
+ get;
+ set
+ {
+ if (field == value) return;
+
+ field = value;
+
+ InvalidateLayout();
+ }
+ }
+
+ public float ParagraphSpacing
+ {
+ get;
+ set
+ {
+ if (field == value) return;
+
+ field = value;
+
+ InvalidateLayout();
+ }
+ } = 0.5f;
+
+ public float LineSpacing
+ {
+ get;
+ set
+ {
+ if (field == value) return;
+
+ field = value;
+
+ InvalidateLayout();
+ }
+ }
+
+ public Anchor TextAnchor
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+
+ Anchor = value;
+ Origin = value;
+
+ InvalidateLayout();
+ }
+ } = Anchor.TopLeft;
+
+ protected override IEnumerable ComputeLayoutPositions()
+ {
+ var max = MaximumSize;
+
+ if (max == Vector2.Zero)
+ {
+ var s = ChildSize;
+
+ max.X = AutoSizeAxes.HasFlagFast(Axes.X) ? float.PositiveInfinity : s.X;
+ max.Y = AutoSizeAxes.HasFlagFast(Axes.Y) ? float.PositiveInfinity : s.Y;
+ }
+
+ var children = FlowingChildren.ToArray();
+ if (children.Length == 0)
+ yield break;
+
+ var layoutPositions = ArrayPool.Shared.Rent(children.Length);
+
+ var rowIndices = ArrayPool.Shared.Rent(children.Length);
+
+ var rowWidths = new List { 0 };
+ var lineBaseHeights = new List { 0 };
+
+ var rowHeight = 0.0f;
+ var current = Vector2.Zero;
+
+ var size = Vector2.Zero;
+
+ try
+ {
+ for (var i = 0; i < children.Length; ++i)
+ {
+ var c = children[i];
+
+ static Axes toAxes(FillDirection direction)
+ {
+ switch (direction)
+ {
+ case FillDirection.Full:
+ return Axes.Both;
+
+ case FillDirection.Horizontal:
+ return Axes.X;
+
+ case FillDirection.Vertical:
+ return Axes.Y;
+
+ default:
+ throw new ArgumentException($"{direction} is not defined");
+ }
+ }
+
+ if ((c.RelativeSizeAxes & AutoSizeAxes & toAxes(Direction)) != 0
+ && (c.FillMode != FillMode.Fit || c.RelativeSizeAxes != Axes.Both || c.Size.X > RelativeChildSize.X
+ || c.Size.Y > RelativeChildSize.Y || AutoSizeAxes == Axes.Both))
+ {
+ throw new InvalidOperationException(
+ "Drawables inside a fill flow container may not have a relative size axis that the fill flow container is filling in and auto sizing for. " +
+ $"The fill flow container is set to flow in the {Direction} direction and autosize in {AutoSizeAxes} axes and the child is set to relative size in {c.RelativeSizeAxes} axes.");
+ }
+
+ if (c.RelativeAnchorPosition != Vector2.Zero)
+ throw new InvalidOperationException($"All drawables in a {nameof(MsdfTextFlowContainer)} must not specify custom {nameof(RelativeAnchorPosition)}s. Only (0,0) is supported.");
+ if (c.RelativeOriginPosition != Vector2.Zero)
+ throw new InvalidOperationException($"All drawables in a {nameof(MsdfTextFlowContainer)} must not specify custom {nameof(RelativeOriginPosition)}s. Only (0,0) is supported.");
+
+ if (i == 0)
+ {
+ size = c.BoundingBox.Size;
+ current.X = ContentIndent + FirstLineIndent;
+ }
+
+ var rowWidth = current.X + size.X;
+
+ if (Direction != FillDirection.Horizontal && (Precision.DefinitelyBigger(rowWidth, max.X) || Direction == FillDirection.Vertical || c is NewLineContainer))
+ {
+ current.X = ContentIndent;
+ current.Y += rowHeight * (1 + LineSpacing);
+
+ if (c is NewLineContainer nlc)
+ current.Y += nlc.IndicatesNewParagraph ? rowHeight * ParagraphSpacing : 0;
+
+ layoutPositions[i] = current;
+
+ rowWidths.Add(current.X + size.X);
+ lineBaseHeights.Add((c as IHasLineBaseHeight)?.LineBaseHeight ?? 0);
+
+ rowHeight = 0;
+ }
+ else
+ {
+ layoutPositions[i] = current;
+
+ rowWidths[^1] = rowWidth;
+ lineBaseHeights[^1] = Math.Max(lineBaseHeights[^1], (c as IHasLineBaseHeight)?.LineBaseHeight ?? 0);
+ }
+
+ rowIndices[i] = rowWidths.Count - 1;
+ var stride = Vector2.Zero;
+
+ if (i < children.Length - 1)
+ {
+ stride = size;
+
+ c = children[i + 1];
+ size = c.BoundingBox.Size;
+ }
+
+ stride += Spacing;
+
+ if (stride.Y > rowHeight)
+ rowHeight = stride.Y;
+
+ current.X += stride.X;
+ }
+
+ if (!float.IsFinite(max.X))
+ {
+ var newMax = 0.0f;
+ foreach (var rowWidth in rowWidths)
+ newMax = MathF.Max(newMax, rowWidth);
+ max.X = newMax;
+ }
+
+ for (var i = 0; i < children.Length; i++)
+ {
+ var c = children[i];
+ var layoutPosition = layoutPositions[i];
+ var rowOffsetToEnd = max.X - rowWidths[rowIndices[i]];
+
+ if (TextAnchor.HasFlagFast(Anchor.x1))
+ layoutPosition.X += rowOffsetToEnd / 2;
+ else if (TextAnchor.HasFlagFast(Anchor.x2))
+ layoutPosition.X += rowOffsetToEnd;
+
+ if (c is IHasLineBaseHeight hasLineBaseHeight)
+ layoutPosition.Y += lineBaseHeights[rowIndices[i]] - hasLineBaseHeight.LineBaseHeight;
+
+ yield return layoutPosition;
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(layoutPositions);
+ ArrayPool.Shared.Return(rowIndices);
+ }
+ }
+ }
+
+ public partial class NewLineContainer : Container
+ {
+ public readonly bool IndicatesNewParagraph;
+
+ public NewLineContainer(bool newParagraph)
+ {
+ IndicatesNewParagraph = newParagraph;
+ }
+ }
+}
diff --git a/lib/Graphics/Containers/MsdfTextNewLine.cs b/lib/Graphics/Containers/MsdfTextNewLine.cs
new file mode 100644
index 0000000..de4bbf8
--- /dev/null
+++ b/lib/Graphics/Containers/MsdfTextNewLine.cs
@@ -0,0 +1,20 @@
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers;
+
+public class MsdfTextNewLine : MsdfTextPart
+{
+ private readonly bool indicatesNewParagraph;
+
+ public MsdfTextNewLine(bool indicatesNewParagraph)
+ {
+ this.indicatesNewParagraph = indicatesNewParagraph;
+ }
+
+ protected override IEnumerable CreateDrawablesFor(MsdfTextFlowContainer textFlowContainer)
+ {
+ var newLineContainer = new MsdfTextFlowContainer.NewLineContainer(indicatesNewParagraph);
+ return newLineContainer.Yield();
+ }
+}
diff --git a/lib/Graphics/Containers/MsdfTextPart.cs b/lib/Graphics/Containers/MsdfTextPart.cs
new file mode 100644
index 0000000..078f1d8
--- /dev/null
+++ b/lib/Graphics/Containers/MsdfTextPart.cs
@@ -0,0 +1,26 @@
+using osu.Framework.Graphics;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers;
+
+public abstract class MsdfTextPart : IMsdfTextPart
+{
+ public IEnumerable Drawables { get; }
+
+ public event Action>? DrawablePartsRecreated;
+
+ private readonly List drawables = [];
+
+ protected MsdfTextPart()
+ {
+ Drawables = drawables.AsReadOnly();
+ }
+
+ public void RecreateDrawablesFor(MsdfTextFlowContainer textFlowContainer)
+ {
+ drawables.Clear();
+ drawables.AddRange(CreateDrawablesFor(textFlowContainer));
+ DrawablePartsRecreated?.Invoke(drawables);
+ }
+
+ protected abstract IEnumerable CreateDrawablesFor(MsdfTextFlowContainer textFlowContainer);
+}
diff --git a/lib/Graphics/Containers/MsdfTextPartManual.cs b/lib/Graphics/Containers/MsdfTextPartManual.cs
new file mode 100644
index 0000000..01aad96
--- /dev/null
+++ b/lib/Graphics/Containers/MsdfTextPartManual.cs
@@ -0,0 +1,24 @@
+using System.Collections.Immutable;
+using osu.Framework.Graphics;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers;
+
+public class MsdfTextPartManual : IMsdfTextPart
+{
+ public IEnumerable Drawables { get; }
+
+ public event Action>? DrawablePartsRecreated
+ {
+ add { }
+ remove { }
+ }
+
+ public MsdfTextPartManual(IEnumerable drawables)
+ {
+ Drawables = drawables.ToImmutableArray();
+ }
+
+ public void RecreateDrawablesFor(MsdfTextFlowContainer textFlowContainer)
+ {
+ }
+}
diff --git a/lib/Graphics/Sprites/MsdfSpriteText.cs b/lib/Graphics/Sprites/MsdfSpriteText.cs
new file mode 100644
index 0000000..1847e38
--- /dev/null
+++ b/lib/Graphics/Sprites/MsdfSpriteText.cs
@@ -0,0 +1,597 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using kajarlabs.osu.Framework.MsdfTextRendering.IO.Stores;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Development;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.Graphics.Shaders.Types;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Layout;
+using osu.Framework.Localisation;
+using osu.Framework.Text;
+using osu.Framework.Utils;
+using osuTK;
+using osuTK.Graphics;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites;
+
+public partial class MsdfSpriteText : Drawable, IHasLineBaseHeight, ITexturedShaderDrawable, IHasText, IHasFilterTerms, IFillFlowContainer, IHasCurrentValue
+{
+ private static readonly char[] default_never_fixed_width_characters = ['.', ',', ':', ' ', '\u00A0', '\u202F'];
+
+ [Resolved]
+ private MsdfFontStore msdfFonts { get; set; } = null!;
+
+ [Resolved]
+ private LocalisationManager localisation { get; set; } = null!;
+
+ private ILocalisedBindableString localisedText = null!;
+
+ public IShader? TextureShader { get; protected set; }
+
+ private float distanceRange;
+ private bool storeReady;
+
+ public MsdfSpriteText()
+ {
+ current.BindValueChanged(text =>
+ {
+ if (localisedText == null || text.NewValue != localisedText.Value)
+ Text = text.NewValue;
+ });
+
+ AddLayout(charactersCache);
+ AddLayout(shadowOffsetCache);
+ AddLayout(textBuilderCache);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ShaderManager shaders)
+ {
+ TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "MsdfGlyph");
+
+ localisedText = localisation.GetLocalisedBindableString(Text);
+
+ updateFontStore();
+
+ storeReady = true;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ localisedText.BindValueChanged(str =>
+ {
+ current.Value = localisedText.Value;
+
+ if (string.IsNullOrEmpty(str.NewValue))
+ {
+ if (requiresAutoSizedWidth)
+ base.Width = Padding.TotalHorizontal;
+ if (requiresAutoSizedHeight)
+ base.Height = Padding.TotalVertical;
+ }
+
+ invalidate(true);
+ }, true);
+ }
+
+ public LocalisableString Text
+ {
+ get;
+ set
+ {
+ if (field.Equals(value))
+ return;
+
+ field = value;
+ localisedText?.Text = value;
+ }
+ } = string.Empty;
+
+ private readonly BindableWithCurrent current = new();
+
+ public Bindable Current
+ {
+ get => current.Current;
+ set => current.Current = value;
+ }
+
+ private string displayedText => localisedText?.Value ?? Text.ToString();
+
+ public FontUsage Font
+ {
+ get;
+ set
+ {
+ field = value;
+ invalidate(true, true);
+ shadowOffsetCache.Invalidate();
+
+ if (storeReady)
+ updateFontStore();
+ }
+ }
+
+ public bool AllowMultiline
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ if (value)
+ Truncate = false;
+
+ field = value;
+ invalidate(true, true);
+ }
+ } = true;
+
+ public bool Shadow
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+ Invalidate(Invalidation.DrawNode);
+ }
+ }
+
+ public Color4 ShadowColour
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+ Invalidate(Invalidation.DrawNode);
+ }
+ } = new Color4(0, 0, 0, 0.2f);
+
+ public Vector2 ShadowOffset
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+ invalidate(true);
+ shadowOffsetCache.Invalidate();
+ }
+ } = new Vector2(0, 0.06f);
+
+ public bool UseFullGlyphHeight
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+ invalidate(true, true);
+ }
+ } = true;
+
+ public bool Truncate
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ if (value)
+ AllowMultiline = false;
+
+ field = value;
+ invalidate(true, true);
+ }
+ }
+
+ public string EllipsisString
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+ invalidate(true, true);
+ }
+ } = "…";
+
+ public bool IsTruncated { get; private set; }
+
+ private bool requiresAutoSizedWidth => explicitWidth == null && (RelativeSizeAxes & Axes.X) == 0;
+
+ private bool requiresAutoSizedHeight => explicitHeight == null && (RelativeSizeAxes & Axes.Y) == 0;
+
+ private float? explicitWidth;
+
+ public override float Width
+ {
+ get
+ {
+ if (requiresAutoSizedWidth)
+ computeCharacters();
+ return base.Width;
+ }
+ set
+ {
+ if (explicitWidth == value)
+ return;
+
+ base.Width = value;
+ explicitWidth = value;
+
+ invalidate(true, true);
+ }
+ }
+
+ public float MaxWidth
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+ invalidate(true, true);
+ }
+ } = float.PositiveInfinity;
+
+ private float? explicitHeight;
+
+ public override float Height
+ {
+ get
+ {
+ if (requiresAutoSizedHeight)
+ computeCharacters();
+ return base.Height;
+ }
+ set
+ {
+ if (explicitHeight == value)
+ return;
+
+ base.Height = value;
+ explicitHeight = value;
+
+ invalidate(true, true);
+ }
+ }
+
+ public override Vector2 Size
+ {
+ get
+ {
+ if (requiresAutoSizedWidth || requiresAutoSizedHeight)
+ computeCharacters();
+ return base.Size;
+ }
+ set
+ {
+ Width = value.X;
+ Height = value.Y;
+ }
+ }
+
+ public Vector2 Spacing
+ {
+ get;
+ set
+ {
+ if (field == value)
+ return;
+
+ field = value;
+ invalidate(true, true);
+ }
+ }
+
+ public MarginPadding Padding
+ {
+ get;
+ set
+ {
+ if (field.Equals(value))
+ return;
+
+ if (!Validation.IsFinite(value))
+ throw new ArgumentException($@"{nameof(Padding)} must be finite, but is {value}.");
+
+ field = value;
+ invalidate(true, true);
+ }
+ }
+
+ public override bool IsPresent => base.IsPresent && (AlwaysPresent || !string.IsNullOrEmpty(displayedText));
+
+ private readonly LayoutValue charactersCache = new(Invalidation.DrawSize | Invalidation.Presence, InvalidationSource.Parent);
+
+ private readonly List charactersBacking = [];
+
+ private List characters
+ {
+ get
+ {
+ computeCharacters();
+ return charactersBacking;
+ }
+ }
+
+ private void computeCharacters()
+ {
+ if (LoadState >= LoadState.Loaded)
+ Debug.Assert(ThreadSafety.IsUpdateThread);
+
+ if (!storeReady)
+ return;
+
+ if (charactersCache.IsValid)
+ return;
+
+ IsTruncated = false;
+
+ charactersBacking.Clear();
+
+ var textBounds = Vector2.Zero;
+
+ try
+ {
+ if (string.IsNullOrEmpty(displayedText))
+ return;
+
+ var textBuilder = getTextBuilder();
+
+ textBuilder.Reset();
+ textBuilder.AddText(displayedText);
+ textBounds = textBuilder.Bounds;
+
+ if (textBuilder is TruncatingTextBuilder truncatingTextBuilder)
+ IsTruncated = truncatingTextBuilder.IsTruncated;
+ }
+ finally
+ {
+ if (requiresAutoSizedWidth)
+ base.Width = textBounds.X + Padding.Right;
+ if (requiresAutoSizedHeight)
+ base.Height = textBounds.Y + Padding.Bottom;
+
+ base.Width = Math.Min(base.Width, MaxWidth);
+
+ charactersCache.Validate();
+ }
+ }
+
+ private readonly LayoutValue shadowOffsetCache = new(Invalidation.DrawInfo, InvalidationSource.Parent);
+
+ private Vector2 premultipliedShadowOffset =>
+ shadowOffsetCache.IsValid ? shadowOffsetCache.Value : shadowOffsetCache.Value = ToScreenSpace(ShadowOffset * Font.Size) - ToScreenSpace(Vector2.Zero);
+
+ private void invalidate(bool characters = false, bool textBuilder = false)
+ {
+ if (characters)
+ charactersCache.Invalidate();
+
+ if (textBuilder)
+ InvalidateTextBuilder();
+
+ Invalidate(Invalidation.RequiredParentSizeToFit);
+ }
+
+ protected override DrawNode CreateDrawNode() => new MsdfSpriteTextDrawNode(this);
+
+ ///
+ /// The characters that should be excluded from fixed-width application. Defaults to (".", ",", ":", " ") if null.
+ ///
+ protected virtual char[]? FixedWidthExcludeCharacters => null;
+
+ ///
+ /// The character to use to calculate the fixed width width. Defaults to 'm'.
+ ///
+ protected virtual char FixedWidthReferenceCharacter => 'm';
+
+ ///
+ /// The character to fallback to use if a character glyph lookup failed.
+ ///
+ ///
+ /// Native defaults this to '•', but MSDF atlases are generated from a
+ /// charset file that does not necessarily include it, in
+ /// which case the fallback lookup itself silently fails and the character is dropped entirely.
+ /// '?' is used instead since it's part of the basic ASCII range every generated atlas covers.
+ ///
+ protected virtual char FallbackCharacter => '?';
+
+ private readonly LayoutValue textBuilderCache = new(Invalidation.DrawSize, InvalidationSource.Parent);
+
+ protected void InvalidateTextBuilder() => textBuilderCache.Invalidate();
+
+ protected virtual TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store)
+ {
+ var excludeCharacters = FixedWidthExcludeCharacters ?? default_never_fixed_width_characters;
+
+ var builderMaxWidth = requiresAutoSizedWidth
+ ? MaxWidth
+ : ApplyRelativeAxes(RelativeSizeAxes, new Vector2(Math.Min(MaxWidth, base.Width), base.Height), FillMode).X - Padding.Right;
+
+ if (AllowMultiline)
+ {
+ return new MultilineTextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), Spacing, charactersBacking,
+ excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter);
+ }
+
+ if (Truncate)
+ {
+ return new TruncatingTextBuilder(store, Font, builderMaxWidth, EllipsisString, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), Spacing, charactersBacking,
+ excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter);
+ }
+
+ return new TextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, new Vector2(Padding.Left, Padding.Top), Spacing, charactersBacking,
+ excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter);
+ }
+
+ private TextBuilder getTextBuilder()
+ {
+ if (!textBuilderCache.IsValid)
+ textBuilderCache.Value = CreateTextBuilder(msdfFonts);
+
+ return textBuilderCache.Value;
+ }
+
+ private void updateFontStore()
+ {
+ distanceRange = msdfFonts.GetDistanceRange(Font.FontName) ?? 0f;
+ }
+
+ public override string ToString() => $@"""{displayedText}"" " + base.ToString();
+
+ public float LineBaseHeight
+ {
+ get
+ {
+ computeCharacters();
+ return textBuilderCache.Value.LineBaseHeight;
+ }
+ }
+
+ public IEnumerable FilterTerms => Text.Yield();
+
+ private class MsdfSpriteTextDrawNode : TexturedShaderDrawNode
+ {
+ protected new MsdfSpriteText Source => (MsdfSpriteText)base.Source;
+
+ private bool shadow;
+ private ColourInfo shadowColour;
+ private Vector2 shadowOffset;
+ private float distanceRange;
+
+ private List? parts;
+ private IUniformBuffer? parametersBuffer;
+
+ public MsdfSpriteTextDrawNode(MsdfSpriteText source)
+ : base(source)
+ {
+ }
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ updateScreenSpaceCharacters();
+ distanceRange = Source.distanceRange;
+ shadow = Source.Shadow;
+
+ if (shadow)
+ {
+ shadowColour = Source.ShadowColour;
+ shadowOffset = Source.premultipliedShadowOffset;
+ }
+ }
+
+ protected override void Draw(IRenderer renderer)
+ {
+ Debug.Assert(parts != null);
+
+ base.Draw(renderer);
+
+ BindTextureShader(renderer);
+
+ var avgColour = (Color4)DrawColourInfo.Colour.AverageColour;
+ var shadowAlpha = MathF.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2);
+
+ var finalShadowColour = DrawColourInfo.Colour;
+ finalShadowColour.ApplyChild(shadowColour.MultiplyAlpha(shadowAlpha));
+
+ for (var i = 0; i < parts.Count; i++)
+ {
+ if (shadow)
+ {
+ var shadowQuad = parts[i].DrawQuad;
+
+ renderer.DrawQuad(parts[i].Texture,
+ new Quad(
+ shadowQuad.TopLeft + shadowOffset,
+ shadowQuad.TopRight + shadowOffset,
+ shadowQuad.BottomLeft + shadowOffset,
+ shadowQuad.BottomRight + shadowOffset),
+ finalShadowColour, inflationPercentage: parts[i].InflationPercentage);
+ }
+
+ renderer.DrawQuad(parts[i].Texture, parts[i].DrawQuad, DrawColourInfo.Colour, inflationPercentage: parts[i].InflationPercentage);
+ }
+
+ UnbindTextureShader(renderer);
+ }
+
+ protected override void BindUniformResources(IShader shader, IRenderer renderer)
+ {
+ base.BindUniformResources(shader, renderer);
+
+ parametersBuffer ??= renderer.CreateUniformBuffer();
+ parametersBuffer.Data = new MsdfParameters { DistanceRange = new UniformFloat { Value = distanceRange } };
+
+ shader.BindUniformBlock("m_MsdfParameters", parametersBuffer);
+ }
+
+ private void updateScreenSpaceCharacters()
+ {
+ var partCount = Source.characters.Count;
+
+ if (parts == null)
+ parts = new List(partCount);
+ else
+ {
+ parts.Clear();
+ parts.EnsureCapacity(partCount);
+ }
+
+ foreach (var character in Source.characters)
+ {
+ parts.Add(new ScreenSpaceGlyphPart
+ {
+ DrawQuad = Source.ToScreenSpace(character.DrawRectangle),
+ InflationPercentage = Vector2.Zero,
+ Texture = character.Texture,
+ });
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private record struct MsdfParameters
+ {
+ public UniformFloat DistanceRange;
+ private readonly UniformPadding12 padding;
+ }
+
+ private struct ScreenSpaceGlyphPart
+ {
+ public Quad DrawQuad;
+ public Vector2 InflationPercentage;
+ public Texture Texture;
+ }
+ }
+}
diff --git a/lib/IO/Stores/MsdfFontStore.cs b/lib/IO/Stores/MsdfFontStore.cs
new file mode 100644
index 0000000..ca9d174
--- /dev/null
+++ b/lib/IO/Stores/MsdfFontStore.cs
@@ -0,0 +1,144 @@
+using System.Collections.Concurrent;
+using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.IO.Stores;
+using osu.Framework.Logging;
+using osu.Framework.Text;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.IO.Stores;
+
+public class MsdfFontStore : IDisposable, ITexturedGlyphLookupStore
+{
+ private readonly IRenderer renderer;
+ private readonly ShaderManager shaders;
+
+ private readonly List sources = [];
+ private readonly ConcurrentDictionary<(string, char), ITexturedCharacterGlyph?> namespacedGlyphCache = [];
+
+ private Task? childStoreLoadTasks;
+
+ public MsdfFontStore(IRenderer renderer, ShaderManager shaders)
+ {
+ this.renderer = renderer;
+ this.shaders = shaders;
+ }
+
+ public void AddFont(IResourceStore store, string family, string weight)
+ {
+ var source = new MsdfGlyphSource(renderer, shaders, store, family, weight);
+
+ sources.Add(source);
+ queueLoad(source);
+ }
+
+ public ITexturedCharacterGlyph? Get(string? fontName, char character)
+ {
+ var key = (fontName ?? string.Empty, character);
+
+ if (namespacedGlyphCache.TryGetValue(key, out var existing))
+ return existing;
+
+ return namespacedGlyphCache[key] = resolveGlyph(fontName, character);
+ }
+
+ public float? GetDistanceRange(string? fontName)
+ => tryGetSource(fontName)?.DistanceRange;
+
+ public IShader? GetShader(string? fontName)
+ => tryGetSource(fontName)?.Shader;
+
+ ITexturedCharacterGlyph? ITexturedGlyphLookupStore.Get(string? fontName, char character)
+ => Get(fontName, character);
+
+ Task ITexturedGlyphLookupStore.GetAsync(string fontName, char character)
+ => Task.FromResult(Get(fontName, character));
+
+ public static float ReadDistanceRange(byte[] json)
+ => MsdfGlyphSource.ReadDistanceRange(json);
+
+ private MsdfGlyphSource? tryGetSource(string? fontName)
+ {
+ var name = fontName ?? string.Empty;
+
+ foreach (var source in sources)
+ {
+ if (source.FontName.EndsWith(name, StringComparison.Ordinal))
+ return source;
+ }
+
+ foreach (var source in sources)
+ {
+ if (!name.StartsWith(source.Family, StringComparison.Ordinal))
+ continue;
+
+ var style = name[source.Family.Length..];
+
+ if (style.Length > 0)
+ {
+ if (style[0] != '-')
+ continue;
+
+ style = style[1..];
+ }
+
+ var (weight, italic) = MsdfGlyphSource.ParseStyle(style);
+
+ if (italic == source.IsItalic && weight.Equals(source.NormalizedWeight, StringComparison.Ordinal))
+ return source;
+ }
+
+ return null;
+ }
+
+ private ITexturedCharacterGlyph? resolveGlyph(string? fontName, char character)
+ {
+ var primary = string.IsNullOrEmpty(fontName) ? null : tryGetSource(fontName);
+
+ if (primary?.HasGlyph(character) == true)
+ return primary.Get(character);
+
+ foreach (var source in sources.OrderBy(s => primary != null && s.Family == primary.Family ? 0 : 1))
+ {
+ if (source == primary || !source.HasGlyph(character))
+ continue;
+
+ if (primary != null && source.DistanceRange != primary.DistanceRange)
+ {
+ Logger.Log($"{nameof(MsdfFontStore)}: '{character}' (U+{(int)character:X4}) falls back from '{primary.FontName}' to '{source.FontName}', " +
+ $"whose distanceRange ({source.DistanceRange}) differs -- a single text draw uses one distanceRange, so its edges may look off.",
+ LoggingTarget.Runtime, LogLevel.Important);
+ }
+
+ return source.Get(character, primary);
+ }
+
+ return null;
+ }
+
+ private void queueLoad(MsdfGlyphSource source)
+ {
+ var previousLoadStream = childStoreLoadTasks;
+
+ childStoreLoadTasks = Task.Run(async () =>
+ {
+ if (previousLoadStream != null)
+ await previousLoadStream.ConfigureAwait(false);
+
+ try
+ {
+ Logger.Log($"Loading Typeface {source.FontName}...", level: LogLevel.Debug);
+ source.Load();
+ Logger.Log($"Loaded Typeface {source.FontName}!", level: LogLevel.Debug);
+ }
+ catch
+ {
+ }
+ });
+ }
+
+ public void Dispose()
+ {
+ foreach (var source in sources)
+ source?.Dispose();
+ }
+}
diff --git a/lib/IO/Stores/MsdfGlyphSource.cs b/lib/IO/Stores/MsdfGlyphSource.cs
new file mode 100644
index 0000000..c8b3323
--- /dev/null
+++ b/lib/IO/Stores/MsdfGlyphSource.cs
@@ -0,0 +1,305 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using kajarlabs.osu.Framework.MsdfTextRendering.Text;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.IO.Stores;
+using osu.Framework.Logging;
+using osu.Framework.Text;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.IO.Stores;
+
+internal sealed class MsdfGlyphSource : IDisposable
+{
+ public Texture Atlas
+ {
+ get
+ {
+ ensureLoaded();
+ return field;
+ }
+ private set;
+ } = null!;
+
+ public IShader Shader
+ {
+ get
+ {
+ ensureLoaded();
+ return field;
+ }
+ private set;
+ } = null!;
+
+ public float Ascender
+ {
+ get
+ {
+ ensureLoaded();
+ return field;
+ }
+ private set;
+ }
+
+ public float Descender
+ {
+ get
+ {
+ ensureLoaded();
+ return field;
+ }
+ private set;
+ }
+
+ public float LineHeight
+ {
+ get
+ {
+ ensureLoaded();
+ return field;
+ }
+ private set;
+ }
+
+ public float DistanceRange
+ {
+ get
+ {
+ ensureLoaded();
+ return field;
+ }
+ private set;
+ }
+
+ public string Family { get; }
+
+ public string FontName { get; }
+
+ public string NormalizedWeight { get; }
+
+ public bool IsItalic { get; }
+
+ public IReadOnlyDictionary Glyphs
+ {
+ get
+ {
+ ensureLoaded();
+ return field;
+ }
+ private set;
+ } = new Dictionary();
+
+ public IReadOnlyDictionary<(char First, char Second), float> Kerning
+ {
+ get
+ {
+ ensureLoaded();
+ return field;
+ }
+ private set;
+ } = new Dictionary<(char, char), float>();
+
+ private readonly IRenderer renderer;
+ private readonly ShaderManager shaders;
+ private readonly IResourceStore store;
+ private readonly string weight;
+
+ private readonly TaskCompletionSource loadCompletionSource = new();
+
+ private TextureStore? textures;
+
+ public MsdfGlyphSource(IRenderer renderer, ShaderManager shaders, IResourceStore store, string family, string weight)
+ {
+ this.renderer = renderer;
+ this.shaders = shaders;
+ this.store = store;
+ this.weight = weight;
+
+ Family = family;
+ FontName = string.IsNullOrEmpty(weight) ? family : $"{family}-{weight}";
+
+ (NormalizedWeight, IsItalic) = ParseStyle(weight);
+ }
+
+ public void Load()
+ {
+ textures = new TextureStore(
+ renderer,
+ new TextureLoaderStore(store),
+ useAtlas: false,
+ filteringMode: TextureFilteringMode.Linear,
+ manualMipmaps: true
+ );
+
+ var texturePath = $@"Typefaces/{Family}/{Family}-{weight}.png";
+ var jsonPath = $@"Typefaces/{Family}/{Family}-{weight}.json";
+
+ Atlas = textures.Get(texturePath);
+ Shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "MsdfGlyph");
+
+ var json = store.Get(jsonPath) ?? throw new InvalidOperationException($"Could not find embedded resource '{jsonPath}'.");
+ var atlasJson = parseAtlasJson(json);
+
+ Ascender = atlasJson.Metrics.Ascender;
+ Descender = atlasJson.Metrics.Descender;
+ LineHeight = atlasJson.Metrics.LineHeight;
+ DistanceRange = atlasJson.Atlas.DistanceRange;
+
+ var newGlyphs = new Dictionary();
+
+ foreach (var glyph in atlasJson.Glyphs)
+ {
+ newGlyphs[(char)glyph.Unicode] = new MsdfGlyph(
+ glyph.Advance,
+ toRectangle(glyph.PlaneBounds),
+ toRectangle(glyph.AtlasBounds)
+ );
+ }
+
+ Glyphs = newGlyphs;
+
+ var newKerning = new Dictionary<(char, char), float>();
+
+ foreach (var pair in atlasJson.Kerning)
+ newKerning[((char)pair.Unicode1, (char)pair.Unicode2)] = pair.Advance;
+
+ Kerning = newKerning;
+
+ loadCompletionSource.SetResult(true);
+ }
+
+ public bool HasGlyph(char character)
+ {
+ try
+ {
+ ensureLoaded();
+ }
+ catch
+ {
+ return false;
+ }
+
+ return Glyphs.ContainsKey(character);
+ }
+
+ public ITexturedCharacterGlyph? Get(char character, MsdfGlyphSource? metricsSource = null)
+ {
+ ensureLoaded();
+
+ if (!Glyphs.TryGetValue(character, out var glyph))
+ {
+ Logger.Log($"{nameof(MsdfGlyphSource)}: no glyph for character '{character}' (U+{(int)character:X4}) in atlas '{FontName}' -- skipping.", LoggingTarget.Runtime, LogLevel.Debug);
+ return null;
+ }
+
+ var texture = glyph.AtlasBounds is RectangleF atlasBound ? Atlas.Crop(atlasBound) : Atlas;
+ return new MsdfTexturedCharacterGlyph(this, metricsSource ?? this, character, glyph, texture);
+ }
+
+ public static float ReadDistanceRange(byte[] json)
+ => parseAtlasJson(json).Atlas.DistanceRange;
+
+ public static (string Weight, bool Italic) ParseStyle(string? style)
+ {
+ var weight = style ?? string.Empty;
+ var italic = weight.EndsWith("Italic", StringComparison.Ordinal);
+
+ if (italic)
+ weight = weight[..^"Italic".Length];
+
+ if (weight.Equals("Regular", StringComparison.Ordinal))
+ weight = string.Empty;
+
+ return (weight, italic);
+ }
+
+ private void ensureLoaded()
+ => loadCompletionSource.Task.GetResultSafely();
+
+ private static AtlasJson parseAtlasJson(byte[] json)
+ => JsonSerializer.Deserialize(json) ?? throw new InvalidOperationException("Failed to parse MSDF atlas JSON.");
+
+ private static RectangleF? toRectangle(BoundsJson? bounds)
+ => bounds == null ? null : new RectangleF(bounds.Left, bounds.Top, bounds.Right - bounds.Left, bounds.Bottom - bounds.Top);
+
+ public void Dispose()
+ => textures?.Dispose();
+
+ private sealed class AtlasJson
+ {
+ [JsonPropertyName("atlas")]
+ public AtlasMetaJson Atlas { get; set; } = new AtlasMetaJson();
+
+ [JsonPropertyName("metrics")]
+ public MetricsJson Metrics { get; set; } = new MetricsJson();
+
+ [JsonPropertyName("glyphs")]
+ public List Glyphs { get; set; } = new List();
+
+ [JsonPropertyName("kerning")]
+ public List Kerning { get; set; } = new List();
+ }
+
+ private sealed class AtlasMetaJson
+ {
+ [JsonPropertyName("distanceRange")]
+ public float DistanceRange { get; set; }
+ }
+
+ private sealed class MetricsJson
+ {
+ [JsonPropertyName("ascender")]
+ public float Ascender { get; set; }
+
+ [JsonPropertyName("descender")]
+ public float Descender { get; set; }
+
+ [JsonPropertyName("lineHeight")]
+ public float LineHeight { get; set; }
+ }
+
+ private sealed class GlyphJson
+ {
+ [JsonPropertyName("unicode")]
+ public int Unicode { get; set; }
+
+ [JsonPropertyName("advance")]
+ public float Advance { get; set; }
+
+ [JsonPropertyName("planeBounds")]
+ public BoundsJson? PlaneBounds { get; set; }
+
+ [JsonPropertyName("atlasBounds")]
+ public BoundsJson? AtlasBounds { get; set; }
+ }
+
+ private sealed class BoundsJson
+ {
+ [JsonPropertyName("left")]
+ public float Left { get; set; }
+
+ [JsonPropertyName("top")]
+ public float Top { get; set; }
+
+ [JsonPropertyName("right")]
+ public float Right { get; set; }
+
+ [JsonPropertyName("bottom")]
+ public float Bottom { get; set; }
+ }
+
+ private sealed class KerningJson
+ {
+ [JsonPropertyName("unicode1")]
+ public int Unicode1 { get; set; }
+
+ [JsonPropertyName("unicode2")]
+ public int Unicode2 { get; set; }
+
+ [JsonPropertyName("advance")]
+ public float Advance { get; set; }
+ }
+}
diff --git a/lib/MsdfTextRendering.cs b/lib/MsdfTextRendering.cs
new file mode 100644
index 0000000..5c578de
--- /dev/null
+++ b/lib/MsdfTextRendering.cs
@@ -0,0 +1,11 @@
+using osu.Framework.IO.Stores;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering;
+
+public static class MsdfTextRendering
+{
+ public static void CreateCapabilities(ResourceStore store)
+ {
+ store.AddStore(new NamespacedResourceStore(new DllResourceStore(typeof(MsdfTextRendering).Assembly), @"Resources"));
+ }
+}
diff --git a/lib/Resources/Shaders/sh_MsdfGlyph.fs b/lib/Resources/Shaders/sh_MsdfGlyph.fs
new file mode 100644
index 0000000..f50d9bc
--- /dev/null
+++ b/lib/Resources/Shaders/sh_MsdfGlyph.fs
@@ -0,0 +1,43 @@
+#ifndef MSDFGLYPH_FS
+#define MSDFGLYPH_FS
+
+#include "sh_Utils.h"
+#include "sh_Masking.h"
+#include "sh_TextureWrapping.h"
+
+layout(location = 2) in mediump vec2 v_TexCoord;
+
+layout(std140, set = 0, binding = 0) uniform m_MsdfParameters
+{
+ mediump float g_DistanceRange;
+};
+
+layout(set = 1, binding = 0) uniform lowp texture2D m_Texture;
+layout(set = 1, binding = 1) uniform lowp sampler m_Sampler;
+
+layout(location = 0) out vec4 o_Colour;
+
+float medianOfThree(float r, float g, float b)
+{
+ return max(min(r, g), min(max(r, g), b));
+}
+
+float screenPxRange(vec2 texCoord)
+{
+ vec2 unitRange = vec2(g_DistanceRange) / vec2(textureSize(sampler2D(m_Texture, m_Sampler), 0));
+ vec2 screenTexSize = vec2(1.0) / fwidth(texCoord);
+ return max(0.5 * dot(unitRange, screenTexSize), 1.0);
+}
+
+void main(void)
+{
+ vec2 wrappedCoord = wrap(v_TexCoord, v_TexRect);
+ vec3 msd = texture(sampler2D(m_Texture, m_Sampler), wrappedCoord).rgb;
+
+ float signedDistance = medianOfThree(msd.r, msd.g, msd.b) - 0.5;
+ float coverage = clamp(signedDistance * screenPxRange(v_TexCoord) + 0.5, 0.0, 1.0);
+
+ o_Colour = getRoundedColor(vec4(1.0, 1.0, 1.0, coverage), wrappedCoord);
+}
+
+#endif
diff --git a/lib/Resources/Shaders/sh_MsdfGlyphHardThreshold.fs b/lib/Resources/Shaders/sh_MsdfGlyphHardThreshold.fs
new file mode 100644
index 0000000..8e24730
--- /dev/null
+++ b/lib/Resources/Shaders/sh_MsdfGlyphHardThreshold.fs
@@ -0,0 +1,30 @@
+#ifndef MSDFGLYPHHARDTHRESHOLD_FS
+#define MSDFGLYPHHARDTHRESHOLD_FS
+
+#include "sh_Utils.h"
+#include "sh_Masking.h"
+#include "sh_TextureWrapping.h"
+
+layout(location = 2) in mediump vec2 v_TexCoord;
+
+layout(set = 0, binding = 0) uniform lowp texture2D m_Texture;
+layout(set = 0, binding = 1) uniform lowp sampler m_Sampler;
+
+layout(location = 0) out vec4 o_Colour;
+
+float medianOfThree(float r, float g, float b)
+{
+ return max(min(r, g), min(max(r, g), b));
+}
+
+void main(void)
+{
+ vec2 wrappedCoord = wrap(v_TexCoord, v_TexRect);
+ vec3 msd = texture(sampler2D(m_Texture, m_Sampler), wrappedCoord).rgb;
+
+ float coverage = medianOfThree(msd.r, msd.g, msd.b) > 0.5 ? 1.0 : 0.0;
+
+ o_Colour = getRoundedColor(vec4(1.0, 1.0, 1.0, coverage), wrappedCoord);
+}
+
+#endif
diff --git a/lib/Text/MsdfGlyph.cs b/lib/Text/MsdfGlyph.cs
new file mode 100644
index 0000000..149c6d6
--- /dev/null
+++ b/lib/Text/MsdfGlyph.cs
@@ -0,0 +1,5 @@
+using osu.Framework.Graphics.Primitives;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Text;
+
+public readonly record struct MsdfGlyph(float Advance, RectangleF? PlaneBounds, RectangleF? AtlasBounds);
diff --git a/lib/Text/MsdfTexturedCharacterGlyph.cs b/lib/Text/MsdfTexturedCharacterGlyph.cs
new file mode 100644
index 0000000..485df9f
--- /dev/null
+++ b/lib/Text/MsdfTexturedCharacterGlyph.cs
@@ -0,0 +1,52 @@
+using kajarlabs.osu.Framework.MsdfTextRendering.IO.Stores;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Text;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Text;
+
+public readonly struct MsdfTexturedCharacterGlyph : ITexturedCharacterGlyph
+{
+ public Texture Texture { get; }
+
+ public float Width { get; }
+
+ public float Height { get; }
+
+ public float XOffset { get; }
+
+ public float YOffset { get; }
+
+ public float XAdvance { get; }
+
+ public float Baseline { get; }
+
+ public char Character { get; }
+
+ private readonly MsdfGlyphSource source;
+ private readonly float lineHeight;
+
+ internal MsdfTexturedCharacterGlyph(MsdfGlyphSource source, MsdfGlyphSource metricsSource, char character, MsdfGlyph glyph, Texture texture)
+ {
+ this.source = source;
+
+ Character = character;
+ Texture = texture;
+
+ this.lineHeight = metricsSource.LineHeight;
+ var lineHeight = this.lineHeight;
+ var planeBounds = glyph.PlaneBounds ?? default;
+
+ Width = planeBounds.Width / lineHeight;
+ Height = planeBounds.Height / lineHeight;
+ XOffset = planeBounds.Left / lineHeight;
+ YOffset = (planeBounds.Top - metricsSource.Ascender) / lineHeight;
+
+ XAdvance = glyph.Advance / lineHeight;
+
+ Baseline = -metricsSource.Ascender / lineHeight;
+ }
+
+ public float GetKerning(T lastGlyph)
+ where T : ICharacterGlyph
+ => source.Kerning.TryGetValue((lastGlyph.Character, Character), out var kerning) ? kerning / lineHeight : 0;
+}
diff --git a/tests/Graphics/SampleGameFont.cs b/tests/Graphics/SampleGameFont.cs
new file mode 100644
index 0000000..673433c
--- /dev/null
+++ b/tests/Graphics/SampleGameFont.cs
@@ -0,0 +1,48 @@
+using osu.Framework.Graphics.Sprites;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Graphics;
+
+public static class SampleGameFont
+{
+ public const float DEFAULT_FONT_SIZE = 18.0f;
+
+ public static FontUsage WorkSans => GetFont(FontTypeface.WorkSans);
+
+ public static FontUsage GetFont(FontTypeface typeface = FontTypeface.WorkSans, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Regular, bool italics = false, bool fixedWidth = false)
+ {
+ var familyStr = GetFamilyString(typeface);
+
+ return new FontUsage(familyStr, size, GetWeightString(familyStr, weight), getItalics(italics), fixedWidth);
+ }
+
+ public static string GetFamilyString(FontTypeface typeface)
+ {
+ return typeface.ToString();
+ }
+
+ public static string GetWeightString(string family, FontWeight weight)
+ {
+ return weight.ToString();
+ }
+
+ private static bool getItalics(in bool italicsRequested)
+ => false;
+}
+
+public enum FontTypeface
+{
+ WorkSans,
+}
+
+public enum FontWeight
+{
+ Thin = 100,
+ ExtraLight = 200,
+ Light = 300,
+ Regular = 400,
+ Medium = 500,
+ SemiBold = 600,
+ Bold = 700,
+ ExtraBold = 800,
+ Black = 900,
+}
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-Black.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-Black.fnt
index aee16ae..8b821ad 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-Black.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-Black.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic.fnt
index d1992e3..8d320fa 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic_0.png
index 940fcf7..92d8a12 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-BlackItalic_0.png differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-Black_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-Black_0.png
index 329797c..636e0f5 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-Black_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-Black_0.png differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic.fnt
index 3bda659..e46a86d 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic_0.png
index 95a0832..5c27fc2 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-BoldItalic_0.png differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic.fnt
index 270d17e..f5b6bd3 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic_0.png
index 1477a18..af419ca 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraBoldItalic_0.png differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic.fnt
index 0eb8e9e..3387cb3 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic_0.png
index e58e65c..f254ffe 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-ExtraLightItalic_0.png differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic.fnt
index f9d4350..bbc903e 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic_0.png
index a4bd60d..9192fba 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-LightItalic_0.png differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-Medium.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-Medium.fnt
index b92852e..fffa54c 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-Medium.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-Medium.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic.fnt
index a453487..226ebe3 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic_0.png
index aeff560..698b61d 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-MediumItalic_0.png differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic.fnt
index cf3597a..eb66f02 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic_0.png
index f39b66f..111cc73 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-RegularItalic_0.png differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBold.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBold.fnt
index 8c375ea..0de90d4 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBold.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBold.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic.fnt
index e3a887c..9e2270e 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic_0.png
index 55e68cf..15d85bf 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-SemiBoldItalic_0.png differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-Thin.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-Thin.fnt
index 9a2cf33..a61fddc 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-Thin.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-Thin.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic.fnt b/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic.fnt
index 7239fa0..ec69a2e 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic.fnt and b/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic.fnt differ
diff --git a/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic_0.png b/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic_0.png
index 99ec6fa..b107cd9 100644
Binary files a/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic_0.png and b/tests/Resources/Fonts/WorkSans/WorkSans-ThinItalic_0.png differ
diff --git a/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.json b/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.json
new file mode 100644
index 0000000..9928545
--- /dev/null
+++ b/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.json
@@ -0,0 +1 @@
+{"atlas":{"type":"msdf","distanceRange":4,"distanceRangeMiddle":0,"size":48,"width":856,"height":856,"yOrigin":"top"},"metrics":{"emSize":1,"lineHeight":1.448,"ascender":-1.1599999999999999,"descender":0.28800000000000003,"underlineY":0.14999999999999999,"underlineThickness":0.050000000000000003},"glyphs":[{"unicode":12288,"advance":1},{"unicode":12289,"advance":1,"planeBounds":{"left":0.0090000000000000375,"top":-0.28125,"right":0.38400000000000001,"bottom":0.11458333333333333},"atlasBounds":{"left":186.5,"top":797.5,"right":204.5,"bottom":816.5}},{"unicode":12290,"advance":1,"planeBounds":{"left":-0.003416666666666659,"top":-0.30208333333333331,"right":0.39241666666666669,"bottom":0.11458333333333333},"atlasBounds":{"left":105.5,"top":797.5,"right":124.5,"bottom":817.5}},{"unicode":12291,"advance":1,"planeBounds":{"left":0.17708333333333331,"top":-0.67708333333333326,"right":0.82291666666666663,"bottom":-0.072916666666666671},"atlasBounds":{"left":407.5,"top":764.5,"right":438.5,"bottom":793.5}},{"unicode":12292,"advance":1,"planeBounds":{"left":-0.010416666666666703,"top":-0.88541666666666663,"right":1.0104166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":0.5,"top":96.5,"right":49.5,"bottom":145.5}},{"unicode":12293,"advance":1,"planeBounds":{"left":0.054333333333333296,"top":-0.84374999999999989,"right":0.8876666666666666,"bottom":0.072916666666666657},"atlasBounds":{"left":495.5,"top":338.5,"right":535.5,"bottom":382.5}},{"unicode":12294,"advance":1,"planeBounds":{"left":0.10624999999999998,"top":-0.82291666666666663,"right":0.91875000000000007,"bottom":0.11458333333333333},"atlasBounds":{"left":666.5,"top":244.5,"right":705.5,"bottom":289.5}},{"unicode":12295,"advance":1,"planeBounds":{"left":-5.3290705182007515e-18,"top":-0.88541666666666663,"right":1,"bottom":0.11458333333333333},"atlasBounds":{"left":492.5,"top":96.5,"right":540.5,"bottom":144.5}},{"unicode":12296,"advance":1,"planeBounds":{"left":0.53883333333333328,"top":-0.90624999999999989,"right":0.99716666666666665,"bottom":0.13541666666666669},"atlasBounds":{"left":407.5,"top":0.5,"right":429.5,"bottom":50.5}},{"unicode":12297,"advance":1,"planeBounds":{"left":0.0028333333333333483,"top":-0.90624999999999989,"right":0.46116666666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":430.5,"top":0.5,"right":452.5,"bottom":50.5}},{"unicode":12298,"advance":1,"planeBounds":{"left":0.48558333333333331,"top":-0.90624999999999989,"right":1.0064166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":453.5,"top":0.5,"right":478.5,"bottom":50.5}},{"unicode":12299,"advance":1,"planeBounds":{"left":-0.0074166666666666556,"top":-0.90624999999999989,"right":0.51341666666666663,"bottom":0.13541666666666669},"atlasBounds":{"left":479.5,"top":0.5,"right":504.5,"bottom":50.5}},{"unicode":12300,"advance":1,"planeBounds":{"left":0.59966666666666668,"top":-0.90625,"right":1.0163333333333333,"bottom":-0.15625},"atlasBounds":{"left":70.5,"top":59.5,"right":90.5,"bottom":95.5}},{"unicode":12301,"advance":1,"planeBounds":{"left":-0.0163333333333333,"top":-0.61458333333333326,"right":0.40033333333333337,"bottom":0.13541666666666669},"atlasBounds":{"left":91.5,"top":59.5,"right":111.5,"bottom":95.5}},{"unicode":12302,"advance":1,"planeBounds":{"left":0.55333333333333323,"top":-0.90625,"right":1.0116666666666667,"bottom":-0.13541666666666666},"atlasBounds":{"left":633.5,"top":686.5,"right":655.5,"bottom":723.5}},{"unicode":12303,"advance":1,"planeBounds":{"left":-0.011666666666666631,"top":-0.63541666666666663,"right":0.44666666666666671,"bottom":0.13541666666666669},"atlasBounds":{"left":656.5,"top":686.5,"right":678.5,"bottom":723.5}},{"unicode":12304,"advance":1,"planeBounds":{"left":0.61808333333333332,"top":-0.90624999999999989,"right":1.0139166666666666,"bottom":0.13541666666666669},"atlasBounds":{"left":528.5,"top":0.5,"right":547.5,"bottom":50.5}},{"unicode":12305,"advance":1,"planeBounds":{"left":-0.013916666666666641,"top":-0.90624999999999989,"right":0.38191666666666668,"bottom":0.13541666666666669},"atlasBounds":{"left":548.5,"top":0.5,"right":567.5,"bottom":50.5}},{"unicode":12306,"advance":1,"planeBounds":{"left":0.11458333333333331,"top":-0.76041666666666663,"right":0.88541666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":818.5,"top":338.5,"right":855.5,"bottom":377.5}},{"unicode":12307,"advance":1,"planeBounds":{"left":0.052083333333333322,"top":-0.82291666666666663,"right":0.94791666666666663,"bottom":0.072916666666666657},"atlasBounds":{"left":812.5,"top":292.5,"right":855.5,"bottom":335.5}},{"unicode":12308,"advance":1,"planeBounds":{"left":0.66983333333333328,"top":-0.90624999999999989,"right":1.0031666666666665,"bottom":0.15625},"atlasBounds":{"left":185.5,"top":0.5,"right":201.5,"bottom":51.5}},{"unicode":12309,"advance":1,"planeBounds":{"left":-0.0031666666666666466,"top":-0.90624999999999989,"right":0.33016666666666672,"bottom":0.15625},"atlasBounds":{"left":202.5,"top":0.5,"right":218.5,"bottom":51.5}},{"unicode":12310,"advance":1,"planeBounds":{"left":0.57574999999999998,"top":-0.90624999999999989,"right":1.01325,"bottom":0.13541666666666669},"atlasBounds":{"left":568.5,"top":0.5,"right":589.5,"bottom":50.5}},{"unicode":12311,"advance":1,"planeBounds":{"left":-0.013249999999999977,"top":-0.90624999999999989,"right":0.42425000000000002,"bottom":0.13541666666666669},"atlasBounds":{"left":590.5,"top":0.5,"right":611.5,"bottom":50.5}},{"unicode":12312,"advance":1,"planeBounds":{"left":0.60966666666666669,"top":-0.92708333333333337,"right":1.0263333333333333,"bottom":0.15625},"atlasBounds":{"left":143.5,"top":0.5,"right":163.5,"bottom":52.5}},{"unicode":12313,"advance":1,"planeBounds":{"left":-0.026333333333333302,"top":-0.92708333333333337,"right":0.39033333333333337,"bottom":0.15625},"atlasBounds":{"left":164.5,"top":0.5,"right":184.5,"bottom":52.5}},{"unicode":12314,"advance":1,"planeBounds":{"left":0.64449999999999996,"top":-0.90624999999999989,"right":1.0195000000000001,"bottom":0.13541666666666669},"atlasBounds":{"left":612.5,"top":0.5,"right":630.5,"bottom":50.5}},{"unicode":12315,"advance":1,"planeBounds":{"left":-0.019499999999999986,"top":-0.90624999999999989,"right":0.35549999999999998,"bottom":0.13541666666666669},"atlasBounds":{"left":631.5,"top":0.5,"right":649.5,"bottom":50.5}},{"unicode":12316,"advance":1,"planeBounds":{"left":-3.552713678800501e-17,"top":-0.57291666666666663,"right":0.99999999999999989,"bottom":-0.19791666666666666},"atlasBounds":{"left":318.5,"top":797.5,"right":366.5,"bottom":815.5}},{"unicode":12317,"advance":1,"planeBounds":{"left":0.624,"top":-0.90625,"right":0.999,"bottom":-0.40625},"atlasBounds":{"left":782.5,"top":827.5,"right":800.5,"bottom":851.5}},{"unicode":12318,"advance":1,"planeBounds":{"left":0.0010000000000000302,"top":-0.90625,"right":0.376,"bottom":-0.40625},"atlasBounds":{"left":801.5,"top":827.5,"right":819.5,"bottom":851.5}},{"unicode":12319,"advance":1,"planeBounds":{"left":0.0010000000000000302,"top":-0.36458333333333326,"right":0.376,"bottom":0.13541666666666669},"atlasBounds":{"left":763.5,"top":827.5,"right":781.5,"bottom":851.5}},{"unicode":12320,"advance":1,"planeBounds":{"left":-0.010416666666666703,"top":-0.78125,"right":1.0104166666666667,"bottom":0.052083333333333336},"atlasBounds":{"left":72.5,"top":604.5,"right":121.5,"bottom":644.5}},{"unicode":12321,"advance":1,"planeBounds":{"left":0.41466666666666663,"top":-0.86458333333333337,"right":0.58133333333333326,"bottom":0.09375},"atlasBounds":{"left":93.5,"top":244.5,"right":101.5,"bottom":290.5}},{"unicode":12322,"advance":1,"planeBounds":{"left":0.28575,"top":-0.86458333333333337,"right":0.72325000000000006,"bottom":0.09375},"atlasBounds":{"left":102.5,"top":244.5,"right":123.5,"bottom":290.5}},{"unicode":12323,"advance":1,"planeBounds":{"left":0.18024999999999997,"top":-0.86458333333333326,"right":0.86775000000000002,"bottom":0.11458333333333333},"atlasBounds":{"left":178.5,"top":195.5,"right":211.5,"bottom":242.5}},{"unicode":12324,"advance":1,"planeBounds":{"left":0.066916666666666638,"top":-0.82291666666666663,"right":0.92108333333333325,"bottom":0.072916666666666657},"atlasBounds":{"left":490.5,"top":429.5,"right":531.5,"bottom":472.5}},{"unicode":12325,"advance":1,"planeBounds":{"left":0.20183333333333331,"top":-0.84374999999999989,"right":0.78516666666666668,"bottom":0.072916666666666657},"atlasBounds":{"left":588.5,"top":338.5,"right":616.5,"bottom":382.5}},{"unicode":12326,"advance":1,"planeBounds":{"left":0.016333333333333293,"top":-0.82291666666666663,"right":0.97466666666666668,"bottom":-0.40625},"atlasBounds":{"left":809.5,"top":686.5,"right":855.5,"bottom":706.5}},{"unicode":12327,"advance":1,"planeBounds":{"left":0.016833333333333308,"top":-0.82291666666666663,"right":0.97516666666666663,"bottom":-0.11458333333333334},"atlasBounds":{"left":749.5,"top":59.5,"right":795.5,"bottom":93.5}},{"unicode":12328,"advance":1,"planeBounds":{"left":0.027249999999999972,"top":-0.84375,"right":0.96474999999999989,"bottom":0.052083333333333336},"atlasBounds":{"left":532.5,"top":429.5,"right":577.5,"bottom":472.5}},{"unicode":12329,"advance":1,"planeBounds":{"left":0.03816666666666664,"top":-0.86458333333333337,"right":0.95483333333333331,"bottom":0.09375},"atlasBounds":{"left":124.5,"top":244.5,"right":168.5,"bottom":290.5}},{"unicode":12330,"advance":0,"planeBounds":{"left":-1.0449166666666667,"top":0.072916666666666685,"right":-0.77408333333333346,"bottom":0.36458333333333331},"atlasBounds":{"left":484.5,"top":797.5,"right":497.5,"bottom":811.5}},{"unicode":12331,"advance":0,"planeBounds":{"left":-1.0449166666666667,"top":-1.1145833333333333,"right":-0.77408333333333346,"bottom":-0.82291666666666663},"atlasBounds":{"left":456.5,"top":797.5,"right":469.5,"bottom":811.5}},{"unicode":12332,"advance":0,"planeBounds":{"left":-0.22591666666666668,"top":-1.1145833333333333,"right":0.04491666666666666,"bottom":-0.82291666666666663},"atlasBounds":{"left":416.5,"top":797.5,"right":429.5,"bottom":811.5}},{"unicode":12333,"advance":0,"planeBounds":{"left":-0.22591666666666668,"top":0.072916666666666685,"right":0.04491666666666666,"bottom":0.36458333333333331},"atlasBounds":{"left":470.5,"top":797.5,"right":483.5,"bottom":811.5}},{"unicode":12334,"advance":0.25,"planeBounds":{"left":0.010416666666666659,"top":-0.51041666666666663,"right":0.23958333333333331,"bottom":-0.26041666666666669},"atlasBounds":{"left":844.5,"top":59.5,"right":855.5,"bottom":71.5}},{"unicode":12335,"advance":0.25,"planeBounds":{"left":0.010416666666666659,"top":-0.61458333333333337,"right":0.23958333333333331,"bottom":-0.15625},"atlasBounds":{"left":56.5,"top":797.5,"right":67.5,"bottom":819.5}},{"unicode":12336,"advance":1,"planeBounds":{"left":-0.052083333333333259,"top":-0.57291666666666663,"right":1.0520833333333333,"bottom":-0.19791666666666666},"atlasBounds":{"left":264.5,"top":797.5,"right":317.5,"bottom":815.5}},{"unicode":12337,"advance":1,"planeBounds":{"left":0.14290151515151517,"top":-1.3854166666666665,"right":0.78873484848484854,"bottom":0.59375},"atlasBounds":{"left":0.5,"top":0.5,"right":31.5,"bottom":95.5}},{"unicode":12338,"advance":1,"planeBounds":{"left":0.13690151515151516,"top":-1.3854166666666665,"right":0.90773484848484853,"bottom":0.59375},"atlasBounds":{"left":32.5,"top":0.5,"right":69.5,"bottom":95.5}},{"unicode":12339,"advance":1,"planeBounds":{"left":0.13623484848484849,"top":-0.88541666666666652,"right":0.74040151515151498,"bottom":0.17708333333333334},"atlasBounds":{"left":219.5,"top":0.5,"right":248.5,"bottom":51.5}},{"unicode":12340,"advance":1,"planeBounds":{"left":0.13690151515151516,"top":-0.88541666666666652,"right":0.90773484848484853,"bottom":0.17708333333333334},"atlasBounds":{"left":249.5,"top":0.5,"right":286.5,"bottom":51.5}},{"unicode":12341,"advance":1,"planeBounds":{"left":0.14658333333333329,"top":-0.92708333333333337,"right":0.79241666666666666,"bottom":0.09375},"atlasBounds":{"left":50.5,"top":96.5,"right":81.5,"bottom":145.5}},{"unicode":12342,"advance":1,"planeBounds":{"left":-0.010416666666666703,"top":-0.88541666666666663,"right":1.0104166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":82.5,"top":96.5,"right":131.5,"bottom":145.5}},{"unicode":12343,"advance":1,"planeBounds":{"left":-0.012416666666666656,"top":-0.88541666666666663,"right":1.0084166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":132.5,"top":96.5,"right":181.5,"bottom":145.5}},{"unicode":12344,"advance":1,"planeBounds":{"left":0.019333333333333293,"top":-0.86458333333333326,"right":0.97766666666666668,"bottom":0.11458333333333333},"atlasBounds":{"left":212.5,"top":195.5,"right":258.5,"bottom":242.5}},{"unicode":12345,"advance":1,"planeBounds":{"left":0.019333333333333293,"top":-0.86458333333333326,"right":0.97766666666666668,"bottom":0.11458333333333333},"atlasBounds":{"left":259.5,"top":195.5,"right":305.5,"bottom":242.5}},{"unicode":12346,"advance":1,"planeBounds":{"left":0.019333333333333293,"top":-0.86458333333333326,"right":0.97766666666666668,"bottom":0.13541666666666669},"atlasBounds":{"left":647.5,"top":96.5,"right":693.5,"bottom":144.5}},{"unicode":12347,"advance":1,"planeBounds":{"left":0.1953333333333333,"top":-0.88541666666666663,"right":0.77866666666666662,"bottom":0.13541666666666669},"atlasBounds":{"left":182.5,"top":96.5,"right":210.5,"bottom":145.5}},{"unicode":12348,"advance":1,"planeBounds":{"left":0.062499999999999951,"top":-0.82291666666666663,"right":0.93749999999999989,"bottom":0.052083333333333336},"atlasBounds":{"left":104.5,"top":518.5,"right":146.5,"bottom":560.5}},{"unicode":12349,"advance":1,"planeBounds":{"left":0.035659090909090842,"top":-0.76041666666666663,"right":0.97315909090909081,"bottom":0.03125},"atlasBounds":{"left":221.5,"top":686.5,"right":266.5,"bottom":724.5}},{"unicode":12350,"advance":1,"planeBounds":{"left":-4.085620730620576e-17,"top":-0.88541666666666663,"right":0.99999999999999989,"bottom":0.11458333333333333},"atlasBounds":{"left":694.5,"top":96.5,"right":742.5,"bottom":144.5}},{"unicode":12351,"advance":1,"planeBounds":{"left":0.19791666666666663,"top":-0.82291666666666663,"right":0.80208333333333326,"bottom":0.072916666666666657},"atlasBounds":{"left":629.5,"top":429.5,"right":658.5,"bottom":472.5}},{"unicode":12353,"advance":1,"planeBounds":{"left":0.1300833333333333,"top":-0.71875,"right":0.9009166666666667,"bottom":0.09375},"atlasBounds":{"left":450.5,"top":645.5,"right":487.5,"bottom":684.5}},{"unicode":12354,"advance":1,"planeBounds":{"left":0.059083333333333321,"top":-0.84375,"right":0.95491666666666664,"bottom":0.09375},"atlasBounds":{"left":706.5,"top":244.5,"right":749.5,"bottom":289.5}},{"unicode":12355,"advance":1,"planeBounds":{"left":0.13308333333333328,"top":-0.61458333333333326,"right":0.9039166666666667,"bottom":0.072916666666666657},"atlasBounds":{"left":646.5,"top":726.5,"right":683.5,"bottom":759.5}},{"unicode":12356,"advance":1,"planeBounds":{"left":0.075083333333333308,"top":-0.76041666666666663,"right":0.97091666666666665,"bottom":0.052083333333333336},"atlasBounds":{"left":488.5,"top":645.5,"right":531.5,"bottom":684.5}},{"unicode":12357,"advance":1,"planeBounds":{"left":0.19549999999999998,"top":-0.69791666666666663,"right":0.82050000000000001,"bottom":0.11458333333333333},"atlasBounds":{"left":532.5,"top":645.5,"right":562.5,"bottom":684.5}},{"unicode":12358,"advance":1,"planeBounds":{"left":0.12091666666666664,"top":-0.84375,"right":0.8500833333333333,"bottom":0.09375},"atlasBounds":{"left":750.5,"top":244.5,"right":785.5,"bottom":289.5}},{"unicode":12359,"advance":1,"planeBounds":{"left":0.14049999999999996,"top":-0.69791666666666663,"right":0.89049999999999996,"bottom":0.09375},"atlasBounds":{"left":394.5,"top":686.5,"right":430.5,"bottom":724.5}},{"unicode":12360,"advance":1,"planeBounds":{"left":0.064083333333333325,"top":-0.84375,"right":0.95991666666666664,"bottom":0.09375},"atlasBounds":{"left":786.5,"top":244.5,"right":829.5,"bottom":289.5}},{"unicode":12361,"advance":1,"planeBounds":{"left":0.12174999999999996,"top":-0.67708333333333326,"right":0.93424999999999991,"bottom":0.11458333333333333},"atlasBounds":{"left":431.5,"top":686.5,"right":470.5,"bottom":724.5}},{"unicode":12362,"advance":1,"planeBounds":{"left":0.058749999999999948,"top":-0.84375,"right":0.99624999999999986,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":292.5,"right":45.5,"bottom":337.5}},{"unicode":12363,"advance":1,"planeBounds":{"left":0.03191666666666667,"top":-0.84374999999999989,"right":1.0110833333333333,"bottom":0.072916666666666657},"atlasBounds":{"left":638.5,"top":338.5,"right":685.5,"bottom":382.5}},{"unicode":12364,"advance":1,"planeBounds":{"left":0.019,"top":-0.90625,"right":1.0190000000000001,"bottom":0.09375},"atlasBounds":{"left":743.5,"top":96.5,"right":791.5,"bottom":144.5}},{"unicode":12365,"advance":1,"planeBounds":{"left":0.11366666666666668,"top":-0.84375,"right":0.90533333333333332,"bottom":0.09375},"atlasBounds":{"left":46.5,"top":292.5,"right":84.5,"bottom":337.5}},{"unicode":12366,"advance":1,"planeBounds":{"left":0.076666666666666647,"top":-0.88541666666666663,"right":0.99333333333333329,"bottom":0.09375},"atlasBounds":{"left":306.5,"top":195.5,"right":350.5,"bottom":242.5}},{"unicode":12367,"advance":1,"planeBounds":{"left":0.15277419354838706,"top":-0.86458333333333326,"right":0.77777419354838706,"bottom":0.11458333333333333},"atlasBounds":{"left":351.5,"top":195.5,"right":381.5,"bottom":242.5}},{"unicode":12368,"advance":1,"planeBounds":{"left":0.14268333333333333,"top":-0.86458333333333326,"right":0.89268333333333327,"bottom":0.11458333333333333},"atlasBounds":{"left":382.5,"top":195.5,"right":418.5,"bottom":242.5}},{"unicode":12369,"advance":1,"planeBounds":{"left":0.072583333333333319,"top":-0.82291666666666663,"right":0.9684166666666667,"bottom":0.11458333333333333},"atlasBounds":{"left":85.5,"top":292.5,"right":128.5,"bottom":337.5}},{"unicode":12370,"advance":1,"planeBounds":{"left":0.06433333333333334,"top":-0.86458333333333326,"right":1.0226666666666666,"bottom":0.13541666666666669},"atlasBounds":{"left":792.5,"top":96.5,"right":838.5,"bottom":144.5}},{"unicode":12371,"advance":1,"planeBounds":{"left":0.12899999999999998,"top":-0.76041666666666663,"right":0.87899999999999989,"bottom":0.072916666666666657},"atlasBounds":{"left":431.5,"top":604.5,"right":467.5,"bottom":644.5}},{"unicode":12372,"advance":1,"planeBounds":{"left":0.10616666666666666,"top":-0.88541666666666663,"right":1.0228333333333333,"bottom":0.09375},"atlasBounds":{"left":419.5,"top":195.5,"right":463.5,"bottom":242.5}},{"unicode":12373,"advance":1,"planeBounds":{"left":0.11416666666666662,"top":-0.84375,"right":0.90583333333333327,"bottom":0.09375},"atlasBounds":{"left":153.5,"top":292.5,"right":191.5,"bottom":337.5}},{"unicode":12374,"advance":1,"planeBounds":{"left":0.09866666666666668,"top":-0.90625,"right":1.0153333333333332,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":146.5,"right":44.5,"bottom":194.5}},{"unicode":12375,"advance":1,"planeBounds":{"left":0.19249999999999995,"top":-0.82291666666666663,"right":0.94249999999999989,"bottom":0.09375},"atlasBounds":{"left":781.5,"top":338.5,"right":817.5,"bottom":382.5}},{"unicode":12376,"advance":1,"planeBounds":{"left":0.17949999999999994,"top":-0.82291666666666663,"right":0.92949999999999988,"bottom":0.11458333333333333},"atlasBounds":{"left":192.5,"top":292.5,"right":228.5,"bottom":337.5}},{"unicode":12377,"advance":1,"planeBounds":{"left":0.054166666666666606,"top":-0.84375,"right":0.97083333333333333,"bottom":0.11458333333333333},"atlasBounds":{"left":169.5,"top":244.5,"right":213.5,"bottom":290.5}},{"unicode":12378,"advance":1,"planeBounds":{"left":0.020833333333333315,"top":-0.88541666666666663,"right":0.97916666666666663,"bottom":0.11458333333333333},"atlasBounds":{"left":45.5,"top":146.5,"right":91.5,"bottom":194.5}},{"unicode":12379,"advance":1,"planeBounds":{"left":-0.0060833333333333659,"top":-0.82291666666666663,"right":0.9730833333333333,"bottom":0.072916666666666657},"atlasBounds":{"left":0.5,"top":474.5,"right":47.5,"bottom":517.5}},{"unicode":12380,"advance":1,"planeBounds":{"left":-0.024333333333333335,"top":-0.84375,"right":1.0173333333333332,"bottom":0.09375},"atlasBounds":{"left":805.5,"top":146.5,"right":855.5,"bottom":191.5}},{"unicode":12381,"advance":1,"planeBounds":{"left":0.051999999999999956,"top":-0.82291666666666663,"right":0.92699999999999994,"bottom":0.09375},"atlasBounds":{"left":31.5,"top":384.5,"right":73.5,"bottom":428.5}},{"unicode":12382,"advance":1,"planeBounds":{"left":0.023250000000000007,"top":-0.82291666666666663,"right":0.96074999999999999,"bottom":0.09375},"atlasBounds":{"left":74.5,"top":384.5,"right":119.5,"bottom":428.5}},{"unicode":12383,"advance":1,"planeBounds":{"left":0.056583333333333312,"top":-0.84375,"right":0.95241666666666669,"bottom":0.09375},"atlasBounds":{"left":253.5,"top":292.5,"right":296.5,"bottom":337.5}},{"unicode":12384,"advance":1,"planeBounds":{"left":0.02091666666666667,"top":-0.84375,"right":1.0000833333333332,"bottom":0.09375},"atlasBounds":{"left":297.5,"top":292.5,"right":344.5,"bottom":337.5}},{"unicode":12385,"advance":1,"planeBounds":{"left":0.06433333333333334,"top":-0.84375,"right":0.89766666666666661,"bottom":0.09375},"atlasBounds":{"left":345.5,"top":292.5,"right":385.5,"bottom":337.5}},{"unicode":12386,"advance":1,"planeBounds":{"left":0.030916666666666637,"top":-0.90625,"right":1.0100833333333332,"bottom":0.09375},"atlasBounds":{"left":92.5,"top":146.5,"right":139.5,"bottom":194.5}},{"unicode":12387,"advance":1,"planeBounds":{"left":0.10966666666666668,"top":-0.55208333333333326,"right":0.90133333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":329.5,"top":764.5,"right":367.5,"bottom":794.5}},{"unicode":12388,"advance":1,"planeBounds":{"left":0.021749999999999985,"top":-0.69791666666666663,"right":0.95925000000000005,"bottom":0.03125},"atlasBounds":{"left":352.5,"top":59.5,"right":397.5,"bottom":94.5}},{"unicode":12389,"advance":1,"planeBounds":{"left":0.01541666666666662,"top":-0.88541666666666663,"right":0.99458333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":386.5,"top":292.5,"right":433.5,"bottom":337.5}},{"unicode":12390,"advance":1,"planeBounds":{"left":0.035583333333333335,"top":-0.78124999999999989,"right":0.93141666666666667,"bottom":0.072916666666666657},"atlasBounds":{"left":310.5,"top":561.5,"right":353.5,"bottom":602.5}},{"unicode":12391,"advance":1,"planeBounds":{"left":0.034749999999999948,"top":-0.78125,"right":0.97224999999999995,"bottom":0.09375},"atlasBounds":{"left":235.5,"top":518.5,"right":280.5,"bottom":560.5}},{"unicode":12392,"advance":1,"planeBounds":{"left":0.15641666666666665,"top":-0.82291666666666663,"right":0.88558333333333328,"bottom":0.072916666666666657},"atlasBounds":{"left":820.5,"top":429.5,"right":855.5,"bottom":472.5}},{"unicode":12393,"advance":1,"planeBounds":{"left":0.12508333333333332,"top":-0.86458333333333337,"right":1.0209166666666667,"bottom":0.09375},"atlasBounds":{"left":214.5,"top":244.5,"right":257.5,"bottom":290.5}},{"unicode":12394,"advance":1,"planeBounds":{"left":0.044249999999999963,"top":-0.84375,"right":0.9817499999999999,"bottom":0.09375},"atlasBounds":{"left":434.5,"top":292.5,"right":479.5,"bottom":337.5}},{"unicode":12395,"advance":1,"planeBounds":{"left":0.073999999999999969,"top":-0.80208333333333337,"right":0.94899999999999995,"bottom":0.09375},"atlasBounds":{"left":132.5,"top":474.5,"right":174.5,"bottom":517.5}},{"unicode":12396,"advance":1,"planeBounds":{"left":0.030916666666666672,"top":-0.84374999999999989,"right":1.0100833333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":177.5,"top":384.5,"right":224.5,"bottom":428.5}},{"unicode":12397,"advance":1,"planeBounds":{"left":0.0035833333333333377,"top":-0.84375,"right":1.0244166666666668,"bottom":0.09375},"atlasBounds":{"left":480.5,"top":292.5,"right":529.5,"bottom":337.5}},{"unicode":12398,"advance":1,"planeBounds":{"left":0.039666666666666663,"top":-0.76041666666666663,"right":0.95633333333333326,"bottom":0.072916666666666657},"atlasBounds":{"left":545.5,"top":604.5,"right":589.5,"bottom":644.5}},{"unicode":12399,"advance":1,"planeBounds":{"left":0.063750000000000001,"top":-0.82291666666666663,"right":1.00125,"bottom":0.09375},"atlasBounds":{"left":225.5,"top":384.5,"right":270.5,"bottom":428.5}},{"unicode":12400,"advance":1,"planeBounds":{"left":0.041916666666666637,"top":-0.88541666666666663,"right":1.0210833333333333,"bottom":0.09375},"atlasBounds":{"left":464.5,"top":195.5,"right":511.5,"bottom":242.5}},{"unicode":12401,"advance":1,"planeBounds":{"left":0.043916666666666639,"top":-0.88541666666666663,"right":1.0230833333333333,"bottom":0.09375},"atlasBounds":{"left":512.5,"top":195.5,"right":559.5,"bottom":242.5}},{"unicode":12402,"advance":1,"planeBounds":{"left":0.058749999999999948,"top":-0.78125,"right":0.99624999999999986,"bottom":0.09375},"atlasBounds":{"left":422.5,"top":518.5,"right":467.5,"bottom":560.5}},{"unicode":12403,"advance":1,"planeBounds":{"left":0.042916666666666638,"top":-0.86458333333333326,"right":1.0220833333333332,"bottom":0.11458333333333333},"atlasBounds":{"left":560.5,"top":195.5,"right":607.5,"bottom":242.5}},{"unicode":12404,"advance":1,"planeBounds":{"left":0.045916666666666675,"top":-0.86458333333333326,"right":1.0250833333333333,"bottom":0.11458333333333333},"atlasBounds":{"left":608.5,"top":195.5,"right":655.5,"bottom":242.5}},{"unicode":12405,"advance":1,"planeBounds":{"left":-0.0084166666666667025,"top":-0.82291666666666663,"right":1.0124166666666667,"bottom":0.072916666666666657},"atlasBounds":{"left":175.5,"top":474.5,"right":224.5,"bottom":517.5}},{"unicode":12406,"advance":1,"planeBounds":{"left":-0.030833333333333365,"top":-0.82291666666666663,"right":1.0108333333333333,"bottom":0.09375},"atlasBounds":{"left":271.5,"top":384.5,"right":321.5,"bottom":428.5}},{"unicode":12407,"advance":1,"planeBounds":{"left":-0.03133333333333338,"top":-0.82291666666666663,"right":1.0103333333333333,"bottom":0.09375},"atlasBounds":{"left":322.5,"top":384.5,"right":372.5,"bottom":428.5}},{"unicode":12408,"advance":1,"planeBounds":{"left":0.013916666666666678,"top":-0.69791666666666663,"right":0.99308333333333332,"bottom":0.010416666666666666},"atlasBounds":{"left":796.5,"top":59.5,"right":843.5,"bottom":93.5}},{"unicode":12409,"advance":1,"planeBounds":{"left":0.0049166666666666768,"top":-0.78125,"right":0.98408333333333331,"bottom":0.03125},"atlasBounds":{"left":705.5,"top":645.5,"right":752.5,"bottom":684.5}},{"unicode":12410,"advance":1,"planeBounds":{"left":0.0049166666666666768,"top":-0.76041666666666663,"right":0.98408333333333331,"bottom":0.03125},"atlasBounds":{"left":267.5,"top":686.5,"right":314.5,"bottom":724.5}},{"unicode":12411,"advance":1,"planeBounds":{"left":0.072166666666666657,"top":-0.82291666666666663,"right":0.98883333333333323,"bottom":0.09375},"atlasBounds":{"left":373.5,"top":384.5,"right":417.5,"bottom":428.5}},{"unicode":12412,"advance":1,"planeBounds":{"left":0.043416666666666659,"top":-0.86458333333333337,"right":1.0225833333333332,"bottom":0.09375},"atlasBounds":{"left":258.5,"top":244.5,"right":305.5,"bottom":290.5}},{"unicode":12413,"advance":1,"planeBounds":{"left":0.038500000000000048,"top":-0.90625,"right":1.0385,"bottom":0.09375},"atlasBounds":{"left":175.5,"top":146.5,"right":223.5,"bottom":194.5}},{"unicode":12414,"advance":1,"planeBounds":{"left":0.13858333333333325,"top":-0.84375,"right":0.90941666666666665,"bottom":0.09375},"atlasBounds":{"left":530.5,"top":292.5,"right":567.5,"bottom":337.5}},{"unicode":12415,"advance":1,"planeBounds":{"left":0.020416666666666614,"top":-0.80208333333333326,"right":0.99958333333333327,"bottom":0.11458333333333333},"atlasBounds":{"left":418.5,"top":384.5,"right":465.5,"bottom":428.5}},{"unicode":12416,"advance":1,"planeBounds":{"left":0.048666666666666629,"top":-0.84375,"right":0.96533333333333327,"bottom":0.09375},"atlasBounds":{"left":568.5,"top":292.5,"right":612.5,"bottom":337.5}},{"unicode":12417,"advance":1,"planeBounds":{"left":0.037666666666666668,"top":-0.84375,"right":0.95433333333333326,"bottom":0.09375},"atlasBounds":{"left":613.5,"top":292.5,"right":657.5,"bottom":337.5}},{"unicode":12418,"advance":1,"planeBounds":{"left":0.044499999999999977,"top":-0.84375,"right":0.91949999999999998,"bottom":0.09375},"atlasBounds":{"left":658.5,"top":292.5,"right":700.5,"bottom":337.5}},{"unicode":12419,"advance":1,"planeBounds":{"left":0.070249999999999951,"top":-0.67708333333333326,"right":0.88274999999999992,"bottom":0.13541666666666669},"atlasBounds":{"left":0.5,"top":686.5,"right":39.5,"bottom":725.5}},{"unicode":12420,"advance":1,"planeBounds":{"left":0.017749999999999957,"top":-0.86458333333333337,"right":0.95524999999999993,"bottom":0.09375},"atlasBounds":{"left":306.5,"top":244.5,"right":351.5,"bottom":290.5}},{"unicode":12421,"advance":1,"planeBounds":{"left":0.151,"top":-0.67708333333333326,"right":0.90100000000000002,"bottom":0.13541666666666669},"atlasBounds":{"left":753.5,"top":645.5,"right":789.5,"bottom":684.5}},{"unicode":12422,"advance":1,"planeBounds":{"left":0.081999999999999976,"top":-0.84375,"right":0.95699999999999985,"bottom":0.11458333333333333},"atlasBounds":{"left":352.5,"top":244.5,"right":394.5,"bottom":290.5}},{"unicode":12423,"advance":1,"planeBounds":{"left":0.14774999999999999,"top":-0.67708333333333326,"right":0.83525000000000005,"bottom":0.11458333333333333},"atlasBounds":{"left":511.5,"top":686.5,"right":544.5,"bottom":724.5}},{"unicode":12424,"advance":1,"planeBounds":{"left":0.084249999999999992,"top":-0.84375,"right":0.89675000000000005,"bottom":0.09375},"atlasBounds":{"left":701.5,"top":292.5,"right":740.5,"bottom":337.5}},{"unicode":12425,"advance":1,"planeBounds":{"left":0.12958333333333333,"top":-0.84375,"right":0.90041666666666664,"bottom":0.09375},"atlasBounds":{"left":741.5,"top":292.5,"right":778.5,"bottom":337.5}},{"unicode":12426,"advance":1,"planeBounds":{"left":0.16866666666666663,"top":-0.84375,"right":0.83533333333333326,"bottom":0.09375},"atlasBounds":{"left":779.5,"top":292.5,"right":811.5,"bottom":337.5}},{"unicode":12427,"advance":1,"planeBounds":{"left":0.067333333333333301,"top":-0.80208333333333337,"right":0.90066666666666662,"bottom":0.09375},"atlasBounds":{"left":225.5,"top":474.5,"right":265.5,"bottom":517.5}},{"unicode":12428,"advance":1,"planeBounds":{"left":0.0035833333333332986,"top":-0.84375,"right":1.0244166666666668,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":338.5,"right":49.5,"bottom":383.5}},{"unicode":12429,"advance":1,"planeBounds":{"left":0.063333333333333297,"top":-0.80208333333333337,"right":0.89666666666666661,"bottom":0.09375},"atlasBounds":{"left":286.5,"top":474.5,"right":326.5,"bottom":517.5}},{"unicode":12430,"advance":1,"planeBounds":{"left":0.10024999999999999,"top":-0.69791666666666663,"right":0.91275000000000006,"bottom":0.09375},"atlasBounds":{"left":141.5,"top":686.5,"right":180.5,"bottom":724.5}},{"unicode":12431,"advance":1,"planeBounds":{"left":0.012833333333333316,"top":-0.84375,"right":0.97116666666666662,"bottom":0.09375},"atlasBounds":{"left":50.5,"top":338.5,"right":96.5,"bottom":383.5}},{"unicode":12432,"advance":1,"planeBounds":{"left":0.041583333333333299,"top":-0.82291666666666663,"right":0.93741666666666668,"bottom":0.09375},"atlasBounds":{"left":466.5,"top":384.5,"right":509.5,"bottom":428.5}},{"unicode":12433,"advance":1,"planeBounds":{"left":0.015333333333333334,"top":-0.82291666666666663,"right":0.97366666666666668,"bottom":0.09375},"atlasBounds":{"left":510.5,"top":384.5,"right":556.5,"bottom":428.5}},{"unicode":12434,"advance":1,"planeBounds":{"left":0.050999999999999969,"top":-0.84375,"right":0.92599999999999993,"bottom":0.09375},"atlasBounds":{"left":97.5,"top":338.5,"right":139.5,"bottom":383.5}},{"unicode":12435,"advance":1,"planeBounds":{"left":0.032333333333333339,"top":-0.82291666666666663,"right":0.9906666666666667,"bottom":0.09375},"atlasBounds":{"left":557.5,"top":384.5,"right":603.5,"bottom":428.5}},{"unicode":12436,"advance":1,"planeBounds":{"left":0.088666666666666644,"top":-0.84375,"right":1.0053333333333332,"bottom":0.09375},"atlasBounds":{"left":140.5,"top":338.5,"right":184.5,"bottom":383.5}},{"unicode":12437,"advance":1,"planeBounds":{"left":0.1183333333333333,"top":-0.67708333333333326,"right":0.95166666666666666,"bottom":0.11458333333333333},"atlasBounds":{"left":315.5,"top":686.5,"right":355.5,"bottom":724.5}},{"unicode":12438,"advance":1,"planeBounds":{"left":0.14008333333333331,"top":-0.65624999999999989,"right":0.9109166666666666,"bottom":0.13541666666666669},"atlasBounds":{"left":356.5,"top":686.5,"right":393.5,"bottom":724.5}},{"unicode":12441,"advance":0,"planeBounds":{"left":-0.31016666666666665,"top":-0.92708333333333326,"right":0.023166666666666686,"bottom":-0.61458333333333326},"atlasBounds":{"left":839.5,"top":113.5,"right":855.5,"bottom":128.5}},{"unicode":12442,"advance":0,"planeBounds":{"left":-0.27775,"top":-0.92708333333333337,"right":0.034749999999999941,"bottom":-0.59375},"atlasBounds":{"left":832.5,"top":225.5,"right":847.5,"bottom":241.5}},{"unicode":12443,"advance":1,"planeBounds":{"left":-0.033999999999999968,"top":-0.88541666666666663,"right":0.34100000000000003,"bottom":-0.55208333333333326},"atlasBounds":{"left":825.5,"top":707.5,"right":843.5,"bottom":723.5}},{"unicode":12444,"advance":1,"planeBounds":{"left":-0.016249999999999973,"top":-0.86458333333333337,"right":0.29625000000000001,"bottom":-0.53125},"atlasBounds":{"left":809.5,"top":707.5,"right":824.5,"bottom":723.5}},{"unicode":12445,"advance":1,"planeBounds":{"left":0.27758333333333329,"top":-0.69791666666666663,"right":0.79841666666666666,"bottom":-0.010416666666666666},"atlasBounds":{"left":559.5,"top":726.5,"right":584.5,"bottom":759.5}},{"unicode":12446,"advance":1,"planeBounds":{"left":0.23299999999999996,"top":-0.80208333333333326,"right":0.85799999999999987,"bottom":0.010416666666666666},"atlasBounds":{"left":790.5,"top":645.5,"right":820.5,"bottom":684.5}},{"unicode":12447,"advance":1,"planeBounds":{"left":0.029499999999999978,"top":-0.88541666666666663,"right":0.90449999999999997,"bottom":0.11458333333333333},"atlasBounds":{"left":266.5,"top":146.5,"right":308.5,"bottom":194.5}},{"unicode":12448,"advance":1,"planeBounds":{"left":0.23958333333333331,"top":-0.53125,"right":0.76041666666666663,"bottom":-0.23958333333333331},"atlasBounds":{"left":430.5,"top":797.5,"right":455.5,"bottom":811.5}},{"unicode":12449,"advance":1,"planeBounds":{"left":0.12516666666666662,"top":-0.59374999999999989,"right":0.91683333333333328,"bottom":0.13541666666666669},"atlasBounds":{"left":398.5,"top":59.5,"right":436.5,"bottom":94.5}},{"unicode":12450,"advance":1,"planeBounds":{"left":0.079583333333333311,"top":-0.78125,"right":0.9754166666666666,"bottom":0.09375},"atlasBounds":{"left":614.5,"top":518.5,"right":657.5,"bottom":560.5}},{"unicode":12451,"advance":1,"planeBounds":{"left":0.077000000000000013,"top":-0.65624999999999989,"right":0.82700000000000007,"bottom":0.13541666666666669},"atlasBounds":{"left":596.5,"top":686.5,"right":632.5,"bottom":724.5}},{"unicode":12452,"advance":1,"planeBounds":{"left":0.036999999999999991,"top":-0.84374999999999989,"right":0.91200000000000003,"bottom":0.072916666666666657},"atlasBounds":{"left":629.5,"top":384.5,"right":671.5,"bottom":428.5}},{"unicode":12453,"advance":1,"planeBounds":{"left":0.14491666666666664,"top":-0.67708333333333326,"right":0.87408333333333332,"bottom":0.13541666666666669},"atlasBounds":{"left":600.5,"top":645.5,"right":635.5,"bottom":684.5}},{"unicode":12454,"advance":1,"planeBounds":{"left":0.092333333333333351,"top":-0.84375,"right":0.92566666666666664,"bottom":0.09375},"atlasBounds":{"left":185.5,"top":338.5,"right":225.5,"bottom":383.5}},{"unicode":12455,"advance":1,"planeBounds":{"left":0.10516666666666662,"top":-0.57291666666666663,"right":0.89683333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":368.5,"top":764.5,"right":406.5,"bottom":794.5}},{"unicode":12456,"advance":1,"planeBounds":{"left":0.041666666666666664,"top":-0.71874999999999989,"right":0.95833333333333326,"bottom":0.010416666666666666},"atlasBounds":{"left":437.5,"top":59.5,"right":481.5,"bottom":94.5}},{"unicode":12457,"advance":1,"planeBounds":{"left":0.13158333333333333,"top":-0.65625,"right":0.90241666666666664,"bottom":0.11458333333333333},"atlasBounds":{"left":74.5,"top":726.5,"right":111.5,"bottom":763.5}},{"unicode":12458,"advance":1,"planeBounds":{"left":0.044083333333333322,"top":-0.84374999999999989,"right":0.93991666666666662,"bottom":0.072916666666666657},"atlasBounds":{"left":672.5,"top":384.5,"right":715.5,"bottom":428.5}},{"unicode":12459,"advance":1,"planeBounds":{"left":0.06533333333333334,"top":-0.84374999999999989,"right":0.89866666666666661,"bottom":0.072916666666666657},"atlasBounds":{"left":716.5,"top":384.5,"right":756.5,"bottom":428.5}},{"unicode":12460,"advance":1,"planeBounds":{"left":0.038333333333333296,"top":-0.88541666666666663,"right":0.9966666666666667,"bottom":0.09375},"atlasBounds":{"left":696.5,"top":195.5,"right":742.5,"bottom":242.5}},{"unicode":12461,"advance":1,"planeBounds":{"left":0.064999999999999974,"top":-0.84375,"right":0.93999999999999995,"bottom":0.09375},"atlasBounds":{"left":226.5,"top":338.5,"right":268.5,"bottom":383.5}},{"unicode":12462,"advance":1,"planeBounds":{"left":0.037333333333333295,"top":-0.90625,"right":0.9956666666666667,"bottom":0.11458333333333333},"atlasBounds":{"left":211.5,"top":96.5,"right":257.5,"bottom":145.5}},{"unicode":12463,"advance":1,"planeBounds":{"left":0.047999999999999994,"top":-0.86458333333333337,"right":0.92300000000000004,"bottom":0.09375},"atlasBounds":{"left":395.5,"top":244.5,"right":437.5,"bottom":290.5}},{"unicode":12464,"advance":1,"planeBounds":{"left":0.0084999999999999763,"top":-0.88541666666666663,"right":1.0085,"bottom":0.11458333333333333},"atlasBounds":{"left":309.5,"top":146.5,"right":357.5,"bottom":194.5}},{"unicode":12465,"advance":1,"planeBounds":{"left":0.021749999999999953,"top":-0.84375,"right":0.95924999999999994,"bottom":0.09375},"atlasBounds":{"left":269.5,"top":338.5,"right":314.5,"bottom":383.5}},{"unicode":12466,"advance":1,"planeBounds":{"left":0.0029999999999999949,"top":-0.88541666666666663,"right":1.0030000000000001,"bottom":0.11458333333333333},"atlasBounds":{"left":358.5,"top":146.5,"right":406.5,"bottom":194.5}},{"unicode":12467,"advance":1,"planeBounds":{"left":0.10816666666666666,"top":-0.73958333333333326,"right":0.89983333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":80.5,"top":686.5,"right":118.5,"bottom":724.5}},{"unicode":12468,"advance":1,"planeBounds":{"left":0.09358333333333331,"top":-0.90625,"right":0.98941666666666661,"bottom":0.09375},"atlasBounds":{"left":407.5,"top":146.5,"right":450.5,"bottom":194.5}},{"unicode":12469,"advance":1,"planeBounds":{"left":0.015333333333333327,"top":-0.82291666666666663,"right":0.97366666666666668,"bottom":0.09375},"atlasBounds":{"left":782.5,"top":384.5,"right":828.5,"bottom":428.5}},{"unicode":12470,"advance":1,"planeBounds":{"left":0.0024999999999999432,"top":-0.84375,"right":1.0024999999999999,"bottom":0.11458333333333333},"atlasBounds":{"left":438.5,"top":244.5,"right":486.5,"bottom":290.5}},{"unicode":12471,"advance":1,"planeBounds":{"left":0.061583333333333309,"top":-0.82291666666666663,"right":0.95741666666666669,"bottom":0.072916666666666657},"atlasBounds":{"left":327.5,"top":474.5,"right":370.5,"bottom":517.5}},{"unicode":12472,"advance":1,"planeBounds":{"left":0.049249999999999974,"top":-0.84375,"right":0.9867499999999999,"bottom":0.09375},"atlasBounds":{"left":315.5,"top":338.5,"right":360.5,"bottom":383.5}},{"unicode":12473,"advance":1,"planeBounds":{"left":0.051583333333333301,"top":-0.76041666666666663,"right":0.94741666666666668,"bottom":0.072916666666666657},"atlasBounds":{"left":740.5,"top":561.5,"right":783.5,"bottom":601.5}},{"unicode":12474,"advance":1,"planeBounds":{"left":0.037833333333333316,"top":-0.90625,"right":0.99616666666666664,"bottom":0.09375},"atlasBounds":{"left":451.5,"top":146.5,"right":497.5,"bottom":194.5}},{"unicode":12475,"advance":1,"planeBounds":{"left":0.014666666666666628,"top":-0.82291666666666663,"right":0.93133333333333324,"bottom":0.072916666666666657},"atlasBounds":{"left":371.5,"top":474.5,"right":415.5,"bottom":517.5}},{"unicode":12476,"advance":1,"planeBounds":{"left":0.0014999999999999805,"top":-0.88541666666666663,"right":1.0015000000000001,"bottom":0.09375},"atlasBounds":{"left":768.5,"top":195.5,"right":816.5,"bottom":242.5}},{"unicode":12477,"advance":1,"planeBounds":{"left":0.080750000000000002,"top":-0.78124999999999989,"right":0.89324999999999999,"bottom":0.072916666666666657},"atlasBounds":{"left":554.5,"top":561.5,"right":593.5,"bottom":602.5}},{"unicode":12478,"advance":1,"planeBounds":{"left":0.058749999999999997,"top":-0.90625,"right":0.99624999999999997,"bottom":0.09375},"atlasBounds":{"left":498.5,"top":146.5,"right":543.5,"bottom":194.5}},{"unicode":12479,"advance":1,"planeBounds":{"left":0.044999999999999991,"top":-0.86458333333333337,"right":0.92000000000000004,"bottom":0.09375},"atlasBounds":{"left":487.5,"top":244.5,"right":529.5,"bottom":290.5}},{"unicode":12480,"advance":1,"planeBounds":{"left":0.0094999999999999373,"top":-0.90625,"right":1.0094999999999998,"bottom":0.11458333333333333},"atlasBounds":{"left":258.5,"top":96.5,"right":306.5,"bottom":145.5}},{"unicode":12481,"advance":1,"planeBounds":{"left":0.042166666666666609,"top":-0.82291666666666663,"right":0.95883333333333332,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":429.5,"right":44.5,"bottom":473.5}},{"unicode":12482,"advance":1,"planeBounds":{"left":0.013916666666666673,"top":-0.80208333333333326,"right":0.99308333333333332,"bottom":0.11458333333333333},"atlasBounds":{"left":45.5,"top":429.5,"right":92.5,"bottom":473.5}},{"unicode":12483,"advance":1,"planeBounds":{"left":0.12558333333333335,"top":-0.63541666666666663,"right":0.89641666666666675,"bottom":0.13541666666666669},"atlasBounds":{"left":112.5,"top":726.5,"right":149.5,"bottom":763.5}},{"unicode":12484,"advance":1,"planeBounds":{"left":0.051083333333333279,"top":-0.80208333333333337,"right":0.94691666666666663,"bottom":0.09375},"atlasBounds":{"left":416.5,"top":474.5,"right":459.5,"bottom":517.5}},{"unicode":12485,"advance":1,"planeBounds":{"left":0.028916666666666636,"top":-0.90625,"right":1.0080833333333332,"bottom":0.11458333333333333},"atlasBounds":{"left":307.5,"top":96.5,"right":354.5,"bottom":145.5}},{"unicode":12486,"advance":1,"planeBounds":{"left":0.046666666666666662,"top":-0.80208333333333337,"right":0.96333333333333326,"bottom":0.09375},"atlasBounds":{"left":460.5,"top":474.5,"right":504.5,"bottom":517.5}},{"unicode":12487,"advance":1,"planeBounds":{"left":0.042416666666666658,"top":-0.90625,"right":1.0215833333333333,"bottom":0.11458333333333333},"atlasBounds":{"left":355.5,"top":96.5,"right":402.5,"bottom":145.5}},{"unicode":12488,"advance":1,"planeBounds":{"left":0.28591666666666665,"top":-0.82291666666666663,"right":0.89008333333333323,"bottom":0.072916666666666657},"atlasBounds":{"left":531.5,"top":474.5,"right":560.5,"bottom":517.5}},{"unicode":12489,"advance":1,"planeBounds":{"left":0.25066666666666665,"top":-0.82291666666666663,"right":0.91733333333333322,"bottom":0.09375},"atlasBounds":{"left":93.5,"top":429.5,"right":125.5,"bottom":473.5}},{"unicode":12490,"advance":1,"planeBounds":{"left":0.05116666666666661,"top":-0.82291666666666663,"right":0.96783333333333332,"bottom":0.09375},"atlasBounds":{"left":126.5,"top":429.5,"right":170.5,"bottom":473.5}},{"unicode":12491,"advance":1,"planeBounds":{"left":0.041583333333333299,"top":-0.69791666666666663,"right":0.93741666666666668,"bottom":-0.010416666666666666},"atlasBounds":{"left":456.5,"top":726.5,"right":499.5,"bottom":759.5}},{"unicode":12492,"advance":1,"planeBounds":{"left":0.082166666666666666,"top":-0.76041666666666663,"right":0.87383333333333324,"bottom":0.09375},"atlasBounds":{"left":366.5,"top":561.5,"right":404.5,"bottom":602.5}},{"unicode":12493,"advance":1,"planeBounds":{"left":0.033749999999999988,"top":-0.84375,"right":0.97125000000000006,"bottom":0.09375},"atlasBounds":{"left":361.5,"top":338.5,"right":406.5,"bottom":383.5}},{"unicode":12494,"advance":1,"planeBounds":{"left":0.095999999999999974,"top":-0.80208333333333326,"right":0.84599999999999986,"bottom":0.072916666666666657},"atlasBounds":{"left":46.5,"top":561.5,"right":82.5,"bottom":603.5}},{"unicode":12495,"advance":1,"planeBounds":{"left":0.030749999999999951,"top":-0.76041666666666663,"right":0.96824999999999994,"bottom":0.052083333333333336},"atlasBounds":{"left":636.5,"top":645.5,"right":681.5,"bottom":684.5}},{"unicode":12496,"advance":1,"planeBounds":{"left":0.021916666666666678,"top":-0.86458333333333326,"right":1.0010833333333333,"bottom":0.052083333333333336},"atlasBounds":{"left":171.5,"top":429.5,"right":218.5,"bottom":473.5}},{"unicode":12497,"advance":1,"planeBounds":{"left":0.012499999999999973,"top":-0.86458333333333326,"right":1.0125,"bottom":0.052083333333333336},"atlasBounds":{"left":219.5,"top":429.5,"right":267.5,"bottom":473.5}},{"unicode":12498,"advance":1,"planeBounds":{"left":0.17741666666666667,"top":-0.82291666666666663,"right":0.9065833333333333,"bottom":0.052083333333333336},"atlasBounds":{"left":785.5,"top":518.5,"right":820.5,"bottom":560.5}},{"unicode":12499,"advance":1,"planeBounds":{"left":0.13733333333333328,"top":-0.88541666666666663,"right":0.97066666666666668,"bottom":0.072916666666666657},"atlasBounds":{"left":530.5,"top":244.5,"right":570.5,"bottom":290.5}},{"unicode":12500,"advance":1,"planeBounds":{"left":0.13441666666666663,"top":-0.86458333333333326,"right":0.98858333333333326,"bottom":0.072916666666666657},"atlasBounds":{"left":407.5,"top":338.5,"right":448.5,"bottom":383.5}},{"unicode":12501,"advance":1,"planeBounds":{"left":0.096749999999999961,"top":-0.76041666666666663,"right":0.90924999999999989,"bottom":0.072916666666666657},"atlasBounds":{"left":640.5,"top":604.5,"right":679.5,"bottom":644.5}},{"unicode":12502,"advance":1,"planeBounds":{"left":0.079250000000000015,"top":-0.90625,"right":1.01675,"bottom":0.09375},"atlasBounds":{"left":544.5,"top":146.5,"right":589.5,"bottom":194.5}},{"unicode":12503,"advance":1,"planeBounds":{"left":0.088250000000000065,"top":-0.88541666666666663,"right":1.0257499999999999,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":244.5,"right":45.5,"bottom":291.5}},{"unicode":12504,"advance":1,"planeBounds":{"left":0.015416666666666655,"top":-0.67708333333333326,"right":0.99458333333333326,"bottom":0.010416666666666666},"atlasBounds":{"left":408.5,"top":726.5,"right":455.5,"bottom":759.5}},{"unicode":12505,"advance":1,"planeBounds":{"left":0.0064166666666666556,"top":-0.78125,"right":0.98558333333333326,"bottom":0.03125},"atlasBounds":{"left":329.5,"top":645.5,"right":376.5,"bottom":684.5}},{"unicode":12506,"advance":1,"planeBounds":{"left":0.0064166666666666556,"top":-0.78125,"right":0.98558333333333326,"bottom":0.03125},"atlasBounds":{"left":377.5,"top":645.5,"right":424.5,"bottom":684.5}},{"unicode":12507,"advance":1,"planeBounds":{"left":0.038250000000000006,"top":-0.84374999999999989,"right":0.97575000000000001,"bottom":0.072916666666666657},"atlasBounds":{"left":292.5,"top":429.5,"right":337.5,"bottom":473.5}},{"unicode":12508,"advance":1,"planeBounds":{"left":0.016916666666666674,"top":-0.86458333333333337,"right":0.99608333333333332,"bottom":0.09375},"atlasBounds":{"left":571.5,"top":244.5,"right":618.5,"bottom":290.5}},{"unicode":12509,"advance":1,"planeBounds":{"left":0.015333333333333332,"top":-0.90625,"right":0.97366666666666668,"bottom":0.09375},"atlasBounds":{"left":590.5,"top":146.5,"right":636.5,"bottom":194.5}},{"unicode":12510,"advance":1,"planeBounds":{"left":0.05766666666666663,"top":-0.73958333333333337,"right":0.97433333333333327,"bottom":0.09375},"atlasBounds":{"left":468.5,"top":604.5,"right":512.5,"bottom":644.5}},{"unicode":12511,"advance":1,"planeBounds":{"left":0.10900000000000001,"top":-0.80208333333333326,"right":0.85899999999999999,"bottom":0.072916666666666657},"atlasBounds":{"left":354.5,"top":518.5,"right":390.5,"bottom":560.5}},{"unicode":12512,"advance":1,"planeBounds":{"left":0.024833333333333273,"top":-0.82291666666666663,"right":0.98316666666666663,"bottom":0.09375},"atlasBounds":{"left":338.5,"top":429.5,"right":384.5,"bottom":473.5}},{"unicode":12513,"advance":1,"planeBounds":{"left":0.066916666666666638,"top":-0.80208333333333326,"right":0.92108333333333325,"bottom":0.072916666666666657},"atlasBounds":{"left":281.5,"top":518.5,"right":322.5,"bottom":560.5}},{"unicode":12514,"advance":1,"planeBounds":{"left":0.063083333333333325,"top":-0.76041666666666663,"right":0.95891666666666664,"bottom":0.072916666666666657},"atlasBounds":{"left":812.5,"top":561.5,"right":855.5,"bottom":601.5}},{"unicode":12515,"advance":1,"planeBounds":{"left":0.10175000000000001,"top":-0.65624999999999989,"right":0.91425000000000001,"bottom":0.13541666666666669},"atlasBounds":{"left":471.5,"top":686.5,"right":510.5,"bottom":724.5}},{"unicode":12516,"advance":1,"planeBounds":{"left":0.021250000000000012,"top":-0.84375,"right":0.95874999999999999,"bottom":0.09375},"atlasBounds":{"left":449.5,"top":338.5,"right":494.5,"bottom":383.5}},{"unicode":12517,"advance":1,"planeBounds":{"left":0.10666666666666669,"top":-0.55208333333333326,"right":0.89833333333333332,"bottom":0.052083333333333336},"atlasBounds":{"left":573.5,"top":764.5,"right":611.5,"bottom":793.5}},{"unicode":12518,"advance":1,"planeBounds":{"left":0.033249999999999974,"top":-0.71875,"right":0.97074999999999989,"bottom":-0.010416666666666666},"atlasBounds":{"left":231.5,"top":726.5,"right":276.5,"bottom":760.5}},{"unicode":12519,"advance":1,"planeBounds":{"left":0.15916666666666662,"top":-0.59375,"right":0.82583333333333331,"bottom":0.11458333333333333},"atlasBounds":{"left":176.5,"top":726.5,"right":208.5,"bottom":760.5}},{"unicode":12520,"advance":1,"planeBounds":{"left":0.10658333333333332,"top":-0.76041666666666663,"right":0.87741666666666662,"bottom":0.072916666666666657},"atlasBounds":{"left":0.5,"top":604.5,"right":37.5,"bottom":644.5}},{"unicode":12521,"advance":1,"planeBounds":{"left":0.092833333333333337,"top":-0.80208333333333337,"right":0.92616666666666669,"bottom":0.09375},"atlasBounds":{"left":648.5,"top":474.5,"right":688.5,"bottom":517.5}},{"unicode":12522,"advance":1,"planeBounds":{"left":0.17508333333333329,"top":-0.80208333333333337,"right":0.82091666666666663,"bottom":0.09375},"atlasBounds":{"left":689.5,"top":474.5,"right":720.5,"bottom":517.5}},{"unicode":12523,"advance":1,"planeBounds":{"left":0.019416666666666658,"top":-0.80208333333333326,"right":0.99858333333333327,"bottom":0.072916666666666657},"atlasBounds":{"left":782.5,"top":474.5,"right":829.5,"bottom":516.5}},{"unicode":12524,"advance":1,"planeBounds":{"left":0.17908333333333329,"top":-0.80208333333333326,"right":0.94991666666666663,"bottom":0.072916666666666657},"atlasBounds":{"left":744.5,"top":474.5,"right":781.5,"bottom":516.5}},{"unicode":12525,"advance":1,"planeBounds":{"left":0.096250000000000016,"top":-0.73958333333333326,"right":0.90875000000000006,"bottom":0.052083333333333336},"atlasBounds":{"left":40.5,"top":686.5,"right":79.5,"bottom":724.5}},{"unicode":12526,"advance":1,"planeBounds":{"left":0.14241666666666664,"top":-0.59374999999999989,"right":0.87158333333333327,"bottom":0.13541666666666669},"atlasBounds":{"left":482.5,"top":59.5,"right":517.5,"bottom":94.5}},{"unicode":12527,"advance":1,"planeBounds":{"left":0.087333333333333305,"top":-0.76041666666666663,"right":0.92066666666666663,"bottom":0.09375},"atlasBounds":{"left":405.5,"top":561.5,"right":445.5,"bottom":602.5}},{"unicode":12528,"advance":1,"planeBounds":{"left":0.045666666666666668,"top":-0.82291666666666663,"right":0.96233333333333326,"bottom":0.09375},"atlasBounds":{"left":385.5,"top":429.5,"right":429.5,"bottom":473.5}},{"unicode":12529,"advance":1,"planeBounds":{"left":0.036250000000000004,"top":-0.73958333333333326,"right":0.97375,"bottom":0.010416666666666666},"atlasBounds":{"left":112.5,"top":59.5,"right":157.5,"bottom":95.5}},{"unicode":12530,"advance":1,"planeBounds":{"left":0.11475,"top":-0.76041666666666663,"right":0.92725000000000002,"bottom":0.09375},"atlasBounds":{"left":204.5,"top":561.5,"right":243.5,"bottom":602.5}},{"unicode":12531,"advance":1,"planeBounds":{"left":0.094499999999999959,"top":-0.78124999999999989,"right":0.96949999999999992,"bottom":0.072916666666666657},"atlasBounds":{"left":267.5,"top":561.5,"right":309.5,"bottom":602.5}},{"unicode":12532,"advance":1,"planeBounds":{"left":0.07166666666666667,"top":-0.90625,"right":0.98833333333333329,"bottom":0.09375},"atlasBounds":{"left":637.5,"top":146.5,"right":681.5,"bottom":194.5}},{"unicode":12533,"advance":1,"planeBounds":{"left":0.11791666666666668,"top":-0.65625,"right":0.8470833333333333,"bottom":0.11458333333333333},"atlasBounds":{"left":38.5,"top":726.5,"right":73.5,"bottom":763.5}},{"unicode":12534,"advance":1,"planeBounds":{"left":0.083249999999999991,"top":-0.65624999999999989,"right":0.89575000000000005,"bottom":0.13541666666666669},"atlasBounds":{"left":181.5,"top":686.5,"right":220.5,"bottom":724.5}},{"unicode":12535,"advance":1,"planeBounds":{"left":0.065666666666666651,"top":-0.90625,"right":0.98233333333333328,"bottom":0.11458333333333333},"atlasBounds":{"left":403.5,"top":96.5,"right":447.5,"bottom":145.5}},{"unicode":12536,"advance":1,"planeBounds":{"left":0.037333333333333336,"top":-0.88541666666666663,"right":0.9956666666666667,"bottom":0.09375},"atlasBounds":{"left":46.5,"top":244.5,"right":92.5,"bottom":291.5}},{"unicode":12537,"advance":1,"planeBounds":{"left":0.024333333333333294,"top":-0.90625,"right":0.98266666666666669,"bottom":0.052083333333333336},"atlasBounds":{"left":619.5,"top":244.5,"right":665.5,"bottom":290.5}},{"unicode":12538,"advance":1,"planeBounds":{"left":0.085083333333333344,"top":-0.90625,"right":0.98091666666666666,"bottom":0.11458333333333333},"atlasBounds":{"left":448.5,"top":96.5,"right":491.5,"bottom":145.5}},{"unicode":12539,"advance":1,"planeBounds":{"left":0.34375,"top":-0.53125,"right":0.65625,"bottom":-0.21875},"atlasBounds":{"left":839.5,"top":129.5,"right":854.5,"bottom":144.5}},{"unicode":12540,"advance":1,"planeBounds":{"left":0.051583333333333294,"top":-0.48958333333333331,"right":0.94741666666666668,"bottom":-0.28125},"atlasBounds":{"left":514.5,"top":797.5,"right":557.5,"bottom":807.5}},{"unicode":12541,"advance":1,"planeBounds":{"left":0.24766666666666665,"top":-0.63541666666666663,"right":0.78933333333333322,"bottom":-0.052083333333333329},"atlasBounds":{"left":40.5,"top":827.5,"right":66.5,"bottom":855.5}},{"unicode":12542,"advance":1,"planeBounds":{"left":0.22349999999999998,"top":-0.78124999999999989,"right":0.84850000000000003,"bottom":-0.052083333333333329},"atlasBounds":{"left":518.5,"top":59.5,"right":548.5,"bottom":94.5}},{"unicode":12543,"advance":1,"planeBounds":{"left":0.14924999999999999,"top":-0.82291666666666663,"right":0.83674999999999999,"bottom":0.09375},"atlasBounds":{"left":430.5,"top":429.5,"right":463.5,"bottom":473.5}},{"unicode":12784,"advance":1,"planeBounds":{"left":0.10549999999999998,"top":-0.69791666666666663,"right":0.85550000000000004,"bottom":0.11458333333333333},"atlasBounds":{"left":563.5,"top":645.5,"right":599.5,"bottom":684.5}},{"unicode":12785,"advance":1,"planeBounds":{"left":0.1330833333333333,"top":-0.67708333333333337,"right":0.9039166666666667,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":726.5,"right":37.5,"bottom":763.5}},{"unicode":12786,"advance":1,"planeBounds":{"left":0.12199999999999997,"top":-0.61458333333333337,"right":0.87199999999999989,"bottom":0.09375},"atlasBounds":{"left":311.5,"top":726.5,"right":347.5,"bottom":760.5}},{"unicode":12787,"advance":1,"planeBounds":{"left":0.32058333333333333,"top":-0.65625,"right":0.8414166666666667,"bottom":0.11458333333333333},"atlasBounds":{"left":150.5,"top":726.5,"right":175.5,"bottom":763.5}},{"unicode":12788,"advance":1,"planeBounds":{"left":0.14016666666666666,"top":-0.61458333333333326,"right":0.80683333333333329,"bottom":0.11458333333333333},"atlasBounds":{"left":549.5,"top":59.5,"right":581.5,"bottom":94.5}},{"unicode":12789,"advance":1,"planeBounds":{"left":0.10966666666666668,"top":-0.61458333333333326,"right":0.90133333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":717.5,"top":726.5,"right":755.5,"bottom":759.5}},{"unicode":12790,"advance":1,"planeBounds":{"left":0.21599999999999994,"top":-0.67708333333333326,"right":0.84099999999999986,"bottom":0.072916666666666657},"atlasBounds":{"left":158.5,"top":59.5,"right":188.5,"bottom":95.5}},{"unicode":12791,"advance":1,"planeBounds":{"left":0.15974999999999995,"top":-0.63541666666666663,"right":0.84724999999999995,"bottom":0.072916666666666657},"atlasBounds":{"left":277.5,"top":726.5,"right":310.5,"bottom":760.5}},{"unicode":12792,"advance":1,"planeBounds":{"left":0.099749999999999978,"top":-0.55208333333333337,"right":0.91224999999999989,"bottom":0.03125},"atlasBounds":{"left":0.5,"top":827.5,"right":39.5,"bottom":855.5}},{"unicode":12793,"advance":1,"planeBounds":{"left":0.082749999999999962,"top":-0.69791666666666663,"right":0.89524999999999988,"bottom":0.072916666666666657},"atlasBounds":{"left":747.5,"top":686.5,"right":786.5,"bottom":723.5}},{"unicode":12794,"advance":1,"planeBounds":{"left":0.11975000000000001,"top":-0.65625,"right":0.93225000000000002,"bottom":0.11458333333333333},"atlasBounds":{"left":707.5,"top":686.5,"right":746.5,"bottom":723.5}},{"unicode":12795,"advance":1,"planeBounds":{"left":0.13483333333333328,"top":-0.63541666666666663,"right":0.84316666666666662,"bottom":0.11458333333333333},"atlasBounds":{"left":189.5,"top":59.5,"right":223.5,"bottom":95.5}},{"unicode":12796,"advance":1,"planeBounds":{"left":0.20774999999999996,"top":-0.65625,"right":0.77024999999999999,"bottom":0.11458333333333333},"atlasBounds":{"left":679.5,"top":686.5,"right":706.5,"bottom":723.5}},{"unicode":12797,"advance":1,"planeBounds":{"left":0.094333333333333297,"top":-0.65624999999999989,"right":0.92766666666666664,"bottom":0.072916666666666657},"atlasBounds":{"left":582.5,"top":59.5,"right":622.5,"bottom":94.5}},{"unicode":12798,"advance":1,"planeBounds":{"left":0.25766666666666665,"top":-0.63541666666666663,"right":0.92433333333333323,"bottom":0.09375},"atlasBounds":{"left":623.5,"top":59.5,"right":655.5,"bottom":94.5}},{"unicode":12799,"advance":1,"planeBounds":{"left":0.15725,"top":-0.59374999999999989,"right":0.84475,"bottom":0.072916666666666657},"atlasBounds":{"left":785.5,"top":726.5,"right":818.5,"bottom":758.5}},{"unicode":65281,"advance":1,"planeBounds":{"left":0.38541666666666669,"top":-0.80208333333333326,"right":0.61458333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":354.5,"top":561.5,"right":365.5,"bottom":602.5}},{"unicode":65282,"advance":1,"planeBounds":{"left":0.28125,"top":-0.92708333333333326,"right":0.71875,"bottom":-0.48958333333333331},"atlasBounds":{"left":68.5,"top":797.5,"right":89.5,"bottom":818.5}},{"unicode":65283,"advance":1,"planeBounds":{"left":0.11658333333333336,"top":-0.80208333333333326,"right":0.88741666666666674,"bottom":0.052083333333333336},"atlasBounds":{"left":144.5,"top":561.5,"right":181.5,"bottom":602.5}},{"unicode":65284,"advance":1,"planeBounds":{"left":0.18299999999999997,"top":-0.92708333333333326,"right":0.80800000000000005,"bottom":0.17708333333333334},"atlasBounds":{"left":104.5,"top":0.5,"right":134.5,"bottom":53.5}},{"unicode":65285,"advance":1,"planeBounds":{"left":0.031749999999999952,"top":-0.82291666666666663,"right":0.96924999999999994,"bottom":0.052083333333333336},"atlasBounds":{"left":0.5,"top":561.5,"right":45.5,"bottom":603.5}},{"unicode":65286,"advance":1,"planeBounds":{"left":0.14891666666666661,"top":-0.80208333333333326,"right":0.87808333333333333,"bottom":0.072916666666666657},"atlasBounds":{"left":749.5,"top":518.5,"right":784.5,"bottom":560.5}},{"unicode":65287,"advance":1,"planeBounds":{"left":0.39583333333333331,"top":-0.92708333333333326,"right":0.60416666666666663,"bottom":-0.48958333333333331},"atlasBounds":{"left":844.5,"top":72.5,"right":854.5,"bottom":93.5}},{"unicode":65288,"advance":1,"planeBounds":{"left":0.64741666666666664,"top":-0.90624999999999989,"right":1.0015833333333333,"bottom":0.15625},"atlasBounds":{"left":287.5,"top":0.5,"right":304.5,"bottom":51.5}},{"unicode":65289,"advance":1,"planeBounds":{"left":-0.001583333333333309,"top":-0.90624999999999989,"right":0.3525833333333333,"bottom":0.15625},"atlasBounds":{"left":305.5,"top":0.5,"right":322.5,"bottom":51.5}},{"unicode":65290,"advance":1,"planeBounds":{"left":0.10516666666666666,"top":-0.82291666666666663,"right":0.89683333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":65.5,"top":518.5,"right":103.5,"bottom":560.5}},{"unicode":65291,"advance":1,"planeBounds":{"left":0.09375,"top":-0.78125,"right":0.90625,"bottom":0.03125},"atlasBounds":{"left":258.5,"top":645.5,"right":297.5,"bottom":684.5}},{"unicode":65292,"advance":1,"planeBounds":{"left":0.086666666666666711,"top":-0.28125,"right":0.37833333333333335,"bottom":0.15625},"atlasBounds":{"left":90.5,"top":797.5,"right":104.5,"bottom":818.5}},{"unicode":65293,"advance":1,"planeBounds":{"left":0.09375,"top":-0.46875,"right":0.90625,"bottom":-0.28125},"atlasBounds":{"left":558.5,"top":797.5,"right":597.5,"bottom":806.5}},{"unicode":65294,"advance":1,"planeBounds":{"left":0.11400000000000003,"top":-0.21875,"right":0.36399999999999999,"bottom":0.052083333333333336},"atlasBounds":{"left":843.5,"top":827.5,"right":855.5,"bottom":840.5}},{"unicode":65295,"advance":1,"planeBounds":{"left":-0.010416666666666696,"top":-0.90624999999999989,"right":1.0104166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":673.5,"top":0.5,"right":722.5,"bottom":50.5}},{"unicode":65296,"advance":1,"planeBounds":{"left":0.17708333333333331,"top":-0.80208333333333326,"right":0.82291666666666663,"bottom":0.072916666666666657},"atlasBounds":{"left":563.5,"top":518.5,"right":594.5,"bottom":560.5}},{"unicode":65297,"advance":1,"planeBounds":{"left":0.19599999999999995,"top":-0.78125,"right":0.82099999999999995,"bottom":0.052083333333333336},"atlasBounds":{"left":227.5,"top":645.5,"right":257.5,"bottom":685.5}},{"unicode":65298,"advance":1,"planeBounds":{"left":0.18599999999999997,"top":-0.80208333333333326,"right":0.81100000000000005,"bottom":0.052083333333333336},"atlasBounds":{"left":523.5,"top":561.5,"right":553.5,"bottom":602.5}},{"unicode":65299,"advance":1,"planeBounds":{"left":0.17199999999999999,"top":-0.80208333333333326,"right":0.79700000000000004,"bottom":0.072916666666666657},"atlasBounds":{"left":532.5,"top":518.5,"right":562.5,"bottom":560.5}},{"unicode":65300,"advance":1,"planeBounds":{"left":0.17316666666666666,"top":-0.78125,"right":0.83983333333333332,"bottom":0.052083333333333336},"atlasBounds":{"left":735.5,"top":604.5,"right":767.5,"bottom":644.5}},{"unicode":65301,"advance":1,"planeBounds":{"left":0.17449999999999999,"top":-0.78124999999999989,"right":0.79949999999999999,"bottom":0.072916666666666657},"atlasBounds":{"left":492.5,"top":561.5,"right":522.5,"bottom":602.5}},{"unicode":65302,"advance":1,"planeBounds":{"left":0.18949999999999997,"top":-0.80208333333333326,"right":0.8145,"bottom":0.072916666666666657},"atlasBounds":{"left":468.5,"top":518.5,"right":498.5,"bottom":560.5}},{"unicode":65303,"advance":1,"planeBounds":{"left":0.18899999999999995,"top":-0.78125,"right":0.81399999999999995,"bottom":0.052083333333333336},"atlasBounds":{"left":680.5,"top":604.5,"right":710.5,"bottom":644.5}},{"unicode":65304,"advance":1,"planeBounds":{"left":0.18649999999999997,"top":-0.80208333333333326,"right":0.8115,"bottom":0.072916666666666657},"atlasBounds":{"left":391.5,"top":518.5,"right":421.5,"bottom":560.5}},{"unicode":65305,"advance":1,"planeBounds":{"left":0.18549999999999997,"top":-0.80208333333333326,"right":0.8105,"bottom":0.072916666666666657},"atlasBounds":{"left":323.5,"top":518.5,"right":353.5,"bottom":560.5}},{"unicode":65306,"advance":1,"planeBounds":{"left":0.375,"top":-0.73958333333333326,"right":0.625,"bottom":-0.010416666666666666},"atlasBounds":{"left":656.5,"top":59.5,"right":668.5,"bottom":94.5}},{"unicode":65307,"advance":1,"planeBounds":{"left":0.34816666666666668,"top":-0.73958333333333337,"right":0.63983333333333325,"bottom":0.15625},"atlasBounds":{"left":91.5,"top":474.5,"right":105.5,"bottom":517.5}},{"unicode":65308,"advance":1,"planeBounds":{"left":0.05699999999999996,"top":-0.82291666666666663,"right":0.93199999999999994,"bottom":0.072916666666666657},"atlasBounds":{"left":48.5,"top":474.5,"right":90.5,"bottom":517.5}},{"unicode":65309,"advance":1,"planeBounds":{"left":0.09375,"top":-0.57291666666666663,"right":0.90625,"bottom":-0.17708333333333331},"atlasBounds":{"left":205.5,"top":797.5,"right":244.5,"bottom":816.5}},{"unicode":65310,"advance":1,"planeBounds":{"left":0.067999999999999977,"top":-0.82291666666666663,"right":0.94299999999999995,"bottom":0.072916666666666657},"atlasBounds":{"left":777.5,"top":429.5,"right":819.5,"bottom":472.5}},{"unicode":65311,"advance":1,"planeBounds":{"left":0.20533333333333328,"top":-0.82291666666666663,"right":0.78866666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":0.5,"top":518.5,"right":28.5,"bottom":560.5}},{"unicode":65312,"advance":1,"planeBounds":{"left":0.031249999999999965,"top":-0.88541666666666663,"right":0.96874999999999989,"bottom":0.11458333333333333},"atlasBounds":{"left":46.5,"top":195.5,"right":91.5,"bottom":243.5}},{"unicode":65313,"advance":1,"planeBounds":{"left":0.10958333333333337,"top":-0.78125,"right":0.88041666666666674,"bottom":0.052083333333333336},"atlasBounds":{"left":393.5,"top":604.5,"right":430.5,"bottom":644.5}},{"unicode":65314,"advance":1,"planeBounds":{"left":0.19416666666666665,"top":-0.78125,"right":0.86083333333333323,"bottom":0.052083333333333336},"atlasBounds":{"left":360.5,"top":604.5,"right":392.5,"bottom":644.5}},{"unicode":65315,"advance":1,"planeBounds":{"left":0.1588333333333333,"top":-0.80208333333333326,"right":0.86716666666666664,"bottom":0.072916666666666657},"atlasBounds":{"left":147.5,"top":518.5,"right":181.5,"bottom":560.5}},{"unicode":65316,"advance":1,"planeBounds":{"left":0.17774999999999999,"top":-0.78125,"right":0.86524999999999996,"bottom":0.052083333333333336},"atlasBounds":{"left":294.5,"top":604.5,"right":327.5,"bottom":644.5}},{"unicode":65317,"advance":1,"planeBounds":{"left":0.23433333333333328,"top":-0.78125,"right":0.81766666666666665,"bottom":0.052083333333333336},"atlasBounds":{"left":230.5,"top":604.5,"right":258.5,"bottom":644.5}},{"unicode":65318,"advance":1,"planeBounds":{"left":0.24733333333333327,"top":-0.78125,"right":0.83066666666666666,"bottom":0.052083333333333336},"atlasBounds":{"left":156.5,"top":604.5,"right":184.5,"bottom":644.5}},{"unicode":65319,"advance":1,"planeBounds":{"left":0.12741666666666662,"top":-0.80208333333333326,"right":0.85658333333333325,"bottom":0.072916666666666657},"atlasBounds":{"left":29.5,"top":518.5,"right":64.5,"bottom":560.5}},{"unicode":65320,"advance":1,"planeBounds":{"left":0.15624999999999997,"top":-0.78125,"right":0.84375,"bottom":0.052083333333333336},"atlasBounds":{"left":38.5,"top":604.5,"right":71.5,"bottom":644.5}},{"unicode":65321,"advance":1,"planeBounds":{"left":0.19891666666666663,"top":-0.78125,"right":0.80308333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":671.5,"top":561.5,"right":700.5,"bottom":601.5}},{"unicode":65322,"advance":1,"planeBounds":{"left":0.19766666666666663,"top":-0.78124999999999989,"right":0.73933333333333329,"bottom":0.072916666666666657},"atlasBounds":{"left":117.5,"top":561.5,"right":143.5,"bottom":602.5}},{"unicode":65323,"advance":1,"planeBounds":{"left":0.1938333333333333,"top":-0.78125,"right":0.90216666666666667,"bottom":0.052083333333333336},"atlasBounds":{"left":259.5,"top":604.5,"right":293.5,"bottom":644.5}},{"unicode":65324,"advance":1,"planeBounds":{"left":0.25774999999999992,"top":-0.78125,"right":0.82024999999999992,"bottom":0.052083333333333336},"atlasBounds":{"left":784.5,"top":561.5,"right":811.5,"bottom":601.5}},{"unicode":65325,"advance":1,"planeBounds":{"left":0.10416666666666662,"top":-0.78125,"right":0.89583333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":701.5,"top":561.5,"right":739.5,"bottom":601.5}},{"unicode":65326,"advance":1,"planeBounds":{"left":0.15724999999999997,"top":-0.78125,"right":0.84475,"bottom":0.052083333333333336},"atlasBounds":{"left":122.5,"top":604.5,"right":155.5,"bottom":644.5}},{"unicode":65327,"advance":1,"planeBounds":{"left":0.09375,"top":-0.80208333333333326,"right":0.90625,"bottom":0.072916666666666657},"atlasBounds":{"left":684.5,"top":518.5,"right":723.5,"bottom":560.5}},{"unicode":65328,"advance":1,"planeBounds":{"left":0.20258333333333328,"top":-0.78125,"right":0.8484166666666666,"bottom":0.052083333333333336},"atlasBounds":{"left":513.5,"top":604.5,"right":544.5,"bottom":644.5}},{"unicode":65329,"advance":1,"planeBounds":{"left":0.094333333333333297,"top":-0.80208333333333326,"right":0.92766666666666664,"bottom":0.23958333333333334},"atlasBounds":{"left":723.5,"top":0.5,"right":763.5,"bottom":50.5}},{"unicode":65330,"advance":1,"planeBounds":{"left":0.20516666666666666,"top":-0.78125,"right":0.87183333333333324,"bottom":0.052083333333333336},"atlasBounds":{"left":194.5,"top":645.5,"right":226.5,"bottom":685.5}},{"unicode":65331,"advance":1,"planeBounds":{"left":0.16766666666666663,"top":-0.80208333333333326,"right":0.83433333333333326,"bottom":0.072916666666666657},"atlasBounds":{"left":499.5,"top":518.5,"right":531.5,"bottom":560.5}},{"unicode":65332,"advance":1,"planeBounds":{"left":0.14583333333333331,"top":-0.78125,"right":0.85416666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":123.5,"top":645.5,"right":157.5,"bottom":685.5}},{"unicode":65333,"advance":1,"planeBounds":{"left":0.15875,"top":-0.78124999999999989,"right":0.84625000000000006,"bottom":0.072916666666666657},"atlasBounds":{"left":83.5,"top":561.5,"right":116.5,"bottom":602.5}},{"unicode":65334,"advance":1,"planeBounds":{"left":0.12549999999999997,"top":-0.78125,"right":0.87549999999999994,"bottom":0.052083333333333336},"atlasBounds":{"left":86.5,"top":645.5,"right":122.5,"bottom":685.5}},{"unicode":65335,"advance":1,"planeBounds":{"left":-0.021833333333333364,"top":-0.78125,"right":1.0198333333333334,"bottom":0.052083333333333336},"atlasBounds":{"left":35.5,"top":645.5,"right":85.5,"bottom":685.5}},{"unicode":65336,"advance":1,"planeBounds":{"left":0.14633333333333329,"top":-0.78125,"right":0.85466666666666669,"bottom":0.052083333333333336},"atlasBounds":{"left":0.5,"top":645.5,"right":34.5,"bottom":685.5}},{"unicode":65337,"advance":1,"planeBounds":{"left":0.15624999999999994,"top":-0.78125,"right":0.84374999999999989,"bottom":0.052083333333333336},"atlasBounds":{"left":801.5,"top":604.5,"right":834.5,"bottom":644.5}},{"unicode":65338,"advance":1,"planeBounds":{"left":0.17116666666666666,"top":-0.78125,"right":0.83783333333333332,"bottom":0.052083333333333336},"atlasBounds":{"left":768.5,"top":604.5,"right":800.5,"bottom":644.5}},{"unicode":65339,"advance":1,"planeBounds":{"left":0.6619166666666666,"top":-0.90624999999999989,"right":1.0160833333333332,"bottom":0.13541666666666669},"atlasBounds":{"left":764.5,"top":0.5,"right":781.5,"bottom":50.5}},{"unicode":65340,"advance":1,"planeBounds":{"left":-0.010416666666666696,"top":-0.90624999999999989,"right":1.0104166666666667,"bottom":0.13541666666666669},"atlasBounds":{"left":782.5,"top":0.5,"right":831.5,"bottom":50.5}},{"unicode":65341,"advance":1,"planeBounds":{"left":-0.016083333333333293,"top":-0.90624999999999989,"right":0.3380833333333334,"bottom":0.13541666666666669},"atlasBounds":{"left":832.5,"top":0.5,"right":849.5,"bottom":50.5}},{"unicode":65342,"advance":1,"planeBounds":{"left":0.30066666666666669,"top":-0.98958333333333326,"right":0.71733333333333327,"bottom":-0.76041666666666663},"atlasBounds":{"left":835.5,"top":547.5,"right":855.5,"bottom":558.5}},{"unicode":65343,"advance":1,"planeBounds":{"left":-0.052083333333333259,"top":0.03125,"right":1.0520833333333333,"bottom":0.17708333333333334},"atlasBounds":{"left":643.5,"top":797.5,"right":696.5,"bottom":804.5}},{"unicode":65344,"advance":1,"planeBounds":{"left":0.29325000000000001,"top":-0.98958333333333326,"right":0.60575000000000001,"bottom":-0.76041666666666663},"atlasBounds":{"left":498.5,"top":797.5,"right":513.5,"bottom":808.5}},{"unicode":65345,"advance":1,"planeBounds":{"left":0.20633333333333329,"top":-0.61458333333333326,"right":0.78966666666666663,"bottom":0.072916666666666657},"atlasBounds":{"left":585.5,"top":726.5,"right":613.5,"bottom":759.5}},{"unicode":65346,"advance":1,"planeBounds":{"left":0.20699999999999999,"top":-0.84374999999999989,"right":0.83200000000000007,"bottom":0.072916666666666657},"atlasBounds":{"left":120.5,"top":384.5,"right":150.5,"bottom":428.5}},{"unicode":65347,"advance":1,"planeBounds":{"left":0.20841666666666664,"top":-0.61458333333333326,"right":0.81258333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":378.5,"top":726.5,"right":407.5,"bottom":759.5}},{"unicode":65348,"advance":1,"planeBounds":{"left":0.16749999999999998,"top":-0.84374999999999989,"right":0.79249999999999998,"bottom":0.072916666666666657},"atlasBounds":{"left":0.5,"top":384.5,"right":30.5,"bottom":428.5}},{"unicode":65349,"advance":1,"planeBounds":{"left":0.18058333333333329,"top":-0.61458333333333326,"right":0.82641666666666669,"bottom":0.072916666666666657},"atlasBounds":{"left":614.5,"top":726.5,"right":645.5,"bottom":759.5}},{"unicode":65350,"advance":1,"planeBounds":{"left":0.23299999999999998,"top":-0.86458333333333326,"right":0.85799999999999998,"bottom":0.052083333333333336},"atlasBounds":{"left":750.5,"top":338.5,"right":780.5,"bottom":382.5}},{"unicode":65351,"advance":1,"planeBounds":{"left":0.18366666666666664,"top":-0.61458333333333326,"right":0.85033333333333327,"bottom":0.30208333333333331},"atlasBounds":{"left":717.5,"top":338.5,"right":749.5,"bottom":382.5}},{"unicode":65352,"advance":1,"planeBounds":{"left":0.21333333333333329,"top":-0.84375,"right":0.79666666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":722.5,"top":429.5,"right":750.5,"bottom":472.5}},{"unicode":65353,"advance":1,"planeBounds":{"left":0.38541666666666663,"top":-0.84375,"right":0.61458333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":689.5,"top":429.5,"right":700.5,"bottom":472.5}},{"unicode":65354,"advance":1,"planeBounds":{"left":0.18049999999999997,"top":-0.84375,"right":0.68049999999999999,"bottom":0.30208333333333331},"atlasBounds":{"left":79.5,"top":0.5,"right":103.5,"bottom":55.5}},{"unicode":65355,"advance":1,"planeBounds":{"left":0.23891666666666664,"top":-0.84375,"right":0.8430833333333333,"bottom":0.052083333333333336},"atlasBounds":{"left":659.5,"top":429.5,"right":688.5,"bottom":472.5}},{"unicode":65356,"advance":1,"planeBounds":{"left":0.39466666666666672,"top":-0.84374999999999989,"right":0.81133333333333324,"bottom":0.072916666666666657},"atlasBounds":{"left":617.5,"top":338.5,"right":637.5,"bottom":382.5}},{"unicode":65357,"advance":1,"planeBounds":{"left":0.03425000000000001,"top":-0.61458333333333326,"right":0.97175,"bottom":0.052083333333333336},"atlasBounds":{"left":81.5,"top":764.5,"right":126.5,"bottom":796.5}},{"unicode":65358,"advance":1,"planeBounds":{"left":0.21333333333333329,"top":-0.61458333333333326,"right":0.79666666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":756.5,"top":726.5,"right":784.5,"bottom":758.5}},{"unicode":65359,"advance":1,"planeBounds":{"left":0.16666666666666663,"top":-0.61458333333333326,"right":0.83333333333333326,"bottom":0.072916666666666657},"atlasBounds":{"left":684.5,"top":726.5,"right":716.5,"bottom":759.5}},{"unicode":65360,"advance":1,"planeBounds":{"left":0.20649999999999999,"top":-0.61458333333333326,"right":0.83150000000000002,"bottom":0.28125},"atlasBounds":{"left":617.5,"top":474.5,"right":647.5,"bottom":517.5}},{"unicode":65361,"advance":1,"planeBounds":{"left":0.16799999999999998,"top":-0.61458333333333326,"right":0.79300000000000004,"bottom":0.28125},"atlasBounds":{"left":586.5,"top":474.5,"right":616.5,"bottom":517.5}},{"unicode":65362,"advance":1,"planeBounds":{"left":0.27716666666666662,"top":-0.61458333333333326,"right":0.8188333333333333,"bottom":0.052083333333333336},"atlasBounds":{"left":54.5,"top":764.5,"right":80.5,"bottom":796.5}},{"unicode":65363,"advance":1,"planeBounds":{"left":0.19641666666666666,"top":-0.61458333333333326,"right":0.80058333333333331,"bottom":0.072916666666666657},"atlasBounds":{"left":348.5,"top":726.5,"right":377.5,"bottom":759.5}},{"unicode":65364,"advance":1,"planeBounds":{"left":0.19549999999999998,"top":-0.73958333333333326,"right":0.82050000000000001,"bottom":0.072916666666666657},"atlasBounds":{"left":298.5,"top":645.5,"right":328.5,"bottom":684.5}},{"unicode":65365,"advance":1,"planeBounds":{"left":0.20433333333333328,"top":-0.59374999999999989,"right":0.78766666666666663,"bottom":0.072916666666666657},"atlasBounds":{"left":127.5,"top":764.5,"right":155.5,"bottom":796.5}},{"unicode":65366,"advance":1,"planeBounds":{"left":0.17708333333333331,"top":-0.59375,"right":0.82291666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":262.5,"top":764.5,"right":293.5,"bottom":795.5}},{"unicode":65367,"advance":1,"planeBounds":{"left":0.030249999999999971,"top":-0.59375,"right":0.96774999999999989,"bottom":0.052083333333333336},"atlasBounds":{"left":156.5,"top":764.5,"right":201.5,"bottom":795.5}},{"unicode":65368,"advance":1,"planeBounds":{"left":0.18749999999999997,"top":-0.59375,"right":0.8125,"bottom":0.052083333333333336},"atlasBounds":{"left":202.5,"top":764.5,"right":232.5,"bottom":795.5}},{"unicode":65369,"advance":1,"planeBounds":{"left":0.17708333333333329,"top":-0.59375,"right":0.82291666666666663,"bottom":0.28125},"atlasBounds":{"left":203.5,"top":518.5,"right":234.5,"bottom":560.5}},{"unicode":65370,"advance":1,"planeBounds":{"left":0.21033333333333329,"top":-0.59375,"right":0.79366666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":233.5,"top":764.5,"right":261.5,"bottom":795.5}},{"unicode":65371,"advance":1,"planeBounds":{"left":0.65691666666666659,"top":-0.90624999999999989,"right":1.0110833333333333,"bottom":0.15625},"atlasBounds":{"left":323.5,"top":0.5,"right":340.5,"bottom":51.5}},{"unicode":65372,"advance":1,"planeBounds":{"left":0.42708333333333331,"top":-0.92708333333333326,"right":0.57291666666666663,"bottom":0.17708333333333334},"atlasBounds":{"left":135.5,"top":0.5,"right":142.5,"bottom":53.5}},{"unicode":65373,"advance":1,"planeBounds":{"left":-0.011083333333333327,"top":-0.90624999999999989,"right":0.3430833333333333,"bottom":0.15625},"atlasBounds":{"left":341.5,"top":0.5,"right":358.5,"bottom":51.5}},{"unicode":65374,"advance":1,"planeBounds":{"left":-3.552713678800501e-17,"top":-0.57291666666666663,"right":0.99999999999999989,"bottom":-0.19791666666666666},"atlasBounds":{"left":367.5,"top":797.5,"right":415.5,"bottom":815.5}},{"unicode":65375,"advance":1,"planeBounds":{"left":0.52641666666666664,"top":-0.90624999999999989,"right":1.0055833333333333,"bottom":0.15625},"atlasBounds":{"left":359.5,"top":0.5,"right":382.5,"bottom":51.5}},{"unicode":65376,"advance":1,"planeBounds":{"left":-0.0055833333333333056,"top":-0.90624999999999989,"right":0.47358333333333336,"bottom":0.15625},"atlasBounds":{"left":383.5,"top":0.5,"right":406.5,"bottom":51.5}},{"unicode":65377,"advance":0.5,"planeBounds":{"left":-0.01141666666666666,"top":-0.30208333333333331,"right":0.38441666666666668,"bottom":0.11458333333333333},"atlasBounds":{"left":125.5,"top":797.5,"right":144.5,"bottom":817.5}},{"unicode":65378,"advance":0.5,"planeBounds":{"left":0.099666666666666723,"top":-0.90625,"right":0.51633333333333342,"bottom":-0.15625},"atlasBounds":{"left":224.5,"top":59.5,"right":244.5,"bottom":95.5}},{"unicode":65379,"advance":0.5,"planeBounds":{"left":-0.0163333333333333,"top":-0.61458333333333326,"right":0.40033333333333337,"bottom":0.13541666666666669},"atlasBounds":{"left":245.5,"top":59.5,"right":265.5,"bottom":95.5}},{"unicode":65380,"advance":0.5,"planeBounds":{"left":-0.017999999999999964,"top":-0.28125,"right":0.35699999999999998,"bottom":0.11458333333333333},"atlasBounds":{"left":245.5,"top":797.5,"right":263.5,"bottom":816.5}},{"unicode":65381,"advance":0.5,"planeBounds":{"left":0.11408333333333334,"top":-0.51041666666666663,"right":0.38491666666666668,"bottom":-0.23958333333333331},"atlasBounds":{"left":821.5,"top":547.5,"right":834.5,"bottom":560.5}},{"unicode":65382,"advance":0.5,"planeBounds":{"left":0.032333333333333367,"top":-0.76041666666666663,"right":0.4906666666666667,"bottom":0.09375},"atlasBounds":{"left":446.5,"top":561.5,"right":468.5,"bottom":602.5}},{"unicode":65383,"advance":0.5,"planeBounds":{"left":0.056166666666666684,"top":-0.61458333333333337,"right":0.47283333333333333,"bottom":0.09375},"atlasBounds":{"left":835.5,"top":604.5,"right":855.5,"bottom":638.5}},{"unicode":65384,"advance":0.5,"planeBounds":{"left":0.022583333333333344,"top":-0.65625,"right":0.41841666666666671,"bottom":0.09375},"atlasBounds":{"left":266.5,"top":59.5,"right":285.5,"bottom":95.5}},{"unicode":65385,"advance":0.5,"planeBounds":{"left":0.04766666666666669,"top":-0.65625,"right":0.46433333333333338,"bottom":0.09375},"atlasBounds":{"left":286.5,"top":59.5,"right":306.5,"bottom":95.5}},{"unicode":65386,"advance":0.5,"planeBounds":{"left":0.02675000000000002,"top":-0.55208333333333326,"right":0.46425,"bottom":0.052083333333333336},"atlasBounds":{"left":723.5,"top":764.5,"right":744.5,"bottom":793.5}},{"unicode":65387,"advance":0.5,"planeBounds":{"left":0.025750000000000023,"top":-0.65625,"right":0.46325,"bottom":0.09375},"atlasBounds":{"left":307.5,"top":59.5,"right":328.5,"bottom":95.5}},{"unicode":65388,"advance":0.5,"planeBounds":{"left":0.016333333333333363,"top":-0.65625,"right":0.47466666666666668,"bottom":0.09375},"atlasBounds":{"left":329.5,"top":59.5,"right":351.5,"bottom":95.5}},{"unicode":65389,"advance":0.5,"planeBounds":{"left":0.030750000000000031,"top":-0.55208333333333326,"right":0.46825,"bottom":0.052083333333333336},"atlasBounds":{"left":551.5,"top":764.5,"right":572.5,"bottom":793.5}},{"unicode":65390,"advance":0.5,"planeBounds":{"left":0.060500000000000026,"top":-0.57291666666666663,"right":0.4355,"bottom":0.09375},"atlasBounds":{"left":0.5,"top":764.5,"right":18.5,"bottom":796.5}},{"unicode":65391,"advance":0.5,"planeBounds":{"left":0.026250000000000013,"top":-0.59375,"right":0.46375,"bottom":0.11458333333333333},"atlasBounds":{"left":209.5,"top":726.5,"right":230.5,"bottom":760.5}},{"unicode":65392,"advance":0.5,"planeBounds":{"left":0.0045000000000000127,"top":-0.46875,"right":0.50450000000000006,"bottom":-0.26041666666666669},"atlasBounds":{"left":821.5,"top":674.5,"right":845.5,"bottom":684.5}},{"unicode":65393,"advance":0.5,"planeBounds":{"left":0.01649999999999998,"top":-0.76041666666666663,"right":0.51649999999999996,"bottom":0.09375},"atlasBounds":{"left":646.5,"top":561.5,"right":670.5,"bottom":602.5}},{"unicode":65394,"advance":0.5,"planeBounds":{"left":-0.016999999999999967,"top":-0.82291666666666663,"right":0.48299999999999998,"bottom":0.09375},"atlasBounds":{"left":563.5,"top":338.5,"right":587.5,"bottom":382.5}},{"unicode":65395,"advance":0.5,"planeBounds":{"left":0.016416666666666694,"top":-0.84375,"right":0.49558333333333332,"bottom":0.09375},"atlasBounds":{"left":129.5,"top":292.5,"right":152.5,"bottom":337.5}},{"unicode":65396,"advance":0.5,"planeBounds":{"left":-0.010916666666666642,"top":-0.71874999999999989,"right":0.50991666666666668,"bottom":0.010416666666666666},"atlasBounds":{"left":669.5,"top":59.5,"right":694.5,"bottom":94.5}},{"unicode":65397,"advance":0.5,"planeBounds":{"left":0.0050833333333333199,"top":-0.82291666666666663,"right":0.52591666666666659,"bottom":0.072916666666666657},"atlasBounds":{"left":751.5,"top":429.5,"right":776.5,"bottom":472.5}},{"unicode":65398,"advance":0.5,"planeBounds":{"left":-0.015499999999999984,"top":-0.82291666666666663,"right":0.48449999999999999,"bottom":0.072916666666666657},"atlasBounds":{"left":561.5,"top":474.5,"right":585.5,"bottom":517.5}},{"unicode":65399,"advance":0.5,"planeBounds":{"left":0.012916666666666677,"top":-0.82291666666666663,"right":0.49208333333333332,"bottom":0.09375},"atlasBounds":{"left":268.5,"top":429.5,"right":291.5,"bottom":473.5}},{"unicode":65400,"advance":0.5,"planeBounds":{"left":0.01091666666666671,"top":-0.84375,"right":0.49008333333333337,"bottom":0.09375},"atlasBounds":{"left":229.5,"top":292.5,"right":252.5,"bottom":337.5}},{"unicode":65401,"advance":0.5,"planeBounds":{"left":-0.019916666666666645,"top":-0.82291666666666663,"right":0.50091666666666668,"bottom":0.09375},"atlasBounds":{"left":464.5,"top":429.5,"right":489.5,"bottom":473.5}},{"unicode":65402,"advance":0.5,"planeBounds":{"left":0.030750000000000031,"top":-0.71875,"right":0.46825,"bottom":0.052083333333333336},"atlasBounds":{"left":787.5,"top":686.5,"right":808.5,"bottom":723.5}},{"unicode":65403,"advance":0.5,"planeBounds":{"left":-0.012916666666666646,"top":-0.80208333333333337,"right":0.50791666666666668,"bottom":0.09375},"atlasBounds":{"left":106.5,"top":474.5,"right":131.5,"bottom":517.5}},{"unicode":65404,"advance":0.5,"planeBounds":{"left":-0.0044166666666666269,"top":-0.82291666666666663,"right":0.51641666666666675,"bottom":0.072916666666666657},"atlasBounds":{"left":505.5,"top":474.5,"right":530.5,"bottom":517.5}},{"unicode":65405,"advance":0.5,"planeBounds":{"left":-0.005416666666666627,"top":-0.76041666666666663,"right":0.51541666666666675,"bottom":0.072916666666666657},"atlasBounds":{"left":590.5,"top":604.5,"right":615.5,"bottom":644.5}},{"unicode":65406,"advance":0.5,"planeBounds":{"left":-0.026416666666666665,"top":-0.82291666666666663,"right":0.49441666666666667,"bottom":0.052083333333333336},"atlasBounds":{"left":658.5,"top":518.5,"right":683.5,"bottom":560.5}},{"unicode":65407,"advance":0.5,"planeBounds":{"left":0.010916666666666677,"top":-0.76041666666666663,"right":0.49008333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":711.5,"top":604.5,"right":734.5,"bottom":644.5}},{"unicode":65408,"advance":0.5,"planeBounds":{"left":-0.012999999999999966,"top":-0.86458333333333326,"right":0.48699999999999999,"bottom":0.11458333333333333},"atlasBounds":{"left":743.5,"top":195.5,"right":767.5,"bottom":242.5}},{"unicode":65409,"advance":0.5,"planeBounds":{"left":0.0010000000000000391,"top":-0.82291666666666663,"right":0.501,"bottom":0.09375},"atlasBounds":{"left":757.5,"top":384.5,"right":781.5,"bottom":428.5}},{"unicode":65410,"advance":0.5,"planeBounds":{"left":-0.016416666666666621,"top":-0.78125,"right":0.50441666666666674,"bottom":0.09375},"atlasBounds":{"left":830.5,"top":474.5,"right":855.5,"bottom":516.5}},{"unicode":65411,"advance":0.5,"planeBounds":{"left":0.0010000000000000356,"top":-0.78125,"right":0.501,"bottom":0.09375},"atlasBounds":{"left":724.5,"top":518.5,"right":748.5,"bottom":560.5}},{"unicode":65412,"advance":0.5,"planeBounds":{"left":0.095500000000000029,"top":-0.80208333333333326,"right":0.47050000000000003,"bottom":0.072916666666666657},"atlasBounds":{"left":595.5,"top":518.5,"right":613.5,"bottom":560.5}},{"unicode":65413,"advance":0.5,"planeBounds":{"left":0.0020000000000000391,"top":-0.82291666666666663,"right":0.502,"bottom":0.09375},"atlasBounds":{"left":604.5,"top":384.5,"right":628.5,"bottom":428.5}},{"unicode":65414,"advance":0.5,"planeBounds":{"left":0.0010000000000000338,"top":-0.69791666666666663,"right":0.501,"bottom":-0.010416666666666666},"atlasBounds":{"left":534.5,"top":726.5,"right":558.5,"bottom":759.5}},{"unicode":65415,"advance":0.5,"planeBounds":{"left":0.0053333333333333661,"top":-0.76041666666666663,"right":0.46366666666666667,"bottom":0.09375},"atlasBounds":{"left":469.5,"top":561.5,"right":491.5,"bottom":602.5}},{"unicode":65416,"advance":0.5,"planeBounds":{"left":-0.0034166666666666946,"top":-0.82291666666666663,"right":0.51741666666666664,"bottom":0.09375},"atlasBounds":{"left":151.5,"top":384.5,"right":176.5,"bottom":428.5}},{"unicode":65417,"advance":0.5,"planeBounds":{"left":0.014250000000000009,"top":-0.78124999999999989,"right":0.45174999999999998,"bottom":0.072916666666666657},"atlasBounds":{"left":182.5,"top":561.5,"right":203.5,"bottom":602.5}},{"unicode":65418,"advance":0.5,"planeBounds":{"left":-0.037750000000000034,"top":-0.73958333333333326,"right":0.52474999999999994,"bottom":0.052083333333333336},"atlasBounds":{"left":545.5,"top":686.5,"right":572.5,"bottom":724.5}},{"unicode":65419,"advance":0.5,"planeBounds":{"left":0.046166666666666682,"top":-0.80208333333333326,"right":0.46283333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":182.5,"top":518.5,"right":202.5,"bottom":560.5}},{"unicode":65420,"advance":0.5,"planeBounds":{"left":0.031833333333333352,"top":-0.71875,"right":0.49016666666666669,"bottom":0.09375},"atlasBounds":{"left":682.5,"top":645.5,"right":704.5,"bottom":684.5}},{"unicode":65421,"advance":0.5,"planeBounds":{"left":-0.034750000000000038,"top":-0.69791666666666663,"right":0.52774999999999994,"bottom":0.03125},"atlasBounds":{"left":695.5,"top":59.5,"right":722.5,"bottom":94.5}},{"unicode":65422,"advance":0.5,"planeBounds":{"left":-0.020333333333333301,"top":-0.80208333333333326,"right":0.52133333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":829.5,"top":384.5,"right":855.5,"bottom":426.5}},{"unicode":65423,"advance":0.5,"planeBounds":{"left":-0.00091666666666671403,"top":-0.71875,"right":0.51991666666666669,"bottom":0.09375},"atlasBounds":{"left":830.5,"top":244.5,"right":855.5,"bottom":283.5}},{"unicode":65424,"advance":0.5,"planeBounds":{"left":0.037166666666666688,"top":-0.80208333333333337,"right":0.45383333333333331,"bottom":0.09375},"atlasBounds":{"left":701.5,"top":429.5,"right":721.5,"bottom":472.5}},{"unicode":65425,"advance":0.5,"planeBounds":{"left":-0.021333333333333371,"top":-0.80208333333333337,"right":0.52033333333333331,"bottom":0.09375},"atlasBounds":{"left":602.5,"top":429.5,"right":628.5,"bottom":472.5}},{"unicode":65426,"advance":0.5,"planeBounds":{"left":-8.3333333333289515e-05,"top":-0.80208333333333337,"right":0.47908333333333336,"bottom":0.09375},"atlasBounds":{"left":578.5,"top":429.5,"right":601.5,"bottom":472.5}},{"unicode":65427,"advance":0.5,"planeBounds":{"left":3.7303493627405262e-17,"top":-0.73958333333333326,"right":0.5,"bottom":0.072916666666666657},"atlasBounds":{"left":425.5,"top":645.5,"right":449.5,"bottom":684.5}},{"unicode":65428,"advance":0.5,"planeBounds":{"left":-0.023833333333333321,"top":-0.82291666666666663,"right":0.51783333333333326,"bottom":0.09375},"atlasBounds":{"left":536.5,"top":338.5,"right":562.5,"bottom":382.5}},{"unicode":65429,"advance":0.5,"planeBounds":{"left":-0.010416666666666656,"top":-0.71874999999999989,"right":0.51041666666666674,"bottom":0.010416666666666666},"atlasBounds":{"left":723.5,"top":59.5,"right":748.5,"bottom":94.5}},{"unicode":65430,"advance":0.5,"planeBounds":{"left":0.035750000000000032,"top":-0.73958333333333326,"right":0.47325,"bottom":0.052083333333333336},"atlasBounds":{"left":119.5,"top":686.5,"right":140.5,"bottom":724.5}},{"unicode":65431,"advance":0.5,"planeBounds":{"left":0.024333333333333373,"top":-0.78125,"right":0.48266666666666669,"bottom":0.09375},"atlasBounds":{"left":721.5,"top":474.5,"right":743.5,"bottom":516.5}},{"unicode":65432,"advance":0.5,"planeBounds":{"left":0.046083333333333351,"top":-0.80208333333333337,"right":0.44191666666666668,"bottom":0.09375},"atlasBounds":{"left":266.5,"top":474.5,"right":285.5,"bottom":517.5}},{"unicode":65433,"advance":0.5,"planeBounds":{"left":-0.031250000000000049,"top":-0.78124999999999989,"right":0.53124999999999989,"bottom":0.072916666666666657},"atlasBounds":{"left":618.5,"top":561.5,"right":645.5,"bottom":602.5}},{"unicode":65434,"advance":0.5,"planeBounds":{"left":0.042333333333333362,"top":-0.78124999999999989,"right":0.5006666666666667,"bottom":0.072916666666666657},"atlasBounds":{"left":244.5,"top":561.5,"right":266.5,"bottom":602.5}},{"unicode":65435,"advance":0.5,"planeBounds":{"left":0.023833333333333349,"top":-0.73958333333333326,"right":0.48216666666666669,"bottom":0.052083333333333336},"atlasBounds":{"left":573.5,"top":686.5,"right":595.5,"bottom":724.5}},{"unicode":65436,"advance":0.5,"planeBounds":{"left":0.018416666666666685,"top":-0.73958333333333337,"right":0.49758333333333332,"bottom":0.09375},"atlasBounds":{"left":616.5,"top":604.5,"right":639.5,"bottom":644.5}},{"unicode":65437,"advance":0.5,"planeBounds":{"left":0.017916666666666674,"top":-0.78124999999999989,"right":0.49708333333333332,"bottom":0.072916666666666657},"atlasBounds":{"left":594.5,"top":561.5,"right":617.5,"bottom":602.5}},{"unicode":65438,"advance":0.5,"planeBounds":{"left":-0.02566666666666665,"top":-0.90625,"right":0.3076666666666667,"bottom":-0.57291666666666663},"atlasBounds":{"left":839.5,"top":96.5,"right":855.5,"bottom":112.5}},{"unicode":65439,"advance":0.5,"planeBounds":{"left":-0.022333333333333309,"top":-0.90625,"right":0.26933333333333337,"bottom":-0.55208333333333326},"atlasBounds":{"left":817.5,"top":225.5,"right":831.5,"bottom":242.5}},{"unicode":65441,"advance":0.92000000000000004,"planeBounds":{"left":0.13708333333333328,"top":-0.65625,"right":0.78291666666666659,"bottom":-0.09375},"atlasBounds":{"left":617.5,"top":827.5,"right":648.5,"bottom":854.5}},{"unicode":65442,"advance":0.92000000000000004,"planeBounds":{"left":0.089583333333333362,"top":-0.65625,"right":0.86041666666666672,"bottom":-0.09375},"atlasBounds":{"left":579.5,"top":827.5,"right":616.5,"bottom":854.5}},{"unicode":65443,"advance":0.92000000000000004,"planeBounds":{"left":0.090666666666666687,"top":-0.67708333333333326,"right":0.8823333333333333,"bottom":-0.072916666666666671},"atlasBounds":{"left":817.5,"top":195.5,"right":855.5,"bottom":224.5}},{"unicode":65444,"advance":0.92000000000000004,"planeBounds":{"left":0.13716666666666666,"top":-0.65625,"right":0.80383333333333329,"bottom":-0.09375},"atlasBounds":{"left":546.5,"top":827.5,"right":578.5,"bottom":854.5}},{"unicode":65445,"advance":0.92000000000000004,"planeBounds":{"left":0.10749999999999998,"top":-0.65625,"right":0.85750000000000004,"bottom":-0.072916666666666671},"atlasBounds":{"left":141.5,"top":827.5,"right":177.5,"bottom":855.5}},{"unicode":65446,"advance":0.92000000000000004,"planeBounds":{"left":0.11383333333333333,"top":-0.67708333333333326,"right":0.8221666666666666,"bottom":-0.052083333333333329},"atlasBounds":{"left":294.5,"top":764.5,"right":328.5,"bottom":794.5}},{"unicode":65447,"advance":0.92000000000000004,"planeBounds":{"left":0.14358333333333331,"top":-0.65625,"right":0.78941666666666666,"bottom":-0.09375},"atlasBounds":{"left":328.5,"top":827.5,"right":359.5,"bottom":854.5}},{"unicode":65448,"advance":0.92000000000000004,"planeBounds":{"left":0.1103333333333333,"top":-0.65625,"right":0.81866666666666665,"bottom":-0.09375},"atlasBounds":{"left":466.5,"top":827.5,"right":500.5,"bottom":854.5}},{"unicode":65449,"advance":0.92000000000000004,"planeBounds":{"left":0.13366666666666663,"top":-0.67708333333333326,"right":0.80033333333333323,"bottom":-0.072916666666666671},"atlasBounds":{"left":781.5,"top":764.5,"right":813.5,"bottom":793.5}},{"unicode":65450,"advance":0.92000000000000004,"planeBounds":{"left":0.098916666666666639,"top":-0.67708333333333337,"right":0.82808333333333328,"bottom":-0.09375},"atlasBounds":{"left":292.5,"top":827.5,"right":327.5,"bottom":855.5}},{"unicode":65451,"advance":0.92000000000000004,"planeBounds":{"left":0.10391666666666662,"top":-0.67708333333333337,"right":0.83308333333333329,"bottom":-0.09375},"atlasBounds":{"left":256.5,"top":827.5,"right":291.5,"bottom":855.5}},{"unicode":65452,"advance":0.92000000000000004,"planeBounds":{"left":0.094499999999999987,"top":-0.67708333333333326,"right":0.84450000000000003,"bottom":-0.072916666666666671},"atlasBounds":{"left":651.5,"top":764.5,"right":687.5,"bottom":793.5}},{"unicode":65453,"advance":0.92000000000000004,"planeBounds":{"left":0.098166666666666666,"top":-0.67708333333333326,"right":0.88983333333333325,"bottom":-0.072916666666666671},"atlasBounds":{"left":612.5,"top":764.5,"right":650.5,"bottom":793.5}},{"unicode":65454,"advance":0.92000000000000004,"planeBounds":{"left":0.098499999999999963,"top":-0.67708333333333337,"right":0.84849999999999992,"bottom":-0.09375},"atlasBounds":{"left":104.5,"top":827.5,"right":140.5,"bottom":855.5}},{"unicode":65455,"advance":0.92000000000000004,"planeBounds":{"left":0.10249999999999997,"top":-0.67708333333333337,"right":0.85249999999999992,"bottom":-0.09375},"atlasBounds":{"left":67.5,"top":827.5,"right":103.5,"bottom":855.5}},{"unicode":65456,"advance":0.92000000000000004,"planeBounds":{"left":0.10100000000000002,"top":-0.69791666666666663,"right":0.85099999999999998,"bottom":-0.072916666666666671},"atlasBounds":{"left":819.5,"top":726.5,"right":855.5,"bottom":756.5}},{"unicode":65457,"advance":0.92000000000000004,"planeBounds":{"left":0.13116666666666665,"top":-0.65625,"right":0.79783333333333328,"bottom":-0.09375},"atlasBounds":{"left":360.5,"top":827.5,"right":392.5,"bottom":854.5}},{"unicode":65458,"advance":0.92000000000000004,"planeBounds":{"left":0.14108333333333328,"top":-0.67708333333333326,"right":0.7869166666666666,"bottom":-0.072916666666666671},"atlasBounds":{"left":519.5,"top":764.5,"right":550.5,"bottom":793.5}},{"unicode":65459,"advance":0.92000000000000004,"planeBounds":{"left":0.1153333333333333,"top":-0.67708333333333337,"right":0.82366666666666666,"bottom":-0.09375},"atlasBounds":{"left":821.5,"top":518.5,"right":855.5,"bottom":546.5}},{"unicode":65460,"advance":0.92000000000000004,"planeBounds":{"left":0.11016666666666662,"top":-0.67708333333333326,"right":0.90183333333333326,"bottom":-0.072916666666666671},"atlasBounds":{"left":480.5,"top":764.5,"right":518.5,"bottom":793.5}},{"unicode":65461,"advance":0.92000000000000004,"planeBounds":{"left":0.10041666666666667,"top":-0.67708333333333326,"right":0.82958333333333323,"bottom":-0.072916666666666671},"atlasBounds":{"left":745.5,"top":764.5,"right":780.5,"bottom":793.5}},{"unicode":65462,"advance":0.92000000000000004,"planeBounds":{"left":0.051333333333333328,"top":-0.65625,"right":0.8846666666666666,"bottom":-0.09375},"atlasBounds":{"left":649.5,"top":827.5,"right":689.5,"bottom":854.5}},{"unicode":65463,"advance":0.92000000000000004,"planeBounds":{"left":0.12716666666666665,"top":-0.67708333333333326,"right":0.79383333333333328,"bottom":-0.072916666666666671},"atlasBounds":{"left":814.5,"top":764.5,"right":846.5,"bottom":793.5}},{"unicode":65464,"advance":0.92000000000000004,"planeBounds":{"left":0.11333333333333334,"top":-0.67708333333333326,"right":0.82166666666666666,"bottom":-0.072916666666666671},"atlasBounds":{"left":688.5,"top":764.5,"right":722.5,"bottom":793.5}},{"unicode":65465,"advance":0.92000000000000004,"planeBounds":{"left":0.048833333333333312,"top":-0.67708333333333326,"right":0.88216666666666665,"bottom":-0.072916666666666671},"atlasBounds":{"left":439.5,"top":764.5,"right":479.5,"bottom":793.5}},{"unicode":65466,"advance":0.92000000000000004,"planeBounds":{"left":0.10733333333333331,"top":-0.71874999999999989,"right":0.81566666666666665,"bottom":-0.052083333333333329},"atlasBounds":{"left":19.5,"top":764.5,"right":53.5,"bottom":796.5}},{"unicode":65467,"advance":0.92000000000000004,"planeBounds":{"left":0.12516666666666665,"top":-0.67708333333333326,"right":0.79183333333333328,"bottom":-0.072916666666666671},"atlasBounds":{"left":0.5,"top":797.5,"right":32.5,"bottom":826.5}},{"unicode":65468,"advance":0.92000000000000004,"planeBounds":{"left":0.13616666666666666,"top":-0.67708333333333337,"right":0.80283333333333329,"bottom":-0.09375},"atlasBounds":{"left":178.5,"top":827.5,"right":210.5,"bottom":855.5}},{"unicode":65469,"advance":0.92000000000000004,"planeBounds":{"left":0.11083333333333328,"top":-0.65625,"right":0.8191666666666666,"bottom":-0.072916666666666671},"atlasBounds":{"left":821.5,"top":645.5,"right":855.5,"bottom":673.5}},{"unicode":65470,"advance":0.92000000000000004,"planeBounds":{"left":0.12124999999999998,"top":-0.71875,"right":0.80874999999999997,"bottom":-0.03125},"atlasBounds":{"left":500.5,"top":726.5,"right":533.5,"bottom":759.5}},{"unicode":65474,"advance":0.92000000000000004,"planeBounds":{"left":0.34008333333333335,"top":-0.86458333333333326,"right":0.73591666666666666,"bottom":0.13541666666666669},"atlasBounds":{"left":562.5,"top":96.5,"right":581.5,"bottom":144.5}},{"unicode":65475,"advance":0.92000000000000004,"planeBounds":{"left":0.25916666666666671,"top":-0.86458333333333326,"right":0.67583333333333329,"bottom":0.13541666666666669},"atlasBounds":{"left":541.5,"top":96.5,"right":561.5,"bottom":144.5}},{"unicode":65476,"advance":0.92000000000000004,"planeBounds":{"left":0.32000000000000001,"top":-0.86458333333333326,"right":0.69500000000000006,"bottom":0.13541666666666669},"atlasBounds":{"left":151.5,"top":195.5,"right":169.5,"bottom":243.5}},{"unicode":65477,"advance":0.92000000000000004,"planeBounds":{"left":0.26808333333333334,"top":-0.86458333333333326,"right":0.6639166666666666,"bottom":0.13541666666666669},"atlasBounds":{"left":0.5,"top":195.5,"right":19.5,"bottom":243.5}},{"unicode":65478,"advance":0.92000000000000004,"planeBounds":{"left":0.24658333333333329,"top":-0.86458333333333326,"right":0.64241666666666664,"bottom":0.13541666666666669},"atlasBounds":{"left":92.5,"top":195.5,"right":111.5,"bottom":243.5}},{"unicode":65479,"advance":0.92000000000000004,"planeBounds":{"left":0.18408333333333332,"top":-0.86458333333333326,"right":0.70491666666666664,"bottom":0.13541666666666669},"atlasBounds":{"left":20.5,"top":195.5,"right":45.5,"bottom":243.5}},{"unicode":65482,"advance":0.92000000000000004,"planeBounds":{"left":0.24808333333333329,"top":-0.86458333333333326,"right":0.64391666666666669,"bottom":0.13541666666666669},"atlasBounds":{"left":627.5,"top":96.5,"right":646.5,"bottom":144.5}},{"unicode":65483,"advance":0.92000000000000004,"planeBounds":{"left":0.18058333333333329,"top":-0.86458333333333326,"right":0.70141666666666669,"bottom":0.13541666666666669},"atlasBounds":{"left":149.5,"top":146.5,"right":174.5,"bottom":194.5}},{"unicode":65484,"advance":0.92000000000000004,"planeBounds":{"left":0.00066666666666662349,"top":-0.69791666666666663,"right":0.91733333333333322,"bottom":-0.13541666666666666},"atlasBounds":{"left":421.5,"top":827.5,"right":465.5,"bottom":854.5}},{"unicode":65485,"advance":0.92000000000000004,"planeBounds":{"left":0.0081666666666666051,"top":-0.86458333333333326,"right":0.92483333333333317,"bottom":0.13541666666666669},"atlasBounds":{"left":582.5,"top":96.5,"right":626.5,"bottom":144.5}},{"unicode":65486,"advance":0.92000000000000004,"planeBounds":{"left":0.013916666666666635,"top":-0.86458333333333326,"right":0.86808333333333332,"bottom":0.13541666666666669},"atlasBounds":{"left":224.5,"top":146.5,"right":265.5,"bottom":194.5}},{"unicode":65487,"advance":0.92000000000000004,"planeBounds":{"left":0.048666666666666622,"top":-0.86458333333333326,"right":0.84033333333333327,"bottom":0.13541666666666669},"atlasBounds":{"left":112.5,"top":195.5,"right":150.5,"bottom":243.5}},{"unicode":65490,"advance":0.92000000000000004,"planeBounds":{"left":0.007166666666666606,"top":-0.69791666666666663,"right":0.92383333333333317,"bottom":-0.11458333333333334},"atlasBounds":{"left":211.5,"top":827.5,"right":255.5,"bottom":855.5}},{"unicode":65491,"advance":0.92000000000000004,"planeBounds":{"left":0.0066666666666666237,"top":-0.57291666666666663,"right":0.92333333333333323,"bottom":-0.010416666666666666},"atlasBounds":{"left":501.5,"top":827.5,"right":545.5,"bottom":854.5}},{"unicode":65492,"advance":0.92000000000000004,"planeBounds":{"left":0.032833333333333277,"top":-0.86458333333333326,"right":0.86616666666666664,"bottom":0.13541666666666669},"atlasBounds":{"left":764.5,"top":146.5,"right":804.5,"bottom":194.5}},{"unicode":65493,"advance":0.92000000000000004,"planeBounds":{"left":0.026416666666666658,"top":-0.86458333333333326,"right":0.88058333333333327,"bottom":0.13541666666666669},"atlasBounds":{"left":722.5,"top":146.5,"right":763.5,"bottom":194.5}},{"unicode":65494,"advance":0.92000000000000004,"planeBounds":{"left":0.049749999999999996,"top":-0.86458333333333326,"right":0.86225000000000007,"bottom":0.13541666666666669},"atlasBounds":{"left":682.5,"top":146.5,"right":721.5,"bottom":194.5}},{"unicode":65495,"advance":0.92000000000000004,"planeBounds":{"left":0.00066666666666662349,"top":-0.57291666666666663,"right":0.91733333333333322,"bottom":-0.010416666666666666},"atlasBounds":{"left":718.5,"top":827.5,"right":762.5,"bottom":854.5}},{"unicode":65498,"advance":0.92000000000000004,"planeBounds":{"left":0.00066666666666662349,"top":-0.46875,"right":0.91733333333333322,"bottom":-0.30208333333333337},"atlasBounds":{"left":598.5,"top":797.5,"right":642.5,"bottom":805.5}},{"unicode":65499,"advance":0.92000000000000004,"planeBounds":{"left":0.032249999999999973,"top":-0.84374999999999989,"right":0.84474999999999989,"bottom":0.13541666666666669},"atlasBounds":{"left":656.5,"top":195.5,"right":695.5,"bottom":242.5}},{"unicode":65500,"advance":0.92000000000000004,"planeBounds":{"left":0.38116666666666671,"top":-0.86458333333333326,"right":0.54783333333333328,"bottom":0.13541666666666669},"atlasBounds":{"left":140.5,"top":146.5,"right":148.5,"bottom":194.5}},{"unicode":65504,"advance":1,"planeBounds":{"left":0.19149999999999998,"top":-0.82291666666666663,"right":0.8165,"bottom":0.09375},"atlasBounds":{"left":686.5,"top":338.5,"right":716.5,"bottom":382.5}},{"unicode":65505,"advance":1,"planeBounds":{"left":0.18308333333333332,"top":-0.78125,"right":0.82891666666666663,"bottom":0.052083333333333336},"atlasBounds":{"left":328.5,"top":604.5,"right":359.5,"bottom":644.5}},{"unicode":65506,"advance":1,"planeBounds":{"left":0.072833333333333292,"top":-0.57291666666666663,"right":0.90616666666666668,"bottom":-0.15625},"atlasBounds":{"left":145.5,"top":797.5,"right":185.5,"bottom":817.5}},{"unicode":65507,"advance":1,"planeBounds":{"left":-0.052083333333333259,"top":-0.92708333333333337,"right":1.0520833333333333,"bottom":-0.78125},"atlasBounds":{"left":697.5,"top":797.5,"right":750.5,"bottom":804.5}},{"unicode":65508,"advance":1,"planeBounds":{"left":0.42708333333333331,"top":-0.88541666666666663,"right":0.57291666666666663,"bottom":0.11458333333333333},"atlasBounds":{"left":170.5,"top":195.5,"right":177.5,"bottom":243.5}},{"unicode":65509,"advance":1,"planeBounds":{"left":0.13491666666666663,"top":-0.78125,"right":0.86408333333333331,"bottom":0.052083333333333336},"atlasBounds":{"left":158.5,"top":645.5,"right":193.5,"bottom":685.5}},{"unicode":65510,"advance":1,"planeBounds":{"left":0.041666666666666664,"top":-0.78125,"right":0.95833333333333326,"bottom":0.052083333333333336},"atlasBounds":{"left":185.5,"top":604.5,"right":229.5,"bottom":644.5}},{"unicode":65512,"advance":0.5,"planeBounds":{"left":0.16666666666666669,"top":-0.88541666666666674,"right":0.33333333333333337,"bottom":0.32291666666666663},"atlasBounds":{"left":70.5,"top":0.5,"right":78.5,"bottom":58.5}},{"unicode":65513,"advance":0.5,"planeBounds":{"left":-0.036250000000000018,"top":-0.65625,"right":0.52625,"bottom":-0.09375},"atlasBounds":{"left":690.5,"top":827.5,"right":717.5,"bottom":854.5}},{"unicode":65514,"advance":0.5,"planeBounds":{"left":0.02083333333333335,"top":-0.90624999999999989,"right":0.47916666666666669,"bottom":0.13541666666666669},"atlasBounds":{"left":505.5,"top":0.5,"right":527.5,"bottom":50.5}},{"unicode":65515,"advance":0.5,"planeBounds":{"left":-0.026250000000000016,"top":-0.65625,"right":0.53625,"bottom":-0.09375},"atlasBounds":{"left":393.5,"top":827.5,"right":420.5,"bottom":854.5}},{"unicode":65516,"advance":0.5,"planeBounds":{"left":0.02083333333333335,"top":-0.90624999999999989,"right":0.47916666666666669,"bottom":0.13541666666666669},"atlasBounds":{"left":650.5,"top":0.5,"right":672.5,"bottom":50.5}},{"unicode":65517,"advance":0.5,"planeBounds":{"left":0.020833333333333343,"top":-0.61458333333333337,"right":0.47916666666666669,"bottom":-0.15625},"atlasBounds":{"left":33.5,"top":797.5,"right":55.5,"bottom":819.5}},{"unicode":65518,"advance":0.5,"planeBounds":{"left":0.020333333333333373,"top":-0.61458333333333326,"right":0.47866666666666668,"bottom":-0.13541666666666666},"atlasBounds":{"left":820.5,"top":827.5,"right":842.5,"bottom":850.5}}],"kerning":[]}
diff --git a/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.png b/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.png
new file mode 100644
index 0000000..d865662
Binary files /dev/null and b/tests/Resources/Typefaces/NotoSansJP/NotoSansJP-Regular.png differ
diff --git a/tests/Resources/Typefaces/WorkSans/WorkSans-Italic.json b/tests/Resources/Typefaces/WorkSans/WorkSans-RegularItalic.json
similarity index 100%
rename from tests/Resources/Typefaces/WorkSans/WorkSans-Italic.json
rename to tests/Resources/Typefaces/WorkSans/WorkSans-RegularItalic.json
diff --git a/tests/Resources/Typefaces/WorkSans/WorkSans-Italic.png b/tests/Resources/Typefaces/WorkSans/WorkSans-RegularItalic.png
similarity index 100%
rename from tests/Resources/Typefaces/WorkSans/WorkSans-Italic.png
rename to tests/Resources/Typefaces/WorkSans/WorkSans-RegularItalic.png
diff --git a/tests/SampleGameBase.cs b/tests/SampleGameBase.cs
index b3f1606..f23a9d6 100644
--- a/tests/SampleGameBase.cs
+++ b/tests/SampleGameBase.cs
@@ -1,3 +1,4 @@
+using kajarlabs.osu.Framework.MsdfTextRendering.IO.Stores;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.IO.Stores;
@@ -7,11 +8,28 @@ namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests;
public abstract partial class SampleGameBase : Game
{
private DependencyContainer? dependencies;
+ private MsdfFontStore? msdfFonts;
[BackgroundDependencyLoader]
private void load()
{
Resources.AddStore(new NamespacedResourceStore(new DllResourceStore(typeof(SampleGameBase).Assembly), "Resources"));
+ MsdfTextRendering.CreateCapabilities(Resources);
+
+ msdfFonts = new MsdfFontStore(Host.Renderer, Shaders);
+
+ foreach (var weight in new[] { "Thin", "ExtraLight", "Light", "Regular", "Medium", "SemiBold", "Bold", "Black" })
+ {
+ AddFont(Resources, $"Fonts/WorkSans/WorkSans-{weight}");
+ AddFont(Resources, $"Fonts/WorkSans/WorkSans-{weight}Italic");
+
+ msdfFonts.AddFont(Resources, $"WorkSans", weight);
+ msdfFonts.AddFont(Resources, $"WorkSans", $"{weight}Italic");
+ }
+
+ msdfFonts.AddFont(Resources, $"NotoSansJP", "Regular");
+
+ dependencies?.CacheAs(msdfFonts);
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
diff --git a/tests/Sprites/TestSceneMsdfSpriteTextPresence.cs b/tests/Sprites/TestSceneMsdfSpriteTextPresence.cs
new file mode 100644
index 0000000..f7db8ba
--- /dev/null
+++ b/tests/Sprites/TestSceneMsdfSpriteTextPresence.cs
@@ -0,0 +1,108 @@
+using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites;
+using kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual;
+using NUnit.Framework;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osuTK.Graphics;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Sprites;
+
+[HeadlessTest]
+public partial class TestSceneMsdfSpriteTextPresence : SampleTestScene
+{
+ [Test]
+ public void TestNormalSpriteText()
+ {
+ Container container = null!;
+ MsdfSpriteText text = null!;
+
+ AddStep("reset", () =>
+ {
+ Child = container = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Children =
+ [
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Red.Opacity(0.3f)
+ },
+ text = new MsdfSpriteText
+ {
+ Text = "Hello world!",
+ Font = new FontUsage("WorkSans", 12, "Regular"),
+ }
+ ]
+ };
+ });
+
+ AddAssert("is present", () => text.IsPresent);
+ AddAssert("height == 12", () => Precision.AlmostEquals(12, container.Height));
+ AddStep("empty text", () => text.Text = string.Empty);
+ AddAssert("not present", () => !text.IsPresent);
+ AddAssert("height == 0", () => Precision.AlmostEquals(0, container.Height));
+ }
+
+ [Test]
+ public void TestAlwaysPresentSpriteText()
+ {
+ Container container = null!;
+ MsdfSpriteText text = null!;
+
+ AddStep("reset", () =>
+ {
+ Child = container = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Children =
+ [
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Red.Opacity(0.3f)
+ },
+ text = new AlwaysPresentSpriteText
+ {
+ Text = "Hello world!",
+ Font = new FontUsage(size: 12),
+ }
+ ]
+ };
+ });
+
+ AddAssert("is present", () => text.IsPresent);
+ AddAssert("height == 12", () => Precision.AlmostEquals(12, container.Height));
+ AddStep("empty text", () => text.Text = string.Empty);
+ AddAssert("is present", () => text.IsPresent);
+ AddAssert("height == 0", () => Precision.AlmostEquals(0, container.Height));
+ }
+
+ [Test]
+ public void TestPresenceRemainsTheSameDuringFlow()
+ {
+ AddStep("reset", () =>
+ {
+ Child = new FillFlowContainer
+ {
+ Child = new MsdfSpriteText()
+ };
+ });
+
+ AddWaitStep("wait for some update frames", 2);
+ }
+
+ private partial class AlwaysPresentSpriteText : MsdfSpriteText
+ {
+ public override bool IsPresent => true;
+ }
+}
diff --git a/tests/VIsual/Containers/TestSceneMsdfTextFlowContainer.cs b/tests/VIsual/Containers/TestSceneMsdfTextFlowContainer.cs
new file mode 100644
index 0000000..8f015b3
--- /dev/null
+++ b/tests/VIsual/Containers/TestSceneMsdfTextFlowContainer.cs
@@ -0,0 +1,215 @@
+using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Containers;
+using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites;
+using kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual.Sprites;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Logging;
+using osuTK;
+using osuTK.Graphics;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual.Containers;
+
+public partial class TestSceneMsdfTextFlowContainer : MsdfComparisonTestScene
+{
+ private const float demo_font_size = 20.0f;
+ private const float flow_width = 380.0f;
+
+ private const string multi_paragraph_text =
+ "The quick brown fox jumps over the lazy dog. A swift movement across the tranquil meadow.\n\n" +
+ "Second paragraph with more descriptive text to demonstrate multi-line wrapping and paragraph spacing.\n" +
+ "This line follows a single newline break.";
+
+ private const string cjk_and_urls_text =
+ "Visit https://osu.ppy.sh/home or browse \\local\\storage\\path. " +
+ "We also support hyphen-based and anti-aliased text splitting. " +
+ "CJK support: こんにちは世界!(Hello World) and 日本語の文章です。";
+
+ private static readonly FontUsage default_fu = FontUsage.Default.With(family: "WorkSans", size: demo_font_size, weight: "Regular");
+
+ private TextFlowContainer leftFlow = null!;
+ private MsdfTextFlowContainer rightFlow = null!;
+
+ protected override string LeftLabel => "TextFlowContainer (without MSDF, bitmap WorkSans)";
+ protected override string RightLabel => "MsdfTextFlowContainer (with MSDF)";
+
+ protected override Drawable CreateLeftSample() => new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Child = leftFlow = new TextFlowContainer(s => s.Font = default_fu)
+ {
+ Width = flow_width,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ };
+
+ protected override Drawable CreateRightSample() => new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Child = rightFlow = new MsdfTextFlowContainer(s => s.Font = default_fu)
+ {
+ Width = flow_width,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ };
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AddStep("Default multi-paragraph", () =>
+ {
+ resetProperties();
+ leftFlow.Text = multi_paragraph_text;
+ rightFlow.Text = multi_paragraph_text;
+ });
+
+ AddStep("AddText (\\n is paragraph)", () =>
+ {
+ resetProperties();
+ leftFlow.Clear();
+ rightFlow.Clear();
+
+ leftFlow.AddText("Line 1\nLine 2\nLine 3");
+ rightFlow.AddText("Line 1\nLine 2\nLine 3");
+
+ Schedule(logFlowMetrics);
+ });
+
+ AddStep("AddParagraph (\\n is line break)", () =>
+ {
+ resetProperties();
+ leftFlow.Clear();
+ rightFlow.Clear();
+
+ leftFlow.AddParagraph("Line 1\nLine 2\nLine 3");
+ rightFlow.AddParagraph("Line 1\nLine 2\nLine 3");
+ });
+
+ foreach (var anchor in new[]
+ {
+ Anchor.TopLeft, Anchor.TopCentre, Anchor.TopRight,
+ Anchor.CentreLeft, Anchor.Centre, Anchor.CentreRight,
+ Anchor.BottomLeft, Anchor.BottomCentre, Anchor.BottomRight
+ })
+ {
+ AddStep($"Anchor: {anchor}", () =>
+ {
+ leftFlow.TextAnchor = anchor;
+ rightFlow.TextAnchor = anchor;
+ });
+ }
+
+ AddStep("Indents: FirstLine=40, Content=20", () =>
+ {
+ resetProperties();
+ leftFlow.FirstLineIndent = 40;
+ leftFlow.ContentIndent = 20;
+ rightFlow.FirstLineIndent = 40;
+ rightFlow.ContentIndent = 20;
+ leftFlow.Text = multi_paragraph_text;
+ rightFlow.Text = multi_paragraph_text;
+ });
+
+ AddStep("Spacing: Para=2.0, Line=10", () =>
+ {
+ resetProperties();
+ leftFlow.ParagraphSpacing = 2.0f;
+ leftFlow.LineSpacing = 10f;
+ rightFlow.ParagraphSpacing = 2.0f;
+ rightFlow.LineSpacing = 10f;
+ leftFlow.Text = multi_paragraph_text;
+ rightFlow.Text = multi_paragraph_text;
+ });
+
+ AddStep("URLs, Hyphens & CJK", () =>
+ {
+ resetProperties();
+ leftFlow.Text = cjk_and_urls_text;
+ rightFlow.Text = cjk_and_urls_text;
+ });
+
+ AddStep("Mixed manual instances", () =>
+ {
+ resetProperties();
+ leftFlow.Clear();
+ rightFlow.Clear();
+
+ leftFlow.AddText("Before manual: ");
+ leftFlow.AddText(new SpriteText
+ {
+ Text = "[MANUAL INSTANCE]",
+ Colour = Color4.Gold,
+ Font = default_fu
+ });
+ leftFlow.AddText(" - After manual text continuation.");
+
+ rightFlow.AddText("Before manual: ");
+ rightFlow.AddText(new MsdfSpriteText
+ {
+ Text = "[MANUAL INSTANCE]",
+ Colour = Color4.Gold,
+ Font = default_fu
+ });
+ rightFlow.AddText(" - After manual text continuation.");
+ });
+
+ AddStep("Baseline alignment (mixed sizes)", () =>
+ {
+ resetProperties();
+ leftFlow.Clear();
+ rightFlow.Clear();
+
+ leftFlow.AddText("Normal (20) ", s => s.Font = default_fu.With(size: 20));
+ leftFlow.AddText("Huge (36) ", s => s.Font = default_fu.With(size: 36));
+ leftFlow.AddText("Small (14) ", s => s.Font = default_fu.With(size: 14));
+ leftFlow.AddText("on same baseline.");
+
+ rightFlow.AddText("Normal (20) ", s => s.Font = default_fu.With(size: 20));
+ rightFlow.AddText("Huge (36) ", s => s.Font = default_fu.With(size: 36));
+ rightFlow.AddText("Small (14) ", s => s.Font = default_fu.With(size: 14));
+ rightFlow.AddText("on same baseline.");
+ });
+ }
+
+ private void logFlowMetrics()
+ {
+ Logger.Log($"--- left (native) flow.Height={leftFlow.DrawHeight:F3} ---");
+ foreach (var c in leftFlow.Children)
+ Logger.Log($" {c.GetType().Name,-20} '{(c as SpriteText)?.Text}' Y={c.Y,8:F3} H={c.DrawHeight,8:F3}");
+
+ Logger.Log($"--- right (msdf) flow.Height={rightFlow.DrawHeight:F3} ---");
+ foreach (var c in rightFlow.Children)
+ Logger.Log($" {c.GetType().Name,-20} '{(c as MsdfSpriteText)?.Text}' Y={c.Y,8:F3} H={c.DrawHeight,8:F3}");
+ }
+
+ private void resetProperties()
+ {
+ leftFlow.TextAnchor = Anchor.TopLeft;
+ rightFlow.TextAnchor = Anchor.TopLeft;
+
+ leftFlow.FirstLineIndent = 0;
+ rightFlow.FirstLineIndent = 0;
+
+ leftFlow.ContentIndent = 0;
+ rightFlow.ContentIndent = 0;
+
+ leftFlow.ParagraphSpacing = 0.5f;
+ rightFlow.ParagraphSpacing = 0.5f;
+
+ leftFlow.LineSpacing = 0;
+ rightFlow.LineSpacing = 0;
+
+ leftFlow.Spacing = Vector2.Zero;
+ rightFlow.Spacing = Vector2.Zero;
+ }
+}
+
diff --git a/tests/VIsual/MsdfComparisonTestScene.cs b/tests/VIsual/MsdfComparisonTestScene.cs
new file mode 100644
index 0000000..4ecc662
--- /dev/null
+++ b/tests/VIsual/MsdfComparisonTestScene.cs
@@ -0,0 +1,200 @@
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
+using osuTK;
+using osuTK.Graphics;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual;
+
+public abstract partial class SampleComparisonTestScene : SampleTestScene
+{
+ private const float top_bar_height = 40;
+ private const float min_zoom = 0.5f;
+ private const float max_zoom = 32f;
+
+ private readonly BindableNumber zoom = new(1f)
+ {
+ MinValue = min_zoom,
+ MaxValue = max_zoom,
+ Precision = 0.01f,
+ };
+
+ private readonly Bindable pan = new();
+
+ private SpriteText zoomValueText = null!;
+
+ protected abstract string LeftLabel { get; }
+ protected abstract string RightLabel { get; }
+
+ protected abstract Drawable CreateLeftSample();
+ protected abstract Drawable CreateRightSample();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children =
+ [
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.MidnightBlue,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Top = top_bar_height },
+ Children =
+ [
+ new ComparisonColumn(LeftLabel, CreateLeftSample(), zoom, pan)
+ {
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.X,
+ Width = 0.5f,
+ Padding = new MarginPadding { Top = 8 },
+ },
+ new ComparisonColumn(RightLabel, CreateRightSample(), zoom, pan)
+ {
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.X,
+ Width = 0.5f,
+ X = 0.5f,
+ Padding = new MarginPadding { Top = 8 },
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.Y,
+ RelativePositionAxes = Axes.X,
+ Width = 2,
+ X = 0.5f,
+ Origin = Anchor.TopCentre,
+ Colour = Color4.White,
+ Alpha = 0.25f,
+ },
+ ],
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = top_bar_height,
+ Children =
+ [
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ Alpha = 0.6f,
+ },
+ new SpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ X = 16,
+ Text = "Scroll to zoom, drag to pan",
+ Font = FontUsage.Default.With( size: 16, weight: "Regular"),
+ Colour = Color4.LightGray,
+ },
+ zoomValueText = new SpriteText
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ X = -84,
+ Font = FontUsage.Default.With(size: 18, weight: "Regular"),
+ },
+ new BasicButton
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ X = -8,
+ Width = 64,
+ Height = 28,
+ Text = "Reset",
+ Action = reset,
+ },
+ ],
+ },
+ ];
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ zoom.BindValueChanged(e => zoomValueText.Text = $"{e.NewValue:0.00}x", true);
+ }
+
+ protected override bool OnScroll(ScrollEvent e)
+ {
+ var factor = MathF.Pow(1.2f, e.ScrollDelta.Y);
+ zoom.Value = Math.Clamp(zoom.Value * factor, min_zoom, max_zoom);
+ return true;
+ }
+
+ protected override bool OnDragStart(DragStartEvent e) => true;
+
+ protected override void OnDrag(DragEvent e)
+ {
+ pan.Value += e.Delta;
+ base.OnDrag(e);
+ }
+
+ private void reset()
+ {
+ zoom.Value = 1f;
+ pan.Value = Vector2.Zero;
+ }
+
+ private partial class ComparisonColumn : Container
+ {
+ private readonly string label;
+ private readonly Drawable sample;
+ private readonly Bindable zoom;
+ private readonly Bindable pan;
+
+ private Container zoomArea = null!;
+
+ public ComparisonColumn(string label, Drawable sample, Bindable zoom, Bindable pan)
+ {
+ this.label = label;
+ this.sample = sample;
+ this.zoom = zoom.GetBoundCopy();
+ this.pan = pan.GetBoundCopy();
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Masking = true;
+
+ InternalChildren =
+ [
+ new SpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = label,
+ Font = FontUsage.Default.With(size: 20),
+ },
+ zoomArea = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Child = sample,
+ },
+ ];
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ zoom.BindValueChanged(e => zoomArea.Scale = new Vector2(e.NewValue), true);
+ pan.BindValueChanged(e => zoomArea.Position = e.NewValue, true);
+ }
+ }
+}
diff --git a/tests/VIsual/Sprites/MsdfComparisonTestScene.cs b/tests/VIsual/Sprites/MsdfComparisonTestScene.cs
new file mode 100644
index 0000000..a8af452
--- /dev/null
+++ b/tests/VIsual/Sprites/MsdfComparisonTestScene.cs
@@ -0,0 +1,200 @@
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
+using osuTK;
+using osuTK.Graphics;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual.Sprites;
+
+public abstract partial class MsdfComparisonTestScene : SampleTestScene
+{
+ private const float top_bar_height = 40;
+ private const float min_zoom = 0.5f;
+ private const float max_zoom = 32f;
+
+ private readonly BindableNumber zoom = new(1f)
+ {
+ MinValue = min_zoom,
+ MaxValue = max_zoom,
+ Precision = 0.01f,
+ };
+
+ private readonly Bindable pan = new();
+
+ private SpriteText zoomValueText = null!;
+
+ protected abstract string LeftLabel { get; }
+ protected abstract string RightLabel { get; }
+
+ protected abstract Drawable CreateLeftSample();
+ protected abstract Drawable CreateRightSample();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children =
+ [
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.MidnightBlue,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Top = top_bar_height },
+ Children =
+ [
+ new ComparisonColumn(LeftLabel, CreateLeftSample(), zoom, pan)
+ {
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.X,
+ Width = 0.5f,
+ Padding = new MarginPadding { Top = 8 },
+ },
+ new ComparisonColumn(RightLabel, CreateRightSample(), zoom, pan)
+ {
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.X,
+ Width = 0.5f,
+ X = 0.5f,
+ Padding = new MarginPadding { Top = 8 },
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.Y,
+ RelativePositionAxes = Axes.X,
+ Width = 2,
+ X = 0.5f,
+ Origin = Anchor.TopCentre,
+ Colour = Color4.White,
+ Alpha = 0.25f,
+ },
+ ],
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = top_bar_height,
+ Children =
+ [
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ Alpha = 0.6f,
+ },
+ new SpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ X = 16,
+ Text = "Scroll to zoom, drag to pan",
+ Font = FontUsage.Default.With(size: 16),
+ Colour = Color4.LightGray,
+ },
+ zoomValueText = new SpriteText
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ X = -84,
+ Font = FontUsage.Default.With(size: 18),
+ },
+ new BasicButton
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ X = -8,
+ Width = 64,
+ Height = 28,
+ Text = "Reset",
+ Action = reset,
+ },
+ ],
+ },
+ ];
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ zoom.BindValueChanged(e => zoomValueText.Text = $"{e.NewValue:0.00}x", true);
+ }
+
+ protected override bool OnScroll(ScrollEvent e)
+ {
+ var factor = MathF.Pow(1.2f, e.ScrollDelta.Y);
+ zoom.Value = Math.Clamp(zoom.Value * factor, min_zoom, max_zoom);
+ return true;
+ }
+
+ protected override bool OnDragStart(DragStartEvent e) => true;
+
+ protected override void OnDrag(DragEvent e)
+ {
+ pan.Value += e.Delta;
+ base.OnDrag(e);
+ }
+
+ private void reset()
+ {
+ zoom.Value = 1f;
+ pan.Value = Vector2.Zero;
+ }
+
+ private partial class ComparisonColumn : Container
+ {
+ private readonly string label;
+ private readonly Drawable sample;
+ private readonly Bindable zoom;
+ private readonly Bindable pan;
+
+ private Container zoomArea = null!;
+
+ public ComparisonColumn(string label, Drawable sample, Bindable zoom, Bindable pan)
+ {
+ this.label = label;
+ this.sample = sample;
+ this.zoom = zoom.GetBoundCopy();
+ this.pan = pan.GetBoundCopy();
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Masking = true;
+
+ InternalChildren =
+ [
+ new SpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = label,
+ Font = FontUsage.Default.With(size: 20),
+ },
+ zoomArea = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Child = sample,
+ },
+ ];
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ zoom.BindValueChanged(e => zoomArea.Scale = new Vector2(e.NewValue), true);
+ pan.BindValueChanged(e => zoomArea.Position = e.NewValue, true);
+ }
+ }
+}
diff --git a/tests/VIsual/Sprites/TestSceneMsdfItalic.cs b/tests/VIsual/Sprites/TestSceneMsdfItalic.cs
new file mode 100644
index 0000000..f2ef13d
--- /dev/null
+++ b/tests/VIsual/Sprites/TestSceneMsdfItalic.cs
@@ -0,0 +1,43 @@
+using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osuTK;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual.Sprites;
+
+public partial class TestSceneMsdfItalic : MsdfComparisonTestScene
+{
+ private const float demo_font_size = 28.0f;
+ private const string demo_text = "MSDF Sample Aa Oo 123";
+
+ protected override string LeftLabel => "SpriteText (without MSDF, bitmap WorkSans)";
+ protected override string RightLabel => "MsdfSpriteText (with MSDF)";
+
+ protected override Drawable CreateLeftSample() => createFlow(
+ text => new SpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = text },
+ (drawable, italics) => ((SpriteText)drawable).Font = new FontUsage("WorkSans", demo_font_size, "Regular", italics));
+
+ protected override Drawable CreateRightSample() => createFlow(
+ text => new MsdfSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = text },
+ (drawable, italics) => ((MsdfSpriteText)drawable).Font = new FontUsage("WorkSans", demo_font_size, "Regular", italics));
+
+ private static FillFlowContainer createFlow(Func create, Action setFont)
+ {
+ var upright = create($"Regular: {demo_text}");
+ var italic = create($"Italic: {demo_text}");
+
+ setFont(upright, false);
+ setFont(italic, true);
+
+ return new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Spacing = new Vector2(0, 16),
+ Children = [upright, italic],
+ };
+ }
+}
diff --git a/tests/VIsual/Sprites/TestSceneMsdfSpriteText.cs b/tests/VIsual/Sprites/TestSceneMsdfSpriteText.cs
new file mode 100644
index 0000000..d205fa1
--- /dev/null
+++ b/tests/VIsual/Sprites/TestSceneMsdfSpriteText.cs
@@ -0,0 +1,106 @@
+using kajarlabs.osu.Framework.MsdfTextRendering.Graphics.Sprites;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osuTK.Graphics;
+
+namespace kajarlabs.osu.Framework.MsdfTextRendering.Tests.Visual.Sprites;
+
+public partial class TestSceneMsdfSpriteText : SampleTestScene
+{
+ public TestSceneMsdfSpriteText()
+ {
+ FillFlowContainer flow;
+
+ Children =
+ [
+ new BasicScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children =
+ [
+ flow = new FillFlowContainer
+ {
+ Anchor = Anchor.TopLeft,
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical,
+ }
+ ]
+ }
+ ];
+
+ flow.Add(new MsdfSpriteText
+ {
+ Text = @"the quick red fox jumps over the lazy brown 子犬🐶"
+ });
+ flow.Add(new MsdfSpriteText
+ {
+ Text = @"THE QUICK RED FOX JUMPS OVER THE LAZY BROWN 子犬🐶"
+ });
+ flow.Add(new MsdfSpriteText
+ {
+ Text = @"0123456789!@#$%^&*()_-+-[]{}.,<>;'\"
+ });
+
+ flow.Add(new Container
+ {
+ Margin = new MarginPadding { Vertical = 5 },
+ AutoSizeAxes = Axes.Both,
+ Children =
+ [
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new MsdfSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = Color4.Black,
+ UseFullGlyphHeight = true,
+ Text = "UseFullGlyphHeight = true",
+ },
+ ]
+ });
+
+ flow.Add(new Container
+ {
+ Margin = new MarginPadding { Vertical = 5 },
+ AutoSizeAxes = Axes.Both,
+ Children =
+ [
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new MsdfSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = Color4.Black,
+ UseFullGlyphHeight = false,
+ Text = "UseFullGlyphHeight = false",
+ },
+ ]
+ });
+
+ for (var i = 1; i <= 200; i++)
+ {
+ var text = new MsdfSpriteText
+ {
+ Text = $@"Font testy at size {i}",
+ Font = new FontUsage(
+ "WorkSans",
+ i,
+ i % 4 > 1 ? "Bold" : "Regular",
+ i % 2 == 1),
+ AllowMultiline = true,
+ RelativeSizeAxes = Axes.X,
+ };
+
+ flow.Add(text);
+ }
+ }
+}