Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
122 lines
4.3 KiB
JavaScript
122 lines
4.3 KiB
JavaScript
/* 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));
|