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.
| Object | Purpose |
|---|---|
miniViewer | The Mini Viewer wrapper returned by loadModelById. Use it for product changes. |
viewerApp | The 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.
<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.
| Event | Data | Use |
|---|---|---|
ijewel-file-data | The model file and its saved configuration. | Read tryonConfig for the current model. |
ijewel-viewer-ready | The WebGi ViewerApp. | Add or get the Try-On plugins. |
ijewel-scene-ready | The 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.
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.
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:
- Replace
MODEL_IDwith the model file ID. - Change
DRIVE_BASENAMEonly for an enterprise iJewel3D Platform instance. - Replace the control markup and CSS with your product design.
Keep the event order and plugin initialization unchanged.
<!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.
await tryon.start();
tryon.finger = "ring";You can also change the finger while Try-On is active.
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.
| Action | API |
|---|---|
| Start Try-On | await tryon.start() |
| Stop Try-On | await tryon.stop() |
| Read the state | tryon.running |
| Change the finger | tryon.finger = "ring" |
| Set digital zoom | tryon.videoScale = 1.5 |
| Request hardware camera zoom | tryon.cameraZoom = 2 |
| Flip the camera | await tryon.flipCamera() |
| Select the next camera | await tryon.selectNextCamera() |
| Save an image | await tryon.saveImage(options) |
| Check the save state | tryon.saveImageInProgress |
| Get an image for upload | await 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.
<label for="tryon-zoom">Zoom</label>
<input
id="tryon-zoom"
type="range"
min="1"
max="2"
step="0.05"
value="1.35"
>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.
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.
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.
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.
<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
| Problem | Cause | Action |
|---|---|---|
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. |