Source: controllers/CanvasController.js

/**
 * @module module:CanvasController
 */


import Dot from "../models/Dot.js";
import Drafter from "../views/Drafter.js";
import { _math } from "../helper/_math.js";

export default class CanvasController {
    /**
     * @constructor
     * @param {HTMLElement} target Elemento canvas
     */
    constructor(target, rules = {}) {

        /**
         * Defines animation behavior
         * @type {{screenMargin: number, dotPopulation: number, lineMaxLenght: number}}
         * @private
         */
        this._rules = CanvasController.defaultRules();
        this.setRules(rules);

        this.canvas = {
            element: target,
            width: target.clientWidth,
            height: target.clientHeight
        };

        /**
         * Responsible for drawing on canvas
         * @type {Drafter}
         * @private
         */
        this.drafter = new Drafter(this.canvas.element);

        /**
         * List of dots
         * @type {Dot[]}
         */
        this.dots = [];

        this.init();
    }

    print() {
        var image = this.canvas.element.toDataURL("image/png", 1.0);

        const link = document.createElement('a');
        link.href = image;
        link.target = '_blank';
        link.click();
    }

    static defaultRules() {
        return {
            screenMargin: 5,
            dotBornMode: 'randomChild',
            dotDirectionRange: [0, 359],
            dotColor: "#00ff00",
            dotFade: true,
            dotPopulation: 100,
            dotSize: 2.5,
            dotSpeed: 1,
            dotSpeedVariation: 0.5,
            lineColor: "#00ff00",
            lineFade: true,
            lineMaxLenght: 0,
        };
    }

    init() {
        this.populate(this._rules.dotPopulation);
    }

    /**
     * Atualiza as regras da animação
     * @param {*} newRules 
     */
    setRules(newRules) {
        for (const rule in newRules) {
            this._rules[rule] = newRules[rule];
        }
    }

    get rules() {
        return Object(this._rules);
    }

    /**
     * Atualiza o tamanho do canvas
     * @param {Number} _width nova largura 
     * @param {Number} _height nova altura
     */
    display(_width, _height) {
        this.canvas.width = _width;
        this.canvas.height = _height;

        this.canvas.element.width = _width;
        this.canvas.element.height = _height;
    }

    /**
     * Cria uma nova instancia Dot e adiciona a lista
     * @param {Number} _x 
     * @param {Number} _y 
     * @param {Number} _dir 
     * @param {Number} _speed 
     * @param {"Born"|"Idle"} _state 
     * @param {Number} _hp 
     */
    _createDot(_x = undefined, _y = undefined, _dir = undefined, _speed = undefined, _state = undefined, _hp = undefined) {
        const x = _x || _math.numberBetween(-this._rules.screenMargin, this.canvas.width + this._rules.screenMargin);
        const y = _y || _math.numberBetween(-this._rules.screenMargin, this.canvas.height + this._rules.screenMargin);
        const dir = _dir != undefined ? _dir : _math.numberBetween(...this._rules.dotDirectionRange);
        const speed = _speed || this._rules.dotSpeed + _math.numberBetween(this._rules.dotSpeedVariation, -this._rules.dotSpeedVariation);
        const state = _state || 'idle';
        const hp = state === 'born' ? 0 : 100;

        const dot = new Dot(x, y, dir, speed, state, hp);
        dot.rate = _math.numberBetween(0.1, 1);
        this.dots.push(dot);
    }

    /**
     * Cria n dots com state 'born'
     * @param {Number} n Quantidade de Dots que devem ser criados
     */
    rePopulate(n = 1) {
        for (let i = 1; i <= n; i++) {

            switch (this._rules.dotBornMode) {
                case 'anywere':
                    this._createDot(undefined, undefined, undefined, undefined, 'born');
                    break;
                case 'child':
                    const parent = this.dots[0];
                    this._createDot(parent.x, parent.y, undefined, undefined, 'born');
                    break;
                case 'randomChild':
                    const randomParent = this.dots[Math.floor(Math.random() * this.dots.length)];
                    this._createDot(randomParent.x, randomParent.y, undefined, undefined, 'born');
                    break;
                case 'top':
                    this._createDot(_math.numberBetween(-this._rules.screenMargin, this.canvas.width + this._rules.screenMargin), 0 - this._rules.screenMargin, undefined, undefined, 'born');
                    break;
                default:
                    this._createDot(undefined, undefined, undefined, undefined, 'born');


            }
        }
    }

    /**
     * Cria n dots
     * @param {Number} n 
     */
    populate(n = 1) {
        for (let i = 1; i <= n; i++) {
            this._createDot();
        }
    }

