UNPKG

tapspace

Version:

A zoomable user interface lib for web apps

123 lines (110 loc) 3.85 kB
const affineplane = require('affineplane') const circle2 = affineplane.circle2 const point2 = affineplane.point2 const dir2 = affineplane.dir2 module.exports = function (targets, options) { // @Viewport:limitTo(targets[, options]) // // Zoom until the targets are at least partly visible in the viewport. // Do not zoom at all if some of them are visible. // Useful for preventing users from getting lost in space. // // Parameters: // targets // array of Component // options // optional object with properties: // maxAreaRatio // optional number, default is 2. // .. The largest allowed area of the *smallest* target // .. relative to the viewport area. In other words, // .. if the relative area of the smallest target grows // .. above this maximum area ratio, viewport will zoom out. // minAreaRatio // optional number, default is 0.0002. // .. The smallest allowed area of the *largest* target // .. relative to the viewport area. In other words, // .. if the relative area of the largest target shrinks // .. below this minimum area ratio, viewport will zoom in. // // Return // this, for chaining // // Nil targets, no limiting. if (!targets) { return this } // Normalize targets to array if (!Array.isArray(targets)) { targets = [targets] } // No targets, no limiting. if (targets.length === 0) { return this } // Default options if (!options) { options = {} } if (typeof options.maxAreaRatio !== 'number') { options.maxAreaRatio = 2 } if (typeof options.minAreaRatio !== 'number') { options.minAreaRatio = 0.0002 } // Measure targets const metrics = this.measureMany(targets) // If none are visible, bring some of the targets to view. const noneVisible = metrics.every(m => !m.visible) if (noneVisible) { // Compute bounding circle for the set of targets. const circles = metrics.map(m => m.circle) const targetsCircle = circle2.boundingCircle(circles) // Gap between the targets and viewport. const viewCircle = this.getBoundingCircle().getRaw() const gap = Math.max(0, circle2.gap(viewCircle, targetsCircle)) // Construct a translation towards targets. const dirVec = point2.vectorTo(viewCircle, targetsCircle) const dir = dir2.fromVector(dirVec) const trip = dir2.toVector(dir, gap + viewCircle.r) // Apply this.translateBy(trip) } // Next, bring extreme sizes down to readable and reachable range. const MAX_AREA_RATIO = options.maxAreaRatio const MIN_AREA_RATIO = options.minAreaRatio let smallestAreaRatio = Infinity // let smallestTarget = null let largestAreaRatio = 0 let largestTarget = null metrics.forEach(m => { if (m.areaRatio < smallestAreaRatio) { smallestAreaRatio = m.areaRatio // smallestTarget = m.target } if (m.areaRatio > largestAreaRatio) { largestAreaRatio = m.areaRatio largestTarget = m.target } }) // Assert smallestTarget and largestTarget are set cuz num targets > 0 let scaling = 1 let origin = null if (smallestAreaRatio > MAX_AREA_RATIO) { // The smallest is too large. // Pick a scaling that shrinks the smallest to the limit. scaling = MAX_AREA_RATIO / smallestAreaRatio // Shrink about the viewport origin to bring the target back into view. origin = this.atAnchor() } else if (largestAreaRatio < MIN_AREA_RATIO) { // The largest is too small. // Pick a scaling that dilates the largest to the limit. scaling = MIN_AREA_RATIO / largestAreaRatio // Dilate about the target origin = largestTarget.atAnchor() } if (scaling !== 1) { this.hyperspace.scaleBy(scaling, origin).commit() } return this }