Por un puñado de web-components

Alejandro Arroyo Duque

Adesis
Desarrollador front-end
Aficionado a los videojuegos

¿Qué es Polymer?

Es una librería que nos permite crear elementos personalizados para la web

Conocimiento previo de Polymer

Conocimiento previo de Polymer by FZapata

Índice

  • Organización del proyecto
  • Componentes
  • CSS
    • z-index
    • Animaciones
  • Vulcanizar

bower.json

package.json

app

gulpfile.js

elements

fonts

images

index.html

main.css

elements.html

my-element

audio

images

my-element.html

styles.css

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>MASTER GUN</title>
    <link rel="stylesheet" type="text/css" href="main.css">

    <!-- build:js lib.min.js -->
    <script
      src="./bower_components/webcomponentsjs/webcomponents-lite.js"></script>
    <!-- endbuild -->

    <!-- will be replaced with elements/elements.vulcanized.html -->
    <link rel="import" href="elements/elements.html">
    <!-- endreplace-->
  </head>

  <body>
    <mg-view></mg-view>
  </body>

</html>

elements.html

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

<dom-module id="mg-view">

  <template>
    <p>Plantilla del componente</p>
  </template>

  <style>
    p {color: red;}
  </style>

  <script>
    Polymer({
      is: "mg-view"
    });
  </script>

</dom-module>

Elementos

  • Vista
  • Escenario
  • Cargador
    • Audio
    • Balas
  • Munición disponible
  • Enemigos
  • Munición
  • Barra de menú
  • Puntos

Jerarquía de los elementos

mg-view

mg-menu-bar

mg-apache

mg-cowboy

mg-girl

mg-slot

mg-charger-audio

mg-score

12

mg-scenario

mg-charger

mg-munition

mg-bullet

mg-view.html

  • Define el layout de la aplicación
    • flex-box
    • aspect-ratio del escenario a 16:9
  • Coloca el menú superior
  • Coloca el escenario
  • Coloca el cargador

 

Revisemos el código

mg-menu-bar.html

  • Barra de menú superior
  • Contiene a mg-score
  • Título de juego
  • Botón de reset

 

Revisemos el código

mg-score.html

  • Muestra los puntos
  • Expone métodos para modificar los puntos

 

Revisemos el código

Cuando se crea un componente sus propiedades son accesibles desde otros elementos

document.querySelector('mg-score')

mg-scenario.html

  • Pantalla donde se interactua con el jugador
  • Define el nivel del juego
  • Ambienta el juego
    • Fondos
    • Fogonazos
  • Coloca a los enemigos
  • Los disparos del jugador son gestionados en el scenario

 

Revisemos el código

Cuando se pretende que un atributo nativo de html sea dinámico irá acompañado de $

<div class$="{{flashComputedClass}}"></div>

Por defecto el data binding no propaga el cambio de una variable desde el hijo al padre

shoots: {
    type: Number,
    value: 0,
    notify: true,
    observer: 'shootBullet'
},

Hay veces que necesitamos hacer algo con otro web-component y necesitamos que esté disponible

document.addEventListener('WebComponentsReady', (function(e) {

  this.mgMunition = document.querySelector('mg-munition');

}).bind(this));

Para comunicar al cargador que se ha efectuado un disparo, se emite un evento

this.fire('shoot-bullet-event');

Esto evita pasar la variable shoots al cargador y tener que vigilar su valor

enemigos

  • Comportamiento común
  • Estilos comunes
  • Pero cada enemigo tiene sus particularidades
    • Aspecto
    • Puntos

 

Revisemos el código

mg-bullet.html

  • Aunque el comportamiento es parecido a de los enemigos no se hace uso de él

 

Revisemos el código

mg-charger.html

  • Contiene 6 huecos para balas
  • Con cada disparo en el scenario se consume una bala del cargador
  • Si se han acabado las balas no se puede disparar

 

Revisemos el código

dom-repeat nos permite crear una plantilla que se bindea con un array. Crea una instancia por elemento y expone las  propiedades: item e index.

<template is="dom-repeat" items="{{slots}}" index-as="i">
  <mg-slot status="{{item.status}}" number$="{{i}}"></mg-slot>
</template>

Dado un array de items ¿Cómo notificamos a Polymer que un item ha cambiado?

slots: {
  type: Array,
  value: function() {
    return [
      {status: 'full'}, {status: 'full'}, {status: 'full'},
      {status: 'full'}, {status: 'full'}, {status: 'full'}
    ];
  },
  notify: true
}

...

this.notifyPath('slots.'+ index +'.status', 'empty');

mg-slot.html

  • Con cada disparo, el cargador actualiza el estado de sus huecos
  • Se aplica una clase para visualizar si el hueco del cargador está ocupado o no

 

Revisemos el código

mg-charger-audio.html

  • Importa una colección de audios de disparo
  • Expone unos métodos para reproducir los audios
  • El cargador por cada intento de disparo utiliza el método correspondiente de este componente

 

Revisemos el código

mg-munition.html

  • Muestra la munición disponible que no está dentro del cargador
  • Expone métodos para administrar la munición
  • Con cada recarga:
    • Se restan 6 unidades
    • El contador de disparos del escenario pasa a 0 y esto produce el efecto de recarga del cargador

 

