|
from .labwidget import Widget, Property, minify |
|
import html |
|
|
|
class PaintWidget(Widget): |
|
def __init__(self, |
|
width=256, height=256, |
|
image='', mask='', brushsize=10.0, oneshot=False, disabled=False, |
|
vanishing=True, opacity=0.7, |
|
**kwargs): |
|
super().__init__(**kwargs) |
|
self.mask = Property(mask) |
|
self.image = Property(image) |
|
self.vanishing = Property(vanishing) |
|
self.brushsize = Property(brushsize) |
|
self.erase = Property(False) |
|
self.oneshot = Property(oneshot) |
|
self.disabled = Property(disabled) |
|
self.width = Property(width) |
|
self.height = Property(height) |
|
self.opacity = Property(opacity) |
|
self.startpos = Property(None) |
|
self.dragpos = Property(None) |
|
self.dragging = Property(False) |
|
|
|
def widget_js(self): |
|
return minify(f''' |
|
{PAINT_WIDGET_JS} |
|
var pw = new PaintWidget(element, model); |
|
''') |
|
def widget_html(self): |
|
v = self.view_id() |
|
return minify(f''' |
|
<style> |
|
#{v} {{ position: relative; display: inline-block; }} |
|
#{v} .paintmask {{ |
|
position: absolute; top:0; left: 0; z-index: 1; |
|
opacity: { self.opacity } }} |
|
#{v} .paintmask.vanishing {{ |
|
opacity: 0; transition: opacity .1s ease-in-out; }} |
|
#{v} .paintmask.vanishing:hover {{ opacity: { self.opacity }; }} |
|
</style> |
|
<div id="{v}"></div> |
|
''') |
|
|
|
PAINT_WIDGET_JS = """ |
|
class PaintWidget { |
|
constructor(el, model) { |
|
this.el = el; |
|
this.model = model; |
|
this.size_changed(); |
|
this.model.on('mask', this.mask_changed.bind(this)); |
|
this.model.on('image', this.image_changed.bind(this)); |
|
this.model.on('vanishing', this.mask_changed.bind(this)); |
|
this.model.on('width', this.size_changed.bind(this)); |
|
this.model.on('height', this.size_changed.bind(this)); |
|
} |
|
mouse_stroke(first_event) { |
|
var self = this; |
|
if (first_event.which === 3 || first_event.button === 2) { |
|
first_event.preventDefault(); |
|
self.mask_canvas.style.pointerEvents = 'none'; |
|
setTimeout(() => { |
|
self.mask_canvas.style.pointerEvents = 'all'; |
|
}, 3000); |
|
return; |
|
} |
|
if (self.model.get('disabled')) { return; } |
|
if (self.model.get('oneshot')) { |
|
var canvas = self.mask_canvas; |
|
var ctx = canvas.getContext('2d'); |
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
} |
|
function track_mouse(evt) { |
|
if (evt.type == 'keydown' || self.model.get('disabled')) { |
|
if (self.model.get('disabled') || evt.key === "Escape") { |
|
window.removeEventListener('mousemove', track_mouse); |
|
window.removeEventListener('mouseup', track_mouse); |
|
window.removeEventListener('keydown', track_mouse, true); |
|
if (self.model.get('dragging')) { |
|
self.model.set('dragging', false); |
|
} |
|
self.mask_changed(); |
|
} |
|
return; |
|
} |
|
if (evt.type == 'mouseup' || |
|
(typeof evt.buttons != 'undefined' && evt.buttons == 0)) { |
|
window.removeEventListener('mousemove', track_mouse); |
|
window.removeEventListener('mouseup', track_mouse); |
|
window.removeEventListener('keydown', track_mouse, true); |
|
self.model.set('dragging', false); |
|
self.model.set('mask', self.mask_canvas.toDataURL()); |
|
return; |
|
} |
|
var p = self.cursor_position(evt); |
|
var d = self.model.get('dragging'); |
|
var e = self.model.get('erase') ^ (evt.ctrlKey); |
|
if (!d) { self.model.set('startpos', [p.x, p.y]); } |
|
self.model.set('dragpos', [p.x, p.y]); |
|
if (!d) { self.model.set('dragging', true); } |
|
self.fill_circle(p.x, p.y, |
|
self.model.get('brushsize'), |
|
e); |
|
} |
|
this.mask_canvas.focus(); |
|
window.addEventListener('mousemove', track_mouse); |
|
window.addEventListener('mouseup', track_mouse); |
|
window.addEventListener('keydown', track_mouse, true); |
|
track_mouse(first_event); |
|
} |
|
mask_changed() { |
|
this.mask_canvas.classList.toggle("vanishing", this.model.get('vanishing')); |
|
this.draw_data_url(this.mask_canvas, this.model.get('mask')); |
|
} |
|
image_changed() { |
|
this.image.src = this.model.get('image'); |
|
} |
|
size_changed() { |
|
this.mask_canvas = document.createElement('canvas'); |
|
this.image = document.createElement('img'); |
|
this.mask_canvas.className = "paintmask"; |
|
this.image.className = "paintimage"; |
|
for (var attr of ['width', 'height']) { |
|
this.mask_canvas[attr] = this.model.get(attr); |
|
this.image[attr] = this.model.get(attr); |
|
} |
|
|
|
this.el.innerHTML = ''; |
|
this.el.appendChild(this.image); |
|
this.el.appendChild(this.mask_canvas); |
|
this.mask_canvas.addEventListener('mousedown', |
|
this.mouse_stroke.bind(this)); |
|
this.mask_changed(); |
|
this.image_changed(); |
|
} |
|
|
|
cursor_position(evt) { |
|
const rect = this.mask_canvas.getBoundingClientRect(); |
|
const x = event.clientX - rect.left; |
|
const y = event.clientY - rect.top; |
|
return {x: x, y: y}; |
|
} |
|
|
|
fill_circle(x, y, r, erase, blur) { |
|
var ctx = this.mask_canvas.getContext('2d'); |
|
ctx.save(); |
|
if (blur) { |
|
ctx.filter = 'blur(' + blur + 'px)'; |
|
} |
|
ctx.globalCompositeOperation = ( |
|
erase ? "destination-out" : 'source-over'); |
|
ctx.fillStyle = '#fff'; |
|
ctx.beginPath(); |
|
ctx.arc(x, y, r, 0, 2 * Math.PI); |
|
ctx.fill(); |
|
ctx.restore() |
|
} |
|
|
|
draw_data_url(canvas, durl) { |
|
var ctx = canvas.getContext('2d'); |
|
var img = new Image; |
|
canvas.pendingImg = img; |
|
function imgdone() { |
|
if (canvas.pendingImg == img) { |
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
ctx.drawImage(img, 0, 0); |
|
canvas.pendingImg = null; |
|
} |
|
} |
|
img.addEventListener('load', imgdone); |
|
img.addEventListener('error', imgdone); |
|
img.src = durl; |
|
} |
|
} |
|
""" |
|
|