📱 Подписаться
IT и цифровая трансформация

Оживляем UI на мобилках с Sensor API

📰 Habr 👁️ 5 просмотров

kellas8 часов назад

Оживляем UI на мобилках с Sensor API

Уровень сложностиСложныйВремя на прочтение12 минОхват и читатели5.4KCSS*JavaScript*ТуториалРечь пойдёт о реализации реакции веб-интерфейса на наклон устройства, смещение бликов, теней, для придания ему таким образом интерактивности и объёма.

Device Orientation API существует уже давно, мобильные устройства с гироскопом стали основным окном для приложений и сайтов, в тренде эмоциональный дизайн, всевозможные эффекты "блеска" / градиентов встречаются повсеместно, и кажется пора это всё объединить! И ведь Apple выкатили эту фишку в liquid glass! Но... лично по моему мнению, как-то не "дожали" или она померкла на фоне других нововведений... а жаль, я считаю реакцию ui на положение устройства гораздо более перспективной темой чем новая прозрачность с крутой физикой преломлений которую тут же все побежали повторять. В отличие от преломления фона, адекватная реакция на наклон устройства это не графон ради графона, а микро‑взаимодействие дающее ощущение контроля, отзывчивости, даже "живости" интерфейса. Ведь даже если пользователь не тапает по экрану - он очень даже взаимодействует с интерфейсом(смотрит/читает) и слегка "покачивает" телефон в руке, и UI на эти микродвижения уже чуть-чуть отвечает, маленькая физика (свет/тень/глубина), как будто элементы не нарисованы, а существуют как объекты... Ну это моё субъективное восприятие... тут есть похожие мысли про роль микровзаимодействий и баланс эмоций. Знаю что некоторых людей "лишние" анимации наоборот нервируют, или даже "укачивают", чтож... прекрасно что для них есть опция reduce-motion, для меня такой замечательной обратной опции "сделать красиво" нет )))Если всё ещё не понятно о чём я, можете глянуть это видео(на youtube):

Видео демонстрацияОбратите внимания на блики на ребрах карточки, легкий движущийся градиент на фоне чекбокса и блики на иконках конечно.На своём мобильном устройстве, эффект можно посмотреть в миниаппе телеграм (лучше в тёмной теме, там заметнее). Есть ещё вот такое демо и оно же внутри телеги.

Казалось бы, на этом можно и закончить, дать ссылочки на доки swift, android, MDN - всё, api у вас есть - берите инфу с датчиков - делайте красотульку! Но дьявол кроется в деталях, в которых и предлагаю сейчас шаг за шагом разобраться.

"Торопыги" могут сразу посмотреть полный код реализации на js Вайбкодеры - просто вставьте этот промпт:

Добавь эффекты блеска UI, реагирующие на наклон устройства.
Инструкция: https://raw.githubusercontent.com/alexstep/sensor/main/AI.mdНу а всех кому интересны подробности приглашаю под кат и в комментарии.

Датчики

В современных устройствах установлено куча всевозможных МЭМС (кстати микроэлектромеханические системы прям отдельная интересная тема, посмотрите например как именно устроен современный акселерометр). В спецификации свежего iphone помимо Face ID и LiDAR можно увидеть:

• Barometer (Барометр, измеряет атмосферное давление для определения высоты)
• High dynamic range gyro (Трёхосевой гироскоп для отслеживания ориентации, стабилизации изображения)
• High-g accelerometer (акселерометр, обнаруживает ускорение, повороты, шаги)
• Proximity sensor (Датчик приближения, отключает экран при приближении к лицу во время звонка)
• Dual ambient light sensors (датчики освещённости, регулируют яркость экрана)
• Magnetometer (Магнитометр он же компас, определяет магнитное поле для навигации) В pixel нет LiDAR и FaceID, зато есть сканер отпечатков и дополнительно
• Temperature (Инфракрасный датчик температуры для измерения температуры объектов)
• Hall effect (Для обнаружения чехлов)В целом все эти датчики могут быть интересны, но в рамках статьи рассмотрим только те что связаны с определением положения в пространстве, это:

• гироскоп
• акселерометр
• магнитометр - его намеренно проигнорируемСамое главное что нужно знать фронтендеру про эти датчики, это то что они суперчувствительны, подвержены помехам и при кривом использовании жрут батарею и грузят основной процесс тонной событий. Так что чтобы ваш интерфейс не "дрожал", не "скакал" и не лагал, нужно будет все эти их особенности учесть. Но - всё это достаточно легко решаемо.Вот интересная статья про "капризы" сенсоров.Собственно от магнитометра мы отказываемся именно из-за помех. Прекрасно что есть готовые api для получения информации о положении в пространстве только на основе суммы данных с гироскопа и акселерометра.

API

Где-то в 2010-2011 в Mobile Safari и Android Browser появились DeviceOrientation / DeviceMotion позже присоединился и firefox. Появилась классная либа gyro.js , помню именно на ней тогда сделал весьма посредственный эффект перспективы на своём персональном сайте который с тех пор толком не обновлял. Собственно наличие такой библиотеки как-бы намекает на некоторые недостатки API. Там нет возможности установать частоту обновления, значения какие-то нестабильные, нельзя отдельно с разных сенсоров получать данные, выдается только вместе гиро + аксель, на данный момент только это api доступно в safari Ну примерно вот так:

window.addEventListener('deviceorientation', throttle(function(move){
console.log(move.alpha, move.beta, move.gamma)
}, 111))В 2016-2018 W3C решает переделать всё правильно и появляется Generic Sensor API

const sensor = new RelativeOrientationSensor({ frequency: 60 })
sensor.addEventListener("reading", () => {
const [x, y, z, w] = sensor.quaternion
})
sensor.start()но Apple отказывается его поддерживать из-за privacy/security/fingerprinting и вообще "нам хватает DeviceOrientation" и плюс ко всему(видимо для подтверждения серьезности этих заявлений) они в 2019 году в ios 13 они начинают требовать явное разрешение пользователя DeviceOrientationEvent.requestPermission() которое выдается только на одну короткую сессию , то есть при повторном заходе на сайт - снова надо спрашивать 🥺 Возможно именно это стало причиной того что всевозможные эффекты на основе гироскопа даже не успели зародиться в вебе... Так что когда Telegram в свои мини-приложения прокинул нативные данные с датчиков счастью моему не было предела!

window.Telegram?.WebApp.DeviceOrientation.start({ refresh_rate: 42 })работает на ios без всяких запросов и подтверждений! Там у него и accelerometer и gyroscope и общее DeviceOrientation!

И конечно же если у нас нативное приложение или сайт внутри WebView то мы можем использовать API платформы:

• Coremotion для IOS
• SensorManager в Android и прокидывать в webview показания (я Swift не знаю, но ИИ подсказывает что это делается как-то так):let motionManager = CMMotionManager()
motionManager.deviceMotionUpdateInterval = 1.0 / 30.0 // ~33ms/30Hz
// @TODO: check battery status > 50%
motionManager.startDeviceMotionUpdates(to: .main) { motion, error in
guard let motion = motion else { return }

let q = motion.attitude.quaternion

let payload: [String: Any] = [
"type": "relative-orientation",
"quaternion": [q.x, q.y, q.z, q.w],
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
"source": "native-ios"
]

let json = try! JSONSerialization.data(withJSONObject: payload)
let jsonString = String(data: json, encoding: .utf8)!

let js = "window.__onMotion(\(jsonString));"
webView.evaluateJavaScript(js)
}Можно посмотреть исходники телеграм где он прокидывает в WebView данные сенсоров в его android клиенте и в ios версии

Да в итоге мы в ситуации когда есть ворох из разных api разного уровня качества и поддержки...И всё же на сегодняшний день у нас только одно ограничение - запрос разрешения доступа для сайта на ios. Если ваш сайт уже для каких-то целей его запрашивает - то и этого ограничения у ваc нет. У нас огромное количество приложений запускается в WebView и там свободно можно применять подобные эффекты. +если что-то не поддерживается в ios это не повод теперь не делать классно для пользователей android.

Батарея

Расход батареи конечно становится больше при подключении сенсоров, при чём скорее даже из-за JS, а не из-за того что сам датчик начинает потреблять энергию, сами по себе устройства гироскопа и акселерометра достаточно энергоэффективны и потребляют 1-3 мА·ч, для сравнения подсветка экрана на минимальной яркости ~20–50 мА·ч, gps - 30–60 мА·ч, wi-fi ~10–200 мА·ч при активной передачи данных). Повышенный расход энергии может быть из-за накладных расходов на обработку данных с сенсоров. Так что самое главное - сразу настроить адекватную частоту опроса датчика. В целом в нашем кейсе не должно быть проблем с излишним потреблением батареи. Но, раз уж у нас это всё просто "украшательства" давайте добавим в код

if ((await navigator.getBattery?.())?.level < 0.5) return

Отладка

