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

(새 문서: →‎========================================= FrameGeometry 대문 사선 프레임 공통 정수 좌표계 =========================================: (function (w, document) { 'use strict'; var SVG_NS = 'http://www.w3.org/2000/svg'; var FRAME = { BORDER:1, BEVEL:1, SLANT_RISE:2, SLANT_RUN:1, BUTTON_FACE:20, FRAME_FACE:7, GAP:2, COLORS:{ border:'#050505', surface:'#1d1d1d',...)
 
(Install package: clbiwiki-portal-frame-fixed / js/FrameGeometry.js)
 
(같은 사용자의 중간 판 하나는 보이지 않습니다)
1번째 줄: 1번째 줄:
/* =========================================
/* =========================================
FrameGeometry
FrameGeometry
대문 사선 프레임 공통 정수 좌표계
대문 프레임 공통 canvas 픽셀 렌더러
========================================= */
========================================= */


7번째 줄: 7번째 줄:
     'use strict';
     'use strict';


    var SVG_NS = 'http://www.w3.org/2000/svg';
     var FRAME = {
     var FRAME = {
         BORDER:1,
         BORDER:1,
26번째 줄: 25번째 줄:
     };
     };
     var DIMENSIONS = {
     var DIMENSIONS = {
         TOP_FRAME:FRAME.FRAME_FACE,
         FRAME_SURFACE_TOP:1,
         BUTTON_TOP:FRAME.FRAME_FACE + FRAME.GAP,
         FRAME_SURFACE_BOTTOM:8,
         BUTTON_BOTTOM:FRAME.FRAME_FACE + FRAME.GAP + FRAME.BUTTON_FACE,
         WELL_TOP:8,
         BOTTOM_FRAME:FRAME.FRAME_FACE,
         WELL_BOTTOM:10,
         ROW_HEIGHT:FRAME.FRAME_FACE + FRAME.GAP + FRAME.BUTTON_FACE + FRAME.GAP + FRAME.FRAME_FACE,
        BUTTON_EDGE_TOP:10,
         OVERLAP:FRAME.FRAME_FACE + FRAME.GAP
        BUTTON_SURFACE_TOP:11,
        BUTTON_SURFACE_BOTTOM:31,
        BUTTON_EDGE_BOTTOM:32,
        WELL2_TOP:32,
        WELL2_BOTTOM:34,
        FRAME2_SURFACE_TOP:34,
        FRAME2_SURFACE_BOTTOM:41,
         ROW_HEIGHT:42,
         OVERLAP:9,
        BUTTON_TOTAL:22,
        BOTTOM_START_ABS_Y:33
     };
     };
     var listeners = [];
     var COL = {
        B:[5,5,5],
        S:[29,29,29],
        H:[85,85,85],
        D:[16,16,16],
        BG:[8,8,8],
        T:[226,226,226],
        TW:[255,255,255]
    };
    var seamListeners = [];
    var scaleListeners = [];
    var shellScale = 1;


     DIMENSIONS.BOTTOM_START_ABS_Y = DIMENSIONS.ROW_HEIGHT - DIMENSIONS.OVERLAP;
     DIMENSIONS.TOP_FRAME = DIMENSIONS.FRAME_SURFACE_BOTTOM;
    DIMENSIONS.BUTTON_TOP = DIMENSIONS.BUTTON_EDGE_TOP;
    DIMENSIONS.BUTTON_BOTTOM = DIMENSIONS.BUTTON_EDGE_BOTTOM;
    DIMENSIONS.BOTTOM_FRAME = DIMENSIONS.ROW_HEIGHT - DIMENSIONS.FRAME2_SURFACE_TOP;


     function integer(value) {
     function integer(value) {
42번째 줄: 65번째 줄:


     function snapWidth(raw) {
     function snapWidth(raw) {
         return Math.floor(Number(raw) || 0);
         return Math.max(0, Math.floor(Number(raw) || 0));
     }
     }


     function slantX(xTop, y, dir, phase) {
     function readWidth(node) {
         phase = phase || 0;
         if (!node) return 0;
         return xTop + dir * Math.floor((y + phase) / FRAME.SLANT_RISE) * FRAME.SLANT_RUN;
         return snapWidth(node.clientWidth || node.offsetWidth || 0);
     }
     }


     function segment(leftTopX, rightTopX, height, leftDir, rightDir, phase) {
     function readCssShellScale() {
         return [
         var raw = '';
             [integer(leftTopX), 0],
        var value;
            [integer(rightTopX), 0],
        try {
            [integer(slantX(rightTopX, height, rightDir, phase)), integer(height)],
             raw = w.getComputedStyle(document.documentElement)
            [integer(slantX(leftTopX, height, leftDir, phase)), integer(height)]
                .getPropertyValue('--clbi-shell-scale');
        ];
        } catch (error) {}
        value = parseFloat(raw);
        return isFinite(value) && value > 0 ? value : 1;
     }
     }


     function segmentAt(leftTopX, rightTopX, topY, bottomY, leftDir, rightDir, phase, leftInset, rightInset) {
     function getShellScale() {
         leftInset = integer(leftInset || 0);
         var current = readCssShellScale();
         rightInset = integer(rightInset || 0);
         if (Math.abs(current - shellScale) > 0.0001) shellScale = current;
         return [
         return shellScale;
            [integer(slantX(leftTopX, topY, leftDir, phase) + leftInset), integer(topY)],
            [integer(slantX(rightTopX, topY, rightDir, phase) - rightInset), integer(topY)],
            [integer(slantX(rightTopX, bottomY, rightDir, phase) - rightInset), integer(bottomY)],
            [integer(slantX(leftTopX, bottomY, leftDir, phase) + leftInset), integer(bottomY)]
        ];
     }
     }


     function pts(points) {
     function setShellScale(value) {
         return points.map(function (point) {
         var next = Number(value);
             return integer(point[0]) + ',' + integer(point[1]);
        var previous = shellScale;
         }).join(' ');
        if (!isFinite(next) || next <= 0) next = 1;
        shellScale = next;
        document.documentElement.style.setProperty(
            '--portal-frame-counter-scale',
            String(1 / shellScale)
        );
        if (Math.abs(previous - shellScale) <= 0.0001) return shellScale;
        scaleListeners.slice().forEach(function (listener) {
             try { listener(shellScale, previous); } catch (error) {}
         });
        try {
            document.dispatchEvent(new CustomEvent('framegeometryscale', {
                detail:{ scale:shellScale, previous:previous }
            }));
        } catch (error) {}
        return shellScale;
     }
     }


     function create(name, className) {
     function onScale(listener) {
         var node = document.createElementNS(SVG_NS, name);
         if (typeof listener !== 'function') return function () {};
         if (className) node.setAttribute('class', className);
        scaleListeners.push(listener);
        node.setAttribute('shape-rendering', 'crispEdges');
         return function () {
         return node;
            var index = scaleListeners.indexOf(listener);
            if (index !== -1) scaleListeners.splice(index, 1);
         };
     }
     }


     function polygon(className, points) {
     function readRenderWidth(node) {
         var node = create('polygon', className);
         var layoutWidth = readWidth(node);
         node.setAttribute('points', pts(points));
         if (!layoutWidth) return 0;
         node.setAttribute('pointer-events', 'none');
         return Math.max(1, Math.round(layoutWidth * getShellScale()));
        return node;
     }
     }


     function line(className, start, end) {
     function applyCounterScale(hosts, inner, logicalHeight, overlap) {
         var node = create('line', className);
         var scale = getShellScale();
        node.setAttribute('x1', integer(start[0]));
        var inverse = 1 / scale;
         node.setAttribute('y1', integer(start[1]));
        var hostHeight = logicalHeight / scale;
         node.setAttribute('x2', integer(end[0]));
        var hostList = Array.isArray(hosts) ? hosts : [hosts];
         node.setAttribute('y2', integer(end[1]));
 
        node.setAttribute('vector-effect', 'non-scaling-stroke');
        hostList.forEach(function (host) {
        node.setAttribute('pointer-events', 'none');
            if (!host || !host.style) return;
         return node;
            host.classList.add('portal-frame-counter-host');
            host.style.setProperty('height', hostHeight + 'px', 'important');
            host.style.setProperty('min-height', hostHeight + 'px', 'important');
            host.style.setProperty('max-height', hostHeight + 'px', 'important');
            host.style.setProperty('flex-basis', hostHeight + 'px', 'important');
        });
 
         if (hosts && !Array.isArray(hosts) && typeof overlap === 'number') {
            hosts.style.setProperty('margin-top', (-overlap / scale) + 'px', 'important');
         } else if (Array.isArray(hosts) && hosts[0] && typeof overlap === 'number') {
            hosts[0].style.setProperty('margin-top', (-overlap / scale) + 'px', 'important');
         }
 
        if (inner && inner.style) {
            inner.classList.add('portal-frame-counter-inner');
            inner.style.setProperty('height', logicalHeight + 'px');
            inner.style.setProperty('transform-origin', 'top left');
            inner.style.setProperty('transform', 'scale(' + inverse + ')');
            inner.setAttribute('data-frame-shell-scale', String(scale));
        }
         return { scale:scale, inverse:inverse, hostHeight:hostHeight };
     }
     }


     function rect(className, x, y, width, height) {
    /*
         var node = create('rect', className);
    * 검증본의 버튼 마스크. phase는 2단 접합의 홀짝 위상만 더한다.
         node.setAttribute('x', integer(x));
    * slant: right | left | both
         node.setAttribute('y', integer(y));
    */
         node.setAttribute('width', Math.max(0, integer(width)));
     function buttonMask(BTN_W, ys, slant, phase) {
        node.setAttribute('height', Math.max(0, integer(height)));
         var step = Math.floor((ys + (phase || 0)) / 2);
         node.setAttribute('pointer-events', 'none');
        var left;
         return node;
        var right;
        if (slant === 'right') {
            left = 0;
            right = BTN_W - step;
         } else if (slant === 'left') {
            left = step;
            right = BTN_W;
        } else {
            left = step;
            right = BTN_W - step;
        }
        return { left:left, right:right };
    }
 
    function buildButtonPixels(BTN_W, SURFACE_H, slant, phase) {
        var H = 1 + SURFACE_H + 1;
        var W = BTN_W + 2;
         function io(x, y) {
            if (y < 0 || y >= H) return false;
            var ys = y - 1;
            var ysc = ys < 0 ? 0 : (ys >= SURFACE_H ? SURFACE_H - 1 : ys);
            var m = buttonMask(BTN_W, ysc, slant, phase || 0);
            return x >= m.left && x <= m.right;
        }
        var g = [];
        var src;
         var y;
        var x;
        for (y = 0; y < H; y++) {
            g[y] = [];
            for (x = 0; x < W; x++) {
                if (!io(x, y)) {
                    g[y][x] = 'BG';
                    continue;
                }
                var edge = !(io(x, y - 1) && io(x, y + 1) && io(x - 1, y) && io(x + 1, y));
                g[y][x] = edge ? 'B' : 'S';
            }
        }
        src = g.map(function (row) { return row.slice(); });
         for (y = 0; y < H; y++) {
            for (x = 0; x < W; x++) {
                if (src[y][x] !== 'S') continue;
                var tb = y > 0 && src[y - 1][x] === 'B';
                var rb = x < W - 1 && src[y][x + 1] === 'B';
                var bb = y < H - 1 && src[y + 1][x] === 'B';
                var lb = x > 0 && src[y][x - 1] === 'B';
                if (tb || rb) g[y][x] = 'H';
                else if (bb || lb) g[y][x] = 'D';
            }
        }
         return g;
     }
     }


     function centerX(points) {
     function buildReverseButtonPixels(BTN_W, SURFACE_H, slant, phase) {
         return integer((points[0][0] + points[1][0] + points[2][0] + points[3][0]) / 4);
         var H = 1 + SURFACE_H + 1;
        var W = BTN_W + 2;
        function io(x, y) {
            if (y < 0 || y >= H) return false;
            var ys = y - 1;
            var ysc = ys < 0 ? 0 : (ys >= SURFACE_H ? SURFACE_H - 1 : ys);
            var reversed = SURFACE_H - 1 - ysc;
            var m = buttonMask(BTN_W, reversed, slant, phase || 0);
            return x >= m.left && x <= m.right;
        }
        var g = [];
        var src;
        var y;
        var x;
        for (y = 0; y < H; y++) {
            g[y] = [];
            for (x = 0; x < W; x++) {
                if (!io(x, y)) {
                    g[y][x] = 'BG';
                    continue;
                }
                var edge = !(io(x, y - 1) && io(x, y + 1) && io(x - 1, y) && io(x + 1, y));
                g[y][x] = edge ? 'B' : 'S';
            }
        }
        src = g.map(function (row) { return row.slice(); });
        for (y = 0; y < H; y++) {
            for (x = 0; x < W; x++) {
                if (src[y][x] !== 'S') continue;
                var tb = y > 0 && src[y - 1][x] === 'B';
                var rb = x < W - 1 && src[y][x + 1] === 'B';
                var bb = y < H - 1 && src[y + 1][x] === 'B';
                var lb = x > 0 && src[y][x - 1] === 'B';
                if (tb || rb) g[y][x] = 'H';
                else if (bb || lb) g[y][x] = 'D';
            }
        }
        return g;
     }
     }


     function readWidth(node) {
     function createImage(ctx, width, height, fillCode) {
         if (!node) return 0;
        var image = ctx.createImageData(width, height);
         return snapWidth(node.clientWidth || node.offsetWidth || 0);
        var color = COL[fillCode || 'BG'];
        var i;
        for (i = 0; i < image.data.length; i += 4) {
            image.data[i] = color[0];
            image.data[i + 1] = color[1];
            image.data[i + 2] = color[2];
            image.data[i + 3] = 255;
        }
        return image;
    }
 
    function setPixel(image, width, height, x, y, code) {
        var color;
        var index;
        x = integer(x);
        y = integer(y);
         if (x < 0 || x >= width || y < 0 || y >= height) return;
        color = COL[code] || COL.BG;
        index = (y * width + x) * 4;
        image.data[index] = color[0];
        image.data[index + 1] = color[1];
        image.data[index + 2] = color[2];
        image.data[index + 3] = 255;
    }
 
    function fillRect(image, width, height, x, y, rectWidth, rectHeight, code) {
        var endX = Math.min(width, integer(x + rectWidth));
        var endY = Math.min(height, integer(y + rectHeight));
        var yy;
        var xx;
        x = Math.max(0, integer(x));
        y = Math.max(0, integer(y));
        for (yy = y; yy < endY; yy++) {
            for (xx = x; xx < endX; xx++) setPixel(image, width, height, xx, yy, code);
        }
    }
 
    function paintGrid(image, width, height, grid, offsetX, offsetY, skipBackground) {
        var y;
        var x;
        offsetX = integer(offsetX);
        offsetY = integer(offsetY);
        for (y = 0; y < grid.length; y++) {
            for (x = 0; x < grid[y].length; x++) {
                if (skipBackground && grid[y][x] === 'BG') continue;
                setPixel(image, width, height, offsetX + x, offsetY + y, grid[y][x]);
            }
        }
    }
 
    function paintRaisedRect(image, width, height, x, y, rectWidth, rectHeight) {
        var right = x + rectWidth - 1;
        var bottom = y + rectHeight - 1;
        var i;
        fillRect(image, width, height, x, y, rectWidth, rectHeight, 'B');
        fillRect(image, width, height, x + 1, y + 1, rectWidth - 2, rectHeight - 2, 'S');
        for (i = x + 1; i <= right - 1; i++) setPixel(image, width, height, i, bottom - 1, 'D');
        for (i = y + 1; i <= bottom - 1; i++) setPixel(image, width, height, x + 1, i, 'D');
        for (i = x + 1; i <= right - 1; i++) setPixel(image, width, height, i, y + 1, 'H');
        for (i = y + 1; i <= bottom - 1; i++) setPixel(image, width, height, right - 1, i, 'H');
    }
 
    function paintStandardFrame(image, width, height) {
        paintRaisedRect(image, width, height, 0, 0, width, height);
        fillRect(
            image,
            width,
            height,
            DIMENSIONS.WELL_TOP,
            DIMENSIONS.WELL_TOP,
            Math.max(0, width - DIMENSIONS.WELL_TOP * 2),
            DIMENSIONS.WELL2_BOTTOM - DIMENSIONS.WELL_TOP,
            'BG'
        );
    }
 
    function createCanvas(className, width, height) {
        var canvas = document.createElement('canvas');
        canvas.className = className || 'portal-frame-canvas';
        canvas.width = Math.max(1, integer(width));
        canvas.height = Math.max(1, integer(height));
        canvas.style.width = canvas.width + 'px';
        canvas.style.height = canvas.height + 'px';
        canvas.setAttribute('aria-hidden', 'true');
         return canvas;
    }
 
    function drawLabel(ctx, text, x, y, hover, fontSize) {
        var family = 'Galmuri11, sans-serif';
        try {
            family = w.getComputedStyle(ctx.canvas).fontFamily || family;
        } catch (error) {}
        ctx.save();
        ctx.font = '700 ' + integer(fontSize || 12) + 'px ' + family;
        ctx.fillStyle = hover ? FRAME.COLORS.label.replace('e2e2e2', 'ffffff') : FRAME.COLORS.label;
        if (hover) ctx.fillStyle = '#ffffff';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(String(text || ''), integer(x), integer(y));
        ctx.restore();
     }
     }


123번째 줄: 366번째 줄:
         var frame = w.CLBI.frame;
         var frame = w.CLBI.frame;
         var copy = {};
         var copy = {};
 
         Object.keys(next || {}).forEach(function (key) { copy[key] = next[key]; });
         Object.keys(next || {}).forEach(function (key) {
            copy[key] = next[key];
        });
         copy.revision = integer((frame.seams && frame.seams.revision) || 0) + 1;
         copy.revision = integer((frame.seams && frame.seams.revision) || 0) + 1;
         frame.seams = copy;
         frame.seams = copy;
 
         seamListeners.slice().forEach(function (listener) {
         listeners.slice().forEach(function (listener) {
             try { listener(copy); } catch (error) {}
             try { listener(copy); } catch (error) {}
         });
         });
         try {
         try {
             document.dispatchEvent(new CustomEvent('framegeometryseams', { detail:copy }));
             document.dispatchEvent(new CustomEvent('framegeometryseams', { detail:copy }));
         } catch (error) {}
         } catch (error) {}
 
         if (w.mw && w.mw.hook) w.mw.hook('frame.geometry.seams').fire(copy);
         if (w.mw && w.mw.hook) {
            w.mw.hook('frame.geometry.seams').fire(copy);
        }
         return copy;
         return copy;
     }
     }
146번째 줄: 381번째 줄:
     function onSeams(listener) {
     function onSeams(listener) {
         if (typeof listener !== 'function') return function () {};
         if (typeof listener !== 'function') return function () {};
         listeners.push(listener);
         seamListeners.push(listener);
         if (w.CLBI.frame.seams && w.CLBI.frame.seams.width) {
         if (w.CLBI.frame.seams && w.CLBI.frame.seams.width) listener(w.CLBI.frame.seams);
            listener(w.CLBI.frame.seams);
        }
         return function () {
         return function () {
             var index = listeners.indexOf(listener);
             var index = seamListeners.indexOf(listener);
             if (index !== -1) listeners.splice(index, 1);
             if (index !== -1) seamListeners.splice(index, 1);
         };
         };
     }
     }
160번째 줄: 393번째 줄:
         FRAME:FRAME,
         FRAME:FRAME,
         DIMENSIONS:DIMENSIONS,
         DIMENSIONS:DIMENSIONS,
         slantX:slantX,
         COL:COL,
         segment:segment,
         integer:integer,
        segmentAt:segmentAt,
        pts:pts,
        create:create,
        polygon:polygon,
        line:line,
        rect:rect,
        centerX:centerX,
         snapWidth:snapWidth,
         snapWidth:snapWidth,
         readWidth:readWidth,
         readWidth:readWidth,
        readRenderWidth:readRenderWidth,
        getShellScale:getShellScale,
        setShellScale:setShellScale,
        onScale:onScale,
        applyCounterScale:applyCounterScale,
        buttonMask:buttonMask,
        buildButtonPixels:buildButtonPixels,
        buildReverseButtonPixels:buildReverseButtonPixels,
        createImage:createImage,
        setPixel:setPixel,
        fillRect:fillRect,
        paintGrid:paintGrid,
        paintRaisedRect:paintRaisedRect,
        paintStandardFrame:paintStandardFrame,
        createCanvas:createCanvas,
        drawLabel:drawLabel,
         setSeams:setSeams,
         setSeams:setSeams,
         onSeams:onSeams,
         onSeams:onSeams,
         seams:{}
         seams:{}
     };
     };
    setShellScale(readCssShellScale());
}(window, document));
}(window, document));

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

