# Khan/khan-exercises

### Subversion checkout URL

You can clone with
or
.

Merge branch 'master' of https://github.com/Khan/khan-exercises

commit 08abd643e0df124ecf43600e9ad85077e14c987e 2 parents 87bc7c5 + d1f63a4
Gary Anderson authored
110 exercises/graphing_circles.html
 @@ -0,0 +1,110 @@ + + + + + Graphing circles + + + + +
+
+
+
+ randRange(-5, 5) + randRange(-5, 5) + randRange(1, 5) + H === 0 ? "x^2" : + expr(["^", ["+", "x", -H], 2]) + + K === 0 ? "y^2" : + expr(["^", ["+", "y", -K], 2]) + +
+ +

+ Graph the circle expr(["+", X2T, Y2T]) + = R * R. +

+ +
+
+
+ graphInit({ + range: 11, + scale: 20, + axisArrows: "<->", + tickStep: 1, + labelStep: 1, + gridOpacity: 0.05, + axisOpacity: 0.2, + tickOpacity: 0.4, + labelOpacity: 0.5 + }); + + label( [ 0, 11 ], "y", "above" ); + label( [ 11, 0 ], "x", "right" ); + + addMouseLayer(); + graph.circle = addCircleGraph(); +
+
+ +
+
+ Drag the center point and perimeter of the circle + to graph the equation. +
+
[ + graph.circle.center[0], + graph.circle.center[1], + graph.circle.radius] +
+
+ if (_.isEqual(guess, [0, 0, 2])) { + return ""; + } + return _.isEqual(guess, [H, K, R]); +
+
+ graph.circle.setCenter(guess[0], guess[1]); + graph.circle.setRadius(guess[2]); +
+
+ +
+

+ The equation of a circle with center + (\blue{h}, \green{k}) and radius + \pink{r} is + (x - \blue{h})^2 + (y - \green{k})^2 = + \pink{r}^2. +

+

+ We can rewrite the given equation as + (x - \blue{negParens(H)})^2 + (y - + \green{negParens(K)})^2 = + \pink{R}^2. +

+
+

+ Thus, the center of the circle should be + (\blue{H}, \green{K}) + and the radius should be + \pink{R}. +

+
+ circle([H, K], R, { + stroke: PURPLE, + strokeWidth: 1, + strokeDasharray: "- " + }).toBack(); +
+
+
+
+
+
+ +
183 exercises/graphing_circles_2.html
 @@ -0,0 +1,183 @@ + + + + + Graphing circles 2 + + + + +
+
+
+
+ randRange(-5, 5) + randRange(-5, 5) + randRange(1, 5) + H === 0 ? "x^2" : + expr(["^", ["+", "x", -H], 2]) + + K === 0 ? "y^2" : + expr(["^", ["+", "y", -K], 2]) + + -2 * H + -2 * K + H * H + K * K - R * R +
+ +

+ Graph the circle expr(["+", "x^2", "y^2", + D === 0 ? null : ["*", D, "x"], + E === 0 ? null : ["*", E, "y"], + F === 0 ? null : F]) + = 0. +

+ +
+
+
+ graphInit({ + range: 11, + scale: 20, + axisArrows: "<->", + tickStep: 1, + labelStep: 1, + gridOpacity: 0.05, + axisOpacity: 0.2, + tickOpacity: 0.4, + labelOpacity: 0.5 + }); + + label( [ 0, 11 ], "y", "above" ); + label( [ 11, 0 ], "x", "right" ); + + addMouseLayer(); + graph.circle = addCircleGraph(); +
+
+ +
+
+ Drag the center point and perimeter of the circle + to graph the equation. +
+
[ + graph.circle.center[0], + graph.circle.center[1], + graph.circle.radius] +
+
+ if (_.isEqual(guess, [0, 0, 2])) { + return ""; + } + return _.isEqual(guess, [H, K, R]); +
+
+ graph.circle.setCenter(guess[0], guess[1]); + graph.circle.setRadius(guess[2]); +
+
+ +
+

+ First, convert the equation to standard form by + completing the square. +

+
+

+ Group the \blue{x} and + \green{y} terms on the left side and + move the constant term to the right side. +

+

\qquad + \blue{ + (expr(["+", "x^2", ["*", D, "x"]])) + + (x^2) + } + + \green{ + (expr(["+", "y^2", ["*", E, "y"]])) + + (y^2) + } + \quad = \quad -F +

+
+
+

+ Add + + \blue{H * H} to both + sides to complete the square for the + \blue{x} term and + \green{K * K} to both + sides to complete the square for the + \green{y} term. +

+

\qquad + \blue{ + ( + expr(["+", "x^2", ["*", D, "x"], H * H]) + ) + + (x^2) + } + + \green{ + ( + expr(["+", "y^2", ["*", E, "y"], K * K]) + ) + + (y^2) + } \quad = \quad -F + + + \blue{H * H} + + + + \green{K * K} + +

+
+
+

Simplify and write each term as a square:

+

+

+
+

+ The equation of a circle with center + (\blue{h}, \green{k}) and radius + \pink{r} is + (x - \blue{h})^2 + (y - \green{k})^2 = + \pink{r}^2. +

+
+

+ Thus, the center of the circle should be + (\blue{H}, \green{K}) + and the radius should be + \pink{R}. +

+
+ circle([H, K], R, { + stroke: PURPLE, + strokeWidth: 1, + strokeDasharray: "- " + }).toBack(); +
+
+
+
+
+
+ +
331 exercises/law_of_cosines.html
 @@ -0,0 +1,331 @@ + + + + + Law of cosines + + + +
+
+
+
+ shuffle([null]. + concat(randRangeUnique(5, 16, 2))) + _.map(SIDES, function(s) { + if (s === null) { + return randRange(20, 140); + } + return null; + }) + solveTriangle({ + sides: SIDES.slice(), + angles: ANGLES.slice(), + sideLabels: SIDES.slice(), + angleLabels: _.map(ANGLES, function(a) { + return a == null ? a : a + "^\\circ"; + }), + vertexLabels: ["A", "B", "C"] + }) + SIDES.indexOf(null) + ["BC", "AC", "AB"][UNKNOWN] + "abc"[UNKNOWN] + SIDES[(UNKNOWN + 1) % 3] + SIDES[(UNKNOWN + 2) % 3] + roundTo(1, sqrt(KNOWN_SIDE_1 * KNOWN_SIDE_1 + + KNOWN_SIDE_2 * KNOWN_SIDE_2 - 2 * KNOWN_SIDE_1 * + KNOWN_SIDE_2 * cos(ANGLES[UNKNOWN] * PI / 180))) + + + + rand(2) ? randRange(-20, 20) : randRange(160, 200) + +
+ +

+ Find UNKNOWN_MEASURE. +

+ +
+

+ Round to the nearest tenth. +

+
+ TRIANGLE = addTriangle(_.extend(TRIANGLE, { + xPos: 1, + yPos: 1, + width: 10, + height: 6, + rot: ROTATION + })); + init({ + range: [[0, TRIANGLE.width + 2], + [0, TRIANGLE.height + 2]] + }); + + TRIANGLE.draw(); +
+
+
SOLUTION +
+ +
+
+

You can use the law of cosines:

+

\qquad + \pink{UNKNOWN_SIDE}^2 \quad = \quad + \blue{"abc"[(UNKNOWN + 1) % 3]}^2 + + \green{"abc"[(UNKNOWN + 2) % 3]}^2 - + 2\blue{"abc"[(UNKNOWN + 1) % 3]} + \green{"abc"[(UNKNOWN + 2) % 3]} + \space\cos(\pink{"ABC"[UNKNOWN]}) +

+
+ TRIANGLE.sideLabels = _.map(SIDES, function(s, n) { + return s == null ? "abc"[n] : + "abc"[n] + " = " + s; + }); + TRIANGLE.sideLabels[UNKNOWN] = "\\pink{" + + TRIANGLE.sideLabels[UNKNOWN] + "}"; + TRIANGLE.angleLabels[UNKNOWN] = "\\pink{" + + TRIANGLE.angleLabels[UNKNOWN] + "}"; + TRIANGLE.sideLabels[(UNKNOWN + 1) % 3] = "\\blue{" + + TRIANGLE.sideLabels[(UNKNOWN + 1) % 3] + "}"; + TRIANGLE.sideLabels[(UNKNOWN + 2) % 3] = "\\green{" + + TRIANGLE.sideLabels[(UNKNOWN + 2) % 3] + "}"; + TRIANGLE.color = GRAY; + TRIANGLE.draw(); +
+
+ +
+

Plug in the known values:

+

\qquad + \pink{UNKNOWN_SIDE}^2 \quad = \quad + \blue{KNOWN_SIDE_1}^2 + + \green{KNOWN_SIDE_2}^2 - + 2(\blue{KNOWN_SIDE_1}) + (\green{KNOWN_SIDE_2}) + \space\cos(\pink{ANGLES[UNKNOWN]^\circ}) +

+
+ +
+

\qquad + \pink{UNKNOWN_SIDE}^2 \quad = \quad + + KNOWN_SIDE_1 * KNOWN_SIDE_1 + + KNOWN_SIDE_2 * KNOWN_SIDE_2 + - + 2 * KNOWN_SIDE_1 * KNOWN_SIDE_2 + + \cdot\cos(\pink{ANGLES[UNKNOWN]^\circ}) +

+
+ +
+

Evaluate and simplify the right side:

