Files
Aislo/resources/tester/helper_354_3_scroll_echo.cjs
T

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));