pixi-viewport icon indicating copy to clipboard operation
pixi-viewport copied to clipboard

Indepent x/y-scaling and coord/zoom clamping when world aspect is different

Open aldanor opened this issue 7 years ago • 9 comments
trafficstars

@davidfig Hi and thanks for an awesome idea and a great implementation of it :)

I was trying to make it work in my use case but I've hit a few roadblocks and I was wondering whether it was my stupidity or maybe there are a few gaps in the library too which might be improved.

The use case is like this:

  • There is a fixed screen size, let's say (800px, 600px) viewport.
  • The world coordinates are not related to pixels and are highly uneven -- for example, x is time and goes from 10s to 2000s, and vertical is price and goes from 101.00 to 101.05; there's a bunch of data plotted, as a sort of a gigantic graph. As initial view, I'd like the data to fill the entire viewport, by zooming along x/y accordingly (I've tried playing with fit/fitWidth/fitHeight and scaleX/scaleY but somewhy couldn't make it work out of the box).
  • (By the way, is there a problem if the world coordinates don't start from (0, 0), as in this case?)
  • When plotting, I'd like to be able to plot in world coordinates, so that I don't have to convert coordinates manually.
  • I'd like to be able to wheel-zoom and drag around the plot, but it should never zoom out of world coordinates bounds (I've almost made it work with clamp/clampZoom but again, not fully), so if you mousewheel-out many times it basically just snaps back to the initial view.
  • When zooming in, because the world aspect ratio is so different from screen aspect ratio, I'd like to be able to have different zoom speeds along x and y axes (so that minWidth/minHeight set up in clampZoom could be attained exactly) -- looks like that's not currently possible. Or maybe formulate it differently, as "zoom in towards this specific min width/height", and "zoom out towards the full width/height".
  • It would also be nice to be able to zoom just along one axis (e.g. with shift-mouse-wheel?)

Apologies for a pile of questions :) I'll try to post a toy example too.

Update: most of the points above I've managed to solve by trial & error, see the example below, except for separate x/y-zooming and shift-wheel zooming, and different aspect ratios between screen and world.

aldanor avatar Sep 23 '18 11:09 aldanor

Ok, I did a bit more digging and came up with a version that solves some of it (a lot of it wasn't obvious from the docs so it was mostly trial & error...).

Here's the codepen: https://codepen.io/anon/pen/OoGwMb

What is left unsolved, I guess, is this:

  • Aspect ratios between screen and world are different, that (or something else) seems to mess things up a bit -- initial viewport fits entire world but once you start zooming out it breaks
  • Zooming out fully doesn't show the whole world picture either, something's preventing it -- you have to drag vertically to see the world.
  • Zooming in/out either towards fixed min/max width/height, or with separate speeds on x and y
  • Shift-mouse-wheel zooming in either x or y separately
const width = 900
const height = 400
const worldWidth = 1900
const worldHeight = 1400
const worldMinX = worldWidth / 3
const worldMinY = worldHeight / 3

const app = new PIXI.Application({ width: width, height: height })
const viewport = new Viewport({
    screenWidth: width,
    screenHeight: height,
    worldWidth: worldWidth,
    worldHeight: worldHeight,
    interaction: app.renderer.interaction,
})
viewport.left = worldMinX  // possible to pass it in constructor?
viewport.top = worldMinY  // possible to pass it in constructor?
viewport
    .fitWidth(worldWidth, false, false)
    .fitHeight(worldHeight, false, false)
    .drag()
    .wheel({ percent: 0.3 })
    .decelerate()
    .clampZoom({
      maxWidth: worldWidth, // ?
      maxHeight: worldHeight, // ?
    })
    .clamp({
      left: worldMinX, // ?
      right: worldMinX + worldWidth, // ?
      top: worldMinY, // ?
      bottom: worldMinY + worldHeight, // ?
    })
document.appendChild(app.view)
app.stage.addChild(viewport)

const g = new PIXI.Graphics()
g.beginFill(0xFFFF00)
g.lineStyle(5, 0xFF0000)
g.drawRect(worldMinX, worldMinY, worldWidth, worldHeight)
g.endFill()

g.beginFill(0xFF0000)
g.lineStyle(2, 0x0000FF)
g.drawRect(
    worldMinX + worldWidth * 0.475,
    worldMinY + worldHeight * 0.475,
    worldWidth * 0.05,
    worldHeight * 0.05,
)
g.endFill()

viewport.addChild(g)

app.start()

aldanor avatar Sep 23 '18 14:09 aldanor

If you remember, please let me know which parts of the documentation were not clear. I've added a lot of features over time, and I know some of it is not super clear in the documentation, and I'd love to clear it up. Thanks!

Aspect ratios between screen and world are different, that (or something else) seems to mess things up a bit -- initial viewport fits entire world but once you start zooming out it breaks

pixi-viewport does not have great support for independent scaling of x and y. I have left hooks in there to support it for some plugins, but it wasn't a high priority since I never had a need for it. If your use case is a zoomable graph then I can see why it would be useful. I can start working on support for it. It would be great if you can provide a better graphical example that I can use as the demo to build and demonstrate the support.

Zooming out fully doesn't show the whole world picture either, something's preventing it -- you have to drag vertically to see the world.

Your clampZoom is stopping the zoom. It's maxing out on one of your dimensions.

Zooming in/out either towards fixed min/max width/height, or with separate speeds on x and y Shift-mouse-wheel zooming in either x or y separately

With proper support this would be possible. You'd likely have to override some of the functions since I can't think of how to genericize the controls for zooming on independent axes.

davidfig avatar Sep 24 '18 03:09 davidfig

@davidfig Here's another example: https://codepen.io/anon/pen/EezgzW (try zooming out)

  • The main problem is: it should be possible in this example to simply wheel-zoom in and out maintaining the aspect ratio set initially (you should see squares in the example above)
  • Would also be nice to be able to zoom on x/y independently
  • (As an optional feature, in the example above would be nice if when you zoom in the 1x1 squares becomes 2x1 rectangles, and when you wheel out they zoom out to 1x1 squares again -- -note that this is in screen coordinates; in world coordinates as you can see they are encoded as 5x1 rectangles)

aldanor avatar Sep 24 '18 12:09 aldanor

Phew. It turns out I did code the viewport for axis independence (it's amazing how quickly I forget what I did). But there were bugs in both wheel() (which was caused by a recent addition of a smoothing function) and clampZoom() (which was caused by bad coding) that forced the scale to no longer be independent. I fixed both of those bugs in v3.7.4.

I tried your demo with the new version and it works nicely on 1 & 2 above.

For 3 you'll have to add code to handle that situation since it's too application specific. You can listen to the 'zoomed' event and change the scale to accomplish whatever aspect ratio you're looking to achieve based on the level of zoom.

Thanks for working through this. Let me know if you have any other problems.

davidfig avatar Sep 25 '18 01:09 davidfig

FWIW, I would love to have independent X and Y zoom (I need it often for heap and profile flamegraphs). Would you be able to point me in the vague direction in the code where I could add this?

thomasdullien avatar Dec 05 '20 22:12 thomasdullien

The viewport is based on pixi.display, which handles x and y scale independently. Many of the viewport's plugins (like pinch and wheel) are not designed for independent zooms (mostly because there's not a standard way to do it from the input side).

If you're looking for an axis-independent zoom like with ctrl+mouse wheel or something, then you'll want to derive a new plugin from the closest plugin and add the functionality to a new plugin (or just clone and change the original plugin). They plugins are in the plugin directories and the code is generally straightforward. If you're looking for the fit()-type code, then that's in viewport.js.

If you have a specific input functionality in mind, I can point you more specifically to where in the code to start.

davidfig avatar Dec 05 '20 22:12 davidfig

Awesome, thank you!

thomasdullien avatar Dec 05 '20 22:12 thomasdullien

My first input functionality would be wheel zooms. In essence, my final goals are:

  1. Begin with having the entire flamechart across the entire screen. The world dimension is ~1m pixels wide, and about 8000 pixels high.
  2. Allow wheel-zoom into the diagram, but also "horizontal squash" (scaling just X) and "vertical squash" (scaling just Y) with two different hotkeys + wheel.
  3. Ensure that the user cannot zoom / drag past the boundaries of the diagram, so some form of clamping.

I saw that there is a PluginManager to support user-supplied plugins; do you happen to have a link to some code that uses it? I suppose I need to replace the wheel plugin somehow?

thomasdullien avatar Dec 06 '20 20:12 thomasdullien

It's easiest to copy the wheel plugin from /src/plugins/wheel.js. You'll want to override wheel() (which handles the wheel) event, and optionally update(), which handles smoothing the zoom. Some of the keyboard keys are included in the wheel event. If the one you want to use is not, you'll have to independently track the keydown/keyup events.

Adding the viewport plugin is simple: viewport.plugins.add('independent-wheel', new IndependentWheel(), index)

index is optional. It's used to change the order the plugins are called on events. It's probably not necessary for your use case.

Good luck, and let me know if you have further questions.

davidfig avatar Dec 07 '20 00:12 davidfig