update
This commit is contained in:
parent
d4b17a16e8
commit
f0d93755e9
10 changed files with 2720 additions and 150 deletions
|
|
@ -6,9 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<MkWindow
|
||||
ref="windowEl"
|
||||
:initialWidth="400"
|
||||
:initialHeight="500"
|
||||
:canResize="false"
|
||||
:initialWidth="600"
|
||||
:initialHeight="600"
|
||||
:canResize="true"
|
||||
@close="windowEl.close()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
|
|
@ -18,63 +18,75 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<div class="_gaps_m">
|
||||
<div v-if="imgUrl != null" :class="$style.imgs">
|
||||
<div style="background: #000;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
<div style="background: #222;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
<div style="background: #ddd;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
<div style="background: #fff;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton>
|
||||
<MkInput v-model="name" pattern="[a-z0-9_]" autocapitalize="off">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
<template #caption>{{ i18n.ts.emojiNameValidation }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="category" :datalist="customEmojiCategories">
|
||||
<template #label>{{ i18n.ts.category }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="aliases" autocapitalize="off">
|
||||
<template #label>{{ i18n.ts.tags }}</template>
|
||||
<template #caption>
|
||||
{{ i18n.ts.theKeywordWhenSearchingForCustomEmoji }}<br/>
|
||||
{{ i18n.ts.setMultipleBySeparatingWithSpace }}
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="license" :mfmAutocomplete="true">
|
||||
<template #label>{{ i18n.ts.license }}</template>
|
||||
</MkInput>
|
||||
<MkFolder v-if="!isRequest">
|
||||
<template #label>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction }}</template>
|
||||
<template #suffix>{{ rolesThatCanBeUsedThisEmojiAsReaction.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisEmojiAsReaction.length }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
<div v-for="role in rolesThatCanBeUsedThisEmojiAsReaction" :key="role.id" :class="$style.roleItem">
|
||||
<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
|
||||
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
|
||||
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
|
||||
<div class="_gaps_m" style="display: flex; flex-direction: row">
|
||||
<div>
|
||||
<div v-if="imgUrl != null" :class="$style.imgs">
|
||||
<div style="background: #000;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
<div style="background: #222;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
<div style="background: #ddd;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
<div style="background: #fff;" :class="$style.imgContainer">
|
||||
<img :src="imgUrl" :class="$style.img"/>
|
||||
</div>
|
||||
|
||||
<MkInfo>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }}</MkInfo>
|
||||
<MkInfo warn>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}</MkInfo>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<MkSwitch v-model="isSensitive">{{ i18n.ts.isSensitive }}</MkSwitch>
|
||||
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
|
||||
<MkSwitch v-model="isNotifyIsHome">
|
||||
{{ i18n.ts.isNotifyIsHome }}
|
||||
</MkSwitch>
|
||||
<MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton>
|
||||
<MkInput v-model="name" pattern="[a-z0-9_]" autocapitalize="off">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
<template #caption>{{ i18n.ts.emojiNameValidation }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="category" :datalist="customEmojiCategories">
|
||||
<template #label>{{ i18n.ts.category }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="aliases" autocapitalize="off">
|
||||
<template #label>{{ i18n.ts.tags }}</template>
|
||||
<template #caption>
|
||||
{{ i18n.ts.theKeywordWhenSearchingForCustomEmoji }}<br/>
|
||||
{{ i18n.ts.setMultipleBySeparatingWithSpace }}
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="license" :mfmAutocomplete="true">
|
||||
<template #label>{{ i18n.ts.license }}</template>
|
||||
</MkInput>
|
||||
<MkFolder v-if="!isRequest">
|
||||
<template #label>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction }}</template>
|
||||
<template #suffix>{{ rolesThatCanBeUsedThisEmojiAsReaction.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisEmojiAsReaction.length }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
<div v-for="role in rolesThatCanBeUsedThisEmojiAsReaction" :key="role.id" :class="$style.roleItem">
|
||||
<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
|
||||
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
|
||||
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
|
||||
</div>
|
||||
|
||||
<MkInfo>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }}</MkInfo>
|
||||
<MkInfo warn>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}</MkInfo>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<MkSwitch v-model="isSensitive">{{ i18n.ts.isSensitive }}</MkSwitch>
|
||||
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
|
||||
<MkSwitch v-model="isNotifyIsHome">
|
||||
{{ i18n.ts.isNotifyIsHome }}
|
||||
</MkSwitch>
|
||||
</div>
|
||||
<div v-if="imgUrl">
|
||||
<MkInput v-model="text">
|
||||
<template #label>テスト文章</template>
|
||||
</MkInput><br/>
|
||||
<MkNoteSimple :note="{isHidden:false,replyId:null,renoteId:null,files:[],user: $i,text:text,cw:null, emojis: {[name]: imgUrl}}"/>
|
||||
<p v-if="speed ">基準より眩しい可能性があります。</p>
|
||||
<p v-if="!speed">問題は見つかりませんでした。</p>
|
||||
<p>※上記の物は問題がないことを保証するものではありません。</p>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
||||
<div :class="$style.footer">
|
||||
<div :class="$style.footerButtons">
|
||||
<MkButton v-if="!isRequest" danger rounded style="margin: 0 auto;" @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
|
|
@ -87,9 +99,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { DriveFile } from 'misskey-js/built/entities.js';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
|
@ -102,12 +113,14 @@ import { customEmojiCategories } from '@/custom-emojis.js';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { selectFile, selectFiles } from '@/scripts/select-file.js';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
|
||||
import { $i } from '@/account.js';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
const props = defineProps<{
|
||||
emoji?: any,
|
||||
isRequest: boolean,
|
||||
}>();
|
||||
|
||||
const text = ref<string>('テスト文章');
|
||||
const speed = ref<boolean>(false);
|
||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
||||
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
||||
const category = ref<string>(props.emoji ? props.emoji.category : '');
|
||||
|
|
@ -132,6 +145,12 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
const colorChanges = ref<number | null>(null);
|
||||
|
||||
watch(colorChanges, (value) => {
|
||||
console.log(value);
|
||||
});
|
||||
|
||||
async function changeImage(ev) {
|
||||
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
|
||||
const candidate = file.value.name.replace(/\.(.+)$/, '');
|
||||
|
|
@ -222,6 +241,12 @@ async function del() {
|
|||
windowEl.value.close();
|
||||
});
|
||||
}
|
||||
|
||||
watch(imgUrl, async (value) => {
|
||||
speed.value = await misskeyApi('emoji/speedtest', {
|
||||
url: value,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
@ -243,6 +268,12 @@ async function del() {
|
|||
width: 64px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.preview {
|
||||
display: block;
|
||||
height: 16px;
|
||||
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.roleItem {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
|||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import {misskeyApi} from "@/scripts/misskey-api.js";
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
const isDeleted = ref(false);
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note & {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
|
||||
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { VNode, h, SetupContext, provide } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { ID, Instance } from 'misskey-js/built/entities.js';
|
||||
import MkUrl from '@/components/global/MkUrl.vue';
|
||||
import MkTime from '@/components/global/MkTime.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
|
|
@ -23,7 +24,6 @@ import { defaultStore } from '@/store';
|
|||
import { mixEmoji } from '@/scripts/emojiKitchen/emojiMixer';
|
||||
import { nyaize as doNyaize } from '@/scripts/nyaize.js';
|
||||
import { uhoize as doUhoize } from '@/scripts/uhoize.js';
|
||||
import {ID, Instance} from "misskey-js/built/entities.js";
|
||||
import { safeParseFloat } from '@/scripts/safe-parse.js';
|
||||
|
||||
const QUOTE_STYLE = `
|
||||
|
|
@ -73,7 +73,7 @@ type MfmProps = {
|
|||
emojiUrls?: Record<string, string>;
|
||||
rootScale?: number;
|
||||
nyaize?: boolean | 'respect';
|
||||
uhoize: boolean | 'respect';
|
||||
uhoize?: boolean | 'respect';
|
||||
parsedNodes?: mfm.MfmNode[] | null;
|
||||
enableEmojiMenu?: boolean;
|
||||
enableEmojiMenuReaction?: boolean;
|
||||
|
|
@ -246,7 +246,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
const radius = parseFloat(token.props.args.rad ?? '6');
|
||||
return h('span', {
|
||||
class: '_mfm_blur_',
|
||||
style: `--blur-px: ${radius}px;`
|
||||
style: `--blur-px: ${radius}px;`,
|
||||
}, genEl(token.children, scale));
|
||||
}
|
||||
case 'rainbow': {
|
||||
|
|
@ -363,7 +363,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
key: Math.random(),
|
||||
name: emoji1 + emoji2,
|
||||
normal: props.plain,
|
||||
url: mixedEmojiUrl
|
||||
url: mixedEmojiUrl,
|
||||
});
|
||||
}
|
||||
case 'unixtime': {
|
||||
|
|
@ -474,7 +474,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
|
||||
case 'emojiCode': {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (props.author?.host == null) {
|
||||
if (props.author?.host == null && !props.emojiUrls) {
|
||||
return [h(MkCustomEmoji, {
|
||||
key: Math.random(),
|
||||
name: token.props.name,
|
||||
|
|
@ -487,6 +487,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
})];
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
console.log(props.emojiUrls, props.emojiUrls[token.props.name], token.props.name);
|
||||
if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) {
|
||||
return [h('span', `:${token.props.name}:`)];
|
||||
} else {
|
||||
|
|
@ -495,7 +496,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
name: token.props.name,
|
||||
url: props.emojiUrls && props.emojiUrls[token.props.name],
|
||||
normal: props.plain,
|
||||
host: props.author.host,
|
||||
host: props.author.host ? props.author.host : null,
|
||||
useOriginalSize: scale >= 2.5,
|
||||
})];
|
||||
}
|
||||
|
|
|
|||
135
packages/frontend/src/scripts/emojiColorChangeSecond.ts
Normal file
135
packages/frontend/src/scripts/emojiColorChangeSecond.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
const imageUrl = ref<string>('path_to_your_image.gif'); // または '.apng'
|
||||
|
||||
// 1秒あたりの色変化数を保持するリアクティブ変数
|
||||
const colorChangesPerSecond = ref<number | null>(null);
|
||||
|
||||
// コンポーネントがマウントされたときに画像を取得して解析する
|
||||
onMounted(() => {
|
||||
fetchImage(imageUrl.value);
|
||||
});
|
||||
|
||||
// 画像を取得する関数
|
||||
async function fetchImage(url: string) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
|
||||
if (url.endsWith('.gif')) {
|
||||
analyzeGif(bytes);
|
||||
} else if (url.endsWith('.apng')) {
|
||||
analyzeApng(bytes);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching the image:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// GIFの解析関数
|
||||
function analyzeGif(bytes: Uint8Array) {
|
||||
const frames = extractGifFrames(bytes);
|
||||
calculateColorChanges(frames);
|
||||
}
|
||||
|
||||
// APNGの解析関数
|
||||
function analyzeApng(bytes: Uint8Array) {
|
||||
const frames = extractApngFrames(bytes);
|
||||
calculateColorChanges(frames);
|
||||
}
|
||||
|
||||
// GIFフレーム抽出関数
|
||||
function extractGifFrames(bytes: Uint8Array) {
|
||||
const frames = [];
|
||||
let i = 0;
|
||||
while (i < bytes.length) {
|
||||
// GIFのヘッダーとロジカルスクリーンディスクリプタをスキップ
|
||||
if (i === 0) i += 13;
|
||||
// グローバルカラーテーブルのスキップ
|
||||
if (i === 13) i += (bytes[10] & 0x80 ? 3 * (2 ** ((bytes[10] & 0x07) + 1)) : 0);
|
||||
|
||||
// イメージディスクリプタを探す
|
||||
if (bytes[i] === 0x2C) {
|
||||
const imageLeft = bytes[i + 1] + (bytes[i + 2] << 8);
|
||||
const imageTop = bytes[i + 3] + (bytes[i + 4] << 8);
|
||||
const imageWidth = bytes[i + 5] + (bytes[i + 6] << 8);
|
||||
const imageHeight = bytes[i + 7] + (bytes[i + 8] << 8);
|
||||
const localColorTableFlag = bytes[i + 9] & 0x80;
|
||||
const localColorTableSize = 2 ** ((bytes[i + 9] & 0x07) + 1);
|
||||
|
||||
i += 10;
|
||||
|
||||
if (localColorTableFlag) {
|
||||
i += 3 * localColorTableSize;
|
||||
}
|
||||
|
||||
while (bytes[i] !== 0x00) {
|
||||
const blockSize = bytes[i];
|
||||
i += blockSize + 1;
|
||||
}
|
||||
|
||||
i++;
|
||||
const frame = extractPixelColor(bytes, imageLeft, imageTop, imageWidth, imageHeight);
|
||||
frames.push(frame);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
// APNGフレーム抽出関数
|
||||
function extractApngFrames(bytes: Uint8Array) {
|
||||
const frames = [];
|
||||
let i = 8; // PNGシグネチャをスキップ
|
||||
|
||||
while (i < bytes.length) {
|
||||
const length = (bytes[i] << 24) + (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3];
|
||||
const type = String.fromCharCode(bytes[i + 4], bytes[i + 5], bytes[i + 6], bytes[i + 7]);
|
||||
|
||||
if (type === 'IDAT' || type === 'fdAT') {
|
||||
const imageLeft = 0;
|
||||
const imageTop = 0;
|
||||
const imageWidth = 0;
|
||||
const imageHeight = 0;
|
||||
|
||||
const frame = extractPixelColor(bytes, imageLeft, imageTop, imageWidth, imageHeight);
|
||||
frames.push(frame);
|
||||
}
|
||||
|
||||
i += length + 12;
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
// 中央ピクセルの色を抽出する関数
|
||||
function extractPixelColor(bytes: Uint8Array, left: number, top: number, width: number, height: number) {
|
||||
const centerX = left + Math.floor(width / 2);
|
||||
const centerY = top + Math.floor(height / 2);
|
||||
const index = (centerY * width + centerX) * 4;
|
||||
|
||||
return {
|
||||
r: bytes[index],
|
||||
g: bytes[index + 1],
|
||||
b: bytes[index + 2],
|
||||
};
|
||||
}
|
||||
|
||||
// 色の変化を計算する関数
|
||||
function calculateColorChanges(frames: { r: number; g: number; b: number }[]) {
|
||||
let colorChangeCount = 0;
|
||||
for (let i = 1; i < frames.length; i++) {
|
||||
const prevFrame = frames[i - 1];
|
||||
const currFrame = frames[i];
|
||||
if (prevFrame.r !== currFrame.r || prevFrame.g !== currFrame.g || prevFrame.b !== currFrame.b) {
|
||||
colorChangeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 仮にFPSが30と仮定し、1秒あたりの色変化回数を計算
|
||||
const fps = 30;
|
||||
const durationInSeconds = frames.length / fps;
|
||||
colorChangesPerSecond.value = colorChangeCount / durationInSeconds;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue