Skip to content

Advanced Integration

Plan requirement

Advanced Integration requires an iJewel3D Enterprise plan at the Silver tier or above.

If your website needs different controls, use this integration. Examples include branded buttons, finger selection, image saving, and product switching. Mini Viewer still creates the viewer and loads every model from iJewel3D Platform. Your code controls the Try-On plugin through the existing WebGi ViewerApp. Before you add these controls, prepare and save the Try-On model.

The Web VTO demo shows one possible advanced Try-On experience.

Understand the two viewer objects

The integration uses two related objects.

ObjectPurpose
miniViewerThe Mini Viewer wrapper returned by loadModelById. Use it for product changes.
viewerAppThe WebGi viewer from ijewel-viewer-ready. Use it to get or add plugins.

Do not create another ViewerApp. A second ViewerApp creates another canvas and another render loop.

Load the libraries

The custom integration loads Web VTO with the other libraries. Keep these versions and this script order.

html
<script src="https://releases.ijewel3d.com/libs/webgi-v0/bundle-0.22.0.js"></script>

<script>
  window.webgi = window;
</script>

<script src="https://releases.ijewel3d.com/libs/mini-viewer/0.6.15/bundle.nowebgi.iife.js"></script>
<script src="https://releases.ijewel3d.com/libs/web-vto/0.3.0/web-vto.js"></script>

WebGi provides the viewer. Mini Viewer loads the model from iJewel3D Platform. Web VTO 0.3.0 provides RingTryonPlugin and TryonUIPlugin on window.ij_vto.

Use the Mini Viewer events

Mini Viewer provides the data that the custom integration needs through window events.

EventDataUse
ijewel-file-dataThe model file and its saved configuration.Read tryonConfig for the current model.
ijewel-viewer-readyThe WebGi ViewerApp.Add or get the Try-On plugins.
ijewel-scene-readyThe completed model scene.Apply the saved configuration after the model is ready.

Register the event listeners before you call loadModelById. ijewel-file-data can occur before the loadModelById promise returns.

Add the Try-On plugins

RingTryonPlugin controls the camera, hand tracking, ring placement, and image capture. TryonUIPlugin shows the loading screen, hand prompt, and camera error messages. It does not add the main Start, Stop, Flip, or Save buttons.

Add RingTryonPlugin first because TryonUIPlugin requires an existing Try-On plugin.

javascript
const tryon = await viewerApp.getOrAddPlugin(
  ij_vto.RingTryonPlugin
);

const tryonUi = await viewerApp.getOrAddPlugin(
  ij_vto.TryonUIPlugin
);

Use configure() to change only the required interface values.

javascript
tryonUi.configure({
  loading: {
    title: "Preparing Try-On",
    subText: null
  },
  handPrompt: {
    text: "Show the back of your hand"
  }
});

See the TryonUIPlugin API Reference for all interface properties and messages.

Add the complete custom integration

The example contains a full custom integration in one HTML file.

Edit these three areas:

  1. Replace MODEL_ID with the model file ID.
  2. Change DRIVE_BASENAME only for an enterprise iJewel3D Platform instance.
  3. Replace the control markup and CSS with your product design.

Keep the event order and plugin initialization unchanged.

html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Custom Ring Try-On</title>

  <style>
    html,
    body,
    #viewer {
      width: 100%;
      height: 100%;
      margin: 0;
    }

    body {
      overflow: hidden;
    }

    #tryon-controls {
      position: fixed;
      bottom: 16px;
      left: 50%;
      z-index: 10;
      display: flex;
      gap: 8px;
      align-items: center;
      padding: 8px;
      background: white;
      border-radius: 8px;
      transform: translateX(-50%);
    }

    [hidden] {
      display: none !important;
    }
  </style>
</head>

