# -*- coding: utf-8 -*- """舞台型ショートの「文字が被っている所」を探す(2026-11-07 エイイチ「文字が被ってるところがある」) 使い方: 作業フォルダで butai_kit.py --still を1回走らせて _block.html を作ってから python3 ../舞台型_共通/check_overlap.py 総尺秒 見るもの: 1. モニターの中で、文字どうしが重なっている所(0.4秒以上続くものだけ。画面の切り替えの一瞬は数えない) 2. モニターの中の文字が、エイイチの体の上 40%(頭と肩)にかかる所 (エイイチは絵を後から重ねるのでページの中には無い。butai_kit.py --still が out/ei_rects.json に0.1秒ごとの位置を書く) """ import sys, os, json from playwright.sync_api import sync_playwright TOTAL = float(sys.argv[1]) EIR = {round(r[0], 1): r[1:] for r in json.load(open("out/ei_rects.json"))} JS = r""" (EI) => { const scr = document.querySelector('.scr'); const out = []; const els = [...scr.querySelectorAll('*')].filter(e => [...e.childNodes].some(n => n.nodeType === 3 && n.textContent.trim())); const sr = scr.getBoundingClientRect(); const effOp = (q) => { let o = 1; while (q && q !== scr) { const cs = getComputedStyle(q); o *= parseFloat(cs.opacity); if (cs.display === 'none' || cs.visibility === 'hidden') return 0; q = q.parentElement; } return o; }; els.forEach((e, i) => { let op = 1, a = e; while (a && a !== scr) { const cs = getComputedStyle(a); op *= parseFloat(cs.opacity); if (cs.display === 'none' || cs.visibility === 'hidden') op = 0; a = a.parentElement; } if (op < 0.5) return; const rg = document.createRange(); let r = null; e.childNodes.forEach(n => { if (n.nodeType === 3 && n.textContent.trim()) { rg.selectNodeContents(n); for (const q of rg.getClientRects()) { r = r ? {l: Math.min(r.l, q.left), t: Math.min(r.t, q.top), r: Math.max(r.r, q.right), b: Math.max(r.b, q.bottom)} : {l: q.left, t: q.top, r: q.right, b: q.bottom}; } } }); if (!r || r.r - r.l < 2) return; r = {l: Math.max(r.l, sr.left), t: Math.max(r.t, sr.top), r: Math.min(r.r, sr.right), b: Math.min(r.b, sr.bottom)}; if (r.r <= r.l || r.b <= r.t) return; // 見えている点があるか(上に別の窓が重なって隠れていないか) let vis = 0; for (let gx = 1; gx < 8; gx++) for (let gy = 1; gy < 4; gy++) { const x = r.l + (r.r - r.l) * gx / 8, y = r.t + (r.b - r.t) * gy / 4; // いちばん上に見えているモニターの中の要素(舞台の外の飾りや、透明になった覆いは飛ばす) const h = document.elementsFromPoint(x, y).find(q => q !== scr && scr.contains(q) && effOp(q) > 0.05); if (h && (h === e || e.contains(h) || h.contains(e))) vis++; } out.push({id: i, txt: e.textContent.trim().slice(0, 14), r, vis, el: e}); }); const res = []; for (let i = 0; i < out.length; i++) { const A = out[i]; if (A.vis && A.r.r > EI[0] + 6 && A.r.l < EI[2] - 6 && A.r.b > EI[1] + 6 && A.r.t < EI[3]) res.push(['頭', A.txt, Math.round(A.r.l), Math.round(A.r.t)]); for (let j = i + 1; j < out.length; j++) { const B = out[j]; if (A.el.contains(B.el) || B.el.contains(A.el)) continue; const w = Math.min(A.r.r, B.r.r) - Math.max(A.r.l, B.r.l), h = Math.min(A.r.b, B.r.b) - Math.max(A.r.t, B.r.t); if (w > 4 && h > 4 && A.vis && B.vis) { // 両方とも見えている所があるときだけ(上の窓で覆われた文字は数えない) const s = Math.min((A.r.r - A.r.l) * (A.r.b - A.r.t), (B.r.r - B.r.l) * (B.r.b - B.r.t)); if (w * h > 0.04 * s) res.push(['重', A.txt + ' × ' + B.txt, Math.round(Math.max(A.r.l, B.r.l)), Math.round(Math.max(A.r.t, B.r.t))]); } } } return res; } """ seen = {} with sync_playwright() as p: br = p.chromium.launch(); pg = br.new_page(viewport={"width": 1080, "height": 1920}) pg.goto("file://" + os.path.abspath("_block.html")); pg.evaluate("document.fonts.ready"); pg.wait_for_timeout(300) t = 0.0 while t < TOTAL: pg.evaluate(f"window.setT({t:.2f})") for kind, what, x, y in pg.evaluate(JS, EIR.get(round(t, 1), [0, 9999, 0, 0])): k = (kind, what) seen.setdefault(k, []).append(round(t, 1)) t += 0.1 br.close() bad = 0 for (kind, what), ts in seen.items(): if len(ts) >= (2 if kind == '頭' else 4): bad += 1 print(f"{'文字どうし' if kind == '重' else 'エイイチの頭'}: {what} {ts[0]}〜{ts[-1]}秒({len(ts) / 10:.1f}秒)") print("被り", bad, "件")