Podsumowanie
Wykorzystaliśmy Polymer do stworzenia wydajnego, sterowanego mobilnym urządzeniem Lightsaber, który jest modułowy i konfigurowalny. Omawiamy najważniejsze szczegóły naszego projektu https://lightsaber.withgoogle.com/, aby pomóc Ci zaoszczędzić czas podczas tworzenia własnego projektu następnym razem, gdy natrafisz na gromadę wściekłych szturmowców.
Opis
Jeśli zastanawiasz się, co to jest Polymer lub WebKomponent, najlepiej zacząć od udostępnienia wyodrębnienia z rzeczywistego projektu. Oto próbka ze strony docelowej naszego projektu https://lightsaber.withgoogle.com. Jest to zwykły plik HTML, ale ma w sobie coś magicznego:
<!-- 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>
Obecnie istnieje wiele możliwości tworzenia aplikacji w formacie HTML5. Interfejsy API, platformy, biblioteki, silniki gier itp. Mimo wielu możliwości trudno jest stworzyć konfigurację, która łączy kontrolę nad wysoką wydajnością grafiki z przejrzystą strukturą modułową i skalowalnością. Okazało się, że Polymer pomaga nam utrzymać porządek w projekcie, a jednocześnie pozwala na optymalizowanie wydajności na niskim poziomie. Dlatego starannie podzieliliśmy projekt na komponenty, aby jak najlepiej wykorzystać jego możliwości.
Modułowość z użyciem polimeru
Polymer to biblioteka, która daje większą kontrolę nad sposobem tworzenia projektu na podstawie elementów niestandardowych wielokrotnego użytku. Umożliwia używanie samodzielnych, w pełni funkcjonalnych modułów zawartych w jednym pliku HTML. Obejmują nie tylko strukturę (znaczniki HTML), ale także wbudowane style i logikę.
Przeanalizuj ten przykład:
<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>
Przy większym projekcie warto podzielić te 3 komponenty logiczne (HTML, CSS i JS) i połączyć je tylko podczas kompilacji. Utworzyliśmy więc osobny folder dla każdego elementu w projekcie.
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
Folder każdego elementu ma taką samą strukturę wewnętrzną z osobnymi katalogami i plikami logicznymi (pliki kawy, style (pliki scss) i szablony (plik jade).
Oto przykładowy element sw-ui-logo
:
sw-ui-logo/
|-- bower.json
|-- scripts
| `-- sw-ui-logo.coffee
|-- styles
| `-- sw-ui-logo.scss
`-- sw-ui-logo.jade
A jeśli spojrzysz na plik .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')
Możesz zobaczyć uporządkowanie elementów, dodając style i logikę z oddzielnych plików. Aby umieścić nasze style w elementach Polymer, używamy instrukcji include
Jade, więc po kompilacji mamy wbudowaną zawartość pliku CSS. Element skryptu sw-ui-logo.js
jest uruchamiany w czasie działania.
Zależności modułowe z Bower
Zwykle biblioteki i inne zależności przechowujemy na poziomie projektu.
Jednak w konfiguracji powyżej zobaczysz bower.json
w folderze tego elementu: zależności na poziomie elementu. Oto podejście: w sytuacji, gdy masz wiele elementów o różnych zależnościach, możemy zadbać o to, by wczytać tylko te zależności, które zostały rzeczywiście wykorzystane. Gdy usuwasz element, nie musisz pamiętać o usuwaniu jego zależności, bo usuwasz też plik bower.json
deklarujący te zależności. Każdy element niezależnie wczytuje
związane z nim zależności.
Aby jednak uniknąć duplikowania zależności, w folderze każdego elementu umieszczamy też plik .bowerrc
. To informuje, gdzie przechowywać zależności, dzięki czemu mamy pewność, że na końcu w tym samym katalogu jest tylko jedna z nich:
{
"directory" : "../../../../../bower_components"
}
Dzięki temu, jeśli wiele elementów deklaruje THREE.js
jako zależność, po zainstalowaniu jej dla pierwszego elementu i rozpoczęciu analizy drugiego elementu uświadamia sobie, że zależność jest już zainstalowana i nie pobiera jej ponownie ani nie powiela. Podobnie będzie w przypadku tych plików zależności, dopóki będzie zawierać co najmniej 1 element, który nadal określa go w jego bower.json
.
Skrypt bash znajduje wszystkie pliki bower.json
w strukturze elementów zagnieżdżonych.
Następnie pojedynczo otwiera te katalogi i wykonuje polecenie bower install
w każdym z nich:
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
Szablon szybkiego nowego elementu
Za każdym razem, gdy chcesz utworzyć nowy element, potrzeba trochę czasu: wygenerowanie folderu i podstawowej struktury plików z poprawnymi nazwami. Używamy więc języka Slush do napisania prostego generatora elementów.
Możesz wywołać skrypt z poziomu wiersza poleceń:
$ slush element path/to/your/element-name
Zostanie utworzony nowy element, w tym cała struktura i zawartość pliku.
Zdefiniowaliśmy szablony plików elementów, np. szablon pliku .jade
wygląda tak:
// 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')
Generator Slush zastępuje zmienne rzeczywistymi ścieżkami i nazwami elementów.
Tworzenie elementów przy użyciu Gulp
Grzegorz zachowuje kontrolę nad procesem kompilacji. A w naszej strukturze elementy, dzięki którym Gulp powinien wykonać te czynności:
- Skompiluj pliki
.coffee
elementów do.js
- Skompiluj pliki
.scss
elementów do.css
- Skompiluj pliki
.jade
elementów w formacie.html
, umieszczając w nim pliki.css
.
Więcej informacji:
Kompiluję pliki .coffee
elementów do .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')));
});
W krokach 2 i 3 używamy gulp i wtyczki kompasu, aby skompilować scss
do .css
oraz .jade
do .html
, podobnie jak w przypadku kroku 2.
Z pierwiastkami polimerowymi
Aby uwzględnić elementy Polymer, używamy importu 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">
Optymalizacja elementów Polymer na potrzeby produkcji
W przypadku dużego projektu może się okazać, że zawiera dużo pierwiastków Polymer. W naszym projekcie
jest ich ponad 50. Jeśli weźmiesz pod uwagę, że każdy element ma oddzielny plik .js
i niektóre biblioteki, do których odwołują się biblioteki, stanie się to ponad 100 osobnymi plikami. Oznacza to dużą liczbę żądań, które musi wysłać przeglądarka,
jednocześnie zmniejsza wydajność. Podobnie jak w przypadku procesu konkatenacji i minimalizowania w przypadku kompilacji Angular, na końcu „wulkanizujemy” projekt Polymer.
Wulkanizuj to narzędzie Polymer, które spłaszcza drzewo zależności do jednego pliku HTML, co zmniejsza liczbę żądań. Jest to szczególnie przydatne w przypadku przeglądarek, które natywnie nie obsługują komponentów internetowych.
CSP (Content Security Policy) i Polymer
Podczas tworzenia bezpiecznych aplikacji internetowych musisz wdrożyć CSP. CSP to zestaw reguł, które uniemożliwiają prowadzenie ataków typu cross-site scripting (XSS), czyli wykonywanie skryptów z niebezpiecznych źródeł lub wykonywanie skryptów wbudowanych z plików HTML.
Ten, zoptymalizowany, połączony i zminifikowany plik .html
wygenerowany przez Vulcanize zawiera cały kod JavaScript w formacie niezgodnym z CSP. Aby rozwiązać ten problem, używamy narzędzia Crisper.
Crisper dzieli skrypty wbudowane z pliku HTML i umieszcza je w jednym zewnętrznym pliku JavaScript, aby zapewnić zgodność z CSP. Dlatego przekazujemy wulkanizowany plik HTML przez narzędzie Crisper i w efekcie otrzymujemy 2 pliki: elements.html
i elements.js
. W obrębie elements.html
zajmuje się też wczytywaniem wygenerowanego elements.js
.
Logiczna struktura aplikacji
Elementy w Polymer to wszystko: od narzędzi niewizualnych przez małe, niezależne i wielokrotnego użytku elementy interfejsu (np. przyciski) po większe moduły, takie jak „strony”, a nawet tworzenie pełnych aplikacji.
Przetwarzanie końcowe z wykorzystaniem technologii Polymer i architektury nadrzędnej i podrzędnej
W każdym potoku grafiki 3D zawsze występuje ostatni krok, w którym efekty są dodawane do całego zdjęcia jako rodzaj nakładki. To etap przetwarzania, który obejmuje efekty takie jak poświaty, promienie bokeh, głębię pola, bokeh, rozmycia itp. Efekty są łączone i zastosowane do różnych elementów w zależności od tego, jak powstała scena. W kodzie THREE.js możemy utworzyć niestandardowy program do cieniowania do późniejszego przetwarzania w JavaScripcie albo w Polymer dzięki strukturze nadrzędny-podrzędny.
Jeśli spojrzysz na kod HTML elementu naszego procesora:
<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>
Efekty określamy jako zagnieżdżone elementy Polymer w ramach wspólnej klasy. Następnie w sw-experience-postprocessor.js
:
effects = @querySelectorAll '.effect'
@composer.addPass effect.getPass() for effect in effects
Używamy funkcji HTML i funkcji querySelectorAll
w języku JavaScript, aby znaleźć wszystkie efekty zagnieżdżone jako elementy HTML w obrębie procesora postów, w kolejności, w jakiej zostały określone. Następnie powtarzamy je i dodajemy do narzędzia composer.
Załóżmy teraz, że chcemy usunąć efekt DOF (głębokość pola) oraz zmienić kolejność efektów kwitnienia i winietowania. Wystarczy zmienić definicję osoby przetwarzającej na taką:
<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>
Cała scena będzie po prostu odtwarzana, nie zmieniając ani jednego wiersza rzeczywistego kodu.
Pętla renderowania i pętla aktualizacji w technologii Polymer
Polymer pozwala też elegancko podchodzić do renderowania i aktualizowania silników.
Utworzyliśmy element timer
, który używa parametru requestAnimationFrame
i oblicza wartości takie jak bieżący czas (t
) i czas delta – czas, który upłynął od ostatniej klatki (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()
Następnie używamy wiązania danych, aby powiązać właściwości t
i dt
z naszą wyszukiwarką (experience.jade
):
sw-timer(
t='{ % templatetag openvariable % }t}}',
dt='{ % templatetag openvariable % }dt}}'
)
sw-experience-engine(
t='[t]',
dt='[dt]'
)
Nasłuchujemy też w wyszukiwarce zmian wartości t
i dt
. Gdy te wartości się zmienią, funkcja _update
zostanie wywołana:
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
Jeśli jednak zależy Ci na szybkości FPS, możesz usunąć powiązanie danych Polymer w pętli renderowania, aby zaoszczędzić kilka milisekund wymaganych do powiadamiania elementów o zmianach. Wprowadziliśmy takie narzędzia:
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
# ...
Funkcja addUpdateListener
akceptuje wywołanie zwrotne i zapisuje je w swojej tablicy wywołań zwrotnych. Następnie w pętli aktualizacji powtarzamy każde wywołanie zwrotne i realizujemy je bezpośrednio z argumentami dt
i t
, omijając wiązanie danych lub uruchamianie zdarzeń. Gdy wywołanie zwrotne przestanie być aktywne, dodaliśmy funkcję removeUpdateListener
, która umożliwia usunięcie dodanego wcześniej wywołania zwrotnego.
Miecz świetlny w THREE.js
THREE.js wydobywa niskopoziomową szczegółowość WebGL i umożliwia nam skupienie się na problemie. Nasz problem polega na walce ze szturmowcami, a potrzebujemy broni. Zbudujmy więc miecz świetlny.
Błyszczący ostrze to coś, co odróżnia miecz świetlny od każdej starej broni dwuręcznej. Składa się głównie z dwóch części: belki i szlaku, który widać podczas przenoszenia. Ma jasny kształt walca i dynamiczny ślad, który przemieszcza się wraz z ruchem gracza.
Ostrze
Ostrze składa się z dwóch łopatek podrzędnych. Wewnętrzne i zewnętrzne. Obie sieci są siatkami THREE.js z odpowiednimi materiałami.
Ostrze wewnętrzne
Wewnętrzne ostrze zastosowaliśmy z niestandardowego materiału z niestandardowym cieniem. Wybieramy linię utworzoną przez dwa punkty i rzutujemy ją na płaszczyznę. To samolot, którym sterujesz za pomocą telefonu komórkowego. Daje on poczucie głębi i orientacji miecza.
Aby stworzyć wrażenie okrągłego, świecącego obiektu, sprawdzamy odległość punktu ortogonalnego dowolnego punktu na płaszczyźnie od linii głównej łączącej dwa punkty A i B jak poniżej. Im punkt bliżej osi głównej, tym bardziej głęboko.
Źródło poniżej pokazuje, jak obliczamy obiekt vFactor
, aby kontrolować intensywność w cieniowaniu wierzchołków, a następnie używać go do mieszania się ze sceną w cieniu fragmentów.
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" )
};
Zewnętrzne ostrze
W przypadku blasku zewnętrznego renderujemy go w osobnym buforze renderowania. Wykorzystujemy efekt rozkwitu po przetworzeniu i mieszamy z ostatecznym obrazem, aby uzyskać pożądaną poświatę. Na ilustracji poniżej widać 3 różne regiony, które będą Ci potrzebne, jeśli potrzebujesz porządnej szabry. To białe rdzeń: środkowa, niebieski, poświata i poświata zewnętrzna.
Szlak z mieczem świetlnym
Ścieżka miecza świetlnego jest kluczem do osiągnięcia pełnego efektu, jak w przypadku pierwowzorów z serii Gwiezdne Wojny. Szlak przebyliśmy wachlarzem trójkątów, generowanym dynamicznie ruchem miecza świetlnego. Wentylatory te są następnie przekazywane do zespołu przetwarzającego w celu dalszego uszczegółowienia obrazu. Aby utworzyć geometrię wentylatora, mamy segment linii. Na podstawie poprzedniej przekształcenia oraz przekształcenia bieżącego generujemy w siatce nowy trójkąt, który po pewnej długości wypuszcza część ogona.
Po utworzeniu sieci typu mesh przypisujemy do niej prosty materiał i przekazujemy go do podmiotu przetwarzającego, aby uzyskać płynny efekt. Używamy tego samego efektu rozkwitu dla poświaty zewnętrznego łopatki, aby uzyskać gładki ślad, jak widać:
Poświata na szlaku
Ukończenie finalnej części prac wymagało użycia poświaty wokół faktycznego szlaku, który można było tworzyć na wiele sposobów. Rozwiązaniem, którym nie szczegółowo omawiamy, ze względu na wydajność było utworzenie dla tego bufora niestandardowego kształtu, który tworzy gładką krawędź wokół bufora renderowania. Później łączymy uzyskane dane w końcowym renderowaniu, więc widać poświatę otaczającą szlak:
Podsumowanie
Polymer to zaawansowana biblioteka i koncepcja (tak samo jak w ogóle WebKomponenty). Tylko od Ciebie zależy, co z nimi zrobi. Może to być wszystko, od prostego przycisku interfejsu po pełnowymiarową aplikację WebGL. W poprzednich rozdziałach przedstawiliśmy kilka wskazówek i porad dotyczących efektywnego wykorzystania Polymer w produkcji oraz tworzenia struktury bardziej złożonych modułów, które również dobrze się sprawdzają. Pokazaliśmy też, jak stworzyć ładnie wyglądający miecz świetlny w WebGL. Jeśli więc połączysz to wszystko, pamiętaj o wulkanizacji elementów Polymer przed wdrożeniem ich na serwerze produkcyjnym. Jeśli nie zapomnisz też korzystać z Crispera, jeśli chcesz zachować zgodność z CSP, być może moc jest z Tobą.