diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index f8dd24aea..ef4fc3d08 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Scheibe", "fade": "Überblenden", + "honeycomb": "Honigwabe", + "pixelate": "Verpixeln", "stripes": "Streifen", "wipe": "Wischen" }, diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 5a1379d8b..a545a57f5 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Disc", "fade": "Fade", + "honeycomb": "Honeycomb", + "pixelate": "Pixelate", "stripes": "Stripes", "wipe": "Wipe" }, diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 02b1b740d..8e576afe7 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Disco", "fade": "Desvanecer", + "honeycomb": "Panal", + "pixelate": "Pixelar", "stripes": "Rayas", "wipe": "Barrido" }, diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 28f9fff45..36a07cb22 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Disque", "fade": "Fondu", + "honeycomb": "Nid d'abeille", + "pixelate": "Pixéliser", "stripes": "Rayures", "wipe": "Balayage" }, diff --git a/Assets/Translations/hn.json b/Assets/Translations/hn.json index 836f69e86..05c33a206 100644 --- a/Assets/Translations/hn.json +++ b/Assets/Translations/hn.json @@ -40,8 +40,8 @@ "audio-visualizer": { "color-name-description": "Kjos lìten på framsyningi.", "color-name-label": "Fyllelìt", - "hide-when-idle-description": "Framsyningi dyl seg når dette er på, minder nokot spelar av.", - "hide-when-idle-label": "Dyl når ingen låt spelar av", + "hide-when-idle-description": "Framsyningi løynest når dette er på, minder nokot spelar av.", + "hide-when-idle-label": "Løyn når ingen låt spelar av", "width-description": "Sjølvvald lùtbreidd." }, "battery": { @@ -53,10 +53,10 @@ "display-mode-icon-always": "Teikn - syn % stødt", "display-mode-icon-hover": "Teikn - syn ved sviv", "display-mode-icon-only": "Einast teikn", - "hide-if-idle-description": "Dyl nøytlen når batteriet ikkje ladar og ikkje gjev straum.", - "hide-if-idle-label": "Dyl når uverksam", - "hide-if-not-detected-description": "Dyl nøytlen når kjernen ikkje finn eit batteri.", - "hide-if-not-detected-label": "Dyl når ikkje funne", + "hide-if-idle-description": "Løyn nøytlen når batteriet ikkje ladar og ikkje gjev straum.", + "hide-if-idle-label": "Løyn når uverksam", + "hide-if-not-detected-description": "Løyn nøytlen når kjernen ikkje finn eit batteri.", + "hide-if-not-detected-label": "Løyn når ikkje funne", "low-battery-threshold-description": "Gjev åtvaring når batteristiget gjeng under denne hundradsluten.", "low-battery-threshold-label": "Åtvaringstreskald for lågt batteri", "show-noctalia-performance-description": "Syn brjotaren for Ytestoda frå Noctalia i batterifjøli.
Slær av skuggar og bilætrørslor i Noctalia for å minka emnebruk.", @@ -89,7 +89,31 @@ "color-selection-description": "Lìt nøytelteikn med hamslìtene.", "enable-colorization-description": "Lìt teiknet til styreromet med hamslìtene.", "icon-description": "Kjos teikn frå samningi eller ei onnor fil.", - "select-custom-icon": "Kjos teikn" + "select-custom-icon": "Kjos teikn", + "use-distro-logo-description": "Nytta teiknet til Linuxutbytet ditt helder en eit anna teikn.", + "use-distro-logo-label": "Nytta utbyteteikn" + }, + "custom-button": { + "collapse-condition-description": "Knappen løynest um utskrifti er lik dette.", + "collapse-condition-label": "Løyningsvilkor", + "color-selection-description": "Lìt teiknet og teksti med hamslìtene.", + "display-command-output-description": "Skriv inn eit styrebod som skal køyra jamlege. Syner fyrste utskriftsradi som tekst.", + "display-command-output-label": "Syn utskrift frå styrebod", + "display-command-output-stream-description": "Skriv inn styrebod som skal køyra heile tidi.", + "dynamic-text": "Rørug skrift", + "enable-colorization-description": "Lìt teiknet og teksti på den handgjorde knappen med hamslìtene.", + "enable-colorization-label": "Lìta", + "hide-mode-always-expanded": "Stødt vidka", + "hide-mode-description": "Stjornar um nøytlen er til å sjå når styrebodet ikkje kjem med utskrift.", + "hide-mode-expand-with-output": "Vidka ved utskrift", + "hide-mode-label": "Løyndarluna", + "hide-mode-max-transparent": "Vidka heilt men klår", + "icon-description": "Kjos teikn frå samningi.", + "ipc-identifier-description": "Sermerke for IPC-styrebod. Nytta dette merket med 'qs -c noctalia-shell ipc call cb [gjerning] [merke]' for å styra knappen med IPC.", + "ipc-identifier-label": "IPC-sermerke", + "left-click-description": "Styrebod som køyrer ved vinstretrykk.", + "left-click-label": "Vinstretrykk", + "left-click-update-text": "Før upp ny tekst ved vinstretrykk" } }, "common": { diff --git a/Assets/Translations/hu.json b/Assets/Translations/hu.json index aabf6110e..6e58fa501 100644 --- a/Assets/Translations/hu.json +++ b/Assets/Translations/hu.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Korong", "fade": "Halványítás", + "honeycomb": "Méhsejt", + "pixelate": "Pixelezés", "stripes": "Csíkok", "wipe": "Törlés" }, diff --git a/Assets/Translations/ja.json b/Assets/Translations/ja.json index dfdc5d33d..4f081410e 100644 --- a/Assets/Translations/ja.json +++ b/Assets/Translations/ja.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "ディスク", "fade": "フェード", + "honeycomb": "ハニカム", + "pixelate": "ピクセル化", "stripes": "ストライプ", "wipe": "ワイプ" }, diff --git a/Assets/Translations/ko-KR.json b/Assets/Translations/ko-KR.json index 225b38bdf..2b2aa241b 100644 --- a/Assets/Translations/ko-KR.json +++ b/Assets/Translations/ko-KR.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "디스크", "fade": "페이드", + "honeycomb": "벌집", + "pixelate": "픽셀화", "stripes": "스트라이프", "wipe": "와이프" }, diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index c8dcf88e1..893d4650a 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Schijf", "fade": "Vervagen", + "honeycomb": "Honingraat", + "pixelate": "Pixeliseren", "stripes": "Strepen", "wipe": "Vegen" }, diff --git a/Assets/Translations/pl.json b/Assets/Translations/pl.json index 974527dc1..4271065b8 100644 --- a/Assets/Translations/pl.json +++ b/Assets/Translations/pl.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Dysk", "fade": "Zanikanie", + "honeycomb": "Plaster miodu", + "pixelate": "Pikselizuj", "stripes": "Paski", "wipe": "Wycieranie" }, diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index a1bb9a0b0..2c385ad4d 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Disco", "fade": "Esmaecer", + "honeycomb": "Favo de mel", + "pixelate": "Pixelizar", "stripes": "Listras", "wipe": "Limpar" }, diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 7e05f463a..483631b0e 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Диск", "fade": "Затухание", + "honeycomb": "Соты", + "pixelate": "Пикселизировать", "stripes": "Полосы", "wipe": "Стирание" }, diff --git a/Assets/Translations/sv.json b/Assets/Translations/sv.json index 45ca0c926..56bb579c9 100644 --- a/Assets/Translations/sv.json +++ b/Assets/Translations/sv.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Skiva", "fade": "Tonad", + "honeycomb": "Bikaka", + "pixelate": "Pixla", "stripes": "Ränder", "wipe": "Torkning" }, diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index d1dbe4623..d0be998a1 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Disk", "fade": "Belirsizleşme", + "honeycomb": "Petek", + "pixelate": "Pikselleştir", "stripes": "Çizgiler", "wipe": "Silme" }, diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 0b7431632..513c0adaf 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "Диск", "fade": "Затухання", + "honeycomb": "Стільники", + "pixelate": "Пікселізувати", "stripes": "Смуги", "wipe": "Змітання" }, diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index ee01494c1..5d1238485 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "圆盘", "fade": "淡入淡出", + "honeycomb": "蜂巢", + "pixelate": "像素化", "stripes": "条纹", "wipe": "擦除" }, diff --git a/Assets/Translations/zh-TW.json b/Assets/Translations/zh-TW.json index 6c66bdf47..ce8fed585 100644 --- a/Assets/Translations/zh-TW.json +++ b/Assets/Translations/zh-TW.json @@ -1776,6 +1776,8 @@ "transitions": { "disc": "圓盤", "fade": "淡入", + "honeycomb": "蜂巢", + "pixelate": "像素化", "stripes": "條紋", "wipe": "擦除" }, diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index b9388ebb6..8666cf1e8 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -38,6 +38,14 @@ Variants { property real stripesCount: 16 property real stripesAngle: 0 + // Pixelate + property real pixelateMaxBlockSize: 64.0 + + // Honeycomb + property real honeycombCellSize: 0.04 + property real honeycombCenterX: 0.5 + property real honeycombCenterY: 0.5 + // Used to debounce wallpaper changes property string futureWallpaper: "" // Track the original wallpaper path being transitioned to (before caching) @@ -181,6 +189,10 @@ Variants { return discShaderComponent; case "stripes": return stripesShaderComponent; + case "pixelate": + return pixelateShaderComponent; + case "honeycomb": + return honeycombShaderComponent; case "fade": case "none": default: @@ -319,6 +331,71 @@ Variants { } } + // Pixelate transition shader component + Component { + id: pixelateShaderComponent + ShaderEffect { + anchors.fill: parent + + property variant source1: currentWallpaper + property variant source2: nextWallpaper + property real progress: root.transitionProgress + property real maxBlockSize: root.pixelateMaxBlockSize + + // Fill mode properties + property real fillMode: root.fillMode + property vector4d fillColor: root.fillColor + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + + // Solid color mode + property real isSolid1: root.isSolid1 ? 1.0 : 0.0 + property real isSolid2: root.isSolid2 ? 1.0 : 0.0 + property vector4d solidColor1: root.solidColor1 + property vector4d solidColor2: root.solidColor2 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_pixelate.frag.qsb") + } + } + + // Honeycomb transition shader component + Component { + id: honeycombShaderComponent + ShaderEffect { + anchors.fill: parent + + property variant source1: currentWallpaper + property variant source2: nextWallpaper + property real progress: root.transitionProgress + property real cellSize: root.honeycombCellSize + property real centerX: root.honeycombCenterX + property real centerY: root.honeycombCenterY + property real aspectRatio: root.width / root.height + + // Fill mode properties + property real fillMode: root.fillMode + property vector4d fillColor: root.fillColor + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + + // Solid color mode + property real isSolid1: root.isSolid1 ? 1.0 : 0.0 + property real isSolid2: root.isSolid2 ? 1.0 : 0.0 + property vector4d solidColor1: root.solidColor1 + property vector4d solidColor2: root.solidColor2 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_honeycomb.frag.qsb") + } + } + // Animation for the transition progress NumberAnimation { id: transitionAnimation @@ -576,6 +653,16 @@ Variants { stripesAngle = Math.random() * 360; setWallpaperWithTransition(futureWallpaper); break; + case "pixelate": + pixelateMaxBlockSize = Math.round(Math.random() * 80 + 32); + setWallpaperWithTransition(futureWallpaper); + break; + case "honeycomb": + honeycombCellSize = Math.random() * 0.04 + 0.02; + honeycombCenterX = Math.random(); + honeycombCenterY = Math.random(); + setWallpaperWithTransition(futureWallpaper); + break; default: setWallpaperWithTransition(futureWallpaper); break; @@ -614,6 +701,14 @@ Variants { stripesCount = Math.round(Math.random() * 20 + 4); stripesAngle = Math.random() * 360; break; + case "pixelate": + pixelateMaxBlockSize = 64.0; + break; + case "honeycomb": + honeycombCellSize = 0.04; + honeycombCenterX = 0.5; + honeycombCenterY = 0.5; + break; } // Defer the actual transition start so the compositor can map the window diff --git a/Services/UI/WallpaperService.qml b/Services/UI/WallpaperService.qml index ac6dec433..eb7da706d 100644 --- a/Services/UI/WallpaperService.qml +++ b/Services/UI/WallpaperService.qml @@ -210,6 +210,14 @@ Singleton { "key": "wipe", "name": I18n.tr("wallpaper.transitions.wipe") }); + transitionsModel.append({ + "key": "pixelate", + "name": I18n.tr("wallpaper.transitions.pixelate") + }); + transitionsModel.append({ + "key": "honeycomb", + "name": I18n.tr("wallpaper.transitions.honeycomb") + }); } // ------------------------------------------------------------------- diff --git a/Shaders/frag/wp_honeycomb.frag b/Shaders/frag/wp_honeycomb.frag new file mode 100644 index 000000000..ef7d2ac6b --- /dev/null +++ b/Shaders/frag/wp_honeycomb.frag @@ -0,0 +1,170 @@ +// ===== wp_honeycomb.frag ===== +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source1; +layout(binding = 2) uniform sampler2D source2; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; + float cellSize; // Size of hexagonal cells in UV space (default 0.04) + float centerX; // X coordinate of wave origin (0.0 to 1.0) + float centerY; // Y coordinate of wave origin (0.0 to 1.0) + float aspectRatio; // Width / Height of the screen + + // Fill mode parameters + float fillMode; // 0=center, 1=crop, 2=fit, 3=stretch, 4=repeat + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) + + // Solid color mode + float isSolid1; // 1.0 if source1 is solid color, 0.0 otherwise + float isSolid2; // 1.0 if source2 is solid color, 0.0 otherwise + vec4 solidColor1; // Solid color for source1 + vec4 solidColor2; // Solid color for source2 +} ubuf; + +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imagePixel = (screenPixel - offset) / scale; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 3.5) { + // Mode 3: stretch - Use original UV (stretches to fit) + } + else { + // Mode 4: repeat (tile) - Tile image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + transformedUV = screenPixel / vec2(imgWidth, imgHeight); + } + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight, float isSolid, vec4 solidColor) { + if (isSolid > 0.5) { + return solidColor; + } + + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + if (ubuf.fillMode > 3.5) { + return texture(tex, fract(transformedUV)); + } + + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} + +// Convert cartesian to axial hex coordinates and round to nearest hex center +vec2 hexRound(vec2 axial) { + // Convert axial (q,r) to cube (x,y,z) where x+y+z=0 + float x = axial.x; + float z = axial.y; + float y = -x - z; + + // Round each + float rx = round(x); + float ry = round(y); + float rz = round(z); + + // Fix rounding errors by resetting the component with largest diff + float dx = abs(rx - x); + float dy = abs(ry - y); + float dz = abs(rz - z); + + if (dx > dy && dx > dz) { + rx = -ry - rz; + } else if (dy > dz) { + ry = -rx - rz; + } else { + rz = -rx - ry; + } + + return vec2(rx, rz); +} + +void main() { + vec2 uv = qt_TexCoord0; + + // Sample both textures at original UV + vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1, ubuf.isSolid1, ubuf.solidColor1); + vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2, ubuf.isSolid2, ubuf.solidColor2); + + // Aspect-correct the UV for hex grid so cells appear as regular hexagons + vec2 aspectUV = vec2(uv.x * ubuf.aspectRatio, uv.y); + + // Convert to axial hex coordinates + // Hex grid: q axis along x, r axis at 60 degrees + float size = max(ubuf.cellSize, 0.01); + float q = (aspectUV.x * (2.0 / 3.0)) / size; + float r = ((-aspectUV.x / 3.0) + (sqrt(3.0) / 3.0) * aspectUV.y) / size; + + // Round to nearest hex center + vec2 hex = hexRound(vec2(q, r)); + + // Convert hex center back to aspect-corrected UV space + vec2 hexCenter; + hexCenter.x = size * (3.0 / 2.0) * hex.x; + hexCenter.y = size * (sqrt(3.0) * (hex.y + 0.5 * hex.x)); + + // Calculate distance from this cell's center to the wave origin (aspect-corrected) + vec2 origin = vec2(ubuf.centerX * ubuf.aspectRatio, ubuf.centerY); + float dist = distance(hexCenter, origin); + + // Maximum distance from origin to any corner (for normalization) + float maxDistX = max(ubuf.centerX * ubuf.aspectRatio, (1.0 - ubuf.centerX) * ubuf.aspectRatio); + float maxDistY = max(ubuf.centerY, 1.0 - ubuf.centerY); + float maxDist = length(vec2(maxDistX, maxDistY)); + + // Wave expansion (same approach as disc shader): + // Start radius behind the origin so the smoothstep zone is fully off-screen at progress=0 + float softEdge = 0.15 * maxDist; + float totalDistance = maxDist + 2.0 * softEdge; + float radius = -softEdge + ubuf.progress * totalDistance; + + // factor = 0 inside the wave (revealed), 1 outside (not yet reached) + float factor = smoothstep(radius - softEdge, radius + softEdge, dist); + float cellProgress = 1.0 - factor; + + fragColor = mix(color1, color2, cellProgress) * ubuf.qt_Opacity; +} diff --git a/Shaders/frag/wp_pixelate.frag b/Shaders/frag/wp_pixelate.frag new file mode 100644 index 000000000..0e6101b23 --- /dev/null +++ b/Shaders/frag/wp_pixelate.frag @@ -0,0 +1,118 @@ +// ===== wp_pixelate.frag ===== +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source1; +layout(binding = 2) uniform sampler2D source2; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; + float maxBlockSize; // Maximum block size in pixels (default 64) + + // Fill mode parameters + float fillMode; // 0=center, 1=crop, 2=fit, 3=stretch, 4=repeat + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) + + // Solid color mode + float isSolid1; // 1.0 if source1 is solid color, 0.0 otherwise + float isSolid2; // 1.0 if source2 is solid color, 0.0 otherwise + vec4 solidColor1; // Solid color for source1 + vec4 solidColor2; // Solid color for source2 +} ubuf; + +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imagePixel = (screenPixel - offset) / scale; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 3.5) { + // Mode 3: stretch - Use original UV (stretches to fit) + } + else { + // Mode 4: repeat (tile) - Tile image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + transformedUV = screenPixel / vec2(imgWidth, imgHeight); + } + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight, float isSolid, vec4 solidColor) { + if (isSolid > 0.5) { + return solidColor; + } + + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + if (ubuf.fillMode > 3.5) { + return texture(tex, fract(transformedUV)); + } + + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} + +void main() { + vec2 uv = qt_TexCoord0; + + // Triangle wave: pixelation peaks at progress=0.5 + float pixelIntensity = 1.0 - abs(2.0 * ubuf.progress - 1.0); + + // Block size ramps from 1 pixel to maxBlockSize at peak, then back down + float blockSize = max(1.0, ubuf.maxBlockSize * pixelIntensity); + + // Snap UV to block grid in pixel space + vec2 screenSize = vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 pixelCoord = uv * screenSize; + vec2 snappedPixel = floor(pixelCoord / blockSize) * blockSize + blockSize * 0.5; + vec2 snappedUV = snappedPixel / screenSize; + + // Sample both textures at the snapped (pixelated) UV + vec4 color1 = sampleWithFillMode(source1, snappedUV, ubuf.imageWidth1, ubuf.imageHeight1, ubuf.isSolid1, ubuf.solidColor1); + vec4 color2 = sampleWithFillMode(source2, snappedUV, ubuf.imageWidth2, ubuf.imageHeight2, ubuf.isSolid2, ubuf.solidColor2); + + // Crossfade concentrated around the peak pixelation (progress ~0.5) + float blend = smoothstep(0.4, 0.6, ubuf.progress); + + fragColor = mix(color1, color2, blend) * ubuf.qt_Opacity; +} diff --git a/Shaders/qsb/wp_honeycomb.frag.qsb b/Shaders/qsb/wp_honeycomb.frag.qsb new file mode 100644 index 000000000..4e3542c7f Binary files /dev/null and b/Shaders/qsb/wp_honeycomb.frag.qsb differ diff --git a/Shaders/qsb/wp_pixelate.frag.qsb b/Shaders/qsb/wp_pixelate.frag.qsb new file mode 100644 index 000000000..bde6917d7 Binary files /dev/null and b/Shaders/qsb/wp_pixelate.frag.qsb differ