MaterialXMaterials 1.39.5
Utilities for retrieving materials from remote servers
Loading...
Searching...
No Matches
main.js
1// Global variables
2let allMaterials = [];
3let categories = new Set();
4let currentSelectedMaterial = null;
5let polyHavenAPI = null;
6let svgDataUrl = null;
7let codeMirrorEditor = null;
8const materialPackageCache = {};
9const materialContentCache = {};
10let desiredTextureFormat = 'png';
11
12// DOM elements
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'));
20
21// Target URL for the viewer page
22let targetURL = "https://kwokcb.github.io/MaterialXLab/javascript/shader_utilities/dist/index.html?viewerOnly=1&geom=Geometry/sphere.glb";
23// Set for local testing
24//targetURL = "http://localhost:8000/javascript/shader_utilities/dist/index.html?viewerOnly=1";
25
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';
29}
30
31// Initialize the application
32document.addEventListener('DOMContentLoaded', function () {
33
34 // Inject CSS for disabled-grid if not present
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);
40 }
41
42 // Parse desiredTextureFormat from URL query string
43 const urlParams = new URLSearchParams(window.location.search);
44 const urlFormat = urlParams.get('desiredTextureFormat');
45 if (urlFormat) {
46 testFormat = urlFormat.toLowerCase();
47 // This appears to be all that PolyHaven supports for now
48 if (testFormat === 'jpg' || testFormat === 'png' || testFormat === 'exr') {
49 desiredTextureFormat = testFormat;
50 console.log('Texture format set from URL:', desiredTextureFormat);
51 }
52 }
53
54 // Add UI for selecting texture format
55 /* const formatSelector = document.createElement('select');
56 formatSelector.id = 'textureFormatSelector';
57 formatSelector.className = 'form-select mb-2';
58 ['jpg', 'png', 'tif', 'exr'].forEach(fmt => {
59 const option = document.createElement('option');
60 option.value = fmt;
61 option.textContent = fmt.toUpperCase();
62 if (fmt === desiredTextureFormat) option.selected = true;
63 formatSelector.appendChild(option);
64 });
65 formatSelector.addEventListener('change', function () {
66 desiredTextureFormat = this.value;
67 console.log('Texture format set to:', desiredTextureFormat);
68 });
69 // Insert selector at top of page (before materialsContainer)
70 const container = document.getElementById('materialsContainer');
71 if (container && container.parentNode) {
72 container.parentNode.insertBefore(formatSelector, container);
73 } */
74
75 // Initialize the Poly Haven API
76 polyHavenAPI = new JsPolyHavenAPILoader();
77
78 // Set theme based on browser preference or default to light mode
79 const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)").matches;
80 if (prefersDarkScheme) {
81 setTheme('dark');
82 } else {
83 setTheme('light');
84 }
85
86 document.getElementById('themeToggleBtn').addEventListener('click', function () {
87 const isDark = document.documentElement.getAttribute('data-bs-theme') === 'dark';
88 setTheme(isDark ? 'light' : 'dark');
89 });
90
91 // https://icons.getbootstrap.com/icons/card-image/
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"/>
95 </svg>`;
96
97 // Convert to base64 data URL
98 svgDataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`;''
99 //svgDataUrl = 'https://icons.getbootstrap.com/assets/icons/card-image.svg'
100
101
102 // Disable grid before any rendering
103 materialsContainer.classList.add('disabled-grid');
104
105 // Listen for "loaded" message from iframe to enable grid
106 // Using 'viewer-loaded' for now, but could use 'viewer-document-update'
107 // if wanted to wait for the first material to be loaded in the viewer before enabling the grid.
108 const messagetype = 'viewer-loaded';
109 let viewer = document.getElementById('viewer');
110 viewer.src = targetURL;
111
112 function handleViewerReady(event) {
113 // Only accept messages from the correct iframe origin
114 try {
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);
120 }
121 } catch (e) {}
122 }
123 window.addEventListener('message', handleViewerReady);
124
125 loadMaterials();
126
127 // Event listeners
128 searchButton.addEventListener('click', filterMaterials);
129 searchInput.addEventListener('keyup', function (e) {
130 if (e.key === 'Enter') filterMaterials();
131 });
132 categoryFilter.addEventListener('change', filterMaterials);
133 resolutionFilter.addEventListener('change', filterMaterials);
134
135 // Modal buttons
136 document.getElementById('downloadMaterial').addEventListener('click', downloadMaterial);
137 document.getElementById('copyMaterialLink').addEventListener('click', copyMaterialLink);
138 document.getElementById('previewMaterial').addEventListener('click', previewMaterial);
139
140 // Initialize CodeMirror when modal opens
141 const materialModalElement = document.getElementById('materialModal');
142 materialModalElement.addEventListener('shown.bs.modal', function () {
143 if (codeMirrorEditor)
144 {
145 // Clear content
146 codeMirrorEditor.setValue('');
147 return;
148 }
149
150 let editor = document.getElementById('mtlxEditor')
151 if (editor)
152 codeMirrorEditor = CodeMirror.fromTextArea(editor, {
153 mode: 'xml',
154 theme: 'material',
155 lineNumbers: true,
156 readOnly: true,
157 lineWrapping: true
158 });
159
160 let textureGallery = document.getElementById('textureGallery');
161 if (textureGallery) {
162 // clear
163 //console.info('Texture gallery container cleared.');
164 textureGallery.innerHTML = '';
165 }
166 else {
167 console.info('Texture gallery container not found');
168 }
169 });
170
171 //let viewer = document.getElementById('viewer');
172 viewer.src = targetURL;
173
174 materialModalElement.addEventListener('hidden.bs.modal', function () {
175 if (viewer) {
176 viewer.style.display = 'none';
177 }
178
179 // Clear the cache when the modal closes
180 for (const key in materialPackageCache) {
181 delete materialPackageCache[key];
182 }
183
184 let contentPreview = document.getElementById('contentPreview');
185 if (contentPreview) {
186 contentPreview.style.display = 'none';
187 }
188 });
189
190 document.getElementById('loadContentBtn').addEventListener('click', async () => {
191 if (currentSelectedMaterial) {
192 await loadMaterialContent(currentSelectedMaterial.id);
193 }
194 });
195
196});
197
198async function downloadMaterial() {
199 if (!currentSelectedMaterial || !polyHavenAPI) return;
200
201 console.log(`Preparing download for material: ${currentSelectedMaterial.name} (ID: ${currentSelectedMaterial.id})`);
202
203 const resolution = document.getElementById('materialResolution').value;
204 const materialName = currentSelectedMaterial.name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
205
206 // Show loading state
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;
211
212 try {
213 const cacheKey = `${currentSelectedMaterial.id}_${resolution}`;
214 let zipBlob = materialPackageCache[cacheKey];
215 if (!zipBlob) {
216 zipBlob = await polyHavenAPI.createMaterialXPackage(currentSelectedMaterial, resolution);
217 materialPackageCache[cacheKey] = zipBlob;
218 }
219
220 // Create the MaterialX package using the API class
221 //const zipBlob = await polyHavenAPI.createMaterialXPackage(currentSelectedMaterial, resolution);
222
223 // Trigger download
224 const url = URL.createObjectURL(zipBlob);
225 const a = document.createElement('a');
226 a.href = url;
227 a.download = `${materialName}_${resolution}_materialx.zip`;
228 document.body.appendChild(a);
229 a.click();
230
231 // Clean up
232 setTimeout(() => {
233 document.body.removeChild(a);
234 URL.revokeObjectURL(url);
235 }, 100);
236
237 } catch (error) {
238 console.error('Error creating MaterialX package:', error);
239 alert(`Failed to prepare download: ${error.message}`);
240 } finally {
241 // Restore button state
242 downloadBtn.innerHTML = originalText;
243 downloadBtn.disabled = false;
244 }
245}
246
247function waitForViewerReady(viewer) {
248 return new Promise((resolve) => {
249 function handler(event) {
250 try {
251 const data = JSON.parse(event.data);
252 if (data.type === 'viewer-ready') {
253 window.removeEventListener('message', handler);
254 resolve();
255 }
256 } catch (e) {
257 // Ignore non-JSON messages
258 }
259 }
260 window.addEventListener('message', handler);
261 });
262}
263
264async function previewMaterial() {
265 if (!currentSelectedMaterial || !polyHavenAPI) return;
266
267 let viewer = document.getElementById('viewer');
268 if (!viewer) {
269 console.info('Viewer not found !');
270 }
271
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...';
275
276 const resolution = document.getElementById('materialResolution').value;
277
278 const cacheKey = `${currentSelectedMaterial.id}_${resolution}`;
279
280 // Reuse cached content if available
281 let contentData = materialContentCache[cacheKey];
282 if (!contentData) {
283 contentData = await polyHavenAPI.getMaterialContent(currentSelectedMaterial.id, resolution, desiredTextureFormat);
284 materialContentCache[cacheKey] = contentData;
285 }
286
287
288 let zipBlob = materialPackageCache[cacheKey];
289 if (!zipBlob) {
290 zipBlob = await polyHavenAPI.createMaterialXPackage(currentSelectedMaterial,
291 resolution, contentData);
292 materialPackageCache[cacheKey] = zipBlob;
293 }
294
295 // Convert Blob to ArrayBuffer
296 const arrayBuffer = await zipBlob.arrayBuffer();
297
298
299 // Set up a promise to wait for the viewer to signal it's ready
300 const readyPromise = new Promise((resolve, reject) => {
301 const timeout = setTimeout(() => reject(new Error('Viewer timed out')), 30000);
302 function handler(event) {
303 try {
304 const data = JSON.parse(event.data);
305 if (data.type === 'viewer-ready') {
306 clearTimeout(timeout);
307 window.removeEventListener('message', handler);
308 resolve();
309 }
310 } catch (e) {}
311 }
312 window.addEventListener('message', handler);
313 });
314
315 // Post the ArrayBuffer to the target window (e.g., iframe or parent)
316 console.log('Posting preview data to viewer page...');
317 if (viewer && viewer.contentWindow) {
318 viewer.contentWindow.postMessage(arrayBuffer, targetURL);
319 }
320
321 previewButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Rendering...';
322
323 try {
324 // Wait for the viewer to signal it's ready before showing it
325 await readyPromise;
326 viewer.style.display = 'block';
327 } catch (error) {
328 alert('Preview viewer failed to load. Please try again.');
329 console.error(error);
330 } finally {
331 previewButton.innerHTML = previousHTML;
332 }
333}
334
335
336// Load materials from Poly Haven API
337async function loadMaterials() {
338 if (!polyHavenAPI) {
339 console.error('PolyHaven API not initialized');
340 return;
341 }
342
343 mainSpinner.style.display = 'block';
344 materialsContainer.innerHTML = '';
345
346 try {
347 // Fetch materials using the API class
348 const result = await polyHavenAPI.fetchMaterials();
349
350 allMaterials = result.materials;
351 categories = result.categories;
352
353 // Populate category filter
354 populateCategoryFilter();
355
356 // Display all materials initially
357 filterMaterials();
358 } catch (error) {
359 console.error('Error loading materials:', error);
360 materialsContainer.innerHTML = `
361 <div class="col-12">
362 <div class="alert alert-danger" role="alert">
363 Failed to load materials from Poly Haven. Please try again later.
364 </div>
365 </div>
366 `;
367 } finally {
368 mainSpinner.style.display = 'none';
369 }
370}
371
372// Populate the category filter dropdown
373function populateCategoryFilter() {
374 categoryFilter.innerHTML = '<option value="">All Categories</option>';
375
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);
382 });
383}
384
385// Filter materials based on search and category filters
386function filterMaterials() {
387 const searchTerm = searchInput.value.toLowerCase();
388 const selectedCategory = categoryFilter.value;
389
390 const filtered = allMaterials.filter(material => {
391 // Filter by search term
392 const matchesSearch = material.name.toLowerCase().includes(searchTerm) ||
393 material.description.toLowerCase().includes(searchTerm) ||
394 material.tags.some(tag => tag.toLowerCase().includes(searchTerm));
395
396 // Filter by category
397 const matchesCategory = !selectedCategory || material.categories.includes(selectedCategory);
398
399 return matchesSearch && matchesCategory;
400 });
401
402 displayMaterials(filtered);
403}
404
405// Display materials in the grid
406function displayMaterials(materials) {
407
408 // Grid remains disabled until viewer is loaded
409 materialsContainer.innerHTML = '';
410
411 if (materials.length === 0) {
412 materialsContainer.innerHTML = `
413 <div class="col-12">
414 <div class="alert alert-info" role="alert">
415 No materials found matching your criteria.
416 </div>
417 </div>
418 `;
419 return;
420 }
421
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';
426
427 let thumbUrl = material.thumb_url;
428 thumbUrl = thumbUrl.replace('512', '256');
429
430 col.innerHTML = `
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('')}
437 </div>
438 </div>
439 </div>
440 `;
441
442 col.querySelector('.card').addEventListener('click', () => {
443 showMaterialDetails(material);
444 });
445 fragment.appendChild(col);
446 });
447 materialsContainer.appendChild(fragment);
448}
449
450// New function to load material content
451async function loadMaterialContent(materialId) {
452 if (!polyHavenAPI) {
453 console.error('PolyHaven API not initialized');
454 return;
455 }
456
457 // Show content preview section
458 let contentPreview = document.getElementById('contentPreview');
459 contentPreview.style.display = 'block';
460
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;
465
466 //const previewContainer = document.getElementById('contentPreview');
467
468 const resolution = document.getElementById('materialResolution').value;
469 const cacheKey = `${materialId}_${resolution}`;
470 let contentData = materialContentCache[cacheKey];
471
472 try {
473 if (!contentData)
474 {
475 contentData = await polyHavenAPI.getMaterialContent(materialId, resolution, desiredTextureFormat);
476 materialContentCache[cacheKey] = contentData;
477 }
478
479 // Create preview content
480 let mtlxContent = contentData.mtlxContent || '';
481
482 // Load texture gallery
483 const textureFiles = contentData.textureFiles;
484 const galleryContainer = document.getElementById('textureGallery');
485 galleryContainer.innerHTML = '';
486
487 let textureNames = [];
488
489 const createTextureCard = (textureName, textureUrl) =>
490 {
491 const card = document.createElement('div');
492 card.className = 'col-sm-3 col-md-3 col-lg-3 mb-2';
493
494 textureNames.push(textureName);
495
496 card.innerHTML = `
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"
501 alt="${textureName}"
502 loading="lazy"
503 onerror="this.onerror=null;this.src='${svgDataUrl}';">
504 </div>
505 <div class="card-body p-2">
506 <small class="text-truncate d-block">${textureName}</small>
507 </div>
508 </div>
509 `;
510 return card;
511 };
512
513 // Create gallery items
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);
518 }
519
520 // Set mtlxContent to editor
521 if (codeMirrorEditor) {
522 codeMirrorEditor.setValue(mtlxContent);
523 }
524
525 loadBtn.innerHTML = originalText;
526 loadBtn.disabled = false;
527
528 } catch (error) {
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;
534 return;
535 }
536}
537
538// Modified showMaterialDetails function
539async function showMaterialDetails(material) {
540 currentSelectedMaterial = material;
541
542 if (codeMirrorEditor)
543 {
544 codeMirrorEditor.setValue('');
545 }
546
547 // Set basic info
548 document.getElementById('materialModalLabel').textContent = material.name;
549 //document.getElementById('materialTitle').textContent = material.name;
550 document.getElementById('materialDescription').textContent = material.description;
551 let thumbUrl = material.thumb_url;
552 //thumbUrl = thumbUrl.replace('512', '256');
553 document.getElementById('materialPreview').src = thumbUrl;
554
555 // Set tags
556 const tagsContainer = document.getElementById('materialTags');
557 const tagsList = material.tags.map(tag =>
558 `<span class="badge bg-secondary">${tag}</span>`
559 ).join(' ');
560 tagsContainer.innerHTML = '<span class="badge bg-dark">Tags</span> ' + tagsList
561
562 // Set categories
563 const categoriesContainer = document.getElementById('materialCategories');
564 const categoriesList = material.categories.map(cat =>
565 `<span class="badge bg-secondary">${cat}</span>`
566 ).join(' ');
567 categoriesContainer.innerHTML = '<span class="badge bg-dark">Categories</span> ' + categoriesList
568
569
570 // Set up content loader button
571 //document.getElementById('loadContentBtn').addEventListener('click', async () => {
572 // await loadMaterialContent(material.id);
573 //});
574
575 // Force 1K for downloads
576 document.getElementById('materialResolution').value = '1k';
577
578 // Clear texture gallery and editor content
579 const galleryContainer = document.getElementById('textureGallery');
580 if (galleryContainer) {
581 galleryContainer.innerHTML = '';
582 }
583 if (codeMirrorEditor) {
584 codeMirrorEditor.setValue('');
585 }
586
587 // Show modal
588 materialModal.show();
589}
590
591// Copy material link handler
592function copyMaterialLink() {
593 if (!currentSelectedMaterial) return;
594
595 const materialUrl = `https://polyhaven.com/a/${currentSelectedMaterial.id}`;
596 navigator.clipboard.writeText(materialUrl)
597 .then(() => {
598 const button = document.getElementById('copyMaterialLink');
599 const originalText = button.innerHTML;
600 button.innerHTML = '<i class="bi bi-check-lg me-2"></i>Copied!';
601 setTimeout(() => {
602 button.innerHTML = originalText;
603 }, 2000);
604 })
605 .catch(err => {
606 console.error('Failed to copy link: ', err);
607 alert('Failed to copy link to clipboard');
608 });
609}
JsPolyHavenAPILoader - A JavaScript class for interacting with the Poly Haven API Handles fetching ma...