UNPKG

js-2dmath

Version:

Fast 2d geometry math: Vector2, Rectangle, Circle, Matrix2x3 (2D transformation), Circle, BoundingBox, Line2, Segment2, Intersections, Distances, Transitions (animation/tween), Random numbers, Noise

663 lines (555 loc) 16 kB
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" ></script> <title>js-2dmath: Beizer solve</title> <script src="../dist/js-2dmath-browser.min.js" type="text/javascript" ></script> <!-- <script src="../dist/js-2dmath-browser.min.js" type="text/javascript" ></script> --> </head> <body> <h1>js-2dmath: Beizer solve</h1> <canvas id="canvas" width="640" height="480" style="border: 1px solid red;"></canvas> </body> </html> <script> var ctx = document.getElementById("canvas").getContext("2d"); require("js-2dmath").globalize(window); // modified work found at http://www.victoriakirst.com/beziertool/ // I try to comment all modifications // Bezier Tool Canvas Commands Generator // // history: // - 04/03/2011: born! // // @author Victoria Kirst /////////////////////////////////////////////////////////////////////////////// // Globals /////////////////////////////////////////////////////////////////////////////// var gCanvas; var gCtx; var gBackCanvas; var gBackCtx; var gBezierPath; var Mode = { kAdding : {value: 0, name: "Adding"}, kSelecting : {value: 1, name: "Selecting"}, kDragging: {value: 2, name: "Dragging"}, kRemoving : {value: 3, name: "Removing"}, }; var gState; var gBackgroundImg; var WIDTH; var HEIGHT; /////////////////////////////////////////////////////////////////////////////// // Functions /////////////////////////////////////////////////////////////////////////////// // Main window.onload = function() { gCanvas = document.getElementById('canvas'); // modified gCtx = gCanvas.getContext('2d'); HEIGHT = gCanvas.height; WIDTH = gCanvas.width; gBackCanvas = document.createElement('canvas'); gBackCanvas.height = HEIGHT; gBackCanvas.width = WIDTH; gBackCtx = gBackCanvas.getContext('2d'); //gState = Mode.kAdding; // edit gState = Mode.kSelecting gCanvas.addEventListener("mousedown", handleDown, false); gCanvas.addEventListener("mouseup", handleUp, false); /* removed not needed! var selectButton = document.getElementById('selectMode'); selectButton.addEventListener("click", function() { gState = Mode.kSelecting; }, false); var addButton = document.getElementById('addMode'); addButton.addEventListener("click", function() { gState = Mode.kAdding; }, false); var removeButton = document.getElementById('removeMode'); removeButton.addEventListener("click", function() { gState = Mode.kRemoving; }, false); var lockButton = document.getElementById('lockControl'); lockButton.addEventListener("click", function() { ControlPoint.prototype.syncNeighbor = lockButton.checked; }, false); var clearButton = document.getElementById('clear'); clearButton.addEventListener('click', function() { var doDelete = confirm('r u sure u want to delete all'); if (doDelete) { gBezierPath = null; gBackCtx.clearRect(0, 0, WIDTH, HEIGHT); gCtx.clearRect(0, 0, WIDTH, HEIGHT); } }, false); var setSrcButton = document.getElementById('addImgSrc'); setSrcButton.addEventListener('click', function() { var input = document.getElementById('imageSrc'); gBackgroundImg = document.createElement('img'); // No image if invalid path gBackgroundImg.onerror = function() { gBackgroundImg = null; }; gBackgroundImg.src = input.value; render(); input.value = ''; }, false); */ gBezierPath = new BezierPath(new Point(50, 50)); gBezierPath.addPoint(new Point(250, 250)) render(); }; // Modified from http://diveintohtml5.org/examples/halma.js function getMousePosition(e) { var x; var y; if (e.pageX != undefined && e.pageY != undefined) { x = e.pageX; y = e.pageY; } else { x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; } x -= gCanvas.offsetLeft; y -= gCanvas.offsetTop; return new Point(x, y); } function handleDown(e) { var pos = getMousePosition(e); switch(gState) { case Mode.kAdding: handleDownAdd(pos); break; case Mode.kSelecting: handleDownSelect(pos); break; case Mode.kRemoving: handleDownRemove(pos); break; } } function handleDownAdd(pos) { if (!gBezierPath) gBezierPath = new BezierPath(pos); else { // If this was probably a selection, change to // select/drag mode if (handleDownSelect(pos)) return; gBezierPath.addPoint(pos); } render(); } // Return true/false if dragging mode function handleDownSelect(pos) { if (!gBezierPath) return false; var selected = gBezierPath.selectPoint(pos); if (selected) { gState = Mode.kDragging; gCanvas.addEventListener("mousemove", updateSelected, false); return true; } return false; } function handleDownRemove(pos) { if (!gBezierPath) return; var deleted = gBezierPath.deletePoint(pos); if (deleted) render(); } function updateSelected(e) { var pos = getMousePosition(e); gBezierPath.updateSelected(pos); render(); } function handleUp(e) { if (gState == Mode.kDragging) { gCanvas.removeEventListener("mousemove", updateSelected, false); gBezierPath.clearSelected(); gState = Mode.kSelecting; } } function render() { gBackCtx.clearRect(0, 0, WIDTH, HEIGHT); gCtx.clearRect(0, 0, WIDTH, HEIGHT); if (gBackgroundImg) gBackCtx.drawImage(gBackgroundImg, 0, 0); if (gBezierPath) { gBezierPath.draw(gBackCtx); var codeBox = document.getElementById('putJS'); //edit //console.log(gBezierPath.toJSString()); // add var current = gBezierPath.head; var bz = Beizer.cubic( current.pt.x(), current.pt.y(), current.next.ctrlPt1.x(), current.next.ctrlPt1.y(), current.next.ctrlPt2.x(), current.next.ctrlPt2.y(), current.next.pt.x(), current.next.pt.y() ); //console.log(bz); Draw.circle(gCtx, Circle.fromVec2(Beizer.solve([0, 0], bz, solve_step), 5), "red"); // end - add var plist = Beizer.getPoints(bz, 15); // hacky :) var tplist = Polygon.translate([], plist, [0, 200]); Draw.polygon(ctx, tplist, "blue"); } gCtx.drawImage(gBackCanvas, 0, 0); } // add var solve_step = 0; setInterval(function() { solve_step+=0.05; if (solve_step > 1) { solve_step = 0; } render(); }, 50); //end-add /////////////////////////////////////////////////////////////////////////////// // Classes /////////////////////////////////////////////////////////////////////////////// function Point(newX, newY) { var my = this; var xVal = newX; var yVal = newY; var RADIUS = 3; var SELECT_RADIUS = RADIUS + 2; this.x = function () { return xVal; } this.y = function () { return yVal; } this.set = function(x, y) { xVal = x; yVal = y; }; this.drawSquare = function(ctx) { ctx.fillRect(xVal - RADIUS, yVal - RADIUS, RADIUS * 2, RADIUS * 2); }; this.computeSlope = function(pt) { return (pt.y() - yVal) / (pt.x() - xVal); }; this.contains = function(pt) { var xInRange = pt.x() >= xVal - SELECT_RADIUS && pt.x() <= xVal + SELECT_RADIUS; var yInRange = pt.y() >= yVal - SELECT_RADIUS && pt.y() <= yVal + SELECT_RADIUS; return xInRange && yInRange; }; this.offsetFrom = function(pt) { return { xDelta : pt.x() - xVal, yDelta : pt.y() - yVal, }; }; this.translate = function(xDelta, yDelta) { xVal += xDelta; yVal += yDelta; }; } function ControlPoint(angle, magnitude, owner, isFirst) { var my = this; var _angle = angle; var _magnitude = magnitude; // Pointer to the line segment to which this belongs. var _owner = owner; var _isFirst = isFirst; this.setAngle = function(deg) { // don't update neighbor in risk of infinite loop! // TODO fixme fragile if (_angle != deg) _angle = deg; } this.origin = function origin() { var line = null; if (_isFirst) line = _owner.prev; else line = _owner; if (line) return new Point(line.pt.x(), line.pt.y()); return null; } // Returns the Point at which the knob is located. this.asPoint = function() { return new Point(my.x(), my.y()); }; this.x = function () { return my.origin().x() + my.xDelta(); } this.y = function () { return my.origin().y() + my.yDelta(); } this.xDelta = function() { return _magnitude * Math.cos(_angle); } this.yDelta = function() { return _magnitude * Math.sin(_angle); } function computeMagnitudeAngleFromOffset(xDelta, yDelta) { _magnitude = Math.sqrt(Math.pow(xDelta, 2) + Math.pow(yDelta, 2)); var tryAngle = Math.atan(yDelta /xDelta); if (!isNaN(tryAngle)) { _angle = tryAngle; if (xDelta < 0) _angle += Math.PI } } this.translate = function(xDelta, yDelta) { var newLoc = my.asPoint(); newLoc.translate(xDelta, yDelta); var dist = my.origin().offsetFrom(newLoc); computeMagnitudeAngleFromOffset(dist.xDelta, dist.yDelta); if (my.__proto__.syncNeighbor) updateNeighbor(); }; function updateNeighbor() { var neighbor = null; if (_isFirst && _owner.prev) neighbor = _owner.prev.ctrlPt2; else if (!_isFirst && _owner.next) neighbor = _owner.next.ctrlPt1; if (neighbor) neighbor.setAngle(_angle + Math.PI); } this.contains = function(pt) { return my.asPoint().contains(pt); } this.offsetFrom = function(pt) { return my.asPoint().offsetFrom(pt); } this.draw = function(ctx) { ctx.save(); ctx.fillStyle = 'gray'; ctx.strokeStyle = 'gray'; ctx.beginPath(); var startPt = my.origin(); var endPt = my.asPoint(); ctx.moveTo(startPt.x(), startPt.y()); ctx.lineTo(endPt.x(), endPt.y()); ctx.stroke(); endPt.drawSquare(ctx); ctx.restore(); } // When Constructed if (my.__proto__.syncNeighbor) updateNeighbor(); } // Static variable dictacting if neighbors must be kept in sync. ControlPoint.prototype.syncNeighbor = true; function LineSegment(pt, prev) { var my = this; // Path point. this.pt; // Control point 1. this.ctrlPt1; // Control point 2. this.ctrlPt2; // Next LineSegment in path this.next; // Previous LineSegment in path this.prev; // Specific point on the LineSegment that is selected. this.selectedPoint; init(); this.draw = function(ctx) { my.pt.drawSquare(ctx); // Draw control points if we have them if (my.ctrlPt1) my.ctrlPt1.draw(ctx); if (my.ctrlPt2) my.ctrlPt2.draw(ctx); // If there are at least two points, draw curve. if (my.prev) drawCurve(ctx, my.prev.pt, my.pt, my.ctrlPt1, my.ctrlPt2); } this.toJSString = function() { if (!my.prev) return ' ctx.moveTo(' + Math.round(my.pt.x()) + ' + xoff, ' + Math.round(my.pt.y()) + ' + yoff);'; else { var ctrlPt1x = 0; var ctrlPt1y = 0; var ctrlPt2x = 0; var ctlrPt2y = 0; var x = 0; var y = 0; if (my.ctrlPt1) { ctrlPt1x = Math.round(my.ctrlPt1.x()); ctrlPt1y = Math.round(my.ctrlPt1.y()); } if (my.ctrlPt2) { ctrlPt2x = Math.round(my.ctrlPt2.x()); ctrlPt2y = Math.round(my.ctrlPt2.y()); } if (my.pt) { x = Math.round(my.pt.x()); y = Math.round(my.pt.y()); } return ' ctx.bezierCurveTo(' + ctrlPt1x + ' + xoff, ' + ctrlPt1y + ' + yoff, ' + ctrlPt2x + ' + xoff, ' + ctrlPt2y + ' + yoff, ' + x + ' + xoff, ' + y + ' + yoff);'; } } this.findInLineSegment = function(pos) { if (my.pathPointIntersects(pos)) { my.selectedPoint = my.pt; return true; } else if (my.ctrlPt1 && my.ctrlPt1.contains(pos)) { my.selectedPoint = my.ctrlPt1; return true; } else if (my.ctrlPt2 && my.ctrlPt2.contains(pos)) { my.selectedPoint = my.ctrlPt2; return true; } return false; } this.pathPointIntersects = function(pos) { return my.pt && my.pt.contains(pos); } this.moveTo = function(pos) { var dist = my.selectedPoint.offsetFrom(pos); my.selectedPoint.translate(dist.xDelta, dist.yDelta); }; function drawCurve(ctx, startPt, endPt, ctrlPt1, ctrlPt2) { ctx.save(); ctx.fillStyle = 'black'; ctx.strokeStyle = 'black'; ctx.beginPath(); ctx.moveTo(startPt.x(), startPt.y()); ctx.bezierCurveTo(ctrlPt1.x(), ctrlPt1.y(), ctrlPt2.x(), ctrlPt2.y(), endPt.x(), endPt.y()); ctx.stroke(); ctx.restore(); } function init() { my.pt = pt; my.prev = prev; if (my.prev) { // Make initial line straight and with controls of length 15. var slope = my.pt.computeSlope(my.prev.pt); var angle = Math.atan(slope); if (my.prev.pt.x() > my.pt.x()) angle *= -1; my.ctrlPt1 = new ControlPoint(angle + Math.PI, 15, my, true); my.ctrlPt2 = new ControlPoint(angle, 15, my, false); } }; } function BezierPath(startPoint) { var my = this; // Beginning of BezierPath linked list. this.head = null; // End of BezierPath linked list this.tail = null; // Reference to selected LineSegment var selectedSegment; this.addPoint = function(pt) { var newPt = new LineSegment(pt, my.tail); if (my.tail == null) { my.tail = newPt; my.head = newPt; } else { my.tail.next = newPt; my.tail = my.tail.next; } return newPt; }; // Must call after add point, since init uses // addPoint // TODO: this is a little gross init(); this.draw = function(ctx) { if (my.head == null) return; var current = my.head; while (current != null) { current.draw(ctx); current = current.next; } }; // returns true if point selected this.selectPoint = function(pos) { var current = my.head; while (current != null) { if (current.findInLineSegment(pos)) { selectedSegment = current; return true; } current = current.next; } return false; } // returns true if point deleted this.deletePoint = function(pos) { var current = my.head; while (current != null) { if (current.pathPointIntersects(pos)) { var toDelete = current; var leftNeighbor = current.prev; var rightNeighbor = current.next; // Middle case if (leftNeighbor && rightNeighbor) { leftNeighbor.next = rightNeighbor; rightNeighbor.prev = leftNeighbor } // HEAD CASE else if (!leftNeighbor) { my.head = rightNeighbor; if (my.head) { rightNeighbor.ctrlPt1 = null; rightNeighbor.ctrlPt2 = null; my.head.prev = null; } else my.tail = null; } // TAIL CASE else if (!rightNeighbor) { my.tail = leftNeighbor; if (my.tail) my.tail.next = null; else my.head = null; } return true; } current = current.next; } return false; } this.clearSelected = function() { selectedSegment = null; } this.updateSelected = function(pos) { selectedSegment.moveTo(pos); } this.toJSString = function() { var myString = ['function drawShape(ctx, xoff, yoff) {', ' ctx.beginPath();', ]; var current = my.head; while (current != null) { myString.push(current.toJSString()); current = current.next; } myString.push(' ctx.stroke();'); myString.push('}'); return myString.join('\n'); } function init() { my.addPoint(startPoint); }; } </script>