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 ayudarte a ahorrar tiempo cuando crees el tuyo la próxima vez que te encuentres con un grupo de 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. Por lo tanto, una de las acciones que realizamos fue darle a cada elemento del proyecto su propia carpeta independiente:
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 una duplicación de dependencias, también 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 de 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
Plantilla rápida de elemento nuevo
Lleva un poco de tiempo cada vez que quieres crear un elemento nuevo: 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')));
});
En los pasos 2 y 3, usamos gulp y un complemento de Compass para compilar scss
en .css
y .jade
en .html
, en un enfoque similar al paso 2 anterior.
Cómo incluir elementos de Polymer
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 el CSP. El CSP es un conjunto de reglas que evita los ataques de secuencias de comandos entre sitios (XSS): la ejecución de secuencias de comandos desde fuentes no seguras o la ejecución de secuencias de comandos intercaladas desde archivos HTML.
Ahora, el archivo .html
único, optimizado, concatenado y reducido que genera Vulcanize tiene todo el código de JavaScript intercalado en un formato que no cumple con la CSP. Para esto, usamos una herramienta llamada
Crisper:
Crisper divide las secuencias de comandos intercaladas de un archivo HTML y las coloca en un solo archivo JavaScript externo para garantizar 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 el 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 en 3D, siempre hay un último paso en el que se agregan efectos 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, iteramos sobre ellos y los agregamos al compositor.
Ahora, supongamos que queremos quitar el efecto de DOF (profundidad de campo) y cambiar el orden de los efectos de destello 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 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 nuestro 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 necesitas más FPS, te recomendamos que quites la vinculación de datos de Polymer en el bucle de renderización para ahorrar unos pocos milisegundos necesarios para notificar a los elementos 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 por cada devolución de llamada y
la ejecutamos con los argumentos dt
y t
directamente, sin pasar por la vinculación de datos ni
el disparo de eventos. Una vez que una devolución de llamada ya no debe estar activa, agregamos una función removeUpdateListener
que te permite quitar una devolución de llamada agregada 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, y necesitamos un arma. Así que, hagamos un sable láser.
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. Este plano es básicamente lo que controlas cuando te enfrentas con tu dispositivo móvil, le da la sensación de profundidad y orientación al 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 del eje principal, más brillante será.
En la siguiente fuente, se muestra cómo calculamos un vFactor
para controlar la intensidad en el sombreador de vértices y, luego, usarlo para 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 exterior, renderizamos en un búfer de renderización independiente y usamos un efecto de resplandor de procesamiento posterior y combinamos con la imagen final para obtener el brillo deseado. La imagen a continuación muestra las tres regiones diferentes que necesitar si quieres un sable decente. Es decir, el núcleo blanco, el brillo azulado central y el brillo 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 este resultado en la renderización final. Aquí puedes ver el brillo que rodea el rastro:
Conclusión
Polymer es una biblioteca y un concepto potentes (al igual que WebComponents está en general). Depende de ti lo que hagas con ella. 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 sable láser de aspecto atractivo 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!