Spaces:
Running
Running
Download BeadQR/miniprogram/utils/engine.js from Ethscriptions/cut: direct link, hf CLI and curl.
- Browser
- Download file 8.27 kB
-
https://huggingface.co/spaces/Ethscriptions/cut/resolve/main/BeadQR/miniprogram/utils/engine.js
- Command line
-
hf download hf://spaces/Ethscriptions/cut/BeadQR/miniprogram/utils/engine.js
-
curl -L -o engine.js https://huggingface.co/spaces/Ethscriptions/cut/resolve/main/BeadQR/miniprogram/utils/engine.js
8.27 kB
| // 配色引擎:把底图 + 二维码合成为 MARD 色号网格 | |
| // 性能要点: | |
| // 1. 5bit 量化桶 → 候选 Top-K 查找表(懒加载),每格只精算约 10 个候选,而非整套 291 色 | |
| // 2. OKLab 感知距离 + 色间距离矩阵,全部 Typed Array,无对象分配 | |
| // 3. “颜色连贯度”参考已选中的左/上邻居色号,减少零碎杂色,拼起来更省事 | |
| // 4. 定位/时序/对齐等功能区使用更严格的深浅阈值,显著提升扫码成功率 | |
| const { rgbToOklabInto } = require('./color') | |
| const { MATCH_MODES } = require('./palette') | |
| const { PAPER } = require('./imaging') | |
| const TOP_K = 10 | |
| const C_ANY = 0 | |
| const C_DARK = 1 | |
| const C_LIGHT = 2 | |
| const C_FDARK = 3 | |
| const C_FLIGHT = 4 | |
| const C_QUIET = 5 | |
| const C_EMPTY = 6 | |
| function subsetFor(pal, mode, kind) { | |
| const key = `${mode.id}:${kind}` | |
| if (pal.subsets[key]) return pal.subsets[key] | |
| const { n, lum } = pal | |
| let test | |
| if (kind === C_DARK) test = (i) => lum[i] <= mode.darkMax | |
| else if (kind === C_LIGHT) test = (i) => lum[i] >= mode.lightMin | |
| else if (kind === C_FDARK) test = (i) => lum[i] <= Math.min(mode.darkMax, 0.3) | |
| else if (kind === C_FLIGHT) test = (i) => lum[i] >= Math.max(mode.lightMin, 0.82) | |
| else test = () => true | |
| let list = [] | |
| for (let i = 0; i < n; i += 1) if (test(i)) list.push(i) | |
| if (!list.length) { | |
| // 色库太小时退化为最深/最浅的 3 个颜色 | |
| const order = Array.from({ length: n }, (_, i) => i).sort((a, b) => lum[a] - lum[b]) | |
| list = kind === C_DARK || kind === C_FDARK ? order.slice(0, 3) : order.slice(-3) | |
| } | |
| const member = new Uint8Array(n) | |
| list.forEach((i) => { member[i] = 1 }) | |
| const subset = { key, list: Int16Array.from(list), member, lut: new Map() } | |
| pal.subsets[key] = subset | |
| return subset | |
| } | |
| const tmpLab = new Float32Array(3) | |
| function topKForBucket(pal, subset, bucket) { | |
| let hit = subset.lut.get(bucket) | |
| if (hit) return hit | |
| const r = ((bucket >> 10) & 31) * 8 + 4 | |
| const g = ((bucket >> 5) & 31) * 8 + 4 | |
| const b = (bucket & 31) * 8 + 4 | |
| rgbToOklabInto(r, g, b, tmpLab, 0) | |
| const { lab } = pal | |
| const list = subset.list | |
| const k = Math.min(TOP_K, list.length) | |
| const bestIdx = new Int16Array(k).fill(-1) | |
| const bestDist = new Float32Array(k).fill(Infinity) | |
| for (let t = 0; t < list.length; t += 1) { | |
| const i = list[t] | |
| const dl = lab[i * 3] - tmpLab[0] | |
| const da = lab[i * 3 + 1] - tmpLab[1] | |
| const db = lab[i * 3 + 2] - tmpLab[2] | |
| const d = dl * dl + da * da + db * db | |
| if (d >= bestDist[k - 1]) continue | |
| let p = k - 1 | |
| while (p > 0 && bestDist[p - 1] > d) { bestDist[p] = bestDist[p - 1]; bestIdx[p] = bestIdx[p - 1]; p -= 1 } | |
| bestDist[p] = d | |
| bestIdx[p] = i | |
| } | |
| hit = bestIdx | |
| subset.lut.set(bucket, hit) | |
| return hit | |
| } | |
| /** | |
| * @param base { size, rgb: Uint8Array, mask?: Uint8Array } 或 null(无底图) | |
| * @param qr makeQr() 结果 | |
| * @param placement createPlacement() 结果 | |
| * @param pal preparePalette() 结果 | |
| * @param options { modeId, coherence(0-100), cleanup(bool) } | |
| */ | |
| function computeArtwork(base, qr, placement, pal, options = {}) { | |
| const mode = MATCH_MODES.find((m) => m.id === options.modeId) || MATCH_MODES[1] | |
| const size = placement.size | |
| const total = size * size | |
| const lab = new Float32Array(total * 3) | |
| const bucket = new Int32Array(total) | |
| const kind = new Uint8Array(total) | |
| const { left, top, quiet } = placement | |
| for (let y = 0; y < size; y += 1) { | |
| for (let x = 0; x < size; x += 1) { | |
| const i = y * size + x | |
| const qx = x - left | |
| const qy = y - top | |
| const inQr = qx >= 0 && qy >= 0 && qx < qr.size && qy < qr.size | |
| const inQuiet = !inQr && x >= left - quiet && x < left + qr.size + quiet && y >= top - quiet && y < top + qr.size + quiet | |
| let r; let g; let b | |
| if (inQr) { | |
| const q = qy * qr.size + qx | |
| const isDark = qr.dark[q] | |
| const isFunc = qr.func[q] | |
| kind[i] = isFunc ? (isDark ? C_FDARK : C_FLIGHT) : (isDark ? C_DARK : C_LIGHT) | |
| if (base && !(base.mask && base.mask[i])) { | |
| r = base.rgb[i * 3]; g = base.rgb[i * 3 + 1]; b = base.rgb[i * 3 + 2] | |
| } else if (base) { | |
| r = PAPER[0]; g = PAPER[1]; b = PAPER[2] | |
| } else { | |
| r = g = b = isDark ? 0 : 255 | |
| } | |
| } else if (inQuiet || !base) { | |
| kind[i] = C_QUIET | |
| continue | |
| } else if (base.mask && base.mask[i]) { | |
| kind[i] = C_EMPTY | |
| continue | |
| } else { | |
| kind[i] = C_ANY | |
| r = base.rgb[i * 3]; g = base.rgb[i * 3 + 1]; b = base.rgb[i * 3 + 2] | |
| } | |
| rgbToOklabInto(r, g, b, lab, i * 3) | |
| bucket[i] = ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3) | |
| } | |
| } | |
| const n = pal.n | |
| const plab = pal.lab | |
| const pair = pal.pair | |
| const weight = (Math.max(0, Math.min(100, options.coherence == null ? 40 : options.coherence)) / 100) * 0.6 | |
| const subsets = [C_ANY, C_DARK, C_LIGHT, C_FDARK, C_FLIGHT].map((k) => subsetFor(pal, mode, k)) | |
| const cells = new Int16Array(total).fill(-1) | |
| const distTo = (c, i) => { | |
| const dl = plab[c * 3] - lab[i * 3] | |
| const da = plab[c * 3 + 1] - lab[i * 3 + 1] | |
| const db = plab[c * 3 + 2] - lab[i * 3 + 2] | |
| return Math.sqrt(dl * dl + da * da + db * db) | |
| } | |
| for (let y = 0; y < size; y += 1) { | |
| for (let x = 0; x < size; x += 1) { | |
| const i = y * size + x | |
| const k = kind[i] | |
| if (k >= C_QUIET) continue | |
| const subset = subsets[k] | |
| const cands = topKForBucket(pal, subset, bucket[i]) | |
| const nl = x > 0 ? cells[i - 1] : -1 | |
| const nu = y > 0 ? cells[i - size] : -1 | |
| const nbCount = (nl >= 0 ? 1 : 0) + (nu >= 0 ? 1 : 0) | |
| let best = -1 | |
| let bestScore = Infinity | |
| const consider = (c) => { | |
| let score = distTo(c, i) | |
| if (weight > 0 && nbCount) { | |
| let s = 0 | |
| if (nl >= 0) s += pair[c * n + nl] | |
| if (nu >= 0) s += pair[c * n + nu] | |
| score += weight * (s / nbCount) | |
| } | |
| if (score < bestScore) { bestScore = score; best = c } | |
| } | |
| for (let t = 0; t < cands.length; t += 1) if (cands[t] >= 0) consider(cands[t]) | |
| if (weight > 0) { | |
| if (nl >= 0 && subset.member[nl]) consider(nl) | |
| if (nu >= 0 && nu !== nl && subset.member[nu]) consider(nu) | |
| } | |
| cells[i] = best | |
| } | |
| } | |
| // 合并零散色:只用了 1~2 颗的色号并入最接近的常用色,减少需要购买的色号 | |
| const counts = new Int32Array(n) | |
| for (let i = 0; i < total; i += 1) if (cells[i] >= 0) counts[cells[i]] += 1 | |
| if (options.cleanup !== false) { | |
| const kept = [] | |
| for (let c = 0; c < n; c += 1) if (counts[c] >= 3) kept.push(c) | |
| if (kept.length) { | |
| for (let i = 0; i < total; i += 1) { | |
| const c = cells[i] | |
| if (c < 0 || counts[c] >= 3) continue | |
| const member = subsets[kind[i]].member | |
| let best = -1 | |
| let bestD = Infinity | |
| for (let t = 0; t < kept.length; t += 1) { | |
| const kc = kept[t] | |
| if (!member[kc]) continue | |
| const d = distTo(kc, i) | |
| if (d < bestD) { bestD = d; best = kc } | |
| } | |
| // 与原色差别过大时保留原色,避免破坏画面 | |
| if (best >= 0 && bestD <= distTo(c, i) + 12) cells[i] = best | |
| } | |
| counts.fill(0) | |
| for (let i = 0; i < total; i += 1) if (cells[i] >= 0) counts[cells[i]] += 1 | |
| } | |
| } | |
| // 二维码留白区:使用画面中最常用的浅色;没有则用色库里最浅的颜色 | |
| let quietColor = -1 | |
| let quietCount = -1 | |
| for (let c = 0; c < n; c += 1) { | |
| if (pal.lum[c] >= Math.max(0.8, mode.lightMin) && counts[c] > quietCount) { quietColor = c; quietCount = counts[c] } | |
| } | |
| if (quietCount <= 0) { | |
| quietColor = 0 | |
| for (let c = 1; c < n; c += 1) if (pal.lum[c] > pal.lum[quietColor]) quietColor = c | |
| } | |
| for (let i = 0; i < total; i += 1) { | |
| if (kind[i] === C_QUIET) { cells[i] = quietColor; counts[quietColor] += 1 } | |
| } | |
| const usage = [] | |
| let beads = 0 | |
| for (let c = 0; c < n; c += 1) { | |
| if (counts[c] > 0) { | |
| usage.push({ index: c, id: pal.ids[c], hex: pal.hexes[c], count: counts[c] }) | |
| beads += counts[c] | |
| } | |
| } | |
| usage.sort((a, b) => b.count - a.count) | |
| return { size, cells, usage, beads, pal, qr, placement } | |
| } | |
| module.exports = { computeArtwork } | |