MediaWiki:Pikcross.js

From Pikipedia, the Pikmin wiki
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
// Canvas constants.
const CANVAS = {
    // Width.
    WIDTH: 800,
    // Height.
    HEIGHT: 800
};

// Length of a border gradient effect.
const BORDER_GRADIENT_LENGTH = 20;
// Width and height of each cell, in pixels.
const CELL_SIZE = 32;
// Padding between each cell.
const CELL_PADDING = 1;
// Maximum number of rows or columns a board can have.
const MAX_COLS_OR_ROWS = 40;
// Minimum number of rows or columns a board can have.
const MIN_COLS_OR_ROWS = 2;
// Padding between sections of the game screen.
const SECTION_PADDING = 10;
// Transition total duration.
const TRANSITION_DURATION = 0.4;

// Color for the game's background.
const COLOR_BG = 'rgb(17, 51, 17)';
// Color for the board's background.
const COLOR_BOARD_BG = 'rgba(34, 68, 34, 0.6)';
// Color for the hints' background.
const COLOR_HINTS_BG = 'rgb(51, 85, 51)';
// Color for the footer's background.
const COLOR_FOOTER_BG = 'rgb(68, 102, 68)';
// Colors for the hint text.
const COLOR_HINT = ['#622', '#338'];
// Color for the hint text when defied.
const COLOR_HINT_DEFIED = '#E11';
// Color to tint the hint button with when defied.
const COLOR_HINT_DEFIED_TINT = 'rgba(255, 0, 0, 0.7)';
// Color for a button's background.
const COLOR_BUTTON_BG = '#BCB';
// Colors for a button's shadow.
const COLOR_BUTTON_SHADOW = ['rgba(64, 0, 0, 0.5)', 'rgba(0, 0, 64, 0.5)'];
// Colors for buttons' text.
const COLOR_BUTTON_TEXT = ['#622', '#338'];
// Color for buttons' text while disabled.
const COLOR_BUTTON_TEXT_DISABLED = '#888';
// Color for a blank cell.
const COLOR_BLANK = '#EFE';
// Color for a filled cell.
const COLOR_FILLED = '#696';
// Color for a marked cell.
const COLOR_MARKED = '#EDD';
// Color for a maker tool that's green.
const COLOR_MAKER_GREEN = '#262';
// Color for a maker tool that's red.
const COLOR_MAKER_RED = '#622';
// Colors for generic text.
const COLOR_TEXT_GENERIC = ['#ECC', '#BBE'];

// Value for a blank cell.
const CELL_BLANK = 0;
// Value for a filled cell.
const CELL_FILLED = 1;
// Value for a marked cell.
const CELL_MARKED = 2;

// Main menu.
const STATE_MAIN_MENU = 0;
// Playing a puzzle.
const STATE_PLAYING = 1;
// Making a puzzle.
const STATE_MAKING = 2;

// In-game pause menu.
const PANEL_STATE_PAUSE = 0;
// Congratulating the player on a finished puzzle.
const PANEL_STATE_CONGRATS = 1;
// Asking for the new puzzle's name.
const PANEL_STATE_MAKING_NAME = 2;
// Showing the new puzzle's code.
const PANEL_STATE_MAKING_CODE = 3;
// Asking for a custom puzzle code to play on.
const PANEL_STATE_PLAYING_CODE = 4;
// Warning the player of an error while loading the level.
const PANEL_STATE_LOAD_ERROR = 5;
// Warning the player of an error while saving the level.
const PANEL_STATE_SAVE_ERROR = 6;
// Warning the player they have a bookmark when they try to start a level.
const PANEL_STATE_BOOKMARK_WARNING = 7;
// Info.
const PANEL_STATE_INFO = 8;

// When encoding or decoding data, do it for a level.
const BOARD_DATA_CONTEXT_LEVEL = 0;
// When encoding or decoding data, do it for a bookmark.
const BOARD_DATA_CONTEXT_BOOKMARK = 1;

// Button.
const GUI_ITEM_BUTTON = 0;
// Text.
const GUI_ITEM_TEXT = 1;

// Spritesheet, in the context of loading things.
const LOAD_CONTENT_SPRITES = 0;
// Game setup, in the context of loading things.
const LOAD_CONTENT_SETUP = 1;


// All levels.
const LEVELS = [
    // P.
    'BQV//zEAAVA=',
    // Leaf.
    'CgrAw48///z5548fHwwABExlYWY=',
    // Bud.
    'CgpYsOOee9///79//MAAA0J1ZA==',
    // Flower.
    'Cgow4IHH/887x49//jEDBkZsb3dlcg==',
    // Red Pikmin.
    'CgoAAuz/xh424AMDMggAClJlZCBQaWttaW4=',
    // Yellow Pikmin.
    'Cgr4cIQgyOSr9lf/fA0DDVllbGxvdyBQaWttaW4=',
    // Blue Pikmin.
    'Cgp4GHPaeGEBAxyw0IAEC0JsdWUgUGlrbWlu',
    // Bomb Rock.
    'Cgp40Ofoz78/H7x3vMABCUJvbWIgUm9jaw==',
    // Swooping Snitchbug.
    'Dw/HHxVVPpUgIDawO9g8CD/4L/6X/kv/JX8qnxZAARJTd29vcGluZyBTbml0Y2hidWc=',
    // Purple Pikmin.
    'Dw8AZKAb50fcU2SPnuS/xA/8B/gD/AH+AD4AHgAJAA1QdXJwbGUgUGlrbWlu',
    // Rubber Ugly.
    'Dw/gAfgB/gHXQG+R2YX/hP/8f/6//9//x//Bf4APAAtSdWJiZXIgVWdseQ==',
    // Breadbug.
    'Dw/wDwQI/YsD737///++/+A/YA+wR5kHhP9j/yIAAAhCcmVhZGJ1Zw==',
    // White Pikmin.
    'Dw8Hz3lA+AH4yf5n/wP/g/1j/8A/8F/4P/5P/3f/AQxXaGl0ZSBQaWttaW4=',
    // Burrowing Snagret.
    'Dw8AD8AP/A//h99HzjPj/fE3/Al+gg/AB/AD+Pz/ARFCdXJyb3dpbmcgU25hZ3JldA==',
    // The Key.
    'Dw8YAD6Af+D78PB54Png+fHwf/wf78fL8Q64A/gAAAdUaGUgS2V5',
    // Captain Olimar.
    'FA/gAPwDAUIEEnwKf2W++l/9L/0bARb/Cf4E/gL/gTOAGcAP8AP4AQ5DYXB0YWluIE9saW1hcg==',
    
    // Data File.
    'FA/+hwBE/6WAVdwtmdZYa6g11pJpcbQAWgCtqhZAS6UF0P5vAND/BwlEYXRhIEZpbGU=',
    // Sunseed Berry.
    'Dw8AAAAAOALigoRggVEpdfv2O69eu2b38X/wBwAAAA1TdW5zZWVkIEJlcnJ5',
    // Citrus Lump.
    'Dw8AAAAA/oGNIQ8LEyUCB4gh0IJgBFEQZIDBfwAAAAtDaXRydXMgTHVtcA==',
    // Juice.
    'FA/wBwwG/gMBgf8gkUiQBED8H/2X/kv5pf9SfumplFXyJwGQAQz/AQVKdWljZQ==',
    // Rock Pikmin.
    'FBQA/ADwH8CPA/4x8B+OGJus9cgYl/x/6f+v/v/9/x/9f5H/F/s/E/8r4B8AtgFADADPAwtSb2NrIFBpa21pbg==',
    // Pellet Posy.
    'GQ8AAFABVAG9QT/R/PCedF8cF/oL+waTAbYASAAUAA4ABYACwAEQAagAz0CooKbgDAtQZWxsZXQgUG9zeQ==',
    // Spicy Spray.
    'FBQABgBgAAA+AGAAAA8A/APgfwABCAgAgQAQBADCzCy8d8PuLbi7gf8f8P8A/gfAPwDwAAtTcGljeSBTcHJheQ==',
    // Winged Pikmin.
    'FBQAOAN4fMD4CeO0GIPMMHgEPuBOfu9t7I6CBDDIAIz4gQ2YMEAtAlIioGYGREsADAcAAA1XaW5nZWQgUGlrbWlu',
    // Pyroclasmic Slooch.
    'Hh4ACAAAjg8AgCYCACCHAACMYQAAYxgAADAAAAAMAAAABwAAQAEAAPgAAAAyAADAHwAAEM6DAx6//8H8k/NY4N8z53/uSYZ/Y3IHlrm0/2e+H/7I1z8A//T5PwO+A3AA/h8PANb/AQANAACAPQAAwAcAABJQeXJvY2xhc21pYyBTbG9vY2g=',
    // Sandbelching Meerslug.
    'GRkA4AAA/A8A/v8A/+8DE/8PAvg/BOB52MLh4J/jQYv/gwD/BwP/D/j/H8D/PwD+fwDg/wCA/wEA/wMA/gcA/g/A/B+A/x/QfRR4AAAoAAAACE1lZXJzbHVn',
    // Waddlepuss.
    'FB4A+AcAADEOAPiPDwDx/wRAjnMC8OP4AP5/fsD//z84H/8ft7//59rU97809f1PTnx+x4e/OQL4j1kA+HsYh/wCiGGeABw4MAD48wcKV2FkZGxlcHVzcw==',
    // Plasm Wraith.
    'HhSAHwD+D/DPgf85/BjmB+N+cP4H/3/w/4/////3/wX/T+D8DODPAP4Y4I8D/zjwjwf/cPgPh/99+P/H/1/8/8X/T3j+BC8nwHIBP34MUGxhc20gV3JhaXRo',
    // Skutterchuck.
    'Hh4AAAAAAMgA\AADNAACwSAAAchUAQCQKALBUBAAqFwJAJIIAUIQgAIhACAwSYQKMlPAMTNmwx1Qi9A1anO+Ay/8b4P3/DPj1nwG+/UHAjQs4uP8CBvOAgIAHEBAAAAQDAIAgAAAgBAAAAAEAAEAAAAAAAAxTa3V0dGVyY2h1Y2s=',
    // S.S. Drake.
    'Hh4AAIAADwAg4AQACHgCAAK+8IeAPwYjwMtmCQCZuQPA9/4AGP/vAAMASGAAAiOEQAMSEfAQRYR4SBEQHkIMhAcQHgDBA/sRz4DBfxzgAeAByP9/gIs5IUCBZBBw8B8EEAYMAcQARgARABHAA4AHAAAAAApTLlMuIERyYWtl',
    // Fire-Breathing Feast.
    'Hh4AADg4AIDJCWAwPwFM4lCAeh8SkP25DLKPp0Pe/Nnc8H+qS/yfrjz/o+nf/2z59/9K//2Bw38/4PE/c3h8QD9Wz/eflfz9Z4V//ynz33/P/vPPsfn+Exy8TwI5d/iA3wMe8AeGA/j19wN8/c0A1r8cABRGaXJlLUJyZWF0aGluZyBGZWFzdA==',
    // The 3 Koppaites.
    'HigADwDgB+Z/BvgNGY8J/BsJAAn+GwEACD884TiIH3hSVYQP+BJHxD/O4jjEr8pCEMx3x0IQnAfhgg98h3DnOH4CIA8CvwUQfvAAmgj8bwAsBPAtx+kD+I0oYg/4j6piN8SPOOJjhh/HEcAFLkQIiAVgOATwDUAAHFgYgBB+PigAx/1/BM7//z8EMe/5H4IQ7tATghA4EBAPVGhlIDMgS29wcGFpdGVz',
];


// Sprite data in the spritesheet.
const SPRITES = {
    // Blank cell.
    CELL_BLANK: {
        x: 0, y: 0, width: 32, height: 32
    },
    // Filled cell.
    CELL_FILLED: {
        x: 32, y: 0, width: 32, height: 32
    },
    // Marked cell.
    CELL_MARKED: {
        x: 64, y: 0, width: 32, height: 32
    },
    // Logo.
    LOGO: {
        x: 96, y: 0, width: 200, height: 47
    },
    // Hocotate background.
    HOCOTATE: {
        x: 0, y: 47, width: 800, height: 800
    },
    // Koppai background.
    KOPPAI: {
        x: 800, y: 47, width: 800, height: 800
    }
};


