Patch ledning design: hvordan du giver din GUI et analogt look

(Billedkredit)

Forestil dig en GUI-widget, der udnytter den fulde magt i kontrolstrømmen, uden at brugeren nogensinde skal skrive en kodelinie ... “Patch ledninger” er en kraftig og under-værdsat strategi til at bringe interaktion til din webapp.

I browseren er du sandsynligvis mest fortrolig med dem fra flowchart-apps som Draw.io og "mind mappers" som Coggle.it og MindNode. Men de har potentiale inden for spildesign, indlæringsværktøjer og dybest set enhver brugssag, hvor brugeren skal have scripting-sproglignende kontrol over en app via symboliske grafiske elementer.

En patch-ledning er en visuel forbindelse mellem en ramme eller panel og et eller flere yderligere visuelle elementer. Konceptet stammer fra analog lydrutning, der blev brugt i tidlige telefontavler, som var baseret på endnu tidligere telegrafistavler, der blev brugt i det tidlige 1800-tallet.

Telefonoperatører afbildet i en 1922-udgave af Bell Telefon Magazine. (Billedkredit)

Men den direkte antecedent for mange digitale patch-ledning-stye-grænseflader er den analoge, modulære lydsynthesizer. Modulære synthesizere bevæger spændinger gennem en række trin eller moduler som VVS i en bygning.

Et signal udsendes normalt af en oscillator og ledes derefter gennem en række transformationer som kuglen tumler ned gennem en Rube Goldberg-maskine. Når den passerer gennem forskellige moduler, kan spændingen opdeles i flere separate strømme og kan bruges ikke kun som et lydsignal, men også som styringsspændinger eller logiske forhold for selve modulerne.

De "rør", der udgør denne "VVS" mellem moduler kaldes "patchkabler" eller (eller "patchkabler"), og i en analog synthesizer er de fysiske ledninger, der vilkårligt er tilsluttet input- og outputstik på modulerne efter eget skøn af kunstneren, hvilket giver næsten uendeligt tilpassede kredsløb.

Et ”Hello World” -program i dataflow-programmeringsmiljøet Pure Data, der viser en patch-ledning mellem strengen og udskrivningsfunktionen. (Billedkredit)

Den første digitale lydsoftware havde selvfølgelig ikke en GUI. (Folk har lavet lyde med computere siden længe før den første Fortran-kompilator blev frigivet i 1957.) Men med tilføjelsen af ​​en GUI overførte mange software-synthesizer-emulatorer ideen om at lappe ind i det digitale domæne og bevare programmeringen af ​​"data flow" i æraen med personlig computing.

To programmeringsmiljøer, der i dag er vidt brugt til lyd- og videosyntese - Pure Data (Pd) og Max / MSP - er bygget op omkring ideen om at lappe sammen visuelle elementer, der repræsenterer funktioner og kontrolstrøm.

Et andet eksempel er Scratch, MITs visuelle programmeringssprog, der primært er rettet mod at undervise programmeringskoncepter til børn, som sandsynligvis også er afhængig af en opdateringsmodalitet, selvom den blokbaserede GUI faktisk ikke bruger patch-ledninger.

Et tekst-til-tale-program for Twitter skrevet i Max / MSP af Scott Brown, der viser digitale patch-ledninger. (Billedkredit)

Lav en flowdiagram-app med Interact.js på under 30 minutter

I løbet af denne tutorial vil jeg vise dig, hvordan du bygger denne cool lille flow-chart-applikation på under 30 minutter!

For at gøre dette bruger vi et let JavaScript-bibliotek kaldet Interact.js for at forenkle træk-og-slip-adfærd. (Hvis vi bygger i React, kunne vi også bruge React DnD, men det gemmer jeg til en fremtidig tutorial.)

Jeg har konfigureret CodePen med Interact.js CDN-filer såvel som randomColor, som jeg vil bruge til at præfigurere appen lidt.

Vi begynder med blot at bruge metoderne fra Interacts hurtigstarteksempel næsten ordret. Imidlertid har jeg indstillet inerti-metoden til falsk, og jeg har også fjernet grænsefeltets tilstand for enkelhed.

