535 lines
18 KiB
JavaScript
535 lines
18 KiB
JavaScript
|
/*
|
||
|
* This file is part of Toolkit.
|
||
|
*
|
||
|
* Toolkit 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.
|
||
|
*
|
||
|
* Toolkit 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
|
||
|
* Lesser 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
|
||
|
*/
|
||
|
"use strict";
|
||
|
(function(w, TK){
|
||
|
function vert(O) {
|
||
|
return O.layout === "left" || O.layout === "right";
|
||
|
}
|
||
|
function fill_interval(ctx, w, h, a, is_vertical) {
|
||
|
var i;
|
||
|
if (is_vertical) {
|
||
|
for (i = 0; i < a.length; i+= 2) {
|
||
|
ctx.fillRect(0, h - a[i+1], w, a[i+1]-a[i]);
|
||
|
}
|
||
|
} else {
|
||
|
for (i = 0; i < a.length; i+= 2) {
|
||
|
ctx.fillRect(a[i], 0, a[i+1]-a[i], h);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function clear_interval(ctx, w, h, a, is_vertical) {
|
||
|
var i;
|
||
|
if (is_vertical) {
|
||
|
for (i = 0; i < a.length; i+= 2) {
|
||
|
ctx.clearRect(0, h - a[i+1], w, a[i+1]-a[i]);
|
||
|
}
|
||
|
} else {
|
||
|
for (i = 0; i < a.length; i+= 2) {
|
||
|
ctx.clearRect(a[i], 0, a[i+1]-a[i], h);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function draw_full(ctx, w, h, a, is_vertical) {
|
||
|
|
||
|
ctx.fillRect(0, 0, w, h);
|
||
|
clear_interval(ctx, w, h, a, is_vertical);
|
||
|
}
|
||
|
function make_interval(a) {
|
||
|
var i, tmp, again, j;
|
||
|
|
||
|
do {
|
||
|
again = false;
|
||
|
for (i = 0; i < a.length-2; i+=2) {
|
||
|
if (a[i] > a[i+2]) {
|
||
|
tmp = a[i];
|
||
|
a[i] = a[i+2];
|
||
|
a[i+2] = tmp;
|
||
|
|
||
|
tmp = a[i+1];
|
||
|
a[i+1] = a[i+3];
|
||
|
a[i+3] = tmp;
|
||
|
again = true;
|
||
|
}
|
||
|
}
|
||
|
} while (again);
|
||
|
|
||
|
for (i = 0; i < a.length-2; i+= 2) {
|
||
|
if (a[i+1] > a[i+2]) {
|
||
|
if (a[i+3] > a[i+1]) {
|
||
|
a[i+1] = a[i+3];
|
||
|
}
|
||
|
for (j = i+3; j < a.length; j++) a[j-1] = a[j];
|
||
|
a.length = j-2;
|
||
|
i -=2;
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function cmp_intervals(a, b) {
|
||
|
var ret = 0;
|
||
|
var i;
|
||
|
|
||
|
for (i = 0; i < a.length; i+=2) {
|
||
|
if (a[i] === b[i]) {
|
||
|
if (a[i+1] === b[i+1]) continue;
|
||
|
ret |= (a[i+1] < b[i+1]) ? 1 : 2;
|
||
|
} else if (a[i+1] === b[i+1]) {
|
||
|
ret |= (a[i] > b[i]) ? 1 : 2;
|
||
|
} else return 4;
|
||
|
}
|
||
|
return ret;
|
||
|
}
|
||
|
function subtract_intervals(a, b) {
|
||
|
var i;
|
||
|
var ret = [];
|
||
|
|
||
|
for (i = 0; i < a.length; i+=2) {
|
||
|
if (a[i] === b[i]) {
|
||
|
if (a[i+1] <= b[i+1]) continue;
|
||
|
ret.push(b[i+1], a[i+1]);
|
||
|
} else {
|
||
|
if (a[i] > b[i]) continue;
|
||
|
ret.push(a[i], b[i]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
TK.MeterBase = TK.class({
|
||
|
/**
|
||
|
* TK.MeterBase is a base class to build different meters such as {@link TK.LevelMeter} from.
|
||
|
* TK.MeterBase uses {@link TK.Gradient} and contains a {@link TK.Scale} widget.
|
||
|
* TK.MeterBase inherits all options from {@link TK.Scale}.
|
||
|
*
|
||
|
* Note that the two options <code>format_labels</code> and
|
||
|
* <code>scale_base</code> have different names here.
|
||
|
*
|
||
|
* Note that level meters with high update frequencies can be very demanding when it comes
|
||
|
* to rendering performance. These performance requirements can be reduced by increasing the
|
||
|
* segment size using the <code>segment</code> option. Using a segment, the different level
|
||
|
* meter positions are reduced. This widget will take advantage of that by avoiding rendering those
|
||
|
* changes to the meter level, which fall into the same segment.
|
||
|
*
|
||
|
* @class TK.MeterBase
|
||
|
*
|
||
|
* @extends TK.Widget
|
||
|
*
|
||
|
* @param {Object} [options={ }] - An object containing initial options.
|
||
|
*
|
||
|
* @property {String} [options.layout="left"] - A string describing the layout of the meter.
|
||
|
* Possible values are <code>"left"</code>, <code>"right"</code>, <code>"top"</code> and
|
||
|
* <code>"bottom"</code>. <code>"left"</code> and <code>"right"</code> are vertical
|
||
|
* layouts, where the meter is on the left or right of the scale, respectively. Similarly,
|
||
|
* <code>"top"</code> and <code>"bottom"</code> are horizontal layouts in which the meter
|
||
|
* is at the top or the bottom, respectively.
|
||
|
* @property {Integer} [options.segment=1] - Segment size. Pixel positions of the meter level are
|
||
|
* rounded to multiples of this size. This can be used to give the level meter a LED effect and to
|
||
|
* reduce processor load.
|
||
|
* @property {Number} [options.value=0] - Level value.
|
||
|
* @property {Number} [options.base=false] - The base value of the meter. If set to <code>false</code>,
|
||
|
* the base will coincide with the minimum value <code>options.min</code>. The meter level is drawn
|
||
|
* starting from the base to the value.
|
||
|
* @property {Number} [options.label=0] - Value to be displayed on the label.
|
||
|
* @property {Function} [options.format_label=TK.FORMAT("%.2f")] - Function for formatting the
|
||
|
* label.
|
||
|
* @property {Boolean} [options.show_label=false] - If set to <code>true</code> a label is displayed.
|
||
|
* @property {Number} [options.title=false] - The title of the TK.MeterBase. Set to `false` to hide it.
|
||
|
* @property {Boolean} [options.show_scale=true] - Set to <code>false</code> to hide the scale.
|
||
|
* @property {Number|Boolean} [options.scale_base=false] - Base of the meter scale, see {@link TK.Scale} for more information.
|
||
|
* @property {Boolean} [options.show_labels=true] - If <code>true</code>, display labels on the
|
||
|
* scale.
|
||
|
* @property {Function} [options.format_labels=TK.FORMAT("%.2f")] - Function for formatting the
|
||
|
* scale labels. This is passed to TK.Scale as option <code>labels</code>.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
_class: "MeterBase",
|
||
|
Extends: TK.Widget,
|
||
|
Implements: [TK.Gradient],
|
||
|
_options: Object.assign(Object.create(TK.Widget.prototype._options),
|
||
|
TK.Gradient.prototype._options, TK.Scale.prototype._options, {
|
||
|
layout: "string",
|
||
|
segment: "number",
|
||
|
value: "number",
|
||
|
base: "number|boolean",
|
||
|
min: "number",
|
||
|
max: "number",
|
||
|
label: "number",
|
||
|
title: "string|boolean",
|
||
|
show_labels: "boolean",
|
||
|
format_label: "function",
|
||
|
scale_base: "number|boolean",
|
||
|
format_labels: "function",
|
||
|
background: "string|boolean",
|
||
|
gradient: "object|boolean"
|
||
|
}),
|
||
|
options: {
|
||
|
layout: "left",
|
||
|
segment: 1,
|
||
|
value: 0,
|
||
|
base: false,
|
||
|
label: 0,
|
||
|
title: false,
|
||
|
show_labels: true,
|
||
|
format_label: TK.FORMAT("%.2f"),
|
||
|
levels: [1, 5, 10], // array of steps where to draw labels
|
||
|
scale_base: false,
|
||
|
format_labels: TK.FORMAT("%.2f"),
|
||
|
background: false,
|
||
|
gradient: false
|
||
|
},
|
||
|
static_events: {
|
||
|
set_label: function(value) {
|
||
|
/**
|
||
|
* Is fired when the label changed.
|
||
|
* The argument is the actual label value.
|
||
|
*
|
||
|
* @event TK.MeterBase#labelchanged
|
||
|
*
|
||
|
* @param {string} label - The label of the {@link TK.MeterBase}.
|
||
|
*/
|
||
|
this.fire_event("labelchanged", value);
|
||
|
},
|
||
|
set_title: function(value) {
|
||
|
/**
|
||
|
* Is fired when the title changed.
|
||
|
* The argument is the actual title.
|
||
|
*
|
||
|
* @event TK.MeterBase#titlechanged
|
||
|
*
|
||
|
* @param {string} title - The title of the {@link TK.MeterBase}.
|
||
|
*/
|
||
|
this.fire_event("titlechanged", value);
|
||
|
},
|
||
|
set_segment: function(value) {
|
||
|
// what is this supposed to do?
|
||
|
// -> probably invalidate the value to force a redraw
|
||
|
this.set("value", this.options.value);
|
||
|
},
|
||
|
set_value: function(value) {
|
||
|
/**
|
||
|
* Is fired when the value changed.
|
||
|
* The argument is the actual value.
|
||
|
*
|
||
|
* @event TK.MeterBase#valuechanged
|
||
|
*
|
||
|
* @param {number} value - The value of the {@link TK.MeterBase}.
|
||
|
*/
|
||
|
this.fire_event("valuechanged", value);
|
||
|
},
|
||
|
set_base: function(value) {
|
||
|
if (value === false) {
|
||
|
var O = this.options;
|
||
|
O.base = value = O.min;
|
||
|
}
|
||
|
/**
|
||
|
* Is fired when the base value changed.
|
||
|
* The argument is the actual base value.
|
||
|
*
|
||
|
* @event TK.MeterBase#basechanged
|
||
|
*
|
||
|
* @param {number} base - The value of the base.
|
||
|
*/
|
||
|
this.fire_event("basechanged", value);
|
||
|
},
|
||
|
set_layout: function () {
|
||
|
var O = this.options;
|
||
|
this.set("value", O.value);
|
||
|
this.set("min", O.min);
|
||
|
this.trigger_resize();
|
||
|
},
|
||
|
rangedchanged: function() {
|
||
|
var gradient = this.options.gradient;
|
||
|
if (gradient) {
|
||
|
this.set("gradient", gradient);
|
||
|
}
|
||
|
},
|
||
|
},
|
||
|
|
||
|
initialize: function (options) {
|
||
|
var E;
|
||
|
TK.Widget.prototype.initialize.call(this, options);
|
||
|
var O = this.options;
|
||
|
/**
|
||
|
* @member {HTMLDivElement} TK.MeterBase#element - The main DIV container.
|
||
|
* Has class <code>toolkit-meter-base</code>.
|
||
|
*/
|
||
|
if (!(E = this.element)) this.element = E = TK.element("div");
|
||
|
TK.add_class(E, "toolkit-meter-base");
|
||
|
this.widgetize(E, false, true, true);
|
||
|
|
||
|
this._bar = TK.element("div", "toolkit-bar");
|
||
|
/**
|
||
|
* @member {HTMLCanvas} TK.MeterBase#_canvas - The canvas element drawing the mask.
|
||
|
* Has class <code>toolkit-mask</code>.
|
||
|
*/
|
||
|
this._canvas = document.createElement("canvas");
|
||
|
TK.add_class(this._canvas, "toolkit-mask");
|
||
|
|
||
|
this._fillstyle = false;
|
||
|
|
||
|
E.appendChild(this._bar);
|
||
|
|
||
|
this._bar.appendChild(this._canvas);
|
||
|
|
||
|
/**
|
||
|
* @member {HTMLDivElement} TK.MeterBase#_bar - The DIV element containing the masks
|
||
|
* and drawing the background. Has class <code>toolkit-bar</code>.
|
||
|
*/
|
||
|
this.delegate(this._bar);
|
||
|
this._last_meters = [];
|
||
|
this._current_meters = [];
|
||
|
|
||
|
this.set("label", O.value);
|
||
|
this.set("base", O.base);
|
||
|
},
|
||
|
|
||
|
destroy: function () {
|
||
|
this._bar.remove();
|
||
|
TK.Widget.prototype.destroy.call(this);
|
||
|
},
|
||
|
redraw: function () {
|
||
|
var I = this.invalid;
|
||
|
var O = this.options;
|
||
|
var E = this.element;
|
||
|
|
||
|
if (this._fillstyle === false) {
|
||
|
this._canvas.style.removeProperty("background-color");
|
||
|
TK.S.add(function() {
|
||
|
this._fillstyle = TK.get_style(this._canvas, "background-color");
|
||
|
TK.S.add(function() {
|
||
|
this._canvas.getContext("2d").fillStyle = this._fillstyle;
|
||
|
this._canvas.style.setProperty("background-color", "transparent", "important");
|
||
|
this.trigger_draw();
|
||
|
}.bind(this), 3);
|
||
|
}.bind(this), 2);
|
||
|
}
|
||
|
|
||
|
if (I.reverse) {
|
||
|
I.reverse = false;
|
||
|
TK.toggle_class(E, "toolkit-reverse", O.reverse);
|
||
|
}
|
||
|
if (I.gradient || I.background) {
|
||
|
I.gradient = I.background = false;
|
||
|
this.draw_gradient(this._bar, O.gradient, O.background);
|
||
|
}
|
||
|
|
||
|
TK.Widget.prototype.redraw.call(this);
|
||
|
|
||
|
if (I.layout) {
|
||
|
I.layout = false;
|
||
|
TK.remove_class(E, "toolkit-vertical",
|
||
|
"toolkit-horizontal", "toolkit-left",
|
||
|
"toolkit-right", "toolkit-top", "toolkit-bottom");
|
||
|
var scale = this.scale ? this.scale.element : null;
|
||
|
var bar = this._bar;
|
||
|
switch (O.layout) {
|
||
|
case "left":
|
||
|
TK.add_class(E, "toolkit-vertical", "toolkit-left");
|
||
|
if (scale) TK.insert_after(scale, bar);
|
||
|
break;
|
||
|
case "right":
|
||
|
TK.add_class(E, "toolkit-vertical", "toolkit-right");
|
||
|
if (scale) TK.insert_after(bar, scale);
|
||
|
break;
|
||
|
case "top":
|
||
|
TK.add_class(E, "toolkit-horizontal", "toolkit-top");
|
||
|
if (scale) TK.insert_after(scale, bar);
|
||
|
break;
|
||
|
case "bottom":
|
||
|
TK.add_class(E, "toolkit-horizontal", "toolkit-bottom");
|
||
|
if (scale) TK.insert_after(bar, scale);
|
||
|
break;
|
||
|
default:
|
||
|
throw("unsupported layout");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (this._fillstyle === false) return;
|
||
|
|
||
|
if (I.basis && O._height > 0 && O._width > 0) {
|
||
|
this._canvas.setAttribute("height", Math.round(O._height));
|
||
|
this._canvas.setAttribute("width", Math.round(O._width));
|
||
|
/* FIXME: I am not sure why this is even necessary */
|
||
|
this._canvas.style.width = O._width + "px";
|
||
|
this._canvas.style.height = O._height + "px";
|
||
|
this._canvas.getContext("2d").fillStyle = this._fillstyle;
|
||
|
}
|
||
|
|
||
|
if (I.value && O.show_label) {
|
||
|
this.label.set("label", O.format_label(O.value));
|
||
|
}
|
||
|
|
||
|
if (I.value || I.basis || I.min || I.max) {
|
||
|
I.basis = I.value = I.min = I.max = false;
|
||
|
this.draw_meter();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
resize: function() {
|
||
|
var O = this.options;
|
||
|
TK.Widget.prototype.resize.call(this);
|
||
|
var w = TK.inner_width(this._bar);
|
||
|
var h = TK.inner_height(this._bar);
|
||
|
this.set("_width", w);
|
||
|
this.set("_height", h);
|
||
|
var i = vert(O) ? h : w;
|
||
|
this.set("basis", i);
|
||
|
this._last_meters.length = 0;
|
||
|
this._fillstyle = false;
|
||
|
},
|
||
|
|
||
|
calculate_meter: function(to, value, i) {
|
||
|
var O = this.options;
|
||
|
// Set the mask elements according to options.value to show a value in
|
||
|
// the meter bar
|
||
|
var base = O.base;
|
||
|
var segment = O.segment|0;
|
||
|
var reverse = !!O.reverse;
|
||
|
var size = O.basis|0;
|
||
|
|
||
|
/* At this point the whole meter bar is filled. We now want
|
||
|
* to clear the area between base and value.
|
||
|
*/
|
||
|
|
||
|
/* canvas coordinates are reversed */
|
||
|
var v1 = this.val2px(base)|0;
|
||
|
var v2 = this.val2px(value)|0;
|
||
|
|
||
|
if (segment !== 1) v2 = segment*(Math.round(v2/segment)|0);
|
||
|
|
||
|
if (v2 < v1) {
|
||
|
to[i++] = v2;
|
||
|
to[i++] = v1;
|
||
|
} else {
|
||
|
to[i++] = v1;
|
||
|
to[i++] = v2;
|
||
|
}
|
||
|
|
||
|
return i;
|
||
|
},
|
||
|
|
||
|
draw_meter: function () {
|
||
|
var O = this.options;
|
||
|
var w = Math.round(O._width);
|
||
|
var h = Math.round(O._height);
|
||
|
var i, j;
|
||
|
|
||
|
if (!(w > 0 && h > 0)) return;
|
||
|
|
||
|
var a = this._current_meters;
|
||
|
var tmp = this._last_meters;
|
||
|
|
||
|
var i = this.calculate_meter(a, O.value, 0);
|
||
|
if (i < a.length) a.length = i;
|
||
|
make_interval(a);
|
||
|
|
||
|
this._last_meters = a;
|
||
|
this._current_meters = tmp;
|
||
|
|
||
|
var diff;
|
||
|
|
||
|
if (tmp.length === a.length) {
|
||
|
diff = cmp_intervals(tmp, a)|0;
|
||
|
} else diff = 4;
|
||
|
|
||
|
if (!diff) return;
|
||
|
|
||
|
// FIXME: this is currently broken for some reason
|
||
|
if (diff == 1)
|
||
|
diff = 4;
|
||
|
|
||
|
var ctx = this._canvas.getContext("2d");
|
||
|
ctx.fillStyle = this._fillstyle;
|
||
|
var is_vertical = vert(O);
|
||
|
|
||
|
if (diff === 1) {
|
||
|
/* a - tmp is non-empty */
|
||
|
clear_interval(ctx, w, h, subtract_intervals(a, tmp), is_vertical);
|
||
|
return;
|
||
|
}
|
||
|
if (diff === 2) {
|
||
|
/* tmp - a is non-empty */
|
||
|
fill_interval(ctx, w, h, subtract_intervals(tmp, a), is_vertical);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
draw_full(ctx, w, h, a, is_vertical);
|
||
|
},
|
||
|
|
||
|
// HELPERS & STUFF
|
||
|
_val2seg: function (val) {
|
||
|
// rounds values to fit in the segments size
|
||
|
// always returns values without taking options.reverse into account
|
||
|
var s = +this.val2px(this.snap(val));
|
||
|
s -= s % +this.options.segment;
|
||
|
if (this.options.reverse)
|
||
|
s = +this.options.basis - s;
|
||
|
return s;
|
||
|
},
|
||
|
|
||
|
has_base: function() {
|
||
|
var O = this.options;
|
||
|
return O.base > O.min;
|
||
|
},
|
||
|
|
||
|
});
|
||
|
/**
|
||
|
* @member {TK.Scale} TK.MeterBase#scale - The {@link TK.Scale} of the meter.
|
||
|
*/
|
||
|
TK.ChildWidget(TK.MeterBase, "scale", {
|
||
|
create: TK.Scale,
|
||
|
map_options: {
|
||
|
format_labels: "labels",
|
||
|
scale_base: "base",
|
||
|
},
|
||
|
inherit_options: true,
|
||
|
show: true,
|
||
|
toggle_class: true,
|
||
|
static_events: {
|
||
|
set: function(key, value) {
|
||
|
var p = this.parent;
|
||
|
if (p)
|
||
|
p.fire_event("scalechanged", key, value);
|
||
|
},
|
||
|
},
|
||
|
});
|
||
|
/**
|
||
|
* @member {TK.Label} TK.MeterBase#title - The {@link TK.Label} displaying the title.
|
||
|
* Has class <code>toolkit-title</code>.
|
||
|
*/
|
||
|
TK.ChildWidget(TK.MeterBase, "title", {
|
||
|
create: TK.Label,
|
||
|
show: false,
|
||
|
option: "title",
|
||
|
default_options: { "class" : "toolkit-title" },
|
||
|
map_options: { "title" : "label" },
|
||
|
toggle_class: true,
|
||
|
});
|
||
|
/**
|
||
|
* @member {TK.Label} TK.MeterBase#label - The {@link TK.Label} displaying the label.
|
||
|
*/
|
||
|
TK.ChildWidget(TK.MeterBase, "label", {
|
||
|
create: TK.Label,
|
||
|
show: false,
|
||
|
default_options: { "class" : "toolkit-value" },
|
||
|
toggle_class: true,
|
||
|
});
|
||
|
})(this, this.TK);
|