JoyRemocon

Nintendo SwitchのJoy-Conを動画プレイヤーのリモコンにする.

  1. // ==UserScript==
  2. // @name JoyRemocon
  3. // @namespace https://github.com/segabito/
  4. // @description Nintendo SwitchのJoy-Conを動画プレイヤーのリモコンにする.
  5. // @include *://*.nicovideo.jp/watch/*
  6. // @include *://www.youtube.com/*
  7. // @include *://www.bilibili.com/video/*
  8. // @include *://www.amazon.co.jp/gp/video/*
  9. // @version 1.6.0
  10. // @author segabito macmoto
  11. // @license public domain
  12. // @grant none
  13. // @noframes
  14. // ==/UserScript==
  15.  
  16.  
  17.  
  18.  
  19. (() => {
  20.  
  21. const monkey = () => {
  22. if (!window.navigator.getGamepads) {
  23. window.console.log('%cGamepad APIがサポートされていません', 'background: red; color: yellow;');
  24. return;
  25. }
  26.  
  27. const PRODUCT = 'JoyRemocon';
  28. let isPauseButtonDown = false;
  29. let isRate1ButtonDown = false;
  30. let isMetaButtonDown = false;
  31.  
  32. const getVideo = () => {
  33. switch (location.host) {
  34. case 'www.nicovideo.jp':
  35. return document.querySelector('.MainVideoPlayer video');
  36. case 'www.amazon.co.jp':
  37. return document.querySelector('video[width="100%"]');
  38. default:
  39. return Array.from(document.querySelectorAll('video')).find(v => {
  40. return !!v.src;
  41. });
  42. }
  43. };
  44.  
  45. const video = {
  46. get currentTime() {
  47. try {
  48. return window.__videoPlayer ?
  49. __videoplayer.currentTime() : getVideo().currentTime;
  50. } catch (e) {
  51. console.warn(e);
  52. return 0;
  53. }
  54. },
  55. set currentTime(v) {
  56. try {
  57. if (v <= video.currentTime && location.host === 'www.nicovideo.jp') {
  58. return seekNico(v);
  59. } else if (location.host === 'www.amazon.co.jp') {
  60. return seekPrimeVideo(v);
  61. }
  62. getVideo().currentTime = v;
  63. } catch (e) {
  64. console.warn(e);
  65. }
  66. },
  67.  
  68. get muted() {
  69. try {
  70. return getVideo().muted;
  71. } catch (e) {
  72. console.warn(e);
  73. return false;
  74. }
  75. },
  76.  
  77. set muted(v) {
  78. try {
  79. getVideo().muted = v;
  80. } catch (e) {
  81. console.warn(e);
  82. }
  83. },
  84.  
  85. get playbackRate() {
  86. try {
  87. return window.__videoPlayer ?
  88. __videoplayer.playbackRate() : getVideo().playbackRate;
  89. } catch (e) {
  90. console.warn(e);
  91. return 1;
  92. }
  93. },
  94. set playbackRate(v) {
  95. try {
  96. if (window.__videoPlayer) {
  97. window.__videoPlayer.playbackRate(v);
  98. return;
  99. }
  100. getVideo().playbackRate = Math.max(0.01, v);
  101. } catch (e) {
  102. console.warn(e);
  103. }
  104. },
  105.  
  106. get volume() {
  107. try {
  108. if (location.host === 'www.nicovideo.jp') {
  109. return getVolumeNico();
  110. }
  111. return getVideo().volume;
  112. } catch (e) {
  113. console.warn(e);
  114. return 1;
  115. }
  116. },
  117.  
  118. set volume(v) {
  119. try {
  120. v = Math.max(0, Math.min(1, v));
  121. if (location.host === 'www.nicovideo.jp') {
  122. return setVolumeNico(v);
  123. }
  124. getVideo().volume = v;
  125. } catch (e) {
  126. console.warn(e);
  127. }
  128. },
  129.  
  130. get duration() {
  131. try {
  132. return getVideo().duration;
  133. } catch (e) {
  134. console.warn(e);
  135. return 1;
  136. }
  137. },
  138.  
  139. play() {
  140. try {
  141. return getVideo().play();
  142. } catch (e) {
  143. console.warn(e);
  144. return Promise.reject();
  145. }
  146. },
  147.  
  148. pause() {
  149. try {
  150. return getVideo().pause();
  151. } catch (e) {
  152. console.warn(e);
  153. return Promise.reject();
  154. }
  155. },
  156.  
  157. get paused() {
  158. try {
  159. return getVideo().paused;
  160. } catch (e) {
  161. console.warn(e);
  162. return true;
  163. }
  164. },
  165.  
  166. };
  167.  
  168. const seekNico = time => {
  169. const xs = document.querySelector('.SeekBar .XSlider');
  170. let [min, sec] = document.querySelector`.PlayerPlayTime-duration`.textContent.split(':');
  171. let duration = min * 60 + sec * 1;
  172. let left = xs.getBoundingClientRect().left;
  173. let offsetWidth = xs.offsetWidth;
  174. let per = time / duration * 100;
  175. let clientX = offsetWidth * per / 100 + left;
  176. xs.dispatchEvent(new MouseEvent('mousedown', {clientX}));
  177. document.dispatchEvent(new MouseEvent('mouseup', {clientX}));
  178. };
  179.  
  180. const setVolumeNico = vol => {
  181. const xs = document.querySelector('.VolumeBar .XSlider');
  182. let left = xs.getBoundingClientRect().left;
  183. let offsetWidth = xs.offsetWidth;
  184. let per = vol * 100;
  185. let clientX = offsetWidth * per / 100 + left;
  186. xs.dispatchEvent(new MouseEvent('mousedown', {clientX}));
  187. document.dispatchEvent(new MouseEvent('mouseup', {clientX}));
  188. };
  189.  
  190. const seekPrimeVideo = time => {
  191. const xs = document.querySelector('.seekBar .progressBarContainer');
  192. xs.closest('.bottomPanelItem').style.display = '';
  193. let left = xs.getBoundingClientRect().left;
  194. let offsetWidth = xs.offsetWidth;
  195. let per = (time - 10) / video.duration * 100; // 何故か10秒分ズレてる?
  196. let clientX = offsetWidth * per / 100 + left;
  197. // console.log('seek', video.currentTime, time, left, offsetWidth, per, clientX);
  198. xs.dispatchEvent(new PointerEvent('pointerdown', {clientX}));
  199. xs.dispatchEvent(new PointerEvent('pointerup', {clientX}));
  200. };
  201.  
  202. const getVolumeNico = () => {
  203. try {
  204. const xp = document.querySelector('.VolumeBar .XSlider .ProgressBar-inner');
  205. return (xp.style.transform || '1').replace(/scaleX\(([0-9\.]+)\)/, '$1') * 1;
  206. } catch (e) {
  207. console.warn(e);
  208. return 1;
  209. }
  210. };
  211.  
  212. const execCommand = (command, param) => {
  213. switch (command) {
  214. case 'playbackRate':
  215. video.playbackRate = param;
  216. break;
  217. case 'toggle-play': {
  218. const btn = document.querySelector(
  219. '.ytp-ad-skip-button, .PlayerPlayButton, .PlayerPauseButton, .html5-main-videom, .bilibili-player-video-btn-start, .pausedOverlay');
  220. if (btn) {
  221. if (location.host === 'www.amazon.co.jp') {
  222. btn.dispatchEvent(new CustomEvent('pointerup'));
  223. } else {
  224. btn.click();
  225. }
  226. } else
  227. if (video.paused) {
  228. video.play();
  229. } else {
  230. video.pause();
  231. }
  232. break;
  233. }
  234. case 'toggle-mute': {
  235. const btn = document.querySelector(
  236. '.MuteVideoButton, .UnMuteVideoButton, .ytp-mute-button, .bilibili-player-iconfont-volume-max');
  237. if (btn) {
  238. btn.click();
  239. } else {
  240. video.muted = !video.muted;
  241. }
  242. break;
  243. }
  244. case 'seek':
  245. video.currentTime = param * 1;
  246. break;
  247. case 'seekBy':
  248. video.currentTime += param * 1;
  249. break;
  250. case 'seekNextFrame':
  251. video.currentTime += 1 / 60;
  252. break;
  253. case 'seekPrevFrame':
  254. video.currentTime -= 1 / 60;
  255. break;
  256. case 'volumeUp': {
  257. let v = video.volume;
  258. let r = v < 0.05 ? 1.3 : 1.1;
  259. video.volume = Math.max(0.05, v * r + 0.01);
  260. break;
  261. }
  262. case 'volumeDown': {
  263. let v = video.volume;
  264. let r = 1 / 1.2;
  265. video.volume = Math.max(0.01, v * r);
  266. break;
  267. }
  268. case 'toggle-showComment': {
  269. const btn = document.querySelector('.CommentOnOffButton, .bilibili-player-video-danmaku-switch input');
  270. if (btn) {
  271. btn.click();
  272. }
  273. break;
  274. }
  275. case 'toggle-fullscreen': {
  276. const btn = document.querySelector(
  277. '.EnableFullScreenButton, .DisableFullScreenButton, .ytp-fullscreen-button, .bilibili-player-video-btn-fullscreen, .imageButton.fullscreenButton');
  278. if (btn) {
  279. btn.click();
  280. }
  281. break;
  282. }
  283. case 'playNextVideo': {
  284. const btn = document.querySelector(
  285. '.PlayerSkipNextButton, .ytp-next-button, .nextTitleButton, .skipAdButton');
  286. if (btn) {
  287. btn.click();
  288. }
  289. break;
  290. }
  291. case 'playPreviousVideo': {
  292. const btn = document.querySelector(
  293. '.PlayerSeekBackwardButton');
  294. if (btn) {
  295. btn.click();
  296. }
  297. if (['www.youtube.com'].includes(location.host)) {
  298. history.back();
  299. }
  300. break;
  301. }
  302. case 'screenShot': {
  303. screenShot();
  304. break;
  305. }
  306. case 'deflistAdd': {
  307. const btn = document.querySelector(
  308. '.InstantMylistButton');
  309. if (btn) {
  310. btn.click();
  311. }
  312. break;
  313. }
  314. case 'notify':
  315. notify(param);
  316. break;
  317. case 'unlink':
  318. if (document.hasFocus()) {
  319. JoyRemocon.unlink();
  320. }
  321. break;
  322. default:
  323. console.warn('unknown command "%s" "%o"', command, param);
  324. break;
  325. }
  326. };
  327.  
  328. const notify = message => {
  329. const div = document.createElement('div');
  330. div.textContent = message;
  331. Object.assign(div.style, {
  332. position: 'fixed',
  333. display: 'inline-block',
  334. zIndex: 1000000,
  335. left: 0,
  336. bottom: 0,
  337. transition: 'opacity 0.4s linear, transform 0.5s ease',
  338. padding: '8px 16px',
  339. background: '#00c',
  340. color: 'rgba(255, 255, 255, 0.8)',
  341. fontSize: '16px',
  342. fontWeight: 'bolder',
  343. whiteSpace: 'nowrap',
  344. textAlign: 'center',
  345. boxShadow: '2px 2px 0 #ccc',
  346. userSelect: 'none',
  347. pointerEvents: 'none',
  348. willChange: 'transform',
  349. opacity: 0,
  350. transform: 'translate(0, +100%) translate(48px, +48px) ',
  351. });
  352.  
  353.  
  354. const parent = document.querySelector('.MainContainer') || document.body;
  355. parent.append(div);
  356.  
  357. setTimeout(() => {
  358. Object.assign(div.style, { opacity: 1, transform: 'translate(48px, -48px)' });
  359. }, 100);
  360. setTimeout(() => {
  361. Object.assign(div.style, { opacity: 0, transform: 'translate(48px, -48px) scaleY(0)' });
  362. }, 2000);
  363. setTimeout(() => {
  364. div.remove();
  365. }, 3000);
  366. };
  367.  
  368. const getVideoTitle = () => {
  369. switch (location.host) {
  370. case 'www.nicovideo.jp':
  371. return document.title;
  372. case 'www.youtube.com':
  373. return document.title;
  374. default:
  375. return document.title;
  376. }
  377. };
  378.  
  379. const toSafeName = function(text) {
  380. text = text.trim()
  381. .replace(/</g, '<')
  382. .replace(/>/g, '>')
  383. .replace(/\?/g, '?')
  384. .replace(/:/g, ':')
  385. .replace(/\|/g, '|')
  386. .replace(/\//g, '/')
  387. .replace(/\\/g, '¥')
  388. .replace(/"/g, '”')
  389. .replace(/\./g, '.')
  390. ;
  391. return text;
  392. };
  393.  
  394. const speedUp = () => {
  395. let current = video.playbackRate;
  396. execCommand('playbackRate', Math.floor(Math.min(current + 0.1, 3) * 10) / 10);
  397. };
  398.  
  399. const speedDown = () => {
  400. let current = video.playbackRate;
  401. execCommand('playbackRate', Math.floor(Math.max(current - 0.1, 0.1) * 10) / 10);
  402. };
  403.  
  404. const scrollUp = () => {
  405. document.documentElement.scrollTop =
  406. Math.max(0, document.documentElement.scrollTop - window.innerHeight / 5);
  407. };
  408.  
  409. const scrollDown = () => {
  410. document.documentElement.scrollTop =
  411. document.documentElement.scrollTop + window.innerHeight / 5;
  412. };
  413.  
  414. const scrollToVideo = () => {
  415. getVideo().scrollIntoView({behavior: 'smooth', block: 'center'});
  416. };
  417.  
  418. const screenShot = video => {
  419. video = video || getVideo();
  420. if (!video) {
  421. return;
  422. }
  423. // draw canvas
  424. const width = video.videoWidth;
  425. const height = video.videoHeight;
  426. const canvas = document.createElement('canvas');
  427. canvas.width = width;
  428. canvas.height = height;
  429. const context = canvas.getContext('2d');
  430. context.drawImage(video, 0, 0);
  431. document.body.append(canvas);
  432. // fileName
  433. const videoTitle = getVideoTitle();
  434. const currentTime = video.currentTime;
  435. const min = Math.floor(currentTime / 60);
  436. const sec = (currentTime % 60 + 100).toString().substr(1, 6);
  437. const time = `${min}_${sec}`;
  438. const fileName = `${toSafeName(videoTitle)}@${time}.png`;
  439.  
  440. // to objectURL
  441. console.time('canvas to DataURL');
  442. const dataURL = canvas.toDataURL('image/png');
  443. console.timeEnd('canvas to DataURL');
  444.  
  445. console.time('dataURL to objectURL');
  446. const bin = atob(dataURL.split(',')[1]);
  447. const buf = new Uint8Array(bin.length);
  448. for (let i = 0, len = buf.length; i < len; i++) {
  449. buf[i] = bin.charCodeAt(i);
  450. }
  451. const blob = new Blob([buf.buffer], {type: 'image/png'});
  452. const objectURL = URL.createObjectURL(blob);
  453. console.timeEnd('dataURL to objectURL');
  454.  
  455. // save
  456. const link = document.createElement('a');
  457. link.setAttribute('download', fileName);
  458. link.setAttribute('href', objectURL);
  459. document.body.append(link);
  460. link.click();
  461. setTimeout(() => { link.remove(); URL.revokeObjectURL(objectURL); }, 1000);
  462. };
  463.  
  464. const ButtonMapJoyConL = {
  465. Y: 0,
  466. B: 1,
  467. X: 2,
  468. A: 3,
  469. SUP: 4,
  470. SDN: 5,
  471. SEL: 8,
  472. CAP: 13,
  473. LR: 14,
  474. META: 15,
  475. PUSH: 10
  476. };
  477. const ButtonMapJoyConR = {
  478. Y: 3,
  479. B: 2,
  480. X: 1,
  481. A: 0,
  482. SUP: 5,
  483. SDN: 4,
  484. SEL: 9,
  485. CAP: 12,
  486. LR: 14,
  487. META: 15,
  488. PUSH: 11
  489. };
  490.  
  491. const JoyConAxisCenter = +1.28571;
  492.  
  493. const AxisMapJoyConL = {
  494. CENTER: JoyConAxisCenter,
  495. UP: +0.71429,
  496. U_R: +1.00000,
  497. RIGHT: -1.00000,
  498. D_R: -0.71429,
  499. DOWN: -0.42857,
  500. D_L: -0.14286,
  501. LEFT: +0.14286,
  502. U_L: +0.42857,
  503. };
  504.  
  505. const AxisMapJoyConR = {
  506. CENTER: JoyConAxisCenter,
  507. UP: -0.42857,
  508. U_R: -0.14286,
  509. RIGHT: +0.14286,
  510. D_R: +0.42857,
  511. DOWN: +0.71429,
  512. D_L: +1.00000,
  513. LEFT: -1.00000,
  514. U_L: -0.71429,
  515. };
  516.  
  517.  
  518. const onButtonDown = (button, deviceId) => {
  519. const ButtonMap = deviceId.match(/Vendor: 057e Product: 2006/i) ?
  520. ButtonMapJoyConL : ButtonMapJoyConR;
  521. switch (button) {
  522. case ButtonMap.Y:
  523. if (isPauseButtonDown) {
  524. execCommand('seekPrevFrame');
  525. } else {
  526. execCommand('toggle-showComment');
  527. }
  528. break;
  529. case ButtonMap.B:
  530. isPauseButtonDown = true;
  531. execCommand('toggle-play');
  532. break;
  533. case ButtonMap.X:
  534. if (isMetaButtonDown) {
  535. execCommand('playbackRate', 2);
  536. } else {
  537. isRate1ButtonDown = true;
  538. execCommand('playbackRate', 0.1);
  539. }
  540. break;
  541. case ButtonMap.A:
  542. if (isPauseButtonDown) {
  543. execCommand('seekNextFrame');
  544. } else {
  545. execCommand('toggle-mute');
  546. }
  547. break;
  548. case ButtonMap.SUP:
  549. if (isMetaButtonDown) {
  550. scrollUp();
  551. } else {
  552. execCommand('playPreviousVideo');
  553. }
  554. break;
  555. case ButtonMap.SDN:
  556. if (isMetaButtonDown) {
  557. scrollDown();
  558. } else {
  559. execCommand('playNextVideo');
  560. }
  561. break;
  562. case ButtonMap.SEL:
  563. if (isMetaButtonDown) {
  564. execCommand('unlink');
  565. } else {
  566. execCommand('deflistAdd');
  567. }
  568. break;
  569. case ButtonMap.CAP:
  570. if (location.host === 'www.amazon.co.jp') {
  571. return;
  572. }
  573. execCommand('screenShot');
  574. break;
  575. case ButtonMap.PUSH:
  576. if (isMetaButtonDown) {
  577. scrollToVideo();
  578. } else {
  579. execCommand('seek', 0);
  580. }
  581. break;
  582. case ButtonMap.LR:
  583. execCommand('toggle-fullscreen');
  584. break;
  585. case ButtonMap.META:
  586. isMetaButtonDown = true;
  587. break;
  588. }
  589. };
  590.  
  591.  
  592. const onButtonUp = (button, deviceId) => {
  593. const ButtonMap = deviceId.match(/Vendor: 057e Product: 2006/i) ?
  594. ButtonMapJoyConL : ButtonMapJoyConR;
  595. switch (button) {
  596. case ButtonMap.Y:
  597. break;
  598. case ButtonMap.B:
  599. isPauseButtonDown = false;
  600. break;
  601. case ButtonMap.X:
  602. isRate1ButtonDown = false;
  603. execCommand('playbackRate', 1);
  604. break;
  605. case ButtonMap.META:
  606. isMetaButtonDown = false;
  607. break;
  608. }
  609. };
  610.  
  611.  
  612. const onButtonRepeat = (button, deviceId) => {
  613. const ButtonMap = deviceId.match(/Vendor: 057e Product: 2006/i) ?
  614. ButtonMapJoyConL : ButtonMapJoyConR;
  615. switch (button) {
  616. case ButtonMap.Y:
  617. if (isMetaButtonDown) {
  618. execCommand('seekBy', -15);
  619. } else if (isPauseButtonDown) {
  620. execCommand('seekPrevFrame');
  621. }
  622. break;
  623.  
  624. case ButtonMap.A:
  625. if (isMetaButtonDown) {
  626. execCommand('seekBy', 15);
  627. } else if (isPauseButtonDown) {
  628. execCommand('seekNextFrame');
  629. }
  630. break;
  631. case ButtonMap.SUP:
  632. if (isMetaButtonDown) {
  633. scrollUp();
  634. } else {
  635. execCommand('playPreviousVideo');
  636. }
  637. break;
  638. case ButtonMap.SDN:
  639. if (isMetaButtonDown) {
  640. scrollDown();
  641. } else {
  642. execCommand('playNextVideo');
  643. }
  644. break;
  645. }
  646. };
  647.  
  648.  
  649. const onAxisChange = (axis, value, deviceId) => {};
  650. const onAxisRepeat = (axis, value, deviceId) => {};
  651. const onPovChange = (pov, deviceId) => {
  652. switch(pov) {
  653. case 'UP':
  654. if (isMetaButtonDown) {
  655. speedUp();
  656. } else {
  657. execCommand('volumeUp');
  658. }
  659. break;
  660. case 'DOWN':
  661. if (isMetaButtonDown) {
  662. speedDown();
  663. } else {
  664. execCommand('volumeDown');
  665. }
  666. break;
  667. case 'LEFT':
  668. execCommand('seekBy', isRate1ButtonDown || isMetaButtonDown ? -1 : -5);
  669. break;
  670. case 'RIGHT':
  671. execCommand('seekBy', isRate1ButtonDown || isMetaButtonDown ? +1 : +5);
  672. break;
  673. }
  674. };
  675.  
  676. const onPovRepeat = onPovChange;
  677.  
  678.  
  679. class Handler {
  680. constructor(...args) {
  681. this._list = new Array(...args);
  682. }
  683.  
  684. get length() {
  685. return this._list.length;
  686. }
  687.  
  688. exec(...args) {
  689. if (!this._list.length) {
  690. return;
  691. } else if (this._list.length === 1) {
  692. this._list[0](...args);
  693. return;
  694. }
  695. for (let i = this._list.length - 1; i >= 0; i--) {
  696. this._list[i](...args);
  697. }
  698. }
  699.  
  700. execMethod(name, ...args) {
  701. if (!this._list.length) {
  702. return;
  703. } else if (this._list.length === 1) {
  704. this._list[0][name](...args);
  705. return;
  706. }
  707. for (let i = this._list.length - 1; i >= 0; i--) {
  708. this._list[i][name](...args);
  709. }
  710. }
  711.  
  712. add(member) {
  713. if (this._list.includes(member)) {
  714. return this;
  715. }
  716. this._list.unshift(member);
  717. return this;
  718. }
  719.  
  720. remove(member) {
  721. _.pull(this._list, member);
  722. return this;
  723. }
  724.  
  725. clear() {
  726. this._list.length = 0;
  727. return this;
  728. }
  729.  
  730. get isEmpty() {
  731. return this._list.length < 1;
  732. }
  733. }
  734.  
  735.  
  736. const {Emitter} = (() => {
  737. class Emitter {
  738.  
  739. on(name, callback) {
  740. if (!this._events) {
  741. Emitter.totalCount++;
  742. this._events = {};
  743. }
  744.  
  745. name = name.toLowerCase();
  746. let e = this._events[name];
  747. if (!e) {
  748. e = this._events[name] = new Handler(callback);
  749. } else {
  750. e.add(callback);
  751. }
  752. if (e.length > 10) {
  753. Emitter.warnings.push(this);
  754. }
  755. return this;
  756. }
  757.  
  758. off(name, callback) {
  759. if (!this._events) {
  760. return;
  761. }
  762.  
  763. name = name.toLowerCase();
  764. const e = this._events[name];
  765.  
  766. if (!this._events[name]) {
  767. return;
  768. } else if (!callback) {
  769. delete this._events[name];
  770. } else {
  771. e.remove(callback);
  772.  
  773. if (e.isEmpty) {
  774. delete this._events[name];
  775. }
  776. }
  777.  
  778. if (Object.keys(this._events).length < 1) {
  779. delete this._events;
  780. }
  781. return this;
  782. }
  783.  
  784. once(name, func) {
  785. const wrapper = (...args) => {
  786. func(...args);
  787. this.off(name, wrapper);
  788. wrapper._original = null;
  789. };
  790. wrapper._original = func;
  791. return this.on(name, wrapper);
  792. }
  793.  
  794. clear(name) {
  795. if (!this._events) {
  796. return;
  797. }
  798.  
  799. if (name) {
  800. delete this._events[name];
  801. } else {
  802. delete this._events;
  803. Emitter.totalCount--;
  804. }
  805. return this;
  806. }
  807.  
  808. emit(name, ...args) {
  809. if (!this._events) {
  810. return;
  811. }
  812.  
  813. name = name.toLowerCase();
  814. const e = this._events[name];
  815.  
  816. if (!e) {
  817. return;
  818. }
  819.  
  820. e.exec(...args);
  821. return this;
  822. }
  823.  
  824. emitAsync(...args) {
  825. if (!this._events) {
  826. return;
  827. }
  828.  
  829. setTimeout(() => {
  830. this.emit(...args);
  831. }, 0);
  832. return this;
  833. }
  834. }
  835.  
  836. Emitter.totalCount = 0;
  837. Emitter.warnings = [];
  838.  
  839. return {
  840. Emitter
  841. };
  842. })();
  843.  
  844. class PollingTimer {
  845. constructor(callback, interval) {
  846. this._timer = null;
  847. this._callback = callback;
  848. if (typeof interval === 'number') {
  849. this.changeInterval(interval);
  850. }
  851. }
  852. changeInterval(interval) {
  853. if (this._timer) {
  854. if (this._currentInterval === interval) {
  855. return;
  856. }
  857. window.clearInterval(this._timer);
  858. }
  859. console.log('%cupdate Interval:%s', 'background: lightblue;', interval);
  860. this._currentInterval = interval;
  861. this._timer = window.setInterval(this._callback, interval);
  862. }
  863. pause() {
  864. window.clearInterval(this._timer);
  865. this._timer = null;
  866. }
  867. start() {
  868. if (typeof this._currentInterval !== 'number') {
  869. return;
  870. }
  871. this.changeInterval(this._currentInterval);
  872. }
  873. }
  874.  
  875. class GamePad extends Emitter {
  876. constructor(gamepadStatus) {
  877. super();
  878. this._gamepadStatus = gamepadStatus;
  879. this._buttons = [];
  880. this._axes = [];
  881. this._pov = '';
  882. this._lastTimestamp = 0;
  883. this._povRepeat = 0;
  884. this.initialize(gamepadStatus);
  885. }
  886. initialize(gamepadStatus) {
  887. this._buttons.length = gamepadStatus.buttons.length;
  888. this._axes.length = gamepadStatus.axes.length;
  889. this._id = gamepadStatus.id;
  890. this._index = gamepadStatus.index;
  891. this._isRepeating = false;
  892. this.reset();
  893. }
  894. reset() {
  895. let i, len;
  896. this._pov = '';
  897. this._povRepeat = 0;
  898.  
  899. for (i = 0, len = this._gamepadStatus.buttons.length + 16; i < len; i++) {
  900. this._buttons[i] = {pressed: false, repeat: 0};
  901. }
  902. for (i = 0, len = this._gamepadStatus.axes.length; i < len; i++) {
  903. this._axes[i] = {value: null, repeat: 0};
  904. }
  905. }
  906.  
  907. update() {
  908. let gamepadStatus = (navigator.getGamepads())[this._index];
  909.  
  910. if (!gamepadStatus || !gamepadStatus.connected) { console.log('no status'); return; }
  911.  
  912. if (!this._isRepeating && this._lastTimestamp === gamepadStatus.timestamp) {
  913. return;
  914. }
  915. this._gamepadStatus = gamepadStatus;
  916. this._lastTimestamp = gamepadStatus.timestamp;
  917.  
  918. let buttons = gamepadStatus.buttons, axes = gamepadStatus.axes;
  919. let i, len, axis, isRepeating = false;
  920.  
  921. for (i = 0, len = Math.min(this._buttons.length, buttons.length); i < len; i++) {
  922. let buttonStatus = buttons[i].pressed ? 1 : 0;
  923.  
  924. if (this._buttons[i].pressed !== buttonStatus) {
  925. let eventName = (buttonStatus === 1) ? 'onButtonDown' : 'onButtonUp';
  926. this.emit(eventName, i, 0);
  927. this.emit('onButtonStatusChange', i, buttonStatus);
  928. }
  929. this._buttons[i].pressed = buttonStatus;
  930. if (buttonStatus) {
  931. this._buttons[i].repeat++;
  932. isRepeating = true;
  933. if (this._buttons[i].repeat % 5 === 0) {
  934. //console.log('%cbuttonRepeat%s', 'background: lightblue;', i);
  935. this.emit('onButtonRepeat', i);
  936. }
  937. } else {
  938. this._buttons[i].repeat = 0;
  939. }
  940. }
  941. for (i = 0, len = Math.min(8, this._axes.length); i < len; i++) {
  942. axis = Math.round(axes[i] * 1000) / 1000;
  943.  
  944. if (this._axes[i].value === null) {
  945. this._axes[i].value = axis;
  946. continue;
  947. }
  948.  
  949. let diff = Math.round(Math.abs(axis - this._axes[i].value));
  950. if (diff >= 1) {
  951. this.emit('onAxisChange', i, axis);
  952. }
  953. if (Math.abs(axis) <= 0.1 && this._axes[i].repeat > 0) {
  954. this._axes[i].repeat = 0;
  955. } else if (Math.abs(axis) > 0.1) {
  956. this._axes[i].repeat++;
  957. isRepeating = true;
  958. } else {
  959. this._axes[i].repeat = 0;
  960. }
  961. this._axes[i].value = axis;
  962.  
  963. }
  964.  
  965. if (typeof axes[9] !== 'number') {
  966. this._isRepeating = isRepeating;
  967. return;
  968. }
  969. {
  970. const b = 100000;
  971. const axis = Math.trunc(axes[9] * b);
  972. const margin = b / 10;
  973. let pov = '';
  974. const AxisMap = this._id.match(/Vendor: 057e Product: 2006/i) ? AxisMapJoyConL : AxisMapJoyConR;
  975. if (Math.abs(JoyConAxisCenter * b - axis) <= margin) {
  976. pov = '';
  977. } else {
  978. Object.keys(AxisMap).forEach(key => {
  979. if (Math.abs(AxisMap[key] * b - axis) <= margin) {
  980. pov = key;
  981. }
  982. });
  983. }
  984. if (this._pov !== pov) {
  985. this._pov = pov;
  986. this._povRepeat = 0;
  987. isRepeating = pov !== '';
  988. this.emit('onPovChange', this._pov);
  989. } else if (pov !== '') {
  990. this._povRepeat++;
  991. isRepeating = true;
  992. if (this._povRepeat % 5 === 0) {
  993. this.emit('onPovRepeat', this._pov);
  994. }
  995. }
  996. }
  997.  
  998.  
  999. this._isRepeating = isRepeating;
  1000. }
  1001.  
  1002. dump() {
  1003. let gamepadStatus = this._gamepadStatus, buttons = gamepadStatus.buttons, axes = gamepadStatus.axes;
  1004. let i, len, btmp = [], atmp = [];
  1005. for (i = 0, len = axes.length; i < len; i++) {
  1006. atmp.push('ax' + i + ': ' + axes[i]);
  1007. }
  1008. for (i = 0, len = buttons.length; i < len; i++) {
  1009. btmp.push('bt' + i + ': ' + (buttons[i].pressed ? 1 : 0));
  1010. }
  1011. return atmp.join('\n') + '\n' + btmp.join(', ');
  1012. }
  1013.  
  1014. getButtonStatus(index) {
  1015. return this._buttons[index] || 0;
  1016. }
  1017.  
  1018. getAxisValue(index) {
  1019. return this._axes[index] || 0;
  1020. }
  1021.  
  1022. release() {
  1023. this.clear();
  1024. }
  1025.  
  1026. get isConnected() {
  1027. return this._gamepadStatus.connected ? true : false;
  1028. }
  1029.  
  1030. get deviceId() {
  1031. return this._id;
  1032. }
  1033.  
  1034. get deviceIndex() {
  1035. return this._index;
  1036. }
  1037.  
  1038. get buttonCount() {
  1039. return this._buttons ? this._buttons.length : 0;
  1040. }
  1041.  
  1042. get axisCount() {
  1043. return this._axes ? this._axes.length : 0;
  1044. }
  1045.  
  1046. get pov() {
  1047. return this._pov;
  1048. }
  1049.  
  1050. get x() {
  1051. return this._axes.length > 0 ? this._axes[0] : 0;
  1052. }
  1053.  
  1054. get y() {
  1055. return this._axes.length > 1 ? this._axes[1] : 0;
  1056. }
  1057.  
  1058. get z() {
  1059. return this._axes.length > 2 ? this._axes[2] : 0;
  1060. }
  1061. }
  1062.  
  1063. const noop = () => {};
  1064.  
  1065. const JoyRemocon = (() => {
  1066. let activeGamepad = null;
  1067. let pollingTimer = null;
  1068. let emitter = new Emitter();
  1069. let unlinked = false;
  1070.  
  1071. const detectGamepad = () => {
  1072. if (activeGamepad) {
  1073. return;
  1074. }
  1075. const gamepads = navigator.getGamepads();
  1076. if (gamepads.length < 1) {
  1077. return;
  1078. }
  1079. const pad = Array.from(gamepads).reverse().find(pad => {
  1080. return pad &&
  1081. pad.connected &&
  1082. pad.id.match(/^Joy-Con/i);
  1083. });
  1084. if (!pad) { return; }
  1085.  
  1086. window.console.log(
  1087. '%cdetect gamepad index: %s, id: "%s", buttons: %s, axes: %s',
  1088. 'background: lightgreen; font-weight: bolder;',
  1089. pad.index, pad.id, pad.buttons.length, pad.axes.length
  1090. );
  1091.  
  1092. const gamepad = new GamePad(pad);
  1093. activeGamepad = gamepad;
  1094.  
  1095. gamepad.on('onButtonDown',
  1096. number => emitter.emit('onButtonDown', number, gamepad.deviceIndex));
  1097. gamepad.on('onButtonRepeat',
  1098. number => emitter.emit('onButtonRepeat', number, gamepad.deviceIndex));
  1099. gamepad.on('onButtonUp',
  1100. number => emitter.emit('onButtonUp', number, gamepad.deviceIndex));
  1101. gamepad.on('onPovChange',
  1102. pov => emitter.emit('onPovChange', pov, gamepad.deviceIndex));
  1103. gamepad.on('onPovRepeat',
  1104. pov => emitter.emit('onPovRepeat', pov, gamepad.deviceIndex));
  1105.  
  1106. emitter.emit('onDeviceConnect', gamepad.deviceIndex, gamepad.deviceId);
  1107.  
  1108. pollingTimer.changeInterval(30);
  1109. };
  1110.  
  1111.  
  1112. const onGamepadConnectStatusChange = (e, isConnected) => {
  1113. console.log('onGamepadConnetcStatusChange', e, e.gamepad.index, isConnected);
  1114.  
  1115. if (isConnected) {
  1116. console.log('%cgamepad connected id:"%s"', 'background: lightblue;', e.gamepad.id);
  1117. detectGamepad();
  1118. } else {
  1119. emitter.emit('onDeviceDisconnect', activegamepad.deviceIndex);
  1120. // if (activeGamepad) {
  1121. // activeGamepad.release();
  1122. // }
  1123. // activeGamepad = null;
  1124. console.log('%cgamepad disconneced id:"%s"', 'background: lightblue;', e.gamepad.id);
  1125. }
  1126. };
  1127.  
  1128. const initializeTimer = () => {
  1129. console.log('%cinitializeGamepadTimer', 'background: lightgreen;');
  1130.  
  1131. const onTimerInterval = () => {
  1132. if (unlinked) {
  1133. return;
  1134. }
  1135. if (!activeGamepad) {
  1136. return detectGamepad();
  1137. }
  1138. if (!activeGamepad.isConnected) {
  1139. return;
  1140. }
  1141. activeGamepad.update();
  1142. };
  1143.  
  1144. pollingTimer = new PollingTimer(onTimerInterval, 1000);
  1145. };
  1146.  
  1147. const initializeGamepadConnectEvent = () => {
  1148. console.log('%cinitializeGamepadConnectEvent', 'background: lightgreen;');
  1149.  
  1150. window.addEventListener('gamepadconnected',
  1151. function(e) { onGamepadConnectStatusChange(e, true); });
  1152. window.addEventListener('gamepaddisconnected',
  1153. function(e) { onGamepadConnectStatusChange(e, false); });
  1154.  
  1155. if (activeGamepad) {
  1156. return;
  1157. }
  1158. window.setTimeout(detectGamepad, 1000);
  1159. };
  1160.  
  1161.  
  1162. let hasStartDetect = false;
  1163. return {
  1164. on: (...args) => { emitter.on(...args); },
  1165. startDetect: () => {
  1166. if (hasStartDetect) { return; }
  1167. hasStartDetect = true;
  1168. initializeTimer();
  1169. initializeGamepadConnectEvent();
  1170. },
  1171. startPolling: () => {
  1172. if (pollingTimer) { pollingTimer.start(); }
  1173. },
  1174. stopPolling: () => {
  1175. if (pollingTimer) { pollingTimer.pause(); }
  1176. },
  1177. unlink: () => {
  1178. if (!activeGamepad) {
  1179. return;
  1180. }
  1181. unlinked = true;
  1182. activeGamepad.release();
  1183. activeGamepad = null;
  1184. pollingTimer.changeInterval(1000);
  1185. execCommand(
  1186. 'notify',
  1187. 'JoyRemocon と切断しました'
  1188. );
  1189. }
  1190. };
  1191. })();
  1192.  
  1193.  
  1194. const initGamepad = () => {
  1195.  
  1196. let isActivated = false;
  1197. let deviceId, deviceIndex;
  1198.  
  1199. let notifyDetect = () => {
  1200. if (!document.hasFocus()) { return; }
  1201. isActivated = true;
  1202. notifyDetect = noop;
  1203.  
  1204. // 初めてボタンかキーが押されたタイミングで通知する
  1205. execCommand(
  1206. 'notify',
  1207. 'ゲームパッド "' + deviceId + '" とリンクしました'
  1208. );
  1209. };
  1210.  
  1211.  
  1212. let bindEvents = () => {
  1213. bindEvents = noop;
  1214.  
  1215. JoyRemocon.on('onButtonDown', number => {
  1216. notifyDetect();
  1217. if (!isActivated) { return; }
  1218. onButtonDown(number, deviceId);
  1219. });
  1220. JoyRemocon.on('onButtonRepeat', number => {
  1221. if (!isActivated) { return; }
  1222. onButtonRepeat(number, deviceId);
  1223. });
  1224. JoyRemocon.on('onButtonUp', number => {
  1225. if (!isActivated) { return; }
  1226. onButtonUp(number, deviceId);
  1227. });
  1228. JoyRemocon.on('onPovChange', pov => {
  1229. if (!isActivated) { return; }
  1230. onPovChange(pov, deviceId);
  1231. });
  1232. JoyRemocon.on('onPovRepeat', pov => {
  1233. if (!isActivated) { return; }
  1234. onPovRepeat(pov, deviceId);
  1235. });
  1236. };
  1237.  
  1238. let onDeviceConnect = function(index, id) {
  1239. deviceIndex = index;
  1240. deviceId = id;
  1241.  
  1242. bindEvents();
  1243. };
  1244.  
  1245. JoyRemocon.on('onDeviceConnect', onDeviceConnect);
  1246. JoyRemocon.startDetect();
  1247. };
  1248.  
  1249.  
  1250. const initialize = () => {
  1251. initGamepad();
  1252. };
  1253.  
  1254. initialize();
  1255. };
  1256.  
  1257. const script = document.createElement('script');
  1258. script.id = 'JoyRemoconLoader';
  1259. script.setAttribute('type', 'text/javascript');
  1260. script.setAttribute('charset', 'UTF-8');
  1261. script.appendChild(document.createTextNode(`(${monkey})();`));
  1262. document.documentElement.append(script);
  1263.  
  1264. })();