Kit de herramientas para streamers de Twitch

Kit para Streamers Pro - Encuestas, Ruletas y Emotes | iTechVista

Kit para Streamers Pro

Encuesta de Interacción en el Stream

Cómo usar: Configura tu pregunta y opciones, luego haz clic en "Iniciar Encuesta". Aparecerá una nueva ventana. Añade esa ventana como fuente de "Captura de Ventana" en OBS. ¡Haz clic en los botones de abajo para contar los votos a medida que tu chat responde!
En vivo en el Stream

Ruleta de Sorteos de Alta Resolución

¡Ganador!
???

Combinador de Emotes Pro

Arrastra para mover • Desplaza para cambiar el tamaño
`; pollWindow.document.open(); pollWindow.document.write(html); pollWindow.document.close(); } // --- 4. HIGH-DPI SPINNER SYSTEM --- const spinCanvas = $('ivtk-spinner-canvas'); const spinCtx = spinCanvas.getContext('2d'); let spinNames = []; let spinAngleStart = 0; let startAngle = 0; let spinTime = 0; let spinTimeTotal = 0; let spinTimeout = null; let synth = null; let lastTickSegment = 0; function getCSSVar(name) { return getComputedStyle(document.body).getPropertyValue(name).trim() || '#fff'; } // High DPI Canvas Setup function setupSpinnerCanvas() { // Virtual internal resolution const size = 600; const dpr = window.devicePixelRatio || 1; spinCanvas.width = size * dpr; spinCanvas.height = size * dpr; // CSS scaling is handled by flex container aspect-ratio spinCtx.scale(dpr, dpr); return size; // Internal logical size } function drawSpinner() { const size = setupSpinnerCanvas(); const center = size / 2; const radius = center - 10; const rawNames = $('ivtk-spinner-names').value.split('\n').map(n => n.trim()).filter(n => n); spinNames = rawNames.length > 0 ? rawNames : ['Add', 'Names', 'To', 'Begin']; spinCtx.clearRect(0, 0, size, size); const arc = Math.PI * 2 / spinNames.length; const colors = ['--ivtk-poll-1', '--ivtk-poll-2', '--ivtk-poll-3', '--ivtk-poll-4', '--ivtk-poll-5']; spinCtx.lineWidth = 4; spinCtx.strokeStyle = getCSSVar('--ivtk-bg-secondary'); for(let i = 0; i < spinNames.length; i++) { const angle = startAngle + i * arc; // Draw Slice spinCtx.fillStyle = getCSSVar(colors[i % colors.length]); spinCtx.beginPath(); spinCtx.arc(center, center, radius, angle, angle + arc, false); spinCtx.lineTo(center, center); spinCtx.fill(); spinCtx.stroke(); // Draw Text spinCtx.save(); spinCtx.fillStyle = '#ffffff'; spinCtx.font = 'bold 24px Inter, sans-serif'; spinCtx.translate( center + Math.cos(angle + arc / 2) * (radius * 0.6), center + Math.sin(angle + arc / 2) * (radius * 0.6) ); spinCtx.rotate(angle + arc / 2); const text = spinNames[i]; const textWidth = spinCtx.measureText(text).width; // Truncate logic if text too long (optional) spinCtx.fillText(text, -textWidth / 2, 8); spinCtx.restore(); } } $('ivtk-spinner-names').addEventListener('input', drawSpinner); window.addEventListener('resize', () => { if ($('ivtk-tab-spinner').classList.contains('ivtk-active')) drawSpinner(); }); $('ivtk-btn-spin').addEventListener('click', () => { if (spinNames.length <= 1 && spinNames[0] === 'Add') return; // Default state // Audio init on user gesture if (!synth) { synth = new Tone.PluckSynth().toDestination(); synth.volume.value = -12; } if (Tone.context.state !== 'running') Tone.context.resume(); $('ivtk-btn-spin').disabled = true; $('ivtk-winner-display').style.display = 'none'; spinAngleStart = Math.random() * 10 + 15; // Random initial velocity spinTime = 0; spinTimeTotal = Math.random() * 2000 + 4000; // 4 to 6 seconds duration rotateWheel(); }); function rotateWheel() { spinTime += 30; // 30ms simulation steps if(spinTime >= spinTimeTotal) { stopRotateWheel(); return; } // Easing function (easeOutQuart for smooth slow down) const easeOut = (t, b, c, d) => { t /= d; t--; return -c * (t * t * t * t - 1) + b; }; const spinAngle = easeOut(spinTime, spinAngleStart, -spinAngleStart, spinTimeTotal); startAngle += (spinAngle * Math.PI / 180); // Tick Sound Logic const arc = Math.PI * 2 / spinNames.length; // The pointer is at 0 degrees (right side in standard canvas math). // We calculate which segment is crossing 0 degrees. const currentSegment = Math.floor((Math.PI * 2 - (startAngle % (Math.PI * 2))) / arc) % spinNames.length; if (currentSegment !== lastTickSegment) { lastTickSegment = currentSegment; try { synth.triggerAttack("C5"); } catch(e){} } drawSpinner(); spinTimeout = requestAnimationFrame(rotateWheel); } function stopRotateWheel() { cancelAnimationFrame(spinTimeout); $('ivtk-btn-spin').disabled = false; const arc = Math.PI * 2 / spinNames.length; // Normalize startAngle const normalizedStartAngle = startAngle % (Math.PI * 2); // Angle of pointer is 0 (right side). // Distance from start to pointer: let pointerAngle = (Math.PI * 2) - normalizedStartAngle; let winningIndex = Math.floor(pointerAngle / arc) % spinNames.length; const winner = spinNames[winningIndex]; $('ivtk-winner-name-text').textContent = winner; $('ivtk-winner-display').style.display = 'block'; // Success sound try { const winSynth = new Tone.PolySynth(Tone.Synth).toDestination(); winSynth.volume.value = -8; winSynth.triggerAttackRelease(["C4", "E4", "G4", "C5"], "4n"); } catch(e){} // Confetti confetti({ particleCount: 150, spread: 80, origin: { y: 0.6 }, colors: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899'] }); } // --- 5. EMOTE COMBINER SYSTEM --- const combineCanvas = $('ivtk-combiner-canvas'); const combineCtx = combineCanvas.getContext('2d'); let baseImg = null; let overlayImg = null; // Transform state for the overlay let tx = { x: 0, y: 0, scale: 1 }; let isDragging = false; let dragStart = { x: 0, y: 0 }; function loadEmoteImage(file, isBase) { if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { if (isBase) { baseImg = img; const drop = $('ivtk-drop-base'); drop.innerHTML = ``; // Internal resolution equals base image original resolution combineCanvas.width = img.width; combineCanvas.height = img.height; // Reset overlay positioning to center of new base if (overlayImg) { tx.x = img.width / 2; tx.y = img.height / 2; } $('ivtk-btn-download-emote').disabled = false; } else { overlayImg = img; const drop = $('ivtk-drop-overlay'); drop.innerHTML = ``; if (baseImg) { tx.x = baseImg.width / 2; tx.y = baseImg.height / 2; // Auto scale overlay to fit reasonably tx.scale = (baseImg.width * 0.5) / overlayImg.width; } else { alert("Please upload a Base image first!"); } } drawCombiner(); reattachFileListeners(); }; img.src = e.target.result; }; reader.readAsDataURL(file); } function reattachFileListeners() { $('ivtk-input-base').addEventListener('change', (e) => loadEmoteImage(e.target.files[0], true)); $('ivtk-input-overlay').addEventListener('change', (e) => loadEmoteImage(e.target.files[0], false)); } reattachFileListeners(); // Drag & Drop visual support ['ivtk-drop-base', 'ivtk-drop-overlay'].forEach(id => { const el = $(id); el.addEventListener('dragover', (e) => { e.preventDefault(); el.style.borderColor = 'var(--ivtk-accent)'; }); el.addEventListener('dragleave', () => { el.style.borderColor = ''; }); el.addEventListener('drop', (e) => { e.preventDefault(); el.style.borderColor = ''; if(e.dataTransfer.files.length > 0) { loadEmoteImage(e.dataTransfer.files[0], id === 'ivtk-drop-base'); } }); }); function drawCombiner() { if (!baseImg) { combineCtx.clearRect(0, 0, combineCanvas.width, combineCanvas.height); return; } // Draw base (fills the entire logical canvas) combineCtx.clearRect(0, 0, combineCanvas.width, combineCanvas.height); combineCtx.drawImage(baseImg, 0, 0, combineCanvas.width, combineCanvas.height); // Draw overlay with transforms if (overlayImg) { combineCtx.save(); combineCtx.translate(tx.x, tx.y); combineCtx.scale(tx.scale, tx.scale); // Draw centered at transform point combineCtx.drawImage(overlayImg, -overlayImg.width / 2, -overlayImg.height / 2); combineCtx.restore(); } } // Coordinate mapping from screen CSS size to logical Canvas internal size function getCanvasPos(e) { const rect = combineCanvas.getBoundingClientRect(); // Handle touch or mouse const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; const scaleX = combineCanvas.width / rect.width; const scaleY = combineCanvas.height / rect.height; return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY }; } // Mouse Events combineCanvas.addEventListener('mousedown', startDrag); window.addEventListener('mousemove', drag); window.addEventListener('mouseup', endDrag); // Touch Events combineCanvas.addEventListener('touchstart', (e) => { e.preventDefault(); startDrag(e); }, {passive: false}); window.addEventListener('touchmove', drag, {passive: false}); window.addEventListener('touchend', endDrag); function startDrag(e) { if (!overlayImg) return; isDragging = true; const pos = getCanvasPos(e); dragStart = { x: pos.x - tx.x, y: pos.y - tx.y }; } function drag(e) { if (!isDragging || !overlayImg) return; // e.preventDefault(); // careful with preventDefault on window mousemove const pos = getCanvasPos(e); tx.x = pos.x - dragStart.x; tx.y = pos.y - dragStart.y; drawCombiner(); } function endDrag() { isDragging = false; } // Zoom via Mouse Wheel combineCanvas.addEventListener('wheel', (e) => { if (!overlayImg) return; e.preventDefault(); const delta = e.deltaY > 0 ? 0.9 : 1.1; // 10% step tx.scale *= delta; drawCombiner(); }, {passive: false}); // Download logic $('ivtk-btn-download-emote').addEventListener('click', () => { if (!baseImg) return; const link = document.createElement('a'); link.download = 'ivtk_custom_emote.png'; link.href = combineCanvas.toDataURL('image/png', 1.0); // Full quality export link.click(); }); // --- INIT BOOTSTRAP --- lucide.createIcons(); addPollOption('Yes'); addPollOption('No'); drawSpinner(); })();