Skip to content

Commit 9c0923b

Browse files
authored
Merge pull request #72 from iplweb/cleanup/faza-7-js
refactor(js): podziel selects.js i cursor.js, domknij hook o .js (faza 7)
2 parents 32ac72e + ee208f3 commit 9c0923b

13 files changed

Lines changed: 1355 additions & 1303 deletions

File tree

.pre-commit-config.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ repos:
1919
name: check file length (≤ 600 lines)
2020
entry: python scripts/check_file_length.py
2121
language: python
22-
# Scope is .py only for now, across guidebot_recorder/ and tests/ (CI's
23-
# C901 gate is `ruff check .`, which also covers both). Add |\.js$ here
24-
# once selects.js / cursor.js are split (phase 7)not before, or it
25-
# would fire on the still-unsplit .js files.
26-
files: ^(guidebot_recorder|tests|scripts)/.*\.py$
22+
# Covers .py across guidebot_recorder/ and tests/ (CI's C901 gate is
23+
# `ruff check .`, which also covers both) and the injected .js under
24+
# guidebot_recorder/ (split in phase 7 — each fragment is glued back
25+
# byte-identically in Python; see selects/selects.py, overlay/overlay.py).
26+
files: ^(guidebot_recorder/.*\.(py|js)|(tests|scripts)/.*\.py)$

guidebot_recorder/overlay/cursor.js

Lines changed: 0 additions & 330 deletions
Original file line numberDiff line numberDiff line change
@@ -460,333 +460,3 @@
460460
});
461461
}
462462

