1. Einführung
Webkomponenten
Webkomponenten sind eine Sammlung von Webstandards, die es Entwicklern ermöglichen, HTML-Code um benutzerdefinierte Elemente zu erweitern. In diesem Codelab definieren Sie das <brick-viewer>
-Element, mit dem Steinmodelle angezeigt werden können.
Leuchtelement
Zur Definition des benutzerdefinierten Elements <brick-viewer>
verwenden wir das Lit-Element. lit-element ist eine einfache Basisklasse, die dem Webkomponenten-Standard etwas syntaktischen Zucker hinzufügt. Dies erleichtert uns den Einstieg in unser benutzerdefiniertes Element.
Jetzt starten
Die Programmierung erfolgt in einer Stackblitz-Onlineumgebung. Öffnen Sie daher diesen Link in einem neuen Fenster:
stackblitz.com/edit/brick-viewer
Los gehts!
2. Benutzerdefiniertes Element definieren
Klassendefinition
Wenn Sie ein benutzerdefiniertes Element definieren möchten, erstellen Sie eine Klasse, die LitElement
erweitert, und dekorieren Sie sie mit @customElement
. Das Argument für @customElement
ist der Name des benutzerdefinierten Elements.
Gib in „brick-viewer.ts“ Folgendes ein:
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}
Jetzt kann das <brick-viewer></brick-viewer>
-Element in HTML verwendet werden. Aber wenn Sie es ausprobieren, wird nichts gerendert. Das sollte nicht sein.
Renderingmethode
Um die Ansicht der Komponente zu implementieren, definieren Sie eine Methode namens render. Diese Methode sollte ein Vorlagenliteral zurückgeben, das mit der Funktion html
getaggt ist. Geben Sie den gewünschten HTML-Code in das getaggte Vorlagenliteral ein. Wird gerendert, wenn Sie <brick-viewer>
verwenden.
Fügen Sie die Methode render
hinzu:
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick viewer</div>`;
}
}
3. LDraw-Datei angeben
Property definieren
Es wäre großartig, wenn ein Nutzer von <brick-viewer>
mithilfe eines Attributs angeben könnte, welches Ziegelmodell angezeigt werden soll:
<brick-viewer src="path/to/model.ldraw"></brick-viewer>
Da wir ein HTML-Element erstellen, können wir die deklarative API nutzen und ein Quellattribut genau wie ein <img>
- oder <video>
-Tag definieren. Mit Lit-Elementen ist es genauso einfach wie das Dekorieren einer Klasseneigenschaft mit @property
. Mit der Option type
können Sie angeben, wie das Lichtelement die Eigenschaft parst, um sie als HTML-Attribut zu verwenden.
Definieren Sie die Property und das Attribut src
:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
<brick-viewer>
hat jetzt ein src
-Attribut, das wir in HTML festlegen können. Sein Wert ist dank des Lit-Elements bereits innerhalb unserer BrickViewer
-Klasse lesbar.
Werte anzeigen
Wir können den Wert des Attributs src
anzeigen, indem wir ihn im Vorlagenliteral der Renderingmethode verwenden. Interpolieren Sie Werte mit der ${value}
-Syntax in Vorlagenliterale.
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
Jetzt sehen wir den Wert des Attributs „src“ im Element <brick-viewer>
des Fensters. Versuchen Sie Folgendes: Öffnen Sie die Entwicklertools Ihres Browsers und ändern Sie das Attribut src manuell. Probieren Sie es aus...
Haben Sie bemerkt, dass der Text im Element automatisch aktualisiert wird? Das lit-Element beobachtet die mit @property
dekorierten Klasseneigenschaften und rendert die Ansicht noch einmal für Sie. übernimmt den Großteil der Arbeit, sodass Sie sich nicht darum kümmern müssen.
4. Mit Three.js die Szene festlegen
Licht, Kamera, Rendering!
Unser benutzerdefiniertes Element verwendet 3.js, um unsere 3D-Bausteinmodelle zu rendern. Einige Dinge möchten wir für jede Instanz eines <brick-viewer>
-Elements nur einmal ausführen, z. B. die 3.js-Szene, die Kamera und die Beleuchtung einrichten. Wir fügen diese dem Konstruktor der BrickViewer-Klasse hinzu. Wir behalten einige Objekte als Klasseneigenschaften bei, damit wir sie später verwenden können: Kamera, Szene, Steuerelemente und Renderer.
Fügen Sie die 3.js-Szeneneinrichtung hinzu:
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);
};
}
Das WebGLRenderer
-Objekt stellt ein DOM-Element bereit, mit dem die gerenderte Three.js-Szene angezeigt wird. Der Zugriff erfolgt über die Property domElement
. Wir können diesen Wert mithilfe der Syntax ${value}
in das Renderingvorlagenliteral interpolieren.
Entfernen Sie die src
-Nachricht aus der Vorlage und fügen Sie das DOM-Element des Renderers ein:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
Damit das dom-Element des Renderers vollständig angezeigt wird, müssen wir auch das <brick-viewer>
-Element selbst auf display: block
setzen. Wir können Stile in einer statischen Eigenschaft namens styles
bereitstellen, die auf ein css
-Vorlagenliteral festgelegt ist.
Fügen Sie der Klasse diesen Stil hinzu:
export class BrickViewer extends LitElement {
static styles = css`
/* The :host selector styles the brick-viewer itself! */
:host {
display: block;
}
`;
}
<brick-viewer>
zeigt jetzt eine gerenderte Three.js-Szene an:
Aber... es ist leer. Wir stellen dafür ein Modell bereit.
Ziegellader
Wir übergeben die zuvor definierte src
-Eigenschaft an den LDrawLoader, der mit drei.js ausgeliefert wird.
Mit LDraw-Dateien können Sie ein Ziegelmodell in separate Gebäudeschritte unterteilen. Über die LDrawLoader API kann auf die Gesamtzahl der Schritte und die Sichtbarkeit einzelner Bausteine zugegriffen werden.
Kopieren Sie diese Eigenschaften, die neue Methode _loadModel
und die neue Zeile im Konstruktor:
@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();
});
}
}
Wann soll _loadModel
aufgerufen werden? Sie muss jedes Mal aufgerufen werden, wenn sich das Attribut „src“ ändert. Durch das Dekorieren der src
-Eigenschaft mit @property
haben wir die Eigenschaft für den Aktualisierungszyklus des Litigation-Elements aktiviert. Wenn eine dieser dekorierten Unterkünfte wird eine Reihe von Methoden aufgerufen, die auf die neuen und alten Werte der Eigenschaften zugreifen können. Die Lebenszyklusmethode, an der wir interessiert sind, heißt update
. Die Methode update
verwendet ein PropertyValues
-Argument, das Informationen zu allen gerade geänderten Attributen enthält. Hier können Sie _loadModel
anrufen.
Fügen Sie die Methode update
hinzu:
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
super.update(changedProperties);
}
}
Unser <brick-viewer>
-Element kann jetzt eine Brick-Datei darstellen, die mit dem src
-Attribut angegeben wird.
5. Teilmodelle anzeigen
Lassen Sie uns nun den aktuellen Konstruktionsschritt konfigurierbar machen. Wir möchten gerne <brick-viewer step="5"></brick-viewer>
angeben und sollten uns ansehen, wie das Ziegelmodell im fünften Bauschritt aussieht. Dazu machen wir die Eigenschaft step
zu einer beobachteten Eigenschaft, indem wir sie mit @property
dekorieren.
Dekoriere die Property step
:
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
Jetzt fügen wir eine Hilfsmethode hinzu, die nur die Bausteine bis zum aktuellen Schritt sichtbar macht. Wir rufen das Hilfsprogramm in der Aktualisierungsmethode auf, damit sie jedes Mal ausgeführt wird, wenn das Attribut step
geändert wird.
Aktualisieren Sie die Methode update
und fügen Sie die neue Methode _updateBricksVisibility
hinzu:
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);
}
}
Öffnen Sie jetzt die Entwicklertools Ihres Browsers und prüfen Sie das Element <brick-viewer>
. Fügen Sie dazu ein step
-Attribut hinzu:
Beobachten Sie, was mit dem gerenderten Modell geschieht. Mit dem Attribut step
können Sie festlegen, wie viel vom Modell angezeigt wird. So sollte es aussehen, wenn das Attribut step
auf "10"
gesetzt ist:
6. Navigation im Baustein
Symbol-Schaltfläche für MWC
Der Endnutzer unseres <brick-viewer>
sollte auch in der Lage sein, die Build-Schritte über die Benutzeroberfläche aufzurufen. Jetzt fügen wir Schaltflächen für den nächsten,
vorherigen und ersten Schritt hinzu. Wir verwenden die Schaltflächen-Webkomponente von Material Design, um dies zu vereinfachen. Da @material/mwc-icon-button
bereits importiert wurde, können wir <mwc-icon-button></mwc-icon-button>
einfügen. So können wir das Symbol angeben, das mit dem icon-Attribut verwendet werden soll: <mwc-icon-button icon="thumb_up"></mwc-icon-button>
. Alle möglichen Symbole finden Sie hier: material.io/resources/icons.
Fügen wir der Rendering-Methode einige Symbolschaltflächen hinzu:
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>
`;
}
}
Dank der Webkomponenten ist die Verwendung von Material Design auf unserer Seite so einfach.
Ereignisbindungen
Diese Schaltflächen sollten eigentlich eine Funktion haben. Die Antwort Schaltfläche sollte den Bauschritt auf 1 zurücksetzen. Der Parameter „navi_before“ -Schaltfläche sollte die Zahl der Bauschritte verringern und die Schaltfläche -Schaltfläche erhöht werden soll. Mit dem lit-Element können Sie diese Funktion mithilfe von Ereignisbindungen ganz einfach hinzufügen. Verwenden Sie in Ihrem HTML-Vorlagenliteral die Syntax @eventname=${eventHandler}
als Elementattribut. eventHandler
wird jetzt ausgeführt, wenn ein eventname
-Ereignis für dieses Element erkannt wird. Als Beispiel fügen wir unseren drei Symbolschaltflächen Click-Event-Handler hinzu:
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>
`;
}
}
Versuche jetzt, auf die Schaltflächen zu klicken. Sehr gut!
Stile
Die Schaltflächen funktionieren, aber sie sehen nicht gut aus. Sie sind alle unten eingedrungen. Lassen Sie uns diese so gestalten, dass sie in der Szene eingeblendet werden.
Um Stile auf diese Schaltflächen anzuwenden, kehren wir zur static styles
-Eigenschaft zurück. Da diese Stile auf einen Umfang beschränkt sind, können sie nur auf Elemente innerhalb dieser Webkomponente angewendet werden. Das ist einer der großen Freuden beim Schreiben von Webkomponenten: Selektoren können einfacher sein und CSS ist einfacher zu lesen und zu schreiben. Tschüss, BEM!
Ändern Sie die Stile so, dass sie wie folgt aussehen:
export class BrickViewer extends LitElement {
static styles = css`
:host {
display: block;
position: relative;
}
#controls {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
}
`;
}
Schaltfläche „Kamera zurücksetzen“
Endnutzer unseres <brick-viewer>
können die Szene mit der Maus drehen. Während wir weitere Schaltflächen hinzufügen, füge ich eine hinzu, um die Kamera auf die Standardposition zurückzusetzen. Ein anderer <mwc-icon-button>
mit einer Klickereignisbindung erledigt den Job.
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>
`;
}
}
Schnellere Navigation
Einige Bausteine haben viele Stufen. Ein Nutzer möchte möglicherweise zu einem bestimmten Schritt springen. Wenn Sie einen Schieberegler mit den Schrittnummern hinzufügen, können Sie schneller navigieren. Dazu verwenden wir das Element <mwc-slider>
.
MWC-Schieberegler
Für das Schiebereglerelement sind einige wichtige Daten erforderlich, z. B. der Mindest- und Höchstwert des Schiebereglers. Der Mindestwert des Schiebereglers kann immer „1“ sein. Der maximale Schiebereglerwert sollte this._numConstructionSteps
sein, wenn das Modell geladen wurde. Diese Werte können wir <mwc-slider>
anhand ihrer Attribute mitteilen. Wir können auch die ifDefined
lit-html-Anweisung verwenden, um das Festlegen des Attributs max
zu vermeiden, wenn die Eigenschaft _numConstructionSteps
nicht definiert wurde.
<mwc-slider>
zwischen „Zurück“ hinzufügen und „vorwärts“ Schaltflächen:
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>
`;
}
}
Daten „nach oben“
Wenn ein Nutzer den Schieberegler bewegt, sollte sich der aktuelle Konstruktionsschritt ändern und die Sichtbarkeit des Modells entsprechend aktualisiert werden. Das Schiebereglerelement gibt jedes Mal ein Eingabeereignis aus, wenn der Schieberegler gezogen wird. Fügen Sie dem Schieberegler selbst eine Ereignisbindung hinzu, um dieses Ereignis zu erfassen und den Erstellungsschritt zu ändern.
Fügen Sie die Ereignisbindung hinzu:
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>
`;
}
}
Sehr gut! Mit dem Schieberegler können Sie ändern, welcher Schritt angezeigt wird.
Daten „nach unten“
Es gibt noch eine Sache. Wenn der „Zurück“-Pfeil und „Weiter“ verwenden, um den Schritt zu ändern, muss der Ziehpunkt des Schiebereglers aktualisiert werden. Binden Sie das Wertattribut von <mwc-slider>
an this.step
.
Fügen Sie die Bindung value
hinzu:
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>
`;
}
}
Wir sind mit dem Schieberegler fast fertig. Fügen Sie einen flexiblen Stil hinzu, damit es harmonisch zu den anderen Steuerelementen passt:
export class BrickViewer extends LitElement {
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
}
Außerdem muss layout
für das Schiebereglerelement selbst aufgerufen werden. Dies geschieht in der Lebenszyklusmethode firstUpdated
, die aufgerufen wird, sobald das erste DOM-Layout erstellt wurde. Mit dem query
-Decorator können wir einen Verweis auf das Schiebereglerelement in der Vorlage abrufen.
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
}
Hier sehen Sie alle Ergänzungen der Schieberegler zusammen (mit zusätzlichen pin
- und markers
-Attributen auf dem Schieberegler, damit der Schieberegler cooler wirkt):
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>
`;
}
}
Das ist das Endprodukt!
7. Fazit
Wir haben viel darüber gelernt, wie wir mithilfe von Lit-Elementen unser eigenes HTML-Element erstellen können. Wir haben Folgendes gelernt:
- Benutzerdefiniertes Element definieren
- Attribut-API deklarieren
- Ansicht für ein benutzerdefiniertes Element rendern
- Stile einschließen
- Daten mithilfe von Ereignissen und Properties übergeben
Weitere Informationen zu Licht-Elementen findest du auf der offiziellen Website.
Sie können sich ein fertiggestelltes Brick-Viewer-Element unter stackblitz.com/edit/brick-viewer-complete ansehen.
Brick-Viewer ist auch über NPM verfügbar. Die Quelle finden Sie hier: GitHub-Repository.