Initial commit
This commit is contained in:
187
app/icons.py
Normal file
187
app/icons.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""Vector icons drawn with QPainter (one consistent style, no image files, no emoji).
|
||||
|
||||
`icon(name, color)` returns a QIcon rendered at several sizes. Names: play, stop, record, open, apply, revert,
|
||||
save, export, import, settings, star, star-off, warning, ab, loop, search, wave.
|
||||
`app_icon()` uses the artwork in app/assets (made by tools/make_assets.py from the user's images).
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import QPointF, QRectF, Qt
|
||||
from PySide6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPen, QPixmap
|
||||
|
||||
ACCENT = "#b0242c"
|
||||
STEEL = "#c4c4c4"
|
||||
TEXT = "#e6e6e6"
|
||||
DANGER = "#e0533d"
|
||||
MUTED = "#9a9a9a"
|
||||
|
||||
_cache = {}
|
||||
|
||||
|
||||
def _arrow_arc(p, r, start, span, head=True):
|
||||
rect = QRectF(-r, -r, 2 * r, 2 * r)
|
||||
path = QPainterPath()
|
||||
path.arcMoveTo(rect, start)
|
||||
path.arcTo(rect, start, span)
|
||||
p.drawPath(path)
|
||||
if head:
|
||||
a = math.radians(start + span)
|
||||
tip = QPointF(r * math.cos(a), -r * math.sin(a))
|
||||
d = 1 if span > 0 else -1
|
||||
tang = math.radians(start + span + 90 * d)
|
||||
back = QPointF(math.cos(tang), -math.sin(tang))
|
||||
nrm = QPointF(math.cos(a), -math.sin(a))
|
||||
L = r * 0.55
|
||||
p.drawLine(tip, tip - back * L + nrm * L * 0.6)
|
||||
p.drawLine(tip, tip - back * L - nrm * L * 0.6)
|
||||
|
||||
|
||||
def _draw(p, name, c, s):
|
||||
"""Draw icon `name` in a box of size s (origin top-left)."""
|
||||
pen = QPen(QColor(c), max(1.2, s * 0.09), Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin)
|
||||
p.setPen(pen)
|
||||
p.setBrush(Qt.BrushStyle.NoBrush)
|
||||
m = s * 0.18
|
||||
if name == "play":
|
||||
path = QPainterPath()
|
||||
path.moveTo(s * 0.30, s * 0.20)
|
||||
path.lineTo(s * 0.80, s * 0.50)
|
||||
path.lineTo(s * 0.30, s * 0.80)
|
||||
path.closeSubpath()
|
||||
p.setBrush(QColor(c))
|
||||
p.drawPath(path)
|
||||
elif name == "stop":
|
||||
p.setBrush(QColor(c))
|
||||
p.drawRoundedRect(QRectF(s * 0.26, s * 0.26, s * 0.48, s * 0.48), s * 0.06, s * 0.06)
|
||||
elif name == "record":
|
||||
p.setPen(Qt.PenStyle.NoPen)
|
||||
p.setBrush(QColor(DANGER))
|
||||
p.drawEllipse(QRectF(s * 0.22, s * 0.22, s * 0.56, s * 0.56))
|
||||
elif name == "open":
|
||||
path = QPainterPath()
|
||||
path.moveTo(m, s * 0.30)
|
||||
path.lineTo(s * 0.40, s * 0.30)
|
||||
path.lineTo(s * 0.48, s * 0.38)
|
||||
path.lineTo(s - m, s * 0.38)
|
||||
path.lineTo(s - m, s * 0.78)
|
||||
path.lineTo(m, s * 0.78)
|
||||
path.closeSubpath()
|
||||
p.drawPath(path)
|
||||
elif name == "apply":
|
||||
path = QPainterPath()
|
||||
path.moveTo(s * 0.20, s * 0.52)
|
||||
path.lineTo(s * 0.42, s * 0.74)
|
||||
path.lineTo(s * 0.82, s * 0.28)
|
||||
p.drawPath(path)
|
||||
elif name in ("revert", "loop"):
|
||||
p.translate(s / 2, s / 2)
|
||||
if name == "revert":
|
||||
_arrow_arc(p, s * 0.30, 60, 270)
|
||||
else:
|
||||
_arrow_arc(p, s * 0.30, 20, 150)
|
||||
_arrow_arc(p, s * 0.30, 200, 150)
|
||||
p.translate(-s / 2, -s / 2)
|
||||
elif name == "save": # disk
|
||||
path = QPainterPath()
|
||||
path.moveTo(m, m)
|
||||
path.lineTo(s * 0.70, m)
|
||||
path.lineTo(s - m, s * 0.30)
|
||||
path.lineTo(s - m, s - m)
|
||||
path.lineTo(m, s - m)
|
||||
path.closeSubpath()
|
||||
p.drawPath(path)
|
||||
p.drawRect(QRectF(s * 0.32, m, s * 0.30, s * 0.20))
|
||||
p.drawRect(QRectF(s * 0.30, s * 0.56, s * 0.40, s * 0.26))
|
||||
elif name in ("export", "import"):
|
||||
p.drawLine(QPointF(m, s * 0.62), QPointF(m, s - m))
|
||||
p.drawLine(QPointF(m, s - m), QPointF(s - m, s - m))
|
||||
p.drawLine(QPointF(s - m, s - m), QPointF(s - m, s * 0.62))
|
||||
up = name == "export"
|
||||
a, b = (s * 0.66, s * 0.16) if up else (s * 0.16, s * 0.66)
|
||||
p.drawLine(QPointF(s / 2, a), QPointF(s / 2, b))
|
||||
d = -1 if up else 1
|
||||
p.drawLine(QPointF(s / 2, b), QPointF(s / 2 - s * 0.16, b - d * s * 0.16))
|
||||
p.drawLine(QPointF(s / 2, b), QPointF(s / 2 + s * 0.16, b - d * s * 0.16))
|
||||
elif name == "settings":
|
||||
p.translate(s / 2, s / 2)
|
||||
path = QPainterPath()
|
||||
teeth = 8
|
||||
for i in range(teeth * 2):
|
||||
a = math.pi * i / teeth
|
||||
r = s * (0.36 if i % 2 == 0 else 0.27)
|
||||
pt = QPointF(r * math.cos(a), r * math.sin(a))
|
||||
path.moveTo(pt) if i == 0 else path.lineTo(pt)
|
||||
path.closeSubpath()
|
||||
p.drawPath(path)
|
||||
p.drawEllipse(QPointF(0, 0), s * 0.11, s * 0.11)
|
||||
p.translate(-s / 2, -s / 2)
|
||||
elif name in ("star", "star-off"):
|
||||
path = QPainterPath()
|
||||
for i in range(10):
|
||||
a = -math.pi / 2 + math.pi * i / 5
|
||||
r = s * (0.40 if i % 2 == 0 else 0.17)
|
||||
pt = QPointF(s / 2 + r * math.cos(a), s * 0.54 + r * math.sin(a))
|
||||
path.moveTo(pt) if i == 0 else path.lineTo(pt)
|
||||
path.closeSubpath()
|
||||
if name == "star":
|
||||
p.setBrush(QColor(c))
|
||||
p.drawPath(path)
|
||||
elif name == "warning":
|
||||
path = QPainterPath()
|
||||
path.moveTo(s / 2, s * 0.14)
|
||||
path.lineTo(s * 0.88, s * 0.84)
|
||||
path.lineTo(s * 0.12, s * 0.84)
|
||||
path.closeSubpath()
|
||||
p.drawPath(path)
|
||||
p.drawLine(QPointF(s / 2, s * 0.40), QPointF(s / 2, s * 0.60))
|
||||
p.drawPoint(QPointF(s / 2, s * 0.72))
|
||||
elif name == "ab": # compare: half-filled circle
|
||||
rect = QRectF(s * 0.18, s * 0.18, s * 0.64, s * 0.64)
|
||||
half = QPainterPath()
|
||||
half.moveTo(s / 2, s * 0.18)
|
||||
half.arcTo(rect, 90, 180)
|
||||
half.closeSubpath()
|
||||
p.setBrush(QColor(c))
|
||||
p.drawPath(half)
|
||||
p.setBrush(Qt.BrushStyle.NoBrush)
|
||||
p.drawEllipse(rect)
|
||||
elif name == "search":
|
||||
p.drawEllipse(QRectF(s * 0.18, s * 0.18, s * 0.46, s * 0.46))
|
||||
p.drawLine(QPointF(s * 0.58, s * 0.58), QPointF(s * 0.84, s * 0.84))
|
||||
elif name == "wave": # fallback app glyph: waveform bars
|
||||
p.setPen(Qt.PenStyle.NoPen)
|
||||
p.setBrush(QColor(c))
|
||||
for i, h in enumerate((0.25, 0.5, 0.8, 0.45, 0.65, 0.3)):
|
||||
p.drawRoundedRect(QRectF(s * (0.14 + i * 0.13), s * (0.5 - h / 2), s * 0.08, s * h), s * 0.03, s * 0.03)
|
||||
|
||||
|
||||
def icon(name, color=TEXT):
|
||||
key = (name, color)
|
||||
if key not in _cache:
|
||||
ic = QIcon()
|
||||
for sz in (16, 20, 24, 32, 48, 64, 128, 256):
|
||||
pm = QPixmap(sz, sz)
|
||||
pm.fill(Qt.GlobalColor.transparent)
|
||||
p = QPainter(pm)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
_draw(p, name, color, sz)
|
||||
p.end()
|
||||
ic.addPixmap(pm)
|
||||
_cache[key] = ic
|
||||
return _cache[key]
|
||||
|
||||
|
||||
ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
|
||||
|
||||
|
||||
def asset(name):
|
||||
p = os.path.join(ASSETS, name)
|
||||
return p if os.path.exists(p) else None
|
||||
|
||||
|
||||
def app_icon():
|
||||
"""The app icon (Arkham City logo bat sending out sound arcs); a plain waveform glyph if the assets are missing."""
|
||||
p = asset("app_icon.ico") or asset("app_icon.png")
|
||||
return QIcon(p) if p else icon("wave", ACCENT)
|
||||
|
||||
Reference in New Issue
Block a user