<body>
  <div id="viewer"></div>

  <div id="tryon-controls">
    <button id="tryon-button" type="button" disabled>Start Try-On</button>
    <button id="flip-button" type="button" hidden>Flip camera</button>
    <button id="save-button" type="button" hidden>Save image</button>

    <label>
      Finger
      <select id="finger-select" disabled>
        <option value="thumb">Thumb</option>
        <option value="index">Index</option>
        <option value="middle">Middle</option>
        <option value="ring" selected>Ring</option>
        <option value="pinky">Pinky</option>
      </select>
    </label>
  </div>

  <script src="https://releases.ijewel3d.com/libs/webgi-v0/bundle-0.22.0.js"></script>

  <script>
    window.webgi = window;
  </script>

  <script src="https://releases.ijewel3d.com/libs/mini-viewer/0.6.15/bundle.nowebgi.iife.js"></script>
  <script src="https://releases.ijewel3d.com/libs/web-vto/0.3.0/web-vto.js"></script>

  <script>
    const MODEL_ID = "MODEL_ID";
    const DRIVE_BASENAME = "drive";

    const container = document.getElementById("viewer");
    const tryonButton = document.getElementById("tryon-button");
    const flipButton = document.getElementById("flip-button");
    const saveButton = document.getElementById("save-button");
    const fingerSelect = document.getElementById("finger-select");

    let miniViewer;
    let tryon;

    function waitForWindowEvent(name) {
      return new Promise((resolve) => {
        window.addEventListener(name, resolve, { once: true });
      });
    }

    function parseConfiguration(value) {
      if (!value) {
        return {};
      }

      return typeof value === "string"
        ? JSON.parse(value)
        : value;
    }

    function readModelConfiguration(fileData) {
      const configuration = parseConfiguration(fileData?.config);

      if (Object.keys(configuration).length > 0) {
        return configuration;
      }

      return parseConfiguration(fileData?.defaultConfig);
    }

    async function applyTryonConfiguration(fileData) {
      const modelConfiguration = readModelConfiguration(fileData);
      const tryonConfiguration = modelConfiguration.tryonConfig;

      if (!tryonConfiguration?.enabled) {
        throw new Error("Try-On is not enabled for this model.");
      }

      await tryon.fromJSON({
        ...tryonConfiguration,
        type: ij_vto.RingTryonPlugin.PluginType
      });
    }

    function updateControls() {
      const isReady = Boolean(tryon);
      const isRunning = Boolean(tryon?.running);

      tryonButton.disabled = !isReady;
      tryonButton.textContent = isRunning
        ? "Exit Try-On"
        : "Start Try-On";

      flipButton.hidden = !isRunning;
      saveButton.hidden = !isRunning;
      fingerSelect.disabled = !isReady;
    }

    async function initializeTryon() {
      const fileDataReady = waitForWindowEvent("ijewel-file-data");
      const viewerReady = waitForWindowEvent("ijewel-viewer-ready");
      const sceneReady = waitForWindowEvent("ijewel-scene-ready");

      miniViewer = await window.ijewelViewer.loadModelById(
        MODEL_ID,
        DRIVE_BASENAME,
        container,
        {
          showUiButtons: false,
          hideTryOn: true
        }
      );

      if (!miniViewer) {
        throw new Error("The model did not load.");
      }

      const [fileDataEvent, viewerEvent] = await Promise.all([
        fileDataReady,
        viewerReady
      ]);

      await sceneReady;

      const viewerApp = viewerEvent.detail.viewer;

      tryon = await viewerApp.getOrAddPlugin(
        ij_vto.RingTryonPlugin
      );

      await viewerApp.getOrAddPlugin(ij_vto.TryonUIPlugin);

      await applyTryonConfiguration(
        fileDataEvent.detail.iJewelFileData
      );

      tryon.addEventListener("stop", updateControls);

      tryon.addEventListener("error", (event) => {
        console.error(
          "Try-On error:",
          event.detail?.error ?? event.detail
        );
        updateControls();
      });

      updateControls();
    }

    tryonButton.addEventListener("click", async () => {
      tryonButton.disabled = true;

      try {
        if (tryon.running) {
          await tryon.stop();
        } else {
          await tryon.start();
          tryon.finger = fingerSelect.value;
        }
      } catch (error) {
        console.error("Try-On action failed:", error);
      } finally {
        updateControls();
      }
    });

    fingerSelect.addEventListener("change", () => {
      if (tryon?.running) {
        tryon.finger = fingerSelect.value;
      }
    });

    flipButton.addEventListener("click", async () => {
      if (!tryon?.running) {
        return;
      }

      flipButton.disabled = true;

      try {
        await tryon.flipCamera();
      } catch (error) {
        console.error("Camera change failed:", error);
      } finally {
        flipButton.disabled = false;
      }
    });

    saveButton.addEventListener("click", async () => {
      if (!tryon?.running) {
        return;
      }

      saveButton.disabled = true;

      try {
        await tryon.saveImage({
          name: "ring-tryon",
          extension: "png",
          mode: "medium"
        });
      } catch (error) {
        console.error("Image save failed:", error);
      } finally {
        saveButton.disabled = false;
      }
    });

    initializeTryon().catch((error) => {
      console.error("Try-On setup failed:", error);
    });
  </script>
</body>
</html>

Apply the saved model configuration

ijewel-file-data can contain the configuration as an object or a JSON string. If the file configuration is empty, the example uses defaultConfig. tryon.fromJSON() applies the saved placement values to the plugin. The type value tells WebGi which plugin owns the configuration. See RingTryonPlugin configuration for all configuration groups.

Select the active finger

Web VTO 0.3.0 uses the finger setter. Set the finger after tryon.start() initializes hand tracking.

