Geogebra Substitutes: Paper.js

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()
)
1 Like

Cool example!

The import troubles appear to be a consequence of paper.js somehow not supporting ES6 module syntax properly: Feature Request: Support es6 modules · Issue #1481 · paperjs/paper.js · GitHub