This solution I really like: once the basic drawing routines are set up,
it’s really simple to handle!
The two main problems are:
- getting the canvas set up correctly so the conversions from world to screen coordinates work as expected
- I still can’t get the imports to work: I included paper.js in an html cell:
import param
import panel as pn
pn.extension()
%%html
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.15/paper-full.min.js"></script>
class VectorDecompositionWithPaperJS(pn.custom.JSComponent):
xrange = param.Range(default=(-2, 10))
yrange = param.Range(default=(-2, 10))
width = param.Integer(default=300, doc="Width of the canvas")
height = param.Integer(default=300, doc="Height of the canvas")
s1 = param.List(default=[3, 1], doc="First basis vector (dx, dy).")
s2 = param.List(default=[-1, 2], doc="Second basis vector (dx, dy).")
b = param.List(default=[7, 4], doc="Vector b (dx, dy).")
fixed = param.Boolean(default=True, doc="Disallow moving endpoints of basis vectors.")
vec_name = param.String(default="e", doc="Base name for the basis vectors (e.g., 's' or 'e').")
_esm = """
export function render({ model, el }) {
let width = model.width;
let height = model.height;
let xrange = model.xrange;
let yrange = model.yrange;
// Initialize local vectors from model variables
let s_1 = [...model.s1];
let s_2 = [...model.s2];
const b = model.b
const fixed = model.fixed;
const vec_name = model.vec_name;
// ----------------------------------------------------------------------------------
const canvasId = 'canvas_' + Math.random().toString(36).substr(2, 9);
let canvas = document.getElementById(canvasId);
if (!canvas) {
canvas = document.createElement('canvas');
canvas.id = canvasId;
el.appendChild(canvas);
}
const ratio = window.devicePixelRatio || 1;
canvas.style.width = width + "px";
canvas.style.height = height + "px";
canvas.width = width * ratio;
canvas.height = height * ratio;
canvas.style.border = "1px solid black";
const paperScope = new paper.PaperScope();
paperScope.setup(canvas);
paperScope.view.viewSize = new paperScope.Size(width, height);
// ----------------------------------------------------------------------------------
const worldToScreen = (point) => {
const x = (point.x - xrange[0]) / (xrange[1] - xrange[0]) * width;
const y = height - (point.y - yrange[0]) / (yrange[1] - yrange[0]) * height;
return { x, y };
};
// ----------------------------------------------------------------------------------
const screenToWorld = (point) => {
const x = xrange[0] + (point.x / width) * (xrange[1] - xrange[0]);
const y = yrange[0] + ((height - point.y) / height) * (yrange[1] - yrange[0]);
return { x, y };
};
// ----------------------------------------------------------------------------------
const drawLineSegment = (start, end, color, dotted = false) => {
const startPoint = worldToScreen(start);
const endPoint = worldToScreen(end);
new paperScope.Path.Line({
from: new paperScope.Point(startPoint.x, startPoint.y),
to: new paperScope.Point(endPoint.x, endPoint.y),
strokeColor: color,
strokeWidth: 2,
dashArray: dotted ? [5, 5] : null, // Apply dash pattern if dotted is true
});
};
// ----------------------------------------------------------------------------------
function drawArrow(start, end, color, label) {
const startPoint = worldToScreen(start);
const endPoint = worldToScreen(end);
new paperScope.Path.Line({
from: new paperScope.Point(startPoint.x, startPoint.y),
to: new paperScope.Point(endPoint.x, endPoint.y),
strokeColor: color,
strokeWidth: 2,
});
const arrowSize = 10;
const direction = new paperScope.Point(endPoint.x - startPoint.x, endPoint.y - startPoint.y).normalize();
new paperScope.Path({
segments: [
new paperScope.Point(endPoint.x, endPoint.y),
new paperScope.Point(
endPoint.x - direction.x * arrowSize + direction.y * arrowSize / 2,
endPoint.y - direction.y * arrowSize - direction.x * arrowSize / 2
),
new paperScope.Point(
endPoint.x - direction.x * arrowSize - direction.y * arrowSize / 2,
endPoint.y - direction.y * arrowSize + direction.x * arrowSize / 2
)
],
closed: true,
fillColor: color,
});
if (label) {
new paperScope.PointText({
point: new paperScope.Point(endPoint.x + 10, endPoint.y - 10),
content: label,
fillColor: 'black',
fontSize: 12,
justification: 'center',
});
}
}
// ----------------------------------------------------------------------------------
function drawGrid() {
const maxGridSize = 10;
// Draw lines along multiples of s_1
for (let i = -maxGridSize; i <= maxGridSize; i++) {
const start = { x: i * s_1[0] - maxGridSize * s_2[0], y: i * s_1[1] - maxGridSize * s_2[1] };
const end = { x: i * s_1[0] + maxGridSize * s_2[0], y: i * s_1[1] + maxGridSize * s_2[1] };
drawLineSegment(start, end, 'lightgray');
}
// Draw lines along multiples of s_2
for (let j = -maxGridSize; j <= maxGridSize; j++) {
const start = { x: j * s_2[0] - maxGridSize * s_1[0], y: j * s_2[1] - maxGridSize * s_1[1] };
const end = { x: j * s_2[0] + maxGridSize * s_1[0], y: j * s_2[1] + maxGridSize * s_1[1] };
drawLineSegment(start, end, 'lightgray');
}
}
// ----------------------------------------------------------------------------------
function addControlPoint(paperScope, vector, label, redrawCallback) {
const endpointScreen = worldToScreen({ x: vector[0], y: vector[1] });
const controlPoint = new paperScope.Path.Circle({
center: new paperScope.Point(endpointScreen.x, endpointScreen.y),
radius: 6,
fillColor: '#3B3651',
strokeColor: 'black',
opacity: 0.3,
strokeWidth: 2,
});
controlPoint.onMouseDrag = function (event) {
controlPoint.position = event.point;
const newWorldPoint = screenToWorld({ x: event.point.x, y: event.point.y });
vector[0] = newWorldPoint.x;
vector[1] = newWorldPoint.y;
console.log(`Updated ${label} to:`, vector);
redrawCallback();
};
return controlPoint;
}
// ----------------------------------------------------------------------------------
function decomposeVector(b, s1, s2) {
const det = s1[0] * s2[1] - s1[1] * s2[0];
if (det === 0) {
console.error("Vectors s1 and s2 are linearly dependent.");
return { alpha1: 0, alpha2: 0 };
}
const alpha1 = (b[0] * s2[1] - b[1] * s2[0]) / det;
const alpha2 = (b[1] * s1[0] - b[0] * s1[1]) / det;
return { alpha1, alpha2 };
}
// ----------------------------------------------------------------------------------
function redraw() {
paperScope.project.clear();
const { alpha1, alpha2 } = decomposeVector(b, s_1, s_2);
const vectorAlpha1S1 = { x: alpha1 * s_1[0], y: alpha1 * s_1[1] };
const vectorAlpha2S2 = { x: alpha2 * s_2[0], y: alpha2 * s_2[1] };
drawGrid();
drawArrow({ x: 0, y: 0 }, vectorAlpha1S1, 'orange', null);
drawArrow({ x: 0, y: 0 }, vectorAlpha2S2, 'orange', null);
drawLineSegment(vectorAlpha1S1, { x: b[0], y: b[1] }, 'orange', true);
drawLineSegment(vectorAlpha2S2, { x: b[0], y: b[1] }, 'orange', true);
drawArrow({ x: 0, y: 0 }, { x: s_1[0], y: s_1[1] }, '#3B3651', `${vec_name}_1`);
drawArrow({ x: 0, y: 0 }, { x: s_2[0], y: s_2[1] }, '#3B3651', `${vec_name}_2`);
drawArrow({ x: 0, y: 0 }, { x: b[0], y: b[1] }, 'blue', 'b');
if (!fixed) {
addControlPoint(paperScope, s_1, `${vec_name}_1`, redraw);
addControlPoint(paperScope, s_2, `${vec_name}_2`, redraw);
}
}
// Initial render
redraw();
paperScope.view.draw();
}
"""
# Instantiate and display the component
ex_e = VectorDecompositionWithPaperJS( s1=[1,0], s2=[0,1] )
ex_s = VectorDecompositionWithPaperJS(vec_name="s", fixed=False)
pn.Column( "# Vector Decompositions",
pn.Row( ex_e, pn.Spacer( width=20), ex_s,
width=foo_t.width+10, height=foo_t.height+10).servable()
)