// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: @openzeppelin/contracts/utils/Address.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/Address.sol)

pragma solidity ^0.8.1;

/**
 * @dev Collection of functions related to the address type
 */
library Address {
    /**
     * @dev Returns true if `account` is a contract.
     *
     * [IMPORTANT]
     * ====
     * It is unsafe to assume that an address for which this function returns
     * false is an externally-owned account (EOA) and not a contract.
     *
     * Among others, `isContract` will return false for the following
     * types of addresses:
     *
     *  - an externally-owned account
     *  - a contract in construction
     *  - an address where a contract will be created
     *  - an address where a contract lived, but was destroyed
     * ====
     *
     * [IMPORTANT]
     * ====
     * You shouldn't rely on `isContract` to protect against flash loan attacks!
     *
     * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
     * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
     * constructor.
     * ====
     */
    function isContract(address account) internal view returns (bool) {
        // This method relies on extcodesize/address.code.length, which returns 0
        // for contracts in construction, since the code is only stored at the end
        // of the constructor execution.

        return account.code.length > 0;
    }

    /**
     * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
     * `recipient`, forwarding all available gas and reverting on errors.
     *
     * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
     * of certain opcodes, possibly making contracts go over the 2300 gas limit
     * imposed by `transfer`, making them unable to receive funds via
     * `transfer`. {sendValue} removes this limitation.
     *
     * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
     *
     * IMPORTANT: because control is transferred to `recipient`, care must be
     * taken to not create reentrancy vulnerabilities. Consider using
     * {ReentrancyGuard} or the
     * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
     */
    function sendValue(address payable recipient, uint256 amount) internal {
        require(address(this).balance >= amount, "Address: insufficient balance");

        (bool success, ) = recipient.call{value: amount}("");
        require(success, "Address: unable to send value, recipient may have reverted");
    }

    /**
     * @dev Performs a Solidity function call using a low level `call`. A
     * plain `call` is an unsafe replacement for a function call: use this
     * function instead.
     *
     * If `target` reverts with a revert reason, it is bubbled up by this
     * function (like regular Solidity function calls).
     *
     * Returns the raw returned data. To convert to the expected return value,
     * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
     *
     * Requirements:
     *
     * - `target` must be a contract.
     * - calling `target` with `data` must not revert.
     *
     * _Available since v3.1._
     */
    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, "Address: low-level call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
     * `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but also transferring `value` wei to `target`.
     *
     * Requirements:
     *
     * - the calling contract must have an ETH balance of at least `value`.
     * - the called Solidity function must be `payable`.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
    }

    /**
     * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
     * with `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(address(this).balance >= value, "Address: insufficient balance for call");
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        return functionStaticCall(target, data, "Address: low-level static call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        (bool success, bytes memory returndata) = target.staticcall(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionDelegateCall(target, data, "Address: low-level delegate call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    /**
     * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
     * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
     *
     * _Available since v4.8._
     */
    function verifyCallResultFromTarget(
        address target,
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        if (success) {
            if (returndata.length == 0) {
                // only check isContract if the call was successful and the return data is empty
                // otherwise we already know that it was a contract
                require(isContract(target), "Address: call to non-contract");
            }
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

    /**
     * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
     * revert reason or using the provided one.
     *
     * _Available since v4.3._
     */
    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

    function _revert(bytes memory returndata, string memory errorMessage) private pure {
        // Look for revert reason and bubble it up if present
        if (returndata.length > 0) {
            // The easiest way to bubble the revert reason is using memory via assembly
            /// @solidity memory-safe-assembly
            assembly {
                let returndata_size := mload(returndata)
                revert(add(32, returndata), returndata_size)
            }
        } else {
            revert(errorMessage);
        }
    }
}

// File: @openzeppelin/contracts/utils/Base64.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (utils/Base64.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides a set of functions to operate with Base64 strings.
 *
 * _Available since v4.5._
 */
library Base64 {
    /**
     * @dev Base64 Encoding/Decoding Table
     */
    string internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    /**
     * @dev Converts a `bytes` to its Bytes64 `string` representation.
     */
    function encode(bytes memory data) internal pure returns (string memory) {
        /**
         * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence
         * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol
         */
        if (data.length == 0) return "";

        // Loads the table into memory
        string memory table = _TABLE;

        // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter
        // and split into 4 numbers of 6 bits.
        // The final Base64 length should be `bytes` data length multiplied by 4/3 rounded up
        // - `data.length + 2`  -> Round up
        // - `/ 3`              -> Number of 3-bytes chunks
        // - `4 *`              -> 4 characters for each chunk
        string memory result = new string(4 * ((data.length + 2) / 3));

        /// @solidity memory-safe-assembly
        assembly {
            // Prepare the lookup table (skip the first "length" byte)
            let tablePtr := add(table, 1)

            // Prepare result pointer, jump over length
            let resultPtr := add(result, 32)

            // Run over the input, 3 bytes at a time
            for {
                let dataPtr := data
                let endPtr := add(data, mload(data))
            } lt(dataPtr, endPtr) {

            } {
                // Advance 3 bytes
                dataPtr := add(dataPtr, 3)
                let input := mload(dataPtr)

                // To write each character, shift the 3 bytes (18 bits) chunk
                // 4 times in blocks of 6 bits for each character (18, 12, 6, 0)
                // and apply logical AND with 0x3F which is the number of
                // the previous character in the ASCII table prior to the Base64 Table
                // The result is then added to the table to get the character to write,
                // and finally write it in the result pointer but with a left shift
                // of 256 (1 byte) - 8 (1 ASCII char) = 248 bits

                mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F))))
                resultPtr := add(resultPtr, 1) // Advance

                mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F))))
                resultPtr := add(resultPtr, 1) // Advance

                mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F))))
                resultPtr := add(resultPtr, 1) // Advance

                mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F))))
                resultPtr := add(resultPtr, 1) // Advance
            }

            // When data `bytes` is not exactly 3 bytes long
            // it is padded with `=` characters at the end
            switch mod(mload(data), 3)
            case 1 {
                mstore8(sub(resultPtr, 1), 0x3d)
                mstore8(sub(resultPtr, 2), 0x3d)
            }
            case 2 {
                mstore8(sub(resultPtr, 1), 0x3d)
            }
        }

        return result;
    }
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

// File: contracts/HeartField.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title HeartField
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * A simple flow field which goes around in a circle, or in a heart, or a blend of the two
 */

import "./lib/Coords.sol";
import "./lib/Trig.sol";
import "./lib/Storage.sol";

using HeartFieldLib for HeartField global;

struct HeartField {
    Point center;
    uint16[361] heartAngles;
}

library HeartFieldLib {
    function createField(int centerX, int centerY, mapping(uint=>uint) storage heartAngles) internal view returns (HeartField memory field) {
        unchecked {
            setPointTo(field.center, centerX, centerY);
            
            for(uint i=0;i<=360;i++) {
                field.heartAngles[i] = uint16(Storage.getUnpacked(i, 16, heartAngles));
            }
        }
    }
    
    
    function setNextPoint(HeartField memory field, Point memory current, int distance, int angleShift, uint pctHeart, Point memory result) internal pure {
        unchecked {
            uint angle = uint((Trig.atan2(field.center.y - current.y, current.x - field.center.x) / 1000) + 90 + 360) % 360;
            
            int direction = angleShift + int(((angle+90) * (100 - pctHeart) + field.heartAngles[angle] * pctHeart) * 1000 / 100);
            
            setPointTo(result, 
                int16(current.x+(Trig.sin(direction)*distance)/1000), 
                int16(current.y+(Trig.cos(direction)*distance)/1000)
            );
        }
    }
}
// File: contracts/IWorm1000Artwork.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title IWorm1000Artwork
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Interface for the Worm1000Artwork, to be used by the Token Contract
 */

interface IWorm1000Artwork {
    
    function tokenURI(uint mainDiscipleNumber, uint[4] memory numberOfLightsByLumenLevel, bool animationView) external view returns (string memory);
    
    function royaltyRecipient() external view returns (address);
}

// File: contracts/lib/CollisionGrid.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title CollisionGrid
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Allows tracking for collisions on a 2D space. Points can be added to the grid (addPoint) 
 * which marks them and occupied. Subsequently collisions can be checked (isValidPoint).
 * Points are treated as squares rather than circles for simplicity.
 */



import "./Math.sol";
import "./Coords.sol";

struct CollisionGrid {
    Frame frame;
    uint size;
    uint xUnit;
    uint yUnit;
    uint[] marks;
}

using CollisionGridLib for CollisionGrid global;

library CollisionGridLib {
    
    function createGrid(Frame memory frame, uint size) internal pure returns (CollisionGrid memory grid) {
        unchecked {
            grid.frame = frame;
            grid.size = size;
            grid.xUnit = uint(frame.width)/size + 1;
            grid.yUnit = uint(frame.height)/size + 1;
            grid.marks = new uint[](size+1);
        }
    }
    
    function addPoint(CollisionGrid memory grid, Point memory pt, uint r) internal pure {
        unchecked {
            for (uint i = xLower(grid, pt, r); i <= xUpper(grid, pt, r); i++) {
                for (uint j = yLower(grid, pt, r); j <= yUpper(grid, pt, r); j++) {
                    grid.marks[i] |= 1 << j;
                }
            }
        }
    }
    
    function isValidPoint(CollisionGrid memory grid, Point memory pt, uint r) internal pure returns(bool) {
        unchecked {
            for (uint i = xLower(grid, pt, r); i <= xUpper(grid, pt, r); i++) {
                for (uint j = yLower(grid, pt, r); j <= yUpper(grid, pt, r); j++) {
                    if((grid.marks[i] >> j & 1) == 1) {
                        return false;
                    }
                }
            }
            return true;
        }
    }
    
    function xLower(CollisionGrid memory grid, Point memory pt, uint r) private pure returns (uint) {
        unchecked { return bound(pt.x, -int(r), grid.frame.x1, grid.xUnit, grid.size); }
    }
    
    function xUpper(CollisionGrid memory grid, Point memory pt, uint r) private pure returns (uint) {
        unchecked { return bound(pt.x,  int(r), grid.frame.x1, grid.xUnit, grid.size); }
    }
    
    function yLower(CollisionGrid memory grid, Point memory pt, uint r) private pure returns (uint) {
        unchecked { return bound(pt.y, -int(r), grid.frame.y1, grid.yUnit, grid.size); }
    }
    
    function yUpper(CollisionGrid memory grid, Point memory pt, uint r) private pure returns (uint) {
        unchecked { return bound(pt.y,  int(r), grid.frame.y1, grid.yUnit, grid.size); }
    }
    
    function bound(int v, int r, int v1, uint vUnit, uint size) private pure returns (uint) {
        unchecked { return min(size, uint(max(0, (v+r-v1)/int(vUnit)))); }
    }
}
// File: contracts/lib/Coords.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title Coords
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Base classes for geometry: Point(x,y), Circle((x,y),diameter), Frame(x1,y1,y2,y2,width,height)
 */



struct Point {
    int x;
    int y;
}

struct Circle {
    Point center;
    uint diameter;
}

struct Frame {
    int x1;
    int y1;
    int x2;
    int y2;
    int width;
    int height;
}

function setPointTo(Point memory target, int x, int y) pure {
    target.x = x;
    target.y = y;
}
function setPointTo(Point memory target, Point memory source) pure {
    setPointTo(target, source.x, source.y);
}

function setFrameTo(Frame memory target, int x1, int y1, int x2, int y2) pure {
    unchecked {
        target.x1 = x1;
        target.y1 = y1;
        target.x2 = x2;
        target.y2 = y2;
        target.width = x2-x1;
        target.height = y2-y1;
    }
}

function setFrameToSquareAtCenterAndSize(Frame memory frame, int centerX, int centerY, uint halfSize) pure {
    unchecked {
        setFrameTo(frame,
            centerX-int(halfSize),
            centerY-int(halfSize),
            centerX+int(halfSize),
            centerY+int(halfSize)
        );
    }
}
// File: contracts/lib/Math.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title Math
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Convenience math functions: abs, sign, max, min, avg
 */


function abs(int v) pure returns (uint) {
    unchecked {
        return uint(v < 0 ? -v : v);
    }
}

function sign(int v) pure returns (int) {
    return v>0?int(1):v<0?-1:int(0);
}

function max(int a, int b) pure returns (int) {
    unchecked {
        return a > b ? a : b;
    }
}

function max(uint a, uint b) pure returns (uint) {
    unchecked {
        return a > b ? a : b;
    }
}

function min(int a, int b) pure returns (int) {
    unchecked {
        return a < b ? a : b;
    }
}

function min(uint a, uint b) pure returns (uint) {
    unchecked {
        return a < b ? a : b;
    }
}

function avg(int a, int b) pure returns (int) {
    unchecked {
        return (a+b)/2;
    }
}

// File: contracts/lib/RNG.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title RNG
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Simple 32-bit seeded psuedorandom number generator
 */


struct Random {
    int32 seed;
}

using RNG for Random global;

library RNG {
    
    function initializeRandom(Random memory random, int32 seed) internal pure {
        random.seed = seed;
    }
    
    function createRandom(int32 seed) internal pure returns (Random memory) {
        return Random(seed);
    }
    
    function next(Random memory random, int lower, int upper) internal pure returns (int) {
        unchecked {
            random.seed ^= random.seed << 13;
            random.seed ^= random.seed >> 17;
            random.seed ^= random.seed << 5;
            return ((random.seed < 0 ? ~random.seed + 1 : random.seed)%(upper-lower+1))+lower;
        }
    }
    
    function boolean(Random memory random) internal pure returns (bool) {
        return next(random, 0, 1) == 1;
    }
    
    function sign(Random memory random) internal pure returns (int) {
        return next(random, 0, 1) == 0 ? -1 : int(1);
    }
}


// File: contracts/lib/Storage.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title Storage
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Convenience methods for transferring arrays and strings into storage, and unpacking packed data
 */

library Storage {
    
    
    function fillUintMapping(uint[] memory source, mapping(uint=>uint) storage target) internal {
        unchecked {
            for(uint i=0;i<source.length;i++) {
                target[i] = source[i];
            }
        }
    }
    
    
    
    
    //source structure: 30 bytes padding, uint16 string1Length, bytes of string1, uint16 string2Length, bytes of string2, ...
    //memory efficient but destructive to the original source as it is written to along the way
    function unpackAndFillStringMapping(bytes memory source, mapping(uint=>string) storage target) internal {
        unchecked {
            uint sourceIndex = 30;
            uint targetIndex = 0;
            
            uint currentStringLength;
            bytes memory currentString;
            
            while(sourceIndex < source.length) {
                currentStringLength = (uint16(uint8(source[sourceIndex])) << 8) + uint8(source[sourceIndex+1]);
                sourceIndex += 2;
                
                assembly {
                    currentString := add(source, sourceIndex) //point the current string to the right section of the original
                    mstore(currentString, currentStringLength) //write the length of the current string at the beginning (this is the destructive part)
                }
                
                target[targetIndex++] = string(currentString);
                sourceIndex+= currentStringLength;
            }
        }
    }
    
        
    
    
    
    function extractBits(uint position, uint numBits, uint packedData) internal pure returns (uint) {
        unchecked {
            return packedData << 256-(position+1)*numBits >> 256-numBits;
        }
    }
    
    
    
    
    
    function getUnpacked(uint index, uint numBits, mapping(uint=>uint) storage packedDataMapping) internal view returns (uint) {
        unchecked {
            uint packedData = packedDataMapping[index*numBits/256];
            uint position = index%(256/numBits);
            return extractBits(position, numBits, packedData);
        }
    }
    
}

// File: contracts/lib/StringBuffer.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title StringBuffer
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Allows for efficiently and incrementally building a string. 
 * Memory is pre-allocated upfront and can be written to serially. 
 * toString() will return the resulting string truncated for only the size used.
 */


struct StringBuffer {
    bytes chars;
    uint length;
}

using StringBufferLib for StringBuffer global;

library StringBufferLib {
    
    function create(uint maxLength) internal pure returns (StringBuffer memory buffer) {
        buffer.chars = new bytes(maxLength);
        buffer.length = 0;
    }



    function add(StringBuffer memory buffer, bytes memory segment) internal pure {
        unchecked {
            for(uint i=0;i<segment.length;i++) {
                buffer.chars[buffer.length++]=segment[i];
            }
        }
    }
    
    function add(StringBuffer memory buffer, string memory segment) internal pure {
        add(buffer, bytes(segment));
    }
    
    function add(StringBuffer memory buffer, bytes1 oneByte) internal pure {
        unchecked {
            buffer.chars[buffer.length++]=oneByte;
        }
    }
    
    function addByte(StringBuffer memory buffer, bytes1 oneByte) internal pure {
        add(buffer, oneByte); //for convenience
    }
    
    bytes10 private constant digits = '0123456789';
    
    function add(StringBuffer memory buffer, uint16 segment) internal pure {
        unchecked {
            if(segment==0) {
                add(buffer, digits[0]);
                return;
            }
            uint8 length = 0;
            uint temp = segment;
            while (temp > 0) {
                length++;
                temp /= 10;
            }
            
            uint exp;
            temp = segment;
            while (length > 0) {
                length--;
                exp = 10**length;
                add(buffer, digits[temp/exp]);
                temp %= exp;
            }
        }
    }
    
    function add(StringBuffer memory buffer, int segment) internal pure {
        unchecked {
            if(segment < 0) {
                addByte(buffer, '-');
                add(buffer, uint16(int16(-segment)));
            } else {
                add(buffer, uint16(int16(segment)));
            }
        }
    }
    
    function toString(StringBuffer memory buffer) internal pure returns (string memory) {
        uint length = buffer.length;
        bytes memory chars = buffer.chars;
        assembly { mstore(chars, length) }

        return string(buffer.chars);
    }
}

// File: contracts/lib/Trig.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title Trig
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Approximations for sine, cosine, and 2-argument arctangent, designed to be gas efficient
 */


import "./Math.sol";

library Trig {
    
    function sinCurve(int degreesTimes1000) private pure returns (int) {
        unchecked {
            return 4000 * degreesTimes1000 * (180000 - degreesTimes1000) / (40500000000 - degreesTimes1000 * (180000 - degreesTimes1000) );
        }
    }
    
    function sin(int degreesTimes1000) internal pure returns (int) {
        unchecked {
            int boundedDegreesTimes1000 = (degreesTimes1000+360000*10000000) % 360000;
            return boundedDegreesTimes1000 <= 180000 ? sinCurve(boundedDegreesTimes1000) : -sinCurve(boundedDegreesTimes1000 - 180000);
        }
    }
    
    function cos(int degreesTimes1000) internal pure returns (int) {
        unchecked {
            return sin(degreesTimes1000+90000);
        }
    }
    
    function atanHelper(int y, int x) private pure returns (int) {
        unchecked {
            int d = (1000 * y) / x;
            return (45 * d - ((d * int(int(abs(d)) - 1000) * int(14020277 + 3798 * abs(d))) / 1000000000));
        }
    }

    function atan2(int y, int x) internal pure returns (int) {
        unchecked {
            return abs(y) < abs(x)
                   ? atanHelper(y, x) + 180000 * (x < 0 ? (y < 0 ? -1 : int(1)) : int(0))
                   : -atanHelper(x, y) + 90000 * sign(y);
        }
    }

}

// File: contracts/lib/XML.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title XML
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Convenience class to generate XML
 */


import "./StringBuffer.sol";

using XMLLib for XML global;

struct XML {
    StringBuffer buffer;
    string[20] tags;
    uint8 currentDepth;
    bool currentTagHasChildren;
}

library XMLLib {
    
    bytes1 private constant LT = '<';
    bytes1 private constant GT = '>';
    
    bytes private constant LT_SLASH = '</';
    bytes private constant SLASH_GT = '/>';

    bytes1 private constant SPACE = ' ';
    bytes1 private constant EQUAL = '=';
    bytes1 private constant QUOTE = '"';
    
    function create(uint maxLength) internal pure returns (XML memory xml) {
        xml.buffer = StringBufferLib.create(maxLength);
        xml.currentDepth = 0;
        xml.currentTagHasChildren = false;
    }
    
    
    function prepareForChildren(XML memory xml) internal pure {
        if(xml.currentDepth>0&&!xml.currentTagHasChildren) {
            xml.buffer.add(GT);
        }
        xml.currentTagHasChildren = true;
    }
    
    function startTag(XML memory xml, string memory tag) internal pure {
        unchecked {
            prepareForChildren(xml);
            xml.tags[xml.currentDepth++] = tag;
            xml.buffer.add(LT);
            xml.buffer.add(tag);
            xml.currentTagHasChildren = false;
        }
    }
    
    function endTag(XML memory xml) internal pure {
        unchecked {
            xml.currentDepth--;
            if(xml.currentTagHasChildren) {
                xml.buffer.add(LT_SLASH);
                xml.buffer.add(xml.tags[xml.currentDepth]);
                xml.buffer.add(GT);
            } else {
                xml.buffer.add(SLASH_GT);
            }
            xml.currentTagHasChildren = true;
        }
    }
    
    function startAttribute(XML memory xml, string memory name) internal pure {
        xml.buffer.add(SPACE);
        xml.buffer.add(name);
        xml.buffer.add(EQUAL);
        xml.buffer.add(QUOTE);
    }
    
    function endAttribute(XML memory xml) internal pure {
        xml.buffer.add(QUOTE);
    }
    
    function addAttribute(XML memory xml, string memory name, string memory value) internal pure {
        startAttribute(xml, name);
        xml.buffer.add(value);
        endAttribute(xml);
    }
    
    function addAttribute(XML memory xml, string memory name, uint16 value) internal pure {
        startAttribute(xml, name);
        xml.buffer.add(value);
        endAttribute(xml);
    }
    
    function addAttribute(XML memory xml, string memory name, int value) internal pure {
        startAttribute(xml, name);
        xml.buffer.add(value);
        endAttribute(xml);
    }
    
    function addPreformattedElement(XML memory xml, string memory element) internal pure {
        prepareForChildren(xml);
        xml.buffer.add(element);
    }
    
    function toString(XML memory xml) internal pure returns (string memory) {
        return xml.buffer.toString();
    }
}

// File: contracts/Renderer.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title Renderer
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Main logic to convert from Visuals to SVG.
 */


import "./lib/CollisionGrid.sol";
import "./lib/XML.sol";

import "./Visuals.sol";
import "./StringFragments.sol";

