Compare commits

..

2 Commits

Author SHA1 Message Date
syuilo 8d96d98550 Merge branch 'develop' into renovate/npm-happy-dom-vulnerability 2025-10-19 13:14:05 +09:00
renovate[bot] bcdd7ab384 fix(deps): update dependency happy-dom to v20 [security] 2025-10-17 00:56:32 +00:00
113 changed files with 2409 additions and 2747 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.3.0
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Setup Node.js
uses: actions/setup-node@v4.4.0
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
ref: ${{ matrix.ref }}
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
+3 -3
View File
@@ -41,7 +41,7 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'
@@ -74,7 +74,7 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'
@@ -104,7 +104,7 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
@@ -1,50 +0,0 @@
name: Request release review
on:
issue_comment:
types: [created]
jobs:
reply:
if: github.event.comment.body == '/request-release-review'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Reply
uses: actions/github-script@v6
with:
script: |
const body = `To dev team (@misskey-dev/dev):
リリースが提案されています :rocket:
GOの場合はapprove、NO GOの場合はその旨コメントをお願いいたします。
判断にあたって考慮すべき観点は、
- やり残したことはないか?
- CHANGELOGは過不足ないか?
- バージョンに問題はないか?(月跨いでいるのに更新忘れているなど)
- 再考すべき仕様・実装はないか?
- ベータ版を検証したサーバーから不具合の報告等は上がってないか?
- (セキュリティの修正や重要なバグ修正などのため)リリースを急いだ方が良いか?そうではないか?
- Actionsが落ちていないか?
などが挙げられます。
ご協力ありがとうございます :sparkles:
`
const issue_number = context.payload.issue ? context.payload.issue.number : (context.payload.pull_request && context.payload.pull_request.number)
if (!issue_number) {
console.log('No issue or PR number found in payload; skipping')
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
body,
})
}
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
if: github.event_name == 'pull_request_target'
run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)"
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
+3 -3
View File
@@ -54,7 +54,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
@@ -133,7 +133,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
@@ -177,7 +177,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
+2 -2
View File
@@ -32,7 +32,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
@@ -86,7 +86,7 @@ jobs:
#- uses: browser-actions/setup-firefox@latest
# if: ${{ matrix.browser == 'firefox' }}
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@v4.3.0
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Setup Node.js
uses: actions/setup-node@v4.4.0
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
+2 -25
View File
@@ -1,43 +1,20 @@
## 2025.10.2
### General
-
### Client
- Fix: アプリ内からキャッシュをクリアするとテーマ再適用するまでレンダリングが正しく行われない問題を修正
- Fix: 期限が無期限のアンケートに投票できない問題を修正
### Server
-
## 2025.10.1
### General
- Enhance: リモートユーザーに付与したロールバッジを表示できるように(オプトイン)
- Enhance: リモートユーザーロールバッジを表示できるように(オプトイン)
パフォーマンス上の問題からデフォルトで無効化されています。「コントロールパネル > パフォーマンス」から有効化できます。
- 依存関係の更新
### Client
- Enhance: デッキのメインカラムのヘッダをクリックしてページ上部/下部にスクロールできるように
- Enhance: 下書き/予約投稿一覧は投稿フォームのアカウントメニュー内に移動し、下書き保存は「...」メニュー内に移動されました
- Fix: カスタム絵文字画面(beta)のaliasesで使用される区切り文字が一致していないのを修正 #15614
- Fix: バナー画像の幅が表示領域と一致していない問題を修正
- Fix: 一部のブラウザでバナー画像が上下中央に表示されない問題を修正
- Fix: ナビゲーションバーの設定で削除した項目をその場で再追加できない問題を修正
- Fix: ロールポリシーによりダイレクトメッセージが無効化されている際のデッキのダイレクトメッセージカラムの挙動を改善
- Fix: 画像のマスクでタッチ操作が不安定な問題を修正
- Fix: ウォーターマークの各種挙動修正
- ウォーターマークを回転させると歪む問題を修正
- ウォーターマークを敷き詰めると上下左右反転した画像/文字が表示される問題を修正
- ウォーターマークを回転させた際に画面からはみ出た部分を考慮できるように
- Fix: 投票が終了した後に投票結果が正しく表示されない問題を修正
- Fix: ダークモードの同期が機能しない場合がある問題を修正
- Fix: iOSで動画の圧縮を行うと音声トラックが失われる問題を修正
### Server
- Enhance: 管理者/モデレーターはファイルのアップロード制限をバイパスするように
- Enhance: セキュリティの向上
-
## 2025.10.0
-3
View File
@@ -775,7 +775,6 @@ lockedAccountInfo: "Tret que establiu la visibilitat de la nota a \"Només segui
alwaysMarkSensitive: "Marcar com a sensible per defecte"
loadRawImages: "Carregar les imatges originals en comptes de miniatures "
disableShowingAnimatedImages: "No reproduir imatges animades"
disableShowingAnimatedImages_caption: "Si les imatges animades no es reprodueixen, independentment d'aquesta configuració, és possible que la configuració d'accessibilitat del navegador i el sistema operatiu, els modes d'estalvi d'energia i similars estiguin interferint."
highlightSensitiveMedia: "Ressalta els medis marcats com a sensibles"
verificationEmailSent: "S'ha enviat un correu electrònic de verificació. Fes clic a l'enllaç per completar la verificació."
notSet: "Sense definir"
@@ -1172,7 +1171,6 @@ installed: "Instal·lats "
branding: "Marca"
enableServerMachineStats: "Publicar estadístiques del maquinari del servidor"
enableIdenticonGeneration: "Activar la generació d'icones d'identificació "
showRoleBadgesOfRemoteUsers: "Mostrar insígnies de rols d'instàncies remotes "
turnOffToImprovePerformance: "Desactivant aquesta opció es pot millorar el rendiment."
createInviteCode: "Crear codi d'invitació "
createWithOptions: "Crear invitació amb opcions"
@@ -3201,7 +3199,6 @@ _watermarkEditor:
title: "Editar la marca d'aigua "
cover: "Cobrir-ho tot"
repeat: "Repetir"
preserveBoundingRect: "Ajusta'l per evitar que sobresortir en fer la rotació "
opacity: "Opacitat"
scale: "Mida"
text: "Text"
-3
View File
@@ -775,7 +775,6 @@ lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", yo
alwaysMarkSensitive: "Mark as sensitive by default"
loadRawImages: "Load original images instead of showing thumbnails"
disableShowingAnimatedImages: "Don't play animated images"
disableShowingAnimatedImages_caption: "If animated images do not play even if this setting is disabled, it may be due to browser or OS accessibility settings, power-saving settings, or similar factors."
highlightSensitiveMedia: "Highlight sensitive media"
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
notSet: "Not set"
@@ -1172,7 +1171,6 @@ installed: "Installed"
branding: "Branding"
enableServerMachineStats: "Publish server hardware stats"
enableIdenticonGeneration: "Enable user identicon generation"
showRoleBadgesOfRemoteUsers: "Display the role badges assigned to remote users"
turnOffToImprovePerformance: "Turning this off can increase performance."
createInviteCode: "Generate invite"
createWithOptions: "Generate with options"
@@ -3201,7 +3199,6 @@ _watermarkEditor:
title: "Edit Watermark"
cover: "Cover everything"
repeat: "spread all over"
preserveBoundingRect: "Adjust to prevent overflow when rotating"
opacity: "Opacity"
scale: "Size"
text: "Text"
-3
View File
@@ -775,7 +775,6 @@ lockedAccountInfo: "A menos que configures la visibilidad de tus notas como \"S
alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por defecto"
loadRawImages: "Cargar las imágenes originales en lugar de mostrar las miniaturas"
disableShowingAnimatedImages: "No reproducir imágenes animadas"
disableShowingAnimatedImages_caption: "Si las imágenes animadas no se reproducen independientemente de esta configuración, es posible que la configuración de accesibilidad del navegador o del sistema operativo, los modos de ahorro de energía o funciones similares estén interfiriendo."
highlightSensitiveMedia: "Resaltar medios marcados como sensibles"
verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por favor, acceda al enlace proporcionado en el correo electrónico para completar la configuración."
notSet: "Sin especificar"
@@ -1172,7 +1171,6 @@ installed: "Instalado"
branding: "Marca"
enableServerMachineStats: "Publicar estadísticas de hardware del servidor"
enableIdenticonGeneration: "Activar generación de identicon por usuario"
showRoleBadgesOfRemoteUsers: "Mostrar la insignia de rol asignada a los usuarios remotos."
turnOffToImprovePerformance: "Desactivar esto puede aumentar el rendimiento."
createInviteCode: "Generar invitación"
createWithOptions: "Generar con opciones"
@@ -3201,7 +3199,6 @@ _watermarkEditor:
title: "Editar la marca de agua"
cover: "Cubrir todo"
repeat: "Repetir"
preserveBoundingRect: "Ajuste para evitar que se desborde al rotar."
opacity: "Opacidad"
scale: "Tamaño"
text: "Texto"
-31
View File
@@ -298,7 +298,6 @@ uploadFromUrlMayTakeTime: "Membutuhkan beberapa waktu hingga pengunggahan selesa
explore: "Jelajahi"
messageRead: "Telah dibaca"
noMoreHistory: "Tidak ada sejarah lagi"
startChat: "Kirim pesan"
nUsersRead: "Dibaca oleh {n}"
agreeTo: "Saya setuju kepada {0}"
agree: "Setuju"
@@ -511,7 +510,6 @@ emojiStyle: "Gaya emoji"
native: "Native"
menuStyle: "Gaya menu"
style: "Gaya"
popup: "Pemunculan"
showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk"
showReactionsCount: "Lihat jumlah reaksi dalam catatan"
noHistory: "Tidak ada riwayat"
@@ -568,7 +566,6 @@ showFixedPostForm: "Tampilkan form posting di atas lini masa"
showFixedPostFormInChannel: "Tampilkan form posting di atas lini masa (Kanal)"
withRepliesByDefaultForNewlyFollowed: "Termasuk balasan dari pengguna baru yang diikuti pada lini masa secara bawaan"
newNoteRecived: "Kamu mendapat catatan baru"
newNote: "Catatan baru"
sounds: "Bunyi"
sound: "Bunyi"
listen: "Dengarkan"
@@ -1031,7 +1028,6 @@ permissionDeniedError: "Operasi ditolak"
permissionDeniedErrorDescription: "Akun ini tidak memiliki izin untuk melakukan aksi ini."
preset: "Prasetel"
selectFromPresets: "Pilih dari prasetel"
custom: "Penyesuaian"
achievements: "Pencapaian"
gotInvalidResponseError: "Respon peladen tidak valid"
gotInvalidResponseErrorDescription: "Peladen tidak dapat dijangkau atau sedang dalam perawatan. Mohon coba lagi nanti."
@@ -1114,7 +1110,6 @@ preservedUsernamesDescription: "Daftar nama pengguna yang dicadangkan dipisah de
createNoteFromTheFile: "Buat catatan dari berkas ini"
archive: "Arsipkan"
archived: "Diarsipkan"
unarchive: "Batalkan pengarsipan"
channelArchiveConfirmTitle: "Yakin untuk mengarsipkan {name}?"
channelArchiveConfirmDescription: "Kanal yang diarsipkan tidak akan muncul pada daftar kanal atau hasil pencarian. Postingan baru juga tidak dapat ditambahkan lagi."
thisChannelArchived: "Kanal ini telah diarsipkan."
@@ -1256,7 +1251,6 @@ noDescription: "Tidak ada deskripsi"
alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti"
inquiry: "Hubungi kami"
tryAgain: "Silahkan coba lagi."
sensitiveMediaRevealConfirm: "Media sensitif. Apakah ingin melihat?"
createdLists: "Senarai yang dibuat"
createdAntennas: "Antena yang dibuat"
fromX: "Dari {x}"
@@ -1264,43 +1258,21 @@ noteOfThisUser: "Catatan oleh pengguna ini"
clipNoteLimitExceeded: "Klip ini tak bisa ditambahi lagi catatan."
performance: "Kinerja"
modified: "Diubah"
discard: "Buang"
thereAreNChanges: "Ada {n} perubahan"
signinWithPasskey: "Masuk dengan kunci sandi"
unknownWebAuthnKey: "Kunci sandi tidak terdaftar."
passkeyVerificationFailed: "Verifikasi kunci sandi gagal."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Verifikasi kunci sandi berhasil, namun pemasukan tanpa sandi dinonaktifkan."
messageToFollower: "Pesan kepada pengikut"
prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna"
lockdown: "Kuncitara"
noName: "Tidak ada nama"
skip: "Lewati"
paste: "Tempel"
emojiPalette: "Palet emoji"
postForm: "Buat catatan"
information: "Informasi"
chat: "Obrolan"
directMessage: "Obrolan pengguna"
right: "Kanan"
bottom: "Bawah"
top: "Atas"
advice: "Saran"
inMinutes: "menit"
inDays: "hari"
widgets: "Widget"
_chat:
invitations: "Undang"
history: "Riwayat obrolan"
noHistory: "Tidak ada riwayat"
members: "Anggota"
home: "Beranda"
send: "Kirim"
chatWithThisUser: "Obrolan pengguna"
_settings:
webhook: "Webhook"
contentsUpdateFrequency: "Frekuensi pembaruan konten"
_preferencesProfile:
profileName: "Nama profil"
_abuseUserReport:
accept: "Setuju"
reject: "Tolak"
@@ -1994,7 +1966,6 @@ _sfx:
noteMy: "Catatan (Saya)"
notification: "Notifikasi"
reaction: "Ketika memilih reaksi"
chatMessage: "Obrolan pengguna"
_soundSettings:
driveFile: "Menggunakan berkas audio dalam Drive"
driveFileWarn: "Pilih berkas audio dari Drive"
@@ -2197,7 +2168,6 @@ _widgets:
chooseList: "Pilih daftar"
clicker: "Pengeklik"
birthdayFollowings: "Pengguna yang merayakan hari ulang tahunnya hari ini"
chat: "Obrolan pengguna"
_cw:
hide: "Sembunyikan"
show: "Lihat konten"
@@ -2446,7 +2416,6 @@ _deck:
mentions: "Sebutan"
direct: "Langsung"
roleTimeline: "Lini masa peran"
chat: "Obrolan pengguna"
_dialog:
charactersExceeded: "Kamu telah melebihi batas karakter maksimum! Saat ini pada {current} dari {max}."
charactersBelow: "Kamu berada di bawah batas minimum karakter! Saat ini pada {current} dari {min}."
+1 -5
View File
@@ -4707,7 +4707,7 @@ export interface Locale extends ILocale {
*/
"enableIdenticonGeneration": string;
/**
*
*
*/
"showRoleBadgesOfRemoteUsers": string;
/**
@@ -12345,10 +12345,6 @@ export interface Locale extends ILocale {
*
*/
"repeat": string;
/**
* 調
*/
"preserveBoundingRect": string;
/**
*
*/
+9 -11
View File
@@ -458,7 +458,7 @@ setupOf2fa: "Impostare l'autenticazione a due fattori"
totp: "App di autenticazione a due fattori (2FA/MFA)"
totpDescription: "Puoi autenticarti inserendo un codice OTP tramite la tua App di autenticazione a due fattori (2FA/MFA)"
moderator: "Moderatore"
moderation: "Moderazione"
moderation: "moderazione"
moderationNote: "Promemoria di moderazione"
moderationNoteDescription: "Puoi scrivere promemoria condivisi solo tra moderatori."
addModerationNote: "Aggiungi promemoria di moderazione"
@@ -775,7 +775,6 @@ lockedAccountInfo: "A meno che non imposti la visibilità delle tue note su \"So
alwaysMarkSensitive: "Segnare automaticamente come espliciti gli allegati"
loadRawImages: "Visualizza le intere immagini allegate invece delle miniature."
disableShowingAnimatedImages: "Disabilitare le immagini animate"
disableShowingAnimatedImages_caption: "L'attivazione delle animazioni immagini potrebbe interferire sull'accessibilità e sul risparmio energetico nel dispositivo."
highlightSensitiveMedia: "Evidenzia i media espliciti"
verificationEmailSent: "Una mail di verifica è stata inviata. Si prega di accedere al collegamento per compiere la verifica."
notSet: "Non impostato"
@@ -903,7 +902,7 @@ useBlurEffect: "Utilizza effetto sfocatura"
learnMore: "Per saperne di più"
misskeyUpdated: "Misskey è stato aggiornato!"
whatIsNew: "Informazioni sull'aggiornamento"
translate: "Traduzione"
translate: "Traduci"
translatedFrom: "Traduzione da {x}"
accountDeletionInProgress: "È in corso l'eliminazione del profilo"
usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito."
@@ -914,7 +913,7 @@ pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente"
resolved: "Risolto"
unresolved: "Non risolto"
breakFollow: "Rimuovere Follower"
breakFollow: "Rimuovi Follower"
breakFollowConfirm: "Vuoi davvero togliere questo Follower?"
itsOn: "Abilitato"
itsOff: "Disabilitato"
@@ -1172,7 +1171,6 @@ installed: "Installazione avvenuta"
branding: "Branding"
enableServerMachineStats: "Pubblicare le informazioni sul server"
enableIdenticonGeneration: "Generazione automatica delle Identicon"
showRoleBadgesOfRemoteUsers: "Visualizza i badge per i ruoli concessi ai profili remoti"
turnOffToImprovePerformance: "Disattiva, per migliorare le prestazioni"
createInviteCode: "Genera codice di invito"
createWithOptions: "Genera con opzioni"
@@ -1295,7 +1293,7 @@ sensitiveMediaRevealConfirm: "Questo allegato è esplicito, vuoi vederlo?"
createdLists: "Liste create"
createdAntennas: "Antenne create"
fromX: "Da {x}"
genEmbedCode: "Ottieni il codice per incorporare"
genEmbedCode: "Ottieni il codice di incorporamento"
noteOfThisUser: "Elenco di Note di questo profilo"
clipNoteLimitExceeded: "Non è possibile aggiungere ulteriori Note a questa Clip."
performance: "Prestazioni"
@@ -1349,7 +1347,7 @@ postForm: "Finestra di pubblicazione"
textCount: "Il numero di caratteri"
information: "Informazioni"
chat: "Chat"
directMessage: "Chattare insieme"
directMessage: "Chatta con questa persona"
directMessage_short: "Messaggio"
migrateOldSettings: "Migrare le vecchie impostazioni"
migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali."
@@ -1387,7 +1385,7 @@ pluginsAreDisabledBecauseSafeMode: "Tutti i plugin sono disattivati, poiché la
customCssIsDisabledBecauseSafeMode: "Il CSS personalizzato non è stato applicato, poiché la modalità sicura è attiva."
themeIsDefaultBecauseSafeMode: "Quando la modalità sicura è attiva, viene utilizzato il tema predefinito. Quando la modalità sicura viene disattivata, il tema torna a essere quello precedente."
thankYouForTestingBeta: "Grazie per la tua collaborazione nella verifica delle versioni beta!"
createUserSpecifiedNote: "Crea Nota privata"
createUserSpecifiedNote: "Creare Nota personalizzata"
schedulePost: "Pianificare la pubblicazione"
scheduleToPostOnX: "Pianificare la pubblicazione {x}"
scheduledToPostOnX: "Pubblicazione pianificata {x}"
@@ -3054,7 +3052,7 @@ _customEmojisManager:
confirmClearEmojisDescription: "Annullare le modifiche e cancella le emoji nell'elenco. Confermi?"
confirmUploadEmojisDescription: "Caricamento sul Drive di {count} file locali. Vuoi davvero procedere?"
_embedCodeGen:
title: "Personalizza il codice per incorporare"
title: "Personalizza il codice di incorporamento"
header: "Mostra la testata"
autoload: "Carica automaticamente di più (sconsigliato)"
maxHeight: "Altezza massima"
@@ -3063,8 +3061,8 @@ _embedCodeGen:
previewIsNotActual: "Poiché supera l'intervallo che può essere visualizzato in anteprima, la visualizzazione vera e propria sarà diversa quando effettivamente incorporata."
rounded: "Bordo arrotondato"
border: "Aggiungi un bordo al contenitore"
applyToPreview: "Aggiorna l'anteprima"
generateCode: "Crea il codice per incorporare"
applyToPreview: "Applica all'anteprima"
generateCode: "Crea il codice di incorporamento"
codeGenerated: "Codice generato"
codeGeneratedDescription: "Incolla il codice appena generato sul tuo sito web."
_selfXssPrevention:
+1 -2
View File
@@ -1172,7 +1172,7 @@ installed: "インストール済み"
branding: "ブランディング"
enableServerMachineStats: "サーバーのマシン情報を公開する"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
showRoleBadgesOfRemoteUsers: "リモートユーザーに付与したロールバッジを表示する"
showRoleBadgesOfRemoteUsers: "リモートユーザーロールバッジを表示する"
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
createInviteCode: "招待コードを作成"
createWithOptions: "オプションを指定して作成"
@@ -3305,7 +3305,6 @@ _watermarkEditor:
title: "ウォーターマークの編集"
cover: "全体に被せる"
repeat: "敷き詰める"
preserveBoundingRect: "回転時はみ出ないように調整する"
opacity: "不透明度"
scale: "サイズ"
text: "テキスト"
-3
View File
@@ -775,7 +775,6 @@ lockedAccountInfo: "팔로우를 승인으로 승인받더라도 노트의 공
alwaysMarkSensitive: "미디어를 항상 열람 주의로 설정"
loadRawImages: "첨부한 이미지의 썸네일을 원본화질로 표시"
disableShowingAnimatedImages: "움직이는 이미지를 자동으로 재생하지 않음"
disableShowingAnimatedImages_caption: "이 설정에 상관없이 애니메이션 이미지가 재생되지 않을 때는 브라우저·OS의 액티비티 설정이나 절전 모드 설정 등이 간섭하고 있는 경우가 있습니다."
highlightSensitiveMedia: "미디어가 민감한 내용이라는 것을 알기 쉽게 표시"
verificationEmailSent: "확인 메일을 발송하였습니다. 설정을 완료하려면 메일에 첨부된 링크를 확인해 주세요."
notSet: "설정되지 않음"
@@ -1172,7 +1171,6 @@ installed: "설치됨"
branding: "브랜딩"
enableServerMachineStats: "서버의 머신 사양을 공개하기"
enableIdenticonGeneration: "유저마다의 Identicon 생성 유효화"
showRoleBadgesOfRemoteUsers: "리모트 유저의 역할 배지 표시"
turnOffToImprovePerformance: "이 기능을 끄면 성능이 향상될 수 있습니다."
createInviteCode: "초대 코드 생성"
createWithOptions: "옵션을 지정하여 생성"
@@ -3201,7 +3199,6 @@ _watermarkEditor:
title: "워터마크 편집"
cover: "전체에 붙이기"
repeat: "전면에 깔기"
preserveBoundingRect: "회전 시 빠져나오지 않도록 조정"
opacity: "불투명도"
scale: "크기"
text: "텍스트"
+23 -26
View File
@@ -106,8 +106,8 @@ privacy: "隐私"
makeFollowManuallyApprove: "关注请求需要批准"
defaultNoteVisibility: "默认可见性"
follow: "关注"
followRequest: "申请关注"
followRequests: "关注请"
followRequest: "关注申请"
followRequests: "关注请"
unfollow: "取消关注"
followRequestPending: "关注请求待批准"
enterEmoji: "输入表情符号"
@@ -540,7 +540,7 @@ regenerate: "重新生成"
fontSize: "字体大小"
mediaListWithOneImageAppearance: "仅一张图片的媒体列表高度"
limitTo: "上限为 {x}"
noFollowRequests: "没有关注请"
noFollowRequests: "没有关注请"
openImageInNewTab: "在新标签页中打开图片"
dashboard: "管理面板"
local: "本地"
@@ -775,7 +775,6 @@ lockedAccountInfo: "即使启用该功能,只要帖子可见范围不是「仅
alwaysMarkSensitive: "默认将媒体文件标记为敏感内容"
loadRawImages: "添加附件图像的缩略图时使用原始图像质量"
disableShowingAnimatedImages: "不播放动画"
disableShowingAnimatedImages_caption: "如果即使关闭了此设置但动画仍无法播放,则可能是浏览器或操作系统的辅助功能设置,又或者是省电设置等产生了干扰。"
highlightSensitiveMedia: "高亮显示敏感媒体"
verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。"
notSet: "未设置"
@@ -873,12 +872,12 @@ noMaintainerInformationWarning: "尚未设置管理员信息。"
noInquiryUrlWarning: "尚未设置联络地址。"
noBotProtectionWarning: "尚未设置 Bot 防御。"
configure: "设置"
postToGallery: "创建新图集"
postToGallery: "创建新相册"
postToHashtag: "投稿到这个标签"
gallery: "图集"
gallery: "相册"
recentPosts: "最新发布"
popularPosts: "热门投稿"
shareWithNote: "分享到文"
shareWithNote: "分享到文"
ads: "广告"
expiration: "截止时间"
startingperiod: "开始时间"
@@ -1031,7 +1030,7 @@ tools: "工具"
cannotLoad: "无法加载"
numberOfProfileView: "个人资料展示次数"
like: "点赞!"
unlike: "取消喜欢"
unlike: "取消"
numberOfLikes: "点赞数"
show: "显示"
neverShow: "不再显示"
@@ -1067,7 +1066,7 @@ thisPostMayBeAnnoyingHome: "发到首页"
thisPostMayBeAnnoyingCancel: "取消"
thisPostMayBeAnnoyingIgnore: "就这样发布"
collapseRenotes: "省略显示已经看过的转发内容"
collapseRenotesDescription: "折叠显示回应或转过的帖文。"
collapseRenotesDescription: "回应或转过的贴子折叠表示。"
internalServerError: "内部服务器错误"
internalServerErrorDescription: "内部服务器发生了预期外的错误"
copyErrorInfo: "复制错误信息"
@@ -1108,7 +1107,7 @@ retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
enableChartsForRemoteUser: "生成远程用户的图表"
enableChartsForFederatedInstances: "生成远程服务器的图表"
enableStatsForFederatedInstances: "获取远程服务器的信息"
showClipButtonInNoteFooter: "在文下方显示便签按钮"
showClipButtonInNoteFooter: "在文下方显示便签按钮"
reactionsDisplaySize: "回应显示大小"
limitWidthOfReaction: "限制回应的最大宽度,并将其缩小显示"
noteIdOrUrl: "帖子 ID 或 URL"
@@ -1146,7 +1145,7 @@ archive: "归档"
archived: "已归档"
unarchive: "取消归档"
channelArchiveConfirmTitle: "要将 {name} 归档吗?"
channelArchiveConfirmDescription: "归档后,不会在频道列表与搜索结果中显示,也无法发布新的文。"
channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的文。"
thisChannelArchived: "该频道已被归档。"
displayOfNote: "显示帖子"
initialAccountSetting: "初始设定"
@@ -1172,7 +1171,6 @@ installed: "已安装"
branding: "品牌"
enableServerMachineStats: "公开服务器硬件统计信息"
enableIdenticonGeneration: "启用生成用户 Identicon"
showRoleBadgesOfRemoteUsers: "显示远程用户的角色徽章"
turnOffToImprovePerformance: "关闭该选项可以提高性能。"
createInviteCode: "生成邀请码"
createWithOptions: "使用选项来创建"
@@ -1803,7 +1801,7 @@ _achievements:
_login500:
title: "老熟人Ⅰ"
description: "累计登录 500 天"
flavor: "诸君,我喜欢文"
flavor: "诸君,我喜欢文"
_login600:
title: "老熟人Ⅱ"
description: "累计登录 600 天"
@@ -1822,7 +1820,7 @@ _achievements:
flavor: "感谢您使用 Misskey"
_noteClipped1:
title: "忍不住要收藏到便签"
description: "第一次将帖子加入便签"
description: "第一次将贴文贴进便签"
_noteFavorited1:
title: "观星者"
description: "第一次将帖子加入收藏"
@@ -2028,7 +2026,7 @@ _role:
wordMuteMax: "屏蔽词的字数限制"
webhookMax: "Webhook 创建数量限制"
clipMax: "便签创建数量限制"
noteEachClipsMax: "便签内贴文的最大数量"
noteEachClipsMax: "单个便签内贴文数量限制"
userListMax: "用户列表创建数量限制"
userEachUserListsMax: "单个用户列表内用户数量限制"
rateLimitFactor: "速率限制"
@@ -2114,8 +2112,8 @@ _forgotPassword:
ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。"
contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。"
_gallery:
my: "我的图集"
liked: "喜欢的图集"
my: "我的相册"
liked: "喜欢的相册"
like: "喜欢!"
unlike: "取消喜欢"
_email:
@@ -2368,10 +2366,10 @@ _permissions:
"write:user-groups": "编辑用户组"
"read:channels": "查看频道"
"write:channels": "管理频道"
"read:gallery": "浏览图集"
"write:gallery": "编辑图集"
"read:gallery-likes": "浏览喜欢的图集"
"write:gallery-likes": "管理喜欢的图集"
"read:gallery": "浏览相册"
"write:gallery": "编辑相册"
"read:gallery-likes": "浏览喜欢的相册"
"write:gallery-likes": "管理喜欢的相册"
"read:flash": "查看 Play"
"write:flash": "编辑 Play"
"read:flash-likes": "查看 Play 的点赞"
@@ -2778,7 +2776,7 @@ _webhookSettings:
_events:
follow: "关注时"
followed: "被关注时"
note: "发布文时"
note: "发布文时"
reply: "收到回复时"
renote: "被转发时"
reaction: "被回应时"
@@ -2854,7 +2852,7 @@ _moderationLogTypes:
deleteAccount: "删除了账户"
deletePage: "删除了页面"
deleteFlash: "删除了 Play"
deleteGalleryPost: "删除图集内容"
deleteGalleryPost: "删除相册内容"
deleteChatRoom: "删除聊天室"
updateProxyAccountDescription: "更新代理账户的简介"
_fileViewer:
@@ -3074,8 +3072,8 @@ _selfXssPrevention:
description2: "如果不能完全理解将要粘贴的内容,%c 请立即停止操作并关闭这个窗口。"
description3: "详情请看这里。{link}"
_followRequest:
recieved: "收到的请求"
sent: "已发送的请求"
recieved: "收到申请"
sent: "已发送申请"
_remoteLookupErrors:
_federationNotAllowed:
title: "无法与此服务器通信"
@@ -3201,7 +3199,6 @@ _watermarkEditor:
title: "编辑水印"
cover: "覆盖全体"
repeat: "平铺"
preserveBoundingRect: "调整为旋转时不超出范围"
opacity: "不透明度"
scale: "大小"
text: "文本"
+1 -3
View File
@@ -775,7 +775,6 @@ lockedAccountInfo: "即使追隨需要核准,除非你將貼文的可見性設
alwaysMarkSensitive: "預設標記檔案為敏感內容"
loadRawImages: "以原始圖檔顯示附件圖檔的縮圖"
disableShowingAnimatedImages: "不播放動態圖檔"
disableShowingAnimatedImages_caption: "無論這個設定如何,如果動畫圖片無法播放,可能是因為瀏覽器或作業系統的無障礙設定、省電設定等產生了干擾。"
highlightSensitiveMedia: "強調敏感標記"
verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的連結以完成驗證。"
notSet: "未設定"
@@ -3176,7 +3175,7 @@ _uploader:
abortConfirm: "有些檔案尚未上傳,您要中止嗎?"
doneConfirm: "有些檔案尚未上傳,是否要完成上傳?"
maxFileSizeIsX: "可上傳的最大檔案大小為 {x}。"
allowedTypes: "可上傳的檔案類型"
allowedTypes: "可上傳的檔案類型"
tip: "檔案尚未上傳。您可以在此對話框中進行上傳前的確認、重新命名、壓縮、裁切等操作。準備完成後,請點選「上傳」按鈕開始上傳。\n"
_clientPerformanceIssueTip:
title: "如果覺得電池消耗過快的話"
@@ -3200,7 +3199,6 @@ _watermarkEditor:
title: "編輯浮水印"
cover: "覆蓋整體"
repeat: "佈局"
preserveBoundingRect: "調整使其在旋轉時不會突出"
opacity: "透明度"
scale: "大小"
text: "文字"
+8 -8
View File
@@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2025.10.2-beta.0",
"version": "2025.10.1-alpha.2",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@10.18.2",
"packageManager": "pnpm@10.17.1",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
@@ -68,15 +68,15 @@
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/js-yaml": "4.0.9",
"@types/node": "22.18.10",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"@types/node": "22.18.8",
"@typescript-eslint/eslint-plugin": "8.45.0",
"@typescript-eslint/parser": "8.45.0",
"cross-env": "10.1.0",
"cypress": "15.4.0",
"eslint": "9.37.0",
"cypress": "15.3.0",
"eslint": "9.36.0",
"globals": "16.4.0",
"ncp": "2.0.0",
"pnpm": "10.18.2",
"pnpm": "10.17.1",
"start-server-and-test": "2.1.2"
},
"optionalDependencies": {
+12 -12
View File
@@ -69,8 +69,8 @@
"utf-8-validate": "6.0.5"
},
"dependencies": {
"@aws-sdk/client-s3": "3.908.0",
"@aws-sdk/lib-storage": "3.908.0",
"@aws-sdk/client-s3": "3.901.0",
"@aws-sdk/lib-storage": "3.901.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.3",
"@fastify/cookie": "11.0.2",
@@ -103,7 +103,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.61.0",
"bullmq": "5.59.0",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.6.2",
@@ -120,12 +120,12 @@
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.4",
"got": "14.5.0",
"happy-dom": "20.0.7",
"got": "14.4.9",
"happy-dom": "20.0.2",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",
"ioredis": "5.8.1",
"ioredis": "5.8.0",
"ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0",
"is-svg": "5.1.0",
@@ -145,7 +145,7 @@
"nanoid": "5.1.6",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"nodemailer": "7.0.9",
"nodemailer": "6.10.1",
"nsfwjs": "4.2.0",
"oauth": "0.10.2",
"oauth2orize": "1.12.0",
@@ -171,11 +171,11 @@
"sanitize-html": "2.17.0",
"secure-json-parse": "3.0.2",
"sharp": "0.33.5",
"semver": "7.7.3",
"semver": "7.7.2",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.27.11",
"systeminformation": "5.27.10",
"tinycolor2": "1.6.0",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
@@ -210,7 +210,7 @@
"@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "22.18.10",
"@types/node": "22.18.8",
"@types/nodemailer": "6.4.20",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
@@ -231,8 +231,8 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"@typescript-eslint/eslint-plugin": "8.45.0",
"@typescript-eslint/parser": "8.45.0",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.32.0",
+28 -31
View File
@@ -517,43 +517,40 @@ export class DriveService {
this.registerLogger.debug(`ADD DRIVE FILE: user ${user?.id ?? 'not set'}, name ${detectedName}, tmp ${path}`);
//#region Check drive usage and mime type
if (user != null && !isLink) {
if (user && !isLink) {
const isLocalUser = this.userEntityService.isLocalUser(user);
const isModerator = isLocalUser ? await this.roleService.isModerator(user) : false;
if (!isModerator) {
const policies = await this.roleService.getUserPolicies(user.id);
const policies = await this.roleService.getUserPolicies(user.id);
const allowedMimeTypes = policies.uploadableFileTypes;
const isAllowed = allowedMimeTypes.some((mimeType) => {
if (mimeType === '*' || mimeType === '*/*') return true;
if (mimeType.endsWith('/*')) return info.type.mime.startsWith(mimeType.slice(0, -1));
return info.type.mime === mimeType;
});
if (!isAllowed) {
throw new IdentifiableError('bd71c601-f9b0-4808-9137-a330647ced9b', `Unallowed file type: ${info.type.mime}`);
const allowedMimeTypes = policies.uploadableFileTypes;
const isAllowed = allowedMimeTypes.some((mimeType) => {
if (mimeType === '*' || mimeType === '*/*') return true;
if (mimeType.endsWith('/*')) return info.type.mime.startsWith(mimeType.slice(0, -1));
return info.type.mime === mimeType;
});
if (!isAllowed) {
throw new IdentifiableError('bd71c601-f9b0-4808-9137-a330647ced9b', `Unallowed file type: ${info.type.mime}`);
}
const driveCapacity = 1024 * 1024 * policies.driveCapacityMb;
const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb;
if (maxFileSize < info.size) {
if (isLocalUser) {
throw new IdentifiableError('f9e4e5f3-4df4-40b5-b400-f236945f7073', 'Max file size exceeded.');
}
}
const driveCapacity = 1024 * 1024 * policies.driveCapacityMb;
const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb;
const usage = await this.driveFileEntityService.calcDriveUsageOf(user);
if (maxFileSize < info.size) {
if (isLocalUser) {
throw new IdentifiableError('f9e4e5f3-4df4-40b5-b400-f236945f7073', 'Max file size exceeded.');
}
}
const usage = await this.driveFileEntityService.calcDriveUsageOf(user);
this.registerLogger.debug('drive capacity override applied');
this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
// If usage limit exceeded
if (driveCapacity < usage + info.size) {
if (isLocalUser) {
throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.');
}
await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as MiRemoteUser, driveCapacity - info.size);
this.registerLogger.debug('drive capacity override applied');
this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
// If usage limit exceeded
if (driveCapacity < usage + info.size) {
if (isLocalUser) {
throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.');
}
await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as MiRemoteUser, driveCapacity - info.size);
}
}
//#endregion
+5 -5
View File
@@ -11,15 +11,15 @@
},
"devDependencies": {
"@types/estree": "1.0.8",
"@types/node": "22.18.10",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"rollup": "4.52.4",
"@types/node": "22.18.8",
"@typescript-eslint/eslint-plugin": "8.45.0",
"@typescript-eslint/parser": "8.45.0",
"rollup": "4.52.3",
"typescript": "5.9.3"
},
"dependencies": {
"estree-walker": "3.0.3",
"magic-string": "0.30.19",
"vite": "7.1.9"
"vite": "7.1.7"
}
}
+9 -9
View File
@@ -26,7 +26,7 @@
"mfm-js": "0.25.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.52.4",
"rollup": "4.52.3",
"sass": "1.93.2",
"shiki": "3.13.0",
"tinycolor2": "1.6.0",
@@ -34,7 +34,7 @@
"tsconfig-paths": "4.2.0",
"typescript": "5.9.3",
"uuid": "13.0.0",
"vite": "7.1.9",
"vite": "7.1.7",
"vue": "3.5.22"
},
"devDependencies": {
@@ -43,12 +43,12 @@
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.18.10",
"@types/node": "22.18.8",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"@typescript-eslint/eslint-plugin": "8.45.0",
"@typescript-eslint/parser": "8.45.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/runtime-core": "3.5.22",
"acorn": "8.15.0",
@@ -56,17 +56,17 @@
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.5.0",
"fast-glob": "3.3.3",
"happy-dom": "20.0.7",
"happy-dom": "20.0.2",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.11.5",
"msw": "2.11.3",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"start-server-and-test": "2.1.2",
"tsx": "4.20.6",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.1.1",
"vue-component-type-helpers": "3.1.0",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.1.1"
"vue-tsc": "3.1.0"
}
}
+3 -8
View File
@@ -39,18 +39,13 @@ for (let i = 0; i < emojilist.length; i++) {
export const emojiCharByCategory = _charGroupByCategory;
export function getUnicodeEmojiOrNull(char: string): UnicodeEmojiDef | null {
export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string {
// Colorize it because emojilist.json assumes that
return unicodeEmojisMap.get(colorizeEmoji(char))
// カラースタイル絵文字がjsonに無い場合はテキストスタイル絵文字にフォールバックする
?? unicodeEmojisMap.get(char)
// それでも見つからない場合はnullを返す
?? null;
}
export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string {
// 絵文字が見つからない場合はそのまま返す(絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する)
return getUnicodeEmojiOrNull(char) ?? char;
// それでも見つからない場合はそのまま返す(絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する)
?? char;
}
export function isSupportedEmoji(char: string): boolean {
+3 -3
View File
@@ -21,9 +21,9 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.18.10",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"@types/node": "22.18.8",
"@typescript-eslint/eslint-plugin": "8.45.0",
"@typescript-eslint/parser": "8.45.0",
"esbuild": "0.25.10",
"eslint-plugin-vue": "10.5.0",
"nodemon": "3.1.10",
+14 -15
View File
@@ -24,7 +24,7 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.19.0",
"@sentry/vue": "10.17.0",
"@syuilo/aiscript": "1.1.2",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
@@ -36,7 +36,7 @@
"broadcast-channel": "7.1.0",
"buraha": "0.0.1",
"canvas-confetti": "1.9.3",
"chart.js": "4.5.1",
"chart.js": "4.5.0",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1",
@@ -57,7 +57,7 @@
"json5": "2.2.3",
"magic-string": "0.30.19",
"matter-js": "0.20.0",
"mediabunny": "1.23.0",
"mediabunny": "1.21.1",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@@ -66,7 +66,7 @@
"punycode.js": "2.3.1",
"qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2",
"rollup": "4.52.4",
"rollup": "4.52.3",
"sanitize-html": "2.17.0",
"sass": "1.93.2",
"shiki": "3.13.0",
@@ -79,7 +79,7 @@
"tsconfig-paths": "4.2.0",
"typescript": "5.9.3",
"v-code-diff": "1.13.1",
"vite": "7.1.9",
"vite": "7.1.7",
"vue": "3.5.22",
"vuedraggable": "next",
"wanakana": "5.3.1"
@@ -109,30 +109,30 @@
"@types/estree": "1.0.8",
"@types/matter-js": "0.20.2",
"@types/micromatch": "4.0.9",
"@types/node": "22.18.10",
"@types/node": "22.18.8",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"@typescript-eslint/eslint-plugin": "8.45.0",
"@typescript-eslint/parser": "8.45.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/compiler-core": "3.5.22",
"@vue/runtime-core": "3.5.22",
"acorn": "8.15.0",
"cross-env": "10.1.0",
"cypress": "15.4.0",
"cypress": "15.3.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.5.0",
"fast-glob": "3.3.3",
"happy-dom": "20.0.7",
"happy-dom": "20.0.2",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.0.3",
"msw": "2.11.5",
"msw-storybook-addon": "2.0.6",
"msw": "2.11.3",
"msw-storybook-addon": "2.0.5",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"react": "19.2.0",
@@ -142,12 +142,11 @@
"storybook": "9.1.10",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.20.6",
"vite-plugin-glsl": "1.5.4",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.1.1",
"vue-component-type-helpers": "3.1.0",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.1.1"
"vue-tsc": "3.1.0"
}
}
+6 -4
View File
@@ -211,13 +211,13 @@ export async function switchAccount(host: string, id: string) {
}
}
export async function getAccountMenu(opts: {
export async function openAccountMenu(opts: {
includeCurrentAccount?: boolean;
withExtraOperation: boolean;
active?: Misskey.entities.User['id'];
onChoose?: (account: Misskey.entities.MeDetailed) => void;
}) {
if ($i == null) throw new Error('No current account');
}, ev: MouseEvent) {
if (!$i) return;
const me = $i;
const callback = opts.onChoose;
@@ -338,7 +338,9 @@ export async function getAccountMenu(opts: {
menuItems.push(...accountItems);
}
return menuItems;
popupMenu(menuItems, ev.currentTarget ?? ev.target, {
align: 'left',
});
}
export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
+12 -1
View File
@@ -69,6 +69,9 @@ export async function common(createVue: () => Promise<App<Element>>) {
if (lastVersion !== version) {
miLocalStorage.setItem('lastVersion', version);
// テーマリビルドするため
miLocalStorage.removeItem('theme');
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
isClientUpdated = true;
@@ -173,7 +176,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
})();
applyTheme(theme);
}, { immediate: true });
}, { immediate: isSafeMode || miLocalStorage.getItem('theme') == null });
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
@@ -192,6 +195,14 @@ export async function common(createVue: () => Promise<App<Element>>) {
applyTheme(theme ?? defaultLightTheme);
}
});
}
if (!isSafeMode) {
if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
} else if (prefer.s.lightTheme && !store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
}
fetchInstanceMetaPromise.then(() => {
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
@@ -1,111 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec2 mod289(vec2 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec3 permute(vec3 x) {
return mod289(((x*34.0)+1.0)*x);
}
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
m = m*m;
m = m*m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
in vec2 in_uv;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_spread;
uniform float u_speed;
uniform float u_warp;
uniform float u_focus;
uniform float u_itensity;
out vec4 out_color;
float circle(in vec2 _pos, in vec2 _origin, in float _radius) {
float SPREAD = 0.7 * u_spread;
float SPEED = 0.00055 * u_speed;
float WARP = 1.5 * u_warp;
float FOCUS = 1.15 * u_focus;
vec2 dist = _pos - _origin;
float distortion = snoise(vec2(
_pos.x * 1.587 * WARP + u_time * SPEED * 0.5,
_pos.y * 1.192 * WARP + u_time * SPEED * 0.3
)) * 0.5 + 0.5;
float feather = 0.01 + SPREAD * pow(distortion, FOCUS);
return 1.0 - smoothstep(
_radius - (_radius * feather),
_radius + (_radius * feather),
dot( dist, dist ) * 4.0
);
}
void main() {
vec3 green = vec3(1.0) - vec3(153.0 / 255.0, 211.0 / 255.0, 221.0 / 255.0);
vec3 purple = vec3(1.0) - vec3(195.0 / 255.0, 165.0 / 255.0, 242.0 / 255.0);
vec3 orange = vec3(1.0) - vec3(255.0 / 255.0, 156.0 / 255.0, 136.0 / 255.0);
float ratio = u_resolution.x / u_resolution.y;
vec2 uv = vec2(in_uv.x, in_uv.y / ratio) * 0.5 + 0.5;
vec3 color = vec3(0.0);
float greenMix = snoise(in_uv * 1.31 + u_time * 0.8 * 0.00017) * 0.5 + 0.5;
float purpleMix = snoise(in_uv * 1.26 + u_time * 0.8 * -0.0001) * 0.5 + 0.5;
float orangeMix = snoise(in_uv * 1.34 + u_time * 0.8 * 0.00015) * 0.5 + 0.5;
float alphaOne = 0.35 + 0.65 * pow(snoise(vec2(u_time * 0.00012, uv.x)) * 0.5 + 0.5, 1.2);
float alphaTwo = 0.35 + 0.65 * pow(snoise(vec2((u_time + 1561.0) * 0.00014, uv.x )) * 0.5 + 0.5, 1.2);
float alphaThree = 0.35 + 0.65 * pow(snoise(vec2((u_time + 3917.0) * 0.00013, uv.x )) * 0.5 + 0.5, 1.2);
color += vec3(circle(uv, vec2(0.22 + sin(u_time * 0.000201) * 0.06, 0.80 + cos(u_time * 0.000151) * 0.06), 0.15)) * alphaOne * (purple * purpleMix + orange * orangeMix);
color += vec3(circle(uv, vec2(0.90 + cos(u_time * 0.000166) * 0.06, 0.42 + sin(u_time * 0.000138) * 0.06), 0.18)) * alphaTwo * (green * greenMix + purple * purpleMix);
color += vec3(circle(uv, vec2(0.19 + sin(u_time * 0.000112) * 0.06, 0.25 + sin(u_time * 0.000192) * 0.06), 0.09)) * alphaThree * (orange * orangeMix);
color *= u_itensity + 1.0 * pow(snoise(vec2(in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009)) * 0.5 + 0.5, 2.0);
vec3 inverted = vec3(1.0) - color;
out_color = vec4(color, max(max(color.x, color.y), color.z));
}
@@ -1,15 +0,0 @@
#version 300 es
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 position;
uniform vec2 u_scale;
out vec2 in_uv;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
in_uv = position / u_scale;
}
+120 -3
View File
@@ -10,8 +10,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
import isChromatic from 'chromatic/isChromatic';
import vertexShaderSource from './MkAnimBg.vertex.glsl';
import fragmentShaderSource from './MkAnimBg.fragment.glsl';
import { initShaderProgram } from '@/utility/webgl.js';
const canvasEl = useTemplateRef('canvasEl');
@@ -44,7 +42,126 @@ onMounted(() => {
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const shaderProgram = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
const shaderProgram = initShaderProgram(gl, `#version 300 es
in vec2 position;
uniform vec2 u_scale;
out vec2 in_uv;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
in_uv = position / u_scale;
}
`, `#version 300 es
precision mediump float;
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec2 mod289(vec2 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec3 permute(vec3 x) {
return mod289(((x*34.0)+1.0)*x);
}
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187,
0.366025403784439,
-0.577350269189626,
0.024390243902439);
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m ;
m = m*m ;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
in vec2 in_uv;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_spread;
uniform float u_speed;
uniform float u_warp;
uniform float u_focus;
uniform float u_itensity;
out vec4 out_color;
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
float SPREAD = 0.7 * u_spread;
float SPEED = 0.00055 * u_speed;
float WARP = 1.5 * u_warp;
float FOCUS = 1.15 * u_focus;
vec2 dist = _pos - _origin;
float distortion = snoise( vec2(
_pos.x * 1.587 * WARP + u_time * SPEED * 0.5,
_pos.y * 1.192 * WARP + u_time * SPEED * 0.3
) ) * 0.5 + 0.5;
float feather = 0.01 + SPREAD * pow( distortion, FOCUS );
return 1.0 - smoothstep(
_radius - ( _radius * feather ),
_radius + ( _radius * feather ),
dot( dist, dist ) * 4.0
);
}
void main() {
vec3 green = vec3( 1.0 ) - vec3( 153.0 / 255.0, 211.0 / 255.0, 221.0 / 255.0 );
vec3 purple = vec3( 1.0 ) - vec3( 195.0 / 255.0, 165.0 / 255.0, 242.0 / 255.0 );
vec3 orange = vec3( 1.0 ) - vec3( 255.0 / 255.0, 156.0 / 255.0, 136.0 / 255.0 );
float ratio = u_resolution.x / u_resolution.y;
vec2 uv = vec2( in_uv.x, in_uv.y / ratio ) * 0.5 + 0.5;
vec3 color = vec3( 0.0 );
float greenMix = snoise( in_uv * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
float purpleMix = snoise( in_uv * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
float orangeMix = snoise( in_uv * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
float alphaThree = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 3917.0 ) * 0.00013, uv.x ) ) * 0.5 + 0.5, 1.2 );
color += vec3( circle( uv, vec2( 0.22 + sin( u_time * 0.000201 ) * 0.06, 0.80 + cos( u_time * 0.000151 ) * 0.06 ), 0.15 ) ) * alphaOne * ( purple * purpleMix + orange * orangeMix );
color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
color *= u_itensity + 1.0 * pow( snoise( vec2( in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
vec3 inverted = vec3( 1.0 ) - color;
out_color = vec4(color, max(max(color.x, color.y), color.z));
}
`);
if (shaderProgram == null) return;
gl.useProgram(shaderProgram);
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div :class="$style.container">
<div :class="$style.preview">
<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown.prevent.stop="onImagePointerdown"></canvas>
<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas>
<div :class="$style.previewContainer">
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
<div class="_acrylic" :class="$style.editControls">
@@ -448,7 +448,6 @@ function onImagePointerdown(ev: PointerEvent) {
margin: 20px;
box-sizing: border-box;
object-fit: contain;
touch-action: none;
}
.controls {
+9 -11
View File
@@ -57,8 +57,15 @@ const remaining = computed(() => {
return Math.floor(Math.max(expiresAtTime.value - now.value, 0) / 1000);
});
const remainingWatchStop = watch(remaining, (to) => {
if (to <= 0) {
showResult.value = true;
remainingWatchStop();
}
}, { immediate: true });
const total = computed(() => sum(props.choices.map(x => x.votes)));
const closed = computed(() => props.expiresAt != null && remaining.value <= 0);
const closed = computed(() => remaining.value === 0);
const isVoted = computed(() => !props.multiple && props.choices.some(c => c.isVoted));
const timer = computed(() => i18n.tsx._poll[
remaining.value >= 86400 ? 'remainingDays' :
@@ -71,16 +78,7 @@ const timer = computed(() => i18n.tsx._poll[
d: Math.floor(remaining.value / 86400),
}));
const showResult = ref(props.readOnly || isVoted.value || closed.value);
if (!closed.value) {
const closedWatchStop = watch(closed, (isNowClosed) => {
if (isNowClosed) {
showResult.value = true;
closedWatchStop();
}
});
}
const showResult = ref(props.readOnly || isVoted.value);
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup',
+58 -41
View File
@@ -14,9 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<header :class="$style.header">
<div :class="$style.headerLeft">
<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
<button v-click-anime v-tooltip="i18n.ts.account" :class="$style.account" class="_button" @click="openAccountMenu">
<button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu">
<img :class="$style.avatar" :src="(postAccount ?? $i).avatarUrl" style="border-radius: 100%;"/>
</button>
<button v-if="$i.policies.noteDraftLimit > 0" v-tooltip="(postAccount != null && postAccount.id !== $i.id) ? null : i18n.ts.draftsAndScheduledNotes" class="_button" :class="$style.draftButton" :disabled="postAccount != null && postAccount.id !== $i.id" @click="showDraftMenu"><i class="ti ti-list"></i></button>
</div>
<div :class="$style.headerRight">
<template v-if="!(targetChannel != null && fixed)">
@@ -140,7 +141,7 @@ import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { ensureSignin, notesCount, incNotesCount } from '@/i.js';
import { getAccounts, getAccountMenu } from '@/accounts.js';
import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { deepClone } from '@/utility/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';
@@ -619,19 +620,6 @@ function showOtherSettings() {
action: () => {
toggleReactionAcceptance();
},
}, { type: 'divider' }, {
type: 'button',
text: i18n.ts._drafts.saveToDraft,
icon: 'ti ti-cloud-upload',
action: async () => {
if (!canSaveAsServerDraft.value) {
return os.alert({
type: 'error',
text: i18n.ts._drafts.cannotCreateDraft,
});
}
saveServerDraft();
},
}, ...($i.policies.scheduledNoteLimit > 0 ? [{
icon: 'ti ti-calendar-time',
text: i18n.ts.schedulePost + '...',
@@ -1171,9 +1159,34 @@ function showActions(ev: MouseEvent) {
const postAccount = ref<Misskey.entities.UserDetailed | null>(null);
async function openAccountMenu(ev: MouseEvent) {
function openAccountMenu(ev: MouseEvent) {
if (props.mock) return;
openAccountMenu_({
withExtraOperation: false,
includeCurrentAccount: true,
active: postAccount.value != null ? postAccount.value.id : $i.id,
onChoose: (account) => {
if (account.id === $i.id) {
postAccount.value = null;
} else {
postAccount.value = account;
}
},
}, ev);
}
function showPerUploadItemMenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.contextMenu(menu, ev);
}
function showDraftMenu(ev: MouseEvent) {
function showDraftsDialog(scheduled: boolean) {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNoteDraftsDialog.vue')), {
scheduled,
@@ -1231,44 +1244,34 @@ async function openAccountMenu(ev: MouseEvent) {
});
}
const items = await getAccountMenu({
withExtraOperation: false,
includeCurrentAccount: true,
active: postAccount.value != null ? postAccount.value.id : $i.id,
onChoose: (account) => {
if (account.id === $i.id) {
postAccount.value = null;
} else {
postAccount.value = account;
}
},
});
os.popupMenu([{
type: 'button',
text: i18n.ts._drafts.saveToDraft,
icon: 'ti ti-cloud-upload',
action: async () => {
if (!canSaveAsServerDraft.value) {
return os.alert({
type: 'error',
text: i18n.ts._drafts.cannotCreateDraft,
});
}
saveServerDraft();
},
}, {
type: 'button',
text: i18n.ts._drafts.listDrafts,
icon: 'ti ti-cloud-download',
action: () => {
showDraftsDialog(false);
},
}, {
}, { type: 'divider' }, {
type: 'button',
text: i18n.ts._drafts.listScheduledNotes,
icon: 'ti ti-clock-down',
action: () => {
showDraftsDialog(true);
},
}, { type: 'divider' }, ...items], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
function showPerUploadItemMenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.contextMenu(menu, ev);
}], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
async function schedule() {
@@ -1419,6 +1422,20 @@ defineExpose({
margin: auto;
}
.draftButton {
padding: 8px;
font-size: 90%;
border-radius: 6px;
&:hover {
background: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
}
&:disabled {
background: none;
}
}
.headerRight {
display: flex;
min-height: 48px;
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, inject, onMounted, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { getUnicodeEmojiOrNull } from '@@/js/emojilist.js';
import { getUnicodeEmoji } from '@@/js/emojilist.js';
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
import type { MenuItem } from '@/types/menu';
import XDetails from '@/components/MkReactionsViewer.details.vue';
@@ -60,11 +60,11 @@ const buttonEl = useTemplateRef('buttonEl');
const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, ''));
const canToggle = computed(() => {
const emoji = customEmojisMap.get(emojiName.value) ?? getUnicodeEmojiOrNull(props.reaction);
const emoji = customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction);
// TODO
//return !props.reaction.match(/@\w/) && $i && emoji && checkReactionPermissions($i, props.note, emoji);
return props.reaction.match(/@\w/) == null && $i != null && emoji != null;
return !props.reaction.match(/@\w/) && $i && emoji;
});
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
const isLocalCustomEmoji = props.reaction[0] === ':' && props.reaction.includes('@.');
@@ -64,8 +64,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
import type { Awaitable } from '@/types/misc.js';
export type SuperMenuDef = {
title?: string;
items: ({
@@ -82,7 +80,7 @@ export type SuperMenuDef = {
text: string;
danger?: boolean;
active?: boolean;
action: (ev: MouseEvent) => Awaitable<void>;
action: (ev: MouseEvent) => void | Promise<void>;
} | {
type?: 'link';
to: string;
@@ -33,7 +33,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- クライアントで検出するMIME typeとサーバーで検出するMIME typeが異なる場合があり混乱の元になるのでとりあえず隠しとく -->
<!-- https://github.com/misskey-dev/misskey/issues/16091 -->
<!-- https://github.com/misskey-dev/misskey/issues/16663 -->
<!--<div>{{ i18n.ts._uploader.allowedTypes }}: {{ $i.policies.uploadableFileTypes.join(', ') }}</div>-->
</div>
</div>
@@ -65,10 +65,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="layer.repeat">
<template #label>{{ i18n.ts._watermarkEditor.repeat }}</template>
</MkSwitch>
<MkSwitch v-model="layerPreserveBoundingRect">
<template #label>{{ i18n.ts._watermarkEditor.preserveBoundingRect }}</template>
</MkSwitch>
</template>
<template v-else-if="layer.type === 'image'">
@@ -133,10 +129,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="layer.cover">
<template #label>{{ i18n.ts._watermarkEditor.cover }}</template>
</MkSwitch>
<MkSwitch v-model="layerPreserveBoundingRect">
<template #label>{{ i18n.ts._watermarkEditor.preserveBoundingRect }}</template>
</MkSwitch>
</template>
<template v-else-if="layer.type === 'qr'">
@@ -343,7 +335,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { ref, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import type { WatermarkPreset } from '@/utility/watermark.js';
import { i18n } from '@/i18n.js';
@@ -359,20 +351,6 @@ import { misskeyApi } from '@/utility/misskey-api.js';
const layer = defineModel<WatermarkPreset['layers'][number]>('layer', { required: true });
const layerPreserveBoundingRect = computed({
get: () => {
if (layer.value.type === 'text' || layer.value.type === 'image') {
return !layer.value.noBoundingBoxExpansion;
}
return false;
},
set: (v: boolean) => {
if (layer.value.type === 'text' || layer.value.type === 'image') {
layer.value.noBoundingBoxExpansion = !v;
}
},
});
const driveFile = ref<Misskey.entities.DriveFile | null>(null);
const driveFileError = ref(false);
onMounted(async () => {
@@ -90,7 +90,6 @@ function createTextLayer(): WatermarkPreset['layers'][number] {
angle: 0,
opacity: 0.75,
repeat: false,
noBoundingBoxExpansion: false,
};
}
@@ -105,7 +104,6 @@ function createImageLayer(): WatermarkPreset['layers'][number] {
angle: 0,
opacity: 0.75,
repeat: false,
noBoundingBoxExpansion: false,
cover: false,
};
}
@@ -62,10 +62,9 @@ import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'v
import { scrollToTop } from '@@/js/scroll.js';
import XTabs from './MkPageHeader.tabs.vue';
import { globalEvents } from '@/events.js';
import { getAccountMenu } from '@/accounts.js';
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { $i } from '@/i.js';
import { DI } from '@/di.js';
import * as os from '@/os.js';
const props = withDefaults(defineProps<PageHeaderProps>(), {
tabs: () => ([] as Tab[]),
@@ -100,12 +99,10 @@ const top = () => {
}
};
async function openAccountMenu(ev: MouseEvent) {
const menuItems = await getAccountMenu({
function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
withExtraOperation: true,
});
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}, ev);
}
function onTabClick(): void {
@@ -634,9 +634,7 @@ export function useUploader(options: {
bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW,
},
audio: {
// Explicitly keep audio (don't discard) and copy it if possible
// without re-encoding to avoid WebCodecs limitations on iOS Safari
discard: false,
bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW,
},
});
+1 -2
View File
@@ -5,7 +5,6 @@
import { throttle } from 'throttle-debounce';
import type { Directive } from 'vue';
import type { Awaitable } from '@/types/misc.js';
interface HTMLElementWithObserver extends HTMLElement {
_observer_?: IntersectionObserver;
@@ -32,4 +31,4 @@ export const appearDirective = {
unmounted(src) {
if (src._observer_) src._observer_.disconnect();
},
} as Directive<HTMLElementWithObserver, (() => Awaitable<void>) | null | undefined>;
} as Directive<HTMLElementWithObserver, () => void>;
+2 -3
View File
@@ -15,7 +15,7 @@ const start = isTouchUsing ? 'touchstart' : 'mouseenter';
const end = isTouchUsing ? 'touchend' : 'mouseleave';
type TooltipDirectiveState = {
text: string | null | undefined;
text: string;
_close: null | (() => void);
showTimer: number | null;
hideTimer: number | null;
@@ -53,7 +53,6 @@ export const tooltipDirective = {
if (binding.arg === 'dialog') {
el.addEventListener('click', (ev) => {
if (binding.value == null) return;
ev.preventDefault();
ev.stopPropagation();
alert({
@@ -129,4 +128,4 @@ export const tooltipDirective = {
if (self.checkTimer) window.clearTimeout(self.checkTimer);
self.close();
},
} as Directive<TooltipDirectiveElement, string | null | undefined, TooltipDirectiveModifiers, TooltipDirectiveArg>;
} as Directive<TooltipDirectiveElement, string, TooltipDirectiveModifiers, TooltipDirectiveArg>;
@@ -131,4 +131,4 @@ export const userPreviewDirective = {
if (self == null) return;
self.preview.detach();
},
} as Directive<UserPreviewDirectiveElement, string | Misskey.entities.UserDetailed | null | undefined>;
} as Directive<UserPreviewDirectiveElement, string | Misskey.entities.UserDetailed>;
-1
View File
@@ -25,7 +25,6 @@ export type Keys = (
'bootloaderLocales' |
'theme' |
'themeId' |
'themeCachedVersion' |
'customCss' |
'chatMessageDrafts' |
'scratchpad' |
@@ -295,9 +295,6 @@ const patronsWithIcon = [{
}, {
name: 'しゃどかの',
icon: 'https://assets.misskey-hub.net/patrons/5bec3c6b402942619e03f7a2ae76d69e.jpg',
}, {
name: '大賀愛一郎',
icon: 'https://assets.misskey-hub.net/patrons/c701a797d1df4125970f25d3052250ac.jpg',
}];
const patrons = [
-85
View File
@@ -1,85 +0,0 @@
// Description : Array and textureless GLSL 2D/3D/4D simplex
// noise functions.
// Author : Ian McEwan, Ashima Arts.
// Maintainer : stegu
// Lastmod : 20201014 (stegu)
// License : Copyright (C) 2011 Ashima Arts. All rights reserved.
// Distributed under the MIT License. See LICENSE file.
// https://github.com/ashima/webgl-noise
// https://github.com/stegu/webgl-noise
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 mod289(vec4 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 permute(vec4 x) {
return mod289(((x * 34.0) + 10.0) * x);
}
vec4 taylorInvSqrt(vec4 r) {
return 1.79284291400159 - 0.85373472095314 * r;
}
float snoise(vec3 v) {
const vec2 C = vec2(1.0/6.0, 1.0/3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i);
vec4 p = permute(permute(permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.5 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
m = m * m;
return 105.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}
+4 -11
View File
@@ -10,7 +10,6 @@ import tinycolor from 'tinycolor2';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import JSON5 from 'json5';
import { version } from '@@/js/config.js';
import type { Ref } from 'vue';
import type { BundledTheme } from 'shiki/themes';
import { deepClone } from '@/utility/clone.js';
@@ -124,7 +123,6 @@ function applyThemeInternal(theme: Theme, persist: boolean) {
if (persist) {
miLocalStorage.setItem('theme', JSON.stringify(props));
miLocalStorage.setItem('themeId', theme.id);
miLocalStorage.setItem('themeCachedVersion', version);
miLocalStorage.setItem('colorScheme', colorScheme);
}
@@ -133,7 +131,7 @@ function applyThemeInternal(theme: Theme, persist: boolean) {
}
let timeout: number | null = null;
let currentThemeId = miLocalStorage.getItem('themeId');
let currentTheme: Theme | null = null;
export function applyTheme(theme: Theme, persist = true) {
if (timeout) {
@@ -141,8 +139,9 @@ export function applyTheme(theme: Theme, persist = true) {
timeout = null;
}
if (theme.id === currentThemeId && miLocalStorage.getItem('themeCachedVersion') === version) return;
currentThemeId = theme.id;
if (deepEqual(currentTheme, theme)) return;
// リアクティビティ解除
currentTheme = deepClone(theme);
if (window.document.startViewTransition != null) {
window.document.documentElement.classList.add('_themeChanging_');
@@ -242,9 +241,3 @@ export async function installTheme(code: string): Promise<void> {
if (!theme) return;
await addTheme(theme);
}
export function clearAppliedThemeCache() {
miLocalStorage.removeItem('theme');
miLocalStorage.removeItem('themeId');
miLocalStorage.removeItem('themeCachedVersion');
}
+8
View File
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
export type WithNonNullable<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]> };
-6
View File
@@ -1,6 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type Awaitable <T> = T | Promise<T>;
@@ -55,7 +55,7 @@ import MkButton from '@/components/MkButton.vue';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { getAccountMenu } from '@/accounts.js';
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { $i } from '@/i.js';
import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
@@ -84,12 +84,10 @@ async function more(ev: MouseEvent) {
});
}
async function openAccountMenu(ev: MouseEvent) {
const menuItems = await getAccountMenu({
function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
withExtraOperation: true,
});
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}, ev);
}
onMounted(() => {
+4 -6
View File
@@ -109,7 +109,7 @@ import { instance } from '@/instance.js';
import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
import { useRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { getAccountMenu } from '@/accounts.js';
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { $i } from '@/i.js';
const router = useRouter();
@@ -170,12 +170,10 @@ function toggleRealtimeMode(ev: MouseEvent) {
}], ev.currentTarget ?? ev.target);
}
async function openAccountMenu(ev: MouseEvent) {
const menuItems = await getAccountMenu({
function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
withExtraOperation: true,
});
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}, ev);
}
async function more(ev: MouseEvent) {
+1 -2
View File
@@ -9,15 +9,14 @@ import * as os from '@/os.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { fetchInstance } from '@/instance.js';
import { clearAppliedThemeCache } from '@/theme.js';
export async function clearCache() {
os.waiting();
miLocalStorage.removeItem('instance');
miLocalStorage.removeItem('instanceCachedAt');
miLocalStorage.removeItem('theme');
miLocalStorage.removeItem('emojis');
miLocalStorage.removeItem('lastEmojisFetchedAt');
clearAppliedThemeCache();
await misskeyApiGet('clear-browser-cache', {}).catch(() => {
// ignore
});
@@ -1,43 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform int u_amount;
uniform float u_shiftStrengths[128];
uniform vec2 u_shiftOrigins[128];
uniform vec2 u_shiftSizes[128];
uniform float u_channelShift;
out vec4 out_color;
void main() {
// TODO: ピクセル毎に計算する必要はないのでuniformにする
float aspect_ratio = min(in_resolution.x, in_resolution.y) / max(in_resolution.x, in_resolution.y);
float aspect_ratio_x = in_resolution.x > in_resolution.y ? 1.0 : aspect_ratio;
float aspect_ratio_y = in_resolution.x < in_resolution.y ? 1.0 : aspect_ratio;
float v = 0.0;
for (int i = 0; i < u_amount; i++) {
if (
in_uv.x * aspect_ratio_x > ((u_shiftOrigins[i].x * aspect_ratio_x) - u_shiftSizes[i].x) &&
in_uv.x * aspect_ratio_x < ((u_shiftOrigins[i].x * aspect_ratio_x) + u_shiftSizes[i].x) &&
in_uv.y * aspect_ratio_y > ((u_shiftOrigins[i].y * aspect_ratio_y) - u_shiftSizes[i].y) &&
in_uv.y * aspect_ratio_y < ((u_shiftOrigins[i].y * aspect_ratio_y) + u_shiftSizes[i].y)
) {
v += u_shiftStrengths[i];
}
}
float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r;
float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g;
float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b;
float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a;
out_color = vec4(r, g, b, a);
}
@@ -4,10 +4,49 @@
*/
import seedrandom from 'seedrandom';
import shader from './blockNoise.glsl';
import { defineImageEffectorFx } from '../ImageEffector.js';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform int u_amount;
uniform float u_shiftStrengths[128];
uniform vec2 u_shiftOrigins[128];
uniform vec2 u_shiftSizes[128];
uniform float u_channelShift;
out vec4 out_color;
void main() {
// TODO: ピクセル毎に計算する必要はないのでuniformにする
float aspect_ratio = min(in_resolution.x, in_resolution.y) / max(in_resolution.x, in_resolution.y);
float aspect_ratio_x = in_resolution.x > in_resolution.y ? 1.0 : aspect_ratio;
float aspect_ratio_y = in_resolution.x < in_resolution.y ? 1.0 : aspect_ratio;
float v = 0.0;
for (int i = 0; i < u_amount; i++) {
if (
in_uv.x * aspect_ratio_x > ((u_shiftOrigins[i].x * aspect_ratio_x) - u_shiftSizes[i].x) &&
in_uv.x * aspect_ratio_x < ((u_shiftOrigins[i].x * aspect_ratio_x) + u_shiftSizes[i].x) &&
in_uv.y * aspect_ratio_y > ((u_shiftOrigins[i].y * aspect_ratio_y) - u_shiftSizes[i].y) &&
in_uv.y * aspect_ratio_y < ((u_shiftOrigins[i].y * aspect_ratio_y) + u_shiftSizes[i].y)
) {
v += u_shiftStrengths[i];
}
}
float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r;
float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g;
float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b;
float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a;
out_color = vec4(r, g, b, a);
}
`;
export const FX_blockNoise = defineImageEffectorFx({
id: 'blockNoise',
name: i18n.ts._imageEffector._fxs.glitch + ': ' + i18n.ts._imageEffector._fxs.blockNoise,
@@ -1,78 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
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 bool u_ellipse;
uniform float u_angle;
uniform float u_radius;
uniform int u_samples;
out vec4 out_color;
void main() {
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 = false;
if (u_ellipse) {
vec2 norm = (rotatedUV - u_offset) / u_scale;
isInside = dot(norm, norm) <= 1.0;
} else {
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;
}
if (!isInside) {
out_color = texture(in_texture, in_uv);
return;
}
vec4 result = vec4(0.0);
float totalSamples = 0.0;
// Make blur radius resolution-independent by using a percentage of image size
// This ensures consistent visual blur regardless of image resolution
float referenceSize = min(in_resolution.x, in_resolution.y);
float normalizedRadius = u_radius / 100.0; // Convert radius to percentage (0-15 -> 0-0.15)
vec2 blurOffset = vec2(normalizedRadius) / in_resolution * referenceSize;
// Calculate how many samples to take in each direction
// This determines the grid density, not the blur extent
int sampleRadius = int(sqrt(float(u_samples)) / 2.0);
// Sample in a grid pattern within the specified radius
for (int x = -sampleRadius; x <= sampleRadius; x++) {
for (int y = -sampleRadius; y <= sampleRadius; y++) {
// Normalize the grid position to [-1, 1] range
float normalizedX = float(x) / float(sampleRadius);
float normalizedY = float(y) / float(sampleRadius);
// Scale by radius to get the actual sampling offset
vec2 offset = vec2(normalizedX, normalizedY) * blurOffset;
vec2 sampleUV = in_uv + offset;
// Only sample if within texture bounds
if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 && sampleUV.y >= 0.0 && sampleUV.y <= 1.0) {
result += texture(in_texture, sampleUV);
totalSamples += 1.0;
}
}
}
out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv);
}
@@ -4,9 +4,83 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './blur.glsl';
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 bool u_ellipse;
uniform float u_angle;
uniform float u_radius;
uniform int u_samples;
out vec4 out_color;
void main() {
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 = false;
if (u_ellipse) {
vec2 norm = (rotatedUV - u_offset) / u_scale;
isInside = dot(norm, norm) <= 1.0;
} else {
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;
}
if (!isInside) {
out_color = texture(in_texture, in_uv);
return;
}
vec4 result = vec4(0.0);
float totalSamples = 0.0;
// Make blur radius resolution-independent by using a percentage of image size
// This ensures consistent visual blur regardless of image resolution
float referenceSize = min(in_resolution.x, in_resolution.y);
float normalizedRadius = u_radius / 100.0; // Convert radius to percentage (0-15 -> 0-0.15)
vec2 blurOffset = vec2(normalizedRadius) / in_resolution * referenceSize;
// Calculate how many samples to take in each direction
// This determines the grid density, not the blur extent
int sampleRadius = int(sqrt(float(u_samples)) / 2.0);
// Sample in a grid pattern within the specified radius
for (int x = -sampleRadius; x <= sampleRadius; x++) {
for (int y = -sampleRadius; y <= sampleRadius; y++) {
// Normalize the grid position to [-1, 1] range
float normalizedX = float(x) / float(sampleRadius);
float normalizedY = float(y) / float(sampleRadius);
// Scale by radius to get the actual sampling offset
vec2 offset = vec2(normalizedX, normalizedY) * blurOffset;
vec2 sampleUV = in_uv + offset;
// Only sample if within texture bounds
if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 && sampleUV.y >= 0.0 && sampleUV.y <= 1.0) {
result += texture(in_texture, sampleUV);
totalSamples += 1.0;
}
}
}
out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv);
}
`;
export const FX_blur = defineImageEffectorFx({
id: 'blur',
name: i18n.ts._imageEffector._fxs.blur,
@@ -1,43 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
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 float u_angle;
uniform float u_scale;
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)) * vec2(x_ratio, y_ratio);
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
);
float fmodResult = mod(floor(u_scale * rotatedUV.x) + floor(u_scale * rotatedUV.y), 2.0);
float fin = max(sign(fmodResult), 0.0);
out_color = vec4(
mix(in_color.r, u_color.r, fin * u_opacity),
mix(in_color.g, u_color.g, fin * u_opacity),
mix(in_color.b, u_color.b, fin * u_opacity),
in_color.a
);
}
@@ -4,9 +4,48 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './checker.glsl';
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 float u_angle;
uniform float u_scale;
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)) * vec2(x_ratio, y_ratio);
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
);
float fmodResult = mod(floor(u_scale * rotatedUV.x) + floor(u_scale * rotatedUV.y), 2.0);
float fin = max(sign(fmodResult), 0.0);
out_color = vec4(
mix(in_color.r, u_color.r, fin * u_opacity),
mix(in_color.g, u_color.g, fin * u_opacity),
mix(in_color.b, u_color.b, fin * u_opacity),
in_color.a
);
}
`;
export const FX_checker = defineImageEffectorFx({
id: 'checker',
name: i18n.ts._imageEffector._fxs.checker,
@@ -1,49 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
out vec4 out_color;
uniform float u_amount;
uniform float u_start;
uniform bool u_normalize;
void main() {
int samples = 64;
float r_strength = 1.0;
float g_strength = 1.5;
float b_strength = 2.0;
vec2 size = vec2(in_resolution.x, in_resolution.y);
vec4 accumulator = vec4(0.0);
float normalisedValue = length((in_uv - 0.5) * 2.0);
float strength = clamp((normalisedValue - u_start) * (1.0 / (1.0 - u_start)), 0.0, 1.0);
vec2 vector = (u_normalize ? normalize(in_uv - vec2(0.5)) : in_uv - vec2(0.5));
vec2 velocity = vector * strength * u_amount;
vec2 rOffset = -vector * strength * (u_amount * r_strength);
vec2 gOffset = -vector * strength * (u_amount * g_strength);
vec2 bOffset = -vector * strength * (u_amount * b_strength);
for (int i = 0; i < samples; i++) {
accumulator.r += texture(in_texture, in_uv + rOffset).r;
rOffset -= velocity / float(samples);
accumulator.g += texture(in_texture, in_uv + gOffset).g;
gOffset -= velocity / float(samples);
accumulator.b += texture(in_texture, in_uv + bOffset).b;
bOffset -= velocity / float(samples);
}
out_color = vec4(vec3(accumulator / float(samples)), 1.0);
}
@@ -4,9 +4,53 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './chromaticAberration.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
out vec4 out_color;
uniform float u_amount;
uniform float u_start;
uniform bool u_normalize;
void main() {
int samples = 64;
float r_strength = 1.0;
float g_strength = 1.5;
float b_strength = 2.0;
vec2 size = vec2(in_resolution.x, in_resolution.y);
vec4 accumulator = vec4(0.0);
float normalisedValue = length((in_uv - 0.5) * 2.0);
float strength = clamp((normalisedValue - u_start) * (1.0 / (1.0 - u_start)), 0.0, 1.0);
vec2 vector = (u_normalize ? normalize(in_uv - vec2(0.5)) : in_uv - vec2(0.5));
vec2 velocity = vector * strength * u_amount;
vec2 rOffset = -vector * strength * (u_amount * r_strength);
vec2 gOffset = -vector * strength * (u_amount * g_strength);
vec2 bOffset = -vector * strength * (u_amount * b_strength);
for (int i = 0; i < samples; i++) {
accumulator.r += texture(in_texture, in_uv + rOffset).r;
rOffset -= velocity / float(samples);
accumulator.g += texture(in_texture, in_uv + gOffset).g;
gOffset -= velocity / float(samples);
accumulator.b += texture(in_texture, in_uv + bOffset).b;
bOffset -= velocity / float(samples);
}
out_color = vec4(vec3(accumulator / float(samples)), 1.0);
}
`;
export const FX_chromaticAberration = defineImageEffectorFx({
id: 'chromaticAberration',
name: i18n.ts._imageEffector._fxs.chromaticAberration,
@@ -1,82 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_brightness;
uniform float u_contrast;
uniform float u_hue;
uniform float u_lightness;
uniform float u_saturation;
out vec4 out_color;
// RGB to HSL
vec3 rgb2hsl(vec3 c) {
float maxc = max(max(c.r, c.g), c.b);
float minc = min(min(c.r, c.g), c.b);
float l = (maxc + minc) * 0.5;
float s = 0.0;
float h = 0.0;
if (maxc != minc) {
float d = maxc - minc;
s = l > 0.5 ? d / (2.0 - maxc - minc) : d / (maxc + minc);
if (maxc == c.r) {
h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
} else if (maxc == c.g) {
h = (c.b - c.r) / d + 2.0;
} else {
h = (c.r - c.g) / d + 4.0;
}
h /= 6.0;
}
return vec3(h, s, l);
}
// HSL to RGB
float hue2rgb(float p, float q, float t) {
if (t < 0.0) t += 1.0;
if (t > 1.0) t -= 1.0;
if (t < 1.0/6.0) return p + (q - p) * 6.0 * t;
if (t < 1.0/2.0) return q;
if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
return p;
}
vec3 hsl2rgb(vec3 hsl) {
float r, g, b;
float h = hsl.x;
float s = hsl.y;
float l = hsl.z;
if (s == 0.0) {
r = g = b = l;
} else {
float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
float p = 2.0 * l - q;
r = hue2rgb(p, q, h + 1.0/3.0);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1.0/3.0);
}
return vec3(r, g, b);
}
void main() {
vec4 in_color = texture(in_texture, in_uv);
vec3 color = in_color.rgb;
color = color * u_brightness;
color += vec3(u_lightness);
color = (color - 0.5) * u_contrast + 0.5;
vec3 hsl = rgb2hsl(color);
hsl.x = mod(hsl.x + u_hue, 1.0);
hsl.y = clamp(hsl.y * u_saturation, 0.0, 1.0);
color = hsl2rgb(hsl);
out_color = vec4(color, in_color.a);
}
@@ -4,9 +4,86 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './colorAdjust.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_brightness;
uniform float u_contrast;
uniform float u_hue;
uniform float u_lightness;
uniform float u_saturation;
out vec4 out_color;
// RGB to HSL
vec3 rgb2hsl(vec3 c) {
float maxc = max(max(c.r, c.g), c.b);
float minc = min(min(c.r, c.g), c.b);
float l = (maxc + minc) * 0.5;
float s = 0.0;
float h = 0.0;
if (maxc != minc) {
float d = maxc - minc;
s = l > 0.5 ? d / (2.0 - maxc - minc) : d / (maxc + minc);
if (maxc == c.r) {
h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
} else if (maxc == c.g) {
h = (c.b - c.r) / d + 2.0;
} else {
h = (c.r - c.g) / d + 4.0;
}
h /= 6.0;
}
return vec3(h, s, l);
}
// HSL to RGB
float hue2rgb(float p, float q, float t) {
if (t < 0.0) t += 1.0;
if (t > 1.0) t -= 1.0;
if (t < 1.0/6.0) return p + (q - p) * 6.0 * t;
if (t < 1.0/2.0) return q;
if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
return p;
}
vec3 hsl2rgb(vec3 hsl) {
float r, g, b;
float h = hsl.x;
float s = hsl.y;
float l = hsl.z;
if (s == 0.0) {
r = g = b = l;
} else {
float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
float p = 2.0 * l - q;
r = hue2rgb(p, q, h + 1.0/3.0);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1.0/3.0);
}
return vec3(r, g, b);
}
void main() {
vec4 in_color = texture(in_texture, in_uv);
vec3 color = in_color.rgb;
color = color * u_brightness;
color += vec3(u_lightness);
color = (color - 0.5) * u_contrast + 0.5;
vec3 hsl = rgb2hsl(color);
hsl.x = mod(hsl.x + u_hue, 1.0);
hsl.y = clamp(hsl.y * u_saturation, 0.0, 1.0);
color = hsl2rgb(hsl);
out_color = vec4(color, in_color.a);
}
`;
export const FX_colorAdjust = defineImageEffectorFx({
id: 'colorAdjust',
name: i18n.ts._imageEffector._fxs.colorAdjust,
@@ -1,29 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// colorClamp, colorClampAdvanced共通
// colorClampではmax, minがすべて同じ値となる
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_rMax;
uniform float u_rMin;
uniform float u_gMax;
uniform float u_gMin;
uniform float u_bMax;
uniform float u_bMin;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
float r = min(max(in_color.r, u_rMin), u_rMax);
float g = min(max(in_color.g, u_gMin), u_gMax);
float b = min(max(in_color.b, u_bMin), u_bMax);
out_color = vec4(r, g, b, in_color.a);
}
@@ -4,14 +4,32 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './colorClamp.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_max;
uniform float u_min;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
float r = min(max(in_color.r, u_min), u_max);
float g = min(max(in_color.g, u_min), u_max);
float b = min(max(in_color.b, u_min), u_max);
out_color = vec4(r, g, b, in_color.a);
}
`;
export const FX_colorClamp = defineImageEffectorFx({
id: 'colorClamp',
name: i18n.ts._imageEffector._fxs.colorClamp,
shader,
uniforms: ['rMax', 'rMin', 'gMax', 'gMin', 'bMax', 'bMin'] as const,
uniforms: ['max', 'min'] as const,
params: {
max: {
label: i18n.ts._imageEffector._fxProps.max,
@@ -33,11 +51,7 @@ export const FX_colorClamp = defineImageEffectorFx({
},
},
main: ({ gl, u, params }) => {
gl.uniform1f(u.rMax, params.max);
gl.uniform1f(u.rMin, 1.0 + params.min);
gl.uniform1f(u.gMax, params.max);
gl.uniform1f(u.gMin, 1.0 + params.min);
gl.uniform1f(u.bMax, params.max);
gl.uniform1f(u.bMin, 1.0 + params.min);
gl.uniform1f(u.max, params.max);
gl.uniform1f(u.min, 1.0 + params.min);
},
});
@@ -4,9 +4,31 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './colorClamp.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_rMax;
uniform float u_rMin;
uniform float u_gMax;
uniform float u_gMin;
uniform float u_bMax;
uniform float u_bMin;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
float r = min(max(in_color.r, u_rMin), u_rMax);
float g = min(max(in_color.g, u_gMin), u_gMax);
float b = min(max(in_color.b, u_bMin), u_bMax);
out_color = vec4(r, g, b, in_color.a);
}
`;
export const FX_colorClampAdvanced = defineImageEffectorFx({
id: 'colorClampAdvanced',
name: i18n.ts._imageEffector._fxs.colorClampAdvanced,
@@ -1,30 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
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 float u_phase;
uniform float u_frequency;
uniform float u_strength;
uniform int u_direction; // 0: vertical, 1: horizontal
out vec4 out_color;
void main() {
float v = u_direction == 0 ?
sin((HALF_PI + (u_phase * PI) - (u_frequency / 2.0)) + in_uv.y * u_frequency) * u_strength :
sin((HALF_PI + (u_phase * PI) - (u_frequency / 2.0)) + in_uv.x * u_frequency) * u_strength;
vec4 in_color = u_direction == 0 ?
texture(in_texture, vec2(in_uv.x + v, in_uv.y)) :
texture(in_texture, vec2(in_uv.x, in_uv.y + v));
out_color = in_color;
}
@@ -4,9 +4,35 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './distort.glsl';
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 float u_phase;
uniform float u_frequency;
uniform float u_strength;
uniform int u_direction; // 0: vertical, 1: horizontal
out vec4 out_color;
void main() {
float v = u_direction == 0 ?
sin((HALF_PI + (u_phase * PI) - (u_frequency / 2.0)) + in_uv.y * u_frequency) * u_strength :
sin((HALF_PI + (u_phase * PI) - (u_frequency / 2.0)) + in_uv.x * u_frequency) * u_strength;
vec4 in_color = u_direction == 0 ?
texture(in_texture, vec2(in_uv.x + v, in_uv.y)) :
texture(in_texture, vec2(in_uv.x, in_uv.y + v));
out_color = in_color;
}
`;
export const FX_distort = defineImageEffectorFx({
id: 'distort',
name: i18n.ts._imageEffector._fxs.distort,
@@ -1,50 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
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 bool u_ellipse;
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 = false;
if (u_ellipse) {
vec2 norm = (rotatedUV - u_offset) / u_scale;
isInside = dot(norm, norm) <= 1.0;
} else {
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;
}
@@ -4,9 +4,55 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './fill.glsl';
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 bool u_ellipse;
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 = false;
if (u_ellipse) {
vec2 norm = (rotatedUV - u_offset) / u_scale;
isInside = dot(norm, norm) <= 1.0;
} else {
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_fill = defineImageEffectorFx({
id: 'fill',
name: i18n.ts._imageEffector._fxs.fill,
@@ -1,22 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
out vec4 out_color;
float getBrightness(vec4 color) {
return (color.r + color.g + color.b) / 3.0;
}
void main() {
vec4 in_color = texture(in_texture, in_uv);
float brightness = getBrightness(in_color);
out_color = vec4(brightness, brightness, brightness, in_color.a);
}
@@ -4,9 +4,27 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './grayscale.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
out vec4 out_color;
float getBrightness(vec4 color) {
return (color.r + color.g + color.b) / 3.0;
}
void main() {
vec4 in_color = texture(in_texture, in_uv);
float brightness = getBrightness(in_color);
out_color = vec4(brightness, brightness, brightness, in_color.a);
}
`;
export const FX_grayscale = defineImageEffectorFx({
id: 'grayscale',
name: i18n.ts._imageEffector._fxs.grayscale,
@@ -1,23 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform bool u_r;
uniform bool u_g;
uniform bool u_b;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
out_color.r = u_r ? 1.0 - in_color.r : in_color.r;
out_color.g = u_g ? 1.0 - in_color.g : in_color.g;
out_color.b = u_b ? 1.0 - in_color.b : in_color.b;
out_color.a = in_color.a;
}
@@ -4,9 +4,28 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './invert.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform bool u_r;
uniform bool u_g;
uniform bool u_b;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
out_color.r = u_r ? 1.0 - in_color.r : in_color.r;
out_color.g = u_g ? 1.0 - in_color.g : in_color.g;
out_color.b = u_b ? 1.0 - in_color.b : in_color.b;
out_color.a = in_color.a;
}
`;
export const FX_invert = defineImageEffectorFx({
id: 'invert',
name: i18n.ts._imageEffector._fxs.invert,
@@ -1,26 +0,0 @@
#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform int u_h;
uniform int u_v;
out vec4 out_color;
void main() {
vec2 uv = in_uv;
if (u_h == -1 && in_uv.x > 0.5) {
uv.x = 1.0 - uv.x;
}
if (u_h == 1 && in_uv.x < 0.5) {
uv.x = 1.0 - uv.x;
}
if (u_v == -1 && in_uv.y > 0.5) {
uv.y = 1.0 - uv.y;
}
if (u_v == 1 && in_uv.y < 0.5) {
uv.y = 1.0 - uv.y;
}
out_color = texture(in_texture, uv);
}
@@ -4,9 +4,36 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './mirror.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform int u_h;
uniform int u_v;
out vec4 out_color;
void main() {
vec2 uv = in_uv;
if (u_h == -1 && in_uv.x > 0.5) {
uv.x = 1.0 - uv.x;
}
if (u_h == 1 && in_uv.x < 0.5) {
uv.x = 1.0 - uv.x;
}
if (u_v == -1 && in_uv.y > 0.5) {
uv.y = 1.0 - uv.y;
}
if (u_v == 1 && in_uv.y < 0.5) {
uv.y = 1.0 - uv.y;
}
out_color = texture(in_texture, uv);
}
`;
export const FX_mirror = defineImageEffectorFx({
id: 'mirror',
name: i18n.ts._imageEffector._fxs.mirror,
@@ -1,68 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
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 bool u_ellipse;
uniform float u_angle;
uniform int u_samples;
uniform float u_strength;
out vec4 out_color;
// TODO: pixelateの中心を画像中心ではなく範囲の中心にする
// TODO: 画像のアスペクト比に関わらず各画素は正方形にする
void main() {
if (u_strength <= 0.0) {
out_color = texture(in_texture, in_uv);
return;
}
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 = false;
if (u_ellipse) {
vec2 norm = (rotatedUV - u_offset) / u_scale;
isInside = dot(norm, norm) <= 1.0;
} else {
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;
}
if (!isInside) {
out_color = texture(in_texture, in_uv);
return;
}
float dx = u_strength / 1.0;
float dy = u_strength / 1.0;
vec2 new_uv = vec2(
(dx * (floor((in_uv.x - 0.5 - (dx / 2.0)) / dx) + 0.5)),
(dy * (floor((in_uv.y - 0.5 - (dy / 2.0)) / dy) + 0.5))
) + vec2(0.5 + (dx / 2.0), 0.5 + (dy / 2.0));
vec4 result = vec4(0.0);
float totalSamples = 0.0;
// TODO: より多くのサンプリング
result += texture(in_texture, new_uv);
totalSamples += 1.0;
out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv);
}
@@ -4,9 +4,73 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './pixelate.glsl';
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 bool u_ellipse;
uniform float u_angle;
uniform int u_samples;
uniform float u_strength;
out vec4 out_color;
// TODO: pixelateの中心を画像中心ではなく範囲の中心にする
// TODO: 画像のアスペクト比に関わらず各画素は正方形にする
void main() {
if (u_strength <= 0.0) {
out_color = texture(in_texture, in_uv);
return;
}
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 = false;
if (u_ellipse) {
vec2 norm = (rotatedUV - u_offset) / u_scale;
isInside = dot(norm, norm) <= 1.0;
} else {
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;
}
if (!isInside) {
out_color = texture(in_texture, in_uv);
return;
}
float dx = u_strength / 1.0;
float dy = u_strength / 1.0;
vec2 new_uv = vec2(
(dx * (floor((in_uv.x - 0.5 - (dx / 2.0)) / dx) + 0.5)),
(dy * (floor((in_uv.y - 0.5 - (dy / 2.0)) / dy) + 0.5))
) + vec2(0.5 + (dx / 2.0), 0.5 + (dy / 2.0));
vec4 result = vec4(0.0);
float totalSamples = 0.0;
// TODO: より多くのサンプリング
result += texture(in_texture, new_uv);
totalSamples += 1.0;
out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv);
}
`;
export const FX_pixelate = defineImageEffectorFx({
id: 'pixelate',
name: i18n.ts._imageEffector._fxs.pixelate,
@@ -1,75 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
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 float u_angle;
uniform float u_scale;
uniform float u_major_radius;
uniform float u_major_opacity;
uniform float u_minor_divisions;
uniform float u_minor_radius;
uniform float u_minor_opacity;
uniform vec3 u_color;
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)) * vec2(x_ratio, y_ratio);
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
);
float major_modX = mod(rotatedUV.x, (1.0 / u_scale));
float major_modY = mod(rotatedUV.y, (1.0 / u_scale));
float major_threshold = ((u_major_radius / 2.0) / u_scale);
if (
length(vec2(major_modX, major_modY)) < major_threshold ||
length(vec2((1.0 / u_scale) - major_modX, major_modY)) < major_threshold ||
length(vec2(major_modX, (1.0 / u_scale) - major_modY)) < major_threshold ||
length(vec2((1.0 / u_scale) - major_modX, (1.0 / u_scale) - major_modY)) < major_threshold
) {
out_color = vec4(
mix(in_color.r, u_color.r, u_major_opacity),
mix(in_color.g, u_color.g, u_major_opacity),
mix(in_color.b, u_color.b, u_major_opacity),
in_color.a
);
return;
}
float minor_modX = mod(rotatedUV.x, (1.0 / u_scale / u_minor_divisions));
float minor_modY = mod(rotatedUV.y, (1.0 / u_scale / u_minor_divisions));
float minor_threshold = ((u_minor_radius / 2.0) / (u_minor_divisions * u_scale));
if (
length(vec2(minor_modX, minor_modY)) < minor_threshold ||
length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, minor_modY)) < minor_threshold ||
length(vec2(minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold ||
length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold
) {
out_color = vec4(
mix(in_color.r, u_color.r, u_minor_opacity),
mix(in_color.g, u_color.g, u_minor_opacity),
mix(in_color.b, u_color.b, u_minor_opacity),
in_color.a
);
return;
}
out_color = in_color;
}
@@ -4,9 +4,80 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './polkadot.glsl';
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 float u_angle;
uniform float u_scale;
uniform float u_major_radius;
uniform float u_major_opacity;
uniform float u_minor_divisions;
uniform float u_minor_radius;
uniform float u_minor_opacity;
uniform vec3 u_color;
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)) * vec2(x_ratio, y_ratio);
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
);
float major_modX = mod(rotatedUV.x, (1.0 / u_scale));
float major_modY = mod(rotatedUV.y, (1.0 / u_scale));
float major_threshold = ((u_major_radius / 2.0) / u_scale);
if (
length(vec2(major_modX, major_modY)) < major_threshold ||
length(vec2((1.0 / u_scale) - major_modX, major_modY)) < major_threshold ||
length(vec2(major_modX, (1.0 / u_scale) - major_modY)) < major_threshold ||
length(vec2((1.0 / u_scale) - major_modX, (1.0 / u_scale) - major_modY)) < major_threshold
) {
out_color = vec4(
mix(in_color.r, u_color.r, u_major_opacity),
mix(in_color.g, u_color.g, u_major_opacity),
mix(in_color.b, u_color.b, u_major_opacity),
in_color.a
);
return;
}
float minor_modX = mod(rotatedUV.x, (1.0 / u_scale / u_minor_divisions));
float minor_modY = mod(rotatedUV.y, (1.0 / u_scale / u_minor_divisions));
float minor_threshold = ((u_minor_radius / 2.0) / (u_minor_divisions * u_scale));
if (
length(vec2(minor_modX, minor_modY)) < minor_threshold ||
length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, minor_modY)) < minor_threshold ||
length(vec2(minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold ||
length(vec2((1.0 / u_scale / u_minor_divisions) - minor_modX, (1.0 / u_scale / u_minor_divisions) - minor_modY)) < minor_threshold
) {
out_color = vec4(
mix(in_color.r, u_color.r, u_minor_opacity),
mix(in_color.g, u_color.g, u_minor_opacity),
mix(in_color.b, u_color.b, u_minor_opacity),
in_color.a
);
return;
}
out_color = in_color;
}
`;
// Primarily used for watermark
export const FX_polkadot = defineImageEffectorFx({
id: 'polkadot',
@@ -1,45 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
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 float u_angle;
uniform float u_frequency;
uniform float u_phase;
uniform float u_threshold;
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)) * vec2(x_ratio, y_ratio);
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
);
float phase = u_phase * TWO_PI;
float value = (1.0 + sin((rotatedUV.x * u_frequency - HALF_PI) + phase)) / 2.0;
value = value < u_threshold ? 1.0 : 0.0;
out_color = vec4(
mix(in_color.r, u_color.r, value * u_opacity),
mix(in_color.g, u_color.g, value * u_opacity),
mix(in_color.b, u_color.b, value * u_opacity),
in_color.a
);
}
@@ -4,9 +4,50 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './stripe.glsl';
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 float u_angle;
uniform float u_frequency;
uniform float u_phase;
uniform float u_threshold;
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)) * vec2(x_ratio, y_ratio);
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
);
float phase = u_phase * TWO_PI;
float value = (1.0 + sin((rotatedUV.x * u_frequency - HALF_PI) + phase)) / 2.0;
value = value < u_threshold ? 1.0 : 0.0;
out_color = vec4(
mix(in_color.r, u_color.r, value * u_opacity),
mix(in_color.g, u_color.g, value * u_opacity),
mix(in_color.b, u_color.b, value * u_opacity),
in_color.a
);
}
`;
// Primarily used for watermark
export const FX_stripe = defineImageEffectorFx({
id: 'stripe',
@@ -1,33 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform int u_amount;
uniform float u_shiftStrengths[128];
uniform float u_shiftOrigins[128];
uniform float u_shiftHeights[128];
uniform float u_channelShift;
out vec4 out_color;
void main() {
float v = 0.0;
for (int i = 0; i < u_amount; i++) {
if (in_uv.y > (u_shiftOrigins[i] - u_shiftHeights[i]) && in_uv.y < (u_shiftOrigins[i] + u_shiftHeights[i])) {
v += u_shiftStrengths[i];
}
}
float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r;
float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g;
float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b;
float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a;
out_color = vec4(r, g, b, a);
}
@@ -4,10 +4,39 @@
*/
import seedrandom from 'seedrandom';
import shader from './tearing.glsl';
import { defineImageEffectorFx } from '../ImageEffector.js';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform int u_amount;
uniform float u_shiftStrengths[128];
uniform float u_shiftOrigins[128];
uniform float u_shiftHeights[128];
uniform float u_channelShift;
out vec4 out_color;
void main() {
float v = 0.0;
for (int i = 0; i < u_amount; i++) {
if (in_uv.y > (u_shiftOrigins[i] - u_shiftHeights[i]) && in_uv.y < (u_shiftOrigins[i] + u_shiftHeights[i])) {
v += u_shiftStrengths[i];
}
}
float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r;
float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g;
float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b;
float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a;
out_color = vec4(r, g, b, a);
}
`;
export const FX_tearing = defineImageEffectorFx({
id: 'tearing',
name: i18n.ts._imageEffector._fxs.glitch + ': ' + i18n.ts._imageEffector._fxs.tearing,
@@ -1,23 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_r;
uniform float u_g;
uniform float u_b;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
float r = in_color.r < u_r ? 0.0 : 1.0;
float g = in_color.g < u_g ? 0.0 : 1.0;
float b = in_color.b < u_b ? 0.0 : 1.0;
out_color = vec4(r, g, b, in_color.a);
}
@@ -4,9 +4,28 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './threshold.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_r;
uniform float u_g;
uniform float u_b;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
float r = in_color.r < u_r ? 0.0 : 1.0;
float g = in_color.g < u_g ? 0.0 : 1.0;
float b = in_color.b < u_b ? 0.0 : 1.0;
out_color = vec4(r, g, b, in_color.a);
}
`;
export const FX_threshold = defineImageEffectorFx({
id: 'threshold',
name: i18n.ts._imageEffector._fxs.threshold,
@@ -1,147 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
const float PI = 3.141592653589793;
in vec2 in_uv; // 0..1
uniform sampler2D in_texture; // 背景
uniform vec2 in_resolution; // 出力解像度(px)
uniform sampler2D u_watermark; // ウォーターマーク
uniform vec2 u_wmResolution; // ウォーターマーク元解像度(px)
uniform float u_opacity; // 0..1
uniform float u_scale; // watermarkのスケール
uniform float u_angle; // -1..1 (PI倍)
uniform bool u_cover; // cover基準 or fit基準
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_margin; // 余白(比率)
uniform float u_repeatMargin; // 敷き詰め時の余白(比率)
uniform bool u_noBBoxExpansion; // 回転時のBounding Box拡張を抑止
uniform bool u_wmEnabled; // watermark有効
out vec4 out_color;
mat2 rot(float a) {
float c = cos(a), s = sin(a);
return mat2(c, -s, s, c);
}
// cover/fitとscaleから、最終的なサイズ(px)を計算
vec2 computeWmSize(vec2 outSize, vec2 wmSize, bool cover, float scale) {
float wmAspect = wmSize.x / wmSize.y;
float outAspect = outSize.x / outSize.y;
vec2 size;
if (cover) {
if (wmAspect >= outAspect) {
size.y = outSize.y * scale;
size.x = size.y * wmAspect;
} else {
size.x = outSize.x * scale;
size.y = size.x / wmAspect;
}
} else {
if (wmAspect >= outAspect) {
size.x = outSize.x * scale;
size.y = size.x / wmAspect;
} else {
size.y = outSize.y * scale;
size.x = size.y * wmAspect;
}
}
return size;
}
void main() {
vec2 outSize = in_resolution;
vec2 p = in_uv * outSize; // 出力のピクセル座標
vec4 base = texture(in_texture, in_uv);
if (!u_wmEnabled) {
out_color = base;
return;
}
float theta = u_angle * PI; // ラジアン
vec2 wmSize = computeWmSize(outSize, u_wmResolution, u_cover, u_scale);
vec2 margin = u_repeat ? wmSize * u_repeatMargin : outSize * u_margin;
// アライメントに基づく回転中心を計算
float rotateX = 0.0;
float rotateY = 0.0;
if (abs(theta) > 1e-6 && !u_noBBoxExpansion) {
rotateX = abs(abs(wmSize.x * cos(theta)) + abs(wmSize.y * sin(theta)) - wmSize.x) * 0.5;
rotateY = abs(abs(wmSize.x * sin(theta)) + abs(wmSize.y * cos(theta)) - wmSize.y) * 0.5;
}
float x;
if (u_alignX == 1) {
x = (outSize.x - wmSize.x) * 0.5;
} else if (u_alignX == 0) {
x = rotateX + margin.x;
} else {
x = outSize.x - wmSize.x - margin.x - rotateX;
}
float y;
if (u_alignY == 1) {
y = (outSize.y - wmSize.y) * 0.5;
} else if (u_alignY == 0) {
y = rotateY + margin.y;
} else {
y = outSize.y - wmSize.y - margin.y - rotateY;
}
vec2 rectMin = vec2(x, y);
vec2 rectMax = rectMin + wmSize;
vec2 rectCenter = (rectMin + rectMax) * 0.5;
vec4 wmCol = vec4(0.0);
if (u_repeat) {
// アライメントに基づく中心で回転
vec2 q = rectCenter + rot(theta) * (p - rectCenter);
// タイルグリッドの原点をrectMin(アライメント位置)に設定
vec2 gridOrigin = rectMin - margin;
vec2 qFromOrigin = q - gridOrigin;
// タイルサイズ(ウォーターマーク + マージン)で正規化
vec2 tile = wmSize + margin * 2.0;
vec2 tileUv = qFromOrigin / tile;
// タイル内のローカル座標(0..1)を取得
vec2 localUv = fract(tileUv);
// ローカル座標をピクセル単位に変換
vec2 localPos = localUv * tile;
// マージン領域内かチェック
bool inMargin = any(lessThan(localPos, margin)) || any(greaterThanEqual(localPos, margin + wmSize));
if (!inMargin) {
// ウォーターマーク領域内: UV座標を計算
vec2 uvWm = (localPos - margin) / wmSize;
wmCol = texture(u_watermark, uvWm);
}
// マージン領域の場合は透明(wmCol = vec4(0.0))のまま
} else {
// アライメントと回転に従い一枚だけ描画
vec2 q = rectCenter + rot(theta) * (p - rectCenter);
bool inside = all(greaterThanEqual(q, rectMin)) && all(lessThan(q, rectMax));
if (inside) {
vec2 uvWm = (q - rectMin) / wmSize;
wmCol = texture(u_watermark, uvWm);
}
}
float a = clamp(wmCol.a * u_opacity, 0.0, 1.0);
out_color = mix(base, vec4(wmCol.rgb, 1.0), a);
}
@@ -4,13 +4,93 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './watermarkPlacement.glsl';
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 sampler2D u_texture_watermark;
uniform vec2 u_resolution_watermark;
uniform float u_scale;
uniform float u_angle;
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;
void main() {
vec4 in_color = texture(in_texture, in_uv);
float in_x_ratio = max(in_resolution.x / in_resolution.y, 1.0);
float in_y_ratio = max(in_resolution.y / in_resolution.x, 1.0);
bool contain = u_fitMode == 0;
float x_ratio = u_resolution_watermark.x / in_resolution.x;
float y_ratio = u_resolution_watermark.y / in_resolution.y;
float aspect_ratio = contain ?
(min(x_ratio, y_ratio) / max(x_ratio, y_ratio)) :
(max(x_ratio, y_ratio) / min(x_ratio, y_ratio));
float x_scale = contain ?
(x_ratio > y_ratio ? 1.0 * u_scale : aspect_ratio * u_scale) :
(x_ratio > y_ratio ? aspect_ratio * u_scale : 1.0 * u_scale);
float y_scale = contain ?
(y_ratio > x_ratio ? 1.0 * u_scale : aspect_ratio * u_scale) :
(y_ratio > x_ratio ? aspect_ratio * u_scale : 1.0 * u_scale);
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);
vec2 centeredUv = (in_uv - center);
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
) + center;
// trim
if (!u_repeat) {
bool isInside = rotatedUV.x > x_offset - (x_scale / 2.0) && rotatedUV.x < x_offset + (x_scale / 2.0) &&
rotatedUV.y > y_offset - (y_scale / 2.0) && rotatedUV.y < y_offset + (y_scale / 2.0);
if (!isInside) {
out_color = in_color;
return;
}
}
vec4 watermark_color = texture(u_texture_watermark, vec2(
(rotatedUV.x - (x_offset - (x_scale / 2.0))) / x_scale,
(rotatedUV.y - (y_offset - (y_scale / 2.0))) / y_scale
));
out_color.r = mix(in_color.r, watermark_color.r, u_opacity * watermark_color.a);
out_color.g = mix(in_color.g, watermark_color.g, u_opacity * watermark_color.a);
out_color.b = mix(in_color.b, watermark_color.b, u_opacity * watermark_color.a);
out_color.a = in_color.a * (1.0 - u_opacity * watermark_color.a) + watermark_color.a * u_opacity;
}
`;
export const FX_watermarkPlacement = defineImageEffectorFx({
id: 'watermarkPlacement',
name: '(internal)',
shader,
uniforms: ['opacity', 'scale', 'angle', 'cover', 'repeat', 'alignX', 'alignY', 'margin', 'repeatMargin', 'noBBoxExpansion', 'wmResolution', 'wmEnabled', 'watermark'] as const,
uniforms: ['texture_watermark', 'resolution_watermark', 'scale', 'angle', 'opacity', 'repeat', 'alignX', 'alignY', 'alignMargin', 'fitMode'] as const,
params: {
cover: {
type: 'boolean',
@@ -45,50 +125,29 @@ export const FX_watermarkPlacement = defineImageEffectorFx({
max: 1.0,
step: 0.01,
},
noBoundingBoxExpansion: {
type: 'boolean',
default: false,
},
watermark: {
type: 'texture',
default: null,
},
},
main: ({ gl, u, params, textures }) => {
// 基本パラメータ
gl.uniform1f(u.opacity, params.opacity ?? 1.0);
gl.uniform1f(u.scale, params.scale ?? 0.3);
gl.uniform1f(u.angle, params.angle ?? 0.0);
gl.uniform1i(u.cover, params.cover ? 1 : 0);
gl.uniform1i(u.repeat, params.repeat ? 1 : 0);
const ax = params.align?.x === 'left' ? 0 : params.align?.x === 'center' ? 1 : 2;
const ay = params.align?.y === 'top' ? 0 : params.align?.y === 'center' ? 1 : 2;
gl.uniform1i(u.alignX, ax);
gl.uniform1i(u.alignY, ay);
gl.uniform1f(u.margin, (params.align?.margin ?? 0));
gl.uniform1f(u.repeatMargin, (params.align?.margin ?? 0));
gl.uniform1i(u.noBBoxExpansion, params.noBoundingBoxExpansion ? 1 : 0);
// ウォーターマークテクスチャ
const wm = textures.watermark;
if (wm) {
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, wm.texture);
// リピートモードに応じてWRAP属性を設定
if (params.repeat) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
} else {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
}
gl.uniform1i(u.watermark, 1);
gl.uniform2f(u.wmResolution, wm.width, wm.height);
gl.uniform1i(u.wmEnabled, 1);
} else {
gl.uniform1i(u.wmEnabled, 0);
if (textures.watermark == null) {
return;
}
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, textures.watermark.texture);
gl.uniform1i(u.texture_watermark, 1);
gl.uniform2fv(u.resolution_watermark, [textures.watermark.width, textures.watermark.height]);
gl.uniform1f(u.scale, params.scale);
gl.uniform1f(u.opacity, params.opacity);
gl.uniform1f(u.angle, params.angle);
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);
},
});
@@ -1,48 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// エイリアスを解決してくれないので、プロジェクトルートからの絶対パスにする必要がある
#include /src/shaders/snoise;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform vec2 u_pos;
uniform float u_frequency;
uniform bool u_thresholdEnabled;
uniform float u_threshold;
uniform float u_maskSize;
uniform bool u_black;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
vec2 centeredUv = (in_uv - vec2(0.5, 0.5));
vec2 uv = centeredUv;
float seed = 1.0;
float time = 0.0;
vec2 noiseUV = (uv - u_pos) / distance((uv - u_pos), vec2(0.0));
float noiseX = (noiseUV.x + seed) * u_frequency;
float noiseY = (noiseUV.y + seed) * u_frequency;
float noise = (1.0 + snoise(vec3(noiseX, noiseY, time))) / 2.0;
float t = noise;
if (u_thresholdEnabled) t = t < u_threshold ? 1.0 : 0.0;
// TODO: マスクの形自体も揺らぎを与える
float d = distance(uv * vec2(2.0, 2.0), u_pos * vec2(2.0, 2.0));
float mask = d < u_maskSize ? 0.0 : ((d - u_maskSize) * (1.0 + (u_maskSize * 2.0)));
out_color = vec4(
mix(in_color.r, u_black ? 0.0 : 1.0, t * mask),
mix(in_color.g, u_black ? 0.0 : 1.0, t * mask),
mix(in_color.b, u_black ? 0.0 : 1.0, t * mask),
in_color.a
);
}
@@ -4,9 +4,53 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './zoomLines.glsl';
import { GLSL_LIB_SNOISE } from '@/utility/webgl.js';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
${GLSL_LIB_SNOISE}
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform vec2 u_pos;
uniform float u_frequency;
uniform bool u_thresholdEnabled;
uniform float u_threshold;
uniform float u_maskSize;
uniform bool u_black;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
vec2 centeredUv = (in_uv - vec2(0.5, 0.5));
vec2 uv = centeredUv;
float seed = 1.0;
float time = 0.0;
vec2 noiseUV = (uv - u_pos) / distance((uv - u_pos), vec2(0.0));
float noiseX = (noiseUV.x + seed) * u_frequency;
float noiseY = (noiseUV.y + seed) * u_frequency;
float noise = (1.0 + snoise(vec3(noiseX, noiseY, time))) / 2.0;
float t = noise;
if (u_thresholdEnabled) t = t < u_threshold ? 1.0 : 0.0;
// TODO: マスクの形自体も揺らぎを与える
float d = distance(uv * vec2(2.0, 2.0), u_pos * vec2(2.0, 2.0));
float mask = d < u_maskSize ? 0.0 : ((d - u_maskSize) * (1.0 + (u_maskSize * 2.0)));
out_color = vec4(
mix(in_color.r, u_black ? 0.0 : 1.0, t * mask),
mix(in_color.g, u_black ? 0.0 : 1.0, t * mask),
mix(in_color.b, u_black ? 0.0 : 1.0, t * mask),
in_color.a
);
}
`;
export const FX_zoomLines = defineImageEffectorFx({
id: 'zoomLines',
name: i18n.ts._imageEffector._fxs.zoomLines,
@@ -1,23 +0,0 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec4 v_color;
in float v_rotation;
uniform sampler2D u_texture;
out vec4 out_color;
void main() {
vec2 rotated = vec2(
cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5,
cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5
);
vec4 snowflake = texture(u_texture, rotated);
out_color = vec4(snowflake.rgb * v_color.xyz, snowflake.a * v_color.a);
}
@@ -3,12 +3,59 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import vertexSource from './snowfall-effect.vertex.glsl';
import fragmentSource from './snowfall-effect.fragment.glsl';
export class SnowfallEffect {
private VERTEX_SOURCE = vertexSource;
private FRAGMENT_SOURCE = fragmentSource;
private VERTEX_SOURCE = `#version 300 es
in vec4 a_position;
in vec4 a_color;
in vec3 a_rotation;
in vec3 a_speed;
in float a_size;
out vec4 v_color;
out float v_rotation;
uniform float u_time;
uniform mat4 u_projection;
uniform vec3 u_worldSize;
uniform float u_gravity;
uniform float u_wind;
uniform float u_spin_factor;
uniform float u_turbulence;
void main() {
v_color = a_color;
v_rotation = a_rotation.x + (u_time * u_spin_factor) * a_rotation.y;
vec3 pos = a_position.xyz;
pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x;
pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y;
pos.x += sin(u_time * a_speed.z * u_turbulence) * a_rotation.z;
pos.z += cos(u_time * a_speed.z * u_turbulence) * a_rotation.z;
gl_Position = u_projection * vec4(pos.xyz, a_position.w);
gl_PointSize = (a_size / gl_Position.w) * 100.0;
}
`;
private FRAGMENT_SOURCE = `#version 300 es
precision mediump float;
in vec4 v_color;
in float v_rotation;
uniform sampler2D u_texture;
out vec4 out_color;
void main() {
vec2 rotated = vec2(
cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5,
cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5
);
vec4 snowflake = texture(u_texture, rotated);
out_color = vec4(snowflake.rgb * v_color.xyz, snowflake.a * v_color.a);
}
`;
private gl: WebGLRenderingContext;
private program: WebGLProgram;
@@ -1,37 +0,0 @@
#version 300 es
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec4 a_position;
in vec4 a_color;
in vec3 a_rotation;
in vec3 a_speed;
in float a_size;
out vec4 v_color;
out float v_rotation;
uniform float u_time;
uniform mat4 u_projection;
uniform vec3 u_worldSize;
uniform float u_gravity;
uniform float u_wind;
uniform float u_spin_factor;
uniform float u_turbulence;
void main() {
v_color = a_color;
v_rotation = a_rotation.x + (u_time * u_spin_factor) * a_rotation.y;
vec3 pos = a_position.xyz;
pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x;
pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y;
pos.x += sin(u_time * a_speed.z * u_turbulence) * a_rotation.z;
pos.z += cos(u_time * a_speed.z * u_turbulence) * a_rotation.z;
gl_Position = u_projection * vec4(pos.xyz, a_position.w);
gl_PointSize = (a_size / gl_Position.w) * 100.0;
}
@@ -27,7 +27,6 @@ export type WatermarkPreset = {
type: 'text';
text: string;
repeat: boolean;
noBoundingBoxExpansion: boolean;
scale: number;
angle: number;
align: Align;
@@ -39,7 +38,6 @@ export type WatermarkPreset = {
imageId: string | null;
cover: boolean;
repeat: boolean;
noBoundingBoxExpansion: boolean;
scale: number;
angle: number;
align: Align;
@@ -108,7 +106,6 @@ export class WatermarkRenderer {
id: layer.id,
params: {
repeat: layer.repeat,
noBoundingBoxExpansion: layer.noBoundingBoxExpansion,
scale: layer.scale,
align: layer.align,
angle: layer.angle,
@@ -126,7 +123,6 @@ export class WatermarkRenderer {
id: layer.id,
params: {
repeat: layer.repeat,
noBoundingBoxExpansion: layer.noBoundingBoxExpansion,
scale: layer.scale,
align: layer.align,
angle: layer.angle,

Some files were not shown because too many files have changed in this diff Show More