Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bh worker off #10790

Closed
wants to merge 20 commits into from
15 changes: 2 additions & 13 deletions packages/frontend/src/components/MkGalleryPostPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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>
Expand Down
89 changes: 46 additions & 43 deletions packages/frontend/src/components/MkImgWithBlurhash.vue
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
<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"
<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" :height="height" :width="width" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" @load="onLoad"/>
</TransitionGroup>
</div>
</template>

<script lang="ts" setup>
import { onMounted, shallowRef, useCssModule, watch } from 'vue';
import { decode } from 'blurhash';
import { computed, onMounted, onUnmounted, shallowRef, useCssModule, watch } from 'vue';
import { defaultStore } from '@/store';
import DrawBlurhash from '@/workers/draw-blurhash?worker';

const worker = new DrawBlurhash();

const $style = useCssModule();

const props = withDefaults(defineProps<{
transition?: {
duration?: number | { enter: number; leave: number; };
enterActiveClass?: string;
leaveActiveClass?: string;
enterFromClass?: string;
Expand Down Expand Up @@ -53,8 +55,9 @@ const props = withDefaults(defineProps<{

const canvas = shallowRef<HTMLCanvasElement>();
let loaded = $ref(false);
let width = $ref(props.width);
let height = $ref(props.height);
let canvasWidth = $ref(props.width);
let canvasHeight = $ref(props.height);
const hide = computed(() => !loaded || props.forceBlurhash);

function onLoad() {
loaded = true;
Expand All @@ -63,55 +66,55 @@ function onLoad() {
watch([() => props.width, () => props.height], () => {
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);
}
}, {
immediate: true,
});

function draw() {
if (props.hash == null || !canvas.value) return;
const pixels = decode(props.hash, width, height);
const ctx = canvas.value.getContext('2d');
const imageData = ctx!.createImageData(width, height);
imageData.data.set(pixels);
ctx!.putImageData(imageData, 0, 0);
function draw(offscreen: OffscreenCanvas | null = null) {
if (props.hash == null) return;
worker.postMessage({
hash: props.hash,
width: canvasWidth,
height: canvasHeight,
canvas: offscreen,
}, offscreen ? [offscreen] : []);
}

watch([() => props.hash, canvas], () => {
worker.addEventListener('message', event => {
if (!canvas.value) return;
const ctx = canvas.value.getContext('2d');
if (!ctx) return;
const bitmap = event.data as ImageBitmap;
ctx.drawImage(bitmap, 0, 0, canvasWidth, canvasHeight);
});

watch(() => props.hash, () => {
draw();
});

onMounted(() => {
draw();
if (canvas.value) {
draw(canvas.value.transferControlToOffscreen());
}
});

onUnmounted(() => {
worker.terminate();
});
</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%;
Expand Down
52 changes: 36 additions & 16 deletions packages/frontend/src/components/MkMediaImage.vue
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
<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>-->
<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>
<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>
<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>
<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>
</template>
</div>
</template>

Expand Down Expand Up @@ -53,6 +67,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');
Expand Down
23 changes: 23 additions & 0 deletions packages/frontend/src/workers/draw-blurhash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { decode } from 'blurhash';

let canvas: OffscreenCanvas;

onmessage = async (event) => {
if ('canvas' in event.data) {
canvas = event.data.canvas;
}
if (!canvas) {
throw new Error('Canvas not set');
}
if (!('hash' in event.data && typeof event.data.hash === 'string')) {
return;
}
const width = event.data.width ?? 64;
const height = event.data.height ?? 64;
const ctx = canvas.getContext!('2d');
if (!ctx) return;
const imageData = ctx.createImageData(width, height);
const pixels = decode(event.data.hash, width, height);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
};
5 changes: 5 additions & 0 deletions packages/frontend/src/workers/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"lib": ["esnext", "webworker"],
}
}
4 changes: 4 additions & 0 deletions packages/frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ export function getConfig(): UserConfig {
},
},

worker: {
format: 'es',
},

test: {
environment: 'happy-dom',
deps: {
Expand Down