前回の針を直接回して時刻入力するタイムピッカーのつまんで回転させる動作を流用して作成した、数値を扱えるタイプのinput要素に対して回転動作で入力するためのUIです。
操作感覚は映像編集関連入力機器のジョグシャトルをイメージしています。
実物のジョグシャトル(ジョグダイヤル及びシャトルリング)は主に映像編集関連機器でよく使われる、編集ポイントに移動するための送り戻しを自在に行うための入力装置です。
###設定方法###
script
タグでjogshuttle.js
を読み込み、適用させたいinputタグにdata-jogshuttle
属性を追記します。
基本設定
<script src='jogshuttle.js'></script>
<input type='text' data-jogshuttle>
###設定項目###
####inputタグ自体の属性####
適用させるinputタグにmin
、max
、step
属性があれば、UI側でもその値を使用します。
####data-jogshuttle属性で設定できる項目####
data-jogshuttle
には、以下の項目が指定できます。
size UIのサイズ(px) デフォルトは200px
jog-color ジョグダイヤルの枠色
jog-background ジョグダイヤルの背景色
shuttle-color シャトルリングの枠色
shuttle-background シャトルリングの背景色
jog-ratio UI全体に対するジョグダイヤルのサイズ比(0~1) デフォルト 0.7
0にすると実質シャトルリングのみ、1にするとジョグダイヤルのみとして扱うことができます。
oninput 適用元要素にoninputイベントが存在する場合、UIの数値が変化するたびに適用元要素のoninput()
を発火させます。
onchange 適用元要素にonchangeイベントが存在する場合、UIの数値を変化させ離したタイミングで適用元要素のonchange()
を発火させます。
対象要素にonchange
やoninput
が設定されていてもその要素のvalueをJavaScriptで変更しただけでは発火しないため、元要素に対してonchange()
やoninput()
メソッドを実行することで発火させています。
target-id 別要素のidを指定することで、値を変更する対象の要素を変更することができます。
###スクリプト###
"use strict";
{
const
jsParam = {
styles: {},
ids: {},
attr: {},
displayFlg: 0,
},
d = {},
jsHandler = (function() {
const events = {};
let key = 1;
return {
add: function(target, type, listener, capture) {
target.addEventListener(type, listener, capture);
events[key] = {
target: target,
type: type,
listener: listener,
capture: capture
};
return key++;
},
remove: function(key) {
if(key in events) {
const e = events[key];
e.target.removeEventListener(e.type, e.listener, e.capture);
delete(events[key]);
}
}
};
}());
function jsSetStyle() {
const size = jsParam.attr['size'] === undefined ? 200 :
Math.abs(parseInt(jsParam.attr['size']));
// 外枠
let ds = d.jogshuttle.style;
ds.width = size + 'px';
ds.height = size + 'px';
ds.position = 'absolute';
ds.cursor = 'move';
ds.left = '0px';
ds.top = '0px';
ds.overflow = 'hidden';
// shuttle ring
ds = d.shuttle.style;
ds.position = 'absolute';
ds.width = size + 'px';
ds.height = size + 'px';
ds.left = '0px';
ds.top = '0px';
ds.border = '1px solid ' + (jsParam.attr['shuttle-color'] !== undefined ?
jsParam.attr['shuttle-color'] : '#888');
ds.background = (jsParam.attr['shuttle-background'] !== undefined ?
jsParam.attr['shuttle-background'] : '#ddd');
ds.cursor = 'pointer';
ds.borderRadius = '50%';
ds.transform = 'rotate(180deg)';
ds.boxSizing = 'border-box';
ds.userSelect = 'none';
ds.WebkitUserSelect = 'none';
ds = d.shuttle.querySelector('.jsRefPosition').style;
ds.position = 'absolute';
ds.width = (size * 0.01) + 'px';
ds.height = (size * 0.25) + 'px';
ds.left = (size / 2 - size * 0.005 / 2) + 'px';
ds.bottom = '0px';
ds.background = (jsParam.attr['shuttle-color'] !== undefined ?
jsParam.attr['shuttle-color'] : '#888');
ds.boxSizing = 'border-box';
ds.userSelect = 'none';
ds.WebkitUserSelect = 'none';
// jog dial
let n = Math.abs(parseFloat(jsParam.attr['jog-ratio']));
const jogRatio = jsParam.attr['jog-ratio'] === undefined ? 0.7 :
(n > 1 ? 1 : n < 0 ? 0 : n);
ds = d.jog.style;
ds.position = 'absolute';
ds.display = (jogRatio === 0) ? 'none' : 'block';
ds.width = (size * jogRatio) + 'px';
ds.height = (size * jogRatio) + 'px';
ds.left = (size / 2 - size * jogRatio / 2) + 'px';
ds.top = (size / 2 - size * jogRatio / 2) + 'px';
ds.border = '1px solid ' + (jsParam.attr['jog-color'] !== undefined ?
jsParam.attr['jog-color'] : '#888');
ds.background = (jsParam.attr['jog-background'] !== undefined ?
jsParam.attr['jog-background'] : '#ddd');
ds.cursor = 'pointer';
ds.borderRadius = '50%';
ds.transform = 'rotate(0deg)';
ds.boxSizing = 'border-box';
ds.userSelect = 'none';
ds.WebkitUserSelect = 'none';
ds = d.jog.querySelector('.jsRefPosition').style;
ds.position = 'absolute';
ds.width = '20%';
ds.height = '20%';
ds.left = '40%';
ds.top = '10%';
ds.border = '1px solid ' + (jsParam.attr['jog-color'] !== undefined ?
jsParam.attr['jog-color'] : '#888');
ds.borderRadius = '50%';
ds.boxSizing = 'border-box';
ds.userSelect = 'none';
ds.WebkitUserSelect = 'none';
// フォーカス制御用ダミー
ds = d.dummy.style;
ds.position = 'absolute';
ds.width = '1px';
ds.left = '-999px';
if(jsParam.touchFlg) d.dummy.type = 'checkbox';
}
window.addEventListener('DOMContentLoaded', function() {
document.querySelector('body').insertAdjacentHTML('beforeend',
"<div id='jogshuttle' hidden>" +
" <input type='text' id='jsDummy'>" +
" <div id='jsShuttle'>" +
" <div class='jsRefPosition'></div>" +
" </div>" +
" <div id='jsJog'>" +
" <div class='jsRefPosition'></div>" +
" </div>" +
"</div>"
);
d.jogshuttle = document.querySelector('#jogshuttle');
d.dummy = document.querySelector('#jogshuttle #jsDummy');
d.shuttle = document.querySelector('#jogshuttle #jsShuttle');
d.jog = document.querySelector('#jogshuttle #jsJog');
d.jogshuttle.style.display = 'none';
jsParam.touchFlg = window.ontouchstart !== undefined ? true : false;
jsSetStyle();
let r, r_, x, y, sx, sy,
shuttleFlg = false,
jogFlg = false,
moveFlg = false,
start;
window.addEventListener(jsParam.touchFlg ? 'touchstart' : 'mousedown', function() {
jsParam.clickStart = true;
});
// 枠クリック
d.jogshuttle.addEventListener(jsParam.touchFlg ? 'touchstart' : 'mousedown', function(e) {
if(!moveFlg) {
if(jsParam.touchFlg) jsAddHandler();
moveFlg = true;
sx = (jsParam.touchFlg ? e.touches[0].clientX : e.clientX) - parseInt(this.style.left);
sy = (jsParam.touchFlg ? e.touches[0].clientY : e.clientY) - parseInt(this.style.top);
}
});
// shuttleクリック
d.shuttle.addEventListener(jsParam.touchFlg ? 'touchstart' : 'mousedown', function(e) {
if(!shuttleFlg) {
if(jsParam.touchFlg) jsAddHandler();
shuttleFlg = true;
jsParam.shuttleValue = 0;
jsStartSendShuttle();
const st = d.shuttle.style.transform;
r_ = r = (parseInt(st.replace(/^[^\d-]+/g, '')) + 360 ) % 360;
const js = d.jogshuttle;
const ex = jsParam.touchFlg ? e.touches[0].clientX : e.clientX;
const ey = jsParam.touchFlg ? e.touches[0].clientY : e.clientY;
x = ex - (js.offsetLeft - window.pageXOffset) - (js.clientWidth / 2);
y = ey - (js.offsetTop - window.pageYOffset) - (js.clientHeight / 2);
const at = r_ = Math.atan2(y, x);
start = ((at * (360 / (Math.PI * 2))) + 270) % 360;
setTimeout(function() { d.dummy.focus() }, 10);
}
});
// jogクリック
d.jog.addEventListener(jsParam.touchFlg ? 'touchstart' : 'mousedown', function(e) {
if(!jogFlg) {
if(jsParam.touchFlg) jsAddHandler();
jogFlg = true;
const st = d.jog.style.transform;
r_ = r = (parseInt(st.replace(/^[^\d-]+/g, '')) + 360 ) % 360;
const js = d.jogshuttle;
const ex = jsParam.touchFlg ? e.touches[0].clientX : e.clientX;
const ey = jsParam.touchFlg ? e.touches[0].clientY : e.clientY;
x = ex - (js.offsetLeft - window.pageXOffset) - (js.clientWidth / 2);
y = ey - (js.offsetTop - window.pageYOffset) - (js.clientHeight / 2);
const at = Math.atan2(y, x);
start = ((at * (360 / (Math.PI * 2))) + 450 - r) % 360;
jsParam.ids['inertia'] = 0;
jsParam.attr.diff = 0;
setTimeout(function() { d.dummy.focus() }, 10);
}
});
// ドラッグ
window.addEventListener(jsParam.touchFlg ? 'touchmove' : 'mousemove', function(e) {
const js = d.jogshuttle;
jsParam.clickStart = false;
if(shuttleFlg || jogFlg || moveFlg) {
const ex = jsParam.touchFlg ? e.touches[0].clientX : e.clientX;
const ey = jsParam.touchFlg ? e.touches[0].clientY : e.clientY;
x = ex - (js.offsetLeft - window.pageXOffset) - (js.clientWidth / 2);
y = ey - (js.offsetTop - window.pageYOffset) - (js.clientHeight / 2);
const at = r = Math.atan2(y, x);
let f = (r - r_) > 0 ? 1 : -1;
if(r - r_ > Math.PI) r_ += Math.PI * 2;
else if(r - r_ < -Math.PI) r_ -= Math.PI * 2;
if(shuttleFlg) {
const diff = Math.abs(r - r_) * f;
const dd = diff * (360 / (Math.PI * 2));
let n = (parseFloat(d.shuttle.style.transform.replace(/^[^\d-]+/g, '')) + dd);
n = (n > 330) ? 330 : (n < 30) ? 30 : n;
d.shuttle.style.transform = 'rotate(' + n + 'deg)';
jsParam.shuttleValue = (n - 180) / 150;
}
else if(jogFlg) {
const k = ((at * (360 / (Math.PI * 2))) + 810 - start) % 360;
d.jog.style.transform = 'rotate(' + k + 'deg)';
jsParam.attr.diff = k - jsParam.k_;
if(jsParam.attr.diff > 180) jsParam.attr.diff -= 360;
else if(jsParam.attr.diff < -180) jsParam.attr.diff += 360;
jsParam.valueOrigin = Number(jsParam.valueOrigin) +
Number(jsParam.attr.diff / (360 / 20)) * jsParam.attr.step;
jsRangeCheck();
jsParam.k_ = k;
}
else {
d.jogshuttle.style.left = (ex - sx) + 'px';
d.jogshuttle.style.top = (ey - sy) + 'px';
}
r_ = r;
}
});
// リリース
window.addEventListener(jsParam.touchFlg ? 'touchend' : 'mouseup', function() {
if(shuttleFlg) {
shuttleFlg = false;
jsParam.shuttleValue = 0;
d.shuttle.style.transform = 'rotate(180deg)';
jsStopSendShuttle();
}
if(jogFlg) {
jogFlg = false;
if(Math.abs(jsParam.attr.diff) > 10) {
jsParam.attr.diff *= 10;
jsParam.ids['inertia'] = 1;
jsJogInertia();
}
}
if(moveFlg) {
moveFlg = false;
}
if(d.jogshuttle.style.display !== 'none') {
d.dummy.focus();
if(jsParam.attr.onchange !== undefined && jsParam.o.onchange && jsParam.attr.onchange)
jsParam.o.onchange();
}
if(jsParam.touchFlg) jsRemoveHandler();
if(d.jogshuttle.style.display !== 'none') {
setTimeout(function() {
if(jsParam.clickStart === true) {
d.jogshuttle.style.display = 'none';
jsRemoveHandler();
jsParam.attr.diff = 0;
}
}, 50);
}
});
// マウスホイール
window.addEventListener('wheel', function(e) {
if(!jsParam.ids['wheel']) return;
const delta = e.wheelDelta !== undefined ? e.deltaY / 40 : e.deltaY / 20;
jsParam.valueOrigin = jsParam.valueOrigin + (delta / 4) * jsParam.attr.step;
jsRangeCheck();
const n = parseFloat(d.jog.style.transform.replace(/^[^\d-]+/g, '')) + delta * 4;
d.jog.style.transform = 'rotate(' + (360 + n) % 360 + 'deg)';
});
// dataset(data-jogshuttle)指定のあるinput要素にclickイベントを追加
const inputElements = document.querySelectorAll('input[data-jogshuttle]');
for(let i in inputElements) {
if(inputElements[i].dataset === undefined) continue;
const dataset = inputElements[i].dataset.jogshuttle;
if(dataset === undefined) continue;
inputElements[i].addEventListener('click', function() {
jsJogshuttle(this);
});
}
});
function jsAddHandler() {
if(!jsParam.ids['touchmove']) {
jsParam.ids['touchmove'] = jsHandler.add(window, 'touchmove', function(e) {
e.preventDefault();
}, { passive: false });
}
if(!jsParam.ids['wheel']) {
jsParam.ids['wheel'] = jsHandler.add(window, 'wheel', function(e) {
e.preventDefault();
}, { passive: false });
}
}
function jsRemoveHandler() {
if(jsParam.ids['touchmove'] > 0) {
jsHandler.remove(jsParam.ids['touchmove']);
jsParam.ids['touchmove'] = 0;
}
if(jsParam.ids['wheel'] > 0) {
jsHandler.remove(jsParam.ids['wheel']);
jsParam.ids['wheel'] = 0;
}
}
function jsStartSendShuttle() {
if(jsParam.ids['send'] === undefined || jsParam.ids['send'] === 0) {
jsParam.ids['send'] = 1;
jsSendShuttleValue();
}
}
function jsStopSendShuttle() {
if(jsParam.ids['send'] !== undefined && jsParam.ids['send'] !== 0)
jsParam.ids['send'] = 0;
}
function jsSendShuttleValue() {
jsParam.valueOrigin = Number(jsParam.valueOrigin) +
Math.pow(jsParam.shuttleValue * 9, 3) * jsParam.attr.step;
jsRangeCheck();
if(jsParam.ids['send']) requestAnimationFrame(jsSendShuttleValue);
}
function jsRangeCheck() {
if(jsParam.attr.min !== undefined) {
if(jsParam.valueOrigin < jsParam.attr.min)
jsParam.valueOrigin = jsParam.attr.min;
}
const rs = 1 / jsParam.attr.step;
let value = Math.floor(jsParam.valueOrigin * rs) / rs;
if(jsParam.attr.min !== undefined) value +=
(jsParam.attr.min % jsParam.attr.step);
if(jsParam.attr.max !== undefined) {
if(value > jsParam.attr.max) value = jsParam.attr.max;
if(jsParam.valueOrigin > jsParam.attr.max + jsParam.attr.step)
jsParam.valueOrigin = jsParam.attr.max + jsParam.attr.step;
}
if(jsParam.attr.min !== undefined && value < jsParam.attr.min)
value = jsParam.attr.min;
if(jsParam.attr.stepDecDigit)
value = parseFloat(Number(value).toFixed(jsParam.attr.stepDecDigit))
jsParam.target.value = value;
if(jsParam.attr.oninput !== undefined && jsParam.o.oninput && jsParam.attr.oninput)
jsParam.o.oninput();
}
function jsJogInertia() {
if(!jsParam.ids['inertia']) return;
requestAnimationFrame(jsJogInertia);
jsParam.attr.diff /= 1.1
if(Math.abs(jsParam.attr.diff) < 0.1) {
jsParam.ids['inertia'] = 0;
return;
}
jsParam.valueOrigin = Number(jsParam.valueOrigin) +
Number(jsParam.attr.diff / (360 / 20)) * jsParam.attr.step;
jsRangeCheck();
const n = parseFloat(d.jog.style.transform.replace(/^[^\d-]+/g, '')) + jsParam.attr.diff;
d.jog.style.transform = 'rotate(' + (360 + n) % 360 + 'deg)';
}
function jsJogshuttle(e) {
jsParam.clickStart = false;
const js = d.jogshuttle;
if(js.style.display !== 'none' && jsParam.o === e) {
jsParam.displayFlg ^= 1;
if(!jsParam.displayFlg) {
e.focus();
js.style.display = 'none';
jsRemoveHandler();
jsParam.attr.diff = 0;
return;
}
}
jsParam.displayFlg = 1;
jsParam.attr = {};
const attr = e.attributes;
for(let i = 0; i < attr.length; i++) {
if(['min', 'max', 'step'].indexOf(attr[i].name) >= 0) {
jsParam.attr[attr[i].name] = parseFloat(attr[i].value);
}
}
const dataset = e.dataset.jogshuttle;
if(dataset !== undefined) {
const params = dataset.split(';');
for(let l = 0; l < params.length; l++) {
const param = params[l].split(':');
if(param.length > 2) param[1] = param.slice(1).join(':');
const key = param[0].trim().toLowerCase();
let value = param[1] !== undefined ? param[1].trim() : '';
if(['size',
'jog-ratio',
'jog-color',
'jog-background',
'shuttle-color',
'shuttle-background',
'oninput',
'onchange',
'target-id'].indexOf(key) >= 0) {
if(value.toLowerCase() === 'true') value = true;
else if(value.toLowerCase() === 'false') value = false;
else if(!isNaN(value)) value = Number(value);
jsParam.attr[key] = value;
}
}
}
jsParam.target =
(jsParam.attr['target-id'] !== undefined && document.getElementById(jsParam.attr['target-id']) !== null) ?
document.getElementById(jsParam.attr['target-id']) : e;
jsSetStyle();
if(jsParam.attr.step === undefined || !jsParam.attr.step) jsParam.attr.step = 1;
jsParam.attr.stepDecDigit = String(parseFloat(jsParam.attr.step)).replace(/^[^.]+\.?/, '').length;
if(jsParam.attr.step !== 1 && jsParam.attr.max) {
const m = jsParam.attr.min !== undefined ? jsParam.attr.min : 0;
jsParam.attr.max =
Math.floor((jsParam.attr.max - m) / jsParam.attr.step) * jsParam.attr.step + m;
}
js.style.left = (e.offsetLeft) + 'px';
js.style.top = (4 + e.offsetTop + e.clientHeight) + 'px';
jsParam.o = e;
jsParam.valueOrigin = isNaN(jsParam.target.value) ? 0 : Number(jsParam.target.value);
jsParam.ids['inertia'] = 0;
jsParam.k_ = 0;
js.style.display = 'block';
d.dummy.focus();
jsRemoveHandler();
if(!jsParam.touchFlg) jsAddHandler();
}
}