html5 球形选择,HTML5 一款很棒的球形把手(旋钮)界面交互组件

JavaScript

语言:

JaveScriptBabelCoffeeScript

确定

class KnobInput {

constructor(containerElement, options) {

if (!options) {

options = {};

}

// settings

var step = options.step || 'any';

var min = typeof options.min === 'number' ? options.min : 0;

var max = typeof options.max === 'number' ? options.max : 1;

this.initial = typeof options.initial === 'number' ? options.initial : 0.5 * (min + max);

this.visualElementClass = options.visualElementClass || 'knob-input__visual';

this.dragResistance = typeof options.dragResistance === 'number' ? options.dragResistance : 300;

this.dragResistance /= max - min;

this.wheelResistance = typeof options.wheelResistance === 'number' ? options.wheelResistance : 4000;

this.wheelResistance /= max - min;

this.setupVisualContext = typeof options.visualContext === 'function' ? options.visualContext : KnobInput.setupRotationContext(0, 360);

this.updateVisuals = typeof options.updateVisuals === 'function' ? options.updateVisuals : KnobInput.rotationUpdateFunction;

// setup input

var rangeInput = document.createElement('input');

rangeInput.type = 'range';

rangeInput.step = step;

rangeInput.min = min;

rangeInput.max = max;

rangeInput.value = this.initial;

containerElement.appendChild(rangeInput);

// elements

this._container = containerElement;

this._container.classList.add('knob-input');

this._input = rangeInput;

this._input.classList.add('knob-input__input');

this._visualElement = this._container.querySelector(`.${this.visualElementClass}`);

this._visualElement.classList.add('knob-input__visual');

// visual context

this._visualContext = {

element: this._visualElement

};

this.setupVisualContext.apply(this._visualContext);

this.updateVisuals = this.updateVisuals.bind(this._visualContext);

// internals

this._activeDrag = false;

// define event listeners

// have to store bound versions of handlers so they can be removed later

this._handlers = {

inputChange: this.handleInputChange.bind(this),

touchStart: this.handleTouchStart.bind(this),

touchMove: this.handleTouchMove.bind(this),

touchEnd: this.handleTouchEnd.bind(this),

touchCancel: this.handleTouchCancel.bind(this),

mouseDown: this.handleMouseDown.bind(this),

mouseMove: this.handleMouseMove.bind(this),

mouseUp: this.handleMouseUp.bind(this),

mouseWheel: this.handleMouseWheel.bind(this),

doubleClick: this.handleDoubleClick.bind(this),

focus: this.handleFocus.bind(this),

blur: this.handleBlur.bind(this),

};

// add listeners

this._input.addEventListener('change', this._handlers.inputChange);

this._input.addEventListener('touchstart', this._handlers.touchStart);

this._input.addEventListener('mousedown', this._handlers.mouseDown);

this._input.addEventListener('wheel', this._handlers.mouseWheel);

this._input.addEventListener('dblclick', this._handlers.doubleClick);

this._input.addEventListener('focus', this._handlers.focus);

this._input.addEventListener('blur', this._handlers.blur);

// init

this.updateToInputValue();

}

static setupRotationContext(minRotation, maxRotation) {

return function() {

this.minRotation = minRotation;

this.maxRotation = maxRotation;

};

}

static rotationUpdateFunction(norm) {

this.element.style[transformProp] = `rotate(${this.maxRotation*norm-this.minRotation*(norm-1)}deg)`;

}

// handlers

handleInputChange(evt) {

// console.log('input change');

this.updateToInputValue();

}

handleTouchStart(evt) {

// console.log('touch start');

this.clearDrag();

evt.preventDefault();

var touch = evt.changedTouches.item(evt.changedTouches.length - 1);

this._activeDrag = touch.identifier;

this.startDrag(touch.clientY);

// drag update/end listeners

document.body.addEventListener('touchmove', this._handlers.touchMove);

document.body.addEventListener('touchend', this._handlers.touchEnd);

document.body.addEventListener('touchcancel', this._handlers.touchCancel);

}

handleTouchMove(evt) {

// console.log('touch move');

var activeTouch = this.findActiveTouch(evt.changedTouches);

if (activeTouch) {

this.updateDrag(activeTouch.clientY);

} else if (!this.findActiveTouch(evt.touches)) {

this.clearDrag();

}

}

handleTouchEnd(evt) {

// console.log('touch end');

var activeTouch = this.findActiveTouch(evt.changedTouches);

if (activeTouch) {

this.finalizeDrag(activeTouch.clientY);

}

}

handleTouchCancel(evt) {

// console.log('touch cancel');

if (this.findActiveTouch(evt.changedTouches)) {

this.clearDrag();

}

}

handleMouseDown(evt) {

// console.log('mouse down');

this.clearDrag();

evt.preventDefault();

this._activeDrag = true;

this.startDrag(evt.clientY);

// drag update/end listeners

document.body.addEventListener('mousemove', this._handlers.mouseMove);

document.body.addEventListener('mouseup', this._handlers.mouseUp);

}

handleMouseMove(evt) {

// console.log('mouse move');

if (evt.buttons & 1) {

this.updateDrag(evt.clientY);

} else {

this.finalizeDrag(evt.clientY);

}

}

handleMouseUp(evt) {

// console.log('mouse up');

this.finalizeDrag(evt.clientY);

}

handleMouseWheel(evt) {

// console.log('mouse wheel');

this._input.focus();

this.clearDrag();

this._prevValue = parseFloat(this._input.value);

this.updateFromDrag(evt.deltaY, this.wheelResistance);

}

handleDoubleClick(evt) {

// console.log('double click');

this.clearDrag();

this._input.value = this.initial;

this.updateToInputValue();

}

handleFocus(evt) {

// console.log('focus on');

this._container.classList.add('focus-active');

}

handleBlur(evt) {

// console.log('focus off');

this._container.classList.remove('focus-active');

}

// dragging

startDrag(yPosition) {

this._dragStartPosition = yPosition;

this._prevValue = parseFloat(this._input.value);

this._input.focus();

document.body.classList.add('knob-input__drag-active');

this._container.classList.add('drag-active');

}

updateDrag(yPosition) {

var diff = yPosition - this._dragStartPosition;

this.updateFromDrag(diff, this.dragResistance);

this._input.dispatchEvent(new InputEvent('change'));

}

finalizeDrag(yPosition) {

var diff = yPosition - this._dragStartPosition;

this.updateFromDrag(diff, this.dragResistance);

this.clearDrag();

this._input.dispatchEvent(new InputEvent('change'));

}

clearDrag() {

document.body.classList.remove('knob-input__drag-active');

this._container.classList.remove('drag-active');

this._activeDrag = false;

this._input.dispatchEvent(new InputEvent('change'));

// clean up event listeners

document.body.removeEventListener('mousemove', this._handlers.mouseMove);

document.body.removeEventListener('mouseup', this._handlers.mouseUp);

document.body.removeEventListener('touchmove', this._handlers.touchMove);

document.body.removeEventListener('touchend', this._handlers.touchEnd);

document.body.removeEventListener('touchcancel', this._handlers.touchCancel);

}

updateToInputValue() {

var normVal = this.normalizeValue(parseFloat(this._input.value));

this.updateVisuals(normVal);

}

updateFromDrag(dragAmount, resistance) {

var newVal = this.clampValue(this._prevValue - (dragAmount / resistance));

this._input.value = newVal;

this.updateVisuals(this.normalizeValue(newVal));

}

// utils

clampValue(val) {

var min = parseFloat(this._input.min);

var max = parseFloat(this._input.max);

return Math.min(Math.max(val, min), max);

}

normalizeValue(val) {

var min = parseFloat(this._input.min);

var max = parseFloat(this._input.max);

return (val - min) / (max - min);

}

findActiveTouch(touchList) {

var i, len, touch;

for (i = 0, len = touchList.length; i < len; i++)

if (this._activeDrag === touchList.item(i).identifier)

return touchList.item(i);

return null;

}

// public passthrough methods

addEventListener() {

this._input.addEventListener.apply(this._input, arguments);

}

removeEventListener() {

this._input.removeEventListener.apply(this._input, arguments);

}

focus() {

this._input.focus.apply(this._input, arguments);

}

blur() {

this._input.blur.apply(this._input, arguments);

}

// getters/setters

get value() {

return parseFloat(this._input.value);

}

set value(val) {

this._input.value = val;

this.updateToInputValue();

this._input.dispatchEvent(new Event('change'));

}

}

// Utils

function getSupportedPropertyName(properties) {

for (var i = 0; i < properties.length; i++)

if (typeof document.body.style[properties[i]] !== 'undefined')

return properties[i];

return null;

}

function getTransformProperty() {

return getSupportedPropertyName([

'transform', 'msTransform', 'webkitTransform', 'mozTransform', 'oTransform'

]);

}

function debounce(func, wait, immediate) {

var timeout;

return function() {

var context = this,

args = arguments;

var later = function() {

timeout = null;

if (!immediate) func.apply(context, args);

};

var callNow = immediate && !timeout;

clearTimeout(timeout);

timeout = setTimeout(later, wait);

if (callNow) func.apply(context, args);

};

};

// Demo Setup - Knobs

var transformProp = getTransformProperty();

var envelopeKnobStartPositions = [0, 40, 75, 85, 20, 55];

var envelopeKnobs = [...document.querySelectorAll('.fl-studio-envelope__knob.envelope-knob')];

var envelopeKnobs = envelopeKnobs.map((el, idx) => new KnobInput(el, {

visualContext: function() {

this.indicatorRing = this.element.querySelector('.indicator-ring');

var ringStyle = getComputedStyle(this.element.querySelector('.indicator-ring-bg'));

this.r = parseFloat(ringStyle.r) - (parseFloat(ringStyle.strokeWidth) / 2);

this.indicatorDot = this.element.querySelector('.indicator-dot');

this.indicatorDot.style[`${transformProp}Origin`] = '20px 20px';

},

updateVisuals: function(norm) {

var theta = Math.PI * 2 * norm + 0.5 * Math.PI;

var endX = this.r * Math.cos(theta) + 20;

var endY = this.r * Math.sin(theta) + 20;

// using 2 arcs rather than flags since one arc collapses if it gets near 360deg

this.indicatorRing.setAttribute('d', `M20,20l0,${this.r}${norm> 0.5?`

A$ {

this.r

}, $ {

this.r

}, 0, 0, 1, 20, $ {

20 - this.r

}

`:''}A-${this.r},${this.r},0,0,1,${endX},${endY}Z`);

this.indicatorDot.style[transformProp] = `rotate(${360*norm}deg)`;

},

min: 0,

max: 100,

initial: envelopeKnobStartPositions[idx],

}));

