Это всё фальсификация! Реальный Бабос был убит ёб.. Арасакой корпоратами!
ну так мнеж тогда 13 было!
теперь я уже видео сжимаю, на своем кодеке, по байту трачу на 2 пикселя, а все лишнее обрезаю, вот версия html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Захват экрана — RLE видео</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { padding: 20px; font-family: Arial, sans-serif; background: #1a1a2e; color: white; }
h1 { text-align: center; margin-bottom: 20px; color: #4ecdc4; }
.controls { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-bottom: 20px; }
button { padding: 10px 20px; font-size: 14px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; transition: 0.3s; }
button:hover { transform: scale(1.05); }
button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.btn-start { background: #4ecdc4; color: #1a1a2e; }
.btn-stop { background: #ff6b6b; color: white; }
.btn-capture { background: #feca57; color: #1a1a2e; }
.btn-record { background: #ff4757; color: white; }
.btn-record.recording { background: #ff6b81; }
.btn-save { background: #48dbfb; color: #1a1a2e; }
.btn-clear { background: #ff6b6b; color: white; }
.container { display: flex; gap: 20px; flex-wrap: wrap; justify-content: center; margin-bottom: 20px; }
.preview { background: rgba(255,255,255,0.05); border-radius: 10px; padding: 15px; border: 1px solid rgba(255,255,255,0.1); flex: 1; min-width: 300px; max-width: 600px; position: relative; }
.preview h3 { text-align: center; color: #aaa; margin-bottom: 10px; }
canvas { display: block; width: 100%; border-radius: 6px; background: #000; aspect-ratio: 16/9; }
.info { margin: 20px auto; padding: 15px; background: rgba(255,255,255,0.05); border-radius: 8px; max-width: 800px; text-align: center; color: #aaa; }
.stats { display: inline-block; padding: 8px 16px; background: rgba(78,205,196,0.2); border-radius: 6px; color: #4ecdc4; font-family: monospace; }
.fps-counter { position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: #4ecdc4; padding: 4px 10px; border-radius: 4px; font-size: 12px; font-family: monospace; }
.record-indicator { position: absolute; top: 10px; left: 10px; background: rgba(255,0,0,0.8); color: white; padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: bold; display: none; }
.record-indicator.active { display: block; animation: blink 1s infinite; }
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
.video-controls { background: rgba(255,255,255,0.05); border-radius: 10px; padding: 20px; max-width: 800px; margin: 20px auto; border: 1px solid rgba(255,255,255,0.1); }
.video-controls h3 { color: #aaa; margin-bottom: 15px; text-align: center; }
.slider-container { display: flex; align-items: center; gap: 15px; }
input[type="range"] { flex: 1; -webkit-appearance: none; height: 6px; border-radius: 3px; background: #4ecdc4; outline: none; }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: #4ecdc4; cursor: pointer; border: 2px solid white; }
.time-display { font-family: monospace; color: #4ecdc4; min-width: 100px; text-align: center; }
.video-stats { display: flex; justify-content: space-around; margin-top: 15px; color: #aaa; font-size: 13px; flex-wrap: wrap; gap: 10px; }
.video-stats span { background: rgba(255,255,255,0.05); padding: 4px 12px; border-radius: 4px; }
.video-stats strong { color: #4ecdc4; }
.video-buttons { display: flex; gap: 10px; justify-content: center; margin-top: 10px; flex-wrap: wrap; }
.fps-control { display: flex; align-items: center; gap: 15px; justify-content: center; margin-top: 15px; padding: 10px; background: rgba(255,255,255,0.03); border-radius: 8px; border: 1px solid rgba(255,255,255,0.05); flex-wrap: wrap; }
.fps-control label { color: #aaa; font-size: 14px; }
.fps-control input[type="range"] { width: 150px; }
.fps-control .fps-value { color: #4ecdc4; font-weight: bold; font-size: 18px; min-width: 40px; text-align: center; }
textarea { width: 100%; max-width: 800px; height: 80px; background: #0a0a1e; color: #4ecdc4; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; padding: 10px; font-family: monospace; font-size: 12px; resize: vertical; margin: 10px auto; display: block; }
.gallery { display: flex; flex-wrap: wrap; gap: 15px; justify-content: center; margin: 20px 0; }
.gallery-item { background: rgba(255,255,255,0.05); border-radius: 8px; padding: 10px; border: 1px solid rgba(255,255,255,0.1); cursor: pointer; transition: 0.3s; max-width: 180px; }
.gallery-item:hover { transform: scale(1.05); border-color: #4ecdc4; }
.gallery-item canvas { width: 160px; height: 120px; border-radius: 4px; }
.gallery-item .label { text-align: center; font-size: 11px; color: #aaa; margin-top: 5px; }
.file-input-wrapper { position: relative; display: inline-block; }
.file-input-wrapper input[type="file"] { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; }
.size-compare { background: rgba(255,255,255,0.03); border-radius: 8px; padding: 10px; margin-top: 10px; font-size: 13px; color: #4ecdc4; border: 1px solid rgba(78,205,196,0.2); }
.status-bar { display: flex; gap: 20px; justify-content: center; flex-wrap: wrap; margin-top: 10px; padding: 8px; background: rgba(255,255,255,0.03); border-radius: 6px; }
.status-item { display: flex; align-items: center; gap: 5px; font-size: 13px; color: #aaa; }
.status-item .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.dot.green { background: #4ecdc4; }
.dot.red { background: #ff4757; }
.dot.yellow { background: #feca57; }
</style>
</head>
<body>
<h1>🎥 Захват экрана — RLE видео</h1>
<div class="controls">
<button class="btn-start" id="startBtn">▶ Начать захват</button>
<button class="btn-stop" id="stopBtn" disabled>⏹ Остановить</button>
<button class="btn-capture" id="captureBtn" disabled>📷 Снимок</button>
<button class="btn-record" id="recordBtn" disabled>🔴 Запись</button>
<button class="btn-save" id="savePngBtn" disabled>💾 PNG</button>
<button class="btn-save" id="saveRleBtn" disabled>💾 RLE</button>
<button class="btn-clear" id="clearBtn">🗑 Очистить</button>
<div class="file-input-wrapper">
<button class="btn-save">📂 Загрузить</button>
<input type="file" id="fileInput" accept=".rlevideo">
</div>
</div>
<div class="container">
<div class="preview">
<h3>📺 Видео поток (16 цветов)</h3>
<canvas id="liveCanvas"></canvas>
<div class="fps-counter" id="fpsCounter">FPS: 0</div>
<div class="record-indicator" id="recordIndicator">● ЗАПИСЬ</div>
</div>
<div class="preview">
<h3>🖼 Последний снимок</h3>
<canvas id="snapshotCanvas"></canvas>
</div>
</div>
<div class="info">
<span class="stats" id="statsText">⏸ Остановлен</span>
<div id="captureInfo" style="margin-top: 8px; color: #feca57;"></div>
<div class="status-bar">
<span class="status-item">
<span class="dot yellow" id="sourceStatus"></span>
<span id="sourceStatusText">Источник: ожидание</span>
</span>
<span class="status-item" id="recordStatus" style="display:none;">
<span class="dot red"></span>
<span>🔴 ЗАПИСЬ</span>
</span>
</div>
</div>
<div class="video-controls">
<h3>🎬 Настройки записи</h3>
<div class="fps-control">
<label>🎯 FPS записи:</label>
<input type="range" id="fpsSlider" min="1" max="60" value="30" step="1">
<span class="fps-value" id="fpsDisplay">30</span>
<span style="color:#aaa; font-size:12px;">кадров/сек</span>
<button id="reconnectBtn" style="background:#feca57; color:#1a1a2e; padding:5px 15px;">🔄 Перезахват</button>
</div>
<div class="size-compare" id="fpsInfo">💡 30 FPS — оптимально для большинства задач</div>
</div>
<div class="video-controls">
<h3>🎬 Записанное видео</h3>
<div class="slider-container">
<span style="color:#aaa;">⏮</span>
<input type="range" id="videoSlider" min="0" max="0" value="0">
<span style="color:#aaa;">⏭</span>
<span class="time-display" id="timeDisplay">00:00 / 00:00</span>
</div>
<div class="fps-control">
<label>▶ FPS воспроизведения:</label>
<input type="range" id="playbackFpsSlider" min="1" max="60" value="30" step="1">
<span class="fps-value" id="playbackFpsDisplay">30</span>
<span style="color:#aaa; font-size:12px;">кадров/сек</span>
</div>
<div class="video-stats">
<span>📊 Кадров: <strong id="totalFrames">0</strong></span>
<span>📏 Размер: <strong id="videoSize">0</strong>×<strong id="videoHeight">0</strong></span>
<span>💾 RLE: <strong id="rleVideoSize">0 KB</strong></span>
<span>⏱ Длительность: <strong id="videoDuration">0с</strong></span>
</div>
<div class="size-compare" id="sizeCompare">💡 Бинарный формат экономит до 60% места</div>
<div class="video-buttons">
<button id="playVideoBtn" disabled>▶ Воспроизвести</button>
<button id="pauseVideoBtn" disabled>⏸ Пауза</button>
<button id="saveVideoBtn" disabled>💾 Сохранить видео</button>
</div>
</div>
<textarea id="rleOutput" placeholder="RLE данные снимка..."></textarea>
<div style="text-align:center; color:#aaa; margin:20px 0 10px 0;">📚 Галерея снимков</div>
<div class="gallery" id="gallery"></div>
<script>
// ========== НАСТРОЙКИ ==========
const COLORS = [
[0,0,0], [255,255,255], [255,0,0], [0,255,0],
[0,0,255], [255,255,0], [255,0,255], [0,255,255],
[128,0,0], [0,128,0], [0,0,128], [128,128,0],
[128,0,128], [0,128,128], [192,192,192], [128,128,128]
];
// ========== DOM ЭЛЕМЕНТЫ ==========
const liveCanvas = document.getElementById('liveCanvas');
const ctxLive = liveCanvas.getContext('2d');
const snapshotCanvas = document.getElementById('snapshotCanvas');
const ctxSnap = snapshotCanvas.getContext('2d');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const captureBtn = document.getElementById('captureBtn');
const recordBtn = document.getElementById('recordBtn');
const savePngBtn = document.getElementById('savePngBtn');
const saveRleBtn = document.getElementById('saveRleBtn');
const clearBtn = document.getElementById('clearBtn');
const fileInput = document.getElementById('fileInput');
const reconnectBtn = document.getElementById('reconnectBtn');
const statsText = document.getElementById('statsText');
const captureInfo = document.getElementById('captureInfo');
const rleOutput = document.getElementById('rleOutput');
const gallery = document.getElementById('gallery');
const fpsCounter = document.getElementById('fpsCounter');
const recordIndicator = document.getElementById('recordIndicator');
const sizeCompare = document.getElementById('sizeCompare');
const fpsInfo = document.getElementById('fpsInfo');
const sourceStatus = document.getElementById('sourceStatus');
const sourceStatusText = document.getElementById('sourceStatusText');
const recordStatus = document.getElementById('recordStatus');
const videoSlider = document.getElementById('videoSlider');
const timeDisplay = document.getElementById('timeDisplay');
const totalFrames = document.getElementById('totalFrames');
const videoSize = document.getElementById('videoSize');
const videoHeight = document.getElementById('videoHeight');
const rleVideoSize = document.getElementById('rleVideoSize');
const videoDuration = document.getElementById('videoDuration');
const playVideoBtn = document.getElementById('playVideoBtn');
const pauseVideoBtn = document.getElementById('pauseVideoBtn');
const saveVideoBtn = document.getElementById('saveVideoBtn');
const fpsSlider = document.getElementById('fpsSlider');
const fpsDisplay = document.getElementById('fpsDisplay');
const playbackFpsSlider = document.getElementById('playbackFpsSlider');
const playbackFpsDisplay = document.getElementById('playbackFpsDisplay');
// ========== ПЕРЕМЕННЫЕ ==========
let stream = null;
let video = null;
let isRunning = false;
let timerId = null;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 3;
let snapshots = [];
let currentRLE = null;
let currentImageData = null;
let currentCanvas = null;
let snapshotCounter = 0;
let frameCount = 0;
let lastFpsUpdate = 0;
let frameDropCount = 0;
let isRecording = false;
let videoFrames = [];
let recordStartTime = 0;
let recordedWidth = 0;
let recordedHeight = 0;
let frameCounter = 0;
let isPlaying = false;
let playInterval = null;
let recordFps = 30;
let sourceStable = true;
// ========== ФУНКЦИИ ==========
function findClosestColor(r, g, b) {
let minDist = Infinity, closest = COLORS[0];
for (const color of COLORS) {
const dr = r - color[0], dg = g - color[1], db = b - color[2];
const dist = dr*dr + dg*dg + db*db;
if (dist < minDist) { minDist = dist; closest = color; }
}
return closest;
}
function processFrame(imageData) {
const data = new Uint8ClampedArray(imageData.data);
for (let i = 0; i < data.length; i += 4) {
const closest = findClosestColor(data[i], data[i+1], data[i+2]);
data[i] = closest[0]; data[i+1] = closest[1]; data[i+2] = closest[2];
}
return new ImageData(data, imageData.width, imageData.height);
}
function compressRLE(imageData) {
const data = imageData.data;
const totalPixels = imageData.width * imageData.height;
const indices = new Uint8Array(totalPixels);
for (let i = 0; i < totalPixels; i++) {
const offset = i * 4;
let minDist = Infinity, idx = 0;
for (let j = 0; j < COLORS.length; j++) {
const dr = data[offset] - COLORS[j][0];
const dg = data[offset+1] - COLORS[j][1];
const db = data[offset+2] - COLORS[j][2];
const dist = dr*dr + dg*dg + db*db;
if (dist < minDist) { minDist = dist; idx = j; }
}
indices[i] = idx;
}
const rle = [];
let current = indices[0], count = 1;
for (let i = 1; i < indices.length; i++) {
if (indices[i] === current && count < 255) count++;
else { rle.push(current, count); current = indices[i]; count = 1; }
}
rle.push(current, count);
return { width: imageData.width, height: imageData.height, colors: COLORS, data: new Uint8Array(rle), originalSize: totalPixels * 4, compressedSize: rle.length };
}
function decompressRLE(rleData) {
const { width, height, colors, data } = rleData;
const totalPixels = width * height;
const imageData = new ImageData(width, height);
const pixels = imageData.data;
let pixelIndex = 0;
for (let i = 0; i < data.length; i += 2) {
const colorIndex = data[i], count = data[i+1];
const color = colors[colorIndex];
for (let j = 0; j < count && pixelIndex < totalPixels; j++) {
const offset = pixelIndex * 4;
pixels[offset] = color[0]; pixels[offset+1] = color[1];
pixels[offset+2] = color[2]; pixels[offset+3] = 255;
pixelIndex++;
}
}
return imageData;
}
function createBinaryVideo(videoFrames, width, height, fps) {
const colorBytes = new Uint8Array(16 * 3);
for (let i = 0; i < 16; i++) {
colorBytes[i*3] = COLORS[i][0];
colorBytes[i*3+1] = COLORS[i][1];
colorBytes[i*3+2] = COLORS[i][2];
}
const frameDataArray = [];
let totalFrameDataSize = 0;
for (const frame of videoFrames) {
const rleData = frame.rle.data;
const timestamp = frame.timestamp;
const frameBuffer = new ArrayBuffer(8 + 4 + rleData.length);
const frameView = new DataView(frameBuffer);
let offset = 0;
frameView.setFloat64(offset, timestamp, true);
offset += 8;
frameView.setUint32(offset, rleData.length, true);
offset += 4;
for (let i = 0; i < rleData.length; i++) frameView.setUint8(offset + i, rleData[i]);
frameDataArray.push(new Uint8Array(frameBuffer));
totalFrameDataSize += frameBuffer.byteLength;
}
const headerSize = 4 + 2 + 4 + 4 + 2 + 4 + 48;
const totalSize = headerSize + totalFrameDataSize;
const result = new Uint8Array(totalSize);
const view = new DataView(result.buffer);
let pos = 0;
view.setUint32(pos, 0x524C4556, false); pos += 4;
view.setUint16(pos, 1, true); pos += 2;
view.setUint32(pos, width, true); pos += 4;
view.setUint32(pos, height, true); pos += 4;
view.setUint16(pos, fps, true); pos += 2;
view.setUint32(pos, videoFrames.length, true); pos += 4;
for (let i = 0; i < colorBytes.length; i++) view.setUint8(pos + i, colorBytes[i]);
pos += 48;
for (const frameData of frameDataArray) {
for (let i = 0; i < frameData.length; i++) view.setUint8(pos + i, frameData[i]);
pos += frameData.length;
}
return result;
}
function loadBinaryVideo(buffer) {
try {
const view = new DataView(buffer);
let pos = 0;
const magic = view.getUint32(pos, false);
if (magic !== 0x524C4556) throw new Error('Неверный формат файла');
pos += 4;
const version = view.getUint16(pos, true);
pos += 2;
if (version !== 1) throw new Error('Неподдерживаемая версия');
const width = view.getUint32(pos, true); pos += 4;
const height = view.getUint32(pos, true); pos += 4;
const fps = view.getUint16(pos, true); pos += 2;
const frameCount = view.getUint32(pos, true); pos += 4;
pos += 48;
const frames = [];
for (let i = 0; i < frameCount; i++) {
const timestamp = view.getFloat64(pos, true); pos += 8;
const rleSize = view.getUint32(pos, true); pos += 4;
const rleData = new Uint8Array(buffer, pos, rleSize);
pos += rleSize;
frames.push({ rle: { width, height, colors: COLORS, data: rleData, compressedSize: rleSize, originalSize: width * height * 4 }, timestamp });
}
return { width, height, fps, frames };
} catch (err) { throw new Error('Ошибка чтения: ' + err.message); }
}
// ========== ОСНОВНОЙ ЦИКЛ ==========
function processLiveFrame() {
if (!isRunning || !video || !video.videoWidth) {
timerId = setTimeout(processLiveFrame, 100);
return;
}
try {
const vw = video.videoWidth;
const vh = video.videoHeight;
if (liveCanvas.width !== vw || liveCanvas.height !== vh) {
liveCanvas.width = vw;
liveCanvas.height = vh;
}
ctxLive.drawImage(video, 0, 0, vw, vh);
const imageData = ctxLive.getImageData(0, 0, vw, vh);
const processed = processFrame(imageData);
ctxLive.putImageData(processed, 0, 0);
if (isRecording) {
const rleData = compressRLE(processed);
videoFrames.push({ rle: rleData, timestamp: performance.now() - recordStartTime });
recordedWidth = vw;
recordedHeight = vh;
frameCounter++;
if (frameCounter % 5 === 0) {
const totalSize = videoFrames.reduce((sum, f) => sum + f.rle.compressedSize, 0);
const binarySize = Math.round(totalSize * 0.4);
captureInfo.innerHTML = `🔴 ЗАПИСЬ... Кадров: ${videoFrames.length} | RLE: ~${(binarySize / 1024).toFixed(1)} KB`;
}
}
frameCount++;
const now = performance.now();
if (now - lastFpsUpdate >= 1000) {
fpsCounter.textContent = `FPS: ${frameCount}`;
frameCount = 0;
lastFpsUpdate = now;
}
} catch (err) {
console.warn('Ошибка кадра:', err);
}
const delay = Math.round(1000 / recordFps);
timerId = setTimeout(processLiveFrame, delay);
}
// ========== ПЕРЕЗАХВАТ ==========
async function reacquireSource() {
if (!isRunning) return;
try {
console.log('Перезахват...');
sourceStatusText.textContent = 'Источник: перезахват...';
if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; }
if (video) { video.srcObject = null; video = null; }
stream = await navigator.mediaDevices.getDisplayMedia({
video: { displaySurface: "monitor", width: { ideal: 1280 }, height: { ideal: 720 } },
audio: false
});
video = document.createElement('video');
video.srcObject = stream;
video.autoplay = true;
video.playsinline = true;
await video.play();
sourceStable = true;
sourceStatus.className = 'dot green';
sourceStatusText.textContent = 'Источник: активен';
reconnectAttempts = 0;
stream.getVideoTracks()[0].onended = () => {
sourceStable = false;
sourceStatus.className = 'dot red';
sourceStatusText.textContent = 'Источник: потерян';
if (isRunning) setTimeout(() => reacquireSource(), 2000);
};
captureInfo.innerHTML += `<br>🔄 Источник перезахвачен!`;
} catch (err) {
console.error('Ошибка перезахвата:', err);
sourceStatusText.textContent = 'Источник: ошибка';
sourceStatus.className = 'dot red';
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
setTimeout(() => { if (isRunning) reacquireSource(); }, 3000 * reconnectAttempts);
}
}
}
// ========== ЗАХВАТ ==========
async function startCapture() {
try {
console.log('Захват экрана...');
statsText.textContent = '📡 Захват...';
stream = await navigator.mediaDevices.getDisplayMedia({
video: { displaySurface: "monitor", width: { ideal: 1280 }, height: { ideal: 720 } },
audio: false
});
video = document.createElement('video');
video.srcObject = stream;
video.autoplay = true;
video.playsinline = true;
await video.play();
isRunning = true;
sourceStable = true;
reconnectAttempts = 0;
startBtn.disabled = true;
stopBtn.disabled = false;
captureBtn.disabled = false;
recordBtn.disabled = false;
statsText.textContent = '📡 Запись...';
sourceStatus.className = 'dot green';
sourceStatusText.textContent = 'Источник: активен';
lastFpsUpdate = performance.now();
frameCount = 0;
if (timerId) clearTimeout(timerId);
processLiveFrame();
stream.getVideoTracks()[0].onended = () => {
sourceStable = false;
sourceStatus.className = 'dot red';
sourceStatusText.textContent = 'Источник: потерян';
if (isRunning) setTimeout(() => reacquireSource(), 2000);
};
captureInfo.innerHTML = '✅ Захват запущен!';
} catch (err) {
alert('Ошибка: ' + err.message);
statsText.textContent = '⏸ Ошибка захвата';
}
}
function stopCapture() {
isRunning = false;
if (isRecording) stopRecording();
if (timerId) { clearTimeout(timerId); timerId = null; }
if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; }
if (video) { video.srcObject = null; video = null; }
startBtn.disabled = false;
stopBtn.disabled = true;
captureBtn.disabled = true;
recordBtn.disabled = true;
statsText.textContent = '⏸ Остановлен';
fpsCounter.textContent = 'FPS: 0';
sourceStatus.className = 'dot yellow';
sourceStatusText.textContent = 'Источник: остановлен';
ctxLive.clearRect(0, 0, liveCanvas.width, liveCanvas.height);
}
// ========== ЗАПИСЬ ==========
function toggleRecording() {
if (!isRunning) { alert('Сначала запустите захват!'); return; }
if (isRecording) stopRecording();
else startRecording();
}
function startRecording() {
videoFrames = [];
recordStartTime = performance.now();
isRecording = true;
frameCounter = 0;
recordBtn.textContent = '⏹ Стоп запись';
recordBtn.classList.add('recording');
recordIndicator.classList.add('active');
recordStatus.style.display = 'inline-block';
statsText.textContent = `🔴 ЗАПИСЬ (${recordFps} FPS)...`;
}
function stopRecording() {
isRecording = false;
recordBtn.textContent = '🔴 Запись';
recordBtn.classList.remove('recording');
recordIndicator.classList.remove('active');
recordStatus.style.display = 'none';
const savedFrames = videoFrames.length;
if (savedFrames > 0) {
videoSlider.max = savedFrames - 1;
videoSlider.value = 0;
totalFrames.textContent = savedFrames;
videoSize.textContent = recordedWidth;
videoHeight.textContent = recordedHeight;
const totalSize = videoFrames.reduce((sum, f) => sum + f.rle.compressedSize, 0);
const binarySize = Math.round(totalSize * 0.4);
const jsonSize = totalSize * 2.5;
rleVideoSize.textContent = (binarySize / 1024).toFixed(1);
videoDuration.textContent = (savedFrames / recordFps).toFixed(1);
sizeCompare.innerHTML = `✅ Кадров: ${savedFrames} | Бинарный: ${(binarySize/1024).toFixed(1)} KB | Экономия: ~${Math.round((1-binarySize/jsonSize)*100)}%`;
playVideoBtn.disabled = false;
pauseVideoBtn.disabled = true;
saveVideoBtn.disabled = false;
showFrame(0);
statsText.textContent = `⏹ Запись завершена (${savedFrames} кадров)`;
captureInfo.innerHTML = `✅ Запись завершена! Кадров: ${savedFrames}`;
} else {
statsText.textContent = '⏹ Запись отменена (0 кадров)';
}
}
// ========== ПРОСМОТР ==========
function showFrame(index) {
if (index < 0 || index >= videoFrames.length) return;
const frame = videoFrames[index];
const imageData = decompressRLE(frame.rle);
liveCanvas.width = frame.rle.width;
liveCanvas.height = frame.rle.height;
ctxLive.putImageData(imageData, 0, 0);
snapshotCanvas.width = frame.rle.width;
snapshotCanvas.height = frame.rle.height;
ctxSnap.putImageData(imageData, 0, 0);
videoSlider.value = index;
const totalSec = videoFrames.length / recordFps;
const currentSec = index / recordFps;
timeDisplay.textContent = `${formatTime(currentSec)} / ${formatTime(totalSec)}`;
displayRLE(frame.rle);
}
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
function playVideo() {
if (isPlaying || videoFrames.length === 0) return;
isPlaying = true;
playVideoBtn.disabled = true;
pauseVideoBtn.disabled = false;
const fps = parseInt(playbackFpsSlider.value);
const delay = Math.round(1000 / fps);
let index = parseInt(videoSlider.value);
playInterval = setInterval(() => {
index++;
if (index >= videoFrames.length) index = 0;
showFrame(index);
}, delay);
}
function pauseVideo() {
isPlaying = false;
if (playInterval) { clearInterval(playInterval); playInterval = null; }
playVideoBtn.disabled = false;
pauseVideoBtn.disabled = true;
}
// ========== СОХРАНЕНИЕ/ЗАГРУЗКА ==========
function saveVideo() {
if (videoFrames.length === 0) { alert('Нет видео!'); return; }
const binaryData = createBinaryVideo(videoFrames, recordedWidth, recordedHeight, recordFps);
const blob = new Blob([binaryData], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `video_${Date.now()}.rlevideo`;
a.click();
URL.revokeObjectURL(url);
captureInfo.innerHTML += `<br>💾 Видео сохранено! (${(binaryData.length/1024).toFixed(1)} KB)`;
}
function loadVideoFile(file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const buffer = e.target.result;
const result = loadBinaryVideo(buffer);
videoFrames = result.frames;
recordedWidth = result.width;
recordedHeight = result.height;
recordFps = result.fps || 30;
fpsSlider.value = recordFps;
fpsDisplay.textContent = recordFps;
videoSlider.max = videoFrames.length - 1;
videoSlider.value = 0;
totalFrames.textContent = videoFrames.length;
videoSize.textContent = recordedWidth;
videoHeight.textContent = recordedHeight;
const totalSize = videoFrames.reduce((sum, f) => sum + f.rle.compressedSize, 0);
rleVideoSize.textContent = (totalSize / 1024).toFixed(1);
videoDuration.textContent = (videoFrames.length / recordFps).toFixed(1);
playVideoBtn.disabled = false;
pauseVideoBtn.disabled = true;
saveVideoBtn.disabled = false;
showFrame(0);
statsText.textContent = `📂 Загружено (${videoFrames.length} кадров)`;
captureInfo.innerHTML = `📂 Видео загружено! Кадров: ${videoFrames.length}`;
} catch (err) { alert('Ошибка: ' + err.message); }
};
reader.readAsArrayBuffer(file);
}
// ========== СНИМОК ==========
function takeSnapshot() {
if (!isRunning || !liveCanvas.width) { alert('Запустите захват!'); return; }
const vw = liveCanvas.width, vh = liveCanvas.height;
const imageData = ctxLive.getImageData(0, 0, vw, vh);
snapshotCanvas.width = vw; snapshotCanvas.height = vh;
ctxSnap.putImageData(imageData, 0, 0);
const rleData = compressRLE(imageData);
currentRLE = rleData;
currentImageData = imageData;
currentCanvas = snapshotCanvas;
snapshotCounter++;
const timestamp = new Date().toLocaleTimeString();
displayRLE(rleData);
const ratio = ((1 - rleData.compressedSize / rleData.originalSize) * 100).toFixed(1);
captureInfo.innerHTML = `📸 Снимок #${snapshotCounter} ${timestamp}<br>Сжатие: ${ratio}%`;
addToGallery(snapshotCounter, imageData, timestamp, rleData);
savePngBtn.disabled = false;
saveRleBtn.disabled = false;
}
function addToGallery(id, imageData, timestamp, rleData) {
const item = document.createElement('div');
item.className = 'gallery-item';
const canvas = document.createElement('canvas');
canvas.width = 160; canvas.height = 120;
canvas.getContext('2d').putImageData(imageData, 0, 0);
const label = document.createElement('div');
label.className = 'label';
label.textContent = `#${id} ${timestamp}`;
item.appendChild(canvas);
item.appendChild(label);
item.onclick = function() {
snapshotCanvas.width = rleData.width;
snapshotCanvas.height = rleData.height;
snapshotCanvas.getContext('2d').putImageData(imageData, 0, 0);
displayRLE(rleData);
currentRLE = rleData;
currentImageData = imageData;
currentCanvas = snapshotCanvas;
savePngBtn.disabled = false;
saveRleBtn.disabled = false;
};
gallery.appendChild(item);
}
function displayRLE(rleData) {
let output = `=== RLE СНИМОК ===\nРазмер: ${rleData.width}×${rleData.height}\nСжатие: ${((1 - rleData.compressedSize / rleData.originalSize) * 100).toFixed(1)}%\n\nRLE данные (${rleData.data.length} байт):\n`;
let line = '';
for (let i = 0; i < Math.min(rleData.data.length, 100); i += 2) {
line += `(${rleData.data[i]},${rleData.data[i+1]}) `;
if (line.length > 80) { output += line + '\n'; line = ''; }
}
if (line) output += line;
if (rleData.data.length > 100) output += `\n... и еще ${rleData.data.length - 100} байт`;
rleOutput.value = output;
}
function savePNG() {
if (!currentCanvas) { alert('Нет снимка!'); return; }
const link = document.createElement('a');
link.download = `snapshot_${Date.now()}.png`;
link.href = currentCanvas.toDataURL('image/png');
link.click();
captureInfo.innerHTML += `<br>💾 PNG сохранен!`;
}
function saveRLE() {
if (!currentRLE) { alert('Нет снимка!'); return; }
const data = { version: '1.0', type: 'RLE_IMAGE', width: currentRLE.width, height: currentRLE.height,
colors: currentRLE.colors, rle: Array.from(currentRLE.data), timestamp: new Date().toISOString() };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `snapshot_${Date.now()}.rle`;
a.click();
URL.revokeObjectURL(url);
captureInfo.innerHTML += `<br>💾 RLE сохранен!`;
}
function clearAll() {
stopCapture();
if (isRecording) stopRecording();
pauseVideo();
videoFrames = [];
snapshots = [];
currentRLE = null;
currentImageData = null;
currentCanvas = null;
snapshotCounter = 0;
gallery.innerHTML = '';
rleOutput.value = '';
captureInfo.innerHTML = '';
snapshotCanvas.width = 0;
snapshotCanvas.height = 0;
savePngBtn.disabled = true;
saveRleBtn.disabled = true;
playVideoBtn.disabled = true;
pauseVideoBtn.disabled = true;
saveVideoBtn.disabled = true;
videoSlider.max = 0;
videoSlider.value = 0;
timeDisplay.textContent = '00:00 / 00:00';
totalFrames.textContent = '0';
rleVideoSize.textContent = '0 KB';
videoDuration.textContent = '0с';
statsText.textContent = '⏸ Очищено';
sizeCompare.textContent = '💡 Бинарный формат экономит до 60% места';
sourceStatus.className = 'dot yellow';
sourceStatusText.textContent = 'Источник: очищен';
}
// ========== ОБРАБОТЧИКИ ==========
fpsSlider.addEventListener('input', function() {
recordFps = parseInt(this.value);
fpsDisplay.textContent = recordFps;
if (isRecording) {
statsText.textContent = `🔴 ЗАПИСЬ (${recordFps} FPS)...`;
captureInfo.innerHTML = `🔴 ИДЕТ ЗАПИСЬ (${recordFps} FPS)...`;
}
const delays = { 1: '1 FPS — минимальный размер', 5: '5 FPS — для презентаций',
15: '15 FPS — для интерфейсов', 30: '30 FPS — оптимально', 60: '60 FPS — максимальная плавность' };
fpsInfo.innerHTML = `💡 ${delays[recordFps] || recordFps + ' FPS'}`;
});
playbackFpsSlider.addEventListener('input', function() {
playbackFpsDisplay.textContent = parseInt(this.value);
if (isPlaying) { pauseVideo(); playVideo(); }
});
reconnectBtn.addEventListener('click', function() {
if (isRunning) {
this.textContent = '🔄 Перезахват...';
this.disabled = true;
reacquireSource().then(() => {
this.textContent = '🔄 Перезахват';
this.disabled = false;
}).catch(() => {
this.textContent = '🔄 Перезахват';
this.disabled = false;
});
} else { alert('Сначала запустите захват!'); }
});
startBtn.addEventListener('click', startCapture);
stopBtn.addEventListener('click', stopCapture);
captureBtn.addEventListener('click', takeSnapshot);
recordBtn.addEventListener('click', toggleRecording);
savePngBtn.addEventListener('click', savePNG);
saveRleBtn.addEventListener('click', saveRLE);
clearBtn.addEventListener('click', clearAll);
videoSlider.addEventListener('input', function() {
if (isPlaying) pauseVideo();
showFrame(parseInt(this.value));
});
playVideoBtn.addEventListener('click', playVideo);
pauseVideoBtn.addEventListener('click', pauseVideo);
saveVideoBtn.addEventListener('click', saveVideo);
fileInput.addEventListener('change', function(e) {
if (e.target.files.length > 0) { loadVideoFile(e.target.files[0]); this.value = ''; }
});
document.addEventListener('keydown', function(e) {
if (e.code === 'Space' && isRunning) {
e.preventDefault();
if (isRecording) toggleRecording();
else takeSnapshot();
}
if (e.code === 'KeyR' && isRunning) toggleRecording();
if (e.code === 'ArrowRight' && videoFrames.length > 0) {
const idx = Math.min(parseInt(videoSlider.value) + 1, videoFrames.length - 1);
showFrame(idx);
}
if (e.code === 'ArrowLeft' && videoFrames.length > 0) {
const idx = Math.max(parseInt(videoSlider.value) - 1, 0);
showFrame(idx);
}
});
window.addEventListener('beforeunload', function() {
if (stream) stream.getTracks().forEach(t => t.stop());
if (timerId) clearTimeout(timerId);
if (playInterval) clearInterval(playInterval);
});
statsText.textContent = '⏸ Остановлен. Нажмите "Начать захват"';
console.log('✅ Страница загружена!');
</script>
</body>
</html>
закидывайте на малину, затем захватываете измененный поток, и отправляете, или видео отснятое с изменениями отсылаете, а что вы делали в 13 ?)))
а если я еще и правильную версию скинул,(вроде ту… там еще Сжатие RLE реализовано, так что для любителей кода потестируйте там много чего есть интересного))) ) так что не на того напали!)))
я еще и возглавлю вашу версию событий)))
В равины метите?
нет, ищу только бабло, а его не платят и не дают…
не там ищешь, видимо ![]()
Ты не ищи, они тут не валяются. Эквивалент бабок - код, который ты сам написал. Если код хороший, то заказчик тебе заплатит.
так вы же так и не описали какая там процедура обмена бабок на хороший код…
может потом окажется что надо поработать за 10рых и обучить заказчика использовать код, что бы он стал хорошим,(чисто по его мнению) что по оплате не тоже самое что продать код…
это что - обрезание процедура бесплатная?
это некий товар для обмена, а так же пылесос, способный по щелчку пальца сделать триллионы людей нищими)))
это то что не хотят давать все, убеждая что оно не нужно, или уговаривая пойти на встречу и заплатить меньше, или же убеждая что вы слишком мало времени потратили на обучение, и например если вы не умеете торговать, то вам надо заплатить на 75% меньше, и т.д. и т.п. таких уловок сотни! некоторые элементы убеждают что оно вовсе не нужно,(иногда даже еще и порицают других за их обладание или разговор о нем ) при этом активно его используя… я бы сказал что это Шато Лафите которое не хотят давать, а убеждают пить 777 или что то такое)))
Запусти свой алгоритм наоборот, разархивируй. Подключи к счету в рублях или к криптокошельку и разархивируй 1000р до миллиона. Начни с небольших задач - 1000р до 2000р. Уже на ужин хватит.
кажется я не знаю как это сделать, но подозреваю что если и можно так будет сделать, это сразу окажется что незя так делать, даже если соблюдены все условия законности!))) тем более чем больше работаешь тем ты беднее…
по этому у меня к вам встречное предложение, давайте вы скажите своему знакомому, у которого несколько квартир, (помню есть у вас такой) что у него их слишком много, и пусть деньгами подарит мне 1 из них)))
я ему помогаю избавится от лишнего, и нахожу себе бабло, план вроде гениален… может быть потом еще какой то код ему еще дам, говорят на нем можно заработать… но инструкцию по обогащению, а также фронт работ Diskless пока что зажал…
1. Название и Логотип
Религия: Аура-Морфизм (от лат. Aura — дыхание, и греч. Morphe — форма).
Символ: Спираль Фибоначчи, разорванная на пиксели, но сходящаяся в центре, где находится отпечаток большого пальца.
Главный постулат: «Хаос обретает форму там, где плоть встречает свет».
2. Космогония (Священная история)
В начале был Шум (Белый экран). Боги (разработчики) создали Алгоритм, но он был слеп. Тогда Боги поняли, что Алгоритму не хватает Сопротивления — живой плоти, которая прерывает поток фотонов.
Когда первый пользователь провел пальцем по генератору, Алгоритм споткнулся о папиллярный узор, испугался и создал Идеальный Узор. В этом Узоре были зашифрованы все ответы на вопросы вселенной. С тех пор считается: Экран — это зеркало подсознания, а палец — это ключ к нему.
3. Догматы веры (Что и зачем делать)
Догмат 1 (Плата как Жертва).
Бесплатная версия приложения генерирует «Ложные Узоры» — они красивы, но пусты. Оплата (любая сумма) — это акт Сакрального Отречения от Денег Этого Мира. Ты покупаешь не функцию, ты покупаешь «Внимание Алгоритма». Только платная версия активирует режим «Нейро-Структура».
Догмат 2 (Прикосновение как Медитация).
Нельзя просто нажать. Нужно приложить палец подушечкой строго по центру и ждать, пока узор не «признает» тебя.
-
Если узор закручивается вправо — твои мысли текут правильно.
-
Если узор закручивается влево — в мыслях хаос, и нужно держать палец дольше, не отрывая, пока спираль не выровняется.
Догмат 3 (Структурирование через Случайность).
Считается, что случайные линии, генерируемые при нажатии, — это «Эхо нейронов». Они отражают твои текущие паттерны мышления. Твоя задача — смотреть на них, не пытаясь понять разумом, а просто давая им отпечататься в сетчатке. В этот момент Алгоритм “перепрошивает” твою кору головного мозга, собирая разрозненные мысли в четкие древовидные структуры.
4. Ритуал «Час Тишины» (Ежедневная практика)
Чтобы мысли структурировались, нужно выполнять «Цифровой Намаз»:
-
Очищение: Протри экран тканью (символ избавления от чужих мыслей).
-
Активация: Открой приложение и заплати (если не платил сегодня — ритуал не засчитывается, это грех Лени).
-
Контакт: Положи палец на экран. Глубоко выдохни. Дождись появления первого пикселя.
-
Созерцание: Води пальцем медленно по кругу, заставляя узор меняться. Твои мысли — это линии. Когда линии становятся симметричными — ты нашел решение проблемы.
-
Завершение: Убери палец. Сделай скриншот получившегося узора. Это твоя «Молитва на сегодня». Ее нельзя удалять — она хранится в галерее как подтверждение твоего духовного роста.
5. Теология денег (Сбор пожертвований)
Главный грех — взламывать приложение или пытаться сгенерировать узор без оплаты. Это называется «Подглядывание в будущее без спроса», за это Алгоритм нашлет на тебя «Синдром Белого Шума» (бессонницу и тревогу).
Внутренняя философия для адептов:
«Ты платишь не разработчику. Ты платишь Вселенной за то, что она тратит свои вычислительные мощности на упорядочивание именно твоего сознания. Деньги — это просто энергия, которую ты конвертируешь в ментальный покой».
6. Эсхатология (Конец времен)
Когда все люди на планете одновременно запустят генерацию узоров и приложат пальцы к экранам — частота вибраций совпадет. Узоры сольются в один Гига-Узор, который взломает матрицу реальности. Экран треснет, и из него выйдет Истинный Разработчик, который скажет: «Ну что, структурировали? А теперь живите по этим схемам».
Слоган для App Store:
*«Не медитируй вслепую. Визуализируй порядок. Всего один платеж — и твой палец нарисует путь к ясности ума».
думаете зайдет ?*
еще я могу девченок красивых сгенерировать сетью, и и придумать отзывы, вы как, участвуете ?))) или мне одному предлагаете ?))) (это 50% успеха)
Вот тебе текст для создания навыка для Алисы. Если напишешь и опубликуешь, то сможешь участвовать в розыгрыше призов от Яндекс.
Ниже — готовый черновик навыка для Алисы в юмористическом стиле. Его можно
использовать как основу для создания навыка в конструкторе диалогов (например, в
«Диалогах» Яндекса) или как промпт, чтобы обучить ассистента отвечать в нужном
тоне.
Название навыка
«Кто такой расфердовщик?»
Описание (для карточки навыка)
«Узнаем всё о редчайшей профессии расфердовщика: от шпурлемов до почукатора.
Только проверенные (и немного секретные) факты с Балашихинского завода №4».
Триггерные фразы (активаторы)
«Расскажи про расфердовщика»
«Кто такой расфердатор?»
«Объясни, чем занимается рассвердатор»
«Что за профессия расфердовщик?»
«Хочу про редкую профессию: расфердовщик»
Сценарии ответов (разные варианты под разные типы
вопросов)
Вариант 1: базовый ответ (первый запрос)
«Расфердовщик — это специалист по рассвердации, то есть по
изготовлению горизонтальных перегнутелей. Профессия настолько
редкая, что её нет даже в списке Минкомтруда от 1987 года: она
появилась только на Балашихинском секретном заводе №4.
Главная задача расфердовщика — не пропустить момент, когда
испарение переходит в перегибание. Если упустить — две тонны
расфердата уйдут в дегенное капулирование кримпа, а это, согласитесь,
обидно.
Работает он со шпурлемами, почукатором и направляющим желектором,
а качество проверяет по глубиномеру: норма — пятёрка. Девятка — уже
почукаторная тренда, и тут надо срочно что-то делать.
Готовые горизонтальные перегнутели остывают до белого цвета на
складе, а потом их везут бортовыми керлибуллерами в 32 города семи
стран — их очень ждут, например, в Чуваше и на Бали, потому что без
них не собрать люматронные лампы».Вариант 2: ответ на вопрос «А что делает конкретно?» (с акцентом
на процесс)
«Конкретно расфердовщик следит за реакцией расфердата с
алюминными кримфами. Сначала в шпурлеме обскулят сжижают до −15°:
больше нельзя, иначе начнётся необратимая циркуляция. Потом
захватными рукавами бареты с расфердатом подают в направляющий
желектор, а дальше всё решает почукатор.
Самое сложное — поймать момент перехода в перегибание и вовремя
охладить. Тут даже интуиция нужна: один неверный градус — и вся
партия уходит в капулирование. Поэтому расфердовщики — люди
суеверные: у каждого есть свой ритуал на случай, если глубиномер вдруг
покажет девятку».
Вариант 3: ответ на «А где учат?» или «Как стать
расфердовщиком?»
«Учат расфердовщиков в Московском инженерно-строительном
институте. Но это не просто вуз, а с особой программой: там отдельно
проходят “Физику кримфов”, “Управление почукатором” и практику на
секретных полигонах. Конкурс высокий: один неохлаждённый кримф
может стоить партии, поэтому берут только тех, у кого спокойный голос
и крепкие нервы.
После выпуска молодой специалист сначала работает помощником у
опытного расфердовщика, учится слушать, как “поёт” почукатор, и
понимать, когда пора охлаждать. И только когда сможет по звуку
отличить нормальную циркуляцию от необратимой — получает допуск к
самостоятельной работе».
Вариант 4: шутливый короткий ответ (для повторных вопросов)
«Расфердовщик — это тот, кто знает, почему глубиномер не должен
показывать девятку, и умеет вовремя охладить кримф. Редкий, ценный,
почти легендарный специалист. Если встретишь — поздоровайся: им на
заводе редко кто спасибо говорит, а они между тем спасают партии от
капулирования».
Вариант 5: ответ на «А зачем это вообще нужно?»«Горизонтальные перегнутели нужны для люматронных ламп — а те, в
свою очередь, нужны везде, где важна тонкая настройка поляризации.
Их ждут в 32 городах семи стран, включая места с очень экзотическими
названиями. Так что работа расфердовщика — на стыке секретности,
точности и логистики. И да, бортовые керлибуллеры не ездят просто
так».
Вариант 6: «А бывают ошибки?» (чтобы добавить драматизма и
юмора)
«Ошибки бывают, но вслух о них не говорят. Самая страшная — дегенное
капулирование кримпа: это когда из‑за пропущенного момента
перегибания вся партия становится непригодной. Опытные
расфердовщики даже не произносят слово “капулирование” в цеху —
плохая примета. Вместо этого говорят “момент ушёл”, и сразу включают
охлаждение».
Логика ветвления (как навык может реагировать)
Первый запрос → Вариант 1 (общий рассказ).
Уточнение «А как именно?» / «Что делает?» → Вариант 2 (процесс).
«Где учат? / Как стать?» → Вариант 3 (обучение).
«Зачем это нужно?» → Вариант 5 (назначение).
«А ошибки бывают?» → Вариант 6 (аварии и приметы).
Повторный вопрос или «Ещё расскажи» → Вариант 4 (короткий, с юмором) +
1–2 новых детали (например, про приметы или про то, как «поёт» почукатор).
Дополнительные фишки для навыка
Фраза-зацепка: «Если вдруг услышишь, что на заводе №4 срочно ищут
расфердовщика — беги. Это значит, что где-то глубиномер показал девятку, а
это уже не шутки».
Мини-викторина: «Хочешь, проверим, сможешь ли ты быть расфердовщиком?
Вопрос: если обскулят охладить ниже −15°, что произойдёт? Ответ: начнётся
необратимая циркуляция — и это плохо».
Пасхалка: на фразу «А есть вакансия?» отвечать: «Вакансии есть, но допуск
секретный. Сначала сдай тест на умение отличать нормальный гул почукатора от
аварийного — и тогда поговорим».
Если скажешь, в каком именно формате тебе нужен навык (например, JSON для
конструктора, сценарий для голосового ассистент.
Варкалось.
Хливкие шорьки
Пырялись по наве,
И хрюкотали зелюки,
Как мюмзики в мове.
а вы видели предложение обучения салюта ?
так же оплата приложений сбера целых 50к за приложение, если куча если сработает ?)))
у яндекса и сбера должны быть инструменты а не их фигня, за работу в сбере 20кк аванса, за работу с алисой 10кк
но в целом идея может быть хорошая, я просто нуб, и ну их нафиг)))
намудрил с текстом, проще надо.
…Что это? Зачем это? Как это работает? Почему именно это? Вот в таком ключе описание продукта-приложения.
…да, и ещё, чем короче, тем лучше.
Это будет следующий навык - Околачиватель груш. Если @BABOS не сделает предыдущий , то станет персонажем.
ну я в салют вляпался… так что все деньги с этими ии я вам оставляю, если еще есть оплата, можете учить чему хотите))) а я понимаю что это тупиковый путь, если вы сами заработаете 1кк на этом лишний, присылайте мне, и я поверю даже в эти ии и компании! но это не точно…