// Screen section coordinate information.
const sectionCoords = {
    // Main menu header.
    mainMenuHeader: {
        x: 0,
        y: 0,
        width: CANVAS.WIDTH,
        height: CANVAS.HEIGHT * 0.2
    },
    // Main menu level selection.
    levelSelect: {
        x: 0,
        y: CANVAS.HEIGHT * 0.2,
        width: CANVAS.WIDTH,
        height: CANVAS.HEIGHT * 0.8
    },
    // Board coordinates.
    board: {
        x: 0,
        y: 0,
        width: 0,
        height: 0
    },
    // Miniature coordinates.
    miniature: {
        x: 0,
        y: 0,
        width: 0,
        height: 0
    },
    // Row hints coordinates.
    rowBanner: {
        x: 0,
        y: 0,
        width: 0,
        height: 0
    },
    // Column hints coordinates.
    colBanner: {
        x: 0,
        y: 0,
        width: 0,
        height: 0
    },
    // Footer coordinates.
    footer: {
        x: 0,
        y: 0,
        width: 0,
        height: 0
    },
    // Panel.
    panel: {
        x: CANVAS.WIDTH * 0.2,
        y: CANVAS.HEIGHT * 0.2,
        width: CANVAS.WIDTH * 0.6,
        height: CANVAS.HEIGHT * 0.6
    }
};
//Coordinates for GUI items.
const guiItemCoords = {
    // Title, in the main menu.
    mainMenuTitle: {
        x: 0.3,
        y: 0.2,
        width: 0.4,
        height: 0.4
    },
    // Continue button in the main menu.
    mainMenuContinue: {
        x: 0.025,
        y: 0.12,
        width: 0.20,
        height: 0.30
    },
    // Info button in the main menu.
    mainMenuInfo: {
        x: 0.025,
        y: 0.58,
        width: 0.20,
        height: 0.30
    },
    // Make custom button in the main menu.
    mainMenuMake: {
        x: 0.775,
        y: 0.12,
        width: 0.20,
        height: 0.30
    },
    // Play custom button in the main menu.
    mainMenuCustom: {
        x: 0.775,
        y: 0.58,
        width: 0.20,
        height: 0.30
    },
    // Swap side button in the main menu.
    mainMenuSide: {
        x: 0.40,
        y: 0.65,
        width: 0.20,
        height: 0.22
    },
    // Pause button.
    pause: {
        x: SECTION_PADDING,
        y: SECTION_PADDING,
        width: 45,
        y2: -SECTION_PADDING
    },
    // Title, in the playing state.
    playingTitle: {
        x: 0.425,
        y: 0.10,
        width: 0.15,
        height: 0.40
    },
    // Level number, in the playing state.
    playingLevel: {
        x: 0.00,
        y: 0.50,
        width: 1.00,
        height: 0.40
    },
    // Zoom in button.
    zoomIn: {
        x: -110,
        y: SECTION_PADDING,
        width: 45,
        y2: -SECTION_PADDING
    },
    // Zoom out button.
    zoomOut: {
        x: -55,
        y: SECTION_PADDING,
        width: 45,
        y2: -SECTION_PADDING
    },
    // Pause panel continue button.
    continue: {
        x: 0.2,
        y: 0.1,
        width: 0.6,
        height: 0.15
    },
    // Pause panel restart button.
    restart: {
        x: 0.2,
        y: 0.3,
        width: 0.6,
        height: 0.15
    },
    // Pause panel finish button.
    finish: {
        x: 0.2,
        y: 0.5,
        width: 0.6,
        height: 0.15
    },
    // Pause panel quit button.
    quit: {
        x: 0.2,
        y: 0.75,
        width: 0.6,
        height: 0.15
    },
    // General panel ok button.
    ok: {
        x: 0.2,
        y: 0.75,
        width: 0.6,
        height: 0.15
    },
    // Congrats panel header text.
    congratsHeader: {
        x: 0.2,
        y: 0.05,
        width: 0.6,
        height: 0.15
    },
    // Congrats panel puzzle miniature.
    congratsMiniature: {
        x: 0.2,
        y: 0.20,
        width: 0.6,
        height: 0.40
    },
    // Congrats panel level name text.
    congratsLevelName: {
        x: 0.2,
        y: 0.60,
        width: 0.6,
        height: 0.15
    },
    // New puzzle panel name prompt.
    newPuzzleNamePrompt: {
        x: 0.0,
        y: 0.2,
        width: 1.0,
        height: 0.1
    },
    // New puzzle panel done text.
    newPuzzleDoneText: {
        x: 0.0,
        y: 0.2,
        width: 1.0,
        height: 0.1
    },
    // Custom puzzle panel explanation text.
    customPuzzleText: {
        x: 0.0,
        y: 0.2,
        width: 1.0,
        height: 0.1
    },
    // Load error explanation 1 text.
    loadError1: {
        x: 0.0,
        y: 0.2,
        width: 1.0,
        height: 0.1
    },
    // Load error explanation 2 text.
    loadError2: {
        x: 0.0,
        y: 0.4,
        width: 1.0,
        height: 0.1
    },
    // Load error explanation 3 text.
    loadError3: {
        x: 0.0,
        y: 0.5,
        width: 1.0,
        height: 0.1
    },
    // Bookmark warning explanation 1 text.
    bookmarkWarning1: {
        x: 0.0,
        y: 0.15,
        width: 1.0,
        height: 0.1
    },
    // Bookmark warning explanation 2 text.
    bookmarkWarning2: {
        x: 0.0,
        y: 0.20,
        width: 1.0,
        height: 0.1
    },
    // Bookmark warning explanation 3 text.
    bookmarkWarning3: {
        x: 0.0,
        y: 0.25,
        width: 1.0,
        height: 0.1
    },
    // Bookmark warning explanation 4 text.
    bookmarkWarning4: {
        x: 0.0,
        y: 0.30,
        width: 1.0,
        height: 0.1
    },
    // Bookmark warning explanation 5 text.
    bookmarkWarning5: {
        x: 0.0,
        y: 0.35,
        width: 1.0,
        height: 0.1
    },
    // Bookmark warning go back button.
    bookmarkWarningBack: {
        x: 0.2,
        y: 0.55,
        width: 0.6,
        height: 0.15
    },
    // Bookmark warning play level button.
    bookmarkWarningStart: {
        x: 0.2,
        y: 0.75,
        width: 0.6,
        height: 0.15
    },
    // Info 1 text.
    info1: {
        x: 0.0,
        y: 0.15,
        width: 1.0,
        height: 0.1
    },
    // Info 2 text.
    info2: {
        x: 0.0,
        y: 0.30,
        width: 1.0,
        height: 0.1
    },
    // Info 3 text.
    info3: {
        x: 0.0,
        y: 0.35,
        width: 1.0,
        height: 0.1
    },
    // Info 4 text.
    info4: {
        x: 0.0,
        y: 0.40,
        width: 1.0,
        height: 0.1
    },
    // Info 5 text.
    info5: {
        x: 0.0,
        y: 0.45,
        width: 1.0,
        height: 0.1
    },
};


// Camera information.
let cam = {
    // Current coordinates. Used for panning.
    coords: {x: 0, y: 0},
    // Current zoom level.
    zoom: 1.0
};
// Player input information. Is controlled by both the mouse and mobile touches.
let input = {
    // Coordinates in the game world.
    worldCoords: {x: 0, y: 0},
    // Coordinates on-screen, with 0,0 being the top-left of the canvas.
    screenCoords: {x: 0, y: 0},
    // Is the player currently dragging?
    dragging: false,
    // X/Y coordinates of where the dragging started.
    dragStart: {x: 0, y: 0},
    // Lock either coordinate when dragging.
    dragLockCoord: {x: false, y: false},
    // What is the player currently doing to the cells? -1 means nothing.
    dragAction: -1,
    // Is the player dragging to pan the view?
    dragPanning: false
};
// Board information.
let board = {
    // Number of rows. Cache for convenience.
    nrRows: 0,
    // Number of columns. Cache for convenience.
    nrCols: 0,
    // State of every cell.
    cells: [],
    // Solution.
    solution: [],
    // Hints for each row.
    rowHints: [],
    // Hints for each column.
    colHints: [],
    // Whether a given row is auto-marked.
    autoMarkedRows: [],
    // Whether a given column is auto-marked.
    autoMarkedCols: [],
    // Whether a given row is defied.
    defiedRows: [],
    // Whether a given column is defied.
    defiedCols: [],
    // Puzzle name.
    name: '',
    // Level number. 0 means custom.
    levelNumber: 0,
    // Level code.
    levelCode: '',
};
// Array of true/false, representing whether a given level's been cleared.
let progression = [];
// Array with data about all levels.
let levelsData = [];
// Canvas object.
let canvas;
// Canvas HTML element.
let canvasEl;
// Input box HTML element.
let inputEl;
// Spritesheet image.
let sprites;
// What things have been loaded. If any of these are false, the game's not ready. Use the LOAD_CONTENTS_ constants.
let loaded = [false, false];
// Game state.
let state = STATE_MAIN_MENU;
// Are we currently showing the panel?
let inPanel = false;
// State of the panel.
let panelState = PANEL_STATE_PAUSE;
// Gradients to show on the borders of the board, when parts of the board are off-camera.
let borderGradients = {
    left: null,
    right: null,
    up: null,
    down: null
}
// Have we warned the player of their pending bookmark yet?
let gaveBookmarkWarning = false;
// Current game side. 0 for Hocotate, 1 for Koppai.
let gameSide = 0;
// Scene transition time left.
let transitionAnimTime = 0;
// Transition phase.
let transitionPhase = 0;
// State to change to after the transition.
let transitionNewState = 0;
// Transition code to run after the state change. undefined for none.
let transitionCodeAfter = undefined;


/**
 * Code to run when the player presses Ok when the panel shows the input box.
 */
function acceptInputBox() {
    let input = inputEl.value;
    input = input.replace(/[^\x00-\x7F]/g, "");

    switch(panelState) {
    case PANEL_STATE_MAKING_NAME:

        if(inputEl.value.length == 0) return;
        
        if(input.length == 0) input = 'Puzzle';
        board.name = input;
        var boardData = {
            nrRows: board.nrRows,
            nrCols: board.nrCols,
            cells: board.cells,
            name: board.name
        };

        inputEl.value = encodeBoard(boardData, BOARD_DATA_CONTEXT_LEVEL);
        panelState = PANEL_STATE_MAKING_CODE;
        showInputBox(true);

        break;
    
    case PANEL_STATE_MAKING_CODE:

        hideInputBox();
        inPanel = false;

        break;

    case PANEL_STATE_PLAYING_CODE:

        hideInputBox();

        if(inputEl.value.length == 0) {
            inPanel = false;
        } else {
            if(loadLevel(0, input, STATE_PLAYING)) {
                changeState(STATE_PLAYING, function() { inPanel = false; });
                clearBookmark();
            } else {
                panelState = PANEL_STATE_LOAD_ERROR;
            }
        }

        break;

    }

    updateCanvas();
}


/**
 * Checks if all hints in a row/column have been filled by the player.
 * @param {array} hints Array of hints to check.
 */
function allHintsAreFilled(hints) {
    for(var h = 0; h < hints.length; h++) {
        if(hints[h].state != CELL_FILLED) return false;
    }
    return true;
}


/**
 * Given an X or Y coordinate of a GUI item inside of some parent coordinates, this returns
 * what the final on-screen coordinates are, depending on how the GUI item's coordinates are formatted.
 * This function can take either X coordinates and widths, or it can take Y coordinates and heights.
 * 1 to infinity means the item is offset these many pixels from the parent's start.
 * 0 to 1 means the item is within this ratio of parent size, starting from the parent's start.
 * -1 to 0 means the item is within this ratio of parent size, starting from the parent's end.
 * -Infinity to -1 means the item is offset these many pixels from the parent's end.
 * @param {number} coord Coordinate to calculate.
 * @param {number} parentStart Start coordinate of the parent.
 * @param {number} parentSize Size of the parent.
 * @returns The calculated final coordinate.
 */
function calculateChildCoord(coord, parentStart, parentSize) {
    let parentEnd = parentStart + parentSize;
    if(coord > 1.00) {
        // Pixel offset from start.
        return parentStart + coord;
    } else if(coord >= 0.00) {
        // Ratio of size from start.
        return parentStart + (parentSize * coord);
    } else if(coord > -1.00) {
        // Ratio of size from end.
        return parentEnd - (parentSize * -coord);
    } else {
        // Pixel offset from end.
        return parentEnd - -coord;
    }
}


/**
 * Returns the final X and Y coordinates of a child object, making use of calculateChildCoord.
 * @param {object} coords Object with the child coordinates.
 * @param {object} parentCoords Object with the parent coordinates.
 */
function calculateChildCoords(coords, parentCoords) {
    return {
        x: calculateChildCoord(coords.x, parentCoords.x, parentCoords.width),
        y: calculateChildCoord(coords.y, parentCoords.y, parentCoords.height)
    };
}


/**
 * Given a width or height of a GUI item inside of some parent coordinates, this returns
 * what the final on-screen size is, depending on how the GUI item's width/height is formatted.
 * This function can take either X coordinates and widths, or it can take Y coordinates and heights.
 * 1 to infinity means the item's size is these many pixels.
 * 0 to 1 means the item is this ratio of parent size.
 * @param {number} coord Coordinate to calculate.
 * @param {number} parentSize Size of the parent.
 * @returns The calculated final size.
 */
function calculateChildDimension(coord, parentSize) {
    if(coord > 1.00) {
        // Pixel size.
        return coord;
    } else if(coord > 0.00) {
        // Ratio size.
        return parentSize * coord;
    }
}


/**
 * Returns the final X and Y dimensions of a child object, making use of calculateChildDimension.
 * @param {object} coords Object with the child coordinates.
 * @param {object} finalXY The object's final X and Y coordinates, calculated elsewhere.
 * @param {object} parentCoords Object with the parent coordinates.
 */
function calculateChildDimensions(coords, finalXY, parentCoords) {
    let finalCoords = {};
    if(coords.x2 == undefined) {
        finalCoords.width = calculateChildDimension(coords.width, parentCoords.width);
    } else {
        let x2 = calculateChildCoord(coords.x2, parentCoords.x, parentCoords.width);
        finalCoords.width = x2 - finalXY.x;
    }
    if(coords.y2 == undefined) {
        finalCoords.height = calculateChildDimension(coords.height, parentCoords.height);
    } else {
        let y2 = calculateChildCoord(coords.y2, parentCoords.y, parentCoords.height);
        finalCoords.height = y2 - finalXY.y;
    }
    return finalCoords;
}


/**
 * Changes the state of a cell, and updates everything accordingly.
 * @param {number} row Cell row number.
 * @param {number} col Cell column number.
 * @param {number} newState The cell's new state.
 * @returns True if the cell changed, false otherwise.
 */
function changeCell(row, col, newState) {
    if(board.cells[row][col] == newState) {
        return false;
    }

    board.cells[row][col] = newState;

    if(state == STATE_PLAYING) {
        updateRow(row);
        updateColumn(col);

        let solved = true;
        for(var r = 0; r < board.nrRows; r++) {
            for(var c = 0; c < board.nrCols; c++) {
                var cellState = board.cells[r][c];
                if(cellState == CELL_MARKED) cellState = CELL_BLANK;
                if(cellState != board.solution[r][c]) {
                    solved = false;
                    break;
                }
            }
            if(!solved) break;
        }

        if(solved) {
            if(board.levelNumber != 0) {
                progression[board.levelNumber - 1] = true;
                saveProgression();
            }
            clearBookmark();
            inPanel = true;
            panelState = PANEL_STATE_CONGRATS;
        } else {
            saveBookmark();
        }
    }

    return true;
}


/**
 * Changes the current game state with a transition.
 * @param {number} newState New state.
 * @param {function} codeAfter Code to run after the transition.
 */
function changeState(newState, codeAfter) {
    transitionNewState = newState;
    transitionAnimTime = TRANSITION_DURATION;
    transitionPhase = 0;
    transitionCodeAfter = codeAfter;
}


/**
 * Clamps the camera coordinates to make sure they don't go too far to the left, right, up, or down.
 * Also snaps to 0,0 if it's close enough.
 */
function clampCamera() {
    cam.coords.x = Math.min(cam.coords.x, (board.nrCols - 1) * (CELL_SIZE + CELL_PADDING));
    cam.coords.x = Math.max(cam.coords.x, -(sectionCoords.board.width - CELL_SIZE));
    cam.coords.y = Math.min(cam.coords.y, (board.nrRows - 1) * (CELL_SIZE + CELL_PADDING));
    cam.coords.y = Math.max(cam.coords.y, -(sectionCoords.board.height - CELL_SIZE));

    cam.zoom = Math.max(cam.zoom, 0.1);
    cam.zoom = Math.min(cam.zoom, 5);
}


/**
 * Debug function that automatically marks all levels as cleared.
 * This does not save the player's progression.
 */
function clearAllLevels() {
    for(var l = 0; l < progression.length; l++) {
        progression[l] = true;
    }
    updateCanvas();
}


/**
 * Clears the player's bookmark.
 */
function clearBookmark() {
    localStorage.setItem('pikcrossBookmark', '');
}


/**
 * Decodes a base64-encoded string into a board.
 * @param {string} data Encoded board text.
 * @param {number} context BOARD_DATA_CONTEXT_LEVEL to decode level data. BOARD_DATA_CONTEXT_BOOKMARK to decode bookmark data.
 * @returns Board data. Returns null on error.
 */
function decodeBoard(data, context) {
    let result = {
        nrRows: 0,
        nrCols: 0,
        cells: [],
        name: '',
        nrRowHints: 0,
        rowHints: [],
        nrColHints: 0,
        colHints: [],
        levelNumber: 0,
        levelCode: '',
    }
    let nrCellBits = context == BOARD_DATA_CONTEXT_LEVEL ? 1 : 2;

    try {

        let reader = new StrBitReader(atob(data));

        result.nrRows = reader.readNumber();
        result.nrCols = reader.readNumber();
        for(var r = 0; r < result.nrRows; r++) {
            result.cells.push([]);
            for(var c = 0; c < result.nrCols; c++) {
                result.cells[r].push(reader.readNumber(nrCellBits));
            }
        }

        if(context == BOARD_DATA_CONTEXT_LEVEL) {

            let nrNameChars = reader.readNumber();
            for(var c = 0; c < nrNameChars; c++) {
                result.name += String.fromCharCode(reader.readNumber());
            }

        } else {

            result.nrRowHints = reader.readNumber();
            result.nrColHints = reader.readNumber();
            for(var r = 0; r < result.nrRowHints; r++) {
                result.rowHints.push(reader.readNumber(1));
            }
            for(var c = 0; c < result.nrColHints; c++) {
                result.colHints.push(reader.readNumber(1));
            }

            result.levelNumber = reader.readNumber();

            let nrCodeChars = reader.readNumber();
            for(var c = 0; c < nrCodeChars; c++) {
                result.levelCode += String.fromCharCode(reader.readNumber());
            }

        }

        return result;

    } catch {
        return null;
    }
}


/**
 * Draws the board game screen.
 */
function drawBoard() {
    const boardX2 = sectionCoords.board.x + sectionCoords.board.width;
    const boardY2 = sectionCoords.board.y + sectionCoords.board.height;
    
    // Board, one cell at a time.
    canvas.save();
    canvas.beginPath();
    canvas.rect(sectionCoords.board.x, sectionCoords.board.y, sectionCoords.board.width, sectionCoords.board.height);
    canvas.clip();

    drawSprite(gameSide == 0 ? SPRITES.HOCOTATE : SPRITES.KOPPAI, sectionCoords.board.x, sectionCoords.board.y, sectionCoords.board.width, sectionCoords.board.height);
    canvas.fillStyle = COLOR_BOARD_BG;
    canvas.fillRect(sectionCoords.board.x, sectionCoords.board.y, sectionCoords.board.width, sectionCoords.board.height);
    for(var r = 0; r < board.nrRows; r++) {
        for(var c = 0; c < board.nrCols; c++) {
            let startX = sectionCoords.board.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;
            let startY = sectionCoords.board.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;
            let sprite = null;
            if(board.cells[r][c] == CELL_BLANK) {
                if(board.autoMarkedRows[r] || board.autoMarkedCols[c]) {
                    sprite = SPRITES.CELL_MARKED;
                } else {
                    sprite = SPRITES.CELL_BLANK;
                }
            } else if(board.cells[r][c] == CELL_FILLED) {
                sprite = SPRITES.CELL_FILLED;
            } else {
                sprite = SPRITES.CELL_MARKED;
            }
            drawSprite(sprite, startX, startY, CELL_SIZE, CELL_SIZE);
        }
    }

    // Board fives grid.
    canvas.lineWidth = 3;
    for(var r = 5; r < board.nrRows; r += 5) {
        let y = sectionCoords.board.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;
        let minX = sectionCoords.board.x - cam.coords.x;
        let maxX = sectionCoords.board.x + board.nrCols * CELL_SIZE + board.nrCols * CELL_PADDING - cam.coords.x;
        canvas.beginPath();
        canvas.moveTo(minX, y);
        canvas.lineTo(maxX, y);
        canvas.stroke();
    }
    for(var c = 5; c < board.nrCols; c += 5) {
        let x = sectionCoords.board.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;
        let minY = sectionCoords.board.y - cam.coords.y
        let maxY = sectionCoords.board.y + board.nrRows * CELL_SIZE + board.nrRows * CELL_PADDING - cam.coords.y;
        canvas.beginPath();
        canvas.moveTo(x, minY);
        canvas.lineTo(x, maxY);
        canvas.stroke();
    }
    canvas.font = 'bold 12px sans';
    canvas.fillStyle = '#888';
    canvas.textAlign = 'right';
    canvas.textBaseline = 'bottom';
    for(var r = 5; r < board.nrRows; r += 5) {
        let y = sectionCoords.board.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;
        let maxX = sectionCoords.board.x + board.nrCols * CELL_SIZE + board.nrCols * CELL_PADDING - cam.coords.x;
        maxX = Math.min(maxX, boardX2);
        canvas.fillText(r, maxX - 1, y);
    }
    canvas.textAlign = 'right';
    canvas.textBaseline = 'bottom';
    for(var c = 5; c < board.nrCols; c += 5) {
        let x = sectionCoords.board.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;
        let maxY = sectionCoords.board.y + board.nrRows * CELL_SIZE + board.nrRows * CELL_PADDING - cam.coords.y;
        maxY = Math.min(maxY, boardY2);
        canvas.fillText(c, x - 1, maxY);
    }
    
    // Board border gradients.
    if(cam.coords.x > 0) {
        canvas.fillStyle = borderGradients.left;
        canvas.fillRect(sectionCoords.board.x, sectionCoords.board.y - cam.coords.y, BORDER_GRADIENT_LENGTH, board.nrRows * (CELL_SIZE + CELL_PADDING));
    }
    if(cam.coords.x < -(sectionCoords.board.width - board.nrCols * (CELL_SIZE + CELL_PADDING))) {
        canvas.fillStyle = borderGradients.right;
        canvas.fillRect(boardX2 - BORDER_GRADIENT_LENGTH, sectionCoords.board.y - cam.coords.y, boardX2, board.nrRows * (CELL_SIZE + CELL_PADDING));
    }
    if(cam.coords.y > 0) {
        canvas.fillStyle = borderGradients.up;
        canvas.fillRect(sectionCoords.board.x - cam.coords.x, sectionCoords.board.y, board.nrCols * (CELL_SIZE + CELL_PADDING), BORDER_GRADIENT_LENGTH);
    }
    if(cam.coords.y < -(sectionCoords.board.height - board.nrRows * (CELL_SIZE + CELL_PADDING))) {
        canvas.fillStyle = borderGradients.down;
        canvas.fillRect(sectionCoords.board.x - cam.coords.x, boardY2 - BORDER_GRADIENT_LENGTH, board.nrCols * (CELL_SIZE + CELL_PADDING), boardY2);
    }
    
    canvas.restore();

    // Miniature.
    canvas.fillStyle = COLOR_BOARD_BG;
    canvas.fillRect(sectionCoords.miniature.x, sectionCoords.miniature.y, sectionCoords.miniature.width, sectionCoords.miniature.height);
    drawMiniature(board.nrRows, board.nrCols, board.cells, {x: 0, y: 0, width: 1, height: 1}, sectionCoords.miniature, COLOR_TEXT_GENERIC[gameSide]);

    if(state == STATE_PLAYING) {
            
        // Hints.
        canvas.font = 'bold 24px sans';

        // Row hints.
        let cellWidth = CELL_SIZE;
        let cellHeight = CELL_SIZE;

        canvas.save();
        canvas.beginPath();
        canvas.rect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height);
        canvas.clip();

        canvas.fillStyle = COLOR_HINTS_BG;
        canvas.fillRect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height);

        for(r = 0; r < board.nrRows; r++) {
            for(h = 0; h < board.rowHints[r].length; h++) {
                let xIndexOffset = board.rowHints[r].length - h - 1;
                let startX = sectionCoords.rowBanner.x + sectionCoords.rowBanner.width - cellWidth;
                startX -= xIndexOffset * cellWidth + xIndexOffset * CELL_PADDING;
                let startY = sectionCoords.rowBanner.y + r * cellWidth + r * CELL_PADDING - cam.coords.y;
                let sprite = null;
                if(board.rowHints[r][h].state == CELL_BLANK) {
                    sprite = SPRITES.CELL_BLANK;
                } else {
                    sprite = SPRITES.CELL_FILLED;
                }
                drawSprite(sprite, startX, startY, cellWidth, cellHeight);
                if(board.defiedRows[r]) {
                    canvas.lineWidth = 5;
                    canvas.strokeStyle = COLOR_HINT_DEFIED_TINT;
                    canvas.strokeRect(startX, startY, cellWidth, cellHeight);
                    canvas.fillStyle = COLOR_HINT_DEFIED;
                } else {
                    canvas.fillStyle = COLOR_HINT[gameSide];
                }
                canvas.fillText(board.rowHints[r][h].nr, startX + cellWidth / 2, startY + cellHeight / 2 + 2);
            }
        }

        canvas.restore();

        // Column hints.
        cellWidth = CELL_SIZE;
        cellHeight = CELL_SIZE;
        
        canvas.save();
        canvas.beginPath();
        canvas.rect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height);
        canvas.clip();
        
        canvas.fillStyle = COLOR_HINTS_BG;
        canvas.fillRect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height);

        for(c = 0; c < board.nrCols; c++) {
            for(h = 0; h < board.colHints[c].length; h++) {
                let yIndexOffset = board.colHints[c].length - h - 1;
                let startX = sectionCoords.colBanner.x + c * cellWidth + c * CELL_PADDING - cam.coords.x;
                let startY = sectionCoords.colBanner.y + sectionCoords.colBanner.height - cellHeight;
                startY -= yIndexOffset * cellHeight + yIndexOffset * CELL_PADDING;
                let sprite = null;
                if(board.colHints[c][h].state == CELL_BLANK) {
                    sprite = SPRITES.CELL_BLANK;
                } else {
                    sprite = SPRITES.CELL_FILLED;
                }
                drawSprite(sprite, startX, startY, cellWidth, cellHeight);
                if(board.defiedCols[c]) {
                    canvas.lineWidth = 5;
                    canvas.strokeStyle = COLOR_HINT_DEFIED_TINT;
                    canvas.strokeRect(startX, startY, cellWidth, cellHeight);
                    canvas.fillStyle = COLOR_HINT_DEFIED;
                } else {
                    canvas.fillStyle = COLOR_HINT[gameSide];
                }
                canvas.fillText(board.colHints[c][h].nr, startX + cellWidth / 2, startY + cellHeight / 2 + 2);
            }
        }

        canvas.restore();

    } else if (state == STATE_MAKING) {

        // Maker grid tools.
        canvas.font = 'bold ' + (CELL_SIZE - 2) + 'px sans';
        canvas.textAlign = 'center';
        canvas.textBaseline = 'middle';

        // Row hints.
        canvas.save();
        canvas.beginPath();
        canvas.rect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height);
        canvas.clip();

        canvas.fillStyle = COLOR_HINTS_BG;
        canvas.fillRect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height);

        for(r = 0; r < board.nrRows; r++) {
            for(b = 0; b < 3; b++) {
                let xIndexOffset = 3 - b - 1;
                let startX = sectionCoords.rowBanner.x + sectionCoords.rowBanner.width - CELL_SIZE;
                startX -= xIndexOffset * CELL_SIZE + xIndexOffset * CELL_PADDING;
                let startY = sectionCoords.rowBanner.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;
                let sprite = SPRITES.CELL_BLANK;
                drawSprite(sprite, startX, startY, CELL_SIZE, CELL_SIZE);
                let text = '';
                switch(b) {
                case 0:
                    canvas.fillStyle = COLOR_MAKER_RED;
                    text = 'x'
                    break;
                case 1:
                    canvas.fillStyle = COLOR_MAKER_GREEN;
                    text = '↑'
                    break;
                case 2:
                    canvas.fillStyle = COLOR_MAKER_GREEN;
                    text = '↓';
                    break;
                }
                canvas.fillText(text, startX + CELL_SIZE / 2, startY + CELL_SIZE / 2 + 2);
            }
        }

        canvas.restore();

        // Column hints.
        canvas.save();
        canvas.beginPath();
        canvas.rect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height);
        canvas.clip();
        
        canvas.fillStyle = COLOR_HINTS_BG;
        canvas.fillRect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height);

        for(c = 0; c < board.nrCols; c++) {
            for(b = 0; b < 3; b++) {
                let yIndexOffset = 3 - b - 1;
                let startX = sectionCoords.colBanner.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;
                let startY = sectionCoords.colBanner.y + sectionCoords.colBanner.height - CELL_SIZE;
                startY -= yIndexOffset * CELL_SIZE + yIndexOffset * CELL_PADDING;
                let sprite = SPRITES.CELL_BLANK;
                drawSprite(sprite, startX, startY, CELL_SIZE, CELL_SIZE);
                let text = '';
                switch(b) {
                case 0:
                    canvas.fillStyle = COLOR_MAKER_RED;
                    text = 'x'
                    break;
                case 1:
                    canvas.fillStyle = COLOR_MAKER_GREEN;
                    text = '←'
                    break;
                case 2:
                    canvas.fillStyle = COLOR_MAKER_GREEN;
                    text = '→';
                    break;
                }
                canvas.fillText(text, startX + CELL_SIZE / 2, startY + CELL_SIZE / 2 + 2);
            }
        }

        canvas.restore();

    }

    // Footer.
    canvas.fillStyle = COLOR_FOOTER_BG;
    canvas.fillRect(sectionCoords.footer.x, sectionCoords.footer.y, sectionCoords.footer.width, sectionCoords.footer.height);
    
    let logoXY = calculateChildCoords(guiItemCoords.playingTitle, sectionCoords.footer);
    let logoWH = calculateChildDimensions(guiItemCoords.playingTitle, logoXY, sectionCoords.footer);
    let levelNumberStr = board.levelNumber == 0 ? 'Custom Level' : ('Level ' + board.levelNumber);
    
    drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.pause, sectionCoords.footer, '...', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
    drawSprite(SPRITES.LOGO, logoXY.x, logoXY.y, logoWH.width, logoWH.height);
    drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.playingLevel, sectionCoords.footer, levelNumberStr, COLOR_TEXT_GENERIC[gameSide], 'bold 20px sans');
}


