добрался до анализа проекта наконец)))
еще раз спс за кучу примеров кодов, хотелось бы конечно что бы любой код был к ближе к реальной работе проектов…
как например тут…
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PinPort-style — Рисование шариком на TFT 128×160</title>
<style>
* { box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1600px; margin: 0 auto; padding: 20px;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
}
h1 { text-align: center; margin-bottom: 20px; text-shadow: 2px 2px 4px rgba(0,0,0,.3); }
.main-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
gap: 20px;
background: rgba(255,255,255,.1);
backdrop-filter: blur(10px);
border-radius: 20px; padding: 20px;
box-shadow: 0 8px 32px 0 rgba(31,38,135,.37);
}
.section {
background: white; border-radius: 15px; padding: 20px;
color: #333; box-shadow: 0 4px 6px rgba(0,0,0,.1);
}
.section h2 { margin-top: 0; color: #2a5298; border-bottom: 2px solid #f0f0f0; padding-bottom: 10px; }
.file-input-label {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white; padding: 12px 24px; border-radius: 25px;
cursor: pointer; display: inline-block; transition: transform .3s, box-shadow .3s;
font-size: 16px; font-weight: 600; margin: 10px 0;
}
.file-input-label:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,.3); }
input[type="file"] { display: none; }
.filename { font-size: 14px; color: #666; margin: 10px 0; word-break: break-all; }
.image-preview { background: #f5f5f5; border-radius: 10px; padding: 15px; margin-top: 15px; }
.image-container { display: flex; flex-direction: column; align-items: center; gap: 10px; min-height: 150px; justify-content: center; }
.image-container img { max-width: 100%; max-height: 180px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,.2); }
#placeholder { padding: 40px; color: #999; font-style: italic; }
.controls { margin-top: 20px; display: flex; flex-direction: column; gap: 15px; }
.control-group { display: flex; flex-direction: column; gap: 5px; }
.control-row { display: flex; gap: 10px; align-items: center; }
label { font-weight: 600; color: #555; }
input[type="range"] { flex: 1; height: 5px; background: linear-gradient(90deg, #1e3c72 0%, #2a5298 100%); border-radius: 5px; -webkit-appearance: none; }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; background: white; border: 2px solid #2a5298; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,.2); }
input[type="number"] { width: 70px; padding: 5px; border: 1px solid #dee2e6; border-radius: 5px; text-align: center; }
select { padding: 8px; border: 1px solid #dee2e6; border-radius: 8px; font-size: 15px; background: white; cursor: pointer; }
.button {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white; padding: 12px 24px; border: none; border-radius: 25px;
cursor: pointer; font-size: 16px; font-weight: 600;
transition: transform .3s, box-shadow .3s; width: 100%;
}
.button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,.3); }
.button:disabled { opacity: .5; cursor: not-allowed; }
.button.secondary { background: #6c757d; }
.preview-container {
background: #f8f9fa; border-radius: 8px; padding: 15px;
max-height: 240px; overflow-y: auto;
font-family: 'Courier New', monospace; font-size: 12px;
border: 1px solid #dee2e6; white-space: pre;
}
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 10px; padding: 10px; background: #e9ecef; border-radius: 5px; font-weight: 600; text-align: center; }
canvas { max-width: 100%; border: 2px solid #dee2e6; border-radius: 8px; background: white; display: block; margin: 0 auto; }
.value-display { display: inline-block; background: #2a5298; color: white; padding: 2px 10px; border-radius: 15px; font-size: 14px; margin-left: 10px; }
.warning { color: #dc3545; font-size: 14px; margin-top: 5px; padding: 10px; background: #ffe6e6; border-radius: 5px; text-align: center; }
.info { color: #28a745; font-size: 14px; margin-top: 5px; padding: 10px; background: #e6ffe6; border-radius: 5px; text-align: center; }
.dimensions { background: #f0f0f0; padding: 10px; border-radius: 5px; margin-top: 10px; text-align: center; }
.code-block {
background: #1e1e1e; color: #d4d4d4; border-radius: 8px;
padding: 15px; font-family: 'Courier New', monospace; font-size: 12px;
overflow-x: auto; max-height: 420px; white-space: pre;
}
.screen-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.badge { background: #2a5298; color: white; padding: 4px 12px; border-radius: 12px; font-size: 13px; }
.badge.green { background: #28a745; }
.badge.orange { background: #fd7e14; }
.progress { width: 100%; height: 8px; background: #e9ecef; border-radius: 4px; overflow: hidden; margin-top: 10px; }
.progress > div { height: 100%; width: 0%; background: linear-gradient(90deg,#1e3c72,#2a5298); transition: width .1s; }
.small { font-size: 12px; color: #666; }
.flex-row { display: flex; gap: 10px; flex-wrap: wrap; }
.checkbox-row { display: flex; align-items: center; gap: 8px; padding: 10px; background: #f0f4ff; border-radius: 8px; }
.checkbox-row input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; }
.checkbox-row label { margin: 0; cursor: pointer; font-weight: 500; color: #333; }
.hint { background: #fff8e1; border-left: 4px solid #ffc107; padding: 10px 12px; border-radius: 5px; font-size: 13px; color: #6d4c00; margin-top: 10px; }
</style>
</head>
<body>
<h1>🎯 PinPort-style — Рисование шариком на TFT 128×160</h1>
<div class="main-container">
<!-- 1. Загрузка -->
<div class="section">
<h2>1. Загрузите рисунок или trace.txt</h2>
<div class="upload-container" style="text-align:center;">
<label for="fileInput" class="file-input-label">📁 Выбрать изображение</label>
<input type="file" id="fileInput" accept="image/*">
<div class="filename" id="fileName">Файл не выбран</div>
<div class="image-preview">
<h3 style="margin:5px 0;">Исходный рисунок</h3>
<div class="image-container">
<img id="originalImage" src="#" alt="Исходный рисунок" style="display:none;">
<div id="placeholder">👆 Загрузите рисунок</div>
</div>
</div>
</div>
<div class="controls">
<div class="control-group">
<label>Порог яркости: <span class="value-display" id="thresholdValue">128</span></label>
<input type="range" id="threshold" min="0" max="255" value="128" step="1">
</div>
<div class="control-group">
<label>Толщина линии: <span class="value-display" id="thicknessValue">1</span></label>
<input type="range" id="thickness" min="1" max="3" value="1" step="1">
</div>
<div class="control-group">
<label>Размер выходного изображения:</label>
<div class="control-row"><span>Ширина:</span><input type="number" id="outputWidth" min="10" max="500" value="128"><span>px</span></div>
<div class="control-row" style="margin-top:5px;"><span>Высота:</span><input type="number" id="outputHeight" min="10" max="500" value="160"><span>px</span></div>
<div class="control-row" style="margin-top:10px;"><span>Отступ от края:</span><input type="number" id="margin" min="0" max="50" value="5"><span>px</span></div>
<div class="dimensions" id="dimensionsDisplay">Исходный размер: 128x160 → Целевой: 128x160</div>
</div>
<button id="processBtn" class="button" disabled>🔄 Создать путь из картинки</button>
</div>
<!-- Загрузка trace.txt -->
<div style="margin-top:20px; padding-top:15px; border-top:2px dashed #dee2e6;">
<h3 style="margin:0 0 10px; color:#2a5298;">Или загрузите готовый trace.txt</h3>
<label for="traceFileInput" class="file-input-label">📄 Выбрать trace.txt</label>
<input type="file" id="traceFileInput" accept=".txt,text/plain">
<div class="filename" id="traceFileName">Файл не выбран</div>
<div class="small" style="margin-top:5px;">Формат: строки вида <code>x,y</code>. Комментарии с <code>;</code> игнорируются.</div>
<div class="checkbox-row" style="margin-top:10px;">
<input type="checkbox" id="mergeLine">
<label for="mergeLine">Склеить в одну линию (сортировка по ближайшему соседу + удаление дублей)</label>
</div>
<div class="small" style="margin-top:5px; color:#666;">
Если выключено — шарик едет по точкам в том порядке, как в файле (как в Arduino-скетче).
Если включено — точки пересортируются в непрерывную линию, как при генерации из картинки.
</div>
</div>
<div id="warning" class="warning" style="display:none;"></div>
<div id="info" class="info" style="display:none;"></div>
</div>
<!-- 2. Симулятор -->
<div class="section">
<h2>2. Симулятор TFT (шарик рисует след)</h2>
<div class="screen-wrap">
<div class="flex-row" style="justify-content:center;">
<span class="badge" id="stateBadge">Ожидание</span>
<span class="badge green" id="countBadge">Точек: 0</span>
<span class="badge orange" id="distBadge">Пройдено: 0 px</span>
</div>
<canvas id="tftCanvas" width="128" height="160" style="width:256px;height:320px;image-rendering:pixelated;"></canvas>
<div class="progress"><div id="progressBar"></div></div>
<div class="small" id="progressText">0 / 0</div>
<div class="flex-row" style="width:100%;">
<button id="startBtn" class="button" disabled style="flex:1;">▶ Старт</button>
<button id="stopBtn" class="button secondary" disabled style="flex:1;">⏸ Пауза</button>
<button id="resetBtn" class="button secondary" disabled style="flex:1;">⟲ Сброс</button>
</div>
<button id="showAllBtn" class="button secondary" disabled style="width:100%;">👁 Показать все точки (без анимации)</button>
<!-- Режим шарика (только для HTML) -->
<div class="control-group" style="width:100%;">
<label>Режим шарика (только в HTML):</label>
<select id="ballMode">
<option value="dot" selected>Точка (1 px) — ничего не стирает</option>
<option value="cross">Крестик (4 px) — ничего не стирает</option>
<option value="circle">Круг (как в Arduino) — затирает</option>
</select>
</div>
<div class="checkbox-row" style="width:100%;" id="noEraseRow">
<input type="checkbox" id="noErase">
<label for="noErase">Не стирать след под шариком (только для режима «Круг»)</label>
</div>
<div class="control-group" style="width:100%;" id="radiusRow">
<label>Радиус круга (только для режима «Круг»): <span class="value-display" id="radiusValue">1</span> px</label>
<input type="range" id="radius" min="1" max="6" value="1" step="1">
</div>
<div class="control-group" style="width:100%;">
<label>Скорость шарика (speed): <span class="value-display" id="speedValue">1.5</span> px/кадр</label>
<input type="range" id="speed" min="0.2" max="6" value="1.5" step="0.1">
</div>
<div class="control-group" style="width:100%;">
<label>Задержка кадра (delay): <span class="value-display" id="delayValue">10</span> мс</label>
<input type="range" id="delay" min="1" max="100" value="10" step="1">
</div>
<div class="hint" style="width:100%;">
Режим шарика влияет <b>только на отображение в HTML</b>. Код Arduino не меняется — на реальном TFT шарик остаётся таким, как в скетче (круг фоном + контуром).
Если рисунок затирается — выберите режим «Точка» или «Крестик», они ничего не стирают.
</div>
</div>
</div>
<!-- 3. Координаты + Arduino -->
<div class="section">
<h2>3. Координаты для ESP32</h2>
<div class="stats">
<span>📊 Точек: <strong id="pointCount">0</strong></span>
<span>📏 Длина: <strong id="pathLength">0</strong> пикс.</span>
<span>📐 Масштаб: <strong id="scaleFactor">1.0x</strong></span>
</div>
<div class="preview-container" id="coordinatesDisplay">; Нажмите "Создать путь" или загрузите trace.txt</div>
<button id="saveBtn" class="button" style="margin-top:15px;" disabled>💾 Скачать trace.txt</button>
<div style="margin-top:15px; background:#d4edda; padding:10px; border-radius:5px; color:#155724;">
<strong>✅ ОДНА НЕПРЕРЫВНАЯ ЛИНИЯ!</strong><br>
Координаты масштабированы под размер <span id="finalSize">128x160</span>
</div>
<h3 style="margin-top:20px; color:#2a5298;">Код Arduino (TFT_eSPI) — без изменений</h3>
<div class="code-block" id="arduinoCode"></div>
<button id="copyCodeBtn" class="button secondary" style="margin-top:10px;">📋 Копировать код</button>
</div>
</div>
<script>
/* ============================================================
ЧАСТЬ 1. ГЕНЕРАЦИЯ КООРДИНАТ ИЗ КАРТИНКИ
============================================================ */
const fileInput = document.getElementById('fileInput');
const originalImage = document.getElementById('originalImage');
const placeholder = document.getElementById('placeholder');
const fileNameDisplay = document.getElementById('fileName');
const processBtn = document.getElementById('processBtn');
const saveBtn = document.getElementById('saveBtn');
const bwCanvas = document.createElement('canvas');
const bwCtx = bwCanvas.getContext('2d');
const coordinatesDisplay = document.getElementById('coordinatesDisplay');
const pointCountSpan = document.getElementById('pointCount');
const pathLengthSpan = document.getElementById('pathLength');
const scaleFactorSpan = document.getElementById('scaleFactor');
const finalSizeSpan = document.getElementById('finalSize');
const dimensionsDisplay = document.getElementById('dimensionsDisplay');
const thresholdInput = document.getElementById('threshold');
const thresholdValue = document.getElementById('thresholdValue');
const thicknessInput = document.getElementById('thickness');
const thicknessValue = document.getElementById('thicknessValue');
const outputWidthInput = document.getElementById('outputWidth');
const outputHeightInput = document.getElementById('outputHeight');
const marginInput = document.getElementById('margin');
const warningDiv = document.getElementById('warning');
const infoDiv = document.getElementById('info');
let originalImageData = null;
let allPoints = [];
let sourceWidth = 128, sourceHeight = 160;
thresholdInput.addEventListener('input', () => {
thresholdValue.textContent = thresholdInput.value;
if (originalImageData) updateBWPreview();
});
thicknessInput.addEventListener('input', () => { thicknessValue.textContent = thicknessInput.value; });
function updateDimensions() {
const tw = parseInt(outputWidthInput.value) || 128;
const th = parseInt(outputHeightInput.value) || 160;
const m = parseInt(marginInput.value) || 0;
dimensionsDisplay.textContent = `Исходный размер: ${sourceWidth}x${sourceHeight} → Целевой: ${tw}x${th} (отступ: ${m}px)`;
finalSizeSpan.textContent = `${tw}x${th}`;
}
outputWidthInput.addEventListener('input', updateDimensions);
outputHeightInput.addEventListener('input', updateDimensions);
marginInput.addEventListener('input', updateDimensions);
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) { resetToInitial(); return; }
fileNameDisplay.textContent = `Выбран файл: ${file.name}`;
const reader = new FileReader();
reader.onload = function(ev) {
const img = new Image();
img.onload = function() {
sourceWidth = img.width; sourceHeight = img.height;
bwCanvas.width = sourceWidth; bwCanvas.height = sourceHeight;
bwCtx.drawImage(img, 0, 0, sourceWidth, sourceHeight);
originalImageData = bwCtx.getImageData(0, 0, sourceWidth, sourceHeight);
originalImage.src = ev.target.result;
originalImage.style.display = 'block';
placeholder.style.display = 'none';
updateBWPreview();
processBtn.disabled = false;
updateDimensions();
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
});
function updateBWPreview() {
if (!originalImageData) return;
const threshold = parseInt(thresholdInput.value);
const bwData = convertToBW(originalImageData, threshold);
bwCtx.putImageData(bwData, 0, 0);
}
function convertToBW(imageData, threshold) {
const data = imageData.data;
const bwData = new ImageData(imageData.width, imageData.height);
for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i+1], b = data[i+2];
const brightness = 0.299*r + 0.587*g + 0.114*b;
const v = brightness > threshold ? 255 : 0;
bwData.data[i] = v; bwData.data[i+1] = v; bwData.data[i+2] = v; bwData.data[i+3] = 255;
}
return bwData;
}
function skeletonize(imageData, thickness) {
const width = imageData.width, height = imageData.height;
const data = imageData.data;
const result = new Array(width * height).fill(255);
if (thickness === 1) {
for (let i = 0; i < data.length; i += 4) if (data[i] === 0) result[i/4] = 0;
} else {
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const idx = y * width + x;
if (data[idx * 4] === 0) {
let isEdge = false;
if (data[((y-1)*width + x)*4] === 255 || data[((y+1)*width + x)*4] === 255 ||
data[(y*width + (x-1))*4] === 255 || data[(y*width + (x+1))*4] === 255) isEdge = true;
if (isEdge) result[idx] = 0;
}
}
}
}
return result;
}
function collectPoints(skeleton, width, height) {
const points = [];
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (skeleton[y * width + x] === 0) points.push({x, y});
}
}
if (points.length === 0) return [];
const result = [];
const remaining = [...points];
remaining.sort((a, b) => (a.y === b.y ? a.x - b.x : a.y - b.y));
let current = remaining.shift();
result.push(current);
while (remaining.length > 0) {
let nearestIndex = 0, nearestDist = Infinity;
for (let i = 0; i < remaining.length; i++) {
const dx = remaining[i].x - current.x;
const dy = remaining[i].y - current.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < nearestDist) { nearestDist = dist; nearestIndex = i; }
}
current = remaining[nearestIndex];
result.push(current);
remaining.splice(nearestIndex, 1);
}
return result;
}
function scalePoints(points, targetWidth, targetHeight, margin) {
if (points.length === 0) return [];
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
points.forEach(p => {
minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x);
minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y);
});
if (maxX - minX < 1) maxX = minX + 1;
if (maxY - minY < 1) maxY = minY + 1;
const availW = targetWidth - 2*margin;
const availH = targetHeight - 2*margin;
const scaleX = availW / (maxX - minX);
const scaleY = availH / (maxY - minY);
const scale = Math.min(scaleX, scaleY);
const scaledW = (maxX - minX) * scale;
const scaledH = (maxY - minY) * scale;
const offsetX = margin + (availW - scaledW) / 2;
const offsetY = margin + (availH - scaledH) / 2;
return points.map(p => ({
x: Math.round(offsetX + (p.x - minX) * scale),
y: Math.round(offsetY + (p.y - minY) * scale)
}));
}
function formatPoints(points) {
if (!points || points.length === 0) return '; Нет данных';
let result = '; Путь для рисования одной линией\n';
points.forEach(p => { result += `${p.x},${p.y}\n`; });
return result;
}
function processImage() {
if (!originalImageData) return;
const threshold = parseInt(thresholdInput.value);
const thickness = parseInt(thicknessInput.value);
const targetWidth = parseInt(outputWidthInput.value) || 128;
const targetHeight = parseInt(outputHeightInput.value) || 160;
const margin = parseInt(marginInput.value) || 0;
const bwData = convertToBW(originalImageData, threshold);
const skeleton = skeletonize(bwData, thickness);
const rawPoints = collectPoints(skeleton, sourceWidth, sourceHeight);
allPoints = scalePoints(rawPoints, targetWidth, targetHeight, margin);
applyPoints(allPoints, 'Найдено точек из картинки: ');
}
function applyPoints(points, msgPrefix) {
coordinatesDisplay.textContent = formatPoints(points);
pointCountSpan.textContent = points.length;
let totalLength = 0;
for (let i = 1; i < points.length; i++) {
const dx = points[i].x - points[i-1].x;
const dy = points[i].y - points[i-1].y;
totalLength += Math.sqrt(dx*dx + dy*dy);
}
pathLengthSpan.textContent = Math.round(totalLength);
scaleFactorSpan.textContent = '1.0x';
finalSizeSpan.textContent = `${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}`;
if (points.length === 0) {
showWarning('Линии не найдены!');
saveBtn.disabled = true;
startBtn.disabled = true;
stopBtn.disabled = true;
resetBtn.disabled = true;
showAllBtn.disabled = true;
} else {
showInfo(msgPrefix + points.length);
saveBtn.disabled = false;
resetSimulation();
startBtn.disabled = false;
stopBtn.disabled = true;
resetBtn.disabled = false;
showAllBtn.disabled = false;
}
}
function showWarning(m) { warningDiv.textContent = m; warningDiv.style.display = 'block'; infoDiv.style.display = 'none'; }
function showInfo(m) { infoDiv.textContent = m; infoDiv.style.display = 'block'; warningDiv.style.display = 'none'; }
function resetToInitial() {
fileNameDisplay.textContent = 'Файл не выбран';
originalImage.style.display = 'none';
placeholder.style.display = 'block';
processBtn.disabled = true;
saveBtn.disabled = true;
startBtn.disabled = true;
stopBtn.disabled = true;
resetBtn.disabled = true;
showAllBtn.disabled = true;
coordinatesDisplay.textContent = '; Нажмите "Создать путь" или загрузите trace.txt';
pointCountSpan.textContent = '0';
pathLengthSpan.textContent = '0';
scaleFactorSpan.textContent = '1.0x';
originalImageData = null;
allPoints = [];
resetSimulation();
}
saveBtn.addEventListener('click', function() {
if (!allPoints || allPoints.length === 0) return;
const blob = new Blob([formatPoints(allPoints)], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.download = 'trace.txt'; a.href = url;
document.body.appendChild(a); a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
processBtn.addEventListener('click', processImage);
/* ============================================================
ЧАСТЬ 2. ЗАГРУЗКА ГОТОВОГО trace.txt + склейка
============================================================ */
const traceFileInput = document.getElementById('traceFileInput');
const traceFileName = document.getElementById('traceFileName');
const mergeLineCheckbox = document.getElementById('mergeLine');
function dedupPoints(points, minDist = 1.01) {
if (points.length === 0) return [];
const out = [points[0]];
for (let i = 1; i < points.length; i++) {
const p = points[i];
const prev = out[out.length - 1];
const dx = p.x - prev.x, dy = p.y - prev.y;
if (Math.sqrt(dx*dx + dy*dy) > minDist) out.push(p);
}
return out;
}
function sortByNearestNeighbor(points) {
if (points.length === 0) return [];
const remaining = [...points];
remaining.sort((a, b) => (a.y === b.y ? a.x - b.x : a.y - b.y));
const result = [];
let current = remaining.shift();
result.push(current);
while (remaining.length > 0) {
let nearestIndex = 0, nearestDist = Infinity;
for (let i = 0; i < remaining.length; i++) {
const dx = remaining[i].x - current.x;
const dy = remaining[i].y - current.y;
const d = dx*dx + dy*dy;
if (d < nearestDist) { nearestDist = d; nearestIndex = i; }
}
current = remaining[nearestIndex];
result.push(current);
remaining.splice(nearestIndex, 1);
}
return result;
}
function parseTraceText(text) {
const pts = [];
text.split(/\r?\n/).forEach(line => {
line = line.trim();
if (!line || line.startsWith(';')) return;
const m = line.match(/^(-?\d+)\s*,\s*(-?\d+)$/);
if (m) pts.push({ x: parseInt(m[1], 10), y: parseInt(m[2], 10) });
});
return pts;
}
function loadTraceFromText(text) {
let pts = parseTraceText(text);
if (pts.length === 0) {
showWarning('В файле не найдено координат вида x,y');
return;
}
pts = pts.map(p => ({
x: Math.max(0, Math.min(DISPLAY_WIDTH - 1, p.x)),
y: Math.max(0, Math.min(DISPLAY_HEIGHT - 1, p.y))
}));
let msg = 'Загружено точек из trace.txt: ';
if (mergeLineCheckbox.checked) {
const before = pts.length;
pts = dedupPoints(pts, 1.01);
pts = sortByNearestNeighbor(pts);
msg = `Склеено в одну линию: было ${before}, стало `;
}
allPoints = pts;
applyPoints(allPoints, msg);
}
traceFileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
traceFileName.textContent = `Выбран файл: ${file.name}`;
const reader = new FileReader();
reader.onload = function(ev) { loadTraceFromText(ev.target.result); };
reader.readAsText(file);
});
mergeLineCheckbox.addEventListener('change', function() {
if (traceFileInput.files && traceFileInput.files[0]) {
const reader = new FileReader();
reader.onload = function(ev) { loadTraceFromText(ev.target.result); };
reader.readAsText(traceFileInput.files[0]);
}
});
/* ============================================================
ЧАСТЬ 3. СИМУЛЯТОР TFT (шарик + след)
============================================================ */
const DISPLAY_WIDTH = 128;
const DISPLAY_HEIGHT = 160;
const TFT_BG = '#FFFF00';
const TRACE_COLOR = '#FF0000';
const DARK_YELLOW_CSS = '#808000';
const tftCanvas = document.getElementById('tftCanvas');
const tftCtx = tftCanvas.getContext('2d');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const resetBtn = document.getElementById('resetBtn');
const showAllBtn = document.getElementById('showAllBtn');
const speedInput = document.getElementById('speed');
const speedValue = document.getElementById('speedValue');
const delayInput = document.getElementById('delay');
const delayValue = document.getElementById('delayValue');
const radiusInput = document.getElementById('radius');
const radiusValue = document.getElementById('radiusValue');
const ballModeSelect = document.getElementById('ballMode');
const noEraseCheckbox = document.getElementById('noErase');
const noEraseRow = document.getElementById('noEraseRow');
const radiusRow = document.getElementById('radiusRow');
const stateBadge = document.getElementById('stateBadge');
const countBadge = document.getElementById('countBadge');
const distBadge = document.getElementById('distBadge');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
speedInput.addEventListener('input', () => speedValue.textContent = parseFloat(speedInput.value).toFixed(1));
delayInput.addEventListener('input', () => delayValue.textContent = delayInput.value);
radiusInput.addEventListener('input', () => {
radiusValue.textContent = radiusInput.value;
if (sim.running) drawMovingCircle(sim.currentX, sim.currentY);
});
function updateBallModeUI() {
const mode = ballModeSelect.value;
noEraseRow.style.display = (mode === 'circle') ? 'flex' : 'none';
radiusRow.style.display = (mode === 'circle') ? 'flex' : 'none';
}
ballModeSelect.addEventListener('change', updateBallModeUI);
noEraseCheckbox.addEventListener('change', () => {
if (sim.running) drawMovingCircle(sim.currentX, sim.currentY);
});
updateBallModeUI();
const sim = {
points: [],
index: 0,
posX: 0, posY: 0,
currentX: 0, currentY: 0,
targetX: 0, targetY: 0,
running: false,
timer: null,
drawnLength: 0
};
function initSimDisplay() {
tftCtx.fillStyle = TFT_BG;
tftCtx.fillRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
}
function drawPixel(x, y, color) {
if (x < 0 || y < 0 || x >= DISPLAY_WIDTH || y >= DISPLAY_HEIGHT) return;
tftCtx.fillStyle = color;
tftCtx.fillRect(x, y, 1, 1);
}
function drawLine(x0, y0, x1, y1, color) {
x0 = Math.round(x0); y0 = Math.round(y0); x1 = Math.round(x1); y1 = Math.round(y1);
let dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
let dy = -Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
let err = dx + dy;
while (true) {
drawPixel(x0, y0, color);
if (x0 === x1 && y0 === y1) break;
const e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
function fillCircle(cx, cy, r, color) {
cx = Math.round(cx); cy = Math.round(cy);
tftCtx.fillStyle = color;
for (let y = -r; y <= r; y++) {
for (let x = -r; x <= r; x++) {
if (x*x + y*y <= r*r) {
const px = cx + x, py = cy + y;
if (px >= 0 && py >= 0 && px < DISPLAY_WIDTH && py < DISPLAY_HEIGHT) {
tftCtx.fillRect(px, py, 1, 1);
}
}
}
}
}
function drawCircle(cx, cy, r, color) {
cx = Math.round(cx); cy = Math.round(cy);
tftCtx.strokeStyle = color;
tftCtx.lineWidth = 1;
tftCtx.beginPath();
tftCtx.arc(cx + 0.5, cy + 0.5, r, 0, Math.PI * 2);
tftCtx.stroke();
}
function drawTracePoint(x, y) { drawPixel(x, y, TRACE_COLOR); }
// Шарик: три режима, влияет только на HTML
function drawMovingCircle(x, y) {
const mode = ballModeSelect.value;
const r = parseInt(radiusInput.value);
if (mode === 'dot') {
// Одна точка цвета контура, ничего не стирает
drawPixel(x, y, DARK_YELLOW_CSS);
} else if (mode === 'cross') {
// Крестик из 4 пикселей, ничего не стирает
drawPixel(x - 1, y, DARK_YELLOW_CSS);
drawPixel(x + 1, y, DARK_YELLOW_CSS);
drawPixel(x, y - 1, DARK_YELLOW_CSS);
drawPixel(x, y + 1, DARK_YELLOW_CSS);
} else {
// Круг как в Arduino: заливка фоном + контур.
// Если «Не стирать след» — заливку не делаем, только контур.
if (!noEraseCheckbox.checked) {
fillCircle(x, y, r, TFT_BG);
}
drawCircle(x, y, r, DARK_YELLOW_CSS);
}
}
function resetSimulation() {
if (sim.timer) { clearTimeout(sim.timer); sim.timer = null; }
sim.points = allPoints.map(p => ({x: p.x, y: p.y}));
sim.index = 0;
sim.posX = 0; sim.posY = 0;
sim.currentX = 0; sim.currentY = 0;
sim.targetX = 0; sim.targetY = 0;
sim.running = false;
sim.drawnLength = 0;
initSimDisplay();
stateBadge.textContent = 'Ожидание';
countBadge.textContent = 'Точек: 0';
distBadge.textContent = 'Пройдено: 0 px';
progressBar.style.width = '0%';
progressText.textContent = `0 / ${sim.points.length}`;
if (sim.points.length > 0) {
sim.targetX = sim.points[0].x;
sim.targetY = sim.points[0].y;
sim.posX = sim.targetX; sim.posY = sim.targetY;
sim.currentX = sim.targetX; sim.currentY = sim.targetY;
drawTracePoint(sim.currentX, sim.currentY);
drawMovingCircle(sim.currentX, sim.currentY);
sim.index = 1;
countBadge.textContent = `Точек: 1`;
}
}
function startSimulation() {
if (sim.points.length === 0) return;
if (sim.running) return;
if (sim.index >= sim.points.length) resetSimulation();
sim.running = true;
stateBadge.textContent = 'Рисует...';
stopBtn.disabled = false;
startBtn.disabled = true;
sim.timer = setTimeout(step, parseInt(delayInput.value));
}
function stopSimulation() {
sim.running = false;
if (sim.timer) { clearTimeout(sim.timer); sim.timer = null; }
stateBadge.textContent = 'Пауза';
startBtn.disabled = false;
stopBtn.disabled = true;
}
function step() {
if (!sim.running) return;
const speed = parseFloat(speedInput.value);
let guard = 0;
while (sim.index < sim.points.length && guard++ < 100000) {
const target = sim.points[sim.index];
sim.targetX = target.x;
sim.targetY = target.y;
const dx = sim.targetX - sim.posX;
const dy = sim.targetY - sim.posY;
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance < speed) {
sim.posX = sim.targetX;
sim.posY = sim.targetY;
drawTracePoint(Math.round(sim.posX), Math.round(sim.posY));
sim.currentX = Math.round(sim.posX);
sim.currentY = Math.round(sim.posY);
drawMovingCircle(sim.currentX, sim.currentY);
sim.index++;
updateStats();
} else {
const prevX = sim.currentX, prevY = sim.currentY;
sim.posX += (dx / distance) * speed;
sim.posY += (dy / distance) * speed;
const nx = Math.round(sim.posX), ny = Math.round(sim.posY);
if (nx !== sim.currentX || ny !== sim.currentY) {
drawLine(prevX, prevY, nx, ny, TRACE_COLOR);
sim.currentX = nx; sim.currentY = ny;
drawMovingCircle(sim.currentX, sim.currentY);
const ddx = nx - prevX, ddy = ny - prevY;
sim.drawnLength += Math.sqrt(ddx*ddx + ddy*ddy);
distBadge.textContent = `Пройдено: ${Math.round(sim.drawnLength)} px`;
}
break;
}
}
if (sim.index >= sim.points.length) {
sim.running = false;
stateBadge.textContent = 'Готово!';
startBtn.disabled = false;
stopBtn.disabled = true;
progressBar.style.width = '100%';
progressText.textContent = `${sim.points.length} / ${sim.points.length}`;
return;
}
updateProgress();
sim.timer = setTimeout(step, parseInt(delayInput.value));
}
function updateStats() {
countBadge.textContent = `Точек: ${Math.min(sim.index, sim.points.length)}`;
}
function updateProgress() {
const total = sim.points.length;
const done = Math.min(sim.index, total);
const pct = total > 0 ? (done / total) * 100 : 0;
progressBar.style.width = pct.toFixed(1) + '%';
progressText.textContent = `${done} / ${total}`;
}
function showAllPoints() {
if (sim.timer) { clearTimeout(sim.timer); sim.timer = null; }
sim.running = false;
initSimDisplay();
stateBadge.textContent = 'Все точки';
startBtn.disabled = false;
stopBtn.disabled = true;
let totalLength = 0;
let prev = null;
for (let i = 0; i < allPoints.length; i++) {
const p = allPoints[i];
drawPixel(p.x, p.y, TRACE_COLOR);
if (prev) {
const dx = p.x - prev.x, dy = p.y - prev.y;
totalLength += Math.sqrt(dx*dx + dy*dy);
}
prev = p;
}
sim.drawnLength = totalLength;
distBadge.textContent = `Пройдено: ${Math.round(totalLength)} px`;
progressBar.style.width = '100%';
progressText.textContent = `${allPoints.length} / ${allPoints.length}`;
if (allPoints.length > 0) {
drawCircle(allPoints[0].x, allPoints[0].y, 3, '#00FF00');
}
}
startBtn.addEventListener('click', startSimulation);
stopBtn.addEventListener('click', stopSimulation);
resetBtn.addEventListener('click', () => { resetSimulation(); stateBadge.textContent = 'Ожидание'; });
showAllBtn.addEventListener('click', showAllPoints);
/* ============================================================
ЧАСТЬ 4. Код Arduino — БЕЗ ИЗМЕНЕНИЙ
============================================================ */
const ARDUINO_CODE = `#include <TFT_eSPI.h>
#include <SPI.h>
#include <SD.h>
TFT_eSPI tft = TFT_eSPI();
// Пины SD-карты (для ESP32)
#define SD_CS 5
#define SD_SCLK 18
#define SD_MOSI 23
#define SD_MISO 19
#define LIGHT_YELLOW TFT_YELLOW
#define DARK_YELLOW 0x8400
#define TRACE_COLOR TFT_RED
#define CIRCLE_RADIUS 2
#define DISPLAY_WIDTH 128
#define DISPLAY_HEIGHT 160
int currentX = 0, currentY = 0;
int targetX = 0, targetY = 0;
float posX = 0, posY = 0;
float speed = 1.5;
File dataFile;
bool hasMorePoints = true;
int pointCount = 0;
#define SOURCE_MIN 0
#define SOURCE_MAX 168
void setup() {
Serial.begin(115200);
tft.init();
tft.setRotation(2);
tft.fillScreen(LIGHT_YELLOW);
pinMode(TFT_BL, OUTPUT);
digitalWrite(TFT_BL, HIGH);
SPI.begin(SD_SCLK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS)) { Serial.println("SD card failed!"); showError("SD Error"); return; }
dataFile = SD.open("/trace.txt", FILE_READ);
if (!dataFile) { Serial.println("File not found!"); showError("No trace.txt"); return; }
dataFile.readStringUntil('\\n');
if (readNextPoint()) {
posX = targetX; posY = targetY;
currentX = targetX; currentY = targetY;
drawTracePoint(currentX, currentY);
}
}
void centerCoordinates(int &x, int &y) {
float scaleX = (float)(DISPLAY_WIDTH - 20) / SOURCE_MAX;
float scaleY = (float)(DISPLAY_HEIGHT - 20) / SOURCE_MAX;
float scale = min(scaleX, scaleY);
int scaledX = x * scale, scaledY = y * scale;
int offsetX = (DISPLAY_WIDTH - (SOURCE_MAX * scale)) / 2;
int offsetY = (DISPLAY_HEIGHT - (SOURCE_MAX * scale)) / 2;
x = scaledX + offsetX; y = scaledY + offsetY;
}
bool readNextPoint() {
if (!dataFile || !dataFile.available()) { hasMorePoints = false; return false; }
String line = dataFile.readStringUntil('\\n');
line.trim();
if (line.length() == 0) return readNextPoint();
int commaIndex = line.indexOf(',');
if (commaIndex == -1) return readNextPoint();
targetX = line.substring(0, commaIndex).toInt();
targetY = line.substring(commaIndex + 1).toInt();
centerCoordinates(targetX, targetY);
targetX = constrain(targetX, 0, DISPLAY_WIDTH - 1);
targetY = constrain(targetY, 0, DISPLAY_HEIGHT - 1);
pointCount++;
return true;
}
void drawTracePoint(int x, int y) { tft.drawPixel(x, y, TRACE_COLOR); }
void drawMovingCircle(int x, int y) {
tft.fillCircle(x, y, CIRCLE_RADIUS, LIGHT_YELLOW);
tft.drawCircle(x, y, CIRCLE_RADIUS, DARK_YELLOW);
}
void showError(String msg) {
tft.fillScreen(TFT_RED);
tft.setTextColor(TFT_WHITE);
tft.setTextSize(2);
tft.setCursor(20, 50);
tft.print(msg);
while(1);
}
void loop() {
if (!hasMorePoints) {
for (int i = 0; i < 5; i++) {
tft.fillCircle(currentX, currentY, CIRCLE_RADIUS + 2, TFT_WHITE);
delay(200);
drawMovingCircle(currentX, currentY);
delay(200);
}
while(1) delay(1000);
}
float dx = targetX - posX;
float dy = targetY - posY;
float distance = sqrt(dx*dx + dy*dy);
if (distance < speed) {
posX = targetX; posY = targetY;
drawTracePoint((int)posX, (int)posY);
currentX = (int)posX; currentY = (int)posY;
drawMovingCircle(currentX, currentY);
readNextPoint();
} else {
posX += (dx / distance) * speed;
posY += (dy / distance) * speed;
int newX = (int)posX, newY = (int)posY;
if (newX != currentX || newY != currentY) {
tft.drawLine(currentX, currentY, newX, newY, TRACE_COLOR);
currentX = newX; currentY = newY;
drawMovingCircle(currentX, currentY);
}
}
delay(10);
}`;
function updateArduinoCode() {
document.getElementById('arduinoCode').textContent = ARDUINO_CODE;
}
updateArduinoCode();
document.getElementById('copyCodeBtn').addEventListener('click', () => {
navigator.clipboard.writeText(ARDUINO_CODE).then(() => {
const btn = document.getElementById('copyCodeBtn');
const old = btn.textContent;
btn.textContent = '✅ Скопировано!';
setTimeout(() => btn.textContent = old, 1500);
});
});
/* Инициализация */
resetToInitial();
updateDimensions();
</script>
</body>
</html>
хотел показать реальную работу, и сам уперся в то что это скорее симуляция)))
хотелось бы вообще что бы все учитывалось, но и так офигительно! еще и схема есть у вас, и куча всего…