mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-23 00:29:22 +09:00
perf: MkImgWithBlurhashとMkMediaImageを最適化 (#10782)
* #10781 * fix tsconfig * fetch image?? * Revert "fetch image??" This reverts commit0925c28d5a
. * wip * Revert "wip" This reverts commitbe97c6cb88
. * loading="eager" * loading="eager" 2 * error * wip * wip * wip * wip * clean up * fix * 生成するworkerを1つにする? * clean up * use buraha * wip * smaller width, height * update buraha * clean up * fix * Update MkMediaImage.vue * Update MkImgWithBlurhash.vue * Revert "fix(frontend): センシティブ設定された画像を開くとき一瞬レイアウトが崩れる問題を修正" This reverts commit41e9aa6f9b
. * Update MkMediaList.vue * Update MkMediaList.vue * Update MkMediaList.vue * Update CHANGELOG.md * wait for decode * fix * ? * (test) remove container-type: inline-size; * Revert "(test) remove container-type: inline-size;" This reverts commit9448e64228
. * container-name * Revert "container-name" This reverts commit94385d3221
. * width: 100%; * improve performance * refactor * wip * WIP * wip * Revert "wip" This reverts commit36e3b75cab
. * Revert "WIP" This reverts commit05b729ef91
. * Revert "wip" This reverts commit0801e79361
. * #10860 * wip * no worker * Revert "no worker" This reverts commita9c49e4fb4
. * ✌️ * workerNumber固定は不要 --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
parent
3804c6e7ad
commit
59255e11b8
@ -103,6 +103,7 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
|
||||
* 画像が全て隠れた状態で表示されるようになります
|
||||
- 閲覧注意設定された画像は表示した状態でもそれが閲覧注意だと分かる表示をするように
|
||||
- モデレーターはノートに添付された画像上から直接NSFW設定できるように
|
||||
- 1枚だけのメディアリストの画像のアスペクト比を画像に応じて縦長にするように
|
||||
- プロフィール設定「追加情報」の項目の削除と並び替えができるように
|
||||
- 新しい実績を追加
|
||||
- AiScriptを0.13.2に更新
|
||||
|
@ -25,9 +25,9 @@
|
||||
"@vue-macros/reactivity-transform": "0.3.7",
|
||||
"@vue/compiler-sfc": "3.3.2",
|
||||
"autosize": "6.0.1",
|
||||
"blurhash": "2.0.5",
|
||||
"broadcast-channel": "4.20.2",
|
||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||
"buraha": "github:misskey-dev/buraha",
|
||||
"canvas-confetti": "1.6.0",
|
||||
"chart.js": "4.3.0",
|
||||
"chartjs-adapter-date-fns": "3.0.0",
|
||||
|
@ -5,12 +5,9 @@
|
||||
<ImgWithBlurhash
|
||||
class="img layered"
|
||||
:transition="safe ? null : {
|
||||
enterActiveClass: $style.transition_toggle_enterActive,
|
||||
duration: 500,
|
||||
leaveActiveClass: $style.transition_toggle_leaveActive,
|
||||
enterFromClass: $style.transition_toggle_enterFrom,
|
||||
leaveToClass: $style.transition_toggle_leaveTo,
|
||||
enterToClass: $style.transition_toggle_enterTo,
|
||||
leaveFromClass: $style.transition_toggle_leaveFrom,
|
||||
}"
|
||||
:src="post.files[0].thumbnailUrl"
|
||||
:hash="post.files[0].blurhash"
|
||||
@ -53,24 +50,16 @@ function leaveHover(): void {
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_toggle_enterActive,
|
||||
.transition_toggle_leaveActive {
|
||||
transition: opacity 0.5s;
|
||||
transition: opacity .5s;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.transition_toggle_enterFrom,
|
||||
.transition_toggle_leaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.transition_toggle_enterTo,
|
||||
.transition_toggle_leaveFrom {
|
||||
transition: none;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,30 +1,56 @@
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''">
|
||||
<img v-if="!loaded && src && !forceBlurhash" :class="$style.loader" :src="src" @load="onLoad"/>
|
||||
<Transition
|
||||
mode="in-out"
|
||||
:enter-active-class="defaultStore.state.animation && (props.transition?.enterActiveClass ?? $style['transition_toggle_enterActive']) || undefined"
|
||||
:leave-active-class="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_toggle_leaveActive']) || undefined"
|
||||
<div ref="root" :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''">
|
||||
<TransitionGroup
|
||||
:duration="defaultStore.state.animation && props.transition?.duration || undefined"
|
||||
:enter-active-class="defaultStore.state.animation && props.transition?.enterActiveClass || undefined"
|
||||
:leave-active-class="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style['transition_leaveActive']) || undefined"
|
||||
:enter-from-class="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
|
||||
:leave-to-class="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
|
||||
:enter-to-class="defaultStore.state.animation && (props.transition?.enterToClass ?? $style['transition_toggle_enterTo']) || undefined"
|
||||
:leave-from-class="defaultStore.state.animation && (props.transition?.leaveFromClass ?? $style['transition_toggle_leaveFrom']) || undefined"
|
||||
:enter-to-class="defaultStore.state.animation && props.transition?.enterToClass || undefined"
|
||||
:leave-from-class="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
|
||||
>
|
||||
<canvas v-if="!loaded || forceBlurhash" ref="canvas" :class="$style.canvas" :width="width" :height="height" :title="title ?? undefined"/>
|
||||
<img v-else :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined"/>
|
||||
</Transition>
|
||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined"/>
|
||||
<img v-show="!hide" key="img" ref="img" :height="imgHeight" :width="imgWidth" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async"/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef, useCssModule, watch } from 'vue';
|
||||
import { decode } from 'blurhash';
|
||||
import { defaultStore } from '@/store';
|
||||
<script lang="ts">
|
||||
import DrawBlurhash from '@/workers/draw-blurhash?worker';
|
||||
import TestWebGL2 from '@/workers/test-webgl2?worker';
|
||||
import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch';
|
||||
import { $ref } from 'vue/macros';
|
||||
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
|
||||
|
||||
const workerPromise = new Promise<WorkerMultiDispatch | null>(resolve => {
|
||||
const testWorker = new TestWebGL2();
|
||||
testWorker.addEventListener('message', event => {
|
||||
if (event.data.result) {
|
||||
const workers = new WorkerMultiDispatch(
|
||||
() => new DrawBlurhash(),
|
||||
Math.min(navigator.hardwareConcurrency - 1, 4),
|
||||
);
|
||||
resolve(workers);
|
||||
if (_DEV_) console.log('WebGL2 in worker is supported!');
|
||||
} else {
|
||||
resolve(null);
|
||||
if (_DEV_) console.log('WebGL2 in worker is not supported...');
|
||||
}
|
||||
testWorker.terminate();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onMounted, onUnmounted, shallowRef, useCssModule, watch } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { render } from 'buraha';
|
||||
import { defaultStore } from '@/store';
|
||||
const $style = useCssModule();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
transition?: {
|
||||
duration?: number | { enter: number; leave: number; };
|
||||
enterActiveClass?: string;
|
||||
leaveActiveClass?: string;
|
||||
enterFromClass?: string;
|
||||
@ -51,67 +77,141 @@ const props = withDefaults(defineProps<{
|
||||
forceBlurhash: false,
|
||||
});
|
||||
|
||||
const viewId = uuid();
|
||||
const canvas = shallowRef<HTMLCanvasElement>();
|
||||
const root = shallowRef<HTMLDivElement>();
|
||||
const img = shallowRef<HTMLImageElement>();
|
||||
let loaded = $ref(false);
|
||||
let width = $ref(props.width);
|
||||
let height = $ref(props.height);
|
||||
let canvasWidth = $ref(64);
|
||||
let canvasHeight = $ref(64);
|
||||
let imgWidth = $ref(props.width);
|
||||
let imgHeight = $ref(props.height);
|
||||
let bitmapTmp = $ref<CanvasImageSource | undefined>();
|
||||
const hide = computed(() => !loaded || props.forceBlurhash);
|
||||
|
||||
function onLoad() {
|
||||
loaded = true;
|
||||
function waitForDecode() {
|
||||
if (props.src != null && props.src !== '') {
|
||||
nextTick()
|
||||
.then(() => img.value?.decode())
|
||||
.then(() => {
|
||||
loaded = true;
|
||||
}, error => {
|
||||
console.error('Error occured during decoding image', img.value, error);
|
||||
throw Error(error);
|
||||
});
|
||||
} else {
|
||||
loaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch([() => props.width, () => props.height], () => {
|
||||
watch([() => props.width, () => props.height, root], () => {
|
||||
const ratio = props.width / props.height;
|
||||
if (ratio > 1) {
|
||||
width = Math.round(64 * ratio);
|
||||
height = 64;
|
||||
canvasWidth = Math.round(64 * ratio);
|
||||
canvasHeight = 64;
|
||||
} else {
|
||||
width = 64;
|
||||
height = Math.round(64 / ratio);
|
||||
canvasWidth = 64;
|
||||
canvasHeight = Math.round(64 / ratio);
|
||||
}
|
||||
|
||||
const clientWidth = root.value?.clientWidth ?? 300;
|
||||
imgWidth = clientWidth;
|
||||
imgHeight = Math.round(clientWidth / ratio);
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
function draw() {
|
||||
if (props.hash == null || !canvas.value) return;
|
||||
const pixels = decode(props.hash, width, height);
|
||||
function drawImage(bitmap: CanvasImageSource) {
|
||||
// canvasがない(mountedされていない)場合はTmpに保存しておく
|
||||
if (!canvas.value) {
|
||||
bitmapTmp = bitmap;
|
||||
return;
|
||||
}
|
||||
|
||||
// canvasがあれば描画する
|
||||
bitmapTmp = undefined;
|
||||
const ctx = canvas.value.getContext('2d');
|
||||
const imageData = ctx!.createImageData(width, height);
|
||||
imageData.data.set(pixels);
|
||||
ctx!.putImageData(imageData, 0, 0);
|
||||
if (!ctx) return;
|
||||
ctx.drawImage(bitmap, 0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
watch([() => props.hash, canvas], () => {
|
||||
async function draw() {
|
||||
if (!canvas.value || props.hash == null) return;
|
||||
|
||||
const ctx = canvas.value.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// avgColorでお茶をにごす
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888';
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
const workers = await workerPromise;
|
||||
if (workers) {
|
||||
workers.postMessage(
|
||||
{
|
||||
id: viewId,
|
||||
hash: props.hash,
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
const work = document.createElement('canvas');
|
||||
work.width = canvasWidth;
|
||||
work.height = canvasHeight;
|
||||
render(props.hash, work);
|
||||
ctx.drawImage(work, 0, 0, canvasWidth, canvasHeight);
|
||||
} catch (error) {
|
||||
console.error('Error occured during drawing blurhash', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function workerOnMessage(event: MessageEvent) {
|
||||
if (event.data.id !== viewId) return;
|
||||
drawImage(event.data.bitmap as ImageBitmap);
|
||||
}
|
||||
|
||||
workerPromise.then(worker => {
|
||||
if (worker) {
|
||||
worker.addListener(workerOnMessage);
|
||||
}
|
||||
|
||||
draw();
|
||||
});
|
||||
|
||||
watch(() => props.src, () => {
|
||||
waitForDecode();
|
||||
});
|
||||
|
||||
watch(() => props.hash, () => {
|
||||
draw();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
draw();
|
||||
// drawImageがmountedより先に呼ばれている場合はここで描画する
|
||||
if (bitmapTmp) {
|
||||
drawImage(bitmapTmp);
|
||||
}
|
||||
waitForDecode();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
workerPromise.then(worker => {
|
||||
worker?.removeListener(workerOnMessage);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_toggle_enterActive,
|
||||
.transition_toggle_leaveActive {
|
||||
.transition_leaveActive {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.transition_toggle_enterTo,
|
||||
.transition_toggle_leaveFrom {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
@ -1,29 +1,40 @@
|
||||
<template>
|
||||
<div v-if="hide" :class="$style.hidden" @click="hide = false">
|
||||
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment" :width="image.properties.width" :height="image.properties.height" :force-blurhash="defaultStore.state.enableDataSaverMode"/>
|
||||
<div :class="$style.hiddenText">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
|
||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'">
|
||||
<div :class="hide ? $style.hidden : $style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
|
||||
<a
|
||||
:class="$style.imageContainer"
|
||||
:href="image.url"
|
||||
:title="image.name"
|
||||
>
|
||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
|
||||
<ImgWithBlurhash
|
||||
:hash="image.blurhash"
|
||||
:src="(defaultStore.state.enableDataSaverMode && hide) ? null : url"
|
||||
:force-blurhash="hide"
|
||||
:cover="hide"
|
||||
:alt="image.comment || image.name"
|
||||
:title="image.comment || image.name"
|
||||
:width="image.properties.width"
|
||||
:height="image.properties.height"
|
||||
:style="hide ? 'filter: brightness(0.5);' : null"
|
||||
/>
|
||||
</a>
|
||||
<div :class="$style.indicators">
|
||||
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
||||
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
||||
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
|
||||
</div>
|
||||
<button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button>
|
||||
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots"></i></button>
|
||||
<template v-if="hide">
|
||||
<div :class="$style.hiddenText">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
|
||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div :class="$style.indicators">
|
||||
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
||||
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
||||
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
|
||||
</div>
|
||||
<button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click.stop.prevent="hide = true"><i class="ti ti-eye-off"></i></button>
|
||||
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots"></i></button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -53,6 +64,12 @@ const url = $computed(() => (props.raw || defaultStore.state.loadRawImages)
|
||||
: props.image.thumbnailUrl,
|
||||
);
|
||||
|
||||
function onclick() {
|
||||
if (hide) {
|
||||
hide = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
||||
watch(() => props.image, () => {
|
||||
hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
|
||||
|
@ -7,6 +7,7 @@
|
||||
:class="[
|
||||
$style.medias,
|
||||
count <= 4 ? $style['n' + count] : $style.nMany,
|
||||
$style[`n1${defaultStore.reactiveState.mediaListWithOneImageAppearance.value}`]
|
||||
]"
|
||||
>
|
||||
<template v-for="media in mediaList.filter(media => previewable(media))">
|
||||
@ -19,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, useCssModule, watch } from 'vue';
|
||||
import { onMounted, ref, useCssModule, watch, shallowRef } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||
import PhotoSwipe from 'photoswipe';
|
||||
@ -38,11 +39,42 @@ const props = defineProps<{
|
||||
|
||||
const $style = useCssModule();
|
||||
|
||||
const gallery = ref<HTMLDivElement>();
|
||||
const gallery = shallowRef<HTMLDivElement>();
|
||||
const pswpZIndex = os.claimZIndex('middle');
|
||||
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
|
||||
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
|
||||
|
||||
function calcAspectRatio() {
|
||||
if (!gallery.value) return;
|
||||
|
||||
let img = props.mediaList[0];
|
||||
|
||||
if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) {
|
||||
gallery.value.style.aspectRatio = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// アスペクト比上限設定では、横長の場合は高さを縮小させる
|
||||
const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
|
||||
|
||||
switch (defaultStore.state.mediaListWithOneImageAppearance) {
|
||||
case '16_9':
|
||||
gallery.value.style.aspectRatio = ratioMax(16 / 9);
|
||||
break;
|
||||
case '1_1':
|
||||
gallery.value.style.aspectRatio = ratioMax(1);
|
||||
break;
|
||||
case '2_3':
|
||||
gallery.value.style.aspectRatio = ratioMax(2 / 3);
|
||||
break;
|
||||
default:
|
||||
gallery.value.style.aspectRatio = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
watch([defaultStore.reactiveState.mediaListWithOneImageAppearance, gallery], () => calcAspectRatio());
|
||||
|
||||
onMounted(() => {
|
||||
const lightbox = new PhotoSwipeLightbox({
|
||||
dataSource: props.mediaList
|
||||
@ -162,12 +194,37 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
|
||||
// for webkit
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&.n1 {
|
||||
aspect-ratio: 16/9;
|
||||
grid-template-rows: 1fr;
|
||||
|
||||
// default (expand)
|
||||
min-height: 64px;
|
||||
max-height: clamp(
|
||||
64px,
|
||||
50cqh,
|
||||
min(360px, 50vh)
|
||||
);
|
||||
|
||||
&.n116_9 {
|
||||
min-height: none;
|
||||
max-height: none;
|
||||
aspect-ratio: 16 / 9; // fallback
|
||||
}
|
||||
|
||||
&.n11_1{
|
||||
min-height: none;
|
||||
max-height: none;
|
||||
aspect-ratio: 1 / 1; // fallback
|
||||
}
|
||||
|
||||
&.n12_3 {
|
||||
min-height: none;
|
||||
max-height: none;
|
||||
aspect-ratio: 2 / 3; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
&.n2 {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
|
||||
<img :class="$style.inner" :src="url" decoding="async"/>
|
||||
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true"/>
|
||||
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
||||
<div v-if="user.isCat" :class="[$style.ears]">
|
||||
<div :class="$style.earLeft">
|
||||
@ -30,6 +30,7 @@ import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-bl
|
||||
import { acct, userPage } from '@/filters/user';
|
||||
import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
|
||||
|
||||
const animation = $ref(defaultStore.state.animation);
|
||||
const squareAvatars = $ref(defaultStore.state.squareAvatars);
|
||||
|
@ -56,7 +56,7 @@
|
||||
<option value="ignore">{{ i18n.ts._nsfw.ignore }}</option>
|
||||
<option value="force">{{ i18n.ts._nsfw.force }}</option>
|
||||
</MkSelect>
|
||||
<!--
|
||||
|
||||
<MkRadios v-model="mediaListWithOneImageAppearance">
|
||||
<template #label>{{ i18n.ts.mediaListWithOneImageAppearance }}</template>
|
||||
<option value="expand">{{ i18n.ts.default }}</option>
|
||||
@ -64,7 +64,6 @@
|
||||
<option value="1_1">{{ i18n.t('limitTo', { x: '1:1' }) }}</option>
|
||||
<option value="2_3">{{ i18n.t('limitTo', { x: '2:3' }) }}</option>
|
||||
</MkRadios>
|
||||
-->
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
|
75
packages/frontend/src/scripts/worker-multi-dispatch.ts
Normal file
75
packages/frontend/src/scripts/worker-multi-dispatch.ts
Normal file
@ -0,0 +1,75 @@
|
||||
function defaultUseWorkerNumber(prev: number, totalWorkers: number) {
|
||||
return prev + 1;
|
||||
}
|
||||
|
||||
export class WorkerMultiDispatch<POST = any, RETURN = any> {
|
||||
private symbol = Symbol('WorkerMultiDispatch');
|
||||
private workers: Worker[] = [];
|
||||
private terminated = false;
|
||||
private prevWorkerNumber = 0;
|
||||
private getUseWorkerNumber = defaultUseWorkerNumber;
|
||||
private finalizationRegistry: FinalizationRegistry<symbol>;
|
||||
|
||||
constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) {
|
||||
this.getUseWorkerNumber = getUseWorkerNumber;
|
||||
for (let i = 0; i < concurrency; i++) {
|
||||
this.workers.push(workerConstructor());
|
||||
}
|
||||
|
||||
this.finalizationRegistry = new FinalizationRegistry(() => {
|
||||
this.terminate();
|
||||
});
|
||||
this.finalizationRegistry.register(this, this.symbol);
|
||||
|
||||
if (_DEV_) console.log('WorkerMultiDispatch: Created', this);
|
||||
}
|
||||
|
||||
public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) {
|
||||
let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length);
|
||||
workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length;
|
||||
if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber);
|
||||
this.prevWorkerNumber = workerNumber;
|
||||
|
||||
// 不毛だがunionをoverloadに突っ込めない
|
||||
// https://stackoverflow.com/questions/66507585/overload-signatures-union-types-and-no-overload-matches-this-call-error
|
||||
// https://github.com/microsoft/TypeScript/issues/14107
|
||||
if (Array.isArray(options)) {
|
||||
this.workers[workerNumber].postMessage(message, options);
|
||||
} else {
|
||||
this.workers[workerNumber].postMessage(message, options);
|
||||
}
|
||||
return workerNumber;
|
||||
}
|
||||
|
||||
public addListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
|
||||
this.workers.forEach(worker => {
|
||||
worker.addEventListener('message', callback, options);
|
||||
});
|
||||
}
|
||||
|
||||
public removeListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
|
||||
this.workers.forEach(worker => {
|
||||
worker.removeEventListener('message', callback, options);
|
||||
});
|
||||
}
|
||||
|
||||
public terminate() {
|
||||
this.terminated = true;
|
||||
if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this);
|
||||
this.workers.forEach(worker => {
|
||||
worker.terminate();
|
||||
});
|
||||
this.workers = [];
|
||||
this.finalizationRegistry.unregister(this);
|
||||
}
|
||||
|
||||
public isTerminated() {
|
||||
return this.terminated;
|
||||
}
|
||||
public getWorkers() {
|
||||
return this.workers;
|
||||
}
|
||||
public getSymbol() {
|
||||
return this.symbol;
|
||||
}
|
||||
}
|
15
packages/frontend/src/workers/draw-blurhash.ts
Normal file
15
packages/frontend/src/workers/draw-blurhash.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { render } from 'buraha';
|
||||
|
||||
onmessage = (event) => {
|
||||
// console.log(event.data);
|
||||
if (!('id' in event.data && typeof event.data.id === 'string')) {
|
||||
return;
|
||||
}
|
||||
if (!('hash' in event.data && typeof event.data.hash === 'string')) {
|
||||
return;
|
||||
}
|
||||
const work = new OffscreenCanvas(event.data.width ?? 64, event.data.height ?? 64);
|
||||
render(event.data.hash, work);
|
||||
const bitmap = work.transferToImageBitmap();
|
||||
postMessage({ id: event.data.id, bitmap });
|
||||
};
|
7
packages/frontend/src/workers/test-webgl2.ts
Normal file
7
packages/frontend/src/workers/test-webgl2.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const canvas = new OffscreenCanvas(1, 1);
|
||||
const gl = canvas.getContext('webgl2');
|
||||
if (gl) {
|
||||
postMessage({ result: true });
|
||||
} else {
|
||||
postMessage({ result: false });
|
||||
}
|
5
packages/frontend/src/workers/tsconfig.json
Normal file
5
packages/frontend/src/workers/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "webworker"],
|
||||
}
|
||||
}
|
@ -139,6 +139,10 @@ export function getConfig(): UserConfig {
|
||||
},
|
||||
},
|
||||
|
||||
worker: {
|
||||
format: 'es',
|
||||
},
|
||||
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
deps: {
|
||||
|
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@ -657,15 +657,15 @@ importers:
|
||||
autosize:
|
||||
specifier: 6.0.1
|
||||
version: 6.0.1
|
||||
blurhash:
|
||||
specifier: 2.0.5
|
||||
version: 2.0.5
|
||||
broadcast-channel:
|
||||
specifier: 4.20.2
|
||||
version: 4.20.2
|
||||
browser-image-resizer:
|
||||
specifier: github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3
|
||||
version: github.com/misskey-dev/browser-image-resizer/0227e860621e55cbed0aabe6dc601096a7748c4a
|
||||
buraha:
|
||||
specifier: github:misskey-dev/buraha
|
||||
version: github.com/misskey-dev/buraha/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c
|
||||
canvas-confetti:
|
||||
specifier: 1.6.0
|
||||
version: 1.6.0
|
||||
@ -20410,6 +20410,12 @@ packages:
|
||||
version: 2.2.1-misskey.3
|
||||
dev: false
|
||||
|
||||
github.com/misskey-dev/buraha/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c:
|
||||
resolution: {tarball: https://codeload.github.com/misskey-dev/buraha/tar.gz/92b20c1ab15c5cb5a224cf3b1ecd4f6baca12b7c}
|
||||
name: buraha
|
||||
version: 0.0.0
|
||||
dev: false
|
||||
|
||||
github.com/misskey-dev/sharp-read-bmp/02d9dc189fa7df0c4bea09330be26741772dac01:
|
||||
resolution: {tarball: https://codeload.github.com/misskey-dev/sharp-read-bmp/tar.gz/02d9dc189fa7df0c4bea09330be26741772dac01}
|
||||
name: sharp-read-bmp
|
||||
|
Loading…
Reference in New Issue
Block a user