ambr アバター条件チェッカー

VRoid Hub上のアバターが、ambrで利用可能かどうかを表示します。

// ==UserScript==
// @name        ambr アバター条件チェッカー
// @description VRoid Hub上のアバターが、ambrで利用可能かどうかを表示します。
// @namespace   https://greasyfork.org/users/137
// @version     1.0.2
// @match       https://hub.vroid.com/*
// @require     https://gitcdn.xyz/cdn/greasemonkey/gm4-polyfill/a834d46afcc7d6f6297829876423f58bb14a0d97/gm4-polyfill.js
// @resource    dialog-polyfill.css https://bowercdn.net/c/dialog-polyfill-0.4.10/dialog-polyfill.css
// @require     https://bowercdn.net/c/dialog-polyfill-0.4.10/dialog-polyfill.js
// @require     https://greasyfork.org/scripts/17895/code/polyfill.js?version=625392
// @require     https://greasyfork.org/scripts/19616/code/utilities.js?version=230651
// @license     MPL-2.0
// @contributionURL https://www.amazon.co.jp/registry/wishlist/E7PJ5C3K7AM2
// @compatible  Edge
// @compatible  Firefox 推奨
// @compatible  Opera
// @compatible  Chrome
// @grant       GM.registerMenuCommand
// @grant       GM_registerMenuCommand
// @grant       GM.getResourceUrl
// @grant       GM_getResourceURL
// @icon        https://ambr.co.jp/public/img/favicon.png
// @author      100の人
// @homepageURL https://greasyfork.org/users/137
// ==/UserScript==

'use strict';

const AMBR_LIMITS = {
	triangles: 12000,
	joints: 110,
	materials: 2,
	fileCapacity: 12 * (1000 ** 2),
};

let setModel, modelId, abortController, dialog, title, result, permission, spec;

// ページ側のコンテキストからのメッセージ待ち受け
addEventListener('message', function (event) {
	if (event.origin !== location.origin
		|| typeof event.data !== 'object' || event.data === null || !event.data.latest_character_model_version) {
		return;
	}
	setModel(event.data);
});

function getModeInfo()
{
	/*global dehydrated */
	GreasemonkeyUtils.executeOnUnsafeContext(function (modelId) {
		const models = dehydrated.model.models;
		postMessage(models[modelId], location.origin);
	}, [modelId]);
	return new Promise(function (resolve) {
		setModel = resolve;
	});
}

