Skip to content

Instantly share code, notes, and snippets.

@Tillsten
Created March 30, 2020 23:09
Show Gist options
  • Save Tillsten/c5467e5cc027a70f2f11cb695b597f27 to your computer and use it in GitHub Desktop.
Save Tillsten/c5467e5cc027a70f2f11cb695b597f27 to your computer and use it in GitHub Desktop.
blend2d pyplotter
# %%timeit
import cppyy, time
import cppyy.gbl as g
from pathlib import Path
import numpy as np
import IPython.display as disp
from PIL import Image
from ctypes import c_int, c_uint, c_uint32, c_int16, c_double, c_void_p
import numpy as np
cppyy.add_include_path(str(Path(__file__).parent.parent / 'src'))
cppyy.include('blend2d.h')
cppyy.load_library('blend2d.dll')
from ticker import get_ticks
# %%
import cppyy.ll
#cppyy.ll.set_signals_as_exception(True)
cppyy.cppdef("""
void draw_xy2(float* x, float* y, int n, BLPath& p) {
p.reserve(n);
p.moveTo(x[0], y[0]);
for (int i = 0; i < n; i++) {
p.lineTo(x[i], y[i]);
};
}
""")
cppyy.cppdef("""
#include <vector>
void draw_xy(const std::vector<float>& x, const std::vector<float>& y, BLPath& p)
{
int n = x.size();
p.reserve(n+10);
p.moveTo(x[0], y[0]);
for (int i = 1; i<n;i++) {
p.lineTo(x[i], y[i]);
};
}
"""
)
colors = {}
with open("color.txt") as f:
for l in f.readlines():
idx = l.find("#")
name = l[:idx - 1]
color = l[idx + 1:-2]
color = int(color, base=16)
colors[name] = color
del color
# %%
import attr
from typing import Tuple as T
from typing import List
from qtpy.QtGui import QImage, QPainter, QMouseEvent
from qtpy.QtWidgets import QWidget, QApplication
from qtpy.QtCore import QTimer, Qt, QThread
app = QApplication([])
def m_print(m):
s = ''
for i in range(3):
for j in range(2):
s += '%.2f ' % getattr(m, 'm%d%d' % (i, j))
return s
g.BLMatrix2D.__str__ = m_print
g.BLPoint.__str__ = lambda p: 'Point(x=%.2f, y=%.2f)' % (p.x, p.y)
g.BLBox.__str__ = lambda p: 'Box(x0=%.2f, y0=%.2f, x1=%.2f, y1=%.2f)' % (p.x0, p.y0, p.x1, p.y1)
g.BLContext.strokePath.__release_gil__ = True
face = g.BLFontFace();
err = face.createFromFile("NotoSans-Regular.ttf")
font = g.BLFont()
font.createFromFace(face, 0.1)
print(err)
@attr.s(auto_attribs=True, slots=True)
class Line:
x: np.ndarray
y: np.ndarray
color: int
linewidth: float = 1
path: g.BLPath = attr.ib(factory=g.BLPath)
def set_data(self, x, y):
self.x, self.y = x, y
def draw(self, ctx):
ctx.save()
ctx.setStrokeStyle(g.BLRgba32(self.color + 0xFF000000))
ctx.setStrokeWidth(self.linewidth)
self.path.reset()
g.draw_xy(self.x.astype('f'), self.y.astype('f'), self.path)
ctx.strokePath(self.path)
ctx.restore()
@attr.s(auto_attribs=True)
class Axis:
x0: float
y0: float
w: float
h: float
artists: List[Line] = attr.ib(factory=list)
view_lim: g.BLBox = g.BLBox(-5, -5, 10, 10)
xticks: List[float] = attr.ib(factory=list)
yticks: List[float] = attr.ib(factory=list)
def set_pos(self, x0, y0, w, h):
self.x0 = x0
self.y0 = y0
self.w, self.h = w, h
self.generate_transforms()
self.generate_ticks()
def set_viewlim(self, view_lim):
self.view_lim = view_lim
self.generate_transforms()
self.generate_ticks()
def generate_transforms(self):
xs = self.view_lim.x1 - self.view_lim.x0
ys = self.view_lim.y1 - self.view_lim.y0
self.inch2data = g.BLMatrix2D.makeTranslation(self.x0, self.y0)
self.inch2data.scale(self.w / xs, self.h / ys)
self.inch2data.translate(-self.view_lim.x0, -self.view_lim.y0)
self.data2inch = g.BLMatrix2D(self.inch2data)
self.data2inch.invert()
def generate_ticks(self):
self.xticks = get_ticks((self.view_lim.x0, self.view_lim.x1), self.w)
self.yticks = get_ticks((self.view_lim.y0, self.view_lim.y1), self.h)
@property
def x1(self):
return self.x0 + self.w
@property
def y1(self):
return self.y0 + self.h
def add_line(self, x, y, color, lw=2):
line = Line(x, y, color=colors[color], linewidth=lw)
self.artists.append(line)
return line
def draw(self, ctx):
self.draw_self(ctx)
self.draw_artists(ctx)
def draw_self(self, ctx: g.BLContext):
ctx.save()
ctx.setFillStyle(g.BLRgba32(0xFFFFFFFF));
ctx.fillRect(self.x0, self.y0, self.w, self.h)
ctx.strokeRect(self.x0, self.y0, self.w, self.h)
ctx.setFillStyle(g.BLRgba32(0xFF000000));
for i in self.xticks:
p = self.inch2data.mapPoint(i, 0)
p.y = self.y0
ctx.strokeLine(p.x, self.y0, p.x, self.y0+0.05)
ctx.translate(p)
ctx.scale(-1, 1)
ctx.rotate(-np.pi)
txt = "%.0f"%i
ctx.fillUtf8Text(g.BLPoint(-0.00-len(txt)*0.03, .1), font, txt)
ctx.rotate(np.pi)
ctx.scale(-1, 1)
ctx.translate(-p)
for i in self.yticks:
p = self.inch2data.mapPoint(0, i)
p.x = self.x0
ctx.strokeLine(self.x0, p.y, self.x0+0.05, p.y)
ctx.translate(p)
ctx.scale(-1, 1)
ctx.rotate(-np.pi)
txt = "%.0f"%i
ctx.fillUtf8Text(g.BLPoint(-0.02-len(txt)*0.06, .04), font, txt)
ctx.rotate(np.pi)
ctx.scale(-1, 1)
ctx.translate(-p)
ctx.transform(self.inch2data)
ctx.restore()
def draw_artists(self, ctx):
ctx.save()
ctx.clipToRect(self.x0, self.y0, self.w, self.h)
ctx.transform(self.inch2data)
for a in self.artists:
a.draw(ctx)
ctx.restore()
@attr.s(auto_attribs=True)
class Canvas(QWidget):
qtImage: QImage = attr.attrib(factory=QImage)
blImage: g.BLImage = attr.attrib(factory=g.BLImage)
t0: float = attr.Factory(time.time)
xlim: List[float] = [-10, 10]
ylim: List[float] = [10, -10]
dragging: bool = False
dpi: float = 72
def __attrs_post_init__(self):
super().__init__()
self.make_axis()
def dt(self):
return time.time() - self.t0
def generate_transforms(self, w, h):
p = g.BLPoint(w, h)
p_in = p / self.dpi
self.pixel2inch = g.BLMatrix2D.makeScaling(self.dpi)
self.pixel2inch.scale(1, -1)
self.pixel2inch.translate(0, -p_in.y)
self.inch2pixel = g.BLMatrix2D(self.pixel2inch)
self.inch2pixel.invert()
@property
def size(self):
w, h = self.width(), self.height()
return g.BLPoint((w / self.dpi), (h / self.dpi))
def make_axis(self):
print(self.size)
self.axis = Axis(0.14, 0.1, self.size.x - 1, self.size.y - 1)
def resizeEvent(self, ev):
w, h = self.width(), self.height()
self.dpi = self.logicalDpiX()
if (w == self.qtImage.width() and h == self.qtImage.height()):
return
if (w == 0) or (h == 0):
return
self.qtImage = QImage(w, h, QImage.Format_ARGB32_Premultiplied)
self.blImage.createFromData(w, h, g.BL_FORMAT_PRGB32,
c_void_p(int(self.qtImage.bits())),
self.qtImage.bytesPerLine())
self.generate_transforms(w, h)
self.axis.set_pos(0.18, 0.18, self.size.x - 0.25, self.size.y - 0.25)
def paintEvent(self, ev):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing, False)
self.bl_paint()
painter.drawImage(0, 0, self.qtImage)
def mousePressEvent(self, ev: QMouseEvent):
if ev.button() == Qt.MidButton:
ev.accept()
self.dragging = True
self.start = self.inch2pixel.mapPoint(ev.x(), ev.y())
self.setMouseTracking(True)
def mouseReleaseEvent(self, ev: QMouseEvent):
if (ev.button() == Qt.MidButton):
ev.accept()
self.dragging = False
self.setMouseTracking(False)
def mouseMoveEvent(self, ev: QMouseEvent):
pos = self.inch2pixel.mapPoint(ev.x(), ev.y())
if self.dragging:
dx = pos - self.start
dxd = self.axis.data2inch.mapVector(dx)
self.axis.set_viewlim(self.axis.view_lim - dxd)
self.start = pos
self.bl_paint()
def bl_paint(self):
ctx = g.BLContext()
ctx.begin(self.blImage)
ctx.setFillStyle(g.BLRgba32(0xFFAAAAAA))
ctx.fillAll()
ctx.setCompOp(g.BL_COMP_OP_SRC_COPY )
ctx.setStrokeTransformOrder(g.BL_STROKE_TRANSFORM_ORDER_BEFORE)
ctx.transform(self.pixel2inch)
self.axis.draw(ctx)
ctx.end()
c = Canvas()
x = np.linspace(-10, 10, 100)
y = np.sin(x)
c.show()
t = QTimer()
t.timeout.connect(c.repaint)
fn = []
N = 30
r = np.random.randint(0, len(colors), size=N)
for i in range(N):
l = c.axis.add_line(x, y, list(colors)[i], 2)
c.frames = 0
c.last_t = c.dt()
def update_xy(l=l, i=i):
c.frames += 1
if c.frames % 60 == 0:
t = time.time()
c.setWindowTitle(f"{60 / (t-c.last_t)} fps")
c.last_t = t
for i in range(N):
c.axis.artists[i].y = np.sin(x+(i+1)*c.dt())*np.sin(x/3+5*c.dt()) + i/5.11
t.timeout.connect(update_xy)
t.start()
app.exec()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment