enhance(frontend): マスクエフェクト (#16556)
* wip * wip * Update MkImageEffectorDialog.vue * Update MkImageEffectorDialog.vue * Update MkImageEffectorDialog.vue * Update MkImageEffectorDialog.vue * Update MkImageEffectorDialog.vue * Update fillSquare.ts * Update CHANGELOG.md * Update fillSquare.ts
This commit is contained in:
parent
b231da7c7c
commit
8c413d01e6
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
|
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
|
||||||
|
- Enhance: 画像編集にマスクエフェクトを追加
|
||||||
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
|
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|
|
@ -12378,6 +12378,10 @@ export interface Locale extends ILocale {
|
||||||
* ティアリング
|
* ティアリング
|
||||||
*/
|
*/
|
||||||
"tearing": string;
|
"tearing": string;
|
||||||
|
/**
|
||||||
|
* 塗りつぶし(四角)
|
||||||
|
*/
|
||||||
|
"fillSquare": string;
|
||||||
};
|
};
|
||||||
"_fxProps": {
|
"_fxProps": {
|
||||||
/**
|
/**
|
||||||
|
@ -12392,6 +12396,10 @@ export interface Locale extends ILocale {
|
||||||
* サイズ
|
* サイズ
|
||||||
*/
|
*/
|
||||||
"size": string;
|
"size": string;
|
||||||
|
/**
|
||||||
|
* 位置
|
||||||
|
*/
|
||||||
|
"offset": string;
|
||||||
/**
|
/**
|
||||||
* 色
|
* 色
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3314,11 +3314,13 @@ _imageEffector:
|
||||||
checker: "チェッカー"
|
checker: "チェッカー"
|
||||||
blockNoise: "ブロックノイズ"
|
blockNoise: "ブロックノイズ"
|
||||||
tearing: "ティアリング"
|
tearing: "ティアリング"
|
||||||
|
fillSquare: "塗りつぶし(四角)"
|
||||||
|
|
||||||
_fxProps:
|
_fxProps:
|
||||||
angle: "角度"
|
angle: "角度"
|
||||||
scale: "サイズ"
|
scale: "サイズ"
|
||||||
size: "サイズ"
|
size: "サイズ"
|
||||||
|
offset: "位置"
|
||||||
color: "色"
|
color: "色"
|
||||||
opacity: "不透明度"
|
opacity: "不透明度"
|
||||||
normalize: "正規化"
|
normalize: "正規化"
|
||||||
|
|
|
@ -19,9 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
<div :class="$style.preview">
|
<div :class="$style.preview">
|
||||||
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
|
<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas>
|
||||||
<div :class="$style.previewContainer">
|
<div :class="$style.previewContainer">
|
||||||
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
|
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
|
||||||
|
<div class="_acrylic" :class="$style.editControls">
|
||||||
|
<button class="_button" :class="[$style.previewControlsButton, fillSquare ? $style.active : null]" @click="fillSquare = true"><i class="ti ti-pencil"></i></button>
|
||||||
|
</div>
|
||||||
<div class="_acrylic" :class="$style.previewControls">
|
<div class="_acrylic" :class="$style.previewControls">
|
||||||
<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
|
<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
|
||||||
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
|
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
|
||||||
|
@ -212,6 +215,100 @@ watch(enabled, () => {
|
||||||
renderer.render();
|
renderer.render();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fillSquare = ref(false);
|
||||||
|
|
||||||
|
function onImagePointerdown(ev: PointerEvent) {
|
||||||
|
if (canvasEl.value == null || imageBitmap == null || !fillSquare.value) return;
|
||||||
|
|
||||||
|
const AW = canvasEl.value.clientWidth;
|
||||||
|
const AH = canvasEl.value.clientHeight;
|
||||||
|
const BW = imageBitmap.width;
|
||||||
|
const BH = imageBitmap.height;
|
||||||
|
|
||||||
|
let xOffset = 0;
|
||||||
|
let yOffset = 0;
|
||||||
|
|
||||||
|
if (AW / AH < BW / BH) { // 横長
|
||||||
|
yOffset = AH - BH * (AW / BW);
|
||||||
|
} else { // 縦長
|
||||||
|
xOffset = AW - BW * (AH / BH);
|
||||||
|
}
|
||||||
|
|
||||||
|
xOffset /= 2;
|
||||||
|
yOffset /= 2;
|
||||||
|
|
||||||
|
let startX = ev.offsetX - xOffset;
|
||||||
|
let startY = ev.offsetY - yOffset;
|
||||||
|
|
||||||
|
if (AW / AH < BW / BH) { // 横長
|
||||||
|
startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1));
|
||||||
|
startY = startY / (Math.max(AW, AH) / Math.max(BW / BH, 1));
|
||||||
|
} else { // 縦長
|
||||||
|
startX = startX / (Math.min(AW, AH) / Math.max(BH / BW, 1));
|
||||||
|
startY = startY / (Math.min(AW, AH) / Math.max(BW / BH, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = genId();
|
||||||
|
layers.push({
|
||||||
|
id,
|
||||||
|
fxId: 'fillSquare',
|
||||||
|
params: {
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
scaleX: 0.1,
|
||||||
|
scaleY: 0.1,
|
||||||
|
angle: 0,
|
||||||
|
opacity: 1,
|
||||||
|
color: [1, 1, 1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
_move(ev.offsetX, ev.offsetY);
|
||||||
|
|
||||||
|
function _move(pointerX: number, pointerY: number) {
|
||||||
|
let x = pointerX - xOffset;
|
||||||
|
let y = pointerY - yOffset;
|
||||||
|
|
||||||
|
if (AW / AH < BW / BH) { // 横長
|
||||||
|
x = x / (Math.max(AW, AH) / Math.max(BH / BW, 1));
|
||||||
|
y = y / (Math.max(AW, AH) / Math.max(BW / BH, 1));
|
||||||
|
} else { // 縦長
|
||||||
|
x = x / (Math.min(AW, AH) / Math.max(BH / BW, 1));
|
||||||
|
y = y / (Math.min(AW, AH) / Math.max(BW / BH, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleX = Math.abs(x - startX);
|
||||||
|
const scaleY = Math.abs(y - startY);
|
||||||
|
|
||||||
|
const layerIndex = layers.findIndex((l) => l.id === id);
|
||||||
|
const layer = layerIndex !== -1 ? layers[layerIndex] : null;
|
||||||
|
if (layer != null) {
|
||||||
|
layer.params.offsetX = (x + startX) - 1;
|
||||||
|
layer.params.offsetY = (y + startY) - 1;
|
||||||
|
layer.params.scaleX = scaleX;
|
||||||
|
layer.params.scaleY = scaleY;
|
||||||
|
layers[layerIndex] = layer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function move(ev: PointerEvent) {
|
||||||
|
_move(ev.offsetX, ev.offsetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function up() {
|
||||||
|
canvasEl.value?.removeEventListener('pointermove', move);
|
||||||
|
canvasEl.value?.removeEventListener('pointerup', up);
|
||||||
|
canvasEl.value?.removeEventListener('pointercancel', up);
|
||||||
|
canvasEl.value?.releasePointerCapture(ev.pointerId);
|
||||||
|
|
||||||
|
fillSquare.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasEl.value.addEventListener('pointermove', move);
|
||||||
|
canvasEl.value.addEventListener('pointerup', up);
|
||||||
|
canvasEl.value.setPointerCapture(ev.pointerId);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
|
@ -251,6 +348,18 @@ watch(enabled, () => {
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editControls {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.previewControls {
|
.previewControls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
@ -283,9 +392,11 @@ watch(enabled, () => {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: -webkit-fill-available;
|
||||||
height: 100%;
|
width: stretch;
|
||||||
padding: 20px;
|
height: -webkit-fill-available;
|
||||||
|
height: stretch;
|
||||||
|
margin: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { FX_stripe } from './fxs/stripe.js';
|
||||||
import { FX_threshold } from './fxs/threshold.js';
|
import { FX_threshold } from './fxs/threshold.js';
|
||||||
import { FX_zoomLines } from './fxs/zoomLines.js';
|
import { FX_zoomLines } from './fxs/zoomLines.js';
|
||||||
import { FX_blockNoise } from './fxs/blockNoise.js';
|
import { FX_blockNoise } from './fxs/blockNoise.js';
|
||||||
|
import { FX_fillSquare } from './fxs/fillSquare.js';
|
||||||
import type { ImageEffectorFx } from './ImageEffector.js';
|
import type { ImageEffectorFx } from './ImageEffector.js';
|
||||||
|
|
||||||
export const FXS = [
|
export const FXS = [
|
||||||
|
@ -36,4 +37,5 @@ export const FXS = [
|
||||||
FX_chromaticAberration,
|
FX_chromaticAberration,
|
||||||
FX_tearing,
|
FX_tearing,
|
||||||
FX_blockNoise,
|
FX_blockNoise,
|
||||||
|
FX_fillSquare,
|
||||||
] as const satisfies ImageEffectorFx<string, any>[];
|
] as const satisfies ImageEffectorFx<string, any>[];
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineImageEffectorFx } from '../ImageEffector.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
const shader = `#version 300 es
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
const float PI = 3.141592653589793;
|
||||||
|
const float TWO_PI = 6.283185307179586;
|
||||||
|
const float HALF_PI = 1.5707963267948966;
|
||||||
|
|
||||||
|
in vec2 in_uv;
|
||||||
|
uniform sampler2D in_texture;
|
||||||
|
uniform vec2 in_resolution;
|
||||||
|
uniform vec2 u_offset;
|
||||||
|
uniform vec2 u_scale;
|
||||||
|
uniform float u_angle;
|
||||||
|
uniform vec3 u_color;
|
||||||
|
uniform float u_opacity;
|
||||||
|
out vec4 out_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 in_color = texture(in_texture, in_uv);
|
||||||
|
//float x_ratio = max(in_resolution.x / in_resolution.y, 1.0);
|
||||||
|
//float y_ratio = max(in_resolution.y / in_resolution.x, 1.0);
|
||||||
|
|
||||||
|
float angle = -(u_angle * PI);
|
||||||
|
vec2 centeredUv = in_uv - vec2(0.5, 0.5) - u_offset;
|
||||||
|
vec2 rotatedUV = vec2(
|
||||||
|
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
|
||||||
|
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
|
||||||
|
) + u_offset;
|
||||||
|
|
||||||
|
bool isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y;
|
||||||
|
|
||||||
|
out_color = isInside ? vec4(
|
||||||
|
mix(in_color.r, u_color.r, u_opacity),
|
||||||
|
mix(in_color.g, u_color.g, u_opacity),
|
||||||
|
mix(in_color.b, u_color.b, u_opacity),
|
||||||
|
in_color.a
|
||||||
|
) : in_color;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FX_fillSquare = defineImageEffectorFx({
|
||||||
|
id: 'fillSquare',
|
||||||
|
name: i18n.ts._imageEffector._fxs.fillSquare,
|
||||||
|
shader,
|
||||||
|
uniforms: ['offset', 'scale', 'angle', 'color', 'opacity'] as const,
|
||||||
|
params: {
|
||||||
|
offsetX: {
|
||||||
|
label: i18n.ts._imageEffector._fxProps.offset + ' X',
|
||||||
|
type: 'number',
|
||||||
|
default: 0.0,
|
||||||
|
min: -1.0,
|
||||||
|
max: 1.0,
|
||||||
|
step: 0.01,
|
||||||
|
toViewValue: v => Math.round(v * 100) + '%',
|
||||||
|
},
|
||||||
|
offsetY: {
|
||||||
|
label: i18n.ts._imageEffector._fxProps.offset + ' Y',
|
||||||
|
type: 'number',
|
||||||
|
default: 0.0,
|
||||||
|
min: -1.0,
|
||||||
|
max: 1.0,
|
||||||
|
step: 0.01,
|
||||||
|
toViewValue: v => Math.round(v * 100) + '%',
|
||||||
|
},
|
||||||
|
scaleX: {
|
||||||
|
label: i18n.ts._imageEffector._fxProps.scale + ' X',
|
||||||
|
type: 'number',
|
||||||
|
default: 0.5,
|
||||||
|
min: 0.0,
|
||||||
|
max: 1.0,
|
||||||
|
step: 0.01,
|
||||||
|
toViewValue: v => Math.round(v * 100) + '%',
|
||||||
|
},
|
||||||
|
scaleY: {
|
||||||
|
label: i18n.ts._imageEffector._fxProps.scale + ' Y',
|
||||||
|
type: 'number',
|
||||||
|
default: 0.5,
|
||||||
|
min: 0.0,
|
||||||
|
max: 1.0,
|
||||||
|
step: 0.01,
|
||||||
|
toViewValue: v => Math.round(v * 100) + '%',
|
||||||
|
},
|
||||||
|
angle: {
|
||||||
|
label: i18n.ts._imageEffector._fxProps.angle,
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
min: -1.0,
|
||||||
|
max: 1.0,
|
||||||
|
step: 0.01,
|
||||||
|
toViewValue: v => Math.round(v * 90) + '°',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
label: i18n.ts._imageEffector._fxProps.color,
|
||||||
|
type: 'color',
|
||||||
|
default: [1, 1, 1],
|
||||||
|
},
|
||||||
|
opacity: {
|
||||||
|
label: i18n.ts._imageEffector._fxProps.opacity,
|
||||||
|
type: 'number',
|
||||||
|
default: 1.0,
|
||||||
|
min: 0.0,
|
||||||
|
max: 1.0,
|
||||||
|
step: 0.01,
|
||||||
|
toViewValue: v => Math.round(v * 100) + '%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
main: ({ gl, u, params }) => {
|
||||||
|
gl.uniform2f(u.offset, params.offsetX / 2, params.offsetY / 2);
|
||||||
|
gl.uniform2f(u.scale, params.scaleX / 2, params.scaleY / 2);
|
||||||
|
gl.uniform1f(u.angle, params.angle / 2);
|
||||||
|
gl.uniform3f(u.color, params.color[0], params.color[1], params.color[2]);
|
||||||
|
gl.uniform1f(u.opacity, params.opacity);
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue