moebius-web

web based ansi art editor

moebius-web

editor/editor.js


function editorCanvas(height, palette, codepage, retina) {
    "use strict";
    var canvas, ctx, previewCanvas, previewCtx, imageData, previewImageData, image, shiftKey, altKey, undoQueue, overlays, mirror;

    canvas = ElementHelper.create("canvas", {"width": retina ? 1280 : 640, "height": retina ? height * 32 : height * 16, "style": {"width": "640px", "height": (height * 16) + "px", "verticalAlign": "bottom"}});
    ctx = canvas.getContext("2d");
    previewCanvas = ElementHelper.create("canvas", {"width": retina ? 320 : 160, "height": retina ? height * 8 : height * 4, "style": {"width": "160px", "height": (height * 4) + "px", "verticalAlign": "bottom"}});
    previewCtx = previewCanvas.getContext("2d");
    imageData = ctx.createImageData(retina ? 16 : 8, retina ? 32 : 16);
    previewImageData = ctx.createImageData(retina ? 4 : 2, retina ? 8 : 4);
    image = new Uint8Array(80 * height * 3);
    undoQueue = [];
    overlays = {};
    mirror = false;

    function draw(charCode, x, y, fg, bg) {
        previewImageData.data.set(codepage.smallFont(charCode, fg, bg), 0);
        imageData.data.set(codepage.bigFont(charCode, fg, bg), 0);
        previewCtx.putImageData(previewImageData, x * previewImageData.width, y * previewImageData.height);
        ctx.putImageData(imageData, x * imageData.width, y * imageData.height);
    }

    function update(index) {
        draw(image[index], (index / 3) % 80, Math.floor(index / 240), image[index + 1], image[index + 2]);
    }

    function redraw() {
        var i;
        for (i = 0; i < image.length; i += 3) {
            update(i);
        }
    }

    function set(charCode, fg, bg, index) {
        var x, mirrorIndex;
        undoQueue[0].push([image[index], image[index + 1], image[index + 2], index]);
        image[index] = charCode;
        image[index + 1] = fg;
        image[index + 2] = bg;
        update(index);
        if (mirror) {
            x = (index % 240) / 3;
            if (x > 39) {
                mirrorIndex = (x - 40);
                mirrorIndex = index - (mirrorIndex ? mirrorIndex * 2 + 1 : 1) * 3;
            } else {
                mirrorIndex = (39 - x);
                mirrorIndex = index + (mirrorIndex ? mirrorIndex * 2 + 1 : 1) * 3;
            }
            undoQueue[0].push([image[mirrorIndex], image[mirrorIndex + 1], image[mirrorIndex + 2], mirrorIndex]);
            image[mirrorIndex] = charCode;
            image[mirrorIndex + 1] = fg;
            image[mirrorIndex + 2] = bg;
            update(mirrorIndex);
        }
    }

    function get(coord) {
        var index, charCode, foreground, background, isBlocky, upperBlockColor, lowerBlockColor;
        index = (coord.textY * 80 + coord.textX) * 3;
        charCode = image[index];
        foreground = image[index + 1];
        background = image[index + 2];
        switch (charCode) {
        case codepage.NULL:
        case codepage.SPACE:
        case codepage.NO_BREAK_SPACE:
            upperBlockColor = background;
            lowerBlockColor = background;
            isBlocky = true;
            break;
        case codepage.UPPER_HALF_BLOCK:
            upperBlockColor = foreground;
            lowerBlockColor = background;
            isBlocky = true;
            break;
        case codepage.LOWER_HALF_BLOCK:
            upperBlockColor = background;
            lowerBlockColor = foreground;
            isBlocky = true;
            break;
        case codepage.FULL_BLOCK:
            upperBlockColor = foreground;
            lowerBlockColor = foreground;
            isBlocky = true;
            break;
        default:
            if (foreground === background) {
                isBlocky = true;
                upperBlockColor = foreground;
                lowerBlockColor = foreground;
            } else {
                isBlocky = false;
            }
        }
        return {
            "charCode": charCode,
            "foreground": foreground,
            "background": background,
            "isBlocky": isBlocky,
            "upperBlockColor": upperBlockColor,
            "lowerBlockColor": lowerBlockColor
        };
    }

    function setChunk(coord, color) {
        var block;
        block = get(coord);
        if (block.isBlocky) {
            if (coord.isUpperHalf) {
                if (block.lowerBlockColor === color) {
                    set(codepage.FULL_BLOCK, color, block.background, coord.index);
                } else {
                    set(codepage.UPPER_HALF_BLOCK, color, block.lowerBlockColor, coord.index);
                }
            } else {
                if (block.upperBlockColor === color) {
                    set(codepage.FULL_BLOCK, color, block.background, coord.index);
                } else {
                    set(codepage.LOWER_HALF_BLOCK, color, block.upperBlockColor, coord.index);
                }
            }
        } else {
            if (coord.isUpperHalf) {
                set(codepage.UPPER_HALF_BLOCK, color, block.background, coord.index);
            } else {
                set(codepage.LOWER_HALF_BLOCK, color, block.background, coord.index);
            }
        }
    }

    function getBlockCoord(blockX, blockY) {
        var x, y, textY, modBlockY;

        x = blockX * 8;
        y = blockY * 16;
        textY = Math.floor(blockY / 2);
        modBlockY = blockY % 2;

        return {
            "index": (textY * 80 + blockX) * 3,
            "x": x,
            "y": y,
            "textX": blockX,
            "textY": textY,
            "blockX": blockX,
            "blockY": blockY,
            "isUpperHalf": (modBlockY === 0),
            "isLowerHalf": (modBlockY === 1)
        };
    }

    function chunkLine(from, to, callback) {
        var x0, y0, x1, y1, dx, dy, sx, sy, err, e2;

        x0 = from.blockX;
        y0 = from.blockY;
        x1 = to.blockX;
        y1 = to.blockY;
        dx = Math.abs(x1 - x0);
        sx = (x0 < x1) ? 1 : -1;
        dy = Math.abs(y1 - y0);
        sy = (y0 < y1) ? 1 : -1;
        err = ((dx > dy) ? dx : -dy) / 2;

        while (true) {
            callback(getBlockCoord(x0, y0));
            if (x0 === x1 && y0 === y1) {
                break;
            }
            e2 = err;
            if (e2 > -dx) {
                err -= dy;
                x0 += sx;
            }
            if (e2 < dy) {
                err += dx;
                y0 += sy;
            }
        }
    }

    function setChar(charCode, color, coord) {
        var block;
        block = get(coord);
        if (block.isBlocky) {
            if (coord.isUpperHalf) {
                set(charCode, color, block.upperBlockColor, coord.index);
            } else {
                set(charCode, color, block.lowerBlockColor, coord.index);
            }
        } else {
            set(charCode, color, block.background, coord.index);
        }
    }

    function resolveConflict(coord, currentColorBias) {
        var block, currentColor;
        block = get(coord);
        currentColor = palette.getCurrentColor();
        if (block.background > 7) {
            if (block.isBlocky) {
                if (block.foreground > 7) {
                    if (currentColorBias) {
                        if (block.upperBlockColor === currentColor && block.lowerBlockColor === currentColor) {
                            set(codepage.FULL_BLOCK, currentColor, 0, coord.index);
                        } else if (block.upperBlockColor === currentColor) {
                            set(codepage.UPPER_HALF_BLOCK, block.upperBlockColor, block.lowerBlockColor - 8, coord.index);
                        } else if (block.lowerBlockColor === currentColor) {
                            set(codepage.LOWER_HALF_BLOCK, block.lowerBlockColor, block.upperBlockColor - 8, coord.index);
                        } else {
                            set(image[coord.index], block.foreground, block.background - 8, coord.index);
                        }
                    } else {
                        if (block.upperBlockColor === currentColor && block.lowerBlockColor === currentColor) {
                            set(codepage.FULL_BLOCK, currentColor, 0, coord.index);
                        } else if (block.upperBlockColor === currentColor) {
                            set(codepage.LOWER_HALF_BLOCK, block.lowerBlockColor, block.upperBlockColor - 8, coord.index);
                        } else if (block.lowerBlockColor === currentColor) {
                            set(codepage.UPPER_HALF_BLOCK, block.upperBlockColor, block.lowerBlockColor - 8, coord.index);
                        } else {
                            set(image[coord.index], block.foreground, block.background - 8, coord.index);
                        }
                    }
                } else {
                    if ((block.upperBlockColor === block.background) && (block.lowerBlockColor === block.background)) {
                        set(codepage.FULL_BLOCK, block.background, block.foreground, coord.index);
                    } else if (block.upperBlockColor === block.background) {
                        set(codepage.UPPER_HALF_BLOCK, block.background, block.foreground, coord.index);
                    } else if (block.lowerBlockColor === block.background) {
                        set(codepage.LOWER_HALF_BLOCK, block.background, block.foreground, coord.index);
                    } else {
                        set(codepage.FULL_BLOCK, block.foreground, block.background - 8, coord.index);
                    }
                }
            } else {
                set(image[coord.index], block.foreground, block.background - 8, coord.index);
            }
        }
    }

    function resolveConflicts(currentColorBias) {
        var i;
        for (i = 0; i < image.length; i += 3) {
            resolveConflict({"textX": (i / 3) % 80, "textY": Math.floor(i / 240), "index": i}, currentColorBias);
        }
    }

    function keydown(evt) {
        switch (evt.keyCode || evt.which) {
        case 16:
            shiftKey = true;
            break;
        case 18:
            altKey = true;
            break;
        }
    }

    function keyup(evt) {
        switch (evt.keyCode || evt.which) {
        case 16:
            shiftKey = false;
            break;
        case 18:
            altKey = false;
            break;
        }
    }

    function startListening() {
        document.addEventListener("keydown", keydown, false);
        document.addEventListener("keyup", keyup, false);
        palette.startListening();
    }

    function stopListening() {
        document.removeEventListener("keydown", keydown);
        document.removeEventListener("keyup", keyup);
        palette.stopListening();
    }

    function init(divEditor) {
        var mousedown;

        mousedown = false;
        shiftKey = false;
        altKey = false;

        palette.init();
        redraw();

        function getCoord(pageX, pageY) {
            var x, y, coord;

            x = pageX - divEditor.offsetLeft;
            y = pageY - divEditor.offsetTop;
            coord = getBlockCoord(Math.floor(x / 8), Math.floor(y / 8));
            coord.x = x;
            coord.y = y;
            coord.shiftKey = shiftKey;
            coord.altKey = altKey;

            return coord;
        }

        divEditor.addEventListener("mousedown", function (evt) {
            mousedown = true;
            canvas.dispatchEvent(new CustomEvent("canvasDown", {"detail": getCoord(evt.pageX, evt.pageY)}));
        }, false);

        divEditor.addEventListener("mouseup", function () {
            mousedown = false;
        }, false);

        divEditor.addEventListener("mousemove", function (evt) {
            canvas.dispatchEvent(new CustomEvent(mousedown ? "canvasDrag" : "canvasMove", {"detail": getCoord(evt.pageX, evt.pageY)}));
        }, false);

        startListening();

        canvas.style.position = "absolute";
        canvas.style.left = "0px";
        canvas.style.top = "0px";

        divEditor.appendChild(canvas);
        document.getElementById("preview").appendChild(previewCanvas);
    }

    function undo() {
        var values, i;
        if (undoQueue.length) {
            values = undoQueue.shift();
            for (i = values.length - 1; i >= 0; --i) {
                image[values[i][3]] = values[i][0];
                image[values[i][3] + 1] = values[i][1];
                image[values[i][3] + 2] = values[i][2];
                update(values[i][3]);
            }
        }
    }

    function takeUndoSnapshot() {
        if (undoQueue.unshift([]) > 32) {
            undoQueue.pop();
        }
    }

    function clearUndoHistory() {
        while (undoQueue.length) {
            undoQueue.pop();
        }
    }

    function removeOverlay(uid) {
        document.getElementById("editor").removeChild(overlays[uid]);
        delete overlays[uid];
    }

    function addOverlay(overlayCanvas, uid) {
        if (overlays[uid]) {
            removeOverlay(uid);
        }
        overlayCanvas.style.position = "absolute";
        overlayCanvas.style.left = "0px";
        overlayCanvas.style.top = "0px";
        document.getElementById("editor").appendChild(overlayCanvas);
        overlays[uid] = overlayCanvas;
    }

    function turnOnMirroring() {
        mirror = true;
    }

    function turnOffMirroring() {
        mirror = false;
    }

    return {
        "height": height,
        "init": init,
        "canvas": canvas,
        "set": set,
        "get": get,
        "turnOnMirroring": turnOnMirroring,
        "turnOffMirroring": turnOffMirroring,
        "getBlockCoord": getBlockCoord,
        "setChunk": setChunk,
        "chunkLine": chunkLine,
        "setChar": setChar,
        "resolveConflict": resolveConflict,
        "resolveConflicts": resolveConflicts,
        "redraw": redraw,
        "image": image,
        "takeUndoSnapshot": takeUndoSnapshot,
        "clearUndoHistory": clearUndoHistory,
        "undo": undo,
        "addOverlay": addOverlay,
        "removeOverlay": removeOverlay,
        "stopListening": stopListening,
        "startListening": startListening
    };
}

Download

raw zip tar