Skip to content
Browse files

refactored telling_time_interactive, extracted analogClock to time.js…

…, added fixedDistance snap to interactive.js
  • Loading branch information...
1 parent 368284e commit bcc1b9deebda28fed6cc364ee047d375a8fd3f32 @stephjang stephjang committed Dec 18, 2011
View
32 exercises/telling_time.html
@@ -23,15 +23,31 @@
<div id="analog">
<div class="question">
<p>What time is it?</p>
- <div class="graphie" id="pie" style="float: left;">
- init({ range: [ [-4, 4 ], [ -4, 4] ], scale: 25 });
- PIECH = new analogClock( HOUR, MINUTE, 3.5, false );
- PIECH.draw();
- </div>
- <div class="graphie" id="digital" style="float: left;">
- init({ range: [ [-5, 5], [-3, 3] ], scale: 25 });
+ <div class="graphie" id="pie" style="float: right;">
+ <!-- init({ range: [ [-4, 4 ], [ -4, 4] ], scale: 45 });
+ PIECH = new analogClock( HOUR, MINUTE, 3.5, false );
+ PIECH.draw(); -->
+
+ init({
+ range: [ [-5, 5], [-5, 5] ]
+ });
+
+ addMouseLayer();
+
+ graph.point = addMovablePoint({
+ coord: [-4, 0]
+ });
+
+ graph.line = addMovableLineSegment({
+ pointA: graph.point,
+ coordZ: [0, 0]
+ });
+
</div>
- <div style="clear: both;"></div>
+ <!-- <div class="graphie" id="digital" style="float: left;">
+ init({ range: [ [-5, 5], [-3, 3] ], scale: 25 });
+ </div>
+ <div style="clear: both;"></div> -->
</div>
<div class="solution" data-type="multiple">
<p class="short">The time is <span class="sol"><var>HOUR</var></span> : <span class="sol"><var>NICE_MINUTE</var></span> <var>AM_PM</var></p>
View
267 exercises/telling_time_interactive.html
@@ -0,0 +1,267 @@
+<!DOCTYPE html>
+<html data-require="math math-format graphie interactive angles time">
+<head>
+ <meta charset="UTF-8" />
+ <title>Telling time, interactive</title>
+ <script src="../khan-exercise.js"></script>
+</head>
+<body>
+ <div class="exercise">
+ <div class="problems">
+ <div id="set-hands" data-weight="3">
+
+ <div class="vars">
+ <var id="HOUR">randRange( 1, 12 )</var>
+ <var id="MINUTE_INCREMENT">15</var>
+ <var id="MINUTE">randRange( 0, 60 / MINUTE_INCREMENT - 1 ) * MINUTE_INCREMENT</var>
+ <var id="MINUTE_IS_ZERO">MINUTE === 0</var>
+ <var id="MINUTE_FRACTION">KhanUtil.toFraction(MINUTE/60)</var>
+ <var id="NICE_MINUTE"> MINUTE &gt; 5 ? MINUTE : "0" + MINUTE</var>
+ </div>
+
+ <p class="question">Set the clock to <var>HOUR</var>:<var>NICE_MINUTE</var>. The hour hand is <span class="hint_blue" style="font-weight: bold">blue</span> and the minute hand is <span class="hint_orange" style="font-weight: bold">orange</span>.</p>
+
+ <div class="problem">
+
+ <div class="graphie" id="clock">
+
+ init({ range: [ [-4, 4 ], [ -4, 4 ] ], scale: 45 });
+
+ var clockRadius = 3.75;
+ var minuteSnapPoints = 12;
+ var hourSnapPoints = 12 * 60 / MINUTE_INCREMENT;
+
+ var outerPointRadius = clockRadius * 1.01;
+ var minuteRadius = clockRadius * 0.6;
+ var hourRadius = clockRadius * 0.45;
+
+ graph.minuteSnapDegrees = 360 / minuteSnapPoints;
+ graph.hourSnapDegrees = 360 / hourSnapPoints;
+
+ graph.clock = KhanUtil.addAnalogClock( { radius: clockRadius, minuteTicks: hourSnapPoints } );
+ graph.clock.draw();
+
+ addMouseLayer();
+
+ function movePartnerPoint( options ) {
+
+ var x = options.x;
+ var y = options.y;
+ var point = options.point;
+ var outerPoint = options.outerPoint;
+ var isOuterPoint = options.isOuterPoint;
+
+ var ratio = outerPoint.constraints.fixedDistance.dist / point.constraints.fixedDistance.dist;
+
+ if (isOuterPoint) {
+ ratio = 1 / ratio;
+ point.setCoord( [ x * ratio , y * ratio ] );
+ outerPoint.setCoord( [ x, y ] );
+ } else {
+ point.setCoord( [ x, y ] );
+ outerPoint.setCoord( [ x * ratio, y * ratio ] );
+ }
+
+ point.updateLineEnds();
+ return true;
+ }
+
+ graph.minutePoint = addMovablePoint({
+ coord: [ 0, minuteRadius ],
+ constraints: {
+ fixedDistance: {
+ dist: minuteRadius,
+ point: [ 0, 0 ],
+ snapPoints: 12
+ }
+ },
+ onMove: function( x, y ) {
+ return movePartnerPoint( { x: x, y: y, point: this, outerPoint: graph.outerMinutePoint, isOuterPoint: false } );
+ }
+ });
+
+ graph.outerMinutePoint = addMovablePoint({
+ coord: [ 0, outerPointRadius ],
+ constraints: {
+ fixedDistance: {
+ dist: outerPointRadius,
+ point: [ 0, 0 ],
+ snapPoints: 12
+ }
+ },
+ onMove: function( x, y ) {
+ return movePartnerPoint( { x: x, y: y, point: graph.minutePoint, outerPoint: this, isOuterPoint: true } );
+ }
+ })
+
+ graph.hourPoint = addMovablePoint({
+ coord: [ hourRadius * Math.cos( Math.PI/3 ), hourRadius * Math.sin( Math.PI/3 ) ],
+ constraints: {
+ fixedDistance: {
+ dist: hourRadius,
+ point: [ 0, 0 ],
+ snapPoints: hourSnapPoints
+ }
+ },
+ onMove: function( x, y ) {
+ return movePartnerPoint( { x: x, y: y, point: this, outerPoint: graph.outerHourPoint, isOuterPoint: false } );
+ },
+ normalStyle: {
+ fill: KhanUtil.BLUE,
+ stroke: KhanUtil.BLUE
+ },
+ highlightStyle: {
+ fill: KhanUtil.BLUE,
+ stroke: KhanUtil.BLUE
+ }
+ });
+
+ graph.outerHourPoint = addMovablePoint({
+ coord: [ outerPointRadius * Math.cos( Math.PI/3 ), outerPointRadius * Math.sin( Math.PI/3 ) ],
+ constraints: {
+ fixedDistance: {
+ dist: outerPointRadius,
+ point: [ 0, 0 ],
+ snapPoints: hourSnapPoints
+ }
+ },
+ onMove: function( x, y ) {
+ return movePartnerPoint( { x: x, y: y, point: graph.hourPoint, outerPoint: this, isOuterPoint: true } );
+ },
+ normalStyle: {
+ fill: KhanUtil.BLUE,
+ stroke: KhanUtil.BLUE
+ },
+ highlightStyle: {
+ fill: KhanUtil.BLUE,
+ stroke: KhanUtil.BLUE
+ }
+ })
+
+ graph.minuteHand = addMovableLineSegment({
+ pointA: graph.minutePoint,
+ coordZ: [ 0, 0 ],
+ fixed: true,
+ normalStyle: {
+ fill: KhanUtil.ORANGE,
+ stroke: KhanUtil.ORANGE,
+ "stroke-width": 10
+ },
+ });
+
+ graph.hourHand = addMovableLineSegment({
+ pointA: graph.hourPoint,
+ coordZ: [ 0, 0 ],
+ fixed: true,
+ normalStyle: {
+ fill: KhanUtil.BLUE,
+ stroke: KhanUtil.BLUE,
+ "stroke-width": 10
+ },
+ });
+
+ graph.centerPoint = addMovablePoint({
+ coord: [ 0, 0 ],
+ constraints: {
+ fixed: true
+ },
+ normalStyle: {
+ fill: "#fff",
+ stroke: "#000",
+ "stroke-width": 2
+ }
+ });
+
+ graph.correctMinuteAngle = KhanUtil.timeToDegrees( MINUTE );
+ graph.correctHourAngle = KhanUtil.timeToDegrees( 5 * (HOUR + MINUTE/60) );
+
+ graph.correctMinuteAngle = KhanUtil.roundToNearest( graph.minuteSnapDegrees, graph.correctMinuteAngle );
+ graph.correctHourAngle = KhanUtil.roundToNearest( graph.hourSnapDegrees, graph.correctHourAngle );
+
+ </div>
+ </div>
+
+ <div class="solution" data-type="custom">
+ <div class="instruction">
+ Drag the two hands so the clock reads <var>HOUR</var>:<var>NICE_MINUTE</var>.
+ </div>
+ <div class="guess">
+ [ graph.minutePoint.coord, graph.hourPoint.coord ]
+ </div>
+ <div class="validator-function">
+
+ var minuteAngle = KhanUtil.coordToDegrees( graph.minutePoint.coord );
+ var hourAngle = KhanUtil.coordToDegrees( graph.hourPoint.coord );
+
+ minuteAngle = KhanUtil.roundToNearest( graph.minuteSnapDegrees, minuteAngle );
+ hourAngle = KhanUtil.roundToNearest( graph.hourSnapDegrees, hourAngle );
+
+ return (minuteAngle == graph.correctMinuteAngle) &amp;&amp; (hourAngle == graph.correctHourAngle);
+
+ </div>
+ <div class="show-guess">
+ graph.minutePoint.moveTo( guess[0][0], guess[0][1], true );
+ graph.hourPoint.moveTo( guess[1][0], guess[1][1], true );
+ </div>
+ </div>
+
+ <div class="hints">
+
+ <p>The first part of the time is the hour. The second part of the time is the number of minutes past the hour. So, it is <var>HOUR</var> hours and <var>MINUTE</var> minutes.</p>
+
+ <div>
+ <p>The 12 <strong>long</strong> tick marks correspond to the hours in the day (assuming AM/PM time).</p>
+ <p>Each long tick mark is also an increment of 5 minutes, because 60 minutes / 12 tick marks = 5 minutes per tick mark.</p>
+ <div class="graphie" data-update="clock">
+ graph.clock.drawTicks( { n: 12, p: 0.8, tickAttr: { stroke: "#ff0", "stroke-opacity": 0.3, "stroke-linecap": "round" } } );
+ </div>
+ </div>
+
+ <div>
+ <p>Multiply each long tick mark by 5 to get the corresponding number of minutes. For example, the tick mark labeled "9" corresponds to 45 minutes. The tick mark labeled "12" is an exception; it corresponds to 0 minutes.</p>
+ </div>
+
+ <div>
+ <p>If it is <strong>zero</strong> minutes, the hour hand belongs directly on the corresponding hour mark. However, for any other number of minutes, the hour hand should be proportionally past the hour mark.</p>
+ <p>For example, if it's 5 hours and 45 minutes, the hour hand should be <code>\Large{ \frac{45}{60} = \frac{3}{4} }</code> of the way past the 5 hour mark.</p>
+ </div>
+
+ <div>
+ <p data-if="MINUTE_IS_ZERO">
+ The hour hand should be exactly at the <var>HOUR</var> hour mark. The minute hand should be at the tick mark labeled "12".
+ </p>
+ <p data-else>
+ The hour hand should be <code>\Large{ \frac{<var>MINUTE</var>}{60} = \frac{<var>MINUTE_FRACTION[0]</var>}{<var>MINUTE_FRACTION[1]</var>} }</code> of the way past the <var>HOUR</var> hour mark. The minute hand should be at the tick mark labeled <code>\Large{ \frac{<var>MINUTE</var>}{5} = <var>MINUTE/5</var>}</code>.
+ </p>
+ <div class="graphie" data-update="clock">
+
+ var minuteRadius = graph.minutePoint.constraints.fixedDistance.dist;
+ var hourRadius = graph.hourPoint.constraints.fixedDistance.dist;
+
+ var minuteCoord = KhanUtil.degreesToCoord( minuteRadius, graph.correctMinuteAngle );
+ var hourCoord = KhanUtil.degreesToCoord( hourRadius, graph.correctHourAngle );
+
+ graph.minutePoint.moveTo( minuteCoord[0], minuteCoord[1], true );
+ graph.hourPoint.moveTo( hourCoord[0], hourCoord[1], true );
+
+ </div>
+ </div>
+
+ </div>
+ </div>
+
+ <div id="set-hands-20-minutes" data-type="set-hands" data-weight="2">
+ <div class="vars">
+ <var id="MINUTE_INCREMENT">20</var>
+ </div>
+ </div>
+
+ <div id="set-hands-30-minutes" data-type="set-hands" data-weight="2">
+ <div class="vars">
+ <var id="MINUTE_INCREMENT">30</var>
+ </div>
+ </div>
+ </div>
+ </div>
+</body>
+</html>
View
2 exercises/unit_circle.html
@@ -30,7 +30,7 @@
Move the orange point around the unit circle and select an angle in order to find the <var>FNNAME</var> value above.
</p>
<div class="graphie" id="unitcircle">
- initUnitCircle( DEGREES );
+ initUnitCircle( false );
</div>
</div>
View
5 utils/angles.js
@@ -23,6 +23,11 @@ jQuery.extend( KhanUtil, {
toRadians: function( degrees ) {
return degrees * Math.PI / 180;
},
+
+ // Convert a radian value to a degree value
+ toDegrees: function( radians ) {
+ return radians * 180 / Math.PI;
+ },
wrongCommonAngle: function( angleIdx, i ) {
// i is a value from 1 to 3
View
3 utils/graphie-helpers.js
@@ -462,7 +462,7 @@ function analogClock( hour, minute, radius, labelShown ){
this.minute = minute;
this.radius = radius;
this.set = KhanUtil.currentGraph.raphael.set();
-
+
this.graph = KhanUtil.currentGraph;
this.draw = function(){
for( var x = 0; x < 12; x++ ){
@@ -488,7 +488,6 @@ function analogClock( hour, minute, radius, labelShown ){
};
}
-
// for line graph intuition
function updateEquation() {
var graph = KhanUtil.currentGraph;
View
42 utils/interactive.js
@@ -302,14 +302,50 @@ jQuery.extend( KhanUtil, {
// can't go beyond 10 pixels from the edge
mouseX = Math.max(10, Math.min(graph.xpixels-10, mouseX));
mouseY = Math.max(10, Math.min(graph.ypixels-10, mouseY));
-
+
// snap to grid
if (movablePoint.snapX) {
- mouseX = Math.round(mouseX / (graph.scale[0] * movablePoint.snapX)) * (graph.scale[0] * movablePoint.snapX);
+ mouseX = Math.round(mouseX / (graph.scale[0] * movablePoint.snapX)) * (graph.scale[0] * movablePoint.snapX);
}
if (movablePoint.snapY) {
- mouseY = Math.round(mouseY / (graph.scale[1] * movablePoint.snapY)) * (graph.scale[1] * movablePoint.snapY);
+ mouseY = Math.round(mouseY / (graph.scale[1] * movablePoint.snapY)) * (graph.scale[1] * movablePoint.snapY);
}
+
+ // NOTE: redundancy with functions in angles.js and time.js
+ // snap to points around circle
+ if ( movablePoint.constraints.fixedDistance.snapPoints ) {
+
+ var snapRadians = 2 * Math.PI / movablePoint.constraints.fixedDistance.snapPoints;
+ var radius = movablePoint.constraints.fixedDistance.dist;
+
+ // TODO: make this so that it's relative to the center of the movablePoint
+ // get coordinates relative to origin of graph
+ var centerCoord = movablePoint.constraints.fixedDistance.point;
+ var centerX = (centerCoord[0] - graph.range[0][0]) * graph.scale[0];
+ var centerY = (-centerCoord[1] + graph.range[1][1]) * graph.scale[1];
+
+ var mouseXrel = mouseX - centerX;
+ var mouseYrel = -mouseY + centerY;
+ var radians = Math.atan(mouseYrel / mouseXrel);
+ var outsideArcTanRange = (mouseXrel < 0);
+
+ // correct so that angles increase from 0 to 2 pi as you go around the circle
+ if (outsideArcTanRange) {
+ radians += Math.PI;
+ }
+
+ // perform the snap
+ radians = Math.round(radians / snapRadians) * snapRadians;
+
+ // convert from radians back to pixels
+ mouseXrel = radius * Math.cos(radians);
+ mouseYrel = radius * Math.sin(radians);
+
+ // convert back to coordinates relative to graphie canvas
+ mouseX = mouseXrel + centerX;
+ mouseY = - mouseYrel + centerY;
+ }
+
// coord{X|Y} are the scaled coordinate values
var coordX = mouseX / graph.scale[0] + graph.range[0][0];
var coordY = graph.range[1][1] - mouseY / graph.scale[1];
View
123 utils/time.js
@@ -0,0 +1,123 @@
+jQuery.extend( KhanUtil, {
+
+ addAnalogClock: function( options ) {
+
+ var analogClock = jQuery.extend(true, {
+ graph: KhanUtil.currentGraph,
+ set: KhanUtil.currentGraph.raphael.set(),
+ radius: 3.5,
+ labelShown: true,
+ hour: false,
+ minute: false,
+ hourTicks: 12,
+ minuteTicks: 48,
+ hourTickLength: 0.8, // make this more understandable
+ minuteTickLength: 0.95
+ }, options);
+
+ analogClock.drawTicks = function ( options ) {
+ var n = options.n;
+ var p = options.p;
+ var x, y, outerPoint, innerPoint;
+
+ for( var i = 0; i < n; i++ ) {
+ x = this.radius * Math.cos( 2 * Math.PI * i/n );
+ y = this.radius * Math.sin( 2 * Math.PI * i/n );
+ outerPoint = [ x, y ];
+ innerPoint = [ p*x, p*y ];
+ var line = this.graph.line( outerPoint, innerPoint );
+
+ if ( options.tickAttr ) {
+ line.attr( options.tickAttr );
+ }
+
+ this.set.push(line);
+ }
+ }
+
+ analogClock.drawLabels = function() {
+ for( var i = 1; i < 13; i++ ){
+ this.set.push( this.graph.label( [ 0.7 * this.radius * Math.sin( 2 * Math.PI * i/12 ), 0.7 * this.radius * Math.cos( 2 * Math.PI * i/12 ) ], i ) );
+ }
+ return this.set;
+ }
+
+ analogClock.drawHands = function() {
+ this.set.push( this.graph.line( [ 0.45 * this.radius * Math.sin( 2 * Math.PI * this.hour/12 + ( this.minute / 60 ) / 12 * 2 * Math.PI ), 0.45 * this.radius * Math.cos( 2 * Math.PI * this.hour/12 + ( this.minute / 60 ) / 12 * 2 * Math.PI ) ], [ 0, 0 ] ) );
+ this.set.push( this.graph.line( [ 0.6 * this.radius * Math.sin( ( this.minute / 60 ) * 2 * Math.PI ), 0.6 * this.radius * Math.cos( ( this.minute / 60 ) * 2 * Math.PI ) ], [ 0, 0 ] ) );
+ }
+
+ analogClock.draw = function() {
+
+ if ( this.hourTicks ) {
+ this.drawTicks( {n: this.hourTicks, p: this.hourTickLength} );
+ }
+
+ if ( this.minuteTicks ) {
+ this.drawTicks( {n: this.minuteTicks, p: this.minuteTickLength} );
+ }
+
+ // draw circles
+ this.set.push( this.graph.circle( [ 0, 0 ], this.radius ) );
+ this.set.push( this.graph.circle( [ 0, 0 ], this.radius/ 40 ) );
+
+
+ if ( this.labelShown ) {
+ this.drawLabels();
+ }
+
+ if ( this.hour && this.minute ) {
+ this.drawHands();
+ }
+
+ return this.set;
+ };
+
+ return analogClock;
+ },
+
+ // function to map minutes, which start at positive y-axis and increase clockwise,
+ // to angle in degrees, which starts at positive x-axis and increases counterclockwise
+ // (e.g., 0 minutes => 90 degrees; 15 minutes => 0 degrees; 30 minutes => 270 degrees; 45 minutes => 180 degrees)
+ timeToDegrees: function( minutes ) {
+
+ // p is the proportion of total time
+ var p = minutes / 60;
+ var angleProportion;
+
+ if ( p <= 0.25 ) {
+ angleProportion = (0.25 - p);
+ } else {
+ angleProportion = (1.25 - p);
+ }
+
+ return 360 * angleProportion;
+ },
+
+ // TODO: not limited to time. belongs in another package.
+ coordToDegrees: function( coord ) {
+ var radians = Math.atan(coord[1]/coord[0]);
+
+ if (coord[0] < 0) {
+ radians += Math.PI;
+ } else if (coord[1] < 0) {
+ radians += 2*Math.PI;
+ }
+
+ return KhanUtil.toDegrees(radians);
+ },
+
+ // TODO: not limited to time. belongs in another package.
+ var x = radius * Math.cos( radians );
+ var y = radius * Math.sin( radians );
+ return [x, y];
+ },
+
+ // TODO: not limited to time... belongs in math.js
+ // Round a number to the nearest increment
+ // E.g., if increment = 30 and num = 40, return 30. if increment = 30 and num = 45, return 60.
+ roundToNearest: function( increment, num ) {
+ return Math.round( num / increment ) * increment;
+ },
+
+});

0 comments on commit bcc1b9d

Please sign in to comment.
Something went wrong with that request. Please try again.