6:00 Там же и ссылки на исходники.
https://transfiles.ru/9cqnb вроде нашел, а может и не совсем то… вот это от ардуино головного мозга или от куда ?))) кто делал, какой проект, от куда вырезан этот эффект ?
вот тут вроде даже просмотреть можно, пока не удалили https://ru.files.fm/u/3cnstfjqfxfjyxrv#/view/kct9ey72kbrzpp7c
Наверное его придумали
…и он оригинальный.
Да, Гайвер призывает в конце ролика придумывать 1D игры для ленты. Я придумал, специально для Бабоса и ИИ, чтоб кнопку жмакать и не скучно через минуту было. Аппаратная часть - лента на эн пикселей, кнопка и Ардуино уно. Можно и симуляцию в хтмл файле. Суть:
пиксель-шарик
пиксель - ямка
нажимаем, удерживаем кнопку, отпускаем - длительность удержания задаёт нач.скорость и дальность катания
цель - шарик останавливается именно над лункой…и тогда проваливается, иначе проскакивает мимо
края ленты (ширина экрана) работают как идеальные отражатели для шарика
после цикла игры ямка и шарик псевдослучайно вновь появляются на поле-ленте.
может и так… а может вырезали от куда то, а мне такой проект не попадался))) или я не внимательно смотрел…
1d вроде не то, даже если просто с кодом посидеть нечего не даст…
я бы наверное сделал игру Drilling Billy но монстры в виде пикселя… + не все реализовать можно, + это долго и не интересно вроде…
а почему tft не умеют контролировать яркость каждого пикселя ?)))
вот это тогда бы инструментище был, и сразу много чего можно было бы сделать…
В смысле считывать данные цвета? Так умные пиксели тоже не могут считывать ![]()
на адресной ленте вроде можно контролировать яркость каждого пикселя, а вот на tft нет…
https://wdfiles.ru/Ppj7 салют вроде смотрится, но камера плохо передает, и не желает фокусироваться
даже вроде нашел исходники от предыдущего видео, но это не точно)))
#include <TFT_eSPI.h>
#include <SPI.h>
#include <math.h>
TFT_eSPI tft = TFT_eSPI();
// === Параметры красного шарика ===
struct Ball {
float x = 64;
float y = 80;
float vx = 2.0;
float vy = 1.5;
int radius = 4;
uint16_t color = TFT_RED;
};
// === Параметры синего шарика ===
struct Chaser {
float x = 20;
float y = 20;
float vx = 0;
float vy = 0;
float speed = 1.8;
int radius = 4;
uint16_t color = TFT_BLUE;
bool caught = false;
};
Ball ball;
Chaser chaser;
// === Структура частицы ===
struct Particle {
float x, y;
float vx, vy;
float radius;
uint16_t color;
float life;
float decay;
float gravity;
bool active;
bool isSpark;
bool hasExploded;
int explosionDelay;
};
#define MAX_PARTICLES 500
Particle particles[MAX_PARTICLES];
int particleCount = 0;
// === Флаг взрыва ===
bool explosionTriggered = false;
unsigned long explosionCooldown = 0;
const unsigned long COOLDOWN_TIME = 1500;
// === РАСШИРЕННАЯ ПАЛИТРА ЦВЕТОВ для фейерверка ===
const uint16_t fireColors[] = {
// Основные цвета
TFT_RED, TFT_ORANGE, TFT_YELLOW, TFT_GREEN,
TFT_CYAN, TFT_BLUE, TFT_PURPLE, TFT_PINK,
TFT_MAGENTA, TFT_NAVY, TFT_SKYBLUE, TFT_OLIVE,
TFT_GOLD, TFT_SILVER, TFT_MAROON, TFT_VIOLET,
TFT_BROWN, TFT_GREENYELLOW, TFT_DARKGREEN, TFT_DARKCYAN,
// ДОПОЛНИТЕЛЬНЫЕ ЦВЕТА (яркие и насыщенные)
// Красные оттенки
tft.color565(255, 0, 0), // Ярко-красный
tft.color565(255, 50, 50), // Светло-красный
tft.color565(200, 0, 0), // Темно-красный
tft.color565(255, 20, 80), // Розово-красный
// Оранжевые оттенки
tft.color565(255, 100, 0), // Ярко-оранжевый
tft.color565(255, 150, 0), // Светло-оранжевый
tft.color565(255, 80, 20), // Красно-оранжевый
// Желтые оттенки
tft.color565(255, 255, 0), // Ярко-желтый
tft.color565(255, 200, 0), // Золотистый
tft.color565(255, 255, 100), // Светло-желтый
// Зеленые оттенки
tft.color565(0, 255, 0), // Ярко-зеленый
tft.color565(0, 200, 0), // Темно-зеленый
tft.color565(50, 255, 50), // Светло-зеленый
tft.color565(0, 255, 100), // Мятный
// Голубые оттенки
tft.color565(0, 255, 255), // Ярко-голубой
tft.color565(0, 200, 255), // Небесно-голубой
tft.color565(50, 150, 255), // Сине-голубой
tft.color565(100, 200, 255), // Светло-голубой
// Синие оттенки
tft.color565(0, 0, 255), // Ярко-синий
tft.color565(50, 50, 255), // Светло-синий
tft.color565(0, 0, 200), // Темно-синий
tft.color565(100, 100, 255), // Лавандовый
// Фиолетовые оттенки
tft.color565(150, 0, 255), // Ярко-фиолетовый
tft.color565(200, 0, 255), // Пурпурный
tft.color565(100, 0, 200), // Темно-фиолетовый
tft.color565(200, 100, 255), // Светло-фиолетовый
// Розовые оттенки
tft.color565(255, 0, 150), // Ярко-розовый
tft.color565(255, 50, 200), // Неоново-розовый
tft.color565(255, 100, 150), // Светло-розовый
tft.color565(200, 50, 100), // Темно-розовый
// Бирюзовые оттенки
tft.color565(0, 255, 150), // Бирюзовый
tft.color565(50, 200, 200), // Темно-бирюзовый
tft.color565(100, 255, 200), // Светло-бирюзовый
// Пастельные оттенки
tft.color565(255, 150, 150), // Пастельно-розовый
tft.color565(150, 255, 150), // Пастельно-зеленый
tft.color565(150, 150, 255), // Пастельно-синий
tft.color565(255, 255, 150), // Пастельно-желтый
tft.color565(255, 150, 255), // Пастельно-фиолетовый
tft.color565(150, 255, 255), // Пастельно-голубой
// Яркие неоновые цвета
tft.color565(255, 0, 255), // Неоново-розовый
tft.color565(0, 255, 255), // Неоново-голубой
tft.color565(255, 255, 0), // Неоново-желтый
tft.color565(0, 255, 0), // Неоново-зеленый
tft.color565(255, 0, 0), // Неоново-красный
// Белые и светлые оттенки
TFT_WHITE,
tft.color565(200, 200, 200), // Светло-серый
tft.color565(255, 200, 200), // Розовато-белый
tft.color565(200, 200, 255), // Синевато-белый
tft.color565(200, 255, 200), // Зеленовато-белый
tft.color565(255, 255, 200), // Желтовато-белый
};
const int colorCount = sizeof(fireColors) / sizeof(fireColors[0]);
// === Функция получения случайного цвета из палитры ===
uint16_t getRandomColor() {
return fireColors[random(0, colorCount)];
}
// === Функция создания вторичного взрыва ===
void createSparkExplosion(float cx, float cy, uint16_t baseColor) {
int count = 5 + random(0, 20);
for (int i = 0; i < count && particleCount < MAX_PARTICLES; i++) {
float angle = random(0, 360) * 3.14159 / 180.0;
float speed = 15 + random(0, 120);
uint16_t color;
if (random(0, 100) < 40) { // Увеличил шанс случайного цвета
color = getRandomColor();
} else {
color = baseColor;
}
particles[particleCount].x = cx;
particles[particleCount].y = cy;
particles[particleCount].vx = cos(angle) * speed;
particles[particleCount].vy = sin(angle) * speed - random(0, 30);
particles[particleCount].radius = 0.07 + random(0, 6) / 10.0;
particles[particleCount].color = color;
particles[particleCount].life = 0.6 + random(0, 40) / 100.0;
particles[particleCount].decay = 0.015 + random(0, 40) / 10000.0;
particles[particleCount].gravity = 0.1 + random(0, 50) / 1000.0;
particles[particleCount].active = true;
particles[particleCount].isSpark = true;
particles[particleCount].hasExploded = false;
particles[particleCount].explosionDelay = 0;
particleCount++;
}
}
// === Функция создания основного взрыва ===
void createExplosion(int cx, int cy) {
int count = 100 + random(0, 100);
for (int i = 0; i < count && particleCount < MAX_PARTICLES; i++) {
float angle = random(0, 360) * 3.14159 / 180.0;
float speed = 30 + random(0, 180);
particles[particleCount].x = cx;
particles[particleCount].y = cy;
particles[particleCount].vx = cos(angle) * speed;
particles[particleCount].vy = sin(angle) * speed;
particles[particleCount].radius = 0.12 + random(0, 8) / 10.0;
particles[particleCount].color = getRandomColor(); // Теперь всегда случайный цвет
particles[particleCount].life = 0.8 + random(0, 40) / 100.0;
particles[particleCount].decay = 0.008 + random(0, 30) / 10000.0;
particles[particleCount].gravity = 0.05 + random(0, 50) / 1000.0;
particles[particleCount].active = true;
particles[particleCount].isSpark = false;
particles[particleCount].hasExploded = false;
particles[particleCount].explosionDelay = 10 + random(0, 40);
particleCount++;
}
}
// === Обновление частиц ===
void updateParticles() {
for (int i = 0; i < particleCount; i++) {
if (!particles[i].active) continue;
particles[i].x += particles[i].vx * 0.02;
particles[i].y += particles[i].vy * 0.02;
particles[i].vy += particles[i].gravity;
particles[i].vx *= 0.98;
particles[i].vy *= 0.98;
particles[i].life -= particles[i].decay;
// Каскадный взрыв
if (!particles[i].isSpark && !particles[i].hasExploded) {
if (particles[i].explosionDelay > 0) {
particles[i].explosionDelay--;
} else {
int chance = 5 + random(0, 15);
if (particles[i].life > 0.2 && particles[i].life < 0.6) {
if (random(0, 100) < chance && particleCount < MAX_PARTICLES - 30) {
createSparkExplosion(particles[i].x, particles[i].y, particles[i].color);
particles[i].hasExploded = true;
particles[i].color = TFT_WHITE;
particles[i].radius += 0.4;
}
}
}
}
// Вторичный взрыв искр
if (particles[i].isSpark && !particles[i].hasExploded && particles[i].life > 0.3 && particles[i].life < 0.5) {
if (random(0, 100) < 8 && particleCount < MAX_PARTICLES - 10) {
int miniCount = 3 + random(0, 8);
for (int j = 0; j < miniCount && particleCount < MAX_PARTICLES; j++) {
float angle = random(0, 360) * 3.14159 / 180.0;
float speed = 10 + random(0, 50);
particles[particleCount].x = particles[i].x;
particles[particleCount].y = particles[i].y;
particles[particleCount].vx = cos(angle) * speed;
particles[particleCount].vy = sin(angle) * speed - 10;
particles[particleCount].radius = 0.05 + random(0, 4) / 10.0;
particles[particleCount].color = getRandomColor(); // Случайный цвет
particles[particleCount].life = 0.4 + random(0, 30) / 100.0;
particles[particleCount].decay = 0.02 + random(0, 30) / 10000.0;
particles[particleCount].gravity = 0.1 + random(0, 30) / 1000.0;
particles[particleCount].active = true;
particles[particleCount].isSpark = true;
particles[particleCount].hasExploded = true;
particles[particleCount].explosionDelay = 0;
particleCount++;
}
particles[i].hasExploded = true;
}
}
if (particles[i].life <= 0 || particles[i].y > 170 || particles[i].x > 140 || particles[i].x < -10) {
particles[i].active = false;
}
}
}
// === Обновление красного шарика ===
void updateBall() {
if (random(0, 100) < 2) {
ball.vx += (random(0, 20) - 10) / 20.0;
ball.vy += (random(0, 20) - 10) / 20.0;
float maxSpeed = 3.0;
float speed = sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (speed > maxSpeed) {
ball.vx = (ball.vx / speed) * maxSpeed;
ball.vy = (ball.vy / speed) * maxSpeed;
}
}
ball.x += ball.vx;
ball.y += ball.vy;
const int WIDTH = 128;
const int HEIGHT = 160;
if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
ball.vx = -ball.vx;
}
if (ball.x + ball.radius > WIDTH) {
ball.x = WIDTH - ball.radius;
ball.vx = -ball.vx;
}
if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
ball.vy = -ball.vy;
}
if (ball.y + ball.radius > HEIGHT) {
ball.y = HEIGHT - ball.radius;
ball.vy = -ball.vy;
}
}
// === Обновление синего шарика ===
void updateChaser() {
if (explosionTriggered) {
chaser.vx *= 0.98;
chaser.vy *= 0.98;
} else {
float dx = ball.x - chaser.x;
float dy = ball.y - chaser.y;
float distance = sqrt(dx * dx + dy * dy);
if (distance > 1) {
float dirX = dx / distance;
float dirY = dy / distance;
chaser.vx = dirX * chaser.speed;
chaser.vy = dirY * chaser.speed;
}
}
chaser.x += chaser.vx;
chaser.y += chaser.vy;
const int WIDTH = 128;
const int HEIGHT = 160;
if (chaser.x - chaser.radius < 0) {
chaser.x = chaser.radius;
chaser.vx = -chaser.vx;
}
if (chaser.x + chaser.radius > WIDTH) {
chaser.x = WIDTH - chaser.radius;
chaser.vx = -chaser.vx;
}
if (chaser.y - chaser.radius < 0) {
chaser.y = chaser.radius;
chaser.vy = -chaser.vy;
}
if (chaser.y + chaser.radius > HEIGHT) {
chaser.y = HEIGHT - chaser.radius;
chaser.vy = -chaser.vy;
}
}
// === Проверка столкновения ===
bool checkCollision() {
float dx = ball.x - chaser.x;
float dy = ball.y - chaser.y;
float distance = sqrt(dx * dx + dy * dy);
return distance < (ball.radius + chaser.radius);
}
// === Отрисовка шариков ===
void drawBalls() {
// Красный шарик
int bx = (int)ball.x;
int by = (int)ball.y;
for (int r = ball.radius + 2; r > ball.radius; r--) {
uint16_t color = tft.color565(
map(r - ball.radius, 0, 2, 255, 100),
map(r - ball.radius, 0, 2, 50, 0),
map(r - ball.radius, 0, 2, 50, 0)
);
tft.drawCircle(bx, by, r, color);
}
for (int r = ball.radius; r > 0; r--) {
uint16_t color = tft.color565(
map(r, 0, ball.radius, 255, 200),
map(r, 0, ball.radius, 50, 30),
map(r, 0, ball.radius, 50, 30)
);
tft.drawCircle(bx, by, r, color);
}
tft.fillCircle(bx - 1, by - 1, 1, TFT_WHITE);
// Синий шарик
int cx = (int)chaser.x;
int cy = (int)chaser.y;
for (int r = chaser.radius + 2; r > chaser.radius; r--) {
uint16_t color = tft.color565(
map(r - chaser.radius, 0, 2, 50, 0),
map(r - chaser.radius, 0, 2, 50, 0),
map(r - chaser.radius, 0, 2, 255, 100)
);
tft.drawCircle(cx, cy, r, color);
}
for (int r = chaser.radius; r > 0; r--) {
uint16_t color = tft.color565(
map(r, 0, chaser.radius, 30, 0),
map(r, 0, chaser.radius, 30, 0),
map(r, 0, chaser.radius, 200, 255)
);
tft.drawCircle(cx, cy, r, color);
}
tft.fillCircle(cx - 1, cy - 1, 1, TFT_WHITE);
}
// === Отрисовка частиц ===
void drawParticles() {
for (int i = 0; i < particleCount; i++) {
if (!particles[i].active) continue;
float rad = particles[i].radius;
uint16_t originalColor = particles[i].color;
float life = particles[i].life;
uint8_t r5 = (originalColor >> 11) & 0x1F;
uint8_t g6 = (originalColor >> 5) & 0x3F;
uint8_t b5 = originalColor & 0x1F;
r5 = r5 * life;
g6 = g6 * life;
b5 = b5 * life;
uint16_t fadedColor = (r5 << 11) | (g6 << 5) | b5;
if (rad >= 0.3) {
tft.fillCircle(particles[i].x, particles[i].y, (int)(rad * 1.5), fadedColor);
} else {
tft.drawPixel(particles[i].x, particles[i].y, fadedColor);
}
if (rad > 0.2 && life > 0.15) {
uint16_t glowColor = ((r5/2) << 11) | ((g6/2) << 5) | (b5/2);
tft.drawCircle(particles[i].x, particles[i].y, (int)(rad * 2), glowColor);
}
}
}
// === Очистка старых частиц ===
void cleanupParticles() {
int writeIndex = 0;
for (int i = 0; i < particleCount; i++) {
if (particles[i].active) {
particles[writeIndex++] = particles[i];
}
}
particleCount = writeIndex;
}
// === Обновление экрана ===
void updateDisplay() {
tft.fillScreen(TFT_BLACK);
drawBalls();
drawParticles();
}
// === setup ===
void setup() {
Serial.begin(115200);
tft.init();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
pinMode(12, OUTPUT);
analogWrite(12, 255);
randomSeed(analogRead(0));
tft.setTextColor(TFT_WHITE);
tft.setTextSize(1);
tft.drawString("Catch me!", 45, 75);
delay(1000);
}
// === loop ===
void loop() {
unsigned long currentTime = millis();
updateBall();
updateChaser();
if (checkCollision() && !explosionTriggered && currentTime > explosionCooldown) {
createExplosion((int)ball.x, (int)ball.y);
explosionTriggered = true;
explosionCooldown = currentTime + COOLDOWN_TIME;
float dx = ball.x - chaser.x;
float dy = ball.y - chaser.y;
float dist = sqrt(dx * dx + dy * dy);
if (dist > 0) {
float force = 5.0;
ball.vx += (dx / dist) * force;
ball.vy += (dy / dist) * force;
chaser.vx -= (dx / dist) * force * 0.8;
chaser.vy -= (dy / dist) * force * 0.8;
}
ball.vx += (random(0, 20) - 10) / 5.0;
ball.vy += (random(0, 20) - 10) / 5.0;
}
if (explosionTriggered && particleCount == 0) {
explosionTriggered = false;
}
updateParticles();
cleanupParticles();
updateDisplay();
delay(30);
}
если эффект понравится, и что то с этим придумайте используйте)))
Да не, всё одинаково
На ленте (255,255,255) по RGB и на дисплее также. Разница в том, что один светит, а другой просвечивается ![]()
логично. ведь яркость это те несчастные пару светодиодов, припаянные под матрицей.
это цвета))) а есть еще яркость каждого пикселя…
можно менять или цвета или яркость, и создавать движение…
но за один раз можно делать что то одно))) лента функциональнее! только столько памяти не найти для нее)))
логично то логично, но вот какой инструментище был бы, если бы можно было управлять яркостью, пока что не знаю что, но по любому можно было бы создать что то красивое!)))
Это только в воображении
Есть (255,255,255), а там как хочешь, хошь оттенки, хошь яркости.
в подробности не вдавался, но вроде технология oled именно об отдельных светодиодах для каждого пикселя. короче надо читать.
Генерацию узоров например ![]()
да!))) только они мне не даются))) и хочется еще чего то новенького!
и наверное на олед дисплее))) lilik будет ли выпущена версия узоров под олед дисплей ?)))
я кажется заказчиков начинаю понимать, ты разрабам о прекрасном, или еще более прекрасном, а они не хотят строить что то новое))) ради прекрасного!))) и без еды…
кажется они все таки приелись…
это вроде слишком крутые рисунки что бы описать кодом…
а игру на html наверное не хочется))) если уж пк все равно нужен, наверное надо делать на другом языке…
как то пытался построить анимацию игры, из gif на html, и разочаровался в инструменте))) (с другой стороны на нем можно майнрафт сделать размером 4 мб!) но осваивать html точно не собираюсь…
на микроконтроллере тоже не знаю как кодом что то описать и получить хороший эффект…
че то хочется, для души, а что не знаю))) желательно это что то сделать чужими руками!)))
Это да. Лень она такая.
Алиса ай оказалась не глупой девчонкой и вариант игры мы сделали достаточно быстро. Ошибки мелкие, исправляла быстро, поясняя почему ошиблась. Оперировала терминами версии игры.
// === Подключение библиотеки для работы с адресной лентой WS2812 ===
#include <Adafruit_NeoPixel.h>
// === Назначение пинов ===
#define LED_PIN 6 // Пин, к которому подключена лента (DIN)
#define LED_COUNT 16 // Длина ленты светодиодов
#define BUTTON_PIN 7 // Пин, к которому подключена кнопка (второй контакт — на GND)
// === Механика шагов от длительности нажатия кнопки ===
#define STEPS_PER_TICK 5 // Сколько шагов огонёк делает за один «тик» (0,1 с)
#define TICK_MS 100 // Длительность одного тика в миллисекундах (0,1 с)
#define MIN_STEPS 1 // Минимальное число шагов даже при самом коротком нажатии
// === Замедление огонька ===
#define DELAY_MIN 5 // Задержка на первом шаге, мс (быстрый старт)
#define DELAY_MAX 150 // Задержка на последнем шаге, мс (почти остановка)
// === Цвет движущегося огонька (тёплый красный) ===
#define RED 25
#define GREEN 10
#define BLUE 10
// === Цвет неподвижной цели (зелёный) ===
#define TARGET_R 0
#define TARGET_G 25
#define TARGET_B 0
// === Цвет вспышки при победе (слабый жёлтый) ===
#define FLASH_R 30
#define FLASH_G 25
#define FLASH_B 0
// === Создаём объект ленты: 16 светодиодов, пин 6, формат GRB, частота 800 кГц ===
Adafruit_NeoPixel strip(
LED_COUNT, // количество светодиодов
LED_PIN, // пин данных
NEO_GRB + NEO_KHZ800 // порядок цветов и частота (стандарт для WS2812)
);
// === Глобальные переменные ===
int position = 0; // Текущая позиция подвижного огонька (от 0 до LED_COUNT-1)
int direction = 1; // Направление движения: 1 — вправо, -1 — влево
int targetPosition = 0; // Позиция неподвижной цели
void setup() {
strip.begin(); // Инициализация ленты
strip.setBrightness(255); // Яркость на максимум (0–255)
strip.show(); // Отправляем «пустой» кадр — все светодиоды выключены
// Настраиваем пин кнопки как вход с внутренней подтяжкой.
// Кнопка замыкает пин на GND, поэтому нажатое состояние — это LOW.
pinMode(BUTTON_PIN, INPUT_PULLUP);
// randomSeed() инициализирует генератор случайных чисел
// значением с аналогового пина A0 (там плавает шум, если ничего не подключено).
// Без этого random() всегда выдавал бы одну и ту же последовательность.
randomSeed(analogRead(A0));
// Генерируем первую цель
newTarget();
}
void loop() {
// --- Режим ожидания: рисуем неподвижную цель и подвижный огонёк ---
strip.clear(); // Очищаем весь буфер ленты (все светодиоды — «выкл»)
// Рисуем зелёную цель в её позиции
strip.setPixelColor(targetPosition, strip.Color(TARGET_R, TARGET_G, TARGET_B));
// Рисуем красный огонёк в текущей позиции
// Благодаря этому он остаётся гореть после остановки
strip.setPixelColor(position, strip.Color(RED, GREEN, BLUE));
strip.show(); // Отправляем кадр на ленту
// --- Ждём, пока игрок не нажмёт кнопку ---
while (digitalRead(BUTTON_PIN) == HIGH) {
delay(10); // Опрашиваем пин каждые 10 мс, чтобы не грузить процессор
}
// --- Антидребезг: короткая пауза и повторная проверка ---
delay(20); // Ждём 20 мс, чтобы дребезг контактов успел утихнуть
if (digitalRead(BUTTON_PIN) != LOW) {
// Если через 20 мс кнопка уже не нажата — это был дребезг, игнорируем
return;
}
// --- Считаем, сколько шагов нужно сделать, по длительности удержания ---
int steps = countStepsByHoldTime();
// --- Запускаем огонёк ---
if (steps > 0) {
runLightSequence(steps);
}
}
// ============================================================
// Функция: считает, сколько шагов сделать, по времени удержания
// кнопки. На каждые 100 мс — 5 шагов, минимум 5 шагов.
// ============================================================
int countStepsByHoldTime() {
// Запоминаем момент, когда кнопка была нажата
unsigned long pressStart = millis();
// Пока кнопка удерживается (состояние LOW) — просто ждём
while (digitalRead(BUTTON_PIN) == LOW) {
delay(5); // Поллинг каждые 5 мс
}
// Кнопку отпустили — вычисляем, сколько миллисекунд она была нажата
unsigned long heldMs = millis() - pressStart;
// Считаем, сколько «тиков» по 100 мс прошло
int ticks = heldMs / TICK_MS;
// Переводим тики в шаги: 1 тик = 5 шагов
int steps = ticks * STEPS_PER_TICK;
// Если нажали совсем коротко (меньше 100 мс) — даём минимум
if (steps < MIN_STEPS) {
steps = MIN_STEPS;
}
return steps; // Возвращаем вычисленное число шагов
}
// ============================================================
// Функция: запускает огонёк на заданное число шагов с замедлением.
// После остановки проверяет, попал ли огонёк на цель.
// ============================================================
void runLightSequence(int steps) {
for (int step = 0; step < steps; step++) {
strip.clear(); // Очищаем буфер перед каждым кадром
// Рисуем цель (она видна на протяжении всего движения)
strip.setPixelColor(targetPosition, strip.Color(TARGET_R, TARGET_G, TARGET_B));
// Рисуем подвижный огонёк в текущей позиции
strip.setPixelColor(position, strip.Color(RED, GREEN, BLUE));
strip.show(); // Отправляем кадр на ленту
// --- Вычисляем задержку для этого шага (квадратичное замедление) ---
// progress — доля выполнения от 0.0 (первый шаг) до 1.0 (последний шаг)
// Защита от деления на ноль: если steps == 1, считаем progress = 1.0
float progress = (steps > 1)
? (float)step / (steps - 1)
: 1.0;
// Возводим в квадрат: на первых шагах задержка растёт медленно,
// к концу — быстро (эффект потери инерции)
float eased = progress * progress;
// Линейно интерполируем между DELAY_MIN и DELAY_MAX по eased
// На первом шаге ~20 мс, на последнем ~400 мс
int currentDelay = DELAY_MIN + (int)((DELAY_MAX - DELAY_MIN) * eased);
delay(currentDelay); // Ждём вычисленную задержку
// --- Сдвигаем позицию на всех шагах, КРОМЕ последнего ---
if (step < steps - 1) {
position += direction; // Двигаем огонёк на 1 светодиод
// Отражение от правой стенки: достигли последнего светодиода
if (position >= LED_COUNT - 1) {
position = LED_COUNT - 1; // Фиксируем на краю
direction = -1; // Меняем направление налево
}
// Отражение от левой стенки: достигли первого светодиода
if (position <= 0) {
position = 0; // Фиксируем на краю
direction = 1; // Меняем направление направо
}
}
}
// --- Пауза после остановки, чтобы игрок увидел финальную позицию ---
delay(500);
// --- Проверка победы: огонёк остановился ровно на цели ---
if (position == targetPosition) {
flashYellow(); // Мигаем жёлтым 3 раза
newTarget(); // Переносим цель в новое место
}
// Если промах — ничего не делаем, позиция сохраняется.
// В loop() огонёк продолжит гореть на месте остановки.
}
// ============================================================
// Функция: мигает всей лентой слабым жёлтым 3 раза (победа).
// ============================================================
void flashYellow() {
for (int i = 0; i < 3; i++) {
// Зажигаем все светодиоды жёлтым
for (int j = 0; j < LED_COUNT; j++) {
strip.setPixelColor(j, strip.Color(FLASH_R, FLASH_G, FLASH_B));
}
strip.show(); // Отправляем кадр
delay(200); // Ждём 200 мс
strip.clear(); // Гасим все светодиоды
strip.show(); // Отправляем кадр
delay(200); // Ждём 200 мс
}
}
// ============================================================
// Функция: генерирует новую случайную цель, не совпадающую
// с текущей позицией подвижного огонька.
// ============================================================
void newTarget() {
int newPos;
do {
newPos = random(LED_COUNT); // Случайное число от 0 до LED_COUNT-1
} while (newPos == position); // Повторяем, если совпало с позицией огонька
targetPosition = newPos; // Запоминаем новую цель
}






