JoyRemocon

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

当前为 2019-04-09 提交的版本,查看 最新版本

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