const monitor = []; function eachMonitor(p, f) { monitor.forEach((m) => { if (m[0] === p[0] && m[1] === p[1]) { f(m); } }); } function isMonitored(p) { let result = false; eachMonitor(p, (m) => { result = true; }); return result; } // 2D projection function worldToLocal(pointA, pointB, pointC) { const vectorAB = [pointB[0] - pointA[0], pointB[1] - pointA[1]]; const vectorAC = [pointC[0] - pointA[0], pointC[1] - pointA[1]]; const dot1 = dot2D(vectorAB, vectorAC); const dot2 = dot2D(vectorAB, vectorAB); const localX = dot1 / dot2; const pointD = [pointA[0] + localX * vectorAB[0], pointA[1] + localX * vectorAB[1]]; // localY = distance from pointC to pointD const vectorCD = [pointD[0] - pointC[0], pointD[1] - pointC[1]]; let localY = Math.sqrt(dot2D(vectorCD, vectorCD)); // if pointC is on the right side of vectorAB, localY is negative const cross = cross2D(vectorAB, vectorAC); if (0 < cross) { localY = -localY; } return [localX, localY]; } function distPointFromSeg(pointA, pointB, pointC, lx, ly) { if (lx < 0) { const vectorAC = [pointC[0] - pointA[0], pointC[1] - pointA[1]]; return Math.sqrt(dot2D(vectorAC, vectorAC)); } if (1.0 < lx) { const vectorBC = [pointC[0] - pointB[0], pointC[1] - pointB[1]]; return Math.sqrt(dot2D(vectorBC, vectorBC)); } return Math.abs(ly); } function localToWorld([pointA, pointB], pointC) { // C is local coordinate const vectorAB = [pointB[0] - pointA[0], pointB[1] - pointA[1]]; // pointD = pointA + C.x * vectorAB(垂線との交点) const pointD = [pointA[0] + pointC[0] * vectorAB[0], pointA[1] + pointC[0] * vectorAB[1]]; const v = perpendicular2D(vectorAB); const vLength = Math.sqrt(dot2D(v, v)); if (vLength < 0.0001) { return pointD; } const newLength = pointC[1]; const factor = newLength / vLength; return [pointD[0] + factor * v[0], pointD[1] + factor * v[1]]; } function makeEdgeSegments(pose) { return limbSeq.map((segment) => { const p0 = pose[segment[0]]; const p1 = pose[segment[1]]; return [p0, p1]; }); } function makeNormalizeEdgeSegments(edges) { return edges.map((edge) => normalize2D([edge[1][0] - edge[0][0], edge[1][1] - edge[0][1]])); } const fieldDepth = 5; const FO0i = 0; const FO0t = 1; const FO0u = 2; const FO0d = 3; // c current // p prev // n next // 0 primary // 1 secondary // e edge // s segment // i in // o out // t time // l local // q point // m main // d distance // w weight function calcJointRange( edgeSegments, normalizedEdgeSegments, edgeSegmentsForSideDecision, ep, ec, m) { // parent, child const np = normalizedEdgeSegments[ep]; const nc = normalizedEdgeSegments[ec]; let pnp = perpendicular2D(np); let pnc = perpendicular2D(nc); let side = getSide(edgeSegmentsForSideDecision[ep][0], edgeSegmentsForSideDecision[ep][1], edgeSegmentsForSideDecision[ec][1], m); if (side < 0) { pnp = reverse2D(pnp); pnc = reverse2D(pnc); } return [pnp, pnc]; } function calcJointRangeWithOrder(oldEdgeSegments, oldNormalizedEdgeSegments, newEdgeSegments, e0, m, order) { if (order < 0) { const e1 = findPrevEdgeIndex(e0); if (e1 < 0) { return null; } const [pn1, pn0] = calcJointRange(oldEdgeSegments, oldNormalizedEdgeSegments, newEdgeSegments,e1, e0, m); const oldPivot = oldEdgeSegments[e0][0]; const newPivot = newEdgeSegments[e0][0]; return [pn0, pn1, oldPivot, newPivot, e1]; } else if (0 < order) { const e1 = findNextEdgeIndex(e0); if (e1 < 0) { return null; } const [pn0, pn1] = calcJointRange(oldEdgeSegments, oldNormalizedEdgeSegments, newEdgeSegments, e0, e1, m); const oldPivot = oldEdgeSegments[e0][1]; const newPivot = newEdgeSegments[e0][1]; return [pn0, pn1, oldPivot, newPivot, e1]; } else { return null; } } function buildWeightMap(size, pose) { const threshold = 512; const [w, h] = size; // この状態で、canvasに対して、 // 同じ大きさの3次元配列[x,y,z]を作成する // x,yは画像の2次元座標 // 各Pixelに対して、 // そのPixelに最も近いエッジのindexを[0]に、 // そのときのエッジ座標系におけるtを[1]に記録する // どの骨格とも近くないものは[0]=255とする const field = new Float32Array(w * h * fieldDepth) function fieldIndex(x, y) { return (y * w + x) * fieldDepth; } const edgeSegments = makeEdgeSegments(pose); const normalizedEdgeSegments = makeNormalizeEdgeSegments(edgeSegments); // 各Pixelに対して、一番近いエッジを探す // またそのエッジ座標系におけるtを保存する for (let y = 0 ; y < h ; y++) { for (let x = 0 ; x < w ; x++) { const m = [x, y]; let minDist = threshold; const fidx = fieldIndex(x, y); field[fidx+FO0i] = -1; for (let i = 0 ; i < limbSeq.length ; i++) { // 線分から点への距離を計算 const s = edgeSegments[i]; const lq = worldToLocal(s[0], s[1], m); const dist = distPointFromSeg(s[0], s[1], m, lq[0], lq[1]); if (dist < minDist) { minDist = dist; field[fidx+FO0i] = i; field[fidx+FO0t] = lq[0]; field[fidx+FO0u] = lq[1]; field[fidx+FO0d] = dist; } } } } return field; } function renderField(canvas2, size, field) { function blend(c0, w0, c1, w1) { const r = (c0[0] * w0 + c1[0] * w1); const g = (c0[1] * w0 + c1[1] * w1); const b = (c0[2] * w0 + c1[2] * w1); const a = (c0[3] * w0 + c1[3] * w1); return [r, g, b, a]; } const [w, h] = size; function fieldIndex(x, y) { return (y * w + x) * fieldDepth; } canvas2.width = w; canvas2.height = h; const ctx2 = canvas2.getContext('2d'); bindings = ctx2.createImageData(w, h); const data = bindings.data; for (let y = 0 ; y < h ; y++) { for (let x = 0 ; x < w ; x++) { const fidx = fieldIndex(x, y); const oi = (y * w + x) * 4; let c = [0, 0, 0, 64]; const e0 = field[fidx+FO0i]; if (0 <= e0) { c = colors[e0]; c[3] = 255; // c[3] = 255 - field[fidx+FO0d] * 4; } data[oi + 0] = c[0]; data[oi + 1] = c[1]; data[oi + 2] = c[2]; data[oi + 3] = c[3]; } } ctx2.putImageData(bindings, 0, 0); return field; } function makeImageDataFromPicture(canvas3, size, picture) { const [w, h] = size; canvas3.width = w; canvas3.height = h; const ctx3 = canvas3.getContext("2d"); // ctx3.drawImage(picture, sampleOffset[0], sampleOffset[1]); ctx3.drawImage(picture, 0, 0); return new ImageData(new Uint8ClampedArray(ctx3.getImageData(0, 0, w, h).data), w, h); } function animatePicture(canvas4, size, oldPose, newPose, srcImageData, initialPoseField) { const srcData = srcImageData.data; const [w, h] = size; const ctx4 = canvas4.getContext("2d"); canvas4.width = w; canvas4.height = h; const dstCtx = ctx4; const dstImageData = dstCtx.createImageData(w, h); const dstData = dstImageData.data; const field = buildWeightMap(size, newPose); const canvas2 = document.getElementById("canvas2"); // renderField(canvas2, size, field); drawBodyPoseTo(canvas2.getContext("2d"), [newPose]); function fieldIndex(x, y) { return (y * w + x) * fieldDepth; } const oldEdgeSegments = makeEdgeSegments(oldPose); const oldNormalizedEdgeSegments = makeNormalizeEdgeSegments(oldEdgeSegments); const newEdgeSegments = makeEdgeSegments(newPose); function oldEdgePoint(edgeIndex, p) { const v = oldEdgeSegments[edgeIndex]; return localToWorld(v, p); } function newEdgePoint(edgeIndex, p) { const v = newEdgeSegments[edgeIndex]; return localToWorld(v, p); } debugLines = []; for (let y = 0 ; y < h ; y++) { for (let x = 0 ; x < w ; x++) { // 各点に対して、fieldから近隣エッジ座標系でのローカル座標を取得する const oi = (y * w + x) * 4; const fidx = fieldIndex(x, y); const e0 = field[fidx+FO0i]; if (e0 < 0) { dstData[oi + 0] = 0; dstData[oi + 1] = 0; dstData[oi + 2] = 0; dstData[oi + 3] = 255; continue; } const lq = [field[fidx+FO0t], field[fidx+FO0u]]; let e1 = -1; let order = 0; // 1 == forward, -1 == backward if (lq[0] < 0.5) { e1 = findPrevEdgeIndex(e0); } else if (0.5 <= lq[0]) { e1 = findNextEdgeIndex(e0); } if (0 <= e1) { if (lq[0] < 0) { order = -1; } else if (1 < lq[0]) { order = 1; } } let iq = oldEdgePoint(e0, lq); if (0 != order) { // サイド判定は新ポーズで行う const mNew = [x,y]; const jointRange = calcJointRangeWithOrder( oldEdgeSegments, oldNormalizedEdgeSegments, newEdgeSegments, e0, mNew, order); const [pn0, pn1, oldPivot, newPivot, _] = jointRange; const pivotMNew = [x - newPivot[0], y - newPivot[1]]; const [w0, w1]= [angleBetween(pn0, pivotMNew), angleBetween(pivotMNew, pn1)]; let d0 = field[fidx+FO0d]; let mt = slerp2D(pn0, pn1, w0 / (w0 + w1)); iq = [oldPivot[0] + mt[0] * d0, oldPivot[1] + mt[1] * d0]; eachMonitor([x,y], () => { console.log('animate', x, y, ix, iy, e1, w0, w1, pn0, pn1); debugLines.push([oldPivot, pn0]); debugLines.push([oldPivot, pn1]); }); } iq = [Math.round(iq[0]), Math.round(iq[1])]; var initialOwner = initialPoseField[fieldIndex(iq[0], iq[1]) + FO0i]; if (0 <= initialOwner && initialOwner != e0 && initialOwner != e1) { // ほかの領土 dstData[oi + 0] = 0; dstData[oi + 1] = 0; dstData[oi + 2] = 0; dstData[oi + 3] = 0; } else { // canvas3の[ix,iy]の色を取得して、canvas4の[x,y]に描画する const si = (iq[1] * w + iq[0]) * 4; dstData[oi + 0] = srcData[si + 0]; dstData[oi + 1] = srcData[si + 1]; dstData[oi + 2] = srcData[si + 2]; dstData[oi + 3] = srcData[si + 3]; } } } dstCtx.putImageData(dstImageData, 0, 0); } function handleMicroscopeMouseMove(e) { const [x,y] = mouseCursor; const imageData = ctx.getImageData(x - 2, y - 2, 5, 5); const data = imageData.data; const microscope = document.getElementById('microscope'); const ctx2 = microscope.getContext('2d'); ctx2.clearRect(0, 0, microscope.width, microscope.height); for (let i = 0 ; i < 5 ; i++) { for (let j = 0 ; j < 5 ; j++) { const idx = (i * 5 + j) * 4; ctx2.fillStyle = `rgba(${data[idx]}, ${data[idx+1]}, ${data[idx+2]}, ${data[idx+3]})`; ctx2.fillRect(j * 10, i * 10, 10, 10); } } } function initMicroscope() { // canvas.addEventListener('mousemove', handleMicroscopeMouseMove); // poseData[0][6] = [329, 148]; // poseData[0][7] = [329, 194]; }