Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BitmapFontFactory #6600

Merged
merged 30 commits into from
May 17, 2020
Merged

BitmapFontFactory #6600

merged 30 commits into from
May 17, 2020

Conversation

ShukantPal
Copy link
Member

#6524

(follow up to #6597)

@bigtimebuddy wanted a simpler API. Instead of a dynamic caching layer beneath PIXI.Text, we opted for a static bitmap generation layer beneath PIXI.BitmapText. This has no crazy calculations for tiling and all that.

Description of change
  • @pixi/text-bitmap depends on @pixi/text for styling stuff. The factory should generate any
    ITextStyle.
Pre-Merge Checklist
  • Documentation is changed or added
  • Lint process passed (npm run lint)
  • Tests passed (npm run test)

Copy link
Member

@bigtimebuddy bigtimebuddy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please remove the package-lock.json files from the packages utils/loaders/polyfill/settings

@codecov-io
Copy link

codecov-io commented May 2, 2020

Codecov Report

Merging #6600 into dev will not change coverage.
The diff coverage is n/a.

Impacted file tree graph

@@           Coverage Diff           @@
##              dev    #6600   +/-   ##
=======================================
  Coverage   82.34%   82.34%           
=======================================
  Files          38       38           
  Lines        1903     1903           
=======================================
  Hits         1567     1567           
  Misses        336      336           

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4590b0b...6d2575d. Read the comment docs.

@ShukantPal
Copy link
Member Author

@bigtimebuddy Just did. Why not add packages/**/package-lock.json to .gitignore?

@ShukantPal ShukantPal marked this pull request as ready for review May 2, 2020 18:30
@ShukantPal
Copy link
Member Author

@bigtimebuddy Regarding performance, try running the demo locally. Although Stats.js shows 12 FPS, Chrome DevTools (locally) shows 55-70 FPS range.

Screen Shot 2020-05-02 at 2 36 43 PM

Copy link
Member

@bigtimebuddy bigtimebuddy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this is really awesome and I think is a much safer approach than gutting Text. Maybe in the future this can nudge more users to use this text rendering approach.

Also, what do you think about a convenience method for generating character ranges. For instance here are two ideas:

// Nice and verbose, requires a little more parsing in create
const font = BitmapFontFactory.create({
   chars: [['a', 'z'], ['A', 'Z'], ['0', '9']]
});

// or something like this, where you create a convenience function
const range = BitmapFontFactory.range;
const font = BitmapFontFactory.create({
   chars: range('a','z') + range('A','Z') + range('0','9')
});

One final thing: this needs some unit tests. Just to make sure that the data being generated is valid. Should be pretty easy to test.

packages/text-bitmap/src/BitmapFontFactory.ts Outdated Show resolved Hide resolved
packages/text-bitmap/src/BitmapFontFactory.ts Outdated Show resolved Hide resolved
packages/text-bitmap/src/BitmapFontFactory.ts Outdated Show resolved Hide resolved
packages/text-bitmap/src/BitmapFontFactory.ts Outdated Show resolved Hide resolved
@GoodBoyDigital
Copy link
Member

This is awesome!

Agree that functionality being on BitmapText is the best place!

  • standard Text - Flexible
  • bitmap Text - Speedy

What you have done is basically elevated bitmap text so that it is easy for everyone to use not just people who have hard to find and figure out font generation software!

How do you guys feel about simplifying the API even further and removing the need to actually create the font before its used? As well as exposing the BitmapFontUtils I think also doing it under the hood would be the best developer experience :)


// under here - we generate the texture and store in a cache  based on the text style passed in (defaults filled in minus not visual bits like alignment).

const text = new PIXI.BitmapText('Hello world! This is the new PIXI.BitmapText()', {
    font: 'serif',
    align: 'center' });

@bigtimebuddy
Copy link
Member

I’m not sure that the constructor for BitmapText also works for this without being a breaking change or too overloaded to catch errors. I think create and apply are two distinct steps requiring two lines of code.

For instance, there is not a great way to reconcile the font name if we can use the constructor. There are a couple font states for BitmapText: available system font, installed bitmap font, or missing font. And even if you have a bitmap font, when to regenerate seems like a question you may not want to do inside the constructor.

We could maybe simplify the API and do BitmapFont.create instead of BitmapFontFactory. But I think this needs to be two steps UNLESS we introduce a new display object like SimpleBitmapText, or something like that. Then we can do all the font generation automagically.

@GoodBoyDigital
Copy link
Member

GoodBoyDigital commented May 3, 2020

