Для тех, кто в танке

Apple презентовали свой новый фирменный стиль. Liquid Glass - это новый материал... Красиво ли это? Спорно, конечно, а спорить я сейчас не хочу.

Всё новое - это забытое старое? Как бы да... но нет. Я прочёл десятки комментариев о том, что подобное уже было в прошивках китайских смартфонов, таких как Сяоми или чё там ещё у Китая. На самом деле то, что показали в бета версии iOS - не только не встречалось в Android нигде ранее, но и не появится в Android ближайшее время.

Если вы приглядитесь внимательно - этот стеклянный материал вообще не так прост, как это может показаться на первый взгляд. Это - не размытие. Мы видим искажения изображения под "стеклом", а на самом "стекле" видим отражения того, что находится рядом. Более того, вокруг "стекла" есть небольшой контур отражение света, который меняется в зависимости от положения устройства.

Ну ладно. Челлендж - повторить что-то подобное в Android. Не берусь говорить, что это получится так же хорошо. Да и что получится вообще хоть что-нибудь...

Анализ

Итак, попробуем проанализировать всё, что мы увидели.

Изображение... искажения... размытие... о чём вы подумали? Первое, что приходит в голову – ✨ ШЕЙДЕРЫ ✨

Окей, шейдеры. Ладно. А к чему их применять? Ну, очевидно же, к тому, что находится на экране? А нет, картинка же не статичная, на экране - не изображение. На экране - куча всего: контент, кнопки, текст, всё это движется и пользователь с этим всем взаимодействует...

А вообще на экране, обычно, находятся вьюхи. Ну и напишем какую-то свою вьюху.

К слову, я ни разу не писал шейдеров, не знаком с тем, как это работает. Так что если вам кажется, что я несу чушь - возможно я несу чушь.

Попробуем

Что она (вьюха) будет делать? Ну пускай она будет захватывать картинку под собой, применять какие-то искажения (то есть, применять шейдеры к изображению).

Так как у меня устройство с Android 15 - можно использовать AGSL шейдеры. Почитали документацию, пойдем дальше.

А как захватить то, что находится под вью? Ну я подумал, и решил - пускай у нашей вьюхи будет targetView - цель, к которой она будет применять эффекты. Так даже лучше - мы сможем не обновлять нашу вью постоянно, а будем использовать onPreDrawListener и обновлять нашу вью тогда, когда цель претерпевает изменения.

Ну давайте напишем сначала какую-то вью, которая будет рендерить targetView в bitmap и показывать его на себе.

Окей, targetView... значит пускай будет так:

fun setTargetView(view: View) {
        targetView?.viewTreeObserver?.removeOnPreDrawListener(targetLayoutListener)
        targetView = view
        view.viewTreeObserver.addOnPreDrawListener(targetLayoutListener)
    }

targetLayoutListener будет заниматься рендерингом всей той жести, которую мы придумаем. Но пока он будет заниматься просто отображением того, что происходит в targetView.

private val targetLayoutListener = ViewTreeObserver.OnPreDrawListener {
        updateBitmap()
        true
    }

  private fun updateBitmap() {
        val view = targetView ?: return
        val bmp = view.drawToBitmap(Bitmap.Config.ARGB_8888)
        targetBitmap = bmp

        val shader = runtimeShader ?: return
        val bitmapShader = BitmapShader(
            bmp,
            Shader.TileMode.CLAMP,
            Shader.TileMode.CLAMP
        )

        val targetPos = IntArray(2)
        val selfPos = IntArray(2)

        view.getLocationOnScreen(targetPos)
        getLocationOnScreen(selfPos)

        shader.setInputShader("iImage1", bitmapShader)
        shader.setFloatUniform("iImageResolution", bmp.width.toFloat(), bmp.height.toFloat())
        shader.setFloatUniform("iTargetViewPos", targetPos[0].toFloat(), targetPos[1].toFloat())
        shader.setFloatUniform("iShaderViewPos", selfPos[0].toFloat(), selfPos[1].toFloat())

        shaderPaint?.shader = shader
        invalidate()
    }

Ну и напишем простой шейдер, который будет отрисовывать то, что мы получили в bitmap:

private val shaderCode = """
        uniform shader iImage1;
uniform float2 iTargetViewPos;      // позиция targetView на экране
uniform float2 iShaderViewPos;      // позиция ShaderView на экране
uniform float2 iImageResolution;    // размер targetBitmap

half4 main(float2 fragCoord) {
    float2 globalCoord = fragCoord + iShaderViewPos - iTargetViewPos;
    float2 uv = globalCoord / iImageResolution;
    return iImage1.eval(uv * iImageResolution); // либо просто iImage1.eval(globalCoord);
}
    """.trimIndent()

Для наглядности расположим на экране ScrollView, а в нём - кучу разноцветных кнопок. Это и будет наш targetView. Нашу View расположим внутри cardview с elevation, чтобы тень отличала её от targetView.

Вот она - наша вьюшка, на нёё указывает красная стрелка
Вот она - наша вьюшка, на нёё указывает красная стрелка

Итак... вроде всё работает. На нашей вьюхе видно то, что находится под ней. Уже неплохо.

Стекло, стекло, стекло

Изображение, которое находится под вьюшкой, должны пройти ряд каких-то операций над ними. В итоге должно быть похоже (хотя бы отдалённо) на то, что бы вы увидели через линзу, смотря на него.

Думая насчёт линзы я пришёл к чему-то такому:

Мне кажется, что это должно быть похоже на то, что показала Apple. Свет проходит через линзу. Чем ближе луч к краю линзы, тем больше будет эффект искажения.

Итак... у нашей искусственной линзы должна быть толщина, а также степень кривизны.

Попробуем модифицировать шейдер... добавим функцию, чтобы применить эффект этой линзы:

float2 applyLensDistortion(float2 fragCoord, float2 center, float2 size, float cornerRadius, float curvature, float thickness) {
    float2 delta = fragCoord - center;
    float2 local = abs(delta) - size + cornerRadius;
    float distToEdge = length(max(local, 0.0));
    float inFactor = smoothstep(cornerRadius, cornerRadius * 0.01, distToEdge);

    float2 normDelta = delta / size;
    float len = length(normDelta);
    float distortion = curvature * (1.0 - len * len*len);

    float2 offset = normalize(delta) * distortion * thickness * (1.0 - inFactor);
    return fragCoord + offset;
}

Я не силён в шейдерах. И вообще это мой первый шейдер. Поэтому - и так сойдёт.

Ну и можно добавить размытие, наверное. Я сделал его очень топорно. Можете посмеяться, я разрешаю:

