1. Introduzione
Componenti web
I componenti web sono una raccolta di standard web che consentono agli sviluppatori di estendere l'HTML con elementi personalizzati. In questo codelab, definirai l'elemento <brick-viewer>
, che potrà mostrare i modelli di blocchi.
elemento-lit
Per aiutarci a definire il nostro elemento personalizzato <brick-viewer>
, utilizzeremo lit-element. lit-element è una classe di base leggera che aggiunge alcune funzionalità di sintassi allo standard dei componenti web. In questo modo sarà più facile per noi iniziare a utilizzare il nostro elemento personalizzato.
Inizia
Faremo il codice in un ambiente Stackblitz online, quindi apri questo link in una nuova finestra:
stackblitz.com/edit/brick-viewer
Iniziamo.
2. Definisci un elemento personalizzato
Definizione del corso
Per definire un elemento personalizzato, crea una classe che amplia LitElement
e decoralo con @customElement
. L'argomento per @customElement
sarà il nome dell'elemento personalizzato.
In Brick-viewer.ts, metti:
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}
Ora l'elemento <brick-viewer></brick-viewer>
è pronto per essere utilizzato in HTML. Tuttavia, se provi, non verrà visualizzato nulla. Risolviamo il problema.
Metodo di rendering
Per implementare la visualizzazione del componente, definisci un metodo denominato render. Questo metodo dovrebbe restituire un valore letterale di modello con la funzione html
. Inserisci il codice HTML che preferisci nel literal del modello taggato. Verrà visualizzata quando usi <brick-viewer>
.
Aggiungi il metodo render
:
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick viewer</div>`;
}
}
3. Specifica del file LDraw
Definire una proprietà
Sarebbe bello se un utente dell'<brick-viewer>
potesse specificare il modello di mattoncino da mostrare utilizzando un attributo, ad esempio:
<brick-viewer src="path/to/model.ldraw"></brick-viewer>
Poiché stiamo creando un elemento HTML, possiamo sfruttare l'API dichiarativa e definire un attributo source, proprio come un tag <img>
o <video>
. Con l'elemento lit-element, è facile come decorare una proprietà di classe con @property
. L'opzione type
consente di specificare in che modo lit-element analizza la proprietà per l'utilizzo come attributo HTML.
Definisci la proprietà e l'attributo src
:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
<brick-viewer>
ora ha un attributo src
che possiamo impostare in HTML. Il suo valore è già leggibile all'interno della nostra classe BrickViewer
grazie a lit-element.
Visualizzazione dei valori
Possiamo visualizzare il valore dell'attributo src
utilizzandolo nel valore letterale del modello del metodo di rendering. Interpola i valori nei valori letterali del modello utilizzando la sintassi ${value}
.
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
Ora vediamo il valore dell'attributo src nell'elemento <brick-viewer>
nella finestra. Prova a fare così: apri gli strumenti per sviluppatori del browser e modifica manualmente l'attributo src. Vai, prova…
...Hai notato che il testo dell'elemento si aggiorna automaticamente? lit-element osserva le proprietà della classe decorate con @property
e visualizza di nuovo la vista al posto tuo! lit-element fa il resto, così non devi farlo tu.
4. Creare la scena con Three.js
Luci, fotocamera, rendering!
Il nostro elemento personalizzato utilizzerà tre.js per eseguire il rendering dei nostri modelli di mattoncini 3D. Ci sono alcune cose che vogliamo fare una sola volta per ogni istanza di un elemento <brick-viewer>
, ad esempio configurare la scena terza.js, la fotocamera e l'illuminazione. Li aggiungeremo al costruttore della classe BrickViewer. Manterremo alcuni oggetti come proprietà di classe per poterli utilizzare in un secondo momento: camera, scene, controls e renderer.
Aggiungi la configurazione della scena 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'oggetto WebGLRenderer
fornisce un elemento DOM che mostra la scena terza.js sottoposta a rendering. Si accede tramite la proprietà domElement
. Possiamo interpolare questo valore nel valore letterale del modello di rendering, utilizzando la sintassi ${value}
.
Rimuovi il messaggio src
presente nel modello e inserisci l'elemento DOM del renderer:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
Per consentire la visualizzazione completa dell'elemento dom del renderer, è necessario impostare anche l'elemento <brick-viewer>
su display: block
. Possiamo fornire gli stili in una proprietà statica denominata styles
, impostata su un valore letterale di modello css
.
Aggiungi questo stile al corso:
export class BrickViewer extends LitElement {
static styles = css`
/* The :host selector styles the brick-viewer itself! */
:host {
display: block;
}
`;
}
Ora <brick-viewer>
sta mostrando una scena terza.js sottoposta a rendering:
Ma... è vuota. Forniamo un modello.
Caricatore per mattoncini
Passeremo la proprietà src
che abbiamo definito in precedenza a LDrawLoader, che viene fornito con tre.js.
I file LDraw possono separare un modello Brick in passaggi di costruzione distinti. Il numero totale di passaggi e la visibilità dei singoli mattoncini sono accessibili tramite l'API LDrawLoader.
Copia queste proprietà, il nuovo metodo _loadModel
e la nuova riga nel costruttore:
@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();
});
}
}
Quando deve essere chiamato _loadModel
? Deve essere invocato ogni volta che l'attributo src cambia. Decorando la proprietà src
con @property
, abbiamo attivato il ciclo di vita dell'aggiornamento degli elementi accesi. Ogni volta che il valore di una di queste proprietà decorate cambia, viene chiamata una serie di metodi che possono accedere ai valori nuovi e precedenti delle proprietà. Il metodo del ciclo di vita che ci interessa si chiama update
. Il metodo update
accetta un argomento PropertyValues
, che conterrà informazioni su eventuali proprietà appena modificate. Questo è il posto perfetto per chiamare _loadModel
.
Aggiungi il metodo update
:
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
super.update(changedProperties);
}
}
Il nostro elemento <brick-viewer>
ora può visualizzare un file brick, specificato con l'attributo src
.
5. Visualizzazione di modelli parziali
Ora rendi configurabile il passaggio di costruzione corrente. Vorremmo essere in grado di specificare <brick-viewer step="5"></brick-viewer>
e dovremmo vedere come appare il modello del mattone nel quinto passaggio della costruzione. A tale scopo, rendiamo la proprietà step
una proprietà osservata decorandola con @property
.
Decora la proprietà step
:
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
Ora aggiungeremo un metodo di supporto che rende visibili solo i blocchi fino al passaggio di compilazione corrente. Chiameremo l'helper nel metodo di aggiornamento in modo che venga eseguito ogni volta che la proprietà step
viene modificata.
Aggiorna il metodo update
e aggiungi il nuovo metodo _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);
}
}
Ora apri devtools del browser e ispeziona l'elemento <brick-viewer>
. Aggiungi un attributo step
, in questo modo:
Guarda cosa succede al modello sottoposto a rendering. Possiamo utilizzare l'attributo step
per controllare la quantità di modello da mostrare. Ecco come dovrebbe apparire quando l'attributo step
è impostato su "10"
:
6. Navigazione sul set di mattoncini
pulsante-icona-mwc
L'utente finale del nostro <brick-viewer>
deve anche essere in grado di navigare nei passaggi di compilazione tramite l'interfaccia utente. Aggiungiamo pulsanti per andare al passaggio successivo, al passaggio precedente e al primo passaggio. Per semplificare l'operazione, utilizzeremo il componente web dei pulsanti di Material Design. Poiché @material/mwc-icon-button
è già importato, possiamo inserirlo in <mwc-icon-button></mwc-icon-button>
. Possiamo specificare l'icona che vogliamo utilizzare con l'attributo icon, come segue: <mwc-icon-button icon="thumb_up"></mwc-icon-button>
. Tutte le possibili icone sono disponibili qui: material.io/resources/icons.
Aggiungiamo alcuni pulsanti con icone al metodo di rendering:
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>
`;
}
}
Utilizzare Material Design nella nostra pagina è semplicissimo grazie ai componenti web.
Associazioni di eventi
Questi pulsanti dovrebbero fare qualcosa. Il pulsante "Rispondi" dovrebbe reimpostare il passaggio di creazione su 1. Il pulsante "navigate_before" deve decrementare il passaggio di costruzione e il pulsante "navigate_next" deve incrementarlo. lit-element semplifica l'aggiunta di questa funzionalità con le associazioni di eventi. Nel valore letterale del modello HTML, utilizza la sintassi @eventname=${eventHandler}
come attributo dell'elemento. Ora eventHandler
verrà eseguito quando verrà rilevato un evento eventname
su quell'elemento. Ad esempio, aggiungiamo gestori di eventi di clic ai tre pulsanti con icone:
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>
`;
}
}
Prova a fare clic sui pulsanti ora. Bene!
Stili
I pulsanti funzionano, ma non hanno un bell'aspetto. Sono tutti rannicchiati sul fondo. Scegli uno stile per sovrapporle alla scena.
Per applicare gli stili a questi pulsanti, torniamo alla proprietà static styles
. Questi stili sono limitati, il che significa che verranno applicati solo agli elementi all'interno di questo componente web. Questa è una delle gioie della scrittura dei componenti web: i selettori possono essere più semplici e i CSS sono più facili da leggere e scrivere. Arrivederci, BEM.
Aggiorna gli stili in modo che abbiano il seguente aspetto:
export class BrickViewer extends LitElement {
static styles = css`
:host {
display: block;
position: relative;
}
#controls {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
}
`;
}
Pulsante di reimpostazione della videocamera
Gli utenti finali di <brick-viewer>
possono ruotare la scena utilizzando i controlli del mouse. Mentre aggiungiamo i pulsanti, aggiungine uno per reimpostare la posizione predefinita della fotocamera. Un altro <mwc-icon-button>
con un'associazione di eventi di clic completerà il lavoro.
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>
`;
}
}
Navigazione più rapida
Alcuni mattoncini hanno molti gradini. Un utente potrebbe voler passare a un passaggio specifico. L'aggiunta di un cursore con numeri di passaggi può essere utile per la navigazione rapida. A tale scopo utilizzeremo l'elemento <mwc-slider>
.
mwc-slider
L'elemento del dispositivo di scorrimento richiede alcuni dati importanti, come il valore minimo e massimo del dispositivo di scorrimento. Il valore minimo del cursore può sempre essere "1". Se il modello è stato caricato, il valore massimo del cursore deve essere this._numConstructionSteps
. Possiamo indicare a <mwc-slider>
questi valori tramite i suoi attributi. Possiamo anche utilizzare l'directive lit-html ifDefined
per evitare di impostare l'attributo max
se la proprietà _numConstructionSteps
non è stata definita.
Aggiungi un <mwc-slider>
tra i pulsanti "Indietro" e "Avanti":
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>
`;
}
}
Dati "up"
Quando un utente sposta il cursore, il passaggio di costruzione corrente deve cambiare e la visibilità del modello deve essere aggiornata di conseguenza. L'elemento del cursore emetterà un evento di input ogni volta che viene trascinato. Aggiungi un'associazione di eventi sul dispositivo di scorrimento per individuare l'evento e modificare il passaggio di costruzione.
Aggiungi l'associazione di eventi:
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>
`;
}
}
Evviva! Possiamo usare il cursore per cambiare il passaggio visualizzato.
Dati "in calo"
C'è un'altra cosa. Quando vengono utilizzati i pulsanti "Indietro" e "Avanti" per modificare il passaggio, il punto di manipolazione del cursore deve essere aggiornato. Associa l'attributo del valore di <mwc-slider>
a this.step
.
Aggiungi l'associazione 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>
`;
}
}
Abbiamo quasi finito con il cursore. Aggiungi uno stile flex per integrarlo bene con gli altri controlli:
export class BrickViewer extends LitElement {
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
}
Inoltre, dobbiamo chiamare layout
sull'elemento di scorrimento stesso. Lo faremo nel metodo del ciclo di vita firstUpdated
, che viene chiamato dopo la prima impaginazione del DOM. Il decoratore query
può aiutarci a ottenere un riferimento all'elemento del dispositivo di scorrimento nel modello.
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
}
Di seguito sono riportate tutte le aggiunte dei dispositivi di scorrimento (con attributi pin
e markers
aggiuntivi sul cursore per conferire un aspetto accattivante):
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>
`;
}
}
Ecco il prodotto finale.
7. Conclusione
Abbiamo imparato molto su come utilizzare lit-element per creare il nostro elemento HTML. Abbiamo imparato a:
- Definisci un elemento personalizzato
- Dichiarare un'API di attributi
- Visualizzare una vista per un elemento personalizzato
- Incapsulare gli stili
- Utilizzare eventi e proprietà per trasmettere i dati
Per saperne di più su lit-element, puoi leggere il loro sito ufficiale.
Puoi vedere un elemento completo di Brick-viewer all'indirizzo stackblitz.com/edit/brick-viewer-complete.
Brick-viewer viene anche spedito su Gestione dei partner di rete e puoi visualizzare il codice qui: Repository GitHub.