미디어위키:MainPageManifesto.js: 두 판 사이의 차이

(Install package: clbiwiki-mainpage-mud-optical-volume-center-20260712 / js/MainPageManifesto.js)
 
(Install package: clbiwiki-mainpage-yisunshin-galmuri-method-20260712 / js/MainPageManifesto.js)
태그: 수동 되돌리기
 
(같은 사용자의 중간 판 4개는 보이지 않습니다)
9번째 줄: 9번째 줄:
   centered inside the 660px manifesto column. The title remains geometrically
   centered inside the 660px manifesto column. The title remains geometrically
   centered and the vertical -100px placement remains owned by MainPage.css.
   centered and the vertical -100px placement remains owned by MainPage.css.
  The title divider is the hard horizontal boundary. Optical correction is
  clamped so no rendered line can cross either end of that divider.
*/
*/
(function (window, document, mw) {
(function (window, document, mw) {
16번째 줄: 19번째 줄:
     var SHIFT_PROPERTY = '--manifesto-optical-shift-x';
     var SHIFT_PROPERTY = '--manifesto-optical-shift-x';
     var MAX_SHIFT = 48;
     var MAX_SHIFT = 48;
    var DIVIDER_SAFE_INSET = 1;
     var resizeTimer = 0;
     var resizeTimer = 0;
     var observed = typeof WeakSet === 'function' ? new WeakSet() : null;
     var observed = typeof WeakSet === 'function' ? new WeakSet() : null;
52번째 줄: 56번째 줄:
         var totalWeight = 0;
         var totalWeight = 0;
         var desiredCenter;
         var desiredCenter;
        var minimumLeft = Infinity;
        var maximumRight = -Infinity;
        var minimumShift;
        var maximumShift;
         var shift;
         var shift;


         if (!isVisible(inner)) return;
         if (!isVisible(inner)) return;


         /* Remove the previous correction before reading line geometry. */
         /* Remove the previous correction before reading unshifted line geometry. */
         inner.style.setProperty(SHIFT_PROPERTY, '0px');
         inner.style.setProperty(SHIFT_PROPERTY, '0px');


71번째 줄: 79번째 줄:
                     weightedCenter += (rect.left + (rect.width / 2)) * weight;
                     weightedCenter += (rect.left + (rect.width / 2)) * weight;
                     totalWeight += weight;
                     totalWeight += weight;
                    minimumLeft = Math.min(minimumLeft, rect.left);
                    maximumRight = Math.max(maximumRight, rect.right);
                 });
                 });
             });
             });


             if (!totalWeight) {
             if (!totalWeight || !isFinite(minimumLeft) || !isFinite(maximumRight)) {
                 inner.style.setProperty(SHIFT_PROPERTY, '0px');
                 inner.style.setProperty(SHIFT_PROPERTY, '0px');
                inner.removeAttribute('data-optical-shift-x');
                 return;
                 return;
             }
             }
81번째 줄: 92번째 줄:
             shift = desiredCenter - (weightedCenter / totalWeight);
             shift = desiredCenter - (weightedCenter / totalWeight);
             shift = Math.max(-MAX_SHIFT, Math.min(MAX_SHIFT, shift));
             shift = Math.max(-MAX_SHIFT, Math.min(MAX_SHIFT, shift));
            /*
            * The title divider spans the full inner box. Keep every rendered
            * line inside that same horizontal range. A long line that already
            * reaches the divider therefore limits the optical correction rather
            * than being pushed beyond it.
            */
            minimumShift = Math.ceil((innerRect.left + DIVIDER_SAFE_INSET) - minimumLeft);
            maximumShift = Math.floor((innerRect.right - DIVIDER_SAFE_INSET) - maximumRight);
            if (minimumShift <= maximumShift) {
                shift = Math.max(minimumShift, Math.min(maximumShift, shift));
            } else {
                /* The text itself is wider than the divider; do not compound it. */
                shift = 0;
            }
             shift = Math.round(shift);
             shift = Math.round(shift);
             inner.style.setProperty(SHIFT_PROPERTY, shift + 'px');
             inner.style.setProperty(SHIFT_PROPERTY, shift + 'px');
             inner.setAttribute('data-optical-shift-x', String(shift));
             inner.setAttribute('data-optical-shift-x', String(shift));
            inner.setAttribute('data-optical-shift-min', String(minimumShift));
            inner.setAttribute('data-optical-shift-max', String(maximumShift));
         });
         });
     }
     }

2026년 7월 12일 (일) 05:30 기준 최신판

/* =========================================
   Main page manifesto optical centering
   =========================================
   New shared systems use unprefixed names. Existing prefixed page classes
   remain only because they are legacy layout contracts.

   The prose stays left-aligned. This script measures the actual rendered
   line fragments and shifts only the paragraphs so their visual mass is
   centered inside the 660px manifesto column. The title remains geometrically
   centered and the vertical -100px placement remains owned by MainPage.css.

   The title divider is the hard horizontal boundary. Optical correction is
   clamped so no rendered line can cross either end of that divider.
*/
(function (window, document, mw) {
    'use strict';

    var SELECTOR = '.main-portal .main-manifesto-inner';
    var SHIFT_PROPERTY = '--manifesto-optical-shift-x';
    var MAX_SHIFT = 48;
    var DIVIDER_SAFE_INSET = 1;
    var resizeTimer = 0;
    var observed = typeof WeakSet === 'function' ? new WeakSet() : null;
    var resizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(function (entries) {
        entries.forEach(function (entry) {
            schedule(entry.target);
        });
    }) : null;

    function isVisible(element) {
        var rect;
        if (!element || !element.isConnected) return false;
        rect = element.getBoundingClientRect();
        return rect.width > 0 && rect.height > 0;
    }

    function getLineRects(paragraph) {
        var range;
        var rects;

        if (!paragraph || !paragraph.textContent || !paragraph.textContent.trim()) return [];

        range = document.createRange();
        range.selectNodeContents(paragraph);
        rects = Array.prototype.slice.call(range.getClientRects());
        if (typeof range.detach === 'function') range.detach();

        return rects.filter(function (rect) {
            return rect.width > 1 && rect.height > 1;
        });
    }

    function measureAndApply(inner) {
        var innerRect;
        var weightedCenter = 0;
        var totalWeight = 0;
        var desiredCenter;
        var minimumLeft = Infinity;
        var maximumRight = -Infinity;
        var minimumShift;
        var maximumShift;
        var shift;

        if (!isVisible(inner)) return;

        /* Remove the previous correction before reading unshifted line geometry. */
        inner.style.setProperty(SHIFT_PROPERTY, '0px');

        window.requestAnimationFrame(function () {
            if (!isVisible(inner)) return;

            innerRect = inner.getBoundingClientRect();
            desiredCenter = innerRect.left + (innerRect.width / 2);

            Array.prototype.forEach.call(inner.querySelectorAll('p'), function (paragraph) {
                getLineRects(paragraph).forEach(function (rect) {
                    /* Area weighting approximates the visible typographic mass. */
                    var weight = rect.width * rect.height;
                    weightedCenter += (rect.left + (rect.width / 2)) * weight;
                    totalWeight += weight;
                    minimumLeft = Math.min(minimumLeft, rect.left);
                    maximumRight = Math.max(maximumRight, rect.right);
                });
            });

            if (!totalWeight || !isFinite(minimumLeft) || !isFinite(maximumRight)) {
                inner.style.setProperty(SHIFT_PROPERTY, '0px');
                inner.removeAttribute('data-optical-shift-x');
                return;
            }

            shift = desiredCenter - (weightedCenter / totalWeight);
            shift = Math.max(-MAX_SHIFT, Math.min(MAX_SHIFT, shift));

            /*
             * The title divider spans the full inner box. Keep every rendered
             * line inside that same horizontal range. A long line that already
             * reaches the divider therefore limits the optical correction rather
             * than being pushed beyond it.
             */
            minimumShift = Math.ceil((innerRect.left + DIVIDER_SAFE_INSET) - minimumLeft);
            maximumShift = Math.floor((innerRect.right - DIVIDER_SAFE_INSET) - maximumRight);

            if (minimumShift <= maximumShift) {
                shift = Math.max(minimumShift, Math.min(maximumShift, shift));
            } else {
                /* The text itself is wider than the divider; do not compound it. */
                shift = 0;
            }

            shift = Math.round(shift);
            inner.style.setProperty(SHIFT_PROPERTY, shift + 'px');
            inner.setAttribute('data-optical-shift-x', String(shift));
            inner.setAttribute('data-optical-shift-min', String(minimumShift));
            inner.setAttribute('data-optical-shift-max', String(maximumShift));
        });
    }

    function schedule(inner) {
        if (!inner) return;
        window.requestAnimationFrame(function () {
            measureAndApply(inner);
        });
    }

    function mount(root) {
        var scope = root && root.querySelectorAll ? root : document;
        var candidates = [];

        if (scope.matches && scope.matches(SELECTOR)) candidates.push(scope);
        candidates = candidates.concat(Array.prototype.slice.call(scope.querySelectorAll(SELECTOR)));

        candidates.forEach(function (inner) {
            if (resizeObserver && (!observed || !observed.has(inner))) {
                resizeObserver.observe(inner);
                if (observed) observed.add(inner);
            }
            schedule(inner);
        });
    }

    function recalculateAll() {
        Array.prototype.forEach.call(document.querySelectorAll(SELECTOR), schedule);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function () { mount(document); }, { once:true });
    } else {
        mount(document);
    }

    if (document.fonts && document.fonts.ready) {
        document.fonts.ready.then(recalculateAll).catch(function () {});
    }

    window.addEventListener('resize', function () {
        window.clearTimeout(resizeTimer);
        resizeTimer = window.setTimeout(recalculateAll, 120);
    }, { passive:true });

    try {
        if (mw && mw.hook) {
            mw.hook('wikipage.content').add(function (content) {
                mount(content && content[0] ? content[0] : document);
            });
        }
    } catch (err) {}

    window.MainPageManifesto = window.MainPageManifesto || {};
    window.MainPageManifesto.recalculate = recalculateAll;
})(window, document, window.mw);