interagere ( 'trækbart')
  .draggable ({
    inertia: falsk,
    autoScroll: sandt,
    onmove: dragMoveListener
    });
funktion dragMoveListener (begivenhed) {
    var target = event.target;
        x = (parseFloat (target.getAttribute ('data-x')) || 0) + event.dx,
        y = (parseFloat (target.getAttribute ('data-y')) || 0) + event.dy;
    target.style.webkitTransform =
    target.style.transform =
      'oversætte (' + x + 'px,' + y + 'px)';
    target.setAttribute ('data-x', x);
    target.setAttribute ('data-y', y);
  }

Hvis vi nu opretter en node på DOM og giver den klassen, der kan trækkes, vil vi være i stand til at flytte den

rundt på skærmen med markøren!

Da ethvert flowchart kræver mere end en enkelt tilstand, har vi brug for en måde for brugeren at tilføje underordnede noder til DOM. Vi laver en knap, der kalder en createDiv () -funktion.

     
             
    
                 

Funktionen createDiv () opretter en ny

som et barn i hovednoden og giver det trækbare klassens navn. Vi kan også indstille elementets baggrundsfarve til en tilfældig farve her:

funktion createDiv () {
    lad div = document.createElement ('div');
    document.getElementById ( 'største') appendChild (div.);
    div.className = 'trækbar';
    div.style.backgroundColor = randomColor ({lysstyrke: 'lys'});
}

Der er et par andre ting, som vi måske ønsker at gøre i createDiv () -funktionen.

Hver

skal have et unikt ID, så vi kan henvise til det senere, når vi lapper mellem disse
-elementer, og vi tilføjer disse ID'er som nøgle i et objekt, som vi vil bruge senere:

// Indstil et unikt ID for hver div
div.id = "div" + divCounter;
divs [div.id] = [];
divCounter + = 1;

Vi ønsker heller ikke, at den nye

skal vises ved oprindelsen. Det ville dække knappen. Vi har også brug for this.focus (), som gør det muligt at redigere en indre div, så vi kan tilføje tekst. Meget vigtigt for ethvert flowchart!

// Flyt den nye div ned 70px fra oprindelsen
div.style.transform = "translate (0px, 70px)";
div.setAttribute ('data-y', 70);
div.setAttribute ('onclick', 'this.focus ()');

Og endelig kan vi indstille størrelsen på barnet

med en CSS-variabel, hvilket betyder, at vi kan undgå at bruge "magiske numre" i vores JavaScript for at henvise til forskellige kanter af barnet
, når det er tid til at forbinde vores patch ledninger.

// Indstil højden og bredden af ​​div med CSS-variabler
root.style.setProperty ('- højde', højde + "px");
root.style.setProperty ('- bredde', bredde + "px");

Jeg har tilføjet en lille CSS for at få alt til at se lidt smukkere ud. Her er resultaterne. Så godt indtil videre!

Bygning af patchkabler

For at gengive patch-ledningerne bruger vi SVG-linjer. For at gøre dette tilføjer vi først en tom SVG inden for “hoved”

. I CSS er vi nødt til at indstille pointer-events til ingen. Dette giver os mulighed for at klikke “gennem” den usynlige SVG, så vi stadig kan interagere med en
“under” den. (SVG's z-indeks er også indstillet til et vilkårligt stort antal, så patchkablerne altid vises oven på andre elementer.)

#patchCords {
  position: absolut;
  bredde: 100%;
  højde: 100%;
  pointer-events: none;
  slagtilfælde: rund;
  z-indeks: 100;
}

Regnskab for interaktion

Som enhver kompleks opførsel kan vi opdele patchering ned på en liste over mulige tilstande. Derefter kan vi opbygge lyttere og balsam til at reagere på hver enkelt tilstand. Lad os tænke over, hvilke stater der er involveret i interaktion med patch-ledninger.

En patch-ledning i Max / MSP, et dataflow-programmeringsmiljø til lyd- og videosyntese. (Billedkredit: originalt arbejde)

Vi ønsker at være i stand til at klikke på et element og få patchkablet "fastgjort" til markøren, indtil og medmindre vi slipper ledningen på et andet element. For at gøre det har vi brug for en boolsk for at repræsentere, om en patch-ledning er aktiv på markøren eller ej. Lad os kalde den variabel patchCordActive.

lad patchCordActive = falsk;

patchCordActive starter som falske, fordi der ikke er ”knyttet” nogen patchkabel til musen, før brugeren klikker på en

.

Vi kan opdele lappeadfærden i følgende tilstande:

  1. patchCordActive er falsk, og brugeren flytter et af underordnede
    -elementer.
  2. patchCordActive er falsk, og brugerklik uden for
    .
  3. patchCordActive er falsk, og brugerklik inden i et barn
    . (Resultat: opretter ny patchkabel)
  4. patchCordActive er sandt, og brugeren klikker inde i den samme
    to gange. (dråber patch patch)
  5. patchCordActive er sandt, og brugeren klikker uden for enhver
    . (Resultat: dråber patch-ledning.)
  6. patchCordActive er sandt, og brugeren klikker inde i en anden
    . (Resultat: tilslutter patch patch)
  7. patchCordActive er sandt, og brugeren flytter deres markører overalt i browservinduet.
  8. patchCordActive er falske, men
    -elementer er forbundet med patch-ledninger, og brugeren flytter et af de tilsluttede
    -elementer.

Tilføjelse af begivenhedslyttere

Når man ser på listen ovenfor, er det klart, at vi har at gøre med to slags brugerinteraktioner: markørbevægelser og markørklik. For at redegøre for dette har vi brug for mindst to typer begivenhedslyttere:

window.addEventListener ('klik', funktion (begivenhed) {
  ...
});
windows.addEventListener ('mousemove', funktion (begivenhed) {
  ...
});

Baseret på vores liste over mulige opdateringstilstande kan vi nu opbygge betingelser, der står for hver af de mulige opførsler.

window.addEventListener ('klik', funktion (begivenhed) {
// ====== KLIK INDSIDE DIV (PATCH CORD INACTIVE) ======
 if (document.getElementById ("main"). indeholder (event.target)
       &&! (patchCordActive)) {
    
  }
// ====== KLIK IGJEN INDE I SAMME DIV (PATCH CORD ACTIVE) =======
 ellers hvis (document.getElementById ("main"). indeholder (event.target)
       && (patchCordActive)) {
    
  }
// ====== KLIKK INN I EN DIF DIV (PATCH CORD ACTIVE) ======
 ellers hvis (document.getElementById ("main"). indeholder (event.target)
       && (patchCordActive)
       && (event.target.id! = valgtDiv)) {
      
  }
// ====== KLIK UTENFOR EN DIV (PATCH CORD AKTIV) ======
 ellers hvis (patchCordActive) {
    
  }
// ====== KLIKK UTENFOR EN DIV (PATCH CORD INACTIVE) ======
 ellers hvis (! patchCordActive) {
    // pass
  }
// ====== FALLBACK ======
 ellers {
    kaste "Patch-ledningslogik faldt igennem til fejltilstanden!";
  }
  
});
windows.addEventListener ('mousemove', funktion (begivenhed) {
  if (patchCordActive) {
    
  }
});

Betingelserne 1–7 er opfyldt her, men vi har endnu ikke en måde at håndtere nummer 8, fordi vi endnu ikke har oprettet en måde at holde styr på tilsluttede patch-ledninger og deres vertikater. Det gør vi nu.

Strukturering af opdateringsdata

Patchkablerne er bare SVG-stier. Det betyder, hver gang vi trækker-og-slipper et af

-elementerne, skal alle ledninger "knyttet" til denne div bevæge sig sammen med det. Vi starter med at oprette en struktur til at styre denne forbindelse mellem noder på DOM.

SVG'er har brug for to hjørner for at tegne en linje, og en patch-ledning skal opdateres når som helst, hvad der sker, der påvirker det. Patch ledninger er begrebsmæssigt grundlæggende attributter for

elementerne, de forbinder, så det giver mening at bruge
ID'erne som nøgler til hver linje.

Vi har dog et problem. Det, vi ønsker, er et objekt, der ligner det følgende. Begge

elementer fungerer lige som en nøgle til vertikerne på en linje.

patchCords = {[div1, div2]: [[x1, y1], [x2, y2]],
              ...
              ...
             }

Dog fungerer det ikke. JavaScript-objekter tillader kun, at der bruges en nøgle til hver værdi i et objekt. Hvad vi har brug for, er noget som MySQL's udenlandske nøgle, der gør det muligt at henvise til forskellige tabeller.

Der er dog et JavaScript-mønster, som vi kan bruge til at gøre noget lignende:

const divs = {
  "div0": ["line0", "line1"],
  "div1": ["line0"],
  "div2": [linje1],
  "div3": [],
   
   etc.
}
const ledninger = {
  "line0": [{"div0": [x1, y1]}, "->", {"div1": [x2, y2]}],
  "line1": [{"div0": [x1, y1]}, "->", {"div2": [x2, y2]}],
  etc.
}

Her bruger divs-objektet ID'erne for hver

som en nøgle. Værdien tilknyttet nøglen er i sig selv en nøgle til en linje i objektet "ledninger". I eksemplet er div0 patchet til både div1 og div2, mens div3 ikke i øjeblikket er patchet til noget.

Snorobjektet bevarer hver toppunkt som en matrix af JavaScript-objekter, der er nøglet til linjens overordnede

. Dette gør det muligt at henvise til de to tabeller i begge retninger. Givet kun en nøgle til en linje, kan vi vende tilbage til overordnede
-elementer.

For at øge klarheden af ​​ledningsobjektet og bevare klar retningsbestemmelse bruger jeg også en sammensat syntaks, der adskiller start- og slutlinjeindeks med -> symbolet. Det bestilte array bevarer allerede disse oplysninger, men pilen øger læsbarheden i høj grad ved tydeligt at vise retningen af ​​patch-ledningen, hvilket senere kan være afgørende.

Patcheringslogik.

Opbygning af den betingede logik

Lad os gennemgå, hvordan logikken til begivenhedslytter fungerer. På det højeste niveau har vi to begivenhedslyttere og en Interact.js-funktion, der bliver kaldt hver gang en underordnet knude flyttes.