/* =========================================
FrameGeometry
대문 프레임 공통 canvas 픽셀 렌더러
========================================= */

(function (w, document) {
    'use strict';

    var FRAME = {
        BORDER:1,
        BEVEL:1,
        SLANT_RISE:2,
        SLANT_RUN:1,
        BUTTON_FACE:20,
        FRAME_FACE:7,
        GAP:2,
        COLORS:{
            border:'#050505',
            surface:'#1d1d1d',
            well:'#080808',
            highlight:'#555555',
            shadow:'#101010',
            label:'#e2e2e2'
        }
    };
    var DIMENSIONS = {
        FRAME_SURFACE_TOP:1,
        FRAME_SURFACE_BOTTOM:8,
        WELL_TOP:8,
        WELL_BOTTOM:10,
        BUTTON_EDGE_TOP:10,
        BUTTON_SURFACE_TOP:11,
        BUTTON_SURFACE_BOTTOM:31,
        BUTTON_EDGE_BOTTOM:32,
        WELL2_TOP:32,
        WELL2_BOTTOM:34,
        FRAME2_SURFACE_TOP:34,
        FRAME2_SURFACE_BOTTOM:41,
        ROW_HEIGHT:42,
        OVERLAP:9,
        BUTTON_TOTAL:22,
        BOTTOM_START_ABS_Y:33
    };
    var COL = {
        B:[5,5,5],
        S:[29,29,29],
        H:[85,85,85],
        D:[16,16,16],
        BG:[8,8,8],
        T:[226,226,226],
        TW:[255,255,255]
    };
    var seamListeners = [];
    var scaleListeners = [];
    var shellScale = 1;

    DIMENSIONS.TOP_FRAME = DIMENSIONS.FRAME_SURFACE_BOTTOM;
    DIMENSIONS.BUTTON_TOP = DIMENSIONS.BUTTON_EDGE_TOP;
    DIMENSIONS.BUTTON_BOTTOM = DIMENSIONS.BUTTON_EDGE_BOTTOM;
    DIMENSIONS.BOTTOM_FRAME = DIMENSIONS.ROW_HEIGHT - DIMENSIONS.FRAME2_SURFACE_TOP;

    function integer(value) {
        return Math.round(Number(value) || 0);
    }

    function snapWidth(raw) {
        return Math.max(0, Math.floor(Number(raw) || 0));
    }

    function readWidth(node) {
        if (!node) return 0;
        return snapWidth(node.clientWidth || node.offsetWidth || 0);
    }

    function readCssShellScale() {
        var raw = '';
        var value;
        try {
            raw = w.getComputedStyle(document.documentElement)
                .getPropertyValue('--clbi-shell-scale');
        } catch (error) {}
        value = parseFloat(raw);
        return isFinite(value) && value > 0 ? value : 1;
    }

    function getShellScale() {
        var current = readCssShellScale();
        if (Math.abs(current - shellScale) > 0.0001) shellScale = current;
        return shellScale;
    }

    function setShellScale(value) {
        var next = Number(value);
        var previous = shellScale;
        if (!isFinite(next) || next <= 0) next = 1;
        shellScale = next;
        document.documentElement.style.setProperty(
            '--portal-frame-counter-scale',
            String(1 / shellScale)
        );
        if (Math.abs(previous - shellScale) <= 0.0001) return shellScale;
        scaleListeners.slice().forEach(function (listener) {
            try { listener(shellScale, previous); } catch (error) {}
        });
        try {
            document.dispatchEvent(new CustomEvent('framegeometryscale', {
                detail:{ scale:shellScale, previous:previous }
            }));
        } catch (error) {}
        return shellScale;
    }

    function onScale(listener) {
        if (typeof listener !== 'function') return function () {};
        scaleListeners.push(listener);
        return function () {
            var index = scaleListeners.indexOf(listener);
            if (index !== -1) scaleListeners.splice(index, 1);
        };
    }

    function readRenderWidth(node) {
        var layoutWidth = readWidth(node);
        if (!layoutWidth) return 0;
        return Math.max(1, Math.round(layoutWidth * getShellScale()));
    }

    function applyCounterScale(hosts, inner, logicalHeight, overlap) {
        var scale = getShellScale();
        var inverse = 1 / scale;
        var hostHeight = logicalHeight / scale;
        var hostList = Array.isArray(hosts) ? hosts : [hosts];

        hostList.forEach(function (host) {
            if (!host || !host.style) return;
            host.classList.add('portal-frame-counter-host');
            host.style.setProperty('height', hostHeight + 'px', 'important');
            host.style.setProperty('min-height', hostHeight + 'px', 'important');
            host.style.setProperty('max-height', hostHeight + 'px', 'important');
            host.style.setProperty('flex-basis', hostHeight + 'px', 'important');
        });

        if (hosts && !Array.isArray(hosts) && typeof overlap === 'number') {
            hosts.style.setProperty('margin-top', (-overlap / scale) + 'px', 'important');
        } else if (Array.isArray(hosts) && hosts[0] && typeof overlap === 'number') {
            hosts[0].style.setProperty('margin-top', (-overlap / scale) + 'px', 'important');
        }

        if (inner && inner.style) {
            inner.classList.add('portal-frame-counter-inner');
            inner.style.setProperty('height', logicalHeight + 'px');
            inner.style.setProperty('transform-origin', 'top left');
            inner.style.setProperty('transform', 'scale(' + inverse + ')');
            inner.setAttribute('data-frame-shell-scale', String(scale));
        }
        return { scale:scale, inverse:inverse, hostHeight:hostHeight };
    }

    /*
     * 검증본의 버튼 마스크. phase는 2단 접합의 홀짝 위상만 더한다.
     * slant: right | left | both
     */
    function buttonMask(BTN_W, ys, slant, phase) {
        var step = Math.floor((ys + (phase || 0)) / 2);
        var left;
        var right;
        if (slant === 'right') {
            left = 0;
            right = BTN_W - step;
        } else if (slant === 'left') {
            left = step;
            right = BTN_W;
        } else {
            left = step;
            right = BTN_W - step;
        }
        return { left:left, right:right };
    }

    function buildButtonPixels(BTN_W, SURFACE_H, slant, phase) {
        var H = 1 + SURFACE_H + 1;
        var W = BTN_W + 2;
        function io(x, y) {
            if (y < 0 || y >= H) return false;
            var ys = y - 1;
            var ysc = ys < 0 ? 0 : (ys >= SURFACE_H ? SURFACE_H - 1 : ys);
            var m = buttonMask(BTN_W, ysc, slant, phase || 0);
            return x >= m.left && x <= m.right;
        }
        var g = [];
        var src;
        var y;
        var x;
        for (y = 0; y < H; y++) {
            g[y] = [];
            for (x = 0; x < W; x++) {
                if (!io(x, y)) {
                    g[y][x] = 'BG';
                    continue;
                }
                var edge = !(io(x, y - 1) && io(x, y + 1) && io(x - 1, y) && io(x + 1, y));
                g[y][x] = edge ? 'B' : 'S';
            }
        }
        src = g.map(function (row) { return row.slice(); });
        for (y = 0; y < H; y++) {
            for (x = 0; x < W; x++) {
                if (src[y][x] !== 'S') continue;
                var tb = y > 0 && src[y - 1][x] === 'B';
                var rb = x < W - 1 && src[y][x + 1] === 'B';
                var bb = y < H - 1 && src[y + 1][x] === 'B';
                var lb = x > 0 && src[y][x - 1] === 'B';
                if (tb || rb) g[y][x] = 'H';
                else if (bb || lb) g[y][x] = 'D';
            }
        }
        return g;
    }

    function buildReverseButtonPixels(BTN_W, SURFACE_H, slant, phase) {
        var H = 1 + SURFACE_H + 1;
        var W = BTN_W + 2;
        function io(x, y) {
            if (y < 0 || y >= H) return false;
            var ys = y - 1;
            var ysc = ys < 0 ? 0 : (ys >= SURFACE_H ? SURFACE_H - 1 : ys);
            var reversed = SURFACE_H - 1 - ysc;
            var m = buttonMask(BTN_W, reversed, slant, phase || 0);
            return x >= m.left && x <= m.right;
        }
        var g = [];
        var src;
        var y;
        var x;
        for (y = 0; y < H; y++) {
            g[y] = [];
            for (x = 0; x < W; x++) {
                if (!io(x, y)) {
                    g[y][x] = 'BG';
                    continue;
                }
                var edge = !(io(x, y - 1) && io(x, y + 1) && io(x - 1, y) && io(x + 1, y));
                g[y][x] = edge ? 'B' : 'S';
            }
        }
        src = g.map(function (row) { return row.slice(); });
        for (y = 0; y < H; y++) {
            for (x = 0; x < W; x++) {
                if (src[y][x] !== 'S') continue;
                var tb = y > 0 && src[y - 1][x] === 'B';
                var rb = x < W - 1 && src[y][x + 1] === 'B';
                var bb = y < H - 1 && src[y + 1][x] === 'B';
                var lb = x > 0 && src[y][x - 1] === 'B';
                if (tb || rb) g[y][x] = 'H';
                else if (bb || lb) g[y][x] = 'D';
            }
        }
        return g;
    }

    function createImage(ctx, width, height, fillCode) {
        var image = ctx.createImageData(width, height);
        var color = COL[fillCode || 'BG'];
        var i;
        for (i = 0; i < image.data.length; i += 4) {
            image.data[i] = color[0];
            image.data[i + 1] = color[1];
            image.data[i + 2] = color[2];
            image.data[i + 3] = 255;
        }
        return image;
    }

    function setPixel(image, width, height, x, y, code) {
        var color;
        var index;
        x = integer(x);
        y = integer(y);
        if (x < 0 || x >= width || y < 0 || y >= height) return;
        color = COL[code] || COL.BG;
        index = (y * width + x) * 4;
        image.data[index] = color[0];
        image.data[index + 1] = color[1];
        image.data[index + 2] = color[2];
        image.data[index + 3] = 255;
    }

    function fillRect(image, width, height, x, y, rectWidth, rectHeight, code) {
        var endX = Math.min(width, integer(x + rectWidth));
        var endY = Math.min(height, integer(y + rectHeight));
        var yy;
        var xx;
        x = Math.max(0, integer(x));
        y = Math.max(0, integer(y));
        for (yy = y; yy < endY; yy++) {
            for (xx = x; xx < endX; xx++) setPixel(image, width, height, xx, yy, code);
        }
    }

    function paintGrid(image, width, height, grid, offsetX, offsetY, skipBackground) {
        var y;
        var x;
        offsetX = integer(offsetX);
        offsetY = integer(offsetY);
        for (y = 0; y < grid.length; y++) {
            for (x = 0; x < grid[y].length; x++) {
                if (skipBackground && grid[y][x] === 'BG') continue;
                setPixel(image, width, height, offsetX + x, offsetY + y, grid[y][x]);
            }
        }
    }

    function paintRaisedRect(image, width, height, x, y, rectWidth, rectHeight) {
        var right = x + rectWidth - 1;
        var bottom = y + rectHeight - 1;
        var i;
        fillRect(image, width, height, x, y, rectWidth, rectHeight, 'B');
        fillRect(image, width, height, x + 1, y + 1, rectWidth - 2, rectHeight - 2, 'S');
        for (i = x + 1; i <= right - 1; i++) setPixel(image, width, height, i, bottom - 1, 'D');
        for (i = y + 1; i <= bottom - 1; i++) setPixel(image, width, height, x + 1, i, 'D');
        for (i = x + 1; i <= right - 1; i++) setPixel(image, width, height, i, y + 1, 'H');
        for (i = y + 1; i <= bottom - 1; i++) setPixel(image, width, height, right - 1, i, 'H');
    }

    function paintStandardFrame(image, width, height) {
        paintRaisedRect(image, width, height, 0, 0, width, height);
        fillRect(
            image,
            width,
            height,
            DIMENSIONS.WELL_TOP,
            DIMENSIONS.WELL_TOP,
            Math.max(0, width - DIMENSIONS.WELL_TOP * 2),
            DIMENSIONS.WELL2_BOTTOM - DIMENSIONS.WELL_TOP,
            'BG'
        );
    }

    function createCanvas(className, width, height) {
        var canvas = document.createElement('canvas');
        canvas.className = className || 'portal-frame-canvas';
        canvas.width = Math.max(1, integer(width));
        canvas.height = Math.max(1, integer(height));
        canvas.style.width = canvas.width + 'px';
        canvas.style.height = canvas.height + 'px';
        canvas.setAttribute('aria-hidden', 'true');
        return canvas;
    }

    function drawLabel(ctx, text, x, y, hover, fontSize) {
        var family = 'Galmuri11, sans-serif';
        try {
            family = w.getComputedStyle(ctx.canvas).fontFamily || family;
        } catch (error) {}
        ctx.save();
        ctx.font = '700 ' + integer(fontSize || 12) + 'px ' + family;
        ctx.fillStyle = hover ? FRAME.COLORS.label.replace('e2e2e2', 'ffffff') : FRAME.COLORS.label;
        if (hover) ctx.fillStyle = '#ffffff';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(String(text || ''), integer(x), integer(y));
        ctx.restore();
    }

    function setSeams(next) {
        var frame = w.CLBI.frame;
        var copy = {};
        Object.keys(next || {}).forEach(function (key) { copy[key] = next[key]; });
        copy.revision = integer((frame.seams && frame.seams.revision) || 0) + 1;
        frame.seams = copy;
        seamListeners.slice().forEach(function (listener) {
            try { listener(copy); } catch (error) {}
        });
        try {
            document.dispatchEvent(new CustomEvent('framegeometryseams', { detail:copy }));
        } catch (error) {}
        if (w.mw && w.mw.hook) w.mw.hook('frame.geometry.seams').fire(copy);
        return copy;
    }

    function onSeams(listener) {
        if (typeof listener !== 'function') return function () {};
        seamListeners.push(listener);
        if (w.CLBI.frame.seams && w.CLBI.frame.seams.width) listener(w.CLBI.frame.seams);
        return function () {
            var index = seamListeners.indexOf(listener);
            if (index !== -1) seamListeners.splice(index, 1);
        };
    }

    w.CLBI = w.CLBI || {};
    w.CLBI.frame = {
        FRAME:FRAME,
        DIMENSIONS:DIMENSIONS,
        COL:COL,
        integer:integer,
        snapWidth:snapWidth,
        readWidth:readWidth,
        readRenderWidth:readRenderWidth,
        getShellScale:getShellScale,
        setShellScale:setShellScale,
        onScale:onScale,
        applyCounterScale:applyCounterScale,
        buttonMask:buttonMask,
        buildButtonPixels:buildButtonPixels,
        buildReverseButtonPixels:buildReverseButtonPixels,
        createImage:createImage,
        setPixel:setPixel,
        fillRect:fillRect,
        paintGrid:paintGrid,
        paintRaisedRect:paintRaisedRect,
        paintStandardFrame:paintStandardFrame,
        createCanvas:createCanvas,
        drawLabel:drawLabel,
        setSeams:setSeams,
        onSeams:onSeams,
        seams:{}
    };

    setShellScale(readCssShellScale());
}(window, document));