/* 354-3 스크롤 동기화 메아리 재현 — 진짜 휠 이동(`attachWheelHorizontalScroll`)과 진짜 잇기 * (`linkScrollLeft`)를 가짜 창 · 가짜 스크롤러 위에서 돌려 JSON 으로 냄. * 가짜 창은 브라우저 차례 그대로 — scrollLeft 가 바뀌면 scroll 은 **다음 틀**에 오고(틀 앞머리), * 그 뒤 화면 틀 콜백이 돎. 옛 잇기(동기 플래그)와 새 잇기를 같은 휠로 견줌. * 경우 넷 = 표 켬/끔 × 분배(유토 전체 맞춤) 켬/끔 · 휠을 굴리는 쪽 = 종단 · 유토 둘. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const js = ts.transpileModule( fs.readFileSync(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Wheel.ts"), "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }, ).outputText; function fakeWindow() { const win = { now: 0, frames: [], seq: 0, pending: new Set() }; win.requestAnimationFrame = (fn) => (win.frames.push({ id: ++win.seq, fn }), win.seq); win.cancelAnimationFrame = (id) => (win.frames = win.frames.filter((f) => f.id !== id)); win.getSelection = () => ({ removeAllRanges() {} }); win.frame = () => { win.now += 16; const targets = [...win.pending]; win.pending.clear(); targets.forEach((el) => el.fire("scroll")); const due = win.frames; win.frames = []; due.forEach((f) => f.fn(win.now)); }; return win; } function scroller(win, name, { hidden = false, scrollWidth = 6000 } = {}) { const calls = {}; let left = 0; const el = { name, ownerDocument: { defaultView: win }, clientWidth: hidden ? 0 : 1000, scrollWidth: hidden ? 0 : scrollWidth, scrollTop: 0, style: {}, addEventListener: (type, fn) => (calls[type] ??= []).push(fn), removeEventListener() {}, hasPointerCapture: () => false, fire: (type, ev = {}) => { const e = { preventDefault() {}, ...ev }; (calls[type] ?? []).forEach((fn) => fn(e)); }, }; Object.defineProperty(el, "scrollLeft", { get: () => left, set: (v) => { const next = Math.min(Math.max(0, v), Math.max(0, el.scrollWidth - el.clientWidth)); if (next !== left) win.pending.add(el); left = next; }, }); return el; } /** 옛 잇기 — 유토곡선 · 표가 각자 들고 있던 동기 플래그 꼴 그대로. */ function oldLink(a, b, paused = () => false) { let syncing = false; const mirror = (from, to) => from.addEventListener("scroll", () => { if (syncing || paused()) return; syncing = true; to.scrollLeft = from.scrollLeft; syncing = false; }); mirror(a, b); mirror(b, a); } function run(kind, { table, balance, driver }) { const win = fakeWindow(); const wheel = { exports: {} }; new Function("exports", "module", "require", "window", js)( wheel.exports, wheel, () => ({ viewOf: () => win }), win, ); const body = scroller(win, "body"); // 분배 켬 = 유토곡선 전체 맞춤(스크롤 없음) · 잇기는 멈춤 const mass = scroller(win, "mass", { scrollWidth: balance ? 1000 : 6000 }); const tableEl = scroller(win, "table", { hidden: !table }); const link = kind === "old" ? oldLink : wheel.exports.linkScrollLeft; link(body, mass, () => balance); link(body, tableEl); const lead = driver === "mass" && !balance ? mass : body; [body, mass, tableEl].forEach((el) => wheel.exports.attachWheelHorizontalScroll(el)); const trace = []; for (let i = 0; i < 60; i += 1) { if (i < 20 && i % 2 === 0) lead.fire("wheel", { deltaY: 120 }); win.frame(); trace.push(body.scrollLeft); } let back = 0; for (let i = 1; i < trace.length; i += 1) if (trace[i] < trace[i - 1] - 0.01) back += 1; return { back, end: Math.round(body.scrollLeft), lead: Math.round(lead.scrollLeft), mass: Math.round(mass.scrollLeft), table: Math.round(tableEl.scrollLeft), }; } const out = {}; for (const kind of ["old", "new"]) for (const driver of ["body", "mass"]) for (const table of [true, false]) for (const balance of [false, true]) out[`${kind}|${driver}|table=${table}|balance=${balance}`] = run(kind, { table, balance, driver, }); process.stdout.write(JSON.stringify(out));