Criar um sabre de luz com o Polymer

Captura de tela com sabre de luz

Resumo

Como usamos o Polymer para criar um sabre de luz WebGL de alto desempenho controlado por dispositivos móveis que é modular e configurável. Analisamos alguns detalhes importantes do nosso projeto https://lightsaber.withgoogle.com/ para ajudar você a economizar tempo ao criar o seu próprio da próxima vez que se deparar com um pacote de Stormtroopers bravos.

Visão geral

Se você está se perguntando o que são Polymer ou WebComponents, achamos que é melhor começar compartilhando um trecho de um projeto de trabalho real. Aqui está um exemplo retirado da página inicial do nosso projeto https://lightsaber.withgoogle.com. Está um arquivo HTML normal, mas com uma mágica dentro:

<!-- 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, há muitas opções disponíveis hoje em dia um aplicativo baseado em HTML5. APIs, frameworks, bibliotecas, mecanismos de jogos etc. Apesar de todas as opções, é difícil conseguir uma boa combinação entre o controle sobre o alto desempenho dos gráficos e o design modular limpo estrutura e escalonabilidade. Descobrimos que a Polymer poderia nos ajudar a manter o projeto organizado e, ao mesmo tempo, permitir um desempenho de baixo nível para otimizações, e nós cuidadosamente elaboramos a maneira como dividimos nosso projeto em componentes para aproveitar melhor os recursos da plataforma.

Modularidade com o Polymer

A Polymer é uma biblioteca que permite muito poder sobre como seu projeto é criado a partir de elementos personalizados reutilizáveis. Ela permite usar módulos independentes e totalmente funcionais contidos em um um único arquivo HTML. Eles contêm não apenas a estrutura (marcação HTML), mas também estilos e lógica inline.

Confira 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>

Mas, em um projeto maior, pode ser útil separar esses três conceitos (HTML, CSS, JS) e mesclá-los somente durante a compilaçã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 valores separados diretórios e arquivos de 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')

Você pode ver como as coisas estão organizadas de forma limpa, incluindo estilos e lógica de arquivos separados. Para incluir nossos estilos em nossa plataforma elementos, usamos a instrução include de Jade, então temos um CSS in-line real. o conteúdo do arquivo após a compilação. O elemento de script sw-ui-logo.js será executado no momento da execução.

Dependências modulares com o Bower

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

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

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

Dessa forma, se vários elementos declararem THREE.js como uma dependência, uma vez Boke o instala para o primeiro elemento e começa a analisar o segundo, ele perceberá que a dependência já está instalada e não faça o download novamente ou duplique-o. Da mesma forma, ele vai 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 entra nesses diretórios um por um e executa bower install em para 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

Cada vez que você quer criar um novo elemento demora um pouco: gerar e a estrutura básica de arquivos com os nomes corretos. Então, usamos o Slush para escrever um gerador de elementos simples.

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

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

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

Definimos modelos para os arquivos de elementos, por exemplo, o modelo de arquivo .jade é a seguinte:

// 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

O gulp mantém o processo de build sob controle. E em nossa estrutura, para construir elementos necessários para que o Gulp siga estas etapas:

  1. compilar o arquivo .coffee arquivos para .js
  2. Compilar os arquivos .scss dos elementos para .css
  3. compilar o arquivo Arquivos .jade para .html, incorporando os arquivos .css.

Mais detalhes:

Compilar os elementos .coffee arquivos para .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')));
});

Para as etapas 2 e 3, usamos o gulp e um plug-in de bússola para compilar o scss para .css e .jade para .html, em uma abordagem semelhante ao item 2 acima.

Como incluir elementos poliméricos

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 do polímero para produção

Um projeto grande pode acabar com muitos elementos Polymer. Em nossa projeto, temos mais de cinquenta. Se você considerar que cada elemento tem um um arquivo .js separado e algumas com bibliotecas referenciadas, ele se torna mais de 100 arquivos separados. Isso significa que há muitas solicitações que o navegador precisa fazer, com perda de desempenho. Assim como em um processo de concatenação e minificação, seriam aplicadas a uma versão do Angular, "vulcanizamos" o projeto do final para produção.

O Vulcanize é uma ferramenta da plataforma Polymer que nivela a árvore de dependências em um único arquivo html, reduzindo o de solicitações. Isso é ótimo para navegadores que não têm oferecem suporte nativo a componentes da Web.

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

Ao desenvolver aplicativos da Web seguros, você precisa implementar o CSP. CSP é um conjunto de regras que evitam ataques de scripting em vários locais (XSS): a execução de scripts de fontes não seguras ou de scripts inline de arquivos HTML.

Agora o arquivo .html otimizado, concatenado e minificado gerado da Vulcanize tem todo o código JavaScript inline em uma versão não compatível com a CSP . Para resolver isso, usamos uma ferramenta chamada Crisper (link em inglês).

O Crisper divide scripts inline de um arquivo HTML e os coloca em um único arquivo JavaScript externo para compliance com o CSP. Passamos a camada vulcanizada arquivo HTML pela Crisper e acabará com dois arquivos: elements.html e elements.js. Dentro de elements.html, ele também carrega o gerou elements.js.

Estrutura lógica do aplicativo

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

Uma estrutura lógica de nível superior do aplicativo
Uma estrutura lógica de nível superior do aplicativo, representada por Elementos poliméricos.

Pós-processamento com a arquitetura pai-filho e a plataforma Polymer

Em qualquer pipeline de gráficos 3D, há sempre uma última etapa em que os efeitos são adicionados em cima de toda a imagem como uma espécie de sobreposição. Esta é a etapa de pós-processamento e envolve efeitos como brilhos, raios-deus, profundidade de campo, bokeh, desfoques etc. Os efeitos são combinados e aplicados a diferentes elementos de acordo com como o cenário é construído. Em THREE.js, criar um shader personalizado para o pós-processamento em JavaScript ou podemos fazer isso com o Polymer, graças à sua estrutura pai-filho.

Se você 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 a querySelectorAll do JavaScript para encontrar todos os efeitos aninhados como elementos HTML no pós-processador, na ordem em que foram especificados. Em seguida, fazemos iterações neles e as adicionamos ao composer.

Agora, digamos que queremos remover o efeito DOF (profundidade de campo) e alterar 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 sem alterar nenhuma linha do código real.

Loop de renderização e loop de atualização no Polymer

Com o Polymer, também podemos abordar a renderização e as atualizações de mecanismos de maneira elegante. Criamos um elemento timer que usa requestAnimationFrame e calcula valores como hora atual (t) e tempo delta - tempo decorrido desde ú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 à nossa mecanismo (experience.jade):

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

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

Ouvimos mudanças de t e dt no mecanismo e sempre que o 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

Se você preferir QPS, convém remover os dados do Polymer vinculação no loop de renderização para economizar alguns milissegundos necessários para notificar sobre as mudanças. Implementamos observadores personalizados da seguinte forma:

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 de callbacks do sistema. Em seguida, no loop de atualização, iteramos sobre cada callback e o executamos com os argumentos dt e t diretamente, ignorando a vinculação de dados ou o disparo de eventos. Quando uma chamada de retorno não está mais ativa, adicionamos um 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 para resolver o problema. Nosso problema é combater Stormtroopers, e precisamos arma. Vamos criar um sabre de luz.

A lâmina brilhante é o que diferencia um sabre de luz de qualquer arma de duas mãos. Ela é formada principalmente por duas partes: a barra e a trilha. que aparece ao movê-lo. Nós o construímos com um formato de cilindro brilhante e uma trilha dinâmica que o segue à medida que o jogador se move.

The Blade

A lâmina é composta por duas sub-lâminas. Interno e externo. Ambas são malhas THREE.js com os respectivos materiais.

Lâmina interna

Para a lâmina interna, usamos um material personalizado com um shader personalizado. Qa pegar uma linha criada por dois pontos e projetar a linha entre esses dois em um plano. Esse plano é basicamente o que você controla quando lutam com o celular, isso dá a sensação de profundidade e orientação ao sabre.

Para criar a sensação de um objeto brilhante redondo, olhamos para o distância do ponto ortogonal de qualquer ponto do plano em relação ao ponto principal juntando os dois pontos A e B, conforme mostrado abaixo. Quanto mais próximo um ponto estiver de maior o eixo principal.

Brilho interno da lâmina

A fonte abaixo mostra como calculamos um vFactor para controlar a intensidade no sombreador de vértice para usá-lo na mesclagem com a cena sombreador de fragmento.

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 buffer de renderização separado e usamos uma o efeito de flor de pós-processamento e mistura com a imagem final para obter 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 meio azul-azul e o brilho externo.

Lâmina externa

Trilha do sabre de luz

O rastro do sabre de luz é fundamental para o efeito completo, como o original visto na série "Star Wars". Fizemos a trilha com um fã de triângulos gerados dinamicamente com base no movimento do sabre de luz. Esses fãs se tornam passados para o pós-processador para aprimoramento visual. Para criar o geometria de torcedor, temos um segmento de linha e, com base em sua transformação anterior, e a transformação atual, geramos um novo triângulo na malha, descartando parte da cauda depois de um certo comprimento.

Trilha com sabre de luz à esquerda
Trilha com sabre de luz à direita

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

A trilha completa

Brilhe ao redor da trilha

Para a última peça ficar completa, tivemos que lidar com o brilho em torno do que pode ser criado de várias maneiras. Nossa solução que nós não entramos em detalhes aqui, já que por motivos de desempenho, era criar um para esse buffer que cria uma borda suave ao redor de um fecho do o renderbuffer. Em seguida, combinamos essa saída na renderização final. Aqui é possível veja o brilho ao redor da trilha:

Trilha iluminada

Conclusão

O Polymer é uma biblioteca e um conceito poderosos (assim como os WebComponents em geral). O que você cria com ele depende de você. Pode ser qualquer coisa, um botão de IU simples para um aplicativo WebGL de tamanho normal. Nos capítulos anteriores, mostramos algumas dicas e truques sobre como usar eficientemente o Polymer em produção e como estruturar módulos mais complexos que também muito bem. Também mostramos como criar um sabre de luz bonito no WebGL. Se você combinar tudo isso, lembre-se de vulcanizar seus elementos Polymer antes de implantar no servidor de produção e, se não se esquecer de usar a Crisper se você quiser manter a conformidade com a CSP, que a força esteja com você!

Jogo