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

Итак, в данной задаче нам поможет «магический» 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.
Всем добрых мыслей :))