참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
- 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
- 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
- 오페라: Ctrl-F5를 입력.
/* portal-frame canvas v3 — 2026-07-17 */
/* =========================================
COASTLINE: BLACK ICE - CategoryNav
대문 카테고리 canvas 픽셀 네비 생성기
========================================= */
(function (window, document, mw) {
'use strict';
var BUILD_TAG = 'portal-frame-canvas-v3-20260717';
window.PortalFrameModules = window.PortalFrameModules || {};
window.PortalFrameModules.CategoryNav = BUILD_TAG;
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;
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() {
var frame = 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;
}
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();
if (!frame) return null;
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();
if (!frame || !image || !segment) return null;
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();
if (!frame || !segment) return 0;
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();
if (!frame || !canvas || !layout) return false;
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;
return true;
}
function publishSeams(layout) {
var frame = api();
if (!frame || !layout) return false;
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()
});
return true;
}
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 (!layout) return false;
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);
}
function bindFrameListeners() {
var frame = api();
if (!frame || frameListenersBound) return false;
frameListenersBound = true;
frame.onScale(function () { schedule(document); });
return true;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { renderAll(document); });
} else {
renderAll(document);
}
bindFrameListeners();
document.addEventListener('framegeometryready', function () {
if (bindFrameListeners()) 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:BUILD_TAG,
buildTag:BUILD_TAG,
init:renderAll,
renderAll:renderAll,
renderMount:renderMount
};
window.CLBI = window.CLBI || {};
window.CLBI.categoryNav = {
version:BUILD_TAG,
buildTag:BUILD_TAG,
init:renderAll,
renderMount:renderMount
};
}(window, document, window.mw));