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

(Install package: clbiwiki-portal-frame-canvas-atomic-v3-20260717 / js/CategoryNav.js)
태그: 되돌려진 기여
(Install package: clbiwiki-portal-frame-fixed / js/CategoryNav.js)
태그: 수동 되돌리기
 
1번째 줄: 1번째 줄:
/* portal-frame canvas v3 — 2026-07-17 */
/* =========================================
/* =========================================
COASTLINE: BLACK ICE - CategoryNav
COASTLINE: BLACK ICE - CategoryNav
7번째 줄: 6번째 줄:
(function (window, document, mw) {
(function (window, document, mw) {
     'use strict';
     'use strict';
    var BUILD_TAG = 'portal-frame-canvas-v3-20260717';
    window.PortalFrameModules = window.PortalFrameModules || {};
    window.PortalFrameModules.CategoryNav = BUILD_TAG;


     var ITEMS = [
     var ITEMS = [
20번째 줄: 15번째 줄:
     ];
     ];
     var renderTimer = 0;
     var renderTimer = 0;
    var EXPECTED_FRAME_API = '20260717-canvas-api-003';
    var REQUIRED_FRAME_FUNCTIONS = ['setShellScale', 'getShellScale', 'readRenderWidth', 'createCanvas', 'applyCounterScale', 'createImage', 'paintStandardFrame', 'buildButtonPixels', 'paintGrid', 'setPixel', 'buttonMask', 'drawLabel', 'setSeams', 'onScale'];
    var REQUIRED_FRAME_OBJECTS = ['FRAME', 'DIMENSIONS'];
    var frameWarningSignature = '';
    var frameListenersBound = false;
    function reportFrameContract(frame, missing) {
        var version = frame && frame.apiVersion ? frame.apiVersion : '없음';
        var build = frame && frame.buildTag ? frame.buildTag : '없음';
        var signature = build + '|' + version + '|' + missing.join('|');
        if (signature === frameWarningSignature) return;
        frameWarningSignature = signature;
        if (window.console && typeof window.console.error === 'function') {
            window.console.error(
                '[FrameGeometry 구버전 감지: CategoryNav.js와 MediaWiki:FrameGeometry.js를 같은 배포 세트로 업데이트하세요] ' +
                '필요 빌드=' + BUILD_TAG + ', 현재 빌드=' + build +
                ', 필요 API=' + EXPECTED_FRAME_API + ', 현재 API=' + version +
                ', 누락/불일치=' + (missing.length ? missing.join(', ') : 'apiVersion')
            );
        }
    }


     function api() {
     function api() {
         var frame = window.CLBI && window.CLBI.frame;
         return window.CLBI && window.CLBI.frame;
        var missing = [];
        if (!frame) {
            reportFrameContract(null, ['window.CLBI.frame']);
            return null;
        }
        if (frame.buildTag !== BUILD_TAG) missing.push('buildTag');
        if (frame.apiVersion !== EXPECTED_FRAME_API) missing.push('apiVersion');
        REQUIRED_FRAME_OBJECTS.forEach(function (name) {
            if (!frame[name] || typeof frame[name] !== 'object') missing.push(name);
        });
        REQUIRED_FRAME_FUNCTIONS.forEach(function (name) {
            if (typeof frame[name] !== 'function') missing.push(name + '()');
        });
        if (missing.length) {
            reportFrameContract(frame, missing);
            return null;
        }
        frameWarningSignature = '';
        return frame;
     }
     }


77번째 줄: 31번째 줄:
     function calculateLayout(width) {
     function calculateLayout(width) {
         var frame = api();
         var frame = api();
        if (!frame) return null;
         var f = frame.FRAME;
         var f = frame.FRAME;
         var d = frame.DIMENSIONS;
         var d = frame.DIMENSIONS;
115번째 줄: 68번째 줄:
     function paintProjectPillar(image, width, height, segment) {
     function paintProjectPillar(image, width, height, segment) {
         var frame = api();
         var frame = api();
        if (!frame || !image || !segment) return null;
         var d = frame.DIMENSIONS;
         var d = frame.DIMENSIONS;
         var topStep = Math.floor(d.BUTTON_EDGE_TOP / frame.FRAME.SLANT_RISE);
         var topStep = Math.floor(d.BUTTON_EDGE_TOP / frame.FRAME.SLANT_RISE);
141번째 줄: 93번째 줄:
     function labelCenter(segment) {
     function labelCenter(segment) {
         var frame = api();
         var frame = api();
        if (!frame || !segment) return 0;
         var ys = Math.floor(frame.FRAME.BUTTON_FACE / 2);
         var ys = Math.floor(frame.FRAME.BUTTON_FACE / 2);
         var mask = frame.buttonMask(segment.btnW, ys, segment.slant, 0);
         var mask = frame.buttonMask(segment.btnW, ys, segment.slant, 0);
149번째 줄: 100번째 줄:
     function draw(canvas, layout, hoverIndex) {
     function draw(canvas, layout, hoverIndex) {
         var frame = api();
         var frame = api();
        if (!frame || !canvas || !layout) return false;
         var ctx = canvas.getContext('2d');
         var ctx = canvas.getContext('2d');
         var image = frame.createImage(ctx, layout.width, layout.height, 'BG');
         var image = frame.createImage(ctx, layout.width, layout.height, 'BG');
195번째 줄: 145번째 줄:
         });
         });
         canvas.__projectGeometry = projectGeometry;
         canvas.__projectGeometry = projectGeometry;
        return true;
     }
     }


     function publishSeams(layout) {
     function publishSeams(layout) {
         var frame = api();
         var frame = api();
        if (!frame || !layout) return false;
         var d = frame.DIMENSIONS;
         var d = frame.DIMENSIONS;
         var settings = layout.segments[1];
         var settings = layout.segments[1];
247번째 줄: 195번째 줄:
             shellScale:frame.getShellScale()
             shellScale:frame.getShellScale()
         });
         });
        return true;
     }
     }


314번째 줄: 261번째 줄:


         layout = calculateLayout(width);
         layout = calculateLayout(width);
        if (!layout) return false;
         if (
         if (
             mount.getAttribute('data-category-nav-width') === String(width) &&
             mount.getAttribute('data-category-nav-width') === String(width) &&
390번째 줄: 336번째 줄:
             renderAll(root || document);
             renderAll(root || document);
         }, 0);
         }, 0);
    }
    function bindFrameListeners() {
        var frame = api();
        if (!frame || frameListenersBound) return false;
        frameListenersBound = true;
        frame.onScale(function () { schedule(document); });
        return true;
     }
     }


407번째 줄: 344번째 줄:
     }
     }


     bindFrameListeners();
     if (api()) api().onScale(function () { schedule(document); });
    document.addEventListener('framegeometryready', function () {
        if (bindFrameListeners()) schedule(document);
    });
     if (mw && mw.hook) {
     if (mw && mw.hook) {
         mw.hook('wikipage.content').add(function (content) {
         mw.hook('wikipage.content').add(function (content) {
419번째 줄: 353번째 줄:


     window.CategoryNav = {
     window.CategoryNav = {
         version:BUILD_TAG,
         version:'20260717-canvas-pixel-001',
        buildTag:BUILD_TAG,
         init:renderAll,
         init:renderAll,
         renderAll:renderAll,
         renderAll:renderAll,
427번째 줄: 360번째 줄:
     window.CLBI = window.CLBI || {};
     window.CLBI = window.CLBI || {};
     window.CLBI.categoryNav = {
     window.CLBI.categoryNav = {
        version:BUILD_TAG,
        buildTag:BUILD_TAG,
         init:renderAll,
         init:renderAll,
         renderMount:renderMount
         renderMount:renderMount
     };
     };
}(window, document, window.mw));
}(window, document, window.mw));

2026년 7월 17일 (금) 17:49 기준 최신판

/* =========================================
COASTLINE: BLACK ICE - CategoryNav
대문 카테고리 canvas 픽셀 네비 생성기
========================================= */

(function (window, document, mw) {
    'use strict';

    var ITEMS = [
        { key:'era', label:'시대', title:'시대', slant:'right' },
        { key:'settings', label:'설정', title:'설정', slant:'right' },
        { key:'project', label:'프로젝트', title:'프로젝트:소개', slant:'both', pillar:true },
        { key:'corporations', label:'기업 및 공동체', title:'기업_및_공동체', slant:'left' },
        { key:'military', label:'군, 정치집단', title:'군_정치집단', slant:'left' }
    ];
    var renderTimer = 0;

    function api() {
        return window.CLBI && window.CLBI.frame;
    }

    function buildPath(title) {
        if (typeof window.buildWikiPath === 'function') return window.buildWikiPath(title);
        return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
    }

    function clearNode(node) {
        while (node && node.firstChild) node.removeChild(node.firstChild);
    }

    function calculateLayout(width) {
        var frame = api();
        var f = frame.FRAME;
        var d = frame.DIMENSIONS;
        var contentX = d.WELL_TOP;
        var contentWidth = Math.max(ITEMS.length * 8, width - contentX * 2);
        var btnW = Math.max(4, Math.floor((contentWidth - f.GAP * 4) / ITEMS.length));
        var segments = [];
        var xoff = contentX;
        var i;

        for (i = 0; i < ITEMS.length; i++) {
            segments.push({
                index:i,
                key:ITEMS[i].key,
                label:ITEMS[i].label,
                slant:ITEMS[i].slant,
                x:xoff,
                y:d.BUTTON_EDGE_TOP,
                btnW:btnW,
                pixelWidth:btnW + 2,
                pixelHeight:d.BUTTON_TOTAL
            });
            xoff += btnW + f.GAP;
        }

        return {
            width:width,
            height:d.ROW_HEIGHT,
            contentX:contentX,
            contentWidth:contentWidth,
            buttonWidth:btnW,
            usedRight:xoff - f.GAP,
            segments:segments
        };
    }

    function paintProjectPillar(image, width, height, segment) {
        var frame = api();
        var d = frame.DIMENSIONS;
        var topStep = Math.floor(d.BUTTON_EDGE_TOP / frame.FRAME.SLANT_RISE);
        var leftTop = segment.x - topStep;
        var rightTop = segment.x + segment.btnW + topStep;
        var y;
        var x;

        for (y = 0; y < height; y++) {
            var step = Math.floor(y / frame.FRAME.SLANT_RISE);
            var left = leftTop + step;
            var right = rightTop - step;
            for (x = left; x <= right; x++) frame.setPixel(image, width, height, x, y, 'S');
            frame.setPixel(image, width, height, left, y, 'D');
            frame.setPixel(image, width, height, right, y, 'H');
        }
        return {
            leftTop:leftTop,
            rightTop:rightTop,
            leftAtButton:segment.x,
            rightAtButton:segment.x + segment.btnW
        };
    }

    function labelCenter(segment) {
        var frame = api();
        var ys = Math.floor(frame.FRAME.BUTTON_FACE / 2);
        var mask = frame.buttonMask(segment.btnW, ys, segment.slant, 0);
        return segment.x + Math.round((mask.left + mask.right) / 2);
    }

    function draw(canvas, layout, hoverIndex) {
        var frame = api();
        var ctx = canvas.getContext('2d');
        var image = frame.createImage(ctx, layout.width, layout.height, 'BG');
        var projectGeometry = null;

        frame.paintStandardFrame(image, layout.width, layout.height);
        layout.segments.forEach(function (segment) {
            var grid = frame.buildButtonPixels(
                segment.btnW,
                frame.FRAME.BUTTON_FACE,
                segment.slant,
                0
            );
            frame.paintGrid(
                image,
                layout.width,
                layout.height,
                grid,
                segment.x,
                segment.y,
                true
            );
        });

        projectGeometry = paintProjectPillar(
            image,
            layout.width,
            layout.height,
            layout.segments[2]
        );
        ctx.putImageData(image, 0, 0);

        layout.segments.forEach(function (segment, index) {
            var center = index === 2
                ? Math.round((projectGeometry.leftAtButton + projectGeometry.rightAtButton) / 2)
                : labelCenter(segment);
            frame.drawLabel(
                ctx,
                segment.label,
                center,
                Math.round((frame.DIMENSIONS.BUTTON_SURFACE_TOP + frame.DIMENSIONS.BUTTON_SURFACE_BOTTOM) / 2) + 1,
                hoverIndex === index && index !== 2,
                layout.width <= 820 ? 11 : 12
            );
        });
        canvas.__projectGeometry = projectGeometry;
    }

    function publishSeams(layout) {
        var frame = api();
        var d = frame.DIMENSIONS;
        var settings = layout.segments[1];
        var project = layout.segments[2];
        var corporations = layout.segments[3];
        var topStep = Math.floor(d.BUTTON_EDGE_TOP / frame.FRAME.SLANT_RISE);
        var settingsLeftOriginX = settings.x - topStep;
        var corpRightOriginX = corporations.x + corporations.btnW + topStep;
        var projectLeftOriginX = project.x - topStep;
        var projectRightOriginX = project.x + project.btnW + topStep;
        var seamStep = Math.floor(d.BOTTOM_START_ABS_Y / frame.FRAME.SLANT_RISE);

        frame.setSeams({
            width:layout.width,
            rowHeight:d.ROW_HEIGHT,
            buttonTop:d.BUTTON_EDGE_TOP,
            buttonBottom:d.BUTTON_EDGE_BOTTOM,
            overlap:d.OVERLAP,
            bottomStartAbsY:d.BOTTOM_START_ABS_Y,
            phase:d.BOTTOM_START_ABS_Y % frame.FRAME.SLANT_RISE,
            settingsLeftOriginX:settingsLeftOriginX,
            settingsLeftTopX:settings.x,
            settingsLeftSeamX:settingsLeftOriginX + seamStep,
            settingsLeftDir:1,
            corpRightOriginX:corpRightOriginX,
            corpRightTopX:corporations.x + corporations.btnW,
            corpRightSeamX:corpRightOriginX - seamStep,
            corpRightDir:-1,
            projectLeftOriginX:projectLeftOriginX,
            projectRightOriginX:projectRightOriginX,
            projectLeftTopX:project.x,
            projectRightTopX:project.x + project.btnW,
            projectLeftDir:1,
            projectRightDir:-1,
            buttonWidth:layout.buttonWidth,
            segments:layout.segments.map(function (segment) {
                return {
                    key:segment.key,
                    x:segment.x,
                    y:segment.y,
                    btnW:segment.btnW,
                    pixelWidth:segment.pixelWidth,
                    slant:segment.slant
                };
            }),
            shellScale:frame.getShellScale()
        });
    }

    function createHit(item, index, segment, nav, redraw) {
        var hit;
        if (item.pillar) {
            hit = document.createElement('span');
            hit.setAttribute('aria-label', item.label);
            hit.setAttribute('role', 'presentation');
            hit.className = 'portal-frame-hit portal-category-pillar-hit';
        } else {
            hit = document.createElement('a');
            hit.href = buildPath(item.title);
            hit.setAttribute('aria-label', item.label);
            hit.className = 'portal-frame-hit portal-cat-anchor';
            hit.setAttribute('data-category-key', item.key);
            hit.addEventListener('mouseenter', function () {
                nav.__frameHoverIndex = index;
                redraw();
            });
            hit.addEventListener('mouseleave', function () {
                if (nav.__frameHoverIndex === index) nav.__frameHoverIndex = -1;
                redraw();
            });
            hit.addEventListener('focus', function () {
                nav.__frameHoverIndex = index;
                redraw();
            });
            hit.addEventListener('blur', function () {
                if (nav.__frameHoverIndex === index) nav.__frameHoverIndex = -1;
                redraw();
            });
        }
        hit.style.left = segment.x + 'px';
        hit.style.top = segment.y + 'px';
        hit.style.width = segment.pixelWidth + 'px';
        hit.style.height = segment.pixelHeight + 'px';
        hit.appendChild(document.createElement('span'));
        hit.lastChild.className = 'portal-frame-sr-label';
        hit.lastChild.textContent = item.label;
        return hit;
    }

    function renderMount(mount) {
        var frame = api();
        var width;
        var layout;
        var nav;
        var inner;
        var canvas;
        var hitLayer;
        var redraw;

        if (!mount || !frame) return false;
        frame.setShellScale(frame.getShellScale());
        width = frame.readRenderWidth(mount);
        if (!width) {
            if (!mount.__categoryNavRetry) {
                mount.__categoryNavRetry = window.setTimeout(function () {
                    mount.__categoryNavRetry = 0;
                    renderMount(mount);
                }, 60);
            }
            return false;
        }

        layout = calculateLayout(width);
        if (
            mount.getAttribute('data-category-nav-width') === String(width) &&
            mount.getAttribute('data-category-nav-scale') === String(frame.getShellScale()) &&
            mount.querySelector('.portal-category-canvas')
        ) {
            publishSeams(layout);
            return true;
        }
        nav = document.createElement('nav');
        nav.className = 'portal-category-nav portal-frame-canvas-nav';
        nav.setAttribute('aria-label', 'Main categories');
        nav.__frameHoverIndex = -1;

        inner = document.createElement('div');
        inner.className = 'portal-frame-counter-inner portal-category-counter-inner';
        inner.style.width = width + 'px';

        canvas = frame.createCanvas('portal-frame-canvas portal-category-canvas', width, layout.height);
        canvas.setAttribute('data-frame-width', String(width));
        canvas.setAttribute('data-frame-height', String(layout.height));

        hitLayer = document.createElement('div');
        hitLayer.className = 'portal-frame-hit-layer';
        hitLayer.style.width = width + 'px';
        hitLayer.style.height = layout.height + 'px';

        redraw = function () {
            draw(canvas, layout, nav.__frameHoverIndex);
        };
        ITEMS.forEach(function (item, index) {
            hitLayer.appendChild(createHit(item, index, layout.segments[index], nav, redraw));
        });

        inner.appendChild(canvas);
        inner.appendChild(hitLayer);
        nav.appendChild(inner);
        clearNode(mount);
        mount.appendChild(nav);
        frame.applyCounterScale([mount, nav], inner, layout.height);
        redraw();
        publishSeams(layout);

        mount.setAttribute('data-category-nav-rendered', 'canvas');
        mount.setAttribute('data-category-nav-width', String(width));
        mount.setAttribute('data-category-nav-scale', String(frame.getShellScale()));

        if (window.ResizeObserver && !mount.__categoryNavObserver) {
            mount.__categoryNavObserver = new window.ResizeObserver(function () {
                schedule(mount);
            });
            mount.__categoryNavObserver.observe(mount);
        }
        return true;
    }

    function renderAll(root) {
        var scope = root && root.querySelectorAll ? root : document;
        var mounts = [];
        var rendered = 0;
        if (scope.matches && scope.matches('[data-component="category-nav"]')) mounts.push(scope);
        Array.prototype.forEach.call(scope.querySelectorAll('[data-component="category-nav"]'), function (mount) {
            if (mounts.indexOf(mount) === -1) mounts.push(mount);
        });
        mounts.forEach(function (mount) {
            if (renderMount(mount)) rendered += 1;
        });
        return rendered;
    }

    function schedule(root) {
        window.clearTimeout(renderTimer);
        renderTimer = window.setTimeout(function () {
            renderTimer = 0;
            renderAll(root || document);
        }, 0);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function () { renderAll(document); });
    } else {
        renderAll(document);
    }

    if (api()) api().onScale(function () { schedule(document); });
    if (mw && mw.hook) {
        mw.hook('wikipage.content').add(function (content) {
            schedule(content && content[0] ? content[0] : document);
        });
    }
    window.addEventListener('resize', function () { schedule(document); });

    window.CategoryNav = {
        version:'20260717-canvas-pixel-001',
        init:renderAll,
        renderAll:renderAll,
        renderMount:renderMount
    };
    window.CLBI = window.CLBI || {};
    window.CLBI.categoryNav = {
        init:renderAll,
        renderMount:renderMount
    };
}(window, document, window.mw));