Бывало ли у кого-нибудь так, что начинаете реализовывать одну идею, а она плавно преобразовывается в другую, а затем ещё в одну, и вот, у вас «на выходе» уже совершенно свежая история, только лишь отдалённо напоминающая начальную задумку. Думаю, наверняка, бывало!

Этим особенно хороши свои собственные проекты, когда нет чётко прописанных ТЗ, и начальник не стоит за вашей спиной, одёргивая при любом отклонении от плана. А также этим особенно славится «магия программирования», ведь код — потрясающе гибкая магическая субстанция, которая в соединении с железом современных смартфонов, может творить настоящие чудеса.

Вот и на этот раз, у меня была идея сначала попробовать реализовать простейшую игру, на подобие «крестиков‑ноликов», только с более расширенным сюжетом (о ней как‑нибудь тоже обязательно расскажем). Но в какой то момент совершенно неожиданно пропали все мои программные наработки, которые были написаны на промежуточном этапе, и «со скрипом», но таки пришлось возвращаться к самому началу написания кода.

И вот, когда повторно пишешь что‑нибудь, во‑первых, часто бывает просто лень и неинтересно заново точь‑в-точь воспроизводить то, что делал все последние месяцы, поэтому начинаешь импровизировать. А во‑вторых, обязательно будут появляться новые мысли, и как оказывается, начальный замысел может «уплывать» совсем в другую сторону от первоначального — потому что, заново обдумывая свою главную мысль, может возникнуть вопрос — «В зачем, собственно, это?...» :)

Вот уж, правильно сказал в свое время Гераклит Эфесский: «Нельзя войти в одну и ту же реку дважды...». Но с другой стороны, это и не плохо, тем более, что предыдущая мысль у вас также останется, и может быть, даже преобразуется со временем к более интересному виду — и вопрос «Зачем?» — решится сам собой. Но ведь именно рождение и преобразование мыслей очень важны в творческой жизни, ведь получается, даже на таком простом примере мы отчётливо видим ветвление — раздвоение одной мысли на старую и новую, то есть прорастание дерева идей, главное только, чтобы идеи были полезные.

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

Итак, в данной задаче нам поможет «магический» 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.

Всем добрых мыслей :))