참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
- 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
- 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
- 오페라: Ctrl-F5를 입력.
/* =========================================
PortalSectionNav
대문 상단 카테고리 하단 부착형 canvas 픽셀 버튼
========================================= */
(function (window, document, mw) {
'use strict';
var COMPONENT = 'portal-section-nav';
var VERSION = '20260717-canvas-pixel-001';
var renderTimer = 0;
var ITEMS = [
{ key:'black-ice', label:'블랙 아이스', title:'대문', slant:'left' },
{ key:'anecdote', label:'에넥도트', href:'/index.php/특수:모든문서?namespace=3000', slant:'right' }
];
function api() {
return window.CLBI && window.CLBI.frame;
}
function buildWikiPath(title) {
if (typeof window.buildWikiPath === 'function') return window.buildWikiPath(title);
return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
}
function ensureMount(portal, topMount) {
var mount = portal.querySelector('[data-component="' + COMPONENT + '"]');
if (!mount) {
mount = document.createElement('div');
mount.setAttribute('data-component', COMPONENT);
}
if (mount.parentNode !== topMount.parentNode || topMount.nextSibling !== mount) {
topMount.parentNode.insertBefore(mount, topMount.nextSibling);
}
return mount;
}
function sideAt(geometry, y, side) {
var step = Math.floor((y + geometry.phase) / api().FRAME.SLANT_RISE);
return side === 'left'
? geometry.leftSeamX + step
: geometry.rightSeamX - step;
}
function outerGeometry(seams) {
var frame = api();
var phase = seams.bottomStartAbsY % frame.FRAME.SLANT_RISE;
return {
width:seams.width,
height:frame.DIMENSIONS.ROW_HEIGHT,
phase:phase,
leftSeamX:Math.max(0, Math.round(seams.settingsLeftSeamX)),
rightSeamX:Math.min(seams.width - 1, Math.round(seams.corpRightSeamX))
};
}
function paintOuterFrame(image, geometry) {
var frame = api();
var width = geometry.width;
var height = geometry.height;
var body = [];
var src;
var y;
var x;
function inside(px, py) {
if (py < 0 || py >= height) return false;
return px >= sideAt(geometry, py, 'left') && px <= sideAt(geometry, py, 'right');
}
for (y = 0; y < height; y++) {
body[y] = [];
for (x = 0; x < width; x++) {
if (!inside(x, y)) {
body[y][x] = 'BG';
continue;
}
body[y][x] = !(inside(x, y - 1) && inside(x, y + 1) && inside(x - 1, y) && inside(x + 1, y))
? 'B'
: 'S';
}
}
src = body.map(function (row) { return row.slice(); });
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
if (src[y][x] !== 'S') continue;
var tb = y > 0 && src[y - 1][x] === 'B';
var rb = x < width - 1 && src[y][x + 1] === 'B';
var bb = y < height - 1 && src[y + 1][x] === 'B';
var lb = x > 0 && src[y][x - 1] === 'B';
if (tb || rb) body[y][x] = 'H';
else if (bb || lb) body[y][x] = 'D';
}
}
frame.paintGrid(image, width, height, body, 0, 0, true);
/* 상단 9px은 상단층과 같은 #1d1d1d 접합면으로 연다. */
for (y = 0; y < frame.DIMENSIONS.OVERLAP; y++) {
var left = sideAt(geometry, y, 'left');
var right = sideAt(geometry, y, 'right');
for (x = left + 1; 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');
}
/* 프레임 안쪽 우물. 위·아래 2px 간격은 순수 #080808로 남긴다. */
for (y = frame.DIMENSIONS.WELL_TOP; y < frame.DIMENSIONS.WELL2_BOTTOM; y++) {
var wellLeft = sideAt(geometry, y, 'left') + frame.DIMENSIONS.WELL_TOP;
var wellRight = sideAt(geometry, y, 'right') - frame.DIMENSIONS.WELL_TOP;
for (x = wellLeft; x <= wellRight; x++) frame.setPixel(image, width, height, x, y, 'BG');
}
}
function paintProjectPillar(image, geometry, seams) {
var frame = api();
var absoluteStep = Math.floor(
seams.bottomStartAbsY / frame.FRAME.SLANT_RISE
);
var leftSeam = seams.projectLeftOriginX + absoluteStep;
var rightSeam = seams.projectRightOriginX - absoluteStep;
var y;
var x;
for (y = 0; y < geometry.height; y++) {
var step = Math.floor((y + geometry.phase) / frame.FRAME.SLANT_RISE);
var left = leftSeam + step;
var right = rightSeam - step;
for (x = left; x <= right; x++) {
frame.setPixel(image, geometry.width, geometry.height, x, y, 'S');
}
frame.setPixel(image, geometry.width, geometry.height, left, y, 'D');
frame.setPixel(image, geometry.width, geometry.height, right, y, 'H');
}
}
function splitButtons(geometry) {
var frame = api();
var d = frame.DIMENSIONS;
var left = sideAt(geometry, d.BUTTON_EDGE_TOP, 'left') + d.WELL_TOP;
var right = sideAt(geometry, d.BUTTON_EDGE_TOP, 'right') - d.WELL_TOP;
var available = Math.max(12, right - left + 1);
var btnW = Math.max(4, Math.floor((available - frame.FRAME.GAP) / 2));
return [
{
x:left,
y:d.BUTTON_EDGE_TOP,
btnW:btnW,
pixelWidth:btnW + 2,
pixelHeight:d.BUTTON_TOTAL,
slant:'left',
phase:geometry.phase
},
{
x:left + btnW + frame.FRAME.GAP,
y:d.BUTTON_EDGE_TOP,
btnW:btnW,
pixelWidth:btnW + 2,
pixelHeight:d.BUTTON_TOTAL,
slant:'right',
phase:geometry.phase
}
];
}
function labelCenter(button) {
var frame = api();
var mask = frame.buttonMask(
button.btnW,
Math.floor(frame.FRAME.BUTTON_FACE / 2),
button.slant,
button.phase
);
return button.x + Math.round((mask.left + mask.right) / 2);
}
function draw(canvas, geometry, buttons, seams, hoverIndex) {
var frame = api();
var ctx = canvas.getContext('2d');
var image = frame.createImage(ctx, geometry.width, geometry.height, 'BG');
paintOuterFrame(image, geometry);
buttons.forEach(function (button) {
var grid = frame.buildButtonPixels(
button.btnW,
frame.FRAME.BUTTON_FACE,
button.slant,
button.phase
);
frame.paintGrid(
image,
geometry.width,
geometry.height,
grid,
button.x,
button.y,
true
);
});
paintProjectPillar(image, geometry, seams);
ctx.putImageData(image, 0, 0);
buttons.forEach(function (button, index) {
frame.drawLabel(
ctx,
ITEMS[index].label,
labelCenter(button),
Math.round((frame.DIMENSIONS.BUTTON_SURFACE_TOP + frame.DIMENSIONS.BUTTON_SURFACE_BOTTOM) / 2) + 1,
hoverIndex === index,
geometry.width <= 820 ? 11 : 12
);
});
}
function createHit(item, index, button, nav, redraw) {
var anchor = document.createElement('a');
var label = document.createElement('span');
anchor.className = 'portal-frame-hit portal-section-anchor';
anchor.href = item.href || buildWikiPath(item.title);
anchor.setAttribute('aria-label', item.label);
anchor.setAttribute('data-category-key', item.key);
anchor.style.left = button.x + 'px';
anchor.style.top = button.y + 'px';
anchor.style.width = button.pixelWidth + 'px';
anchor.style.height = button.pixelHeight + 'px';
label.className = 'portal-frame-sr-label';
label.textContent = item.label;
anchor.appendChild(label);
anchor.addEventListener('mouseenter', function () {
nav.__frameHoverIndex = index;
redraw();
});
anchor.addEventListener('mouseleave', function () {
if (nav.__frameHoverIndex === index) nav.__frameHoverIndex = -1;
redraw();
});
anchor.addEventListener('focus', function () {
nav.__frameHoverIndex = index;
redraw();
});
anchor.addEventListener('blur', function () {
if (nav.__frameHoverIndex === index) nav.__frameHoverIndex = -1;
redraw();
});
return anchor;
}
function renderPortal(portal, seams) {
var frame = api();
var current = seams || (frame && frame.seams);
var topMount;
var mount;
var nav;
var inner;
var canvas;
var hitLayer;
var geometry;
var buttons;
var redraw;
if (!frame || !portal || !current || !current.width) return false;
topMount = portal.querySelector('[data-component="category-nav"]');
if (!topMount) return false;
mount = ensureMount(portal, topMount);
geometry = outerGeometry(current);
buttons = splitButtons(geometry);
if (
mount.getAttribute('data-portal-section-width') === String(current.width) &&
mount.getAttribute('data-portal-section-phase') === String(geometry.phase) &&
mount.getAttribute('data-portal-section-scale') === String(frame.getShellScale()) &&
mount.querySelector('.portal-section-canvas')
) {
return true;
}
nav = document.createElement('nav');
nav.className = 'portal-section-nav portal-frame-canvas-nav';
nav.setAttribute('aria-label', '작품 영역');
nav.__frameHoverIndex = -1;
inner = document.createElement('div');
inner.className = 'portal-frame-counter-inner portal-section-counter-inner';
inner.style.width = geometry.width + 'px';
canvas = frame.createCanvas('portal-frame-canvas portal-section-canvas', geometry.width, geometry.height);
canvas.setAttribute('data-frame-width', String(geometry.width));
canvas.setAttribute('data-frame-height', String(geometry.height));
hitLayer = document.createElement('div');
hitLayer.className = 'portal-frame-hit-layer';
hitLayer.style.width = geometry.width + 'px';
hitLayer.style.height = geometry.height + 'px';
redraw = function () { draw(canvas, geometry, buttons, current, nav.__frameHoverIndex); };
ITEMS.forEach(function (item, index) {
hitLayer.appendChild(createHit(item, index, buttons[index], nav, redraw));
});
inner.appendChild(canvas);
inner.appendChild(hitLayer);
nav.appendChild(inner);
mount.replaceChildren(nav);
frame.applyCounterScale(
[mount, nav],
inner,
geometry.height,
frame.DIMENSIONS.OVERLAP
);
redraw();
mount.setAttribute('data-portal-section-rendered', 'canvas');
mount.setAttribute('data-portal-section-width', String(current.width));
mount.setAttribute('data-portal-section-phase', String(geometry.phase));
mount.setAttribute('data-portal-section-scale', String(frame.getShellScale()));
return true;
}
function collectPortals(root) {
var scope = root && root.querySelectorAll ? root : document;
var portals = [];
if (scope.matches && scope.matches('.main-portal')) portals.push(scope);
Array.prototype.forEach.call(scope.querySelectorAll('.main-portal'), function (portal) {
if (portals.indexOf(portal) === -1) portals.push(portal);
});
return portals;
}
function renderAll(root, seams) {
var rendered = 0;
collectPortals(root).forEach(function (portal) {
if (renderPortal(portal, seams)) rendered += 1;
});
return rendered;
}
function schedule(root, seams) {
window.clearTimeout(renderTimer);
renderTimer = window.setTimeout(function () {
renderTimer = 0;
renderAll(root || document, seams);
}, 0);
}
if (api()) {
api().onSeams(function (seams) { schedule(document, seams); });
api().onScale(function () { schedule(document); });
}
if (mw && mw.hook) {
mw.hook('wikipage.content').add(function (content) {
schedule(content && content[0] ? content[0] : document);
});
}
window.PortalSectionNav = {
version:VERSION,
init:renderAll,
render:renderPortal,
renderAll:renderAll
};
}(window, document, window.mw));