javascript
await tryon.start();
tryon.finger = "ring";

You can also change the finger while Try-On is active.

javascript
if (tryon.running) {
  tryon.finger = "index";
}

The supported values are thumb, index, middle, ring, and pinky. assignMainRingToFinger is deprecated. Do not use it in a new integration. See RingTryonPlugin placement for the complete placement API.

Add controls to the Try-On interface

Use tryon.running to check whether Try-On is active.

ActionAPI
Start Try-Onawait tryon.start()
Stop Try-Onawait tryon.stop()
Read the statetryon.running
Change the fingertryon.finger = "ring"
Set digital zoomtryon.videoScale = 1.5
Request hardware camera zoomtryon.cameraZoom = 2
Flip the cameraawait tryon.flipCamera()
Select the next cameraawait tryon.selectNextCamera()
Save an imageawait tryon.saveImage(options)
Check the save statetryon.saveImageInProgress
Get an image for uploadawait tryon.getImageBlob()

See the RingTryonPlugin API Reference for all lifecycle, camera, placement, and image APIs.

Add a zoom slider

Use videoScale for a zoom control that works without camera hardware support. The value scales the camera view and keeps the ring aligned with the hand.

If your interface controls zoom, turn off automatic interaction zoom.

html
<label for="tryon-zoom">Zoom</label>
<input
  id="tryon-zoom"
  type="range"
  min="1"
  max="2"
  step="0.05"
  value="1.35"
>
javascript
const zoomInput = document.getElementById("tryon-zoom");

zoomInput.addEventListener("input", () => {
  if (!tryon) {
    return;
  }

  tryon.interactionZoom.enabled = false;
  tryon.videoScale = Number(zoomInput.value);
});

cameraZoom requests optical or hardware zoom from the active camera. Camera support and available values differ between devices. Use videoScale as the main website zoom control.

Respond to plugin events

Use plugin events when controls can change from more than one part of the page.

javascript
tryon.addEventListener("initialized", updateControls);
tryon.addEventListener("stop", updateControls);

tryon.addEventListener("error", (event) => {
  console.error(
    "Try-On error:",
    event.detail?.error ?? event.detail
  );

  updateControls();
});

See RingTryonPlugin events for the complete event list.

Switch products without another viewer

Stop Try-On before you change the model. Then call loadModelById with the existing miniViewer in the viewer option.

javascript
async function loadProduct(nextModelId) {
  tryonButton.disabled = true;
  fingerSelect.disabled = true;

  try {
    if (tryon?.running) {
      await tryon.stop();
    }

    const fileDataReady = waitForWindowEvent("ijewel-file-data");
    const sceneReady = waitForWindowEvent("ijewel-scene-ready");

    const loadedViewer = await window.ijewelViewer.loadModelById(
      nextModelId,
      DRIVE_BASENAME,
      container,
      {
        viewer: miniViewer // Reuses the ViewerApp from ijewel-viewer-ready.
      }
    );

    if (!loadedViewer) {
      throw new Error("The next model did not load.");
    }

    const fileDataEvent = await fileDataReady;
    await sceneReady;

    await applyTryonConfiguration(
      fileDataEvent.detail.iJewelFileData
    );
  } finally {
    updateControls();
  }
}

The viewer option reuses the canvas, WebGi viewer, and Try-On plugin. Each new model must contain its own saved Try-On configuration.

Stop Try-On before the view closes

Stop the plugin before your application hides or removes the viewer container.

javascript
async function closeTryonView() {
  if (tryon?.running) {
    await tryon.stop();
  }

  // Close the modal or change the route here.
}

This action releases the camera and restores the normal 3D viewer state.

Allow camera access in an iframe

If the integration runs in an iframe, add camera permission to the iframe.

html
<iframe
  src="https://shop.example.com/tryon"
  allow="camera; fullscreen"
  allowfullscreen
></iframe>

Serve the parent page and the iframe page through HTTPS. The parent website permission policy must also allow camera access.

Solve advanced integration problems

ProblemCauseAction
window.ij_vto is undefined.The Web VTO script did not load, or the script order is wrong.Use the four script blocks from this page in the same order.
The saved configuration is undefined.The ijewel-file-data listener was added too late.Register the event promise before loadModelById.
The ring uses the wrong placement.The code did not apply tryonConfig after the scene became ready.Wait for ijewel-scene-ready. Then call tryon.fromJSON().
The finger does not change.The code set tryon.finger before hand tracking started.Set the finger after await tryon.start() or during an active session.
Product changes create extra canvases.The code creates a new Mini Viewer for each product.Pass { viewer: miniViewer } to loadModelById.
The camera stays active after a modal closes.The application removed the view before it stopped Try-On.Call await tryon.stop() before you remove the viewer container.