I’m not sure that the constructor for BitmapText also works for this without being a breaking change or too overloaded to catch errors. I think create and apply are two distinct steps requiring two lines of code.

For instance, there is not a great way to reconcile the font name if we can use the constructor. There are a couple font states for BitmapText: available system font, installed bitmap font, or missing font. And even if you have a bitmap font, when to regenerate seems like a question you may not want to do inside the constructor.

We could maybe simplify the API and do BitmapFont.create instead of BitmapFontFactory. But I think this needs to be two steps UNLESS we introduce a new display object like SimpleBitmapText, or something like that. Then we can do all the font generation automagically.

automagically love that! currently have my head in the whole zero config style thinking. If we can assume intent for 90% of users but keep it open enough to let people get specific then I think thats the sweet spot.

appreciate if it's a breaking change then maybe changing the API for BitmapText might be better off as a v6 thing...

something like?


// I don't know pixi, but daaaanng i love that I can use it so easily..

// new bitmap text...
const text = new BitmapText('omg so easy to use pixi fonts!', {
    font:  'Arial`,
    size: 32  
});

// same as current text..
const textAgain = new Text('omg still so easy to use pixi fonts!', {
    font:  'Arial`,
    size: 32  
});

// im a pixi power user yo! I know this stuff inside and out and want to control whats happening !

const font = BitmapFont.create({
    font:  'Arial`,
    fontSize: 33  
}) // (or from for consistency?)

const text = new BitmapText('omg so easy to use pixi fonts!', font); 

Like you say @bigtimebuddy , i guess the issue lies in if we don't want a breaking API change.. what does the interim API look like. creating a new class may be best here? Also, overloading constructors.. generally thats ok right?

This new text could accept BitmapFont | TextStyle | Object

@GoodBoyDigital
Copy link
Member

GoodBoyDigital commented May 3, 2020

ooh oh so BitmapFont.from could be a good way to go?


// make one on the fly..
const font = BitmapFont.from({
    font:  'Arial`,
    fontSize: 33  
}) 

or get one you already loaded..
const font = BitmapFont.from('font-preloaded');

@bigtimebuddy
Copy link
Member

bigtimebuddy commented May 3, 2020

Let’s start with BitmapFont.from and then look at future PRs to build convenience on top, add a new display object or break BitmapText to support this path as first class. Does that sound good @GoodBoyDigital?

// Provide the font name, so it can register automatically
BitmapFont.from("custom", {
  chars: "0123456789",
  resolution: 2,
  fontFamily: 'Arial'
});

// Reference the font by name
const counter = new BitmapText("0", { font: "custom" });

@GoodBoyDigital
Copy link
Member

cool, sounds good 👍

