Инструменты пользователя

Инструменты сайта


equipment:light:noo-psyche

Различия

Показаны различия между двумя версиями страницы.

Ссылка на это сравнение

Предыдущая версия справа и слеваПредыдущая версия
Следующая версия
Предыдущая версия
equipment:light:noo-psyche [2025/10/22 12:57] – удалено - внешнее изменение (Дата неизвестна) 127.0.0.1equipment:light:noo-psyche [2025/10/28 08:50] (текущий) – Вадим Мартовский
Строка 1: Строка 1:
 +==== Редактор для Noo-psyche K7 PRO и K7 Mini ====
 +<html>
 +<html lang="ru">
 +<head>
 +  <meta charset="UTF-8">
 +    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 +  <title>QR Editor по часам и каналам</title>
 +  <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
 +  <script src="https://cdn.jsdelivr.net/npm/qrcode/build/qrcode.min.js"></script>
 +  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 +  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-dragdata@2.0.2/dist/chartjs-plugin-dragdata.min.js">/script>
 +  <script>  if(typeof ChartDragData !== 'undefined') Chart.register(ChartDragData);</script>
  
 +  <style>
 +    body { 
 +      font-family: Arial, sans-serif; 
 +      margin: 0; 
 +      background: #f4f4f9; 
 +      display: flex; 
 +      justify-content: center; 
 +    }
 +        .container {
 +      max-width: 1200px;
 +      width: 100%;
 +      padding: 0px;
 +      text-align: center;
 +    }
 +    h2 { margin-top: 0; }
 +    table { border-collapse: collapse; margin-top: 10px; }
 +    th, td { border: 1px solid #ccc; padding: 3px; text-align: center; }
 +    input { width: 40px; padding: 2px; text-align: center; }
 +    textarea { width: 100%; height: 100px; margin-top: 10px; font-family: monospace; }
 +    #qrcode { margin-top: 20px; padding: 10px; background: white; display: inline-block; }
 +    #editorContainer { display: flex; gap: 0px; align-items: flex-start; }
 +    #channels { flex: 1; overflow-x: auto; }
 +    #chartContainer { flex: 1;  height: 600px; }
 +    #tableWrapper {width: 100%;}
 +    #chartzone {width: 100%; height: 450px;padding: 0px;}
 +    #channelChart { width: 99% !important;  height: 450px !important;    }
 +    #fileInput { display: none; }
 +    .pad {width: 100%;}
 +    .upload-btn {
 +      display: inline-block;
 +      padding: 6px 14px;
 +      background: #007bff;
 +      color: white;
 +      border-radius: 5px;
 +      cursor: pointer;
 +      font-size: 14px;
 +    }
 +    .upload-btn:hover { background: #0056b3; }
 +    .upload-btn2 {     display: inline-block;     padding: 6px 14px;      background: green;      color: white !important;        border-radius: 5px;      cursor: pointer;      font-size: 14px;    }
 +    .upload-btn2:hover { background: #005600; }
 +        @media (max-width: 900px) {
 +      #editorContainer { flex-direction: column; align-items: center; }
 +      #chartContainer, #tableWrapper { width: 100%; }
 +    }
 +  </style>
 +</head>
 +<body>
 +<div class="container">
 +  <h2>Редактор для Noo-psyche K7 PRO и K7 Mini</h2>
 +   <div style="background: #fff3cd; border: 1px solid #ffeeba; padding: 12px; border-radius: 6px; color: #856404;">
 +    ⚠️ Для <strong>K7 Mini</strong> используются только 3 канала (зелёный считается как УФ).<br>
 +    Все изменения вы делаете <strong>на свой страх и риск</strong>! Проект любительский, в тестовом режиме, возможны ошибки.
 +  </div>
 +  <div id="qrcode"></div>
 +
 +  <div id="editorContainer">
 +    <div id="chartContainer">
 +      <div>
 +           <a href="https://reefekb.ru/noopsyche/" target="_blank" class="upload-btn2">На весь экран</a>
 +        <label for="fileInput" class="upload-btn">Загрузить файл</label>
 +        <input type="file" id="fileInput" hidden>
 +        <button id="generateBtn" class="upload-btn">Сгенерировать QR</button>
 +        <button id="downloadBtn" class="upload-btn">Скачать QR</button>
 +        <canvas id="qrcodeCanvas" style="display:none;"></canvas>
 +        
 +      </div>
 +
 +      <div style="margin-top:10px;">
 +        <label for="targetChannel">Выбрать канал: </label>
 +        <select id="targetChannel">
 +          <option value="all">Всё</option>
 +          <option value="0">1 White</option>
 +          <option value="1">2 Royal Blue</option>
 +          <option value="2">3 Green</option>
 +          <option value="3">4 UV</option>
 +          <option value="4">5 Cyan</option>
 +          <option value="5">6 Red</option>
 +        </select>
 +        <button id="shiftLeftBtn">← (−1ч)</button>
 +        <button id="shiftRightBtn">→ (+1ч)</button>
 +        <button id="scaleUpBtn">↑ +5%</button>
 +        <button id="scaleDownBtn">↓ -5%</button>
 +        <button id="stretchXBtn">←|→ (+1ч)</button>
 +        <button id="compressXBtn">→|← (−1ч)</button>
 +      </div>       
 +
 +        <div id="chartzone" ><canvas id="channelChart"></canvas>  </div>
 +    </div>
 +
 + 
 +  </div>
 +  
 +  
 +  
 +     <div  id="tableWrapper" style="flex:1; min-width: 360px;">
 +      <button id="toggleTableBtn">Показать/Скрыть таблицу и HEX</button>
 +      <div id="tableContainer" style="display: none;">
 +        <div id="channels"></div>
 +        <div style="display:flex; flex-direction:column; width:100%;">
 +         <span id="hexPrefixSpan" style="font-size:0.8em; color:#555; margin-bottom:0px; margin-top:10px;">0A2E130A460A#</span>
 +         <textarea id="hexOutput" style="resize:none; overflow:hidden; margin-top:0px;"></textarea>
 +        </div>
 +
 +      </div>
 +    </div><div style="overflow-x:auto; max-width: 900px; margin: auto;">
 +  <table style="width: 100%;  border-collapse: collapse; margin-bottom: 20px; font-size: 13px;">
 +    <thead>
 +      <tr style="background: #f0f6fc; color: #1a4c7a; text-align: left;">
 +        <th style="padding: 5px; border: 1px solid #d0d7de;">Канал</th>
 +        <th style="padding: 5px; border: 1px solid #d0d7de;">Диоды</th>
 +        <th style="padding: 5px; border: 1px solid #d0d7de;">Назначение</th>
 +      </tr>
 +    </thead>
 +    <tbody>
 +      <tr>
 +        <td style="padding: 5px; border: 1px solid #d0d7de; font-weight: bold;">White</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">6× Osram Cool White</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">Дневной свет, баланс, «белизна» картинки</td>
 +      </tr>
 +      <tr style="background: #f9fbfd;">
 +        <td style="padding: 5px; border: 1px solid #d0d7de; font-weight: bold;">Royal Blue</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">8× Osram Royal Blue + 4× SemiLED 430nm</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">База морского света, флуоресценция кораллов</td>
 +      </tr>
 +      <tr>
 +        <td style="padding: 5px; border: 1px solid #d0d7de; font-weight: bold;">Green</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">2× Osram Blue + Green</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">Акцентный, баланс спектра</td>
 +      </tr>
 +      <tr style="background: #f9fbfd;">
 +        <td style="padding: 5px; border: 1px solid #d0d7de; font-weight: bold;">UV</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">4× SemiLED 415nm + 2× SemiLED 405nm</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">Усиление свечения кораллов, требует дозировки</td>
 +      </tr>
 +      <tr>
 +        <td style="padding: 5px; border: 1px solid #d0d7de; font-weight: bold;">Cyan</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">10× Osram Blue</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">Небесно-голубой, визуально приятный</td>
 +      </tr>
 +      <tr style="background: #f9fbfd;">
 +        <td style="padding: 5px; border: 1px solid #d0d7de; font-weight: bold;">Red</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">2× Osram Red + 2× Osram Warm White</td>
 +        <td style="padding: 5px; border: 1px solid #d0d7de;">Добавляет тепла, но в избытке → рост водорослей</td>
 +      </tr>
 +    </tbody>
 +  </table>
 +</div>
 +    </div>
 +
 +<script>
 +const toggleBtn = document.getElementById('toggleTableBtn');
 +const tableContainer = document.getElementById('tableContainer');
 +const fileInput = document.getElementById('fileInput');
 +const hexOutput = document.getElementById('hexOutput');
 +const channelsDiv = document.getElementById('channels');
 +const qrCanvas = document.getElementById('qrcodeCanvas');
 +const generateBtn = document.getElementById('generateBtn');
 +const downloadBtn = document.getElementById('downloadBtn');
 +const chartCtx = document.getElementById('channelChart').getContext('2d');
 +
 +const NUM_CHANNELS = 6;
 +const NUM_HOURS = 24;
 +
 +// ====== Основные данные ======
 +let baseMatrix = Array.from({length:NUM_CHANNELS},()=>Array(NUM_HOURS).fill(0)); // БАЗА
 +let displayedMatrix = Array.from({length:NUM_CHANNELS},()=>Array(NUM_HOURS).fill(0)); // ВБАЗА
 +let transforms = Array.from({length:NUM_CHANNELS},()=>({shift:0, stretchX:0, stretchY:0})); // коэффициенты трансформаций
 +
 +const qrPrefix = "0A2E130A460A#";
 +
 +// ====== Применение трансформаций ======
 +function applyTransforms(channel){
 +  for(let h=0;h<NUM_HOURS;h++){
 +    let t = transforms[channel];
 +    // смещение по времени
 +    let pos = ((h - t.shift + NUM_HOURS) % NUM_HOURS);
 +    // растяжение по X (шире/уже)
 +    if(t.stretchX !== 0){
 +      pos = NUM_HOURS/2 + (pos - NUM_HOURS/2)/(1 + t.stretchX/NUM_HOURS);
 +      pos = ((pos % NUM_HOURS) + NUM_HOURS) % NUM_HOURS;
 +    }
 +    const i0 = Math.floor(pos);
 +    const i1 = (i0 + 1) % NUM_HOURS;
 +    const frac = pos - i0;
 +    // растяжение по Y (высота)
 +    const v0 = baseMatrix[channel][i0];
 +    const v1 = baseMatrix[channel][i1];
 +    const val = (v0*(1-frac)+v1*frac) * (1 + t.stretchY/100);
 +    displayedMatrix[channel][h] = Math.min(100, Math.max(0, Math.round(val)));
 +  }
 +}
 +
 +// Применяем трансформации ко всем каналам
 +function updateDisplayedMatrix(){
 +  for(let c=0;c<NUM_CHANNELS;c++) applyTransforms(c);
 +  drawTable();
 +  updateChart();
 +  updateHex();
 +}
 +
 +// Сохраняем все изменения из displayedMatrix в baseMatrix
 +function syncBaseWithDisplayed() {
 +  for (let c1 = 0; c1 < baseMatrix.length; c1++) {
 +    for (let h1 = 0; h1 < baseMatrix[c1].length; h1++) {
 +      baseMatrix[c1][h1] = displayedMatrix[c1][h1];
 +    }
 +  }
 +}
 +
 +// Сброс коэффициентов трансформации для всех каналов
 +function resetAllTransformCoefficients() {
 +  for (let c = 0; c < 6; c++) {
 +    resetTransforms(c);
 +  }
 +}
 +
 +// ====== Таблица ======
 +function drawTable(){
 +  channelsDiv.innerHTML='';
 +  const table = document.createElement('table');
 +  const header = document.createElement('tr');
 +  ['Час','White','Royal<br>Blue','Green','UV','Cyan','Red'].forEach(txt=>{
 +    const th=document.createElement('th'); th.innerHTML=txt; header.appendChild(th);
 +  });
 +  table.appendChild(header);
 +
 +  for(let h=0;h<NUM_HOURS;h++){
 +    const tr=document.createElement('tr');
 +    const tdHour=document.createElement('td'); tdHour.textContent=h.toString().padStart(2,'0'); tr.appendChild(tdHour);
 +
 +    for(let c=0;c<NUM_CHANNELS;c++){
 +      const td=document.createElement('td');
 +      const input=document.createElement('input');
 +      input.type='number';
 +      input.value = displayedMatrix[c][h];
 +      input.min=0; input.max=100;
 +      input.dataset.hour=h; input.dataset.channel=c;
 +
 +      input.addEventListener('change', e=>{
 +        syncBaseWithDisplayed();
 +        resetAllTransformCoefficients() 
 +        const val = Math.min(100, Math.max(0, parseInt(e.target.value)||0));
 +        baseMatrix[c][h] = val; // фиксируем базу
 +        displayedMatrix[c][h] = val;
 +        
 +        updateDisplayedMatrix();
 +      });
 +
 +      td.appendChild(input);
 +      tr.appendChild(td);
 +    }
 +    table.appendChild(tr);
 +  }
 +  channelsDiv.appendChild(table);
 +}
 +
 +// ====== График ======
 +let channelChart = new Chart(chartCtx, {
 +  type: 'line',
 +  data: {
 +    labels: Array.from({length:NUM_HOURS}, (_,i)=>i.toString().padStart(2,'0')),
 +    datasets: [
 +      { label:'White', data:Array(NUM_HOURS).fill(0), borderColor:'#CCCCCC', fill:false },
 +      { label:'Royal Blue', data:Array(NUM_HOURS).fill(0), borderColor:'#0000FF', fill:false },
 +      { label:'Green', data:Array(NUM_HOURS).fill(0), borderColor:'green', fill:false },
 +      { label:'UV', data:Array(NUM_HOURS).fill(0), borderColor:'#9400D3', fill:false },
 +      { label:'Cyan', data:Array(NUM_HOURS).fill(0), borderColor:'#00BFFF', fill:false },
 +      { label:'Red', data:Array(NUM_HOURS).fill(0), borderColor:'#FF0000', fill:false },
 +    ]
 +  },
 +  options: {
 +    responsive:true,
 +    maintainAspectRatio:false,
 +    scales:{ y:{ min:0, max:100, title:{ display:true, text:'Мощность (%)' } }, x:{ title:{ display:true, text:'Час' } } },
 +    plugins:{
 +      legend:{ position:'bottom' },
 +      dragData:{
 +    round:0, 
 +    showTooltip:true,
 +    onDrag:function(e,datasetIndex,index,value){
 +        baseMatrix[datasetIndex][index] = Math.round(displayedMatrix[datasetIndex][index]);
 +      displayedMatrix[datasetIndex][index] = Math.min(100, Math.max(0, value));
 +      drawTable();
 +      updateHex();
 +    },
 +        onDragEnd:function(e,datasetIndex,index,value){
 +        syncBaseWithDisplayed();
 +        resetAllTransformCoefficients() 
 +        baseMatrix[datasetIndex][index] = Math.round(displayedMatrix[datasetIndex][index]);       // Присваиваем изменённое значение в базу
 +        updateDisplayedMatrix();      // Пересчитываем всю displayedMatrix от обновлённой базы
 +        }
 +      }
 +    }
 +  }
 +});
 +
 +function updateChart(){
 +  for(let c=0;c<NUM_CHANNELS;c++){
 +    channelChart.data.datasets[c].data = displayedMatrix[c].slice();
 +  }
 +  channelChart.update();
 +}
 +
 +// ====== Кнопки трансформаций ======
 +function resetTransforms(channel){
 +  transforms[channel] = {shift:0, stretchX:0, stretchY:0};
 +}
 +function transformShift(channel,delta){
 +  if(channel==='all'){ for(let c=0;c<NUM_CHANNELS;c++) transforms[c].shift += delta; }
 +  else transforms[channel].shift += delta;
 +  updateDisplayedMatrix();
 +}
 +function transformStretchX(channel,delta){
 +  if(channel==='all'){ for(let c=0;c<NUM_CHANNELS;c++) transforms[c].stretchX += delta; }
 +  else transforms[channel].stretchX += delta;
 +  updateDisplayedMatrix();
 +}
 +function transformStretchY(channel,delta){
 +  if(channel==='all'){ for(let c=0;c<NUM_CHANNELS;c++) transforms[c].stretchY += delta; }
 +  else transforms[channel].stretchY += delta;
 +  updateDisplayedMatrix();
 +}
 +
 +// ====== Привязка кнопок ======
 +function getTargetChannel(){ 
 +  const val = document.getElementById('targetChannel').value;
 +  return val==='all'? 'all' : parseInt(val);
 +}
 +
 +document.getElementById('scaleUpBtn').addEventListener('click', ()=>transformStretchY(getTargetChannel(), +5));
 +document.getElementById('scaleDownBtn').addEventListener('click', ()=>transformStretchY(getTargetChannel(), -5));
 +document.getElementById('shiftLeftBtn').addEventListener('click', ()=>transformShift(getTargetChannel(), -1));
 +document.getElementById('shiftRightBtn').addEventListener('click', ()=>transformShift(getTargetChannel(), +1));
 +document.getElementById('stretchXBtn').addEventListener('click', ()=>transformStretchX(getTargetChannel(), +1));
 +document.getElementById('compressXBtn').addEventListener('click', ()=>transformStretchX(getTargetChannel(), -1));
 +
 +// ====== HEX ======
 +function updateHex(){
 +  let hex='';
 +  for(let h=0;h<NUM_HOURS;h++){
 +    hex += h.toString(16).padStart(2,'0').toUpperCase()+'00';
 +    for(let c=0;c<NUM_CHANNELS;c++){
 +      hex += displayedMatrix[c][h].toString(16).padStart(2,'0').toUpperCase();
 +    }
 +  }
 +  hexOutput.value = hex;
 +}
 +
 +// ====== QR ======
 +let qrDataURL = '';
 +function generateQR(){
 +  const data = hexOutput.value.trim();
 +  if(!data) return;
 +  const fullHex = qrPrefix + data;
 +  QRCode.toCanvas(qrCanvas, fullHex, {version:12,errorCorrectionLevel:'M',scale:5,margin:0}, err=>{
 +    if(err){ console.error(err); return; }
 +    const ctx = qrCanvas.getContext('2d');
 +    const size = qrCanvas.width;
 +    const moduleCount = 61;
 +    const moduleSize = size / moduleCount;
 +    const logoModules = 17;
 +    const start = Math.floor((moduleCount - logoModules)/2);
 +    const x = start * moduleSize, y = start*moduleSize;
 +    const w = logoModules*moduleSize, h = logoModules*moduleSize;
 +    ctx.fillStyle="#ffffff"; ctx.fillRect(x,y,w,h);
 +    const logo = new Image(); logo.src="https://reefekb.ru/noopsyche/logo.png";
 +    logo.onload = ()=>{ ctx.drawImage(logo,x,y,w,h); 
 +      const qrDiv = document.getElementById('qrcode'); qrDiv.innerHTML='';
 +      const img = new Image(); img.src=qrCanvas.toDataURL(); qrDiv.appendChild(img);
 +      qrDataURL = qrCanvas.toDataURL();
 +    };
 +  });
 +}
 +function downloadQR(){
 +  if(!qrDataURL) return;
 +  const a=document.createElement('a');
 +  a.href = qrDataURL;
 +  a.download='reefekb_qr.png';
 +  a.click();
 +}
 +generateBtn.addEventListener('click', generateQR);
 +downloadBtn.addEventListener('click', downloadQR);
 +
 +// ====== Загрузка файла ======
 +fileInput.addEventListener('change', function(event){
 +  const file = event.target.files[0];
 +  if(!file) return;
 +  const reader = new FileReader();
 +  reader.onload = function(e){
 +    const img = new Image();
 +    img.onload = function(){
 +      const canvas = document.createElement('canvas');
 +      const ctx = canvas.getContext('2d');
 +      canvas.width = img.width;
 +      canvas.height = img.height;
 +      ctx.drawImage(img,0,0);
 +      const imageData = ctx.getImageData(0,0,canvas.width,canvas.height);
 +      const code = jsQR(imageData.data, canvas.width, canvas.height);
 +      if(code){
 +        const hexData = code.data.split('#')[1] || code.data;
 +        hexOutput.value = hexData;
 +        parseHexToBase(hexData);
 +        
 +        syncBaseWithDisplayed();
 +        resetAllTransformCoefficients() 
 +        updateDisplayedMatrix();         
 +        
 +        
 +        console.log('QR загружен, все трансформации сброшены.');
 +      } else alert('QR-код не распознан.');
 +    };
 +    img.src = e.target.result;
 +  };
 +  reader.readAsDataURL(file);
 +});
 +
 +// ====== UI ======
 +toggleBtn.addEventListener('click', () => {
 +  if (tableContainer.style.display === 'none') {
 +    tableContainer.style.display = 'block';
 +    toggleBtn.textContent = 'Скрыть таблицу и HEX';
 +  } else {
 +    tableContainer.style.display = 'none';
 +    toggleBtn.textContent = 'Показать таблицу и HEX';
 +  }
 +});
 +
 +// ====== Инициализация ======
 +const defaultHex = "0000000000010000010000000001000002000000000000000300000000000000040000000000000005000000000100000600000100010100070001040001030108000208000307020900030D01060C040A000914020B15050B000F1B041121080C00192308152A0B0D001F2A0C182F0D0E00202E0F1B310D0F001C2D0E1B300B100014270C192C0811000D1F0916260612000818060F1E0413000611040916021400040B02050E01150003060102070016000203000302001700010000010000";
 +window.onload=function(){
 +  parseHexToBase(defaultHex);
 +  updateDisplayedMatrix();
 +  hexOutput.value = defaultHex;
 +};
 +
 +function parseHexToBase(hex){
 +  for(let h=0; h<NUM_HOURS; h++){
 +    for(let c=0;c<NUM_CHANNELS;c++){
 +      const offset = h*8*2 + 4 + c*2;
 +      baseMatrix[c][h] = parseInt(hex.substr(offset,2),16)||0;
 +      displayedMatrix[c][h] = baseMatrix[c][h];
 +    }
 +  }
 +  transforms = Array.from({length:NUM_CHANNELS},()=>({shift:0, stretchX:0, stretchY:0}));
 +}
 +
 +</script>
 +
 +
 +
 +
 +</body>
 +</html>
 +<html><div style="text-align:right;">//Made with ❤️ by @Martovsky//</div></html>