From 65f036e42a545f2fbcd8107404915a968834e857 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 5 Jul 2023 09:47:07 -0400 Subject: [PATCH] add pan and zoom controls to image viewer --- CHANGELOG.md | 6 +- DIFFUSERS.md | 1 + README.md | 13 + TODO.md | 6 +- javascript/imageViewer.js | 19 +- javascript/panZoom.js | 1768 +++++++++++++++++++++++++++++++++++++ 6 files changed, 1797 insertions(+), 16 deletions(-) create mode 100644 javascript/panZoom.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 91ef21dc7..4c4447625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,13 @@ # Change Log for SD.Next -## Update for ... +## Update for 07/07/2023 +- add pan & zoom controls (touch and mouse) to image viewer (lightbox) - add settings -> extra networks -> do not automatically build extra network pages speeds up app start if you have a lot of extra networks and you want to build them manually when needed +- extra network ui tweaks +- merge experimental diffusers support + this will be covered in details in separate post ## Update for 07/01/2023 diff --git a/DIFFUSERS.md b/DIFFUSERS.md index ef6270a3f..ac9dc998e 100644 --- a/DIFFUSERS.md +++ b/DIFFUSERS.md @@ -119,6 +119,7 @@ will need to handle in the code before we get out of alpha > Checkpoint /home/vlado/dev/automatic/models/Stable-diffusion/best/absolutereality_v1.safetensors has both EMA and non-EMA weights. > In this conversion only the EMA weights are extracted. If you want to instead extract the non-EMA weights (useful to continue fine-tuning), please make sure to remove the `--extract_ema` flag. - do you have plans to implement [Restart](https://github.com/vladmandic/automatic/issues/1537) sampler in diffusers? +- `torch.nonzero()` performance issue ## Update diff --git a/README.md b/README.md index eb9bcbac2..4e8610d4d 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,19 @@ Below is partial list of all available parameters, run `webui --help` for the fu ## Notes +### **Extensions** + +SD.Next comes with several extensions pre-installed: + +- [Dyanmic Thresholding](https://github.com/mcmonkeyprojects/sd-dynamic-thresholding) +- [ControlNet](https://github.com/Mikubill/sd-webui-controlnet) +- [Agent Scheduler](https://github.com/ArtVentureX/sd-webui-agent-scheduler) +- [Multi-Diffusion Tiled Diffusion and VAE](https://github.com/pkuliyi2015/multidiffusion-upscaler-for-automatic1111) +- [LyCORIS](https://github.com/KohakuBlueleaf/a1111-sd-webui-lycoris) +- [Image Browser](https://github.com/AlUlkesh/stable-diffusion-webui-images-browser) +- [CLiP Interrogator](https://github.com/pharmapsychotic/clip-interrogator-ext) +- [Rembg Background Removal](https://github.com/AUTOMATIC1111/stable-diffusion-webui-rembg) + ### **Collab** - To avoid having this repo rely just on me, I'd love to have additional maintainers with full admin rights. If you're interested, ping me! diff --git a/TODO.md b/TODO.md index ef0e08985..60ced2f41 100644 --- a/TODO.md +++ b/TODO.md @@ -26,7 +26,8 @@ Stuff to be investigated... Pick & merge PRs from main repo... -- list: +- up-to-date with: df004be +- current todo list: ## Integration @@ -48,7 +49,6 @@ Tech that can be integrated as part of the core workflow... - Bunch of stuff: - -- shared.info - docker - port `p.all_hr_prompts` - test `lyco_patch_lora` @@ -57,6 +57,6 @@ Tech that can be integrated as part of the core workflow... - image watermark - image `imagehash` phash and hdash - custom exif tags -- replace lightbox with iv-viewer - git-rebasin - additional upscalers +- new image browser diff --git a/javascript/imageViewer.js b/javascript/imageViewer.js index c4cac2f90..4a5302fcc 100644 --- a/javascript/imageViewer.js +++ b/javascript/imageViewer.js @@ -149,9 +149,8 @@ onAfterUiUpdate(() => { }); document.addEventListener('DOMContentLoaded', () => { - // const modalFragment = document.createDocumentFragment(); const modal = document.createElement('div'); - modal.onclick = closeModal; + // modal.onclick = closeModal; modal.id = 'lightboxModal'; modal.tabIndex = 0; modal.addEventListener('keydown', modalKeyHandler, true); @@ -182,21 +181,17 @@ document.addEventListener('DOMContentLoaded', () => { modalSave.title = 'Save Image(s)'; modalControls.appendChild(modalSave); - /* - const modalClose = document.createElement('span'); - modalClose.className = 'modalClose cursor'; - modalClose.innerHTML = '×'; - modalClose.onclick = closeModal; - modalClose.title = 'Close image viewer'; - modalControls.appendChild(modalClose); - */ - const modalImage = document.createElement('img'); modalImage.id = 'modalImage'; - modalImage.onclick = closeModal; modalImage.tabIndex = 0; modalImage.addEventListener('keydown', modalKeyHandler, true); modal.appendChild(modalImage); + modalImage.onload = () => panzoom(modalImage, { zoomSpeed: 0.025, minZoom: 0.25, maxZoom: 4.0 }); + let drag = false; + modalImage.addEventListener('mousedown', () => drag = false); + modalImage.addEventListener('mousemove', () => drag = true); + modalImage.addEventListener('mouseup', () => { if (!drag) closeModal(); }); + // modalImage.onclick = closeModal; const modalPrev = document.createElement('a'); modalPrev.className = 'modalPrev'; diff --git a/javascript/panZoom.js b/javascript/panZoom.js new file mode 100644 index 000000000..fec42c597 --- /dev/null +++ b/javascript/panZoom.js @@ -0,0 +1,1768 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.panzoom = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0) { + transform.x += diff; + adjusted = true; + } + // check the other side: + diff = boundingBox.right - clientRect.left; + if (diff < 0) { + transform.x += diff; + adjusted = true; + } + + // y axis: + diff = boundingBox.top - clientRect.bottom; + if (diff > 0) { + // we adjust transform, so that it matches exactly our bounding box: + // transform.y = boundingBox.top - (boundingBox.height + boundingBox.y) * transform.scale => + // transform.y = boundingBox.top - (clientRect.bottom - transform.y) => + // transform.y = diff + transform.y => + transform.y += diff; + adjusted = true; + } + + diff = boundingBox.bottom - clientRect.top; + if (diff < 0) { + transform.y += diff; + adjusted = true; + } + return adjusted; + } + + /** + * Returns bounding box that should be used to restrict scene movement. + */ + function getBoundingBox() { + if (!bounds) return; // client does not want to restrict movement + + if (typeof bounds === 'boolean') { + // for boolean type we use parent container bounds + var ownerRect = owner.getBoundingClientRect(); + var sceneWidth = ownerRect.width; + var sceneHeight = ownerRect.height; + + return { + left: sceneWidth * boundsPadding, + top: sceneHeight * boundsPadding, + right: sceneWidth * (1 - boundsPadding), + bottom: sceneHeight * (1 - boundsPadding) + }; + } + + return bounds; + } + + function getClientRect() { + var bbox = panController.getBBox(); + var leftTop = client(bbox.left, bbox.top); + + return { + left: leftTop.x, + top: leftTop.y, + right: bbox.width * transform.scale + leftTop.x, + bottom: bbox.height * transform.scale + leftTop.y + }; + } + + function client(x, y) { + return { + x: x * transform.scale + transform.x, + y: y * transform.scale + transform.y + }; + } + + function makeDirty() { + isDirty = true; + frameAnimation = window.requestAnimationFrame(frame); + } + + function zoomByRatio(clientX, clientY, ratio) { + if (isNaN(clientX) || isNaN(clientY) || isNaN(ratio)) { + throw new Error('zoom requires valid numbers'); + } + + var newScale = transform.scale * ratio; + + if (newScale < minZoom) { + if (transform.scale === minZoom) return; + + ratio = minZoom / transform.scale; + } + if (newScale > maxZoom) { + if (transform.scale === maxZoom) return; + + ratio = maxZoom / transform.scale; + } + + var size = transformToScreen(clientX, clientY); + + transform.x = size.x - ratio * (size.x - transform.x); + transform.y = size.y - ratio * (size.y - transform.y); + + // TODO: https://github.com/anvaka/panzoom/issues/112 + if (bounds && boundsPadding === 1 && minZoom === 1) { + transform.scale *= ratio; + keepTransformInsideBounds(); + } else { + var transformAdjusted = keepTransformInsideBounds(); + if (!transformAdjusted) transform.scale *= ratio; + } + + triggerEvent('zoom'); + + makeDirty(); + } + + function zoomAbs(clientX, clientY, zoomLevel) { + var ratio = zoomLevel / transform.scale; + zoomByRatio(clientX, clientY, ratio); + } + + function centerOn(ui) { + var parent = ui.ownerSVGElement; + if (!parent) + throw new Error('ui element is required to be within the scene'); + + // TODO: should i use controller's screen CTM? + var clientRect = ui.getBoundingClientRect(); + var cx = clientRect.left + clientRect.width / 2; + var cy = clientRect.top + clientRect.height / 2; + + var container = parent.getBoundingClientRect(); + var dx = container.width / 2 - cx; + var dy = container.height / 2 - cy; + + internalMoveBy(dx, dy, true); + } + + function smoothMoveTo(x, y){ + internalMoveBy(x - transform.x, y - transform.y, true) + } + + function internalMoveBy(dx, dy, smooth) { + if (!smooth) { + return moveBy(dx, dy); + } + + if (moveByAnimation) moveByAnimation.cancel(); + + var from = { x: 0, y: 0 }; + var to = { x: dx, y: dy }; + var lastX = 0; + var lastY = 0; + + moveByAnimation = animate(from, to, { + step: function (v) { + moveBy(v.x - lastX, v.y - lastY); + + lastX = v.x; + lastY = v.y; + } + }); + } + + function scroll(x, y) { + cancelZoomAnimation(); + moveTo(x, y); + } + + function dispose() { + releaseEvents(); + } + + function listenForEvents() { + owner.addEventListener('mousedown', onMouseDown, { passive: false }); + owner.addEventListener('dblclick', onDoubleClick, { passive: false }); + owner.addEventListener('touchstart', onTouch, { passive: false }); + owner.addEventListener('keydown', onKeyDown, { passive: false }); + + // Need to listen on the owner container, so that we are not limited + // by the size of the scrollable domElement + wheel.addWheelListener(owner, onMouseWheel, { passive: false }); + + makeDirty(); + } + + function releaseEvents() { + wheel.removeWheelListener(owner, onMouseWheel); + owner.removeEventListener('mousedown', onMouseDown); + owner.removeEventListener('keydown', onKeyDown); + owner.removeEventListener('dblclick', onDoubleClick); + owner.removeEventListener('touchstart', onTouch); + + if (frameAnimation) { + window.cancelAnimationFrame(frameAnimation); + frameAnimation = 0; + } + + smoothScroll.cancel(); + + releaseDocumentMouse(); + releaseTouches(); + textSelection.release(); + + triggerPanEnd(); + } + + function frame() { + if (isDirty) applyTransform(); + } + + function applyTransform() { + isDirty = false; + + // TODO: Should I allow to cancel this? + panController.applyTransform(transform); + + triggerEvent('transform'); + frameAnimation = 0; + } + + function onKeyDown(e) { + var x = 0, + y = 0, + z = 0; + if (e.keyCode === 38) { + y = 1; // up + } else if (e.keyCode === 40) { + y = -1; // down + } else if (e.keyCode === 37) { + x = 1; // left + } else if (e.keyCode === 39) { + x = -1; // right + } else if (e.keyCode === 189 || e.keyCode === 109) { + // DASH or SUBTRACT + z = 1; // `-` - zoom out + } else if (e.keyCode === 187 || e.keyCode === 107) { + // EQUAL SIGN or ADD + z = -1; // `=` - zoom in (equal sign on US layout is under `+`) + } + + if (filterKey(e, x, y, z)) { + // They don't want us to handle the key: https://github.com/anvaka/panzoom/issues/45 + return; + } + + if (x || y) { + e.preventDefault(); + e.stopPropagation(); + + var clientRect = owner.getBoundingClientRect(); + // movement speed should be the same in both X and Y direction: + var offset = Math.min(clientRect.width, clientRect.height); + var moveSpeedRatio = 0.05; + var dx = offset * moveSpeedRatio * x; + var dy = offset * moveSpeedRatio * y; + + // TODO: currently we do not animate this. It could be better to have animation + internalMoveBy(dx, dy); + } + + if (z) { + var scaleMultiplier = getScaleMultiplier(z * 100); + var offset = transformOrigin ? getTransformOriginOffset() : midPoint(); + publicZoomTo(offset.x, offset.y, scaleMultiplier); + } + } + + function midPoint() { + var ownerRect = owner.getBoundingClientRect(); + return { + x: ownerRect.width / 2, + y: ownerRect.height / 2 + }; + } + + function onTouch(e) { + // let the override the touch behavior + beforeTouch(e); + + if (e.touches.length === 1) { + return handleSingleFingerTouch(e, e.touches[0]); + } else if (e.touches.length === 2) { + // handleTouchMove() will care about pinch zoom. + pinchZoomLength = getPinchZoomLength(e.touches[0], e.touches[1]); + multiTouch = true; + startTouchListenerIfNeeded(); + } + } + + function beforeTouch(e) { + // TODO: Need to unify this filtering names. E.g. use `beforeTouch` + if (options.onTouch && !options.onTouch(e)) { + // if they return `false` from onTouch, we don't want to stop + // events propagation. Fixes https://github.com/anvaka/panzoom/issues/12 + return; + } + + e.stopPropagation(); + e.preventDefault(); + } + + function beforeDoubleClick(e) { + // TODO: Need to unify this filtering names. E.g. use `beforeDoubleClick`` + if (options.onDoubleClick && !options.onDoubleClick(e)) { + // if they return `false` from onTouch, we don't want to stop + // events propagation. Fixes https://github.com/anvaka/panzoom/issues/46 + return; + } + + e.preventDefault(); + e.stopPropagation(); + } + + function handleSingleFingerTouch(e) { + var touch = e.touches[0]; + var offset = getOffsetXY(touch); + lastSingleFingerOffset = offset; + var point = transformToScreen(offset.x, offset.y); + mouseX = point.x; + mouseY = point.y; + + smoothScroll.cancel(); + startTouchListenerIfNeeded(); + } + + function startTouchListenerIfNeeded() { + if (touchInProgress) { + // no need to do anything, as we already listen to events; + return; + } + + touchInProgress = true; + document.addEventListener('touchmove', handleTouchMove); + document.addEventListener('touchend', handleTouchEnd); + document.addEventListener('touchcancel', handleTouchEnd); + } + + function handleTouchMove(e) { + if (e.touches.length === 1) { + e.stopPropagation(); + var touch = e.touches[0]; + + var offset = getOffsetXY(touch); + var point = transformToScreen(offset.x, offset.y); + + var dx = point.x - mouseX; + var dy = point.y - mouseY; + + if (dx !== 0 && dy !== 0) { + triggerPanStart(); + } + mouseX = point.x; + mouseY = point.y; + internalMoveBy(dx, dy); + } else if (e.touches.length === 2) { + // it's a zoom, let's find direction + multiTouch = true; + var t1 = e.touches[0]; + var t2 = e.touches[1]; + var currentPinchLength = getPinchZoomLength(t1, t2); + + // since the zoom speed is always based on distance from 1, we need to apply + // pinch speed only on that distance from 1: + var scaleMultiplier = + 1 + (currentPinchLength / pinchZoomLength - 1) * pinchSpeed; + + var firstTouchPoint = getOffsetXY(t1); + var secondTouchPoint = getOffsetXY(t2); + mouseX = (firstTouchPoint.x + secondTouchPoint.x) / 2; + mouseY = (firstTouchPoint.y + secondTouchPoint.y) / 2; + if (transformOrigin) { + var offset = getTransformOriginOffset(); + mouseX = offset.x; + mouseY = offset.y; + } + + publicZoomTo(mouseX, mouseY, scaleMultiplier); + + pinchZoomLength = currentPinchLength; + e.stopPropagation(); + e.preventDefault(); + } + } + + function handleTouchEnd(e) { + if (e.touches.length > 0) { + var offset = getOffsetXY(e.touches[0]); + var point = transformToScreen(offset.x, offset.y); + mouseX = point.x; + mouseY = point.y; + } else { + var now = new Date(); + if (now - lastTouchEndTime < doubleTapSpeedInMS) { + if (transformOrigin) { + var offset = getTransformOriginOffset(); + smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed); + } else { + // We want untransformed x/y here. + smoothZoom(lastSingleFingerOffset.x, lastSingleFingerOffset.y, zoomDoubleClickSpeed); + } + } + + lastTouchEndTime = now; + + triggerPanEnd(); + releaseTouches(); + } + } + + function getPinchZoomLength(finger1, finger2) { + var dx = finger1.clientX - finger2.clientX; + var dy = finger1.clientY - finger2.clientY; + return Math.sqrt(dx * dx + dy * dy); + } + + function onDoubleClick(e) { + beforeDoubleClick(e); + var offset = getOffsetXY(e); + if (transformOrigin) { + // TODO: looks like this is duplicated in the file. + // Need to refactor + offset = getTransformOriginOffset(); + } + smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed); + } + + function onMouseDown(e) { + // if client does not want to handle this event - just ignore the call + if (beforeMouseDown(e)) return; + + if (touchInProgress) { + // modern browsers will fire mousedown for touch events too + // we do not want this: touch is handled separately. + e.stopPropagation(); + return false; + } + // for IE, left click == 1 + // for Firefox, left click == 0 + var isLeftButton = + (e.button === 1 && window.event !== null) || e.button === 0; + if (!isLeftButton) return; + + smoothScroll.cancel(); + + var offset = getOffsetXY(e); + var point = transformToScreen(offset.x, offset.y); + mouseX = point.x; + mouseY = point.y; + + // We need to listen on document itself, since mouse can go outside of the + // window, and we will loose it + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + textSelection.capture(e.target || e.srcElement); + + return false; + } + + function onMouseMove(e) { + // no need to worry about mouse events when touch is happening + if (touchInProgress) return; + + triggerPanStart(); + + var offset = getOffsetXY(e); + var point = transformToScreen(offset.x, offset.y); + var dx = point.x - mouseX; + var dy = point.y - mouseY; + + mouseX = point.x; + mouseY = point.y; + + internalMoveBy(dx, dy); + } + + function onMouseUp() { + textSelection.release(); + triggerPanEnd(); + releaseDocumentMouse(); + } + + function releaseDocumentMouse() { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + panstartFired = false; + } + + function releaseTouches() { + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleTouchEnd); + document.removeEventListener('touchcancel', handleTouchEnd); + panstartFired = false; + multiTouch = false; + touchInProgress = false; + } + + function onMouseWheel(e) { + // if client does not want to handle this event - just ignore the call + if (beforeWheel(e)) return; + + smoothScroll.cancel(); + + var delta = e.deltaY; + if (e.deltaMode > 0) delta *= 100; + + var scaleMultiplier = getScaleMultiplier(delta); + + if (scaleMultiplier !== 1) { + var offset = transformOrigin + ? getTransformOriginOffset() + : getOffsetXY(e); + publicZoomTo(offset.x, offset.y, scaleMultiplier); + e.preventDefault(); + } + } + + function getOffsetXY(e) { + var offsetX, offsetY; + // I tried using e.offsetX, but that gives wrong results for svg, when user clicks on a path. + var ownerRect = owner.getBoundingClientRect(); + offsetX = e.clientX - ownerRect.left; + offsetY = e.clientY - ownerRect.top; + + return { x: offsetX, y: offsetY }; + } + + function smoothZoom(clientX, clientY, scaleMultiplier) { + var fromValue = transform.scale; + var from = { scale: fromValue }; + var to = { scale: scaleMultiplier * fromValue }; + + smoothScroll.cancel(); + cancelZoomAnimation(); + + zoomToAnimation = animate(from, to, { + step: function (v) { + zoomAbs(clientX, clientY, v.scale); + }, + done: triggerZoomEnd + }); + } + + function smoothZoomAbs(clientX, clientY, toScaleValue) { + var fromValue = transform.scale; + var from = { scale: fromValue }; + var to = { scale: toScaleValue }; + + smoothScroll.cancel(); + cancelZoomAnimation(); + + zoomToAnimation = animate(from, to, { + step: function (v) { + zoomAbs(clientX, clientY, v.scale); + } + }); + } + + function getTransformOriginOffset() { + var ownerRect = owner.getBoundingClientRect(); + return { + x: ownerRect.width * transformOrigin.x, + y: ownerRect.height * transformOrigin.y + }; + } + + function publicZoomTo(clientX, clientY, scaleMultiplier) { + smoothScroll.cancel(); + cancelZoomAnimation(); + return zoomByRatio(clientX, clientY, scaleMultiplier); + } + + function cancelZoomAnimation() { + if (zoomToAnimation) { + zoomToAnimation.cancel(); + zoomToAnimation = null; + } + } + + function getScaleMultiplier(delta) { + var sign = Math.sign(delta); + var deltaAdjustedSpeed = Math.min(0.25, Math.abs(speed * delta / 128)); + return 1 - sign * deltaAdjustedSpeed; + } + + function triggerPanStart() { + if (!panstartFired) { + triggerEvent('panstart'); + panstartFired = true; + smoothScroll.start(); + } + } + + function triggerPanEnd() { + if (panstartFired) { + // we should never run smooth scrolling if it was multiTouch (pinch zoom animation): + if (!multiTouch) smoothScroll.stop(); + triggerEvent('panend'); + } + } + + function triggerZoomEnd() { + triggerEvent('zoomend'); + } + + function triggerEvent(name) { + api.fire(name, api); + } +} + +function parseTransformOrigin(options) { + if (!options) return; + if (typeof options === 'object') { + if (!isNumber(options.x) || !isNumber(options.y)) + failTransformOrigin(options); + return options; + } + + failTransformOrigin(); +} + +function failTransformOrigin(options) { + console.error(options); + throw new Error( + [ + 'Cannot parse transform origin.', + 'Some good examples:', + ' "center center" can be achieved with {x: 0.5, y: 0.5}', + ' "top center" can be achieved with {x: 0.5, y: 0}', + ' "bottom right" can be achieved with {x: 1, y: 1}' + ].join('\n') + ); +} + +function noop() { } + +function validateBounds(bounds) { + var boundsType = typeof bounds; + if (boundsType === 'undefined' || boundsType === 'boolean') return; // this is okay + // otherwise need to be more thorough: + var validBounds = + isNumber(bounds.left) && + isNumber(bounds.top) && + isNumber(bounds.bottom) && + isNumber(bounds.right); + + if (!validBounds) + throw new Error( + 'Bounds object is not valid. It can be: ' + + 'undefined, boolean (true|false) or an object {left, top, right, bottom}' + ); +} + +function isNumber(x) { + return Number.isFinite(x); +} + +// IE 11 does not support isNaN: +function isNaN(value) { + if (Number.isNaN) { + return Number.isNaN(value); + } + + return value !== value; +} + +function rigidScroll() { + return { + start: noop, + stop: noop, + cancel: noop + }; +} + +function autoRun() { + if (typeof document === 'undefined') return; + + var scripts = document.getElementsByTagName('script'); + if (!scripts) return; + var panzoomScript; + + for (var i = 0; i < scripts.length; ++i) { + var x = scripts[i]; + if (x.src && x.src.match(/\bpanzoom(\.min)?\.js/)) { + panzoomScript = x; + break; + } + } + + if (!panzoomScript) return; + + var query = panzoomScript.getAttribute('query'); + if (!query) return; + + var globalName = panzoomScript.getAttribute('name') || 'pz'; + var started = Date.now(); + + tryAttach(); + + function tryAttach() { + var el = document.querySelector(query); + if (!el) { + var now = Date.now(); + var elapsed = now - started; + if (elapsed < 2000) { + // Let's wait a bit + setTimeout(tryAttach, 100); + return; + } + // If we don't attach within 2 seconds to the target element, consider it a failure + console.error('Cannot find the panzoom element', globalName); + return; + } + var options = collectOptions(panzoomScript); + console.log(options); + window[globalName] = createPanZoom(el, options); + } + + function collectOptions(script) { + var attrs = script.attributes; + var options = {}; + for (var i = 0; i < attrs.length; ++i) { + var attr = attrs[i]; + var nameValue = getPanzoomAttributeNameValue(attr); + if (nameValue) { + options[nameValue.name] = nameValue.value; + } + } + + return options; + } + + function getPanzoomAttributeNameValue(attr) { + if (!attr.name) return; + var isPanZoomAttribute = + attr.name[0] === 'p' && attr.name[1] === 'z' && attr.name[2] === '-'; + + if (!isPanZoomAttribute) return; + + var name = attr.name.substr(3); + var value = JSON.parse(attr.value); + return { name: name, value: value }; + } +} + +autoRun(); + +},{"./lib/createTextSelectionInterceptor.js":2,"./lib/domController.js":3,"./lib/kinetic.js":4,"./lib/svgController.js":5,"./lib/transform.js":6,"amator":7,"ngraph.events":9,"wheel":10}],2:[function(require,module,exports){ +/** + * Disallows selecting text. + */ +module.exports = createTextSelectionInterceptor; + +function createTextSelectionInterceptor(useFake) { + if (useFake) { + return { + capture: noop, + release: noop + }; + } + + var dragObject; + var prevSelectStart; + var prevDragStart; + var wasCaptured = false; + + return { + capture: capture, + release: release + }; + + function capture(domObject) { + wasCaptured = true; + prevSelectStart = window.document.onselectstart; + prevDragStart = window.document.ondragstart; + + window.document.onselectstart = disabled; + + dragObject = domObject; + dragObject.ondragstart = disabled; + } + + function release() { + if (!wasCaptured) return; + + wasCaptured = false; + window.document.onselectstart = prevSelectStart; + if (dragObject) dragObject.ondragstart = prevDragStart; + } +} + +function disabled(e) { + e.stopPropagation(); + return false; +} + +function noop() {} + +},{}],3:[function(require,module,exports){ +module.exports = makeDomController + +module.exports.canAttach = isDomElement; + +function makeDomController(domElement, options) { + var elementValid = isDomElement(domElement); + if (!elementValid) { + throw new Error('panzoom requires DOM element to be attached to the DOM tree') + } + + var owner = domElement.parentElement; + domElement.scrollTop = 0; + + if (!options.disableKeyboardInteraction) { + owner.setAttribute('tabindex', 0); + } + + var api = { + getBBox: getBBox, + getOwner: getOwner, + applyTransform: applyTransform, + } + + return api + + function getOwner() { + return owner + } + + function getBBox() { + // TODO: We should probably cache this? + return { + left: 0, + top: 0, + width: domElement.clientWidth, + height: domElement.clientHeight + } + } + + function applyTransform(transform) { + // TODO: Should we cache this? + domElement.style.transformOrigin = '0 0 0'; + domElement.style.transform = 'matrix(' + + transform.scale + ', 0, 0, ' + + transform.scale + ', ' + + transform.x + ', ' + transform.y + ')' + } +} + +function isDomElement(element) { + return element && element.parentElement && element.style; +} + +},{}],4:[function(require,module,exports){ +/** + * Allows smooth kinetic scrolling of the surface + */ +module.exports = kinetic; + +function kinetic(getPoint, scroll, settings) { + if (typeof settings !== 'object') { + // setting could come as boolean, we should ignore it, and use an object. + settings = {}; + } + + var minVelocity = typeof settings.minVelocity === 'number' ? settings.minVelocity : 5; + var amplitude = typeof settings.amplitude === 'number' ? settings.amplitude : 0.25; + var cancelAnimationFrame = typeof settings.cancelAnimationFrame === 'function' ? settings.cancelAnimationFrame : getCancelAnimationFrame(); + var requestAnimationFrame = typeof settings.requestAnimationFrame === 'function' ? settings.requestAnimationFrame : getRequestAnimationFrame(); + + var lastPoint; + var timestamp; + var timeConstant = 342; + + var ticker; + var vx, targetX, ax; + var vy, targetY, ay; + + var raf; + + return { + start: start, + stop: stop, + cancel: dispose + }; + + function dispose() { + cancelAnimationFrame(ticker); + cancelAnimationFrame(raf); + } + + function start() { + lastPoint = getPoint(); + + ax = ay = vx = vy = 0; + timestamp = new Date(); + + cancelAnimationFrame(ticker); + cancelAnimationFrame(raf); + + // we start polling the point position to accumulate velocity + // Once we stop(), we will use accumulated velocity to keep scrolling + // an object. + ticker = requestAnimationFrame(track); + } + + function track() { + var now = Date.now(); + var elapsed = now - timestamp; + timestamp = now; + + var currentPoint = getPoint(); + + var dx = currentPoint.x - lastPoint.x; + var dy = currentPoint.y - lastPoint.y; + + lastPoint = currentPoint; + + var dt = 1000 / (1 + elapsed); + + // moving average + vx = 0.8 * dx * dt + 0.2 * vx; + vy = 0.8 * dy * dt + 0.2 * vy; + + ticker = requestAnimationFrame(track); + } + + function stop() { + cancelAnimationFrame(ticker); + cancelAnimationFrame(raf); + + var currentPoint = getPoint(); + + targetX = currentPoint.x; + targetY = currentPoint.y; + timestamp = Date.now(); + + if (vx < -minVelocity || vx > minVelocity) { + ax = amplitude * vx; + targetX += ax; + } + + if (vy < -minVelocity || vy > minVelocity) { + ay = amplitude * vy; + targetY += ay; + } + + raf = requestAnimationFrame(autoScroll); + } + + function autoScroll() { + var elapsed = Date.now() - timestamp; + + var moving = false; + var dx = 0; + var dy = 0; + + if (ax) { + dx = -ax * Math.exp(-elapsed / timeConstant); + + if (dx > 0.5 || dx < -0.5) moving = true; + else dx = ax = 0; + } + + if (ay) { + dy = -ay * Math.exp(-elapsed / timeConstant); + + if (dy > 0.5 || dy < -0.5) moving = true; + else dy = ay = 0; + } + + if (moving) { + scroll(targetX + dx, targetY + dy); + raf = requestAnimationFrame(autoScroll); + } + } +} + +function getCancelAnimationFrame() { + if (typeof cancelAnimationFrame === 'function') return cancelAnimationFrame; + return clearTimeout; +} + +function getRequestAnimationFrame() { + if (typeof requestAnimationFrame === 'function') return requestAnimationFrame; + + return function (handler) { + return setTimeout(handler, 16); + } +} +},{}],5:[function(require,module,exports){ +module.exports = makeSvgController +module.exports.canAttach = isSVGElement; + +function makeSvgController(svgElement, options) { + if (!isSVGElement(svgElement)) { + throw new Error('svg element is required for svg.panzoom to work') + } + + var owner = svgElement.ownerSVGElement + if (!owner) { + throw new Error( + 'Do not apply panzoom to the root element. ' + + 'Use its child instead (e.g. ). ' + + 'As of March 2016 only FireFox supported transform on the root element') + } + + if (!options.disableKeyboardInteraction) { + owner.setAttribute('tabindex', 0); + } + + var api = { + getBBox: getBBox, + getScreenCTM: getScreenCTM, + getOwner: getOwner, + applyTransform: applyTransform, + initTransform: initTransform + } + + return api + + function getOwner() { + return owner + } + + function getBBox() { + var bbox = svgElement.getBBox() + return { + left: bbox.x, + top: bbox.y, + width: bbox.width, + height: bbox.height, + } + } + + function getScreenCTM() { + var ctm = owner.getCTM(); + if (!ctm) { + // This is likely firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=873106 + // The code below is not entirely correct, but still better than nothing + return owner.getScreenCTM(); + } + return ctm; + } + + function initTransform(transform) { + var screenCTM = svgElement.getCTM() + + // The above line returns null on Firefox + if (screenCTM === null) { + screenCTM = document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGMatrix() + } + + transform.x = screenCTM.e; + transform.y = screenCTM.f; + transform.scale = screenCTM.a; + owner.removeAttributeNS(null, 'viewBox'); + } + + function applyTransform(transform) { + svgElement.setAttribute('transform', 'matrix(' + + transform.scale + ' 0 0 ' + + transform.scale + ' ' + + transform.x + ' ' + transform.y + ')') + } +} + +function isSVGElement(element) { + return element && element.ownerSVGElement && element.getCTM; +} +},{}],6:[function(require,module,exports){ +module.exports = Transform; + +function Transform() { + this.x = 0; + this.y = 0; + this.scale = 1; +} + +},{}],7:[function(require,module,exports){ +var BezierEasing = require('bezier-easing') + +// Predefined set of animations. Similar to CSS easing functions +var animations = { + ease: BezierEasing(0.25, 0.1, 0.25, 1), + easeIn: BezierEasing(0.42, 0, 1, 1), + easeOut: BezierEasing(0, 0, 0.58, 1), + easeInOut: BezierEasing(0.42, 0, 0.58, 1), + linear: BezierEasing(0, 0, 1, 1) +} + + +module.exports = animate; +module.exports.makeAggregateRaf = makeAggregateRaf; +module.exports.sharedScheduler = makeAggregateRaf(); + + +function animate(source, target, options) { + var start = Object.create(null) + var diff = Object.create(null) + options = options || {} + // We let clients specify their own easing function + var easing = (typeof options.easing === 'function') ? options.easing : animations[options.easing] + + // if nothing is specified, default to ease (similar to CSS animations) + if (!easing) { + if (options.easing) { + console.warn('Unknown easing function in amator: ' + options.easing); + } + easing = animations.ease + } + + var step = typeof options.step === 'function' ? options.step : noop + var done = typeof options.done === 'function' ? options.done : noop + + var scheduler = getScheduler(options.scheduler) + + var keys = Object.keys(target) + keys.forEach(function(key) { + start[key] = source[key] + diff[key] = target[key] - source[key] + }) + + var durationInMs = typeof options.duration === 'number' ? options.duration : 400 + var durationInFrames = Math.max(1, durationInMs * 0.06) // 0.06 because 60 frames pers 1,000 ms + var previousAnimationId + var frame = 0 + + previousAnimationId = scheduler.next(loop) + + return { + cancel: cancel + } + + function cancel() { + scheduler.cancel(previousAnimationId) + previousAnimationId = 0 + } + + function loop() { + var t = easing(frame/durationInFrames) + frame += 1 + setValues(t) + if (frame <= durationInFrames) { + previousAnimationId = scheduler.next(loop) + step(source) + } else { + previousAnimationId = 0 + setTimeout(function() { done(source) }, 0) + } + } + + function setValues(t) { + keys.forEach(function(key) { + source[key] = diff[key] * t + start[key] + }) + } +} + +function noop() { } + +function getScheduler(scheduler) { + if (!scheduler) { + var canRaf = typeof window !== 'undefined' && window.requestAnimationFrame + return canRaf ? rafScheduler() : timeoutScheduler() + } + if (typeof scheduler.next !== 'function') throw new Error('Scheduler is supposed to have next(cb) function') + if (typeof scheduler.cancel !== 'function') throw new Error('Scheduler is supposed to have cancel(handle) function') + + return scheduler +} + +function rafScheduler() { + return { + next: window.requestAnimationFrame.bind(window), + cancel: window.cancelAnimationFrame.bind(window) + } +} + +function timeoutScheduler() { + return { + next: function(cb) { + return setTimeout(cb, 1000/60) + }, + cancel: function (id) { + return clearTimeout(id) + } + } +} + +function makeAggregateRaf() { + var frontBuffer = new Set(); + var backBuffer = new Set(); + var frameToken = 0; + + return { + next: next, + cancel: next, + clearAll: clearAll + } + + function clearAll() { + frontBuffer.clear(); + backBuffer.clear(); + cancelAnimationFrame(frameToken); + frameToken = 0; + } + + function next(callback) { + backBuffer.add(callback); + renderNextFrame(); + } + + function renderNextFrame() { + if (!frameToken) frameToken = requestAnimationFrame(renderFrame); + } + + function renderFrame() { + frameToken = 0; + + var t = backBuffer; + backBuffer = frontBuffer; + frontBuffer = t; + + frontBuffer.forEach(function(callback) { + callback(); + }); + frontBuffer.clear(); + } + + function cancel(callback) { + backBuffer.delete(callback); + } +} + +},{"bezier-easing":8}],8:[function(require,module,exports){ +/** + * https://github.com/gre/bezier-easing + * BezierEasing - use bezier curve for transition easing function + * by Gaëtan Renaudeau 2014 - 2015 – MIT License + */ + +// These values are established by empiricism with tests (tradeoff: performance VS precision) +var NEWTON_ITERATIONS = 4; +var NEWTON_MIN_SLOPE = 0.001; +var SUBDIVISION_PRECISION = 0.0000001; +var SUBDIVISION_MAX_ITERATIONS = 10; + +var kSplineTableSize = 11; +var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0); + +var float32ArraySupported = typeof Float32Array === 'function'; + +function A (aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } +function B (aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } +function C (aA1) { return 3.0 * aA1; } + +// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. +function calcBezier (aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; } + +// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. +function getSlope (aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } + +function binarySubdivide (aX, aA, aB, mX1, mX2) { + var currentX, currentT, i = 0; + do { + currentT = aA + (aB - aA) / 2.0; + currentX = calcBezier(currentT, mX1, mX2) - aX; + if (currentX > 0.0) { + aB = currentT; + } else { + aA = currentT; + } + } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); + return currentT; +} + +function newtonRaphsonIterate (aX, aGuessT, mX1, mX2) { + for (var i = 0; i < NEWTON_ITERATIONS; ++i) { + var currentSlope = getSlope(aGuessT, mX1, mX2); + if (currentSlope === 0.0) { + return aGuessT; + } + var currentX = calcBezier(aGuessT, mX1, mX2) - aX; + aGuessT -= currentX / currentSlope; + } + return aGuessT; +} + +function LinearEasing (x) { + return x; +} + +module.exports = function bezier (mX1, mY1, mX2, mY2) { + if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { + throw new Error('bezier x values must be in [0, 1] range'); + } + + if (mX1 === mY1 && mX2 === mY2) { + return LinearEasing; + } + + // Precompute samples table + var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); + for (var i = 0; i < kSplineTableSize; ++i) { + sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); + } + + function getTForX (aX) { + var intervalStart = 0.0; + var currentSample = 1; + var lastSample = kSplineTableSize - 1; + + for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { + intervalStart += kSampleStepSize; + } + --currentSample; + + // Interpolate to provide an initial guess for t + var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); + var guessForT = intervalStart + dist * kSampleStepSize; + + var initialSlope = getSlope(guessForT, mX1, mX2); + if (initialSlope >= NEWTON_MIN_SLOPE) { + return newtonRaphsonIterate(aX, guessForT, mX1, mX2); + } else if (initialSlope === 0.0) { + return guessForT; + } else { + return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); + } + } + + return function BezierEasing (x) { + // Because JavaScript number are imprecise, we should guarantee the extremes are right. + if (x === 0) { + return 0; + } + if (x === 1) { + return 1; + } + return calcBezier(getTForX(x), mY1, mY2); + }; +}; + +},{}],9:[function(require,module,exports){ +module.exports = function eventify(subject) { + validateSubject(subject); + + var eventsStorage = createEventsStorage(subject); + subject.on = eventsStorage.on; + subject.off = eventsStorage.off; + subject.fire = eventsStorage.fire; + return subject; +}; + +function createEventsStorage(subject) { + // Store all event listeners to this hash. Key is event name, value is array + // of callback records. + // + // A callback record consists of callback function and its optional context: + // { 'eventName' => [{callback: function, ctx: object}] } + var registeredEvents = Object.create(null); + + return { + on: function (eventName, callback, ctx) { + if (typeof callback !== 'function') { + throw new Error('callback is expected to be a function'); + } + var handlers = registeredEvents[eventName]; + if (!handlers) { + handlers = registeredEvents[eventName] = []; + } + handlers.push({callback: callback, ctx: ctx}); + + return subject; + }, + + off: function (eventName, callback) { + var wantToRemoveAll = (typeof eventName === 'undefined'); + if (wantToRemoveAll) { + // Killing old events storage should be enough in this case: + registeredEvents = Object.create(null); + return subject; + } + + if (registeredEvents[eventName]) { + var deleteAllCallbacksForEvent = (typeof callback !== 'function'); + if (deleteAllCallbacksForEvent) { + delete registeredEvents[eventName]; + } else { + var callbacks = registeredEvents[eventName]; + for (var i = 0; i < callbacks.length; ++i) { + if (callbacks[i].callback === callback) { + callbacks.splice(i, 1); + } + } + } + } + + return subject; + }, + + fire: function (eventName) { + var callbacks = registeredEvents[eventName]; + if (!callbacks) { + return subject; + } + + var fireArguments; + if (arguments.length > 1) { + fireArguments = Array.prototype.splice.call(arguments, 1); + } + for(var i = 0; i < callbacks.length; ++i) { + var callbackInfo = callbacks[i]; + callbackInfo.callback.apply(callbackInfo.ctx, fireArguments); + } + + return subject; + } + }; +} + +function validateSubject(subject) { + if (!subject) { + throw new Error('Eventify cannot use falsy object as events subject'); + } + var reservedWords = ['on', 'fire', 'off']; + for (var i = 0; i < reservedWords.length; ++i) { + if (subject.hasOwnProperty(reservedWords[i])) { + throw new Error("Subject cannot be eventified, since it already has property '" + reservedWords[i] + "'"); + } + } +} + +},{}],10:[function(require,module,exports){ +/** + * This module used to unify mouse wheel behavior between different browsers in 2014 + * Now it's just a wrapper around addEventListener('wheel'); + * + * Usage: + * var addWheelListener = require('wheel').addWheelListener; + * var removeWheelListener = require('wheel').removeWheelListener; + * addWheelListener(domElement, function (e) { + * // mouse wheel event + * }); + * removeWheelListener(domElement, function); + */ + +module.exports = addWheelListener; + +// But also expose "advanced" api with unsubscribe: +module.exports.addWheelListener = addWheelListener; +module.exports.removeWheelListener = removeWheelListener; + + +function addWheelListener(element, listener, useCapture) { + element.addEventListener('wheel', listener, useCapture); +} + +function removeWheelListener( element, listener, useCapture ) { + element.removeEventListener('wheel', listener, useCapture); +} +},{}]},{},[1])(1) +});