1143 lines
27 KiB
JavaScript
1143 lines
27 KiB
JavaScript
/*
|
|
* pure-knob
|
|
*
|
|
* Canvas-based JavaScript UI element implementing touch,
|
|
* keyboard, mouse and scroll wheel support.
|
|
*
|
|
* Copyright 2018 - 2021 Andre Plötze
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
(function (root, factory) {
|
|
|
|
if ((typeof define === 'function') && define.amd) {
|
|
/* AMD. Register as an anonymous module. */
|
|
define([], factory);
|
|
} else if ((typeof module === 'object') && module.exports) {
|
|
/*
|
|
* Node.
|
|
*
|
|
* Does not work with strict CommonJS, but
|
|
* only CommonJS-like environments that support
|
|
* module.exports, like Node.
|
|
*/
|
|
module.exports = factory();
|
|
} else {
|
|
/* Browser globals (root is window). */
|
|
root.pureknob = factory();
|
|
}
|
|
|
|
} (typeof self !== 'undefined' ? self : this, function () {
|
|
|
|
function PureKnob() {
|
|
|
|
/*
|
|
* Creates a bar graph element.
|
|
*/
|
|
this.createBarGraph = function(width, height) {
|
|
const heightString = height.toString();
|
|
const widthString = width.toString();
|
|
const canvas = document.createElement('canvas');
|
|
const div = document.createElement('div');
|
|
div.style.display = 'inline-block';
|
|
div.style.height = heightString + 'px';
|
|
div.style.position = 'relative';
|
|
div.style.textAlign = 'center';
|
|
div.style.width = widthString + 'px';
|
|
div.appendChild(canvas);
|
|
|
|
/*
|
|
* The bar graph object.
|
|
*/
|
|
const graph = {
|
|
'_canvas': canvas,
|
|
'_div': div,
|
|
'_height': height,
|
|
'_width': width,
|
|
|
|
/*
|
|
* Properties of this bar graph.
|
|
*/
|
|
'_properties': {
|
|
'colorBG': '#181818',
|
|
'colorFG': '#ff8800',
|
|
'colorMarkers': '#888888',
|
|
'markerStart': 0,
|
|
'markerEnd': 100,
|
|
'markerStep': 20,
|
|
'trackWidth': 0.5,
|
|
'valMin': 0,
|
|
'valMax': 100,
|
|
'valPeaks': [],
|
|
'val': 0
|
|
},
|
|
|
|
/*
|
|
* Returns the peak values for this bar graph.
|
|
*/
|
|
'getPeaks': function() {
|
|
const properties = this._properties;
|
|
const peaks = properties.valPeaks;
|
|
const numPeaks = peaks.length;
|
|
const peaksCopy = [];
|
|
|
|
/*
|
|
* Iterate over the peak values and copy them.
|
|
*/
|
|
for (let i = 0; i < numPeaks; i++) {
|
|
const peak = peaks[i];
|
|
peaksCopy.push(peak);
|
|
}
|
|
|
|
return peaksCopy;
|
|
},
|
|
|
|
/*
|
|
* Returns the value of a property of this bar graph.
|
|
*/
|
|
'getProperty': function(key) {
|
|
const properties = this._properties;
|
|
const value = properties[key];
|
|
return value;
|
|
},
|
|
|
|
/*
|
|
* Returns the current value of the bar graph.
|
|
*/
|
|
'getValue': function() {
|
|
const properties = this._properties;
|
|
const value = properties.val;
|
|
return value;
|
|
},
|
|
|
|
/*
|
|
* Return the DOM node representing this bar graph.
|
|
*/
|
|
'node': function() {
|
|
const div = this._div;
|
|
return div;
|
|
},
|
|
|
|
/*
|
|
* Redraw the bar graph on the canvas.
|
|
*/
|
|
'redraw': function() {
|
|
this.resize();
|
|
const properties = this._properties;
|
|
const colorTrack = properties.colorBG;
|
|
const colorFilling = properties.colorFG;
|
|
const colorMarkers = properties.colorMarkers;
|
|
const markerStart = properties.markerStart;
|
|
const markerEnd = properties.markerEnd;
|
|
const markerStep = properties.markerStep;
|
|
const trackWidth = properties.trackWidth;
|
|
const valMin = properties.valMin;
|
|
const valMax = properties.valMax;
|
|
const peaks = properties.valPeaks;
|
|
const value = properties.val;
|
|
const height = this._height;
|
|
const width = this._width;
|
|
const lineWidth = Math.round(trackWidth * height);
|
|
const halfWidth = 0.5 * lineWidth;
|
|
const centerY = 0.5 * height;
|
|
const lineTop = centerY - halfWidth;
|
|
const lineBottom = centerY + halfWidth;
|
|
const relativeValue = (value - valMin) / (valMax - valMin);
|
|
const fillingEnd = width * relativeValue;
|
|
const numPeaks = peaks.length;
|
|
const canvas = this._canvas;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
/*
|
|
* Clear the canvas.
|
|
*/
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
/*
|
|
* Check if markers should be drawn.
|
|
*/
|
|
if ((markerStart !== null) & (markerEnd !== null) & (markerStep !== null) & (markerStep !== 0)) {
|
|
ctx.lineCap = 'butt';
|
|
ctx.lineWidth = '2';
|
|
ctx.strokeStyle = colorMarkers;
|
|
|
|
/*
|
|
* Draw the markers.
|
|
*/
|
|
for (let v = markerStart; v <= markerEnd; v += markerStep) {
|
|
const relativePos = (v - valMin) / (valMax - valMin);
|
|
const pos = Math.round(width * relativePos);
|
|
ctx.beginPath();
|
|
ctx.moveTo(pos, 0);
|
|
ctx.lineTo(pos, height);
|
|
ctx.stroke();
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
* Draw the track.
|
|
*/
|
|
ctx.beginPath();
|
|
ctx.rect(0, lineTop, width, lineWidth);
|
|
ctx.fillStyle = colorTrack;
|
|
ctx.fill();
|
|
|
|
/*
|
|
* Draw the filling.
|
|
*/
|
|
ctx.beginPath();
|
|
ctx.rect(0, lineTop, fillingEnd, lineWidth);
|
|
ctx.fillStyle = colorFilling;
|
|
ctx.fill();
|
|
|
|
/*
|
|
* Prepare for drawing the peaks.
|
|
*/
|
|
ctx.strokeStyle = colorFilling;
|
|
|
|
/*
|
|
* Draw the peaks.
|
|
*/
|
|
for (let i = 0; i < numPeaks; i++) {
|
|
const peak = peaks[i];
|
|
const relativePeak = (peak - valMin) / (valMax - valMin);
|
|
const pos = Math.round(width * relativePeak);
|
|
ctx.beginPath();
|
|
ctx.moveTo(pos, lineTop);
|
|
ctx.lineTo(pos, lineBottom);
|
|
ctx.stroke();
|
|
}
|
|
|
|
},
|
|
|
|
/*
|
|
* This is called as the canvas or the surrounding DIV is resized.
|
|
*/
|
|
'resize': function() {
|
|
const canvas = this._canvas;
|
|
canvas.style.height = '100%';
|
|
canvas.style.width = '100%';
|
|
canvas.height = this._height;
|
|
canvas.width = this._width;
|
|
},
|
|
|
|
/*
|
|
* Sets the peak values of this bar graph.
|
|
*/
|
|
'setPeaks': function(peaks) {
|
|
const properties = this._properties;
|
|
const peaksCopy = [];
|
|
const numPeaks = peaks.length;
|
|
|
|
/*
|
|
* Iterate over the peak values and append them to the array.
|
|
*/
|
|
for (let i = 0; i < numPeaks; i++) {
|
|
const peak = peaks[i];
|
|
peaksCopy.push(peak);
|
|
}
|
|
|
|
this.setProperty('valPeaks', peaksCopy);
|
|
},
|
|
|
|
/*
|
|
* Sets the value of a property of this bar graph.
|
|
*/
|
|
'setProperty': function(key, value) {
|
|
this._properties[key] = value;
|
|
this.redraw();
|
|
},
|
|
|
|
/*
|
|
* Sets the value of this bar graph.
|
|
*/
|
|
'setValue': function(value) {
|
|
const properties = this._properties;
|
|
const valMin = properties.valMin;
|
|
const valMax = properties.valMax;
|
|
|
|
/*
|
|
* Clamp the actual value into the [valMin; valMax] range.
|
|
*/
|
|
if (value < valMin) {
|
|
value = valMin;
|
|
} else if (value > valMax) {
|
|
value = valMax;
|
|
}
|
|
|
|
value = Math.round(value);
|
|
this.setProperty('val', value);
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* This is called when the size of the canvas changes.
|
|
*/
|
|
const resizeListener = function(e) {
|
|
graph.redraw();
|
|
};
|
|
|
|
canvas.addEventListener('resize', resizeListener);
|
|
return graph;
|
|
}
|
|
|
|
/*
|
|
* Creates a knob element.
|
|
*/
|
|
this.createKnob = function(width, height) {
|
|
const heightString = height.toString();
|
|
const widthString = width.toString();
|
|
const smaller = width < height ? width : height;
|
|
const fontSize = 0.2 * smaller;
|
|
const fontSizeString = fontSize.toString();
|
|
const canvas = document.createElement('canvas');
|
|
const div = document.createElement('div');
|
|
|
|
div.style.display = 'inline-block';
|
|
div.style.height = heightString + 'px';
|
|
div.style.position = 'relative';
|
|
div.style.textAlign = 'center';
|
|
div.style.width = widthString + 'px';
|
|
div.appendChild(canvas);
|
|
|
|
const input = document.createElement('input');
|
|
input.style.appearance = 'textfield';
|
|
input.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
|
input.style.border = 'none';
|
|
input.style.color = '#ff8800';
|
|
input.style.fontFamily = 'sans-serif';
|
|
input.style.fontSize = fontSizeString + 'px';
|
|
input.style.height = heightString + 'px';
|
|
input.style.margin = 'auto';
|
|
input.style.padding = '0px';
|
|
input.style.textAlign = 'center';
|
|
input.style.width = widthString + 'px';
|
|
|
|
const inputMode = document.createAttribute('inputmode');
|
|
inputMode.value = 'numeric';
|
|
input.setAttributeNode(inputMode);
|
|
|
|
const inputDiv = document.createElement('div');
|
|
inputDiv.style.bottom = '0px';
|
|
inputDiv.style.display = 'none';
|
|
inputDiv.style.left = '0px';
|
|
inputDiv.style.position = 'absolute';
|
|
inputDiv.style.right = '0px';
|
|
inputDiv.style.top = '0px';
|
|
inputDiv.appendChild(input);
|
|
div.appendChild(inputDiv);
|
|
|
|
/*
|
|
* The knob object.
|
|
*/
|
|
const knob = {
|
|
'_canvas': canvas,
|
|
'_div': div,
|
|
'_height': height,
|
|
'_input': input,
|
|
'_inputDiv': inputDiv,
|
|
'_listeners': [],
|
|
'_mousebutton': false,
|
|
'_previousVal': 0,
|
|
'_timeout': null,
|
|
'_timeoutDoubleTap': null,
|
|
'_touchCount': 0,
|
|
'_width': width,
|
|
|
|
/*
|
|
* Notify listeners about value changes.
|
|
*/
|
|
'_notifyUpdate': function() {
|
|
const properties = this._properties;
|
|
const value = properties.val;
|
|
const listeners = this._listeners;
|
|
const numListeners = listeners.length;
|
|
|
|
/*
|
|
* Call all listeners.
|
|
*/
|
|
for (let i = 0; i < numListeners; i++) {
|
|
const listener = listeners[i];
|
|
|
|
/*
|
|
* Call listener, if it exists.
|
|
*/
|
|
if (listener !== null) {
|
|
listener(this, value);
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
/*
|
|
* Properties of this knob.
|
|
*/
|
|
'_properties': {
|
|
'angleEnd': 2.0 * Math.PI,
|
|
'angleOffset': -0.5 * Math.PI,
|
|
'angleStart': 0,
|
|
'colorBG': '#181818',
|
|
'colorFG': '#ff8800',
|
|
'colorLabel': '#ffffff',
|
|
'fnStringToValue': function(string) { return parseInt(string); },
|
|
'fnValueToString': function(value) { return value.toString(); },
|
|
'label': null,
|
|
'needle': false,
|
|
'readonly': false,
|
|
'textScale': 1.0,
|
|
'trackWidth': 0.4,
|
|
'valMin': 0,
|
|
'valMax': 100,
|
|
'val': 0
|
|
},
|
|
|
|
/*
|
|
* Abort value change, restoring the previous value.
|
|
*/
|
|
'abort': function() {
|
|
const previousValue = this._previousVal;
|
|
const properties = this._properties;
|
|
properties.val = previousValue;
|
|
this.redraw();
|
|
},
|
|
|
|
/*
|
|
* Adds an event listener.
|
|
*/
|
|
'addListener': function(listener) {
|
|
const listeners = this._listeners;
|
|
listeners.push(listener);
|
|
},
|
|
|
|
/*
|
|
* Commit value, indicating that it is no longer temporary.
|
|
*/
|
|
'commit': function() {
|
|
const properties = this._properties;
|
|
const value = properties.val;
|
|
this._previousVal = value;
|
|
this.redraw();
|
|
this._notifyUpdate();
|
|
},
|
|
|
|
/*
|
|
* Returns the value of a property of this knob.
|
|
*/
|
|
'getProperty': function(key) {
|
|
const properties = this._properties;
|
|
const value = properties[key];
|
|
return value;
|
|
},
|
|
|
|
/*
|
|
* Returns the current value of the knob.
|
|
*/
|
|
'getValue': function() {
|
|
const properties = this._properties;
|
|
const value = properties.val;
|
|
return value;
|
|
},
|
|
|
|
/*
|
|
* Return the DOM node representing this knob.
|
|
*/
|
|
'node': function() {
|
|
const div = this._div;
|
|
return div;
|
|
},
|
|
|
|
/*
|
|
* Redraw the knob on the canvas.
|
|
*/
|
|
'redraw': function() {
|
|
this.resize();
|
|
const properties = this._properties;
|
|
const needle = properties.needle;
|
|
const angleStart = properties.angleStart;
|
|
const angleOffset = properties.angleOffset;
|
|
const angleEnd = properties.angleEnd;
|
|
const actualStart = angleStart + angleOffset;
|
|
const actualEnd = angleEnd + angleOffset;
|
|
const label = properties.label;
|
|
const value = properties.val;
|
|
const valueToString = properties.fnValueToString;
|
|
const valueStr = valueToString(value);
|
|
const valMin = properties.valMin;
|
|
const valMax = properties.valMax;
|
|
const relValue = (value - valMin) / (valMax - valMin);
|
|
const relAngle = relValue * (angleEnd - angleStart);
|
|
const angleVal = actualStart + relAngle;
|
|
const colorTrack = properties.colorBG;
|
|
const colorFilling = properties.colorFG;
|
|
const colorLabel = properties.colorLabel;
|
|
const textScale = properties.textScale;
|
|
const trackWidth = properties.trackWidth;
|
|
const height = this._height;
|
|
const width = this._width;
|
|
const smaller = width < height ? width : height;
|
|
const centerX = 0.5 * width;
|
|
const centerY = 0.5 * height;
|
|
const radius = 0.4 * smaller;
|
|
const labelY = centerY + radius;
|
|
const lineWidth = Math.round(trackWidth * radius);
|
|
const labelSize = Math.round(0.8 * lineWidth);
|
|
const labelSizeString = labelSize.toString();
|
|
const fontSize = (0.2 * smaller) * textScale;
|
|
const fontSizeString = fontSize.toString();
|
|
const canvas = this._canvas;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
/*
|
|
* Clear the canvas.
|
|
*/
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
/*
|
|
* Draw the track.
|
|
*/
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, radius, actualStart, actualEnd);
|
|
ctx.lineCap = 'butt';
|
|
ctx.lineWidth = lineWidth;
|
|
ctx.strokeStyle = colorTrack;
|
|
ctx.stroke();
|
|
|
|
/*
|
|
* Draw the filling.
|
|
*/
|
|
ctx.beginPath();
|
|
|
|
/*
|
|
* Check if we're in needle mode.
|
|
*/
|
|
if (needle) {
|
|
ctx.arc(centerX, centerY, radius, angleVal - 0.1, angleVal + 0.1);
|
|
} else {
|
|
ctx.arc(centerX, centerY, radius, actualStart, angleVal);
|
|
}
|
|
|
|
ctx.lineCap = 'butt';
|
|
ctx.lineWidth = lineWidth;
|
|
ctx.strokeStyle = colorFilling;
|
|
ctx.stroke();
|
|
|
|
/*
|
|
* Draw the number.
|
|
*/
|
|
ctx.font = fontSizeString + 'px sans-serif';
|
|
ctx.fillStyle = colorFilling;
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(valueStr, centerX, centerY);
|
|
|
|
/*
|
|
* Draw the label
|
|
*/
|
|
if (label !== null) {
|
|
ctx.font = labelSizeString + 'px sans-serif';
|
|
ctx.fillStyle = colorLabel;
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(label, centerX, labelY);
|
|
}
|
|
|
|
/*
|
|
* Set the color and font size of the input element.
|
|
*/
|
|
const elemInput = this._input;
|
|
elemInput.style.color = colorFilling;
|
|
elemInput.style.fontSize = fontSizeString + 'px';
|
|
},
|
|
|
|
/*
|
|
* This is called as the canvas or the surrounding DIV is resized.
|
|
*/
|
|
'resize': function() {
|
|
const canvas = this._canvas;
|
|
canvas.style.height = '100%';
|
|
canvas.style.width = '100%';
|
|
canvas.height = this._height;
|
|
canvas.width = this._width;
|
|
},
|
|
|
|
/*
|
|
* Sets the value of a property of this knob.
|
|
*/
|
|
'setProperty': function(key, value) {
|
|
this._properties[key] = value;
|
|
this.redraw();
|
|
},
|
|
|
|
/*
|
|
* Sets the value of this knob.
|
|
*/
|
|
'setValue': function(value) {
|
|
this.setValueFloating(value);
|
|
this.commit();
|
|
},
|
|
|
|
/*
|
|
* Sets floating (temporary) value of this knob.
|
|
*/
|
|
'setValueFloating': function(value) {
|
|
const properties = this._properties;
|
|
const valMin = properties.valMin;
|
|
const valMax = properties.valMax;
|
|
|
|
/*
|
|
* Clamp the actual value into the [valMin; valMax] range.
|
|
*/
|
|
if (value < valMin) {
|
|
value = valMin;
|
|
} else if (value > valMax) {
|
|
value = valMax;
|
|
}
|
|
|
|
value = Math.round(value);
|
|
this.setProperty('val', value);
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* Convert mouse event to value.
|
|
*/
|
|
const mouseEventToValue = function(e, properties) {
|
|
const canvas = e.target;
|
|
const width = canvas.scrollWidth;
|
|
const height = canvas.scrollHeight;
|
|
const centerX = 0.5 * width;
|
|
const centerY = 0.5 * height;
|
|
const x = e.offsetX;
|
|
const y = e.offsetY;
|
|
const relX = x - centerX;
|
|
const relY = y - centerY;
|
|
const angleStart = properties.angleStart;
|
|
const angleEnd = properties.angleEnd;
|
|
const angleDiff = angleEnd - angleStart;
|
|
let angle = Math.atan2(relX, -relY) - angleStart;
|
|
const twoPi = 2.0 * Math.PI;
|
|
|
|
/*
|
|
* Make negative angles positive.
|
|
*/
|
|
if (angle < 0) {
|
|
|
|
if (angleDiff >= twoPi) {
|
|
angle += twoPi;
|
|
} else {
|
|
angle = 0;
|
|
}
|
|
|
|
}
|
|
|
|
const valMin = properties.valMin;
|
|
const valMax = properties.valMax;
|
|
let value = ((angle / angleDiff) * (valMax - valMin)) + valMin;
|
|
|
|
/*
|
|
* Clamp values into valid interval.
|
|
*/
|
|
if (value < valMin) {
|
|
value = valMin;
|
|
} else if (value > valMax) {
|
|
value = valMax;
|
|
}
|
|
|
|
return value;
|
|
};
|
|
|
|
/*
|
|
* Convert touch event to value.
|
|
*/
|
|
const touchEventToValue = function(e, properties) {
|
|
const canvas = e.target;
|
|
const rect = canvas.getBoundingClientRect();
|
|
const offsetX = rect.left;
|
|
const offsetY = rect.top;
|
|
const width = canvas.scrollWidth;
|
|
const height = canvas.scrollHeight;
|
|
const centerX = 0.5 * width;
|
|
const centerY = 0.5 * height;
|
|
const touches = e.targetTouches;
|
|
let touch = null;
|
|
|
|
/*
|
|
* If there are touches, extract the first one.
|
|
*/
|
|
if (touches.length > 0) {
|
|
touch = touches.item(0);
|
|
}
|
|
|
|
let x = 0.0;
|
|
let y = 0.0;
|
|
|
|
/*
|
|
* If a touch was extracted, calculate coordinates relative to
|
|
* the element position.
|
|
*/
|
|
if (touch !== null) {
|
|
const touchX = touch.pageX;
|
|
x = touchX - offsetX;
|
|
const touchY = touch.pageY;
|
|
y = touchY - offsetY;
|
|
}
|
|
|
|
const relX = x - centerX;
|
|
const relY = y - centerY;
|
|
const angleStart = properties.angleStart;
|
|
const angleEnd = properties.angleEnd;
|
|
const angleDiff = angleEnd - angleStart;
|
|
const twoPi = 2.0 * Math.PI;
|
|
let angle = Math.atan2(relX, -relY) - angleStart;
|
|
|
|
/*
|
|
* Make negative angles positive.
|
|
*/
|
|
if (angle < 0) {
|
|
|
|
if (angleDiff >= twoPi) {
|
|
angle += twoPi;
|
|
} else {
|
|
angle = 0;
|
|
}
|
|
|
|
}
|
|
|
|
const valMin = properties.valMin;
|
|
const valMax = properties.valMax;
|
|
let value = ((angle / angleDiff) * (valMax - valMin)) + valMin;
|
|
|
|
/*
|
|
* Clamp values into valid interval.
|
|
*/
|
|
if (value < valMin) {
|
|
value = valMin;
|
|
} else if (value > valMax) {
|
|
value = valMax;
|
|
}
|
|
|
|
return value;
|
|
};
|
|
|
|
/*
|
|
* Show input element on double click.
|
|
*/
|
|
const doubleClickListener = function(e) {
|
|
const properties = knob._properties;
|
|
const readonly = properties.readonly;
|
|
|
|
/*
|
|
* If knob is not read-only, display input element.
|
|
*/
|
|
if (!readonly) {
|
|
e.preventDefault();
|
|
const inputDiv = knob._inputDiv;
|
|
inputDiv.style.display = 'block';
|
|
const inputElem = knob._input;
|
|
inputElem.focus();
|
|
knob.redraw();
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* This is called when the mouse button is depressed.
|
|
*/
|
|
const mouseDownListener = function(e) {
|
|
const btn = e.buttons;
|
|
|
|
/*
|
|
* It is a left-click.
|
|
*/
|
|
if (btn === 1) {
|
|
const properties = knob._properties;
|
|
const readonly = properties.readonly;
|
|
|
|
/*
|
|
* If knob is not read-only, process mouse event.
|
|
*/
|
|
if (!readonly) {
|
|
e.preventDefault();
|
|
const val = mouseEventToValue(e, properties);
|
|
knob.setValueFloating(val);
|
|
}
|
|
|
|
knob._mousebutton = true;
|
|
}
|
|
|
|
/*
|
|
* It is a middle click.
|
|
*/
|
|
if (btn === 4) {
|
|
const properties = knob._properties;
|
|
const readonly = properties.readonly;
|
|
|
|
/*
|
|
* If knob is not read-only, display input element.
|
|
*/
|
|
if (!readonly) {
|
|
e.preventDefault();
|
|
const inputDiv = knob._inputDiv;
|
|
inputDiv.style.display = 'block';
|
|
const inputElem = knob._input;
|
|
inputElem.focus();
|
|
knob.redraw();
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* This is called when the mouse cursor is moved.
|
|
*/
|
|
const mouseMoveListener = function(e) {
|
|
const btn = knob._mousebutton;
|
|
|
|
/*
|
|
* Only process event, if mouse button is depressed.
|
|
*/
|
|
if (btn) {
|
|
const properties = knob._properties;
|
|
const readonly = properties.readonly;
|
|
|
|
/*
|
|
* If knob is not read-only, process mouse event.
|
|
*/
|
|
if (!readonly) {
|
|
e.preventDefault();
|
|
const val = mouseEventToValue(e, properties);
|
|
knob.setValueFloating(val);
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* This is called when the mouse button is released.
|
|
*/
|
|
const mouseUpListener = function(e) {
|
|
const btn = knob._mousebutton;
|
|
|
|
/*
|
|
* Only process event, if mouse button was depressed.
|
|
*/
|
|
if (btn) {
|
|
const properties = knob._properties;
|
|
const readonly = properties.readonly;
|
|
|
|
/*
|
|
* If knob is not read only, process mouse event.
|
|
*/
|
|
if (!readonly) {
|
|
e.preventDefault();
|
|
const val = mouseEventToValue(e, properties);
|
|
knob.setValue(val);
|
|
}
|
|
|
|
}
|
|
|
|
knob._mousebutton = false;
|
|
};
|
|
|
|
/*
|
|
* This is called when the drag action is canceled.
|
|
*/
|
|
const mouseCancelListener = function(e) {
|
|
const btn = knob._mousebutton;
|
|
|
|
/*
|
|
* Abort action if mouse button was depressed.
|
|
*/
|
|
if (btn) {
|
|
knob.abort();
|
|
knob._mousebutton = false;
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* This is called when a user touches the element.
|
|
*/
|
|
const touchStartListener = function(e) {
|
|
const properties = knob._properties;
|
|
const readonly = properties.readonly;
|
|
|
|
/*
|
|
* If knob is not read-only, process touch event.
|
|
*/
|
|
if (!readonly) {
|
|
const touches = e.targetTouches;
|
|
const numTouches = touches.length;
|
|
const singleTouch = (numTouches === 1);
|
|
|
|
/*
|
|
* Only process single touches, not multi-touch
|
|
* gestures.
|
|
*/
|
|
if (singleTouch) {
|
|
knob._mousebutton = true;
|
|
|
|
/*
|
|
* If this is the first touch, bind double tap
|
|
* interval.
|
|
*/
|
|
if (knob._touchCount === 0) {
|
|
|
|
/*
|
|
* This is executed when the double tap
|
|
* interval times out.
|
|
*/
|
|
const f = function() {
|
|
|
|
/*
|
|
* If control was tapped exactly
|
|
* twice, enable on-screen keyboard.
|
|
*/
|
|
if (knob._touchCount === 2) {
|
|
const properties = knob._properties;
|
|
const readonly = properties.readonly;
|
|
|
|
/*
|
|
* If knob is not read-only,
|
|
* display input element.
|
|
*/
|
|
if (!readonly) {
|
|
e.preventDefault();
|
|
const inputDiv = knob._inputDiv;
|
|
inputDiv.style.display = 'block';
|
|
const inputElem = knob._input;
|
|
inputElem.focus();
|
|
knob.redraw();
|
|
}
|
|
|
|
}
|
|
|
|
knob._touchCount = 0;
|
|
};
|
|
|
|
let timeout = knob._timeoutDoubleTap;
|
|
window.clearTimeout(timeout);
|
|
timeout = window.setTimeout(f, 500);
|
|
knob._timeoutDoubleTap = timeout;
|
|
}
|
|
|
|
knob._touchCount++;
|
|
const val = touchEventToValue(e, properties);
|
|
knob.setValueFloating(val);
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* This is called when a user moves a finger on the element.
|
|
*/
|
|
var touchMoveListener = function(e) {
|
|
const btn = knob._mousebutton;
|
|
|
|
/*
|
|
* Only process event, if mouse button is depressed.
|
|
*/
|
|
if (btn) {
|
|
const properties = knob._properties;
|
|
const readonly = properties.readonly;
|
|
|
|
/*
|
|
* If knob is not read-only, process touch event.
|
|
*/
|
|
if (!readonly) {
|
|
const touches = e.targetTouches;
|
|
const numTouches = touches.length;
|
|
const singleTouch = (numTouches === 1);
|
|
|
|
/*
|
|
* Only process single touches, not multi-touch
|
|
* gestures.
|
|
*/
|
|
if (singleTouch) {
|
|
e.preventDefault();
|
|
const val = touchEventToValue(e, properties);
|
|
knob.setValueFloating(val);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* This is called when a user lifts a finger off the element.
|
|
*/
|
|
const touchEndListener = function(e) {
|
|
const btn = knob._mousebutton;
|
|
|
|
/*
|
|
* Only process event, if mouse button was depressed.
|
|
*/
|
|
if (btn) {
|
|
const properties = knob._properties;
|
|
const readonly = properties.readonly;
|
|
|
|
/*
|
|
* If knob is not read only, process touch event.
|
|
*/
|
|
if (!readonly) {
|
|
const touches = e.targetTouches;
|
|
const numTouches = touches.length;
|
|
const noMoreTouches = (numTouches === 0);
|
|
|
|
/*
|
|
* Only commit value after the last finger has
|
|
* been lifted off.
|
|
*/
|
|
if (noMoreTouches) {
|
|
e.preventDefault();
|
|
knob._mousebutton = false;
|
|
knob.commit();
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
knob._mousebutton = false;
|
|
};
|
|
|
|
/*
|
|
* This is called when a user cancels a touch action.
|
|
*/
|
|
const touchCancelListener = function(e) {
|
|
const btn = knob._mousebutton;
|
|
|
|
/*
|
|
* Abort action if mouse button was depressed.
|
|
*/
|
|
if (btn) {
|
|
knob.abort();
|
|
knob._touchCount = 0;
|
|
const timeout = knob._timeoutDoubleTap;
|
|
window.clearTimeout(timeout);
|
|
}
|
|
|
|
knob._mousebutton = false;
|
|
};
|
|
|
|
/*
|
|
* This is called when the size of the canvas changes.
|
|
*/
|
|
const resizeListener = function(e) {
|
|
knob.redraw();
|
|
};
|
|
|
|
/*
|
|
* This is called when the mouse wheel is moved.
|
|
*/
|
|
const scrollListener = function(e) {
|
|
const readonly = knob.getProperty('readonly');
|
|
|
|
/*
|
|
* If knob is not read only, process mouse wheel event.
|
|
*/
|
|
if (!readonly) {
|
|
e.preventDefault();
|
|
const delta = e.deltaY;
|
|
const direction = delta > 0 ? 1 : (delta < 0 ? -1 : 0);
|
|
let val = knob.getValue();
|
|
val += direction;
|
|
knob.setValueFloating(val);
|
|
|
|
/*
|
|
* Perform delayed commit.
|
|
*/
|
|
const commit = function() {
|
|
knob.commit();
|
|
};
|
|
|
|
let timeout = knob._timeout;
|
|
window.clearTimeout(timeout);
|
|
timeout = window.setTimeout(commit, 250);
|
|
knob._timeout = timeout;
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* This is called when the user presses a key on the keyboard.
|
|
*/
|
|
const keyDownListener = function(e) {
|
|
const k = e.key;
|
|
|
|
/*
|
|
* Hide input element when user presses enter or escape.
|
|
*/
|
|
if ((k === 'Enter') || (k === 'Escape')) {
|
|
const inputDiv = knob._inputDiv;
|
|
inputDiv.style.display = 'none';
|
|
const input = e.target;
|
|
|
|
/*
|
|
* Only evaluate value when user pressed enter.
|
|
*/
|
|
if (k === 'Enter') {
|
|
const properties = knob._properties;
|
|
const value = input.value;
|
|
const stringToValue = properties.fnStringToValue;
|
|
const val = stringToValue(value);
|
|
const valid = isFinite(val);
|
|
|
|
/*
|
|
* Check if input is a valid number.
|
|
*/
|
|
if (valid) {
|
|
knob.setValue(val);
|
|
}
|
|
|
|
}
|
|
|
|
input.value = '';
|
|
}
|
|
|
|
};
|
|
|
|
canvas.addEventListener('dblclick', doubleClickListener);
|
|
canvas.addEventListener('mousedown', mouseDownListener);
|
|
canvas.addEventListener('mouseleave', mouseCancelListener);
|
|
canvas.addEventListener('mousemove', mouseMoveListener);
|
|
canvas.addEventListener('mouseup', mouseUpListener);
|
|
canvas.addEventListener('resize', resizeListener);
|
|
canvas.addEventListener('touchstart', touchStartListener);
|
|
canvas.addEventListener('touchmove', touchMoveListener);
|
|
canvas.addEventListener('touchend', touchEndListener);
|
|
canvas.addEventListener('touchcancel', touchCancelListener);
|
|
canvas.addEventListener('wheel', scrollListener);
|
|
input.addEventListener('keydown', keyDownListener);
|
|
return knob;
|
|
};
|
|
|
|
}
|
|
|
|
return new PureKnob();
|
|
}));
|
|
|