NovaStar MX2000 Pro
NovaStar MX2000 Pro
Its remarkable features include 12-bit color depth, 480 Hz capability, real-time multi-screen scaling, 0-frame latency, and HDR supportability, providing precise brightness control, true-to-life color fidelity, and an excellent image quality. Its card-based modular design is specifically tailored for future LED displays, allowing for flexible input and output card configurations that are stable and easy to maintain. With a compact 2U size, it supports up to 8x 4K@60Hz or 4x 8K@30Hz video inputs, with a maximum load capacity of 35.38 million pixels, making it ideal for large-screen configurations. Input options: 2x input card slots supporting 4K cards (4x HDMI 2.0, 4x DP1.2, 4x 12G-SDI), 8K cards (1x DP 1.4 8K@60Hz, 2x HDMI 2.1, 2x DP 1.4, 1x DP 1.4 + 1x HDMI 2.1), or VoIP cards (1x ST 2110 100G, 1x ST 2110 25G, 2x ST 2110 25G). Fiber output options: up to 2x 4x10G fiber output cards or up to 2x 1x40G fiber output cards.
ð¡ïž 3-Year NovaStar Warranty
ð Pricing Notice: Pricing shown above is MAP pricing. Please
contact us if you're a reseller.
Key Highlights
ð
Up to 35.38 million pixels
ð
Modular fiber output cards (10G or 40G)
Key Features
âïž
Full-Grayscale Calibration
âïž
Grayscale Calibration
âïž Modular Configuration
The MX2000 Pro features a modular design - configure it with your choice of input and output cards
ð¥ Input Slots:
2
ð€ Output Slots:
2
ð¬ Layers per Output:
16
ð ïž Configure Your System
Available Input Cards (12)
MX_1 2110(25G) Input Card
$3,525
MX_1xDP1.4+1xHDMI2.1 (8K@30Hz) Input Card
$3,688
MX_1xST2110 (100G) Input Card
$8,250
MX_1xST2110 (25G) Input Card
$3,525
MX_1ÃDP 1.4 (8K@60Hz) Input Card
$4,188
MX_2 2110(25G) Input Card
$5,298
MX_2xDP1.4 (8K@30Hz) Input Card
$3,688
MX_2xHDMI2.1 (8K@30Hz) Input Card
$3,688
MX_2xST2110 (25G) Input Card
$5,298
MX_4 12G SDI Input Card
$3,442
MX_4 DP1.2 input Card
$3,442
MX_4 HDMI2.0 Input Card
$3,195
Available Output Cards (4)
MX_1x40G Fiber Output Card-M 5G
$4,664
MX_1x40G Fiber Output Card-S 5G
$4,664
MX_4 10G Fiber Output Card-M
$4,664
MX_4 10G Fiber Output Card-S
$4,664
Technical Specifications
Video Processing Layers 4 layers Maximum Pixels Up to 35.38 million pixels
Physical Specifications Dimensions 482.6 mm à 94.6 mm à 529.7 mm (19.0" à 3.7" à 20.9") IP Rating IP20 Packing box 660.0 mm à 570.0 mm à 210.0 mm (26.0" à 22.4" à 8.3")
Features HDR 3D Genlock 4K 8K 10G Fiber Low Latency Backup 12-bit Shutter Fit Full-Grayscale Calibration Grayscale Calibration
Why Choose the MX2000 Pro?
â All-in-One Design
Integrated solution saves rack space and simplifies setup
â Professional Features
Advanced processing with low latency and pixel-level calibration
â Maximum Reliability
Backed by 3-year NovaStar warranty plus Olympian LED support
â Proven Performance
Trusted by production companies and rental houses nationwide
Frequently Asked Questions
Q: What warranty is included?
All NovaStar products include a full 3-year manufacturer warranty when purchased from authorized distributors like Olympian LED.
Q: Do you offer volume pricing?
Yes! As the largest NovaStar distributor in the USA, we offer special pricing for bulk orders and repeat customers. Contact our sales team for a custom quote.
Q: Do you have this in stock?
We maintain over $2 million in inventory at our Titusville, Florida warehouse. Most orders ship same-day. Contact us to confirm current availability.
Q: Can I get NET 30 payment terms?
NET 30 terms are available for qualified businesses and established customers. Please contact our sales team to discuss payment options.
Frequently Purchased Together
Customers who bought this product also purchased:
Fiber Converters
CVT10PRO-S
It offers a way to convert optical signals to electrical signals, or electrical signals to optical s...
Fiber Converters
CVT10PRO-M
It offers a way to convert optical signals to electrical signals, or electrical signals to optical s...
Accessories
MFN300-B
External multifunction box with expanded monitoring and sensor connectivity for NovaStar systems....
Receiving Cards
A10s Pro
For 8bit and 10bit video sources and PWM driver ICs, a single A10s Pro supports resolutions up to 51...
Why Buy from Olympian LED?
ð
Largest US Distributor
Best pricing guaranteed as the largest NovaStar distributor in America
ðŠ
Ships Same Day
Over $2M in stock at our Florida warehouse for immediate delivery
ð³
Flexible Terms
NET 30 payment terms and volume discounts for qualified businesses
ð ïž
Expert Support
Technical support from our team of LED display specialists
â
Input Slots (2)
Output Slots (2)
Configuration Summary
Total Price:
$4,417
ð Add Configuration to Cart
ð Share Configuration Link
ð Export Configuration Report
ðïž Clear Configuration
// HDMI connector (icon_030.png)
hdmi: ' ',
// DisplayPort (icon_074.png)
dp: ' ',
// DVI (icon_014.png)
dvi: ' ',
// BNC/SDI (icon_069.png)
sdi: ' ',
// RJ45 Ethernet (actual connector)
rj45: ' ',
// Fiber optic (icon_021.png)
fiber: ' ',
// HDBaseT (uses RJ45 connector)
hdbaset: ' ',
// IP/Network (actual connector icon)
ip: ' ',
// Audio (1/4" or XLR-style circular - icon_109.png)
audio: ' ',
// NDI (uses ethernet/RJ45 since it's network-based)
ndi: ' ',
// ST2110 (icon_091.png)
st2110: ' ',
// Standard I/O (generic connector)
io: ' ',
// Preview/Monitor output
preview: ' '
};
function renderAvailableCards() {
const inputDiv = document.getElementById('availableInputCards');
const outputDiv = document.getElementById('availableOutputCards');
const accessDiv = document.getElementById('availableAccessories');
try {
inputDiv.innerHTML = hSeriesConfig.inputCards.map((card, i) => {
const slug = card.name.toLowerCase().replace(/ /g, '-').replace(/[_+()\u00d7]/g, c => {
if (c === '_') return '-';
if (c === '+') return 'plus';
if (c === '\u00d7') return 'x';
return '';
}).replace(/,/g, '');
return `
${card.name.replace('H_', '')}
${card.port_badges_html || ''}
`;
}).join('');
outputDiv.innerHTML = hSeriesConfig.outputCards.map((card, i) => {
const slug = card.name.toLowerCase().replace(/ /g, '-').replace(/[_+()\u00d7]/g, c => {
if (c === '_') return '-';
if (c === '+') return 'plus';
if (c === '\u00d7') return 'x';
return '';
}).replace(/,/g, '');
return `
${card.name.replace('H_', '')}
${card.port_badges_html || ''}
`;
}).join('');
if (accessDiv && hSeriesConfig.accessories.length > 0) {
accessDiv.innerHTML = hSeriesConfig.accessories.map((acc, i) => {
const slug = acc.name.toLowerCase().replace(/ /g, '-').replace(/[_+()\u00d7]/g, c => {
if (c === '_') return '-';
if (c === '+') return 'plus';
if (c === '\u00d7') return 'x';
return '';
}).replace(/,/g, '');
return `
${acc.name}
${generatePortBadges(acc.name)}
`;
}).join('');
}
} catch (error) {
console.error('â Error in renderAvailableCards:', error);
console.error('Stack:', error.stack);
}
}
function renderSlots() {
try {
// Check if we have a grid layout (like H2)
if (hSeriesConfig.layoutGrid) {
renderGridLayout();
} else {
renderListLayout();
}
} catch (error) {
console.error('â Error in renderSlots:', error);
console.error('Stack:', error.stack);
}
}
function renderGridLayout() {
try {
const inputDiv = document.getElementById('inputSlots');
const outputDiv = document.getElementById('outputSlots');
// Hide the output div and its heading (but not the parent panel)
outputDiv.style.display = 'none';
const outputHeading = outputDiv.previousElementSibling;
if (outputHeading && outputHeading.tagName === 'H3') {
outputHeading.style.display = 'none';
}
// Change the input heading
const inputHeading = inputDiv.previousElementSibling;
if (inputHeading && inputHeading.tagName === 'H3') {
inputHeading.textContent = 'Slot Configuration';
}
// Change inputDiv display from grid to block to allow custom layout
inputDiv.style.display = 'block';
// Check if it's a column-based layout (H5+) or row-based layout (H2)
const isColumnLayout = hSeriesConfig.layoutGrid.some(cell => cell.column);
if (isColumnLayout) {
// Separate into columns
const leftColumn = hSeriesConfig.layoutGrid.filter(cell => cell.column === 'left');
const rightColumn = hSeriesConfig.layoutGrid.filter(cell => cell.column === 'right');
const htmlContent = `
${leftColumn.map(cell => renderGridCell(cell)).join('')}
${rightColumn.map(cell => renderGridCell(cell)).join('')}
`;
inputDiv.innerHTML = htmlContent;
} else {
// Row-based layout (H2)
inputDiv.innerHTML = `
${hSeriesConfig.layoutGrid.map((row, rowIdx) => `
${row.map((cell, colIdx) => renderGridCell(cell, rowIdx, colIdx)).join('')}
`).join('')}
`;
}
} catch (error) {
console.error('â Error in renderGridLayout:', error);
console.error('Stack:', error.stack);
}
}
function renderGridCell(cell, rowIdx, colIdx) {
// Check if this slot is blocked by a double-slot card below (H2 only)
if (hSeriesConfig.frameName === 'H2' && rowIdx !== undefined && colIdx !== undefined) {
const rowBelow = hSeriesConfig.layoutGrid[rowIdx + 1];
if (rowBelow && rowBelow[colIdx]) {
const cellBelow = rowBelow[colIdx];
if (cellBelow.type === 'output' && cellBelow.slot !== undefined) {
const cardBelow = selectedCards.output[cellBelow.slot];
if (cardBelow && cardBelow.slots_required === 2) {
// This slot is blocked by a double-slot card below
return `${cell.label}
â¬ïž Occupied by ${cardBelow.name.replace('H_', '').substring(0, 15)}...
`;
}
}
}
}
if (cell.type === 'fixed') {
return ``;
}
if (cell.type === 'blank') {
return ``;
}
// Optional slots (like MVR) are now treated like regular input slots but with special styling
if (cell.slot_type === 'optional') {
const card = selectedCards[cell.type][cell.slot];
const slotClass = card ? 'h-slot-filled' : 'h-slot-empty h-slot-optional';
const isPending = pendingCardSelection && pendingCardSelection.type === cell.type && pendingCardSelection.slot === cell.slot;
const pendingClass = isPending ? 'h-slot-pending' : '';
return `
${cell.label}
${card ? `
${card.port_badges_html || ''}
à ` : `
${isPending ? 'Slot selected â' : 'Optional MVR'}
`}
`;
}
const card = selectedCards[cell.type][cell.slot];
const slotClass = card ? 'h-slot-filled' : 'h-slot-empty';
const isPending = pendingCardSelection && pendingCardSelection.type === cell.type && pendingCardSelection.slot === cell.slot;
const pendingClass = isPending ? 'h-slot-pending' : '';
// Check if this is a double-slot card (for visual badge)
const doubleSlotClass = (card && card.slots_required === 2) ? 'h-slot-double' : '';
const doubleSlotBadge = (card && card.slots_required === 2) ? '2 SLOTS ' : '';
return `
${cell.label}
${card ? `
${card.port_badges_html || ''}
${doubleSlotBadge}
à ` : `
${isPending ? 'Slot selected â' : 'Click to select'}
`}
`;
}
function getSlotLabel(type, slot) {
if (hSeriesConfig.layoutGrid) {
const cell = hSeriesConfig.layoutGrid.find(c => c.type === type && c.slot === slot);
return cell ? cell.label : `${type.toUpperCase()}-${slot + 1}`;
}
if (hSeriesConfig.slotLabels) {
return hSeriesConfig.slotLabels[type][slot];
}
return `${type.toUpperCase()}-${slot + 1}`;
}
function renderListLayout() {
const inputDiv = document.getElementById('inputSlots');
const outputDiv = document.getElementById('outputSlots');
const inputLabels = hSeriesConfig.slotLabels ? hSeriesConfig.slotLabels.input : Array(hSeriesConfig.inputSlots).fill(0).map((_, i) => `I-${i+1}`);
const outputLabels = hSeriesConfig.slotLabels ? hSeriesConfig.slotLabels.output : Array(hSeriesConfig.outputSlots).fill(0).map((_, i) => `O-${i+1}`);
inputDiv.innerHTML = inputLabels.map((label, i) => {
const card = selectedCards.input[i];
const isPending = pendingCardSelection && pendingCardSelection.type === 'input' && pendingCardSelection.slot === i;
const pendingClass = isPending ? 'h-slot-pending' : '';
return `
${label}
${card ? `
${card.port_badges_html || ''}
à ` : `
${isPending ? 'Slot selected â' : 'Click to select'}
`}
`;
}).join('');
outputDiv.innerHTML = outputLabels.map((label, i) => {
const card = selectedCards.output[i];
const isPending = pendingCardSelection && pendingCardSelection.type === 'output' && pendingCardSelection.slot === i;
const pendingClass = isPending ? 'h-slot-pending' : '';
return `
${label}
${card ? `
${card.port_badges_html || ''}
à ` : `
${isPending ? 'Slot selected â' : 'Click to select'}
`}
`;
}).join('');
}
function promptCardSelection(type, slot) {
pendingCardSelection = { type, slot };
// Highlight the selected slot
renderSlots();
// Highlight and scroll to the appropriate card selection panel
const inputPanel = document.getElementById('inputCardsPanel');
const outputPanel = document.getElementById('outputCardsPanel');
// Remove previous highlights
if (inputPanel) inputPanel.classList.remove('h-cards-panel-highlight');
if (outputPanel) outputPanel.classList.remove('h-cards-panel-highlight');
// Add highlight to the relevant panel
const targetPanel = type === 'input' ? inputPanel : outputPanel;
if (targetPanel) {
targetPanel.classList.add('h-cards-panel-highlight');
targetPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// Show a message
const slotLabel = getSlotLabel(type, slot);
}
let pendingCardSelection = null;
function clearCardPanelHighlights() {
const inputPanel = document.getElementById('inputCardsPanel');
const outputPanel = document.getElementById('outputCardsPanel');
if (inputPanel) inputPanel.classList.remove('h-cards-panel-highlight');
if (outputPanel) outputPanel.classList.remove('h-cards-panel-highlight');
}
function addCardToSlot(type, cardIndex) {
const card = type === 'input' ? hSeriesConfig.inputCards[cardIndex] : hSeriesConfig.outputCards[cardIndex];
// If we have a pending slot selection, use that
if (pendingCardSelection && pendingCardSelection.type === type) {
selectedCards[type][pendingCardSelection.slot] = card;
pendingCardSelection = null;
clearCardPanelHighlights();
renderSlots();
updateSummary();
saveConfiguration();
return;
}
// Special handling for MVR cards - find the MVR slot
if (card.name.includes('MVR') || card.name.includes('Preview')) {
let mvrSlot = -1;
// Check layoutGrid first
if (hSeriesConfig.layoutGrid) {
const mvrCell = hSeriesConfig.layoutGrid.find(cell =>
cell.type === type && cell.label && cell.label.includes('MVR')
);
if (mvrCell) {
mvrSlot = mvrCell.slot;
}
}
// Fallback to slotLabels
if (mvrSlot === -1 && hSeriesConfig.slotLabels) {
const labels = type === 'input' ? hSeriesConfig.slotLabels.input : hSeriesConfig.slotLabels.output;
mvrSlot = labels.findIndex(label => label && label.includes('MVR'));
}
// If found MVR slot, use it (even if occupied - will alert user)
if (mvrSlot !== -1) {
if (selectedCards[type][mvrSlot]) {
if (!confirm(`MVR slot is occupied by ${selectedCards[type][mvrSlot].name}. Replace it?`)) {
return;
}
}
selectedCards[type][mvrSlot] = card;
clearCardPanelHighlights();
renderSlots();
updateSummary();
saveConfiguration();
return;
}
}
// Otherwise find first empty slot
const emptySlot = selectedCards[type].findIndex(s => !s);
if (emptySlot === -1) {
alert(`All ${type} slots are full! Click on a specific empty slot first to replace a card.`);
return;
}
selectedCards[type][emptySlot] = card;
clearCardPanelHighlights();
renderSlots();
updateSummary();
saveConfiguration();
}
function removeCard(type, slotIndex) {
selectedCards[type][slotIndex] = null;
renderSlots();
updateSummary();
saveConfiguration();
}
let draggedCard = null;
function handleDragStart(event, type, slot) {
draggedCard = {
type: type,
slot: slot,
card: selectedCards[type][slot]
};
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/html', event.target.innerHTML);
event.target.style.opacity = '0.5';
// Highlight only valid drop zones (same type)
document.querySelectorAll('.h-slot').forEach(slotEl => {
const slotType = slotEl.getAttribute('data-type');
const isFixed = slotEl.classList.contains('h-slot-fixed');
const isBlank = slotEl.classList.contains('h-slot-blank');
// Only highlight slots of the same type that aren't fixed or blank
if (slotType === type && !isFixed && !isBlank) {
slotEl.classList.add('h-slot-drop-zone');
}
});
}
function handleDragOver(event) {
if (event.preventDefault) {
event.preventDefault();
}
event.dataTransfer.dropEffect = 'move';
return false;
}
function handleDrop(event, targetType, targetSlot) {
if (event.stopPropagation) {
event.stopPropagation();
}
event.preventDefault();
if (!draggedCard) {
return false;
}
// Prevent dropping input cards in output slots and vice versa
if (draggedCard.type !== targetType) {
alert(`Cannot place an ${draggedCard.type} card into an ${targetType} slot!`);
// Remove drop zone highlights
document.querySelectorAll('.h-slot-drop-zone').forEach(slot => {
slot.classList.remove('h-slot-drop-zone');
});
draggedCard = null;
return false;
}
// Swap or move cards
const sourceCard = draggedCard.card;
const targetCard = selectedCards[targetType][targetSlot];
// Move source card to target
selectedCards[targetType][targetSlot] = sourceCard;
// Clear source slot (or put target card there if swapping)
selectedCards[draggedCard.type][draggedCard.slot] = targetCard;
// Remove drop zone highlights
document.querySelectorAll('.h-slot-drop-zone').forEach(slot => {
slot.classList.remove('h-slot-drop-zone');
});
draggedCard = null;
renderSlots();
updateSummary();
saveConfiguration();
return false;
}
function toggleAccessory(accIndex) {
const acc = hSeriesConfig.accessories[accIndex];
const idx = selectedCards.accessories.findIndex(a => a && a.name === acc.name);
if (idx > -1) {
selectedCards.accessories.splice(idx, 1);
} else {
selectedCards.accessories.push(acc);
}
updateSummary();
saveConfiguration();
}
function updateSummary() {
let total = hSeriesConfig.framePrice;
let inputSubtotal = 0;
let outputSubtotal = 0;
let accessoriesSubtotal = 0;
let summary = `Frame: ${hSeriesConfig.frameName} - $${hSeriesConfig.framePrice.toLocaleString()}
`;
// Input cards
const inputCards = selectedCards.input.filter(c => c);
if (inputCards.length > 0) {
summary += `Input Cards (${inputCards.length}):
`;
inputCards.forEach(card => {
inputSubtotal += card.price;
summary += `${card.name} - $${card.price.toLocaleString()}
`;
});
summary += `Subtotal: $${inputSubtotal.toLocaleString()}
`;
}
// Output cards
const outputCards = selectedCards.output.filter(c => c);
if (outputCards.length > 0) {
summary += `Output Cards (${outputCards.length}):
`;
outputCards.forEach(card => {
outputSubtotal += card.price;
summary += `${card.name} - $${card.price.toLocaleString()}
`;
});
summary += `Subtotal: $${outputSubtotal.toLocaleString()}
`;
}
// Accessories
if (selectedCards.accessories.length > 0) {
summary += `Accessories (${selectedCards.accessories.length}):
`;
selectedCards.accessories.forEach(acc => {
accessoriesSubtotal += acc.price;
summary += `${acc.name} - $${acc.price.toLocaleString()}
`;
});
summary += `Subtotal: $${accessoriesSubtotal.toLocaleString()}
`;
}
// Capacity info
const inputSlotsUsed = selectedCards.input.filter(c => c).length;
const outputSlotsUsed = selectedCards.output.filter(c => c).length;
const totalLayers = outputSlotsUsed * (hSeriesConfig.layersPerOutput || 16);
// Calculate total pixel capacity from output cards
let totalPixelCapacity = 0;
outputCards.forEach(card => {
// Parse RJ45 (Ethernet) ports: 650,000 pixels per port
const rj45Match = card.name.match(/(\d+)x\s*RJ45/i);
if (rj45Match) {
totalPixelCapacity += parseInt(rj45Match[1]) * 650000;
}
// Parse Fiber ports: 5,200,000 pixels per 10G fiber port
const fiberMatch = card.name.match(/(\d+)x\s*Fiber/i);
if (fiberMatch) {
totalPixelCapacity += parseInt(fiberMatch[1]) * 5200000;
}
// Parse HDMI outputs (assume 4K max: 8.3M pixels)
const hdmiMatch = card.name.match(/(\d+)x\s*HDMI/i) || card.name.match(/HDMI.*Output.*Card/i);
if (hdmiMatch) {
const hdmiCount = hdmiMatch[1] ? parseInt(hdmiMatch[1]) : 1;
const hdmiPixels = hdmiCount * 8300000;
totalPixelCapacity += hdmiPixels;
}
// Parse HDBaseT outputs (assume 1080p: 2.1M pixels)
const hdbasetMatch = card.name.match(/(\d+)x?\s*HDBaseT/i);
if (hdbasetMatch) {
const hdbasetCount = parseInt(hdbasetMatch[1]);
const hdbasetPixels = hdbasetCount * 2100000;
totalPixelCapacity += hdbasetPixels;
}
});
summary += `Capacity:
`;
summary += `Input Slots: ${inputSlotsUsed} / ${hSeriesConfig.inputSlots}
`;
summary += `Output Slots: ${outputSlotsUsed} / ${hSeriesConfig.outputSlots}
`;
if (outputSlotsUsed > 0) {
summary += `Total Layers: ${totalLayers}
`;
}
if (totalPixelCapacity > 0) {
summary += `Pixel Capacity: ${(totalPixelCapacity / 1000000).toFixed(1)}M pixels
`;
}
total = hSeriesConfig.framePrice + inputSubtotal + outputSubtotal + accessoriesSubtotal;
document.getElementById('configSummary').innerHTML = summary;
document.getElementById('totalPrice').textContent = `$${total.toLocaleString()}`;
}
function addConfigurationToCart() {
const totalPrice = parseInt(document.getElementById('totalPrice').textContent.replace(/[$,]/g, ''));
if (totalPrice === hSeriesConfig.framePrice) {
alert('Please add at least one card to your configuration before adding to cart.');
return;
}
const config = {
name: `${hSeriesConfig.frameName} - Custom Configuration`,
price: totalPrice,
quantity: 1,
configuration: {
frame: hSeriesConfig.frameName,
inputCards: selectedCards.input.filter(c => c),
outputCards: selectedCards.output.filter(c => c),
accessories: selectedCards.accessories
}
};
// Use the global addToCart function
addToCart(config.name, config.price, 1, JSON.stringify(config.configuration));
alert('â
Configuration added to cart!');
closeHSeriesCustomizer();
}
function exportConfiguration() {
const totalPrice = parseInt(document.getElementById('totalPrice').textContent.replace(/[$,]/g, ''));
if (totalPrice === hSeriesConfig.framePrice) {
alert('Please configure at least one card before exporting.');
return;
}
if (!window.jspdf || !window.jspdf.jsPDF) {
alert('PDF library not loaded. Please try again.');
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
// Calculate totals and capacity
const inputSlotsUsed = selectedCards.input.filter(c => c).length;
const outputSlotsUsed = selectedCards.output.filter(c => c).length;
const totalLayers = outputSlotsUsed * (hSeriesConfig.layersPerOutput || 16);
let inputSubtotal = 0;
let outputSubtotal = 0;
let accessoriesSubtotal = 0;
// Calculate pixel capacity
let totalPixelCapacity = 0;
const outputCards = selectedCards.output.filter(c => c);
outputCards.forEach(card => {
// Check if card has max loading capacity in description
if (card.description && card.description.includes('10,400,000')) {
totalPixelCapacity += 10400000;
} else if (card.description && card.description.includes('13,000,000')) {
totalPixelCapacity += 13000000;
} else if (card.description && card.description.includes('20,800,000')) {
totalPixelCapacity += 20800000;
} else {
// Fallback to port-based calculation
const rj45Match = card.name.match(/(\d+)x\s*RJ45/i);
if (rj45Match) {
totalPixelCapacity += parseInt(rj45Match[1]) * 650000;
}
const fiberMatch = card.name.match(/(\d+)x\s*Fiber/i);
if (fiberMatch) {
totalPixelCapacity += parseInt(fiberMatch[1]) * 5200000;
}
}
});
// Load and add logo
const logo = new Image();
logo.onload = function() {
let yPos = 15;
// Add logo
try {
doc.addImage(logo, 'PNG', 15, yPos, 40, 10);
} catch (e) {
// Fallback to text if logo fails
doc.setFontSize(16);
doc.setFont(undefined, 'bold');
doc.text('Olympian LED', 15, yPos + 5);
}
// Header info
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.text(`Generated: ${new Date().toLocaleString()}`, 200, yPos + 5, { align: 'right' });
yPos += 20;
// Title
doc.setFontSize(18);
doc.setFont(undefined, 'bold');
doc.setTextColor(220, 53, 69);
doc.text('H SERIES CONFIGURATION REPORT', 15, yPos);
doc.setTextColor(0, 0, 0);
yPos += 15;
// Frame Configuration Box
doc.setFillColor(240, 240, 240);
doc.rect(15, yPos - 5, 180, 45, 'F');
doc.setFontSize(12);
doc.setFont(undefined, 'bold');
doc.text('Frame Configuration', 20, yPos);
yPos += 8;
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.text(`Model: ${hSeriesConfig.frameName}`, 20, yPos);
yPos += 6;
doc.text(`Rack Size: ${hSeriesConfig.frameName.replace('H', '') + 'U'}`, 20, yPos);
yPos += 6;
doc.text(`Input Slots: ${inputSlotsUsed} / ${hSeriesConfig.inputSlots} used`, 20, yPos);
yPos += 6;
doc.text(`Output Slots: ${outputSlotsUsed} / ${hSeriesConfig.outputSlots} used`, 20, yPos);
yPos += 6;
if (outputSlotsUsed > 0) {
doc.text(`Total Layers: ${totalLayers}`, 20, yPos);
yPos += 6;
}
if (totalPixelCapacity > 0) {
doc.setFont(undefined, 'bold');
doc.setTextColor(220, 53, 69);
doc.text(`Pixel Capacity: ${(totalPixelCapacity / 1000000).toFixed(1)}M pixels`, 20, yPos);
doc.setTextColor(0, 0, 0);
doc.setFont(undefined, 'normal');
}
yPos += 12;
// Input Cards Section
if (selectedCards.input.filter(c => c).length > 0) {
doc.setFontSize(12);
doc.setFont(undefined, 'bold');
doc.text(`Input Cards (${selectedCards.input.filter(c => c).length})`, 15, yPos);
yPos += 2;
const inputData = [];
selectedCards.input.filter(c => c).forEach(card => {
inputSubtotal += card.price;
inputData.push([
card.name.replace('H_', ''),
card.description.substring(0, 50) + (card.description.length > 50 ? '...' : ''),
`$${card.price.toLocaleString()}`
]);
});
doc.autoTable({
startY: yPos,
head: [['Card Name', 'Description', 'Price']],
body: inputData,
theme: 'grid',
headStyles: { fillColor: [220, 53, 69], fontSize: 9 },
bodyStyles: { fontSize: 9 },
margin: { left: 15, right: 15 },
columnStyles: {
0: { cellWidth: 60 },
1: { cellWidth: 90 },
2: { cellWidth: 30, halign: 'right' }
}
});
yPos = doc.lastAutoTable.finalY + 3;
doc.setFontSize(10);
doc.setFont(undefined, 'bold');
doc.text(`Subtotal: $${inputSubtotal.toLocaleString()}`, 195, yPos, { align: 'right' });
yPos += 8;
}
// Output Cards Section
if (selectedCards.output.filter(c => c).length > 0) {
doc.setFontSize(12);
doc.setFont(undefined, 'bold');
doc.text(`Output Cards (${selectedCards.output.filter(c => c).length})`, 15, yPos);
yPos += 2;
const outputData = [];
selectedCards.output.filter(c => c).forEach(card => {
outputSubtotal += card.price;
let desc = card.description.substring(0, 40);
// Add pixel capacity to description if it's a sending card
if (card.description.includes('Gigabit Ethernet') || card.description.includes('Fiber')) {
const cap = card.description.match(/([\d,]+)\s*pixels/);
if (cap) desc += ` (Max: ${cap[1]})`;
}
outputData.push([
card.name.replace('H_', ''),
desc + (card.description.length > 40 ? '...' : ''),
`$${card.price.toLocaleString()}`
]);
});
doc.autoTable({
startY: yPos,
head: [['Card Name', 'Description', 'Price']],
body: outputData,
theme: 'grid',
headStyles: { fillColor: [220, 53, 69], fontSize: 9 },
bodyStyles: { fontSize: 9 },
margin: { left: 15, right: 15 },
columnStyles: {
0: { cellWidth: 60 },
1: { cellWidth: 90 },
2: { cellWidth: 30, halign: 'right' }
}
});
yPos = doc.lastAutoTable.finalY + 3;
doc.setFontSize(10);
doc.setFont(undefined, 'bold');
doc.text(`Subtotal: $${outputSubtotal.toLocaleString()}`, 195, yPos, { align: 'right' });
yPos += 8;
}
// Accessories Section
if (selectedCards.accessories.length > 0) {
doc.setFontSize(12);
doc.setFont(undefined, 'bold');
doc.text(`Accessories (${selectedCards.accessories.length})`, 15, yPos);
yPos += 2;
const accessoryData = [];
selectedCards.accessories.forEach(acc => {
accessoriesSubtotal += acc.price;
accessoryData.push([
acc.name,
acc.description.substring(0, 50) + (acc.description.length > 50 ? '...' : ''),
`$${acc.price.toLocaleString()}`
]);
});
doc.autoTable({
startY: yPos,
head: [['Accessory Name', 'Description', 'Price']],
body: accessoryData,
theme: 'grid',
headStyles: { fillColor: [220, 53, 69], fontSize: 9 },
bodyStyles: { fontSize: 9 },
margin: { left: 15, right: 15 },
columnStyles: {
0: { cellWidth: 60 },
1: { cellWidth: 90 },
2: { cellWidth: 30, halign: 'right' }
}
});
yPos = doc.lastAutoTable.finalY + 3;
doc.setFontSize(10);
doc.setFont(undefined, 'bold');
doc.text(`Subtotal: $${accessoriesSubtotal.toLocaleString()}`, 195, yPos, { align: 'right' });
yPos += 8;
}
// Price Summary Box
yPos += 5;
doc.setFillColor(220, 53, 69);
doc.rect(15, yPos - 5, 180, 30, 'F');
doc.setTextColor(255, 255, 255);
doc.setFontSize(11);
doc.setFont(undefined, 'bold');
doc.text('PRICE SUMMARY', 20, yPos);
yPos += 6;
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.text(`Frame:`, 20, yPos);
doc.text(`$${hSeriesConfig.framePrice.toLocaleString()}`, 190, yPos, { align: 'right' });
yPos += 5;
if (inputSubtotal > 0) {
doc.text(`Input Cards:`, 20, yPos);
doc.text(`$${inputSubtotal.toLocaleString()}`, 190, yPos, { align: 'right' });
yPos += 5;
}
if (outputSubtotal > 0) {
doc.text(`Output Cards:`, 20, yPos);
doc.text(`$${outputSubtotal.toLocaleString()}`, 190, yPos, { align: 'right' });
yPos += 5;
}
if (accessoriesSubtotal > 0) {
doc.text(`Accessories:`, 20, yPos);
doc.text(`$${accessoriesSubtotal.toLocaleString()}`, 190, yPos, { align: 'right' });
yPos += 5;
}
doc.setDrawColor(255, 255, 255);
doc.line(20, yPos, 190, yPos);
yPos += 5;
doc.setFontSize(12);
doc.setFont(undefined, 'bold');
doc.text(`TOTAL:`, 20, yPos);
doc.text(`$${totalPrice.toLocaleString()}`, 190, yPos, { align: 'right' });
doc.setTextColor(0, 0, 0);
// Footer
doc.setFontSize(8);
doc.setFont(undefined, 'normal');
doc.text('For more information visit: novastarled.com', 105, 285, { align: 'center' });
doc.text('Contact: sales@olympianled.com | 321-747-3220', 105, 290, { align: 'center' });
// Save PDF
doc.save(`${hSeriesConfig.frameName.replace(/ /g, '_')}_Configuration.pdf`);
};
logo.onerror = function() {
// Continue without logo
alert('Logo failed to load, generating PDF without logo...');
logo.onload();
};
logo.src = '/logos/LOGO_MSB_BLKBack.png';
}
function shareConfiguration() {
const totalPrice = parseInt(document.getElementById('totalPrice').textContent.replace(/[$,]/g, ''));
if (totalPrice === hSeriesConfig.framePrice) {
alert('Please configure at least one card before sharing.');
return;
}
// Create shareable configuration object
const shareConfig = {
input: selectedCards.input.map((card, i) => card ? { slot: i, name: card.name } : null).filter(c => c),
output: selectedCards.output.map((card, i) => card ? { slot: i, name: card.name } : null).filter(c => c),
accessories: selectedCards.accessories.map(acc => acc.name)
};
// Encode configuration as base64
const configJson = JSON.stringify(shareConfig);
const configEncoded = btoa(configJson);
// Generate shareable URL
const currentUrl = window.location.origin + window.location.pathname;
const shareUrl = `${currentUrl}?config=${configEncoded}`;
// Copy to clipboard
navigator.clipboard.writeText(shareUrl).then(() => {
alert(`Configuration link copied to clipboard!\n\nShare this link:\n${shareUrl}`);
}).catch(() => {
// Fallback if clipboard fails
prompt('Copy this link to share your configuration:', shareUrl);
});
}
function loadSharedConfiguration() {
const urlParams = new URLSearchParams(window.location.search);
const configParam = urlParams.get('config');
if (configParam) {
try {
const configJson = atob(configParam);
const shareConfig = JSON.parse(configJson);
// Map card names back to card objects
shareConfig.input.forEach(item => {
const card = hSeriesConfig.inputCards.find(c => c.name === item.name);
if (card) {
selectedCards.input[item.slot] = card;
} else {
console.warn(`Card not found: ${item.name}`);
}
});
shareConfig.output.forEach(item => {
const card = hSeriesConfig.outputCards.find(c => c.name === item.name);
if (card) {
selectedCards.output[item.slot] = card;
} else {
console.warn(`Card not found: ${item.name}`);
}
});
shareConfig.accessories.forEach(name => {
const acc = hSeriesConfig.accessories.find(a => a.name === name);
if (acc) {
selectedCards.accessories.push(acc);
}
});
return true;
} catch (e) {
console.error('Failed to load shared config:', e);
return false;
}
}
return false;
}
// Auto-open customizer on page load if URL parameter is set
window.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const configParam = urlParams.get('config');
// Open customizer if ?configure=true or if there's a shared config
if (urlParams.get('configure') === 'true' || configParam) {
// Small delay to ensure page is fully loaded
setTimeout(() => {
openHSeriesCustomizer();
// Load shared configuration AFTER customizer is open
if (configParam) {
setTimeout(() => {
if (loadSharedConfiguration()) {
// Refresh the UI with loaded config
renderSlots();
updateSummary();
// Save to sessionStorage so it persists
saveConfiguration();
}
}, 100);
}
}, 500);
}
});
// Close modals on outside click
document.addEventListener('click', (e) => {
const customizerModal = document.getElementById('hSeriesCustomizerModal');
const detailsModal = document.getElementById('cardDetailsModal');
if (e.target === customizerModal) closeHSeriesCustomizer();
if (e.target === detailsModal) closeCardDetails();
});
// Close card details modal on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && document.getElementById('cardDetailsModal').style.display === 'flex') {
closeCardDetails();
}
});