画板

一个画板,扫描同步二维码即可查看画作并保存相册

适配设备:Pro

作者:lrj

下载量:22 · 点赞:0

可信源码风险提示:保存或下载前请阅读源码和权限声明。
SHA-2561410be16248309cd29f217d2dec086c444b8b5f8e19b95164ac37ddda38b73a9
const GRID = 8;
const PALETTE = [
  {hex:'#FFFFFF', name:'白'}, {hex:'#000000', name:'黑'},
  {hex:'#FF0000', name:'红'}, {hex:'#00FF00', name:'绿'},
  {hex:'#0000FF', name:'蓝'}, {hex:'#FFFF00', name:'黄'},
  {hex:'#FF00FF', name:'品'}, {hex:'#00FFFF', name:'青'},
  {hex:'#FFA500', name:'橙'}, {hex:'#800080', name:'紫'},
  {hex:'#FFC0CB', name:'粉'}, {hex:'#A52A2A', name:'棕'},
  {hex:'#808080', name:'灰'}, {hex:'#FFD700', name:'金'},
  {hex:'#C0C0C0', name:'银'}, {hex:'#000080', name:'深蓝'}
];

const SYNC_URL = 'https://ZLhr66cc.html2.link';

const pixels = ui.signal(Array(GRID * GRID).fill(0));
const picked = ui.signal(1);
const currentView = ui.signal('canvas');
const historyList = ui.signal([]);
let autoSaveTimer;

function encodeArt() {
  const p = pixels.get();
  let h = '';
  for (let i = 0; i < p.length; i++) h += p[i].toString(16).toUpperCase();
  return `JSLAB;ART;${GRID};${GRID};${h}`;
}

function decodeArt(s) {
  if (!s || !s.includes('JSLAB;ART;')) return null;
  const p = s.split(';');
  if (p.length < 5) return null;
  const w = parseInt(p[2], 10), h = parseInt(p[3], 10);
  const hex = p[4];
  if (w !== GRID || h !== GRID || !hex) return null;
  const arr = [];
  const len = Math.min(hex.length, GRID * GRID);
  for (let i = 0; i < len; i++) arr.push(parseInt(hex[i], 16) || 0);
  while (arr.length < GRID * GRID) arr.push(0);
  return arr;
}

function paint(idx) {
  const p = pixels.get(), c = picked.get();
  if (p[idx] === c) return;
  const n = [...p]; n[idx] = c;
  pixels.set(n);
  clearTimeout(autoSaveTimer);
  autoSaveTimer = setTimeout(() => {
    script.data.set('autoSave', encodeArt()).catch(() => {});
  }, 500);
}

async function saveToHistory() {
  const list = await script.data.get('history', []);
  const now = Date.now();
  const d = new Date(now);
  const name = `${d.getMonth()+1}/${d.getDate()} ${d.getHours()}:${d.getMinutes().toString().padStart(2,'0')}`;
  list.unshift({ id: String(now), name, data: encodeArt(), time: now });
  if (list.length > 20) list.length = 20;
  await script.data.set('history', list);
  historyList.set(list);
  script.toast('已保存到历史');
}

async function loadHistory() {
  const list = await script.data.get('history', []);
  historyList.set(list);
}

function loadFromHistory(item) {
  const d = decodeArt(item.data);
  if (d) pixels.set(d);
  currentView.set('canvas');
  ui.setTitle('像素画板');
  ui.refresh();
}

async function deleteHistoryItem(id) {
  const list = await script.data.get('history', []);
  const filtered = list.filter(x => x.id !== id);
  await script.data.set('history', filtered);
  historyList.set(filtered);
}

async function clearAll() {
  const r = await dialog.confirm({
    title: '确认清空',
    message: '确定要清空画布吗?',
    confirmText: '清空',
    cancelText: '取消'
  });
  if (r.action !== 'confirm') return;
  pixels.set(Array(GRID * GRID).fill(0));
  script.data.set('autoSave', encodeArt()).catch(() => {});
}

function renderMiniGrid(data) {
  const p = decodeArt(data) || Array(GRID * GRID).fill(0);
  const rows = [];
  for (let y = 0; y < GRID; y++) {
    const cells = [];
    for (let x = 0; x < GRID; x++) {
      const i = y * GRID + x;
      cells.push(ui.text(' ', {
        width: 10, height: 10,
        background: PALETTE[p[i]].hex,
        color: PALETTE[p[i]].hex,
        lines: 1
      }));
    }
    rows.push(ui.row(cells, { gap: 1 }));
  }
  return rows;
}

