// Wild Dragon — Blackmagic DeckLink port diagram. // // Renders an inline SVG that mirrors the real card's physical layout so // operators can pick a port visually instead of guessing what "BM1" maps to. // // Usage: // const svg = BMDCards.render({ // model: 'DeckLink Duo 2', // deviceCount: 4, // selectedIndex: 0, // onSelect: (i) => console.log('port', i), // }); // container.appendChild(svg); // // Add new cards by appending to MODELS. Coordinates are in SVG user units — // each card is drawn with the bracket on the left (matches a PC case fitting). window.BMDCards = (function () { const NS = 'http://www.w3.org/2000/svg'; // Each model entry describes the physical layout. `portPositions` is an // ordered list — port 0 is the first entry, matching ffmpeg's -i device index. const MODELS = { 'DeckLink Duo 2': { friendly: 'DeckLink Duo 2', form: 'PCIe half-height', portKind: 'BNC SDI', bidirectional: true, width: 360, height: 180, bracket: { x: 8, y: 14, w: 40, h: 152 }, portPositions: [ { cx: 28, cy: 36, label: '1', role: 'SDI 1' }, { cx: 28, cy: 72, label: '2', role: 'SDI 2' }, { cx: 28, cy: 108, label: '3', role: 'SDI 3' }, { cx: 28, cy: 144, label: '4', role: 'SDI 4' }, ], notes: 'Each BNC is independently configurable as In or Out via Blackmagic Desktop Video Setup.', }, 'DeckLink Quad 2': { friendly: 'DeckLink Quad 2', form: 'PCIe full-height', portKind: 'BNC SDI', bidirectional: true, width: 360, height: 300, bracket: { x: 8, y: 14, w: 40, h: 272 }, portPositions: [ { cx: 28, cy: 36, label: '1', role: 'SDI 1' }, { cx: 28, cy: 70, label: '2', role: 'SDI 2' }, { cx: 28, cy: 104, label: '3', role: 'SDI 3' }, { cx: 28, cy: 138, label: '4', role: 'SDI 4' }, { cx: 28, cy: 172, label: '5', role: 'SDI 5' }, { cx: 28, cy: 206, label: '6', role: 'SDI 6' }, { cx: 28, cy: 240, label: '7', role: 'SDI 7' }, { cx: 28, cy: 274, label: '8', role: 'SDI 8' }, ], notes: '8 reconfigurable BNCs. Pair adjacent ports for 6G/12G-SDI dual-link.', }, 'DeckLink Mini Recorder 4K': { friendly: 'DeckLink Mini Recorder 4K', form: 'PCIe half-height', portKind: 'BNC SDI + HDMI', bidirectional: false, width: 360, height: 180, bracket: { x: 8, y: 14, w: 40, h: 152 }, portPositions: [ { cx: 28, cy: 50, label: 'SDI', role: 'SDI In', kind: 'sdi' }, { cx: 28, cy: 130, label: 'HDMI', role: 'HDMI In', kind: 'hdmi' }, ], notes: 'Input-only. SDI and HDMI share a single capture channel — pick which input ffmpeg should open.', }, 'DeckLink Mini Monitor 4K': { friendly: 'DeckLink Mini Monitor 4K', form: 'PCIe half-height', portKind: 'BNC SDI + HDMI', bidirectional: false, width: 360, height: 180, bracket: { x: 8, y: 14, w: 40, h: 152 }, portPositions: [ { cx: 28, cy: 50, label: 'SDI', role: 'SDI Out', kind: 'sdi' }, { cx: 28, cy: 130, label: 'HDMI', role: 'HDMI Out', kind: 'hdmi' }, ], notes: 'Output-only — for playback monitoring, not capture.', }, 'UltraStudio 4K Mini': { friendly: 'UltraStudio 4K Mini', form: 'Thunderbolt 3', portKind: 'BNC SDI + HDMI', bidirectional: true, width: 360, height: 220, bracket: { x: 8, y: 14, w: 40, h: 192 }, portPositions: [ { cx: 28, cy: 44, label: '1', role: 'SDI 1' }, { cx: 28, cy: 84, label: '2', role: 'SDI 2' }, { cx: 28, cy: 124, label: '3', role: 'SDI 3' }, { cx: 28, cy: 164, label: '4', role: 'SDI 4' }, ], notes: '4 reconfigurable BNCs + HDMI on the rear.', }, }; function el(tag, attrs) { const node = document.createElementNS(NS, tag); for (const [k, v] of Object.entries(attrs || {})) { if (v !== undefined && v !== null) node.setAttribute(k, String(v)); } return node; } function resolveModel(name) { if (!name) return null; const n = String(name).toLowerCase().trim(); for (const [k, v] of Object.entries(MODELS)) { if (k.toLowerCase() === n) return { ...v, name: k }; } // Loose match — accept things like "Blackmagic DeckLink Duo 2 (1)" or // "decklinkduo2" by stripping non-alphanumerics. const clean = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, ''); const target = clean(n); for (const [k, v] of Object.entries(MODELS)) { const ck = clean(k); if (target.includes(ck) || ck.includes(target)) return { ...v, name: k }; } return null; } function genericModel(portCount) { const ports = Math.max(1, portCount || 1); const h = 30 + ports * 36; return { friendly: 'Generic DeckLink', form: 'unknown', portKind: 'SDI', bidirectional: false, width: 360, height: h, bracket: { x: 8, y: 14, w: 40, h: h - 28 }, portPositions: Array.from({ length: ports }, (_, i) => ({ cx: 28, cy: 36 + i * 36, label: String(i + 1), role: `Port ${i + 1}`, })), notes: 'Card model not recognised — pick the port by its system index.', }; } /** * Render the SVG card. * * Options: * model model name (string) * deviceCount number of detected ports — used to size the generic fallback * selectedIndex currently-selected port index * onSelect (index) => void * compact drop the model label + footer (default false) */ function render({ model, deviceCount, selectedIndex, onSelect, compact }) { const m = resolveModel(model) || genericModel(deviceCount || 2); // If we resolved a model but the cluster reports more ports than we // know about, fall back to a generic with the reported count so we // don't hide ports. const actualPorts = deviceCount || m.portPositions.length; const positions = actualPorts > m.portPositions.length ? genericModel(actualPorts).portPositions : m.portPositions.slice(0, actualPorts); const svg = el('svg', { viewBox: `0 0 ${m.width} ${m.height}`, class: 'bmd-card-svg', role: 'img', 'aria-label': `${m.friendly} — ${actualPorts} ports`, }); // PCB body (right of bracket) svg.appendChild(el('rect', { x: m.bracket.x + m.bracket.w + 6, y: 10, width: m.width - m.bracket.x - m.bracket.w - 18, height: m.height - 20, rx: 8, class: 'bmd-card-body', })); // Decorative trace lines on the PCB const traceCount = Math.min(positions.length, 6); for (let i = 0; i < traceCount; i++) { const y = 24 + i * ((m.height - 48) / traceCount); svg.appendChild(el('path', { d: `M ${m.bracket.x + m.bracket.w + 16} ${y} L ${m.width - 20} ${y}`, class: 'bmd-card-trace', })); } // Bracket (metal slot fitting) svg.appendChild(el('rect', { x: m.bracket.x, y: m.bracket.y, width: m.bracket.w, height: m.bracket.h, rx: 4, class: 'bmd-card-bracket', })); // Ports positions.forEach((p, i) => { const isSel = i === selectedIndex; const g = el('g', { class: 'bmd-port-group' + (isSel ? ' is-selected' : ''), 'data-port': i, }); if (typeof onSelect === 'function') { g.style.cursor = 'pointer'; g.addEventListener('click', () => onSelect(i)); } // BNC outer ring — bigger flange for SDI, smaller for HDMI const isHdmi = p.kind === 'hdmi'; g.appendChild(el(isHdmi ? 'rect' : 'circle', isHdmi ? { x: p.cx - 14, y: p.cy - 6, width: 28, height: 12, rx: 1.5, class: 'bmd-port-ring' } : { cx: p.cx, cy: p.cy, r: 12, class: 'bmd-port-ring' } )); // Inner pin g.appendChild(el('circle', { cx: p.cx, cy: p.cy, r: isHdmi ? 1.5 : 4, class: 'bmd-port-pin', })); // Port label to the right of the connector g.appendChild(el('text', { x: p.cx + 24, y: p.cy - 2, class: 'bmd-port-label', })).textContent = p.label; g.appendChild(el('text', { x: p.cx + 24, y: p.cy + 11, class: 'bmd-port-sublabel', })).textContent = p.role; svg.appendChild(g); }); if (!compact) { // Model strip across the bottom of the PCB svg.appendChild(el('text', { x: m.bracket.x + m.bracket.w + 18, y: m.height - 14, class: 'bmd-card-model', })).textContent = m.friendly.toUpperCase(); } svg.dataset.bmdModel = m.name || 'generic'; svg.dataset.portCount = String(actualPorts); return svg; } function getModelInfo(name) { return resolveModel(name); } function listModels() { return Object.keys(MODELS); } return { render, resolveModel, getModelInfo, listModels, MODELS }; })();