3let categories =
new Set();
4let currentSelectedMaterial =
null;
5let polyHavenAPI =
null;
7let codeMirrorEditor =
null;
8const materialPackageCache = {};
9const materialContentCache = {};
10let desiredTextureFormat =
'png';
13const materialsContainer = document.getElementById(
'materialsContainer');
14const searchInput = document.getElementById(
'searchInput');
15const searchButton = document.getElementById(
'searchButton');
16const categoryFilter = document.getElementById(
'categoryFilter');
17const resolutionFilter = document.getElementById(
'resolutionFilter');
18const mainSpinner = document.getElementById(
'mainSpinner');
19const materialModal =
new bootstrap.Modal(document.getElementById(
'materialModal'));
22let targetURL =
"https://kwokcb.github.io/MaterialXLab/javascript/shader_utilities/dist/index.html?viewerOnly=1&geom=Geometry/sphere.glb";
26 function setTheme(mode) {
27 document.documentElement.setAttribute(
'data-bs-theme', mode);
28 document.getElementById(
'themeIcon').className = mode ===
'dark' ?
'bi bi-sun' :
'bi bi-moon';
32document.addEventListener(
'DOMContentLoaded',
function () {
35 if (!document.getElementById(
'disabled-grid-style')) {
36 const style = document.createElement(
'style');
37 style.id =
'disabled-grid-style';
38 style.textContent =
'.disabled-grid { pointer-events: none !important; opacity: 0.5 !important; filter: grayscale(0.5); transition: opacity 0.3s; }';
39 document.head.appendChild(style);
43 const urlParams =
new URLSearchParams(window.location.search);
44 const urlFormat = urlParams.get(
'desiredTextureFormat');
46 testFormat = urlFormat.toLowerCase();
48 if (testFormat ===
'jpg' || testFormat ===
'png' || testFormat ===
'exr') {
49 desiredTextureFormat = testFormat;
50 console.log(
'Texture format set from URL:', desiredTextureFormat);
79 const prefersDarkScheme = window.matchMedia(
"(prefers-color-scheme: dark)").matches;
80 if (prefersDarkScheme) {
86 document.getElementById(
'themeToggleBtn').addEventListener(
'click',
function () {
87 const isDark = document.documentElement.getAttribute(
'data-bs-theme') ===
'dark';
88 setTheme(isDark ?
'light' :
'dark');
92 const svgString = `<svg xmlns=
"http://www.w3.org/2000/svg" width=
"16" height=
"16" fill=
"currentColor" class=
"bi bi-card-image" viewBox=
"0 0 16 16">
93 <path d=
"M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/>
94 <path d=
"M1.5 2A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2zm13 1a.5.5 0 0 1 .5.5v6l-3.775-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12v.54L1 12.5v-9a.5.5 0 0 1 .5-.5z"/>
98 svgDataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`;
''
103 materialsContainer.classList.add(
'disabled-grid');
108 const messagetype =
'viewer-loaded';
109 let viewer = document.getElementById(
'viewer');
110 viewer.src = targetURL;
112 function handleViewerReady(event) {
115 const data = typeof
event.data ===
'string' ? JSON.parse(event.data) :
event.data;
116 if (data && data.type === messagetype) {
117 console.log(
'>> Viewer loaed. Allow interaction with material grid.');
118 materialsContainer.classList.remove(
'disabled-grid');
119 window.removeEventListener(
'message', handleViewerReady);
123 window.addEventListener(
'message', handleViewerReady);
128 searchButton.addEventListener(
'click', filterMaterials);
129 searchInput.addEventListener(
'keyup',
function (e) {
130 if (e.key ===
'Enter') filterMaterials();
132 categoryFilter.addEventListener(
'change', filterMaterials);
133 resolutionFilter.addEventListener(
'change', filterMaterials);
136 document.getElementById(
'downloadMaterial').addEventListener(
'click', downloadMaterial);
137 document.getElementById(
'copyMaterialLink').addEventListener(
'click', copyMaterialLink);
138 document.getElementById(
'previewMaterial').addEventListener(
'click', previewMaterial);
141 const materialModalElement = document.getElementById(
'materialModal');
142 materialModalElement.addEventListener(
'shown.bs.modal',
function () {
143 if (codeMirrorEditor)
146 codeMirrorEditor.setValue(
'');
150 let editor = document.getElementById(
'mtlxEditor')
152 codeMirrorEditor = CodeMirror.fromTextArea(editor, {
160 let textureGallery = document.getElementById(
'textureGallery');
161 if (textureGallery) {
164 textureGallery.innerHTML =
'';
167 console.info(
'Texture gallery container not found');
172 viewer.src = targetURL;
174 materialModalElement.addEventListener(
'hidden.bs.modal',
function () {
176 viewer.style.display =
'none';
180 for (
const key in materialPackageCache) {
181 delete materialPackageCache[key];
184 let contentPreview = document.getElementById(
'contentPreview');
185 if (contentPreview) {
186 contentPreview.style.display =
'none';
190 document.getElementById(
'loadContentBtn').addEventListener(
'click', async () => {
191 if (currentSelectedMaterial) {
192 await loadMaterialContent(currentSelectedMaterial.id);
198async
function downloadMaterial() {
199 if (!currentSelectedMaterial || !polyHavenAPI)
return;
201 console.log(`Preparing download
for material: ${currentSelectedMaterial.name} (ID: ${currentSelectedMaterial.id})`);
203 const resolution = document.getElementById(
'materialResolution').value;
204 const materialName = currentSelectedMaterial.name.replace(/[^a-z0-9]/gi,
'_').toLowerCase();
207 const downloadBtn = document.getElementById(
'downloadMaterial');
208 const originalText = downloadBtn.innerHTML;
209 downloadBtn.innerHTML =
'<i class="bi bi-arrow-clockwise spin me-2"></i>Preparing download...';
210 downloadBtn.disabled =
true;
213 const cacheKey = `${currentSelectedMaterial.id}_${resolution}`;
214 let zipBlob = materialPackageCache[cacheKey];
216 zipBlob = await polyHavenAPI.createMaterialXPackage(currentSelectedMaterial, resolution);
217 materialPackageCache[cacheKey] = zipBlob;
224 const url = URL.createObjectURL(zipBlob);
225 const a = document.createElement(
'a');
227 a.download = `${materialName}_${resolution}_materialx.zip`;
228 document.body.appendChild(a);
233 document.body.removeChild(a);
234 URL.revokeObjectURL(url);
238 console.error(
'Error creating MaterialX package:', error);
239 alert(`Failed to prepare download: ${error.message}`);
242 downloadBtn.innerHTML = originalText;
243 downloadBtn.disabled =
false;
247function waitForViewerReady(viewer) {
248 return new Promise((resolve) => {
249 function handler(event) {
251 const data = JSON.parse(event.data);
252 if (data.type ===
'viewer-ready') {
253 window.removeEventListener(
'message', handler);
260 window.addEventListener(
'message', handler);
264async
function previewMaterial() {
265 if (!currentSelectedMaterial || !polyHavenAPI)
return;
267 let viewer = document.getElementById(
'viewer');
269 console.info(
'Viewer not found !');
272 let previewButton = document.getElementById(
'previewMaterial')
273 let previousHTML = previewButton.innerHTML;
274 previewButton.innerHTML =
'<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Fetching Data...';
276 const resolution = document.getElementById(
'materialResolution').value;
278 const cacheKey = `${currentSelectedMaterial.id}_${resolution}`;
281 let contentData = materialContentCache[cacheKey];
283 contentData = await polyHavenAPI.getMaterialContent(currentSelectedMaterial.id, resolution, desiredTextureFormat);
284 materialContentCache[cacheKey] = contentData;
288 let zipBlob = materialPackageCache[cacheKey];
290 zipBlob = await polyHavenAPI.createMaterialXPackage(currentSelectedMaterial,
291 resolution, contentData);
292 materialPackageCache[cacheKey] = zipBlob;
296 const arrayBuffer = await zipBlob.arrayBuffer();
300 const readyPromise =
new Promise((resolve, reject) => {
301 const timeout = setTimeout(() => reject(
new Error(
'Viewer timed out')), 30000);
302 function handler(event) {
304 const data = JSON.parse(event.data);
305 if (data.type ===
'viewer-ready') {
306 clearTimeout(timeout);
307 window.removeEventListener(
'message', handler);
312 window.addEventListener(
'message', handler);
316 console.log(
'Posting preview data to viewer page...');
317 if (viewer && viewer.contentWindow) {
318 viewer.contentWindow.postMessage(arrayBuffer, targetURL);
321 previewButton.innerHTML =
'<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Rendering...';
326 viewer.style.display =
'block';
328 alert(
'Preview viewer failed to load. Please try again.');
329 console.error(error);
331 previewButton.innerHTML = previousHTML;
337async
function loadMaterials() {
339 console.error(
'PolyHaven API not initialized');
343 mainSpinner.style.display =
'block';
344 materialsContainer.innerHTML =
'';
348 const result = await polyHavenAPI.fetchMaterials();
350 allMaterials = result.materials;
351 categories = result.categories;
354 populateCategoryFilter();
359 console.error(
'Error loading materials:', error);
360 materialsContainer.innerHTML = `
362 <div
class=
"alert alert-danger" role=
"alert">
363 Failed to load materials from Poly Haven. Please
try again later.
368 mainSpinner.style.display =
'none';
373function populateCategoryFilter() {
374 categoryFilter.innerHTML =
'<option value="">All Categories</option>';
376 const sortedCategories = Array.from(categories).sort();
377 sortedCategories.forEach(category => {
378 const option = document.createElement(
'option');
379 option.value = category;
380 option.textContent = category.charAt(0).toUpperCase() + category.slice(1);
381 categoryFilter.appendChild(option);
386function filterMaterials() {
387 const searchTerm = searchInput.value.toLowerCase();
388 const selectedCategory = categoryFilter.value;
390 const filtered = allMaterials.filter(material => {
392 const matchesSearch = material.name.toLowerCase().includes(searchTerm) ||
393 material.description.toLowerCase().includes(searchTerm) ||
394 material.tags.some(tag => tag.toLowerCase().includes(searchTerm));
397 const matchesCategory = !selectedCategory || material.categories.includes(selectedCategory);
399 return matchesSearch && matchesCategory;
402 displayMaterials(filtered);
406function displayMaterials(materials) {
409 materialsContainer.innerHTML =
'';
411 if (materials.length === 0) {
412 materialsContainer.innerHTML = `
414 <div
class=
"alert alert-info" role=
"alert">
415 No materials found matching your criteria.
422 const fragment = document.createDocumentFragment();
423 materials.forEach(material => {
424 const col = document.createElement(
'div');
425 col.className =
'col-md-3 col-lg-2 mb-4';
427 let thumbUrl = material.thumb_url;
428 thumbUrl = thumbUrl.replace(
'512',
'256');
431 <div
class=
"card material-card" data-material-
id=
"${material.id}">
432 <img src=
"${thumbUrl}" class=
"card-img-top material-img" alt=
"${material.name}" loading=
"lazy" decoding=
"async" onerror=
"this.src=${svgDataUrl}">
433 <div
class=
"card-body">
434 <div
class=
"card-title">${material.name}</div>
435 <div
class=
"d-flex flex-wrap">
436 ${material.categories.map(cat => `<span
class=
"badge bg-secondary category-badge">${cat}</span>`).join(
'')}
442 col.querySelector(
'.card').addEventListener(
'click', () => {
443 showMaterialDetails(material);
445 fragment.appendChild(col);
447 materialsContainer.appendChild(fragment);
451async
function loadMaterialContent(materialId) {
453 console.error(
'PolyHaven API not initialized');
458 let contentPreview = document.getElementById(
'contentPreview');
459 contentPreview.style.display =
'block';
461 const loadBtn = document.getElementById(
'loadContentBtn');
462 const originalText = loadBtn.innerHTML;
463 loadBtn.innerHTML =
'<i class="bi bi-arrow-clockwise spin me-2"></i>Loading...';
464 loadBtn.disabled =
true;
468 const resolution = document.getElementById(
'materialResolution').value;
469 const cacheKey = `${materialId}_${resolution}`;
470 let contentData = materialContentCache[cacheKey];
475 contentData = await polyHavenAPI.getMaterialContent(materialId, resolution, desiredTextureFormat);
476 materialContentCache[cacheKey] = contentData;
480 let mtlxContent = contentData.mtlxContent ||
'';
483 const textureFiles = contentData.textureFiles;
484 const galleryContainer = document.getElementById(
'textureGallery');
485 galleryContainer.innerHTML =
'';
487 let textureNames = [];
489 const createTextureCard = (textureName, textureUrl) =>
491 const card = document.createElement(
'div');
492 card.className =
'col-sm-3 col-md-3 col-lg-3 mb-2';
494 textureNames.push(textureName);
497 <div
class=
"card h-100">
498 <div
class=
"ratio ratio-1x1">
499 <img src=
"${textureUrl}"
500 class=
"card-img-top object-fit-contain p-2"
503 onerror=
"this.onerror=null;this.src='${svgDataUrl}';">
505 <div
class=
"card-body p-2">
506 <small
class=
"text-truncate d-block">${textureName}</small>
514 for (
const [path, fileData] of Object.entries(textureFiles)) {
515 const textureURI = path.split(
'/').pop();
516 let card = createTextureCard(textureURI, fileData.url);
517 galleryContainer.appendChild(card);
521 if (codeMirrorEditor) {
522 codeMirrorEditor.setValue(mtlxContent);
525 loadBtn.innerHTML = originalText;
526 loadBtn.disabled =
false;
529 console.error(
'Error loading content:', error);
530 const contentPreview = document.getElementById(
'contentPreview');
531 contentPreview.innerHTML += `<div
class=
"alert alert-danger">Failed to load content: ${error.message}</div>`;
532 loadBtn.innerHTML = originalText;
533 loadBtn.disabled =
false;
539async
function showMaterialDetails(material) {
540 currentSelectedMaterial = material;
542 if (codeMirrorEditor)
544 codeMirrorEditor.setValue(
'');
548 document.getElementById(
'materialModalLabel').textContent = material.name;
550 document.getElementById(
'materialDescription').textContent = material.description;
551 let thumbUrl = material.thumb_url;
553 document.getElementById(
'materialPreview').src = thumbUrl;
556 const tagsContainer = document.getElementById(
'materialTags');
557 const tagsList = material.tags.map(tag =>
558 `<span
class=
"badge bg-secondary">${tag}</span>`
560 tagsContainer.innerHTML =
'<span class="badge bg-dark">Tags</span> ' + tagsList
563 const categoriesContainer = document.getElementById(
'materialCategories');
564 const categoriesList = material.categories.map(cat =>
565 `<span
class=
"badge bg-secondary">${cat}</span>`
567 categoriesContainer.innerHTML =
'<span class="badge bg-dark">Categories</span> ' + categoriesList
576 document.getElementById(
'materialResolution').value =
'1k';
579 const galleryContainer = document.getElementById(
'textureGallery');
580 if (galleryContainer) {
581 galleryContainer.innerHTML =
'';
583 if (codeMirrorEditor) {
584 codeMirrorEditor.setValue(
'');
588 materialModal.show();
592function copyMaterialLink() {
593 if (!currentSelectedMaterial)
return;
595 const materialUrl = `https:
596 navigator.clipboard.writeText(materialUrl)
598 const button = document.getElementById(
'copyMaterialLink');
599 const originalText = button.innerHTML;
600 button.innerHTML =
'<i class="bi bi-check-lg me-2"></i>Copied!';
602 button.innerHTML = originalText;
606 console.error(
'Failed to copy link: ', err);
607 alert(
'Failed to copy link to clipboard');
JsPolyHavenAPILoader - A JavaScript class for interacting with the Poly Haven API Handles fetching ma...