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 = {};
9
10// DOM elements
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'));
18
19// Target URL for the viewer page
20let targetURL = "https://kwokcb.github.io/MaterialXLab/javascript/shader_utilities/dist/index.html?viewerOnly=1";
21// Set for local testing
22//targetURL = "http://localhost:8010/javascript/shader_utilities/dist/index.html?viewerOnly=1";
23
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';
27}
28
29// Initialize the application
30document.addEventListener('DOMContentLoaded', function () {
31 // Initialize the Poly Haven API
32 polyHavenAPI = new JsPolyHavenAPILoader();
33
34 // Set theme based on browser preference or default to light mode
35 const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)").matches;
36 if (prefersDarkScheme) {
37 setTheme('dark');
38 } else {
39 setTheme('light');
40 }
41
42 document.getElementById('themeToggleBtn').addEventListener('click', function () {
43 const isDark = document.documentElement.getAttribute('data-bs-theme') === 'dark';
44 setTheme(isDark ? 'light' : 'dark');
45 });
46
47 // https://icons.getbootstrap.com/icons/card-image/
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"/>
51 </svg>`;
52
53 // Convert to base64 data URL
54 // svgDataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`;''
55 svgDataUrl = 'https://icons.getbootstrap.com/assets/icons/card-image.svg'
56
57
58 loadMaterials();
59
60 // Event listeners
61 searchButton.addEventListener('click', filterMaterials);
62 searchInput.addEventListener('keyup', function (e) {
63 if (e.key === 'Enter') filterMaterials();
64 });
65 categoryFilter.addEventListener('change', filterMaterials);
66 resolutionFilter.addEventListener('change', filterMaterials);
67
68 // Modal buttons
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);
73
74 // Initialize CodeMirror when modal opens
75 const materialModalElement = document.getElementById('materialModal');
76 materialModalElement.addEventListener('shown.bs.modal', function () {
77 if (codeMirrorEditor) return;
78
79 let editor = document.getElementById('mtlxEditor')
80 if (editor)
81 codeMirrorEditor = CodeMirror.fromTextArea(editor, {
82 mode: 'xml',
83 theme: 'material',
84 lineNumbers: true,
85 readOnly: true,
86 lineWrapping: true
87 });
88 });
89
90 let viewer = document.getElementById('viewer');
91 viewer.src = targetURL;
92
93 materialModalElement.addEventListener('hidden.bs.modal', function () {
94 if (viewer) {
95 viewer.style.display = 'none';
96 }
97
98 // Clear the cache when the modal closes
99 for (const key in materialPackageCache) {
100 delete materialPackageCache[key];
101 }
102 });
103});
104
105async function downloadMaterial() {
106 if (!currentSelectedMaterial || !polyHavenAPI) return;
107
108 const resolution = document.getElementById('materialResolution').value;
109 const materialName = currentSelectedMaterial.name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
110
111 // Show loading state
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;
116
117 try {
118 const cacheKey = `${currentSelectedMaterial.id}_${resolution}`;
119 let zipBlob = materialPackageCache[cacheKey];
120 if (!zipBlob) {
121 zipBlob = await polyHavenAPI.createMaterialXPackage(currentSelectedMaterial, resolution);
122 materialPackageCache[cacheKey] = zipBlob;
123 }
124
125 // Create the MaterialX package using the API class
126 //const zipBlob = await polyHavenAPI.createMaterialXPackage(currentSelectedMaterial, resolution);
127
128 // Trigger download
129 const url = URL.createObjectURL(zipBlob);
130 const a = document.createElement('a');
131 a.href = url;
132 a.download = `${materialName}_${resolution}_materialx.zip`;
133 document.body.appendChild(a);
134 a.click();
135
136 // Clean up
137 setTimeout(() => {
138 document.body.removeChild(a);
139 URL.revokeObjectURL(url);
140 }, 100);
141
142 } catch (error) {
143 console.error('Error creating MaterialX package:', error);
144 alert(`Failed to prepare download: ${error.message}`);
145 } finally {
146 // Restore button state
147 downloadBtn.innerHTML = originalText;
148 downloadBtn.disabled = false;
149 }
150}
151
152function waitForViewerReady(viewer) {
153 return new Promise((resolve) => {
154 function handler(event) {
155 try {
156 const data = JSON.parse(event.data);
157 if (data.type === 'viewer-ready') {
158 window.removeEventListener('message', handler);
159 resolve();
160 }
161 } catch (e) {
162 // Ignore non-JSON messages
163 }
164 }
165 window.addEventListener('message', handler);
166 });
167}
168
169async function previewMaterial() {
170 if (!currentSelectedMaterial || !polyHavenAPI) return;
171
172 let viewer = document.getElementById('viewer');
173 if (!viewer) {
174 console.info('Viewer not found !');
175 }
176
177 let previewButton = document.getElementById('previewMaterial')
178 let previousText = previewButton.textContent;
179 previewButton.textContent = 'Loading...';
180
181 const resolution = document.getElementById('materialResolution').value;
182 try {
183 const cacheKey = `${currentSelectedMaterial.id}_${resolution}`;
184 let zipBlob = materialPackageCache[cacheKey];
185 if (!zipBlob) {
186 zipBlob = await polyHavenAPI.createMaterialXPackage(currentSelectedMaterial, resolution);
187 materialPackageCache[cacheKey] = zipBlob;
188 }
189
190 // Create the MaterialX package using the API class
191 //const zipBlob = await polyHavenAPI.createMaterialXPackage(currentSelectedMaterial, resolution);
192
193 // Convert Blob to ArrayBuffer
194 const arrayBuffer = await zipBlob.arrayBuffer();
195
196 // Post the ArrayBuffer to the target window (e.g., iframe or parent)
197 console.log('Posting preview data to viewer page...');
198 if (viewer && viewer.contentWindow) {
199 viewer.contentWindow.postMessage(arrayBuffer, targetURL);
200 }
201
202 // Wait for viewer-ready message.
203 // In case of failure, throw a timeout error.
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 => {
207 throw error;
208 });
209
210 viewer.style.display = 'block';
211
212 } catch (error) {
213 console.error('Error preparing preview:', error);
214 alert(`Failed to prepare preview: ${error.message}`);
215 }
216 previewButton.textContent = previousText;
217
218}
219
220
221// Load materials from Poly Haven API
222async function loadMaterials() {
223 if (!polyHavenAPI) {
224 console.error('PolyHaven API not initialized');
225 return;
226 }
227
228 mainSpinner.style.display = 'block';
229 materialsContainer.innerHTML = '';
230
231 try {
232 // Fetch materials using the API class
233 const result = await polyHavenAPI.fetchMaterials();
234
235 allMaterials = result.materials;
236 categories = result.categories;
237
238 // Populate category filter
239 populateCategoryFilter();
240
241 // Display all materials initially
242 filterMaterials();
243 } catch (error) {
244 console.error('Error loading materials:', error);
245 materialsContainer.innerHTML = `
246 <div class="col-12">
247 <div class="alert alert-danger" role="alert">
248 Failed to load materials from Poly Haven. Please try again later.
249 </div>
250 </div>
251 `;
252 } finally {
253 mainSpinner.style.display = 'none';
254 }
255}
256
257// Populate the category filter dropdown
258function populateCategoryFilter() {
259 categoryFilter.innerHTML = '<option value="">All Categories</option>';
260
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);
267 });
268}
269
270// Filter materials based on search and category filters
271function filterMaterials() {
272 const searchTerm = searchInput.value.toLowerCase();
273 const selectedCategory = categoryFilter.value;
274
275 const filtered = allMaterials.filter(material => {
276 // Filter by search term
277 const matchesSearch = material.name.toLowerCase().includes(searchTerm) ||
278 material.description.toLowerCase().includes(searchTerm) ||
279 material.tags.some(tag => tag.toLowerCase().includes(searchTerm));
280
281 // Filter by category
282 const matchesCategory = !selectedCategory || material.categories.includes(selectedCategory);
283
284 return matchesSearch && matchesCategory;
285 });
286
287 displayMaterials(filtered);
288}
289
290// Display materials in the grid
291function displayMaterials(materials) {
292 materialsContainer.innerHTML = '';
293
294 if (materials.length === 0) {
295 materialsContainer.innerHTML = `
296 <div class="col-12">
297 <div class="alert alert-info" role="alert">
298 No materials found matching your criteria.
299 </div>
300 </div>
301 `;
302 return;
303 }
304
305 materials.forEach(material => {
306 const col = document.createElement('div');
307 col.className = 'col-md-3 col-lg-2 mb-4';
308
309 col.innerHTML = `
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('')}
316 </div>
317 </div>
318 </div>
319 `;
320
321 col.querySelector('.card').addEventListener('click', () => showMaterialDetails(material));
322 materialsContainer.appendChild(col);
323 });
324}
325
326// New function to load material content
327async function loadMaterialContent(materialId) {
328 if (!polyHavenAPI) {
329 console.error('PolyHaven API not initialized');
330 return;
331 }
332
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;
337
338 try {
339 const resolution = document.getElementById('materialResolution').value;
340
341 // Get material content using the API class
342 const contentData = await polyHavenAPI.getMaterialContent(materialId, resolution);
343
344 // Create preview content
345 const previewContainer = document.getElementById('contentPreview');
346 previewContainer.innerHTML = `
347 <div class="mt-4">
348 <b>MaterialX Document</b>
349 <textarea id="mtlxEditor">${contentData.mtlxContent}</textarea>
350 <div class="mt-2">
351 <b>Textures</b>
352 <div id="textureGallery" class="row g-2"></div>
353 </div>
354 </div>
355 `;
356
357 // Initialize CodeMirror
358 if (codeMirrorEditor) {
359 codeMirrorEditor.toTextArea();
360 }
361 let editor = document.getElementById('mtlxEditor');
362 if (editor) {
363 codeMirrorEditor = CodeMirror.fromTextArea(editor, {
364 mode: 'xml',
365 theme: 'material',
366 lineNumbers: true,
367 readOnly: true,
368 lineWrapping: true
369 });
370 }
371
372 // Load texture gallery
373 const textureFiles = contentData.textureFiles;
374 const galleryContainer = document.getElementById('textureGallery');
375 galleryContainer.innerHTML = '';
376
377 const createTextureCard = (textureName, textureUrl) => {
378 const card = document.createElement('div');
379 card.className = 'col-6 col-md-4 col-lg-3 mb-3';
380
381 card.innerHTML = `
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"
386 alt="${textureName}"
387 loading="lazy"
388 onerror="this.onerror=null;this.src='${svgDataUrl}';">
389 </div>
390 <div class="card-body p-2">
391 <small class="text-truncate d-block">${textureName}</small>
392 </div>
393 </div>
394 `;
395 return card;
396 };
397
398 // Create gallery items
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);
403 }
404
405 } catch (error) {
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}
410 </div>
411 `;
412 } finally {
413 loadBtn.innerHTML = originalText;
414 loadBtn.disabled = false;
415 }
416}
417
418// Modified showMaterialDetails function
419async function showMaterialDetails(material) {
420 currentSelectedMaterial = material;
421
422 if (codeMirrorEditor)
423 {
424 codeMirrorEditor.setValue('');
425 }
426
427 // Set basic info
428 document.getElementById('materialModalLabel').textContent = material.name;
429 //document.getElementById('materialTitle').textContent = material.name;
430 document.getElementById('materialDescription').textContent = material.description;
431 document.getElementById('materialPreview').src = material.thumb_url;
432
433 // Set tags
434 const tagsContainer = document.getElementById('materialTags');
435 tagsList = material.tags.map(tag =>
436 `<span class="badge bg-secondary">${tag}</span>`
437 ).join(' ');
438 tagsContainer.innerHTML = '<span class="badge bg-dark">Tags</span> ' + tagsList
439
440 // Set categories
441 const categoriesContainer = document.getElementById('materialCategories');
442 categoriesList = material.categories.map(cat =>
443 `<span class="badge bg-secondary">${cat}</span>`
444 ).join(' ');
445 categoriesContainer.innerHTML = '<span class="badge bg-dark">Categories</span> ' + categoriesList
446
447
448 // Reset content preview section
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
453 </button>
454 </div>
455 `;
456
457 // Set up content loader button
458 document.getElementById('loadContentBtn').addEventListener('click', async () => {
459 await loadMaterialContent(material.id);
460 });
461
462 // Force 1K for downloads
463 document.getElementById('materialResolution').value = '1k';
464
465 // Show modal
466 materialModal.show();
467}
468
469// Update the maps display based on selected resolution
470function updateMapsDisplay() {
471 if (!currentSelectedMaterial) return;
472
473 const resolution = document.getElementById('materialResolution').value;
474 const mapsContainer = document.getElementById('materialMaps');
475 mapsContainer.innerHTML = currentSelectedMaterial.maps;
476
477 /* for (const [mapType, available] of Object.entries(currentSelectedMaterial.maps)) {
478 if (available) {
479 const badge = document.createElement('span');
480 badge.className = 'badge bg-info text-dark map-badge';
481 badge.textContent = `${mapType} (${resolution})`;
482 mapsContainer.appendChild(badge);
483 }
484 } */
485}
486
487// Copy material link handler
488function copyMaterialLink() {
489 if (!currentSelectedMaterial) return;
490
491 const materialUrl = `https://polyhaven.com/a/${currentSelectedMaterial.id}`;
492 navigator.clipboard.writeText(materialUrl)
493 .then(() => {
494 const button = document.getElementById('copyMaterialLink');
495 const originalText = button.innerHTML;
496 button.innerHTML = '<i class="bi bi-check-lg me-2"></i>Copied!';
497 setTimeout(() => {
498 button.innerHTML = originalText;
499 }, 2000);
500 })
501 .catch(err => {
502 console.error('Failed to copy link: ', err);
503 alert('Failed to copy link to clipboard');
504 });
505}
JsPolyHavenAPILoader - A JavaScript class for interacting with the Poly Haven API Handles fetching ma...