bigtimebuddy and others added 3 commits May 3, 2020 20:34
+ Support for character ranges (e.g. chars: [['a', 'z'], ['A', 'Z']]
+ Support for arrays of characters (e.g. chars: ['abcdef', 'GHIJK'])
+ Documented drawGlyph (local, internal function)
+ Mandatory arguments for BitmapFont.from()
Copy link
Member

@bigtimebuddy bigtimebuddy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work. Thanks for hitting the feedback, this is getting close. One small request below and still need a couple of unit tests for this. Some ideas to cover:

  • handling different chars variations (string, array, ranges)
  • supporting different resolutions
  • using default TextStyle options

packages/text-bitmap/src/BitmapFont.ts Show resolved Hide resolved
@ShukantPal
Copy link
Member Author

@bigtimebuddy Will do right now! I can't seem to get the next build here (https://pixijs.download/feature/bitmap-font-factory/pixi.js) - the BitmapFont.from method isn't showing. I cleared the cache on Firefox/Safari and it doesn't show up.

Could you see if it comes up?

@ShukantPal
Copy link
Member Author

@bigtimebuddy Could you deploy this branch so that the demos can be updated?

@ShukantPal
Copy link
Member Author

@bigtimebuddy I'm not sure how we would add unit tests for checking whether the glyphs rendered at the right "resolution".

A TextStyle object is constructed from the passed options so we should be using the defaults. How do you want to test that as well?

@bigtimebuddy
Copy link
Member

@cursedcoder do you know what Deploy Non-Tag Branch is not getting invoked with these branch changes? https://github.com/pixijs/pixi.js/pull/6600/checks?check_run_id=647208137

@bigtimebuddy
Copy link
Member

bigtimebuddy commented May 5, 2020

For resolution @SukantPal, render two glyphs at different resolutions and compare the font data dimension sizes.

We should probably be using the same defaults or, probably better, internally convert into a TextStyle object.

@ShukantPal
Copy link
Member Author

@bigtimebuddy

  • The texture sizes are same as the base textures resolution changes so texture frame stays same. The difference would be in the same real sizes.

  • the .from method creates a TextStyle object already from the passed options. But that object isn’t “saved” anywhere.

Copy link
Member

@themoonrat themoonrat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a font with no chars given at all should be an error.

The xAdvance being created for each char are to decimal places; no bitmap generator would ever give data in this way, so these should be rounded in some way. Might get blurry text when rendering to decimal positions.

The texture rects being created are also incorrectly to decimal places

The 'page' in the data for each char just gives the id of the char itself. It should say which sheet # the text is found on. So if generating takes up 2 base textures, the page should either be '0' or '1'. Useful to know how many textures this font generation is requiring.

Should these Textures and BaseTexture be added to the caches so we know they've been created? Like they are for textures created in the Text class?

Visual issue with drop shadows AND stroke on text: https://www.pixiplayground.com/#/edit/nvZj_usqdMGPmDbfLIFUR
Issues if Italic is used: https://www.pixiplayground.com/#/edit/Y6pgxZRlVMR5qND9igixc

@ShukantPal
Copy link
Member Author

ShukantPal commented May 12, 2020

@bigtimebuddy @themoonrat Hey Dave, I wanted to first touch upon the issue with drop-shadows. I think the "buggy" look of the stroke + drop-shadow will still stay there because the browser renders it like that.

  • PIXI.Text does not suffer from the stroke being hidden by the drop-shadow because it uses two passes - and the first pass renders the drop-shadow separately.

  • This fiddle shows a stroke + drop-shadow used directly on a Canvas2D context. It isn't perfect - and that's why BitmapText won't be either. https://jsfiddle.net/ShukantPal/qtxozr1s/

  • This a BitmapText (top) vs Text (comparison): https://jsfiddle.net/ShukantPal/5qoxwcak/

Screen Shot 2020-05-12 at 10 59 39 AM

Screen Shot 2020-05-12 at 11 00 47 AM

  • You can see that BitmapText looks more like the Canvas 2D render. This is the atlas generated by the BitmapText for your pixi-playground:

Screen Shot 2020-05-12 at 10 08 19 AM

  • Since we render glyphs one-by-one the sometimes the stroke is rendered over the drop-shadow. This accounts for those subtle differences b/w BitmapText and Canvas2D.

  • BitmapText cannot handle scenarios where characters overlap. This is because

    • all glyphs strokes are supposed to be rendered before all glyph fills
    • but we are rendering each glyph (stroke+fill) sequentially
    • and we don't want to cache the stroke & fill separately (too complicated, takes 2x space, BitmapFontData doesn't support it)
  • Side note: Your pixi-playground links has a little blurry BitmapText (w.r.t the canvas text) because you didn't set the resolution of the BitmapText. If you add a resolution: window.devicePixelRatio || 1 to the IBitmapFontOptions, it would look better.

@themoonrat
Copy link
Member

Regarding drop shadows, yeps, I understand what you're saying, that's fine. No different to if you created a bitmap font now with drop shadows, then tries to place them close to each other so they overlapped :) I worried that it was being caused by the the glyphs in the atlas themselves perhaps overlapping due to the drop shadow.

@ShukantPal
Copy link
Member Author

@themoonrat Okay, so:

  • italics should be working now

  • if no characters are resolved, then an error is thrown

  • I round up texture frames for glyphs and xadvance

@ShukantPal
Copy link
Member Author

@themoonrat Regarding the pages => char mapping issue, I need more information on how BitmapFont uses those pages & the textures array. TBH, I'm confused b/w (x,y) & (xoffset, yoffset) in the font data.

@themoonrat
Copy link
Member

So, taking a look at https://pixijs.io/examples/#/text/bitmap-text.js
https://pixijs.io/examples/examples/assets/bitmap-font/desyrel.xml describing https://pixijs.io/examples/examples/assets/bitmap-font/desyrel.png

In this bitmap font, there is only 1 image atlas, desyrel.png, so the page is '0' for all of the characters.

If there were 2 image atlases, say, a desyrel1.png and a desyrel2.png, then some of the characters would have a page 0, and some a page 1.

So in this scenario, where you do
// Create new atlas once current has filled up
increment a pageIndex and use that as the page value

@ShukantPal
Copy link
Member Author

@themoonrat Okay, but another question: what is (x, y) and (xoffset, yoffset)?

@themoonrat
Copy link
Member

When bitmap fonts are generated, in the xml, the x, y, width and height are to tightly draw a box over each glyph in the image. Now, how do you then know how to draw each glyph with the same baseline? That's where yoffset comes in. It lets you know how much to adjust the y position when drawing that glpyh. So a lowercase z will have a bigger y offset than an uppercase Z

