Criar um sabre de luz com o Polymer

captura de tela do sabre de luz

Resumo

Como usamos o Polymer para criar um sabre de luz WebGL de alto desempenho controlado para dispositivos móveis, modular e configurável. Reunimos alguns detalhes importantes do nosso projeto https://lightsaber.withgoogle.com/ para ajudar você a economizar tempo criando seu próprio projeto na próxima vez que encontrar um grupo de Stormtroopers irritados.

Informações gerais

Se você está se perguntando o que são Polymer ou WebComponents, achamos que seria melhor começar compartilhando um trecho de um projeto real. Confira um exemplo retirado da página de destino do nosso projeto https://lightsaber.withgoogle.com. Ele é um arquivo HTML normal, mas com um pouco de magia:

<!-- Element-->
<dom-module id="sw-page-landing">
    <!-- Template-->
    <template>
    <style>
        <!-- include elements/sw/pages/sw-page-landing/styles/sw-page-landing.css-->
    </style>
    <div class="centered content">
        <sw-ui-logo></sw-ui-logo>
        <div class="connection-url-wrapper">
        <sw-t key="landing.type" class="type"></sw-t>
        <div id="url" class="connection-url">.</div>
        <sw-ui-toast></sw-ui-toast>
        </div>
    </div>
    <div class="disclaimer epilepsy">
        <sw-t key="disclaimer.epilepsy" class="type"></sw-t>
    </div>
    <sw-ui-footer state="extended"></sw-ui-footer>
    </template>
    <!-- Polymer element script-->
    <script src="scripts/sw-page-landing.js"></script>
</dom-module>

Portanto, existem muitas opções hoje em dia quando se você quiser criar um aplicativo baseado em HTML5. APIs, frameworks, bibliotecas, mecanismos de jogos etc. Apesar de todas as opções, é difícil ter uma configuração que combine entre o controle sobre o alto desempenho de gráficos e a escalonabilidade e a estrutura modulares limpas. Descobrimos que a Polymer poderia nos ajudar a manter o projeto organizado e, ao mesmo tempo, permitir otimizações de desempenho de baixo nível, e elaboramos cuidadosamente a maneira como dividimos nosso projeto em componentes para aproveitar melhor os recursos da Polymer.

Modularidade com polímero

A Polymer (link em inglês) é uma biblioteca que oferece muito poder sobre como seu projeto é criado com elementos personalizados reutilizáveis. Com ele, é possível usar módulos autônomos e totalmente funcionais contidos em um único arquivo HTML. Eles contêm não apenas a estrutura (marcação HTML), mas também estilos e lógicas in-line.

Veja o exemplo abaixo:

<link rel="import" href="bower_components/polymer/polymer.html">

<dom-module id="picture-frame">
    <template>
    <!-- scoped CSS for this element -->
    <style>
        div {
        display: inline-block;
        background-color: #ccc;
        border-radius: 8px;
        padding: 4px;
        }
    </style>
    <div>
        <!-- any children are rendered here -->
        <content></content>
    </div>
    </template>

    <script>
    Polymer({
        is: "picture-frame",
    });
    </script>
</dom-module>

Porém, em um projeto maior, pode ser útil separar esses três componentes lógicos (HTML, CSS, JS) e apenas mesclá-los no momento da compilação. Então, uma coisa que fizemos foi dar a cada elemento do projeto sua própria pasta separada:

src/elements/
|-- elements.jade
`-- sw
    |-- debug
    |   |-- sw-debug
    |   |-- sw-debug-performance
    |   |-- sw-debug-version
    |   `-- sw-debug-webgl
    |-- experience
    |   |-- effects
    |   |-- sw-experience
    |   |-- sw-experience-controller
    |   |-- sw-experience-engine
    |   |-- sw-experience-input
    |   |-- sw-experience-model
    |   |-- sw-experience-postprocessor
    |   |-- sw-experience-renderer
    |   |-- sw-experience-state
    |   `-- sw-timer
    |-- input
    |   |-- sw-input-keyboard
    |   `-- sw-input-remote
    |-- pages
    |   |-- sw-page-calibration
    |   |-- sw-page-connection
    |   |-- sw-page-connection-error
    |   |-- sw-page-error
    |   |-- sw-page-experience
    |   `-- sw-page-landing
    |-- sw-app
    |   |-- bower.json
    |   |-- scripts
    |   |-- styles
    |   `-- sw-app.jade
    |-- system
    |   |-- sw-routing
    |   |-- sw-system
    |   |-- sw-system-audio
    |   |-- sw-system-config
    |   |-- sw-system-environment
    |   |-- sw-system-events
    |   |-- sw-system-remote
    |   |-- sw-system-social
    |   |-- sw-system-tracking
    |   |-- sw-system-version
    |   |-- sw-system-webrtc
    |   `-- sw-system-websocket
    |-- ui
    |   |-- experience
    |   |-- sw-preloader
    |   |-- sw-sound
    |   |-- sw-ui-button
    |   |-- sw-ui-calibration
    |   |-- sw-ui-disconnected
    |   |-- sw-ui-final
    |   |-- sw-ui-footer
    |   |-- sw-ui-help
    |   |-- sw-ui-language
    |   |-- sw-ui-logo
    |   |-- sw-ui-mask
    |   |-- sw-ui-menu
    |   |-- sw-ui-overlay
    |   |-- sw-ui-quality
    |   |-- sw-ui-select
    |   |-- sw-ui-toast
    |   |-- sw-ui-toggle-screen
    |   `-- sw-ui-volume
    `-- utils
        `-- sw-t

E a pasta de cada elemento tem a mesma estrutura interna com diretórios e arquivos separados para lógica (arquivos de café), estilos (arquivos scss) e modelo (arquivo jade).

Confira um exemplo de elemento sw-ui-logo:

sw-ui-logo/
|-- bower.json
|-- scripts
|   `-- sw-ui-logo.coffee
|-- styles
|   `-- sw-ui-logo.scss
`-- sw-ui-logo.jade

E se você observar o arquivo .jade:

// Element
dom-module(id='sw-ui-logo')

    // Template
    template
    style
        include elements/sw/ui/sw-ui-logo/styles/sw-ui-logo.css

    img(src='[[url]]')

    // Polymer element script
    script(src='scripts/sw-ui-logo.js')

É possível conferir como tudo é organizado de maneira limpa, incluindo estilos e lógica de arquivos separados. Para incluir estilos nos elementos Polymer, usamos a instrução include de Jade. Assim, temos o conteúdo real do arquivo CSS inline após a compilação. O elemento do script sw-ui-logo.js será executado durante a execução.

Dependências modulares com Bower

Normalmente, mantemos bibliotecas e outras dependências no nível do projeto. No entanto, na configuração acima, você vai perceber que um bower.json está na pasta do elemento: dependências no nível do elemento. A ideia por trás dessa abordagem é que, em uma situação em que você tem muitos elementos com dependências diferentes, podemos carregar apenas as dependências que são realmente usadas. E se você remover um elemento, não precisará se lembrar de remover a dependência dele, porque também terá removido o arquivo bower.json que declara essas dependências. Cada elemento carrega de maneira independente as dependências relacionadas a ele.

No entanto, para evitar a duplicação de dependências, também incluímos um arquivo .bowerrc na pasta de cada elemento. Isso informa ao bower onde armazenar as dependências para que possamos garantir que haja apenas uma no final no mesmo diretório:

{
    "directory" : "../../../../../bower_components"
}

Dessa forma, se vários elementos declararem THREE.js como uma dependência, depois que o bower a instalar para o primeiro elemento e começar a analisar o segundo, ele perceberá que essa dependência já está instalada e não vai fazer o download novamente ou duplicá-la. Da mesma forma, ele manterá esses arquivos de dependência enquanto houver pelo menos um elemento que ainda o defina no bower.json.

Um script bash encontra todos os arquivos bower.json na estrutura de elementos aninhados. Em seguida, ele insere esses diretórios um por um e executa bower install em cada um deles:

echo installing bower components...
modules=$(find /vagrant/app -type f -name "bower.json" -not -path "*node_modules*" -not -path "*bower_components*")
for module in $modules; do
    pushd $(dirname $module)
    bower install --allow-root -q
    popd
done

Modelo de novo elemento rápido

Toda vez que você quer criar um novo elemento pode demorar um pouco: gerar a estrutura de pastas e arquivos básica com os nomes corretos. Portanto, usamos o Slush para criar um gerador de elementos simples.

É possível chamar o script na linha de comando:

$ slush element path/to/your/element-name

E o novo elemento é criado, incluindo toda a estrutura e todo o conteúdo do arquivo.

Definimos modelos para os arquivos de elementos, por exemplo, o modelo de arquivo .jade tem esta aparência:

// Element
dom-module(id='<%= name %>')

    // Template
    template
    style
        include elements/<%= path %>/styles/<%= name %>.css

    span This is a '<%= name %>' element.

    // Polymer element script
    script(src='scripts/<%= name %>.js')

O gerador de slush substitui as variáveis por caminhos e nomes de elementos reais.

Como usar o Gulp para criar elementos

A Gulp mantém o processo de compilação sob controle. E, na nossa estrutura, para construir os elementos, precisamos que o Gulp siga estas etapas:

  1. Compile os arquivos .coffee dos elementos para .js.
  2. Compile os arquivos .scss dos elementos para .css.
  3. Compile os arquivos .jade dos elementos em .html, incorporando os arquivos .css.

Em mais detalhes:

Compilação dos arquivos .coffee dos elementos em .js.

gulp.task('elements-coffee', function () {
    return gulp.src(abs(config.paths.app + '/elements/**/*.coffee'))
    .pipe($.replaceTask({
        patterns: [{json: getVersionData()}]
    }))
    .pipe($.changed(abs(config.paths.static + '/elements'), {extension: '.js'}))
    .pipe($.coffeelint())
    .pipe($.coffeelint.reporter())
    .pipe($.sourcemaps.init())
    .pipe($.coffee({
    }))
    .on('error', gutil.log)
    .pipe($.sourcemaps.write())
    .pipe(gulp.dest(abs(config.paths.static + '/elements')));
});

Nas etapas 2 e 3, usamos gulp e um plug-in de bússola para compilar scss para .css e .jade para .html, de maneira semelhante à etapa 2 acima.

Inclusão de elementos polimétricos

Para incluir os elementos Polymer, usamos importações HTML.

<link rel="import" href="elements.html">

<!-- Polymer -->
<link rel="import" href="../bower_components/polymer/polymer.html">

<!-- Custom elements -->
<link rel="import" href="sw/sw-app/sw-app.html">
<link rel="import" href="sw/system/sw-system/sw-system.html">
<link rel="import" href="sw/system/sw-routing/sw-routing.html">
<link rel="import" href="sw/system/sw-system-version/sw-system-version.html">
<link rel="import" href="sw/system/sw-system-environment/sw-system-environment.html">
<link rel="import" href="sw/pages/sw-page-landing/sw-page-landing.html">
<link rel="import" href="sw/pages/sw-page-connection/sw-page-connection.html">
<link rel="import" href="sw/pages/sw-page-calibration/sw-page-calibration.html">
<link rel="import" href="sw/pages/sw-page-experience/sw-page-experience.html">
<link rel="import" href="sw/ui/sw-preloader/sw-preloader.html">
<link rel="import" href="sw/ui/sw-ui-overlay/sw-ui-overlay.html">
<link rel="import" href="sw/ui/sw-ui-button/sw-ui-button.html">
<link rel="import" href="sw/ui/sw-ui-menu/sw-ui-menu.html">

Otimização de elementos Polymer para produção

Um projeto grande pode ter muitos elementos da plataforma Polymer. Em nosso projeto, temos mais de 50. Se você considerar que cada elemento tem um arquivo .js separado e alguns têm bibliotecas referenciadas, ele se tornará mais de 100 arquivos separados. Isso significa que o navegador precisa fazer muitas solicitações, com perda de desempenho. De forma semelhante a um processo de concatenação e minificação que aplicamos a um build do Angular, "vulcanizamos" o projeto Polymer no final para produção.

Vulcanize é uma ferramenta Polymer que nivela a árvore de dependências em um único arquivo html, reduzindo o número de solicitações. Isso é especialmente ótimo para navegadores que não são compatíveis com componentes da Web de forma nativa.

CSP (Política de Segurança de Conteúdo) e Polymer

Ao desenvolver aplicativos da Web seguros, é preciso implementar a CSP. O CSP é um conjunto de regras que evitam ataques de scripting em vários sites (XSS), que são a execução de scripts de fontes não seguras ou a execução de scripts in-line de arquivos HTML.

Agora, o arquivo .html otimizado, concatenado e minificado gerado pelo Vulcanize tem todo o código JavaScript inline em um formato não compatível com a CSP. Para resolver isso, usamos uma ferramenta chamada Crisper.

A Crisper divide os scripts inline de um arquivo HTML e os coloca em um único arquivo JavaScript externo para conformidade com a CSP. Assim, transmitimos o arquivo HTML vulcanizado pelo Crisper e terminamos com dois arquivos: elements.html e elements.js. Dentro de elements.html, ele também cuida de carregar o elements.js gerado.

Estrutura lógica do aplicativo

No Polymer, os elementos podem ser qualquer coisa, desde um utilitário não visual até pequenos elementos de IU, independentes e reutilizáveis, como botões, até módulos maiores, como "páginas" e até mesmo compor aplicativos completos.

Uma estrutura lógica de nível superior do aplicativo.
Uma estrutura lógica de nível superior do nosso aplicativo representada por elementos da Polymer.

Pós-processamento com polímero e arquitetura pai-filho

Em qualquer pipeline de gráficos 3D, há sempre uma última etapa em que os efeitos são adicionados sobre a imagem inteira como uma espécie de sobreposição. Essa é a etapa de pós-processamento que envolve efeitos como brilhos, raios-divinos, profundidade de campo, bokeh, desfoques etc. Os efeitos são combinados e aplicados a diferentes elementos de acordo com a forma como a cena é criada. No THREE.js, é possível criar um sombreador personalizado para o pós-processamento em JavaScript. Se preferir, podemos fazer isso com o Polymer, graças à estrutura pai-filho.

Se observar o código HTML do elemento do nosso pós-processador:

<dom-module id="sw-experience-postprocessor">
    <!-- Template-->
    <template>
    <sw-experience-effect-bloom class="effect"></sw-experience-effect-bloom>
    <sw-experience-effect-dof class="effect"></sw-experience-effect-dof>
    <sw-experience-effect-vignette class="effect"></sw-experience-effect-vignette>
    </template>
    <!-- Polymer element script-->
    <script src="scripts/sw-experience-postprocessor.js"></script>
</dom-module>

Especificamos os efeitos como elementos Polymer aninhados em uma classe comum. Em seguida, em sw-experience-postprocessor.js, fazemos o seguinte:

effects = @querySelectorAll '.effect'
@composer.addPass effect.getPass() for effect in effects

Usamos o recurso HTML e o querySelectorAll do JavaScript para encontrar todos os efeitos aninhados como elementos HTML no pós-processador, na ordem em que foram especificados. Em seguida, iteramos sobre elas e as adicionamos ao compositor.

Agora, digamos que você queira remover o efeito DOF (profundidade de campo) e mudar a ordem dos efeitos de flores e vinheta. Tudo o que precisamos fazer é editar a definição do pós-processador para algo como:

<dom-module id="sw-experience-postprocessor">
    <!-- Template-->
    <template>
    <sw-experience-effect-vignette class="effect"></sw-experience-effect-vignette>
    <sw-experience-effect-bloom class="effect"></sw-experience-effect-bloom>
    </template>
    <!-- Polymer element script-->
    <script src="scripts/sw-experience-postprocessor.js"></script>
</dom-module>

e a cena será executada automaticamente, sem mudar uma linha sequer do código real.

Repetição de renderização e atualização no Polymer

Com o Polymer, também podemos realizar atualizações de renderização e mecanismo de maneira elegante. Criamos um elemento timer que usa requestAnimationFrame e calcula valores como tempo atual (t) e tempo delta - tempo decorrido desde o último frame (dt):

Polymer
    is: 'sw-timer'

    properties:
    t:
        type: Number
        value: 0
        readOnly: true
        notify: true
    dt:
        type: Number
        value: 0
        readOnly: true
        notify: true

    _isRunning: false
    _lastFrameTime: 0

    ready: ->
    @_isRunning = true
    @_update()

    _update: ->
    if !@_isRunning then return
    requestAnimationFrame => @_update()
    currentTime = @_getCurrentTime()
    @_setT currentTime
    @_setDt currentTime - @_lastFrameTime
    @_lastFrameTime = @_getCurrentTime()

    _getCurrentTime: ->
    if window.performance then performance.now() else new Date().getTime()

Em seguida, usamos a vinculação de dados para vincular as propriedades t e dt ao mecanismo (experience.jade):

sw-timer(
    t='{ % templatetag openvariable % }t}}',
    dt='{ % templatetag openvariable % }dt}}'
)

sw-experience-engine(
    t='[t]',
    dt='[dt]'
)

Além disso, detectamos as mudanças de t e dt no mecanismo e, sempre que os valores mudarem, a função _update será chamada:

Polymer
    is: 'sw-experience-engine'

    properties:
    t:
        type: Number

    dt:
        type: Number

    observers: [
    '_update(t)'
    ]

    _update: (t) ->
    dt = @dt
    @_physics.update dt, t
    @_renderer.render dt, t

No entanto, se você estiver com fome de QPS, remova a vinculação de dados do Polymer no loop de renderização para economizar alguns milissegundos necessários para notificar os elementos sobre as mudanças. Implementamos observadores personalizados da seguinte maneira:

sw-timer.coffee:

addUpdateListener: (listener) ->
    if @_updateListeners.indexOf(listener) == -1
    @_updateListeners.push listener
    return

removeUpdateListener: (listener) ->
    index = @_updateListeners.indexOf listener
    if index != -1
    @_updateListeners.splice index, 1
    return

_update: ->
    # ...
    for listener in @_updateListeners
        listener @dt, @t
    # ...

A função addUpdateListener aceita um callback e o salva na matriz de callbacks. Em seguida, na repetição de atualização, iteramos cada callback e os executamos com argumentos dt e t diretamente, ignorando a vinculação de dados ou o disparo de eventos. Quando um callback não precisa mais estar ativo, adicionamos uma função removeUpdateListener que permite remover um callback adicionado anteriormente.

Um sabre de luz em THREE.js

O THREE.js abstrai os detalhes de baixo nível do WebGL e nos permite focar no problema. E nosso problema é lutar contra os Stormtroopers e precisamos de uma arma. Então, vamos construir um sabre de luz.

A lâmina brilhante é o que diferencia um sabre de luz de qualquer arma de duas mãos antiga. Ele é composto principalmente de duas partes: a viga e a trilha, que é vista ao movê-la. Ele tem um formato de cilindro brilhante e uma trilha dinâmica que o acompanha à medida que o jogador se move.

A lâmina

A lâmina é composta por duas sub-pás. Um interno e outro externo. Ambas são malhas TRÊS.js com os respectivos materiais.

A lâmina interna

Para a lâmina interna, usamos um material personalizado com um sombreador personalizado. Pegamos uma linha criada por dois pontos e projetamos a linha entre esses dois pontos em um plano. Esse avião é basicamente o que você controla quando luta com seu dispositivo móvel. Ele dá uma sensação de profundidade e orientação ao sabre.

Para criar a sensação de um objeto brilhante redondo, observamos a distância de ponto ortogonal de qualquer ponto no plano a partir da linha principal que une os dois pontos A e B, conforme mostrado abaixo. Quanto mais próximo um ponto estiver do eixo principal, mais brilhante ele será.

Brilho da lâmina interna

A fonte abaixo mostra como computamos um vFactor para controlar a intensidade no sombreador de vértice e usá-lo para combinar com a cena no sombreador de fragmentos.

THREE.LaserShader = {

    uniforms: {
    "uPointA": {type: "v3", value: new THREE.Vector3(0, -1, 0)},
    "uPointB": {type: "v3", value: new THREE.Vector3(0, 1, 0)},
    "uColor": {type: "c", value: new THREE.Color(1, 0, 0)},
    "uMultiplier": {type: "f", value: 3.0},
    "uCoreColor": {type: "c", value: new THREE.Color(1, 1, 1)},
    "uCoreOpacity": {type: "f", value: 0.8},
    "uLowerBound": {type: "f", value: 0.4},
    "uUpperBound": {type: "f", value: 0.8},
    "uTransitionPower": {type: "f", value: 2},
    "uNearPlaneValue": {type: "f", value: -0.01}
    },

    vertexShader: [

    "uniform vec3 uPointA;",
    "uniform vec3 uPointB;",
    "uniform float uMultiplier;",
    "uniform float uNearPlaneValue;",
    "varying float vFactor;",

    "float getDistanceFromAB(vec2 a, vec2 b, vec2 p) {",

        "vec2 l = b - a;",
        "float l2 = dot( l, l );",
        "float t = dot( p - a, l ) / l2;",
        "if( t < 0.0 ) return distance( p, a );",
        "if( t > 1.0 ) return distance( p, b );",
        "vec2 projection = a + (l * t);",
        "return distance( p, projection );",

    "}",

    "vec3 getIntersection(vec4 a, vec4 b) {",

        "vec3 p = a.xyz;",
        "vec3 q = b.xyz;",
        "vec3 v = normalize( q - p );",
        "float t = ( uNearPlaneValue - p.z ) / v.z;",
        "return p + (v * t);",

    "}",

    "void main() {",

        "vec4 a = modelViewMatrix * vec4(uPointA, 1.0);",
        "vec4 b = modelViewMatrix * vec4(uPointB, 1.0);",
        "if(a.z > uNearPlaneValue) a.xyz = getIntersection(a, b);",
        "if(b.z > uNearPlaneValue) b.xyz = getIntersection(a, b);",
        "a = projectionMatrix * a; a /= a.w;",
        "b = projectionMatrix * b; b /= b.w;",
        "vec4 p = projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
        "gl_Position = p;",
        "p /= p.w;",
        "float d = getDistanceFromAB(a.xy, b.xy, p.xy) * gl_Position.z;",
        "vFactor = 1.0 - clamp(uMultiplier * d, 0.0, 1.0);",

    "}"

    ].join( "\n" ),

    fragmentShader: [

    "uniform vec3 uColor;",
    "uniform vec3 uCoreColor;",
    "uniform float uCoreOpacity;",
    "uniform float uLowerBound;",
    "uniform float uUpperBound;",
    "uniform float uTransitionPower;",
    "varying float vFactor;",

    "void main() {",

        "vec4 col = vec4(uColor, vFactor);",
        "float factor = smoothstep(uLowerBound, uUpperBound, vFactor);",
        "factor = pow(factor, uTransitionPower);",
        "vec4 coreCol = vec4(uCoreColor, uCoreOpacity);",
        "vec4 finalCol = mix(col, coreCol, factor);",
        "gl_FragColor = finalCol;",

    "}"

    ].join( "\n" )

};

O brilho da lâmina externa

Para o brilho externo, renderizamos em um renderbuffer separado e usamos um efeito de flor pós-processamento e mesclamos com a imagem final para ter o brilho desejado. A imagem abaixo mostra as três regiões diferentes que você precisa ter para ter um sabre decente. Ou seja, o núcleo branco, o brilho azul-médio e o brilho externo.

Lâmina externa

Trilha com sabre de luz

O rastro do sabre de luz é a chave para o efeito completo como o original visto na série Star Wars. Criamos a trilha com um leque de triângulos gerados dinamicamente com base no movimento do sabre de luz. Esses ventiladores são transmitidos ao pós-processador para ainda mais melhorias visuais. Para criar a geometria em leque, temos um segmento de linha e, com base na transformação anterior e atual, geramos um novo triângulo na malha, descartando a parte da cauda após um determinado comprimento.

Trilha do sabre de luz à esquerda
Trilha do sabre de luz à direita

Depois de criar uma malha, atribuímos um material simples a ela e o transmitimos ao pós-processador para criar um efeito suave. Usamos o mesmo efeito de flor que aplicamos ao brilho da lâmina externa e conseguimos um rastro suave, como você pode ver:

A trilha completa

Brilho ao redor da trilha

Para que a peça final ficasse completa, tivemos que lidar com o brilho ao redor da trilha real, que poderia ser criado de várias maneiras. Nossa solução que não vamos entrar em detalhes aqui, por motivos de desempenho, foi criar um sombreador personalizado para esse buffer que cria uma borda suave em torno de um limite do buffer de renderização. Em seguida, combinamos essa saída na renderização final. Aqui é possível ver o brilho ao redor da trilha:

Trilha iluminada

Conclusão

O Polymer é uma biblioteca e um conceito avançados (assim como os WebComponents são em geral). Cabe apenas a você escolher o que fazer com ele. Pode ser qualquer coisa, desde um simples botão de interface até um aplicativo WebGL de tamanho normal. Nos capítulos anteriores, mostramos algumas dicas e sugestões para usar o Polymer de forma eficiente na produção e como estruturar módulos mais complexos que também funcionam bem. Também mostramos como encontrar um sabre de luz bonito em WebGL. Se você combinar tudo isso, lembre-se de vulcanizar seus elementos do Polymer antes de implantá-los no servidor de produção. Se você não se esquecer de usar a Crisper para manter a conformidade com a CSP, talvez seja necessário ajudar.

Jogo