Componente pagini web SVG pentru IoT și producători (Partea a 2-a)
Publicat: 2022-03-10Deci, avem deja modalități de a încărca dinamic un meniu de pictograme SVG făcute să reacționeze prin încărcarea panourilor dacă dorim, dar pictogramele nu erau componente reale. Am putut folosi un truc simplu de a aduce SVG pentru fiecare pictogramă și de a-l trece în aplicația Vue. A fost suficient de simplu pentru a genera o listă de pictograme și fiecare pictogramă a reacționat într-un mod similar, cu excepția micilor diferențe de date. Diferența de date a făcut posibilă legarea numelui unui panou la fiecare pictogramă, astfel încât operatorul pentru clicul pe butonul pictogramei să-l transmită mai departe.
Când un panou este încărcat sub forma unei componente Vue, totul despre panou și componentele sale trebuie încărcat, șabloane, JavaScript și multe altele. Deci, sarcina de a gestiona doar încărcarea panoului este mai mare decât ceea ce am întâlnit până acum în această discuție.
Să ne uităm la modul Vue de a oferi un cârlig pentru încărcarea asincronă. Următorul fragment este din ghidul Vue.
Vue.component('async-example', function (resolve, reject) { setTimeout(function () { // Pass the component definition to the resolve callback resolve({ template: '<div>I am async!</div>' }) }, 1000) })
Ghidul ne spune că funcția setTimeout este un exemplu de utilizare a sincronicității cu componentele Vue. Observați că acolo unde înainte exista un obiect ca al doilea parametru pentru Vue.component
, acum există o funcție, care este denumită funcție din fabrică. În cadrul apelului resolve
este o definiție a componentei, care ar fi fost al doilea parametru pentru Vue.component
înainte.
Așadar, a trebuit să mă uit la acest exemplu un timp înainte să aibă sens pentru mine. Iată un alt exemplu, care mi se potrivește mai bine:
Vue.component('async-example', function (resolve, reject) { // Vue will call this function and promise itself to handle // it when it gets back with data. // this function can then call a promising object loader // here the 'loader' function is some abstract function. // Most likely the application will use 'fetch' // but it could be something else. loader('/my/resource/on/server.json'). then(function (JSON_data) { var object = transformJSONToJSObject(JSON_data); resolve(object) }).catch( (error) => { handle it } );
Pare a fi lucrul corect de făcut pentru a face o funcție mai generală pentru a parcurge acest formular.
function componentLoader(c_name,resource_url) { Vue.component(c_name, function (resolve, reject) { loader(resource_url). then(function (JSON_data) { var object = transformJSONToJSObject(JSON_data); resolve(object) }).catch( (error) => { handle it } ); }
Deci, în general, pentru a încărca o componentă, am avea nevoie doar de o linie ca următoarea:
componentLoader('ThermoPanel','./JSON/thermo-panel.json');
Deci acum, care este JSON-ul care este încărcat? Poate include totul despre componentă. În acest caz, ca componentă a panoului, poate include termometre, comutatoare ale mașinii, glisoare, manometre și multe altele. Deși părea mai plăcut să păstrezi părțile componente pe pagina web, ar putea funcționa mai bine să folosești câmpul subcomponent care este în exemplul mai lung pentru „termo-panou” pe care l-am făcut înainte și, de asemenea, pentru celelalte panouri construite în mod similar. JSON va conține o structură completă a panoului.
Cu toate acestea, dacă cititorul va observa includerea apelului de funcție pentru transformJSONToJSObject
, el va înțelege că JSON ar putea fi codificat într-un fel pentru a face transportul mai ușor și pentru a face mai ușor ca un server să gestioneze definiția. La urma urmei, definiția va include șabloane SVG complete, definiții de funcții și alte expresii JavaScript. De asemenea, obiectul JSON poate conține mai mult decât definiția panoului, deoarece unele informații pot ajuta pur și simplu în contabilitate sau validare. Deci, se poate aștepta ca obiectul să fie tratat la primire.
În ceea ce privește codificarea, datele care vin de la server pot fi codificate în mai multe moduri. Poate că va fi pur și simplu codificat URL. Sau mai sigur, ar putea fi cifrat. Pentru această discuție, putem folosi doar codificarea URL.
Unele dintre instrumentele care sunt disponibile pentru crearea aplicațiilor Vue se ocupă, fără îndoială, de transformarea JSON. Dar, această discuție a evitat până acum utilizarea instrumentelor din linia de comandă. Această omisiune nu este atât de rea, deoarece am folosit și Vue cu un minim de resurse, folosind o singură etichetă de script pentru referirea la CDN. Cu toate acestea, cu siguranță vă recomand să vă uitați la instrumentele din linia de comandă, în special pentru organizarea proiectelor.
Când JSON ajunge la pagină, având în vedere că componenta este complet asamblată cu subcomponente, nu mai trebuie depusă muncă pentru a prelua piesele. Putem presupune că toate componentele vor fi complet definite pentru restul acestei discuții. Dar, asamblarea ierarhiilor de componente complete va necesita instrumente de linie de comandă la un moment dat.
Procesul de editare SVG va necesita, de asemenea, ceva muncă. Procesele de editare SVG permit unui designer să deseneze un panou și toate componentele de pe acesta. Dar, fiecare subcomponentă trebuie să fie identificată, numită într-un grup sau să i se acorde un loc titular. Orice abordare a utilizării desenului necesită un anumit tratament al SVG, astfel încât etichetele componentei Vue să poată înlocui grupurile sau elementele grafice. În acest fel, orice redare de artist poate deveni un șablon. Și, subcomponentele desenate vor trebui să fie dezasamblate în șabloane pentru subcomponentele Vue.
Acest tip de parcimonie este contrar fluxului de lucru al majorității cadrelor JavaScript. Frame-urile sunt despre asamblarea paginilor. Dar, editarea sau desenul, rezultă ceva deja asamblat de un artist. În practică, rezultatul editării nu oferă un fișier text care să corespundă direct unei definiții de componentă a cadrului.
Mai multe despre procesul de editare pot fi luate în considerare în alte discuții. Sunt multe. Dar, deocamdată, avem instrumentele de care avem nevoie pentru a încărca componente ierarhale și a le face să prindă viață.
Aplicația Leneșă
Pentru construcția panoului nostru IoT, avem deja o bară de selecție care răspunde la căutări. Și, avem o modalitate de a încărca componente atunci când avem nevoie de ele. Trebuie doar să conectăm aceste părți. Și, în sfârșit, trebuie să ne asigurăm că panourile apar și că încep să funcționeze când apar.
Încărcarea leneșă a panourilor realizată de codul asincron de mai sus oferă o schiță a unei idei. Dar, din fericire, unii oameni au experimentat pentru a găsi modalități de a se asigura că pot fi încărcate tot felul de componente. Există o intrare codepen care arată cum să actualizați aplicațiile Vue cu componente noi de diferite tipuri. Acesta este mecanismul necesar pentru actualizarea unei părți desemnate a paginii cu diferite tipuri de panouri.
Cu posibilitatea de a adăuga diferite tipuri de panouri și cu un mecanism simplu de încărcare a definițiilor acestora, putem, în sfârșit, să avem pagina noastră de căutare a panourilor.
Iată codul HTML de care avem nevoie în pagina noastră, astfel încât aplicația Vue să poată plasa componente în mod dinamic:
<template v-for="(panel, index) in panelList"> <component :is="panel" :key="panel.name"></component> </template>
Eticheta component
este o metaetichetă Vue. Consultați referința pentru componentele dinamice. Proprietățile, atributele speciale, utilizate pentru eticheta de component
în acest caz sunt is și cheia. Atributul is
există pentru componentele dinamice. Și, key
asigură că noii copii vor avea identități diferite unul față de celălalt și îl ajută pe Vue să decidă ce să deseneze.
„Copiii aceluiași părinte comun trebuie să aibă chei unice. Cheile duplicate vor cauza erori de randare.”
Eticheta template
va trece prin componentele care sunt furnizate în câmpul de date panelList
al aplicației.
Deci, începând cu definiția Vue la nivel de aplicație pentru aplicația pictogramă, putem face modificări pentru a include panelList în elementele de date. (Să-i spunem acum panelApp).
var panelApp = new Vue({ el: '#PanelApp', data: { iconList: [ // Where is the data? Still on the server. ], panelList: [ ], queryToken : "Thermo Batches" // picked a name for demo }, methods : { goGetPanel: function (pname) { // var url = panelURL(pname); // this is custom to the site. fetch(url).then((response) => { // this is now browser native response.text().then((text) => { var newData = decodeURIComponent(text); eval(pHat); // widgdef = object def, must be assignment pHat = widgdef; var pnameHat = pname + pcount++; pHat.name = pnameHat; // this is needed for the key this.panelList.push(pHat); // now it's there. }).catch( error => { /* handle it */ }); } } });
Pe lângă adăugarea în panou, goGetPanel
este acum într-o formă necesară pentru obținerea unei definiții a componentei dintr-o bază de date sau alt magazin. Partea server trebuie să fie atentă la livrarea codului JavaScript în formatul corect. Cât despre cum arată obiectul venind de pe server, l-am văzut deja. Este tipul de obiect folosit ca parametru pentru Vue.component
.
Iată corpul complet al aplicației Vue, care oferă un meniu ca rezultat al căutării și un loc pentru a pune panouri preluate de pe server atunci când utilizatorul face clic pe o pictogramă.
<div> <!-- Recognize the name from the Vue doc --> <div> <h2 itemprop="name">Request MCU Groups</h2> <p itemprop="description">These are groups satistfying this query: {{queryToken}}.</p> <button>Find All</button> <button>Find 5 Point</button> <button>Find 6 Point</button> </div> <!-- Here is a Vue loop for generating a lit --> <div class="entryart"> <button v-for="iconEntry in iconList" @click="goGetPanel(iconEntry.name)" > <div v-html="iconEntry.icon"> </div> </button> </div> <div class="entryart" > <template v-for="(panel, index) in panelList"> <component :is="panel" :key="panel.name" :ref="panel.name" ></component> </template> </div> </div>
În ultimul div
, eticheta de component
are acum un parametru ref
legat de numele panoului. Parametrul ref permite aplicației Vue să identifice ce componentă să actualizeze cu date și să păstreze componentele separate. Parametrii ref
permit, de asemenea, accesul aplicației noastre la noile componente încărcate dinamic.
Într-o versiune de testare a aplicației panou, am următorul handler de intervale:
setInterval(() => { var refall = panelApp.$refs; // all named children that panels for ( var pname in refall ) { // in an object var pdata = refall[pname][0]; // off Vue translation, but it's there. pdata.temp1 = Math.round(Math.random()*100); // make thermos jump around. pdata.temp2 = Math.round(Math.random()*100); } },2000)
Codul oferă o mică animație, schimbând termometrele aleatoriu. Fiecare panou are două termometre, iar aplicația permite utilizatorului să continue să adauge panouri. (În versiunea finală, unele panouri trebuie aruncate.) Refs-urile sunt accesate folosind panelApp.$refs
refs
un câmp pe care Vue îl creează având în vedere informațiile referințe din eticheta component
.
Așadar, așa arată termometrele cu sărituri aleatorii într-un singur instantaneu:
Conectarea panoului la dispozitivul IoT
Deci, ultima bucată de cod este un test setInterval
care actualizează termometrele cu valori aleatorii la fiecare două secunde. Dar ceea ce vrem să facem este să citim în date reale de la mașini reale. Pentru a face asta, vom avea nevoie de o formă de comunicare.
Există o varietate de moduri. Dar, să folosim MQTT, care este un sistem de mesaje pub/sub. SPWA nostru se poate abona la mesaje de pe dispozitive în orice moment. Când primește aceste mesaje, SPWA poate direcționa fiecare mesaj către gestionarea de date adecvată pentru panoul mapat la dispozitivul identificat în mesaj.
Deci, practic, ceea ce trebuie să facem este să înlocuim setInterval
cu un handler de răspuns. Și asta va fi pentru un singur panou. Probabil că vrem să mapăm panourile la handler pe măsură ce sunt încărcate. Și, depinde de serverul web să vadă că maparea corectă este livrată.
Odată ce serverul web și SPWA au pagina pregătită pentru funcționare, serverul web nu mai trebuie să se ocupe de mesajele dintre pagină și dispozitiv. protocolul MQTT specifică un server de rutare care să gestioneze pub/sub. Au fost realizate un număr de servere MQTT. Unele dintre ele sunt open source. Unul foarte popular este Mosquito și există câteva dezvoltate pe Node.js.
Procesul pentru pagină este simplu. SPWA se abonează la un subiect. O versiune bună a unui subiect este un identificator pentru un MCU, cum ar fi o adresă MAC sau un număr de serie. Sau, SPWA s-ar putea abona la toate citirile de temperatură. Dar, atunci pagina ar trebui să facă treaba de filtrare a mesajelor de pe toate dispozitivele. Publicarea în MQTT este în esență o difuzare sau multicast.
Să aruncăm o privire la modul în care SPWA va interfața cu MQTT.
Inițializarea MQTT pe SPWA
Există mai multe biblioteci client din care să alegeți. Unul, de exemplu, este un MQTT.js. Un altul este eclipsa paho. Sunt mai multe desigur. Să folosim Eclipse Paho, deoarece are o versiune CDN stocată. Trebuie doar să adăugăm următoarea linie pe pagina noastră:
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
Clientul MQTT trebuie să se conecteze la un server înainte de a putea trimite și primi mesaje. Deci, liniile de configurare a conexiunii trebuie să fie incluse și în JavaScript. Putem adăuga o funcție MQTTinitialize
care setează clientul și răspunsurile pentru gestionarea conexiunii și primirea mesajului.
var messagesReady = false; var mqttClient = null; function MQTTinitialize() { mqttClient = new Paho.MQTT.Client(MQTTHostname, Number(MQTTPort), "clientId"); mqttClient.onMessageArrived = onMessageArrived; // connect the client mqttClient.connect({ onSuccess: () => { messagesReady = true; } }); // set callback handlers mqttClient.onConnectionLost = (response) => { // messagesReady = false; // if (response.errorCode !== 0) { console.log("onConnectionLost:"+response.errorMessage); } setTimeout(() => { MQTTinitialize() },1000); // try again in a second }; }
Configurarea abonamentului
Cu conexiunea gata, clientul se poate abona la canalele de mesaje, trimite mesaje pe ele etc. Doar câteva rutine pot face cea mai mare parte a muncii necesare pentru a conecta panourile cu căile MQTT.
Pentru panoul SPWA, momentul abonării poate fi folosit pentru a stabili asocierea dintre panou și subiect, identificatorul MCU.
function panelSubcription(topic,panel) { gTopicToPanel[topic] = panel; gPanelToTopic[panel] = topic; mqttClient.subscribe(topic); }
Având în vedere că un MCU publică pe tema sa, SPWA va primi un mesaj. Aici, mesajul Paho este despachetat. Și apoi mesajul este transmis în mecanica aplicației.
function onMessageArrived(pmessage) { // var topic = pmessage.destinationName; var message = pmessage.payloadString; // var panel = gTopicToPanel[topic]; deliverToPanel(panel,message); }
Deci, acum tot ce trebuie să facem este să creăm deliverToPanel
, care ar trebui să fie oarecum asemănător cu gestionarea de intervale pe care o aveam înainte. Cu toate acestea, panoul este identificat în mod clar și numai datele cu cheie trimise în mesajul respectiv pot fi actualizate.
function deliverToPanel(panel,message) { var refall = panelApp.$refs; // all named children that panels var pdata = refall[panel][0]; // off Vue translation, but it's there. var MCU_updates = JSON.parse(message); for ( var ky in MCU_updates ) { pdata[ky] = MCU_updates[ky] } }
Această funcție deliverToPanel
este suficient de abstractă pentru a permite definirea oricărui panou cu orice număr de puncte de date pentru animație.
Trimiterea de Mesaje
Pentru a finaliza bucla de aplicație între MCU și SPWA, definim o funcție pentru a trimite un mesaj.
function sendPanelMessage(panel,message) { var topic = gPanelToTopic[panel]; var pmessage = new Paho.MQTT.Message(message); pmessage.destinationName = topic; mqttClient.send(pmessage); }
Funcția sendPanelMessage
nu face altceva decât să trimită mesajul pe aceeași cale a subiectului la care este abonat SPWA.
Deoarece intenționăm să facem butoanele pictograme responsabile pentru aducerea unui număr de panouri pentru un singur grup de MCU-uri, va fi mai mult de un panou de care trebuie să avem grijă. Dar, avem în vedere că fiecare panou corespunde unui singur MCU, așa că avem o mapare unu-unu, pentru care putem folosi două hărți JavaScript pentru hartă și invers.
Deci, când trimitem mesaje? De obicei, aplicația panoului va trimite un mesaj când dorește să schimbe starea MCU.
Menținerea stării de vizualizare (Vue) sincronizată cu dispozitivele
Unul dintre lucrurile grozave despre Vue este că este foarte ușor să păstrați modelul de date sincronizat cu activitatea utilizatorului, care poate edita câmpuri, face clic pe butoane, folosește glisoare etc. Se poate fi sigur că modificările butoanelor și câmpului vor se reflectă imediat în câmpurile de date ale componentelor.
Dar, dorim ca modificări să declanșeze mesaje către MCU de îndată ce apar modificări. Deci, căutăm să folosim evenimentele de interfață pe care Vue le poate guverna. Căutăm să răspundem la un astfel de eveniment, dar numai după ce modelul de date Vue este gata cu valoarea curentă.
Am creat un alt fel de panou, acesta cu un buton cu aspect destul de artistic (poate inspirat de Jackson Pollock). Și, am încercat să-l transform în ceva al cărui clic raportează starea înapoi către panoul care îl conține. Nu a fost un proces atât de simplu.
Un lucru care m-a dezamăgit este că am uitat unele dintre ciudateniile în gestionarea SVG. Am încercat mai întâi să schimb șirul de stil, astfel încât câmpul de display
al stilului CSS să fie fie „Niciun”, fie „ceva”. Dar, browserul nu a rescris niciodată șirul de stiluri. Dar, pentru că era greoi, am încercat să schimb clasa CSS. Nici asta nu a avut nici un efect. Dar, există și atributul de visibility
, pe care cei mai mulți dintre noi îl amintim din vechiul HTML (poate versiunea 1.0), dar care este foarte actualizat în SVG. Și, asta funcționează bine. Tot ce trebuia să fac a fost să propage evenimentul clic pe butonul.
Vue a proiectat proprietăți pentru a se propaga într-o singură direcție, de la părinte la copil. Deci, pentru a schimba datele din aplicație sau din panou, trebuie să trimiteți un eveniment de modificare părintelui. Apoi, puteți modifica datele. Schimbarea elementului de date care controlează butonul face ca Vue să actualizeze proprietatea care afectează vizibilitatea elementului SVG pe care l-am ales să indicăm starea. Iată un exemplu:
Fiecare instanță a panoului cu butoane ondulate este independentă. Deci, unele sunt ON și altele sunt OFF.
Acest fragment de SVG conține indicatorul galben cu aspect ciudat:
<path :visibility="stateView" d="m -36.544616,12.266886 c 19.953088,17.062165 5.07961,-19.8251069 5.317463,8.531597 0.237853,28.356704 13.440044,-8.847959 -3.230451,10.779678 -16.670496,19.627638 14.254699,-2.017715 -11.652451,3.586456 -25.90715,5.60417 10.847826,19.889979 -8.095928,-1.546575 -18.943754,-21.436555 -1.177383,14.210702 -4.176821,-12.416207 -2.999438,-26.6269084 -17.110198,8.030902 2.14399,-8.927709 19.254188,-16.9586105 -19.075538,-8.0837048 9.448721,-5.4384245 28.52426,2.6452804 -9.707612,-11.6309807 10.245477,5.4311845 z" transform="translate(78.340803,6.1372042)" />
Vizibilitatea este populată de stateView
, o variabilă calculată care mapează starea booleană la un șir pentru SVG.
Iată șablonul de definire a componentei panoului:
<script type="text/x-template"> <div> <control-switch :state="bstate" v-on:changed="saveChanges" ></control-switch> <gauge :level="fluidLevel" ></gauge> </div> </script>
Și aceasta este definiția JavaScript a panoului Vue cu copiii săi ca subcomponente:
var widgdef = { data: function () { var currentPanel = { // at the top level, values controlling children bstate : true, fluidLevel : Math.round(Math.random()*100) } // return currentPanel }, template: '#mcu-control-panel-template', methods: { saveChanges: function() { // in real life, there is more specificity this.bstate = !this.bstate relayToMCU(this.name,"button",this.bstate) // to be defined } }, components: { 'control-switch' : { // the odd looking button props: ['state'], template: '#control-switch-template', // for demo it is in the page. computed: { // you saw this in the SVG above. stateView : function() { return ( this.state ) ? "visible" : "hidden" } }, methods : { // the button handler is in the SVG template at the top. stateChange : function () { // can send this.$emit('changed'); // tell the parent. See on the template instance } } }, 'gauge' : { // some other nice bit of SVG props: ['level'], template: '#gauge-template' } } }
Deci, acum a fost stabilit mecanismul pentru un singur buton încorporat într-un panou. Și, trebuie să existe un cârlig pentru a spune MCU că ceva a avut loc. Acesta trebuie apelat imediat după ce starea datelor a componentei panoului a fost actualizată. Să-l definim aici:
function relayToMCU(panel,switchName,bstate) { var message = switchName + ':' + bstate // a on element parameter string. sendPanelMessage(panel,message) }
Există schimbarea stării pe drumul său către hardware în doar două linii de cod.
Dar, acesta este un caz destul de simplu. Orice comutator poate fi văzut ca un apel de funcție către o piesă hardware din lume. Deci, șirul poate conține numele comutatorului și alte câteva elemente de date. Deci, metoda componentelor care înregistrează modificarea va trebui să aibă o manipulare personalizată în ea, pentru a putea aduna toate seturile de date de pe panou și le poate trimite într-un singur șir de comandă. Chiar și șirul de comandă este puțin simplu. Dacă MCU este destul de mic, este posibil ca șirul de comandă să fie tradus într-un cod. Dacă MCU are o mare capacitate, șirul de comandă ar putea fi de fapt o structură JSON sau poate toate datele pe care le găzduiește panoul.
În această discuție, butoanele de pe panoul cu pictograme conțin numele panoului de preluat. Acest lucru poate fi, de asemenea, destul de simplificat. Pare să aibă sens că acel parametru poate reprezenta orice panou care ar putea fi stocat în bazele de date ale unei întreprinderi. Dar, poate este o formulă. Poate că informațiile despre panou ar trebui să fie incluse în definiția panoului pe care o primim de la server. În orice caz, elementele de bază pot fi extinse cu ușurință odată ce anumite dureri de cap sunt îndepărtate, cum ar fi ca SVG-ul să răspundă corect la clicuri.
Concluzie
Această discuție a prezentat câțiva pași și decizii de bază care duc la realizarea unei aplicații web cu o singură pagină (SPWA) care poate interfața cu dispozitivele IoT. Acum știm cum să obținem panouri de la un server web și să le transformăm în interfață MCU.
Există mult mai mult în această discuție, cu câteva alte discuții care pot urma. Să începi cu Vue este un lucru la care să te gândești. Dar, mai este toată povestea MCU, despre care am atins-o doar pe scurt.
În special, selectând MQTT ca substrat de comunicare, presupunem că dispozitivele IoT de la celălalt capăt pot fi cumva guvernate de MQTT. Dar, s-ar putea să nu fie întotdeauna cazul. Uneori sunt necesare gateway-uri dacă MQTT dorește să obțină acces la un dispozitiv cu legături seriale sau Bluetooth. Sau, poate tot ceea ce aveți nevoie vreodată de pe pagina web este WebSockets. Cu toate acestea, am folosit MQTT ca exemplu pentru a arăta cum Vue ar putea să primească și să trimită date, păstrând în același timp starea datelor în sincronizare cu dispozitivele.
Încă o dată avem doar o parte din poveste. De data aceasta este pentru sincronizare, deoarece pagina ar trebui să poată face față alertelor și să deranjeze utilizatorul dacă se întâmplă ceva critic. Uneori mesajele se pot pierde. Deci, trebuie să avem un mecanism de recunoaștere.
În cele din urmă, părerea mea este că Vue face actualizarea datelor la primire destul de elegantă. Dar, trimiterea modificărilor de stat nu este atât de simplă. Nu pare să facă treaba mult mai simplă decât se poate face cu JavaScript vanilla. Dar, există o cale și are sens.
Poate că o bibliotecă curată poate fi construită pentru a face un set universal de componente pentru toate panourile. S-au menționat pe scurt elementele pentru realizarea unor astfel de biblioteci și stocarea lor într-o bază de date. S-ar putea să trebuiască dezvoltate instrumente care depășesc doar realizarea de imagini SVG. În orice caz, probabil că există multe lucruri care pot fi făcute pentru următorii pași.