half4 gaussianBlur(float2 uv, float2 resolution, float radius) {
    if (radius <= 0.0) {
        return iImage1.eval(uv * resolution);
    }
    
    half4 color = half4(0.0);
    float totalWeight = 0.0;
    
    float2 texelSize = radius / resolution;
    
    float2 offset = float2(-2.0, -2.0) * texelSize;
    float weight = exp(-8.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(-1.0, -2.0) * texelSize;
    weight = exp(-5.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(0.0, -2.0) * texelSize;
    weight = exp(-4.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(1.0, -2.0) * texelSize;
    weight = exp(-5.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(2.0, -2.0) * texelSize;
    weight = exp(-8.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(-2.0, -1.0) * texelSize;
    weight = exp(-5.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(-1.0, -1.0) * texelSize;
    weight = exp(-2.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(0.0, -1.0) * texelSize;
    weight = exp(-1.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(1.0, -1.0) * texelSize;
    weight = exp(-2.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(2.0, -1.0) * texelSize;
    weight = exp(-5.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(-2.0, 0.0) * texelSize;
    weight = exp(-4.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(-1.0, 0.0) * texelSize;
    weight = exp(-1.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    weight = 1.0;
    color += iImage1.eval(uv * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(1.0, 0.0) * texelSize;
    weight = exp(-1.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(2.0, 0.0) * texelSize;
    weight = exp(-4.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(-2.0, 1.0) * texelSize;
    weight = exp(-5.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(-1.0, 1.0) * texelSize;
    weight = exp(-2.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(0.0, 1.0) * texelSize;
    weight = exp(-1.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(1.0, 1.0) * texelSize;
    weight = exp(-2.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(2.0, 1.0) * texelSize;
    weight = exp(-5.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(-2.0, 2.0) * texelSize;
    weight = exp(-8.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(-1.0, 2.0) * texelSize;
    weight = exp(-5.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(0.0, 2.0) * texelSize;
    weight = exp(-4.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(1.0, 2.0) * texelSize;
    weight = exp(-5.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    offset = float2(2.0, 2.0) * texelSize;
    weight = exp(-8.0 / (2.0 * radius * radius * 0.25));
    color += iImage1.eval((uv + offset) * resolution) * weight;
    totalWeight += weight;
    
    return color / totalWeight;
}

Добавим юниформы, чтобы контролировать параметры линзы и размытия, а также обновим main функцию чтобы она не обиделась:

uniform shader iImage1;
uniform float2 iImageResolution;
uniform float2 iTargetViewPos;
uniform float2 iShaderViewPos;
uniform float2 iShaderResolution;

uniform float iCurvature;
uniform float iThickness;
uniform float iCornerRadius;
uniform float iBlurRadius;

half4 main(float2 fragCoord) {
    float2 center = iShaderResolution * 0.5;
    float2 lensSize = iShaderResolution * 0.48;
    float2 distortedCoord = applyLensDistortion(
        fragCoord, center, lensSize, iCornerRadius, iCurvature, iThickness
    );
    float2 uv = getUV(distortedCoord);
    return gaussianBlur(uv, iImageResolution, iBlurRadius);
}

Мне лень показывать дальше. Да и нет смысла - там ничего интересного. Я ещё чуть-чуть модифицировал код View. Если коротко - эти параметры теперь можно задать в атрибутах в коде разметки XML.

Магия ✨

Ну и тут ниже покажу примеры того, что получилось в итоге. Играясь с параметрами линзы можно получить разные эффекты.

Стеклокнопка
Стеклокнопка
Стекло можно подкрасить для красоты
Стекло можно подкрасить для красоты

Всё хорошо? Ну... нет

  • Эти шейдеры влияют на производительность. Очень. Очень. Очень. Может быть косяк в моей реализации.

  • Этот материал нельзя применить ко всему подряд. Например, к AppBarLayout - точно нет. Он же LinearLayout. А если он не LinearLayout - прощай liftOnScroll.

  • Чтобы добавить к такому шейдеру поведение, зависящее от положение устройства в пространстве - нужно проделать много работы.

Итог и выводы

Я не знаю, как Эпл это сделали.

После проделанной мной работы я стал уважать Liquid Glass. Я не знаю, как это устроено у них, могу только предположить, что Liquid Glass - тоже шейдеры, только более продуманные. Система, которую Эпл выстраивали так долго, позволяет им это делать.

Android же пока в стороне. Большая надежда на китайцев, может быть у Xiaomi получится сделать что-то подобное, они как раз любят "заимствовать" всякие шутки у Эпл (но это и не плохо). Но в любом случае, пока не появится открытого опен-сурц решения для подобных стеклянных плюшек - ловить в стекломорфизме нечего.

Комментарии (40)


  1. ArkadiyMak
    12.06.2025 20:35

    Я свечку не держал, но думаю, что там всё сделано проще - предварительный гауссблюр скринкопии в мипчейн, а у стекляшек текстура, у которой в двух каналах DuDv и в третьем - степень размытия - мип скринкопии. И каждый канал семплится с разным скейлом оффсета для дисперсии. В GLSL это выглядело бы как-то так:

    vec3 dudvlod = texture( ddltex, uv ).rgb;
    vec3 result;
    result.r = textureLod( screencopy, screen_uv + dudvlod.xy * 5.0, dudvlod.z ).r;
    result.g = textureLod( screencopy, screen_uv + dudvlod.xy * 6.0, dudvlod.z ).g;
    result.b = textureLod( screencopy, screen_uv + dudvlod.xy * 7.0, dudvlod.z ).b;
    


    1. Metotron0
      12.06.2025 20:35

      каждый канал семплится с разным скейлом оффсета

      Почему вы не пишете просто по-английски?


      1. Torvald3d
        12.06.2025 20:35

        Потому что это общепринятый сленг в комп графике


        1. Metotron0
          12.06.2025 20:35

          Не рационально ли будет этому набору действий дать своё название? Всё равно это понятно только своим.


          1. Kenya-West
            12.06.2025 20:35

            Поэтому и не дают названия - оно уже есть. Свои-то как раз поймут.

            В конце концов, пользуемся бритвой Оккама.


        1. space2pacman
          12.06.2025 20:35

          Одно дело "резервное копирование" заменить как "бэкап"

          а другое замена "смещение" как "оффсет"


          1. vcKomm
            12.06.2025 20:35

            Когда переменные называются offset, а не smeshenie, на автомате вспоминается именно оффсет


      1. quazar000
        12.06.2025 20:35

        А почему вы душните?

        Вы действительно ждёте ответ на свой вопрос или так, чисто минусануться решили?


        1. Metotron0
          12.06.2025 20:35

          Я хочу научиться понимать мотивацию людей, чтобы предсказывать их поведение, это у меня что-то типа отголосков Аспергера.

          Написание английских слов транслитом мне не очень понятно. Правда, лет в 15 я тоже предпочитал использовать фразы "юзер не ламер, сам дрова поставит", но потом это стало как-то не актуально и не элитарно.

          А на минусы не обижаюсь, я привык, что люди вокруг часто мыслят иначе, чем я.

          Если кому-то душно от того, что я пытаюсь в них разобраться, то моя ли в этом вина? Я же не должен раади этого отказываться от изучения людей. Или да?


    1. ArkadiyMak
      12.06.2025 20:35

      Мне прилетел минус в карму за "плохое оформление, ошибки". Напишите хоть, что за ошибки.


      1. ImagineTables
        12.06.2025 20:35

        Плюс вам в карму за содержательный комментарий.


      1. Dertefter Автор
        12.06.2025 20:35

        А мне кто-то поставил минус на эту статью с причиной "Личная неприязнь к автору..." ^^
        Забавно, но такого я не ожидал


  1. Metotron0
    12.06.2025 20:35

    Большая надежда на китайцев, может быть у Xiaomi получится сделать что-то подобное

    А какова цель? Предположим, что Apple это сделали, чтобы отличаться, а зачем за ними повторять, если отличаться уже не получится? Это как каверы играть.


    1. Dertefter Автор
      12.06.2025 20:35

      Я так понимаю, это риторический вопрос?)
      За Apple так или иначе все повторяют. Фишки из iOS кочуют в MIUI. (Хотя иногда они кочуют и в обратном направлении)


      1. goremukin
        12.06.2025 20:35

        Вот прям все? А эпл конечно ни за кем?


        1. Dertefter Автор
          12.06.2025 20:35

          • Ответ на первый вопрос: Нет, не все. Я написал "все" образно, и образнасть эта очевидна.

          • Ответ на второй вопрос можно найти в комментарии, на которвый вы ответили.


      1. Metotron0
        12.06.2025 20:35

        Но зачем повторяют? Вау-эффект, который "я тоже себе хочу"?


    1. Jijiki
      12.06.2025 20:35

      нет не совсем так, это не как кавер, а это как соляк, тоесть это практики

      вот щас играем арпеджио, потом уходим в пассаж, потом бендами дотягиваем и немного флажолетов, по гамме пройтись сверху вниз пройтись по обращенным гаммам тоже наверно интересно

      а как тогда писать музыку в фрути лупс - это удивительно, но там похожая ситуация, берем семплы, и творим, получается практиками тоже

      в IT это всё таки практики, иначе не понятно даже как кидать тени и можно долго доходить до некоторых практик

      например как еффективно выделять память, когда делать симд, а когда нет, а может просто написать и посмотреть как оно вообще типо того


      1. Metotron0
        12.06.2025 20:35

        Я не против практики, но тут получается, что практиковаться начинают, когда кто-то сделает, и очень срочно практикуются, да ещё и у себя добавляют.


        1. Jijiki
          12.06.2025 20:35

          ну понимаете еффект размытия и плюшки стали интересны потомучто чипы стали помощнее, стали наверно изучать математику потомучто мощность чипа позволит сделать какую-то прикольную штуку, ну так а это давно было КДЕ Плазма там есть настройка Garuda_Linux, которая сделает ультра стеклянный интерфейс, что-то там с виджетами помните? LXQt помните?, не могу не отметить Gnome наверняка тут тоже что-то есть, потом винда ушла сначала в определенный стиль плашки - по-сути квадратики и метки для кнопок при наведении, у винды есть или был стиль Аква или Аеро не помню, где каверы это просто дизайн + интеграция математики наверно, так тут характер будет только визуально напоминать эталон остаётся у IOS же, да и вопрос этот в плоскости похожести как кидать тени, чур каждый кидает по разному чтоли?)


          1. Metotron0
            12.06.2025 20:35

            что-то там с виджетами помните? LXQt помните?

            Я — нет, я с gnome2 перешёл на xfce и там и остаюсь.

            Раз чипы толтко что стали способны это делать, значит это что-то тяжёлое, значит, оно расходует батарею просто так. Мы называли это свистелками. Кому-то нравится, конечно же, но у меня вызывало бы тревожность.


    1. quazar000
      12.06.2025 20:35

      А что собственно плохого в каверах? Разве каверы делаются с той логикой, которую вы описали?


      1. Metotron0
        12.06.2025 20:35

        Если человек чувствует в себе желание творить, он будет творить, а если не чувствует, а лишь пытается повторить чужой успех на чужих песнях — это это голая коммерция. У поп-групп коммерция хотя бы перемешана с творчеством.

        Я не против того, чтобы учиться у фирмовых групп, но я не понимаю, зачем строить свой репертуар только из чужих песен. Есть даже группы, которые 40 лет на сцене, а ничего своего не играют. Неужели у них нет ни одного способного написать песню, у кого есть, что сказать людям, выразить себя?


  1. Torvald3d
    12.06.2025 20:35

    По сути всё делается проще:

    1. блур уменьшенной текстурки фона ui элемента

    2. нормал мапа бокс элемента - единственная, заранее заготовленная маленькая текстура, которая применяется ко всем ui элементам

      типа такой
    3. итоговый пиксель формируется смещением блура по нормал мапе

    Такое должно работать не сильно медленнее старого размытия элементов интерфейса


    1. Dertefter Автор
      12.06.2025 20:35

      Очень интересно. Надо попробовать! Спасибо


      1. ermouth
        12.06.2025 20:35

        Тут уже CSS-ом и SVG-шным displacement map-ом этот эффект отсимулировали. Довольно грубо из-за зума и особенностей дисплейсмента (смещает только на целое значение), но вполне рабочий вариант. Просто копипаст html кода из гиста перед тегом </body> на любую страницу – и вуаля.

        https://gist.github.com/rebane2001/8ba35ad6e1b17c4cb5b2b2431d9e992c

        Может, чем-то вам пригодится.


        1. ImagineTables
          12.06.2025 20:35

          Ссылка оттуда на кодепен, чтобы проще было посмотреть:

          https://codepen.io/rebane2001/details/OPVQXMv


    1. SadOcean
      12.06.2025 20:35

      Технически это не normal map, это displacement map. Используются только 2 канала и в целом может отличаться, просто для вот таких форм похожа


      1. Or_Ganica
        12.06.2025 20:35

        Зелёный, синий, красный. Почему два? И причем тут дисплейсмент, если он для прямого изменения геометрии нужен?


  1. SadOcean
    12.06.2025 20:35

    Моделировать честное поведение стекла в реальном мире нет нужды, для таких вещей используется displacement map текстура, с предрасчитанными искажениями. Она же неплохо тянется и масштабируется, что даёт возможность использовать уменьшенные текстуры.

    Таким образом основной эффект - комбинация размытия для задника и сдвигов пикселей для элементов.

    Блики могут быть сделаны немного по разному, надо смотреть на разные примеры, но возможно они сделаны через тот же дисплей момент, для вашего примера это вероятно.

    Аберрации могут немного усложнить этот эффект.

    Самое тяжёлое тут на самом деле блюр, потому что дисплейсмент это одна выборка, а размытие - чем больше выборок, тем качественнее, может быть и 3*3 и 15*15

    Самая большая проблема тут скорее организация - нет проблемы сделать такой эффект для 2х слоев, задник и элементы, но может быть проблемы с множественными перекрытиями. Впрочем современное железо кушает и не такое.

    По сути ничего технически сложного тут нет со времён win vista, когда MS поддержали лёгкий бобр для всех поверхностей и полупрозрачные окна (не факт что они были первые, но тут это было массово)

    Что действительно хорошо у Эппл, так это холистический подход - они обновили весь дизайн и для системы, и для приложений, создаёт ощущение целостности. И у них есть много возможностей навязать целостный дизайн сторонним разработчикам. У большинства вендоров android с этим проблемы, да и зоопарк больше.

    Поэтому мы конечно скоро увидим такие оболочки на android, но эффект будет не тот, конечно


    1. Jijiki
      12.06.2025 20:35

      блик это спекуляр мап - текстура (и центр вьюхи тоесть центра екрана смотрит внутрь тогда будут блики)) на скоко помню , я думаю в телефоне на рабочем столе нету смысла рендерить PBR), блюр в картинке не так сложен кстати как может казаться если в ios крутой чип то они могли уложиться векторкой и если отходить от случая счета векторкой на чипе в случае если он мощный, там могли быть какието статические переходные посчитанные данные, но именно посчитать блюр не накладно вроде, а ну вот спекуляр мап да это статик числа наверно

      итого имеем 4 точки надо найти текущий центр(а не тот который уехал вниз/вверх) и ситуация что то что в вьюхе плывёт вниз вверх а центр остаётся, операция не нагрузная вроде, потомучто размытие будет по замапленым сдвигам чтоли


      1. SadOcean
        12.06.2025 20:35

        Спекулар мап описывает интенсивность и цвет блика на разных частях материала, когда это уместно, например, когда текстура содержит много материалов - одежда не блестит, пуговицы блестят золотым, а металл ремней - синеватым. Или например ржавое железо на технике - некоторые части бликуют, а некоторые - нет.

        Если материал у вас одинаковый (в данном случае - гладкое стекло), то смысла в экстра текстуре нет, мощность блика описывается целиком для всей поверхности.

        При этом сами блики такого рода должны быть ориентированы от какого то источника, который обычно расположен со стороны наблюдателя.

        Здесь же больше похоже, как будто это вторичный цвет от иконок под ними, отраженный краем.
        Для такого технологии классических бликов не подойдут, нужны множественные источники света, нужно что-то типа radial cascades.

        Но так как эффект простой, с большой вероятностью он сделан на тех же displacement map - с ними можно сделать эффект, когда искажение захватывает объекты за пределами иконки, это не проблема.

        Конечно возможно там более сложный эффект, но на примерах этого не видно.
        Я бы сказал, что всего что видно можно достичь только комбинацией этих двух эффектов - смещение и размытие.
        Остальное - лишь аккуратный и правильный дизайн.

        Еще крутая фишка там - плавное перетекание из одной формы в другую.
        Такое можно сделать через distance field или маски, и эти эффекты довольно сильно замороченные.
        Если Apple сделала их действительно легкими для любых элементов, то это действительно интересно.
        Но возможно они просто заморочились в нескольких местах стандартными методами и чуда там нет - ваши элементы сделать с такой анимацией легко не получится.


        1. Jijiki
          12.06.2025 20:35

          в целом согласен, со светом много всякого, да и подмешивание цветов нюанс, там видимо действительно по разному можно сделать


  1. VADemon
    12.06.2025 20:35

    На Хабре давно была статья про быстрое размытие на Android. Быстрее всего было сначала ужать картинку в 0.5x разрешения и ниже, а потом проходиться алгоритмом.

    Насчет самого эффекта: плохое UX, поскольку цвета проявляются в ином от объекта месте или, как у вас на предпоследней картинке, площадь цвета преумножается. Если проще: больше цветов меняется при прокрутке, отвлекает. Это примерно как солнечный зайчик у вас в помещении по стене пролетит, потому что кто-то на улице машину открыл. У Apple этот эффект меньше проявляется, чем на простом шейдере, но он есть.


  1. Egres
    12.06.2025 20:35

    Я прочёл десятки комментариев о том, что подобное уже было в прошивках китайских смартфонов, таких как Сяоми или чё там ещё у Китая.

    Всю эту ерунду пишут люди не заставшие (или позабывшие) Apple Aqua, который и принёс в мир UI всю эту "стеклянную" моду. А было это 25 лет назад.


  1. fxux
    12.06.2025 20:35

    К подобным "красивостям" привыкаешь за пару дней, зато сливается так, что усложняет чтение текста, а как выглядит панель quick settings поверх рабочего стола с иконками это вообще шик, непонятно где иконка приложения, а где кнопка wi-fi. Да, видно, но задача дизайна быть как можно более читаемым и понятным, с этим он справляется плохо. Плюс размытие неплохо отнимает процессорное время. Я понимаю, что его некуда девать в айфоне, но всё же, батареи тоже не бесконечные, по моему это растрата зазря.


  1. Psychosynthesis
    12.06.2025 20:35

    Отличный пример убогого блевотного дизайна, который вместо читаемости и удобства даёт юзеру прозрачность и какой-то всратый блюр.

    Какие причины пытаться повторить это убожество, если ты не сумасшедший?


    1. Dertefter Автор
      12.06.2025 20:35

      Всё это вкусновщина и субъективщина. Мне, например, понравился такой эффект, поэтому я решил попытаться его воссоздать.
      Проблема с читабельностью на текущем этапе моей реализации очевидна, но у меня уже есть идеи что с этим можно сделать. Кроме того, у Apple с этим всё не так плохо. Стоит признать, что их "цифровое стекло" более продуманное, нежели простенький AGSL шейжер, написанный "на коленке".
      Ну и переходя ко второй части вопроса, если у меня есть причины, значит я - сумашедший и мой ответ вас не устроит)


    1. Metotron0
      12.06.2025 20:35

      Прикол. Я задал примерно этот же вопрос, но без экспрессии, люди оценили минусами, а тут — плюсами.

      Зафиксировал.


  1. castus
    12.06.2025 20:35

    А может, для начала, стоит сделать так чтобы система то тут, то там не лагала и не глючила, а уже потом вся эта мишура? Вычислительные ресурсы тратятся на рюшечки...