mcuo's picture
download
raw
17.3 kB
/*!
* Anime2.5DRig — rigger.js
* PSD (parsed by ag-psd, useImageData mode) -> rig definition.
* Pure typed-array implementation: runs in browser and Node (testable).
* MIT License
*/
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
else root.Rigger = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
// ---------- layer naming ----------
function normName(n) {
n = (n || '').normalize('NFKC').trim().replace(/ のコピー\s*\d*$/,'').toLowerCase();
if (n === 'eyelash_c') n = 'eye_close'; // legacy aliases
if (n === 'mouth_c') n = 'mouth_close';
if (n === 'レイヤー 1') n = 'facedetail';
return n;
}
function baseName(n) { return n.replace(/_\d+$/, ''); }
var SLOTS = {
'back hair': { depth: 0.55, group: 'head', phys: 'hair' },
'bottomwear': { depth: 0.88, group: 'body' },
'neck': { depth: 0.95, group: 'body' },
'topwear': { depth: 0.90, group: 'body' },
'handwear': { depth: 0.86, group: 'body' },
'earwear': { depth: 0.97, group: 'head' },
'ears': { depth: 0.96, group: 'head' },
'face': { depth: 1.00, group: 'head' },
'facedetail': { depth: 1.02, group: 'head' },
'headwear': { depth: 1.20, group: 'head' },
'mouth_close': { depth: 1.08, group: 'head', fade: 'mouthClose' },
'mouth_open': { depth: 1.08, group: 'head', fade: 'mouthOpen' },
'nose': { depth: 1.15, group: 'head' },
'eyewhite': { depth: 1.06, group: 'head', split: true, fade: 'eyeOpen' },
'eyebrow': { depth: 1.14, group: 'head', split: true },
'irides': { depth: 1.08, group: 'head', split: true, fade: 'eyeOpen' },
'eyelash': { depth: 1.12, group: 'head', split: true, fade: 'eyeOpen' },
'eye_close': { depth: 1.12, group: 'head', split: true, fade: 'eyeClose' },
'front hair': { depth: 1.28, group: 'head', phys: 'hair' }
};
// ---------- image ops (full-canvas alpha as Uint8Array) ----------
function fullAlphaOf(layer, W, H) {
var a = new Uint8Array(W * H);
var img = layer.imageData, lw = img.width, lh = img.height;
var lx = layer.left | 0, ly = layer.top | 0, d = img.data;
for (var y = 0; y < lh; y++) {
var cy = y + ly; if (cy < 0 || cy >= H) continue;
var ro = cy * W, lo = y * lw;
for (var x = 0; x < lw; x++) {
var cx = x + lx; if (cx < 0 || cx >= W) continue;
a[ro + cx] = d[(lo + x) * 4 + 3];
}
}
return a;
}
function labelComponents(alpha, W, H, thr) {
var lab = new Int32Array(W * H), sizes = [0], sumX = [0], cnt = 0;
var stack = new Int32Array(W * H);
for (var s = 0; s < W * H; s++) {
if (lab[s] || alpha[s] <= thr) continue;
cnt++; var sp = 0; stack[sp++] = s; lab[s] = cnt;
var size = 0, sx = 0;
while (sp) {
var q = stack[--sp]; size++; sx += q % W;
var x = q % W, y = (q / W) | 0;
if (x > 0 && !lab[q - 1] && alpha[q - 1] > thr) { lab[q - 1] = cnt; stack[sp++] = q - 1; }
if (x < W - 1 && !lab[q + 1] && alpha[q + 1] > thr) { lab[q + 1] = cnt; stack[sp++] = q + 1; }
if (y > 0 && !lab[q - W] && alpha[q - W] > thr) { lab[q - W] = cnt; stack[sp++] = q - W; }
if (y < H - 1 && !lab[q + W] && alpha[q + W] > thr) { lab[q + W] = cnt; stack[sp++] = q + W; }
}
sizes.push(size); sumX.push(sx);
}
return { lab: lab, count: cnt, sizes: sizes, sumX: sumX };
}
function dilate(mask, W, H, r) {
var tmp = new Uint8Array(W * H), out = new Uint8Array(W * H), x, y, k;
for (y = 0; y < H; y++) {
var o = y * W;
for (x = 0; x < W; x++) {
var v = 0;
for (k = -r; k <= r; k++) { var xx = x + k; if (xx >= 0 && xx < W && mask[o + xx]) { v = 1; break; } }
tmp[o + x] = v;
}
}
for (x = 0; x < W; x++) for (y = 0; y < H; y++) {
var v2 = 0;
for (k = -r; k <= r; k++) { var yy = y + k; if (yy >= 0 && yy < H && tmp[yy * W + x]) { v2 = 1; break; } }
out[y * W + x] = v2;
}
return out;
}
// kill stray dust: keep components >= minPx (on alpha>16), dilate, zero the rest
function cleanAlpha(alpha, W, H, minPx) {
var L = labelComponents(alpha, W, H, 16);
if (!L.count) return alpha;
var keepComp = new Uint8Array(L.count + 1), any = false, i;
for (i = 1; i <= L.count; i++) if (L.sizes[i] >= minPx) { keepComp[i] = 1; any = true; }
if (!any) return alpha;
var mask = new Uint8Array(W * H);
for (i = 0; i < W * H; i++) if (keepComp[L.lab[i]]) mask[i] = 1;
mask = dilate(mask, W, H, 3);
for (i = 0; i < W * H; i++) if (!mask[i]) alpha[i] = 0;
return alpha;
}
// split into left/right masks by component centroid vs face center x
function splitSides(alpha, W, H, faceCx) {
var L = labelComponents(alpha, W, H, 16);
var side = new Uint8Array(L.count + 1); // 1=left 2=right
for (var c = 1; c <= L.count; c++) {
if (L.sizes[c] < 20) continue;
side[c] = (L.sumX[c] / L.sizes[c]) < faceCx ? 1 : 2;
}
var ml = new Uint8Array(W * H), mr = new Uint8Array(W * H), i;
for (i = 0; i < W * H; i++) {
if (side[L.lab[i]] === 1) ml[i] = 1; else if (side[L.lab[i]] === 2) mr[i] = 1;
}
var out = {};
var nl = 0, nr = 0;
for (i = 0; i < W * H; i++) { nl += ml[i]; nr += mr[i]; }
if (nl) out.l = dilate(ml, W, H, 3);
if (nr) out.r = dilate(mr, W, H, 3);
return out;
}
function bboxOf(alpha, W, H, thr) {
var x0 = W, y0 = H, x1 = -1, y1 = -1;
for (var y = 0; y < H; y++) {
var o = y * W;
for (var x = 0; x < W; x++) if (alpha[o + x] > thr) {
if (x < x0) x0 = x; if (x > x1) x1 = x;
if (y < y0) y0 = y; if (y > y1) y1 = y;
}
}
return x1 < 0 ? null : { x0: x0, y0: y0, x1: x1, y1: y1 };
}
function centroidOf(alpha, W, H) {
var sx = 0, sy = 0, s = 0;
for (var y = 0; y < H; y++) {
var o = y * W;
for (var x = 0; x < W; x++) { var a = alpha[o + x]; if (a) { sx += x * a; sy += y * a; s += a; } }
}
return s ? { cx: sx / s, cy: sy / s } : null;
}
// ---------- strand detection ----------
function findPeaks(a, minDist, minProm) {
var n = a.length, cand = [], i;
for (i = 1; i < n - 1; i++) if (a[i] > a[i - 1] && a[i] >= a[i + 1]) cand.push(i);
var peaks = [];
for (var ci = 0; ci < cand.length; ci++) {
var p = cand[ci], lmin = a[p], rmin = a[p], j;
for (j = p - 1; j >= 0; j--) { if (a[j] > a[p]) break; if (a[j] < lmin) lmin = a[j]; }
for (j = p + 1; j < n; j++) { if (a[j] > a[p]) break; if (a[j] < rmin) rmin = a[j]; }
var prom = a[p] - Math.max(lmin, rmin);
if (prom >= minProm) peaks.push({ x: p, prom: prom });
}
peaks.sort(function (u, v) { return v.prom - u.prom; });
var kept = [];
for (var k = 0; k < peaks.length; k++) {
var pk = peaks[k], ok = true;
for (var m = 0; m < kept.length; m++) if (Math.abs(kept[m].x - pk.x) < minDist) { ok = false; break; }
if (ok) kept.push(pk);
}
return kept; // prominence-ordered
}
function detectStrands(alpha, W, H, minSep, want) {
var bottom = new Float32Array(W), top = new Float32Array(W);
var minX = W, maxX = -1, x, y;
for (x = 0; x < W; x++) {
top[x] = -1; bottom[x] = 0;
for (y = 0; y < H; y++) if (alpha[y * W + x] > 16) { top[x] = y; break; }
if (top[x] < 0) continue;
for (y = H - 1; y >= 0; y--) if (alpha[y * W + x] > 16) { bottom[x] = y; break; }
if (x < minX) minX = x; if (x > maxX) maxX = x;
}
if (maxX < 0) return [];
// box smooth k=41, zero-padded 'same'
var k = 41, hk = 20, sm = new Float32Array(W);
var pre = new Float32Array(W + 1);
for (x = 0; x < W; x++) pre[x + 1] = pre[x] + bottom[x];
for (x = 0; x < W; x++) {
var a0 = Math.max(0, x - hk), a1 = Math.min(W - 1, x + hk);
sm[x] = (pre[a1 + 1] - pre[a0]) / k;
}
var pk = findPeaks(sm, minSep, 10);
var xs = [];
for (var i = 0; i < pk.length && xs.length < want; i++) xs.push(pk[i].x);
// fill up: farthest-from-existing candidates with content
var guard = 0;
while (xs.length < want && guard++ < 50) {
var best = -1, bestD = -1;
for (var t = 0; t < 40; t++) {
var cx = Math.round(minX + 30 + (maxX - minX - 60) * t / 39);
if (cx < 0 || cx >= W || top[cx] < 0) continue;
var dmin = 1e9;
for (var m = 0; m < xs.length; m++) dmin = Math.min(dmin, Math.abs(cx - xs[m]));
if (xs.length === 0) dmin = 1e9 - t;
if (dmin > bestD) { bestD = dmin; best = cx; }
}
if (best < 0) break;
xs.push(best);
}
xs.sort(function (a, b) { return a - b; });
var strands = [];
for (var s = 0; s < xs.length; s++) {
var sx = xs[s];
if (top[sx] < 0) continue;
strands.push({ x: sx, tipY: bottom[sx], rootY: top[sx] });
}
return strands;
}
// ---------- part record ----------
// mask: optional Uint8 side mask multiplied onto alpha
function makePart(name, layer, fullAlpha, mask, W, H, slot, z, side, strands) {
var eff = fullAlpha;
if (mask) {
eff = new Uint8Array(W * H);
for (var i = 0; i < W * H; i++) eff[i] = mask[i] ? fullAlpha[i] : 0;
}
var bb = bboxOf(eff, W, H, 8);
if (!bb) return null;
var pad = 2;
var x0 = Math.max(0, bb.x0 - pad), y0 = Math.max(0, bb.y0 - pad);
var x1 = Math.min(W, bb.x1 + 1 + pad), y1 = Math.min(H, bb.y1 + 1 + pad);
var w = x1 - x0, h = y1 - y0;
var data = new Uint8ClampedArray(w * h * 4);
var img = layer.imageData, lw = img.width, lh = img.height;
var lx = layer.left | 0, ly = layer.top | 0, ld = img.data;
for (var y = y0; y < y1; y++) {
for (var x = x0; x < x1; x++) {
var di = ((y - y0) * w + (x - x0)) * 4;
var yy = y - ly, xx = x - lx;
if (yy >= 0 && yy < lh && xx >= 0 && xx < lw) {
var li = (yy * lw + xx) * 4;
data[di] = ld[li]; data[di + 1] = ld[li + 1]; data[di + 2] = ld[li + 2];
}
data[di + 3] = eff[y * W + x];
}
}
return {
name: name, x: x0, y: y0, w: w, h: h, z: z,
depth: slot.depth, group: slot.group, phys: slot.phys || null,
fade: slot.fade || null, side: side || null, strands: strands || null,
img: { width: w, height: h, data: data }
};
}
// ---------- main ----------
function buildRig(psd, opts) {
opts = opts || {};
var W = psd.width, H = psd.height;
var warnings = [];
var kids = (psd.children || []).filter(function (c) { return c.imageData; });
if (!kids.length) throw new Error('レイヤーが見つかりません(グループは未対応・フラット構成にしてください)');
// full alphas, cleaned
var entries = [];
for (var i = 0; i < kids.length; i++) {
var name = normName(kids[i].name);
var fa = cleanAlpha(fullAlphaOf(kids[i], W, H), W, H, 40);
entries.push({ name: name, layer: kids[i], alpha: fa });
}
var byName = {};
entries.forEach(function (e) { byName[e.name] = e; });
// face anchor (required-ish)
var faceE = byName['face'];
var FACE;
if (faceE) {
var fb = bboxOf(faceE.alpha, W, H, 8), fc = centroidOf(faceE.alpha, W, H);
FACE = { cx: fc.cx, cy: fc.cy, x0: fb.x0, x1: fb.x1, y0: fb.y0, y1: fb.y1 };
} else {
warnings.push('face レイヤーがありません — キャンバス中央を顔とみなします');
FACE = { cx: W / 2, cy: H * 0.3, x0: W * 0.35, x1: W * 0.65, y0: H * 0.1, y1: H * 0.5 };
}
// build parts
var parts = [], z = 0, sided = {};
for (var ei = 0; ei < entries.length; ei++) {
var e = entries[ei];
var bn = baseName(e.name);
var slot = SLOTS[bn];
if (!slot) {
var c0 = centroidOf(e.alpha, W, H);
slot = { depth: 1.0, group: (c0 && c0.cy < FACE.y1) ? 'head' : 'body' };
warnings.push('未知のレイヤー名 "' + e.name + '" — ' + slot.group + ' として扱います');
}
if (slot.split) {
var masks = splitSides(e.alpha, W, H, FACE.cx);
var got = false;
['l', 'r'].forEach(function (s) {
if (!masks[s]) return;
var rec = makePart(e.name + '_' + s, e.layer, e.alpha, masks[s], W, H, slot, z, s.toUpperCase(), null);
if (rec) {
parts.push(rec); z++;
var ma = new Uint8Array(W * H);
for (var q = 0; q < W * H; q++) ma[q] = masks[s][q] ? e.alpha[q] : 0;
sided[e.name + '|' + s] = ma;
got = true;
}
});
if (!got) warnings.push('"' + e.name + '" の左右分離に失敗(空レイヤー?)');
} else if (slot.phys === 'hair') {
var isPart = /_\d+$/.test(e.name);
var bb2 = bboxOf(e.alpha, W, H, 16);
var wpx = bb2 ? (bb2.x1 - bb2.x0) : 0;
var want = isPart ? Math.max(2, Math.min(6, Math.round(wpx / 110))) : 6;
var minSep = Math.max(30, Math.round(wpx / (want * 1.6)));
var strands = detectStrands(e.alpha, W, H, minSep, want);
var rec2 = makePart(e.name, e.layer, e.alpha, null, W, H, slot, z, null, strands);
if (rec2) { parts.push(rec2); z++; }
} else {
var rec3 = makePart(e.name, e.layer, e.alpha, null, W, H, slot, z, null, null);
if (rec3) { parts.push(rec3); z++; }
}
}
// anchors
var anchors = { face: FACE };
['l', 'r'].forEach(function (s) {
var ew = sided['eyewhite|' + s], ir = sided['irides|' + s], ec = sided['eye_close|' + s];
var K = 'eye' + s.toUpperCase();
if (ew) {
var b = bboxOf(ew, W, H, 8);
var a = { x0: b.x0, x1: b.x1, y0: b.y0, y1: b.y1 };
var ic = ir ? centroidOf(ir, W, H) : centroidOf(ew, W, H);
a.icx = ic.cx; a.icy = ic.cy;
var cc = ec ? centroidOf(ec, W, H) : null;
a.closeY = cc ? cc.cy : (b.y0 + (b.y1 - b.y0) * 0.62);
anchors[K] = a;
}
});
if (!anchors.eyeL || !anchors.eyeR) warnings.push('目のアンカーが不完全です(eyewhite/irides を確認)');
// fallbacks below are relative to the detected face size, so they scale with any PSD
var faceW = FACE.x1 - FACE.x0, faceH = FACE.y1 - FACE.y0;
var mSrc = byName['mouth_open'] || byName['mouth_close'];
if (mSrc) {
var mb = bboxOf(mSrc.alpha, W, H, 8), mc = centroidOf(mSrc.alpha, W, H);
anchors.mouth = { x0: mb.x0, x1: mb.x1, y0: mb.y0, y1: mb.y1, cx: mc.cx, cy: mc.cy };
} else {
warnings.push('mouth_open / mouth_close がありません');
anchors.mouth = { x0: FACE.cx - faceW * 0.06, x1: FACE.cx + faceW * 0.06,
y0: FACE.cy + faceH * 0.10, y1: FACE.cy + faceH * 0.15, cx: FACE.cx, cy: FACE.cy + faceH * 0.13 };
}
var nE = byName['neck'];
if (nE) {
var nb = bboxOf(nE.alpha, W, H, 8), nc = centroidOf(nE.alpha, W, H);
anchors.neckPivot = { cx: nc.cx, cy: nb.y0 + (nb.y1 - nb.y0) * 0.85 };
anchors.neckTop = nb.y0; anchors.neckBottom = nb.y1;
} else {
anchors.neckPivot = { cx: FACE.cx, cy: FACE.y1 + faceH * 0.05 };
anchors.neckTop = FACE.y1; anchors.neckBottom = FACE.y1 + faceH * 0.15;
}
anchors.bodyPivot = { cx: anchors.neckPivot.cx, cy: H };
anchors.faceScale = faceW / 333.0;
anchors.hairRootY = FACE.y0 + faceH * 0.15;
return { canvas: { w: W, h: H }, layers: parts, anchors: anchors, warnings: warnings };
}
// in-place preprocessing: denoise every layer (connected components >= 40px)
// and trim to content bbox. Fixes PSDs with low-alpha noise across the canvas.
function cleanPsdLayers(psd) {
var stats = { noisy: 0, layers: 0 };
var kids = (psd.children || []).filter(function (c) { return c.imageData; });
for (var i = 0; i < kids.length; i++) {
var c = kids[i], img = c.imageData, W = img.width, H = img.height, d = img.data;
stats.layers++;
var a = new Uint8Array(W * H), j, before = 0, after = 0;
for (j = 0; j < W * H; j++) { a[j] = d[j * 4 + 3]; if (a[j]) before++; }
cleanAlpha(a, W, H, 40);
for (j = 0; j < W * H; j++) { if (a[j]) after++; d[j * 4 + 3] = a[j]; }
if (after < before) stats.noisy++;
var bb = bboxOf(a, W, H, 0);
if (!bb) continue;
var pad = 4;
var x0 = Math.max(0, bb.x0 - pad), y0 = Math.max(0, bb.y0 - pad);
var x1 = Math.min(W - 1, bb.x1 + pad), y1 = Math.min(H - 1, bb.y1 + pad);
var w = x1 - x0 + 1, h = y1 - y0 + 1;
if (w >= W && h >= H) continue;
var nd = new Uint8ClampedArray(w * h * 4);
for (var y = 0; y < h; y++) {
var so = ((y + y0) * W + x0) * 4;
nd.set(d.subarray(so, so + w * 4), y * w * 4);
}
c.imageData = { width: w, height: h, data: nd };
c.left = (c.left | 0) + x0; c.top = (c.top | 0) + y0;
c.right = c.left + w; c.bottom = c.top + h;
if (c.canvas) c.canvas = undefined;
}
return stats;
}
return { buildRig: buildRig, normName: normName, baseName: baseName, cleanPsdLayers: cleanPsdLayers,
_internals: { findPeaks: findPeaks, detectStrands: detectStrands,
labelComponents: labelComponents, cleanAlpha: cleanAlpha } };
});

Xet Storage Details

Size:
17.3 kB
·
Xet hash:
87c227dac18c4968052764a9f0438ea6950c8fecce8e02412d3635ebfc040f89

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.