vindue
├── Klik på begivenhedslytter
│ ├── klik inden div (patchkabel inaktiv)
│ ├── klik igen inden for den samme div (patchkabel aktiv)
│ ├── klik inden i en anden div (patchkabel aktiv)
│ ├── Klik uden for enhver div (patchkabel aktiv)
│ ├── Klik uden for en div (patchkabel er inaktiv
│ └── fallback (kastfejl)
├── mousemove begivenhedslytter
│ └── mousemove (patch patch aktiv)
├── funktion dragMoveListener
│ └── Træk barndelen med den vedhæftede patchkabel

Nu kan vi blot skrive logikken for hver stat.

Klik inde
(patchkabel inaktiv)

Vi ønsker at registrere et klik inden for hvert barn

men ikke inde i det tekstredigerbare område af
. Uden for enhver begivenhedslytter kan vi skrive en pilefunktion, der returnerer en boolsk:

lad notTextDiv = (divId) => {return true? (divId! = "indre") && (divId! = "flex"): falsk};

Nu indstiller vi betinget for at registrere placeringen af ​​kliket kun, når der ikke er nogen aktuel patch-ledning "fastgjort" til markøren.

if (document.getElementById ("main"). indeholder (event.target)
      &&! (patchCordActive)
      && notTextDiv (event.target.id)
      )
{
}

Derefter får vi placeringen af ​​målet

og placeringen af ​​musen i vinduet, og vi forbinder dem med en linje.

valgtDiv = event.target.id;
currentSelectionX = event.target.getBoundingClientRect (). x;
currentSelectionY = event.target.getBoundingClientRect (). y;
lad mouse_x = event.clientX; // Få div-koordinaterne
lad mouse_y = event.clientY;
drawLine (strømValgX + (bredde / 2), strømValg Y + højde, mus_x, mus_y);

Endelig opdaterer vi vores divs-objekt og vores snorobjekt. Vi tilføjer ID for den valgte

som et indeks for den nye patchkabel:

divs [selectedDiv] .push ( `linje $ {patchCordCounter}`);

Og så tilføjer vi en toppunkt af patch-ledningen til ledningsobjektet ved hjælp af ES6s initialiseringssyntaks til Comput Property Names til at indstille nøglen til det indlejrede objekt rent:

ledninger [`linje $ {patchCordCounter}`] =
[{[valgt div]: [nuværende valgX, nuværende valgY]}, "->"];

Endelig satte vi patchCordActive boolsk:

patchCordActive = sandt;

Klik igen inde i den samme
(patchkabel aktiv)

Hvis du klikker på en anden gang i den samme

, skal du slette (slette) den nyligt oprettede patchkabel og indstille "patchCordActive" -booleanen til falsk.

ellers hvis (document.getElementById ("main"). indeholder (event.target)
       && (patchCordActive)
       && notTextDiv (event.target.id)
       && (event.target.id == valgtDiv)
      )
  {
      deleteCord ();
      divs [selectedDiv] .pop ();
      slet ledninger [`linje $ {patchCordCounter}`]
      patchCordActive = falsk;
  }

Vi registrerer et andet klik inden for den samme

ved at sammenligne det aktuelle hændelsesmål med den valgteDiv-variabel, som vi indstiller, når en
først klikkes.

Funktionen deleteCord () fungerer ved blot at fjerne det sidste (senest oprettede) barn i patchCords-noden.

funktion deleteCord () {
  const select = document.getElementById ('patchCords');
  select.removeChild (select.lastChild);
}

Klik inde i en anden
(patch patch aktiv)

Dette er kernen i patchning, muligheden for at droppe en patch-ledning på en anden

og forbinde to
-elementer sammen! Vi starter med at fjerne plasterkablet fra musen. Derefter opdaterer vi ledningsobjektet til at indeholde de nye start- og sluttekoder på linjen:

deleteCord ();
divs [event.target.id] .push ( `linje $ {patchCordCounter}`);
ledninger [`linje $ {patchCordCounter}`] .push ({[event.target.id]: [event.target.getBoundingClientRect (). x,
    . Event.target.getBoundingClientRect () y]});

Husk, at syntaksen af ​​knudepunkterne i ledningsobjektet var et objekt indlejret i en matrix:

const ledninger = {
  "line0": [{"div0": [x1, y1]}, "->", {"div1": [x2, y2]}]
}

Så for faktisk at tegne den nye linje, er vi nødt til at bore ned i objektet for at få den første og eneste værdi inden for hvert indlejret objekt.

lad først = v => v [Object.keys (v) [0]];
lad x1 = først (ledninger [`linje $ {patchCordCounter}`] [0]);
lad y1 = først (ledninger [`linje $ {patchCordCounter}`] [0]);
lad x2 = først (ledninger [`linje $ {patchCordCounter}`] [2]);
lad y2 = først (ledninger [`linje $ {patchCordCounter}`] [2]);
drawLine (x1 [0] + (bredde / 2), y1 [1] + højde, x2 [0] + (bredde / 2), y2 [1]);

Endelig nulstiller vi værdien af ​​patchCordActive og øger patchCordCounter, som bruges til at tildele unikke ID'er til hver ledning.

patchCordActive = falsk;
patchCordCounter + = 1;

Klik uden for

Hvis du klikker uden for en

, slettes (slettes) patchkablet, hvis patchCordActive er indstillet til sand.

Klik uden for nogen

med patchCordActive indstillet til falske har ingen effekt, men det kan spille en rolle er fremtidige funktioner. "Ellers" -tilstanden er indstillet som en tilbagefaldsbetingelse for at kaste en fejl.

// ====== KLIK UTENFOR EN DIV (PATCH CORD AKTIV) ======
  andet hvis (patchCordActive)
  {
    deleteCord ();
    divs [selectedDiv] .pop ();
    slet ledninger [`linje $ {patchCordCounter}`]
    patchCordActive = falsk;
  }
// ====== KLIKK UTENFOR EN DIV (PATCH CORD INACTIVE) ======
  ellers hvis (! patchCordActive) {
    // pass
  }
// ====== FALLBACK ======
  ellers {
    kaste "Patch-ledningslogik faldt igennem til fejltilstanden!";
  }

MouseMove Event Listener

Den eneste tilstand, der er specifik for markørbevægelse (som anvendt til et klik), udløses af, at patchCordActive er indstillet til sand. Når en patch-ledning er "aktiv", ønsker vi, at den ene ende af denne ledning skal følge markøren, indtil den er "faldet" ned på en

.

Dette er relativt ligetil. Inde i en betinget indstiller vi x2 / y2-attributten for det sidste barn i patchCords-noden til den aktuelle placering af markøren.

windows.addEventListener ('mousemove', funktion (begivenhed) {
   // Hvis der er en aktiv patch-ledning, skal du centrere den ene ende på markøren
   if (patchCordActive) {
      lad mouse_x = event.clientX;
      lad mouse_y = event.clientY;
      document.getElementById ( "patchcords")
      .lastElementChild.setAttribute ('x2', mus_x);
      document.getElementById ( "patchcords")
      .lastElementChild.setAttribute ('y2', mouse_y);
      }
});

DragMoveListener

Som tingene står nu, kan vi trække og slippe patch-ledninger til

-elementer. Men hvis vi flytter disse
elementer med vores Interact.js-kode, forbliver patch-ledningerne bare på deres sidste placering!

(Gif lavet med groovo.io.)

Det skyldes, at vi endnu ikke eksplicit har opbygget forbindelsen mellem patch-ledninger og

-elementerne, de forbinder. For at gøre det har vi brug for funktionen “dragMoveListener” for at kontrollere “divs” -objektet (som i tern indekserer alle tilsluttede linjer) når som helst en
flyttes.

Først bruger vi ID'et for målet

til at søge efter en matrix af alle patch-ledninger, der er knyttet til den
.

lad movingDiv = target.id;
lad movingCords = divs [movingDiv];

Så er vi nødt til at iterere over alle vedhæftede patch-ledninger og opdatere deres start- eller slutkoordinater til dem fra

, når de bevæger sig.

for (const pc af bevægelseskabler) {
      
   // Bare få et af ledningerne knyttet til en div
   lad oneCord = document.getElementById (pc);
if (Object.keys (ledninger [pc] [0]) [0] == movingDiv) {
     oneCord.setAttribute ('x1', x + (bredde / 2));
     oneCord.setAttribute ('y1', y + højde);
   }
   andet hvis (Object.keys (ledninger [pc] [2]) [0] == movingDiv) {
     oneCord.setAttribute ('x2', x + (bredde / 2));
     oneCord.setAttribute ('y2', y);
   }
   ellers {
     smide "oneCord.setAttribute faldt igennem til
            fejltilstand ";
   }
}

Bemærk, at betinget bruges til at sikre, at vi opdaterer placeringen af ​​den rigtige ende af plasterkablet! Vi er nødt til at vide, hvilken ende af ledningen er “fra” ende, og hvilken er “til” ende. Ledningsobjektet koder denne information som nøglen til hvert koordinatpar.

Endeligt resultat

Her er det endelige resultat:

Næste skridt

Her har vi i bedste fald en MVP af en flowchart-app. Men jeg håber, at denne tutorial har vist, at selv en relativt kompleks lappeadfærd kan være enkel at opbygge på kort tid, hvis vi opdeler problemet i mindre trin. For at gøre flowchart-appen mere komplet, vil vi gerne bygge mindst nogle af følgende funktioner:

  1. Tilføj en metode til at slette underordnede
    -elementer og patch-ledninger.
  2. Tillad at ændre størrelsen på underordnede
    -elementer og give en måde at ændre deres form på.
  3. Byg funktionalitet for at tillade lapning fra specifikke kanter af
    elementer, ikke kun fra toppen og bunden.
  4. Implementér flersegmenterede patchledninger.
  5. Tillad, at patch-ledninger styles, så de viser retning (for eksempel med pile).

Koden til denne tutorial finder du på CodePen.io og downloades fra min GitHub. Kommenter - eller Tweet mig @PleathrStarfish - hvis du har et forslag, observation eller en cool gaffel af min kode!