
Resumen
Cómo usamos Polymer para crear una interfaz web con WebGL de alto rendimiento y controlada Un sable de luz es modular y configurable. Revisamos algunos detalles clave de nuestro proyecto: https://lightsaber.withgoogle.com/ para que ahorres tiempo cuando crees el tuyo la próxima vez que te encuentres con un los Stormtroopers enojados.
Descripción general
Si te preguntas qué Polymer o WebComponents pensamos que sería mejor empezar compartiendo un extracto de un proyecto de trabajo real. Aquí hay una muestra tomada de la página de destino de nuestro proyecto https://lightsaber.withgoogle.com. Es un archivo HTML normal, pero con funciones especiales:
<!-- 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>
Así que hoy en día existen muchas opciones cuando quieres crear una aplicación basada en HTML5. APIs, frameworks, bibliotecas, motores de juego, etcétera A pesar de todas las elecciones, es difícil conseguir un equipo adecuado. entre el control del alto rendimiento de los gráficos y el diseño la estructura y la escalabilidad. Descubrimos que Polymer podía ayudarnos a mantener la el proyecto organizado y, al mismo tiempo, permitir un rendimiento de bajo nivel optimizaciones, y creamos con cuidado la forma en que desglosamos nuestro proyecto en componentes para aprovechar mejor las capacidades de Polymer.
Modularidad con Polymer
Polymer es una biblioteca que permite mucha potencia sobre cómo se crea tu proyecto a partir de elementos personalizados reutilizables. Permite usar módulos independientes y completamente funcionales contenidos en un único archivo HTML. No solo incluyen la estructura (lenguaje de marcado HTML), sino que también estilos intercalados y lógica.
Observa el siguiente ejemplo:
<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>
Pero, en un proyecto más grande, puede ser útil separar estas tres lógicas (HTML, CSS y JS) y combinarlos solo en el tiempo de compilación. Una cosa que hicimos fue darle a cada elemento del proyecto su propia carpeta:
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
Y la carpeta de cada elemento tiene la misma estructura interna con directorios y archivos para lógica (archivos de café), estilos (archivos scss) y plantilla (archivo jade).
Este es un elemento sw-ui-logo
de ejemplo:
sw-ui-logo/
|-- bower.json
|-- scripts
| `-- sw-ui-logo.coffee
|-- styles
| `-- sw-ui-logo.scss
`-- sw-ui-logo.jade
Y si observas el archivo .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')
Puedes ver cómo se organizan los elementos de forma clara si incluyes estilos
y lógica de archivos separados. Para incluir nuestros estilos en Polymer
usamos la sentencia include
de Jade, por lo que tenemos CSS intercalados reales.
el contenido del archivo después de la compilación. El elemento de la secuencia de comandos sw-ui-logo.js
se ejecutan en el tiempo de ejecución.
Dependencias modulares con Bower
Normalmente, mantenemos las bibliotecas y otras dependencias en el nivel del proyecto.
Sin embargo, en la configuración anterior, verás un bower.json
en el
carpeta del elemento: dependencias a nivel de elemento. La idea de este enfoque
es que cuando tienes muchos elementos con diferentes
dependencias, podemos asegurarnos de cargar solo aquellas que son
que usó realmente. Si quitas un elemento, no es necesario que recuerdes
quita su dependencia porque también quitaste el archivo bower.json
que declare estas dependencias. Cada elemento carga de forma independiente el
las dependencias relacionadas con ella.
Sin embargo, para evitar la duplicación de dependencias, incluimos un archivo .bowerrc
.
en la carpeta de cada elemento. Esta le indica a Bogo
dónde almacenar contenido
dependencias de modo que podamos asegurarnos de que solo haya una al final en la misma
directorio:
{
"directory" : "../../../../../bower_components"
}
De esta manera, si varios elementos declaran THREE.js
como dependencia, una vez
Boberer lo instala para el primer elemento y comienza a analizar el segundo.
se dará cuenta de que esta dependencia ya está instalada y no
volver a descargarlo o duplicarlo. Del mismo modo, mantendrá esa dependencia
siempre que haya al menos un elemento que lo defina
su bower.json
.
Una secuencia de comandos bash encuentra todos los archivos bower.json
en la estructura de elementos anidados.
Luego, ingresa a estos directorios uno por uno y ejecuta bower install
en
cada uno de ellos:
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
Nueva plantilla de elemento rápido
Cada vez que deseas crear un nuevo elemento lleva algo de tiempo: generar la carpeta y la estructura de archivos básica con los nombres correctos. Así que usamos Slush para escribir un generador de elementos simple
Puedes llamar a la secuencia de comandos desde la línea de comandos:
$ slush element path/to/your/element-name
Y se crea el nuevo elemento, incluida toda la estructura del archivo y el contenido.
Definimos plantillas para los archivos de elementos, p.ej., la plantilla de archivo .jade
se ve de la siguiente manera:
// 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')
El generador de barra negra reemplaza las variables por rutas y nombres reales de los elementos.
Cómo usar Gulp para compilar elementos
Gulp mantiene el proceso de compilación bajo control. Y en nuestra estructura, para construir los elementos que necesitamos Gulp deben seguir estos pasos:
- Compila los elementos
.coffee
archivos a.js
- Compila los elementos
.scss
archivos a.css
- Compila los elementos
.jade
en.html
, incorporando los archivos.css
.
En más detalle:
Compilar los elementos .coffee
archivos a .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 los pasos 2 y 3, usamos gulp y un complemento de brújula para compilar scss
en
.css
y .jade
a .html
, en un enfoque similar al de 2 anteriores.
Incluye elementos polímeros
Para incluir realmente los elementos de Polymer usamos importaciones 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">
Cómo optimizar los elementos de Polymer para la producción
Un proyecto grande puede terminar teniendo muchos elementos de Polymer. En nuestra
tenemos más de cincuenta. Si consideras que cada elemento tiene una
un archivo .js
separado y algunos tienen bibliotecas referenciadas, pasa a ser más de
100 archivos separados Esto implica muchas solicitudes
que debe hacer el navegador
con la pérdida de rendimiento. Del mismo modo que en los procesos de concatenación y reducción,
a una compilación de Angular, “vulcanizamos” el proyecto Polymer en
para la producción.
Vulcanize es una herramienta Polymer aplana el árbol de dependencias en un solo archivo html, lo que reduce el la cantidad de solicitudes. Esto es especialmente útil para los navegadores que no admiten componentes web de forma nativa.
CSP (Política de Seguridad del Contenido) y Polymer
Cuando desarrollas aplicaciones web seguras, debes implementar CSP. CSP es un conjunto de reglas que evitan los ataques de secuencia de comandos entre sitios (XSS): la ejecución de secuencias de comandos de fuentes no seguras o de secuencias de comandos intercaladas desde archivos HTML.
Ahora se generó el archivo .html
optimizado, concatenado y reducido
de Vulcanize tiene todo el código JavaScript intercalado en una CSP que no cumple con los requisitos
de un conjunto de datos
tengan un formato común. Para esto, usamos una herramienta llamada
Crisper:
La herramienta divide secuencias de comandos integradas a partir de un archivo HTML y las ubica en un solo
un archivo JavaScript externo
para el cumplimiento de la CSP. Así que pasamos la capa vulcanizada
a través de Crisper y terminarán con dos archivos: elements.html
y
elements.js
Dentro de elements.html
, también se encarga de cargar
elements.js
generado.
Estructura lógica de la aplicación
En Polymer, los elementos pueden ser desde una utilidad no visual hasta una pequeña elementos de IU independientes y reutilizables (como botones) para módulos más grandes, como "páginas" e incluso redactar aplicaciones completas.

Procesamiento posterior con Polymer y arquitectura superior-secundaria
En cualquier canalización de gráficos 3D, siempre hay un último paso donde los efectos se agregan sobre toda la imagen como una especie de superposición. Este es el e incluye efectos como resplandores, rayos dioses, profundidad de campo, bokeh, desenfoque, etc. Los efectos se combinan y se aplican diferentes elementos según cómo se construye la escena. En THREE.js, podrías crear un sombreador personalizado para el procesamiento posterior en JavaScript. podemos hacerlo con Polymer gracias a su estructura superior-secundario.
Si observas el código HTML de los elementos del postprocesador:
<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 los efectos como elementos Polymer anidados en una clase común. Luego,
En sw-experience-postprocessor.js
, hacemos lo siguiente:
effects = @querySelectorAll '.effect'
@composer.addPass effect.getPass() for effect in effects
Usamos la función de HTML y querySelectorAll
de JavaScript para encontrar todos
efectos anidados como elementos HTML dentro del procesador de entradas, en el orden
en las que se especificaron. Luego, los iteramos y los agregamos al compositor.
Ahora, digamos que queremos quitar el efecto DOF (profundidad de campo) y cambiar el orden de los efectos de floración y viñeta. Todo lo que tenemos que hacer es editar la definición del postprocesador a algo como lo siguiente:
<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>
y la escena se ejecutará sin cambiar ni una sola línea de código real.
Bucle de renderización y de actualización en Polymer
Con Polymer también podemos abordar la renderización y las actualizaciones del motor con elegancia.
Creamos un elemento timer
que usa requestAnimationFrame
y calcula
como la hora actual (t
) y la hora delta, como el tiempo transcurrido
último fotograma (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()
Luego, usamos la vinculación de datos para vincular las propiedades t
y dt
a nuestra
motor (experience.jade
):
sw-timer(
t='{ % templatetag openvariable % }t}}',
dt='{ % templatetag openvariable % }dt}}'
)
sw-experience-engine(
t='[t]',
dt='[dt]'
)
Además, escuchamos los cambios de t
y dt
en el motor y cada vez que se
cambian los valores, se llamará a la función _update
:
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
Sin embargo, si te gusta tener FPS, puedes quitar los datos de Polymer vinculación en bucle de renderización para ahorrar algunos milisegundos necesarios para notificar sobre los cambios. Implementamos los observadores personalizados de la siguiente manera:
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
# ...
La función addUpdateListener
acepta una devolución de llamada y la guarda en su
de devolución de llamada. Luego, en el bucle de actualización, iteramos en cada devolución de llamada
lo ejecutamos con los argumentos dt
y t
directamente, sin pasar por la vinculación de datos o
la activación del evento. Cuando una devolución de llamada ya no está activa, agregamos
Función removeUpdateListener
que te permite quitar una devolución de llamada que se agregó anteriormente.
Un sable de luz en THREE.js
THREE.js abstrae los detalles de bajo nivel de WebGL y nos permite enfocarnos en el problema. Y nuestro problema es luchar contra los soldados de asalto. Necesitamos un con un arma de fuego. Así que construyamos un sable de luz.
La hoja brillante es lo que diferencia a un sable de luz de cualquier de dos manos. Está compuesto principalmente por dos partes: la viga y el sendero. que se ve cuando se mueve. La creamos con una forma de cilindro brillante y un rastro dinámico que lo sigue a medida que el jugador se mueve.
La hoja
La hoja se compone de dos hojas secundarias. Una interna y otra externa. Ambas son mallas de THREE.js con sus materiales respectivos.
El cuchillo interior
Para la hoja interior, usamos un material personalizado con un sombreador personalizado. Mié tomar una línea creada por dos puntos y proyectar la línea entre estos dos puntos en un plano. Básicamente, este plano es lo que tú controlas cuando pelea con tu dispositivo móvil, te da una sensación de profundidad y orientación hacia el sable.
Para crear la sensación de un objeto redondo brillante, miramos la a una distancia ortogonal de cualquier punto del plano con respecto una línea que une los dos puntos A y B, como se muestra a continuación. Cuanto más cerca está un punto de en el eje principal, más brillante será.

En la siguiente fuente, se muestra cómo calculamos una vFactor
para controlar la intensidad.
en el sombreador de vértices para usarlo a fin de combinarlo con la escena en el
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" )
};
El brillo de la hoja exterior
Para el brillo externo, renderizamos en un búfer de renderización independiente y usamos un efecto flor después del procesamiento y combinar con la imagen final para obtener el brillo deseado. La imagen a continuación muestra las tres regiones diferentes que necesitas si quieres un sable decente. A saber, el núcleo blanco, el centro un resplandor azulado y uno exterior.

Sendero con sable de luz
El rastro del sable de luz es la clave para lograr el efecto completo como se ve en la versión original de la serie Star Wars. Hicimos el sendero con un abanico de triángulos generados dinámicamente, según el movimiento del sable de luz. Estos fans son entonces pasan al postprocesador para mejorar la visión. Para crear el la geometría del ventilador, tenemos un segmento de línea y, según su transformación previa, y la transformación actual, generamos un nuevo triángulo en la malla, dejando de la cola después de cierta longitud.


Una vez que tenemos una malla, le asignamos un material simple y lo pasamos al posprocesador para crear un efecto suave. Usamos el mismo efecto "bloom" que aplicamos al brillo de la hoja exterior y obtenemos un rastro suave, como puedes ver:

Brilla por el camino
Para completar la pieza final, tuvimos que controlar el brillo alrededor del espacio ruta, que se pueden crear de varias maneras. La solución que creamos por motivos de rendimiento, crear un perfil para este búfer que crea un borde suave alrededor de una abrazadera de la búfer de renderización. Luego, combinamos esta salida en la renderización final. Aquí puedes verás el brillo que rodea el sendero:

Conclusión
Polymer es una biblioteca y un concepto potentes (al igual que WebComponents está en general). Solo tú decides lo que crees con él. Puede ser cualquier cosa, desde un simple botón de IU a una aplicación WebGL de tamaño completo. En los capítulos anteriores te mostramos algunas sugerencias y trucos para usar Polymer en producción y cómo estructurar módulos más complejos que también realizan en la nube. También te mostramos cómo lograr un bonito sable de luz en WebGL. Así que, si combinas todo eso, recuerda vulcanizar los elementos de Polymer antes de implementarlos en el servidor de producción y, si no te olvidas de usar Crisper si quieres seguir cumpliendo con los CSP, ¡que la fuerza te acompañe!
