
Résumé
Comment nous avons utilisé Polymer pour créer une technologie WebGL très performante, contrôlée sur les mobiles Sabre laser modulaire et configurable. Nous examinons certains détails clés de notre projet https://lightsaber.withgoogle.com/ pour vous faire gagner du temps lorsque vous créerez le vôtre la prochaine fois que vous rencontrerez des Stormtroopers en colère.
Présentation
Si vous vous demandez quels sont les composants Polymer ou WebComponents, il serait préférable de commencer par partager un extrait d'un projet opérationnel réel. Voici un exemple tiré de la page d’accueil de notre projet https://lightsaber.withgoogle.com. Il est un fichier HTML standard, mais qui contient un peu de magie:
<!-- 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>
Il existe de nombreuses options pour créer des modèles une application HTML5. API, frameworks, bibliothèques, moteurs de jeu, etc. Malgré tous les choix, il est difficile d'obtenir une configuration qui soit un bon mix entre contrôle des hautes performances des graphismes et modulaire épurée la structure et l'évolutivité. Polymer pouvait nous aider à maintenir du projet tout en permettant des performances de bas niveau des optimisations, et nous avons soigneusement élaboré la manière dont nous avons décomposé notre projet en composants pour exploiter au mieux les capacités de Polymer.
Modularité avec Polymer
Polymer est une bibliothèque qui permet vous permet de créer votre projet à partir d'éléments personnalisés réutilisables. Il vous permet d'utiliser des modules autonomes entièrement fonctionnels contenus dans un un seul fichier HTML. Ils contiennent non seulement la structure (balisage HTML), mais aussi la logique et les styles intégrés.
Examinez l'exemple ci-dessous:
<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>
Mais pour un projet plus vaste, il peut être utile de séparer ces trois éléments (HTML, CSS, JS) et ne les fusionner qu'au moment de la compilation. Une chose nous avons attribué à chaque élément du projet son propre dossier:
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
Et le dossier de chaque élément a la même structure interne avec des des répertoires et des fichiers pour la logique (fichiers Coffee), les styles (fichiers Scss) et (fichier Jade).
Voici un exemple d'élément sw-ui-logo
:
sw-ui-logo/
|-- bower.json
|-- scripts
| `-- sw-ui-logo.coffee
|-- styles
| `-- sw-ui-logo.scss
`-- sw-ui-logo.jade
Et si vous examinez le fichier .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')
Incluez des styles pour vous assurer que tout est organisé de façon claire
et la logique à partir de fichiers distincts. Pour inclure nos styles dans notre modèle
nous utilisons l'instruction include
de Jade, ce qui nous permet d'avoir du code CSS intégré
le contenu du fichier après la compilation. L'élément de script sw-ui-logo.js
va
au moment de l'exécution.
Dépendances modulaires avec Bower
Normalement, nous conservons les bibliothèques et autres dépendances au niveau du projet.
Cependant, dans la configuration ci-dessus, vous remarquerez un bower.json
qui se trouve dans
dossier de l'élément: dépendances au niveau de l'élément. L'idée derrière cette approche
est que lorsqu'une situation où vous avez de nombreux éléments
dépendances, nous pouvons faire en sorte de
charger uniquement les dépendances qui sont
réellement utilisées. Si vous supprimez un élément, vous n'avez pas besoin de penser à
supprimer sa dépendance, car vous aurez également supprimé le fichier bower.json
qui déclare ces dépendances. Chaque élément charge indépendamment
les dépendances qui s’y rapportent.
Toutefois, pour éviter la duplication des dépendances, nous incluons un fichier .bowerrc
.
dans le dossier de chaque élément. Cela indique à
bower où stocker
afin de s'assurer qu'il n'y en a qu'une à la fin
répertoire:
{
"directory" : "../../../../../bower_components"
}
De cette façon, si plusieurs éléments déclarent THREE.js
comme dépendance, une fois
bower l'installe pour le premier élément
et commence à analyser le deuxième,
il détecte que cette dépendance est déjà installée et n'effectue pas
téléchargez-le à nouveau ou dupliquez-le. De même, il maintient cette dépendance
à condition qu'au moins un élément le définisse
son bower.json
.
Un script bash recherche tous les fichiers bower.json
dans la structure des éléments imbriqués.
Ensuite, il entre dans ces répertoires un par un et exécute bower install
dans
chacun d'entre eux:
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
Modèle "Nouvel élément rapide"
Chaque fois que vous voulez créer un élément, cela prend un peu de temps : le dossier et la structure de base des fichiers avec les noms corrects. Nous utilisons donc Slush pour écrire un générateur d'éléments simple.
Vous pouvez appeler le script à partir de la ligne de commande:
$ slush element path/to/your/element-name
Le nouvel élément est créé, y compris l'ensemble de la structure et du contenu des fichiers.
Nous avons défini des modèles pour les fichiers d'éléments, par exemple le modèle de fichier .jade
se présente comme suit:
// 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')
Le générateur Slush remplace les variables par les chemins et les noms réels des éléments.
Utiliser Gulp pour créer des éléments
Gulp garde le contrôle du processus de compilation. Et dans notre structure, pour construire les éléments dont nous avons besoin de Gulp pour suivre les étapes suivantes:
- Compiler les éléments
.coffee
fichiers dans.js
- Compiler les éléments
.scss
fichiers dans.css
- Compiler les éléments
.jade
dans.html
, intégrant les fichiers.css
.
Plus en détail:
La compilation des éléments .coffee
fichiers dans .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')));
});
Aux étapes 2 et 3, nous utilisons gulp et un plug-in Boussole pour compiler scss
afin de
.css
et .jade
à .html
, selon une approche semblable à l'étape 2 ci-dessus.
Avec des éléments en polymère
Pour inclure les éléments Polymer, nous utilisons des importations 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">
Optimiser les éléments Polymers pour la production
Un grand projet peut finir par comporter de nombreux éléments Polymer. Dans notre
nous en avons plus de cinquante. Si vous considérez que chaque
élément a une
fichier .js
distinct et dont certaines ont des bibliothèques référencées, il devient supérieur à
100 fichiers distincts. Cela signifie que le navigateur doit effectuer
beaucoup de requêtes,
avec une perte de performances. Comme pour un processus de concaténation
et de minimisation, nous
applicable à un build Angular, nous "vulcanons" le projet Polymer
pour la production.
Vulcanize est un outil Polymer qui aplatit l'arborescence des dépendances en un seul fichier HTML, ce qui réduit le nombre de requêtes. Cela est particulièrement utile pour les navigateurs prend en charge les composants Web de manière native.
CSP (Content Security Policy) et Polymer
Lorsque vous développez des applications Web sécurisées, vous devez implémenter CSP. CSP est un ensemble de règles qui empêchent les attaques par script intersites (XSS) : l'exécution de scripts provenant de sources non sécurisées ou l'exécution de scripts intégrés ; à partir de fichiers HTML.
Le fichier .html
optimisé, concaténé et réduit dont la taille a été réduite
par Vulcanize intègre tout le code JavaScript dans un environnement
. Pour résoudre ce problème, nous utilisons un outil appelé
Crisper :
Crisper divise les scripts intégrés à partir d'un fichier HTML et les place en un seul,
fichier JavaScript externe pour la conformité CSP. Nous transmettons donc le modèle vulcanisé
HTML dans Crisper et vous obtenez deux fichiers: elements.html
et
elements.js
Dans elements.html
, il se charge également de charger le
a généré elements.js
.
Structure logique de l'application
Dans Polymer, les éléments peuvent être n'importe quoi, d'un utilitaire non visuel à de petits, des éléments d'interface utilisateur autonomes et réutilisables (comme des boutons) à des modules plus grands, comme "pages" et même de rédiger des applications complètes.
<ph type="x-smartling-placeholder">
Post-traitement avec Polymer et une architecture parent-enfant
Dans tout pipeline de graphismes 3D, il y a toujours une dernière étape où les effets sont ajoutés au-dessus de l'ensemble de l'image comme une sorte de superposition. Il s'agit de la l'étape de post-traitement, qui implique des effets tels que des éclats, des rayons divins, la profondeur de champ, le bokeh, le floutage, etc. Les effets sont combinés et appliqués différents éléments en fonction de la construction de la scène. Dans THREE.js, nous vous pouvez créer un nuanceur personnalisé pour le post-traitement en JavaScript c'est possible avec Polymer grâce à sa structure parent-enfant.
Si vous examinez le code HTML de notre élément de post-traitement:
<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>
Nous spécifions les effets en tant qu'éléments Polymer imbriqués dans une classe commune. Ensuite,
Dans sw-experience-postprocessor.js
, nous procédons ainsi:
effects = @querySelectorAll '.effect'
@composer.addPass effect.getPass() for effect in effects
Nous utilisons la fonctionnalité HTML et le code JavaScript querySelectorAll
pour trouver toutes
les effets imbriqués en tant qu'éléments HTML dans le post-traitement, dans l'ordre
dans lesquels elles
ont été spécifiées. Nous effectuons ensuite une itération sur eux et les ajoutons au compositeur.
Maintenant, supposons que nous voulons supprimer l'effet DOF (Profondeur de champ) et modifier l'ordre des effets de fleur et de vignetage. Tout ce que nous avons à faire est de modifier la définition du post-traitement par quelque chose comme:
<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>
et la scène va s'exécuter, sans modifier une seule ligne de code.
Boucle de rendu et de mise à jour dans Polymer
Avec Polymer, nous pouvons aussi aborder le rendu et les mises à jour du moteur de manière élégante.
Nous avons créé un élément timer
qui utilise requestAnimationFrame
et calcule
telles que l'heure actuelle (t
) et le temps différentiel (temps écoulé depuis le
dernière image (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()
Ensuite, nous utilisons la liaison de données pour lier les propriétés t
et dt
à notre
moteur (experience.jade
):
sw-timer(
t='{ % templatetag openvariable % }t}}',
dt='{ % templatetag openvariable % }dt}}'
)
sw-experience-engine(
t='[t]',
dt='[dt]'
)
Nous écoutons aussi les modifications de t
et de dt
dans le moteur et chaque fois que
les valeurs changent, la fonction _update
est appelée:
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
Si vous êtes managé de FPS, vous voudrez peut-être supprimer les données de Polymer de liaison dans la boucle de rendu pour gagner quelques millisecondes nécessaires pour informer sur les modifications. Nous avons implémenté les observateurs personnalisés comme suit:
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 fonction addUpdateListener
accepte un rappel et l'enregistre dans son
de rappels. Ensuite, dans la boucle de mise à jour, nous effectuons une itération sur chaque rappel.
nous l'exécutons directement avec les arguments dt
et t
, en contournant la liaison de données ou
le déclenchement d'événements. Une fois qu'un rappel n'est plus censé être actif, nous avons ajouté une
removeUpdateListener
, qui vous permet de supprimer un rappel ajouté précédemment.
Sabre laser dans THREE.js
THREE.js élimine les détails de bas niveau de WebGL et nous permet de nous concentrer sur le problème. Notre problème est d'affronter les Stormtroopers, arme. Faisons donc un sabre laser.
La lame brillante différencie un sabre laser des anciens. arme à deux mains. Il se compose principalement de deux parties: la poutre et la traînée. que l'on voit lorsqu'on le déplace. Nous l'avons conçue en forme de cylindre brillant et un sentier dynamique qui le suit au fur et à mesure que le joueur se déplace.
The Blade
La lame se compose de deux sous-pales. Un intérieur et un extérieur. Il s'agit tous deux de maillages THREE.js avec leurs matériaux respectifs.
La lame intérieure
Pour la lame intérieure, nous avons utilisé un matériau personnalisé avec un nuanceur personnalisé. Mer à partir d'une droite créée par deux points et projeter la ligne entre ces deux dans un plan. C'est ce que vous contrôlez avec votre mobile, cela donne une impression de profondeur et d'orientation au sabre.
Pour donner l'impression qu'il s'agit d'un objet rond et lumineux, nous observons les distance orthogonale de n'importe quel point du plan par rapport au point principal reliant les deux points A et B comme ci-dessous. Plus un point est proche de plus l'axe principal est lumineux.

La source ci-dessous montre comment nous calculons un vFactor
pour contrôler l'intensité.
dans le nuanceur de sommets pour qu'il se fonde dans la scène
nuanceur de fragments.
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" )
};
Éclat de la lame extérieure
Pour le halo externe, nous effectuons le rendu dans un tampon de rendu distinct et utilisons un et se fondre dans l'image finale pour obtenir l'éclat souhaité. L'image ci-dessous montre les trois régions différentes si vous voulez un sabre décent. à savoir le cœur blanc, le milieu une teinte bleue et un halo extérieur.

Sentier du sabre laser
La traînée du sabre laser est la clé de l'effet complet, comme le montre l'original. de la série Star Wars. Nous avons créé la piste avec un fan de triangles générés de manière dynamique en fonction du mouvement du sabre laser. Ces fans sont alors transmis au postprocesseur pour une amélioration visuelle supplémentaire. Pour créer le une forme de ventilateur, nous avons un segment de droite basé sur sa transformation précédente et "current transform", nous générons un nouveau triangle dans le maillage, de la queue après une certaine longueur.


Une fois que nous avons un maillage, nous lui attribuons un matériau simple et nous le transmettons pour créer un effet fluide. Nous utilisons le même effet de fleur que nous avons appliqué le halo de la pale externe pour laisser une trace lisse, comme vous pouvez le voir:

Lueur autour de la piste
Pour que la dernière pièce soit complète, nous avons dû gérer l'éclat autour de qui peuvent être créées de différentes manières. La solution que nous n'entrez pas dans les détails ici, pour des raisons de performances, c'est de créer pour ce tampon, qui crée un bord lisse autour d'une pince RenderBuffer. Nous combinons ensuite cette sortie dans le rendu final. Ici, vous pouvez voir l'éclat qui entoure la piste:

Conclusion
Polymer est un concept et une bibliothèque puissants (tout comme les WebComponents sont général). C'est à vous de décider de ce que vous en créez. Il peut s’agir de n’importe quoi, d'un bouton d'interface utilisateur à une application WebGL en taille réelle. Dans les chapitres précédents, voici quelques conseils et astuces pour utiliser efficacement Polymer en production et comment structurer des modules plus complexes bien. Nous vous avons également montré comment créer un sabre laser d'aspect esthétique avec WebGL. Si vous combinez tout cela, n'oubliez pas de Vulcaniser vos éléments Polymer avant le déploiement sur le serveur de production et, si vous n'oubliez pas d'utiliser Crisper, Si vous souhaitez rester en conformité avec les CSP, vous pouvez être obligé de faire appel à vous !