    /**
     * Atualiza o estado e posição de cada dot
     * @returns {Number} Nova população
     */
    updateDots() {
        this.dots.forEach((dot, idx) => {
            dot.update(this.canvas.width, this.canvas.height);

            const isInsideWindow = this._isPointInsideRect(
                dot,
                [-this._rules.screenMargin, -this._rules.screenMargin],
                [this.canvas.width + this._rules.screenMargin, this.canvas.height + this._rules.screenMargin]);

            if (!isInsideWindow)
                delete this.dots[idx];

            if (dot.hp === 0 && dot.state === 'dying')
                dot.die();
        });

        this.dots = this.dots.filter(d => d);
        return this.dots.length;
    }

    /**
     * Muda o state de dots aleatorios para 'dying'
     * @param {Number} n Quantidade de dots para matar
     */
    kill(n) {
        for (let i = 0; i < n; i++) {
            this.dots[i].rate = _math.numberBetween(0.01, 2);
            this.dots[i].die();
        }
    }

    /**
     * Desenha dots visiveis
     * @returns {Number} Quantidade de dots visiveis
     */
    renderDots() {
        this.drafter.brush.clearRect(0, 0, this.canvas.width, this.canvas.height);

        const visibleDots = this.dots.filter(dot =>
            dot.x >= - 5 && dot.x <= this.canvas.width + 5 &&
            dot.y >= - 5 && dot.y <= this.canvas.height + 5
        );

        visibleDots.forEach(dot => {
            let opacity = 'ff';

            if (this._rules.dotFade) {
                opacity = Math.round(255 * dot.hp / 100)
                    .toString(16).padStart(2, '0');
            }

            const color = this._rules.dotColor + opacity;
            this.drafter.dot(dot.x, dot.y, this._rules.dotSize, color);
        });

        return visibleDots.length;
    }

    /**
     * Desenha linhas visiveis
     */
    renderLines() {
        const dotList = [].concat(this.dots);
        const maxDistance = 255 * this._rules.lineMaxLenght / 100;
        const screenWalls = [
            [{ x: 0, y: 0 }, { x: this.canvas.width, y: 0 }],
            [{ x: 0, y: this.canvas.height }, { x: this.canvas.width, y: this.canvas.height }],
            [{ x: 0, y: 0 }, { x: 0, y: this.canvas.height }],
            [{ x: this.canvas.width, y: 0 }, { x: this.canvas.width, y: this.canvas.height }],
        ];

        // evita linhas repetidas
        for (let i = 0; i < this.dots.length; i++) {
            const dotStart = dotList.splice(0, 1)[0];

            for (let j = i + 1; j < this.dots.length; j++) {
                const dotEnd = this.dots[j];

                const distance = dotStart._distanceTo(dotEnd);

                if (distance < maxDistance) {
                    const lineWillBeVisible = screenWalls.map(([w1, w2]) => {

                        if ((this._isPointInsideRect(dotStart, [0, 0], [this.canvas.width, this.canvas.height])) ||
                            this._isPointInsideRect(dotEnd, [0, 0], [this.canvas.width, this.canvas.height]))
                            return true;

                        return this._checkIfLinesCross(w1, w2, dotStart, dotEnd);
                    }).reduce((acc, cur) => acc || cur);


                    if (lineWillBeVisible) {
                        let opacity = 'ff';
                        if (this._rules.lineFade) {
                            const baseLineOpacity = Math.abs((distance / maxDistance) - 1) * 255;
                            const medDotsOpacity = ((dotStart.hp + dotEnd.hp) / 2) / 100;
                            opacity = Number(Math.round(baseLineOpacity * medDotsOpacity))
                                .toString(16).padStart(2, '0');
                        }

                        const color = this._rules.lineColor + opacity;

                        this.drafter.line(dotStart, dotEnd, color);
                    }
                }
            }
        }
    }

    /**
     * Atalho para atualizar e renderizar tudo
     * @returns {Number} população de dots
     */
    step() {
        const population = this.updateDots();
        this.renderDots();
        this.renderLines();
        return population;
    }

    _checkIfLinesCross(p1, p2, q1, q2) {
        // code from 
        const a = p1.x;
        const b = p1.y;
        const c = p2.x;
        const d = p2.y;
        const p = q1.x;
        const q = q1.y;
        const r = q2.x;
        const s = q2.y;

        var det, gamma, lambda;
        det = (c - a) * (s - q) - (r - p) * (d - b);
        if (det === 0) {
            return false;
        } else {
            lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
            gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
            return ((0 < lambda && lambda < 1) && (0 < gamma && gamma < 1));
        }
    }

    _isPointInsideRect({ x, y }, [x1, y1], [x2, y2]) {
        return (x > x1 && x < x2) && (y > y1 && y < y2);
    }

}