library Renderer {
    
    uint private constant MAX_SVG_SIZE = 250000;
    uint private constant MAX_SVG_DISCIPLE_GROUP_SIZE = 35000;
    
    struct DiscipleColorGroup {
        Color color;
        uint width;
        XML content;
    }
    
    struct Circles {
        Circle[50] list;
        uint count;
    }
    
    struct RenderData {
        uint mainDiscipleNumber;
        Line discipleLine;
        Line wormLine;
        DiscipleColorGroup[7] discipleGroups;
        uint[10] groupIndicesPlusOne;
        uint numGroups;
        
        Frame linesFrame;
        Frame pieceFrame;
        Frame starsFrame;
        Frame backgroundFrame;
        
        CollisionGrid grid;
        
        Circles lights;
        Circles stars;
        
        Fragments fragments;
    }
    
    function initializeData(uint mainDiscipleNumber) internal pure returns (RenderData memory renderData) {
        unchecked {
            renderData.mainDiscipleNumber = mainDiscipleNumber;
            setFrameToSquareAtCenterAndSize(renderData.linesFrame, Visuals.centerX, Visuals.centerY, 0);
            Frame memory collisionGridFrame;
            setFrameToSquareAtCenterAndSize(collisionGridFrame, Visuals.centerX, Visuals.centerY, mainDiscipleNumber*4+2000);
            renderData.grid = CollisionGridLib.createGrid(collisionGridFrame, 100);
        }
    }
    
    function getColorCode(Color color, Fragments memory fragments) private view returns (string memory) {
        return fragments.get(StringFragment.COLOR_CODE_NONE, uint(color));
    }
    
    function addCurrentDiscipleLine(RenderData memory renderData) internal view {
        unchecked {
            uint indexPlusOne = renderData.groupIndicesPlusOne[uint(renderData.discipleLine.color)];
            if(indexPlusOne == 0) {
                renderData.numGroups++;
                indexPlusOne = renderData.numGroups;
                prepareGroup(renderData.discipleGroups[indexPlusOne-1], renderData.discipleLine.color, getColorCode(renderData.discipleLine.color, renderData.fragments), renderData.discipleLine.width);
                renderData.groupIndicesPlusOne[uint(renderData.discipleLine.color)] = indexPlusOne;
            }
            addDiscipleLine(renderData.discipleGroups[indexPlusOne-1].content, renderData.discipleLine, renderData.fragments);
        }
    }
    
    function prepareGroup(DiscipleColorGroup memory group, Color color, string memory colorCode, uint width) internal pure {
        group.color = color;
        group.width = width;
        
        XML memory content = XMLLib.create(MAX_SVG_DISCIPLE_GROUP_SIZE);
        content.startTag('g');
        if(group.color == Color.BLACK) {
            content.startAttribute('id');
            content.buffer.add(string('black_'));
            content.buffer.add(uint16(width));
            content.endAttribute();
        } else {
            writeColorAndWidth(content, colorCode, width);
        }
        group.content = content;
    }
    
    function addStrokeAttribute(XML memory svg, string memory colorCode) private pure {
        svg.addAttribute('stroke', colorCode);
    }
    
    function addStrokeWidthAttribute(XML memory svg, uint width) private pure {
        svg.addAttribute('stroke-width', uint16(width));
    }
    
    function writeColorAndWidth(XML memory svg, string memory colorCode, uint width) private pure {
        addStrokeAttribute(svg, colorCode);
        addStrokeWidthAttribute(svg, width);
    }
    
    function writePoint(XML memory svg, int x, int y) private pure {
        svg.buffer.add(int16(x));
        svg.buffer.addByte(' ');
        svg.buffer.add(int16(y));
    }
    
    function addAttributeWithUrlValueForId(XML memory svg, string memory attribute, string memory id) private pure {
        svg.addAttribute(attribute, string(abi.encodePacked('url(#',id,')')));
    }
    
    function addEndMarker(XML memory svg, string memory markerId) private pure {
        addAttributeWithUrlValueForId(svg, 'marker-end', markerId);
    }
    
    function startPath(XML memory svg) private pure {
        svg.startTag('path');
    }
    
    function endPath(XML memory svg, Line memory line, uint startPoint, uint endPoint, string memory colorCode, uint width) private pure {
        writeColorAndWidth(svg, colorCode, width);
        writePathD(svg, line, startPoint, endPoint);
        svg.endTag();
    }
    
    function idOfColor(string memory id, bool isFelixColor) private pure returns (string memory) {
        return string(abi.encodePacked(id, isFelixColor?'_felix':''));
    }
    
    function addDiscipleLine(XML memory svg, Line memory line, Fragments memory fragments) internal view {
        unchecked {
            if(line.number == 794 || line.number == 795) {
                startPath(svg);
                addEndMarker(svg, fragments.get(StringFragment.ITEM_NAME_HORNS, 0));
                writePathD(svg, line, 0, line.numPoints);
                svg.endTag();
            }
            startPath(svg);
            svg.startAttribute('id');
            svg.buffer.addByte('D');
            svg.buffer.add(uint16(line.number));
            svg.endAttribute();
            addEndMarker(svg, idOfColor(fragments.get(StringFragment.FACE_NAME_1_SLEEPING, line.number==794?6:line.number==795?7:line.speedLevel-1),line.color==Color.BLACK));
            writePathD(svg, line, 0, line.numPoints);
            svg.endTag();
        }
    }
    
    function addWormLine(XML memory svg, Line memory line, Fragments memory fragments) internal view {
        unchecked {
            Evolution evolution = Visuals.evolution(line.number);
            Color bodyColor = Color.PINK;
            uint width = line.width;
            
            if(evolution==Evolution.RESURRECTEDWORM || evolution == Evolution.MECHSUIT) {
                startPath(svg);
                addEndMarker(svg, fragments.get(evolution==Evolution.RESURRECTEDWORM ? StringFragment.ITEM_NAME_HALO : StringFragment.ITEM_NAME_MECHHALO, 0));
                endPath(svg, line, 4, line.numPoints, getColorCode(Color.NONE, fragments), width);
                
                bodyColor = Color.WHITE;
            }
            startPath(svg);
            svg.addAttribute('id', 'W');
            addEndMarker(svg, fragments.get(StringFragment.FACE_NAME_1_SLEEPING, line.number==794?5:line.number==795?2:line.speedLevel-1));
            endPath(svg, line, 0, line.numPoints, getColorCode(bodyColor, fragments), width);
            if(evolution == Evolution.MECHSUIT) {
                bool isSimplified = line.numPoints == 7;
                
                startPath(svg);
                endPath(svg, line, 0, 3, getColorCode(Color.PINK, fragments), width);
                
                startPath(svg);
                svg.addAttribute('stroke-linecap', 'butt');
                endPath(svg, line, 1, isSimplified?6:10, getColorCode(Color.PINK, fragments), width);
                
                startPath(svg);
                endPath(svg, line, isSimplified?1:2, isSimplified?4:6, getColorCode(Color.BLACK, fragments), width/10);
                
                string memory suitId = 'item_suit';
                string memory markerMid = 'marker-mid';
                
                startPath(svg);
                addEndMarker(svg, suitId);
                addAttributeWithUrlValueForId(svg, 'marker-start', suitId);
                endPath(svg, line, isSimplified?1:2, isSimplified?4:6, getColorCode(Color.NONE, fragments), width);
                
                startPath(svg);
                addAttributeWithUrlValueForId(svg, markerMid, suitId);
                endPath(svg, line, isSimplified?2:5, isSimplified?6:9, getColorCode(Color.NONE, fragments), width);
                
                startPath(svg);
                addAttributeWithUrlValueForId(svg, markerMid, suitId);
                endPath(svg, line, isSimplified?3:7, isSimplified?7:11, getColorCode(Color.NONE, fragments), width);
            }
        }
    }
    
    function writePathD(XML memory svg, Line memory line, uint startPoint, uint endPoint) private pure {
        unchecked {
            svg.startAttribute('d');
            
            svg.buffer.add(string('M '));
            writePoint(svg, avg(line.points[startPoint].x, line.points[startPoint+1].x), avg(line.points[startPoint].y, line.points[startPoint+1].y));
            
            for(uint i=startPoint+1;i<endPoint-1;i++) {
                svg.buffer.add(string(' Q '));
                writePoint(svg, line.points[i].x, line.points[i].y);
                svg.buffer.addByte(' ');
                writePoint(svg, avg(line.points[i].x, line.points[i+1].x), avg(line.points[i].y, line.points[i+1].y));
            }
            svg.endAttribute();
        }
    }
    
    function addUseForBlackGroup(DiscipleColorGroup memory group, string memory colorCode, uint strokeWidth) private pure {
        XML memory content = group.content;
        content.startTag('use');
        content.startAttribute('href');
        content.buffer.add(string('#black_'));
        content.buffer.add(uint16(group.width));
        content.endAttribute();
        writeColorAndWidth(content, colorCode, strokeWidth);
        content.endTag();
    }
    
    function endGroupAndWriteToSVG(XML memory svg, DiscipleColorGroup memory group, Fragments memory fragments) internal view {
        unchecked {
            group.content.endTag();
            if(group.color == Color.BLACK) {
                addUseForBlackGroup(group, getColorCode(Color.FELIX, fragments), max(28,group.width*12/10));
                addUseForBlackGroup(group, getColorCode(Color.BLACK, fragments), group.width);
            }
            svg.addPreformattedElement(group.content.toString());
        }
    }
    
    function startRectWithDims(XML memory svg, Frame memory frame) private pure {
        unchecked {
            svg.startTag('rect');
            svg.addAttribute('x', frame.x1);
            svg.addAttribute('y', frame.y1);
            svg.addAttribute('width', frame.width);
            svg.addAttribute('height', frame.height);
        }
    }
    
    function updateDataToIncludeLine(RenderData memory renderData, Line memory line, uint numPointsFromEnd, uint width) internal pure {
        unchecked {
            Frame memory linesFrame = renderData.linesFrame;
            for(uint i=line.numPoints-numPointsFromEnd;i<line.numPoints;i++) {
                Point memory point = line.points[i];
                setFrameTo(linesFrame, 
                    min(linesFrame.x1, point.x-int(width)),
                    min(linesFrame.y1, point.y-int(width)),
                    max(linesFrame.x2, point.x+int(width)),
                    max(linesFrame.y2, point.y+int(width))
                );
                
            }
            
            updateGridToIncludeLine(renderData, line);
        }
    }
    
    function updateDataToIncludeMainLine(RenderData memory renderData, Line memory line, bool isSelfie) internal pure {
        unchecked {
            updateDataToIncludeLine(renderData, line, isSelfie?1:line.numPoints, line.width*(isSelfie?3:1)/2);
        }
    }
    
    function updateGridToIncludeLine(RenderData memory renderData, Line memory line) private pure {
        unchecked {
            Point memory tmpPoint = line.tmpPoint;
            for(uint i=1;i<line.numPoints;i++) {
                setPointTo(tmpPoint, 
                    avg(line.points[i-1].x, line.points[i].x),
                    avg(line.points[i-1].y, line.points[i].y)
                );
                renderData.grid.addPoint(tmpPoint, line.width);
            }
        }
    }
    
    function addStars(RenderData memory renderData, Circles memory circles, uint size, uint count, uint numTries, Random memory random) internal pure {
        unchecked {
            uint numAdded = 0;
            Frame memory starsFrame = renderData.starsFrame;
            for(uint i=0;i<numTries && circles.count < circles.list.length && numAdded < count;i++) {
                Circle memory newCircle = circles.list[circles.count];
                setPointTo(newCircle.center, 
                    random.next(starsFrame.x1, starsFrame.x2),
                    random.next(starsFrame.y1, starsFrame.y2)
                );
                newCircle.diameter = size;
                if(renderData.grid.isValidPoint(newCircle.center, size*2)) {
                    circles.count++;
                    numAdded++;
                    renderData.grid.addPoint(newCircle.center, newCircle.diameter);
                }
            }
        }
    }
    

    function calcResultFrames(RenderData memory renderData, uint extraPadding) internal pure {
        unchecked {
            Frame memory linesFrame = renderData.linesFrame;
            Frame memory pieceFrame = renderData.pieceFrame;
            
            int centerX = avg(linesFrame.x1, linesFrame.x2);
            int centerY = avg(linesFrame.y1, linesFrame.y2);
            uint size = uint(max(linesFrame.width, linesFrame.height))+extraPadding;
            
            setFrameToSquareAtCenterAndSize(pieceFrame, centerX, centerY, size*12/20);
            setFrameToSquareAtCenterAndSize(renderData.starsFrame, centerX, centerY, size*11/20);
            
            uint backgroundFrameSize = uint(max(max(Visuals.centerX-pieceFrame.x1, Visuals.centerY-pieceFrame.y1), max(pieceFrame.x2-Visuals.centerX, pieceFrame.y2-Visuals.centerY)));
            setFrameToSquareAtCenterAndSize(renderData.backgroundFrame, Visuals.centerX, Visuals.centerY, backgroundFrameSize);
        }
    }
    
    function renderLights(XML memory svg, Circles memory lights, string memory whiteColorCode, Random memory random) private pure {
        unchecked {
            svg.startTag('g');
            addAttributeWithUrlValueForId(svg, 'fill', 'light');
            for(uint i=0;i<lights.count;i++) {
                Circle memory light = lights.list[i];
                svg.startTag('circle');
                svg.addAttribute('cx', light.center.x);
                svg.addAttribute('cy', light.center.y);
                svg.addAttribute('r', uint16(light.diameter*10));
                svg.startTag('animate');
                svg.addAttribute('attributeName', 'opacity');
                svg.addAttribute('values', '0;1;0');
                svg.addAttribute('repeatCount', 'indefinite');
                svg.startAttribute('dur');
                svg.buffer.add(random.next(5000, 20000));
                svg.buffer.add(string('ms'));
                svg.endAttribute();
                svg.endTag();
                svg.endTag();
            }
            svg.endTag();
            
            renderStars(svg, lights, whiteColorCode);
        }
    }
    
    function renderStars(XML memory svg, Circles memory stars, string memory whiteColorCode) private pure {
        unchecked {
            svg.startTag('g');
            addStrokeAttribute(svg, whiteColorCode);
            for(uint i=0;i<stars.count;i++) {
                Circle memory star = stars.list[i];
                startPath(svg);
                addStrokeWidthAttribute(svg, star.diameter);
                svg.startAttribute('d');
                svg.buffer.add(string('M '));
                writePoint(svg, star.center.x, star.center.y);
                svg.buffer.add(string(' l 0 0'));
                svg.endAttribute();
                svg.endTag();
            }
            svg.endTag();
        }
    }
    
    function startMarker(XML memory svg, Fragments memory fragments, StringFragment id, StringFragment markerAttribs, bool isFelixColor) private view {
        svg.startTag('marker');
        svg.addAttribute('id', idOfColor(fragments.get(id, 0), isFelixColor));
        svg.buffer.add(fragments.get(markerAttribs, 0));
    }
    
    function addPathWithColor(XML memory svg, Fragments memory fragments, Color color, StringFragment path, StringFragment extraAttributes) private view {
        startPath(svg);
        addStrokeAttribute(svg, getColorCode(color, fragments));
        svg.addAttribute('stroke-width', '.18');
        svg.buffer.add(fragments.get(extraAttributes, 0));
        svg.addAttribute('d', fragments.get(path, 0));
        svg.endTag();
    }
    
    function addSinglePathMarker(XML memory svg, Fragments memory fragments, StringFragment id, Color color, StringFragment path) private view {
        addSinglePathMarker(svg, fragments, id, color, path, StringFragment.NONE);
    }
    
    function addSinglePathMarker(XML memory svg, Fragments memory fragments, StringFragment id, Color color, StringFragment path, StringFragment extraAttributes) private view {
        startMarker(svg, fragments, id, StringFragment.SVG_MARKER_ATTRIB, color==Color.FELIX);
        addPathWithColor(svg, fragments, color, path, extraAttributes);
        svg.endTag();
    }
    
    function addCommonFaces(XML memory svg, Fragments memory fragments, Color color) private view {
        addSinglePathMarker(svg, fragments, StringFragment.FACE_NAME_5_EXCITED, color, StringFragment.PATH_FACE_EXCITED);
        addSinglePathMarker(svg, fragments, StringFragment.FACE_NAME_4_HAPPY, color, StringFragment.PATH_FACE_HAPPY);
        addSinglePathMarker(svg, fragments, StringFragment.FACE_NAME_3_NORMAL, color, StringFragment.PATH_FACE_NORMAL);
        addSinglePathMarker(svg, fragments, StringFragment.FACE_NAME_2_UNAMUSED, color, StringFragment.PATH_FACE_UNAMUSED);
        addSinglePathMarker(svg, fragments, StringFragment.FACE_NAME_1_SLEEPING, color, StringFragment.PATH_FACE_SLEEPING);
    }
    
    function addWrigglerFace(XML memory svg, Fragments memory fragments, StringFragment id, StringFragment path) private view {
        startMarker(svg, fragments, id, StringFragment.SVG_MARKER_ATTRIB, false);
        addPathWithColor(svg, fragments, Color.WHITE, StringFragment.PATH_FACE_WHITE_EYES, StringFragment.NONE);
        addPathWithColor(svg, fragments, Color.BLACK, path, StringFragment.NONE);
        svg.endTag();
    }

    
    function addMarkers(XML memory svg, Fragments memory fragments) private view {
        addCommonFaces(svg, fragments, Color.BLACK);
        addSinglePathMarker(svg, fragments, StringFragment.FACE_NAME_FROWNY, Color.BLACK, StringFragment.PATH_FACE_FROWNY);
        addCommonFaces(svg, fragments, Color.FELIX);
        
        addWrigglerFace(svg, fragments, StringFragment.FACE_NAME_SINISTER, StringFragment.PATH_FACE_SINISTER);
        addWrigglerFace(svg, fragments, StringFragment.FACE_NAME_DEFEATED, StringFragment.PATH_FACE_DEFEATED);
        
        addSinglePathMarker(svg, fragments, StringFragment.ITEM_NAME_HALO, Color.WHITE, StringFragment.PATH_ITEM_HALO);
        addSinglePathMarker(svg, fragments, StringFragment.ITEM_NAME_MECHHALO, Color.PINK, StringFragment.PATH_ITEM_MECHHALO);
        addSinglePathMarker(svg, fragments, StringFragment.ITEM_NAME_HORNS, Color.RED, StringFragment.PATH_ITEM_HORNS, StringFragment.SVG_HORNS_ATTRIB);
        
        startMarker(svg, fragments, StringFragment.ITEM_NAME_SUIT, StringFragment.SVG_SUIT_MARKER_ATTRIB, false);
        addPathWithColor(svg, fragments, Color.BLACK, StringFragment.PATH_ITEM_SUIT, StringFragment.NONE);
        svg.endTag();

    }
    
    
    function renderAll(RenderData memory renderData, bool animationView) internal view returns (string memory) {
        unchecked {
            string memory round = 'round';
            string memory frame = 'frame';
            string memory fill = 'fill';
            
            Fragments memory fragments = renderData.fragments;
            
            XML memory svg = XMLLib.create(MAX_SVG_SIZE);
            svg.startTag('svg');
            svg.addAttribute('xmlns', 'http://www.w3.org/2000/svg');
            
            svg.startAttribute('viewBox');
            Frame memory pieceFrame = renderData.pieceFrame;
            writePoint(svg, pieceFrame.x1, pieceFrame.y1);
            svg.buffer.addByte(' ');
            writePoint(svg, pieceFrame.width, pieceFrame.height);
            svg.endAttribute();
            
            svg.addAttribute('stroke-linecap',round);
            svg.addAttribute('stroke-linejoin',round);
            svg.addAttribute(fill,getColorCode(Color.NONE, fragments));
            
            svg.startTag('defs');
            
            
            svg.startTag('clipPath');
            svg.addAttribute('id', frame);
            startRectWithDims(svg, pieceFrame);
            svg.endTag();
            svg.endTag();
            
            svg.addPreformattedElement(fragments.get(StringFragment.SVG_GRADIENTS,0));
            
            addMarkers(svg, fragments);
            
            svg.endTag();
            
            svg.startTag('g');
            addAttributeWithUrlValueForId(svg, 'clip-path', frame);
            
            
            
            startRectWithDims(svg, renderData.backgroundFrame);
            addAttributeWithUrlValueForId(svg, fill, 'bg');
            svg.endTag();
            
            renderLights(svg, renderData.lights, getColorCode(Color.WHITE, fragments), RNG.createRandom(int32(uint32(renderData.mainDiscipleNumber))));
            renderStars(svg, renderData.stars, getColorCode(Color.WHITE, fragments));
            
            if(!animationView) {
                addWormLine(svg, renderData.wormLine, fragments);
            }
            
            DiscipleColorGroup memory group;
            
            for(uint i=0;i<renderData.discipleGroups.length;i++) {
                group = renderData.discipleGroups[i];
                if(group.width>0) {
                    Renderer.endGroupAndWriteToSVG(svg, group, fragments);
                }
            }
            
            if(!animationView) {
                Renderer.prepareGroup(group, renderData.discipleLine.color, getColorCode(renderData.discipleLine.color, fragments), renderData.discipleLine.width);
                Renderer.addDiscipleLine(group.content, renderData.discipleLine, fragments);
                Renderer.endGroupAndWriteToSVG(svg, group, fragments);
            }
            
            
            svg.endTag();
            svg.endTag();

            return svg.toString();
        }
    }
}
// File: contracts/StringFragments.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title StringFragments
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Some parts of the final outputs are static strings, this make it convenient to access them.
 */


enum StringFragment {
    NONE,
    JSON_ENCODING,
    JSON_1_NAME,
    JSON_2_DESCRIPTION_START,
    JSON_3_DESCRIPTION_END_CHAPTER,
    JSON_4_TOTAL_LIGHTS,
    JSON_5_HIGHEST_LIGHT_TIER,
    JSON_6_SPEED,
    JSON_7_EVOLUTION,
    JSON_8_EVOLUTION_CLOSE,
    JSON_9_SELFIE,
    JSON_10_IMAGE,
    JSON_11_IMAGE_CLOSE,
    LIGHT_TIER_0,
    LIGHT_TIER_1,
    LIGHT_TIER_2,
    LIGHT_TIER_3,
    SPEED_NAME_1,
    SPEED_NAME_2,
    SPEED_NAME_3,
    SPEED_NAME_4,
    SPEED_NAME_5,
    EVOLUTION_THEWORM,
    EVOLUTION_RESURRECTEDWORM,
    EVOLUTION_BIGWORM,
    EVOLUTION_MECHSUIT,
    SVG_GRADIENTS,
    SVG_MARKER_ATTRIB,
    SVG_SUIT_MARKER_ATTRIB,
    SVG_HORNS_ATTRIB,
    PATH_FACE_SLEEPING,
    PATH_FACE_UNAMUSED,
    PATH_FACE_NORMAL,
    PATH_FACE_HAPPY,
    PATH_FACE_EXCITED,
    PATH_FACE_FROWNY,
    PATH_FACE_WHITE_EYES,
    PATH_FACE_SINISTER,
    PATH_FACE_DEFEATED,
    PATH_ITEM_HALO,
    PATH_ITEM_MECHHALO,
    PATH_ITEM_HORNS,
    PATH_ITEM_SUIT,
    FACE_NAME_1_SLEEPING,
    FACE_NAME_2_UNAMUSED,
    FACE_NAME_3_NORMAL,
    FACE_NAME_4_HAPPY,
    FACE_NAME_5_EXCITED,
    FACE_NAME_FROWNY,
    FACE_NAME_SINISTER,
    FACE_NAME_DEFEATED,
    ITEM_NAME_HALO,
    ITEM_NAME_MECHHALO,
    ITEM_NAME_HORNS,
    ITEM_NAME_SUIT,
    COLOR_CODE_NONE,
    COLOR_CODE_FELIX,
    COLOR_CODE_GOLD,
    COLOR_CODE_PURPLE,
    COLOR_CODE_BLUE,
    COLOR_CODE_ORANGE,
    COLOR_CODE_RED,
    COLOR_CODE_BLACK,
    COLOR_CODE_WHITE,
    COLOR_CODE_PINK,
    ERROR_INVALID_DISCIPLE,
    ERROR_ONCHAIN_CALL_NOT_FROM_TOKEN_CONTRACT,
    ERROR_ROYALTY_ADDRESS_IS_CONTRACT,
    ERROR_TOKEN_CONTRACT_FROZEN,
    LENGTH
}