var tensionKnobStartPositions = [0, 0, -80];

var tensionKnobs = [...document.querySelectorAll('.fl-studio-envelope__knob.tension-knob')];

var tensionKnobs = tensionKnobs.map((el, idx) => new KnobInput(el, {

visualContext: function() {

this.indicatorRing = this.element.querySelector('.indicator-ring');

var ringStyle = getComputedStyle(this.element.querySelector('.indicator-ring-bg'));

this.r = parseFloat(ringStyle.r) - (parseFloat(ringStyle.strokeWidth) / 2);

},

updateVisuals: function(norm) {

var theta = Math.PI * 2 * norm + 0.5 * Math.PI;

var endX = this.r * Math.cos(theta) + 20;

var endY = this.r * Math.sin(theta) + 20;

this.indicatorRing.setAttribute('d', `M20,20l0,-${this.r}A${this.r},${this.r},0,0,${norm<0.5?0:1},${endX},${endY}Z`);

},

min: -100,

max: 100,

initial: tensionKnobStartPositions[idx],

}));

// Demo Setup - Envelope Visualization

var container = document.querySelector('.envelope-visualizer');

var enveloperVisualizer = {

container: container,

shape: container.querySelector('.envelope-shape'),

delay: container.querySelector('.delay'),

attack: container.querySelector('.attack'),

hold: container.querySelector('.hold'),

decay: container.querySelector('.decay'),

release: container.querySelector('.release'),

};

var updateVisualization = debounce(function(evt) {

var maxPtSeparation = 75;

var ptDelay = (maxPtSeparation * envelopeKnobs[0].value / 100);

var ptAttack = ptDelay + (maxPtSeparation * envelopeKnobs[1].value / 100);

var ptHold = ptAttack + (maxPtSeparation * envelopeKnobs[2].value / 100);

var ptDecay = ptHold + (maxPtSeparation * envelopeKnobs[3].value / 100) * (100 - envelopeKnobs[4].value) / 100;

var ptSustain = 100 - envelopeKnobs[4].value; // y value

var ptRelease = ptDecay + (maxPtSeparation * envelopeKnobs[5].value / 100);

// TODO: better tension visualization

var tnAttack = (ptAttack - ptDelay) * tensionKnobs[0].value / 100;

var tnDecay = (ptDecay - ptHold) * tensionKnobs[1].value / 100;

var tnRelease = (ptRelease - ptDecay) * tensionKnobs[2].value / 100;

enveloperVisualizer.shape.setAttribute('d',

`M${ptDelay},100` +

`C${tnAttack<0?ptDelay-tnAttack:ptDelay},100,${tnAttack>0?ptAttack-tnAttack:ptAttack},0,${ptAttack},0` +

`L${ptHold},0` +

`C${tnDecay>0?ptHold+tnDecay:ptHold},0,${tnDecay<0?ptDecay+tnDecay:ptDecay},${ptSustain},${ptDecay},${ptSustain}` +

`C${tnRelease>0?ptDecay+tnRelease:ptDecay},${ptSustain},${tnRelease<0?ptRelease+tnRelease:ptRelease},100,${ptRelease},100`

);

enveloperVisualizer.delay.setAttribute('cx', ptDelay);

enveloperVisualizer.attack.setAttribute('cx', ptAttack);

enveloperVisualizer.hold.setAttribute('cx', ptHold);

enveloperVisualizer.decay.setAttribute('cx', ptDecay);

enveloperVisualizer.decay.setAttribute('cy', ptSustain);

enveloperVisualizer.release.setAttribute('cx', ptRelease);

}, 10);

envelopeKnobs.concat(tensionKnobs)

.forEach(knob => {

knob.addEventListener('change', updateVisualization);

});

updateVisualization();

var panelElement = document.querySelector('.fl-studio-envelope');

var panel = {

element: panelElement,

originalTransform: getComputedStyle(panelElement)[transformProp],

width: panelElement.getBoundingClientRect().width,

height: panelElement.getBoundingClientRect().height,

};

var resizePanel = () => {

var pw = (window.innerWidth - 40) / panel.width;

var ph = (window.innerHeight - 40) / panel.height;

var size = Math.min(pw, ph);

if (size > 1.4) {

size -= 0.4;

} else if (size > 1) {

size = Math.min(size, 1);

}

panel.element.style[transformProp] = `${panel.originalTransform} scale(${size})`;

};

window.addEventListener('resize', resizePanel);

resizePanel();

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值