summaryrefslogtreecommitdiff
path: root/packages/frontend/src/utility/image-effector
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2025-09-19 21:02:30 +0900
committerGitHub <noreply@github.com>2025-09-19 21:02:30 +0900
commit42b2aea53364c57c39ebb953359ece4b7b0017a5 (patch)
tree8abe7de01705ed3fb6324c6a174162c7bb9288f2 /packages/frontend/src/utility/image-effector
parent🎨 (diff)
downloadmisskey-42b2aea53364c57c39ebb953359ece4b7b0017a5.tar.gz
misskey-42b2aea53364c57c39ebb953359ece4b7b0017a5.tar.bz2
misskey-42b2aea53364c57c39ebb953359ece4b7b0017a5.zip
feat(frontend): 自分のプロフィールページの二次元コード(QRコード)を表示し、他の人のコードを読み取りするページを追加 (#16456)
* wip (qr.show.vue) * added to navbar * qr.show.vue * fix * added to navbar * fix size * :art: * :art: * fix div warn * fix * use * 0.25 * fix?? * fix lint * clean up * ??? * ? * fix * :art: * :art: * refactor * :art: * :art: * :ar:t * :art: * iphone flip * no lazy import * :art: * :art: * :art: * ユーザー全部flipでいいや * :v: * fix * fix * fix lint * :art: * fix type * fix: local user profile url cannot be resolved with ap/show * fix: local user url with hostname could not be resolved * chore: use common utility for checking self host * wip * :art: * :art: * fix imports * fix * fix * fix * :art: * fix... * set spacer-w * :v: * 全画面でQRを読むように * fix * :art: * modify navbar.ts * start/stop on vue activation * display raw content read from qr * 端末のQRをスキャンするボタンを追加 * chore * やっぱりmfmを先に表示する * :art: * fix 18n * QRの内容は/users/:userIdにする * add spdx * use cqh * `defineProps` is a compiler macro and no longer needs to be imported. * use MkUserName * 🎨 * 🎨 * refactor * clean up * refactor * 🎨 * Update qr.show.vue * Misskeyロゴにdrop-shadowを追加 * clean up: do not use empty css * fix os.select usage * Update qr.vue * Update qr.show.vue * Update qr.show.vue * Update get-user-menu.ts * ✌️ * Update show.ts * Update ja-JP.yml * watermark * Update CHANGELOG.md * Update qr.read.vue * Update qr.read.vue * wip * Update MkWatermarkEditorDialog.Layer.vue --------- Co-authored-by: anatawa12 <anatawa12@icloud.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src/utility/image-effector')
-rw-r--r--packages/frontend/src/utility/image-effector/ImageEffector.ts75
-rw-r--r--packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts9
2 files changed, 79 insertions, 5 deletions
diff --git a/packages/frontend/src/utility/image-effector/ImageEffector.ts b/packages/frontend/src/utility/image-effector/ImageEffector.ts
index 66b4d1026c..26c74bfae5 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: 'qr'; data: string | null;
+ } | null;
};
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}>...`);
- 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 === 'qr' ? await createTextureFromQr(this.gl, { data: v.data }) :
+ null;
if (texture == null) continue;
this.paramTextures.set(textureKey, texture);
@@ -352,7 +366,12 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
private getTextureKeyForParam(v: ParamTypeToPrimitive['texture']) {
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 === 'qr' ? `qr:${v.data}` :
+ ''
+ );
}
/*
@@ -467,3 +486,53 @@ async function createTextureFromText(gl: WebGL2RenderingContext, text: string |
return info;
}
+
+async function createTextureFromQr(gl: WebGL2RenderingContext, options: { data: string | null }, 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: options.data == null || options.data === '' ? `${url}/users/${$i.id}` : options.data,
+ 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 image = 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, image);
+ 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);
},
});