Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets

Add a timed frame callback to DrawingWidget

Changed files
+67 -13
drawSvg
+35 -6
drawSvg/widgets/drawing_javascript.py
···
this.model.on('change:_image', this.image_changed, this);
this.model.on('change:_mousemove_blocked', this.block_changed,
this);
+
this.model.on('change:frame_delay', this.delay_changed,
+
this);
+
this.model.on('change:_frame_blocked', this.delay_changed,
+
this);
+
this.model.on('change:disable', this.delay_changed,
+
this);
+
this.delay_changed();
},
image_changed: function() {
this.container.innerHTML = this.model.get('_image');
···
this.register_events();
},
last_move: null,
+
last_mousemove_blocked: null,
+
last_timer: null,
block_changed: function() {
var widget = this;
window.setTimeout(function() {
-
if (!widget.model.get('_mousemove_blocked') && widget.last_move) {
+
if (widget.model.get('_mousemove_blocked')
+
!= widget.last_mousemove_blocked && widget.last_move) {
widget.send_mouse_event('mousemove', widget.last_move);
}
}, 0);
···
this.last_move = null;
if (this.model.get('disable')) {
return;
-
}
-
if (this.model.get('throttle')) {
-
this.model.set('_mousemove_blocked', true);
-
this.model.save_changes();
}
this.cursor_point.x = e.clientX;
···
relatedTargetId: e.relatedTarget ? e.relatedTarget.id : null,
});
},
+
delay_changed: function() {
+
var widget = this;
+
window.clearTimeout(widget.last_timer);
+
if (widget.model.get('disable')) {
+
return;
+
}
+
var delay = widget.model.get('frame_delay');
+
if (delay > 0) {
+
widget.last_timer = window.setTimeout(function() {
+
widget.send_timed_event('timed');
+
}, delay);
+
}
+
},
+
send_timed_event: function(name) {
+
if (this.model.get('disable')) {
+
return;
+
}
+
+
this.send({
+
name: name,
+
});
+
},
register_events: function() {
var widget = this;
this.svg_view.addEventListener('mousedown', function(e) {
···
});
this.svg_view.addEventListener('mousemove', function(e) {
e.preventDefault();
-
if (widget.model.get('_mousemove_blocked')) {
+
if (widget.model.get('_mousemove_blocked')
+
== widget.last_mousemove_blocked) {
widget.last_move = e;
} else {
widget.send_mouse_event('mousemove', e);
+32 -7
drawSvg/widgets/drawing_widget.py
···
from ipywidgets import widgets
-
from traitlets import Unicode, Bool
+
from traitlets import Unicode, Bool, Int
# Register front end javascript
···
_view_module = Unicode('drawingview').tag(sync=True)
_view_module_version = Unicode('0.1.0').tag(sync=True)
_image = Unicode().tag(sync=True)
-
_mousemove_blocked = Bool(False).tag(sync=True)
+
_mousemove_blocked = Int(0).tag(sync=True)
+
_frame_blocked = Int(0).tag(sync=True)
throttle = Bool(True).tag(sync=True)
disable = Bool(False).tag(sync=True)
+
frame_delay = Int(-1).tag(sync=True)
-
def __init__(self, drawing, throttle=True, disable=False):
+
def __init__(self, drawing, throttle=True, disable=False, frame_delay=-1):
'''
DrawingWidget is an interactive Jupyter notebook widget. It works
similarly to displaying a Drawing as a cell output but DrawingWidget
···
throttle: If True, limit the rate of mousemove events. For drawings
with many elements, this will significantly reduce lag.
disable: While True, mouse events will be disabled.
+
frame_delay: If greater than or equal to zero, a timed callback will
+
occur frame_delay milliseconds after the previous drawing
+
update.
'''
super().__init__()
self.throttle = throttle
self.disable = disable
+
self.frame_delay = frame_delay
self.drawing = drawing
self.mousedown_callbacks = []
self.mousemove_callbacks = []
self.mouseup_callbacks = []
+
self.timed_callbacks = []
self.on_msg(self._receive_msg)
···
def _receive_msg(self, _, content, buffers):
if not isinstance(content, dict):
return
+
name = content.get('name')
callbacks = {
'mousedown': self.mousedown_callbacks,
'mousemove': self.mousemove_callbacks,
'mouseup': self.mouseup_callbacks,
-
}.get(content.get('name'), ())
+
'timed': self.timed_callbacks,
+
}.get(name, ())
try:
if callbacks:
-
self._call_handlers(callbacks, content.get('x'),
-
content.get('y'), content)
+
if name == 'timed':
+
self._call_handlers(callbacks, content)
+
else:
+
self._call_handlers(callbacks, content.get('x'),
+
content.get('y'), content)
finally:
-
self._mousemove_blocked = False
+
if name == 'timed':
+
self._frame_blocked += 1
+
else:
+
self._mousemove_blocked += 1
def mousedown(self, handler, remove=False):
···
'''
self._register_handler(
self.mouseup_callbacks, handler, remove=remove)
+
+
def timed(self, handler, remove=False):
+
'''
+
Register (or unregister) a handler for the timed event.
+
+
Arguments:
+
remove: If True, unregister, otherwise register.
+
'''
+
self._register_handler(
+
self.timed_callbacks, handler, remove=remove)
def _register_handler(self, callback_list, handler, remove=False):
if remove: