emojiKitchenベースのmix関数を追加

This commit is contained in:
Fairy-Phy 2023-09-16 22:16:47 +09:00 committed by mattyatea
parent dd56446677
commit 61dad2ce35
5 changed files with 37278 additions and 21 deletions

View File

@ -0,0 +1,46 @@
<template>
<span v-if="errored">{{ alt }}</span>
<img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
const props = defineProps<{
name: string;
normal?: boolean;
url: string;
}>();
const rawUrl = computed(() => props.url);
const url = computed(() => rawUrl.value);
const alt = computed(() => props.name);
let errored = $ref(url.value == null);
</script>
<style lang="scss" module>
.root {
height: 2em;
vertical-align: middle;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.2);
}
}
.normal {
height: 1.25em;
vertical-align: -0.25em;
&:hover {
transform: none;
}
}
.noStyle {
height: auto !important;
}
</style>

View File

@ -1,8 +1,3 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { VNode, h } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
@ -11,12 +6,14 @@ import MkLink from '@/components/MkLink.vue';
import MkMention from '@/components/MkMention.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkEmojiKitchen from '@/components/global/MkEmojiKitchen.vue';
import MkCode from '@/components/MkCode.vue';
import MkGoogle from '@/components/MkGoogle.vue';
import MkSparkle from '@/components/MkSparkle.vue';
import MkA from '@/components/global/MkA.vue';
import { host } from '@/config';
import { defaultStore } from '@/store';
import { mixEmoji } from '@/scripts/emojiKitchen/emojiMixer';
const QUOTE_STYLE = `
display: block;
@ -27,6 +24,38 @@ border-left: solid 3px var(--fg);
opacity: 0.7;
`.split('\n').join(' ');
const colorRegexp = /^([0-9a-f]{3,4}?|[0-9a-f]{6}?|[0-9a-f]{8}?)$/i;
function checkColorHex(text: string) {
return colorRegexp.test(text);
}
const gradientCounterRegExp = /^(color|step)(\d+)/;
function toGradientText(args: Record<string, string>) {
const colors: { index: number; step?: string, color?: string }[] = [];
for (const k in args) {
const matches = k.match(gradientCounterRegExp);
if (matches == null) continue;
const mindex = parseInt(matches[2]);
let i = colors.findIndex(v => v.index === mindex);
if (i === -1) {
i = colors.length;
colors.push({ index: mindex });
}
colors[i][matches[1]] = args[k];
}
let deg = parseFloat(args.deg || '90');
let res = `linear-gradient(${deg}deg`;
for (const colorProp of colors.sort((a, b) => a.index - b.index)) {
let color = colorProp.color;
if (!color || !checkColorHex(color)) color = 'f00';
let step = parseFloat(colorProp.step ?? '');
let stepText = isNaN(step) ? '' : ` ${step}%`;
res += `, #${color}${stepText}`;
}
return res + ')';
}
export default function(props: {
text: string;
plain?: boolean;
@ -44,7 +73,7 @@ export default function(props: {
const ast = (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
const validTime = (t: string | null | undefined) => {
if (t == null) return null;
if (t == null || typeof t === 'boolean') return null;
return t.match(/^[0-9.]+s$/) ? t : null;
};
@ -170,18 +199,15 @@ export default function(props: {
break;
}
case 'blur': {
const radius = parseFloat(token.props.args.rad ?? '6');
return h('span', {
class: '_mfm_blur_',
style: `--blur-px: ${radius}px;`
}, genEl(token.children, scale));
}
case 'rainbow': {
if (!useAnim) {
return h('span', {
class: '_mfm_rainbow_fallback_',
}, genEl(token.children, scale));
}
const speed = validTime(token.props.args.speed) ?? '1s';
style = `animation: mfm-rainbow ${speed} linear infinite;`;
style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
break;
}
case 'sparkle': {
@ -192,7 +218,23 @@ export default function(props: {
}
case 'rotate': {
const degrees = parseFloat(token.props.args.deg ?? '90');
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
let rotateText = `rotate(${degrees}deg)`;
if (!token.props.args.deg && (token.props.args.x || token.props.args.y || token.props.args.z)) {
rotateText = '';
}
if (token.props.args.x) {
const degrees = parseFloat(token.props.args.x ?? '0');
rotateText += ` rotateX(${degrees}deg)`;
}
if (token.props.args.y) {
const degrees = parseFloat(token.props.args.y ?? '0');
rotateText += ` rotateY(${degrees}deg)`;
}
if (token.props.args.z) {
const degrees = parseFloat(token.props.args.z ?? '0');
rotateText += ` rotateZ(${degrees}deg)`;
}
style = `transform: ${rotateText}; transform-origin: center center;`;
break;
}
case 'position': {
@ -213,18 +255,101 @@ export default function(props: {
scale = scale * Math.max(x, y);
break;
}
case 'skew': {
if (!defaultStore.state.advancedMfm) {
style = '';
break;
}
const x = parseFloat(token.props.args.x ?? '0');
const y = parseFloat(token.props.args.y ?? '0');
style = `transform: skew(${x}deg, ${y}deg);`;
break;
}
case 'fgg': {
if (!defaultStore.state.advancedMfm) break;
style = `-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-image: ${toGradientText(token.props.args)};`
break;
}
case 'fg': {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
if (!checkColorHex(color)) color = 'f00';
style = `color: #${color};`;
break;
}
case 'bgg': {
if (!defaultStore.state.advancedMfm) break;
style = `background-image: ${toGradientText(token.props.args)};`
break;
}
case 'bg': {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
if (!checkColorHex(color)) color = 'f00';
style = `background-color: #${color};`;
break;
}
case 'clip': {
if (!defaultStore.state.advancedMfm) break;
let path = '';
if (token.props.args.circle) {
const percent = parseFloat(token.props.args.circle ?? '');
const percentText = isNaN(percent) ? '' : `${percent}%`;
path = `circle(${percentText})`;
}
else {
const top = parseFloat(token.props.args.t ?? '0');
const bottom = parseFloat(token.props.args.b ?? '0');
const left = parseFloat(token.props.args.l ?? '0');
const right = parseFloat(token.props.args.r ?? '0');
path = `inset(${top}% ${right}% ${bottom}% ${left}%)`;
}
style = `clip-path: ${path};`;
break;
}
case 'move': {
const speed = validTime(token.props.args.speed) ?? '1s';
const fromX = parseFloat(token.props.args.fromx ?? '0');
const fromY = parseFloat(token.props.args.fromy ?? '0');
const toX = parseFloat(token.props.args.tox ?? '0');
const toY = parseFloat(token.props.args.toy ?? '0');
const ease =
token.props.args.ease ? 'ease' :
token.props.args.easein ? 'ease-in' :
token.props.args.easeout ? 'ease-out' :
token.props.args.easeinout ? 'ease-in-out' :
'linear';
const delay = validTime(token.props.args.delay) ?? '0s';
const direction =
token.props.args.rev && token.props.args.once ? 'reverse' :
token.props.args.rev ? 'alternate-reverse' :
token.props.args.once ? 'normal' :
'alternate';
style = useAnim ? `--move-fromX: ${fromX}em; --move-fromY: ${fromY}em; --move-toX: ${toX}em; --move-toY: ${toY}em; animation: ${speed} ${ease} ${delay} infinite ${direction} mfm-move;` : '';
break;
}
case 'mix': {
const ch = token.children;
if (ch.length != 2 || ch.some(c => c.type !== 'unicodeEmoji')) {
style = null;
break;
}
const emoji1 = ch[0].props.emoji;
const emoji2 = ch[1].props.emoji;
const mixedEmojiUrl = mixEmoji(emoji1, emoji2);
if (!mixedEmojiUrl) {
style = null;
break;
}
return h(MkEmojiKitchen, {
key: Math.random(),
name: emoji1 + emoji2,
normal: props.plain,
url: mixedEmojiUrl
});
}
}
if (style == null) {
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,60 @@
import * as data from './emojiData';
const mixEmojiUrl = (r, c) => {
let padZeros = r < 20220500; // Revisions before 0522 had preceding zeros
c[0] = c[0].split(/-/g).map(s => padZeros ? s.padStart(4, "0") : s).join("-u");
c[1] = c[1].split(/-/g).map(s => padZeros ? s.padStart(4, "0") : s).join("-u");
return `https://www.gstatic.com/android/keyboard/emojikitchen/${r}/u${c[0]}/u${c[0]}_u${c[1]}.png`;
};
const convertBase = (value, from_base, to_base) => {
value = value.toString();
var range = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/'.split('');
var from_range = range.slice(0, from_base);
var to_range = range.slice(0, to_base);
var dec_value = value.split('').reverse().reduce(function (carry, digit, index) {
if (from_range.indexOf(digit) === -1) throw new Error('Invalid digit `' + digit + '` for base ' + from_base + '.');
return carry += from_range.indexOf(digit) * (Math.pow(from_base, index));
}, 0);
var new_value = '';
while (dec_value > 0) {
new_value = to_range[dec_value % to_base] + new_value;
dec_value = (dec_value - (dec_value % to_base)) / to_base;
}
return new_value || '0';
};
const hexEncodeEmoji = (chr) => {
if (chr.length !== 1) {
const hi = chr.charCodeAt(0);
const lo = chr.charCodeAt(1);
if (0xD800 <= hi && hi < 0xDC00 && 0xDC00 <= lo && lo < 0xE000) {
return (0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00)).toString(16);
}
return ("000" + hi.toString(16)).slice(-4) + '-' + ("000" + lo.toString(16)).slice(-4);
}
else {
return ("000" + chr.charCodeAt(0).toString(16)).slice(-4);
}
};
const pairsMatchingMap = match => {
const mv = match[0];
let [d, c1, c2] = mv.split('.');
c1 = data.points[convertBase(c1, 64, 10)];
c2 = data.points[convertBase(c2, 64, 10)];
d = data.revisions[convertBase(d, 64, 10)];
return mixEmojiUrl(d, [c1, c2]);
};
export const mixEmoji = (emoji1, emoji2) => {
const encordedEmoji1 = convertBase(data.points.indexOf(hexEncodeEmoji(emoji1)), 10, 64);
const encordedEmoji2 = convertBase(data.points.indexOf(hexEncodeEmoji(emoji2)), 10, 64);
return [
...data.pairs.matchAll(new RegExp("^.*\\." + encordedEmoji1 + "\\." + encordedEmoji2 + "\\.$", "gm")),
...data.pairs.matchAll(new RegExp("^.*\\." + encordedEmoji2 + "\\." + encordedEmoji1 + "\\.$", "gm"))
].map(pairsMatchingMap).pop();
};

View File

@ -1,6 +1 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate'];
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'skew', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'fgg', 'bgg', 'clip', 'move', 'mix'];