Wprowadzenie do lil-gui

W artykule omówię lil-gui - nowoczesny panel sterowania dla aplikacji JavaScript i Three.js. Podczas tworzenia aplikacji wykorzystujących Three.js, WebGL, Canvas, Web Audio API czy nawet zwykłe animacje JavaScript, bardzo szybko pojawia się potrzeba wygodnej zmiany parametrów programu bez ciągłego edytowania kodu i odświeżania strony. Do tego celu przez wiele lat wykorzystywano bibliotekę dat.GUI. Obecnie jej następcą jest lil-gui – lżejsza, szybsza i aktywnie rozwijana biblioteka oferująca praktycznie identyczny sposób użycia. W tym artykule poznamy podstawy pracy z lil-gui.

Czym jest lil-gui?

Czym jest lil-gui? lil-gui jest niewielką biblioteką JavaScript służącą do tworzenia paneli sterowania (GUI - Graphical User Interface). Pozwala w czasie działania programu:
  • zmieniać liczby,
  • przełączać opcje logiczne,
  • wybierać kolory,
  • uruchamiać funkcje,
  • rozwijać grupy ustawień,
  • obserwować wartości zmiennych.
Dzięki temu można budować bardzo wygodne narzędzia do eksperymentowania z aplikacją. Najczęściej lil-gui spotkamy w projektach:
  • Three.js
  • Babylon.js
  • PixiJS
  • p5.js
  • WebGL
  • Shaderów GLSL
  • Web Audio API
  • symulacji fizycznych
  • generative art

Instalacja

Najprościej wykorzystać CDN.
<script type="module">
import GUI from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';
</script>
lub w projekcie npm:
npm install lil-gui
Następnie:
import GUI from 'lil-gui';
Najprostszy możliwy przykład:
import GUI from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';

const settings = {
    speed: 5
};

const gui = new GUI();

gui.add(settings, 'speed');
Po uruchomieniu pojawi się panel zawierający jeden suwak.

Jak działa lil-gui?

Biblioteka działa na bardzo prostym mechanizmie. Tworzymy obiekt:
const settings = {
    speed: 5
};
Następnie przekazujemy:
gui.add(settings, 'speed');
Od tej chwili GUI modyfikuje właściwość obiektu.Jeżeli użytkownik przesunie suwak to automatycznie settings.speed otrzyma nową wartość.

Ustawienie zakresu

Domyślnie lil-gui nie zna zakresu liczb. Można go określić:
gui.add(settings, 'speed', 0, 20);
lub
gui.add(settings, 'speed')
    .min(0)
    .max(20);

Krok zmiany wartości

Przykład:
gui.add(settings, 'speed', 0, 20, 0.5);
lub
gui.add(settings, 'speed')
    .min(0)
    .max(20)
    .step(0.5);
Użytkownik będzie zmieniał wartość co 0.5.

Nadanie etykiety

gui
.add(settings, 'speed')
.name('Prędkość');
Panel pokaże tekst Prędkość zamiast speed.

Wartości logiczne

Typ boolean automatycznie tworzy checkbox.
const settings = {
    wireframe: false
};

gui.add(settings, 'wireframe');

Lista rozwijana

Jeżeli chcemy wybrać jedną z wielu wartości:
const settings = {
    quality: 'Medium'
};

gui.add(settings, 'quality', [
    'Low',
    'Medium',
    'High',
    'Ultra'
]);
Powyższy kod tworzy listę rozwijaną. Można również użyć mapy:
gui.add(settings, 'quality', {
    Niska: 1,
    Średnia: 2,
    Wysoka: 3
});

Wybór koloru

Jedna z najczęściej używanych funkcji. Po kliknięciu otworzy się klasyczny color picker.
const settings = {
    color: '#00ffcc'
};

gui.addColor(settings, 'color');

Uruchamianie funkcji

GUI może zawierać przyciski. Kliknięcie przycisku uruchamia metodę.
const settings = {
    reset() {
        console.log("Reset");
    }
};

gui.add(settings, 'reset');

Foldery

Przy większych projektach warto grupować ustawienia.
const appearance = gui.addFolder('Wygląd');

appearance.add(settings, 'wireframe');

appearance.addColor(settings, 'color');
Można tworzyć wiele folderów.
const animation = gui.addFolder('Animacja');

const camera = gui.addFolder('Kamera');

const lights = gui.addFolder('Światło');
Możesz określić czy folder ma być domyślnie otwarty folder.open() czy zamknięty folder.close().

Nasłuchiwanie zmian

Najczęściej wykorzystujemy:
gui.add(settings, 'speed')
.onChange(value => {
    console.log(value);
});
Kod wykona się po każdej zmianie suwaka.

Zdarzenie zakończenia edycji

Jeżeli chcemy wykonać operację dopiero po puszczeniu suwaka:
gui.add(settings, 'speed')
.onFinishChange(value => {
    console.log("Nowa wartość:", value);
});
Jest to przydatne przy kosztownych obliczeniach.

Obserwowanie zmiennej

Czasami wartość zmienia sam program.
gui.add(player, 'fps').listen();
GUI będzie automatycznie odświeżane.

Ukrywanie panelu

Można ukryć panel:
gui.hide();
i ponownie pokazać:
gui.show();
Jeżeli panel nie będzie już potrzebny metoda gui.destroy() usunie cały panel z dokumentu.

Przykład z animacją

Zmieniając wartość suwaka natychmiast wpływamy na prędkość obrotu.
import GUI from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';

const settings = {
    speed: 1,
    rotation: true
};

const gui = new GUI();

gui.add(settings, 'speed', 0, 10);
gui.add(settings, 'rotation');

function animate() {
    requestAnimationFrame(animate);
    if (settings.rotation) {
        cube.rotation.y += settings.speed * 0.01;
    }
}

animate();
Poniżej znajduje się kompletny przykład wykorzystujący Three.js oraz lil-gui. Jest to prosta scena z obracającym się sześcianem, którego prędkość oraz możliwość obracania można kontrolować z poziomu panelu GUI.
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Three.js + lil-gui - Przykład animacji</title>
<style>
body{
    margin:0;
    overflow:hidden;
    background:#111;
    font-family:Arial,Helvetica,sans-serif;
}

canvas{
    display:block;
}
</style>
<script type="importmap">
{
    "imports": {
        "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
        "lil-gui": "https://cdn.jsdelivr.net/npm/[email protected]/+esm"
    }
}
</script>
</head>
<body>
<script type="module">

import * as THREE from "three";
import GUI from "lil-gui";

// -------------------------------------------------
// Renderer
// -------------------------------------------------

const renderer = new THREE.WebGLRenderer({
    antialias:true
});

renderer.setSize(window.innerWidth,window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);

document.body.appendChild(renderer.domElement);

// -------------------------------------------------
// Scene
// -------------------------------------------------

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x202020);

// -------------------------------------------------
// Camera
// -------------------------------------------------

const camera = new THREE.PerspectiveCamera(
    60,
    window.innerWidth/window.innerHeight,
    0.1,
    100
);

camera.position.set(0, 0.5, 5);

// -------------------------------------------------
// Lights
// -------------------------------------------------

const ambientLight = new THREE.AmbientLight(0xffffff,1.5);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff,2);
directionalLight.position.set(5,5,5);

scene.add(directionalLight);

// -------------------------------------------------
// Cube
// -------------------------------------------------

const geometry = new THREE.BoxGeometry();

const material = new THREE.MeshStandardMaterial({
    color:0x00ccff,
    roughness:0.35,
    metalness:0.2
});

const cube = new THREE.Mesh(
    geometry,
    material
);

scene.add(cube);

// -------------------------------------------------
// Settings
// -------------------------------------------------

const settings = {
    speed:1,
    rotation:true
};

// -------------------------------------------------
// GUI
// -------------------------------------------------

const gui = new GUI({
    title:"Animacja"
});

gui
.add(settings,"speed",0,10,0.1)
.name("Prędkość");

gui
.add(settings,"rotation")
.name("Obrót");

// -------------------------------------------------
// Animation
// -------------------------------------------------

function animate() {

    requestAnimationFrame(animate);

    if(settings.rotation){

        cube.rotation.x += settings.speed*0.01;
        cube.rotation.y += settings.speed*0.015;

    }

    renderer.render(scene,camera);

}

animate();

// -------------------------------------------------
// Resize
// -------------------------------------------------

window.addEventListener("resize", () => {
    camera.aspect=window.innerWidth/window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(
        window.innerWidth,
        window.innerHeight
    );
});

</script>
</body>
</html>
Kontrola obracającego sześcianu Three.js + lil.gui Kontrola obracającego sześcianu Three.js + lil.gui