/**
 * Draws a generic GUI item in specific coordinates, like a button, some text, an image, etc.
 * @param {number} type Type of GUI item.
 * @param {object} coords Coordinates of the object, relative to the parent.
 * @param {object} parentCoords Coordinates of the parent section.
 * @param {any} fg Contents of the foreground, if any.
 * @param {any} fgStyle Foreground style, if any.
 * @param {any} fgFont Foreground content font, if any.
 * @param {any} bgStyle Background style. If undefined, the background won't be drawn.
 */
function drawGuiItem(type, coords, parentCoords, fg, fgStyle, fgFont, bgStyle) {
    if(parentCoords == undefined) {
        parentCoords = {
            x: 0,
            y: 0,
            width: CANVAS.WIDTH,
            height: CANVAS.HEIGHT
        };
    }

    let finalXY = calculateChildCoords(coords, parentCoords);
    let finalWH = calculateChildDimensions(coords, finalXY, parentCoords);
    let finalCoords = {
        x: finalXY.x,
        y: finalXY.y,
        width: finalWH.width,
        height: finalWH.height
    };

    // Button shadow.
    if(type == GUI_ITEM_BUTTON && bgStyle != undefined) {
        canvas.fillStyle = COLOR_BUTTON_SHADOW[gameSide];
        canvas.fillRect(finalCoords.x + 4, finalCoords.y + 4, finalCoords.width, finalCoords.height);
    }

    // Background.
    if(bgStyle != undefined) {
        canvas.fillStyle = bgStyle;
        canvas.fillRect(finalCoords.x, finalCoords.y, finalCoords.width, finalCoords.height);
    }

    // Foreground.
    if(fg != undefined) {
        if(fgStyle != undefined) {
            canvas.fillStyle = fgStyle;
        }
        if(fgFont != undefined) {
            canvas.font = fgFont;
        }
        canvas.fillText(fg, finalCoords.x + finalCoords.width / 2, finalCoords.y + finalCoords.height / 2 + 2);
    }
}


/**
 * Draws the main menu.
 */
function drawMainMenu() {
    // Header.
    canvas.fillStyle = COLOR_FOOTER_BG;
    canvas.fillRect(sectionCoords.mainMenuHeader.x + SECTION_PADDING, sectionCoords.mainMenuHeader.y + SECTION_PADDING, sectionCoords.mainMenuHeader.width - SECTION_PADDING * 2, sectionCoords.mainMenuHeader.height - SECTION_PADDING * 2);

    let bookmarkData = getBookmarkData();
    let logoXY = calculateChildCoords(guiItemCoords.mainMenuTitle, sectionCoords.mainMenuHeader);
    let logoWH = calculateChildDimensions(guiItemCoords.mainMenuTitle, logoXY, sectionCoords.mainMenuHeader);
    drawSprite(SPRITES.LOGO, logoXY.x, logoXY.y, logoWH.width, logoWH.height);
    drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuContinue, sectionCoords.mainMenuHeader, 'Continue', bookmarkData != null ? COLOR_BUTTON_TEXT[gameSide] : COLOR_BUTTON_TEXT_DISABLED, '20px sans', COLOR_BUTTON_BG);
    drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuInfo, sectionCoords.mainMenuHeader, 'Info', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG);
    drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuMake, sectionCoords.mainMenuHeader, 'Make custom', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG);
    drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuCustom, sectionCoords.mainMenuHeader, 'Play custom', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG);
    if(firstSideCleared()) drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuSide, sectionCoords.mainMenuHeader, 'Swap sides', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG);

    // Level selection.
    drawSprite(gameSide == 0 ? SPRITES.HOCOTATE : SPRITES.KOPPAI, sectionCoords.levelSelect.x + SECTION_PADDING, sectionCoords.levelSelect.y + SECTION_PADDING, sectionCoords.levelSelect.width - SECTION_PADDING * 2, sectionCoords.levelSelect.height - SECTION_PADDING * 2);
    canvas.fillStyle = COLOR_BOARD_BG;
    canvas.fillRect(sectionCoords.levelSelect.x + SECTION_PADDING, sectionCoords.levelSelect.y + SECTION_PADDING, sectionCoords.levelSelect.width - SECTION_PADDING * 2, sectionCoords.levelSelect.height - SECTION_PADDING * 2);

    let buttonPadding = SECTION_PADDING * 2;
    let buttonWidth = (sectionCoords.levelSelect.width - buttonPadding * 5) / 4;
    let levelHeight = (sectionCoords.levelSelect.height - buttonPadding * 5) / 4;
    let buttonHeight = levelHeight * 0.75;
    
    for(var c = 0; c < 4; c++) {
        for(var r = 0; r < 4; r++) {
            let levelNumber = r * 4 + c + 1;
            levelNumber += 16 * gameSide;
            let cleared = progression[levelNumber - 1];
            drawGuiItem(
                GUI_ITEM_BUTTON,
                {
                    x: buttonPadding + c * (buttonWidth + buttonPadding),
                    y: buttonPadding + r * (levelHeight + buttonPadding),
                    width: buttonWidth,
                    height: buttonHeight
                },
                sectionCoords.levelSelect,
                '',
                COLOR_BUTTON_TEXT[gameSide],
                'bold 48px sans',
                COLOR_BUTTON_BG
            );
            if(cleared) {
                drawMiniature(
                    levelsData[levelNumber - 1].nrRows,
                    levelsData[levelNumber - 1].nrCols,
                    levelsData[levelNumber - 1].cells,
                    {
                        x: buttonPadding + c * (buttonWidth + buttonPadding) + 8,
                        y: buttonPadding + r * (levelHeight + buttonPadding) + 8,
                        width: buttonWidth - 16,
                        height: buttonHeight - 16
                    },
                    sectionCoords.levelSelect,
                    COLOR_BUTTON_TEXT[gameSide]
                );
                canvas.textAlign = 'left';
                canvas.textBaseline = 'top';
                drawGuiItem(
                    GUI_ITEM_TEXT,
                    {
                        x: buttonPadding + c * (buttonWidth + buttonPadding),
                        y: buttonPadding + r * (levelHeight + buttonPadding),
                        width: 10,
                        height: 5
                    },
                    sectionCoords.levelSelect,
                    levelsData[levelNumber - 1].nrRows + 'x' + levelsData[levelNumber - 1].nrCols,
                    COLOR_BUTTON_SHADOW[gameSide],
                    '9px sans'
                );
            }
            let levelName = levelNumber + ': ' + (cleared ? levelsData[levelNumber - 1].name : '???');
            canvas.textAlign = 'center';
            canvas.textBaseline = 'middle';
            drawGuiItem(
                GUI_ITEM_TEXT,
                {
                    x: buttonPadding + c * (buttonWidth + buttonPadding),
                    y: buttonPadding + r * (levelHeight + buttonPadding) + buttonHeight + 2,
                    width: buttonWidth,
                    height: (levelHeight - buttonHeight)
                },
                sectionCoords.levelSelect,
                levelName,
                COLOR_TEXT_GENERIC[gameSide],
                'bold 14px sans'
            );
        }
    }
}


/**
 * Draws a miniature of the puzzle on-screen.
 * @param {number} nrRows Number of rows to draw.
 * @param {number} nrCols Number of columns to draw.
 * @param {array} cells Cells to draw.
 * @param {object} coords X, Y, width, and height of the miniature.
 * @param {object} parentCoords Object with the parent coordinates.
 * @param {color} color Color of each pixel.
 */
function drawMiniature(nrRows, nrCols, cells, coords, parentCoords, color) {
    if(parentCoords == undefined) {
        parentCoords = {
            x: 0,
            y: 0,
            width: CANVAS.WIDTH,
            height: CANVAS.HEIGHT
        };
    }

    let finalXY = calculateChildCoords(coords, parentCoords);
    let finalWH = calculateChildDimensions(coords, finalXY, parentCoords);
    let finalCoords = {
        x: finalXY.x,
        y: finalXY.y,
        width: finalWH.width,
        height: finalWH.height
    };

    let miniatureCellNormalWidth = finalCoords.width / nrCols;
    let miniatureCellNormalHeight = finalCoords.height / nrRows;
    let miniatureCellSize = Math.min(miniatureCellNormalWidth, miniatureCellNormalHeight);
    let miniatureFullWidth = miniatureCellSize * nrCols;
    let miniatureFullHeight = miniatureCellSize * nrRows;
    let miniatureStartX = finalCoords.x + (finalCoords.width - miniatureFullWidth) / 2;
    let miniatureStartY = finalCoords.y + (finalCoords.height - miniatureFullHeight) / 2;
    for(var r = 0; r < nrRows; r++) {
        for(var c = 0; c < nrCols; c++) {
            if(cells[r][c] != CELL_FILLED) {
                continue;
            }
            let startX = Math.round(miniatureStartX + c * miniatureCellSize);
            let startY = Math.round(miniatureStartY + r * miniatureCellSize);

            canvas.fillStyle = color;
            canvas.fillRect(
                startX,
                startY,
                Math.ceil(miniatureCellSize),
                Math.ceil(miniatureCellSize)
            );
        }
    }
}


/**
 * Draws the panel.
 */
function drawPanel() {
    // Background.
    canvas.fillStyle = 'rgba(0, 0, 0, 0.5)';
    canvas.fillRect(0, 0, CANVAS.WIDTH, CANVAS.HEIGHT);

    canvas.fillStyle = 'rgba(0, 0, 0, 0.5)';
    canvas.fillRect(sectionCoords.panel.x + 8, sectionCoords.panel.y + 8, sectionCoords.panel.width, sectionCoords.panel.height);
    
    canvas.fillStyle = COLOR_FOOTER_BG;
    canvas.fillRect(sectionCoords.panel.x, sectionCoords.panel.y, sectionCoords.panel.width, sectionCoords.panel.height);

    switch(panelState) {
    case PANEL_STATE_PAUSE:
        // Menu buttons.
        drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.continue, sectionCoords.panel, 'Continue', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.restart, sectionCoords.panel, 'Restart', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        if(state == STATE_MAKING) {
            drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.finish, sectionCoords.panel, 'Finish', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        }
        drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.quit, sectionCoords.panel, 'Quit', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        break;
    
    case PANEL_STATE_CONGRATS:
        // Congrats screen.
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.congratsHeader, sectionCoords.panel, (board.levelNumber == 0 ? 'CUSTOM LEVEL' : ('LEVEL ' + board.levelNumber)) + ' CLEAR!', COLOR_BUTTON_TEXT[gameSide], '36px sans');
        drawMiniature(board.nrRows, board.nrCols, board.cells, guiItemCoords.congratsMiniature, sectionCoords.panel, COLOR_BUTTON_TEXT[gameSide]);
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.congratsLevelName, sectionCoords.panel, board.name, COLOR_BUTTON_TEXT[gameSide], 'italic 24px sans');
        drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        break;

    case PANEL_STATE_MAKING_NAME:
        // Asking the new puzzle's name.
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.newPuzzleNamePrompt, sectionCoords.panel, 'What\'s this puzzle\'s name?', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        break;

    case PANEL_STATE_MAKING_CODE:
        // Showing the new puzzle's code.
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.newPuzzleDoneText, sectionCoords.panel, 'Done! Copy this code and share it around!', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        break;

    case PANEL_STATE_PLAYING_CODE:
        // Asking a puzzle's code to play on.
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.customPuzzleText, sectionCoords.panel, 'Please paste the puzzle\'s code here.', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        break;

    case PANEL_STATE_LOAD_ERROR:
        // Warning the player there was an error while loading.
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError1, sectionCoords.panel, 'Error loading level!', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError2, sectionCoords.panel, 'Please make sure everything', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError3, sectionCoords.panel, 'is correct and try again.', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        break;

    case PANEL_STATE_SAVE_ERROR:
        // Warning the player there was an error while saving.
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError1, sectionCoords.panel, 'Error saving level!', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError2, sectionCoords.panel, 'You must have at least', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError3, sectionCoords.panel, 'one filled cell!', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        break;

    case PANEL_STATE_BOOKMARK_WARNING:
        // Warning the player they have a bookmark when starting a level.
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning1, sectionCoords.panel, 'If you start a new puzzle, you\'ll', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning2, sectionCoords.panel, 'lose your old bookmark progress! In', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning3, sectionCoords.panel, 'the main menu, press "Continue" to', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning4, sectionCoords.panel, 'resume your bookmark, or press the', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning5, sectionCoords.panel, 'puzzle button again to start anyway.', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        break;

    case PANEL_STATE_INFO:
        // Game info.
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info1, sectionCoords.panel, '**TODO: INFO GOES HERE**', COLOR_BUTTON_BG, 'bold 20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info2, sectionCoords.panel, '( Note added by Pikipedia:', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info3, sectionCoords.panel, 'Information on Pikcross, including', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info4, sectionCoords.panel, 'how to play, can be found here:', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info5, sectionCoords.panel, 'https://www.pikminwiki.com/Pikcross )', COLOR_BUTTON_BG, '20px sans');
        drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
        break;

    }
    
}


/**
 * Draws a sprite.
 * @param object data Data about the sprite in the spritesheet, in the form {x, y, width, height}.
 * @param number x Top-left X coordinate to draw on.
 * @param number y Top-left Y coordinate to draw on.
 * @param number w Width to draw at.
 * @param number h Height to draw at.
 */
function drawSprite(data, x, y, w, h) {
    canvas.drawImage(sprites, data.x, data.y, data.width, data.height, x, y, w, h);
}


/**
 * Encodes a board and some more data into a base64-encoded string.
 * @param {object} data Board data to encode.
 * @param {number} context BOARD_DATA_CONTEXT_LEVEL to encode level data. BOARD_DATA_CONTEXT_BOOKMARK to encode bookmark data.
 * @returns Encoded board text. Returns null on error.
 */
