Canvas: living geometrie

  1. draw a red and a blue square and connect corners of blue square to corners of canvas
  2. use pointerevents to drag blue square around (hold down mouse key )
  3. should work for mouse or touchscreen

Source /home/ch45859/web/wlkl.ch/public_html/inf/js/c03canvasGeo.php

<!DOCTYPE html>
<html>
<head>
  <title> <?php echo basename(__file__, '.php'); ?> </title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=3.0, user-scalable=yes"/>
  <meta name="HandheldFriendly" content="true" />
  <meta name="apple-mobile-web-app-capable" content="YES" /> 
  <style>
      canvas {
        touch-action:none;
        border: 1px solid black;
      }
  </style>
</head>
<body onload="draw();">

<h1>Canvas: living geometrie</h1>
<ol><li>draw a red and a blue square and connect corners of blue square to corners of canvas 
</li><li>use pointerevents to drag blue square around (hold down mouse key ) 
</li><li>should work for mouse or touchscreen
</li></ol>
   <div style="float: left; margin-right: 30px;"> <canvas  id="tutorial" width="150" height="150"></canvas> </div>
<ul><li>event = <span id="ty"></span>
</li><li>start = <span id="st"></span>
</li><li>move = <span id="press">empty</span>
</li></ul>
<h1 style="clear: both;">Source <?php echo __file__; ?> </h1>
<?php highlight_file(__file__) ?>
</body>

<script>
    const can = document.getElementById("tutorial")
        , ctx = can.getContext("2d")
        , dim = {w: 150, h: 150};
    class Line {
        constructor(a, b) { //  x * cos(a) + y * sin(a) + b = 0 rsp. distance from line
            this.a = a;
            this.b = b;
        }
        draw() {
            // document.getElementById("ty").innerHTML = 'draw 0, ' + (- dim.h * this.b / Math.sin(this.a)) + ' to 150,' + (- dim.h * ( Math.cos(this.a) + this.b) / Math.sin(this.a));
            const sa = Math.sin(this.a.v)
                , ca = Math.cos(this.a.v);
            ctx.beginPath();
            if (Math.abs(ca) < 0.7) {
                ctx.moveTo(0, - dim.h * this.b.v / sa);
                ctx.lineTo(dim.w, - dim.h * ( ca + this.b.v) / sa);
            } else {
                ctx.moveTo(- dim.w * this.b.v / ca, 0);
                ctx.lineTo(- dim.w * ( sa + this.b.v) / ca, dim.h);
            }
        ctx.stroke();    
        }

        dist(x,y) { // distance to point x, y
            return x * Math.cos(this.a.v) + y * Math.sin(this.a.v) + this.b.v
        }

        dist2(x,y) { // square of distance to point x, y
            return Math.pow(this.dist(x, y), 2)
        }
    }
    class Point {
        constructor(x, y) { 
            this.x = x;
            this.y = y;
        }

        draw() {
            ctx.beginPath();
            ctx.ellipse(dim.w * this.x.v, dim.h * this.y.v, 8, 8, 0, 0, 2 * Math.PI);
        ctx.fill();    
        }

        dist2(x,y) { // square of distance to point x, y
            return Math.pow(x - this.x, 2) + Math.pow(y - this.y, 2)
        }
    }

    class Geo {
        eles = []
        vars = []
        match = []

        varNew(v) {
            let n = {v: v, g: 0};
            this.vars.push(n)
            return n
        }
        
        constNew(v) {
            return {v: v};
        }

        eleNear(x, y) { // return ele nearest to x,y or null if none near enough
            let pD = .01, r=null;
            for (let i=0; i < this.eles.length; i++) {
                let d = this.eles[i].dist2(x, y)
                if (d < pD ) {
                    r = i
                    pD = d
                }
            }
            document.getElementById("st").innerHTML = 'selected ' + r + ' d2=' + pD.toExponential(2) + ' @ ' + x.toFixed(4) + ', ' + y.toFixed(4);
            return r === null ? null : this.eles[r];
        }

        matchDist2() { // calculate the sum of squares of the distance of each match
            let d2 = 0;
            for (const m of this.match) {
                if (m.length === 2)
                    d2 += m[1].dist2(m[0].x.v, m[0].y.v)
            }
            return d2;        
        }
        gradientCalc() { // calculate the gradient and return the square of its length
            for (const v of this.vars) 
                v.g = 0;
            for (const m of this.match) {
                const d2 = 2 * m[1].dist(m[0].x.v, m[0].y.v)
                    , sa = Math.sin(m[1].a.v)
                    , ca = Math.cos(m[1].a.v)
                ;
                if ('g' in m[0].x)
                    m[0].x.g += d2 * ca
                if ('g' in m[0].y)
                    m[0].y.g += d2 * sa
                if ('g' in m[1].a)
                m[1].a.g += d2 * (- m[0].x.v * sa + m[0].y.v * ca)
                if ('g' in m[1].b)
                    m[1].b.g += d2
            }
            let g2 = 0;
            for (const v of this.vars) 
                g2 += v.g * v.g;
            return g2;        
         }

        gradientMov1(d) {  // move the current coordinates by d * gradient          
            for (const v of this.vars) 
                v.v += d * v.g;
            return this.matchDist2()
        }

        gradientMove() { // move by the gradient, until dist2 is small enough
            const pr = 1/Math.pow(Math.max(dim.w, dim.h), 2) / 5;               
            let d2 = this.matchDist2()
                , t = 'd2 ' + d2.toExponential(2);
            for (let i=0; i < 10 && d2 > pr; i++ ) {
                this.gradientMov1(-d2 / this.gradientCalc())
                d2 = this.matchDist2();
                t += ', ' + d2.toExponential(2);
            }
            document.getElementById("press").innerHTML = t;
        }
    }

    function draw() { // clear canvas and draw all elements
        // document.getElementById("ty").innerHTML = 'drawing';
        ctx.clearRect(0, 0, dim.w, dim.h);
        ctx.strokeStyle = "rgba(0, 0, 200)";
        ctx.fillStyle = "rgba(200, 0, 0)";
        ctx.lineWidth = 3;
        for (const e of geo.eles)
          e.draw();
    }

    function pointerAna(e) { // compute coordinates (normalized to unit square) from a pointer event
        mv.ax = e.offsetX / dim.w;
        mv.ay = e.offsetY / dim.h;
        document.getElementById("ty").innerHTML = e.type + ' ' + ++cnt + ', @ '+ mv.ax.toFixed(4) + ", " + mv.ay.toFixed(4);
    }

    geo = new Geo();
    geo.eles.push(new Line(geo.varNew(Math.PI / 4), geo.varNew(- Math.sqrt(0.5)))
               , new Line(geo.varNew(0), geo.varNew(-.5))
               , new Line(geo.varNew(Math.PI /2), geo.varNew(-.5))
               , new Point(geo.varNew(0.5), geo.varNew(0.5))
               , new Point(geo.constNew(0), geo.constNew(1))
               )
    const e = geo.eles
    geo.match.push([0], [e[4], e[0]], [e[3], e[0]], [e[3], e[1]], [e[3], e[2]])
    var mv = {sx: -1, sy: -1, se: null, ax:0, ay:0}
        , cnt = 0
    ;

    draw();
    can.addEventListener("pointerdown", (e) => { // select blue square, if cursor on it
        pointerAna(e);
        mv.se = geo.eleNear(mv.sx = mv.ax, mv.sy = mv.ay)
    });

    can.addEventListener("pointerup", (e) => { // unselect
        pointerAna(e);
        mv.se = null
        draw();
    });

    can.addEventListener("pointermove", (e) => { // move blue square, if it is selected        
        pointerAna(e);
        if (mv.se === null)
            return;
        geo.match[0] = [new Point(geo.constNew(mv.ax), geo.constNew(mv.ay)), mv.se]
        geo.gradientMove()
        //document.getElementById("press").innerHTML = 'ad2=' + matchDist2();
        draw();
   });

</script>
</html>