Canvas: living geometrie
- draw a red and a blue square and connect corners of blue square to corners of canvas
- use pointerevents to drag blue square around (hold down mouse key )
- should work for mouse or touchscreen
- event =
- start =
- move = empty
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>