+

\qquad + \pink{UNKNOWN_SIDE}^2 \quad \approx \quad + roundTo(9, + KNOWN_SIDE_1 * KNOWN_SIDE_1 + + KNOWN_SIDE_2 * KNOWN_SIDE_2 - + 2 * KNOWN_SIDE_1 * KNOWN_SIDE_2 * + cos(ANGLES[UNKNOWN] * Math.PI / 180)) + +

+
+ +
+

Take the positive square root of both sides (we only + need to worry about the positive square root because the + side of a triangle can't have negative length):

+

\qquad + \pink{UNKNOWN_SIDE} \quad \approx \quad + \sqrt{roundTo(9, + KNOWN_SIDE_1 * KNOWN_SIDE_1 + + KNOWN_SIDE_2 * KNOWN_SIDE_2 - + 2 * KNOWN_SIDE_1 * KNOWN_SIDE_2 * + cos(ANGLES[UNKNOWN] * Math.PI / 180)) + } +

+
+ +
+

Evaluate and round to the nearest tenth:

+

+
+ TRIANGLE.sideLabels[UNKNOWN] = "\\pink{" + + UNKNOWN_SIDE + " \\approx " + SOLUTION + "}"; + TRIANGLE.draw(); +
+
+
+
+ +
+
+ randRange(5, 15, 3) + solveTriangle({ + sides: SIDES.slice(), + angles: [null, null, null], + sideLabels: SIDES.slice(), + vertexLabels: ["A", "B", "C"] + }) + randRange(0, 2) + "ABC"[UNKNOWN] + SIDES[(UNKNOWN + 1) % 3] + SIDES[(UNKNOWN + 2) % 3] + fraction( + KNOWN_SIDE_1 * KNOWN_SIDE_1 + + KNOWN_SIDE_2 * KNOWN_SIDE_2 - + SIDES[UNKNOWN] * SIDES[UNKNOWN], + 2 * KNOWN_SIDE_1 * KNOWN_SIDE_2) + + roundTo(1, acos( + (KNOWN_SIDE_1 * KNOWN_SIDE_1 + + KNOWN_SIDE_2 * KNOWN_SIDE_2 - + SIDES[UNKNOWN] * SIDES[UNKNOWN]) / (2 * + KNOWN_SIDE_1 * KNOWN_SIDE_2)) / + PI * 180) + + + + rand(2) ? randRange(-20, 20) : randRange(160, 200) + +
+ +

+ Find m\angle UNKNOWN_ANGLE. +

+ +
+

+ Round to the nearest degree. +

+
+ TRIANGLE = addTriangle(_.extend(TRIANGLE, { + xPos: 1, + yPos: 1, + width: 10, + height: 6, + rot: ROTATION + })); + init({ + range: [[0, TRIANGLE.width + 2], + [0, TRIANGLE.height + 2]] + }); + + TRIANGLE.draw(); +
+
+
+ SOLUTION + \Large{^\circ} +
+ +
+
+

You can use the law of cosines:

+

\qquad + \pink{"abc"[UNKNOWN]}^2 \quad = \quad + \blue{"abc"[(UNKNOWN + 1) % 3]}^2 + + \green{"abc"[(UNKNOWN + 2) % 3]}^2 - + 2\blue{"abc"[(UNKNOWN + 1) % 3]} + \green{"abc"[(UNKNOWN + 2) % 3]} + \space\cos(\pink{"ABC"[UNKNOWN]}) +

+
+ TRIANGLE.sideLabels = _.map(SIDES, function(s, n) { + return "abc"[n] + " = " + s; + }); + TRIANGLE.sideLabels[UNKNOWN] = "\\pink{" + + TRIANGLE.sideLabels[UNKNOWN] + "}"; + TRIANGLE.vertexLabels[UNKNOWN] = "\\pink{" + + "ABC"[UNKNOWN] + "}"; + TRIANGLE.sideLabels[(UNKNOWN + 1) % 3] = "\\blue{" + + TRIANGLE.sideLabels[(UNKNOWN + 1) % 3] + "}"; + TRIANGLE.sideLabels[(UNKNOWN + 2) % 3] = "\\green{" + + TRIANGLE.sideLabels[(UNKNOWN + 2) % 3] + "}"; + TRIANGLE.color = GRAY; + TRIANGLE.draw(); +
+
+ +
+

+ Rewrite the law of cosines to solve for + \cos(\pink{"ABC"[UNKNOWN]}): +

+

\qquad + \cos(\pink{"ABC"[UNKNOWN]}) \quad = \quad + \dfrac{ + \blue{"abc"[(UNKNOWN + 1) % 3]}^2 + + \green{"abc"[(UNKNOWN + 2) % 3]}^2 - + \pink{"abc"[UNKNOWN]}^2 + }{2\blue{"abc"[(UNKNOWN + 1) % 3]} + \green{"abc"[(UNKNOWN + 2) % 3]}} +

+
+ +
+

Plug in the known values:

+

\qquad + \cos(\pink{"ABC"[UNKNOWN]}) \quad = \quad + \dfrac{ + \blue{KNOWN_SIDE_1}^2 + + \green{KNOWN_SIDE_2}^2 - + \pink{SIDES[UNKNOWN]}^2 + }{2(\blue{KNOWN_SIDE_1}) + (\green{KNOWN_SIDE_2})} +

+
+ +
+

Simplify the right side:

+

+
+ +
+

+ Evaluate the inverse cosine to find + m\angle UNKNOWN_ANGLE + and round to the nearest degree: +

+

+
+ TRIANGLE.angleLabels[UNKNOWN] = "\\pink{" + + SOLUTION + "^\\circ}"; + TRIANGLE.draw(); +
+
+
+
+ +
+
308 exercises/law_of_sines.html
 @@ -0,0 +1,308 @@ + + + + + Law of sines + + + +
+
+ +
+
+ randRange(20, 140) + randRange(20, 160 - ANGLE1) + randRange(5, 25) + shuffle([0, 1, 2]) + [ + KNOWN === 0 ? SIDELEN : null, + KNOWN === 1 ? SIDELEN : null, + KNOWN === 2 ? SIDELEN : null + ] + !!rand(2) + + [ + KNOWN === 0 ? ANGLE1 : + (UNKNOWN === 0 ^ GIVE_OPPOSITE ? null : ANGLE2), + KNOWN === 1 ? ANGLE1 : + (UNKNOWN === 1 ^ GIVE_OPPOSITE ? null : ANGLE2), + KNOWN === 2 ? ANGLE1 : + (UNKNOWN === 2 ^ GIVE_OPPOSITE ? null : ANGLE2) + ] + solveTriangle({ + sides: SIDES.slice(), + angles: ANGLES.slice(), + sideLabels: SIDES.slice(), + angleLabels: _.map(ANGLES, function(a) { + return a == null ? a : a + "^\\circ"; + }), + vertexLabels: ["A", "B", "C"] + }) + ["BC", "AC", "AB"][UNKNOWN] + roundTo(1,TRIANGLE.sides[UNKNOWN]) + + + rand(2) ? randRange(-20, 20) : randRange(160, 200) + +
+ +

+ Find UNKNOWN_MEASURE. +

+ +
+

+ Round to the nearest tenth. +

+
+ TRIANGLE = addTriangle(_.extend(TRIANGLE, { + xPos: 1, + yPos: 1, + width: 10, + height: 6, + rot: ROTATION + })); + init({ + range: [[0, TRIANGLE.width + 2], + [0, TRIANGLE.height + 2]] + }); + + TRIANGLE.draw(); + +
+
+
SOLUTION +
+ +
+
+

You can use the law of sines:

+

+
+ +
+

+ Fill in the unknown angle using the fact that the + angles of a triangle always sum to + 180^\circ. +

+

+
+ TRIANGLE.angleLabels[UNKNOWN] = "\\pink{" + + TRIANGLE.angles[UNKNOWN] + "^\\circ}"; + TRIANGLE.draw(); +
+
+ +
+

+ Set up a useful relationship using the law of sines: +

+

+
+ TRIANGLE.sideLabels[KNOWN] = "\\blue{" + + TRIANGLE.sides[KNOWN] + "}"; + TRIANGLE.angleLabels[KNOWN] = "\\blue{" + + TRIANGLE.angles[KNOWN] + "^\\circ}"; + TRIANGLE.sideLabels[UNKNOWN] = "\\pink{" + + UNKNOWN_MEASURE + "}"; + TRIANGLE.angleLabels[UNKNOWN] = "\\pink{" + + TRIANGLE.angles[UNKNOWN] + "^\\circ}"; + TRIANGLE.color = GRAY; + TRIANGLE.draw(); +
+
+ +
+

+ Solve for the unknown side: +

+

+ \qquad \pink{UNKNOWN_MEASURE} \quad = \quad + \dfrac{\blue{TRIANGLE.sides[KNOWN]} + \cdot + \sin(\pink{TRIANGLE.angles[UNKNOWN]^\circ}) + }{ + \sin(\blue{TRIANGLE.angles[KNOWN]^\circ})} +

+
+ +
+

Evaluate and round to the nearest tenth:

+

+
+ TRIANGLE.sideLabels[UNKNOWN] = "\\pink{" + + SOLUTION + "}"; + TRIANGLE.draw(); +
+
+
+
+ +
+
+ randRange(5, 15, 3) + shuffle([0, 1, 2]) + solveTriangle({ + sides: SIDES.slice(), + angles: [null, null, null], + sideLabels: SIDES.slice(), + vertexLabels: ["A", "B", "C"] + }) + _.map(TRIANGLE.angles, round) + "ABC"[UNKNOWN] + round(Math.asin((TRIANGLE.sides[UNKNOWN] * + sin(ANGLES[KNOWN] * Math.PI / 180)) / + TRIANGLE.sides[KNOWN]) / Math.PI * 180) + + + + rand(2) ? randRange(-20, 20) : randRange(160, 200) + +
+ +

+ Find m\angle UNKNOWN_MEASURE. +

+ +
+

+ Round to the nearest degree. +

+
+ TRIANGLE.angleLabels = [ + KNOWN === 0 ? ANGLES[0] + "^\\circ" : null, + KNOWN === 1 ? ANGLES[1] + "^\\circ" : null, + KNOWN === 2 ? ANGLES[2] + "^\\circ" : null + ]; + TRIANGLE = addTriangle(_.extend(TRIANGLE, { + xPos: 1, + yPos: 1, + width: 10, + height: 6, + rot: ROTATION + })); + init({ + range: [[0, TRIANGLE.width + 2], + [0, TRIANGLE.height + 2]] + }); + + TRIANGLE.draw(); +
+
+
+ SOLUTION + \Large{^\circ} +
+ +
+
+

You can use the law of sines:

+

+
+ +
+

+ Set up a useful relationship using the law of sines: +

+

+
+ TRIANGLE.sideLabels[KNOWN] = "\\blue{" + + TRIANGLE.sides[KNOWN] + "}"; + TRIANGLE.angleLabels[KNOWN] = "\\blue{" + + ANGLES[KNOWN] + "^\\circ}"; + TRIANGLE.sideLabels[UNKNOWN] = "\\pink{" + + TRIANGLE.sides[UNKNOWN] + "}"; + TRIANGLE.angleLabels[UNKNOWN] = "\\pink{?}"; + TRIANGLE.color = GRAY; + TRIANGLE.draw(); +
+
+ +
+

+ Solve for the sine of the unknown angle: +

+

+ \qquad \sin(\pink{m\angle UNKNOWN_MEASURE}) + \quad = \quad + \dfrac{\pink{TRIANGLE.sides[UNKNOWN]} + \cdot + \sin(\blue{ANGLES[KNOWN]^\circ}) + }{\blue{TRIANGLE.sides[KNOWN]}} +

+
+ +
+

Evaluate the right side:

+

+ \qquad \sin(\pink{m\angle UNKNOWN_MEASURE}) + \quad \approx \quad + roundTo(9, (TRIANGLE.sides[UNKNOWN] * + sin(ANGLES[KNOWN] * Math.PI / 180)) / + TRIANGLE.sides[KNOWN]) +

+
+ +
+

+ Evaluate the inverse sine to find + m\angle UNKNOWN_MEASURE and round to + the nearest degree: +

+

+
+ TRIANGLE.angleLabels[UNKNOWN] = "\\pink{" + + SOLUTION + "^\\circ}"; + TRIANGLE.draw(); +
+
+
+
+ +
+
3  khan-exercise.js
 @@ -316,7 +316,8 @@ var Khan = (function() { "math-model": ["ast"], "simplify": ["math-model", "ast", "expr-helpers", "expr-normal-form", "steps-helpers"], "congruency": ["angles", "interactive"], - "graphie-3d": ["graphie", "matrix"] + "graphie-3d": ["graphie", "matrix"], + "graphie-geometry": ["graphie", "matrix"] }, warnTimeout: function() {
268 utils/graphie-geometry.js
 @@ -970,3 +970,271 @@ $.extend(KhanUtil, { return shape; } }); + + +// The following triangley code creates hopefully more legible triangles +// TODO(eater): Collapse this stuff into the existing triangle code above +// without breaking a bunch of stuff (and refactor things so +// Quadrilateral no longer inherits from Triangle) +// +// angles[0] +// /\ +// / \ +// sides[1] / \ sides[2] +// / \ +// /________\ +// angles[2] sides[0] angles[1] +// + +// The following solves the triangle by filling in any side and angle measures +// that were not included. +// +// The sides and angles included must unambiguously specify one triangle. duh. +// If not, behavior is undefined. +// +KhanUtil.solveTriangle = function(triangle) { + var sides = triangle.sides; + var angles = triangle.angles; + var numSides = _.reduce(sides, function(n, side) { + return n + (typeof side === "number" ? 1 : 0); + }, 0); + var numAngles = _.reduce(angles, function(n, angle) { + return n + (typeof angle === "number" ? 1 : 0); + }, 0); + + // If we have 2 sides, we must have the angle opposite the unknown side. + // (SAS is the only valid postulate in this case) + // We can use the law of cosines to find the third side + if (numSides === 2) { + var missingSide = sides.indexOf(null); + var side1 = sides[(missingSide + 1) % 3]; + var side2 = sides[(missingSide + 2) % 3]; + sides[missingSide] = Math.sqrt(side1 * side1 + side2 * side2 - 2 * + side1 * side2 * Math.cos(angles[missingSide] * Math.PI / 180)); + numSides = 3; + } + + // If we have all three sides, we can use the law of cosines to find all + // the angles + if (numSides === 3) { + // Use law of cosines to find all the angles + angles = _.map(angles, function(angle, n) { + var oppSide = sides[n]; + var adjSide1 = sides[(n + 1) % 3]; + var adjSide2 = sides[(n + 2) % 3]; + return Math.acos((adjSide1 * adjSide1 + adjSide2 * adjSide2 - + oppSide * oppSide) / (2 * adjSide1 * adjSide2)); + }); + angles = _.map(angles, KhanUtil.toDegrees); + } + + // If we have 2 angles, we can easily fill in the third angle + if (numAngles === 2) { + var missingAngle = angles.indexOf(null); + angles[missingAngle] = 180 - angles[(missingAngle + 1) % 3] - + angles[(missingAngle + 2) % 3]; + numAngles = 3; + } + + // 3 angles and 1 side is enough to figure out the rest of the sides using + // the law of sines + if (numAngles === 3 && numSides >= 1) { + var knownSide = sides.indexOf(sides[0] || sides[1] || sides[2]); + sides[(knownSide + 1) % 3] = (sides[knownSide] * + Math.sin(angles[(knownSide + 1) % 3] * Math.PI / 180)) + / Math.sin(angles[knownSide] * Math.PI / 180); + sides[(knownSide + 2) % 3] = (sides[knownSide] * + Math.sin(angles[(knownSide + 2) % 3] * Math.PI / 180)) + / Math.sin(angles[knownSide] * Math.PI / 180); + } + + triangle.sides = sides; + triangle.angles = angles; + + triangle.isRight = function() { + return (this.angles[0] === 90 || this.angles[1] === 90 || + this.angles[2] === 90); + }; + + triangle.isScalene = function() { + return (this.angles[0] !== this.angles[1] && + this.angles[1] !== this.angles[2] && + this.angles[0] !== this.angles[2]); + } + + triangle.isNotDegenerate = function() { + return (this.sides[1] + this.sides[2] > this.sides[0] && + this.sides[0] + this.sides[2] > this.sides[1] && + this.sides[0] + this.sides[1] > this.sides[2]); + }; + + return triangle; +}; + + +KhanUtil.addTriangle = function(triangle) { + triangle =$.extend({ + sides: [], + angles: [], + points: [], + sideLabels: [], + angleLabels: [], + vertexLabels: [], + labels: [], + rot: 0, + xPos: 0, + yPos: 0, + width: 10, + height: 10, + color: KhanUtil.BLUE + }, triangle); + + var getDistance = function(point1, point2) { + return Math.sqrt((point1[0] - point2[0]) * (point1[0] - point2[0]) + + (point1[1] - point2[1]) * (point1[1] - point2[1])); + }; + + var rotatePoint = function(point, angle) { + var matrix = KhanUtil.makeMatrix([ + [Math.cos(angle), -Math.sin(angle), 0], + [Math.sin(angle), Math.cos(angle), 0], + [0, 0, 1] + ]); + var vector = KhanUtil.makeMatrix([[point[0]], [point[1]], [1]]); + var prod = KhanUtil.matrixMult(matrix, vector); + return [prod[0], prod[1]]; + }; + + var findCenterPoints = function(triangle, points) { + var Ax = points[0][0]; + var Ay = points[0][1]; + var Bx = points[1][0]; + var By = points[1][1]; + var Cx = points[2][0]; + var Cy = points[2][1]; + var D = 2 * (Ax * (By - Cy) + Bx * (Cy - Ay) + Cx * (Ay - By)); + var a = triangle.sides[0]; + var b = triangle.sides[1]; + var c = triangle.sides[2]; + var P = a + b + c; + var x1 = (a * Ax + b * Bx + c * Cx) / P; + var y1 = (a * Ay + b * By + c * Cy) / P; + var x = ((Ay * Ay + Ax * Ax) * (By - Cy) + (By * By + Bx * Bx) * + (Cy - Ay) + (Cy * Cy + Cx * Cx) * (Ay - By)) / D; + var y = ((Ay * Ay + Ax * Ax) * (Cx - Bx) + (By * By + Bx * Bx) * + (Ax - Cx) + (Cy * Cy + Cx * Cx) * (Bx - Ax)) / D; + return { + circumCenter: [x, y], + centroid: [1 / 3 * (Ax + Bx + Cx), 1 / 3 * (Ay + By + Cy)], + inCenter: [x1, y1] + }; + }; + + triangle.draw = function() { + var graphie = KhanUtil.currentGraph; + if (triangle.set != null) { + triangle.set.remove(); + } + _.each(triangle.labels, function(lbl) { + lbl.remove(); + }); + triangle.set = graphie.raphael.set(); + triangle.set.push(graphie.path(triangle.points.concat([true]),{ + stroke: triangle.color + })); + + var centerPoints = findCenterPoints(triangle, triangle.points); + _(3).times(function(i) { + if (triangle.angleLabels[i] != null) { + var ang = Math.atan2(centerPoints.inCenter[1] - + triangle.points[i][1], centerPoints.inCenter[0] - + triangle.points[i][0]); + + // The angle measure label needs to be further from the vertex + // for small angles and closer for large angles. This is an + // empirically determined formula for figuring out how far. + var labelDist = (3.51470560176242 - 0.5687298702748785) * + Math.exp(-0.037587715462826674 * triangle.angles[i]) + + 0.5687298702748785; + + triangle.labels.push(graphie.label([ + triangle.points[i][0] + Math.cos(ang) * labelDist, + triangle.points[i][1] + Math.sin(ang) * labelDist], + triangle.angleLabels[i], "center")); + } + }); + + _(3).times(function(i) { + if (triangle.sideLabels[i] != null) { + var x = (triangle.points[(i + 1) % 3][0] + + triangle.points[(i + 2) % 3][0]) / 2; + var y = (triangle.points[(i + 1) % 3][1] + + triangle.points[(i + 2) % 3][1]) / 2; + var ang; + var labelDist = 0.4; + if (triangle.angles[i] < 90) { + ang = Math.atan2(y - centerPoints.circumCenter[1], + x - centerPoints.circumCenter[0]); + } else if (triangle.angles[i] > 90) { + ang = Math.atan2(centerPoints.circumCenter[1] - y, + centerPoints.circumCenter[0] - x); + } else { + ang = Math.atan2(y - centerPoints.centroid[1], + x - centerPoints.centroid[0]); + } + + triangle.labels.push(graphie.label([x, y], + triangle.sideLabels[i], ang)); + } + }); + + _(3).times(function(i) { + if (triangle.vertexLabels[i] != null) { + var ang = Math.atan2(triangle.points[i][1] - + centerPoints.inCenter[1], triangle.points[i][0] - + centerPoints.inCenter[0]); + var labelDist = 0.4; + + var lbl = graphie.label([ + triangle.points[i][0] + Math.cos(ang) * labelDist, + triangle.points[i][1] + Math.sin(ang) * labelDist], + triangle.vertexLabels[i], "center"); + lbl.css("color", triangle.color); + triangle.labels.push(lbl); + } + }); + }; + + triangle.points[2] = [0, 0]; + triangle.points[1] = [triangle.sides[0], 0]; + triangle.points[0] = [Math.cos(triangle.angles[2] * Math.PI / 180) * + triangle.sides[1], Math.sin(triangle.angles[2] * Math.PI / 180) * + triangle.sides[1]]; + + // Rotate the triangle + triangle.points[0] = rotatePoint(triangle.points[0], triangle.rot * + Math.PI / 180); + triangle.points[1] = rotatePoint(triangle.points[1], triangle.rot * + Math.PI / 180); + triangle.points[2] = rotatePoint(triangle.points[2], triangle.rot * + Math.PI / 180); + + // Scale and translate the triangle such that it fits within the + // specified width/height constraint and is positioned at xPos, yPos + var minX = _.min(_.map(triangle.points, function(p) { return p[0]; })); + var maxX = _.max(_.map(triangle.points, function(p) { return p[0]; })); + var minY = _.min(_.map(triangle.points, function(p) { return p[1]; })); + var maxY = _.max(_.map(triangle.points, function(p) { return p[1]; })); + var xScale = triangle.width / (maxX - minX); + var yScale = triangle.height / (maxY - minY); + var scale = _.min([xScale, yScale]); + triangle.width = (maxX - minX) * scale; + triangle.height = (maxY - minY) * scale; + + triangle.points = _.map(triangle.points, function(p) { + return [(p[0] - minX) * scale + triangle.xPos, + (p[1] - minY) * scale + triangle.yPos]; + }); + + return triangle; +};
169 utils/interactive.js
 @@ -1120,8 +1120,175 @@ $.extend(KhanUtil, { return sorter; - } + }, + + // center: movable point + // radius: int + // circ: graphie circle + // perim: invisible mouse target for dragging/changing radius + addCircleGraph: function(options) { + var graphie = KhanUtil.currentGraph; + var circle =$.extend({ + center: [0, 0], + radius: 2 + }, options); + + circle.centerPoint = KhanUtil.addMovablePoint({ + graph: graphie, + coord: circle.center, + normalStyle: { + stroke: KhanUtil.BLUE, + fill: KhanUtil.BLUE + }, + snapX: 0.5, + snapY: 0.5 + }); + circle.circ = graphie.circle(circle.center, circle.radius, { + stroke: KhanUtil.BLUE, + fill: KhanUtil.ORANGE, + fillOpacity: 0 + }); + circle.perim = graphie.mouselayer.circle( + graphie.scalePoint(circle.center)[0], + graphie.scalePoint(circle.center)[1], + graphie.scaleVector(circle.radius)[0]).attr({ + "stroke-width": 20, + "opacity": 0.0 + }); + + $(circle.centerPoint.mouseTarget[0]).on( + "vmouseover vmouseout", function(event) { + if (circle.centerPoint.highlight) { + circle.circ.animate({ + stroke: KhanUtil.ORANGE, + "fill-opacity": 0.05 + }, 50); + } else { + circle.circ.animate({ + stroke: KhanUtil.BLUE, + "fill-opacity": 0 + }, 50); + } + }); + + circle.toFront = function() { + circle.circ.toFront(); + circle.perim.toFront(); + circle.centerPoint.visibleShape.toFront(); + circle.centerPoint.mouseTarget.toFront(); + }; + + circle.centerPoint.onMove = function(x, y) { + circle.toFront(); + circle.circ.attr({ + cx: graphie.scalePoint(x)[0], + cy: graphie.scalePoint(y)[1], + }); + circle.perim.attr({ + cx: graphie.scalePoint(x)[0], + cy: graphie.scalePoint(y)[1], + }); + }; + + circle.centerPoint.onMoveEnd = function(x, y) { + circle.center = [x, y]; + }; + + // circle.setCenter(x, y) moves the circle to the specified + // x, y coordinate as if the user had dragged it there. + circle.setCenter = function(x, y) { + circle.centerPoint.setCoord([x, y]); + circle.centerPoint.onMove(x, y); + circle.center = [x, y]; + }; + + // circle.setRadius(r) sets the circle's radius to the specified + // value as if the user had dragged it there. + circle.setRadius = function(r) { + circle.radius = r; + circle.perim.attr({ + r: graphie.scaleVector(r)[0], + }); + circle.circ.attr({ + rx: graphie.scaleVector(r)[0], + ry: graphie.scaleVector(r)[1] + }); + }; + +$(circle.perim[0]).css("cursor", "move"); + $(circle.perim[0]).on( + "vmouseover vmouseout vmousedown", function(event) { + if (event.type === "vmouseover") { + circle.highlight = true; + if (!KhanUtil.dragging) { + circle.circ.animate({ + stroke: KhanUtil.ORANGE, + "fill-opacity": 0.05 + }, 50); + circle.centerPoint.visibleShape.animate({ + stroke: KhanUtil.ORANGE, + fill: KhanUtil.ORANGE + }, 50); + } + + } else if (event.type === "vmouseout") { + circle.highlight = false; + if (!circle.dragging) { + circle.circ.animate({ + stroke: KhanUtil.BLUE, + "fill-opacity": 0 + }, 50); + circle.centerPoint.visibleShape.animate({ + stroke: KhanUtil.BLUE, + fill: KhanUtil.BLUE + }, 50); + } + + } else if (event.type === "vmousedown" && + (event.which === 1 || event.which === 0)) { + event.preventDefault(); + circle.toFront(); + +$(document).on("vmousemove vmouseup", function(event) { + event.preventDefault(); + circle.dragging = true; + KhanUtil.dragging = true; + + if (event.type === "vmousemove") { + // mouse{X|Y} are in pixels relative to the SVG + var mouseX = event.pageX - $(graphie.raphael. + canvas.parentNode).offset().left; + var mouseY = event.pageY -$(graphie.raphael. + canvas.parentNode).offset().top; + // can't go beyond 10 pixels from the edge + mouseX = Math.max(10, Math.min(graphie.xpixels - 10, + mouseX)); + mouseY = Math.max(10, Math.min(graphie.ypixels - 10, + mouseY)); + + // coord{X|Y} are the scaled coordinate values + var coordX = mouseX / graphie.scale[0] + + graphie.range[0][0]; + var coordY = graphie.range[1][1] - mouseY / + graphie.scale[1]; + + var radius = KhanUtil.getDistance( + circle.centerPoint.coord, [coordX, coordY]); + radius = Math.max(1, + Math.round(radius / 0.5) * 0.5); + circle.setRadius(radius); + } else if (event.type === "vmouseup") { + \$(document).off("vmousemove vmouseup"); + circle.dragging = false; + KhanUtil.dragging = false; + } + }); + } + }); + + return circle; + } });