À propos de cet atelier de programmation
1. Introduction
Composants Web
Les composants Web sont un ensemble de normes Web qui permettent aux développeurs d'étendre le code HTML avec des éléments personnalisés. Dans cet atelier de programmation, vous allez définir l'élément <brick-viewer>
, qui pourra afficher des modèles de briques.
Lit-element
Pour définir plus facilement notre élément personnalisé <brick-viewer>
, nous allons utiliser lit-element. lit-element est une classe de base légère qui ajoute du sucre syntaxique à la norme de composants Web. Cela facilitera la prise en main de notre élément personnalisé.
Premiers pas
Nous allons coder dans un environnement Stackblitz en ligne. Ouvrez ce lien dans une nouvelle fenêtre:
stackblitz.com/edit/brick-viewer
C'est parti !
2. Définir un élément personnalisé
Définition de la classe
Pour définir un élément personnalisé, créez une classe qui étend LitElement
et décorez-le avec @customElement
. L'argument de @customElement
est le nom de l'élément personnalisé.
Dans briques-viewer.ts, saisissez:
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}
L'élément <brick-viewer></brick-viewer>
est maintenant prêt à être utilisé en HTML. Mais si vous essayez, rien ne s'affichera. Résolvons ce problème.
Méthode d'affichage
Pour implémenter la vue du composant, définissez une méthode nommée "render". Cette méthode doit renvoyer un littéral de modèle tagué avec la fonction html
. Placez le code HTML de votre choix dans le littéral de modèle balisé. Celui-ci s'affichera lorsque vous utiliserez <brick-viewer>
.
Ajoutez la méthode render
:
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick viewer</div>`;
}
}
3. Spécifier le fichier LDraw
Définir une propriété
Il serait idéal si un utilisateur de <brick-viewer>
pouvait spécifier le modèle de briques à afficher à l'aide d'un attribut, comme ceci:
<brick-viewer src="path/to/model.ldraw"></brick-viewer>
Étant donné que nous créons un élément HTML, nous pouvons exploiter l'API déclarative et définir un attribut source, comme une balise <img>
ou <video>
. Avec Litelement, il vous suffit de décorer une propriété de classe avec @property
. L'option type
vous permet de spécifier la manière dont Litelement analyse la propriété afin de l'utiliser en tant qu'attribut HTML.
Définissez la propriété et l'attribut src
:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
<brick-viewer>
dispose désormais d'un attribut src
que nous pouvons définir en HTML. Sa valeur est déjà lisible depuis notre classe BrickViewer
grâce à lit-element.
Affichage des valeurs
Nous pouvons afficher la valeur de l'attribut src
en l'utilisant dans le littéral de modèle de la méthode de rendu. Interpoler les valeurs dans des littéraux de modèle à l'aide de la syntaxe ${value}
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
Nous voyons maintenant la valeur de l'attribut src dans l'élément <brick-viewer>
de la fenêtre. Essayez ceci: ouvrez les outils pour les développeurs de votre navigateur et modifiez manuellement l'attribut src. Allez-y, essayez...
...Avez-vous remarqué que le texte de l'élément est mis à jour automatiquement ? lit-element observe les propriétés de classe décorées avec @property
et affiche à nouveau la vue. lit-element effectue le plus gros du travail à votre place.
4. Plantez le décor avec Three.js
Lumière, caméra, rendu !
Notre élément personnalisé utilisera 3.js pour afficher nos modèles de briques 3D. Vous pouvez effectuer certaines opérations une seule fois pour chaque instance d'un élément <brick-viewer>
, comme configurer la scène, la caméra et l'éclairage de three.js. Nous allons les ajouter au constructeur de la classe BrickViewer. Nous conserverons certains objets en tant que propriétés de classe afin de pouvoir les utiliser ultérieurement: appareil photo, scène, commandes et moteur de rendu.
Ajoutez la configuration de la scène three.js:
export class BrickViewer extends LitElement {
private _camera: THREE.PerspectiveCamera;
private _scene: THREE.Scene;
private _controls: OrbitControls;
private _renderer: THREE.WebGLRenderer;
constructor() {
super();
this._camera = new THREE.PerspectiveCamera(45, this.clientWidth/this.clientHeight, 1, 10000);
this._camera.position.set(150, 200, 250);
this._scene = new THREE.Scene();
this._scene.background = new THREE.Color(0xdeebed);
const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
this._scene.add( ambientLight );
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(-1000, 1200, 1500);
this._scene.add(directionalLight);
this._renderer = new THREE.WebGLRenderer({antialias: true});
this._renderer.setPixelRatio(window.devicePixelRatio);
this._renderer.setSize(this.offsetWidth, this.offsetHeight);
this._controls = new OrbitControls(this._camera, this._renderer.domElement);
this._controls.addEventListener("change", () =>
requestAnimationFrame(this._animate)
);
this._animate();
const resizeObserver = new ResizeObserver(this._onResize);
resizeObserver.observe(this);
}
private _onResize = (entries: ResizeObserverEntry[]) => {
const { width, height } = entries[0].contentRect;
this._renderer.setSize(width, height);
this._camera.aspect = width / height;
this._camera.updateProjectionMatrix();
requestAnimationFrame(this._animate);
};
private _animate = () => {
this._renderer.render(this._scene, this._camera);
};
}
L'objet WebGLRenderer
fournit un élément DOM qui affiche la scène three.js affichée. Il est accessible via la propriété domElement
. Nous pouvons interpoler cette valeur dans le littéral de modèle de rendu à l'aide de la syntaxe ${value}
.
Supprimez le message src
que nous avions dans le modèle et insérez l'élément DOM du moteur de rendu:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
Pour que l'élément dom du moteur de rendu puisse s'afficher dans son intégralité, nous devons également définir l'élément <brick-viewer>
lui-même sur display: block
. Nous pouvons fournir des styles dans une propriété statique appelée styles
, définie sur un littéral de modèle css
.
Ajoutez ce style à la classe:
export class BrickViewer extends LitElement {
static styles = css`
/* The :host selector styles the brick-viewer itself! */
:host {
display: block;
}
`;
}
<brick-viewer>
affiche maintenant une scène Three.js:
Mais... elle est vide. Donnons-lui un modèle.
Chargeur de briques
Nous allons transmettre la propriété src
que nous avons définie précédemment à LDrawLoader, qui est livrée avec three.js.
Les fichiers LDraw peuvent séparer un modèle Brick en étapes de construction distinctes. Le nombre total d'étapes et la visibilité de chaque brique sont accessibles via l'API LDrawLoader.
Copiez ces propriétés, la nouvelle méthode _loadModel
et la nouvelle ligne dans le constructeur:
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
private _loader = new LDrawLoader();
private _model: any;
private _numConstructionSteps?: number;
step?: number;
constructor() {
// ...
// Add this line right before this._animate();
(this._loader as any).separateObjects = true;
this._animate();
}
private _loadModel() {
if (this.src === null) {
return;
}
this._loader
.setPath('')
// Using our src property!
.load(this.src, (newModel) => {
if (this._model !== undefined) {
this._scene.remove(this._model);
this._model = undefined;
}
this._model = newModel;
// Convert from LDraw coordinates: rotate 180 degrees around OX
this._model.rotation.x = Math.PI;
this._scene.add(this._model);
this._numConstructionSteps = this._model.userData.numConstructionSteps;
this.step = this._numConstructionSteps!;
const bbox = new THREE.Box3().setFromObject(this._model);
this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
this._controls.update();
this._controls.saveState();
});
}
}
Quand _loadModel
doit-il être appelé ? Elle doit être appelée chaque fois que l'attribut src change. En décorant la propriété src
avec @property
, nous avons activé cette propriété dans le cycle de vie de la mise à jour de Litelement. Si l'une de ces propriétés décorées les modifications de valeur, une série de méthodes est appelée pour accéder aux nouvelles et aux anciennes valeurs des propriétés. La méthode de cycle de vie qui nous intéresse s'appelle update
. La méthode update
utilise un argument PropertyValues
, qui contient des informations sur les propriétés qui viennent d'être modifiées. C'est l'adresse idéale pour appeler _loadModel
.
Ajoutez la méthode update
:
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
super.update(changedProperties);
}
}
Notre élément <brick-viewer>
peut désormais afficher un fichier brique, spécifié à l'aide de l'attribut src
.
5. Affichage de modèles partiels
Rendez l'étape de construction actuelle configurable. Nous aimerions pouvoir spécifier <brick-viewer step="5"></brick-viewer>
. Nous devrions voir à quoi ressemble le modèle en brique à la 5e étape de construction. Pour ce faire, transformons la propriété step
en propriété observée en la décorant avec @property
.
Décorez la propriété step
:
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
Nous allons maintenant ajouter une méthode d'assistance qui permet de rendre visibles uniquement les briques jusqu'à l'étape de construction actuelle. Nous allons appeler l'application auxiliaire dans la méthode de mise à jour pour qu'elle s'exécute chaque fois que la propriété step
est modifiée.
Mettez à jour la méthode update
et ajoutez la nouvelle méthode _updateBricksVisibility
:
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
if (changedProperties.has('step')) {
this._updateBricksVisibility();
}
super.update(changedProperties);
}
private _updateBricksVisibility() {
this._model && this._model.traverse((c: any) => {
if (c.isGroup && this.step) {
c.visible = c.userData.constructionStep <= this.step;
}
});
requestAnimationFrame(this._animate);
}
}
À présent, ouvrez les outils de développement de votre navigateur et inspectez l'élément <brick-viewer>
. Ajoutez-y un attribut step
, comme ceci:
Observez ce qu'il advient du modèle rendu. Nous pouvons utiliser l'attribut step
pour contrôler la partie du modèle à afficher. Voici ce à quoi cela devrait ressembler lorsque l'attribut step
est défini sur "10"
:
6. Navigation dans l'ensemble de briques
icône-mwc
L'utilisateur final de notre <brick-viewer>
doit également pouvoir parcourir les étapes de compilation via l'interface utilisateur. Ajoutons des boutons pour passer à l'étape suivante, à l'étape précédente et à la première étape. Pour vous faciliter la tâche, nous allons utiliser le composant Web de boutons de Material Design. Comme @material/mwc-icon-button
a déjà été importé, nous sommes prêts à ajouter <mwc-icon-button></mwc-icon-button>
. Nous pouvons spécifier l'icône à utiliser avec l'attribut icon, comme ceci: <mwc-icon-button icon="thumb_up"></mwc-icon-button>
. Toutes les icônes possibles sont accessibles à l'adresse suivante: material.io/resources/icons.
Ajoutons des boutons d'icône à la méthode de rendu:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button icon="replay"></mwc-icon-button>
<mwc-icon-button icon="navigate_before"></mwc-icon-button>
<mwc-icon-button icon="navigate_next"></mwc-icon-button>
</div>
`;
}
}
Utiliser Material Design sur notre page est aussi facile, grâce aux composants Web !
Liaisons d'événements
Ces boutons devraient faire quelque chose. La fonction "Répondre" doit réinitialiser l'étape de construction sur 1. La commande "Navigate_before" doit diminuer l'étape de construction, et la commande "Navigate_next" doit l'incrémenter. lit-element facilite l'ajout de cette fonctionnalité à l'aide de liaisons d'événements. Dans votre littéral de modèle HTML, utilisez la syntaxe @eventname=${eventHandler}
comme attribut d'élément. eventHandler
s'exécute désormais lorsqu'un événement eventname
est détecté sur cet élément. Par exemple, ajoutons des gestionnaires d'événements de clic à nos trois boutons d'icône:
export class BrickViewer extends LitElement {
private _restart() {
this.step! = 1;
}
private _stepBack() {
this.step! -= 1;
}
private _stepForward() {
this.step! += 1;
}
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button @click=${this._restart} icon="replay"></mwc-icon-button>
<mwc-icon-button @click=${this._stepBack} icon="navigate_before"></mwc-icon-button>
<mwc-icon-button @click=${this._stepForward} icon="navigate_next"></mwc-icon-button>
</div>
`;
}
}
Essayez de cliquer sur les boutons maintenant. Bravo !
Styles
Les boutons fonctionnent, mais ils n'ont pas l'air parfaits. Ils sont tous regroupés au bas de l'écran. Appliquez un style à ces éléments pour les superposer à la scène.
Pour appliquer des styles à ces boutons, revenons à la propriété static styles
. Ces styles sont délimités, ce qui signifie qu'ils ne s'appliquent qu'aux éléments de ce composant Web. C'est l'un des plaisirs de l'écriture des composants Web: les sélecteurs peuvent être plus simples, et le code CSS est plus facile à lire et à écrire. Au revoir, BEM !
Modifiez les styles pour qu'ils se présentent comme suit:
export class BrickViewer extends LitElement {
static styles = css`
:host {
display: block;
position: relative;
}
#controls {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
}
`;
}
Bouton "Réinitialiser l'appareil photo"
Les utilisateurs finaux de notre <brick-viewer>
peuvent faire pivoter la scène à l'aide des commandes de la souris. Lorsque nous ajoutons des boutons, ajoutons-en un pour rétablir la position par défaut de la caméra. Un autre <mwc-icon-button>
avec une liaison d'événement de clic effectuera la tâche.
export class BrickViewer extends LitElement {
private _resetCamera() {
this._controls.reset();
}
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add this button: -->
<mwc-icon-button @click=${this._resetCamera} icon="center_focus_strong"></mwc-icon-button>
</div>
`;
}
}
Consultation plus rapide
Certains jeux de briques ont de nombreuses marches. Un utilisateur peut vouloir passer à une étape spécifique. L'ajout d'un curseur avec des numéros de pas peut faciliter la navigation rapide. Pour cela, nous allons utiliser l'élément <mwc-slider>
.
curseur-mwc
L'élément curseur a besoin de quelques données importantes, comme les valeurs minimale et maximale du curseur. La valeur minimale du curseur peut toujours être "1". La valeur maximale du curseur doit être this._numConstructionSteps
si le modèle est chargé. Nous pouvons indiquer ces valeurs à <mwc-slider>
à l'aide de ses attributs. Nous pouvons également utiliser la directive lit-html ifDefined
pour éviter de définir l'attribut max
si la propriété _numConstructionSteps
n'a pas été définie.
Ajoutez un <mwc-slider>
entre le "retour" et "avancer" boutons:
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... backwards button -->
<!-- Add this slider: -->
<mwc-slider
step="1"
pin
markers
min="1"
max=${ifDefined(this._numConstructionSteps)}
></mwc-slider>
<!-- ... forwards button -->
</div>
`;
}
}
Données "en haut"
Lorsqu'un utilisateur déplace le curseur, l'étape de construction actuelle doit changer et la visibilité du modèle doit être mise à jour en conséquence. L'élément "curseur" émet un événement d'entrée chaque fois que vous faites glisser le curseur. Ajoutez une liaison d'événement sur le curseur lui-même pour intercepter cet événement et modifier l'étape de construction.
Ajoutez la liaison d'événements:
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add the @input event binding: -->
<mwc-slider
...
@input=${(e: CustomEvent) => this.step = e.detail.value}
></mwc-slider>
<!-- ... -->
</div>
`;
}
}
Bravo ! Nous pouvons utiliser le curseur pour changer l'étape à afficher.
Données "en panne"
Il y a encore une chose. Lorsque le bouton "Retour" et "suivant" servent à modifier l'étape, vous devez mettre à jour la poignée du curseur. Liez l'attribut de valeur de <mwc-slider>
à this.step
.
Ajoutez la liaison value
:
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add the value property binding: -->
<mwc-slider
...
value=${ifDefined(this.step)}
></mwc-slider>
<!-- ... -->
</div>
`;
}
}
Nous avons presque terminé le curseur. Ajoutez un style flexible pour qu'il s'intègre parfaitement aux autres commandes:
export class BrickViewer extends LitElement {
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
}
Nous devons également appeler layout
sur le curseur lui-même. Pour ce faire, nous allons utiliser la méthode de cycle de vie firstUpdated
, qui est appelée une fois le DOM positionné. Le décorateur query
peut nous aider à obtenir une référence à l'élément de curseur dans le modèle.
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
}
Voici tous les ajouts de curseurs (avec des attributs pin
et markers
supplémentaires sur le curseur pour plus de clarté):
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button @click=${this._restart} icon="replay"></mwc-icon-button>
<mwc-icon-button @click=${this._stepBack} icon="navigate_before"></mwc-icon-button>
<mwc-slider
step="1"
pin
markers
min="1"
max=${ifDefined(this._numConstructionSteps)}
?disabled=${this._numConstructionSteps === undefined}
value=${ifDefined(this.step)}
@input=${(e: CustomEvent) => this.constructionStep = e.detail.value}
></mwc-slider>
<mwc-icon-button @click=${this._stepForward} icon="navigate_next"></mwc-icon-button>
<mwc-icon-button @click=${this._resetCamera} icon="center_focus_strong"></mwc-icon-button>
</div>
`;
}
}
Voici le produit final !
7. Conclusion
Nous avons beaucoup appris sur l'utilisation de LitElement pour créer notre propre élément HTML. Nous avons appris à:
- Définir un élément personnalisé
- Déclarer une API d'attribut
- Afficher une vue pour un élément personnalisé
- Encapsuler les styles
- Utiliser des événements et des propriétés pour transmettre des données
Pour en savoir plus sur LitElement, consultez son site officiel.
Vous pouvez afficher un élément "brique-viewer" terminé à l'adresse stackblitz.com/edit/brick-viewer-complete.
briques-viewer est également envoyé sur NPM, et vous pouvez voir la source ici: dépôt GitHub.