GM.registerMenuCommand('ambrで使用できるアバターかどうか確認', async function () {
	const r = /^\/characters\/[0-9]+\/models\/([0-9]+)$/.exec(location.pathname);
	if (!r) {
		return;
	}
	modelId = r[1];

	const model = await getModeInfo();

	if (!dialog) {
		document.head.insertAdjacentHTML('beforeend', `<style>
			#ambr-avatar-checker {
				border-radius: 4px;
				cursor: auto;
				border: unset;
				color: #5C5C5C;
				background: whitesmoke;
			}
		
			#ambr-avatar-checker h1 {
				font-size: 16px;
				font-weight: bold;
				color: #1F1F1F;
				text-align: center;
			}
		
			#ambr-avatar-checker button {
				border: none;
				position: absolute;
				top: 1em;
				right: 1em;
				width: 1.5em;
				height: 1.5em;
				cursor: pointer;
				overflow: hidden;
				white-space: nowrap;
			}
		
			#ambr-avatar-checker button::before {
				content: "✖";
				margin-right: 10em;
			}

			#ambr-avatar-checker .satisfied,
			#ambr-avatar-checker .unsatisfied {
				font-weight: bold;
			}

			#ambr-avatar-checker .satisfied {
				color: limegreen;
			}

			#ambr-avatar-checker .unsatisfied {
				color: crimson;
			}

			#ambr-avatar-checker b,
			#ambr-avatar-checker p {
				text-aling: center;
			}

			/*====================================
				アバターの人格に関する許容範囲
			 */
			#ambr-avatar-checker dl {
				display: grid; 
				grid-auto-columns: auto auto;
			}

			#ambr-avatar-checker dt {
				grid-column: 1;
				grid-row: span 10;
			}

			#ambr-avatar-checker dd {
				grid-column: 2;
			}

			/*====================================
				3Dモデルデータの制限事項
			 */
			#ambr-avatar-checker table {
				width: 100%;
			}

			#ambr-avatar-checker th,
			#ambr-avatar-checker td {
				text-align: left;
				padding: 0.1em;
			}

			#ambr-avatar-checker table tr {
				background: gainsboro;
				border-width: 1px;
				border-style: solid none;
			}

			#ambr-avatar-checker tbody {
				border-top: solid;
				border-bottom: solid;
			}

			#ambr-avatar-checker tbody tr:nth-of-type(2n-1) {
				background: unset;
			}
		</style>`);
		
		document.body.insertAdjacentHTML('afterbegin', h`<dialog id="ambr-avatar-checker">
			<h1>ambr アバター条件チェッカー</h1>
			<button>閉じる</button>
			<b></b>
			<p></p>
			<section>
				<h1>アバターの人格に関する許容範囲</h1>
			</section>
			<section>
				<h1>3Dモデルデータの制限事項</h1>
			</section>
		</dialog>`);

		dialog = document.getElementById('ambr-avatar-checker');
		dialog.getElementsByTagName('button')[0].addEventListener('click', function () {
			dialog.close();
			if (abortController) {
				abortController.abort();
			}
		});
		title = dialog.getElementsByTagName('b')[0];
		result = dialog.getElementsByTagName('p')[0];
		[permission, spec] = dialog.getElementsByTagName('section');

		document.head.insertAdjacentHTML(
			'beforeend',
			h`<link rel="stylesheet" href="${await GM.getResourceUrl('dialog-polyfill.css')}" />`
		);

		/*globals dialogPolyfill */
		dialogPolyfill.registerDialog(dialog);
	}

	for (const element of dialog.querySelectorAll('section > :not(h1)')) {
		element.remove();
	}

	dialog.showModal();

	title.textContent = model.name;
	
	if (document.querySelector('[class*="ModelViewer-editButton-"]')) {
		permission.insertAdjacentHTML('beforeend', '<p class="satisfied">自分がアップロードしたモデルなので、制約はありません。</p>');
	} else {
		const license = model.license;
		permission.insertAdjacentHTML('beforeend', `<dl>
			<dt>連携サービスでの利用</dt>
			${model.is_other_users_available ? '<dd class="satisfied">OK</dd>' : '<dd class="unsatisfied">NG</dd>'}
			<dt>ダウンロード</dt>
			${model.is_downloadable ? '<dd class="satisfied">OK</dd>' : '<dd class="unsatisfied">NG</dd>'}
			<dt>アバターとしての利用</dt>
			${license.characterization_allowed_user === 'everyone' ? '<dd class="satisfied">OK</dd>' : '<dd class="unsatisfied">NG</dd>'}
			<dt>法人の商用利用</dt>
			${license.corporate_commercial_use === 'allow' ? '<dd class="satisfied">OK</dd>' : '<dd class="unsatisfied">NG</dd>'}
			<dt>クレジット表記</dt>
			${license.credit === 'unnecessary' ? '<dd class="satisfied">不要</dd>' : '<dd class="unsatisfied">必要</dd>'}
		</dl>`);
	}
	
	const version = model.latest_character_model_version;
	spec.insertAdjacentHTML('beforeend', h`<table>
		<thead>
			<tr>
				<th>要素</th>
				<th>モデルの値</th>
				<th>ambrの制限値</th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<th>ポリゴン数</th>
				<td class="${version.triangle_count <= AMBR_LIMITS.triangles ? 'satisfied' : 'unsatisfied'}">${version.triangle_count}</td>
				<td>${AMBR_LIMITS.triangles}</td>
			</tr>
			<tr>
				<th>ボーン数</th>
				<td class="${version.joint_count <= AMBR_LIMITS.joints ? 'satisfied' : 'unsatisfied'}">${version.joint_count}</td>
				<td>${AMBR_LIMITS.joints}</td>
			</tr>
			<tr>
				<th>マテリアル数</th>
				<td class="${version.material_count <= AMBR_LIMITS.materials ? 'satisfied' : 'unsatisfied'}">${version.material_count}</td>
				<td>${AMBR_LIMITS.materials}</td>
			</tr>
			<tr>
				<th>ファイル容量</th>
				<td class="${version.original_file_size <= AMBR_LIMITS.fileCapacity ? 'satisfied' : 'unsatisfied'}">
					${(version.original_file_size / (2 ** 20)).toFixed(2)} MiB
				</td>
				<td>${(AMBR_LIMITS.fileCapacity / (2 ** 20)).toFixed(2)} MiB</td>
			</tr>
		</tbody>
	</dl>`);

	if (dialog.getElementsByClassName('unsatisfied').length > 0) {
		result.className = 'unsatisfied';
		result.textContent = 'このアバターはambrでは使用できません。';
	} else {
		result.className = 'satisfied';
		result.textContent = 'このアバターはambrで使用可能です。';
	}
});