mirror of
https://iceshrimp.dev/Crimekillz/jointrashposs.git
synced 2024-11-22 00:43:50 +01:00
(add) avatar decoration preview
This commit is contained in:
parent
8aae90fdbb
commit
3178fad065
@ -9,6 +9,11 @@ export default <NavSection[]>[
|
|||||||
description: "_mfmPlayground.description",
|
description: "_mfmPlayground.description",
|
||||||
to: "/tools/mfm-playground/",
|
to: "/tools/mfm-playground/",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
i18n: "_avatarDecorationPreview.title",
|
||||||
|
description: "_avatarDecorationPreview.description",
|
||||||
|
to: "/tools/avatar-decoration-preview/",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
i18n: "_aidConverter.title",
|
i18n: "_aidConverter.title",
|
||||||
description: "_aidConverter.description",
|
description: "_aidConverter.description",
|
||||||
|
67
components/tools/avatar-decoration/MkNote.vue
Normal file
67
components/tools/avatar-decoration/MkNote.vue
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-xl h-auto w-full px-4 py-3.5 md:px-8 md:py-7 flex max-w-sm border border-slate-300 dark:border-slate-950">
|
||||||
|
<div class="mr-3.5">
|
||||||
|
<div class="w-[46px] h-[46px] md:w-[58px] md:h-[58px] relative rounded-full bg-slate-50 dark:bg-slate-900">
|
||||||
|
<img class="w-full h-full object-cover rounded-full" :src="avatar" />
|
||||||
|
<img
|
||||||
|
v-for="decoration in decorations"
|
||||||
|
:src="decoration.url"
|
||||||
|
class="absolute block min-w-[200%] w-[200%] -top-1/2 -left-1/2 select-none pointer-events-none"
|
||||||
|
:style="getStyle(decoration)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<div class="font-bold">{{ $t('_avatarDecorationPreview._placeholder.username') }}</div>
|
||||||
|
<div class="opacity-70">@ai</div>
|
||||||
|
</div>
|
||||||
|
<div>{{ $t('_avatarDecorationPreview._placeholder.noteText') }}</div>
|
||||||
|
<div class="flex space-x-3.5 md:space-x-7 opacity-60">
|
||||||
|
<div class="p-2"><ReplyIco class="w-3 h-3 md:h-4 md:w-4 block stroke-1 stroke-current reply" /></div>
|
||||||
|
<div class="p-2"><RenoteIco class="w-3 h-3 md:h-4 md:w-4 block stroke-1 stroke-current" /></div>
|
||||||
|
<div class="p-2"><ReactionIco class="w-3 h-3 md:h-4 md:w-4 block stroke-1 stroke-current" /></div>
|
||||||
|
<div class="p-2"><MoreIco class="w-3 h-3 md:h-4 md:w-4 block stroke-1 stroke-current" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type * as Misskey from 'misskey-js';
|
||||||
|
import ReplyIco from 'bi/arrow-return-left.svg';
|
||||||
|
import RenoteIco from 'bi/repeat.svg';
|
||||||
|
import ReactionIco from 'bi/plus-lg.svg';
|
||||||
|
import MoreIco from 'bi/three-dots.svg';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
avatar: string;
|
||||||
|
decorations?: (Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; })[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function getStyle(decoration: Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; }): HTMLAttributes['style'] {
|
||||||
|
const angle = decoration.angle ?? 0;
|
||||||
|
const rotate = angle === 0 ? undefined : `${angle * 360}deg`;
|
||||||
|
const scaleX = decoration.flipH ? -1 : 1;
|
||||||
|
const scale = scaleX === 1 ? undefined : `${scaleX} 1`;
|
||||||
|
const offsetX = decoration.offsetX ?? 0;
|
||||||
|
const offsetY = decoration.offsetY ?? 0;
|
||||||
|
const translate = (offsetX === 0 && offsetY === 0) ? undefined : `${offsetX * 100}% ${offsetY * 100}%`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
rotate,
|
||||||
|
scale,
|
||||||
|
translate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.reply {
|
||||||
|
transform: rotateX(180deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
|
||||||
|
-->
|
50
components/tools/avatar-decoration/MkProf.vue
Normal file
50
components/tools/avatar-decoration/MkProf.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-xl h-auto w-72 border border-slate-300 dark:border-slate-950 bg-slate-200 dark:bg-slate-700">
|
||||||
|
<div class="mt-[84px] bg-white dark:bg-slate-950 rounded-b-xl divide-y divide-slate-200 dark:divide-slate-900">
|
||||||
|
<div class="flex items-center space-x-2 px-4 py-2">
|
||||||
|
<div class="w-[66px] h-[66px] relative -mt-8 rounded-full border-4 border-white dark:border-slate-950 bg-slate-50 dark:bg-slate-900">
|
||||||
|
<img class="w-full h-full object-cover rounded-full" :src="avatar" />
|
||||||
|
<img
|
||||||
|
v-for="decoration in decorations"
|
||||||
|
:src="decoration.url"
|
||||||
|
class="absolute block min-w-[200%] w-[200%] -top-1/2 -left-1/2 select-none pointer-events-none"
|
||||||
|
:style="getStyle(decoration)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-bold text-sm">{{ $t('_avatarDecorationPreview._placeholder.username') }}</div>
|
||||||
|
<div class="opacity-70 text-xs">@ai</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 text-xs">{{ $t('_avatarDecorationPreview._placeholder.profileDescription') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
avatar: string;
|
||||||
|
decorations?: (Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; })[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function getStyle(decoration: Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; }): HTMLAttributes['style'] {
|
||||||
|
const angle = decoration.angle ?? 0;
|
||||||
|
const rotate = angle === 0 ? undefined : `${angle * 360}deg`;
|
||||||
|
const scaleX = decoration.flipH ? -1 : 1;
|
||||||
|
const scale = scaleX === 1 ? undefined : `${scaleX} 1`;
|
||||||
|
const offsetX = decoration.offsetX ?? 0;
|
||||||
|
const offsetY = decoration.offsetY ?? 0;
|
||||||
|
const translate = (offsetX === 0 && offsetY === 0) ? undefined : `${offsetX * 100}% ${offsetY * 100}%`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
rotate,
|
||||||
|
scale,
|
||||||
|
translate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
@ -6,6 +6,7 @@ copy: "コピー"
|
|||||||
share: "共有する"
|
share: "共有する"
|
||||||
note: "ノート"
|
note: "ノート"
|
||||||
other: "その他"
|
other: "その他"
|
||||||
|
add: "追加"
|
||||||
goToLegacyHub: "従来のMisskey Hub"
|
goToLegacyHub: "従来のMisskey Hub"
|
||||||
|
|
||||||
_error:
|
_error:
|
||||||
@ -295,6 +296,24 @@ _goToMisskey:
|
|||||||
title: "Misskey Webに移動"
|
title: "Misskey Webに移動"
|
||||||
heading: "このページを開きたいサーバーを選択してください"
|
heading: "このページを開きたいサーバーを選択してください"
|
||||||
|
|
||||||
|
_avatarDecorationPreview:
|
||||||
|
title: "アバターデコレーション プレビュー"
|
||||||
|
description: "アバターデコレーションがきれいに反映できるかをチェックできます。"
|
||||||
|
preview: "プレビュー"
|
||||||
|
settings: "設定"
|
||||||
|
decoration: "デコ #{number}"
|
||||||
|
placeholder: "「追加」から、アバターデコレーションを追加してプレビューできます。"
|
||||||
|
_options:
|
||||||
|
offsetX: "横位置調整"
|
||||||
|
offsetY: "縦位置調整"
|
||||||
|
angle: "角度"
|
||||||
|
flip: "左右反転"
|
||||||
|
overlayTemplate: "テンプレートを重ねる"
|
||||||
|
_placeholder:
|
||||||
|
username: "藍"
|
||||||
|
noteText: "チョコのかかったドーナツを食べました🍩😋"
|
||||||
|
profileDescription: "Misskey常駐AIの藍です!よろしくお願いします♪"
|
||||||
|
|
||||||
_api:
|
_api:
|
||||||
_permissions:
|
_permissions:
|
||||||
title: "権限"
|
title: "権限"
|
||||||
|
199
pages/tools/avatar-decoration-preview.vue
Normal file
199
pages/tools/avatar-decoration-preview.vue
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<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('_avatarDecorationPreview.title') }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div class="p-6 space-y-4 rounded-lg bg-white dark:bg-slate-950">
|
||||||
|
<header class="-mt-6 -mx-6 px-6 py-3 font-bold text-lg border-b border-slate-300 dark:border-slate-800">
|
||||||
|
{{ $t('_avatarDecorationPreview.preview') }}
|
||||||
|
</header>
|
||||||
|
<div class="flex gap-8 items-center justify-center flex-wrap">
|
||||||
|
<ToolsAvatarDecorationMkProf
|
||||||
|
:avatar="avatar"
|
||||||
|
:decorations="realDecorations"
|
||||||
|
/>
|
||||||
|
<ToolsAvatarDecorationMkNote
|
||||||
|
:avatar="avatar"
|
||||||
|
:decorations="realDecorations"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 rounded-lg bg-white dark:bg-slate-950">
|
||||||
|
<header class="-mt-6 -mx-6 px-6 py-3 border-b border-slate-300 dark:border-slate-800 flex items-center">
|
||||||
|
<h2 class="font-bold text-lg">{{ $t('_avatarDecorationPreview.settings') }}</h2>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<button class="btn btn-primary" @click="addDecoration"><PlusIco class="mr-1" />{{ $t('add') }}</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<TransitionGroup v-if="decorations.length > 0" name="decorationList" tag="div">
|
||||||
|
<div v-for="decoration, index in decorations" :key="decoration.id" class="mt-8 p-4 rounded-lg bg-white dark:bg-slate-950 border border-slate-300 dark:border-slate-800">
|
||||||
|
<h3 class="-mt-7 font-bold flex items-center w-fit px-3 bg-white dark:bg-slate-950 mb-4">
|
||||||
|
<div class="btn-group mr-2" role="group" aria-label="Basic outlined example">
|
||||||
|
<button type="button" @click="downDecorationOrder(index, decorations)" :disabled="(index + 1) === decorations.length" class="btn btn-sm btn-outline-primary"><ChevronDownIco class="h-3.5 w-3.5 stroke-1 stroke-current" /></button>
|
||||||
|
<button type="button" @click="upDecorationOrder(index, decorations)" :disabled="index === 0" class="btn btn-sm btn-outline-primary"><ChevronUpIco class="h-3.5 w-3.5 stroke-1 stroke-current" /></button>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger mr-2" @click="deleteDecoration(index)"><XIco class="h-3.5 w-3.5 stroke-1 stroke-current" /></button>
|
||||||
|
<div>{{ $t('_avatarDecorationPreview.decoration', { number: (index + 1) }) }}</div>
|
||||||
|
</h3>
|
||||||
|
<div class="md:flex items-center justify-center space-y-4 md:space-y-0">
|
||||||
|
<div class="w-full md:w-auto md:ml-8 md:order-2 border-slate-300 dark:border-slate-800 p-10 md:p-16 rounded-lg">
|
||||||
|
<div class="w-24 h-24 mx-auto relative rounded-full bg-slate-50 dark:bg-slate-900">
|
||||||
|
<img class="w-full h-full object-cover rounded-full" :src="avatar" />
|
||||||
|
<img
|
||||||
|
:src="decoration.url"
|
||||||
|
class="absolute block min-w-[200%] w-[200%] -top-1/2 -left-1/2 select-none pointer-events-none"
|
||||||
|
:style="getStyle(decoration)"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="/img/misc/avatar-decoration-template.png"
|
||||||
|
v-if="decoration.overlayTemplate"
|
||||||
|
class="absolute block min-w-[200%] w-[200%] -top-1/2 -left-1/2 select-none pointer-events-none mix-blend-multiply opacity-70"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="md:order-1 w-full mx-auto md:mx-0 max-w-md space-y-4">
|
||||||
|
<div class="md:flex items-center md:space-x-4 space-y-2 md:space-y-0">
|
||||||
|
<div class="flex-shrink-0 text-sm w-20">{{ $t('_avatarDecorationPreview._options.offsetY') }}</div>
|
||||||
|
<div class="flex-grow flex items-center space-x-2 md:space-x-4">
|
||||||
|
<input class="flex-grow form-range" type="range" min="-0.25" max="0.25" step="0.025" v-model="decoration.offsetY" />
|
||||||
|
<div class="flex-shrink-0 text-sm text-end -ml-4 w-14">{{ Math.floor((decoration.offsetY ?? 0) * 100) }} %</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="md:flex items-center md:space-x-4 space-y-2 md:space-y-0">
|
||||||
|
<div class="flex-shrink-0 text-sm w-20">{{ $t('_avatarDecorationPreview._options.offsetX') }}</div>
|
||||||
|
<div class="flex-grow flex items-center space-x-2 md:space-x-4">
|
||||||
|
<input class="flex-grow form-range" type="range" min="-0.25" max="0.25" step="0.025" v-model="decoration.offsetX" />
|
||||||
|
<div class="flex-shrink-0 text-sm text-end -ml-4 w-14">{{ Math.floor((decoration.offsetX ?? 0) * 100) }} %</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="md:flex items-center md:space-x-4 space-y-2 md:space-y-0">
|
||||||
|
<div class="flex-shrink-0 text-sm w-20">{{ $t('_avatarDecorationPreview._options.angle') }}</div>
|
||||||
|
<div class="flex-grow flex items-center space-x-2 md:space-x-4">
|
||||||
|
<input class="flex-grow form-range" type="range" min="-0.5" max="0.5" step="0.025" v-model="decoration.angle" />
|
||||||
|
<div class="flex-shrink-0 text-sm text-end -ml-4 w-14">{{ Math.floor((decoration.angle ?? 0) * 360) }} °</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="flex-shrink-0 text-sm w-20 hidden md:block"></div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<div class="pb-1.5 w-full form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" :id="`flipHSwitch_${index}`" v-model="decoration.flipH">
|
||||||
|
<label class="form-check-label" :for="`flipHSwitch_${index}`">{{ $t('_avatarDecorationPreview._options.flip') }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="pt-1.5 w-full border-t border-slate-300 dark:border-slate-800 form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" :id="`overlayTemplate_${index}`" v-model="decoration.overlayTemplate">
|
||||||
|
<label class="form-check-label" :for="`overlayTemplate_${index}`">{{ $t('_avatarDecorationPreview._options.overlayTemplate') }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
<div v-else class="p-6 text-center">
|
||||||
|
<h2 class="font-bold text-lg">{{ $t('_avatarDecorationPreview.placeholder') }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang='ts'>
|
||||||
|
import type * as Misskey from 'misskey-js';
|
||||||
|
import PlusIco from 'bi/plus-lg.svg';
|
||||||
|
import ChevronUpIco from 'bi/chevron-up.svg';
|
||||||
|
import ChevronDownIco from 'bi/chevron-down.svg';
|
||||||
|
import XIco from 'bi/x-lg.svg';
|
||||||
|
import type { HTMLAttributes } from 'nuxt/dist/app/compat/capi';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'tools',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const avatar = ref('/img/docs/fukidashi/doya_ai.webp');
|
||||||
|
const decorations = ref<(Misskey.entities.User['avatarDecorations'][number] & { offsetX?: number; offsetY?: number; overlayTemplate?: boolean; })[]>([
|
||||||
|
]);
|
||||||
|
|
||||||
|
function getStyle(decoration: Omit<Misskey.entities.User['avatarDecorations'][number], 'id'> & { offsetX?: number; offsetY?: number; }): HTMLAttributes['style'] {
|
||||||
|
const angle = decoration.angle ?? 0;
|
||||||
|
const rotate = angle === 0 ? undefined : `${angle * 360}deg`;
|
||||||
|
const scaleX = decoration.flipH ? -1 : 1;
|
||||||
|
const scale = scaleX === 1 ? undefined : `${scaleX} 1`;
|
||||||
|
const offsetX = decoration.offsetX ?? 0;
|
||||||
|
const offsetY = decoration.offsetY ?? 0;
|
||||||
|
const translate = (offsetX === 0 && offsetY === 0) ? undefined : `${offsetX * 100}% ${offsetY * 100}%`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
rotate,
|
||||||
|
scale,
|
||||||
|
translate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteDecoration(index: number) {
|
||||||
|
decorations.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function upDecorationOrder(index: number, array: any[]) {
|
||||||
|
decorations.value.splice(index - 1, 2, array[index], array[index - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downDecorationOrder(index: number, array: any[]) {
|
||||||
|
decorations.value.splice(index, 2, array[index + 1], array[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const realDecorations = computed(() => {
|
||||||
|
const g = [...decorations.value];
|
||||||
|
return g.reverse();
|
||||||
|
});
|
||||||
|
|
||||||
|
function addDecoration() {
|
||||||
|
if (!process.client) return;
|
||||||
|
|
||||||
|
function createImage(file: Blob, callback: () => void) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('load', (e) => {
|
||||||
|
if (!e.target?.result) return;
|
||||||
|
|
||||||
|
decorations.value.unshift({
|
||||||
|
id: decorations.value.length.toString(),
|
||||||
|
url: e.target.result as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = document.createElement('input');
|
||||||
|
el.type = 'file';
|
||||||
|
el.accept = 'image/*';
|
||||||
|
el.addEventListener('change', (ev: { target: any; }) => {
|
||||||
|
const target = ev.target;
|
||||||
|
if (target && target.files) {
|
||||||
|
createImage(target.files[0], () => {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
el.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
route.meta.title = t('_avatarDecorationPreview.title');
|
||||||
|
route.meta.description = t('_avatarDecorationPreview.description');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.decorationList-move, /* 移動する要素にトランジションを適用 */
|
||||||
|
.decorationList-enter-active,
|
||||||
|
.decorationList-leave-active {
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user