Added LowPassFilter and support for smooth scatter rotation and zoom.
This commit is contained in:
parent
2a11f02bd2
commit
cd76ae22a4
2600
dist/iwmlib.js
vendored
2600
dist/iwmlib.js
vendored
File diff suppressed because it is too large
Load Diff
129
dist/iwmlib.pixi.js
vendored
129
dist/iwmlib.pixi.js
vendored
@ -3265,8 +3265,8 @@
|
|||||||
|
|
||||||
// Distance == 0.0 indicates an inside relation.
|
// Distance == 0.0 indicates an inside relation.
|
||||||
static distanceToRect(p, r) {
|
static distanceToRect(p, r) {
|
||||||
var cx = Math.max(Math.min(p.x, r.x + r.width), r.x);
|
let cx = Math.max(Math.min(p.x, r.x + r.width), r.x);
|
||||||
var cy = Math.max(Math.min(p.y, r.y + r.height), r.y);
|
let cy = Math.max(Math.min(p.y, r.y + r.height), r.y);
|
||||||
return Math.sqrt((p.x - cx) * (p.x - cx) + (p.y - cy) * (p.y - cy))
|
return Math.sqrt((p.x - cx) * (p.x - cx) + (p.y - cy) * (p.y - cy))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3702,6 +3702,92 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LowPassFilter {
|
||||||
|
|
||||||
|
constructor(smoothing = 0.5, bufferMaxSize=10) {
|
||||||
|
this.smoothing = smoothing; // must be smaller than 1
|
||||||
|
this.buffer = []; // FIFO queue
|
||||||
|
this.bufferMaxSize = bufferMaxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup buffer with array of values
|
||||||
|
*
|
||||||
|
* @param {array} values
|
||||||
|
* @returns {array}
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
setup(values) {
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
this.__push(values[i]);
|
||||||
|
}
|
||||||
|
return this.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear buffer to prepare for new values.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.buffer = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add new value to buffer (FIFO queue)
|
||||||
|
*
|
||||||
|
* @param {integer|float} value
|
||||||
|
* @returns {integer|float}
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
__push(value) {
|
||||||
|
let removed = (this.buffer.length === this.bufferMaxSize)
|
||||||
|
? this.buffer.shift()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
this.buffer.push(value);
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smooth value from stream
|
||||||
|
*
|
||||||
|
* @param {integer|float} nextValue
|
||||||
|
* @returns {integer|float}
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
next(nextValue) {
|
||||||
|
|
||||||
|
// push new value to the end, and remove oldest one
|
||||||
|
let removed = this.__push(nextValue);
|
||||||
|
// smooth value using all values from buffer
|
||||||
|
let result = this.buffer.reduce((last, current) => {
|
||||||
|
return this.smoothing * current + (1 - this.smoothing) * last
|
||||||
|
}, removed);
|
||||||
|
// replace smoothed value
|
||||||
|
this.buffer[this.buffer.length - 1] = result;
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smooth array of values
|
||||||
|
*
|
||||||
|
* @param {array} values
|
||||||
|
* @returns {undefined}
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
smoothArray(values) {
|
||||||
|
let value = values[0];
|
||||||
|
for (let i = 1; i < values.length; i++) {
|
||||||
|
let currentValue = values[i];
|
||||||
|
value += (currentValue - value) * this.smoothing;
|
||||||
|
values[i] = Math.round(value);
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* global apollo, subscriptions, gql */
|
/* global apollo, subscriptions, gql */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -6053,11 +6139,8 @@
|
|||||||
window.Capabilities = Capabilities;
|
window.Capabilities = Capabilities;
|
||||||
window.CapabilitiesTests = CapabilitiesTests;
|
window.CapabilitiesTests = CapabilitiesTests;
|
||||||
|
|
||||||
/** Basic class for poppable elements that need to be closed as soon as one poppable is
|
|
||||||
* shown.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base class for scatter specific events.
|
* A base class for scatter specific events.
|
||||||
*
|
*
|
||||||
@ -6322,7 +6405,8 @@
|
|||||||
scaleAutoClose = false,
|
scaleAutoClose = false,
|
||||||
scaleCloseThreshold = 0.10,
|
scaleCloseThreshold = 0.10,
|
||||||
scaleCloseBuffer = 0.05,
|
scaleCloseBuffer = 0.05,
|
||||||
maxRotation = Angle.degree2radian(5)
|
maxRotation = Angle.degree2radian(5),
|
||||||
|
useLowPassFilter = true
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (rotationDegrees != null && rotation != null) {
|
if (rotationDegrees != null && rotation != null) {
|
||||||
throw new Error('Use rotationDegrees or rotation but not both')
|
throw new Error('Use rotationDegrees or rotation but not both')
|
||||||
@ -6366,7 +6450,12 @@
|
|||||||
this.resizable = resizable;
|
this.resizable = resizable;
|
||||||
this.mouseZoomFactor = mouseZoomFactor;
|
this.mouseZoomFactor = mouseZoomFactor;
|
||||||
this.autoBringToFront = autoBringToFront;
|
this.autoBringToFront = autoBringToFront;
|
||||||
|
this.useLowPassFilter = useLowPassFilter;
|
||||||
|
if (useLowPassFilter) {
|
||||||
|
this.rotateLPF = new LowPassFilter();
|
||||||
|
this.zoomLPF = new LowPassFilter();
|
||||||
|
this.zoomLPF.setup([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
|
||||||
|
}
|
||||||
this.dragging = false;
|
this.dragging = false;
|
||||||
this.onTransform = onTransform != null ? [onTransform] : null;
|
this.onTransform = onTransform != null ? [onTransform] : null;
|
||||||
this.onClose = onClose != null ? [onClose] : null;
|
this.onClose = onClose != null ? [onClose] : null;
|
||||||
@ -6390,6 +6479,11 @@
|
|||||||
this.bringToFront();
|
this.bringToFront();
|
||||||
this.killAnimation();
|
this.killAnimation();
|
||||||
this.observeVelocity();
|
this.observeVelocity();
|
||||||
|
if (this.useLowPassFilter) {
|
||||||
|
this.rotateLPF.clear();
|
||||||
|
this.zoomLPF.clear();
|
||||||
|
this.zoomLPF.setup([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6403,14 +6497,19 @@
|
|||||||
let delta = interaction.delta();
|
let delta = interaction.delta();
|
||||||
if (delta != null) {
|
if (delta != null) {
|
||||||
this.addVelocity(delta);
|
this.addVelocity(delta);
|
||||||
let alpha = delta.rotate;
|
let rotate = delta.rotate;
|
||||||
|
let zoom = delta.zoom;
|
||||||
if (this.maxRotation != null) {
|
if (this.maxRotation != null) {
|
||||||
if (Math.abs(alpha) > this.maxRotation) {
|
if (Math.abs(rotate) > this.maxRotation) {
|
||||||
alpha = 0;
|
rotate = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.transform(delta, delta.zoom, alpha, delta.about);
|
if (this.useLowPassFilter) {
|
||||||
if (delta.zoom != 1) this.interactionAnchor = delta.about;
|
rotate = this.rotateLPF.next(rotate);
|
||||||
|
zoom = this.zoomLPF.next(zoom);
|
||||||
|
}
|
||||||
|
this.transform(delta, zoom, rotate, delta.about);
|
||||||
|
if (zoom != 1) this.interactionAnchor = delta.about;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14870,7 +14969,7 @@
|
|||||||
* @extends Popup
|
* @extends Popup
|
||||||
* @see {@link https://www.iwm-tuebingen.de/iwmbrowser/lib/pixi/popupmenu.html|DocTest}
|
* @see {@link https://www.iwm-tuebingen.de/iwmbrowser/lib/pixi/popupmenu.html|DocTest}
|
||||||
*/
|
*/
|
||||||
class PopupMenu$1 extends Popup {
|
class PopupMenu extends Popup {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of a PopupMenu.
|
* Creates an instance of a PopupMenu.
|
||||||
@ -15519,7 +15618,7 @@
|
|||||||
window.Stylus = Stylus;
|
window.Stylus = Stylus;
|
||||||
window.Switch = Switch;
|
window.Switch = Switch;
|
||||||
window.Popup = Popup;
|
window.Popup = Popup;
|
||||||
window.PopupMenu = PopupMenu$1;
|
window.PopupMenu = PopupMenu;
|
||||||
window.Modal = Modal;
|
window.Modal = Modal;
|
||||||
window.Volatile = Volatile;
|
window.Volatile = Volatile;
|
||||||
window.Message = Message;
|
window.Message = Message;
|
||||||
|
@ -16,7 +16,7 @@ import {FrameContainer, FrameTarget} from './frames.js'
|
|||||||
import {Inspect} from './inspect.js'
|
import {Inspect} from './inspect.js'
|
||||||
import {PointMap, InteractionPoints, Interaction, IInteractionTarget, InteractionDelta, InteractionMapper, InteractionDelegate, IInteractionMapperTarget} from './interaction.js'
|
import {PointMap, InteractionPoints, Interaction, IInteractionTarget, InteractionDelta, InteractionMapper, InteractionDelegate, IInteractionMapperTarget} from './interaction.js'
|
||||||
import {ResizeEvent, DOMScatterContainer, AbstractScatter, DOMScatter, ScatterEvent, BaseEvent} from './scatter.js'
|
import {ResizeEvent, DOMScatterContainer, AbstractScatter, DOMScatter, ScatterEvent, BaseEvent} from './scatter.js'
|
||||||
import {Cycle, Colors, Elements, Angle, Dates, Points, Polygon, Rect, Sets, Strings, isEmpty, getId, lerp, debounce, randomInt, randomFloat} from './utils.js'
|
import {Cycle, Colors, Elements, Angle, Dates, Points, Polygon, Rect, Sets, Strings, isEmpty, getId, lerp, debounce, randomInt, randomFloat, LowPassFilter} from './utils.js'
|
||||||
import UITest from './uitest.js'
|
import UITest from './uitest.js'
|
||||||
|
|
||||||
/* Needed to ensure that rollup.js includes class definitions and the classes
|
/* Needed to ensure that rollup.js includes class definitions and the classes
|
||||||
@ -62,6 +62,7 @@ window.InteractionMapper = InteractionMapper
|
|||||||
window.InteractionPoints = InteractionPoints
|
window.InteractionPoints = InteractionPoints
|
||||||
window.Interface = Interface
|
window.Interface = Interface
|
||||||
window.Logging = Logging
|
window.Logging = Logging
|
||||||
|
window.LowPassFilter = LowPassFilter
|
||||||
window.PointMap = PointMap
|
window.PointMap = PointMap
|
||||||
window.Rect = Rect
|
window.Rect = Rect
|
||||||
window.Points = Points
|
window.Points = Points
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
/* globals TweenLite debugCanvas */
|
/* globals TweenLite debugCanvas */
|
||||||
import { Points, Polygon, Angle, Elements } from './utils.js'
|
import { Points, Polygon, Angle, Elements, LowPassFilter } from './utils.js'
|
||||||
import Events from './events.js'
|
import Events from './events.js'
|
||||||
import { InteractionMapper } from './interaction.js'
|
import { InteractionMapper } from './interaction.js'
|
||||||
import { Capabilities } from './capabilities.js'
|
import { Capabilities } from './capabilities.js'
|
||||||
import PopupMenu from './popupmenu.js'
|
|
||||||
/**
|
/**
|
||||||
* A base class for scatter specific events.
|
* A base class for scatter specific events.
|
||||||
*
|
*
|
||||||
@ -270,7 +270,8 @@ export class AbstractScatter extends Throwable {
|
|||||||
scaleAutoClose = false,
|
scaleAutoClose = false,
|
||||||
scaleCloseThreshold = 0.10,
|
scaleCloseThreshold = 0.10,
|
||||||
scaleCloseBuffer = 0.05,
|
scaleCloseBuffer = 0.05,
|
||||||
maxRotation = Angle.degree2radian(5)
|
maxRotation = Angle.degree2radian(5),
|
||||||
|
useLowPassFilter = true
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (rotationDegrees != null && rotation != null) {
|
if (rotationDegrees != null && rotation != null) {
|
||||||
throw new Error('Use rotationDegrees or rotation but not both')
|
throw new Error('Use rotationDegrees or rotation but not both')
|
||||||
@ -314,7 +315,12 @@ export class AbstractScatter extends Throwable {
|
|||||||
this.resizable = resizable
|
this.resizable = resizable
|
||||||
this.mouseZoomFactor = mouseZoomFactor
|
this.mouseZoomFactor = mouseZoomFactor
|
||||||
this.autoBringToFront = autoBringToFront
|
this.autoBringToFront = autoBringToFront
|
||||||
|
this.useLowPassFilter = useLowPassFilter
|
||||||
|
if (useLowPassFilter) {
|
||||||
|
this.rotateLPF = new LowPassFilter()
|
||||||
|
this.zoomLPF = new LowPassFilter()
|
||||||
|
this.zoomLPF.setup([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
|
||||||
|
}
|
||||||
this.dragging = false
|
this.dragging = false
|
||||||
this.onTransform = onTransform != null ? [onTransform] : null
|
this.onTransform = onTransform != null ? [onTransform] : null
|
||||||
this.onClose = onClose != null ? [onClose] : null
|
this.onClose = onClose != null ? [onClose] : null
|
||||||
@ -338,6 +344,11 @@ export class AbstractScatter extends Throwable {
|
|||||||
this.bringToFront()
|
this.bringToFront()
|
||||||
this.killAnimation()
|
this.killAnimation()
|
||||||
this.observeVelocity()
|
this.observeVelocity()
|
||||||
|
if (this.useLowPassFilter) {
|
||||||
|
this.rotateLPF.clear()
|
||||||
|
this.zoomLPF.clear()
|
||||||
|
this.zoomLPF.setup([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,14 +362,19 @@ export class AbstractScatter extends Throwable {
|
|||||||
let delta = interaction.delta()
|
let delta = interaction.delta()
|
||||||
if (delta != null) {
|
if (delta != null) {
|
||||||
this.addVelocity(delta)
|
this.addVelocity(delta)
|
||||||
let alpha = delta.rotate
|
let rotate = delta.rotate
|
||||||
|
let zoom = delta.zoom
|
||||||
if (this.maxRotation != null) {
|
if (this.maxRotation != null) {
|
||||||
if (Math.abs(alpha) > this.maxRotation) {
|
if (Math.abs(rotate) > this.maxRotation) {
|
||||||
alpha = 0
|
rotate = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.transform(delta, delta.zoom, alpha, delta.about)
|
if (this.useLowPassFilter) {
|
||||||
if (delta.zoom != 1) this.interactionAnchor = delta.about
|
rotate = this.rotateLPF.next(rotate)
|
||||||
|
zoom = this.zoomLPF.next(zoom)
|
||||||
|
}
|
||||||
|
this.transform(delta, zoom, rotate, delta.about)
|
||||||
|
if (zoom != 1) this.interactionAnchor = delta.about
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +132,27 @@ is compatible with arrays of absolute points.
|
|||||||
context.stroke()
|
context.stroke()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
<h2>
|
||||||
|
Low Pass Filter
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Low Pass Filter muffles fast (high-frequency) changes to the signal.
|
||||||
|
For more information visit the <a href="http://en.wikipedia.org/wiki/Low-pass_filter">wikipedia article</a>.
|
||||||
|
</p>
|
||||||
|
<script class="doctest">
|
||||||
|
let lpf = new LowPassFilter(0.5)
|
||||||
|
Doctest.expect(lpf.smoothArray([10,8,9,10,12,8,50,10,12,8]), [10,9,9,10,11,9,30,20,16,12])
|
||||||
|
|
||||||
|
Doctest.expect(lpf.next(20), 10.0)
|
||||||
|
Doctest.expect(lpf.next(20), 12.5)
|
||||||
|
Doctest.expect(lpf.next(20), 14.375)
|
||||||
|
Doctest.expect(lpf.next(20), 15.78125)
|
||||||
|
|
||||||
|
lpf = new LowPassFilter(0.2)
|
||||||
|
lpf.setup([10,10,10,10,10,10,10,10,10,10])
|
||||||
|
Doctest.expect(lpf.next(20), 12.0)
|
||||||
|
Doctest.expect(lpf.next(10), 10.32)
|
||||||
|
</script>
|
||||||
<h2>
|
<h2>
|
||||||
References
|
References
|
||||||
</h2>
|
</h2>
|
||||||
|
91
lib/utils.js
91
lib/utils.js
@ -75,7 +75,7 @@ export function sample(population, k) {
|
|||||||
if (n <= setsize) {
|
if (n <= setsize) {
|
||||||
// An n-length list is smaller than a k-length set
|
// An n-length list is smaller than a k-length set
|
||||||
let pool = population.slice()
|
let pool = population.slice()
|
||||||
for (let i = 0; i < k; i++) { // invariant: non-selected at [0,n-i)
|
for (let i = 0; i < k; i++) { // inletiant: non-selected at [0,n-i)
|
||||||
let j = Math.random() * (n - i) | 0
|
let j = Math.random() * (n - i) | 0
|
||||||
result[i] = pool[j]
|
result[i] = pool[j]
|
||||||
pool[j] = pool[n - i - 1] // move non-selected item into vacancy
|
pool[j] = pool[n - i - 1] // move non-selected item into vacancy
|
||||||
@ -404,8 +404,8 @@ export class Points {
|
|||||||
|
|
||||||
// Distance == 0.0 indicates an inside relation.
|
// Distance == 0.0 indicates an inside relation.
|
||||||
static distanceToRect(p, r) {
|
static distanceToRect(p, r) {
|
||||||
var cx = Math.max(Math.min(p.x, r.x + r.width), r.x)
|
let cx = Math.max(Math.min(p.x, r.x + r.width), r.x)
|
||||||
var cy = Math.max(Math.min(p.y, r.y + r.height), r.y)
|
let cy = Math.max(Math.min(p.y, r.y + r.height), r.y)
|
||||||
return Math.sqrt((p.x - cx) * (p.x - cx) + (p.y - cy) * (p.y - cy))
|
return Math.sqrt((p.x - cx) * (p.x - cx) + (p.y - cy) * (p.y - cy))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -968,4 +968,89 @@ export class Strings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class LowPassFilter {
|
||||||
|
|
||||||
|
constructor(smoothing = 0.5, bufferMaxSize=10) {
|
||||||
|
this.smoothing = smoothing // must be smaller than 1
|
||||||
|
this.buffer = [] // FIFO queue
|
||||||
|
this.bufferMaxSize = bufferMaxSize
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup buffer with array of values
|
||||||
|
*
|
||||||
|
* @param {array} values
|
||||||
|
* @returns {array}
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
setup(values) {
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
this.__push(values[i])
|
||||||
|
}
|
||||||
|
return this.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear buffer to prepare for new values.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.buffer = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add new value to buffer (FIFO queue)
|
||||||
|
*
|
||||||
|
* @param {integer|float} value
|
||||||
|
* @returns {integer|float}
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
__push(value) {
|
||||||
|
let removed = (this.buffer.length === this.bufferMaxSize)
|
||||||
|
? this.buffer.shift()
|
||||||
|
: 0
|
||||||
|
|
||||||
|
this.buffer.push(value)
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smooth value from stream
|
||||||
|
*
|
||||||
|
* @param {integer|float} nextValue
|
||||||
|
* @returns {integer|float}
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
next(nextValue) {
|
||||||
|
|
||||||
|
// push new value to the end, and remove oldest one
|
||||||
|
let removed = this.__push(nextValue)
|
||||||
|
// smooth value using all values from buffer
|
||||||
|
let result = this.buffer.reduce((last, current) => {
|
||||||
|
return this.smoothing * current + (1 - this.smoothing) * last
|
||||||
|
}, removed)
|
||||||
|
// replace smoothed value
|
||||||
|
this.buffer[this.buffer.length - 1] = result
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smooth array of values
|
||||||
|
*
|
||||||
|
* @param {array} values
|
||||||
|
* @returns {undefined}
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
smoothArray(values) {
|
||||||
|
let value = values[0]
|
||||||
|
for (let i = 1; i < values.length; i++) {
|
||||||
|
let currentValue = values[i]
|
||||||
|
value += (currentValue - value) * this.smoothing
|
||||||
|
values[i] = Math.round(value)
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user