463-
function styleTransient(element, zIndex) {
464-
element.setAttribute("aria-hidden", "true");
465-
setImportant(element, "position", "fixed");
466-
setImportant(element, "display", "block");
467-
setImportant(element, "margin", "0");
468-
setImportant(element, "padding", "0");
469-
setImportant(element, "pointer-events", "none");
470-
setImportant(element, "z-index", zIndex);
471-
setImportant(element, "box-sizing", "border-box");
472-
}
473-
474-
function removeAfterAnimation(element, animation, fallbackMs) {
475-
animation.addEventListener("finish", () => element.remove(), { once: true });
476-
animation.addEventListener("cancel", () => element.remove(), { once: true });
477-
window.setTimeout(() => element.remove(), fallbackMs);
478-
}
479-
480-
function ripple(flash = false) {
481-
if (!ensure()) {
482-
return false;
483-
}
484-
const root = mountRoot();
485-
if (!root) {
486-
return false;
487-
}
488-
489-
const click = CFG.click || {};
490-
const ringColor = click.color || "rgba(37, 99, 235, .9)";
491-
const endScale = Number.isFinite(Number(click.scale)) ? Number(click.scale) : 3.25;
492-
493-
const ring = document.createElement("div");
494-
ring.setAttribute("data-guidebot-ripple", "");
495-
styleTransient(ring, "2147483646");
496-
setImportant(ring, "left", `${state.x - 8}px`);
497-
setImportant(ring, "top", `${state.y - 8}px`);
498-
setImportant(ring, "width", "16px");
499-
setImportant(ring, "height", "16px");
500-
setImportant(ring, "border", `3px solid ${ringColor}`);
501-
setImportant(ring, "border-radius", "9999px");
502-
root.appendChild(ring);
503-
504-
const animation = ring.animate(
505-
[
506-
{ opacity: 0.95, transform: "scale(.35)" },
507-
{ opacity: 0, transform: `scale(${endScale})` },
508-
],
509-
{ duration: 500, easing: "cubic-bezier(.16,1,.3,1)", fill: "forwards" },
510-
);
511-
removeAfterAnimation(ring, animation, 600);
512-
513-
if (flash && click.flash) {
514-
const disc = document.createElement("div");
515-
disc.setAttribute("data-guidebot-flash", "");
516-
styleTransient(disc, "2147483645");
517-
setImportant(disc, "left", `${state.x - 8}px`);
518-
setImportant(disc, "top", `${state.y - 8}px`);
519-
setImportant(disc, "width", "16px");
520-
setImportant(disc, "height", "16px");
521-
setImportant(disc, "background", ringColor);
522-
setImportant(disc, "border-radius", "9999px");
523-
root.appendChild(disc);
524-
525-
const flashAnimation = disc.animate(
526-
[
527-
{ opacity: 0.55, transform: "scale(.2)" },
528-
{ opacity: 0, transform: "scale(2)" },
529-
],
530-
{ duration: 420, easing: "cubic-bezier(.16,1,.3,1)", fill: "forwards" },
531-
);
532-
removeAfterAnimation(disc, flashAnimation, 520);
533-
}
534-
535-
return true;
536-
}
537-
538-
/**
539-
* Lap an ellipse around a target, drawing a marker trail behind the cursor.
540-
*
541-
* This is what the `highlight` scenario command animates. It has nothing to do
542-
* with the `highlight()` function below, which draws a one-off pulsing
543-
* rectangle and survives only for API compatibility: the names collide, the
544-
* features do not.
545-
*
546-
* The loop mirrors moveTo's on purpose: progress comes from the clock (a
547-
* dropped frame must not desynchronize us from the duration Python treats as
548-
* authoritative), the handle lives in `state.raf` so `cancelMove()` can preempt
549-
* us, and a fallback timer settles the promise even when rAF stops firing in a
550-
* backgrounded document — without it a recording would hang forever.
551-
*/
552-
/**
553-
* Angle lookup that walks an ellipse at a CONSTANT SPEED.
554-
*
555-
* Stepping the angle linearly would not: on an ellipse, equal angles cover
556-
* wildly unequal arcs (at rx/ry = 8 the cursor crawls around the ends and
557-
* bolts along the flat runs, ~5x apart), and the trail — which is drawn by
558-
* arc length via stroke-dashoffset — would come unstuck from the cursor
559-
* mid-lap. Sampling cumulative chord length once and inverting it keeps both
560-
* on the same clock.
561-
*/
562-
function arcLengthTable(rx, ry, samples = ENCIRCLE_ARC_SAMPLES) {
563-
const cumulative = new Float64Array(samples + 1);
564-
let previousX = rx;
565-
let previousY = 0;
566-
for (let i = 1; i <= samples; i++) {
567-
const angle = (i / samples) * 2 * Math.PI;
568-
const x = rx * Math.cos(angle);
569-
const y = ry * Math.sin(angle);
570-
cumulative[i] = cumulative[i - 1] + Math.hypot(x - previousX, y - previousY);
571-
previousX = x;
572-
previousY = y;
573-
}
574-
const total = cumulative[samples];
575-
return (fraction) => {
576-
if (!(total > 0)) {
577-
return fraction * 2 * Math.PI;
578-
}
579-
const wanted = fraction * total;
580-
let low = 0;
581-
let high = samples;
582-
while (low + 1 < high) {
583-
const mid = (low + high) >> 1;
584-
if (cumulative[mid] <= wanted) {
585-
low = mid;
586-
} else {
587-
high = mid;
588-
}
589-
}
590-
const span = cumulative[high] - cumulative[low];
591-
const within = span > 0 ? (wanted - cumulative[low]) / span : 0;
592-
return ((low + within) / samples) * 2 * Math.PI;
593-
};
594-
}
595-
596-
function encircle(cx, cy, rx, ry, options = {}) {
597-
const centreX = Number(cx);
598-
const centreY = Number(cy);
599-
const radiusX = Number(rx);
600-
const radiusY = Number(ry);
601-
if (
602-
![centreX, centreY, radiusX, radiusY].every(Number.isFinite) ||
603-
radiusX <= 0 ||
604-
radiusY <= 0
605-
) {
606-
throw new TypeError("encircle needs a finite centre and positive radii");
607-
}
608-
const laps = Math.max(1, Math.round(Number(options.loops) || 1));
609-
const msPerLap = Math.max(1, Number(options.msPerLap) || 900);
610-
const holdMs = Math.max(0, Number(options.holdMs) || 0);
611-
const color = options.color || CURSOR_GLOW;
612-
613-
cancelMove();
614-
const cursor = ensure();
615-
const root = mountRoot();
616-
// The lap starts and ends at 3 o'clock — where an SVG ellipse path also
617-
// starts — so the trail unrolls exactly under the cursor.
618-
const entryX = centreX + radiusX;
619-
state.x = entryX;
620-
state.y = centreY;
621-
if (!cursor || !root) {
622-
scheduleMount();
623-
return Promise.resolve();
624-
}
625-
626-
const svg = document.createElementNS(SVG_NS, "svg");
627-
svg.setAttribute("aria-hidden", "true");
628-
setImportant(svg, "position", "fixed");
629-
setImportant(svg, "left", "0px");
630-
setImportant(svg, "top", "0px");
631-
setImportant(svg, "width", "100%");
632-
setImportant(svg, "height", "100%");
633-
setImportant(svg, "overflow", "visible");
634-
setImportant(svg, "pointer-events", "none");
635-
setImportant(svg, "z-index", "2147483645");
636-
637-
const trail = document.createElementNS(SVG_NS, "ellipse");
638-
trail.setAttribute("data-guidebot-encircle", "");
639-
trail.setAttribute("cx", centreX);
640-
trail.setAttribute("cy", centreY);
641-
trail.setAttribute("rx", radiusX);
642-
trail.setAttribute("ry", radiusY);
643-
trail.setAttribute("fill", "none");
644-
trail.setAttribute("stroke", color);
645-
trail.setAttribute("stroke-width", ENCIRCLE_STROKE_WIDTH);
646-
trail.setAttribute("stroke-linecap", "round");
647-
svg.appendChild(trail);
648-
root.appendChild(svg);
649-
650-
const perimeter =
651-
typeof trail.getTotalLength === "function"
652-
? trail.getTotalLength()
653-
: Math.PI * (radiusX + radiusY);
654-
trail.style.strokeDasharray = `${perimeter}`;
655-
trail.style.strokeDashoffset = `${perimeter}`;
656-
657-
setImportant(cursor, "transition", "none");
658-
const spinMs = laps * msPerLap;
659-
const angleAt = arcLengthTable(radiusX, radiusY);
660-
661-
return new Promise((resolve) => {
662-
let start = null;
663-
let fallbackTimer = 0;
664-
665-
const settle = () => {
666-
window.clearTimeout(fallbackTimer);
667-
state.raf = null;
668-
state.finishMove = null;
669-
const fade = svg.animate([{ opacity: 1 }, { opacity: 0 }], {
670-
duration: ENCIRCLE_FADE_MS,
671-
easing: "ease-out",
672-
fill: "forwards",
673-
});
674-
removeAfterAnimation(svg, fade, ENCIRCLE_FADE_MS + 100);
675-
// Resolve only once the trail has faded, so the next step never opens on
676-
// a frame that still carries this one's mark.
677-
window.setTimeout(resolve, ENCIRCLE_FADE_MS);
678-
};
679-
// `cancelMove` releases a superseded animation through this hook; without
680-
// it a newer move would leave this promise pending forever.
681-
state.finishMove = settle;
682-
683-
const land = () => {
684-
setImportant(cursor, "left", `${entryX}px`);
685-
setImportant(cursor, "top", `${centreY}px`);
686-
trail.style.strokeDashoffset = "0";
687-
settle();
688-
};
689-
690-
const step = (timestamp) => {
691-
if (start === null) {
692-
start = timestamp;
693-
}
694-
const elapsed = timestamp - start;
695-
if (elapsed >= spinMs + holdMs) {
696-
land(); // the final frame puts the cursor exactly back on the entry point
697-
return;
698-
}
699-
// Only the spin phase advances the angle; during the hold the cursor
700-
// rests at the entry point and the trail stands whole.
701-
const spun = Math.min(elapsed, spinMs);
702-
const laps = spun / msPerLap;
703-
const angle = angleAt(laps - Math.floor(laps));
704-
setImportant(cursor, "left", `${centreX + radiusX * Math.cos(angle)}px`);
705-
setImportant(cursor, "top", `${centreY + radiusY * Math.sin(angle)}px`);
706-
// The first lap draws the trail; later laps trace over what is there.
707-
trail.style.strokeDashoffset = `${perimeter * (1 - Math.min(1, spun / msPerLap))}`;
708-
state.raf = window.requestAnimationFrame(step);
709-
};
710-
711-
fallbackTimer = window.setTimeout(() => {
712-
if (state.raf !== null) {
713-
window.cancelAnimationFrame(state.raf);
714-
state.raf = null;
715-
}
716-
land();
717-
}, spinMs + holdMs + 50);
718-
state.raf = window.requestAnimationFrame(step);
719-
});
720-
}
721-
722-
function highlight(x, y, width, height) {
723-
const values = [x, y, width, height].map(Number);
724-
if (!values.every(Number.isFinite) || values[2] < 0 || values[3] < 0) {
725-
throw new TypeError("highlight bounds must be finite with non-negative size");
726-
}
727-
const root = mountRoot();
728-
if (!root) {
729-
scheduleMount();
730-
return false;
731-
}
732-
733-
const box = document.createElement("div");
734-
box.setAttribute("data-guidebot-highlight", "");
735-
styleTransient(box, "2147483645");
736-
setImportant(box, "left", `${values[0]}px`);
737-
setImportant(box, "top", `${values[1]}px`);
738-
setImportant(box, "width", `${values[2]}px`);
739-
setImportant(box, "height", `${values[3]}px`);
740-
setImportant(box, "border", "3px solid rgba(37, 99, 235, .95)");
741-
setImportant(box, "border-radius", "6px");
742-
setImportant(box, "background", "rgba(59, 130, 246, .12)");
743-
setImportant(box, "box-shadow", "0 0 0 4px rgba(59, 130, 246, .16)");
744-
root.appendChild(box);
745-
746-
const animation = box.animate(
747-
[
748-
{ opacity: 0, transform: "scale(.98)" },
749-
{ opacity: 1, transform: "scale(1)", offset: 0.2 },
750-
{ opacity: 0, transform: "scale(1.015)" },
751-
],
752-
{ duration: 800, easing: "ease-out", fill: "forwards" },
753-
);
754-
removeAfterAnimation(box, animation, 900);
755-
return true;
756-
}
757-
758-
function hide() {
759-
hidden = true;
760-
const cursor = document.querySelector(CURSOR_SELECTOR);
761-
if (cursor) {
762-
setImportant(cursor, "display", "none");
763-
}
764-
}
765-
766-
function show() {
767-
hidden = false;
768-
ensure();
769-
}
770-
771-
const api = {
772-
__guidebotVersion: API_VERSION,
773-
ensure,
774-
moveTo,
775-
ripple,
776-
highlight,
777-
encircle,
778-
hide,
779-
show,
780-
get position() {
781-
return [state.x, state.y];
782-
},
783-
};
784-
785-
Object.defineProperty(window, API_KEY, {
786-
configurable: true,
787-
enumerable: false,
788-
writable: true,
789-
value: api,
790-
});
791-
ensure();
792-
})();

0 commit comments

Comments
 (0)