Revisemos el código

Jugando con el z-index

Todos los clicks son recibidos por la capa con el z-index más alto

Hay que encontrar el método de que sea posible hacer click sobre los enemigos

pointer-events: none;
CSS

Animaciones del cargador

.charger {
  transition: transform .2s ease;
}
CSS

Animaciones del cargador

.charger.move-0 {transform:rotate(0deg);}

.charger.move-1 {transform:rotate(-60deg);}

.charger.move-2 {transform:rotate(-120deg);}

.charger.move-3 {transform:rotate(-180deg);}

.charger.move-4 {transform:rotate(-240deg);}

.charger.move-5 {transform:rotate(-300deg);}
CSS
if (this.mgScenario.getShoots() < 6) {
  this.computedClass = 'charger move-' + this.mgScenario.getShoots();
}

Animaciones de enemigos

La primera animación tiene lugar cuando un enemigo aparece en la pantalla

Si el jugador no ha disparado a este enemigo

Estructura de un enemigo

.enemy-container {
  position: relative;
  height: 180px; /* double of 90px */
  width: 70px;
  overflow: hidden;
  transition: z-index linear 0.4s;
}
.enemy {
  position: relative;
  height: 100%;
  width: 70px;
  opacity: 0;
  overflow: hidden;
  background-size: 70px 90px;
  background-repeat: no-repeat;
  transform-style: preserve-3d;
}
<div class$="{{computedContainer}}">
  <div class$="{{computedEnemy}}" >
    <div class="head" on-click="shootOnHead"></div>
    <div class="body" on-click="shootOnBody"></div>
  </div>
</div>
CSS
CSS

Definiendo animaciones



@keyframes show-slide-animation {
  0% {
    opacity: 0;
    top: 40px;
  }

  10% {
    opacity: 1;
  }

  100% {
    opacity: 1;
    top: 0px;
  }
}


@keyframes hide-slide-animation {
  0% {
    opacity: 1;
    top: 0px;
  }

  90% {
    opacity: 1;
  }

  100% {
    opacity: 0;
    top: 70px;
  }
}
CSS
CSS

Creando las clases


.enemy.show {
  animation-name: show-slide-animation;
  animation-timing-function: cubic-bezier(0,0.63,0.58,1);
  animation-duration: 0.4s;
  animation-fill-mode: forwards;
}

.enemy.hide {
  animation-name: hide-slide-animation;
  animation-timing-function: cubic-bezier(0,0.63,0.58,1);
  animation-duration: 0.3s;
  animation-fill-mode: forwards;
}
CSS

Animaciones de enemigos

Cuando un enemigo es disparado se reproduce una animación 3d

Vista frontal 

Esquema lateral

70px

90px

Recordemos la estructura

.enemy-container {
  position: relative;
  height: 180px; /* double of 90px */
  width: 70px;
  overflow: hidden;
  transition: z-index linear 0.4s;
}
.enemy {
  position: relative;
  height: 100%;
  width: 70px;
  opacity: 0;
  overflow: hidden;
  background-size: 70px 90px;
  background-repeat: no-repeat;
  transform-style: preserve-3d;
}
<div class$="{{computedContainer}}">
  <div class$="{{computedEnemy}}" >
    <div class="head" on-click="shootOnHead"></div>
    <div class="body" on-click="shootOnBody"></div>
  </div>
</div>
CSS
CSS

Definiendo la animación

@keyframes kill-flip-animation {
  0% {
    opacity: 1;
    transform: rotateX(0deg);
  }

  15% {
    transform: rotateX(0deg);
  }

  95% {
    opacity: 1;
  }

  100% {
    opacity: 0;
    transform: rotateX(90deg);
  }
}
CSS

Creando la clase


.enemy.kill {
  animation-name: kill-flip-animation;
  animation-timing-function: cubic-bezier(0.57, 1.88, 0.21, 0.57);
  animation-duration: 0.6s;
  animation-fill-mode: forwards;
}
CSS

Notas sobre CSS en Polymer

  • El selector :host da estilos al nodo del elemento
  • Las hojas CSS de un elemento se importan dentro de la etiqueta <dom-module>
  • Los estilos también pueden ser un elemento Polymer
  • Puedes dar estilos a un componente desde fuera de la forma: html /deep/ mg-cowboy {...}
  • Mucho más que conocer en Polymer

Vulcanizar

El objetivo es conseguir un solo HTML con todos los elementos de Polymer

gulp.task('vulcanize', function() {
  return gulp.src('./app/elements/elements.html')
    .pipe(vulcanize({
        inlineScripts: true,
        inlineCss: true,
        stripExcludes: false
    }))
    .pipe(minifyInline())
    .pipe(rename('elements/elements.vulcanized.html'))
    .pipe(gulp.dest('./dist'));
});

Sobre el vulcanizado

  • Si algun elemento tiene una dependencia mal enlazada el vulcanizado no funcionará
  • Se puede optar por vulcanizar el index.hmtl y ahorrar la petición a elements.vulcanized.html, sin embargo queda un poco "sucio"
Made with Slides.com