Added LowPassFilter and support for smooth scatter rotation and zoom.

This commit is contained in:
Uwe Oestermeier 2019-07-11 15:41:03 +02:00
parent 2a11f02bd2
commit cd76ae22a4
6 changed files with 1602 additions and 1276 deletions

2600
dist/iwmlib.js vendored

File diff suppressed because it is too large Load Diff

129
dist/iwmlib.pixi.js vendored
View File

@ -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;

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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>

View File

@ -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
}
}