{{ i18n.ts._watermarkEditor.text }}
{{ i18n.ts._watermarkEditor.image }}
+ {{ i18n.ts._watermarkEditor.accountQr }}
{{ i18n.ts._watermarkEditor.stripe }}
{{ i18n.ts._watermarkEditor.polkadot }}
{{ i18n.ts._watermarkEditor.checker }}
@@ -95,7 +85,7 @@ function createTextLayer(): WatermarkPreset['layers'][number] {
id: genId(),
type: 'text',
text: `(c) @${$i.username}`,
- align: { x: 'right', y: 'bottom' },
+ align: { x: 'right', y: 'bottom', margin: 0 },
scale: 0.3,
angle: 0,
opacity: 0.75,
@@ -109,7 +99,7 @@ function createImageLayer(): WatermarkPreset['layers'][number] {
type: 'image',
imageId: null,
imageUrl: null,
- align: { x: 'right', y: 'bottom' },
+ align: { x: 'right', y: 'bottom', margin: 0 },
scale: 0.3,
angle: 0,
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] {
return {
id: genId(),
@@ -165,7 +165,7 @@ const props = defineProps<{
const preset = reactive(deepClone(props.preset) ?? {
id: genId(),
name: '',
- layers: [createTextLayer()],
+ layers: [],
});
const emit = defineEmits<{
@@ -187,28 +187,6 @@ async function cancel() {
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) => {
if (renderer != null) {
renderer.setLayers(preset.layers);
@@ -338,6 +316,11 @@ function addLayer(ev: MouseEvent) {
action: () => {
preset.layers.push(createImageLayer());
},
+ }, {
+ text: i18n.ts._watermarkEditor.accountQr,
+ action: () => {
+ preset.layers.push(createAccountQrLayer());
+ },
}, {
text: i18n.ts._watermarkEditor.stripe,
action: () => {
diff --git a/packages/frontend/src/utility/image-effector/ImageEffector.ts b/packages/frontend/src/utility/image-effector/ImageEffector.ts
index 66b4d1026c..f58d53f99e 100644
--- a/packages/frontend/src/utility/image-effector/ImageEffector.ts
+++ b/packages/frontend/src/utility/image-effector/ImageEffector.ts
@@ -3,8 +3,11 @@
* 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 { initShaderProgram } from '../webgl.js';
+import { ensureSignin } from '@/i.js';
export type ImageEffectorRGB = [r: number, g: number, b: number];
@@ -48,6 +51,7 @@ interface AlignParamDef extends CommonParamDef {
default: {
x: 'left' | 'center' | 'right';
y: 'top' | 'center' | 'bottom';
+ margin?: number;
};
};
@@ -58,7 +62,13 @@ interface SeedParamDef extends CommonParamDef {
interface TextureParamDef extends CommonParamDef {
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 {
@@ -324,7 +334,11 @@ export class ImageEffector...`);
- 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;
this.paramTextures.set(textureKey, texture);
@@ -352,7 +366,12 @@ export class ImageEffector {
+ 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,
+ };
+}
diff --git a/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts b/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts
index 9b79e2bf94..f79acb44b0 100644
--- a/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts
+++ b/packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts
@@ -23,6 +23,7 @@ uniform float u_opacity;
uniform bool u_repeat;
uniform int u_alignX; // 0: left, 1: center, 2: right
uniform int u_alignY; // 0: top, 1: center, 2: bottom
+uniform float u_alignMargin;
uniform int u_fitMode; // 0: contain, 1: cover
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 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);
vec2 center = vec2(x_offset, y_offset);
//vec2 centeredUv = (in_uv - center) * vec2(in_x_ratio, in_y_ratio);
@@ -86,7 +90,7 @@ export const FX_watermarkPlacement = defineImageEffectorFx({
id: 'watermarkPlacement',
name: '(internal)',
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: {
cover: {
type: 'boolean',
@@ -112,7 +116,7 @@ export const FX_watermarkPlacement = defineImageEffectorFx({
},
align: {
type: 'align',
- default: { x: 'right', y: 'bottom' },
+ default: { x: 'right', y: 'bottom', margin: 0 },
},
opacity: {
type: 'number',
@@ -143,6 +147,7 @@ export const FX_watermarkPlacement = defineImageEffectorFx({
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.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);
},
});
diff --git a/packages/frontend/src/utility/watermark.ts b/packages/frontend/src/utility/watermark.ts
index 75807b30c4..edd6ed2122 100644
--- a/packages/frontend/src/utility/watermark.ts
+++ b/packages/frontend/src/utility/watermark.ts
@@ -3,11 +3,11 @@
* 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_stripe } from '@/utility/image-effector/fxs/stripe.js';
import { FX_polkadot } from '@/utility/image-effector/fxs/polkadot.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';
const WATERMARK_FXS = [
@@ -17,6 +17,8 @@ const WATERMARK_FXS = [
FX_checker,
] as const satisfies ImageEffectorFx[];
+type Align = { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom'; margin?: number; };
+
export type WatermarkPreset = {
id: string;
name: string;
@@ -27,7 +29,7 @@ export type WatermarkPreset = {
repeat: boolean;
scale: number;
angle: number;
- align: { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom' };
+ align: Align;
opacity: number;
} | {
id: string;
@@ -38,7 +40,13 @@ export type WatermarkPreset = {
repeat: boolean;
scale: 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;
} | {
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') {
return {
fxId: 'stripe',
@@ -164,7 +188,7 @@ export class WatermarkRenderer {
},
};
} else {
- throw new Error(`Unknown layer type`);
+ throw new Error(`Unrecognized layer type: ${(layer as any).type}`);
}
});
}