struct Fragments {
    function(StringFragment, uint) view returns (string memory) getStringFragmentWithOffset;
}

using FragmentsLib for Fragments global;

library FragmentsLib {
    function get(Fragments memory fragments, StringFragment stringFragment, uint offset) internal view returns (string memory) {
        return fragments.getStringFragmentWithOffset(stringFragment, offset);
    }
}

// File: contracts/Visuals.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 * @title Visuals
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Main logic for how to calculate the shapes of all Disciples and the Worm.
 */


import "./lib/RNG.sol";
import "./HeartField.sol";

// Each chapter has a shape the reflects the average speed of transfer of all the Disciples in the chapter.
// Shapes are calculated as such:
// 
// 1. Calculate transfer speed: time sent - time received in seconds, set to 0 for Arsonic, Disciple #1, Praise Be!
// 
// 2. Calculate speed level based on transfer speed:
//       0 seconds  : 5 Lightning (fastest)
//       < 5 minutes: 4 Swift
//       < 6 hours  : 3 Nimble
//       < 72 hours : 2 Reluctant
//       otherwise  : 1 Time Out (slowest)
//    Speed levels determine the expression of The Disciple, as well as the size of The Disciple and The Worm in their piece
// 
// 3. Rank chapters separately by average transfer speed and by speed level
// 
// 4. Blend the two rankings, and assign chapters to shapes in order of ranking to get about 200 Disciples per shape.
// 
//    The chapter shapes are: 
//      - Line       (looks like the letter 'l'), fastest
//      - Undulating (looks like the letter 'u') 
//      - Wiggly     (looks like the letter 'w') 
//      - Squiggly   (looks like the letter 's') 
//      - Curly      (looks like the letter 'c'), slowest
//   
//    Also the shape of Selfie is used in the code for Disciples who get the selfie

enum Shape {
    NONE,
    LINE,
    UNDULATING,
    WIGGLY,
    SQUIGGLY,
    CURLY,
    SELFIE
}


// Each chapter has one of 7 colors (Felix through Black below). colors are pre-set for some chapters, 
// and for others, they are chosen to create the maximum visual distance between chapters of similar colors and shapes
enum Color {
    NONE,
    FELIX, //a special green
    GOLD,
    PURPLE,
    BLUE,
    ORANGE,
    RED,
    BLACK,
    WHITE,
    PINK
}

// The Worm has evolved through its journey, this indicates which visual style to draw it in
enum Evolution {
    THEWORM,
    RESURRECTEDWORM,
    BIGWORM,
    MECHSUIT
}




struct Line {
    uint number;
    uint chapter;
    Color color;
    Shape shape;
    uint speedLevel;
    Point startingPoint;
    uint spacing; 
    uint width;
    uint pctHeart;
    Random random;
    Point tmpPoint;
    
    uint numSegments;
    uint numPoints;
    Point[13] points;
}


library Visuals {
    
    int internal constant centerX = 5000;
    int internal constant centerY = 5000;
    
    
    uint internal constant othersSize = 20;

    function mainSize(uint discipleNumber, uint speedLevel, bool selfie) internal pure returns (uint) {
        unchecked {
            uint mainDiscipleBaseSize = max(60,(discipleNumber+300)*15/100);
            return mainDiscipleBaseSize*(selfie?2:1)*speedLevel/2;
        }
    }
    
    
    function evolution(uint discipleNumber) internal pure returns (Evolution) {
        return discipleNumber >= 431 && discipleNumber <= 454 ? Evolution.BIGWORM
             : discipleNumber <  273                          ? Evolution.THEWORM
             : discipleNumber <  827                          ? Evolution.RESURRECTEDWORM
             :                                                  Evolution.MECHSUIT;
    }
    
    function calcPercentHeart(uint discipleNumber) private pure returns (uint) {
        unchecked {
            return discipleNumber < 300 ? 0 : discipleNumber < 800 ? (discipleNumber - 300) / 5 : 100;
        }
    }
    
    
    
    
    
    
    function buildSimplifiedLineWithAngleFunction(Line memory line, HeartField memory field, function(uint, uint, int) pure returns (int) getAngle, int randomValue) private pure {
        unchecked {
            line.numPoints = line.numSegments;
            for(uint i=1;i<line.numPoints;i++) {
                int angle = getAngle(i, line.numPoints, randomValue);
                field.setNextPoint(line.points[i-1], int(line.spacing), angle, line.pctHeart, line.tmpPoint);
                field.setNextPoint(line.tmpPoint, int(line.spacing), angle, line.pctHeart, line.points[i]);
            }
        }
    }

    function buildDetailedLineWithAngleFunction(Line memory line, HeartField memory field, function(uint, uint, int, int) pure returns (int) getAngle, int randomValue1, int randomValue2) private pure {
        unchecked {
            line.numPoints = line.numSegments*2-1;
            for(uint i=1;i<line.numPoints;i++) {
                field.setNextPoint(line.points[i-1], int(line.spacing), getAngle(i, line.numPoints, randomValue1, randomValue2), line.pctHeart, line.points[i]);
            }
        }
    }
    
    
    
    
    
    
    function buildLineAsShapeSelfie(Line memory line, HeartField memory field) private pure {
        unchecked {
            line.numPoints = line.numSegments;
            setPointTo(line.points[line.numPoints-1], line.startingPoint);
            
            for(uint i=line.numPoints-1;i>0;i--) {
                field.setNextPoint(line.points[i], int(line.spacing), 270000, line.pctHeart, line.tmpPoint);
                field.setNextPoint(line.tmpPoint, int(line.spacing), 270000, line.pctHeart, line.points[i-1]);
            }
        }
    }
    
    
    
    
    
    
    function angleForLineShape(uint, uint, int) private pure returns (int) {
        return 0;
    }
    
    function buildLineAsShapeLine(Line memory line, HeartField memory field) private pure {
        unchecked {
            buildSimplifiedLineWithAngleFunction(line, field, angleForLineShape, 0);
        }
    }
    
    
    
    
    
    function angleForUndulatingShape(uint i, uint n, int randomValue) private pure returns (int) {
        unchecked {
            return (int(n)-int(i)*2)*randomValue;
        }
    }
    
    function buildLineAsShapeUndulating(Line memory line, HeartField memory field) private pure {
        unchecked {
            int randomValue = line.random.boolean()?line.random.next(12000,15000):-line.random.next(8000,11000);
            field.setNextPoint(line.startingPoint, randomValue*int(line.width)/10000/2, 270000, line.pctHeart, line.points[0]);
            
            buildSimplifiedLineWithAngleFunction(line, field, angleForUndulatingShape, randomValue);
        }
    }
    
    
    
    
    
    function angleForWigglyShape(uint i, uint n, int randomValue) private pure returns (int) {
        unchecked {
            return (int((n+i+1)%2)*2-int(1))*randomValue;
        }
    }
    
    function buildLineAsShapeWiggly(Line memory line, HeartField memory field) private pure {
        unchecked {
            int randomValue = line.random.sign();
            randomValue *= line.random.next(35000,50000);
            
            buildSimplifiedLineWithAngleFunction(line, field, angleForWigglyShape, randomValue);
        }
    }
    
    
    
    
    
    function angleForSquigglyShape(uint i, uint n, int randomValue1, int) private pure returns (int) {
        unchecked {
            return (int((min(i, n-i)-1)*(n==13?26:44))-100)*randomValue1;
        }
    }
    
    function buildLineAsShapeSquiggly(Line memory line, HeartField memory field) private pure {
        unchecked {
            int randomValue = line.random.sign();
            randomValue *= line.random.next(800, 1200);
            
            field.setNextPoint(line.startingPoint, randomValue*int(line.width)*2/1000, 90000, line.pctHeart, line.points[0]);
            
            buildDetailedLineWithAngleFunction(line, field, angleForSquigglyShape, randomValue, 0);
        }
    }
    
    
    
    
    
    function angleForCurlyShape(uint i, uint, int randomValue1, int randomValue2) private pure returns (int) {
        unchecked {
            return randomValue1/int(14+(i-1))+randomValue2;
        }
    }
    
    function buildLineAsShapeCurly(Line memory line, HeartField memory field) private pure {
        unchecked {
            int randomValue1 = line.random.sign();
            randomValue1 *= line.random.next(11000000,13000000);
            int randomValue2 = line.random.next(0,360)*1000;
            
            buildDetailedLineWithAngleFunction(line, field, angleForCurlyShape, randomValue1, randomValue2);
        }
    }
    
    
    
    
    
    
    function buildLine(Line memory line, HeartField memory field) private pure {
        unchecked {
            
            RNG.initializeRandom(line.random, int32(uint32(line.number+uint(line.startingPoint.x))));

            setPointTo(line.points[0], line.startingPoint);
            
            ( line.shape==Shape.LINE       ? buildLineAsShapeLine
            : line.shape==Shape.UNDULATING ? buildLineAsShapeUndulating
            : line.shape==Shape.WIGGLY     ? buildLineAsShapeWiggly
            : line.shape==Shape.SQUIGGLY   ? buildLineAsShapeSquiggly
            : line.shape==Shape.CURLY      ? buildLineAsShapeCurly 
            :                                buildLineAsShapeSelfie)(line, field);
            
        }
    }
    
    
    function buildDiscipleLine(Line memory line, HeartField memory field) internal pure {
        unchecked {
            line.spacing = othersSize;
            line.width = othersSize;
            line.pctHeart = calcPercentHeart(line.number);
            line.numSegments = 5;
            
            buildLine(line, field);
        }
    }
    
    function buildMainDiscipleAndWormLines(Line memory mainDiscipleLine, Line memory wormLine, HeartField memory field, bool selfie) internal pure {
        unchecked {
            Random memory discipleRandom = RNG.createRandom(int16(mainDiscipleLine.startingPoint.x));
            Random memory wormRandom = RNG.createRandom(int16(mainDiscipleLine.startingPoint.y));
            
            
            mainDiscipleLine.spacing = mainSize(mainDiscipleLine.number, mainDiscipleLine.speedLevel, selfie);
            mainDiscipleLine.width = mainDiscipleLine.spacing;
            mainDiscipleLine.pctHeart = calcPercentHeart(mainDiscipleLine.number/2);
            mainDiscipleLine.numSegments = 5;
            if(selfie) {
                mainDiscipleLine.shape = Shape.SELFIE;
                mainDiscipleLine.spacing = mainDiscipleLine.spacing * 8 / 10;
                field.setNextPoint(mainDiscipleLine.startingPoint, int(mainDiscipleLine.width)*15/10, 270000, mainDiscipleLine.pctHeart, mainDiscipleLine.startingPoint);
            } else {
                field.setNextPoint(mainDiscipleLine.startingPoint, int(mainDiscipleLine.width)*discipleRandom.next(200,500)/100, 270000+discipleRandom.next(-30000,30000), mainDiscipleLine.pctHeart, mainDiscipleLine.startingPoint);
            }
            
            buildLine(mainDiscipleLine, field);
            
            bool isBigWorm = evolution(mainDiscipleLine.number) == Evolution.BIGWORM;
            wormLine.spacing = mainDiscipleLine.spacing * (isBigWorm ? 2 : 1);
            wormLine.width = mainDiscipleLine.width * (isBigWorm ? 25 : 10) / 10;
            wormLine.pctHeart = mainDiscipleLine.pctHeart;
            wormLine.numSegments = 7;
            if(selfie) {
                wormLine.shape = Shape.SELFIE;
                field.setNextPoint(mainDiscipleLine.points[4], int(wormLine.width)*12/10, int24(wormRandom.boolean()?20000:160000), wormLine.pctHeart, wormLine.startingPoint);
            } else {
                field.setNextPoint(mainDiscipleLine.points[uint(wormRandom.next(0, int(mainDiscipleLine.numPoints-1)))], int(wormLine.spacing)*wormRandom.next(300,500)/100, 270000+wormRandom.next(-90000,45000), wormLine.pctHeart, wormLine.startingPoint);
            }
            
            buildLine(wormLine, field);
        }
    }
    
    
    
    
    
    
    function starSize(Frame memory starsFrame) internal pure returns (uint) {
        unchecked {
            return uint(starsFrame.width)/300;
        }
    }
    
    function lightSize(uint computedStarSize, uint lumenLevel) internal pure returns (uint) {
        unchecked {
            return computedStarSize*[1,2,3,5][lumenLevel];
        }
    }
}
// File: contracts/Worm1000Artwork.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* 
 *   ,--------.,--.                ,--.   ,--.                                   ,--. ,---.  ,--.
 *   '--.  .--'|  ,---.  ,---.     |  |   |  | ,---. ,--.--.,--,--,--.     ,---. `--'/  .-',-'  '-. ,---.
 *      |  |   |  .-.  || .-. :    |  |.'.|  || .-. ||  .--'|        |    | .-. |,--.|  `-,'-.  .-'(  .-'
 *      |  |   |  | |  |\   --.    |   ,'.   |' '-' '|  |   |  |  |  |    ' '-' '|  ||  .-'  |  |  .-'  `)
 *      `--'   `--' `--' `----'    '--'   '--' `---' `--'   `--`--`--'    .`-  / `--'`--'    `--'  `----'
 *     ,--.  ,--.     ,--.           ,--.   ,--.                     ,--. `---'
 *   ,-'  '-.|  ,---. `--' ,---.     |  |-. |  | ,---.  ,---.  ,---. `--',--,--,  ,---.
 *   '-.  .-'|  .-.  |,--.(  .-'     | .-. '|  || .-. :(  .-' (  .-' ,--.|      \| .-. |
 *     |  |  |  | |  ||  |.-'  `)    | `-' ||  |\   --..-'  `).-'  `)|  ||  ||  |' '-' '
 *     `--'  `--' `--'`--'`----'      `---' `--' `----'`----' `----' `--'`--''--'.`-  /
 *     ,--.               ,--.  ,--.                 ,---.,--.               ,--.`---'
 *   ,-'  '-. ,---.     ,-'  '-.|  ,---.  ,---.     /  .-'`--',--.--. ,---.,-'  '-.
 *   '-.  .-'| .-. |    '-.  .-'|  .-.  || .-. :    |  `-,,--.|  .--'(  .-''-.  .-'
 *     |  |  ' '-' '      |  |  |  | |  |\   --.    |  .-'|  ||  |   .-'  `) |  |
 *     `--'   `---'       `--'  `--' `--' `----'    `--'  `--'`--'   `----'  `--'
 *    ,--.  ,--.    ,--.    ,--.      ,------.  ,--.             ,--.       ,--.
 *   /   | /    \  /    \  /    \     |  .-.  \ `--' ,---.  ,---.`--' ,---. |  | ,---.  ,---.
 *   `|  ||  ()  ||  ()  ||  ()  |    |  |  \  :,--.(  .-' | .--',--.| .-. ||  || .-. :(  .-'
 *    |  | \    /  \    /  \    /     |  '--'  /|  |.-'  `)\ `--.|  || '-' '|  |\   --..-'  `).--.
 *    `--'  `--'    `--'    `--'      `-------' `--'`----'  `---'`--'|  |-' `--' `----'`----' '--'
 *                                                                   `--'
 *   
 * @title Worm1000Artwork
 * @author minimizer <me@minimizer.art>; https://minimizer.art/ 
 * 
 * Praise Be! The Worm hereby offers this gift to the first 1000 Disciples that have helped It with Its mission
 * to visit every wallet on the Ethereum blockchain.
 * 
 * The Worm, The Ambition Team, and minimizer have teamed together to produce this 100% fully on-chain generative artwork. 
 * For the fully on-chain appreciators, the entire artwork for all 1000 pieces can be created from a single contract, 
 * created in a single transaction, with no additional data subsequently uploaded, in a fun exercise of solidity golfing.
 * 
 * The main contract offers the tokenURI method which will generate a full base64-encoded JSON metadata for the token including
 * the artwork in SVG format. The art is precomputed, however the holder of the token can acquire more lights and the artwork 
 * will be updated to include them, and therefore the number of lights held is a parameter to the tokenURI method. 
 * There is also an option to generate each frame of an animation of all 1000 pieces which depicts how all 1000 Disciples have 
 * come together to build the loving community around The Worm.
 * 
 * Apart from the lights held, the artwork is effectively frozen with the owner of this contract, minimizer, having no 
 * ability to make any changes to the art in the future. 
 */

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/utils/Base64.sol";

import "./lib/Storage.sol";
import "./lib/XML.sol";
import "./IWorm1000Artwork.sol";
import "./HeartField.sol";
import "./Visuals.sol";
import "./Renderer.sol";
import "./StringFragments.sol";

contract Worm1000Artwork is IWorm1000Artwork, Ownable {
    
    // The token contract is the only contract allowed to make an on-chain call to the artwork. 
    // Upon deployment this setting will be frozen forever
    address public tokenContract;
    bool public tokenContractFrozen = false;
    
    // The address to which a split of royalties is sent
    address public royaltyRecipient;
    
    // All the data below is used to generate the artwork, passed into the constructor and frozen
    mapping(uint=>uint) private discipleStartingPointsPacked;
    mapping(uint=>uint) private discipleChapterNumbersPacked;
    mapping(uint=>uint) private discipleSpeedLevelsPacked;
    mapping(uint=>uint) private discipleSelfiePacked;
    
    mapping(uint=>string) private chapterNames;
    uint256 private chapterColorsPacked;
    uint256 private chapterShapesPacked;
    
    mapping(uint=>uint) private heartAngles;
    
    mapping(uint=>string) private stringFragments;
    
    uint private constant TOKENURI_MAX_LENGTH = 400000;
    
    // Constructor stores all artwork data for use in tokenURI. The data is packed efficiently to save gas
    constructor(uint256[] memory discipleStartingPointsPacked_, 
                uint256[] memory discipleChapterNumbersPacked_, 
                uint256[] memory discipleSpeedLevelsPacked_, 
                uint256[] memory discipleSelfiePacked_, 
                
                bytes memory chapterNamesPacked_,
                uint256 chapterColorsPacked_,
                uint256 chapterShapesPacked_,
                
                uint256[] memory heartAngles_, 
            
                bytes memory stringFragmentsPacked_) {
        
        Storage.fillUintMapping(discipleStartingPointsPacked_, discipleStartingPointsPacked);
        Storage.fillUintMapping(discipleChapterNumbersPacked_, discipleChapterNumbersPacked);
        Storage.fillUintMapping(discipleSpeedLevelsPacked_, discipleSpeedLevelsPacked);
        Storage.fillUintMapping(discipleSelfiePacked_, discipleSelfiePacked);
        
        Storage.unpackAndFillStringMapping(chapterNamesPacked_, chapterNames);
        chapterColorsPacked = chapterColorsPacked_;
        chapterShapesPacked = chapterShapesPacked_;
        
        Storage.fillUintMapping(heartAngles_, heartAngles);
        
        Storage.unpackAndFillStringMapping(stringFragmentsPacked_, stringFragments);
        
        royaltyRecipient = owner();
    }
    
    // Admin function to be used during deployment, to point the artwork to the token contract
    function setTokenContract(address tokenContract_) public onlyOwner {
        requireOrError(!tokenContractFrozen, StringFragment.ERROR_TOKEN_CONTRACT_FROZEN);
        tokenContract = tokenContract_;
    }
    
    // Upon successful deployment, the token contract is frozen
    function freezeTokenContract() public onlyOwner {
        tokenContractFrozen = true;
    }
    
    // Which address to direct royalties to, this setting is read by the token contract
    function setRoyaltyRecipient(address royaltyRecipient_) public onlyOwner {
        requireOrError(!Address.isContract(royaltyRecipient_), StringFragment.ERROR_ROYALTY_ADDRESS_IS_CONTRACT);
        royaltyRecipient = royaltyRecipient_;
    }
    
    
    
    
    // The below methods all extract and unpack if necessary the data for each disciple / chapter
    
    function getDiscipleChapter(uint discipleNumber) private view returns (uint) {
        unchecked {
            return Storage.getUnpacked((discipleNumber-1), 8, discipleChapterNumbersPacked);
        }
    }
    
    function getDiscipleSpeedLevel(uint discipleNumber) private view returns (uint) {
        unchecked {
            return Storage.getUnpacked((discipleNumber-1), 4, discipleSpeedLevelsPacked);
        }
    }
    
    function getDiscipleSelfie(uint discipleNumber) private view returns (bool) {
        unchecked {
            return Storage.getUnpacked((discipleNumber-1), 1, discipleSelfiePacked) > 0;
        }
    }
    
    function getStringFragmentWithOffset(StringFragment index, uint offset) private view returns (string memory) {
        unchecked {
            return stringFragments[uint(index)+offset];
        }
    }
    
    function populateLineWithDiscipleInfo(uint discipleNumber, Line memory line) private view {
        unchecked {
            line.number = discipleNumber;
            line.chapter = getDiscipleChapter(discipleNumber);
            line.color = Color(Storage.extractBits(line.chapter, 4, chapterColorsPacked));
            line.shape = Shape(Storage.extractBits(line.chapter, 4, chapterShapesPacked));
            line.speedLevel = getDiscipleSpeedLevel(discipleNumber);
            setPointTo(line.startingPoint, 
                int(Storage.getUnpacked((discipleNumber-1)*2  , 16, discipleStartingPointsPacked)),
                int(Storage.getUnpacked((discipleNumber-1)*2+1, 16, discipleStartingPointsPacked))
            );
        }
    }
    
    
    
    // The main method for generating the art. It takes 3 arguments:
    //   mainDiscipleNumber: 1-1000
    //   numberOfLightsByLumenLevel: an array of 4 values indiciting the number of lights held at lumen levels 0, 1, 2 and 3
    //   animationView: boolean
    //     false - generate the artwork as intended for each token
    //     true  - generate one frame of the composite animation
    // 
    // This method can only be called on-chain from the token contract, but can be called by any end-user account without restriction
    function tokenURI(uint mainDiscipleNumber, uint[4] calldata numberOfLightsByLumenLevel, bool animationView) public view returns (string memory) {
        unchecked {
            requireOrError(msg.sender == tx.origin || msg.sender == tokenContract, StringFragment.ERROR_ONCHAIN_CALL_NOT_FROM_TOKEN_CONTRACT);
            requireOrError(mainDiscipleNumber >= 1 && mainDiscipleNumber <= 1000, StringFragment.ERROR_INVALID_DISCIPLE);
            
            Renderer.RenderData memory renderData = Renderer.initializeData(mainDiscipleNumber);
            renderData.fragments.getStringFragmentWithOffset = getStringFragmentWithOffset;
            HeartField memory field = HeartFieldLib.createField(Visuals.centerX, Visuals.centerY, heartAngles);
            
            uint maxDiscipleToDrawSmall = animationView ? mainDiscipleNumber : mainDiscipleNumber - 1;
            
            for(uint i=1;i<=maxDiscipleToDrawSmall;i++) {
                populateLineWithDiscipleInfo(i, renderData.discipleLine);
                Visuals.buildDiscipleLine(renderData.discipleLine, field);
                Renderer.addCurrentDiscipleLine(renderData);
                Renderer.updateDataToIncludeLine(renderData, renderData.discipleLine, renderData.discipleLine.numPoints, renderData.discipleLine.width/2);
            }
            
            if(!animationView) {
                populateLineWithDiscipleInfo(mainDiscipleNumber, renderData.discipleLine);
                populateLineWithDiscipleInfo(mainDiscipleNumber, renderData.wormLine);
                bool isSelfie = getDiscipleSelfie(mainDiscipleNumber) && !animationView;
                Visuals.buildMainDiscipleAndWormLines(renderData.discipleLine, renderData.wormLine, field, isSelfie);
                Renderer.updateDataToIncludeMainLine(renderData, renderData.discipleLine, isSelfie);
                Renderer.updateDataToIncludeMainLine(renderData, renderData.wormLine,     isSelfie);
            }
            
            Renderer.calcResultFrames(renderData, animationView?200:0);
            
            Random memory starsRandom = RNG.createRandom(int32(uint32(mainDiscipleNumber)));
            uint starSize = Visuals.starSize(renderData.starsFrame);
            for(int lumenLevel=3;lumenLevel>=0;lumenLevel--) {
                uint lightSize = Visuals.lightSize(starSize, uint(lumenLevel));
                Renderer.addStars(renderData, renderData.lights, lightSize, numberOfLightsByLumenLevel[uint(lumenLevel)], 300, starsRandom);
            }
            Renderer.addStars(renderData, renderData.stars, starSize, 50, 100, starsRandom);
            
            return generateTokenURI(mainDiscipleNumber, numberOfLightsByLumenLevel, Renderer.renderAll(renderData, animationView));
        }
    }
    
    // Helper method to wrap the svg up into the metadata
    function generateTokenURI(uint mainDiscipleNumber, uint[4] calldata numberOfLightsByLumenLevel, string memory svg) private view returns (string memory) {
        unchecked {
            uint totalLights = numberOfLightsByLumenLevel[0]+numberOfLightsByLumenLevel[1]+numberOfLightsByLumenLevel[2]+numberOfLightsByLumenLevel[3];
            
            uint highestLightTier = numberOfLightsByLumenLevel[3]>0?3
                                  : numberOfLightsByLumenLevel[2]>0?2
                                  : numberOfLightsByLumenLevel[1]>0?1
                                  : 0;
            
            StringBuffer memory json = StringBufferLib.create(TOKENURI_MAX_LENGTH);
            
            append(json, StringFragment.JSON_1_NAME);
            json.add(uint16(mainDiscipleNumber));
            append(json, StringFragment.JSON_2_DESCRIPTION_START);
            json.add(uint16(mainDiscipleNumber));
            append(json, StringFragment.JSON_3_DESCRIPTION_END_CHAPTER);
            json.add(chapterNames[getDiscipleChapter(mainDiscipleNumber)]);
                
            if(totalLights>0) {
                append(json, StringFragment.JSON_4_TOTAL_LIGHTS);
                json.add(uint16(totalLights));
                append(json, StringFragment.JSON_5_HIGHEST_LIGHT_TIER);
                appendOffset(json, StringFragment.LIGHT_TIER_0, highestLightTier);
            }
            
            append(json, StringFragment.JSON_6_SPEED);
            appendOffset(json, StringFragment.SPEED_NAME_1, getDiscipleSpeedLevel(mainDiscipleNumber)-1);
            append(json, StringFragment.JSON_7_EVOLUTION);
            appendOffset(json, StringFragment.EVOLUTION_THEWORM, uint(Visuals.evolution(mainDiscipleNumber)));
            append(json, StringFragment.JSON_8_EVOLUTION_CLOSE);
            
            if(getDiscipleSelfie(mainDiscipleNumber)) {
                append(json, StringFragment.JSON_9_SELFIE);
            }
            
            append(json, StringFragment.JSON_10_IMAGE);
            json.add(Base64.encode(bytes(svg)));
            append(json, StringFragment.JSON_11_IMAGE_CLOSE);
            
            return string(abi.encodePacked(getStringFragmentWithOffset(StringFragment.JSON_ENCODING, 0), Base64.encode(bytes(json.toString()))));
        }
    }
    
    //helper functions for tokenURI
    function appendOffset(StringBuffer memory buffer, StringFragment index, uint offset) private view {
        buffer.add(getStringFragmentWithOffset(index, offset));
    }
    function append(StringBuffer memory buffer, StringFragment index) private view {
        appendOffset(buffer, index, 0);
    }
    
    
    //helper method for require
    function requireOrError(bool test, StringFragment message) private view {
        require(test, getStringFragmentWithOffset(message, 0));
    }
}