function encodeBoard(data, context) {
    try {
        let writer = new StrBitWriter();
        let nrCellBits = context == BOARD_DATA_CONTEXT_LEVEL ? 1 : 2;
        
        writer.writeNumber(data.nrRows);
        writer.writeNumber(data.nrCols);
        for(var r = 0; r < data.nrRows; r++) {
            for(var c = 0; c < data.nrCols; c++) {
                writer.writeNumber(data.cells[r][c], nrCellBits);
            }
        }

        if(context == BOARD_DATA_CONTEXT_LEVEL) {

            writer.writeNumber(data.name.length);
            for(var c = 0; c < data.name.length; c++) {
                writer.writeNumber(data.name.charCodeAt(c));
            }

        } else {

            writer.writeNumber(data.nrRowHints);
            writer.writeNumber(data.nrColHints);
            for(var r = 0; r < data.nrRowHints; r++) {
                writer.writeNumber(data.rowHints[r], 1);
            }
            for(var c = 0; c < data.nrColHints; c++) {
                writer.writeNumber(data.colHints[c], 1);
            }

            writer.writeNumber(data.levelNumber);

            writer.writeNumber(data.levelCode.length);
            for(var c = 0; c < data.levelCode.length; c++) {
                writer.writeNumber(data.levelCode.charCodeAt(c));
            }

        }

        return btoa(writer.getStr());

    } catch {
        return null;
    }
}


/**
 * Given a list of cells, finds the first cell that is marked. It only checks inside of a given range.
 * @param {array} cells List of cells to check in.
 * @param {number} startIdx Index to start the check in, inclusive.
 * @param {number} length Number of cells to check.
 * @returns The index of the first marked cell, or -1 if none are marked.
 */
function findMarkedCellInList(cells, startIdx, length) {
    for(var c = startIdx; c < startIdx + length; c++) {
        if(c >= cells.length) return c;
        if(cells[c] == CELL_MARKED) return c;
    }
    return -1;
}


/**
 * Returns whether the first side (i.e. the first 16 puzzles) is cleared or not.
 * @returns True if cleared, false otherwise.
 */
function firstSideCleared() {
    for(var l = 0; l < 16; l++) {
        if(!progression[l]) return false;
    }
    return true;
}


/**
 * Returns which cell of the board is under the given screen coordinates.
 * @param {object} coords Object with the coordinates.
 * @returns The row and column, in an array.
 */
function getCellInCoords(coords) {
    let x = coords.x - sectionCoords.board.x + cam.coords.x;
    let y = coords.y - sectionCoords.board.y + cam.coords.y;
    let col = Math.floor(x / (CELL_SIZE + CELL_PADDING));
    let row = Math.floor(y / (CELL_SIZE + CELL_PADDING));
    return [row, col];
}


/**
 * Returns which column hint/maker button is under the given screen coordinates.
 * @param {object} coords Object with the coordinates.
 * @returns The column and button index numbers, in an array. Returns null if none.
 */
function getColumnBannerButtonInCoords(coords) {
    let hintsClickCoords = {
        x: coords.x - sectionCoords.colBanner.x + cam.coords.x,
        y: coords.y - sectionCoords.colBanner.y
    };
    let col = Math.floor(hintsClickCoords.x / (CELL_SIZE + CELL_PADDING));
    
    if(col < 0 || col >= board.nrCols) return null;

    let button = Math.floor(hintsClickCoords.y / (CELL_SIZE + CELL_PADDING));
    let maxButtons = Math.floor(sectionCoords.colBanner.height / (CELL_SIZE + CELL_PADDING));
    let nrButtonsInCol = 0;
    if(state == STATE_PLAYING) {
        nrButtonsInCol = board.colHints[col].length;
    } else if(state == STATE_MAKING) {
        nrButtonsInCol = 3;
    }
    button -= maxButtons - nrButtonsInCol;

    if(button < 0) return null;
    if(state == STATE_PLAYING) {
        if(button >= board.colHints[col].length) return null;
    } else if(state == STATE_MAKING) {
        if(button >= 3) return null;
    }

    return [col, button];
}


/**
 * Returns an array with the combos the player has for the given line.
 * @param {number} row Row number, if we're checking a row. null otherwise.
 * @param {number} col Column number, if we're checking a column. null otherwise.
 * @returns An array where each object is a combo.
 */
function getPlayerLineCombos(row, col) {
    let playerCombos = [];
    let curCombo = 0;
    let curComboStart = 0;

    if(row != null) {
        for(var c = 0; c < board.nrCols; c++) {
            if(board.cells[row][c] == CELL_FILLED) {
                if(curCombo == 0) {
                    curComboStart = c;
                }
                curCombo++;
            } else {
                if(curCombo > 0) playerCombos.push({start: curComboStart, nr: curCombo});
                curCombo = 0;
            }
        }    
    } else {
        for(var r = 0; r < board.nrRows; r++) {
            if(board.cells[r][col] == CELL_FILLED) {
                if(curCombo == 0) {
                    curComboStart = r;
                }
                curCombo++;
            } else {
                if(curCombo > 0) playerCombos.push({start: curComboStart, nr: curCombo});
                curCombo = 0;
            }
        } 
    }

    if(curCombo > 0) playerCombos.push({start: curComboStart, nr: curCombo});

    return playerCombos;
}


/**
 * Returns which row hint/maker button is under the given screen coordinates.
 * @param {object} coords Object with the coordinates.
 * @returns The row and button index numbers, in an array. Returns null if none.
 */
function getRowBannerButtonInCoords(coords) {
    let hintsClickCoords = {
        x: coords.x - sectionCoords.rowBanner.x,
        y: coords.y - sectionCoords.rowBanner.y + cam.coords.y
    };
    let row = Math.floor(hintsClickCoords.y / (CELL_SIZE + CELL_PADDING));

    if(row < 0 || row >= board.nrRows) return null;

    let button = Math.floor(hintsClickCoords.x / (CELL_SIZE + CELL_PADDING));
    let maxButtons = Math.floor(sectionCoords.rowBanner.width / (CELL_SIZE + CELL_PADDING));
    let nrButtonsInRow = 0;
    if(state == STATE_PLAYING) {
        nrButtonsInRow = board.rowHints[row].length;
    } else if(state == STATE_MAKING) {
        nrButtonsInRow = 3;
    }
    button -= maxButtons - nrButtonsInRow;

    if(button < 0) return null;
    if(state == STATE_PLAYING) {
        if(button >= board.rowHints[row].length) return null;
    } else if(state == STATE_MAKING) {
        if(button >= 3) return null;
    }

    return [row, button];
}


/**
 * Hides the input box HTML element in the middle of the canvas.
 */
function hideInputBox() {
    inputEl.style.display = 'none';
}


/**
 * Checks if a given point is inside a set of coordinates.
 * @param point Point to check, in the format {x, y}.
 * @param coords Coordinates to check, in the format {x, y, width, height}.
 * @param parentCoords If not undefined, then the previous coordinates are relative to these ones.
 */ 
function isPointInCoords(point, coords, parentCoords) {
    let finalCoords = {
        x: coords.x,
        y: coords.y,
        width: coords.width,
        height: coords.height
    };

    if(parentCoords != undefined) {
        let finalXY = calculateChildCoords(coords, parentCoords);
        let finalWH = calculateChildDimensions(coords, finalXY, parentCoords);
        finalCoords = {
            x: finalXY.x,
            y: finalXY.y,
            width: finalWH.width,
            height: finalWH.height
        };
    }

    return (
        point.x >= finalCoords.x &&
        point.y >= finalCoords.y &&
        point.x <= finalCoords.x + finalCoords.width &&
        point.y <= finalCoords.y + finalCoords.height
    );
}


/**
 * Loads the player's bookmark, if any, and returns its info.
 * @returns null if no data exists, or an object with the data otherwise.
 */
function getBookmarkData() {
    var bookmarkString = localStorage.getItem('pikcrossBookmark');
    
    if(bookmarkString == null || bookmarkString == '') return null;

    return decodeBoard(bookmarkString, BOARD_DATA_CONTEXT_BOOKMARK);
}


/**
 * Loads a level and initializes the board game screen.
 * @param {number} levelNumber Level number, or 0 for custom.
 * @param {string} levelCode Level code.
 * @param {number} stateContext What game state is this being loaded for?
 * @returns True on success, false on failure.
 */
function loadLevel(levelNumber, levelCode, stateContext) {
    if(stateContext == STATE_PLAYING && levelCode.length < 3) {
        return false;
    }

    let nrRowBannerCells = 0;
    let nrColBannerCells = 0;

    // Cleanup.
    cam.coords.x = 0;
    cam.coords.y = 0;

    board.name = 'Puzzle';
    board.nrRows = 0;
    board.nrCols = 0;
    board.cells = [];
    board.solution = [];
    board.rowHints = [];
    board.colHints = [];
    board.autoMarkedRows = [];
    board.autoMarkedCols = [];
    board.defiedRows = [];
    board.defiedCols = [];

    board.levelNumber = levelNumber;
    board.levelCode = levelCode;

    if(stateContext == STATE_PLAYING) {

        // Load board.
        let levelData = decodeBoard(levelCode, BOARD_DATA_CONTEXT_LEVEL);
        
        // Sanity check.
        if(levelData == null) return false;
        if(levelData.nrRows < MIN_COLS_OR_ROWS) return false;
        if(levelData.nrRows > MAX_COLS_OR_ROWS) return false;
        if(levelData.nrCols < MIN_COLS_OR_ROWS) return false;
        if(levelData.nrCols > MAX_COLS_OR_ROWS) return false;
        let hasFilledCells = false;
        for(var r = 0; r < levelData.nrRows; r++) {
            for(var c = 0; c < levelData.nrCols; c++) {
                if(levelData.cells[r][c] == CELL_FILLED) {
                    hasFilledCells = true;
                    break;
                }
            }
            if(hasFilledCells) break;
        }
        if(!hasFilledCells) return false;
        if(levelData.name.length == 0) levelData.name = 'Puzzle';
        
        board.nrRows = levelData.nrRows;
        board.nrCols = levelData.nrCols;
        board.solution = levelData.cells;
        board.name = levelData.name;

        // Hints.
        for(var r = 0; r < board.nrRows; r++) {
            board.rowHints.push([]);
            let hintNr = 0;
            for(var c = 0; c < board.nrCols; c++) {
                if(board.solution[r][c] == CELL_FILLED) {
                    hintNr++;
                } else {
                    if(hintNr > 0) {
                        board.rowHints[r].push({ state: CELL_BLANK, nr: hintNr });
                    }
                    hintNr = 0;
                }
            }
            if(hintNr > 0) {
                board.rowHints[r].push({ state: CELL_BLANK, nr: hintNr });
            }
            nrRowBannerCells = Math.max(nrRowBannerCells, board.rowHints[r].length);
        }
        for(var c = 0; c < board.nrCols; c++) {
            board.colHints.push([]);
            let hintNr = 0;
            for(var r = 0; r < board.nrRows; r++) {
                if(board.solution[r][c] == CELL_FILLED) {
                    hintNr++;
                } else {
                    if(hintNr > 0) {
                        board.colHints[c].push({ state: CELL_BLANK, nr: hintNr });
                    }
                    hintNr = 0;
                }
            }
            if(hintNr > 0) {
                board.colHints[c].push({ state: CELL_BLANK, nr: hintNr });
            }
            nrColBannerCells = Math.max(nrColBannerCells, board.colHints[c].length);
        }

    } else if(stateContext == STATE_MAKING) {

        // Create an empty board.
        board.nrRows = 10;
        board.nrCols = 10;
        for(var r = 0; r < board.nrRows; r++) {
            board.cells.push([]);
            for(var c = 0; c < board.nrCols; c++) {
                board.cells[r].push(CELL_BLANK);
            }
        }

        // Maker grid tools.
        nrRowBannerCells = 3;
        nrColBannerCells = 3;

    }
    
    // Current cells state.
    board.cells = [];
    for(var r = 0; r < board.nrRows; r++) {
        board.cells.push([]);
        for(var c = 0; c < board.nrCols; c++) {
            board.cells[r].push(CELL_BLANK);
        }
    }

    for(var r = 0; r < board.nrRows; r++) {
        board.autoMarkedRows.push(false);
        board.defiedRows.push(false);
    }
    for(var c = 0; c < board.nrCols; c++) {
        board.autoMarkedCols.push(false);
        board.defiedCols.push(false);
    }

    // Coordinates.
    sectionCoords.miniature.x = SECTION_PADDING;
    sectionCoords.miniature.y = SECTION_PADDING;
    sectionCoords.miniature.width = nrRowBannerCells * (CELL_SIZE + CELL_PADDING);
    sectionCoords.miniature.height = nrColBannerCells * (CELL_SIZE + CELL_PADDING);
    
    sectionCoords.footer.x = sectionCoords.miniature.x;
    sectionCoords.footer.y = CANVAS.HEIGHT - SECTION_PADDING - 64;
    sectionCoords.footer.width = CANVAS.WIDTH - SECTION_PADDING * 2;
    sectionCoords.footer.height = 64;

    sectionCoords.rowBanner.x = sectionCoords.miniature.x;
    sectionCoords.rowBanner.y = sectionCoords.miniature.y + sectionCoords.miniature.height + SECTION_PADDING;
    sectionCoords.rowBanner.width = sectionCoords.miniature.width;
    sectionCoords.rowBanner.height = sectionCoords.footer.y - SECTION_PADDING - sectionCoords.rowBanner.y;

    sectionCoords.colBanner.x = sectionCoords.miniature.x + sectionCoords.miniature.width + SECTION_PADDING;
    sectionCoords.colBanner.y = sectionCoords.miniature.y;
    sectionCoords.colBanner.width = CANVAS.WIDTH - SECTION_PADDING - sectionCoords.colBanner.x;
    sectionCoords.colBanner.height = sectionCoords.miniature.height;

    sectionCoords.board.x = sectionCoords.colBanner.x;
    sectionCoords.board.y = sectionCoords.rowBanner.y;
    sectionCoords.board.width = sectionCoords.colBanner.width;
    sectionCoords.board.height = sectionCoords.rowBanner.height;

    // Border gradients.
    const boardX2 = sectionCoords.board.x + sectionCoords.board.width;
    const boardY2 = sectionCoords.board.y + sectionCoords.board.height;
    
    borderGradients.left = canvas.createLinearGradient(sectionCoords.board.x, 0, sectionCoords.board.x + BORDER_GRADIENT_LENGTH, 0),
    borderGradients.left.addColorStop(1, 'rgba(0, 128, 0, 0.0)');
    borderGradients.left.addColorStop(0, 'rgba(0, 128, 0, 0.5)');
    
    borderGradients.right = canvas.createLinearGradient(boardX2, 0, boardX2 - BORDER_GRADIENT_LENGTH, 0),
    borderGradients.right.addColorStop(1, 'rgba(0, 128, 0, 0.0)');
    borderGradients.right.addColorStop(0, 'rgba(0, 128, 0, 0.5)');
    
    borderGradients.up = canvas.createLinearGradient(0, sectionCoords.board.y, 0, sectionCoords.board.y + BORDER_GRADIENT_LENGTH),
    borderGradients.up.addColorStop(1, 'rgba(0, 128, 0, 0.0)');
    borderGradients.up.addColorStop(0, 'rgba(0, 128, 0, 0.5)');
    
    borderGradients.down = canvas.createLinearGradient(0, boardY2, 0, boardY2 - BORDER_GRADIENT_LENGTH),
    borderGradients.down.addColorStop(1, 'rgba(0, 128, 0, 0.0)');
    borderGradients.down.addColorStop(0, 'rgba(0, 128, 0, 0.5)');
    
    if(stateContext == STATE_PLAYING) {
        for(var r = 0; r < board.nrRows; r++) {
            updateRow(r);
        }
        for(var c = 0; c < board.nrCols; c++) {
            updateColumn(c);
        }
    }

    return true;
}


/**
 * Loads the player's global progression.
 */
function loadProgression() {
    let progressionStr = localStorage.getItem('pikcrossProgression');
    if(progressionStr == null || progressionStr.length == 0) {
        return;
    }
    let reader = new StrBitReader(progressionStr);
    for(var l = 0; l < LEVELS.length; l++) {
        progression[l] = (reader.readNumber(1) == 1);
    }
}


/**
 * Handler for when the player does an input down on the canvas.
 * This happens regardless of it being a mouse button down press, or a mobile touch start.
 * @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons.
 * @param {number} mobile True if it was a mobile touch, false otherwise.
 */
function onCanvasInputDown(button, mobile) {
    for(var l = 0; l < loaded.length; l++) {
        if(!loaded[l]) return;
    }
    if(transitionAnimTime > 0) return;

    if(inPanel) {
        onCanvasInputDownInPanel(button, mobile);
        return;
    }

    switch(state) {
    case STATE_MAIN_MENU:
        onCanvasInputDownInMainMenu(button, mobile);
        break;
    case STATE_PLAYING:
    case STATE_MAKING:
        let changesMade = onCanvasInputDownInGameplay(button, mobile);
        if(changesMade) {
            input.dragging = true;
            input.dragStart.x = input.screenCoords.x;
            input.dragStart.y = input.screenCoords.y;
        }
        break;
    }
}


/**
 * Handler for when the player does an input down on the canvas, in the board game screen.
 * This happens regardless of it being a mouse button down press, or a mobile touch start.
 * @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons.
 * @param {number} mobile True if it was a mobile touch, false otherwise.
 * @returns True if something happened, false otherwise.
 */
function onCanvasInputDownInGameplay(button, mobile) {
    let changesMade = false;

    // Figure out where the player clicked.
    if(isPointInCoords(input.screenCoords, sectionCoords.board)) {
        // Clicked on the board.

        let idxs = getCellInCoords(input.screenCoords);
        if(mobile) {
            if(toggleCellFillAndMark(idxs[0], idxs[1])) {
                changesMade = true;
            }
        } else if(button == 0) {
            if(toggleCellFill(idxs[0], idxs[1])) {
                changesMade = true;
            }
        } else if(button == 2 && state == STATE_PLAYING) {
            if(toggleCellMark(idxs[0], idxs[1])) {
                changesMade = true;
            }
        }

    } else if(
        state == STATE_PLAYING &&
        isPointInCoords(input.screenCoords, sectionCoords.rowBanner)
    ) {
        // Clicked on the row hints.

        let idxs = getRowBannerButtonInCoords(input.screenCoords);
        if(idxs != null && button == 0) {
            if(toggleRowHint(idxs[0], idxs[1])) {
                updateRow(idxs[0]);
                saveBookmark();
                changesMade = true;
            }
        }

    } else if(
        state == STATE_PLAYING &&
        isPointInCoords(input.screenCoords, sectionCoords.colBanner)
    ) {
        // Clicked on the column hints.

        let idxs = getColumnBannerButtonInCoords(input.screenCoords);
        if(idxs != null && button == 0) {
            if(toggleColumnHint(idxs[0], idxs[1])) {
                updateColumn(idxs[0]);
                saveBookmark();
                changesMade = true;
            }
        }
        
    } else if(
        state == STATE_MAKING &&
        isPointInCoords(input.screenCoords, sectionCoords.rowBanner)
    ) {
        // Clicked on the maker mode row buttons.

        let idxs = getRowBannerButtonInCoords(input.screenCoords);
        if(idxs != null && button == 0) {
            if(idxs[1] == 0) {
                // Delete row.
                if(board.nrRows > MIN_COLS_OR_ROWS) {
                    board.cells.splice(idxs[0], 1);
                    board.nrRows--;
                    clampCamera();
                    changesMade = true;
                }
            } else if(idxs[1] == 1) {
                // New row above.
                if(board.nrRows < MAX_COLS_OR_ROWS) {
                    let newCol = [];
                    for(var c = 0; c < board.nrCols; c++) {
                        newCol.push(CELL_BLANK);
                    }
                    board.cells.splice(idxs[0], 0, newCol);
                    board.nrRows++;
                    changesMade = true;
                }
            } else if(idxs[1] == 2) {
                // New row below.
                if(board.nrRows < MAX_COLS_OR_ROWS) {
                    let newCol = [];
                    for(var c = 0; c < board.nrCols; c++) {
                        newCol.push(CELL_BLANK);
                    }
                    board.cells.splice(idxs[0] + 1, 0, newCol);
                    board.nrRows++;
                    changesMade = true;
                }
            }
        }

    } else if(
        state == STATE_MAKING &&
        isPointInCoords(input.screenCoords, sectionCoords.colBanner)
    ) {
        // Clicked on the maker mode column buttons.

        let idxs = getColumnBannerButtonInCoords(input.screenCoords);
        if(idxs != null && button == 0) {
            if(idxs[1] == 0) {
                // Delete column.
                if(board.nrCols > MIN_COLS_OR_ROWS) {
                    for(var r = 0; r < board.nrRows; r++) {
                        board.cells[r].splice(idxs[0], 1);
                    }
                    board.nrCols--;
                    clampCamera();
                    changesMade = true;
                }
            } else if(idxs[1] == 1) {
                // New column above.
                if(board.nrCols < MAX_COLS_OR_ROWS) {
                    for(var r = 0; r < board.nrRows; r++) {
                        board.cells[r].splice(idxs[0], 0, CELL_BLANK);
                    }
                    board.nrCols++;
                    changesMade = true;
                }
            } else if(idxs[1] == 2) {
                // New column below.
                if(board.nrCols < MAX_COLS_OR_ROWS) {
                    for(var r = 0; r < board.nrRows; r++) {
                        board.cells[r].splice(idxs[0] + 1, 0, CELL_BLANK);
                    }
                    board.nrCols++;
                    changesMade = true;
                }
            }
        }
        
    } else if(isPointInCoords(input.screenCoords, guiItemCoords.pause, sectionCoords.footer)) {
        // Clicked on the pause button.

        inPanel = true;
        panelState = PANEL_STATE_PAUSE;
        changesMade = true;

    } else if(isPointInCoords(input.screenCoords, guiItemCoords.zoomIn, sectionCoords.footer)) {
        // Clicked on the zoom in button.

        cam.zoom += 0.2;

    } else if(isPointInCoords(input.screenCoords, guiItemCoords.zoomOut, sectionCoords.footer)) {
        // Clicked on the zoom out button.

        cam.zoom -= 0.2;

    }

    if(!changesMade) {
        input.dragPanning = true;
        changesMade = true;
    }

    if(changesMade) {
        updateCanvas();
    }

    return changesMade;
}


/**
 * Handler for when the player does an input down on the canvas, in the main menu.
 * This happens regardless of it being a mouse button down press, or a mobile touch start.
 * @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons.
 * @param {number} mobile True if it was a mobile touch, false otherwise.
 */
function onCanvasInputDownInMainMenu(button, mobile) {
    if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuContinue, sectionCoords.mainMenuHeader)) {
        // Clicked on the continue button.
        var bookmarkData = getBookmarkData();
        if(bookmarkData == null) return;

        if(loadLevel(bookmarkData.levelNumber, bookmarkData.levelCode, STATE_PLAYING)) {
            
            board.cells = bookmarkData.cells;
            let hintIdx = 0;
            for(var r = 0; r < board.nrRows; r++) {
                for(var h = 0; h < board.rowHints[r].length; h++) {
                    board.rowHints[r][h].state = bookmarkData.rowHints[hintIdx];
                    hintIdx++;
                }
            }
            hintIdx = 0;
            for(var c = 0; c < board.nrCols; c++) {
                for(var h = 0; h < board.colHints[c].length; h++) {
                    board.colHints[c][h].state = bookmarkData.colHints[hintIdx];
                    hintIdx++;
                }
            }

            for(c = 0; c < board.nrCols; c++) {
                updateColumn(c);
            }
            for(r = 0; r < board.nrRows; r++) {
                updateRow(r);
            }
            
            changeState(STATE_PLAYING);

        } else {

            panelState = PANEL_STATE_LOAD_ERROR;

        }

    } else if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuInfo, sectionCoords.mainMenuHeader)) {
        // Clicked on the info button.
        inPanel = true;
        panelState = PANEL_STATE_INFO;

    } else if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuMake, sectionCoords.mainMenuHeader)) {
        // Clicked on the make custom button.
        loadLevel(0, '', STATE_MAKING);
        changeState(STATE_MAKING);

    } else if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuCustom, sectionCoords.mainMenuHeader)) {
        // Clicked on the play custom button.
        let bookmarkData = getBookmarkData();
        if(bookmarkData != null && !gaveBookmarkWarning) {
            inPanel = true;
            panelState = PANEL_STATE_BOOKMARK_WARNING;
            gaveBookmarkWarning = true;
        } else {
            inPanel = true;
            panelState = PANEL_STATE_PLAYING_CODE;
            inputEl.value = '';
            showInputBox(false);
        }

    } else if(firstSideCleared() && isPointInCoords(input.screenCoords, guiItemCoords.mainMenuSide, sectionCoords.mainMenuHeader)) {
        // Clicked on the side swapping button.
        gameSide = (gameSide == 0 ? 1 : 0);
        updateCanvas();

    } else {
        // Check if the player clicked on one of the level buttons.
        let buttonPadding = SECTION_PADDING * 2;
        let buttonWidth = (sectionCoords.levelSelect.width - buttonPadding * 5) / 4;
        let levelHeight = (sectionCoords.levelSelect.height - buttonPadding * 5) / 4;
        let buttonHeight = levelHeight * 0.75;
        
        var clickedLevelIdx = -1;
        for(var c = 0; c < 4; c++) {
            for(var r = 0; r < 4; r++) {
                if(
                    isPointInCoords(
                        input.screenCoords,
                        {
                            x: buttonPadding + c * (buttonWidth + buttonPadding),
                            y: buttonPadding + r * (levelHeight + buttonPadding),
                            width: buttonWidth,
                            height: buttonHeight
                        },
                        sectionCoords.levelSelect
                    )
                ) {
                    clickedLevelIdx = r * 4 + c;
                    clickedLevelIdx += 16 * gameSide;
                    break;
                }
            }
            if(clickedLevelIdx != -1) {
                break;
            }
        }

        if(clickedLevelIdx != -1) {
            let bookmarkData = getBookmarkData();
            if(bookmarkData != null && !gaveBookmarkWarning) {
                inPanel = true;
                panelState = PANEL_STATE_BOOKMARK_WARNING;
                gaveBookmarkWarning = true;
            } else {
                if(loadLevel(clickedLevelIdx + 1, LEVELS[clickedLevelIdx], STATE_PLAYING)) {
                    changeState(STATE_PLAYING);
                    clearBookmark();
                } else {
                    panelState = PANEL_STATE_LOAD_ERROR;
                }
            }
        }
    }
}


/**
 * Handler for when the player does an input down on the canvas, in the panel.
 * This happens regardless of it being a mouse button down press, or a mobile touch start.
 * @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons.
 * @param {number} mobile True if it was a mobile touch, false otherwise.
 */
function onCanvasInputDownInPanel(button, mobile) {
    // Figure out where the player clicked.
    switch(panelState) {
    case PANEL_STATE_PAUSE:

        if(isPointInCoords(input.screenCoords, guiItemCoords.continue, sectionCoords.panel)) {
            // Clicked on the continue button.
    
            inPanel = false;
    
        } else if(isPointInCoords(input.screenCoords, guiItemCoords.restart, sectionCoords.panel)) {
            // Clicked on the restart button.
    
            inPanel = false;
            loadLevel(board.levelNumber, board.levelCode, state);
            clearBookmark();
    
        } else if(isPointInCoords(input.screenCoords, guiItemCoords.finish, sectionCoords.panel)) {
            // Clicked on the finish button.
    
            if(state == STATE_MAKING) {
                let hasFilledCells = false;
                for(var r = 0; r < board.nrRows; r++) {
                    for(var c = 0; c < board.nrCols; c++) {
                        if(board.cells[r][c] == CELL_FILLED) {
                            hasFilledCells = true;
                            break;
                        }
                    }
                    if(hasFilledCells) break;
                }

                if(hasFilledCells) {
                    panelState = PANEL_STATE_MAKING_NAME;
                    inputEl.value = board.name;
                    showInputBox(false, 20);
                } else {
                    panelState = PANEL_STATE_SAVE_ERROR;
                }
            }
    
        } else if(isPointInCoords(input.screenCoords, guiItemCoords.quit, sectionCoords.panel)) {
            // Clicked on the quit button.
    
            changeState(STATE_MAIN_MENU, function() { inPanel = false; });
            
        } else {
            return;
    
        }
        break;

    case PANEL_STATE_CONGRATS:

        if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
            // Clicked on the ok button.

            changeState(STATE_MAIN_MENU, function() { inPanel = false; });

        }
        
        break;

    case PANEL_STATE_MAKING_NAME:

        if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
            // Clicked on the ok button.

            acceptInputBox();
        }
        
        break;

    case PANEL_STATE_MAKING_CODE:

        if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
            // Clicked on the ok button.

            acceptInputBox();

        }
        
        break;

    case PANEL_STATE_PLAYING_CODE:

        if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
            // Clicked on the ok button.

            acceptInputBox();

        }
        
        break;

    case PANEL_STATE_LOAD_ERROR:

        if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
            // Clicked on the ok button.

            inPanel = false;

        }
        
        break;

    case PANEL_STATE_SAVE_ERROR:

        if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
            // Clicked on the ok button.

            inPanel = false;

        }
        
        break;

    case PANEL_STATE_BOOKMARK_WARNING:

        if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
            // Clicked on the ok button.

            inPanel = false;

        }
        
        break;

    case PANEL_STATE_INFO:

        if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
            // Clicked on the ok button.

            inPanel = false;

        }
        
        break;
    }

    updateCanvas();
}


/**
 * Handler for when the player does an input move on the canvas.
 * This happens regardless of it being a mouse button down press, or a mobile touch start.
 */
function onCanvasInputMove() {
    if(input.dragging && !input.dragPanning) {

        let snappedMouse = { x: input.screenCoords.x, y: input.screenCoords.y };
        let somethingChanged = false;
        if(input.dragLockCoord.x) snappedMouse.x = input.dragStart.x;
        if(input.dragLockCoord.y) snappedMouse.y = input.dragStart.y;

        if(
            isPointInCoords(input.dragStart, sectionCoords.board) &&
            isPointInCoords(input.screenCoords, sectionCoords.board)
        ) {
            let idxs = getCellInCoords(snappedMouse);
            if(idxs != null) {
                if(toggleCellFill(idxs[0], idxs[1])) {
                    somethingChanged = true;
                }
            }

        } else if(
            state == STATE_PLAYING &&
            isPointInCoords(input.dragStart, sectionCoords.rowBanner) &&
            isPointInCoords(input.screenCoords, sectionCoords.rowBanner)
        ) {
            let idxs = getRowBannerButtonInCoords(snappedMouse);
            if(idxs != null) {
                if(toggleRowHint(idxs[0], idxs[1])) {
                    updateRow(idxs[0]);
                    somethingChanged = true;
                }
            }

        } else if(
            state == STATE_PLAYING &&
            isPointInCoords(input.dragStart, sectionCoords.colBanner) &&
            isPointInCoords(input.screenCoords, sectionCoords.colBanner)
        ) {
            let idxs = getColumnBannerButtonInCoords(snappedMouse);
            if(idxs != null) {
                if(toggleColumnHint(idxs[0], idxs[1])) {
                    updateColumn(idxs[0]);
                    somethingChanged = true;
                }
            }
        }

        if(somethingChanged) {
            if(!input.dragLockCoord.x && !input.dragLockCoord.y) {
                // This means we changed something that wasn't the thing the drag start changed.
                // We should lock the coordinates.
                let axisSnappedMouse = snapCoordsToAxis(input.screenCoords, input.dragStart);
                if(input.dragStart.x == axisSnappedMouse.x) {
                    input.dragLockCoord.x = true;
                } else {
                    input.dragLockCoord.y = true;
                }
            }
            updateCanvas();
        }

    }
}


/**
 * Handler for when the player presses the mouse button down on the canvas.
 * @param {event} event Event that triggered this callback.
 */
function onCanvasMouseDown(event) {
    if(event.button == 1) {
        event.preventDefault();
    }
    updateInputFromMouse(event);
    onCanvasInputDown(event.button, false);
}


/**
 * Handler for when the player moves the mouse cursor on the canvas.
 * @param {event} event Event that triggered this callback.
 */
function onCanvasMouseMove(event) {
    updateInputFromMouse(event);
    onCanvasInputMove();
}


/**
 * Handler for when the player scrolls the mouse wheel on the canvas.
 * @param {event} event Event that triggered this callback.
 */
function onCanvasMouseWheel(event) {
    for(var l = 0; l < loaded.length; l++) {
        if(!loaded[l]) return;
    }
    if(transitionAnimTime > 0) return;

    updateCanvas();
}


/**
 * Handler for when the player moves a mobile touch on the canvas.
 * @param {event} event Event that triggered this callback.
 */
function onCanvasTouchMove(event) {
    event.preventDefault();
    updateInputFromTouch(event);
    onCanvasInputMove();
}


/**
 * Handler for when the player starts a mobile touch on the canvas.
 * @param {event} event Event that triggered this callback.
 */
function onCanvasTouchStart(event) {
    event.preventDefault();
    updateInputFromTouch(event);
    onCanvasInputDown(0, true);
}


/**
 * Handler for when the player does an input move on the page.
 * This happens regardless of it being a mouse cursor move, or a mobile touch move.
 */
function onPageInputMove() {
    if(input.dragging && input.dragPanning) {
        cam.coords.x -= (input.screenCoords.x - input.dragStart.x);
        cam.coords.y -= (input.screenCoords.y - input.dragStart.y);
        input.dragStart.x = input.screenCoords.x;
        input.dragStart.y = input.screenCoords.y;
        clampCamera();
        updateCanvas();
    }
}


/**
 * Handler for when the player does an input up on the page.
 * This happens regardless of it being a mouse button up press, or a mobile touch end.
 */
function onPageInputUp() {
    input.dragging = false;
    input.dragAction = -1;
    input.dragLockCoord.x = false;
    input.dragLockCoord.y = false;
    if(input.dragPanning) {
        input.dragPanning = false;
        if(Math.abs(cam.coords.x) < 10) {
            cam.coords.x = 0;
        }
        if(Math.abs(cam.coords.y) < 10) {
            cam.coords.y = 0;
        }
    }
    updateCanvas();
}


/**
 * Handler for when the player moves their mouse on the page.
 * @param {event} event Event that triggered this callback.
 */
function onPageMouseMove(event) {
    updateInputFromMouse(event);
    onPageInputMove();
}


/**
 * Handler for when the player releases their mouse button on the page.
 * @param {event} event Event that triggered this callback.
 */
function onPageMouseUp(event) {
    onPageInputUp();
}


/**
 * Handler for when the player moves their mobile touch on the page.
 * @param {event} event Event that triggered this callback.
 */
function onPageTouchMove(event) {
    if(!input.dragging) event.preventDefault();
    updateInputFromTouch(event);
    onPageInputMove();
}


/**
 * Handler for when the player releases their mobile touch on the page.
 * @param {event} event Event that triggered this callback.
 */
function onPageTouchEnd(event) {
    onPageInputUp();
}


/**
 * Handler for when the global game timer ticks.
 */
function onTimer() {
    if(transitionAnimTime > 0) {

        transitionAnimTime -= 1 / 60;
        if(transitionAnimTime <= 0) {
            if(transitionPhase == 0) {
                transitionPhase = 1;
                transitionAnimTime = TRANSITION_DURATION;
                state = transitionNewState;
                if(transitionCodeAfter !== undefined) transitionCodeAfter();
            } else {
                transitionAnimTime = 0;
            }
        }
        updateCanvas();

    }
}


/**
 * Checks whether a player's combo can ever be exactly the given size,
 * provided that the player can still fill more cells next to it.
 * Of course, if there are filled cells farther ahead that make the combo
 * too big, this will return false.
 * @param {object} combo Object with data about this combo.
 * @param {number} goalSize Size to check for.
 * @param {array} cells Array of cells in this line.
 * @returns True if it can, false if not.
 */
function playerComboCanBeSize(combo, goalSize, cells) {
    if(combo.nr > goalSize) return false;

    let curSize = combo.nr;
    for(var c = combo.start + size; c <= cells.length; c++) {
        if(cells[c] == CELL_FILLED) {
            // It must use this filled cell as part of its combo.
            curSize++;
        } else if(cells[c] == CELL_BLANK) {
            // The player can stop here or keep going.
            if(curSize == goalSize) return true;
            curSize++;
        } else if(c == cells.length || cells[c] == CELL_MARKED) {
            // End of the line for this combo.
            return curSize == goalSize;
        }
    }
    return false;
}


/**
 * Checks whether one of the player board's rows or columns defies the corresponding solution hint.
 * @param {number} row Row number, if we're checking a row. null otherwise.
 * @param {number} col Column number, if we're checking a column. null otherwise.
 * @param {array} playerCombos Array with the player's combos for this column/row.
 * @param {array} hints Array of hints for this column/row.
 * @returns True if it defies, false otherwise.
 */
function playerLineDefiesHints(row, col, playerCombos, hints) {
    // Setup.
    let playerCells = [];
    if(row != null) {
        for(var c = 0; c < board.nrCols; c++) {
            let cell = board.cells[row][c];
            if(cell == CELL_BLANK && board.autoMarkedCols[c]) cell = CELL_MARKED;
            playerCells.push(cell);
        }    
    } else {
        for(var r = 0; r < board.nrRows; r++) {
            let cell = board.cells[r][col];
            if(cell == CELL_BLANK && board.autoMarkedRows[r]) cell = CELL_MARKED;
            playerCells.push(cell);
        } 
    }

    // Start by checking if any marked cell is in the way of the hints.
    let curCellIdx = 0;
    for(var h = 0; h < hints.length;) {
        if(curCellIdx >= playerCells.length) {
            // We've reached the end of the cells, and we still have hints to check. Too bad.
            return true;
        }
        let markedCellIdx = findMarkedCellInList(playerCells, curCellIdx, hints[h].nr);
        if(markedCellIdx == -1) {
            // No marked cell. Move on to the next hint.
            curCellIdx += hints[h].nr + 1;
            h++;
        } else {
            // We've hit a marked cell. Let's move to after that cell and retry.
            curCellIdx = markedCellIdx + 1;
        }
    }
    
    if(playerCombos.length > hints.length) {
        //return true;

    } else if(playerCombos.length == hints.length) {
        for(var c = 0; c < playerCombos.length; c++) {
            if(playerCombos[c] > hints[c].nr) return true;
        }

    } else {
        let highestPlayerCombo = 0;
        let highestHintCombo = 0;

        for(var c = 0; c < playerCombos.length; c++) {
            highestPlayerCombo = Math.max(highestPlayerCombo, playerCombos[c]);
        }
        for(var h = 0; h < hints.length; h++) {
            highestHintCombo = Math.max(highestHintCombo, hints[h].nr);
        }
        
        if(highestPlayerCombo > highestHintCombo) return true;

    }

    // All good!
    return false;

    /*
    // Possible combos for each hint.
    let possibleCombosPerHint = [];

    for(var h = 0; h < hints.length; h++) {

        let possibleCombos = [];
        
        for(var c = 0; c < playerCombos.length; c++) {
            if(playerComboCanBeSize(playerCombos[c], hints[h].nr, playerCells)) {
                possibleCombos.push(playerCombos[c]);
            }
        }

        possibleCombosPerHint[h] = possibleCombos;
    }

    // I don't know what to do with this :(
    */
}


/**
 * Checks whether one of the player board's rows or columns matches the corresponding solution hint.
 * This can be true even if the player marked the wrong cells.
 * @param {array} playerCombos Array with the player's combos for this column/row.
 * @param {array} hints Array of hints for this column/row.
 * @returns True if it matches, false otherwise.
 */
function playerLineMatchesHints(playerCombos, hints) {
    if(playerCombos.length != hints.length) return false;
    
    for(var h = 0; h < hints.length; h++) {
        if(hints[h].nr != playerCombos[h].nr) return false;
    }

    return true;
}


/**
 * Populates the board with cells from the given code, in the puzzle making state.
 * Debug function.
 * @param {string} code Code.
 * @returns True on success, false on failure.
 */
function populateFromCode(levelCode) {
    if(state != STATE_MAKING) return false;
    let levelData = decodeBoard(levelCode, BOARD_DATA_CONTEXT_LEVEL);
    if(levelData == null) return false;

    board.nrRows = levelData.nrRows;
    board.nrCols = levelData.nrCols;
    board.cells = levelData.cells;
    board.name = levelData.name;
    updateCanvas();
    return true;
}


/**
 * Saves the player's bookmark.
 */
function saveBookmark() {
    if(state != STATE_PLAYING) return;

    var boardData = {
        nrRows: board.nrRows,
        nrCols: board.nrCols,
        cells: board.cells,
        nrRowHints: 0,
        nrColHints: 0,
        rowHints: [],
        colHints: [],
        name: board.name,
        levelNumber: board.levelNumber,
        levelCode: board.levelCode
    };
    for(var r = 0; r < board.nrRows; r++) {
        for(var h = 0; h < board.rowHints[r].length; h++) {
            boardData.rowHints.push(board.rowHints[r][h].state);
        }
    }
    boardData.nrRowHints = boardData.rowHints.length;
    for(var c = 0; c < board.nrCols; c++) {
        for(var h = 0; h < board.colHints[c].length; h++) {
            boardData.colHints.push(board.colHints[c][h].state);
        }
    }
    boardData.nrColHints = boardData.colHints.length;
    let boardStr = encodeBoard(boardData, BOARD_DATA_CONTEXT_BOOKMARK);
    localStorage.setItem('pikcrossBookmark', boardStr);

    gaveBookmarkWarning = false;
}


/**
 * Saves the player's global progression.
 */
function saveProgression() {
    let writer = new StrBitWriter();
    for(var l = 0; l < LEVELS.length; l++) {
        writer.writeNumber(progression[l] ? 1 : 0, 1);
    }
    localStorage.setItem('pikcrossProgression', writer.getStr());
}


/**
 * Sets up the whole game.
 */
function pikcrossSetup() {
    // HTML setup.
    let pikcrossDiv = document.getElementById('pikcross');
    if(pikcrossDiv == null) return;
    pikcrossDiv.style.position = 'relative';
    pikcrossDiv.style.width = '800px';
    pikcrossDiv.style.height = '800px';

    canvasEl = document.createElement('canvas');
    canvasEl.width = CANVAS.WIDTH;
    canvasEl.height = CANVAS.HEIGHT;
    pikcrossDiv.appendChild(canvasEl);

    inputEl = document.createElement('input');
    inputEl.type = 'text';
    inputEl.style.left = '30%';
    inputEl.style.width = '40%';
    inputEl.style.top = '48%';
    inputEl.style.height = '4%';
    inputEl.style.position = 'absolute';
    inputEl.style.backgroundColor = '#BDB';
    inputEl.style.fontFamily = 'monospace';
    inputEl.addEventListener('focus', function(e) { e.target.select(); });
    inputEl.addEventListener('keyup', function(e) { if(e.key === 'Enter') acceptInputBox(); });
    hideInputBox();
    pikcrossDiv.appendChild(inputEl);

    canvas = canvasEl.getContext('2d');
    canvas.width = CANVAS.WIDTH;
    canvas.height = CANVAS.HEIGHT;
    canvas.textAlign = 'center';
    canvas.textBaseline = 'middle';
    
    // Listeners.
    canvasEl.addEventListener('wheel', onCanvasMouseWheel);
    canvasEl.addEventListener('mousedown', onCanvasMouseDown);
    canvasEl.addEventListener('touchstart', onCanvasTouchStart);
    canvasEl.addEventListener('mousemove', onCanvasMouseMove);
    canvasEl.addEventListener('touchmove', onCanvasTouchMove);
    canvasEl.addEventListener('contextmenu', function(e) { e.preventDefault(); }, false);
    document.addEventListener('mousemove', onPageMouseMove);
    document.addEventListener('touchmove', onPageTouchMove);
    document.addEventListener('mouseup', onPageMouseUp);
    document.addEventListener('touchend', onPageTouchEnd);

    // Spritesheet.
    sprites = new Image();
    sprites.onload = function() {
        loaded[LOAD_CONTENT_SPRITES] = true;
        updateCanvas();
    };
    sprites.src = 'https://pikmin.wiki.gallery/images/c/c7/Pikcross_spritesheet.png';

    // Player progression.
    for(var l = 0; l < LEVELS.length; l++) {
        progression.push(false);
    }
    loadProgression();

    // Levels data.
    for(var l = 0; l < LEVELS.length; l++) {
        levelsData.push(decodeBoard(LEVELS[l], BOARD_DATA_CONTEXT_LEVEL));
    }

    // Global timer.
    setInterval(onTimer, 1000 / 60);

    loaded[LOAD_CONTENT_SETUP] = true;
}


/**
 * Shows the input box HTML element in the middle of the canvas.
 * It also selects the text.
 * @param {boolean} readOnly Whether the box should be read only or not.
 * @param {number} maxLength Maximum length for the input box. undefined for default.
 */
function showInputBox(readOnly, maxLength) {
    inputEl.style.display = 'block';

    inputEl.removeAttribute('maxlength');
    inputEl.removeAttribute('readonly');
    if(maxLength > 0) inputEl.maxLength = maxLength;
    if(readOnly) inputEl.readOnly = true;

    // This weird hack is necessary for some reason.
    window.setTimeout(
        function() {
            inputEl.focus({ focusVisible: true });
            inputEl.select();
        },
        0
    );
}


/**
 * Given a set of coordinates, it snaps them so they are in the same axis as the anchor coordinates.
 * @param {object} coords Coordinates to snap.
 * @param {object} anchor Coordinates to compare against.
 * @returns An object with the snapped coordinates.
 */
function snapCoordsToAxis(coords, anchor) {
    let h_diff = Math.abs(coords.x - anchor.x);
    let v_diff = Math.abs(coords.y - anchor.y);
    if(h_diff > v_diff) {
        return {x: coords.x, y: anchor.y};
    } else {
        return {x: anchor.x, y: coords.y};
    }
}


/**
 * Solves the puzzle.
 * Debug function.
 */
function solve() {
    board.cells = board.solution;
    for(c = 0; c < board.nrCols; c++) {
        updateColumn(c);
    }
    for(r = 0; r < board.nrRows; r++) {
        updateRow(r);
    }
    updateCanvas();
}


/**
 * Toggles whether a cell is filled or blank, if possible.
 * @param {number} row Row index number.
 * @param {number} col Column index number.
 * @returns True if the cell changed, false otherwise.
 */
function toggleCellFill(row, col) {
    if(row < 0 || row >= board.nrRows) return false;
    if(col < 0 || col >= board.nrCols) return false;

    if(input.dragAction == -1) {
        // Figure out what the player wants to do in this move.
        if(board.cells[row][col] == CELL_FILLED) {
            input.dragAction = CELL_BLANK;
        } else {
            input.dragAction = CELL_FILLED;
        }
    }

    return changeCell(row, col, input.dragAction);
}


/**
 * Toggles whether a cell is filled, marked, or blank, if possible.
 * @param {number} row Row index number.
 * @param {number} col Column index number.
 * @returns True if the cell changed, false otherwise.
 */
function toggleCellFillAndMark(row, col) {
    if(row < 0 || row >= board.nrRows) return false;
    if(col < 0 || col >= board.nrCols) return false;

    if(input.dragAction == -1) {
        // Figure out what the player wants to do in this move.
        if(board.cells[row][col] == CELL_FILLED) {
            input.dragAction = CELL_MARKED;
        } else if(board.cells[row][col] == CELL_MARKED) {
            input.dragAction = CELL_BLANK;
        } else {
            input.dragAction = CELL_FILLED;
        }
    }

    return changeCell(row, col, input.dragAction);
}


/**
 * Toggles whether a cell is marked or blank, if possible.
 * @param {number} row Row index number.
 * @param {number} col Column index number.
 * @returns True if the cell changed, false otherwise.
 */
function toggleCellMark(row, col) {
    if(row < 0 || row >= board.nrRows) return false;
    if(col < 0 || col >= board.nrCols) return false;

    if(input.dragAction == -1) {
        // Figure out what the player wants to do in this move.
        if(board.cells[row][col] == CELL_MARKED) {
            input.dragAction = CELL_BLANK;
        } else {
            input.dragAction = CELL_MARKED;
        }
    }

    return changeCell(row, col, input.dragAction);
}


/**
 * Toggles whether a column hint is marked or blank, if possible.
 * @param {number} col Column index number.
 * @param {number} hint Hint index number.
 * @returns True if the hint changed, false otherwise.
 */
function toggleColumnHint(col, hint) {
    if(input.dragAction == -1) {
        // Figure out what the player wants to do in this move.
        if(board.colHints[col][hint].state == CELL_FILLED) {
            input.dragAction = CELL_BLANK;
        } else {
            input.dragAction = CELL_FILLED;
        }
    }

    if(board.colHints[col][hint].state != input.dragAction) {
        board.colHints[col][hint].state = input.dragAction;
        return true;
    }
    return false;
}


/**
 * Toggles whether a row hint is marked or blank, if possible.
 * @param {number} row Row index number.
 * @param {number} hint Hint index number.
 * @returns True if the hint changed, false otherwise.
 */
function toggleRowHint(row, hint) {
    if(input.dragAction == -1) {
        // Figure out what the player wants to do in this move.
        if(board.rowHints[row][hint].state == CELL_FILLED) {
            input.dragAction = CELL_BLANK;
        } else {
            input.dragAction = CELL_FILLED;
        }
    }

    if(board.rowHints[row][hint].state != input.dragAction) {
        board.rowHints[row][hint].state = input.dragAction;
        return true;
    }
    return false;
}


/**
 * Updates the contents of the canvas.
 */
function updateCanvas() {
    for(var l = 0; l < loaded.length; l++) {
        if(!loaded[l]) return;
    }

    // Basic setup.
    canvas.setTransform(1, 0, 0, 1, 0, 0);
    canvas.fillStyle = COLOR_BG;
    canvas.fillRect(0, 0, CANVAS.WIDTH, CANVAS.HEIGHT);

    switch(state) {
    case STATE_MAIN_MENU:
        drawMainMenu();
        break;
    case STATE_PLAYING:
        drawBoard();
        break;
    case STATE_MAKING:
        drawBoard();
        break;
    }
    if(inPanel) {
        drawPanel();
    }
    if(transitionAnimTime > 0) {
        canvas.fillStyle = 'black';
        let fillRatio = 1 - (transitionAnimTime / TRANSITION_DURATION);
        if(transitionPhase == 1) fillRatio = 1 - fillRatio;
        fillRatio *= 0.5;
        canvas.fillRect(0, 0, CANVAS.WIDTH, CANVAS.HEIGHT * fillRatio);
        canvas.fillRect(0, 0, CANVAS.WIDTH * fillRatio, CANVAS.HEIGHT);
        canvas.fillRect(CANVAS.WIDTH * (1 - fillRatio), 0, CANVAS.WIDTH, CANVAS.HEIGHT);
        canvas.fillRect(0, CANVAS.HEIGHT * (1 - fillRatio), CANVAS.WIDTH, CANVAS.HEIGHT);
    }
}


/**
 * Updates the state of a given column.
 * @param {number} col Column number to update.
 */
function updateColumn(col) {
    let playerCombos = getPlayerLineCombos(null, col);
    if(allHintsAreFilled(board.colHints[col]) && playerLineMatchesHints(playerCombos, board.colHints[col])) {
        board.autoMarkedCols[col] = true;
    } else {
        board.autoMarkedCols[col] = false;
    }
    if(playerLineDefiesHints(null, col, playerCombos, board.colHints[col])) {
        board.defiedCols[col] = true;
    } else {
        board.defiedCols[col] = false;
    }
}


/**
 * Updates the variables that hold the mouse state.
 * @param {event} event The event that triggered this.
 */
function updateInputFromMouse(event) {
    let br = canvasEl.getBoundingClientRect();
    input.screenCoords.x = event.clientX - br.left;
    input.screenCoords.y = event.clientY - br.top;
    input.worldCoords.x = input.screenCoords.x;
    input.worldCoords.y = input.screenCoords.y;
    input.worldCoords.x -= CANVAS.WIDTH / 2;
    input.worldCoords.y -= CANVAS.HEIGHT / 2;
    //input.worldCoords.x /= cam.zoom;
    //input.worldCoords.y /= cam.zoom;
    input.worldCoords.x += cam.coords.x;
    input.worldCoords.y += cam.coords.y;
}


/**
 * Updates the variables that hold the mouse state, using data from a mobile touch event.
 * @param {event} event The event that triggered this.
 */
function updateInputFromTouch(event) {
    let br = canvasEl.getBoundingClientRect();
    input.screenCoords.x = event.touches[0].clientX - br.left;
    input.screenCoords.y = event.touches[0].clientY - br.top;
    input.worldCoords.x = input.screenCoords.x;
    input.worldCoords.y = input.screenCoords.y;
    input.worldCoords.x -= CANVAS.WIDTH / 2;
    input.worldCoords.y -= CANVAS.HEIGHT / 2;
    //input.worldCoords.x /= cam.zoom;
    //input.worldCoords.y /= cam.zoom;
    input.worldCoords.x += cam.coords.x;
    input.worldCoords.y += cam.coords.y;
}


/**
 * Updates the state of a given row.
 * @param {number} row Row number to update.
 */
function updateRow(row) {
    let playerCombos = getPlayerLineCombos(row, null);
    if(allHintsAreFilled(board.rowHints[row]) && playerLineMatchesHints(playerCombos, board.rowHints[row])) {
        board.autoMarkedRows[row] = true;
    } else {
        board.autoMarkedRows[row] = false;
    }
    if(playerLineDefiesHints(row, null, playerCombos, board.rowHints[row])) {
        board.defiedRows[row] = true;
    } else {
        board.defiedRows[row] = false;
    }
}


/**
 * Makes writing bits into a string (8-bit int array) easy.
 */
class StrBitWriter {
    // Final string.
    #str = '';
    // Current working byte value.
    #byteValue = 0;
    // Current bit's index.
    #bitIdx = 0;
    
    /**
     * Encodes a number into the string.
     * @param {number} number Number to encode. 0 to 8 bits long.
     * @param {number} nrBits Amount of bits the number takes. 8 by default.
     */
    writeNumber(number, nrBits) {
        if(nrBits === undefined) nrBits = 8;
        if(this.#bitIdx + nrBits > 8) {
            this.nextByte();
        }
        for(var b = 0; b < nrBits; b++) {
            let bitValue = (number & (1 << b)) > 0;
            this.#byteValue |= (bitValue << this.#bitIdx);
            this.#bitIdx++;
        }
        if(this.#bitIdx >= 8) {
            this.nextByte();
        }
    }
    
    /**
     * Finishes writing to the current byte and goes to the next one.
     */
    nextByte() {
        this.#str += String.fromCharCode(this.#byteValue);
        this.#byteValue = 0;
        this.#bitIdx = 0;
    }

    /**
     * Obtains the finalized string (8-bit int array).
     * @returns The finalized string.
     */
    getStr() {
        let str = this.#str;
        if(this.#bitIdx > 0) {
            str += String.fromCharCode(this.#byteValue);
        }
        return str;
    }

    /**
     * Resets the state of the writer.
     */
    reset() {
        this.#str = '';
        this.#byteValue = 0;
        this.#bitIdx = 0;
    }
}


/**
 * Makes reading bits from a string (8-bit int array) easy.
 */
class StrBitReader {
    // Full string.
    #str = '';
    // Current byte's index.
    #byteIdx = 0;
    // Current bit's index.
    #bitIdx = 0;

    /**
     * Construcs a new instance.
     * @param {string} str String (8-bit int array) with the bits to read from.
     */
    constructor(str) {
        this.#str = str;
    }

    /**
     * Reads a number from the next bits in the string.
     * @param {number} nrBits Amount of bits the number takes. 8 by default.
     * @returns The read number.
     */
    readNumber(nrBits) {
        if(nrBits === undefined) nrBits = 8;
        if(this.#bitIdx + nrBits > 8) {
            this.nextByte();
        }
        let number = 0;
        let byteValue = this.#str.charCodeAt(this.#byteIdx);
        for(var b = 0; b < nrBits; b++) {
            let bitMask = (1 << this.#bitIdx);
            let bitValue = (byteValue & bitMask) > 0;
            number |= (bitValue << b);
            this.#bitIdx++;
        }
        if(this.#bitIdx >= 8) {
            this.nextByte();
        }
        return number;
    }

    /**
     * Finishes reading the current byte and goes to the next one.
     */
    nextByte() {
        this.#byteIdx++;
        this.#bitIdx = 0;
    }

    /**
     * Resets the state of the reader.
     */
    reset() {
        this.#str = '';
        this.#byteIdx = 0;
        this.#bitIdx = 0;
    }
}


/**
 * Runs the game.
 */
pikcrossSetup();