- // ==UserScript==
- // @name Masked Watch
- // @namespace https://github.com/segabito/
- // @description 動画上のテキストや顔を検出してコメントを透過する
- // @match *://www.nicovideo.jp/*
- // @match *://live.nicovideo.jp/*
- // @match *://anime.nicovideo.jp/*
- // @match *://embed.nicovideo.jp/watch/*
- // @match *://sp.nicovideo.jp/watch/*
- // @exclude *://ads*.nicovideo.jp/*
- // @exclude *://www.nicovideo.jp/favicon.ico*
- // @exclude *://www.nicovideo.jp/robots.txt*
- // @version 0.3.2
- // @grant none
- // @author 名無しさん
- // @license public domain
- // ==/UserScript==
- /* eslint-disable */
-
- // chrome://flags/#enable-experimental-web-platform-features
-
-
- /**
- * @typedf BoundingBox
- * @property {number} x
- * @property {number} y
- * @property {number} width
- * @property {number} height
- * @property {'face'|'text'} type
- */
-
-
- (() => {
- const PRODUCT = 'MaskedWatch';
-
- const monkey = (PRODUCT) => {
- 'use strict';
- var VER = '0.3.2';
- const ENV = 'STABLE';
-
- let ZenzaWatch = null;
-
- const DEFAULT_CONFIG = {
- interval: 300,
- enabled: true,
- debug: false,
- faceDetection: true,
- textDetection: !navigator.userAgent.toLowerCase().includes('windows'),
- fastMode: true,
- tmpWidth: 854,
- tmpHeight: 480,
- };
- const config = new class extends Function {
- toString() {
- return `
- *** CONFIG MENU (設定はサービスごとに保存) ***
- enabled: ${config.enabled}, // 有効/無効
- debug: ${config.debug}, // デバッグON/OFF
- faceDetection: ${config.faceDetection}, // 顔検出ON/OFF
- textDetection: ${config.textDetection}, // テキスト検出ON/OFF
- fastMode: ${config.fastMode}, // false 精度重視 true 速度重視
- tmpWidth: ${config.tmpWidth}, // 検出処理用キャンバスの横解像度
- tmpHeight: ${config.tmpHeight} // 検出処理用キャンバスの縦解像度
- interval: ${config.interval} // マスクの更新間隔
- `;
- }
- }, def = {};
- Object.keys(DEFAULT_CONFIG).sort().forEach(key => {
- const storageKey = `${PRODUCT}_${key}`;
- def[key] = {
- enumerable: true,
- get() {
- return localStorage.hasOwnProperty(storageKey) ?
- JSON.parse(localStorage[storageKey]) : DEFAULT_CONFIG[key];
- },
- set(value) {
- const currentValue = this[key];
- if (value === currentValue) {
- return;
- }
- if (value === DEFAULT_CONFIG[key]) {
- localStorage.removeItem(storageKey);
- } else {
- localStorage[storageKey] = JSON.stringify(value);
- }
- document.body.dispatchEvent(
- new CustomEvent(`${PRODUCT}-config.update`,
- {detail: {key, value, lastValue: currentValue}, bubbles: true, composed: true}
- ));
- }
- };
- });
- Object.defineProperties(config, def);
-
- const MaskedWatch = window.MaskedWatch = { config };
-
- const createWorker = (func, options = {}) => {
- const src = `(${func.toString()})(self);`;
- const blob = new Blob([src], {type: 'text/javascript'});
- const url = URL.createObjectURL(blob);
- return new Worker(url, options);
- };
- const bounce = {
- origin: Symbol('origin'),
- idle(func, time) {
- let reqId = null;
- let lastArgs = null;
- let promise = new PromiseHandler();
- const [caller, canceller] =
- (time === undefined && self.requestIdleCallback) ?
- [self.requestIdleCallback, self.cancelIdleCallback] : [self.setTimeout, self.clearTimeout];
- const callback = () => {
- const lastResult = func(...lastArgs);
- promise.resolve({lastResult, lastArgs});
- reqId = lastArgs = null;
- promise = new PromiseHandler();
- };
- const result = (...args) => {
- if (reqId) {
- reqId = canceller(reqId);
- }
- lastArgs = args;
- reqId = caller(callback, time);
- return promise;
- };
- result[this.origin] = func;
- return result;
- },
- time(func, time = 0) {
- return this.idle(func, time);
- }
- };
- const throttle = (func, interval) => {
- let lastTime = 0;
- let timer;
- let promise = new PromiseHandler();
- const result = (...args) => {
- if (timer) {
- return promise;
- }
- const now = performance.now();
- const timeDiff = now - lastTime;
- timer = setTimeout(() => {
- lastTime = performance.now();
- timer = null;
- const lastResult = func(...args);
- promise.resolve({lastResult, lastArgs: args});
- promise = new PromiseHandler();
- }, Math.max(interval - timeDiff, 0));
- return promise;
- };
- result.cancel = () => {
- if (timer) {
- timer = clearTimeout(timer);
- }
- promise.resolve({lastResult: null, lastArgs: null});
- promise = new PromiseHandler();
- };
- return result;
- };
- throttle.time = (func, interval = 0) => throttle(func, interval);
- throttle.raf = function(func) {
- // let promise;// = new PromiseHandler();
- let promise;
- let cancelled = false;
- const result = (...args) => {
- if (promise) {
- return promise;
- }
- if (!this.req) {
- this.req = new Promise(res => requestAnimationFrame(res)).then(() => {
- this.req = null;
- });
- }
- promise = this.req.then(() => {
- if (cancelled) {
- cancelled = false;
- return;
- }
- try { func(...args); } catch (e) { console.warn(e); }
- promise = null;
- });
- return promise;
- };
- result.cancel = () => {
- cancelled = true;
- promise = null;
- };
- return result;
- }.bind({req: null, count: 0, id: 0});
- throttle.idle = func => {
- let id;
- const request = (self.requestIdleCallback || self.setTimeout);
- const cancel = (self.cancelIdleCallback || self.clearTimeout);
- const result = (...args) => {
- if (id) {
- return;
- }
- id = request(() => {
- id = null;
- func(...args);
- }, 0);
- };
- result.cancel = () => {
- if (id) {
- id = cancel(id);
- }
- };
- return result;
- };
- const css = (() => {
- const setPropsTask = [];
- const applySetProps = throttle.raf(
- () => {
- const tasks = setPropsTask.concat();
- setPropsTask.length = 0;
- for (const [element, prop, value] of tasks) {
- try {
- element.style.setProperty(prop, value);
- } catch (error) {
- console.warn('element.style.setProperty fail', {element, prop, value, error});
- }
- }
- });
- const css = {
- addStyle: (styles, option, document = window.document) => {
- const elm = Object.assign(document.createElement('style'), {
- type: 'text/css'
- }, typeof option === 'string' ? {id: option} : (option || {}));
- if (typeof option === 'string') {
- elm.id = option;
- } else if (option) {
- Object.assign(elm, option);
- }
- elm.classList.add(global.PRODUCT);
- elm.append(styles.toString());
- (document.head || document.body || document.documentElement).append(elm);
- elm.disabled = option && option.disabled;
- elm.dataset.switch = elm.disabled ? 'off' : 'on';
- return elm;
- },
- registerProps(...args) {
- if (!CSS || !('registerProperty' in CSS)) {
- return;
- }
- for (const definition of args) {
- try {
- (definition.window || window).CSS.registerProperty(definition);
- } catch (err) { console.warn('CSS.registerProperty fail', definition, err); }
- }
- },
- setProps(...tasks) {
- setPropsTask.push(...tasks);
- return setPropsTask.length ? applySetProps() : Promise.resolve();
- },
- addModule: async function(func, options = {}) {
- if (!CSS || !('paintWorklet' in CSS) || this.set.has(func)) {
- return;
- }
- this.set.add(func);
- const src =
- `(${func.toString()})(
- this,
- registerPaint,
- ${JSON.stringify(options.config || {}, null, 2)}
- );`;
- const blob = new Blob([src], {type: 'text/javascript'});
- const url = URL.createObjectURL(blob);
- await CSS.paintWorklet.addModule(url).then(() => URL.revokeObjectURL(url));
- return true;
- }.bind({set: new WeakSet}),
- escape: value => CSS.escape ? CSS.escape(value) : value.replace(/([\.#()[\]])/g, '\\$1'),
- number: value => CSS.number ? CSS.number(value) : value,
- s: value => CSS.s ? CSS.s(value) : `${value}s`,
- ms: value => CSS.ms ? CSS.ms(value) : `${value}ms`,
- pt: value => CSS.pt ? CSS.pt(value) : `${value}pt`,
- px: value => CSS.px ? CSS.px(value) : `${value}px`,
- percent: value => CSS.percent ? CSS.percent(value) : `${value}%`,
- vh: value => CSS.vh ? CSS.vh(value) : `${value}vh`,
- vw: value => CSS.vw ? CSS.vw(value) : `${value}vw`,
- trans: value => self.CSSStyleValue ? CSSStyleValue.parse('transform', value) : value,
- word: value => self.CSSKeywordValue ? new CSSKeywordValue(value) : value,
- image: value => self.CSSStyleValue ? CSSStyleValue.parse('background-image', value) : value,
- };
- return css;
- })();
- const cssUtil = css;
-
- const 業務 = function(self) {
- let fastMode, faceDetection, textDetection, debug, enabled;
- const init = params => {
- updateConfig({config: params.config});
- };
-
- const updateConfig = ({config}) => {
- ({fastMode, faceDetection, textDetection, debug, enabled} = config);
- faceDetector = new (self || window).FaceDetector({fastMode});
- textDetector = new (self || window).TextDetector();
- };
-
- let faceDetector;
- let textDetector;
- const detect = async ({bitmap}) => {
- // debug && console.time('detect');
- const tasks = [];
- faceDetection && (tasks.push(faceDetector.detect(bitmap).catch(() => [])));
- textDetection && (tasks.push(textDetector.detect(bitmap).catch(() => [])));
- const detected = (await Promise.all(tasks)).flat();
-
- const boxes = detected.map(d => {
- const {x, y , width, height} = d.boundingBox;
- return {x, y , width, height, type: d.landmarks ? 'face' : 'text'};
- });
- // debug && console.timeEnd('detect');
- return {boxes};
- };
-
- self.onmessage = async e => {
- const {command, params} = e.data.body;
- try {
- switch (command) {
- case 'init':
- init(params);
- self.postMessage({body: {command: 'init', params: {}, status: 'ok'}});
- break;
- case 'config':
- updateConfig(params);
- break;
- case 'detect': {
- const {dataURL, boxes} = await detect(params);
- self.postMessage({body: {command: 'data', params: {dataURL, boxes}, status: 'ok'}});
- }
- break;
- }
- } catch(err) {
- console.error('error', {command, params}, err);
- }
- };
- };
-
- const 下請 = function(self, registerPaint, config) {
- registerPaint('塗装', class {
- static get inputProperties() {
- return ['--json-args', '--config'];
- }
- paint(ctx, {width, height}, props) {
- const args = JSON.parse(props.get('--json-args').toString() || '{}');
- const config = JSON.parse(props.get('--config').toString() || '{}');
-
- ctx.beginPath();
- ctx.fillStyle = 'rgba(255, 255, 255, 1)';
- ctx.fillRect(0, 0, width, height);
- if (!args.history || !config.enabled) {
- return;
- }
-
- const ratio = Math.min(width / config.tmpWidth, height / config.tmpHeight);
- const transX = (width - (config.tmpWidth * ratio)) / 2;
- const transY = (height - (config.tmpHeight * ratio)) / 2;
- const tmpArea = (config.tmpWidth * ratio) * (config.tmpHeight * ratio);
-
-
- /** @type {(BoundingBox[])[]} */
- const history = args.history;
- for (const boxes of history) {
- ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
- ctx.fillRect(0, 0, width, height);
-
- for (const box of boxes) {
- let {x, y , width, height, type} = box;
- const area = width * height;
- const opacity = area / tmpArea * 0.3;
- ctx.fillStyle = `rgba(255, 255, 255, ${opacity})`;
-
- x = x * ratio + transX;
- y = y * ratio + transY;
- width *= ratio;
- height *= ratio;
- if (type === 'face') {
- const mx = 16 * ratio, my = 24 * ratio; // margin
- ctx.clearRect(x - mx, y - my, width + mx * 2, height + my * 2);
- ctx.fillRect (x - mx, y - my, width + mx * 2, height + my * 2);
- } else {
- const mx = 16 * ratio, my = 16 * ratio; // margin
- ctx.clearRect(x - mx, y - my, width + mx * 2, height + my * 2);
- ctx.fillRect (x - mx, y - my, width + mx * 2, height + my * 2);
- }
- }
- }
- }
- });
- };
-
- const createDetector = async ({video, layer, interval, type}) => {
- const worker = createWorker(業務, {name: 'Facelook'});
- await css.addModule(下請, {config: {...config}});
- const transferCanvas = new OffscreenCanvas(config.tmpWidth, config.tmpHeight);
- const ctx = transferCanvas.getContext('2d', {alpha: false, desynchronized: true});
- const debugLayer = document.createElement('div');
- [layer, debugLayer].forEach(layer => {
- layer.style.setProperty('--config', JSON.stringify({...config}));
- layer.style.setProperty('--json-args', '{}');
- });
- 'maskImage' in layer.style ?
- (layer.style.maskImage = 'paint(塗装)') :
- (layer.style.webkitMaskImage = 'paint(塗装)');
-
- // for debug
- Object.assign(debugLayer.style, {
- border: '1px solid #888',
- left: 0,
- bottom: '48px',
- position: 'fixed',
- zIndex: '100000',
- width: '160px',
- height: '90px',
- opacity: 0.5,
- background: '#333',
- pointerEvents: 'none',
- userSelect: 'none'
- });
- debugLayer.classList.add('zen-family');
- debugLayer.dataset.type = type;
- debugLayer.style.backgroundImage = 'paint(塗装)';
- config.debug && document.body.append(debugLayer);
- worker.postMessage({body: {command: 'init', params: {config: {...config}}}});
-
- let isBusy = true, currentTime = video.currentTime, boxHistory = [];
- worker.addEventListener('message', e => {
- const {command, params} = e.data.body;
- switch (command) {
- case 'init':
- console.log('initialized');
- isBusy = false;
- break;
- case 'data': {
- isBusy = false;
- if (!config.enabled) { return; }
- /** @type {BoundingBox[]} */
- const boxes = params.boxes;
- boxHistory.push(boxes);
- while (boxHistory.length > 5) { boxHistory.shift(); }
- const arg = JSON.stringify({
- tmpWidth: config.tmpWidth, tmpHeight: config.tmpHeight,
- history: boxHistory
- });
- layer.style.setProperty('--json-args', arg);
- config.debug && debugLayer.style.setProperty('--json-args', arg);
- }
- break;
- }
- });
-
- const onTimer = () => {
- if (isBusy ||
- currentTime === video.currentTime ||
- document.visibilityState !== 'visible') {
- return;
- }
-
- currentTime = video.currentTime;
- const vw = video.videoWidth, vh = video.videoHeight;
- const tmpWidth = config.tmpWidth, tmpHeight = config.tmpHeight;
- const ratio = Math.min(tmpWidth / vw, tmpHeight / vh);
- const dw = vw * ratio, dh = vh * ratio;
- ctx.beginPath();
- ctx.drawImage(
- video,
- 0, 0, vw, vh,
- (tmpWidth - dw) / 2, (tmpHeight - dh) / 2, dw, dh
- );
- const bitmap = transferCanvas.transferToImageBitmap();
- isBusy = true;
- worker.postMessage({body: {command: 'detect', params: {bitmap}}}, [bitmap]);
- };
- let timer = setInterval(onTimer, interval);
-
- const start = () => timer = setInterval(onTimer, interval);
- const stop = () => timer = clearInterval(timer);
-
- window.addEventListener(`${PRODUCT}-config.update`, e => {
- worker.postMessage({body: {command: 'config', params: {config: {...config}}}});
- const {key, value} = e.detail;
- layer.style.setProperty('--config', JSON.stringify({...config}));
- debugLayer.style.setProperty('--config', JSON.stringify({...config}));
- switch (key) {
- case 'enabled':
- value ? start() : stop();
- break;
- case 'debug':
- value ? document.body.append(debugLayer) : debugLayer.remove();
- break;
- case 'tmpWidth':
- transferCanvas.width = value;
- break;
- case 'tmpHeight':
- transferCanvas.height = value;
- break;
- }
- }, {passive: true});
- return { start, stop };
- };
-
- const dialog = ((config) => {
- class MaskedWatchDialog extends HTMLElement {
- init() {
- if (this.shadow) { return; }
- this.shadow = this.attachShadow({mode: 'open'});
- this.shadow.innerHTML = this.getTemplate(config);
- this.root = this.shadow.querySelector('#root');
- this.shadow.querySelector('.close-button').addEventListener('click', e => {
- this.close(); e.stopPropagation(); e.preventDefault();
- });
- this.root.addEventListener('click', e => {
- if (e.target === this.root) { this.close(); }
- e.stopPropagation();
- });
- this.classList.add('zen-family');
- this.root.classList.add('zen-family');
- this.update();
-
- this.root.addEventListener('change', e => {
- const input = e.target;
- const name = input.name;
- const value = JSON.parse(input.value);
- config.debug && console.log('update config', {name, value});
- config[name] = value;
- });
- }
- getTemplate(config) {
- return `
- <dialog id="root" class="root">
- <div>
- <style>
- .root {
- position: fixed;
- z-index: 10000;
- left: 0;
- top: 50%;
- transform: translate(0, -50%);
- background: rgba(240, 240, 240, 0.95);
- color: #222;
- padding: 16px 24px 8px;
- border: 0;
- user-select: none;
- box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.8);
- text-shadow: 1px 1px 0 #fff;
- border-radius: 4px;
- }
- .title {
- margin: 0;
- padding: 0 0 16px;
- font-size: 20px;
- text-align: center;
- }
- .config {
- padding: 0 0 16px;
- line-height: 20px;
- }
- .name {
- display: inline-block;
- min-width: 240px;
- white-space: nowrap;
- margin: 0;
- }
- label {
- display: inline-block;
- padding: 8px;
- line-height: 20px;
- min-width: 100px;
- border: 1px groove silver;
- border-radius: 4px;
- cursor: pointer;
- }
- label + label {
- margin-left: 8px;
- }
- label:hover {
- background: rgba(255, 255, 255, 1);
- }
- input[type=radio] {
- transform: scale(1.5);
- margin-right: 12px;
- }
- .close-button {
- display: block;
- margin: 8px auto 0;
- min-width: 180px;
- padding: 8px;
- font-size: 16px;
- border-radius: 4px;
- text-align: center;
- cursor: pointer;
- outline: none;
- }
- </style>
- <h1 class="title">††† Masked Watch 設定 †††</h1>
- <div class="config">
- <h3 class="name">顔の検出</h3>
- <label><input type="radio" name="faceDetection" value="true">ON</label>
- <label><input type="radio" name="faceDetection" value="false">OFF</label>
- </div>
-
- <div class="config">
- <h3 class="name">テキストの検出<br>
- <span class="name" style="font-size: 80%;">windowsで動かないっぽい?</span>
- </h3>
- <label><input type="radio" name="textDetection" value="true">ON</label>
- <label><input type="radio" name="textDetection" value="false">OFF</label>
- </div>
-
- <div class="config">
- <h3 class="name">動作モード</h3>
- <label><input type="radio" name="fastMode" value="true">速度重視</label>
- <label><input type="radio" name="fastMode" value="false">精度重視</label>
- </div>
-
- <div class="config">
- <h3 class="name">デバッグ</h3>
- <label><input type="radio" name="debug" value="true">ON</label>
- <label><input type="radio" name="debug" value="false">OFF</label>
- </div>
-
- <div class="config">
- <h3 class="name">MaskedWatch有効/無効</h3>
- <label><input type="radio" name="enabled" value="true">有効</label>
- <label><input type="radio" name="enabled" value="false">無効</label>
- </div>
- <div class="config">
- <button class="close-button">閉じる</button>
- </div>
- </div>
- </dialog>
- `;
- }
-
- update() {
- this.init();
- [...this.shadow.querySelectorAll('input')].forEach(input => {
- const name = input.name, value = JSON.parse(input.value);
- input.checked = config[name] === value;
- });
- }
-
- get isOpen() {
- return !!this.root && !!this.root.open;
- }
-
- open() {
- this.update();
- this.root.showModal();
- }
-
- close() {
- this.root && this.root.close();
- }
-
- toggle() {
- this.init();
- if (this.isOpen) {
- this.root.close();
- } else {
- this.open();
- }
- }
- }
- window.customElements.define(`${PRODUCT.toLowerCase()}-dialog`, MaskedWatchDialog);
- return document.createElement(`${PRODUCT.toLowerCase()}-dialog`);
- })(config);
- MaskedWatch.dialog = dialog;
-
- const createToggleButton = (config, dialog) => {
- class ToggleButton extends HTMLElement {
- constructor() {
- super();
- this.init();
- }
- init() {
- if (this.shadow) { return; }
- this.shadow = this.attachShadow({mode: 'open'});
- this.shadow.innerHTML = this.getTemplate(config);
- this.root = this.shadow.querySelector('#root');
- this.root.addEventListener('click', e => {
- dialog.toggle(); e.stopPropagation(); e.preventDefault();
- });
- }
- getTemplate() {
- return `
- <style>
- .controlButton {
- position: relative;
- display: inline-block;
- transition: opacity 0.4s ease, margin-left 0.2s ease, margin-top 0.2s ease;
- box-sizing: border-box;
- text-align: center;
- cursor: pointer;
- color: #fff;
- opacity: 0.8;
- vertical-align: middle;
- }
- .controlButton:hover {
- cursor: pointer;
- opacity: 1;
- }
- .controlButton .controlButtonInner {
- filter: grayscale(100%);
- }
- .switch {
- font-size: 16px;
- width: 32px;
- height: 32px;
- line-height: 30px;
- cursor: pointer;
- }
- .is-Enabled .controlButtonInner {
- color: #aef;
- filter: none;
- }
-
- .controlButton .tooltip {
- display: none;
- pointer-events: none;
- position: absolute;
- left: 16px;
- top: -30px;
- transform: translate(-50%, 0);
- font-size: 12px;
- line-height: 16px;
- padding: 2px 4px;
- border: 1px solid !000;
- background: #ffc;
- color: #000;
- text-shadow: none;
- white-space: nowrap;
- z-index: 100;
- opacity: 0.8;
- }
-
- .controlButton:hover {
- background: #222;
- }
-
- .controlButton:hover .tooltip {
- display: block;
- opacity: 1;
- }
-
- </style>
- <div id="root" class="switch controlButton root">
- <div class="controlButtonInner" title="MaskedWatch">☻</div>
- <div class="tooltip">Masked Watch</div>
- </div>
- `;
- }
- }
- window.customElements.define(`${PRODUCT.toLowerCase()}-toggle-button`, ToggleButton);
- return document.createElement(`${PRODUCT.toLowerCase()}-toggle-button`);
- };
-
- const ZenzaDetector = (() => {
- const promise =
- (window.ZenzaWatch && window.ZenzaWatch.ready) ?
- Promise.resolve(window.ZenzaWatch) :
- new Promise(resolve => {
- [window, (document.body || document.documentElement)]
- .forEach(e => e.addEventListener('ZenzaWatchInitialize', () => {
- resolve(window.ZenzaWatch);
- }, {once: true}));
- });
- return {detect: () => promise};
- })();
-
- const vmap = new WeakMap();
- let timer;
- const watch = () => {
- if (!config.enabled || document.visibilityState !== 'visible') { return; }
- [...document.querySelectorAll('video, zenza-video')]
- .filter(video => !video.paused && !vmap.has(video))
- .forEach(video => {
- // 対応プレイヤー増やすならココ
- let layer, type = 'UNKNOWN';
- if (video.closest('#MainVideoPlayer')) {
- layer = document.querySelector('.CommentRenderer');
- type = 'NICO VIDEO';
- } else if (video.closest('#rootElementId')) {
- layer = document.querySelector('#comment canvas');
- type = 'NICO EMBED';
- } else if (video.closest('#watchVideoContainer')) {
- layer = document.querySelector('#jsPlayerCanvasComment canvas');
- type = 'NICO SP';
- } else if (video.closest('.zenzaPlayerContainer')) {
- layer = document.querySelector('.commentLayerFrame');
- type = 'ZenzaWatch';
- } else if (video.closest('[class*="__leo"]')) {
- layer = document.querySelector('#comment-layer-container canvas');
- type = 'NICO LIVE';
- } else if (video.closest('#bilibiliPlayer')) {
- layer = document.querySelector('.bilibili-player-video-danmaku').parentElement;
- type = 'BILI BILI [´ω`]';
- } else if (video.id === 'js-video') {
- layer = document.querySelector('#cmCanvas');
- type = 'HIMAWARI';
- }
-
- console.log('%ctype: "%s"', 'font-weight: bold', layer ? type : 'UNKNOWN???');
- layer && Object.assign(layer.style, {
- backgroundSize: 'contain',
- maskSize: 'contain',
- webkitMaskSize: 'contain',
- maskRepeat: 'no-repeat',
- webkitMaskRepeat: 'no-repeat',
- maskPosition: 'center center',
- webkitMaskPosition: 'center center'
- });
- layer && video.dispatchEvent(
- new CustomEvent(`${PRODUCT}-start`,
- {detail: {type, video, layer}, bubbles: true, composed: true}
- ));
-
- vmap.set(video,
- layer ?
- createDetector({video: video.drawableElement || video, layer, interval: config.interval, type}) :
- type
- );
- layer && !location.href.startsWith('https://www.nicovideo.jp/watch/') && clearInterval(timer);
- });
- };
-
- const init = () => {
- css.registerProps(
- {name: '--json-args', syntax: '*', initialValue: '{}', inherits: false },
- {name: '--config', syntax: '*', initialValue: '{}', inherits: false }
- );
- timer = setInterval(watch, 1000);
-
- document.body.append(dialog);
-
- window.setTimeout(() => {
- const li = document.createElement('li');
- li.innerHTML = `<a href="javascript:;">${PRODUCT}設定</a>`;
- li.style.whiteSpace = 'nowrap';
- li.addEventListener('click', () => dialog.toggle());
- document.querySelector('#siteHeaderRightMenuContainer').append(li);
- }, document.querySelector('#siteHeaderRightMenuContainer') ? 1000 : 15000);
-
- ZenzaDetector.detect().then(zen => {
- console.log('ZenzaWatch found ver.%s', zen.version);
- ZenzaWatch = zen;
- ZenzaWatch.emitter.promise('videoControBar.addonMenuReady').then(({container}) => {
- container.append(createToggleButton(config, dialog));
- });
- ZenzaWatch.emitter.promise('videoContextMenu.addonMenuReady.list').then(({container}) => {
- const faceMenu = document.createElement('li');
- faceMenu.className = 'command';
- faceMenu.dataset.command = 'nop';
- faceMenu.textContent = '顔の検出';
- faceMenu.classList.toggle('selected', config.faceDetection);
- faceMenu.addEventListener('click', () => {
- config.faceDetection = !config.faceDetection;
- });
- const textMenu = document.createElement('li');
- textMenu.className = 'command';
- textMenu.dataset.command = 'nop';
- textMenu.textContent = 'テキストの検出';
- textMenu.classList.toggle('selected', config.textDetection);
- textMenu.addEventListener('click', () => {
- config.textDetection = !config.textDetection;
- });
- ZenzaWatch.emitter.on('showMenu', () => {
- faceMenu.classList.toggle('selected', config.faceDetection);
- textMenu.classList.toggle('selected', config.textDetection);
- });
-
- container.append(faceMenu, textMenu);
- });
-
- });
- };
- init();
-
- // eslint-disable-next-line no-undef
- console.log('%cMasked Watch', 'font-size: 200%;', `ver ${VER}`, '\nconfig: ', JSON.stringify({...config}));
- };
-
- const loadGm = () => {
- const script = document.createElement('script');
- script.id = `${PRODUCT}Loader`;
- script.setAttribute('type', 'text/javascript');
- script.setAttribute('charset', 'UTF-8');
- script.append(`
- (() => {
- (${monkey.toString()})("${PRODUCT}");
- })();`);
- (document.head || document.documentElement).append(script);
- };
-
- loadGm();
- })();