Twitch Streamer Toolkit

Pro Streamer Toolkit - Polls, Spinners & Emotes | iTechVista

Pro Streamer Toolkit

Stream Interaction Poll

How to use: Set up your question and options, then click "Start Poll". A new window will pop up. Add that window as a "Window Capture" source in OBS. Click the buttons below to tally votes as your chat responds!
Live on Stream

High-DPI Giveaway Wheel

Winner!
???

Pro Emote Combiner

Drag to move • Scroll to resize
`; 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(); })();