Integration · Orbit

Zumly + Orbit

Orbit places elements around a center: rings, arcs, gauges, radial menus. Zumly zooms into an element to open the next view. Together, every satellite of a dial can open its own dial, and the satellites of one ring are siblings you can move between.

Live example · OrbitOpen ↗

Rooms are satellites, and each one opens its own dial. Open a device, move the slider and come back: the room's gauge repaints.

How they divide the work

Neither library knows about the other, and no glue code is needed. Orbit lays out and draws what is inside a view. Zumly moves between views.

WhoDoes
OrbitSizes a layout from its .bigbang, places satellites on rings, draws <o-arc> and <o-progress>, moves .depth layers in 3D.
ZumlyOpens a view from a trigger, keeps the parent around it, moves to siblings, zooms back out.
YouMark the satellites that open something with zoom-me and data-to, and keep the app state outside the views.

Orbit redraws by itself when its elements are inserted, resized or changed, so a view that Zumly renders, moves, hides behind a sibling or brings back draws correctly with every Zumly driver.

Install

npm install zumly @zumer/orbit
import Zumly from 'zumly'
import 'zumly/style.css'
import '@zumer/orbit/style'
import '@zumer/orbit'

Or from a CDN:

<link rel="stylesheet" href="https://unpkg.com/zumly/dist/zumly.css">
<link rel="stylesheet" href="https://unpkg.com/@zumer/orbit/dist/orbit.min.css">
<script src="https://unpkg.com/zumly/dist/zumly.js"></script>
<script defer src="https://unpkg.com/@zumer/orbit/dist/orbit.min.js"></script>

This guide uses Orbit 2, where a layout takes its size from its .bigbang box. With Orbit 1.x, set the size on the .gravity-spot instead: --o-force: 600px.

A dial that opens dials

Put a .bigbang inside a view and give the view a size. The layout fills the view. Make each satellite's capsule a trigger:

const dial = (title, items, className = 'z-view dial') => `
  <div class="${className}">
    <div class="bigbang">
      <div class="gravity-spot">
        <div class="orbit-10" style="--o-size-ratio: 1.5">
          ${items.map(() => '<o-arc></o-arc>').join('')}
        </div>
        <div class="orbit-7">
          ${items.map(item => `
            <div class="satellite">
              <div class="capsule zoom-me" data-to="${item}">${item}</div>
            </div>`).join('')}
        </div>
        <div class="orbit-0" style="--o-size-ratio: 6">
          <div class="satellite at-center"><div class="capsule">${title}</div></div>
        </div>
      </div>
    </div>
  </div>`

const app = new Zumly({
  mount: '.canvas',
  initialView: 'home',
  views: {
    // The first view fills the canvas and centers its dial
    home: `<div class="z-view home">${dial('Home', ['kitchen', 'living', 'garage'], 'dial')}</div>`,
    kitchen: dial('Kitchen', ['oven', 'lights']),
    living: dial('Living', ['tv', 'lights']),
    garage: dial('Garage', ['door']),
    // …
  },
  transitions: { duration: '700ms', ease: 'cubic-bezier(.2,.8,.2,1)' }
})
app.init()
.home { width: 100%; height: 100%; display: grid; place-items: center; }
.dial { width: min(80vmin, 600px); aspect-ratio: 1; }
.dial .capsule { width: 100%; height: 100%; border-radius: 50%; display: grid; place-items: center; }

Zooming in scales the dial until the capsule you clicked covers the next dial, so each level grows out of its satellite. The capsules on orbit-7 are siblings: the sibling bar, a click on a neighbor that is still visible, and goTo(name, { mode: 'lateral' }) move around the ring without going back up.

The live example at the top of this page is this dial, complete: open it on its own to read the source.

Which element is the trigger

Zumly zooms from the box of the trigger element. Use an element whose box matches what the user sees:

TriggerResult
.capsule or .satelliteExact: the next view grows out of the satellite. Recommended.
The center (.satellite.at-center > .capsule)Exact: good for "open the detail of this dial".
<o-arc> or <o-progress>The box of an arc is its whole ring, so the next view grows out of the full dial, not the segment. To open from a segment, put a satellite on the segment and make it the trigger.

A clickable band that should not open a view (for example to change a value) is an <o-arc interactive> without zoom-me. Clicks inside the current view that do not land on a trigger are left to your code.

State and repainting

Views behind the current one stay in the DOM and are not rendered again. If a value changes while the user is in a detail view, the gauge in the parent still shows the old value when they come back. Keep the values outside the views and repaint when a view becomes current again:

const state = { battery: 72 }

const views = {
  car: ({ target, onActivate }) => {
    target.className = 'z-view dial'   // target is the view; paint() fills its content
    const paint = () => {
      target.innerHTML = `
        <div class="bigbang"><div class="gravity-spot">
          <div class="orbit-10" style="--o-size-ratio: 2">
            <o-progress value="${state.battery}" shape="rounded"></o-progress>
          </div>
          <div class="orbit-0" style="--o-size-ratio: 6"><div class="satellite at-center">
            <div class="capsule zoom-me" data-to="battery">${state.battery}%</div>
          </div></div>
        </div></div>`
    }
    paint()
    onActivate(paint)
  },
  battery: ({ target }) => { /* a slider that writes state.battery */ }
}

To update only the value, change the attribute instead of the markup: target.querySelector('o-progress').setAttribute('value', state.battery). Orbit redraws it.

To open a view from code, for example when an alert arrives, call app.zoomIn(element) with the satellite: the view opens from it. app.zoomTo(name) opens from the center of the canvas.

Depth layers inside a view

Orbit's .depth layers (rings stacked in 3D with data-depth) work inside a Zumly view. They are a different kind of depth: Orbit layers are parts of one object, Zumly levels are places. Keep them visually distinct. A pattern that works:

To tilt the whole object with the pointer, add outer-perspective next to depth and set the perspective on your own wrapper.

Touch and gestures

A swipe to the left zooms out. Knobs, dials you drag and rings you spin need that gesture for themselves: add data-no-swipe to them. Sliders and text fields are excluded automatically. Give touch targets room: <o-arc interactive hit-width="12"> adds 12px of touch area on each side of a band.

Full-screen dials leave no parent visible around them, so "tap outside to go back" has no outside. Keep the back button (depthNav) or make the views smaller than the canvas.

Design tips

See it in action

The complete dial example, with dials that open dials.

Open this example ↗ Learn in six steps