watermark
This commit is contained in:
parent
f45383220d
commit
4cbe741f86
|
@ -12235,10 +12235,18 @@ export interface Locale extends ILocale {
|
||||||
* テキスト
|
* テキスト
|
||||||
*/
|
*/
|
||||||
"text": string;
|
"text": string;
|
||||||
|
/**
|
||||||
|
* 二次元コード (アカウント)
|
||||||
|
*/
|
||||||
|
"accountQr": string;
|
||||||
/**
|
/**
|
||||||
* 位置
|
* 位置
|
||||||
*/
|
*/
|
||||||
"position": string;
|
"position": string;
|
||||||
|
/**
|
||||||
|
* マージン
|
||||||
|
*/
|
||||||
|
"margin": string;
|
||||||
/**
|
/**
|
||||||
* タイプ
|
* タイプ
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3275,7 +3275,9 @@ _watermarkEditor:
|
||||||
opacity: "不透明度"
|
opacity: "不透明度"
|
||||||
scale: "サイズ"
|
scale: "サイズ"
|
||||||
text: "テキスト"
|
text: "テキスト"
|
||||||
|
accountQr: "二次元コード (アカウント)"
|
||||||
position: "位置"
|
position: "位置"
|
||||||
|
margin: "マージン"
|
||||||
type: "タイプ"
|
type: "タイプ"
|
||||||
image: "画像"
|
image: "画像"
|
||||||
advanced: "高度"
|
advanced: "高度"
|
||||||
|
|
|
@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root]">
|
<div :class="[$style.root]">
|
||||||
<div :class="$style.items">
|
<div :class="$style.items">
|
||||||
<button class="_button" :class="[$style.item, x === 'left' && y === 'top' ? $style.active : null]" @click="() => { x = 'left'; y = 'top'; }"><i class="ti ti-align-box-left-top"></i></button>
|
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'top' ? $style.active : null]" @click="() => { x = 'left'; y = 'top'; }"><i class="ti ti-arrow-up-left"></i></button>
|
||||||
<button class="_button" :class="[$style.item, x === 'center' && y === 'top' ? $style.active : null]" @click="() => { x = 'center'; y = 'top'; }"><i class="ti ti-align-box-center-top"></i></button>
|
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'top' ? $style.active : null]" @click="() => { x = 'center'; y = 'top'; }"><i class="ti ti-arrow-up"></i></button>
|
||||||
<button class="_button" :class="[$style.item, x === 'right' && y === 'top' ? $style.active : null]" @click="() => { x = 'right'; y = 'top'; }"><i class="ti ti-align-box-right-top"></i></button>
|
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'top' ? $style.active : null]" @click="() => { x = 'right'; y = 'top'; }"><i class="ti ti-arrow-up-right"></i></button>
|
||||||
<button class="_button" :class="[$style.item, x === 'left' && y === 'center' ? $style.active : null]" @click="() => { x = 'left'; y = 'center'; }"><i class="ti ti-align-box-left-middle"></i></button>
|
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'center' ? $style.active : null]" @click="() => { x = 'left'; y = 'center'; }"><i class="ti ti-arrow-left"></i></button>
|
||||||
<button class="_button" :class="[$style.item, x === 'center' && y === 'center' ? $style.active : null]" @click="() => { x = 'center'; y = 'center'; }"><i class="ti ti-align-box-center-middle"></i></button>
|
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'center' ? $style.active : null]" @click="() => { x = 'center'; y = 'center'; }"><i class="ti ti-focus-2"></i></button>
|
||||||
<button class="_button" :class="[$style.item, x === 'right' && y === 'center' ? $style.active : null]" @click="() => { x = 'right'; y = 'center'; }"><i class="ti ti-align-box-right-middle"></i></button>
|
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'center' ? $style.active : null]" @click="() => { x = 'right'; y = 'center'; }"><i class="ti ti-arrow-right"></i></button>
|
||||||
<button class="_button" :class="[$style.item, x === 'left' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'left'; y = 'bottom'; }"><i class="ti ti-align-box-left-bottom"></i></button>
|
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'left'; y = 'bottom'; }"><i class="ti ti-arrow-down-left"></i></button>
|
||||||
<button class="_button" :class="[$style.item, x === 'center' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'center'; y = 'bottom'; }"><i class="ti ti-align-box-center-bottom"></i></button>
|
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'center'; y = 'bottom'; }"><i class="ti ti-arrow-down"></i></button>
|
||||||
<button class="_button" :class="[$style.item, x === 'right' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'right'; y = 'bottom'; }"><i class="ti ti-align-box-right-bottom"></i></button>
|
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'right'; y = 'bottom'; }"><i class="ti ti-arrow-down-right"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -18,6 +18,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
></MkPositionSelector>
|
></MkPositionSelector>
|
||||||
</FormSlot>
|
</FormSlot>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
:modelValue="layer.align.margin ?? 0"
|
||||||
|
:min="0"
|
||||||
|
:max="0.5"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
@update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'account-qr' }>).align.margin = v"
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
<MkRange
|
<MkRange
|
||||||
v-model="layer.scale"
|
v-model="layer.scale"
|
||||||
:min="0"
|
:min="0"
|
||||||
|
@ -66,6 +78,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
></MkPositionSelector>
|
></MkPositionSelector>
|
||||||
</FormSlot>
|
</FormSlot>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
:modelValue="layer.align.margin ?? 0"
|
||||||
|
:min="0"
|
||||||
|
:max="0.5"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
@update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'account-qr' }>).align.margin = v"
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
<MkRange
|
<MkRange
|
||||||
v-model="layer.scale"
|
v-model="layer.scale"
|
||||||
:min="0"
|
:min="0"
|
||||||
|
@ -107,6 +131,50 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="layer.type === 'account-qr'">
|
||||||
|
<FormSlot>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.position }}</template>
|
||||||
|
<MkPositionSelector
|
||||||
|
v-model:x="layer.align.x"
|
||||||
|
v-model:y="layer.align.y"
|
||||||
|
></MkPositionSelector>
|
||||||
|
</FormSlot>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
:modelValue="layer.align.margin ?? 0"
|
||||||
|
:min="0"
|
||||||
|
:max="0.5"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
@update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'account-qr' }>).align.margin = v"
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.scale"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.opacity"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else-if="layer.type === 'stripe'">
|
<template v-else-if="layer.type === 'stripe'">
|
||||||
<MkRange
|
<MkRange
|
||||||
v-model="layer.frequency"
|
v-model="layer.frequency"
|
||||||
|
|
|
@ -30,22 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.controls">
|
<div :class="$style.controls">
|
||||||
<div class="_spacer _gaps">
|
<div class="_spacer _gaps">
|
||||||
<MkSelect v-model="type" :items="typeDef">
|
<div class="_gaps_s">
|
||||||
<template #label>{{ i18n.ts._watermarkEditor.type }}</template>
|
|
||||||
</MkSelect>
|
|
||||||
|
|
||||||
<div v-if="type === 'text' || type === 'image'">
|
|
||||||
<XLayer
|
|
||||||
v-for="(layer, i) in preset.layers"
|
|
||||||
:key="layer.id"
|
|
||||||
v-model:layer="preset.layers[i]"
|
|
||||||
></XLayer>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="type === 'advanced'" class="_gaps_s">
|
|
||||||
<MkFolder v-for="(layer, i) in preset.layers" :key="layer.id" :defaultOpen="false" :canPage="false">
|
<MkFolder v-for="(layer, i) in preset.layers" :key="layer.id" :defaultOpen="false" :canPage="false">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div v-if="layer.type === 'text'">{{ i18n.ts._watermarkEditor.text }}</div>
|
<div v-if="layer.type === 'text'">{{ i18n.ts._watermarkEditor.text }}</div>
|
||||||
<div v-if="layer.type === 'image'">{{ i18n.ts._watermarkEditor.image }}</div>
|
<div v-if="layer.type === 'image'">{{ i18n.ts._watermarkEditor.image }}</div>
|
||||||
|
<div v-if="layer.type === 'account-qr'">{{ i18n.ts._watermarkEditor.accountQr }}</div>
|
||||||
<div v-if="layer.type === 'stripe'">{{ i18n.ts._watermarkEditor.stripe }}</div>
|
<div v-if="layer.type === 'stripe'">{{ i18n.ts._watermarkEditor.stripe }}</div>
|
||||||
<div v-if="layer.type === 'polkadot'">{{ i18n.ts._watermarkEditor.polkadot }}</div>
|
<div v-if="layer.type === 'polkadot'">{{ i18n.ts._watermarkEditor.polkadot }}</div>
|
||||||
<div v-if="layer.type === 'checker'">{{ i18n.ts._watermarkEditor.checker }}</div>
|
<div v-if="layer.type === 'checker'">{{ i18n.ts._watermarkEditor.checker }}</div>
|
||||||
|
@ -95,7 +85,7 @@ function createTextLayer(): WatermarkPreset['layers'][number] {
|
||||||
id: genId(),
|
id: genId(),
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: `(c) @${$i.username}`,
|
text: `(c) @${$i.username}`,
|
||||||
align: { x: 'right', y: 'bottom' },
|
align: { x: 'right', y: 'bottom', margin: 0 },
|
||||||
scale: 0.3,
|
scale: 0.3,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
opacity: 0.75,
|
opacity: 0.75,
|
||||||
|
@ -109,7 +99,7 @@ function createImageLayer(): WatermarkPreset['layers'][number] {
|
||||||
type: 'image',
|
type: 'image',
|
||||||
imageId: null,
|
imageId: null,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
align: { x: 'right', y: 'bottom' },
|
align: { x: 'right', y: 'bottom', margin: 0 },
|
||||||
scale: 0.3,
|
scale: 0.3,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
opacity: 0.75,
|
opacity: 0.75,
|
||||||
|
@ -118,6 +108,16 @@ function createImageLayer(): WatermarkPreset['layers'][number] {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createAccountQrLayer(): WatermarkPreset['layers'][number] {
|
||||||
|
return {
|
||||||
|
id: genId(),
|
||||||
|
type: 'account-qr',
|
||||||
|
align: { x: 'right', y: 'bottom', margin: 0 },
|
||||||
|
scale: 0.3,
|
||||||
|
opacity: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function createStripeLayer(): WatermarkPreset['layers'][number] {
|
function createStripeLayer(): WatermarkPreset['layers'][number] {
|
||||||
return {
|
return {
|
||||||
id: genId(),
|
id: genId(),
|
||||||
|
@ -165,7 +165,7 @@ const props = defineProps<{
|
||||||
const preset = reactive<WatermarkPreset>(deepClone(props.preset) ?? {
|
const preset = reactive<WatermarkPreset>(deepClone(props.preset) ?? {
|
||||||
id: genId(),
|
id: genId(),
|
||||||
name: '',
|
name: '',
|
||||||
layers: [createTextLayer()],
|
layers: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -187,28 +187,6 @@ async function cancel() {
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
|
||||||
model: type,
|
|
||||||
def: typeDef,
|
|
||||||
} = useMkSelect({
|
|
||||||
items: [
|
|
||||||
{ label: i18n.ts._watermarkEditor.text, value: 'text' },
|
|
||||||
{ label: i18n.ts._watermarkEditor.image, value: 'image' },
|
|
||||||
{ label: i18n.ts._watermarkEditor.advanced, value: 'advanced' },
|
|
||||||
],
|
|
||||||
initialValue: preset.layers.length > 1 ? 'advanced' : preset.layers[0].type,
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(type, () => {
|
|
||||||
if (type.value === 'text') {
|
|
||||||
preset.layers = [createTextLayer()];
|
|
||||||
} else if (type.value === 'image') {
|
|
||||||
preset.layers = [createImageLayer()];
|
|
||||||
} else if (type.value === 'advanced') {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(preset, async (newValue, oldValue) => {
|
watch(preset, async (newValue, oldValue) => {
|
||||||
if (renderer != null) {
|
if (renderer != null) {
|
||||||
renderer.setLayers(preset.layers);
|
renderer.setLayers(preset.layers);
|
||||||
|
@ -338,6 +316,11 @@ function addLayer(ev: MouseEvent) {
|
||||||
action: () => {
|
action: () => {
|
||||||
preset.layers.push(createImageLayer());
|
preset.layers.push(createImageLayer());
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts._watermarkEditor.accountQr,
|
||||||
|
action: () => {
|
||||||
|
preset.layers.push(createAccountQrLayer());
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
text: i18n.ts._watermarkEditor.stripe,
|
text: i18n.ts._watermarkEditor.stripe,
|
||||||
action: () => {
|
action: () => {
|
||||||
|
|
|
@ -3,8 +3,11 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import QRCodeStyling from 'qr-code-styling';
|
||||||
|
import { url, host } from '@@/js/config.js';
|
||||||
import { getProxiedImageUrl } from '../media-proxy.js';
|
import { getProxiedImageUrl } from '../media-proxy.js';
|
||||||
import { initShaderProgram } from '../webgl.js';
|
import { initShaderProgram } from '../webgl.js';
|
||||||
|
import { ensureSignin } from '@/i.js';
|
||||||
|
|
||||||
export type ImageEffectorRGB = [r: number, g: number, b: number];
|
export type ImageEffectorRGB = [r: number, g: number, b: number];
|
||||||
|
|
||||||
|
@ -48,6 +51,7 @@ interface AlignParamDef extends CommonParamDef {
|
||||||
default: {
|
default: {
|
||||||
x: 'left' | 'center' | 'right';
|
x: 'left' | 'center' | 'right';
|
||||||
y: 'top' | 'center' | 'bottom';
|
y: 'top' | 'center' | 'bottom';
|
||||||
|
margin?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -58,7 +62,13 @@ interface SeedParamDef extends CommonParamDef {
|
||||||
|
|
||||||
interface TextureParamDef extends CommonParamDef {
|
interface TextureParamDef extends CommonParamDef {
|
||||||
type: 'texture';
|
type: 'texture';
|
||||||
default: { type: 'text'; text: string | null; } | { type: 'url'; url: string | null; } | null;
|
default: {
|
||||||
|
type: 'text'; text: string | null;
|
||||||
|
} | {
|
||||||
|
type: 'url'; url: string | null;
|
||||||
|
} | {
|
||||||
|
type: 'account-qr';
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ColorParamDef extends CommonParamDef {
|
interface ColorParamDef extends CommonParamDef {
|
||||||
|
@ -324,7 +334,11 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
|
||||||
|
|
||||||
if (_DEV_) console.log(`Baking texture of <${textureKey}>...`);
|
if (_DEV_) console.log(`Baking texture of <${textureKey}>...`);
|
||||||
|
|
||||||
const texture = v.type === 'text' ? await createTextureFromText(this.gl, v.text) : v.type === 'url' ? await createTextureFromUrl(this.gl, v.url) : null;
|
const texture =
|
||||||
|
v.type === 'text' ? await createTextureFromText(this.gl, v.text) :
|
||||||
|
v.type === 'url' ? await createTextureFromUrl(this.gl, v.url) :
|
||||||
|
v.type === 'account-qr' ? await createTextureFromAccountQr(this.gl) :
|
||||||
|
null;
|
||||||
if (texture == null) continue;
|
if (texture == null) continue;
|
||||||
|
|
||||||
this.paramTextures.set(textureKey, texture);
|
this.paramTextures.set(textureKey, texture);
|
||||||
|
@ -352,7 +366,12 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
|
||||||
|
|
||||||
private getTextureKeyForParam(v: ParamTypeToPrimitive['texture']) {
|
private getTextureKeyForParam(v: ParamTypeToPrimitive['texture']) {
|
||||||
if (v == null) return '';
|
if (v == null) return '';
|
||||||
return v.type === 'text' ? `text:${v.text}` : v.type === 'url' ? `url:${v.url}` : '';
|
return (
|
||||||
|
v.type === 'text' ? `text:${v.text}` :
|
||||||
|
v.type === 'url' ? `url:${v.url}` :
|
||||||
|
v.type === 'account-qr' ? 'account-qr' :
|
||||||
|
''
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -467,3 +486,53 @@ async function createTextureFromText(gl: WebGL2RenderingContext, text: string |
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createTextureFromAccountQr(gl: WebGL2RenderingContext, resolution = 512): Promise<{ texture: WebGLTexture, width: number, height: number } | null> {
|
||||||
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
const qrCodeInstance = new QRCodeStyling({
|
||||||
|
width: resolution,
|
||||||
|
height: resolution,
|
||||||
|
margin: 42,
|
||||||
|
type: 'canvas',
|
||||||
|
data: `${url}/users/${$i.id}`,
|
||||||
|
image: $i.avatarUrl,
|
||||||
|
qrOptions: {
|
||||||
|
typeNumber: 0,
|
||||||
|
mode: 'Byte',
|
||||||
|
errorCorrectionLevel: 'H',
|
||||||
|
},
|
||||||
|
imageOptions: {
|
||||||
|
hideBackgroundDots: true,
|
||||||
|
imageSize: 0.3,
|
||||||
|
margin: 16,
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
},
|
||||||
|
dotsOptions: {
|
||||||
|
type: 'dots',
|
||||||
|
},
|
||||||
|
cornersDotOptions: {
|
||||||
|
type: 'dot',
|
||||||
|
},
|
||||||
|
cornersSquareOptions: {
|
||||||
|
type: 'extra-rounded',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = await qrCodeInstance.getRawData('png') as Blob | null;
|
||||||
|
if (blob == null) return null;
|
||||||
|
|
||||||
|
const data = await window.createImageBitmap(blob);
|
||||||
|
|
||||||
|
const texture = createTexture(gl);
|
||||||
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, resolution, resolution, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
texture,
|
||||||
|
width: resolution,
|
||||||
|
height: resolution,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ uniform float u_opacity;
|
||||||
uniform bool u_repeat;
|
uniform bool u_repeat;
|
||||||
uniform int u_alignX; // 0: left, 1: center, 2: right
|
uniform int u_alignX; // 0: left, 1: center, 2: right
|
||||||
uniform int u_alignY; // 0: top, 1: center, 2: bottom
|
uniform int u_alignY; // 0: top, 1: center, 2: bottom
|
||||||
|
uniform float u_alignMargin;
|
||||||
uniform int u_fitMode; // 0: contain, 1: cover
|
uniform int u_fitMode; // 0: contain, 1: cover
|
||||||
out vec4 out_color;
|
out vec4 out_color;
|
||||||
|
|
||||||
|
@ -51,6 +52,9 @@ void main() {
|
||||||
float x_offset = u_alignX == 0 ? x_scale / 2.0 : u_alignX == 2 ? 1.0 - (x_scale / 2.0) : 0.5;
|
float x_offset = u_alignX == 0 ? x_scale / 2.0 : u_alignX == 2 ? 1.0 - (x_scale / 2.0) : 0.5;
|
||||||
float y_offset = u_alignY == 0 ? y_scale / 2.0 : u_alignY == 2 ? 1.0 - (y_scale / 2.0) : 0.5;
|
float y_offset = u_alignY == 0 ? y_scale / 2.0 : u_alignY == 2 ? 1.0 - (y_scale / 2.0) : 0.5;
|
||||||
|
|
||||||
|
x_offset += (u_alignX == 0 ? 1.0 : u_alignX == 2 ? -1.0 : 0.0) * u_alignMargin;
|
||||||
|
y_offset += (u_alignY == 0 ? 1.0 : u_alignY == 2 ? -1.0 : 0.0) * u_alignMargin;
|
||||||
|
|
||||||
float angle = -(u_angle * PI);
|
float angle = -(u_angle * PI);
|
||||||
vec2 center = vec2(x_offset, y_offset);
|
vec2 center = vec2(x_offset, y_offset);
|
||||||
//vec2 centeredUv = (in_uv - center) * vec2(in_x_ratio, in_y_ratio);
|
//vec2 centeredUv = (in_uv - center) * vec2(in_x_ratio, in_y_ratio);
|
||||||
|
@ -86,7 +90,7 @@ export const FX_watermarkPlacement = defineImageEffectorFx({
|
||||||
id: 'watermarkPlacement',
|
id: 'watermarkPlacement',
|
||||||
name: '(internal)',
|
name: '(internal)',
|
||||||
shader,
|
shader,
|
||||||
uniforms: ['texture_watermark', 'resolution_watermark', 'scale', 'angle', 'opacity', 'repeat', 'alignX', 'alignY', 'fitMode'] as const,
|
uniforms: ['texture_watermark', 'resolution_watermark', 'scale', 'angle', 'opacity', 'repeat', 'alignX', 'alignY', 'alignMargin', 'fitMode'] as const,
|
||||||
params: {
|
params: {
|
||||||
cover: {
|
cover: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
@ -112,7 +116,7 @@ export const FX_watermarkPlacement = defineImageEffectorFx({
|
||||||
},
|
},
|
||||||
align: {
|
align: {
|
||||||
type: 'align',
|
type: 'align',
|
||||||
default: { x: 'right', y: 'bottom' },
|
default: { x: 'right', y: 'bottom', margin: 0 },
|
||||||
},
|
},
|
||||||
opacity: {
|
opacity: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
@ -143,6 +147,7 @@ export const FX_watermarkPlacement = defineImageEffectorFx({
|
||||||
gl.uniform1i(u.repeat, params.repeat ? 1 : 0);
|
gl.uniform1i(u.repeat, params.repeat ? 1 : 0);
|
||||||
gl.uniform1i(u.alignX, params.align.x === 'left' ? 0 : params.align.x === 'right' ? 2 : 1);
|
gl.uniform1i(u.alignX, params.align.x === 'left' ? 0 : params.align.x === 'right' ? 2 : 1);
|
||||||
gl.uniform1i(u.alignY, params.align.y === 'top' ? 0 : params.align.y === 'bottom' ? 2 : 1);
|
gl.uniform1i(u.alignY, params.align.y === 'top' ? 0 : params.align.y === 'bottom' ? 2 : 1);
|
||||||
|
gl.uniform1f(u.alignMargin, params.align.margin ?? 0);
|
||||||
gl.uniform1i(u.fitMode, params.cover ? 1 : 0);
|
gl.uniform1i(u.fitMode, params.cover ? 1 : 0);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
|
||||||
import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
|
import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
|
||||||
import { FX_stripe } from '@/utility/image-effector/fxs/stripe.js';
|
import { FX_stripe } from '@/utility/image-effector/fxs/stripe.js';
|
||||||
import { FX_polkadot } from '@/utility/image-effector/fxs/polkadot.js';
|
import { FX_polkadot } from '@/utility/image-effector/fxs/polkadot.js';
|
||||||
import { FX_checker } from '@/utility/image-effector/fxs/checker.js';
|
import { FX_checker } from '@/utility/image-effector/fxs/checker.js';
|
||||||
import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
|
|
||||||
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
|
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
|
||||||
|
|
||||||
const WATERMARK_FXS = [
|
const WATERMARK_FXS = [
|
||||||
|
@ -17,6 +17,8 @@ const WATERMARK_FXS = [
|
||||||
FX_checker,
|
FX_checker,
|
||||||
] as const satisfies ImageEffectorFx<string, any>[];
|
] as const satisfies ImageEffectorFx<string, any>[];
|
||||||
|
|
||||||
|
type Align = { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom'; margin?: number; };
|
||||||
|
|
||||||
export type WatermarkPreset = {
|
export type WatermarkPreset = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -27,7 +29,7 @@ export type WatermarkPreset = {
|
||||||
repeat: boolean;
|
repeat: boolean;
|
||||||
scale: number;
|
scale: number;
|
||||||
angle: number;
|
angle: number;
|
||||||
align: { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom' };
|
align: Align;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
} | {
|
} | {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -38,7 +40,13 @@ export type WatermarkPreset = {
|
||||||
repeat: boolean;
|
repeat: boolean;
|
||||||
scale: number;
|
scale: number;
|
||||||
angle: number;
|
angle: number;
|
||||||
align: { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom' };
|
align: Align;
|
||||||
|
opacity: number;
|
||||||
|
} | {
|
||||||
|
id: string;
|
||||||
|
type: 'account-qr';
|
||||||
|
scale: number;
|
||||||
|
align: Align;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
} | {
|
} | {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -125,6 +133,22 @@ export class WatermarkRenderer {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
} else if (layer.type === 'account-qr') {
|
||||||
|
return {
|
||||||
|
fxId: 'watermarkPlacement',
|
||||||
|
id: layer.id,
|
||||||
|
params: {
|
||||||
|
repeat: false,
|
||||||
|
scale: layer.scale,
|
||||||
|
align: layer.align,
|
||||||
|
angle: 0,
|
||||||
|
opacity: layer.opacity,
|
||||||
|
cover: false,
|
||||||
|
watermark: {
|
||||||
|
type: 'account-qr',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
} else if (layer.type === 'stripe') {
|
} else if (layer.type === 'stripe') {
|
||||||
return {
|
return {
|
||||||
fxId: 'stripe',
|
fxId: 'stripe',
|
||||||
|
@ -164,7 +188,7 @@ export class WatermarkRenderer {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown layer type`);
|
throw new Error(`Unrecognized layer type: ${(layer as any).type}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue