Created
March 30, 2020 23:09
-
-
Save Tillsten/c5467e5cc027a70f2f11cb695b597f27 to your computer and use it in GitHub Desktop.
blend2d pyplotter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# %%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