3let categories =
new Set();
4let currentSelectedMaterial =
null;
5let polyHavenAPI =
null;
7let codeMirrorEditor =
null;
8const materialPackageCache = {};
11const materialsContainer = document.getElementById(
'materialsContainer');
12const searchInput = document.getElementById(
'searchInput');
13const searchButton = document.getElementById(
'searchButton');
14const categoryFilter = document.getElementById(
'categoryFilter');
15const resolutionFilter = document.getElementById(
'resolutionFilter');
16const mainSpinner = document.getElementById(
'mainSpinner');
17const materialModal =
new bootstrap.Modal(document.getElementById(
'materialModal'));
20let targetURL =
"https://kwokcb.github.io/MaterialXLab/javascript/shader_utilities/dist/index.html?viewerOnly=1";
24 function setTheme(mode) {
25 document.documentElement.setAttribute(
'data-bs-theme', mode);
26 document.getElementById(
'themeIcon').className = mode ===
'dark' ?
'bi bi-sun' :
'bi bi-moon';
30document.addEventListener(
'DOMContentLoaded',
function () {
35 const prefersDarkScheme = window.matchMedia(
"(prefers-color-scheme: dark)").matches;
36 if (prefersDarkScheme) {
42 document.getElementById(
'themeToggleBtn').addEventListener(
'click',
function () {
43 const isDark = document.documentElement.getAttribute(
'data-bs-theme') ===
'dark';
44 setTheme(isDark ?
'light' :
'dark');
48 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">
49 <path d=
"M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/>
50 <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"/>
55 svgDataUrl =
'https://icons.getbootstrap.com/assets/icons/card-image.svg'
61 searchButton.addEventListener(
'click', filterMaterials);
62 searchInput.addEventListener(
'keyup',
function (e) {
63 if (e.key ===
'Enter') filterMaterials();
65 categoryFilter.addEventListener(
'change', filterMaterials);
66 resolutionFilter.addEventListener(
'change', filterMaterials);
69 document.getElementById(
'downloadMaterial').addEventListener(
'click', downloadMaterial);
70 document.getElementById(
'copyMaterialLink').addEventListener(
'click', copyMaterialLink);
71 document.getElementById(
'materialResolution').addEventListener(
'change', updateMapsDisplay);
72 document.getElementById(
'previewMaterial').addEventListener(
'click', previewMaterial);
75 const materialModalElement = document.getElementById(
'materialModal');
76 materialModalElement.addEventListener(
'shown.bs.modal',
function () {
77 if (codeMirrorEditor)
return;
79 let editor = document.getElementById(
'mtlxEditor')
81 codeMirrorEditor = CodeMirror.fromTextArea(editor, {
90 let viewer = document.getElementById(
'viewer');
91 viewer.src = targetURL;
93 materialModalElement.addEventListener(
'hidden.bs.modal',
function () {
95 viewer.style.display =
'none';
99 for (
const key in materialPackageCache) {
100 delete materialPackageCache[key];
105async
function downloadMaterial() {
106 if (!currentSelectedMaterial || !polyHavenAPI)
return;
108 const resolution = document.getElementById(
'materialResolution').value;
109 const materialName = currentSelectedMaterial.name.replace(/[^a-z0-9]/gi,
'_').toLowerCase();
112 const downloadBtn = document.getElementById(
'downloadMaterial');
113 const originalText = downloadBtn.innerHTML;
114 downloadBtn.innerHTML =
'<i class="bi bi-arrow-clockwise spin me-2"></i>Preparing download...';
115 downloadBtn.disabled =
true;
118 const cacheKey = `${currentSelectedMaterial.id}_${resolution}`;
119 let zipBlob = materialPackageCache[cacheKey];
121 zipBlob = await polyHavenAPI.createMaterialXPackage(currentSelectedMaterial, resolution);
122 materialPackageCache[cacheKey] = zipBlob;
129 const url = URL.createObjectURL(zipBlob);
130 const a = document.createElement(
'a');
132 a.download = `${materialName}_${resolution}_materialx.zip`;
133 document.body.appendChild(a);
138 document.body.removeChild(a);
139 URL.revokeObjectURL(url);
143 console.error(
'Error creating MaterialX package:', error);
144 alert(`Failed to prepare download: ${error.message}`);
147 downloadBtn.innerHTML = originalText;
148 downloadBtn.disabled =
false;
152function waitForViewerReady(viewer) {
153 return new Promise((resolve) => {
154 function handler(event) {
156 const data = JSON.parse(event.data);
157 if (data.type ===
'viewer-ready') {
158 window.removeEventListener(
'message', handler);
165 window.addEventListener(
'message', handler);
169async
function previewMaterial() {
170 if (!currentSelectedMaterial || !polyHavenAPI)
return;
172 let viewer = document.getElementById(
'viewer');
174 console.info(
'Viewer not found !');
177 let previewButton = document.getElementById(
'previewMaterial')
178 let previousText = previewButton.textContent;
179 previewButton.textContent =
'Loading...';
181 const resolution = document.getElementById(
'materialResolution').value;
183 const cacheKey = `${currentSelectedMaterial.id}_${resolution}`;
184 let zipBlob = materialPackageCache[cacheKey];
186 zipBlob = await polyHavenAPI.createMaterialXPackage(currentSelectedMaterial, resolution);
187 materialPackageCache[cacheKey] = zipBlob;
194 const arrayBuffer = await zipBlob.arrayBuffer();
197 console.log(
'Posting preview data to viewer page...');
198 if (viewer && viewer.contentWindow) {
199 viewer.contentWindow.postMessage(arrayBuffer, targetURL);
204 let failed_messaage =
false;
205 const timeoutPromise =
new Promise((_, reject) => setTimeout(() => reject(
new Error(
"Timeout waiting for viewer-ready")), 5000));
206 await Promise.race([waitForViewerReady(viewer), timeoutPromise]).catch(error => {
210 viewer.style.display =
'block';
213 console.error(
'Error preparing preview:', error);
214 alert(`Failed to prepare preview: ${error.message}`);
216 previewButton.textContent = previousText;
222async
function loadMaterials() {
224 console.error(
'PolyHaven API not initialized');
228 mainSpinner.style.display =
'block';
229 materialsContainer.innerHTML =
'';
233 const result = await polyHavenAPI.fetchMaterials();
235 allMaterials = result.materials;
236 categories = result.categories;
239 populateCategoryFilter();
244 console.error(
'Error loading materials:', error);
245 materialsContainer.innerHTML = `
247 <div
class=
"alert alert-danger" role=
"alert">
248 Failed to load materials from Poly Haven. Please
try again later.
253 mainSpinner.style.display =
'none';
258function populateCategoryFilter() {
259 categoryFilter.innerHTML =
'<option value="">All Categories</option>';
261 const sortedCategories = Array.from(categories).sort();
262 sortedCategories.forEach(category => {
263 const option = document.createElement(
'option');
264 option.value = category;
265 option.textContent = category.charAt(0).toUpperCase() + category.slice(1);
266 categoryFilter.appendChild(option);
271function filterMaterials() {
272 const searchTerm = searchInput.value.toLowerCase();
273 const selectedCategory = categoryFilter.value;
275 const filtered = allMaterials.filter(material => {
277 const matchesSearch = material.name.toLowerCase().includes(searchTerm) ||
278 material.description.toLowerCase().includes(searchTerm) ||
279 material.tags.some(tag => tag.toLowerCase().includes(searchTerm));
282 const matchesCategory = !selectedCategory || material.categories.includes(selectedCategory);
284 return matchesSearch && matchesCategory;
287 displayMaterials(filtered);
291function displayMaterials(materials) {
292 materialsContainer.innerHTML =
'';
294 if (materials.length === 0) {
295 materialsContainer.innerHTML = `
297 <div
class=
"alert alert-info" role=
"alert">
298 No materials found matching your criteria.
305 materials.forEach(material => {
306 const col = document.createElement(
'div');
307 col.className =
'col-md-3 col-lg-2 mb-4';
310 <div
class=
"card material-card" data-material-
id=
"${material.id}">
311 <img src=
"${material.thumb_url}" class=
"card-img-top material-img" alt=
"${material.name}" onerror=
"this.src=${svgDataUrl}">
312 <div
class=
"card-body">
313 <div
class=
"card-title">${material.name}</div>
314 <div
class=
"d-flex flex-wrap">
315 ${material.categories.map(cat => `<span
class=
"badge bg-secondary category-badge">${cat}</span>`).join(
'')}
321 col.querySelector(
'.card').addEventListener(
'click', () => showMaterialDetails(material));
322 materialsContainer.appendChild(col);
327async
function loadMaterialContent(materialId) {
329 console.error(
'PolyHaven API not initialized');
333 const loadBtn = document.getElementById(
'loadContentBtn');
334 const originalText = loadBtn.innerHTML;
335 loadBtn.innerHTML =
'<i class="bi bi-arrow-clockwise spin me-2"></i>Loading...';
336 loadBtn.disabled =
true;
339 const resolution = document.getElementById(
'materialResolution').value;
342 const contentData = await polyHavenAPI.getMaterialContent(materialId, resolution);
345 const previewContainer = document.getElementById(
'contentPreview');
346 previewContainer.innerHTML = `
348 <b>MaterialX Document</b>
349 <textarea
id=
"mtlxEditor">${contentData.mtlxContent}</textarea>
352 <div
id=
"textureGallery" class=
"row g-2"></div>
358 if (codeMirrorEditor) {
359 codeMirrorEditor.toTextArea();
361 let editor = document.getElementById(
'mtlxEditor');
363 codeMirrorEditor = CodeMirror.fromTextArea(editor, {
373 const textureFiles = contentData.textureFiles;
374 const galleryContainer = document.getElementById(
'textureGallery');
375 galleryContainer.innerHTML =
'';
377 const createTextureCard = (textureName, textureUrl) => {
378 const card = document.createElement(
'div');
379 card.className =
'col-6 col-md-4 col-lg-3 mb-3';
382 <div
class=
"card h-100">
383 <div
class=
"ratio ratio-1x1 bg-light">
384 <img src=
"${textureUrl}"
385 class=
"card-img-top object-fit-contain p-2"
388 onerror=
"this.onerror=null;this.src='${svgDataUrl}';">
390 <div
class=
"card-body p-2">
391 <small
class=
"text-truncate d-block">${textureName}</small>
399 for (
const [path, fileData] of Object.entries(textureFiles)) {
400 const textureName = path.split(
'/').pop();
401 const card = createTextureCard(textureName, fileData.url);
402 galleryContainer.appendChild(card);
406 console.error(
'Error loading content:', error);
407 document.getElementById(
'contentPreview').innerHTML += `
408 <div
class=
"alert alert-danger mt-3">
409 Failed to load content: ${error.message}
413 loadBtn.innerHTML = originalText;
414 loadBtn.disabled =
false;
419async
function showMaterialDetails(material) {
420 currentSelectedMaterial = material;
422 if (codeMirrorEditor)
424 codeMirrorEditor.setValue(
'');
428 document.getElementById(
'materialModalLabel').textContent = material.name;
430 document.getElementById(
'materialDescription').textContent = material.description;
431 document.getElementById(
'materialPreview').src = material.thumb_url;
434 const tagsContainer = document.getElementById(
'materialTags');
435 tagsList = material.tags.map(tag =>
436 `<span
class=
"badge bg-secondary">${tag}</span>`
438 tagsContainer.innerHTML =
'<span class="badge bg-dark">Tags</span> ' + tagsList
441 const categoriesContainer = document.getElementById(
'materialCategories');
442 categoriesList = material.categories.map(cat =>
443 `<span
class=
"badge bg-secondary">${cat}</span>`
445 categoriesContainer.innerHTML =
'<span class="badge bg-dark">Categories</span> ' + categoriesList
449 document.getElementById(
'contentPreview').innerHTML = `
450 <div
class=
"text-center py-1">
451 <button style=
"font-size: 11px;" class=
"btn btn-primary" id=
"loadContentBtn">
452 <i
class=
"bi bi-eye me-2"></i>Show Content
458 document.getElementById(
'loadContentBtn').addEventListener(
'click', async () => {
459 await loadMaterialContent(material.id);
463 document.getElementById(
'materialResolution').value =
'1k';
466 materialModal.show();
470function updateMapsDisplay() {
471 if (!currentSelectedMaterial)
return;
473 const resolution = document.getElementById(
'materialResolution').value;
474 const mapsContainer = document.getElementById(
'materialMaps');
475 mapsContainer.innerHTML = currentSelectedMaterial.maps;
488function copyMaterialLink() {
489 if (!currentSelectedMaterial)
return;
491 const materialUrl = `https:
492 navigator.clipboard.writeText(materialUrl)
494 const button = document.getElementById(
'copyMaterialLink');
495 const originalText = button.innerHTML;
496 button.innerHTML =
'<i class="bi bi-check-lg me-2"></i>Copied!';
498 button.innerHTML = originalText;
502 console.error(
'Failed to copy link: ', err);
503 alert(
'Failed to copy link to clipboard');
JsPolyHavenAPILoader - A JavaScript class for interacting with the Poly Haven API Handles fetching ma...