Give KO reactions an animated arcade overlay

This commit is contained in:
Peter Stockings
2026-10-05 15:52:30 +11:00
parent f3eeda71e6
commit b5f09a9e9e
3 changed files with 101 additions and 3 deletions
+80 -1
View File
@@ -4,7 +4,8 @@ from pathlib import Path
import struct
import wave
from PyQt6.QtCore import Qt, QTimer, QPropertyAnimation, QPoint, QEasingCurve, QEvent
from PyQt6.QtCore import Qt, QTimer, QPropertyAnimation, QVariantAnimation, QPoint, QEasingCurve, QEvent
from PyQt6.QtGui import QPainter, QPainterPath, QFont, QColor, QPen, QLinearGradient, QTransform
from PyQt6.QtWidgets import QApplication, QWidget, QFrame, QLabel, QHBoxLayout, QVBoxLayout
@@ -50,6 +51,58 @@ def write_reaction_sound(directory, kind):
return path
class ArcadeKnockout(QWidget):
"""Vector-painted arcade lettering scales cleanly with the video surface."""
def __init__(self, parent):
super().__init__(parent)
self.progress = 0.0
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
def set_progress(self, value):
self.progress = value
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
t = self.progress
opacity = min(1.0, t / .035, max(0.0, (1.0 - t) / .18))
painter.setOpacity(opacity)
painter.translate(self.width() / 2, self.height() / 2)
entry = min(1.0, t / .18)
scale = 1.0 + .65 * (1 - entry) ** 3 - .06 * math.sin(entry * math.pi)
painter.scale(scale, scale)
font = QFont("Impact")
font.setPixelSize(180)
font.setBold(True)
letters = QPainterPath()
letters.addText(0, 0, font, "K.O.")
letters = QTransform().shear(-.12, 0).map(letters)
bounds = letters.boundingRect()
fit = min(self.width() * .63 / bounds.width(), self.height() * .40 / bounds.height())
painter.scale(fit, fit)
painter.translate(-bounds.center())
shadow = QTransform().translate(7, 10).map(letters)
painter.setPen(QPen(QColor("#131017"), 18, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
painter.setBrush(QColor("#131017"))
painter.drawPath(shadow)
painter.setPen(QPen(QColor("#8b1028"), 14, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
painter.setBrush(QColor("#f33c35"))
painter.drawPath(letters)
gradient = QLinearGradient(0, bounds.top(), 0, bounds.bottom())
gradient.setColorAt(0, QColor("#fff7b0"))
gradient.setColorAt(.40, QColor("#ffe252"))
gradient.setColorAt(1, QColor("#ff9c22"))
painter.setPen(QPen(QColor("#ff583e"), 5, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
painter.setBrush(gradient)
painter.drawPath(letters)
if t < .32:
painter.setOpacity(opacity * (1 - t / .32) * .55)
painter.setClipPath(letters)
sweep = bounds.left() + bounds.width() * min(1.0, t / .32)
painter.fillRect(int(sweep), int(bounds.top() - 10), 16, int(bounds.height() + 20), QColor("#fffef0"))
class RingsideStrip(QWidget):
"""A separate native window stays above VLC's native video surface."""
def __init__(self, video_frame):
@@ -65,6 +118,15 @@ class RingsideStrip(QWidget):
outer.setContentsMargins(0, 0, 0, 0)
self.panel = QFrame()
outer.addWidget(self.panel)
self.ko_canvas = ArcadeKnockout(self)
outer.addWidget(self.ko_canvas)
self.ko_canvas.hide()
self.kind = None
self.ko_animation = QVariantAnimation(self)
self.ko_animation.setDuration(2200)
self.ko_animation.setStartValue(0.0)
self.ko_animation.setEndValue(1.0)
self.ko_animation.valueChanged.connect(self.ko_canvas.set_progress)
row = QHBoxLayout(self.panel)
row.setContentsMargins(14, 10, 14, 10)
row.setSpacing(14)
@@ -112,8 +174,18 @@ class RingsideStrip(QWidget):
self.caption.setText(f"{author}: {caption}")
self.metadata.setText(f"Room reaction · {position}")
self.panel.setStyleSheet(f"QFrame {{ background: #1a1b20; border-left: 3px solid {color}; border-radius: 6px; }} QLabel {{ border: none; }}")
self.kind = kind
self.panel.setVisible(kind != "ko")
self.ko_canvas.setVisible(kind == "ko")
self.ko_animation.stop()
self.slide.stop()
self._place()
if kind == "ko":
self.ko_canvas.set_progress(0.0)
self.show()
self.ko_animation.start()
self.dismiss_timer.start(2200)
return True
end = self.pos()
self.move(end + QPoint(0, 12))
self.show()
@@ -124,6 +196,12 @@ class RingsideStrip(QWidget):
return True
def _place(self):
if self.kind == "ko":
self.setFixedSize(self.video_frame.size())
self.move(self.video_frame.mapToGlobal(QPoint(0, 0)))
return
self.setMinimumSize(0, 0)
self.setMaximumSize(16777215, 16777215)
width = max(1, min(540, self.video_frame.width() - 32))
self.setFixedWidth(width)
height = self.layout().heightForWidth(width)
@@ -141,6 +219,7 @@ class RingsideStrip(QWidget):
return super().eventFilter(watched, event)
def dismiss(self):
self.ko_animation.stop()
self.slide.stop()
self.dismiss_timer.stop()
self.hide()