Muuri: Rounding problem

Created on 1 Apr 2020  ·  20Comments  ·  Source: haltu/muuri

The scenario

I have two grids in which to exchange items. The items take on different sizes based on the grid that contains them. In both cases the dimensions are relative (e.g. width: 80% in the first grid, width: 30% in the second grid).

When an item is dragged, it is inserted into the dragContainer and I have to fix its size (e.g. width: 100px). If an item is dragged into the other grid, I have to calculate what dimensions it would have and apply them.

The problem

Since rounding up the dimensions could break the layout I have to use

layout: {
  rounding: false
}

The problem is that I can't accurately calculate the size of the item. The dimension values ​​given to me by the DOM are already rounded. Even if the error in the calculation is of some fraction of a pixel it is enough to break the layout.

Possible solution

The best solution would be to set Muuri to round the dimensions down instead of up, It should be enough to replace Math.round with Math.floor here.

This will also remove any possible layout breakage due to the use of relative dimensions (using rounding), do you think it's a feasible change?

bug ready

All 20 comments

The layout.rounding = false is meant to be used exactly for these situations where you need to use relative dimensions, is it failing somehow?

I remember I investigated quite a lot how different browsers handle sub-pixel rendering and Math.round() was the best choice overall IMHO at that time at least. Math.round() does not round the values only up, it also rounds the value down if it's less than 0.5. Also, Muuri uses getBoundingClientRect() to retrieve item dimensions which actually returns sub-pixel (not rounded) values.

layout.rounding = true is only advised to use when you apply exact (pixel) dimensions for your items, when sub-pixel rendering is not an issue.

Could you show a live demo of the problem? It might shed more light to this issue.

Using layout.rounding = false works as expected.

In the case above the only solution I see is to use layout.rounding = true, but it has a problem with relative dimensions.

For example, if I use items with width: calc (100% / 3), when this dimension is rounded down there will be 3 items per row, when it is rounded up there will be 2 items per row.

Here is an example, try resizing the window to change the size of the items.

In summary, the problem occurs with layout.rounding=true and relative dimensions.

The case is not very common but the solution is so simple that I have proposed it to you (already tried with Math.floor and it works).

@Paol-imi I'm still not understanding _why_ you can't use the layout.rounding = false. I'm actually thinking the whole rounding feature should probably be deprecated as it usually leads to more confusion than necessary _and_ all dimensions (relative or absolute) _should_ work when layout.rounding is set to false.

That being said I actually noticed a little issue in your demo when I set layout.rounding = false. When I tried to resize the window very quickly sometimes the last item was positioned to the bottom right instead of bottom left. Not sure if it's an issue with Muuri itself or your muuri-react lib, but I'll try to take a look at that.

If you really really need to manipulate the item sizes just for the layout algorithm you can feed the packer manipulated values: https://codepen.io/niklasramo/pen/xPoVPe

Thanks for the advice, I think I will manage it as in the example.

This problem that you have encountered is the reason why I can't use layout.rounding = false.

I reproduced the example on codepen and the problem still appears when the window is resized (but less often).

The effect is the same as if the height of the 4th block was greater than the 5th and 6th. The layout seems "broken", but the 9th position is on the left just because the block is positioned lower.

I have associated this problem with the rounding of item sizes by doing some tests, but there is probably more to investigate.

Thanks for all the fantastic work you are doing for Muuri, I can't wait to see the new version.

Yep, that issue definitely should not happen when rounding is disabled, so if that happens with vanilla Muuri too it's a bug which needs to be fixed.

I did other tests and maybe I found it.

Could the issue be linked to this and margin: 2%;?

I'm pretty sure it's a problem with the way IEEE 754 floating point math works (0.1 + 0.2 !== 0.3). I've tried to work around it in the Packer logic, but I guess there are still some bits to work out.

@Paol-imi After a little more testing it seems that the problem may actually be somewhere else that the Packer's logic or in any rounding problems. I created a modified test case of your example where the grid's item dimensions are force refreshed and the grid layout is recalculated every second: https://codepen.io/niklasramo/pen/BaNebmN. In this demo the item positions get corrected by the forced refresh loop after they are falsely positioned sometimes, which indicates that the problem is actually in the layoutOnResize logic or somewhere in that direction.

Could you test if you can break the layout in any way and make it stay broken while the force refresh loop is running? If that is possible then there is _also_ a problem in the layout calculations.

I don't think the problem is related to layoutOnResize, rather I think there are dimensions that can break the layout, and increasing the refresh rate increases the chances that the layout is calculated with these dimensions.

Another clue that prompts me to think about this is that the problem always appears with the first item on the third row, regardless of the number of items per row.


I tried to get the width of the grid when the problem arose and reapply it later to make it fixed (something like width: 809.0624542236328px;).

In the demo in this link you can see how all the dimensions are fixed, but the problem still appears when the window is resized, and therefore the layout is recalculated.

I added some console.log() inside the packer code, you can find the layout data when it breaks in the console.

Taking a quick look at the packer code and at the first decimal places of the width .06245 that I got, I would say that the eps value .001 is one of the causes of the problem.

I think I fixed it.

I don't think the problem is related to layoutOnResize, rather I think there are dimensions that can break the layout, and increasing the refresh rate increases the chances that the layout is calculated with these dimensions.

Yes, you're right, there are edge case dimensions that do break the layout currently.

Another clue that prompts me to think about this is that the problem always appears with the first item on the third row, regardless of the number of items per row.

I actually got this issue reproduced in a variety of ways with even only four items in the grid.

Taking a quick look at the packer code and at the first decimal places of the width .06245 that I got, I would say that the eps value .001 is one of the causes of the problem.

The epsilon value is fine as is actually (at least according to my tests), but the problem is that it isn't used in all places where needed at the moment.

I just updated the dev branch with a potential fix to this whole mess, did a _deep_ look into the logic of Packer and refactored it to be more resilient with decimal values. Here's your original test case with the latest dev version: https://codepen.io/niklasramo/pen/BaNebmN. Let me know if you can break it ;)

Let me know if you can break it

I'll do my best ;)


Can you tell me how accurate the new rounding process you have implemented is (if it still exists) ?

Now, in the case of the above gif, enabling the rounding option no longer solves the problem.

To summarize when the item is dragged I have to fix its size.

.item {
  width: 25%;
}
if (isDragging) {
  itemElement.style.width = getFixedWidthInPx();
}

But the results I get, even using layout.rounding = true are not precise enough (especially if I have to calculate the margin) and the layout is not correct.

function getFixedWidthInPx() {
  return gridElement.getBoundingClientRect().width * .25;
}

Can you tell me if I'm doing something wrong in the calculations? The only solution for now is to round the dimensions manually before layout, but I don't like it that much.

Now, in the case of the above gif, enabling the rounding option no longer solves the problem.

Do you mean you can still break the layout? If so could you please give me a live test case that shows it so I can tweak the code more? I also might be totally missing the real problem you are facing 🤔

Can you tell me if I'm doing something wrong in the calculations? The only solution for now is to round the dimensions manually before layout, but I don't like it that much.

I added a small explanation here: https://github.com/haltu/muuri/tree/dev#layout-. Basically the new layout works on _raw_ values retrieved from the DOM (using getBoundingClientRect()). The rounding option now works differently and is by default disabled. It first rounds the raw pixel value to three decimals precision and then floors the result to two decimals precision. This kind of rounding process was actually enough already by itself to get rid of the layout issues, but it wasn't the _right_ fix, which is why added the the EPSILON checks to appropriate places. However, I left the rounding feature there as an option to "stabilize" the layout if needed (just in case).

Thank you for the detailed explanation.

Do you mean you can still break the layout?

No, sorry for the misunderstanding. From the first tests Muuri works perfectly.


The problem I am facing is how to implement this use case, and it is not related to a Muuri bug / problem.

I try to better expose the problem, just to know if you see a workaround.


  • The item has a relative width
.item{
   width: 25%
}
  • When the item is dragged, it is inserted into the document.body, so the width cannot remain 25%.
options.dragContainer = document.body
  • I try to calculate the width in px and insert it in the style.
grid.on ("dragInit", (item) => {
   // item.style.width = "25%" --> item.style.width = "?px"
   item.getElement().style.width = getWidthInPx()
}
  • The problem is that I can't calculate the width precisely, the difference from the right result can be even greater than 0.01px.
function getWidthInPx() {
  // How should I calculate the width?
  return gridElement.getBoundingClientRect().width * .25;
}

So the final effect is that all items in the grid have the same width, say 100px, and the dragPlaceholder of the item being dragged will be 100.01px.

Can't you do something like this (use the cached dimensions which are also used in the layout calculations)?

grid.on ("dragInit", (item) => {
  const style = item.getElement().style;
  style.width = item.getWidth() + 'px';
  style.height = item.getHeight() + 'px';
}

Alternatively you could do this (but it would be slower than the latter approach as you'd have to read from the DOM):

grid.on ("dragInit", (item) => {
  const el = item.getElement();
  const rect = el.getBoundingClientRect();
  el.style.width = rect.width + 'px';
  el.style.height = rect.height + 'px';
}

Nope, the dimensions change according to the grid in which the item is dragged.

Ah, yes, now I get your drift! Yep, it's a tricky problem and not something that Muuri handles nicely out of the box.

To get the correct dimensions you'd have to temporarily add the element within the new grid's container element, read the dimensions and then take it out (a nasty hack). And after that you'd probably want to handle keeping the dragged element somehow in the same position relative to the pointer so that the pointer is always intersecting with the element even if the element's dimensions change during drag... but that's only for aesthetics, not required for functionality.

In Packery lib they use a technique where they have a "sizer" element in the grid (completely hidden visibly) which can be used to read the dimensions. Something like that could also be leveraged in this case.

I am creating muuri-react utilities to generate the style of the components. I was hoping to find a simpler solution that did not require access to the DOM, but in any case I will try to make them as simple as possible.

Thanks a lot for the help and for the fast fix!

Since I am doing many tests and working with different use cases, if you wanted to open an issue in which to collect feedback on version 0.9.0 before its release, I would gladly contribute.

I will do other tests, but I think you can already highlight this issue as _ready_ :)

@Paol-imi Here's the feedback issue for v0.9.0: https://github.com/haltu/muuri/issues/373 🙂

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Sashkan picture Sashkan  ·  3Comments

syfgkjasdkn picture syfgkjasdkn  ·  3Comments

argie09 picture argie09  ·  6Comments

ppwfx picture ppwfx  ·  4Comments

niklasramo picture niklasramo  ·  7Comments