Бывало ли у кого-нибудь так, что начинаете реализовывать одну идею, а она плавно преобразовывается в другую, а затем ещё в одну, и вот, у вас «на выходе» уже совершенно свежая история, только лишь отдалённо напоминающая начальную задумку. Думаю, наверняка, бывало!
Этим особенно хороши свои собственные проекты, когда нет чётко прописанных ТЗ, и начальник не стоит за вашей спиной, одёргивая при любом отклонении от плана. А также этим особенно славится «магия программирования», ведь код — потрясающе гибкая магическая субстанция, которая в соединении с железом современных смартфонов, может творить настоящие чудеса.
Вот и на этот раз, у меня была идея сначала попробовать реализовать простейшую игру, на подобие «крестиков‑ноликов», только с более расширенным сюжетом (о ней как‑нибудь тоже обязательно расскажем). Но в какой то момент совершенно неожиданно пропали все мои программные наработки, которые были написаны на промежуточном этапе, и «со скрипом», но таки пришлось возвращаться к самому началу написания кода.
И вот, когда повторно пишешь что‑нибудь, во‑первых, часто бывает просто лень и неинтересно заново точь‑в-точь воспроизводить то, что делал все последние месяцы, поэтому начинаешь импровизировать. А во‑вторых, обязательно будут появляться новые мысли, и как оказывается, начальный замысел может «уплывать» совсем в другую сторону от первоначального — потому что, заново обдумывая свою главную мысль, может возникнуть вопрос — «В зачем, собственно, это?...» :)
Вот уж, правильно сказал в свое время Гераклит Эфесский: «Нельзя войти в одну и ту же реку дважды...». Но с другой стороны, это и не плохо, тем более, что предыдущая мысль у вас также останется, и может быть, даже преобразуется со временем к более интересному виду — и вопрос «Зачем?» — решится сам собой. Но ведь именно рождение и преобразование мыслей очень важны в творческой жизни, ведь получается, даже на таком простом примере мы отчётливо видим ветвление — раздвоение одной мысли на старую и новую, то есть прорастание дерева идей, главное только, чтобы идеи были полезные.
Так вот, вернёмся к нашей конкретной сегодняшней задаче — реализации новой идеи! Предлагаем сделать мобильное приложение, которое поможет нам создавать «каплю росы» простым прикосновением пальца к поверхности смартфона. Эта капелька будет реагировать на наши действия с телефоном, и перемещаться по поверхности экрана в зависимости от его угла наклона. В дальнейшем можно будет дополнить это приложение какими‑нибудь более «расширенными заклинаниями», но пока остановимся на простейшем варианте! Задачка, в первую очередь, обучающая, и придумана для того, чтобы прививать молодёжи интерес к изучению техники, да и просто самому интересно поработать со встроенными в смартфон сенсорами.

Итак, в данной задаче нам поможет «магический» 3-х осевой акселерометр, встроенный в наш смартфон. В качестве софта для написания логики мы снова возьмём IDE DroidScript, так как эта IDE позволяет отрабатывать программу прямо на смартфоне без эмуляторов, а это особенно важно, когда мы пишем код с использованием датчиков, так как можно сразу же «в живую» увидеть, как «материализуется» и живёт наша магия.
Для начала нам нужно сделать поле, по которому будет перемещаться капелька, допустим, оно будет размером 10×10 ячеек (в нашем случае, в качестве ячеек мы можем использовать обычные кнопки). По нажатию на любую из этих ячеек на поверхности смартфона будет появляться наша капелька. Которая в дальнейшем будет перемещаться по экрану смартфона...
Хочу отметить, что с написанием программы в среде DroidScript, и пониманием основ JavaScript, мне очень помог Espirito Embriagado, а также сам DroidScript, который имеет множество встроенных примеров с открытыми кодами в своем составе. Весь код написан на языке JavaScript.
// Dew Drop interactive // Капля создана = 1, нет = 0 var dewPointExist; // Массив ячеек поля var buttons = []; // Предыдущие значения с датчика var prevX=0; var prevY=0; var prevZ=10; // Размер ячеек поля var width = 0.1; // ширина var height = 0.05; // высота var margin = 0; // расстояние между ячейками var rows = 10; // число строк var columns = 10; // число столбцов // Создаем класс для поля class gameField { constructor() { // Создание массива из 100 ячеек this.cellsArray = new Array(rows*columns); // Обнуление всех ячеек for (var i=0; i<rows* columns; i++) { this.cellsArray[i] = 0; } } // Значение в ячейке: // 0 - свободна // 1 - капелька росы // 2 - ...? ( доработка ) setCellValue(cellNum, cellValue) { this.cellsArray[cellNum] = cellValue; } getCellValue(cellNum) { return this.cellsArray[cellNum]; } } // Создаем класс для нашей капельки росы class dewPoint { constructor( dewPoint ) { // Координата капельки this.dewPoint = dewPoint; } setDewPoint( dewPoint ) { this.dewPoint = dewPoint; } getDewPoint( ) { return this.dewPoint; } } // Создаём новое чистое поле var currentGameField = new gameField(); // Подготавливаем наше капельку var dewPoint1 = new dewPoint(0);

Теперь, в соответствии с изменениями показаний внутреннего акселерометра, наша капелька должна будет перемещаться по поверхности (т.е. изменять свои координаты, и перерисовываться снова и снова).
Чтобы было интереснее, давайте создадим эффект переливания капельки разными цветами. Для этого по мере обновления данных с нашего акселерометра, будем формировать строку нового цвета следующим образом.
// Переменные для работы с акселерометром sampleCount = 10; updateInterval = 10; sensorData = null; // Инициализация акселерометра function accelerometerInitializing() { snsAcc = app.CreateSensor( "Accelerometer", "Fast" ); snsAcc.Start(); // Считывание данных var vals = snsAcc.GetValues(); var series = GetSeries( sampleCount, [vals[1],vals[2],vals[3]] ); // Запись данных с акселерометра в текстовые поля txt1.SetText( "X: " + vals[1] ); txt2.SetText( "Y: " + vals[2] ); txt3.SetText( "Z: " + vals[3] ); // Обновляем данные с акселерометра Update(); } // Функция обновления данных с акселерометра function Update() { // Считывание данных var vals = snsAcc.GetValues(); var series = GetSeries( sampleCount, [vals[1],vals[2],vals[3]] ); // Обновляем данные в текстовых полях txt1.SetText( "X: " + Math.round(vals[1]) ); txt2.SetText( "Y: " + Math.round(vals[2]) ); txt3.SetText( "Z: " + Math.round(vals[3]) ); // Перемннные для изменения цвета капельки let s1 = transtoStr16Value(calc10to16in10( vals[1] )); let s2 = transtoStr16Value(calc10to16in10( vals[2] )); let s3 = transtoStr16Value(calc10to16in10( vals[3] )); // Считываем новые данные с акселкрометра let newX=Math.round(vals[1]); let newY=Math.round(vals[2]); let newZ=Math.round(vals[3]); // Переменная для новой координаты капельки let nnewCoordinate=0; // Капелька создана? if (dewPointExist == 1) { // Далее перерисовываем капельку в зависимости от изменения показаний датчика if(newX > prevX) { if(cellsVerticalWallChecking( "right")==false) { // cell[i+1] = 1 nnewCoordinate=dewPoint1.getDewPoint( ) +1; currentGameField.setCellValue(nnewCoordinate, 1); // cell[i] = 0 currentGameField.setCellValue(dewPoint1.getDewPoint( ), 0); dewPoint1.setDewPoint( nnewCoordinate); } } if (newY > prevY) { if (dewPoint1.getDewPoint( )<rows*columns-rows) { // cell[i+7] = 1 nnewCoordinate=dewPoint1.getDewPoint( ) +rows; currentGameField.setCellValue(nnewCoordinate, 1); // cell[i] = 0 currentGameField.setCellValue(dewPoint1.getDewPoint( ), 0); dewPoint1.setDewPoint( nnewCoordinate); } } if (newX < prevX) { if(cellsVerticalWallChecking( "left")==false) { // cell[i-1] = 1 nnewCoordinate=dewPoint1.getDewPoint( ) -1; currentGameField.setCellValue(nnewCoordinate, 1); // cell[i] = 0 currentGameField.setCellValue(dewPoint1.getDewPoint( ), 0); dewPoint1.setDewPoint( nnewCoordinate); } } if (newY < prevY) { if (dewPoint1.getDewPoint( )>=rows) { // cell[i-7] = 1 nnewCoordinate=dewPoint1.getDewPoint( ) -rows; currentGameField.setCellValue(nnewCoordinate, 1); // cell[i] = 0 currentGameField.setCellValue(dewPoint1.getDewPoint( ), 0); dewPoint1.setDewPoint( nnewCoordinate); } } changingButtonsColors( colorCompilation( s1, s2, s3) ); prevSensorDataStore( Math.round(vals[1]), Math.round(vals[2]), Math.round(vals[3])); } txtNewCo.SetText( "NewCoordinate: " + nnewCoordinate); // Повторяем вызов функции setTimeout( Update, updateInterval ); } // Обновление предыдущих данных с акселерометра function prevSensorDataStore( prevX, prevY, prevZ ) { prevX = prevX; prevY = prevY; prevZ = prevZ; } // Получение саккумулированных данных акселерометра function GetSeries( points, funcs ) { if( typeof flt_dataStore == "undefined" ) { flt_dataStore = new Array( funcs.length ); } var series = []; for( var i=0; i< funcs.length; i++ ) { if( !flt_dataStore[i] ) { flt_dataStore[i] = []; } if( flt_dataStore[i].length > sampleCount ) { flt_dataStore[i] = flt_dataStore[i].slice(1); } var val = funcs[i]; flt_dataStore[i].push( val ); var res = []; var len = flt_dataStore[i].length; for( var j = 0; j < len; j++ ) { res.push( [j-len, flt_dataStore[i][j]] ) } series[i] = res; } return series; } // Запускаем при загрузке function OnStart() { // Создаем наше новое поле с ячейками buttonsArrayCreation(); // Инициализируем акселерометр accelerometerInitializing(); // Инициализируем датчик света lightSensorInitialisation() // Изначально капельки ещё нет dewPointExist = 0; // Задаем начальные значения с акселерометра prevSensorDataStore( 0, 0, 10 ) } // Функция создания поля с ячейками function buttonsArrayCreation() { app.SetOrientation("Portrait"); layVert = app.CreateLayout( "Linear", "Vertical"); // Цикл создания поля ячеек for (var row = 0; row < rows; row++) { // Новая горизонтальная строка ячеек layHoriz = app.CreateLayout( "Linear", "Horizontal"); layVert.AddChild( layHoriz ); // Вертикальные ячейки for (var col = 0; col < columns; col++) { var index = row * columns + col; // Вычисляем текущий индекс ячейки поля // Создание кнопок поля var btn = app.CreateButton(index, width, height); btn.SetMargins(0, margin); btn.data = { index: index, customInfo: "Coordinate: "+ row + ", " + col }; btn.SetOnTouch(GenericButtonCallback); btn.SetBackColor( "#4B4C4E" ); buttons.push(btn); layHoriz.AddChild(btn); } } // 3 горизонтальных поля для отображения данных с акселерометра layHoriz = app.CreateLayout( "Linear", "Horizontal"); layVert.AddChild( layHoriz ); txt1 = app.CreateText( "X:" ); txt1.SetMargins(0.05, 0); txt1.SetTextSize( 20 ); layHoriz.AddChild(txt1); layHoriz = app.CreateLayout( "Linear", "Horizontal"); layVert.AddChild( layHoriz ); txt2 = app.CreateText( "Y:" ); txt2.SetMargins(0.05, 0); txt2.SetTextSize( 20 ); layHoriz.AddChild(txt2); layHoriz = app.CreateLayout("Linear", "Horizontal"); layVert.AddChild( layHoriz ); txt3 = app.CreateText( "Z:" ); txt3.SetMargins(0.05, 0); txt3.SetTextSize( 20 ); layHoriz.AddChild(txt3); // Инфо поле для отображения составленного цвета капли layHoriz = app.CreateLayout( "Linear", "Horizontal"); layVert.AddChild( layHoriz ); txtInfo = app.CreateText( "Info:" ); txtInfo.SetMargins(0.05, 0); txtInfo.SetTextSize( 20 ); layHoriz.AddChild(txtInfo); // Поле для отображения данных с датчика света layHoriz = app.CreateLayout( "Linear", "Horizontal"); layVert.AddChild( layHoriz ); txtLux = app.CreateText( "Lux:" ); txtLux.SetMargins(0.05, 0); txtLux.SetTextSize( 20 ); layHoriz.AddChild(txtLux); // Поле для отображения текущей координаты капельки layHoriz = app.CreateLayout( "Linear", "Horizontal"); layVert.AddChild( layHoriz ); txtNewCo = app.CreateText( "NewCoordinate:" ); txtNewCo.SetMargins(0.05, 0); txtNewCo.SetTextSize( 20 ); layHoriz.AddChild(txtNewCo); app.AddLayout( layVert ); } // Функция по нажатию ячейки поля function GenericButtonCallback() { // Отображаем на экране данные нажатой ячейки app.ShowPopup("Button " + this.data.index + " with " + this.data.customInfo); // Изменяем цвет нажатой кнопки var button = this; button.SetBackColor("blue"); // Изменяем значения классов поля и капельки currentGameField.setCellValue(this.data.index, 1); dewPoint1.setDewPoint(this.data.index); // Теперь наша капелька создана dewPointExist = 1; } // Функция ограничения поля по вертикали слева и справа function cellsVerticalWallChecking( sleftOrRight) { let boolResult=false; if ( sleftOrRight == "left" ) { if ( dewPoint1.getDewPoint() % 10 == 0) { //app.Alert("%10 = " + dewPoint1.getDewPoint( ) % 10); boolResult=true; } } if ( sleftOrRight == "right" ) { if ( dewPoint1.getDewPoint() % 10 == 9) { //app.Alert("%10 = " + dewPoint1.getDewPoint( ) % 10); boolResult=true; } } return boolResult; } // Функция перевода данных из 10-ричной // В 16-ричную форму с акселерометра // -10...+10 -> 0...255 (00...FF) // X = 128*(y/10 + 1) function calc10to16in10( nmeas10Value ) { let nmeas16Value = Math.round(128 * ((nmeas10Value/10) +1)); return nmeas16Value; } // Преобразование X в 16-ричное представление: // Например, 255 / 16 = 15 остаток 15 (FF) function transtoStr16Value ( nmeas16Value ) { let sdivideFirst = s16ValueTransform(String(Math.floor( nmeas16Value / 16))); let sdivideSecond = s16ValueTransform(String(nmeas16Value % 16)); let s16measValue = ""; s16measValue = s16measValue.concat(sdivideSecond, sdivideFirst); return s16measValue; } // Заменяем десятичные числа из двух цифр на буквы в 16-ричном виде function s16ValueTransform( sValue ) { switch ( sValue ) { case "10": sValue = "A"; break; case "11": sValue = "B"; break; case "12": sValue = "C"; break; case "13": sValue = "D"; break; case "14": sValue = "E"; break; case "15": sValue = "F"; break; default: sValue = sValue; } return sValue; } // Формируем строку цвета, составленную из 16-ричных чисел с 3-х осевого датчика function colorCompilation( s1, s2, s3) { let scommonColor = ""; scommonColor = scommonColor.concat("#", s1, s2, s3); txtInfo.SetText( "Info: " + scommonColor); return scommonColor; } // Изменяем цвета ячеек нашего поля function changingButtonsColors( scommonColor ) { // Цикл для подкрашивания капелек на поле for (var i = 0; i < rows*columns; i++) { // Проверяем есть ли капелька на данной ячейке if (currentGameField.getCellValue(i) == 1) { buttons[i].SetBackColor( scommonColor ); } else { buttons[i].SetBackColor( "#4B4C4E" ); } } }
А теперь давайте ещё сделаем так, чтобы можно было убрать капельку с поля взмахом руки над смартфоном. Для этого задействуем встроенный датчик света.
// Инициализируем датчик света function lightSensorInitialisation() { snsLux = app.CreateSensor( "Light" ); snsLux.SetOnChange( sns_OnChange ); snsLux.Start(); } // При изменении значений с датчика света мигает желтым светом и стираем нашу капельку function sns_OnChange( lux ) { txtLux.SetText( "level = " + lux + " lux" ); // При уменьшении света изменяем цвет капельки на цвет фона м удаляем if (lux < 5) { layVert.SetBackColor("#FAED27"); dewPointExist=0; currentGameField.setCellValue(dewPoint1.getDewPoint( ), 0); buttons[dewPoint1.getDewPoint( )].SetBackColor( "#4B4C4E" ); dewPoint1.setDewPoint( 0 ); } else { layVert.SetBackColor("#013212"); } }
Вот так, теперь запустив нашу программу в DroidDscript, получаем волшебную площадку в лице нашего смартфона, на которой мы можем создавать капельку росы, «катать» её по экрану из стороны в сторону, и стирать с поля простым взмахом руки вблизи экрана. Небольшое, но волшебство :))
Само приложение можно скачать и посмотреть на своем Android-смартфоне из RuStore.
Всем добрых мыслей :))
