Model.js

/**
 * All other Linen elements extend this Model.
 */
Linen.Model = class {

    /**
     * @param {Linen} Linen - The instance of Linen.
     */
    constructor(Linen) {
        this.Linen = Linen;
        this.dimensions = {
            x: 0,
            y: 0,
            width: "100%",
            height: "100%"
        };

        this.settings = {
            alignment: "left",
            bold: false,
            fillStyle: "#000000",
            filter: "none",
            fontFamily: "Arial",
            fontSize: "12pt",
            globalAlpha: 1,
            italic: false,
            lineCap: "butt",
            lineDashOffset: 0,
            textLineHeight: 1.2,
            lineJoin: "miter",
            lineWidth: 1,
            miterLimit: 10,
            shadowBlur: 0,
            shadowColor: "#000000",
            shadowOffsetX: 0,
            shadowOffsetY: 0,
            strokeStyle: "#000000",
            textAlign: "left",
            textBaseline: "top",
            v_alignment: "top",
            zindex: 1
        };

        this.fill = false;
        this.stroke = false;
        this.clip = false;
        this.trasform = false;
        this.callback = function(){}
    }

    /**
     * Get the dpi of the Linen instance
     * @return {number} dpi
     */
    dpi() {
        return this.Linen.dpi;
    }

    /**
     * Get the width of the element
     * @return {number} px
     */
    width() {
        return this.getDimensionPx("width");
    }

    /**
     * Get the height of the element
     * @return {number} px
     */
    height() {
        return this.getDimensionPx("height");
    }

    /**
     * Get the x positioning of the element with reference point considered.
     * @return {number} px
     */
    x() {
        var x = this.getDimensionPx("x"); //px
        var w = this.width(); //px
        switch (this.settings.alignment) {
            case "center":
                return x - (w / 2);
            case "right":
                return x - w;
        }
        return x;
    }

    /**
     * Get the y positioning of the element with reference point considered.
     * @return {number} px
     */
    y() {
        const y = this.getDimensionPx("y"); //px
        const h = this.height(); //px
        switch (this.settings.v_alignment) {
            case "middle":
                return y - (h / 2);
            case "bottom":
                return y - h;
        }
        return y;
    }

    /**
     * Set the width of the element
     * @param {number} val - width
     * @return {self} self
     */
    setWidth(val) {
        return this.setDimension("width", val);
    }

    /**
     * Set the height of the element
     * @param {number} val - height
     * @return {self} self
     */
    setHeight(val) {
        return this.setDimension("height", val);
    }

    /**
     * Set the x positioning of the element
     * @param {number} val - x positioning
     * @return {self} self
     */
    setX(val) {
        return this.setDimension("x", val);
    }

    /**
     * Set the y positioning of the element
     * @param {number} val - y positioning
     * @return {self} self
     */
    setY(val) {
        return this.setDimension("y", val);
    }

    /**
     * Shorthand to set the x and y positioning of the element
     * @param {number} x - x positioning
     * @param {number} y - y positioning
     * @return {self} self
     */
    setXY(x, y) {
        return this.setX(x).setY(y);
    }

    /**
     * Set the x reference point for positioning
     * @param {'left' | 'center' | 'right'} [alignment=left] - left|center|right
     * @return {self} self
     */
    setAlignment(alignment = 'left') {
        return this.setSetting("alignment", alignment.toLowerCase());
    }

    /**
     * Shorthand to set the reference point for positioning to center
     * @return {self} self
     */
    center() {
        return this.setAlignment("center");
    }

    /**
     * Set the y reference point for positioning
     * @param {'top' | 'middle' | 'bottom'} [v_alignment=top] - top|middle|bottom
     * @return {self} self
     */
    setVAlignment(v_alignment = "top") {
        return this.setSetting("v_alignment", v_alignment.toLowerCase());
    }

    /**
     * Shorthand to set the y reference point for positioning to bottom
     * @return {self} self
     */
    middle() {
        return this.setVAlignment("middle");
    }

    /**
     * Set the transform to be used on 
     * @see {@link https://www.w3schools.com/tags/canvas_transform.asp|w3schools}
     * @param {type} a - Horizontal scaling. A value of 1 results in no scaling.
     * @param {type} b - Vertical skewing.
     * @param {type} c - Horizontal skewing.
     * @param {type} d - Vertical scaling. A value of 1 results in no scaling.
     * @param {type} e - Horizontal translation (moving).
     * @param {type} f - Vertical translation (moving).
     * @returns {self} self
     */
    setTransorm(a = 1, b = 0, c = 0, d = 1, e = 0, f = 0) {
        return this.setProp("transform", {"a": a, "b": b, "c": c, "d": d, "e": e, "f": f});
    }

    /**
     * Enable or disable rendering the fill
     * @param {bool} bool - TRUE|FALSE
     * @return {self} self
     */
    setFill(bool = true) {
        return this.setProp("fill", bool);
    }

    /**
     * Enable or disable rendering the stroke
     * @param {bool} bool - TRUE|FALSE
     * @return {self} self
     */
    setStroke(bool = true) {
        return this.setProp("stroke", bool);
    }

    /**
     * Enable or disable using the path as a clipping mask.
     * @param {bool} bool - TRUE|FALSE
     * @return {self} self
     */
    setClip(bool = true) {
        return this.setProp("clip", bool);
    }

    /**
     * Set the value of a given dimension.
     * @param {string} dimension - dimension name.
     * @param {*} value - mixed value
     * @return {self} self
     */
    setDimension(dimension, value) {
        this.dimensions[dimension] = value;
        return this;
    }

    /**
     * Get the value of a given setting.
     * @param {string} setting - setting name.
     * @return {string} value
     */
    getSetting(setting) {
        return this.settings[setting] || null;
    }

    /**
     * Set the value of a given setting.
     * @param {string} setting - setting name.
     * @param {*} value - mixed value
     * @return {self} self
     */
    setSetting(setting, value) {
        this.settings[setting] = value;
        return this;
    }

    /**
     * Set the value of a given property. This should probably not be used directly.
     * @param {string} prop - property name.
     * @param {*} value - mixed value
     * @return {self} self
     */
    setProp(prop, value) {
        this[prop] = value;
        return this;
    }

    /**
     * Canvas 2d Context Settings
     */

    /**
     * Set the fillStyle to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_fillstyle.asp|w3schools}
     * @param {string} fillStyle - color|gradient|pattern
     * @return {self} self
     */
    setFillStyle(fillStyle) {
        return this.setSetting("fillStyle", fillStyle);
    }

    /**
     * Set the filter to be used for rendering.
     * @see {@link https://www.w3schools.com/cssref/css3_pr_filter.asp|w3schools}
     * @param {string} filter - none | blur() | brightness() | contrast() | drop-shadow() | grayscale() | hue-rotate() | invert() | opacity() | saturate() | sepia() | url();
     * @return {self} self
     */
    setFilter(filter) {
        return this.setSetting("fillter", filter);
    }

    /**
     * Set the font to be used for rendering.
     * @desription Do not set font directly. Use setFontSize(), setFontFamily(), setBold(), and setItalic() on Linen.Text instead.
     * @access private
     * @see {@link https://www.w3schools.com/tags/canvas_font.asp|w3schools}
     * @return {self} self
     */
    setFont() {
        const font = [
            this.settings.bold ? "bold " : "",
            this.settings.italic ? "italic " : "",
            this.settings.fontSize + "px",
            " ",
            this.settings.fontFamily
        ];
        this.context().font = font.join("");
        return this.setSetting("font", font.join(""));
    }

    /**
     * Set the globalAlpha to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_globalalpha.asp|w3schools}
     * @param {float} globalAlpha - 1 being completely opaque and 0 being completely transparent
     * @return {self} self
     */
    setGlobalAlpha(globalAlpha) {
        return this.setSetting("globalAlpha", globalAlpha);
    }

    /**
     * Set the lineCap to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_linecap.asp|w3schools}
     * @param {string} lineCap - butt|round|square
     * @return {self} self
     */
    setLineCap(lineCap) {
        return this.setSetting("lineCap", lineCap);
    }

    /**
     * Set the lineJoin to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_linejoin.asp|w3schools}
     * @param {string} lineJoin - bevel|round|miter
     * @return {self} self
     */
    setLineJoin(lineJoin) {
        return this.setSetting("lineJoin", style);
    }

    /**
     * Set the lineWidth to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_linewidth.asp|w3schools}
     * @param {*} lineWidth - size in px, pt, inches (in), or percentage (%)
     * @return {self} self
     */
    setLineWidth(lineWidth) {
        return this.setSetting("lineWidth", this.translateToPx(lineWidth));
    }

    /**
     * Set the miterLimit to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_miterlimit.asp|w3schools}
     * @param {number} miterLimit - A positive number that specifies the maximum miter length.
     * @return {self} self
     */
    setMiterLimit(miterLimit) {
        return this.setSetting("miterLimit", miterLimit);
    }

    /**
     * Set the shadowBlur to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_shadowblur.asp|w3schools}
     * @param {number} shadowBlur - The blur level for the shadow.
     * @return {self} self
     */
    setShadowBlur(shadowBlur) {
        return this.setSetting("shadowBlur", shadowBlur);
    }

    /**
     * Set the shadowColor to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_shadowcolor.asp|w3schools}
     * @param {string} shadowColor - color|gradient|pattern
     * @return {self} self
     */
    setShadowColor(shadowColor) {
        return this.setSetting("shadowColor", shadowColor);
    }

    /**
     * Set the shadowOffsetX to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_shadowoffsetx.asp|w3schools}
     * @param {number} shadowOffsetX - A positive or negative number that defines the horizontal distance of the shadow from the shape.
     * @return {self} self
     */
    setShadowOffsetX(shadowOffsetX) {
        return this.setSetting("shadowOffsetX", this.translateToPx(shadowOffsetX));
    }

    /**
     * Set the shadowOffsetY to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_shadowoffsety.asp|w3schools}
     * @param {number} shadowOffsetY - A positive or negative number that defines the vertical distance of the shadow from the shape.
     * @return {self} self
     */
    setShadowOffsetY(shadowOffsetY) {
        return this.setSetting("shadowOffsetY", this.translateToPx(shadowOffsetY));
    }

    /**
     * Set the strokeStyle to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_strokestyle.asp|w3schools}
     * @param {string} strokeStyle - color|gradient|pattern
     * @return {self} self
     */
    setStrokeStyle(strokeStyle) {
        return this.setSetting("strokeStyle", strokeStyle);
    }

    /**
     * Set the textAlign to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_textalign.asp|w3schools}
     * @param {string} textAlign - center|end|left|right|start
     * @return {self} self
     */
    setTextAlign(textAlign) {
        return this.setSetting("textAlign", textAlign);
    }

    /**
     * Set the textBaseline to be used for rendering.
     * @see {@link https://www.w3schools.com/tags/canvas_textbaseline.asp|w3schools}
     * @param {string} textBaseline - alphabetic|top|hanging|middle|ideographic|bottom
     * @return {self} self
     */
    setTextBaseline(textBaseline) {
        return this.setSetting("textBaseline", textBaseline);
    }
    
    /**
     * Add a callback for when the afterRender() in ran.
     * @param {function} callback
     * @returns {self} self
     */
    setCallback(callback){
        return this.setProp("callback", callback);
    }

    /**
     * Private Methods
     */

    /**
     * Get the canvas context
     * @access private
     * @return {CanvasRenderingContext2D} instance of 2d context of Canvas
     */
    context() {
        return this.Linen.context();
    }

    /**
     * Get the value in px of a 2d attribute.
     * @access private
     * @param {string} dimension - dimension name.
     * @return {mixed} value
     */
    getDimension(dimension) {
        return this.dimensions[dimension] || null;
    }

    /**
     * Get the value in px of a 2d attribute.
     * @access private
     * @param {string} dimension - dimension name.
     * @return {number} px
     */
    getDimensionPx(dimension) {
        const value = this.getDimension(dimension);
        if (value === null) {
            return 0;
        }
        const type = typeof value;
        switch (type) {
            case "string":
                return this.translateToPx(value, dimension);
            case "function":
                return value(this);
            case "number":
                return value;
            default:
                return 0;
        }
    }

    /**
     * Translate a string into a px value in the context of the given dimension.
     * @access private
     * @param {string} value - The dimension raw value.
     * @param {string} dimension - The dimension name.
     * @returns {number} dimension in px
     */

    translateToPx(value, dimension = "") {
        if (typeof value !== "string") {
            return value;
        }
        const pattern = /^([0-9.]+)([^0-9]+)/i;
        const parts = value.match(pattern);
        const number = parseFloat(parts[1]);
        const unit = parts[2].toLowerCase();
        switch (unit) {
            case "%":
                return Math.round(this.percentage(number, dimension));
            case "pt":
                return Math.round(number * (this.dpi() / 72));
            case "in":
                return Math.round(number * this.dpi());
            default:
                return number + "px";
    }
    }

    /**
     * Set the value of a given propery. Primarily for 2d attributes.
     * @access private
     * @param {number} px - px value to compare from.
     * @param {string} dimension - dimension name.
     * @return {number} px
     */
    percentage(number, dimension = "") {
        const ratio = number / 100;
        var aspect = 0;
        switch (dimension) {
            case "width":
            case "x":
            case "x2":
                aspect = this.context().canvas.width;
                break;
            case "height":
            case "y":
            case "y2":
                aspect = this.context().canvas.height;
                break;
            default:
                return 0;
        }
        return ratio * aspect;
    }

    /**
     * This method is executed before rendering the element. Do not call it directly.
     * @access private
     */
    render() {
        Object.assign(this.Linen.ctx, this.settings);
        this.setFont();
        this.context().resetTransform();
        if (typeof this.transform === "object") {
            const t = this.transform;
            this.context().transform(t.a, t.b, t.c, t.d, t.e, t.f);
        }
        return this;
    }

    /**
     * This method is executed after rendering the element. Do not call it directly.
     * @access private
     */
    afterRender() {
        if (this.clip) {
            this.context().clip();
        }
        this.runCallback();
        this.Linen.renderQueued();
        return this;
    }
    
    /**
     * This method is executed after rendering the element. Do not call it directly.
     * @access private
     */
    runCallback(){
        return this.callback();
    }
};