diff --git a/ukui-search-qml/src/org.ukui.search/ui/InstantResultCard.qml b/ukui-search-qml/src/org.ukui.search/ui/InstantResultCard.qml index 9b7a2f943c7b85c3588f38f70ffacc3a3fb02db6..8f21bb78c0bd40dfb1c912c9833bcf0cc8305526 100644 --- a/ukui-search-qml/src/org.ukui.search/ui/InstantResultCard.qml +++ b/ukui-search-qml/src/org.ukui.search/ui/InstantResultCard.qml @@ -28,6 +28,7 @@ import org.ukui.quick.platform 1.0 MouseArea { id: cardMouseArea + hoverEnabled: true readonly property Search.InstantResultState instantResult: searchView.instantResultState readonly property bool isCalculation: instantResult.valid @@ -43,8 +44,41 @@ MouseArea { DtThemeBackground { anchors.fill: parent useStyleTransparency: false + alpha: 0.5 radius: GlobalTheme.kRadiusMenu - backgroundColor: GlobalTheme.BaseActive + backgroundColor: GlobalTheme.kContainGeneralNormal + } + + ShaderEffect { + id: animatedBackground + anchors.fill: parent + + property real time: 0 + property vector2d resolution: Qt.vector2d(width, height) + property real cornerRadius: GlobalTheme.kRadiusMenu + + fragmentShader: "qrc:/ukui-search/org.ukui.search/ui/QuickSearchCardBackground.frag" + + // 窗口隐藏后暂停动画,动画更新限制约为30FPS + Timer { + interval: 33 + repeat: true + running: cardMouseArea.visible && searchView.visible + onTriggered: animatedBackground.time = (animatedBackground.time + interval) % 1000000 + } + } + + ShaderEffect { + id: focusBorder + + anchors.fill: parent + visible: cardMouseArea.containsMouse || cardMouseArea.containsPress || cardMouseArea.activeFocus + + property vector2d resolution: Qt.vector2d(width, height) + property real cornerRadius: GlobalTheme.kRadiusMenu + property real borderWidth: 1.0 + + fragmentShader: "qrc:/ukui-search/org.ukui.search/ui/QuickSearchCardBorder.frag" } Loader { diff --git a/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBackground.frag b/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBackground.frag new file mode 100644 index 0000000000000000000000000000000000000000..fe22c4be2bcb9d21df9fa37cb03d503532b7333f --- /dev/null +++ b/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBackground.frag @@ -0,0 +1,163 @@ +uniform lowp float qt_Opacity; +uniform highp float time; +uniform highp vec2 resolution; +uniform highp float cornerRadius; + +varying highp vec2 qt_TexCoord0; + +//设计稿相关CSS代码: +//第一层:渐变遮罩 background: linear-gradient(180deg, #ffffffb3 0%, #ffffff8f 52%, #ffffff00 100%); +//第二层:GLSL动画 透明度为100% +//第三层:遮罩层 box-shadow: inset 0px -8px 32px 0px rgba(255, 255, 255, 0.25) + +// 动画流动速度,数值越大流动越快。 +const float SPEED = 0.2; +// 有机形变强度,控制颜色区域的流体扭曲程度。 +const float DISTORTION = 1.0; +// 旋涡扭曲强度。 +const float SWIRL = 0.5; +// 参与混合的动态颜色数量。 +const int COLOR_COUNT = 3; +// 颜色团聚合度,数值越大边缘越集中。 +const float DIST_POWER = 4.8; +// 颜色边缘的颗粒扰动强度。 +const float GRAIN_MIXER = 0.4; +// 动态颜色内阴影的透明度。 +const float ANIMATION_OPACITY = 1.0; +// 对应 box-shadow 的 rgba(255, 255, 255, 0.25)。 +const float EDGE_HIGHLIGHT_OPACITY = 0.25; +// 对应 box-shadow 的 32px blur,使用距离场平滑模拟。 +const float EDGE_HIGHLIGHT_BLUR_RADIUS = 32.0; + +// 动态渐变使用的三种颜色,第四个分量为各颜色自身透明度。 +const vec4 COLOR_0 = vec4(0.1412, 0.5843, 1.0, 0.9); +const vec4 COLOR_1 = vec4(0.5560, 0.3805, 1.0, 0.7); +const vec4 COLOR_2 = vec4(0.8319, 0.8991, 1.0, 1.0); + +float hash21(vec2 position) +{ + position = fract(position * vec2(0.3183099, 0.3678794)) + 0.1; + position += dot(position, position + 19.19); + return fract(position.x * position.y); +} + +float valueNoise(vec2 position) +{ + vec2 integerPart = floor(position); + vec2 fractionalPart = fract(position); + float bottomLeft = hash21(integerPart); + float bottomRight = hash21(integerPart + vec2(1.0, 0.0)); + float topLeft = hash21(integerPart + vec2(0.0, 1.0)); + float topRight = hash21(integerPart + vec2(1.0, 1.0)); + vec2 interpolation = fractionalPart * fractionalPart * (3.0 - 2.0 * fractionalPart); + + return mix(mix(bottomLeft, bottomRight, interpolation.x), + mix(topLeft, topRight, interpolation.x), interpolation.y); +} + +vec2 rotate(vec2 position, float angle) +{ + return mat2(cos(angle), sin(angle), -sin(angle), cos(angle)) * position; +} + +vec2 getPosition(int index, float currentTime) +{ + float floatIndex = float(index); + float phase = floatIndex * 0.37; + float horizontalSpeed = 0.6 + fract(floatIndex / 3.0) * 0.9; + float verticalSpeed = 0.8 + fract((floatIndex + 1.0) / 4.0); + + return 0.5 + 0.5 * vec2(sin(currentTime * horizontalSpeed + phase), + cos(currentTime * verticalSpeed + phase * 1.5)); +} + +float roundedRectangleDistance(vec2 uv) +{ + vec2 halfSize = resolution * 0.5; + vec2 position = (uv - 0.5) * resolution; + vec2 cornerDistance = abs(position) - halfSize + vec2(cornerRadius); + + return length(max(cornerDistance, 0.0)) + + min(max(cornerDistance.x, cornerDistance.y), 0.0) + - cornerRadius; +} + +float roundedRectangleMask(vec2 uv) +{ + return 1.0 - smoothstep(-1.0, 1.0, roundedRectangleDistance(uv)); +} + +vec4 compositeOver(vec4 background, vec4 foreground) +{ + return foreground + background * (1.0 - foreground.a); +} + +void main() +{ + vec2 uv = qt_TexCoord0; + float currentTime = (time * 0.001 + 41.5) * SPEED; + vec2 grainUv = uv * 1000.0; + float mixerGrain = 0.4 * GRAIN_MIXER * (valueNoise(grainUv) - 0.5); + + float radius = smoothstep(0.0, 1.0, length(uv - 0.5)); + float center = 1.0 - radius; + + for (float index = 1.0; index <= 2.0; index++) { + uv.x += DISTORTION * center / index + * sin(currentTime + index * 0.4 * smoothstep(0.0, 1.0, uv.y)) + * cos(0.2 * currentTime + index * 2.4 * smoothstep(0.0, 1.0, uv.y)); + uv.y += DISTORTION * center / index + * cos(currentTime + index * 2.0 * smoothstep(0.0, 1.0, uv.x)); + } + + vec2 rotatedUv = rotate(uv - 0.5, -3.0 * SWIRL * radius) + 0.5; + vec3 color = vec3(0.0); + float alpha = 0.0; + float totalWeight = 0.0; + + for (int index = 0; index < COLOR_COUNT; index++) { + vec4 currentColor = COLOR_0; + if (index == 1) + currentColor = COLOR_1; + else if (index == 2) + currentColor = COLOR_2; + + vec2 colorPosition = getPosition(index, currentTime) + mixerGrain; + float distance = pow(length(rotatedUv - colorPosition), DIST_POWER); + float weight = 1.0 / (distance + 1e-3); + + color += currentColor.rgb * currentColor.a * weight; + alpha += currentColor.a * weight; + totalWeight += weight; + } + + color /= max(1e-4, totalWeight); + alpha = clamp(alpha / max(1e-4, totalWeight), 0.0, 1.0); + + // 第三层:基于 inset 0px -8px 32px 0px rgba(255, 255, 255, 0.25) + // 的强度和模糊范围,扩展为四周向内渐隐的白色内阴影。 + // 距离场在圆角矩形内为负值,取反后得到到最近边缘的内部距离。 + float distanceFromEdge = max(0.0, -roundedRectangleDistance(qt_TexCoord0)); + float edgeHighlightMask = 1.0 - smoothstep(0.0, + EDGE_HIGHLIGHT_BLUR_RADIUS, distanceFromEdge); + float edgeHighlightOpacity = EDGE_HIGHLIGHT_OPACITY * edgeHighlightMask; + vec4 result = vec4(vec3(edgeHighlightOpacity), edgeHighlightOpacity); + + // 第二层:与第三层使用相同范围的动态颜色内阴影。 + vec3 dynamicColor = color / max(1e-4, alpha); + float dynamicEdgeOpacity = alpha * edgeHighlightOpacity * ANIMATION_OPACITY; + result = compositeOver(result, vec4(dynamicColor * dynamicEdgeOpacity, dynamicEdgeOpacity)); + + // 第一层:对应 linear-gradient(180deg, #ffffffb3 0%, #ffffff8f 52%, #ffffff00 100%)。 + float gradientOpacity; + if (qt_TexCoord0.y <= 0.52) { + gradientOpacity = mix(0.702, 0.561, + qt_TexCoord0.y / 0.52); + } else { + gradientOpacity = mix(0.561, 0.0, + (qt_TexCoord0.y - 0.52) / 0.48); + } + result = compositeOver(result, vec4(vec3(gradientOpacity), gradientOpacity)); + + gl_FragColor = result * roundedRectangleMask(qt_TexCoord0) * qt_Opacity; +} diff --git a/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBorder.frag b/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBorder.frag new file mode 100644 index 0000000000000000000000000000000000000000..25be326f3ae93dd613be1231ee6aefeb4bf606d1 --- /dev/null +++ b/ukui-search-qml/src/org.ukui.search/ui/QuickSearchCardBorder.frag @@ -0,0 +1,44 @@ +uniform lowp float qt_Opacity; +uniform highp vec2 resolution; +uniform highp float cornerRadius; +uniform highp float borderWidth; + +varying highp vec2 qt_TexCoord0; +// 边框渐变参数:border: 1px solid; +// border-image: linear-gradient(270deg, #0036f5 0%, #4e75ff 34%, #8a41ff 71%, #e888f8 100%) 1; +float roundedRectangleDistance(vec2 uv) +{ + vec2 halfSize = resolution * 0.5; + vec2 position = (uv - 0.5) * resolution; + vec2 cornerDistance = abs(position) - halfSize + vec2(cornerRadius); + + return length(max(cornerDistance, 0.0)) + + min(max(cornerDistance.x, cornerDistance.y), 0.0) + - cornerRadius; +} + +vec3 borderColor(float position) +{ + const vec3 firstColor = vec3(0.0, 0.2118, 0.9608); + const vec3 secondColor = vec3(0.3059, 0.4588, 1.0); + const vec3 thirdColor = vec3(0.5412, 0.2549, 1.0); + const vec3 fourthColor = vec3(0.9098, 0.5333, 0.9725); + + if (position <= 0.34) + return mix(firstColor, secondColor, position / 0.34); + if (position <= 0.71) + return mix(secondColor, thirdColor, (position - 0.34) / 0.37); + return mix(thirdColor, fourthColor, (position - 0.71) / 0.29); +} + +void main() +{ + float distance = roundedRectangleDistance(qt_TexCoord0); + float outerCoverage = 1.0 - smoothstep(-0.5, 0.5, distance); + float innerCoverage = smoothstep(-borderWidth - 0.5, + -borderWidth + 0.5, distance); + float alpha = outerCoverage * innerCoverage; + vec3 color = borderColor(1.0 - qt_TexCoord0.x); + + gl_FragColor = vec4(color * alpha, alpha) * qt_Opacity; +} diff --git a/ukui-search-qml/src/qml.qrc b/ukui-search-qml/src/qml.qrc index 64c17d2133c967ef38c1be3ebae0478398352b6a..b242140579cb09c82db5a0b4038b31e958a72aed 100644 --- a/ukui-search-qml/src/qml.qrc +++ b/ukui-search-qml/src/qml.qrc @@ -17,5 +17,8 @@ org.ukui.search/ui/ResultItem.qml org.ukui.search/ui/ResultSortFilterButton.qml org.ukui.search/ui/InstantResultCard.qml + + org.ukui.search/ui/QuickSearchCardBackground.frag + org.ukui.search/ui/QuickSearchCardBorder.frag