/*
* This file is part of AUX.
*
* AUX is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* AUX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General
* Public License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*/
/* jshint -W018 */
import { defineChildWidget } from '../child_widget.js';
import { Meter } from './meter.js';
import { State } from './state.js';
import { addClass, toggleClass } from '../utils/dom.js';
import { effectiveValue } from '../modules/range.js';
import { defineRender, defineMeasure, deferRenderNext } from '../renderer.js';
import {
createTimer,
startTimer,
destroyTimer,
cancelTimer,
} from '../utils/timers.js';
/**
* LevelMeter is a fully functional meter bar displaying numerical values.
* LevelMeter is an enhanced {@link Meter} containing a clip LED and hold markers.
* In addition, LevelMeter has an optional falling animation, top and bottom peak
* values and more.
*
* @class LevelMeter
*
* @extends Meter
*
* @param {Object} [options={ }] - An object containing initial options.
*
* @property {Boolean} [options.show_clip=false] - If set to <code>true</code>, show the clipping LED.
* @property {Number} [options.clipping=0] - If clipping is enabled, this is the threshold for the
* clipping effect.
* @property {Integer|Boolean} [options.auto_clip=false] - This is the clipping timeout. If set to
* <code>false</code> automatic clipping is disabled. If set to <code>n</code> the clipping effect
* times out after <code>n</code> ms, if set to <code>-1</code> it remains forever.
* @property {Boolean} [options.clip=false] - If clipping is enabled, this option is set to
* <code>true</code> when clipping happens. When automatic clipping is disabled, it can be set to
* <code>true</code> to set the clipping state.
* @property {Boolean} [options.show_hold=false] - If set to <code>true</code>, show the hold value LED.
* @property {Integer} [options.hold_size=1] - Size of the hold value LED in the number of segments.
* @property {Number|boolean} [options.auto_hold=false] - If set to <code>false</code> the automatic
* hold LED is disabled, if set to <code>n</code> the hold value is reset after <code>n</code> ms and
* if set to <code>-1</code> the hold value is not reset automatically.
* @property {Number} [options.top=false] - The top hold value. If set to <code>false</code> it will
* equal the meter level.
* @property {Number} [options.bottom=false] - The bottom hold value. This only exists if a
* <code>base</code> value is set and the value falls below the base.
* @property {Integer|Boolean} [options.peak_value=false] - If set to <code>false</code> the automatic peak
* label is disabled, if set to <code>n</code> the peak label is reset after <code>n</code> ms and
* if set to <code>-1</code> it remains forever.
* @property {Number} [options.falling=0] - If set to a positive number, activates the automatic falling
* animation. The meter level will fall by this amount over the time set via `options.falling_duration`.
* @property {Number} [options.falling_duration=1000] - This is the time in milliseconds for the falling
* animation. The level falls by `options.falling` in this period of time.
* @property {Number} [options.falling_init=50] - Initial falling delay in milliseconds. This option
* can be used to delay the start of the falling animation in order to avoid flickering if internal
* and external falling are combined.
*/
export class LevelMeter extends Meter {
static get _options() {
return {
falling: 'number',
falling_duration: 'int',
falling_init: 'number',
top: 'number',
bottom: 'number',
hold_size: 'int',
show_hold: 'boolean',
clipping: 'number',
auto_clip: 'int|boolean',
auto_hold: 'int|boolean',
peak_value: 'int|boolean',
};
}
static get options() {
return {
clip: false,
falling: 0,
falling_duration: 1000,
falling_init: 50,
top: false,
bottom: false,
hold_size: 1,
show_hold: false,
clipping: 0,
auto_clip: false,
auto_hold: false,
peak_value: false,
};
}
static get static_events() {
return {
set_auto_clip: function (value) {
if (!(value > 0)) this._clip_timer = destroyTimer(this._clip_timer);
},
set_peak_value: function (value) {
if (!(value > 0)) this._value_timer = destroyTimer(this._value_timer);
if (value !== false) this.set('sync_value', false);
},
set_auto_hold: function (value) {
if (!(value > 0)) {
this._top_timer = destroyTimer(this._top_timer);
this._bottom_timer = destroyTimer(this._bottom_timer);
}
},
};
}
static get renderers() {
return [
defineRender('show_hold', function (show_hold) {
toggleClass(this.element, 'aux-has-hold', show_hold);
}),
defineMeasure(['top', 'bottom', 'base'], function () {
/* top and bottom require a meter redraw, so lets invalidate
* value */
this.invalidate('value');
}),
defineRender('clip', function (clip) {
toggleClass(this.element, 'aux-clipping', clip);
}),
];
}
initialize(options) {
/* track the age of the value option */
super.initialize(options);
this._value_timer = createTimer(this.resetValue.bind(this));
this._clip_timer = createTimer(this.resetClip.bind(this));
this._top_timer = createTimer(this.resetTop.bind(this));
this._bottom_timer = createTimer(this.resetBottom.bind(this));
/**
* @member {HTMLDivElement} LevelMeter#element - The main DIV container.
* Has class <code>.aux-levelmeter</code>.
*/
const O = this.options;
if (O.top === false) O.top = O.value;
if (O.bottom === false) O.bottom = O.value;
if (O.falling < 0) O.falling = -O.falling;
if (O.peak_value !== false) this.set('peak_value', O.peak_value);
}
draw(O, element) {
addClass(element, 'aux-levelmeter');
super.draw(O, element);
}
drawMeter() {
super.drawMeter();
const O = this.options;
const falling = +O.falling;
if (!falling) return;
const base = +O.base;
const value = this.effectiveValue();
if (value === base) return null;
return deferRenderNext(() => {
return this.drawMeter();
});
}
destroy() {
this._clip_timer = destroyTimer(this._clip_timer);
this._top_timer = destroyTimer(this._top_timer);
this._bottom_timer = destroyTimer(this._bottom_timer);
this._value_timer = destroyTimer(this._value_timer);
super.destroy();
}
effectiveValue() {
const {
value,
base,
falling,
falling_duration,
falling_init,
_value_time,
} = this.options;
return effectiveValue(
value,
base,
falling,
falling_duration,
falling_init,
_value_time
);
}
/**
* Resets the value.
*
* @method LevelMeter#resetValue
*
* @emits LevelMeter#resetvalue
*/
resetValue() {
this._value_timer = cancelTimer(this._value_timer);
this.set('value_label', this.effectiveValue());
/**
* Is fired when the value label was reset.
*
* @event LevelMeter#resetvalue
*/
this.emit('resetvalue');
}
/**
* Resets the clipping LED.
*
* @method LevelMeter#resetClip
*
* @emits LevelMeter#resetclip
*/
resetClip() {
this._clip_timer = cancelTimer(this._clip_timer);
this.set('clip', false);
/**
* Is fired when the clipping LED was reset.
*
* @event LevelMeter#resetclip
*/
this.emit('resetclip');
}
/**
* Resets the top hold.
*
* @method LevelMeter#resetTop
*
* @emits LevelMeter#resettop
*/
resetTop() {
this._top_timer = cancelTimer(this._top_timer);
this.set('top', this.effectiveValue());
/**
* Is fired when the top hold was reset.
*
* @event LevelMeter#resettop
*/
this.emit('resettop');
}
/**
* Resets the bottom hold.
*
* @method LevelMeter#resetBottom
*
* @emits LevelMeter#resetbottom
*/
resetBottom() {
this._bottom_timer = cancelTimer(this._bottom_timer);
this.set('bottom', this.effectiveValue());
/**
* Is fired when the bottom hold was reset.
*
* @event LevelMeter#resetbottom
*/
this.emit('resetbottom');
}
/**
* Resets all hold features.
*
* @method LevelMeter#resetAll
*
* @emits LevelMeter#resetvalue
* @emits LevelMeter#resetclip
* @emits LevelMeter#resettop
* @emits LevelMeter#resetbottom
*/
resetAll() {
this.resetValue();
this.resetClip();
this.resetTop();
this.resetBottom();
}
/*
* This is an _internal_ method, which calculates the non-filled regions
* in the overlaying canvas as pixel positions. The canvas is only modified
* using this information when it has _actually_ changed. This can save a lot
* of performance in cases where the segment size is > 1 or on small devices where
* the meter has a relatively small pixel size.
*/
calculateMeter(to, value, i) {
const O = this.options;
const base = +O.base;
const transformation = O.transformation;
value = +this.effectiveValue();
i = super.calculateMeter(to, value, i);
if (!O.show_hold) return i;
// shorten things
let hold = +O.top;
const segment = O.segment | 0;
const hold_size = O.hold_size * segment;
let pos;
if (!(hold_size > 0)) return i;
const pos_base = +transformation.valueToPixel(base);
if (hold > base) {
/* TODO: lets snap in set() */
pos = transformation.valueToPixel(hold) | 0;
if (segment !== 1) pos = segment * (Math.round(pos / segment) | 0);
if (pos > pos_base) {
to[i++] = pos - hold_size;
to[i++] = pos;
} else {
to[i++] = pos;
to[i++] = pos + hold_size;
}
}
hold = +O.bottom;
if (hold < base) {
pos = transformation.valueToPixel(hold) | 0;
if (segment !== 1) pos = segment * (Math.round(pos / segment) | 0);
if (pos > pos_base) {
to[i++] = pos - hold_size;
to[i++] = pos;
} else {
to[i++] = pos;
to[i++] = pos + hold_size;
}
}
return i;
}
// GETTER & SETTER
set(key, value) {
if (key === 'value') {
const O = this.options;
const { base, falling } = O;
if (falling) {
const effectiveValue = this.effectiveValue();
if (
(base < effectiveValue && value < effectiveValue) ||
(base > effectiveValue && value > effectiveValue)
) {
return value;
}
}
this.set('_value_time', performance.now());
// snap will enforce clipping
value = O.snap_module.snap(value);
if (O.auto_clip !== false && value >= O.clipping && !this.hasBase()) {
this.set('clip', true);
if (O.auto_clip >= 0)
this._clip_timer = startTimer(this._clip_timer, O.auto_clip);
}
const peak_value = O.peak_value;
if (O.show_value && peak_value !== false) {
const value_label = O.value_label;
if (
(value > value_label && value > base) ||
(value < value_label && value < base)
) {
this.set('value_label', value);
if (peak_value >= 0)
this._value_timer = startTimer(this._value_timer, peak_value);
}
}
if (O.auto_hold !== false && O.show_hold) {
const auto_hold = O.auto_hold;
const top = O.top;
if (value >= top) {
if (auto_hold >= 0)
this._top_timer = startTimer(this._top_timer, auto_hold);
this.set('top', value);
}
if (this.hasBase()) {
const bottom = O.bottom;
if (value <= bottom) {
this.set('bottom', value);
if (auto_hold >= 0)
this._bottom_timer = startTimer(this._bottom_timer, auto_hold);
}
}
}
} else if (key === 'top' || key === 'bottom') {
value = this.options.snap_module.snap(value);
}
return super.set(key, value);
}
}
/**
* @member {State} LevelMeter#clip - The {@link State} instance for the clipping LED.
* @member {HTMLDivElement} LevelMeter#clip.element - The DIV element of the clipping LED.
* Has class <code>.aux-clip</code>.
*/
defineChildWidget(LevelMeter, 'clip', {
create: State,
show: false,
map_options: {
clip: 'state',
},
default_options: {
class: 'aux-clip',
},
toggle_class: true,
});