Desenvolvimento

26 abr, 2019

TypeScript 3.4: o que veio de novidade?

Publicidade

É isso mesmo, galera, o nosso TS está na versão 3.4, e hoje eu irei apresentar uma de suas novidades no momento do build: a flag incremental.

Incremental flag

Quem já está trabalhando com .ts há algum tempo, sabe como ele funciona, mas aqueles que estão tendo o seu primeiro contato com ele neste artigo, saibam que o TypeScript é um superset de JavaScript. Em outras palavras, nós desenvolvemos em TypeScript e transpilamos para JavaScript.

Abaixo você tem uma imagem demonstrando esse processo:

TypeScript transpiler

O processo é simples. Nós criamos um arquivo .ts e executamos o comando tsc para transpilar esse arquivo para .js.

Mas como funciona esse processo em uma estrutura com mais arquivos? Precisamos ficar fazendo esse processo toda vez para cada arquivo?

Não. Nós podemos executar o comando tsc — watch para que o compilador do TypeScript fique monitorando e verificando a nossa estrutura de x em x times, fazendo o transpile para .js.

Até aqui, tranquilo – não apresentei nenhuma novidade para aqueles que já estão utilizando o TypeScript no dia a dia.

Agora, avançando para nova versão 3.4 do TS, e indo para o tema desse artigo, vamos conhecer a flag – incremental.

Essa nova flag salva as ultimas alterações do compilador, fazendo com que ele seja mais rápido.

Para ficar mais claro, vamos criar um exemplo prático. O primeiro passo será atualizar o TS para nova versão. Para isso, abra um terminal no seu computador e execute o comando abaixo:

npm install -g typescript@latest

Agora execute o comando tsc -v para verificar se você está com a ultima versão instalada. Abaixo você tem uma imagem demonstrando esse passo no meu computador:

TypeScript 3.4 novidades

Com a versão atualizada do TS, crie um novo diretório no seu computador com o nome typescript-news, e em seguida crie um novo arquivo chamado tsincremental.ts e atualize ele com o trecho de código abaixo:

class Vector {

    constructor(public x: number, public y: number, public z: number) { }

    static times(k: number, v: Vector) {
        return new Vector(k * v.x, k * v.y, k * v.z);
    }

    static minus(v1: Vector, v2: Vector) {
        return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z);
    }

    static plus(v1: Vector, v2: Vector) {
        return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
    }

    static dot(v1: Vector, v2: Vector) {
        return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
    }

    static mag(v: Vector) {
        return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
    }

    static norm(v: Vector) {
        let mag = Vector.mag(v);
        let div = (mag === 0) ? Infinity : 1.0 / mag;
        return Vector.times(div, v);
    }

    static cross(v1: Vector, v2: Vector) {
        return new Vector(v1.y * v2.z - v1.z * v2.y,
                          v1.z * v2.x - v1.x * v2.z,
                          v1.x * v2.y - v1.y * v2.x);
    }

}

class Color {

    constructor(public r: number, public g: number, public b: number) { }

    static scale(k: number, v: Color) {
        return new Color(k * v.r, k * v.g, k * v.b);
    }

    static plus(v1: Color, v2: Color) {
        return new Color(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b);
    }

    static times(v1: Color, v2: Color) {
        return new Color(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b);
    }

    static white = new Color(1.0, 1.0, 1.0);
    static grey = new Color(0.5, 0.5, 0.5);
    static black = new Color(0.0, 0.0, 0.0);
    static background = Color.black;
    static defaultColor = Color.black;

    static toDrawingColor(c: Color) {
        let legalize = d => d > 1 ? 1 : d;
        return {
            r: Math.floor(legalize(c.r) * 255),
            g: Math.floor(legalize(c.g) * 255),
            b: Math.floor(legalize(c.b) * 255)
        }
    }

}

class Camera {

    forward: Vector;
    right: Vector;
    up: Vector;

    constructor(public pos: Vector, lookAt: Vector) {
        let down = new Vector(0.0, -1.0, 0.0);
        this.forward = Vector.norm(Vector.minus(lookAt, this.pos));
        this.right = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, down)));
        this.up = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, this.right)));
    }

}

interface Ray {
    start: Vector;
    dir: Vector;
}

interface Intersection {
    thing: Thing;
    ray: Ray;
    dist: number;
}

interface Surface {
    diffuse: (pos: Vector) => Color;
    specular: (pos: Vector) => Color;
    reflect: (pos: Vector) => number;
    roughness: number;
}

interface Thing {
    intersect: (ray: Ray) => Intersection;
    normal: (pos: Vector) => Vector;
    surface: Surface;
}

interface Light {
    pos: Vector;
    color: Color;
}

interface Scene {
    things: Thing[];
    lights: Light[];
    camera: Camera;
}

class Sphere implements Thing {

    radius2: number;

    constructor(public center: Vector, radius: number, public surface: Surface) {
        this.radius2 = radius * radius;
    }

    normal(pos: Vector): Vector {
        return Vector.norm(Vector.minus(pos, this.center));
    }

    intersect(ray: Ray) {
        let eo = Vector.minus(this.center, ray.start);
        let v = Vector.dot(eo, ray.dir);
        let dist = 0;
        if (v >= 0) {
            let disc = this.radius2 - (Vector.dot(eo, eo) - v * v);
            if (disc >= 0) {
                dist = v - Math.sqrt(disc);
            }
        }
        if (dist === 0) {
            return null;
        } else {
            return { thing: this, ray: ray, dist: dist };
        }
    }

}

class Plane implements Thing {

    normal: (pos: Vector) => Vector;
    intersect: (ray: Ray) => Intersection;

    constructor(norm: Vector, offset: number, public surface: Surface) {
        this.normal = function(pos: Vector) { return norm; }
        this.intersect = function(ray: Ray): Intersection {
            let denom = Vector.dot(norm, ray.dir);

            if (denom > 0) {
                return null;
            }
            else {
                let dist = (Vector.dot(norm, ray.start) + offset) / (-denom);
                return { thing: this, ray: ray, dist: dist };
            }
        }
    }

}

namespace Surfaces {

    export let shiny: Surface = {
        diffuse: function(pos) { return Color.white; },
        specular: function(pos) { return Color.grey; },
        reflect: function(pos) { return 0.7; },
        roughness: 250
    }

    export let checkerboard: Surface = {
        diffuse: function(pos) {
            if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) {
                return Color.white;
            } else {
                return Color.black;
            }
        },
        specular: function(pos) { return Color.white; },
        reflect: function(pos) {
            if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) {
                return 0.1;
            } else {
                return 0.7;
            }
        },
        roughness: 150
    }

}


class RayTracer {

    private maxDepth = 5;

    private intersections(ray: Ray, scene: Scene) {
        let closest = +Infinity;
        let closestInter: Intersection = undefined;
        for (let i in scene.things) {
            let inter = scene.things[i].intersect(ray);
            if (inter != null && inter.dist < closest) {
                closestInter = inter;
                closest = inter.dist;
            }
        }
        return closestInter;
    }

    private testRay(ray: Ray, scene: Scene) {
        let isect = this.intersections(ray, scene);
        if (isect != null) {
            return isect.dist;
        } else {
            return undefined;
        }
    }

    private traceRay(ray: Ray, scene: Scene, depth: number): Color {
        let isect = this.intersections(ray, scene);
        if (isect === undefined) {
            return Color.background;
        } else {
            return this.shade(isect, scene, depth);
        }
    }

    private shade(isect: Intersection, scene: Scene, depth: number) {
        let d = isect.ray.dir;
        let pos = Vector.plus(Vector.times(isect.dist, d), isect.ray.start);
        let normal = isect.thing.normal(pos);
        let reflectDir = Vector.minus(d, Vector.times(2, Vector.times(Vector.dot(normal, d), normal)));
        let naturalColor = Color.plus(Color.background,
                                      this.getNaturalColor(isect.thing, pos, normal, reflectDir, scene));
        let reflectedColor = (depth >= this.maxDepth) ? Color.grey : this.getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth);
        return Color.plus(naturalColor, reflectedColor);
    }

    private getReflectionColor(thing: Thing, pos: Vector, normal: Vector, rd: Vector, scene: Scene, depth: number) {
        return Color.scale(thing.surface.reflect(pos), this.traceRay({ start: pos, dir: rd }, scene, depth + 1));
    }

    private getNaturalColor(thing: Thing, pos: Vector, norm: Vector, rd: Vector, scene: Scene) {
        let addLight = (col, light) => {
            let ldis = Vector.minus(light.pos, pos);
            let livec = Vector.norm(ldis);
            let neatIsect = this.testRay({ start: pos, dir: livec }, scene);
            let isInShadow = (neatIsect === undefined) ? false : (neatIsect <= Vector.mag(ldis));
            if (isInShadow) {
                return col;
            }
            else {
                let illum = Vector.dot(livec, norm);
                let lcolor = (illum > 0) ? Color.scale(illum, light.color)
                                          : Color.defaultColor;
                let specular = Vector.dot(livec, Vector.norm(rd));
                let scolor = (specular > 0) ? Color.scale(Math.pow(specular, thing.surface.roughness), light.color)
                                          : Color.defaultColor;
                return Color.plus(col, Color.plus(Color.times(thing.surface.diffuse(pos), lcolor),
                                                  Color.times(thing.surface.specular(pos), scolor)));
            }
        }
        return scene.lights.reduce(addLight, Color.defaultColor);
    }

    render(scene, ctx, screenWidth, screenHeight) {
        let getPoint = (x, y, camera) => {
            let recenterX = x => (x - (screenWidth / 2.0)) / 2.0 / screenWidth;
            let recenterY = y => -(y - (screenHeight / 2.0)) / 2.0 / screenHeight;
            return Vector.norm(Vector.plus(camera.forward, Vector.plus(Vector.times(recenterX(x), camera.right), Vector.times(recenterY(y), camera.up))));
        }
        for (let y = 0; y < screenHeight; y++) {
            for (let x = 0; x < screenWidth; x++) {
                let color = this.traceRay({ start: scene.camera.pos, dir: getPoint(x, y, scene.camera) }, scene, 0);
                let c = Color.toDrawingColor(color);
                ctx.fillStyle = "rgb(" + String(c.r) + ", " + String(c.g) + ", " + String(c.b) + ")";
                ctx.fillRect(x, y, x + 1, y + 1);
            }
        }
    }

}


function defaultScene(): Scene {
    return {
        things: [new Plane(new Vector(0.0, 1.0, 0.0), 0.0, Surfaces.checkerboard),
                 new Sphere(new Vector(0.0, 1.0, -0.25), 1.0, Surfaces.shiny),
                 new Sphere(new Vector(-1.0, 0.5, 1.5), 0.5, Surfaces.shiny)],
        lights: [{ pos: new Vector(-2.0, 2.5, 0.0), color: new Color(0.49, 0.07, 0.07) },
                 { pos: new Vector(1.5, 2.5, 1.5), color: new Color(0.07, 0.07, 0.49) },
                 { pos: new Vector(1.5, 2.5, -1.5), color: new Color(0.07, 0.49, 0.071) },
                 { pos: new Vector(0.0, 3.5, 0.0), color: new Color(0.21, 0.21, 0.35) }],
        camera: new Camera(new Vector(3.0, 2.0, 4.0), new Vector(-1.0, 0.5, 0.0))
    };
}

function exec() {
    let canv = document.createElement("canvas");
    canv.width = 256;
    canv.height = 256;
    document.body.appendChild(canv);
    let ctx = canv.getContext("2d");
    let rayTracer = new RayTracer();
    return rayTracer.render(defaultScene(), ctx, 256, 256);
}

exec();

Um trecho gigante, né? Peguei ele para que vermos a diferença do compiler utilizando o incremental.

Esse trecho de código foi retirado do site: typescriptlang.

Agora crie um novo arquivo chamado tsconfig.json, e em seguida atualize-o com o trecho de código abaixo:

{
    "compilerOptions": { 
        "outDir": "./dist"
    },
    "include": ["./*"]
}

Esse trecho de código está com uma configuração mínima para trabalharmos com o TS. Ele percorrerá o nosso código por arquivos .ts e gerará o .js dentro do diretório ./dist.

Agora execute o comando tsc -w -diagnostics no seu terminal. Esse comando fará o transpile e passará o diagnóstico desse processo. Abaixo você tem uma imagem demonstrando esse passo:

  • Note que o tempo total foi 1.68s

Agora atualize o arquivo tsconfig.json com o trecho de código abaixo e execute o mesmo comando do passo anterior novamente: tsc -w -diagnostics.

{
    "compilerOptions": { 
        "outDir": "./dist",
        "incremental": true,
    },
    "include": ["./*"]
}

Resultado:

O tempo total para executar esse passo foi 0.29s, um tempo melhor que o do passo anterior.

Agora vem aquela pergunta: como o compilador conseguiu melhorar esse tempo?

Ao adicionar a flag incremental = true no arquivo tsconfig.json, ele irá gerar um novo arquivo na nossa estrutura, chamado tsconfig.tsbuildinfo, onde ele armazenará as informações do último transpile do projeto.

Esse processo ajuda o compilador a ganhar mais velocidade no momento do type-check.

Abaixo você tem as duas imagens geradas nos testes dos passos anteriores. A da direita foi o nosso primeiro teste sem a flah incremental = true e a da esquerda com a flag = true:

TypeScritp diagnostic

A ideia desse artigo era demonstrar uma das novas funcionalidades da nova versão do TypeScript. Espero que tenham gostado e até a próxima, pessoal!