HTTPS - важно вам нужно защищенное соединение, просто на "localhost" браузер не отдаст данные. Так что проверяйте на реальном домене или используйте сервисы типа ngrok / locatunnel для прокидывания локального порта на домен с https.

В ChromeDevTools есть панелька в которой можно выставить координаты сенсоров, нажимаете Cmd+Shift+P - пишете "sensor" - выбираете пункт показать сенсоры. В safari такого нет, но всё равно лучше всегда отлаживать на реальном устройстве, подключайте его в дебаг режиме крутите-вертите. Только держа телефон в руке вы сможете заметить все лишние "дрожания" или наоборот тормоза.

Реализация

Если коротко: берём с датчиков 2 координаты соотносящиеся с условной горизонталью и вертикалью, приводим их к формату 0-100(%) и устанавливаем как css переменные, которые используем в calc() для расчёта transform элементов

transform: translateY(calc(60% - var(--gyro-gamma-percent) * 2%));Давайте начнем с самого древнего и поддерживаемого везде api, позже это останется фоллбэком для safari

<section id="test"></section>
<script defer>
// пока что совсем базово
window.addEventListener('deviceorientation', e => {
document.documentElement.style.setProperty('--gyro-gamma', e.gamma)
document.documentElement.style.setProperty('--gyro-beta', e.beta)

console.log(e)
})
</script>
<style>
:root {
--gyro-gamma: 50; --gyro-beta: 50;
}
section {
position:relative;
margin: 40px auto;
width: 70%; height: 50%;
background: #333;
&::before {
content: "";
position: absolute;
border-radius: 100%;
top: 50%; left: 50%;
width: 10px; height: 10px;
background: yellow;
transform: translate(calc(var(--gyro-gamma) * 1px), calc(var(--gyro-beta) * 1px));
}
}
</style>Худо-бедно работает(в Chrome) - посмотрите на мобилке(нужен android), "точка" двигается при наклоне.Давайте сразу разберемся с safari чтобы можно было в нём тестить, допишем:

document.querySelector('#test').addEventListener('click', async ()=>{
if (typeof DeviceOrientationEvent?.requestPermission !== 'function') return

const result = await DeviceOrientationEvent.requestPermission()
console.log(result)
})Событий deviceorientation очень много, поэтому добавим тротлинг

function throttle(fn, ms) {
let last = 0;
return (...args) => {
const now = performance.now()
if (now - last < ms) return
last = now
fn(...args)
}
}

const handler = throttle(e => {
if (e.beta == null || e.gamma == null) return
document.documentElement.style.setProperty('--gyro-gamma', e.gamma)
document.documentElement.style.setProperty('--gyro-beta', e.beta)
}, 50)

window.addEventListener('deviceorientation', handler)50 мс, примерно 20 обновлений в секунду, для бликов вроде хватает, можно будет поэкспериментировать позже.

Данные приходят но в css использовать "cырые" углы beta 0-180, gamma -90-90 не особо удобно, так что давайте сразу переводить всё в диапазон 0–100, где 50 - нейтраль:

const GAMMA_RANGE = 70 // gamma до 90, но на краях мусор — режем
const BETA_OFFSET = 45 // beta 45° = "телефон в руке"
const BETA_RANGE = 45

const handler = throttle(e => {
if (e.beta == null || e.gamma == null) return
if (e.beta > 90) return // экран вниз — игнорим

const gammaNorm = clamp(e.gamma / GAMMA_RANGE)
const betaNorm = clamp((e.beta - BETA_OFFSET) / BETA_RANGE)
const gammaPercent = ((gammaNorm + 1) * 50).toFixed(2)
const betaPercent = ((betaNorm + 1) * 50).toFixed(2)

document.documentElement.style.setProperty('--gyro-gamma-percent', gammaPercent)
document.documentElement.style.setProperty('--gyro-beta-percent', betaPercent)
}, 50)

function clamp(v, min = -1, max = 1) {
return Math.min(max, Math.max(min, v))
}Константы подбирал эмпирически

теперь у нас в css есть --gyro-gamma-percent и --gyro-beta-percent меняющиеся в диапазоне от 0 до 100, что значительно упрощает логику, в самом css теперь можно абстрагироваться от реальных углов, 50 это середина, ну и 0 и 100 - края, удобно делать всякие сдвиги на % от ширины контейнера. Разве что можно добавить ещё offset от центра (-50..50), я его часто использую в коде:

:root { --gyro-gamma-percent: 50; --gyro-beta-percent: 50;

--g-offset: calc(var(--gyro-gamma-percent) - 50);
--b-offset: calc(var(--gyro-beta-percent) - 50);
}конечно вы можете назвать переменные по своему например --g-horizontal вместо gamma и --g-vertical вместо beta, я пока c js возился запомнил уже что к какой оси относится )

Возможно, кому-то, даже этого уже будет достаточно, в целом норм работает

Но мы пойдём дальше.

RelativeOrientationSensor

В отличие от deviceorientation в современном эйпиай можно задать frequency/реальную частоту опроса датчика, и обойтись без тротлер-функции поверх.

if (!window.RelativeOrientationSensor) { /* fallback на deviceorientation */ return }

const sensor = new RelativeOrientationSensor({ frequency: 42, referenceFrame: "screen" })

sensor.addEventListener('reading', () => {
const [qx, qy, qz, qw] = sensor.quaternion
// ...
})
sensor.start()этот сенсор отдаёт нам не просто "массивчик" из 4х чисел, а Кватернион! как сообщает Википедия

Кватернио́ны (от лат. quaterni, по четыре) — система гиперкомплексных чисел, образующая векторное пространство размерностью четыре над полем вещественных чисел.Гиперкомплексные же числа обобщают комплексные числа, вводя несколько мнимых единиц, и применяются в геометрии, физике (особенно квантовой) и компьютерной графике как раз для описания вращений и преобразований.

Ну а кооомплексные числа, - это уже все знают! Это... вот эти вот.. специальные числа, которые там... для всяких умных штук короч нужны )))Так что запомните этот момент, потом похвастаетесь где-нибудь, какой у нас тут рокетсайнс на фронтенде )))Не знаю... может это не лучшее решение, но я через вектор гравитации в итоге привожу эти координаты к такому же формату как у deviceorientation, делаю те же gamma и beta, для совместимости так сказать:

// сначала получим вектора гравитации из кватериона
const gx = 2 * (qx * qz - qw * qy)
const gy = 2 * (qy * qz + qw * qx)
// а из них уже наши бету и гамма
const betaDeg = Math.asin(clamp(gy)) * (180 / Math.PI)
const gravityGammaDeg = Math.asin(clamp(gx)) * (180 / Math.PI)всё вместе c нормализацией:

const GAMMA_RANGE = 70, BETA_OFFSET = 45, BETA_RANGE = 45; // Диапазоны нормализации углов, подобраны эмпирически
const clamp = (value, min = -1, max = 1) => Math.min(max, Math.max(min, value))
const RAD2DEG = 180 / Math.PI

sensor.addEventListener('reading', () => {
const [qx, qy, qz, qw] = sensor.quaternion
const gx = 2 * (qx * qz - qw * qy)
const gy = 2 * (qy * qz + qw * qx)
const betaDeg = Math.asin(clamp(gy)) * RAD2DEG
const betaNorm = clamp((betaDeg - BETA_OFFSET) / BETA_RANGE)
const gravityGammaDeg = Math.asin(clamp(gx)) * RAD2DEG
const gravityGamma = clamp(-gravityGammaDeg / GAMMA_RANGE)
})
sensor.start()

API Telegram Mini Apps

Если ваш сайт открыт как Telegram Mini App, можно использовать DeviceOrientation из WebApp API. На iOS работает без запроса разрешения так как Telegram уже получил доступ к сенсорам.

function initTWASensor() { const TWA = window.Telegram?.WebApp

if (!TWA?.DeviceOrientation || !['ios', 'android'].includes(TWA.platform)) {
return false
}

TWA.DeviceOrientation.start({ refresh_rate: 42, need_absolute: false })

TWA.onEvent('deviceOrientationChanged', () => {
const { gamma, beta } = TWA.DeviceOrientation
document.documentElement.style.setProperty("--gyro-gamma",clamp(gamma))
document.documentElement.style.setProperty("--gyro-beta",clamp(beta))
})

return true
}с JS вроде разобрались, теперь перейдём к использованию этих css-переменных и реализации различных эффектов в UI

CSS эффекты

Итак у нас есть --gyro-gamma и --gyro-beta cо значениями от 0 до 100 где 50 это некая "середина"/"нормальное"/"дефолтное" положение. Теперь нам нужно добавить эти переменные в calc(.. для вычисления позиционирования наших (псевдо)элементов, типа

transform: translateX(calc(var(--gyro-gamma) * 1%)) translateY(calc(var(--gyro-beta) * 0.2%));Тут можно много чего напридумывать(надеюсь вы подкините ещё идей в комментариях), поделюсь парочкой моих любимых приёмов.

Блики

Сначала надо бы сделать "карточку"/блок с гранями(border) например так:

section {
position:relative;
overflow: hidden;
background: #fff;
border-top: 1px solid #fff;
border-right: 1px solid #e9e9e9;
border-left: 1px solid #e9e9e9;
border-bottom: 1px solid #dadada;
box-shadow:
0 15px 20px rgba(0, 0, 0, 0.05),
0px 5px 100px rgba(0, 0, 0, 0.05) inset;
border-radius: 10px;
corner-shape: superellipse(1);теперь добавим внутрь неё "блик" яркую белую точку по правой границе

section::after { content: ""; display: block; position: absolute; pointer-events: none;

/* позиционируем блик примерно по середике по вертикали */
top: 20%; right: 0;
height: 60%; width: 1px;

/* на фон ставим градиент от почти прозрачного цвета
до белого */
background: linear-gradient(to top, #0000 10%, #fff1 40%, #fff 55%, #fff1 70%, #0000 90%);
}осталось начать его двигать вместе с устройством

section::after {
/* можно сразу вынести некоторые параметры в переменные
чтобы удобнее было "тюнить" эффект, смотреть на телефоне
и подгонять скорость/заметность
*/
--edge-speed: -2.5%; /* скорость скольжения */

/* тут мы переводим диапазон от0до100 в от-50до50 */
--b-offset: calc(var(--gyro-beta) - 50);

/* ну и двигаем по вертикали в зависимости от того как наклонен телефон */
transform: translateY(calc(var(--b-offset) * var(--edge-speed)));
will-change: transform;
}Вот в общем-то и всё - тут самое сложное подобрать оптимальные цвета и скорость под ваш дизайн. Я всё-таки не дизайнер, и дизайн тут делаю постольку-поскольку, так что в моих демках могут быть не лучшие его примеры... Но думаю общая механика понятна и настоящие дизайнеры смогут сделать конфетку!Вот ещё один пример, теперь это блик поверх иконки, допустим у нас такой html

<figure class="icon"><svg>...</svg></figure>мы добавляем

figure.icon::before {
display: block;
content: "";
pointer-events: none;
position: absolute;
z-index: 2;
top: 0;
width: 64px;
height: 64px;
left: calc(160px + var(--gyro-gamma-percent) * -3px);
background: linear-gradient(90deg, #0000 20%, #fff6 38%, #0000 62%);
}результат как на первом видео в этом посте.Или вот например у нас checkbox в старом стиле apple

input[type="checkbox"].toggle {
appearance: none;
cursor: pointer;
display: inline-block;
background: #ccc;
border-radius: 16px;
corner-shape: superellipse(1);
width: 50px;
height: 28px;
overflow: hidden;
position: relative;
vertical-align: middle;
transition: background 0.25s;
border-top: 1px solid #aaa;
&:before,
&:after {
content: "";
}
&:before {
display: block;
background: #fefefe;
filter: saturate(2);
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.08);
border-radius: 50%;
height: calc(100% - 4px);
aspect-ratio: 1;
position: absolute;
z-index:1;
top: 1.5px;
left: 2px;
transition: transform 0.25s;
}

&:checked {
background: #66cc67;
&:before {
transform: translateX(calc(100% - 2px));
}
}
}можно также добавить ему плавающий градиент на фон

input[type="checkbox"].toggle::after {
display: block;
content: "";
pointer-events: none;
position: absolute;
z-index: 0;
top: 0;
width: 100px;
height: 28px;
left: calc(160px + var(--gyro-gamma-percent) * -3px);
background: linear-gradient(90deg, #0000 20%, #fff5 38%, #0000 62%);
}Больше css-рецептов можно посмотреть по ссылке, я постарался там собрать самые ненавязчивые эффекты чтобы AI не превратил UI в "новогоднюю ёлку" )

Заключение

Весь код и примеры есть в репозитории https://github.com/alexstep/sensor/ хочу подчеркнуть что это не готовая библиотека, а вот именно что просто пример реализации который писался только для этой статьи и его обязательно нужно адаптировать под ваш случай.Надеюсь было интересно. Буду очень благодарен если покидаете ссылки на приложения где как-то необычно используются данные с сенсоров устройства, ну или просто свои идеи что ещё с ними можно сделать.Теги:• sensors
• ui
• js
• css
• twa
• tma
• telegram
• gyroscope
• accelerometerХабы:• CSS
• JavaScript

Получайте больше инсайтов о систематизации бизнеса

Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности

💬 Подписаться на канал