http://www.angelcode.com/products/bmfont/doc/render_text.html

In the atlases produced here, each item drawn is with the same baseline, so that kinda solves not needing to have a yoffset. The fact that when doing our measuring, width wise there is empty spacing around the edge, solves not needing to have a xoffset.

But that does mean that they work in slightly different ways. With regular bitmap fonts, each glyph data is tight around the character with xoffset and yoffset to normalise their positions. Whereas this 'from' method, each glyph data is not tight around each character, and instead has the padding build it.
I think we're ok with that? But if you measure the of a Sprite for an Arial 30px bitmap, or Arial 30px generated, those widths would be quite different, so I don't know if that's a potential compatibility issue. I'm not brave enough to make that call :D

@ShukantPal
Copy link
Member Author

@themoonrat OK, I'll try changing the pages mapping to per-BaseTexture instead of per-glyph 😄

@themoonrat
Copy link
Member

PS, I found a way to effectively turn all numbers fixed width
https://www.pixiplayground.com/#/edit/-VOO6_S1qQoVcbvBDUwi9
So it's easily to go into the font data and edit without requiring any extra support Pixi wise :)

@ShukantPal
Copy link
Member Author

@bigtimebuddy @themoonrat I also got the issue with glyph pages resolved. You should see fewer different pages in the whole character set. This should be it!

@themoonrat
Copy link
Member

Looks good!

My only remaining question is for @bigtimebuddy, regarding whether these generated base textures & textures should be added to the BaseTextureCache and TextureCache. Normal Text has it's textures added there, which I find useful to be able to track asset usage.

I believe it's just the 'adding' to the cache required, as the 'destroy' function in BitmapFont would already handle the removing? Tho the destroy function for bitmap text never destroys the base texture?

But yeah, just clarity on what to do that wise. I'm super happy with how the class works now

@bigtimebuddy
Copy link
Member

bigtimebuddy commented May 13, 2020

@themoonrat, this implementation is somewhat consistent with current approach to handling BitmapFonts, that each glyph Texture is created by and destroyed by BitmapFont. It's the responsibility of the font to maintain the references (i.e., not added to TextureCache).

The BaseTextures (page textures), however, are added to the cache if it's create from the Loader. BitmapFont.from here doesn't do that. How to reconcile?

In general, Pixi should follow a "campsite rule", which is, if a class creates garbage, it should also destroy it. Graphics, for example, only destroys geometry if it was created internally and not passed by reference.

That's a longwinded way to say that I think we need to fix this. I believe BitmapFont should maintain reference to page textures and we should provide a way in the destroy method (like a boolean similar to Sprite) whether to destroy the page textures as well. This change seems a maybe little out of scope for this PR? However, the page textures created with BitmapFont.from should probably be added to the caches so that it's consistent (however poorly executed) with BitmapFonts via the Loader.

What do you think @SukantPal?

@ShukantPal
Copy link
Member Author

ShukantPal commented May 13, 2020

@bigtimebuddy A g r e e. But I think a general BitmapFont.destroy would be better suited than changing BitmapFont.from - there is really no difference b/w loading a bitmap font vs generation one at runtime. The textures can be destroyed in the same manner. But destroying fonts isn't really a priority (in my opinion) for most use cases of bitmap fonts.

@bigtimebuddy
Copy link
Member

Are you proposing adding a static destroy method BitmapFont.destroy? We don't really have that pattern anywhere in Pixi. Everything is an instance destroy.

Destroying actually is probably more important for this workflow in-case you want to build something on top of this PR that auto-generates the font based on characters. I think I'll do a follow-up to improve this. I think your work here is done @SukantPal. You've put a lot of effort into this and we super appreciate it.

Scrolling back, here are some future PRs related to this:

  • Make BitmapFont destroy cleanup base textures from Loader or from Bitmap.from
  • Improve ergonomics around regenerating fonts (destroy to remove from available)
  • Clean-up code duplication of generateFillStyle

@ShukantPal
Copy link
Member Author

@bigtimebuddy I was talking about an instance destroy, not static. But it is really a trivial difference.

@themoonrat
Copy link
Member

Happy for follow up PRs to handle the missing bits.
Thanks @SukantPal :)

@bigtimebuddy bigtimebuddy added the ✅ Ready To Merge Helpful when issues are in the queue waiting to get merged. This means the PR is completed and has t label May 13, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✅ Ready To Merge Helpful when issues are in the queue waiting to get merged. This means the PR is completed and has t
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants