Coder Social home page Coder Social logo

Feedback / suggestions about textfit HOT 6 CLOSED

strml avatar strml commented on May 30, 2024
Feedback / suggestions

from textfit.

Comments (6)

STRML avatar STRML commented on May 30, 2024

Hey @joshribakoff,

Thanks for the feedback.

I don't want to fix the corner case with line-heights because the plugin is intended to preserve line-heights. Unless I am misunderstanding, the fix would obliterate any present line-height.

As for running on resize, if you bind to window resize (window.addEventListener('resize', function() { ... })) you won't have any trouble with resize being fired by the plugin. I believe the 'resize' event you're catching is actually a jQuery addition and doesn't actually exist in the DOM.

This library actually isn't asynchronous. A completion callback wouldn't make sense.

As for that browser bug, I've seen it before (and put it in Chromium's bug tracker). I'll give it some more thought. Do you have a jsfiddle that is broken by this that you could show me?

from textfit.

joshribakoff avatar joshribakoff commented on May 30, 2024

the fix would obliterate any present line-height.

The whole point of textFit is that I want to "obliterate" my font-size (and line-height). Checkout the screenshot I emailed you. I attribute the aesthetic text to modifying line-height. It depends on your use case. The plugin could support both use cases with a setting to disable it. If you wanted to make it really extensible you could have a strategy method allowing the user to define a strategy (modify other stuff besides just font-size) or define their own "test" (other than the "innerSpan.offsetWidth <= originalWidth"....).

Edit - ok it seems here the issue is I didn't know about setting line-height to a percentage value, let me play around some more.

As for running on resize, if you bind to window resize (window.addEventListener('resize', function() { ... })) you won't have any trouble with resize being fired by the plugin. I believe the 'resize' event you're catching is actually a jQuery addition and doesn't actually exist in the DOM.

I'm binding to the resize event of the "parent" div. textFit increases the inner span height which will increase the size of it's parent div (unless it is set to overflow:hidden, the div's height increases to accommodate the larger span - or in the case of overflow:scroll; the scrollbar's toggling enabled/disabled state appears to fire resize events).

In simpler terms, the "trying of different font sizes" sometimes causes "resize" events on the parent div. I need to bind to the parent div, and not the window. The reason is the page reflows for other reasons than resizing the window. Maybe I'm toggling a jQuery sidebar, or I have it in an Angular app where a column has ng-show/ng-hide. In any case, other elements on the page can cause reflows without necessarily firing a "window" resize. My concern then is how do I respond to these reflows & notify "textFit" to redraw my text?

This library actually isn't asynchronous. A completion callback wouldn't make sense.

Technically your demo is since it uses setInterval(). But you're right the real version is synchronous. But I'm right that it still takes time, and I wish to call a function that takes time from an asynchronous event.

Can you show me then an example of how to bind it to the "resize" event without any race conditions, and without having to use lodash's "Throtttle" method to workaround the fact the plugin takes time? It needs a callback to do so I think. This way I can unbind the event until the callback completes, then rebind the event. My concern with the "throttle" approach is even if I set it to let's say 100ms, someone could run the app on a slow computer where drawing the text takes longer.

As for that browser bug, I've seen it before (and put it in Chromium's bug tracker). I'll give it some more thought. Do you have a jsfiddle that is broken by this that you could show me?

Your JSFiddle is very helpful, I was having issues coming up with my own JSFiddle. I'll try to play around with some ideas on workarounds for the issue & report back with results.

from textfit.

joshribakoff avatar joshribakoff commented on May 30, 2024

To simulate the browser bug in a consistent manner I hardcoded a "+1" in the "test" where it checks if the text is within bounds. I then modified the code so that fitted the text properly in the presence of the bug.

I also removed my line-height hacks & set line-height: 100% in the CSS which scales the line-height correctly as the fonts change.

I found that as long as I use the version of your script with the while loop instead of setInterval it behaves in a synchronous manner & can be called without throttling.. I think I was testing with the "slow" version that you modified to behave asynchronously when I saw those race conditions. So that is solved now.

Also in my screenshot I sent you you maybe saw I was working at micro font sizes. As a part of my fix, I modified the script to have an "adjustment Amount" that I set to 0.1 instead of 1.0 that you had. This makes a world of difference & fixes issues where up to 25% of my "height" goes unused because it doesn't try out fractional sizes.

Here's the part of the code I modified, the binary search:

 // Binary search for best fit
            var previousValueOfInnerSpanOffsetWidth = false;
            var previousValueOfInnerSpanOffsetHeight = false;
            var adjustmentAmount = 0.1;

            while ( low <= high) {
                mid = parseFloat((low + high) / 2, 10);
                innerSpan.style.fontSize = mid + 'px';

                var withinHeightBounds = innerSpan.offsetHeight <= originalHeight;
                var withinWidthBounds = innerSpan.offsetWidth <= originalWidth;
                var withinBounds = withinWidthBounds && (settings.widthOnly || withinHeightBounds);

                // if there was a previous w/h, and the width has stayed the same & the height has decreased, and we're stil inside the bounds
                if(previousValueOfInnerSpanOffsetWidth !== false && previousValueOfInnerSpanOffsetHeight !== false &&
                    innerSpan.offsetHeight <= previousValueOfInnerSpanOffsetHeight &&
                    previousValueOfInnerSpanOffsetWidth === innerSpan.offsetWidth && withinHeightBounds
                ) {
                    console.info('chromium issue #289511 detected, treating as within bounds instead of outside');
                    low = mid + adjustmentAmount;
                } else if(withinBounds){
                    low = mid + adjustmentAmount;
                } else {
                    high = mid - adjustmentAmount;
                }

                previousValueOfInnerSpanOffsetWidth = innerSpan.offsetWidth;
                previousValueOfInnerSpanOffsetHeight = innerSpan.offsetHeight;
            }
            // Sub 1 at the very end, this is closer to what we wanted.
            //innerSpan.style.fontSize = (mid - 1) + 'px';

As you see I also commented out the part where it subtracts 1px at the end. This was part of the issue with it leaving up to 25% unused height when your div is small like mine.

My "new feature" (sub-pixel fonts) could be improved by adjusting the adjustment amount. It could start with an adjustment amount of let's say 1px. Once it detects that low & high are within 1px of eachother, it decreases the adjustment amount by a "meta adjustment amount" & continues, until low & high are within some configurable threshold of each other. (Eg. it makes adjustments of 1px until its detected the best fit to a precision of 1px, then it starts making 0.1px adjustments until it's within 0.1px of the best fit, it could then continue with 0.01px adjusments, depending on a user-defined precision. For now its sufficient for my purposes so I'm not going to modify any more.

from textfit.

joshribakoff avatar joshribakoff commented on May 30, 2024

@STRML Hopefully I'm not annoying you with my "feedback" ;)

I lied when I said I was done hackin on it. It still has rounding issues where it makes bad use of height, here's a demonstration... even after increasing it's precision by an order of magnitude as shown above, it leaves a "blank last line"... Here's my parent div that I need my text to fill up (div.theText). This is the element I call the plugin on:
menu_176

After the plugin sizes up the text, I can see the span is not utilizing the full height:
menu_177

Margin of error: ~50px

Here's a possible solution I'm playing around with:

// Binary search for best fit
            var previousValueOfInnerSpanOffsetWidth = false;
            var previousValueOfInnerSpanOffsetHeight = false;
            var adjustmentAmount = 0.00000000000001;

            innerSpan.style.opacity = 0;
            while ( low <= high) {
                mid = parseFloat((low + high) / 2, 10);
                innerSpan.style.fontSize = mid + 'px';

                var innerSpanOffsetHeight = innerSpan.offsetHeight;
                var innerSpanOffsetWidth = innerSpan.offsetWidth;
                var withinHeightBounds =  innerSpanOffsetHeight  <= originalHeight;
                var withinWidthBounds = innerSpanOffsetWidth <= originalWidth;
                var withinBounds = withinWidthBounds && (settings.widthOnly || withinHeightBounds);

                var totalAreaOutsideBounds =  (innerSpanOffsetHeight - originalHeight) *
                    (innerSpanOffsetWidth <= originalWidth);

                // shouldn't the algorithm stop here?!
                if(totalAreaOutsideBounds >= -1  && totalAreaOutsideBounds <= 3 && Math.round(high-low) === 0) {
                    break;
                }

                // if there was a previous w/h, and the width has stayed the same & the height has decreased, and we're stil inside the bounds
                if(previousValueOfInnerSpanOffsetWidth !== false && previousValueOfInnerSpanOffsetHeight !== false &&
                    innerSpanOffsetHeight <= previousValueOfInnerSpanOffsetHeight &&
                    previousValueOfInnerSpanOffsetWidth === innerSpanOffsetWidth && withinHeightBounds
                ) {
                    //console.info('chromium issue #289511 detected, treating as within bounds instead of outside');
                    low = mid + adjustmentAmount;
                } else if(withinBounds){
                    low = mid + adjustmentAmount;
                } else {
                    high = mid - adjustmentAmount;
                }

                previousValueOfInnerSpanOffsetWidth = innerSpanOffsetWidth;
                previousValueOfInnerSpanOffsetHeight = innerSpanOffsetHeight;
            }
            // Sub 1 at the very end, this is closer to what we wanted.
            innerSpan.style.fontSize = (mid + adjustmentAmount) + 'px';
            innerSpan.style.opacity = 1;

Basically, I changed the adjustment amount to 1-e15, which is the most precision that I can see inside Chrome's debugger. I also hide the text while we're sizing it around by setting it's opacity to 0.

When playing around in the debugger now, and setting breakpoints on the 3 lines that can either increment or decrement mid, I see that the binary search works well until we get to smaller precisions. The "test" of doing low <= high does not work well for small fine tunings. It continues oscillating between going 1px out of bounds, and ~ -30px within bounds, and seems to unpredictably settle on -30px within bounds before it gets to 0px exactly in bounds. I don't think we have enough floating point precision for the binary search to always find the perfect 0px within bounds match every time, due to rounding errors & such.

I added a piece of logic that short circuits the binary search if the "total area within bounds" is within some acceptable threshold of an "exact match". This stops the binary search early as soon as it finds it something that's within a given threshold. This allows the algorithm to stop searching when it finds a "suitable fit" rather than the "perfect fit" which may not be found before we round out of floating point precision. (eg. in the case where the search is oscillating between 1px too big, and -50px too small, it'll short circuit on the acceptable margin of error, and prevent the chance of the binary search stopping before it ever finds the best fit & risking a 50/50 chance of getting the larger margin of error)

After these changes, the text always fills up my div for a perfect "visual fit". I even found that some of the fonts include extra space for letters like "g" that dip below line-height. Since I allow the 3px overflow on the height, it actually cancels this out & gives "the perfect fit" every time (knock on wood before I go down another 12hr debugging session lol!):

Here my div is 117px tall
menu_178

My short circuit stopped the search as soon as it found something 3px 'too tall' (120px) which is actually "visually perfect" & more performant than trying to find the mathematically "perfect" fit.
menu_179

Margin of error: 3px

Edit: FYI the 1-e15 adjustment amount is not needed most likely. The key is setting this small enough to prevent the binary search from ending too early (0.0001 is likely fine), but then have a short circuit that stops the binary search from oscillating too long & running out of hairs to split due to rounding. With the right tuning of these parameters I believe you'll achieve the true "perfect fit" that we're all after.

from textfit.

joshribakoff avatar joshribakoff commented on May 30, 2024

Still finding more corner cases with the "last line" being blank. I think this time I finally fixed it!

Basically I set the binary search back to a precision of 1px. Then after that is done, I have a system of 10 passes that make subsequently smaller adjustments to the font-size (+/- 1px, +/- 0.1px, +/- 0.01px, etc..) Here's the progress so far below. Since the container will not be divisible by a font-size that gives a perfect fit, I'm going to try to use translateY() to stretch it into place as a final step of the algorithm.

selection_181

menu_182

Updated Code:

// Binary search for best fit
            var previousValueOfInnerSpanOffsetWidth = false;
            var previousValueOfInnerSpanOffsetHeight = false;
            var adjustmentAmount = 1;

            var innerSpanOffsetHeight;
            var innerSpanOffsetWidth;
            var withinHeightBounds;
            var withinWidthBounds;
            var withinBounds;
            var totalAreaOutsideBounds;
            var totalPercentageOutsideBounds;

            //innerSpan.style.opacity = 0;
            while ( low <= high || (Math.round(high-low)==0 && totalPercentageOutsideBounds < 0.1 )) {
                mid = parseFloat((low + high) / 2, 10);
                innerSpan.style.fontSize = mid + 'px';

                innerSpanOffsetHeight = innerSpan.offsetHeight;
                innerSpanOffsetWidth = innerSpan.offsetWidth;
                withinHeightBounds =  innerSpanOffsetHeight  <= originalHeight;
                withinWidthBounds = innerSpanOffsetWidth <= originalWidth;
                withinBounds = withinWidthBounds && (settings.widthOnly || withinHeightBounds);

                totalAreaOutsideBounds =  (innerSpanOffsetHeight - originalHeight) *
                    (innerSpanOffsetWidth <= originalWidth);

                totalPercentageOutsideBounds = totalAreaOutsideBounds / (originalHeight*originalWidth);

                // if there was a previous w/h, and the width has stayed the same & the height has decreased, and we're stil inside the bounds
                if(previousValueOfInnerSpanOffsetWidth !== false && previousValueOfInnerSpanOffsetHeight !== false &&
                    innerSpanOffsetHeight <= previousValueOfInnerSpanOffsetHeight &&
                    previousValueOfInnerSpanOffsetWidth === innerSpanOffsetWidth && withinHeightBounds
                ) {
                    //console.info('chromium issue #289511 detected, treating as within bounds instead of outside');
                    low = mid + adjustmentAmount;
                } else if(withinBounds){
                    low = mid + adjustmentAmount;
                } else {
                    high = mid - adjustmentAmount;
                }

                previousValueOfInnerSpanOffsetWidth = innerSpanOffsetWidth;
                previousValueOfInnerSpanOffsetHeight = innerSpanOffsetHeight;
            }
            console.log('Binary search finished @ font-size',mid, totalAreaOutsideBounds, 'px overflow is present');

            var fontMicroStepSize = 1;
            var passNum = 0;
            var lastFontSize = false;

            while(passNum <= 10 && Math.abs(totalAreaOutsideBounds) > 1) {
                passNum++;
                fontMicroStepSize /= 10;
                while (totalAreaOutsideBounds > 0) {
                    mid -= fontMicroStepSize;
                    innerSpan.style.fontSize = mid + 'px';

                    innerSpanOffsetHeight = innerSpan.offsetHeight;
                    innerSpanOffsetWidth = innerSpan.offsetWidth;
                    withinHeightBounds = innerSpanOffsetHeight <= originalHeight;
                    withinWidthBounds = innerSpanOffsetWidth <= originalWidth;
                    withinBounds = withinWidthBounds && (settings.widthOnly || withinHeightBounds);

                    totalAreaOutsideBounds = (innerSpanOffsetHeight - originalHeight) *
                    (innerSpanOffsetWidth <= originalWidth);

                    console.log('adjust -=', fontMicroStepSize, 'to ', mid, 'with overflow', totalAreaOutsideBounds);
                }
                while (totalAreaOutsideBounds < 0) {
                    mid += fontMicroStepSize;
                    innerSpan.style.fontSize = mid + 'px';

                    innerSpanOffsetHeight = innerSpan.offsetHeight;
                    innerSpanOffsetWidth = innerSpan.offsetWidth;
                    withinHeightBounds = innerSpanOffsetHeight <= originalHeight;
                    withinWidthBounds = innerSpanOffsetWidth <= originalWidth;
                    withinBounds = withinWidthBounds && (settings.widthOnly || withinHeightBounds);

                    totalAreaOutsideBounds = (innerSpanOffsetHeight - originalHeight) *
                    (innerSpanOffsetWidth <= originalWidth);

                    console.log('adjust +=', fontMicroStepSize, 'to ', mid, 'with overflow', totalAreaOutsideBounds);
                }
                console.log('Micro adjust passNum', passNum, ' w/ precision',fontMicroStepSize,' finished @ font-size',mid, totalAreaOutsideBounds, 'px overflow is present');
                console.log('delta for pass was ', mid-lastFontSize);
                lastFontSize = mid;
            }
            console.log('Micro adjust done & finished @ font-size',mid, totalAreaOutsideBounds, 'px overflow is present');


            innerSpan.style.opacity = 1;

from textfit.

joshribakoff avatar joshribakoff commented on May 30, 2024

This is the new code that goes at the end of that script, stretches it into place with CSS to workaround containers that have no ideal font-size that is divisible for a perfect fit:

selection_183

var totalHeightOutsideBounds = totalAreaOutsideBounds; // @todo fix this...

            var stretchY = originalHeight/innerSpanOffsetHeight;
            var offsetY = -1*(totalHeightOutsideBounds/2);

            //console.log(stretchY, offsetY);

            innerSpan.style.opacity = 1;
            innerSpan.style.transform = 'matrix(1.000, -0.000, 0.000, '+stretchY+', 0.000, '+offsetY+')';

from textfit.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.