feat(tools): 初期アイコンジェネレーター (#131)

This commit is contained in:
かっこかり 2024-03-25 19:24:24 +09:00 committed by GitHub
parent 5d188439cb
commit ee1e3cf773
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 227 additions and 0 deletions

View File

@ -24,6 +24,11 @@ export default <NavSection[]>[
description: "_shareLinkGenerator.description", description: "_shareLinkGenerator.description",
to: "/tools/share-link-generator/", to: "/tools/share-link-generator/",
}, },
{
i18n: '_identiconGenerator.title',
description: '_identiconGenerator.description',
to: '/tools/identicon-generator/',
},
], ],
}, },
{ {

View File

@ -0,0 +1,104 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Identicon generator
* https://en.wikipedia.org/wiki/Identicon
*/
import gen from 'random-seed';
const size = 128; // px
const n = 5; // resolution
const margin = (size / 4);
const colors = [
['#FF512F', '#DD2476'],
['#FF61D2', '#FE9090'],
['#72FFB6', '#10D164'],
['#FD8451', '#FFBD6F'],
['#305170', '#6DFC6B'],
['#00C0FF', '#4218B8'],
['#009245', '#FCEE21'],
['#0100EC', '#FB36F4'],
['#FDABDD', '#374A5A'],
['#38A2D7', '#561139'],
['#121C84', '#8278DA'],
['#5761B2', '#1FC5A8'],
['#FFDB01', '#0E197D'],
['#FF3E9D', '#0E1F40'],
['#766eff', '#00d4ff'],
['#9bff6e', '#00d4ff'],
['#ff6e94', '#00d4ff'],
['#ffa96e', '#00d4ff'],
['#ffa96e', '#ff009d'],
['#ffdd6e', '#ff009d'],
];
const actualSize = size - (margin * 2);
const cellSize = actualSize / n;
const sideN = Math.floor(n / 2);
/**
* canvasにIdenticonを描画する
* @param seed acct
*/
export function genIdenticon(seed: string, canvas: HTMLCanvasElement) {
const rand = gen.create(seed);
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const bgColors = colors[rand(colors.length)];
const bg = ctx.createLinearGradient(0, 0, size, size);
bg.addColorStop(0, bgColors[0]);
bg.addColorStop(1, bgColors[1]);
ctx.fillStyle = bg as any;
ctx.beginPath();
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = '#ffffff';
// side bitmap (filled by false)
const side: boolean[][] = new Array(sideN);
for (let i = 0; i < side.length; i++) {
side[i] = new Array(n).fill(false);
}
// 1*n (filled by false)
const center: boolean[] = new Array(n).fill(false);
for (let x = 0; x < side.length; x++) {
for (let y = 0; y < side[x].length; y++) {
side[x][y] = rand(3) === 0;
}
}
for (let i = 0; i < center.length; i++) {
center[i] = rand(3) === 0;
}
// Draw
for (let x = 0; x < n; x++) {
for (let y = 0; y < n; y++) {
const isXCenter = x === ((n - 1) / 2);
if (isXCenter && !center[y]) continue;
const isLeftSide = x < ((n - 1) / 2);
if (isLeftSide && !side[x][y]) continue;
const isRightSide = x > ((n - 1) / 2);
if (isRightSide && !side[sideN - (x - sideN)][y]) continue;
const actualX = margin + (cellSize * x);
const actualY = margin + (cellSize * y);
ctx.beginPath();
ctx.fillRect(actualX, actualY, cellSize, cellSize);
}
}
}

View File

@ -7,6 +7,8 @@ share: "共有する"
note: "ノート" note: "ノート"
other: "その他" other: "その他"
add: "追加" add: "追加"
generate: "生成"
download: "ダウンロード"
browse: "参照" browse: "参照"
settings: "設定" settings: "設定"
goToLegacyHub: "従来のMisskey Hub" goToLegacyHub: "従来のMisskey Hub"
@ -336,6 +338,12 @@ _customEmojiPreview:
_placeholder: _placeholder:
noteText: "カスタム絵文字はこんな感じで表示されます→ :emoji_preview_1:\n文章を書き換えて、使い勝手を試してみてくださいね✨" noteText: "カスタム絵文字はこんな感じで表示されます→ :emoji_preview_1:\n文章を書き換えて、使い勝手を試してみてくださいね✨"
_identiconGenerator:
title: "初期アイコンジェネレーター"
description: "Misskeyに登録した際にデフォルトで指定される初期アイコンを生成できます。"
userName: "ユーザー名"
includeDomain: "サーバーのドメイン名を含む完全なユーザー名を指定してください。"
_api: _api:
_permissions: _permissions:
title: "権限" title: "権限"

View File

@ -17,6 +17,7 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^20.11.28", "@types/node": "^20.11.28",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/random-seed": "^0.3.5",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.5", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.5",
"autoprefixer": "^10.4.18", "autoprefixer": "^10.4.18",
@ -29,6 +30,7 @@
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"nuxt": "^3.11.0", "nuxt": "^3.11.0",
"postcss": "^8.4.36", "postcss": "^8.4.36",
"random-seed": "^0.3.0",
"sass": "^1.72.0", "sass": "^1.72.0",
"schema-dts": "^1.1.2", "schema-dts": "^1.1.2",
"sitemap": "^7.1.1", "sitemap": "^7.1.1",

View File

@ -0,0 +1,87 @@
<template>
<div class='container mx-auto max-w-screen-xl px-6 py-6'>
<h1 class='text-2xl lg:text-3xl font-bold mb-4'>
{{ $t('_identiconGenerator.title') }}
</h1>
<div class="mx-auto max-w-lg">
<label class="mb-1" for="acct">{{ $t('_identiconGenerator.userName') }}</label>
<input class="form-control" id="acct" v-model="acct" placeholder="@ai@misskey.example.com" />
<div class="form-text">{{ $t('_identiconGenerator.includeDomain') }}</div>
<div class="mt-2 mb-4 text-center">
<button class="btn btn-primary" @click="genIdenticon()">{{ $t('generate') }}</button>
</div>
<div class="text-center">
<CaretDownFillIco class="w-12 h-12 mx-auto" />
</div>
<div class="mb-2 p-4 rounded-lg border bg-white dark:bg-slate-950 border-slate-300 dark:border-slate-800">
<canvas ref="canvas" width="128" height="128" class="w-full max-w-40 h-auto mx-auto mb-4 rounded-full bg-slate-200 dark:bg-slate-700" />
<div class="text-center">
<button class="btn btn-primary" @click="download()" :disabled="!onceGenerated">{{ $t('download') }}</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang='ts'>
import { genIdenticon as _genIdenticon } from '@/assets/js/mi/gen-identicon';
import CaretDownFillIco from 'bi/caret-down-fill.svg';
definePageMeta({
layout: 'tools',
});
const { t } = useI18n();
const route = useRoute();
const onceGenerated = ref(false);
const acct = ref('@ai@misskey.example.com');
const normalizedAcct = computed(() => {
const normalized = acct.value.replace(/^@/, '');
if (normalized.includes('@')) {
return normalized;
} else {
return null;
}
});
const canvas = ref<HTMLCanvasElement | null>(null);
function genIdenticon() {
if (!process.client || !canvas.value) return;
if (!normalizedAcct.value) {
onceGenerated.value = false;
const ctx = canvas.value.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
}
return;
}
_genIdenticon(normalizedAcct.value, canvas.value);
onceGenerated.value = true;
}
function download() {
if (!process.client || !normalizedAcct.value || !canvas.value) return;
const url = canvas.value.toDataURL('image/png');
const a = document.createElement('a');
a.href = url;
a.download = 'identicon.png';
a.click();
a.remove();
}
route.meta.title = t('_aidConverter.title');
route.meta.description = t('_aidConverter.description');
</script>
<style module>
.mfmRoot {
@apply rounded-lg p-6 border break-words overflow-hidden;
font-family: Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;
line-height: 1.35;
}
.mfmRoot img {
display: inline;
}
</style>

View File

@ -34,6 +34,9 @@ devDependencies:
'@types/nprogress': '@types/nprogress':
specifier: ^0.2.3 specifier: ^0.2.3
version: 0.2.3 version: 0.2.3
'@types/random-seed':
specifier: ^0.3.5
version: 0.3.5
'@types/ua-parser-js': '@types/ua-parser-js':
specifier: ^0.7.39 specifier: ^0.7.39
version: 0.7.39 version: 0.7.39
@ -70,6 +73,9 @@ devDependencies:
postcss: postcss:
specifier: ^8.4.36 specifier: ^8.4.36
version: 8.4.36 version: 8.4.36
random-seed:
specifier: ^0.3.0
version: 0.3.0
sass: sass:
specifier: ^1.72.0 specifier: ^1.72.0
version: 1.72.0 version: 1.72.0
@ -2294,6 +2300,10 @@ packages:
resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
dev: true dev: true
/@types/random-seed@0.3.5:
resolution: {integrity: sha512-CftxcDPAHgs0SLHU2dt+ZlDPJfGqLW3sZlC/ATr5vJDSe5tRLeOne7HMvCOJnFyF8e1U41wqzs3h6AMC613xtA==}
dev: true
/@types/resolve@1.20.2: /@types/resolve@1.20.2:
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
dev: true dev: true
@ -4849,6 +4859,10 @@ packages:
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
dev: true dev: true
/json-stringify-safe@5.0.1:
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
dev: true
/json5@2.2.3: /json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -6854,6 +6868,13 @@ packages:
resolution: {integrity: sha512-yUUd5VTiFtcMEx0qFUxGAv5gbMc1un4RvEO1JZdP7ZUl/RHygZK6PknIKntmQRZxnMY3ZXD2ISaw1ij8GYW1yg==} resolution: {integrity: sha512-yUUd5VTiFtcMEx0qFUxGAv5gbMc1un4RvEO1JZdP7ZUl/RHygZK6PknIKntmQRZxnMY3ZXD2ISaw1ij8GYW1yg==}
dev: true dev: true
/random-seed@0.3.0:
resolution: {integrity: sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==}
engines: {node: '>= 0.6.0'}
dependencies:
json-stringify-safe: 5.0.1
dev: true
/randombytes@2.1.0: /randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
dependencies: dependencies: