diff options
| author | tamaina <tamaina@hotmail.co.jp> | 2025-09-19 21:02:30 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-09-19 21:02:30 +0900 |
| commit | 42b2aea53364c57c39ebb953359ece4b7b0017a5 (patch) | |
| tree | 8abe7de01705ed3fb6324c6a174162c7bb9288f2 /packages/frontend/src/utility/image-effector | |
| parent | 🎨 (diff) | |
| download | misskey-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.ts | 75 | ||||
| -rw-r--r-- | packages/frontend/src/utility/image-effector/fxs/watermarkPlacement.ts | 9 |
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); }, }); |