function renderCanvas() {
  const p = pixels.get(), cur = picked.get();
  const rows = [];
  for (let y = 0; y < GRID; y++) {
    const cells = [];
    for (let x = 0; x < GRID; x++) {
      const i = y * GRID + x, ci = p[i];
      cells.push(ui.button(' ', () => paint(i), {
        id: `px${i}`,
        background: PALETTE[ci].hex,
        color: PALETTE[ci].hex,
        height: 48,
        flex: 1
      }));
    }
    rows.push(ui.row(cells, { gap: 2 }));
  }
  const palettes = [];
  for (let r = 0; r < 2; r++) {
    const cells = [];
    for (let c = 0; c < 8; c++) {
      const i = r * 8 + c, col = PALETTE[i], sel = i === cur;
      cells.push(ui.button(sel ? '●' : ' ', () => picked.set(i), {
        id: `cl${i}`,
        background: col.hex,
        color: sel ? '#00FF00' : col.hex,
        height: 48,
        flex: 1
      }));
    }
    palettes.push(ui.row(cells, { gap: 2 }));
  }
  return [
    ui.text('点击方格作画', { align: 'center', size: 16, lines: 1, color: '#aaa' }),
    ...rows,
    ui.spacer(4),
    ui.text('颜色', { size: 14, lines: 1 }),
    ...palettes,
    ui.spacer(6),
    ui.buttonRow([
      { text: '历史', onPress: () => { loadHistory(); currentView.set('history'); ui.setTitle('历史记录'); ui.refresh(); }, id: 'btnHist' },
      { text: '放大', onPress: () => { currentView.set('zoom'); ui.setTitle('全屏预览'); ui.refresh(); }, id: 'btnZoom' },
      { text: '保存', onPress: () => saveToHistory(), id: 'btnSave' }
    ], { gap: 4 }),
    ui.spacer(4),
    ui.buttonRow([
      { text: '同步', onPress: () => { currentView.set('syncArt'); ui.setTitle('同步到手机'); ui.refresh(); }, id: 'btnSyncArt' },
      { text: '网站码', onPress: () => { currentView.set('syncWeb'); ui.setTitle('同步器网站'); ui.refresh(); }, id: 'btnSyncWeb' },
      { text: '清空', onPress: () => clearAll(), id: 'btnClear', tone: 'danger' }
    ], { gap: 4 })
  ];
}

function renderHistory() {
  const list = historyList.get();
  const items = [];
  items.push(ui.text(`共 ${list.length} 幅作品`, { align: 'center', size: 14, lines: 1, color: '#aaa' }));
  if (!list.length) {
    items.push(ui.spacer(16));
    items.push(ui.text('暂无历史记录', { align: 'center', size: 16 }));
  }
  for (const item of list) {
    const mini = renderMiniGrid(item.data);
    items.push(ui.row([
      ui.column(mini, { gap: 1, width: 92 }),
      ui.column([
        ui.text(item.name, { size: 14, lines: 1 }),
        ui.row([
          ui.button('加载', () => loadFromHistory(item), { id: `ld${item.id}`, height: 38, flex: 1, tone: 'primary' }),
          ui.button('删', () => deleteHistoryItem(item.id), { id: `dl${item.id}`, height: 38, flex: 1, tone: 'danger' })
        ], { gap: 3 })
      ], { gap: 2, padding: 2, flex: 1 })
    ], { gap: 6, align: 'center' }));
    items.push(ui.divider({ color: '#333', height: 1 }));
  }
  items.push(ui.spacer(6));
  items.push(ui.button('返回画板', () => { currentView.set('canvas'); ui.setTitle('像素画板'); ui.refresh(); }, { id: 'backCanvas', tone: 'neutral' }));
  return items;
}

function renderZoom() {
  const p = pixels.get();
  const rows = [];
  const sz = 36;
  for (let y = 0; y < GRID; y++) {
    const cells = [];
    for (let x = 0; x < GRID; x++) {
      const i = y * GRID + x;
      cells.push(ui.text(' ', {
        width: sz, height: sz,
        background: PALETTE[p[i]].hex,
        color: PALETTE[p[i]].hex,
        lines: 1
      }));
    }
    rows.push(ui.row(cells, { gap: 2, justify: 'center' }));
  }
  return [
    ui.spacer(8),
    ...rows,
    ui.spacer(16),
    ui.button('退出全屏', () => { currentView.set('canvas'); ui.setTitle('像素画板'); ui.refresh(); }, { id: 'exitZoom', tone: 'neutral' })
  ];
}

function renderSyncWeb() {
  return [
    ui.text('手机扫描打开同步网页', { align: 'center', size: 16, lines: 1 }),
    ui.spacer(10),
    ui.qrcode(SYNC_URL, { size: 240 }),
    ui.spacer(10),
    ui.button('返回画板', () => { currentView.set('canvas'); ui.setTitle('像素画板'); ui.refresh(); }, { id: 'backWeb', tone: 'neutral' })
  ];
}

function renderSyncArt() {
  const url = `${SYNC_URL}#${encodeArt()}`;
  return [
    ui.text('手机扫描直接查看画作', { align: 'center', size: 16, lines: 1 }),
    ui.spacer(10),
    ui.qrcode(url, { size: 240 }),
    ui.spacer(10),
    ui.text('扫描后自动显示作品', { align: 'center', size: 13, color: '#aaa', lines: 1 }),
    ui.spacer(6),
    ui.button('返回画板', () => { currentView.set('canvas'); ui.setTitle('像素画板'); ui.refresh(); }, { id: 'backArt', tone: 'neutral' })
  ];
}

function mainView() {
  switch (currentView.get()) {
    case 'canvas': return renderCanvas();
    case 'history': return renderHistory();
    case 'zoom': return renderZoom();
    case 'syncWeb': return renderSyncWeb();
    case 'syncArt': return renderSyncArt();
    default: return renderCanvas();
  }
}

async function main() {
  const saved = await script.data.get('autoSave', '');
  const d = decodeArt(saved);
  if (d) pixels.set(d);
  ui.setTitle('像素画板');
  ui.render(mainView);
}

return main();