├── .gitignore
├── BigInt.js
├── LICENSE
├── README.md
├── calc.js
├── graph.png
├── images
├── button_bg.png
├── derivative.png
├── graph_small.png
├── intersect.png
├── minmax.png
├── pointer.png
├── root.png
├── settings.png
├── trace.png
├── zoombox.png
├── zoomin.png
└── zoomout.png
├── index.html
├── jsgcalc.css
├── jsgcalc.js
└── jsgui.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 | *.pyc
3 | *.pyo
4 | *.project
--------------------------------------------------------------------------------
/BigInt.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/BigInt.js
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2011-2013 Richard Ye
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Graphr, the JavaScript Graphing Calculator
2 |
3 | **Check out the web-based demo at http://www.graphr.org/**
4 |
5 | This is a JavaScript implementation of a graphical calculator, with support for
6 | standard algebraic functions in the Cartesian plane. Features include fast, accurate
7 | graphing and full mouse zoom and pan support. Additionally, there are many graphing
8 | tools available, such as tracing a function, finding a root, finding the derivative, etc.
9 |
10 | Designed for demonstration purposes only - https://www.desmos.com/calculator is a better
11 | calculator for real mathematics.
12 |
--------------------------------------------------------------------------------
/calc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file handles math calculations
3 | */
4 |
5 | //Machine epsilon
6 | function calcEps(){
7 | var temp1, temp2, mchEps;
8 | temp1 = 1.0;
9 | do {
10 | mchEps = temp1;
11 | temp1 /= 2;
12 | temp2 = 1.0 + temp1;
13 | }
14 | while (temp2 > 1.0);
15 | return mchEps;
16 | }
17 |
18 | var Calc = {};
19 |
20 | function calc() {
21 | this.eqcache = new Object;
22 | this.angles = 'radians';
23 | this.loopcounter = 0;
24 | this.eps = calcEps(); //Machine epsilon - the maximum expected floating point error
25 |
26 | /* Basic Math Functions (sin, cos, csc, etc.)
27 | */
28 |
29 | //This will take a number and covert it to radians, based on the current setting
30 | this.convAngles = function(value) {
31 | if(this.angles === 'degrees')
32 | return value*(Math.PI/180);
33 | if(this.angles === 'gradians')
34 | return value*(Math.PI/200);
35 | return value;
36 | };
37 |
38 | //This will take a radian value and convert it to the proper unit, based on the current setting
39 | this.convRadians = function(value) {
40 | if(this.angles === 'degrees')
41 | return (value * 180 / Math.PI);
42 | if(this.angles === 'gradians')
43 | return (value * 200 / Math.PI);
44 | return value;
45 | };
46 |
47 | var replacements = {};
48 | _(['sin', 'cos', 'tan', 'sec', 'cot', 'csc']).each(function(name) {
49 | var fn = math[name]; // the original function
50 | replacements[name] = function replacement(x) {
51 | return fn(Calc.convAngles(x));
52 | };
53 | });
54 |
55 | _(['asin', 'acos', 'atan', 'atan2']).each(function(name) {
56 | var fn = math[name]; // the original function
57 | replacements[name] = function replacement(x) {
58 | return Calc.convertRadians(fn(x));
59 | };
60 | });
61 |
62 | // import all replacements into math.js, override existing trigonometric functions
63 | math.import(replacements, {override: true});
64 |
65 | /* Algorithms
66 | */
67 |
68 | //Terribly Inaccurate. Ah well.
69 | this.getVertex = function(f, start, end, precision){
70 | this.loopcounter++;
71 | if(Math.abs(end - start) <= precision) {
72 | this.loopcounter = 0;
73 | return (end + start) / 2;
74 | }
75 | if(this.loopcounter > 200) {
76 | this.loopcounter = 0;
77 | return false;
78 | }
79 |
80 | var interval = (end-start) / 40;
81 | var xval = start - interval;
82 | var prevanswer = startanswer = f(xval);
83 | for(xval = start; xval <= end; xval += interval) {
84 | xval = this.roundFloat(xval);
85 | var answer = f(xval);
86 | if((prevanswer > startanswer && answer < prevanswer) || (prevanswer < startanswer && answer > prevanswer)) {
87 | return this.getVertex(f, xval - 2*interval, xval, precision);
88 | }
89 | prevanswer = answer;
90 | }
91 | this.loopcounter = 0;
92 | return false;
93 | };
94 |
95 | //Uses Newton's method to find the root of the equation. Accurate enough for these purposes.
96 | this.getRoot = function(equation, guess, range, shifted){
97 | var expr = math.parse(equation);
98 | var variables = this.variablesInExpression(expr);
99 |
100 | dump(equation + ', guess: ' + guess);
101 | //Newton's method becomes very inaccurate if the root is too close to zero. Therefore we just whift everything over a few units.
102 | if((guess > -0.1 && guess < 0.1) && shifted != true) {
103 | var replacedEquation = equation;
104 |
105 | if (variables.length > 0) {
106 | var v = variables[0];
107 | replacedEquation = replacedEquation.replace(new RegExp('\\b' + v + '\\b', 'g'), '(' + v + '+5)');
108 | }
109 |
110 | dump('Replaced equation = ' + replacedEquation);
111 | var answer = this.getRoot(replacedEquation, (guess - 5), range, true);
112 | dump(answer);
113 | if(answer !== false)
114 | return answer + 5;
115 | return false;
116 | }
117 |
118 | if(!range)
119 | var range = 5;
120 |
121 | var center = guess;
122 | var prev = guess;
123 | var j = 0;
124 |
125 | var code = expr.compile(math);
126 | var variables = this.variablesInExpression(expr);
127 |
128 | var f = function (x) {
129 | var scope = {};
130 |
131 | _(variables).each(function (name) {
132 | scope[name] = x;
133 | });
134 |
135 | return code.eval(scope);
136 | };
137 |
138 | while (prev > center - range && prev < center + range && j < 100) {
139 | var xval = prev;
140 | var answer = f(xval);
141 |
142 | if (answer > -this.eps && answer < this.eps) {
143 | return prev;
144 | }
145 |
146 | var derivative = this.getDerivative(f, xval);
147 | if (!isFinite(derivative))
148 | break;
149 |
150 | //dump('d/dx = ' + derivative);
151 | prev = prev - answer / derivative;
152 | j++;
153 | }
154 |
155 | if (j >= 100) {
156 | dump('Convergence failed, best root = ' + prev);
157 | return prev;
158 | }
159 |
160 | dump('false: center at ' + center + ' but guess at ' + prev);
161 |
162 | return false;
163 | };
164 |
165 | //Uses Newton's method for finding the intersection of the two equations. Actually very simple.
166 | this.getIntersection = function(equation1, equation2, guess, range){
167 | //dump("("+equation1 + ") - (" + equation2 + "); guess at "+guess);
168 | return this.getRoot('(' + equation1 + ') - (' + equation2 + ')', guess, range);
169 | }
170 |
171 | this.getDerivative = function(f, xval){
172 | /*
173 | * This is a brute force method of calculating derivatives, using
174 | * Newton's difference quotient (except without a limit)
175 | *
176 | * The derivative of a function f and point x can be approximated by
177 | * taking the slope of the secant from x to x+h, provided that h is sufficently
178 | * small. However, if h is too small, then floating point errors may result.
179 | *
180 | * This algorithm is an effective 100-point stencil in one dimension for
181 | * calculating the derivative of any real function y=equation.
182 | */
183 | var ddx = 0;
184 |
185 | //The suitable value for h is given at http://www.nrbook.com/a/bookcpdf/c5-7.pdf to be sqrt(eps) * x
186 | var x = xval;
187 | if(x > 1 || x < -1)
188 | var h = Math.sqrt(this.eps) * x;
189 | else
190 | var h = Math.sqrt(this.eps);
191 |
192 | var answerx = f(x);
193 | for(var i = 1; i <= 50; i++) {
194 | var diff = (h * i);
195 | var inverseDiff = 1 / diff;
196 |
197 | //h is positive
198 | xval = x + diff;
199 | var answer = f(xval);
200 | ddx += (answer - answerx) * inverseDiff;
201 |
202 | //h is negative
203 | xval = x - diff;
204 | answer = f(xval);
205 | ddx += (answerx - answer) * inverseDiff;
206 | }
207 |
208 | return ddx / 100;
209 | };
210 |
211 | /* Utility functions
212 | */
213 | this.variablesInExpression = function (expr) {
214 | var obj = {};
215 |
216 | expr.traverse(function (node) {
217 | if ((node.type === 'SymbolNode') && (math[node.name] === undefined)) {
218 | obj[node.name] = true;
219 | }
220 | });
221 |
222 | return Object.keys(obj).sort();
223 | };
224 |
225 | this.makeFunction = function (equation) {
226 | var expr = math.parse(equation);
227 | var code = expr.compile(math);
228 | var variables = Calc.variablesInExpression(expr);
229 |
230 | return function (x) {
231 | var scope = {};
232 |
233 | _(variables).each(function (name) {
234 | scope[name] = x;
235 | });
236 |
237 | return code.eval(scope);
238 | };
239 | };
240 |
241 | this.roundToSignificantFigures = function (num, n) {
242 | if(num === 0) {
243 | return 0;
244 | }
245 |
246 | d = Math.ceil(math.log10(num < 0 ? -num: num));
247 | power = n - d;
248 |
249 | magnitude = Math.pow(10, power);
250 | shifted = Math.round(num*magnitude);
251 | return shifted/magnitude;
252 | };
253 |
254 | this.roundFloat = function(val) { //Stupid flaoting point inprecision...
255 | return (Math.round(val * 100000000000) / 100000000000);
256 | };
257 | }
258 |
259 | Calc = new calc();
260 |
--------------------------------------------------------------------------------
/graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/graph.png
--------------------------------------------------------------------------------
/images/button_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/button_bg.png
--------------------------------------------------------------------------------
/images/derivative.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/derivative.png
--------------------------------------------------------------------------------
/images/graph_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/graph_small.png
--------------------------------------------------------------------------------
/images/intersect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/intersect.png
--------------------------------------------------------------------------------
/images/minmax.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/minmax.png
--------------------------------------------------------------------------------
/images/pointer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/pointer.png
--------------------------------------------------------------------------------
/images/root.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/root.png
--------------------------------------------------------------------------------
/images/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/settings.png
--------------------------------------------------------------------------------
/images/trace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/trace.png
--------------------------------------------------------------------------------
/images/zoombox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/zoombox.png
--------------------------------------------------------------------------------
/images/zoomin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/zoomin.png
--------------------------------------------------------------------------------
/images/zoomout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yerich/Graphr/bf919fb540fbafe93c0c77a61c1bc8a072801be3/images/zoomout.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Graphr - The JavaScript Graphing Calculator
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
97 |
98 |
99 |
142 |
143 |
144 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/jsgcalc.css:
--------------------------------------------------------------------------------
1 | @import url(http://fonts.googleapis.com/css?family=Lora:400,400italic);
2 | @import url(http://fonts.googleapis.com/css?family=Monda);
3 | @import url(http://fonts.googleapis.com/css?family=Droid+Serif:400,400italic);
4 | body { font-family: "Open Sans", sans-serif; font-size: 9pt;}
5 |
6 | #wrapper { position: absolute; top: 0px; left: 0px; height: 100%; width: 100%;overflow: hidden;}
7 |
8 | #toolbar { width: 36px; padding: 35px 5px 0px 5px; height: 100%; background: #EDEDED; border-left: 1px solid #BBB; position: absolute; z-index: 10; right: 252px; top: 0;}
9 | #tool_select img { border: 1px solid transparent; opacity: 0.5; border-radius: 5px; }
10 | #tool_select img:hover { opacity: 1.0;}
11 | #tool_select a.toolbar_selected img { border: 1px solid #CCC; opacity: 1.0 !important; background: #FFF;
12 | -webkit-box-shadow: inset 0px -900px 50px -900px rgba(150, 150, 150, 0.3), inset 0px 1px 0px 0px #FFF;
13 | box-shadow: inset 0px -900px 40px -900px rgba(150, 150, 150, 0.3), inset 0px 1px 0px 0px #FFF;}
14 |
15 | #sidewrapper { position: absolute; top: 1px; right: 1px; width: 250px; z-index: 5; background: #F6F6F6; border-left: 1px solid #BBB; height: 100%; overflow: auto;}
16 | #graph_sidebar { margin: 5px 10px;}
17 | #hideSidebar, #showSidebar { z-index: 11; font-size: 15pt; font-weight: bold; border-bottom: 1px solid #999; width: 46px;
18 | background: #F7F7F7; position: absolute; top: -1px;}
19 | #showSidebar { display: none; right: 0px; }
20 | #hideSidebar { right: 252px; display: inline;}
21 | #showSidebar a, #hideSidebar a { color: #666; text-decoration: none; display: block; padding-top: 0px; text-align: right; padding-right: 13px;}
22 | #showSidebar a {text-align: left; padding-left: 13px;}
23 |
24 | .toolbox { width: 210px; background: #FFFFFF; border: 1px solid #999; position: absolute; right: 292px; top: 20px; z-index: 9; display: none;}
25 | .toolbox_header { font-weight: bold; background: #E8E8E8; padding: 3px;}
26 | .toolbox_close { float: right; display: inline-block;}
27 | .toolbox_main { padding: 3px;}
28 |
29 | .graph_input_wrapper { margin-bottom: 5px; padding: 4px 6px; margin: 1px 0px; font-family: "Droid Serif", "Georgia", serif; font-size: 12pt;}
30 | .active_equation { background: #F0F0F0; padding: 3px 5px; border: 1px solid #CCC; border-radius: 5px; text-shadow:1px 1px 0px #FAFAFA;
31 | -webkit-box-shadow: inset 0px -900px 50px -900px rgba(150, 150, 150, 0.3), inset 0px 1px 0px 0px #FFF;
32 | box-shadow: inset 0px -900px 40px -900px rgba(150, 150, 150, 0.3), inset 0px 1px 0px 0px #FFF; }
33 | .graph_equation_display span { padding-right: 5px;}
34 | .graph_color_indicator { height: 22px; width: 22px; border: 1px solid #BBB; float: right;}
35 | #graph_inputs input { height: 16px; padding: 1px; clear: both; width: 156px; border: 1px solid #BBB; padding: 3px; font-size: 12pt; font-family: "Droid Serif", "Georgia", serif;}
36 |
37 | #settings_button {padding: 5px 6px; height: 16px; vertical-align: bottom;}
38 | #settings_button img { height: 100%; opacity: 0.5;}
39 | #settings { display: none; margin-bottom: 20px;}
40 |
41 | #graph_wrapper { position: absolute; top: 0px; left: 0px; height: 100%; width: 100%; z-index: 2; margin-right: 250px;}
42 | #graph_wrapper:hover { cursor: crosshair;}
43 | #graph_wrapper:active { cursor: move;}
44 | #graph { width: 100%; height: 100%;}
45 |
46 | h1 { margin: 0; padding: 0;}
47 |
48 | .options_list { line-height: 25px;}
49 | .option { color: #666; border: 1px solid #BBB; background: #E7E7E7; border-radius: 5px; padding: 1px 4px;}
50 | .option_selected { font-weight: bold; color: #000; background: #F6F6F6; border: 1px solid #999; }
51 |
52 | textarea { border: 1px dashed #56ADFF; outline: none; resize: none; overflow: hidden; white-space: nowrap; font-family: 'times new roman' ,georgia, serif;
53 | font-size: 14pt; min-width: 1em; height: 1em; padding: 0.2em}
54 | textarea:focus { border: 1px solid #56ADFF;}
55 |
56 | a { text-decoration: none;}
57 | a:hover { text-decoration: underline;}
58 |
59 | .fancybutton {
60 | -moz-box-shadow:inset 0px 1px 0px 0px #97c4fe;
61 | -webkit-box-shadow:inset 0px 1px 0px 0px #97c4fe;
62 | box-shadow:inset 0px 1px 0px 0px #97c4fe;
63 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #3d94f6), color-stop(1, #1e62d0) );
64 | background:-moz-linear-gradient( center top, #3d94f6 5%, #1e62d0 100% );
65 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#3d94f6', endColorstr='#1e62d0');
66 | background-color:#3d94f6;
67 | -moz-border-radius:6px;
68 | -webkit-border-radius:6px;
69 | border-radius:6px;
70 | border:1px solid #337fed;
71 | display:inline-block;
72 | color:#ffffff;
73 | font-family:'open sans', arial;
74 | font-size:13px;
75 | font-weight:bold;
76 | padding:4px 12px;
77 | text-decoration:none;
78 | text-shadow:1px 1px 0px #1570cd;
79 | }.fancybutton:hover {
80 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #1e62d0), color-stop(1, #3d94f6) );
81 | background:-moz-linear-gradient( center top, #1e62d0 5%, #3d94f6 100% );
82 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#1e62d0', endColorstr='#3d94f6');
83 | background-color:#1e62d0;
84 | }.fancybutton:active {
85 | position:relative;
86 | top:1px;
87 | }
88 |
89 | .greybutton {
90 | -moz-box-shadow:inset 0px 1px 0px 0px #ffffff;
91 | -webkit-box-shadow:inset 0px 1px 0px 0px #ffffff;
92 | box-shadow:inset 0px 1px 0px 0px #ffffff;
93 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #f9f9f9), color-stop(1, #e9e9e9) );
94 | background:-moz-linear-gradient( center top, #f9f9f9 5%, #e9e9e9 100% );
95 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f9f9f9', endColorstr='#e9e9e9');
96 | background-color:#f9f9f9;
97 | border:1px solid #dcdcdc;
98 | color:#666666;
99 | }.greybutton:hover {
100 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #e9e9e9), color-stop(1, #f9f9f9) );
101 | background:-moz-linear-gradient( center top, #e9e9e9 5%, #f9f9f9 100% );
102 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e9e9e9', endColorstr='#f9f9f9');
103 | background-color:#e9e9e9;
104 | }
105 |
106 | .hidden { display: none;}
--------------------------------------------------------------------------------
/jsgcalc.js:
--------------------------------------------------------------------------------
1 | function dump(text) {
2 | console.log(text);
3 | }
4 |
5 | function float_fix(num) {
6 | //Rounds floating points
7 | return Math.round(num * 10000000) / 10000000
8 | }
9 |
10 | function widthPlusPadding(elem) {
11 | return $(elem).width() + parseFloat($(elem).css('paddingRight')) + parseFloat($(elem).css('paddingLeft'));
12 | }
13 |
14 | function JSgCalc (element){
15 | this.graph = document.getElementById(element);
16 | this.graphElement = $("#"+element);
17 | this.width = $("#wrapper").width();
18 | this.height = $("#wrapper").height();
19 | this.maxgridlines = {x : 13, y : 13};
20 | this.charHeight = 8;
21 | this.startDrag = {x : 0, y : 0};
22 | this.prevDrag = {x : 0, y : 0};
23 | this.startCoord = {x1 : 0, y1 : 0, x2 : 0, y2 : 0};
24 | this.currCoord = {x1 : -5, y1 : -5, x2 : 5, y2 : 5};
25 | this.mousebutton = 0;
26 | this.canvasX = this.graph.offsetLeft;
27 | this.canvasY = this.graph.offsetTop;
28 | this.calccache = new Object;
29 | this.quality = 1;
30 | this.zoomFactor = 0.1;
31 | this.lines = [];
32 | this.fillareapath;
33 |
34 | this.arbRound = function(value, roundTo) {
35 | return Math.round(value/roundTo)*roundTo;
36 | };
37 |
38 | this.arbFloor = function(value, roundTo) {
39 | return Math.floor(value/roundTo)*roundTo;
40 | };
41 |
42 | this.copyCoord = function(coord) {
43 | return {x1 : coord.x1, y1 : coord.y1, x2 : coord.x2, y2 : coord.y2};
44 | };
45 |
46 | this.clearScreen = function() {
47 | this.ctx.fillStyle = "rgb(255,255,255)";
48 | this.ctx.fillRect (0, 0, this.width, this.height);
49 | };
50 |
51 | this.getEquation = function(lineid) {
52 | if(this.lines[lineid])
53 | return this.lines[lineid].equation;
54 | return false;
55 | };
56 |
57 | this.getColor = function(lineid) {
58 | if(this.lines[lineid])
59 | return this.lines[lineid].color;
60 | return "#000000";
61 | };
62 |
63 | this.drawEquation = function(equation, color, thickness) {
64 | if(!equation)
65 | return false;
66 |
67 | var x1 = this.currCoord.x1;
68 | var x2 = this.currCoord.x2;
69 | var y1 = this.currCoord.y1;
70 | var y2 = this.currCoord.y2;
71 |
72 | var xrange = x2 - x1;
73 | var yrange = y2 - y1;
74 |
75 | var scale = this.getScale();
76 |
77 | if(!this.calccache[equation])
78 | this.calccache[equation] = new Object;
79 |
80 | this.ctx.strokeStyle = color;
81 | var old_linewidth = this.ctx.linewidth
82 | if(thickness)
83 | this.ctx.linewidth = thickness;
84 | this.ctx.beginPath();
85 | //We don't want to draw lines that go off the screen too much, so we keep track of how many times we've had
86 | //to go off the screen here
87 | var lineExists = 0;
88 | var lastpoint = 0;
89 |
90 | this.fillareapath = [];
91 | this.fillareapath.push([0, this.height - ((-y1) * scale.y)]);
92 | //Loop through each pixel
93 |
94 | var inverseQuality = 1.0 / this.quality;
95 | var inverseScaleX = 1.0 / scale.x;
96 |
97 | var maxxval = this.width + inverseQuality;
98 |
99 | var f = Calc.makeFunction(equation);
100 |
101 | for(var i = 0; i < maxxval; i += inverseQuality) {
102 | var xval = i * inverseScaleX + x1; //calculate the x-value for a given pixel
103 | var yval = f(xval);
104 |
105 | var ypos = this.height - ((yval - y1) * scale.y);
106 | //The line is on the screen, or pretty close to it
107 | if(ypos >= (this.height * -1) && ypos <= this.height * 2) {
108 | if(lineExists > 1)
109 | this.ctx.beginPath();
110 |
111 | if(lastpoint !== false && ((lastpoint > 0 && yval < 0) || (lastpoint < 0 && yval > 0))) {
112 | this.ctx.moveTo(i, ypos);
113 | }
114 | else {
115 | this.ctx.lineTo(i, ypos);
116 | }
117 |
118 | lineExists = 0;
119 | lastpoint = false;
120 | }
121 | else if(lineExists <= 1) { //The line is off the screen
122 | this.ctx.lineTo(i, ypos);
123 | lastpoint = yval;
124 | this.ctx.stroke();
125 | lineExists++;
126 | }
127 | this.fillareapath.push([i, ypos]);
128 | //this.ctx.fillRect(i - 0.5, ypos - 0.5, 1, 1);
129 | }
130 | this.fillareapath.push([maxxval, this.height - ((-y1) * scale.y)]);
131 | this.ctx.stroke();
132 | this.ctx.linewidth = old_linewidth
133 | };
134 |
135 | this.drawFillArea = function() {
136 | if (this.fillareapath.length < 1) {
137 | return;
138 | }
139 |
140 | this.ctx.beginPath();
141 | this.ctx.fillStyle="rgba(0, 0, 0, 0.1)";
142 | for(var i = 0; i < this.fillareapath.length; i++) {
143 | if (i === 0) {
144 | this.ctx.lineTo(this.fillareapath[i][0], this.fillareapath[i][1]);
145 | }
146 | else {
147 | this.ctx.lineTo(this.fillareapath[i][0], this.fillareapath[i][1]);
148 | }
149 | }
150 | this.ctx.fill();
151 | }
152 |
153 | //Draws an arbritrary straight line from (x1, y1) to (x2, y2)
154 | this.drawLine = function(x1, y1, x2, y2, color, thickness) {
155 | if(!color)
156 | color = "#000000";
157 |
158 | this.ctx.strokeStyle = color;
159 | this.ctx.beginPath();
160 | var start = this.getCoord(x1, y1);
161 | var end = this.getCoord(x2, y2);
162 | this.ctx.moveTo(start.x, start.y);
163 | this.ctx.lineTo(end.x, end.y);
164 |
165 | var tmp = this.ctx.lineWidth
166 | if(thickness)
167 | this.ctx.lineWidth = thickness
168 |
169 | this.ctx.stroke();
170 | this.ctx.lineWidth = tmp
171 | };
172 |
173 | //Draws an arbritrary label on the graph, given the numeric values (rather than the pixel values)
174 | this.drawLabel = function(xval, yval, text, color) {
175 | if(!color)
176 | color = "#000000";
177 |
178 | var labelCoord = this.getCoord(xval, yval);
179 | var xpos = labelCoord.x;
180 | var ypos = labelCoord.y;
181 |
182 | this.ctx.font = "12pt 'open sans'";
183 | this.ctx.fillStyle = color;
184 | this.ctx.beginPath();
185 | this.ctx.moveTo(xpos, ypos);
186 |
187 | if(ypos-4 < this.charHeight)
188 | ypos += this.charHeight * 2;
189 | var textwidth = this.ctx.measureText(text).width;
190 | if(xpos-4 < textwidth)
191 | xpos += textwidth + 3;
192 | this.ctx.fillText(text, xpos-3, ypos-3);
193 | };
194 |
195 | this.drawDot = function(xval, yval, color, radius) {
196 | if(!radius)
197 | radius = 4;
198 | if(!color)
199 | color = "#000000";
200 |
201 | var coord = this.getCoord(xval, yval);
202 | this.ctx.beginPath();
203 | this.ctx.fillStyle = color;
204 | this.ctx.arc(coord.x, coord.y, radius, 0, Math.PI*2, false);
205 | this.ctx.fill();
206 | };
207 |
208 | //Draws thge vertex of an equation (i.e. when it changes direction)
209 | this.drawVertex = function(equation, color, x) {
210 | var f = Calc.makeFunction(equation);
211 |
212 | var scale = this.getScale();
213 | var xpos = x / scale.x + this.currCoord.x1;
214 | var matchingDist = 20 / scale.x;
215 | var answer = Calc.getVertex(f, xpos-matchingDist, xpos+matchingDist, 0.0000001);
216 | var tries = 0;
217 | while(answer === false) {
218 | tries++;
219 | if(tries > 5)
220 | return false;
221 | answer = Calc.getVertex(f, xpos-matchingDist-Math.random()/100, xpos+matchingDist+Math.random()/100, 0.0000001);
222 | }
223 | var xval = Calc.roundFloat(answer);
224 | var yval = f(xval);
225 | var yval = Calc.roundFloat(this.arbRound(yval, 0.0000001));
226 | this.drawDot(xval, yval, color, 4);
227 |
228 | //draw label text
229 | this.drawLabel(xval, yval, Calc.roundFloat(this.arbRound(xval, 0.0000001))+", " + yval, "#000000");
230 | };
231 |
232 | //Draws the root of an equation (i.e. where x=0)
233 | this.drawRoot = function(equation, color, x) {
234 | var scale = this.getScale();
235 | var xpos = x / scale.x + this.currCoord.x1;
236 | //Calculate the root (within 50 pixels)
237 | var answer = Calc.getRoot(equation, xpos, 50 / scale.x);
238 | if(answer === false)
239 | return false;
240 |
241 | answer = Math.round(answer * 10000000) / 10000000;
242 |
243 | var xval = Calc.roundFloat(answer);
244 | var yval = 0;
245 |
246 | this.drawDot(xval, yval, color, 4); //draw the dot
247 | //draw label text
248 | this.drawLabel(xval, yval, Calc.roundFloat(this.arbRound(xval, 0.00000001))+", " + yval);
249 | };
250 |
251 | //draws the intersection of an equation and the nearest equation to the mouse pointer
252 | this.drawIntersect = function(equation1, color, x) {
253 | var scale = this.getScale();
254 | var xpos = x / scale.x + this.currCoord.x1;
255 | var equation;
256 |
257 | var answer = false;
258 | for(i in this.lines) {
259 | if(this.getEquation(i) == equation1)
260 | continue;
261 |
262 | var tempanswer = Calc.getIntersection(equation1, this.getEquation(i), xpos, 50 / scale.x);
263 | if(tempanswer === false)
264 | continue;
265 | tempanswer = Math.round(tempanswer * 10000000) / 10000000;
266 | dump(tempanswer);
267 | if(tempanswer !== false && (answer === false || Math.abs(xpos - answer) > Math.abs(xpos - tempanswer))) {
268 | answer = tempanswer;
269 | equation = equation1;
270 | }
271 | }
272 | if(answer === false)
273 | return false;
274 |
275 | var xval = Calc.roundFloat(answer);
276 | var f = Calc.makeFunction(equation);
277 |
278 | var yval = f(xval);
279 |
280 | //Draw dot
281 | this.drawDot(xval, yval, color, 4);
282 |
283 | //draw label text
284 | this.drawLabel(xval, yval, float_fix(xval) + ", " + float_fix(yval), color);
285 | };
286 |
287 | this.drawDerivative = function(equation, color, x) {
288 | var f = Calc.makeFunction(equation);
289 |
290 | var scale = this.getScale();
291 | var xpos = Calc.roundFloat(this.arbRound(x / scale.x + this.currCoord.x1, this.xgridscale/100));
292 |
293 | //Do the actual calculation.
294 | var slope = Math.round(Calc.getDerivative(f, xpos) * 1000000) / 1000000;
295 |
296 | var xval = xpos;
297 | var yval = f(xval);
298 | yval = Calc.roundFloat(this.arbRound(yval, 0.0000001));
299 | var pos = this.getCoord(xval, yval);
300 | this.ctx.beginPath();
301 | this.ctx.fillStyle = color;
302 | this.ctx.arc(pos.x, pos.y, 4, 0, Math.PI*2, false);
303 | this.ctx.fill();
304 |
305 | //draw derivative lines of exactly 2*xgridscale long
306 | var xdist = this.xgridscale*2 / (Math.sqrt(Math.pow(slope, 2) + 1));
307 | var ydist = xdist * slope;
308 | var linestart = {x : xval - xdist, y : yval - ydist};
309 | var lineend = {x : xval + xdist, y : yval + ydist};
310 | this.ctx.beginPath();
311 | this.ctx.strokeStyle = "#000000";
312 | linestart = this.getCoord(linestart.x, linestart.y);
313 | lineend = this.getCoord(lineend.x, lineend.y);
314 | this.ctx.moveTo(linestart.x, linestart.y);
315 | this.ctx.lineTo(lineend.x, lineend.y);
316 | this.ctx.stroke();
317 |
318 | //draw label text
319 | this.ctx.font = "10pt 'open sans'";
320 | this.ctx.fillStyle = "#000000";
321 | var text = "x="+xval+", d/dx="+slope;
322 | var xval2 = xval; //find out whether to put label above or below dot
323 | xval -= this.xgridscale / 5;
324 | var answer2 = f(xval);
325 | xval += this.xgridscale / 10;
326 | var answer3 = f(x);
327 | if(pos.y-4 < this.charHeight || answer2 > answer3)
328 | pos.y += this.charHeight + 3;
329 | var textwidth = this.ctx.measureText(text).width;
330 | if(pos.x-4 < textwidth)
331 | pos.x += textwidth + 3;
332 | this.ctx.fillText(text, pos.x-4, pos.y-4);
333 | };
334 |
335 |
336 | //Draws the trace on an equation
337 | //xpos is the pixel value of x, not the numerical value
338 | this.drawTrace = function(equation, color, xval) {
339 | var f = Calc.makeFunction(equation);
340 | var scale = this.getScale();
341 |
342 | var xval = float_fix(this.arbRound(xval, this.xgridscale / 100));
343 | var yval = f(xval); //evaluate the equation
344 | yval = float_fix(yval);
345 | var xpos = this.getCoord(xval, yval).x;
346 | var ypos = this.getCoord(xval, yval).y;
347 |
348 | this.ctx.strokeStyle = color;
349 | //Draw the lines if the y-value is on the screen
350 | if(ypos <= this.height && ypos >= 0) {
351 | //Draw a line from the point to the x-axis
352 | this.drawLine(xval, yval, xval, 0, "#999");
353 |
354 | //Draw line from point to the y-axis
355 | this.drawLine(xval, yval, 0, yval, "#999");
356 |
357 | //draw label text
358 | this.drawLabel(xval, yval, xval + ", " + yval, "#000000");
359 | }
360 |
361 | //Draw dot
362 | this.drawDot(xval, yval, color, 4);
363 |
364 | //Update displayed trace values
365 | $("input.jsgcalc_trace_input").val(xval);
366 | $("input.jsgcalc_trace_output").val(yval);
367 | };
368 |
369 | this.drawGrid = function() {
370 | this.clearScreen();
371 |
372 | var x1 = this.currCoord.x1;
373 | var x2 = this.currCoord.x2;
374 | var y1 = this.currCoord.y1;
375 | var y2 = this.currCoord.y2;
376 |
377 | var xrange = x2 - x1;
378 | var yrange = y2 - y1;
379 |
380 | //Calculate the numeric value of each pixel (scale of the graph)
381 | var xscale = Math.max(xrange/this.width, 1E-20);
382 | var yscale = Math.max(yrange/this.height, 1E-20);
383 |
384 | //Calculate the scale of the gridlines
385 | for(i = 0.000000000001, c = 0; xrange/i > this.maxgridlines.x -1; c++) {
386 | if(c % 3 === 1) i *= 2.5; //alternating between 2, 5 and 10
387 | else i *= 2;
388 |
389 | // Ensure we don't get into an infinite loop
390 | if (c > 10000) {
391 | break;
392 | }
393 | }
394 | this.xgridscale = i;
395 |
396 | //do the same for the y-axis
397 | for(i = 0.000000000001, c = 0; yrange/i > this.maxgridlines.y -1; c++) {
398 | if(c % 3 == 1) i *= 2.5;
399 | else i *= 2;
400 |
401 | // Ensure we don't get into an infinite loop
402 | if (c > 10000) {
403 | break;
404 | }
405 | }
406 | this.ygridscale = i;
407 |
408 | this.ctx.font = "10pt 'open sans'"; //set the font
409 | this.ctx.textAlign = "center";
410 |
411 | var xaxis = yaxis = null;
412 |
413 | //currx is the current gridline being drawn, as a numerical value (not a pixel value)
414 | var currx = this.arbFloor(x1, this.xgridscale); //set it to before the lowest x-value on the screen
415 | var curry = this.arbFloor(y1, this.ygridscale);
416 | var xmainaxis = this.charHeight * 1.5; //the next two variables are the axis on which text is going to be placed
417 | var ymainaxis = -1;
418 | currx = float_fix(currx); //flix floating point errors
419 | curry = float_fix(curry);
420 |
421 | if(y2 >= 0 && y1 <= 0) //y=0 appears on the screen - move the text to follow
422 | xmainaxis = this.height - ((0-y1)/(y2-y1))*this.height + (this.charHeight * 1.5);
423 | else if(y1 > 0) //the smallest value of y is below the screen - the x-axis labels get pushed to the bottom of the screen
424 | xmainaxis = this.height - 5;
425 |
426 | //the x-axis labels have to be a certain distance from the bottom of the screen
427 | if(xmainaxis > this.height - (this.charHeight / 2))
428 | xmainaxis = this.height - 5;
429 |
430 | //do the same as above with the y-axis
431 | if(x2 >= 0 && x1 <= 0) //y-axis in the middle of the screen
432 | ymainaxis = ((0-x1)/(x2-x1))*this.width - 2;
433 | else if(x2 < 0) //y-axis on the right side of the screen
434 | ymainaxis = this.width-6;
435 |
436 | if(ymainaxis < (this.ctx.measureText(curry).width + 1)) {
437 | ymainaxis = -1;
438 | }
439 |
440 | var sigdigs = String(currx).length + 3;
441 | //VERTICAL LINES
442 | for(i = 0; i < this.maxgridlines.x; i++) {
443 | xpos = ((currx-x1)/(x2-x1))*this.width; //position of the line (in pixels)
444 | //make sure it is on the screen
445 | if(xpos-0.5 > this.width + 1 || xpos < 0) {
446 | currx += this.xgridscale;
447 | continue;
448 | }
449 |
450 | //currx = Calc.roundToSignificantFigures(currx, sigdigs);
451 | currx = float_fix(currx);
452 |
453 | if(currx === 0)
454 | xaxis = xpos;
455 |
456 | if(jsgui.gridlines === "normal" || (jsgui.gridlines === "less" && Calc.roundFloat(currx) % Calc.roundFloat((this.xgridscale*2)) === 0)) {
457 | this.ctx.fillStyle = "rgb(190,190,190)";
458 | this.ctx.fillRect (xpos-0.5, 0, 1, this.height);
459 | }
460 | this.ctx.fillStyle = "rgb(0,0,0)";
461 |
462 | //Draw label
463 | if (currx != 0) {
464 | var xtextwidth = this.ctx.measureText(currx).width;
465 | if (xpos + xtextwidth * 0.5 > this.width) //cannot overflow the screen
466 | xpos = this.width - xtextwidth * 0.5 + 1;
467 | else
468 | if (xpos - xtextwidth * 0.5 < 0)
469 | xpos = xtextwidth * 0.5 + 1;
470 | this.ctx.fillText(currx, xpos, xmainaxis);
471 | }
472 |
473 | currx += this.xgridscale;
474 |
475 | }
476 | this.ctx.textAlign = "right";
477 | sigdigs = String(curry).length + 3;
478 |
479 | //HORIZONTAL LINES
480 | for(i = 0; i < this.maxgridlines.y; i++) {
481 | ypos = this.height - ((curry-y1)/(y2-y1))*this.height; //position of the line (in pixels)
482 | //make sure it is on the screen
483 | if(ypos-0.5 > this.height + 1 || ypos < 0) {
484 | curry += this.ygridscale;
485 | continue;
486 | }
487 |
488 | //curry = Calc.roundToSignificantFigures(curry, sigdigs);
489 | curry = float_fix(curry);
490 |
491 | if(curry == 0)
492 | yaxis = ypos;
493 |
494 | if(jsgui.gridlines == "normal" || (jsgui.gridlines == "less" && Calc.roundFloat(curry) % (Calc.roundFloat(this.ygridscale*2)) == 0)) {
495 | this.ctx.fillStyle = "rgb(190,190,190)";
496 | this.ctx.fillRect (0, ypos-0.5, this.width, 1);
497 | }
498 | this.ctx.fillStyle = "rgb(0,0,0)";
499 |
500 | //Draw label
501 | if (curry != 0) {
502 | var ytextwidth = this.ctx.measureText(curry).width;
503 | if (ypos + (this.charHeight / 2) > this.height) //cannot overflow the screen
504 | ypos = this.height - (this.charHeight / 2) - 1;
505 | if (ypos - 4 < 0)
506 | ypos = 4;
507 | var xaxispos = ymainaxis;
508 | if (ymainaxis == -1)
509 | xaxispos = ytextwidth + 1;
510 | this.ctx.fillText(curry, xaxispos, ypos + 3);
511 | }
512 | curry += this.ygridscale;
513 | }
514 | //Draw the axis
515 | if(xaxis)
516 | this.ctx.fillRect (xaxis-0.5, 0, 1, this.height);
517 | if(yaxis)
518 | this.ctx.fillRect (0, yaxis-0.5, this.width, 1);
519 | };
520 |
521 | //get the pixel coordinates of a value
522 | this.getCoord = function(x, y) {
523 | var xpos = ((x-this.currCoord.x1)/(this.currCoord.x2-this.currCoord.x1))*this.width;
524 | var ypos = this.height - ((y-this.currCoord.y1)/(this.currCoord.y2-this.currCoord.y1))*this.height;
525 | return {x : xpos, y : ypos};
526 | };
527 |
528 | //get the (numerical) position of a (pixel) coordinate
529 | this.getValue = function(x, y){
530 | var scale = this.getScale();
531 | var xpos = x / scale.x + this.currCoord.x1;
532 | var ypos = (this.height - y) / scale.y + this.currCoord.y1;
533 | return {x : xpos, y : ypos};
534 | };
535 |
536 | //zoom to a box. the inputs are pixel coordinates
537 | this.doZoomBox = function(x1, y1, x2, y2) {
538 | if(x1 === x2 || y1 === y2) {
539 | dump("Invalid doZoomBox");
540 | return;
541 | }
542 | var coord1 = this.getValue(x1, y1);
543 | var coord2 = this.getValue(x2, y2);
544 |
545 | if(x1 > x2) {
546 | this.currCoord.x1 = coord2.x;
547 | this.currCoord.x2 = coord1.x;
548 | }
549 | else {
550 | this.currCoord.x1 = coord1.x;
551 | this.currCoord.x2 = coord2.x;
552 | }
553 |
554 | if(y2 > y1) {
555 | this.currCoord.y1 = coord2.y;
556 | this.currCoord.y2 = coord1.y;
557 | }
558 | else {
559 | this.currCoord.y1 = coord1.y;
560 | this.currCoord.y2 = coord2.y;
561 | }
562 |
563 | this.startCoord = this.copyCoord(this.currCoord);
564 | this.draw();
565 | };
566 |
567 | this.draw = function() {
568 | this.drawGrid();
569 | for(var i in this.lines) {
570 | //dump(this.lines[i].equation);
571 | //try {
572 | var equation = this.lines[i].equation;
573 | this.drawEquation(equation, this.lines[i].color, 3);
574 | /*
575 | } catch (e) {
576 | console.warn('Error drawing equation "' +
577 | this.lines[i].equation + '"', e);
578 |
579 | } */
580 | }
581 | jsgui.updateValues();
582 | };
583 |
584 | //Gets the scale (pixels per unit)
585 | this.getScale = function() {
586 | return {x : (this.width / (this.startCoord.x2 - this.startCoord.x1)),
587 | y : (this.height / (this.startCoord.y2 - this.startCoord.y1))}
588 | };
589 |
590 | //get the range of values on the screen
591 | this.getRange = function() {
592 | return {x : Math.abs(this.startCoord.x2 - this.startCoord.x1),
593 | y : Math.abs(this.startCoord.y2 - this.startCoord.y1)}
594 | };
595 |
596 | var started = false;
597 | this.checkMove = function(x, y) {
598 | if(x === this.prevDrag.x && y === this.prevDrag.y)
599 | return;
600 |
601 | var scale = this.getScale();
602 | if(this.mousebutton === 1) {
603 | if(jsgui.currtool === "zoombox" || jsgui.currtool === "zoombox_active") { //ZOOM BOX
604 | this.draw();
605 | this.ctx.strokeStyle = "rgb(150,150,150)";
606 | this.ctx.strokeRect (this.startDrag.x, this.startDrag.y, x-this.startDrag.x, y-this.startDrag.y);
607 | }
608 | else { //CLICK AND DRAG
609 | //dump(scale.x + " " + scale.y + " -- " + this.startCoord.x1 + " " + this.startCoord.y1);
610 | //dump(this.startCoord.x1 + " " +(y - this.startDrag.y) / scale.y);
611 | this.currCoord.x1 = this.startCoord.x1 - ((x - this.startDrag.x) / scale.x);
612 | this.currCoord.x2 = this.startCoord.x2 - ((x - this.startDrag.x) / scale.x);
613 |
614 | this.currCoord.y1 = this.startCoord.y1 + ((y - this.startDrag.y) / scale.y);
615 |
616 | this.currCoord.y2 = this.startCoord.y2 + ((y - this.startDrag.y) / scale.y);
617 |
618 | this.draw();
619 | }
620 | }
621 | else if(jsgui.currtool === "trace") { //TRACE
622 | this.draw();
623 | this.drawTrace(this.getEquation(jsgui.currEq), this.getColor(jsgui.currEq), x / scale.x + this.currCoord.x1);
624 | }
625 | else if(jsgui.currtool === "vertex") {
626 | this.draw();
627 | this.drawVertex(this.getEquation(jsgui.currEq), this.getColor(jsgui.currEq), x);
628 | }
629 | else if(jsgui.currtool === "root") {
630 | this.draw();
631 | this.drawRoot(this.getEquation(jsgui.currEq), this.getColor(jsgui.currEq), x);
632 | }
633 | else if(jsgui.currtool === "intersect") {
634 | this.draw();
635 | this.drawIntersect(this.getEquation(jsgui.currEq), this.getColor(jsgui.currEq), x);
636 | }
637 | else if(jsgui.currtool === "derivative") {
638 | this.draw();
639 | this.drawDerivative(this.getEquation(jsgui.currEq), this.getColor(jsgui.currEq), x);
640 | }
641 | this.prevDrag = {x : x, y : y};
642 | };
643 |
644 | this.mouseDown = function(event) {
645 | document.body.style.cursor = "hand";
646 | if(this.mousebutton == 0) {
647 | if(jsgui.currtool === "zoombox") {
648 | jsgui.currtool = "zoombox_active";
649 | }
650 | this.startDrag.x = event.pageX - this.canvasX;
651 | this.startDrag.y = event.pageY - this.canvasY;
652 | this.startCoord = this.copyCoord(this.currCoord);
653 | }
654 | this.mousebutton = 1;
655 | };
656 |
657 | this.mouseUp = function(event) {
658 | //document.body.style.cursor = "auto";
659 | if(jsgui.currtool === "zoombox_active") {
660 | this.doZoomBox(this.startDrag.x, this.startDrag.y, event.pageX - this.canvasX, event.pageY - this.canvasY);
661 | jsgui.setTool("pointer");
662 | }
663 | if(jsgui.currtool === "zoomin") {
664 | if(Math.abs((event.pageX - this.canvasX) - this.startDrag.x) + Math.abs((event.pageY - this.canvasY) - this.startDrag.y) < 5)
665 | this.zoom(0.10, event);
666 | }
667 | if(jsgui.currtool === "zoomout") {
668 | if(Math.abs((event.pageX - this.canvasX) - this.startDrag.x) + Math.abs((event.pageY - this.canvasY) - this.startDrag.y) < 5)
669 | this.zoom(-0.10, event);
670 | }
671 | this.mousebutton = 0;
672 | this.startCoord = this.copyCoord(this.currCoord);
673 | };
674 |
675 | this.mouseWheel = function(event, delta) {
676 | if(delta > 0) {
677 | this.zoom(this.zoomFactor, event);
678 | }
679 | else {
680 | this.zoom(-this.zoomFactor, event);
681 | }
682 | };
683 |
684 | this.setWindow = function(x1, x2, y1, y2) {
685 | this.currCoord.x1 = x1;
686 | this.currCoord.x2 = x2;
687 | this.currCoord.y1 = y1;
688 | this.currCoord.y2 = y2;
689 | this.startCoord = this.copyCoord(this.currCoord);
690 | this.draw();
691 | };
692 |
693 | this.zoom = function(scale, event) {
694 | var range = this.getRange();
695 | if(event) {
696 | var mousex = event.pageX - this.canvasX;
697 | var mousey = event.pageY - this.canvasY;
698 | var mousetop = 1-(mousey / this.height); //if we divide the screen into two halves based on the position of the mouse, this is the top half
699 | var mouseleft = mousex / this.width; //as above, but the left hald
700 | this.currCoord.x1 += range.x * scale * mouseleft;
701 | this.currCoord.y1 += range.y * scale * mousetop;
702 | this.currCoord.x2 -= range.x * scale * (1-mouseleft);
703 | this.currCoord.y2 -= range.y * scale * (1-mousetop);
704 | }
705 | else {
706 | this.currCoord.x1 += range.x * scale;
707 | this.currCoord.y1 += range.y * scale;
708 | this.currCoord.x2 -= range.x * scale;
709 | this.currCoord.y2 -= range.y * scale;
710 | }
711 | this.startCoord = this.copyCoord(this.currCoord);
712 | this.draw();
713 | };
714 |
715 | this.animate = function() {
716 | this.currCoord.x1 += 0.05;
717 | this.currCoord.y1 += 0.05;
718 | this.currCoord.x2 += 0.05;
719 | this.currCoord.y2 += 0.05;
720 | this.draw();
721 | setTimeout('jsgcalc.animate()', 50);
722 | };
723 |
724 | this.resizeGraph = function(width, height) {
725 | var oldheight = this.height;
726 | var oldwidth = this.width;
727 |
728 | //Resize the elements
729 | $("#graph").width(width);
730 | $("#graph").height(height);
731 | this.ctx.height = height;
732 | this.ctx.width = width;
733 | this.graph.height = height;
734 | this.graph.width = width;
735 | this.height = height;
736 | this.width = width;
737 | dump("Resized to " + width + "x" + height);
738 |
739 | //Compute the new boundaries of the graph
740 | this.currCoord.x1 *= (width/oldwidth);
741 | this.currCoord.x2 *= (width/oldwidth);
742 | this.currCoord.y1 *= (height/oldheight);
743 | this.currCoord.y2 *= (height/oldheight);
744 | this.startCoord = this.copyCoord(this.currCoord);
745 |
746 | //Compute how many grid lines to show
747 | this.maxgridlines.x = 0.015 * width;
748 | this.maxgridlines.y = 0.015 * height;
749 | this.draw();
750 | };
751 |
752 | this.resetZoom = function() {
753 | this.currCoord = {x1 : -5 * (this.width / this.height), y1 : -5, x2 : 5 * (this.width / this.height), y2 : 5};
754 | this.startCoord = this.copyCoord(this.currCoord);
755 | this.draw();
756 | };
757 |
758 | this.initCanvas = function() {
759 | if (this.graph.getContext){
760 | this.ctx = this.graph.getContext('2d');
761 | //this.ctx.height = 953;
762 | $("#graph_wrapper").width($("#graph_wrapper").width() - $("#sidewrapper").innerWidth() - $("#toolbar").innerWidth());
763 | this.resizeGraph($("#graph_wrapper").innerWidth(), $("#graph_wrapper").height());
764 | this.currCoord = {x1 : -5 * (this.width / this.height), y1 : -5, x2 : 5 * (this.width / this.height), y2 : 5};
765 | this.startCoord = this.copyCoord(this.currCoord);
766 | jsgui.evaluate();
767 |
768 | //this.animate();
769 |
770 | var self = this;
771 | $("#graph").mousemove(function(event) {
772 | self.canvasX = self.graph.offsetLeft;
773 | self.canvasY = self.graph.offsetTop;
774 | self.checkMove(event.pageX - self.canvasX, event.pageY - self.canvasY);
775 | }).mousedown(function(event) {
776 | self.mouseDown(event);
777 | }).mousewheel(function(event, delta) {
778 | self.mouseWheel(event, delta);
779 | return false;
780 | }).mouseup(function(event) {
781 | self.mouseUp(event);
782 | });
783 |
784 | $(window).resize(function() {
785 | if($("#sidewrapper").is(":visible"))
786 | $("#graph_wrapper").width($("#wrapper").width() - $("#sidewrapper").innerWidth() - $("#toolbar").innerWidth());
787 | else
788 | $("#graph_wrapper").width($("#wrapper").width() - $("#toolbar").innerWidth());
789 | self.resizeGraph($("#graph_wrapper").width(), $("#graph_wrapper").height());
790 | });
791 | }
792 | else {
793 | alert("Sorry, your browser is not supported.");
794 | }
795 | };
796 | };
797 |
798 | $(document).ready(function() {
799 | jsgcalc = new JSgCalc("graph");
800 | jsgcalc.initCanvas();
801 | });
802 |
803 | function about() {
804 | alert("For demonstration purposes only.\n\nCalculations are not guaranteed to be correct and are often inaccurate due to floating point errors. Use at your own risk.");
805 | }
806 |
--------------------------------------------------------------------------------
/jsgui.js:
--------------------------------------------------------------------------------
1 | function JSgui() {
2 | this.currInput = 0;
3 | this.lineColors = {"#FF0000" : -1, "#0000FF" : -1, "#00FF00" : -1, "#FF00FF" : -1, "#00FFFF" : -1,
4 | "#000000" : -1, "#990000" : -1, "#000099" : -1, "#009900" : -1, "#999900" : -1, "#990099" : -1, "#009999" : -1};
5 | this.lineSettings = {0 : {color : "#FF0000"}};
6 | this.currtool = "pointer";
7 | this.currEq = 0;
8 | this.gridlines = "normal";
9 | this.settings = {};
10 |
11 | this.setQuality = function(q) {
12 | $("#quality_select a").removeClass("option_selected");
13 | q2 = String(q).replace(".", "");
14 | $("#quality_select_"+q2).addClass("option_selected");
15 |
16 | jsgcalc.quality = q;
17 | jsgcalc.draw();
18 | }
19 |
20 | this.setAngles = function(q) {
21 | $("#angle_select a").removeClass("option_selected");
22 | $("#angle_select_"+q).addClass("option_selected");
23 |
24 | Calc.angles = q;
25 | jsgcalc.draw();
26 | }
27 |
28 | this.selectEquation = function(x) {
29 | this.currEq = x;
30 | $("#graph_inputs div.graph_input_wrapper").removeClass("active_equation");
31 | $("#graph_input_wrapper_"+x).addClass("active_equation");
32 | jsgcalc.draw();
33 | }
34 |
35 | this.setTool = function(t) {
36 | $("#tool_select a").removeClass("toolbar_selected");
37 | $("#tool_select_"+t).addClass("toolbar_selected");
38 |
39 | //Toolbox
40 | $(".toolbox").hide();
41 | $("#toolbox_"+t).show();
42 | $("#toolbox_"+t).css("top", $("#tool_select_"+t).offset().top - 23);
43 | $("#toolbox_"+t).css("right", $(document).width() - $("#tool_select_"+t).offset().left + 5);
44 |
45 | this.currtool = t;
46 | jsgcalc.draw();
47 | }
48 |
49 | this.doTrace = function(xval) {
50 | jsgcalc.draw();
51 | jsgcalc.drawTrace(jsgcalc.getEquation(this.currEq), "#000000", xval);
52 | }
53 |
54 | this.setGridlines = function(t) {
55 | $("#gridlines_select a").removeClass("option_selected");
56 | $("#gridlines_select_"+t).addClass("option_selected");
57 |
58 | this.gridlines = t;
59 | jsgcalc.draw();
60 | }
61 |
62 | this.hideSidebar = function() {
63 | $("#sidewrapper").hide();
64 | $("#hideSidebar").hide();
65 | $("#showSidebar").show();
66 | $("#toolbar").css("right", "0px");
67 | jsgcalc.resizeGraph($("#wrapper").width() - widthPlusPadding("#toolbar"), $("#wrapper").height());
68 |
69 | this.setTool(this.currtool);
70 | }
71 |
72 | this.showSidebar = function() {
73 | $("#sidewrapper").show();
74 | $("#hideSidebar").show();
75 | $("#showSidebar").hide();
76 | $("#toolbar").css("right", "252px");
77 | jsgcalc.resizeGraph($("#wrapper").width() - $("#sidewrapper").width() - widthPlusPadding("#toolbar"), $("#wrapper").height());
78 |
79 | this.setTool(this.currtool);
80 | }
81 |
82 | this.updateInputData = function() {
83 | jsgcalc.lines = [];
84 | $("#graph_inputs div.graph_input_wrapper").each(function() {
85 | jsgcalc.lines.push({equation : $("input", this).val(), color : $(".graph_color_indicator", this).css('backgroundColor')});
86 | });
87 | }
88 |
89 | this.evaluate = function() {
90 | this.updateInputData();
91 | jsgcalc.draw();
92 | this.refreshInputs();
93 | }
94 |
95 | this.findAvailableColor = function() {
96 | for(var color in this.lineColors) {
97 | if(this.lineColors[color] == -1)
98 | return color;
99 | }
100 | }
101 |
102 | //Update gui values
103 | this.updateValues = function() {
104 | $("input.jsgcalc_xmin").val(Math.round(jsgcalc.currCoord.x1 * 1000) / 1000);
105 | $("input.jsgcalc_xmax").val(Math.round(jsgcalc.currCoord.x2 * 1000) / 1000);
106 | $("input.jsgcalc_ymin").val(Math.round(jsgcalc.currCoord.y1 * 1000) / 1000);
107 | $("input.jsgcalc_ymax").val(Math.round(jsgcalc.currCoord.y2 * 1000) / 1000);
108 | }
109 |
110 | this.addInput = function() {
111 | this.updateInputData();
112 | var newcolor = this.findAvailableColor();
113 | this.lineColors[newcolor] = this.currInput;
114 | jsgcalc.lines.push({
115 | equation: "",
116 | color: newcolor
117 | });
118 | this.currInput++;
119 | this.refreshInputs();
120 | }
121 |
122 | this.refreshInputs = function() {
123 | var equations = jsgcalc.lines;
124 |
125 | $("#graph_inputs").html("");
126 | for(i in equations) {
127 | $("#graph_inputs").append("");
130 | $("#graph_color_indicator_"+i).css("backgroundColor", equations[i].color);
131 | this.lineColors[equations[i].color] = i;
132 | }
133 |
134 | $("#graph_inputs div.graph_input_wrapper").each(function() {
135 | $(this).bind("click", function() {
136 | var id = $(this).attr("id");
137 | var num = String(id).replace("graph_input_wrapper_", "");
138 | jsgui.selectEquation(num);
139 | });
140 | });
141 |
142 | this.currInput = i + 1;
143 |
144 | $("#graph_input_wrapper_"+this.currEq).addClass("active_equation");
145 | }
146 |
147 |
148 | }
149 |
150 | jsgui = new JSgui;
151 |
152 | $(document).ready(function() {
153 | jsgui.addInput();
154 | $(".toolbox_close a").click(function() {
155 | $(".toolbox").hide();
156 | })
157 |
158 | document.body.onselectstart = function () { return false; }
159 | });
160 |
--------------------------------------------------------------------------------