diff --git a/content/functions/components/card-graph/card-graph.scss b/content/functions/components/card-graph/card-graph.scss
new file mode 100644
index 000000000..ffb8e4a67
--- /dev/null
+++ b/content/functions/components/card-graph/card-graph.scss
@@ -0,0 +1,86 @@
+
+x-card-graph svg {
+ overflow: visible;
+}
+
+x-card-graph .ranges {
+ stroke-width: 2px;
+ stroke: $medium-grey;
+ stroke-dasharray: 4;
+
+ .range-tick,.range-line {
+ transition: 0.2s;
+ }
+}
+
+x-card-graph .ranges:not(.hover) {
+ .range-line {
+ opacity: 0;
+ }
+ .range-tick {
+ opacity: 1;
+ }
+}
+
+x-card-graph .ranges.hover {
+ .range-lines:not(.hover) line {
+ opacity: 0;
+ }
+ .range-lines.hover {
+ .range-line {
+ opacity: 1;
+ }
+ .range-tick {
+ opacity: 0;
+ }
+ }
+}
+
+x-card-graph .dot {
+ stroke-width: 2px;
+ stroke: $medium-grey;
+ fill: $light-grey;
+}
+
+x-card-graph .card-content {
+ cursor: pointer;
+ transition: 0.2s;
+
+ .card-circle {
+ fill: none;
+ stroke: $medium-grey;
+ stroke-width: 2px;
+ }
+
+ .card-fill {
+ fill: $grey-background;
+ stroke: none;
+ }
+
+ text {
+ font-size: 12px;
+ }
+}
+
+x-card-graph .card-instruction {
+ fill: $medium-grey;
+ font-style: italic;
+ transition: 0.2s;
+ font-size: 22px;
+}
+
+x-card-graph .card-description {
+ transition: 0.2s;
+ font-size: 22px;
+}
+
+x-card-graph .card-outline {
+ fill: none;
+ stroke: $light-grey;
+ stroke-width: 2px;
+ stroke-dasharray: 4;
+}
+
+x-card-graph svg .labels {
+ pointer-events: none;
+}
\ No newline at end of file
diff --git a/content/functions/components/card-graph/card-graph.ts b/content/functions/components/card-graph/card-graph.ts
new file mode 100644
index 000000000..72143067f
--- /dev/null
+++ b/content/functions/components/card-graph/card-graph.ts
@@ -0,0 +1,212 @@
+// =============================================================================
+// Card Graph Component
+// (c) Mathigon
+// =============================================================================
+
+import {Step} from '@mathigon/studio';
+import {$N, animate, CustomElementView, Draggable, ease, ElementView, hover, register, SVGView} from '@mathigon/boost';
+import {Point} from '@mathigon/euclid';
+import {lerp} from '@mathigon/fermat';
+import {shuffle} from '@mathigon/fermat/src/random';
+import {CoordinateSystem} from '../../../shared/types';
+
+type Plot = {
+ color: string,
+ function: (x: number) => number,
+}
+
+type Card = {
+ description?: string,
+ imagePath?: string,
+ label?: string,
+ hint?: string,
+ point: Point,
+ domain?: number[],
+ invertDomainLines?: boolean,
+}
+
+@register('x-card-graph')
+export class CardGraph extends CustomElementView {
+ private $graph!: CoordinateSystem;
+ private $overlay!: ElementView;
+ private $instructionText!: ElementView;
+ private $descriptionText!: ElementView;
+ private graphWidth!: number;
+ private graphHeight!: number;
+ private $step?: Step;
+
+ ready() {
+ this.$graph = this.$('x-coordinate-system')! as CoordinateSystem;
+ this.$overlay = this.$graph.$svg.$('.overlay')!;
+
+ this.graphWidth = parseInt(this.$graph.attr('width'));
+ this.graphHeight = parseInt(this.$graph.attr('height'));
+
+ this.$graph.$svg.setAttr('height', this.graphHeight + 100);
+ this.$graph.$svg.setAttr('viewBox', `0 0 ${this.graphWidth} ${this.graphHeight - 50}`);
+
+ this.$instructionText = $N('text', {class: 'card-instruction', 'alignment-baseline': 'middle', 'text-anchor': 'middle', transform: `translate(${this.graphWidth / 2}, ${this.graphHeight + 20})`}, this.$overlay);
+ this.$descriptionText = $N('text', {class: 'card-description', 'alignment-baseline': 'middle', 'text-anchor': 'middle', transform: `translate(${this.graphWidth / 2}, ${this.graphHeight + 20})`}, this.$overlay);
+
+ this.$instructionText.text = 'Drag each card onto its corresponding point';
+ }
+
+ bindStep($step: Step) {
+ this.$step = $step;
+ }
+
+ setPlots(plots: Plot[]) {
+ this.$graph.setFunctions(...plots.map((plot) => plot.function));
+
+ for (let i = 0; i < plots.length; i++) {
+this.$graph.$('.plot')!.$$('g')[i].$('path')!.setAttr('class', plots[i].color);
+ }
+ }
+
+ setCards(cards: Card[]) {
+ cards = shuffle(cards);
+
+ const $rangeGroup = $N('g', {class: 'ranges'}, this.$overlay);
+ const $dotGroup = $N('g', {class: 'dots'}, this.$overlay);
+ const $cardGroup = $N('g', {class: 'cards'}, this.$overlay);
+
+ let $hoverDot: ElementView|null = null;
+ let $dragCard: ElementView|null = null;
+
+ const _$cards = cards.map((card, i) => {
+ const origin = new Point(this.graphWidth * (i + 1 / 2) / cards.length, -30);
+
+ const _$outline = $N('circle', {cx: origin.x, cy: origin.y, r: 30, class: 'card-outline'}, $cardGroup);
+
+ const $g = $N('g', {transform: `translate(${origin.x}, ${origin.y})`}, $cardGroup) as SVGView;
+
+ const $content = $N('g', {class: 'card-content'}, $g);
+
+ if (card.imagePath) {
+ $N('image', {href: card.imagePath, x: -30, y: -30, width: 60, height: 60}, $content);
+ } else {
+ $N('circle', {r: 30, class: 'card-fill'}, $content);
+ $N('text', {'alignment-baseline': 'middle', 'text-anchor': 'middle'}, $content).text = card.label!;
+ }
+
+ const _$circle = $N('circle', {cx: 0, cy: 0, r: 30, class: 'card-circle'}, $content);
+
+ const dotPosition = this.$graph.toViewportCoords(card.domain ? new Point((card.domain[0] + card.domain[1]) / 2, card.point.y) : card.point);
+ const $dot = $N('circle', {class: 'dot', transform: `translate(${dotPosition.x}, ${dotPosition.y})`, r: 15}, $dotGroup);
+
+ const $rangeLines = $N('g', {class: 'range-lines'}, $rangeGroup);
+
+ if (card.domain) {
+ const rangeStartPoint = new Point(card.domain[0]!, card.point.y);
+ const rangeEndPoint = new Point(card.domain[1]!, card.point.y);
+
+ const rangeStartPosition = this.$graph.toViewportCoords(rangeStartPoint);
+ const rangeEndPosition = this.$graph.toViewportCoords(rangeEndPoint);
+
+ const axisHeight = this.$graph.$yAxis!.height;
+ const yOffset = (this.graphHeight - axisHeight) / 2;
+
+ const _$rangeSpan = $N('line', {x1: rangeStartPosition.x, x2: rangeEndPosition.x, y1: rangeStartPosition.y, y2: rangeEndPosition.y}, $rangeLines);
+
+ const _$rangeStartLine = $N('line', {class: 'range-line', x1: rangeStartPosition.x, x2: rangeStartPosition.x, y1: yOffset, y2: axisHeight + yOffset}, $rangeLines);
+ const _$rangeEndLine = $N('line', {class: 'range-line', x1: rangeEndPosition.x, x2: rangeEndPosition.x, y1: yOffset, y2: axisHeight + yOffset}, $rangeLines);
+
+ const _$rangeStartTick = $N('line', {class: 'range-tick', x1: rangeStartPosition.x, x2: rangeStartPosition.x, y1: rangeStartPosition.y - 10, y2: rangeStartPosition.y + 10}, $rangeLines);
+ const _$rangeEndTick = $N('line', {class: 'range-tick', x1: rangeEndPosition.x, x2: rangeEndPosition.x, y1: rangeStartPosition.y - 10, y2: rangeStartPosition.y + 10}, $rangeLines);
+ }
+
+ hover($g, {
+ enter: () => {
+ if (!$dragCard && card.description) {
+ this.$descriptionText.text = card.description;
+ this.$descriptionText.css('opacity', 1);
+ this.$instructionText.css('opacity', 0);
+ }
+ },
+ exit: () => {
+ if (!$dragCard && card.description) {
+ this.$descriptionText.css('opacity', 0);
+ this.$instructionText.css('opacity', 1);
+ }
+ }
+ });
+
+ hover($dot, {
+ enter: () => {
+ $hoverDot = $dot;
+
+ $rangeGroup.addClass('hover');
+ $rangeLines.addClass('hover');
+
+ if ($dragCard) {
+$dragCard.$('.card-content')!.setAttr('transform', `scale(${1 / 2})`);
+ }
+ },
+ exit: () => {
+ $hoverDot = null;
+
+ $rangeGroup.removeClass('hover');
+ $rangeLines.removeClass('hover');
+
+ if ($dragCard) {
+$dragCard.$('.card-content')!.setAttr('transform', `scale(1)`);
+ }
+ }
+ });
+
+ const drag = new Draggable($g, this.$graph.$svg, {useTransform: true, margin: -60});
+
+ drag.setPosition(origin.x, origin.y);
+
+ drag.on('start', () => {
+ $dragCard = $g;
+
+ $g.css('pointer-events', 'none');
+ });
+
+ drag.on('end', () => {
+ $dragCard = null;
+
+ const dropPosition = drag.position;
+ let restPosition: Point;
+
+ if ($hoverDot == $dot) {
+ if (this.$step) {
+ this.$step.addHint('correct');
+
+ if (card.hint) {
+ this.$step.addHint(card.hint);
+ }
+
+ this.$step.score('card' + i);
+ }
+
+ restPosition = dotPosition;
+
+ this.$descriptionText.css('opacity', 0);
+ this.$instructionText.css('opacity', 1);
+ } else if ($hoverDot) {
+ if (this.$step) {
+ this.$step.addHint('incorrect');
+ }
+
+ restPosition = origin;
+
+ $content.setAttr('transform', 'scale(1)');
+ $g.css('pointer-events', 'all');
+ } else {
+ restPosition = origin;
+
+ $g.css('pointer-events', 'all');
+ }
+
+ animate((p) => {
+ const q = ease('sine', p);
+ drag.setPosition(lerp(dropPosition.x, restPosition.x, q), lerp(dropPosition.y, restPosition.y, q));
+ }, 500);
+ });
+
+ return $g;
+ });
+ }
+}
diff --git a/content/functions/components/draw-graph/draw-graph.scss b/content/functions/components/draw-graph/draw-graph.scss
new file mode 100644
index 000000000..2cd742e0b
--- /dev/null
+++ b/content/functions/components/draw-graph/draw-graph.scss
@@ -0,0 +1,106 @@
+x-draw-graph {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 2em auto;
+
+ .crosshair {
+ pointer-events: none;
+ }
+
+ x-coordinate-system {
+ margin: 0 auto;
+ }
+
+ >button {
+ margin: 20px auto 2em auto;
+ }
+
+ .axes {
+ pointer-events: none;
+ }
+
+ .plot.solution {
+ pointer-events: none;
+ transition: 1s;
+ }
+ .plot.solution[show=true] {
+ opacity: 1;
+ }
+ .plot.solution[show=false] {
+ opacity: 0;
+ }
+
+ .overlay {
+ .hints {
+ pointer-events: none;
+
+ >* {
+ transition: 2s;
+ opacity: 0;
+ }
+ >*[show=true] {
+ opacity: 1;
+ }
+
+ .hint-line {
+ stroke-width: 2px;
+ stroke: $blue;
+ stroke-dasharray: 6 4;
+ }
+
+ .hint-circle {
+ fill: $blue;
+ }
+ }
+ .plot-points {
+ circle {
+ cursor: pointer;
+ }
+ }
+ }
+
+ .labels {
+ pointer-events: none;
+ }
+
+ .scoring-row {
+ display: flex;
+ margin-top: 20px;
+ align-items: center;
+
+ .judge-text {
+ margin-left: 20px;
+ }
+
+ .scores {
+ display: flex;
+
+ >div {
+ display: flex;
+ margin-left: 20px;
+ justify-content: center;
+ background-color: $light-grey;
+
+ min-width: 50px;
+ min-height: 50px;
+
+ div {
+ align-self: center;
+ }
+ }
+ }
+
+ .scores[show=true] {
+ >div {
+ transform: rotateY(0);
+ }
+ }
+
+ .scores[show=false] {
+ >div {
+ transform: rotateY(90deg);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/content/functions/components/draw-graph/draw-graph.ts b/content/functions/components/draw-graph/draw-graph.ts
new file mode 100644
index 000000000..96568b597
--- /dev/null
+++ b/content/functions/components/draw-graph/draw-graph.ts
@@ -0,0 +1,503 @@
+// =============================================================================
+// Draw Graph Component
+// (c) Mathigon
+// =============================================================================
+
+import {Step} from '@mathigon/studio';
+import {$N, CustomElementView, ElementView, register, slide, SVGView} from '@mathigon/boost';
+import {last} from '@mathigon/core';
+import {Point} from '@mathigon/euclid';
+import {clamp, lerp} from '@mathigon/fermat';
+import {CoordinateSystem} from '../../../shared/types';
+
+// TODO: Import d3 helpers for curvy paths
+// import { path as d3path, line as d3line, curveMonotoneX } from 'd3';
+
+type PlotPoint = {
+ point: Point,
+ position: Point,
+ $el: SVGView,
+}
+
+type HintPoint = {
+ x: number,
+ hint: string,
+ id?: string,
+ relevanceThresholdDistance?: number,
+ $el?: ElementView,
+ $g?: ElementView,
+ drawCircle?: boolean,
+ drawLine?: boolean,
+ revealed?: boolean,
+}
+
+function unlerp(a: number, b: number, t: number) {
+ return (t - a) / (b - a);
+}
+
+@register('x-draw-graph')
+export class DrawGraph extends CustomElementView {
+ private $graph!: CoordinateSystem;
+ private $hints!: ElementView;
+ private $solution!: ElementView;
+ private $plot!: ElementView;
+
+ private points: PlotPoint[] = [];
+ private hintPoints: HintPoint[] = [];
+ private scored = false;
+ private firstHint = true;
+
+ private $step?: Step;
+ private scoreThreshold = 0;
+ private snap: Point = new Point(0, 0);
+
+ private solutionFunction!: (x: number) => number;
+
+ private $submitButton!: ElementView;
+
+ private $scoreContainer!: ElementView;
+ private $scores!: ElementView[];
+
+ private $judgeText!: ElementView;
+
+ ready() {
+ // Parse attributes
+ this.scoreThreshold = this.attr('score-threshold') ?
+ Number.parseFloat(this.attr('score-threshold')) : 0.95;
+
+ let snapString = this.attr('snap');
+ if (snapString) {
+ // Surely there is an existing method for parsing these strings to points?
+ snapString = snapString.replace('(', '');
+ snapString = snapString.replace(')', '');
+ const snapNumbers = snapString.split(',');
+ if (snapNumbers.length == 1) {
+ const snapNumber = Number.parseFloat(snapNumbers[0].trim());
+ this.snap = new Point(snapNumber, snapNumber);
+ } else {
+ const x = Number.parseFloat(snapNumbers[0].trim());
+ const y = Number.parseFloat(snapNumbers[1].trim());
+ this.snap = new Point(x, y);
+ }
+ }
+
+ // Set up judge text
+ this.$judgeText = this.$('.judge-text')!;
+ const refreshJudgeText = () => {
+ if (this.points.length < 2) {
+ this.$judgeText.text = 'Add points to your graph.';
+ } else if (!this.scored) {
+ this.$judgeText.text = 'Judges are ready!';
+ }
+ };
+ refreshJudgeText();
+
+ // Set up score card container
+ this.$scores = [];
+ this.$scoreContainer = this.$('.scores')!;
+ this.hideScoreCards();
+
+ // Create score cards
+ for (let i = 0; i < 3; i++) {
+ const $score = $N('div', {style: `transition: ${0.2 * (i + 1)}s;`}, this.$scoreContainer);
+ $N('div', {}, $score);
+ this.$scores.push($score);
+ }
+
+ // Set up submit button
+ this.$submitButton = this.$('button')!;
+ this.$submitButton.on('click', this.submit.bind(this));
+ this.refreshSubmitButton();
+
+ // Set up graph
+ this.$graph = this.$('x-coordinate-system') as CoordinateSystem;
+
+ const $svg = this.$graph.$svg;
+ const $overlay = this.$graph.$overlay;
+ this.$plot = this.$('.plot')!;
+ this.$solution = $N('g', {class: 'solution'});
+ this.$('.grid')!.insertAfter(this.$solution);
+ this.$hints = $N('g', {class: 'hints'}, $overlay);
+ const _$plotPath = $N('path', {class: 'plot-path'}, $overlay);
+ const $plotPoints = $N('g', {class: 'plot-points'}, $overlay);
+
+ const redrawPath = () => {
+ this.points.sort((a, b) => a.point.x - b.point.x);
+
+ last(this.$plot.children)?.remove();
+
+ this.$graph.drawLinePlot(this.points.map(p => p.point));
+
+ // TODO: Curvy paths
+ // const plotPath = d3line().curve(curveMonotoneX);
+ // $plotPath.setAttr('d', plotPath(this.points.map(p => [p.position.x, p.position.y])));
+ };
+
+ let engagingPoint = false;
+ let placingPoint: PlotPoint | null = null;
+
+ const graphModified = () => {
+ this.scored = false;
+ this.hideSolution();
+ this.hideScoreCards();
+ this.refreshSubmitButton();
+ refreshJudgeText();
+ redrawPath();
+ };
+
+ slide($svg, {
+ down: (p) => {
+ if (engagingPoint) {
+ return;
+ }
+
+ const clickPosition = this.bindViewportPoint(p);
+ const clickPoint = this.$graph.toPlotCoords(clickPosition);
+
+ const $point = $N('circle', {transform: `translate(${clickPosition.x}, ${clickPosition.y})`, r: 6, class: 'plot-point'}, $plotPoints) as SVGView;
+
+ const plotPoint = {
+ point: clickPoint,
+ position: clickPosition,
+ $el: $point
+ };
+ placingPoint = plotPoint;
+
+ this.points.push(plotPoint);
+
+ slide($point, {
+ down: () => {
+ engagingPoint = true;
+ },
+ move: (position) => {
+ position = this.bindViewportPoint(position);
+ let point = this.$graph.toPlotCoords(position);
+
+ const x = this.snap.x <= 0 ? point.x : Math.round(point.x / this.snap.x) * this.snap.x;
+ const y = this.snap.y <= 0 ? point.y : Math.round(point.y / this.snap.y) * this.snap.y;
+
+ point = new Point(x, y);
+ position = this.$graph.toViewportCoords(point);
+
+ plotPoint.position = position;
+ plotPoint.point = point;
+
+ $point.setAttr('transform', `translate(${position.x}, ${position.y})`);
+
+ graphModified();
+ },
+ up: () => {
+ engagingPoint = false;
+ },
+ click: () => {
+ const i = this.points.indexOf(plotPoint);
+ this.points.splice(i, 1);
+ $point.remove();
+
+ graphModified();
+ }
+ });
+
+ graphModified();
+ },
+ move: (position) => {
+ if (placingPoint) {
+ position = this.bindViewportPoint(position);
+ let point = this.$graph.toPlotCoords(position);
+
+ const x = this.snap.x <= 0 ? point.x : Math.round(point.x / this.snap.x) * this.snap.x;
+ const y = this.snap.y <= 0 ? point.y : Math.round(point.y / this.snap.y) * this.snap.y;
+
+ point = new Point(x, y);
+ position = this.$graph.toViewportCoords(point);
+
+ placingPoint.point = point;
+ placingPoint.position = position;
+
+ placingPoint.$el.setAttr('transform', `translate(${position.x}, ${position.y})`);
+
+ redrawPath();
+ }
+ },
+ up: () => {
+ placingPoint = null;
+ }
+ });
+ }
+
+ bindStep($step: Step) {
+ this.$step = $step;
+ }
+
+ setHintPoints(hintPoints: HintPoint[]) {
+ this.hintPoints = hintPoints;
+ for (const hintPoint of hintPoints) {
+ hintPoint.$g = $N('g', {show: false}, this.$hints);
+
+ if (hintPoint.id) {
+ hintPoint.$el = this.$('#' + hintPoint.id);
+ }
+
+ if (hintPoint.drawLine) {
+ const p1 = this.$graph.toViewportCoords(new Point(hintPoint.x, this.$graph.plotBounds.yMin));
+ const p2 = this.$graph.toViewportCoords(new Point(hintPoint.x, this.$graph.plotBounds.yMax));
+ $N('line', {class: 'hint-line', show: false, x1: p1.x, x2: p2.x, y1: p1.y, y2: p2.y}, hintPoint.$g);
+ }
+ if (hintPoint.drawCircle) {
+ const c = this.$graph.toViewportCoords(new Point(hintPoint.x, this.solutionFunction(hintPoint.x)));
+ $N('circle', {class: 'hint-circle', show: false, cx: c.x, cy: c.y, r: 6}, hintPoint.$g);
+ }
+ }
+ }
+
+ setSolutionFunction(solutionFunction: (x: number) => number) {
+ this.solutionFunction = solutionFunction;
+ this.$graph.setFunctions(solutionFunction);
+
+ const $plot = this.$plot.children[0];
+
+ this.$solution.append($plot);
+ $plot.addClass('blue');
+ this.$solution.setAttr('show', false);
+ this.$solution.addClass('plot');
+ }
+
+ hideSolution() {
+ this.$solution.setAttr('show', false);
+ }
+
+ showSolution() {
+ this.$solution.setAttr('show', true);
+ }
+
+ hideScoreCards() {
+ this.$scoreContainer.setAttr('show', false);
+ }
+
+ showScoreCards() {
+ this.$scoreContainer.setAttr('show', true);
+ }
+
+ refreshSubmitButton() {
+ if (this.points.length < 2 || this.scored) {
+ this.$submitButton.setAttr('disabled', true);
+ } else {
+ this.$submitButton.removeAttr('disabled');
+ }
+ }
+
+ // Constrain a point within plot bounds
+ bindPlotPoint(point: Point) {
+ const bounds = this.$graph.plotBounds;
+
+ const x = clamp(point.x, bounds.xMin, bounds.xMax);
+ const y = clamp(point.y, bounds.yMin, bounds.yMax);
+
+ return new Point(x, y);
+ }
+
+ // Constrain a point within viewport bounds
+ bindViewportPoint(point: Point) {
+ const bounds = this.$graph.viewportBounds;
+
+ const x = clamp(point.x, bounds.xMin, bounds.xMax);
+ const y = clamp(point.y, bounds.yMin, bounds.yMax);
+
+ return new Point(x, y);
+ }
+
+ // Sample user graph in plot space
+ sampleUserGraph(x: number) {
+ if (this.points.length == 0) {
+ return 0;
+ }
+
+ // Grab points from beginning and end of user plot
+ const firstPlotPoint = this.points[0];
+ const lastPlotPoint = this.points[this.points.length - 1];
+
+ // Create virtual "leading and trailing" points at beginning and end of graph
+ let leadingPoint = this.bindPlotPoint(new Point(Number.NEGATIVE_INFINITY, firstPlotPoint.point.y));
+ let trailingPoint = this.bindPlotPoint(new Point(Number.POSITIVE_INFINITY, lastPlotPoint.point.y));
+
+ // Ensure leading/trailing points lie outside min/max possible input x values (prevents errors when plot points lie on viewport bounds)
+ leadingPoint = new Point(leadingPoint.x - 1, leadingPoint.y);
+ trailingPoint = new Point(trailingPoint.x + 1, trailingPoint.y);
+
+ // Start off by feeding the leading point into the point pair
+ let pointA: Point;
+ let pointB: Point = leadingPoint;
+
+ let pointsFound = false;
+
+ // Loop over each consecutive pair of points to find A/B pair that bounds x
+ for (let i = 0; i < this.points.length; i++) {
+ // Shift to next point pair
+ pointA = pointB;
+ pointB = this.points[i].point;
+
+ // If x is now less than point B, x is between A and B (so we can stop)
+ if (x <= pointB.x) {
+ pointsFound = true;
+ break;
+ }
+ }
+
+ // If above loop did not establish point pair, x must be between last point and trailing point
+ if (!pointsFound) {
+ pointA = lastPlotPoint.point;
+ pointB = trailingPoint;
+ }
+
+ // Unlerp x from within A/B
+ const p = unlerp(pointA!.x, pointB.x, x);
+
+ // Lerp between A/B to get y value
+ return lerp(pointA!.y, pointB.y, p);
+ }
+
+ computeError() {
+ const height = this.$graph.viewportBounds.yMax;
+ const width = this.$graph.viewportBounds.xMax;
+
+ // Accumulate difference between user and solution functions, measured in pixels
+ // (basically we are integrating here)
+ let errorArea = 0;
+ for (let i = 0; i < width; i++) {
+ const x = this.$graph.toPlotCoords(new Point(i, 0)).x;
+
+ const userY = this.sampleUserGraph(x);
+ const solutionY = this.solutionFunction(x);
+
+ const userPoint = new Point(x, userY);
+ const solutionPoint = new Point(x, solutionY);
+
+ const userPosition = this.$graph.toViewportCoords(userPoint);
+ const solutionPosition = this.$graph.toViewportCoords(solutionPoint);
+
+ const error = Math.abs(userPosition.y - solutionPosition.y);
+ errorArea += error;
+ }
+
+ // Total viewport area in pixels
+ const totalArea = width * height;
+
+ // Return fraction of error area compared to total viewport area
+ return errorArea / totalArea;
+ }
+
+ // Estimates closest point on the user graph to a given point
+ computeClosestUserGraphPoint(point: Point) {
+ const width = this.$graph.viewportBounds.xMax;
+
+ let minimumDistance = Number.POSITIVE_INFINITY;
+ let userGraphPosition: Point = new Point();
+
+ for (let i = 0; i < width; i++) {
+ const x = this.$graph.toPlotCoords(new Point(i, 0)).x;
+
+ const y = this.sampleUserGraph(x);
+ const p = new Point(x, y);
+
+ const v = p.subtract(point);
+ const d = v.length;
+
+ if (d < minimumDistance) {
+ minimumDistance = d;
+ userGraphPosition = p;
+ }
+ }
+
+ return userGraphPosition;
+ }
+
+ selectRelevantHint() {
+ if (this.hintPoints.length == 0) {
+ return null;
+ }
+
+ const hints = this.hintPoints.map((hintPoint) => {
+ const point = new Point(hintPoint.x, this.solutionFunction(hintPoint.x));
+ const userGraphPoint = this.computeClosestUserGraphPoint(point);
+
+ const position = this.$graph.toViewportCoords(point);
+ const userGraphPosition = this.$graph.toViewportCoords(userGraphPoint);
+
+ const vector = userGraphPoint.subtract(point);
+ const distance = vector.length;
+
+ const viewportVector = userGraphPosition.subtract(position);
+ const viewportDistance = viewportVector.length;
+
+ return {
+ hintPoint,
+ relevanceThresholdDistance: hintPoint.relevanceThresholdDistance || 8,
+ point,
+ userGraphPoint,
+ position,
+ userGraphPosition,
+ vector,
+ distance,
+ viewportVector,
+ viewportDistance
+ };
+ });
+
+ // Sort hints in descending order of pixel distance from user graph
+ hints.sort((a, b) => b.viewportDistance - a.viewportDistance);
+
+ // Remove any hints that are below their "relevance threshold distance" or already revealed
+ const relevantHints = hints.filter((hint) => !hint.hintPoint.revealed && hint.relevanceThresholdDistance < hint.viewportDistance);
+
+ if (relevantHints.length == 0) {
+ return null;
+ }
+
+ return relevantHints[0].hintPoint;
+ }
+
+ submit() {
+ this.scored = true;
+
+ this.$judgeText.text = 'Judges say:';
+
+ const error = this.computeError();
+ const score = Math.pow(1 - error, 4);
+
+ if (this.$step) {
+ this.$step.score('submit');
+
+ const hintPoint = this.selectRelevantHint();
+
+ if (this.scoreThreshold < score) {
+ this.$solution.setAttr('show', true);
+
+ this.$step.addHint('correct');
+ this.$step.score('submitCorrect');
+ } else if (hintPoint) {
+ this.$step.addHint(hintPoint.hint);
+
+ if (this.firstHint) {
+ this.$step.addHint('Try again!');
+ this.firstHint = false;
+ }
+
+ hintPoint.revealed = true;
+ hintPoint.$el?.setAttr('show', true);
+ hintPoint.$g?.setAttr('show', true);
+ } else {
+ this.$solution.setAttr('show', true);
+
+ this.$step.addHint('Close!');
+ }
+ }
+
+ for (const $score of this.$scores) {
+ const cardScore = Math.round(score * 100 - Math.random() * 10) / 10;
+ $score.$('div')!.text = cardScore.toString();
+ }
+
+ this.refreshSubmitButton();
+ this.showScoreCards();
+ }
+}
diff --git a/content/functions/components/function-machine/function-machine.pug b/content/functions/components/function-machine/function-machine.pug
new file mode 100644
index 000000000..add358644
--- /dev/null
+++ b/content/functions/components/function-machine/function-machine.pug
@@ -0,0 +1,6 @@
+.input
+ slot(name="input")
+.operation
+ slot(name="operation")
+.output
+ slot(name="output")
\ No newline at end of file
diff --git a/content/functions/components/function-machine/function-machine.scss b/content/functions/components/function-machine/function-machine.scss
new file mode 100644
index 000000000..d2bf2e920
--- /dev/null
+++ b/content/functions/components/function-machine/function-machine.scss
@@ -0,0 +1,44 @@
+// =============================================================================
+// Function Machine Component
+// (c) Mathigon
+// =============================================================================
+
+x-function-machine {
+ user-select: none;
+ font-size: 24px;
+
+ svg {
+ margin-bottom: 18px;
+ }
+
+ .input, .output, .operation {
+ &.active { background: mix($grey-background, $border-light); }
+ text, image {
+ width: 100%;
+ pointer-events: none;
+ text-anchor: middle;
+ // alignment-baseline: middle;
+ }
+ }
+
+ .input, .output {
+ rect {
+ fill: $grey-background;
+ stroke-width: 2px;
+ stroke: $border-light;
+ stroke-dasharray: 4px, 4px;
+ }
+ }
+
+ .operation text {
+ fill: #FFF;
+ }
+
+ .operation rect {
+ fill: $medium-grey;
+ stroke-width: 2px;
+ stroke: $dark-grey;
+ }
+
+ .input { cursor: pointer; }
+}
diff --git a/content/functions/components/function-machine/function-machine.ts b/content/functions/components/function-machine/function-machine.ts
new file mode 100644
index 000000000..d2a934260
--- /dev/null
+++ b/content/functions/components/function-machine/function-machine.ts
@@ -0,0 +1,156 @@
+// =============================================================================
+// Function Machine Component
+// (c) Mathigon
+// =============================================================================
+
+import {Step} from '@mathigon/studio';
+import {$N, animate, CustomElementView, Draggable, ease, register, SVGParentView, SVGView} from '@mathigon/boost';
+import {clamp, lerp} from '@mathigon/fermat';
+
+type Mapping = {
+ inputY: number,
+ $input: SVGView,
+ $output: SVGView,
+ consumed: boolean,
+}
+
+function unlerp(a: number, b: number, t: number) {
+ return (t - a) / (b - a);
+}
+
+const itemHeight = 60;
+
+@register('x-function-machine')
+export class FunctionMachine extends CustomElementView {
+ private $inputs!: SVGView[];
+ private $operation!: SVGView;
+ private $outputs!: SVGView[];
+ private $outputInstances!: SVGView[];
+ private mappings!: Mapping[];
+ private $svg!: SVGParentView;
+ private $step?: Step;
+ private callbacks: ((inputString: string, outputString: string) => void)[] = [];
+
+ private svgWidth = 0;
+ private svgHeight = 0;
+
+ private leftColumnX = 0;
+ private rightColumnX = 0;
+
+ private topY = 0;
+ private bottomY = 0;
+
+ ready() {
+ this.$svg = this.$('svg') as SVGParentView;
+ this.$inputs = this.$$('.input') as SVGView[];
+ this.$operation = this.$('.operation') as SVGView;
+ this.$outputs = this.$$('.output') as SVGView[];
+
+ this.mappings = this.$inputs.map(($input) => {
+ return {
+ inputY: 0,
+ $input,
+ $output: this.$outputs.find($output => $output.attr('name') == $input.attr('output'))!,
+ consumed: false
+ };
+ });
+
+ this.resize();
+
+ // I assume there is a better way to do this with templates?
+ for (const $el of this.$$('g') as SVGView[]) {
+ $el.children[0]!.insertBefore($N('rect', {
+ x: -itemHeight / 2,
+ y: -itemHeight / 2,
+ width: itemHeight,
+ height: itemHeight,
+ rx: 4,
+ ry: 4
+ }));
+
+ $el.$('text')!.setAttr('y', 9);
+ }
+
+ for (const [_i, $input] of this.$inputs.entries()) {
+ const mapping = this.mappings.find((mapping) => mapping.$input == $input)!;
+ const drag = new Draggable($input, this.$svg, {useTransform: true});
+
+ drag.setPosition(this.leftColumnX, mapping.inputY);
+
+ drag.on('move', (posn) => {
+ const dragProgress = clamp(unlerp(this.leftColumnX, this.rightColumnX, posn.x), 0, 1);
+ const y = lerp(mapping.inputY, this.topY, dragProgress);
+ drag.setPosition(clamp(drag.position.x, this.leftColumnX, this.rightColumnX), y);
+ });
+
+ drag.on('end', async () => {
+ const posn = drag.position;
+
+ if (posn.x < this.rightColumnX) {
+ await animate((p) => {
+ const x = lerp(posn.x, this.leftColumnX, p);
+ const y = lerp(posn.y, mapping.inputY, p);
+ drag.setPosition(x, y);
+ }, 500);
+ } else {
+ $input.hide();
+
+ const $output = mapping.$output.copy(true, false);
+ this.$operation.insertBefore($output);
+ $output.show();
+
+ if (this.$step) {
+ this.$step.score($input.attr('name'));
+ this.$step.addHint($input.attr('hint') || 'correct');
+ }
+
+ for (const cb of this.callbacks) {
+ cb($input.attr('value'), $output.attr('value'));
+ }
+
+ await animate((p) => {
+ const q = ease('bounce-out', p);
+ $output.setAttr('transform', `translate(${this.rightColumnX}, ${lerp(this.topY, this.bottomY, q)})`);
+ }, 500);
+ }
+ });
+ }
+
+ for (const $output of this.$outputs) {
+ $output.hide();
+ }
+ }
+
+ resize() {
+ this.svgWidth = parseInt(this.attr('width'));
+ this.svgHeight = parseInt(this.attr('height'));
+
+ this.$svg.setAttr('width', this.svgWidth);
+ this.$svg.setAttr('height', this.svgHeight);
+
+ this.leftColumnX = 50;
+ this.rightColumnX = this.svgWidth - 50;
+
+ this.topY = (this.svgHeight / (this.$inputs.length)) / 2;
+ this.bottomY = this.svgHeight - this.topY;
+
+ const inputSpacing = this.svgHeight / this.$inputs.length;
+
+ for (const [i, $input] of this.$inputs.entries()) {
+ const mapping = this.mappings.find(mapping => mapping.$input == $input)!;
+ mapping.inputY = inputSpacing * (i + 0.5);
+ $input.setAttr('transform', `translate(${this.leftColumnX}, ${mapping.inputY})`);
+ }
+
+ this.$operation.setAttr('transform', `translate(${this.rightColumnX}, ${this.topY})`);
+ }
+
+ // Seems like there should be a better way to do this, but it will work for now
+ bindStep($step: Step) {
+ this.$step = $step;
+ }
+
+ bindCallback(cb: (inputString: string, outputString: string) => void) {
+ this.callbacks.push(cb);
+ }
+}
diff --git a/content/functions/components/piecewise-endpoint-puzzle/piecewise-endpoint-puzzle.scss b/content/functions/components/piecewise-endpoint-puzzle/piecewise-endpoint-puzzle.scss
new file mode 100644
index 000000000..1f379ecf8
--- /dev/null
+++ b/content/functions/components/piecewise-endpoint-puzzle/piecewise-endpoint-puzzle.scss
@@ -0,0 +1,56 @@
+x-piecewise-endpoint-puzzle {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ margin: 2em auto;
+
+ x-coordinate-system {
+ margin: 0 auto 0 auto;
+ }
+
+ >button {
+ margin: 20px auto 2em auto;
+ }
+
+ .overlay {
+ .burst {
+ transition: 0.5s;
+ opacity: 1;
+ }
+ .burst.fade {
+ opacity: 0;
+ }
+
+ .endpoints {
+ stroke-width: 3px;
+ stroke: $red;
+ fill: transparent;
+
+ .closed {
+ stroke: none;
+ fill: $red;
+ }
+ }
+ .segments {
+ stroke-width: 3px;
+ stroke: $red;
+ stroke-linecap: butt;
+ }
+ .strings {
+ text-anchor: middle;
+ font-size: 18px;
+ fill: $red;
+ }
+ }
+
+ .scoring-row {
+ display: flex;
+ margin-top: 20px;
+ align-items: center;
+
+ .prompt-text {
+ margin-left: 20px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/content/functions/components/piecewise-endpoint-puzzle/piecewise-endpoint-puzzle.ts b/content/functions/components/piecewise-endpoint-puzzle/piecewise-endpoint-puzzle.ts
new file mode 100644
index 000000000..46c342e9a
--- /dev/null
+++ b/content/functions/components/piecewise-endpoint-puzzle/piecewise-endpoint-puzzle.ts
@@ -0,0 +1,237 @@
+// =============================================================================
+// Piecewise Endpoint Puzzle Component
+// (c) Mathigon
+// =============================================================================
+
+import {Step} from '@mathigon/studio';
+import {$N, CustomElementView, ElementView, register, SVGParentView, SVGView} from '@mathigon/boost';
+import {wait} from '@mathigon/core';
+import {Point} from '@mathigon/euclid';
+import {Burst} from '../../../shared/components/burst';
+import {CoordinateSystem} from '../../../shared/types';
+
+type Segment = {
+ point0: Point,
+ point1: Point,
+ include0?: boolean,
+ include1?: boolean,
+ $line?: SVGView,
+ $text?: SVGView,
+ end0?: Endpoint,
+ end1?: Endpoint,
+ label?: string,
+}
+
+type Endpoint = {
+ point: Point,
+ $point: SVGView,
+ closed: boolean,
+ correctValue: number,
+ sibling?: Endpoint,
+}
+
+@register('x-piecewise-endpoint-puzzle')
+export class PiecewiseEndpointPuzzle extends CustomElementView {
+ private $graph!: CoordinateSystem;
+ private $prompt!: ElementView;
+ private $submit!: ElementView;
+ private $step!: Step;
+
+ private shuffling = false;
+
+ private segments!: Segment[];
+ private endpoints: Endpoint[] = [];
+
+ ready() {
+ this.$graph = this.$('x-coordinate-system')! as CoordinateSystem;
+
+ this.$prompt = this.$('.prompt-text')!;
+ this.$prompt.text = 'Tap points to flip them.';
+
+ this.$submit = this.$('button')!;
+ this.$submit.on('click', () => {
+ this.submit();
+ });
+ }
+
+ bindStep($step: Step) {
+ this.$step = $step;
+ }
+
+ async submit() {
+ this.$prompt.text = 'Σ(-᷅_-᷄๑) Hmmm…';
+ this.$submit.setAttr('disabled', true);
+ await wait(2000);
+
+ const success = this.checkSuccess();
+
+ if (success) {
+ this.$prompt.text = '(☞ ゚ヮ ゚)☞';
+ this.$step.addHint('correct');
+ this.$step.score(this.attr('score') || 'endpoint-puzzle');
+ } else {
+ this.$step.addHint('incorrect');
+ this.$prompt.text = '╰( ゚ー゚╰) Shuffling…';
+ await this.reshuffle();
+ this.$prompt.text = 'Try again! (⊃ᵔ‿ᵔ)b';
+ this.$submit.removeAttr('disabled');
+ }
+ }
+
+ async flipPoint(endpoint: Endpoint, animate = true) {
+ if (endpoint.closed) {
+ endpoint.closed = false;
+ endpoint.$point.removeClass('closed');
+ } else {
+ endpoint.closed = true;
+ endpoint.$point.addClass('closed');
+ }
+
+ if (!animate) {
+ return;
+ }
+
+ const position = this.$graph.toViewportCoords(endpoint.point);
+ const burstElement = $N('g', {class: 'burst'}, this.$graph.$overlay);
+ const burst = new Burst(burstElement as SVGParentView, 10);
+ burst.play(500, [position.x, position.y], [5, 15]).then(() => {
+ burstElement.remove();
+ });
+ await wait(1);
+ burstElement.addClass('fade');
+ }
+
+ checkSuccess() {
+ let success = true;
+
+ for (const endpoint of this.endpoints) {
+ if (endpoint.correctValue != -1) {
+ if (endpoint.closed != (endpoint.correctValue == 1)) {
+ success = false;
+ }
+ } else if (endpoint.sibling) {
+ if (endpoint.closed && endpoint.sibling.closed) {
+ success = false;
+ } else if (!endpoint.closed && !endpoint.sibling.closed) {
+ success = false;
+ }
+ } else if (!endpoint.closed) {
+ success = false;
+ }
+ }
+
+ return success;
+ }
+
+ async reshuffle() {
+ this.shuffling = true;
+
+ let i = 0;
+ while (i++ < 7) {
+ this.randomize();
+ await wait(100 * i);
+ }
+
+ while (this.checkSuccess()) {
+ this.randomize(false);
+ }
+
+ this.shuffling = false;
+ }
+
+ randomize(animate = true) {
+ let flipped = false;
+ do {
+ for (const e of this.endpoints) {
+ if (Math.random() > 0.5) {
+ this.flipPoint(e, animate);
+ flipped = true;
+ }
+ }
+ } while (!flipped);
+ }
+
+ setSegments(segments: Segment[]) {
+ this.segments = segments;
+
+ const $segments = $N('g', {class: 'segments'}, this.$graph.$overlay);
+ const $endpoints = $N('g', {class: 'endpoints'}, this.$graph.$overlay);
+ const $labels = $N('g', {class: 'strings'}, this.$graph.$overlay);
+
+ const addPoint = (point: Point, closed: boolean, specified: boolean) => {
+ for (const endpoint of this.endpoints) {
+ if (endpoint.point.equals(point)) {
+ return endpoint;
+ }
+ }
+
+ const position = this.$graph.toViewportCoords(point);
+ const $point = $N('circle', {class: 'endpoint', r: 6, cx: position.x, cy: position.y}, $endpoints) as SVGView;
+
+ const e: Endpoint = {
+ point,
+ $point,
+ closed,
+ correctValue: specified ? closed ? 1 : 0 : -1
+ };
+
+ if (closed) {
+ $point.addClass('closed');
+ }
+
+ $point.on('click', () => {
+ if (!this.shuffling) {
+ this.flipPoint(e);
+ }
+ });
+
+ for (const endpoint of this.endpoints) {
+ if (endpoint.point.x == point.x) {
+ e.sibling = endpoint;
+ endpoint.sibling = e;
+ return e;
+ }
+ }
+
+ return e;
+ };
+
+ for (const segment of segments) {
+ segment.end0 = addPoint(segment.point0, segment.include0 as boolean, segment.include0 !== undefined);
+ segment.end1 = addPoint(segment.point1, segment.include1 as boolean, segment.include1 !== undefined);
+ this.endpoints.push(segment.end0);
+ this.endpoints.push(segment.end1);
+
+ let position0 = this.$graph.toViewportCoords(segment.point0);
+ let position1 = this.$graph.toViewportCoords(segment.point1);
+
+ const direction = position1.subtract(position0).unitVector;
+
+ position0 = position0.add(new Point(direction.x * 5, direction.y * 5));
+ position1 = position1.subtract(new Point(direction.x * 5, direction.y * 5));
+
+ segment.$line = $N('line', {x1: position0.x, y1: position0.y, x2: position1.x, y2: position1.y}, $segments) as SVGView;
+ segment.$text = $N('text', {x: (position0.x + position1.x) / 2, y: (position0.y + position1.y) / 2 - 8}, $labels) as SVGView;
+
+ if (segment.label === undefined && !(segment.include0 === undefined || segment.include1 === undefined)) {
+ let label = segment.point0.x.toString();
+ label += segment.include0 ? ' ≤ ' : ' < ';
+ label += 'x';
+ label += segment.include1 ? ' ≤ ' : ' < ';
+ label += segment.point1.x.toString();
+ segment.$text.text = label;
+ } else if (segment.label) {
+ segment.$text.text = segment.label;
+ }
+ }
+
+ let i = 0;
+ do {
+ this.randomize(false);
+ if (i++ > 1000) {
+ console.error('No unsuccessful condition found for this puzzle', this.segments);
+ break;
+ }
+ } while (this.checkSuccess());
+ }
+}
diff --git a/content/functions/components/pong/pong.scss b/content/functions/components/pong/pong.scss
new file mode 100644
index 000000000..d45ff975e
--- /dev/null
+++ b/content/functions/components/pong/pong.scss
@@ -0,0 +1,34 @@
+x-pong {
+ .paddle {
+ line {
+ stroke-width: 8px;
+ stroke-linecap: round;
+ }
+ }
+
+ .ball {
+ fill: $red;
+ }
+
+ .arrow {
+ stroke: $blue;
+ stroke-width: 3px;
+ }
+
+ .bounce {
+ stroke-width: 2px;
+ stroke: $blue;
+ }
+
+ .bounce.hide {
+ display: none;
+ }
+}
+
+x-pong.launched {
+ pointer-events: none;
+
+ .arrow {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/content/functions/components/pong/pong.ts b/content/functions/components/pong/pong.ts
new file mode 100644
index 000000000..cb796ed10
--- /dev/null
+++ b/content/functions/components/pong/pong.ts
@@ -0,0 +1,191 @@
+// =============================================================================
+// Pong Component
+// (c) Mathigon
+// =============================================================================
+
+import {Step} from '@mathigon/studio';
+import {$N, animate, CustomElementView, ElementView, register, slide} from '@mathigon/boost';
+import {Point} from '@mathigon/euclid';
+import {CoordinateSystem} from '../../../shared/types';
+import {clamp, lerp} from '@mathigon/fermat';
+import {wait} from '@mathigon/core';
+
+@register('x-pong')
+export class Pong extends CustomElementView {
+ private $graph!: CoordinateSystem;
+ private $step?: Step;
+
+ private $paddle!: ElementView;
+ private $ball!: ElementView;
+ private $ballArrow!: ElementView;
+ private $bounceLineA!: ElementView;
+ private $bounceLineB!: ElementView;
+
+ private ballOriginPoint!: Point;
+ private ballOriginPosition!: Point;
+
+ private ballStrikePoint!: Point;
+ private ballStrikePosition!: Point;
+
+ private ballBouncePoint!: Point;
+ private ballBouncePosition!: Point;
+
+ private paddleMinPoint!: Point;
+ private paddleMinPosition!: Point;
+
+ private paddleMaxPoint!: Point;
+ private paddleMaxPosition!: Point;
+
+ ready() {
+ const $graph = this.$('x-coordinate-system') as CoordinateSystem;
+ this.$graph = $graph;
+
+ const paddleOriginPoint = new Point($graph.plotBounds.xMax, ($graph.plotBounds.yMax + $graph.plotBounds.yMin) / 2);
+ const paddleOriginPosition = $graph.toViewportCoords(paddleOriginPoint);
+
+ const paddlePixelSize = $graph.viewportBounds.dy / 10;
+ const paddlePixelRadius = paddlePixelSize / 2;
+
+ const paddleMinPoint = new Point($graph.plotBounds.xMax, $graph.plotBounds.yMin);
+ const paddleMinPosition = $graph.toViewportCoords(paddleMinPoint);
+ this.paddleMinPoint = paddleMinPoint;
+ this.paddleMinPosition = paddleMinPosition;
+
+ const paddleMaxPoint = new Point($graph.plotBounds.xMax, $graph.plotBounds.yMax);
+ const paddleMaxPosition = $graph.toViewportCoords(paddleMaxPoint);
+ this.paddleMaxPoint = paddleMaxPoint;
+ this.paddleMaxPosition = paddleMaxPosition;
+
+ const $paddle = $N('g', {class: 'paddle', transform: `translate(${paddleOriginPosition.x},${paddleOriginPosition.y})`}, $graph.$overlay);
+ this.$paddle = $paddle;
+
+ const _$paddleLine = $N('line', {x1: 0, x2: 0, y1: -paddlePixelRadius, y2: paddlePixelRadius}, $paddle);
+
+ const $ballArrow = $N('g', {class: 'arrow'}, $graph.$overlay);
+ $N('line', {class: 'shaft', x1: 0, y1: 0, x2: 40, y2: 0}, $ballArrow);
+ $N('line', {class: 'head', x1: 40, y1: 0, x2: 35, y2: 5}, $ballArrow);
+ $N('line', {class: 'head', x1: 40, y1: 0, x2: 35, y2: -5}, $ballArrow);
+ this.$ballArrow = $ballArrow;
+
+ this.$bounceLineA = $N('line', {class: 'bounce'}, $graph.$overlay);
+ this.$bounceLineB = $N('line', {class: 'bounce'}, $graph.$overlay);
+
+ const $ball = $N('circle', {class: 'ball', r: 10}, $graph.$overlay);
+ this.$ball = $ball;
+
+ this.reset();
+
+ slide($graph.$svg, {
+ down: () => {
+ // Begin sliding
+ },
+ move: (position) => {
+ const y = clamp(position.y, paddleMinPosition.y + paddlePixelRadius, paddleMaxPosition.y - paddlePixelRadius);
+ $paddle.setAttr('transform', `translate(${paddleOriginPosition.x},${y})`);
+ },
+ up: () => {
+ this.launch();
+ }
+ });
+ }
+
+ reset() {
+ const $graph = this.$graph;
+
+ // Point where ball will strike the paddle-side wall
+ const ballStrikePoint = new Point(this.paddleMaxPoint.x, lerp(this.paddleMinPoint.y, this.paddleMaxPoint.y, Math.random() * 0.8));
+
+ // Point where ball will bounce on floor
+ const ballBouncePoint = new Point(lerp($graph.plotBounds.xMin, $graph.plotBounds.xMax, Math.random() * 0.8 + 0.1), 0);
+ const ballBouncePosition = $graph.toViewportCoords(ballBouncePoint);
+
+ // Vector and slope for the line from the bounce point to the strike point
+ const path1Vector = ballStrikePoint.subtract(ballBouncePoint);
+ const path1Slope = path1Vector.y / path1Vector.x;
+
+ // Slope for the line from the ball origin to the bounce point
+ const path0Slope = -path1Slope;
+
+ // Minimum possible ball origin point on the left-side wall
+ const minOriginX = $graph.plotBounds.xMin;
+ const minOriginVectorX = minOriginX - ballBouncePoint.x;
+ const minOriginVectorY = minOriginVectorX * path0Slope;
+ const minOriginVector = new Point(minOriginVectorX, minOriginVectorY);
+ const minOriginPoint = ballBouncePoint.add(minOriginVector);
+
+ // Minimum possible ball origin point factoring in the ceiling
+ const cappedOriginY = Math.min($graph.plotBounds.yMin, minOriginPoint.y);
+ const cappedOriginVectorY = cappedOriginY;
+ const cappedOriginVectorX = cappedOriginVectorY / path0Slope;
+ const cappedOriginVector = new Point(cappedOriginVectorX, cappedOriginVectorY);
+ const cappedOriginPoint = ballBouncePoint.add(cappedOriginVector);
+ const cappedOriginPosition = $graph.toViewportCoords(cappedOriginPoint);
+
+ // Ball origin is selected as a random point between bounce point and capped origin point
+ const lerpFactor = Math.random() * 0.8 + 0.2;
+ const ballOriginPoint = new Point(lerp(ballBouncePoint.x, cappedOriginPoint.x, lerpFactor), lerp(ballBouncePoint.y, cappedOriginPoint.y, lerpFactor));
+ const ballOriginPosition = $graph.toViewportCoords(ballOriginPoint);
+
+ // Debugging markers
+ // const $bouncePoint = $N('circle', {class: 'bounce', cx: ballBouncePosition.x, cy: ballBouncePosition.y, r: 10}, $graph.$overlay);
+ // const $strikePoint = $N('circle', {class: 'strike', cx: ballStrikePosition.x, cy: ballStrikePosition.y, r: 10}, $graph.$overlay);
+ // const $minPoint = $N('circle', {class: 'strike', cx: cappedOriginPosition.x, cy: cappedOriginPosition.y, r: 10}, $graph.$overlay);
+
+ this.ballOriginPoint = ballOriginPoint;
+ this.ballStrikePoint = ballStrikePoint;
+ this.ballBouncePoint = ballBouncePoint;
+
+ const ballDirection = cappedOriginVector.flip.unitVector;
+
+ const arrowAngle = Math.atan2(ballDirection.x, -ballDirection.y) * 180 / Math.PI;
+ this.$ballArrow.setAttr('transform', `translate(${ballOriginPosition.x}, ${ballOriginPosition.y}) rotate(${arrowAngle})`);
+
+ this.$ball.setAttr('cx', ballOriginPosition.x);
+ this.$ball.setAttr('cy', ballOriginPosition.y);
+
+ this.$bounceLineA.setAttr('x1', cappedOriginPosition.x);
+ this.$bounceLineA.setAttr('y1', cappedOriginPosition.y);
+
+ this.$bounceLineA.setAttr('x2', ballOriginPosition.x);
+ this.$bounceLineA.setAttr('y2', ballOriginPosition.y);
+
+ this.$bounceLineB.setAttr('x1', ballBouncePosition.x);
+ this.$bounceLineB.setAttr('y1', ballBouncePosition.y);
+
+ this.$bounceLineB.addClass('hide');
+ }
+
+ async launch() {
+ this.addClass('launched');
+
+ const traverse = async (pointA: Point, pointB: Point, speed: number, $line: ElementView) => {
+ const duration = pointA.subtract(pointB).length / speed;
+
+ await animate((p) => {
+ const point = new Point(lerp(pointA.x, pointB.x, p), lerp(pointA.y, pointB.y, p));
+ const position = this.$graph.toViewportCoords(point);
+
+ this.$ball.setAttr('cx', position.x);
+ this.$ball.setAttr('cy', position.y);
+
+ $line.setAttr('x2', position.x);
+ $line.setAttr('y2', position.y);
+ }, duration * 1000).promise;
+ };
+
+ this.$bounceLineB.addClass('hide');
+ await traverse(this.ballOriginPoint, this.ballBouncePoint, 10, this.$bounceLineA);
+
+ this.$bounceLineB.removeClass('hide');
+ await traverse(this.ballBouncePoint, this.ballStrikePoint, 10, this.$bounceLineB);
+
+ await wait(100);
+
+ this.removeClass('launched');
+ this.reset();
+ }
+
+ bindStep($step: Step) {
+ this.$step = $step;
+ }
+}
diff --git a/content/functions/components/video-graph/video-graph.scss b/content/functions/components/video-graph/video-graph.scss
new file mode 100644
index 000000000..98e7d4d59
--- /dev/null
+++ b/content/functions/components/video-graph/video-graph.scss
@@ -0,0 +1,4 @@
+x-video-graph.horizontal {
+ display: flex;
+ align-items: center;
+}
\ No newline at end of file
diff --git a/content/functions/components/video-graph/video-graph.ts b/content/functions/components/video-graph/video-graph.ts
new file mode 100644
index 000000000..8d9ea8076
--- /dev/null
+++ b/content/functions/components/video-graph/video-graph.ts
@@ -0,0 +1,65 @@
+// =============================================================================
+// Video Graph Component
+// (c) Mathigon
+// =============================================================================
+
+import {Video} from '@mathigon/studio';
+import {$N, CustomElementView, register, SVGView} from '@mathigon/boost';
+import {Point} from '@mathigon/euclid';
+import {CoordinateSystem} from '../../../shared/types';
+
+const avatarSize = 32;
+
+@register('x-video-graph')
+export class VideoGraph extends CustomElementView {
+ private $video!: Video;
+ private $videoEl!: Node;
+ private $graph!: CoordinateSystem;
+ private functions: ((t:number)=>number)[] = [];
+ private colors: string[] = [];
+
+ ready() {
+ this.$video = this.$('x-video')! as Video;
+ this.$videoEl = this.$video.$('video')?._el as Node;
+ this.$graph = this.$('x-coordinate-system')! as CoordinateSystem;
+ }
+
+ addPlot(xFunction: (t: number) => number, yFunction: (t: number) => number, avatarPath: string, color = 'red') {
+ this.functions.push(yFunction);
+ this.$graph.setFunctions(...this.functions);
+
+ if (color) {
+ this.colors.push(color);
+ } else {
+ this.colors.push('red');
+ }
+
+ for (let i = 0; i < this.colors.length; i++) {
+this.$graph.$('.plot')!.$$('g')[i].$('path')!.setAttr('class', this.colors[i]);
+ }
+
+
+ const $avatar = $N('g', {}, this.$graph.$svg.$('.overlay')!) as SVGView;
+
+ if (avatarPath) {
+ $N('image', {href: avatarPath, transform: `translate(${-avatarSize / 2}, ${-avatarSize / 2})`, width: avatarSize, height: avatarSize}, $avatar);
+ } else {
+ $N('circle', {r: 4}, $avatar);
+ }
+
+ const setAvatarPosition = (t: number) => {
+ const x = xFunction(t);
+ const y = yFunction(x);
+ const athletePoint = this.$graph.toViewportCoords(new Point(x, y));
+
+ $avatar.setAttr('transform', `translate(${athletePoint.x}, ${athletePoint.y})`);
+ };
+
+ this.$video.on('timeupdate', () => {
+ const t: number = (this.$videoEl as any).currentTime;
+ setAvatarPosition(t);
+ });
+
+ setAvatarPosition(0);
+ }
+}
diff --git a/content/functions/content.md b/content/functions/content.md
index 8d0dd831e..40d063c19 100644
--- a/content/functions/content.md
+++ b/content/functions/content.md
@@ -92,7 +92,8 @@ A connection between two sets is called a [__relation__](gloss:relation). Relati
| Houses | Classes | Wands |
| ------ | ------- | ----- |
-|[[Many]] student(s) can be in [[one]] house(s). A relation like this is called many-to-one.| [[Many students]] can be in [[many classes]]. A relation like this is called many-to-many. | [[One wand]] chooses [[one student]]. A relation like this is called one-to-one. |
+|Multiple students can be in a single house. A relation like this is called [[many-to-one\|one-to-one\|one-to-many\|many-to-many]].|Multiple students can be in multiple classes. A relation like this is called [[many-to-many\|one-to-one\|one-to-many\|many-to-one]].|A single wand chooses a single student. A relation like this is called [[one-to-one\|many-to-many\|one-to-many\|many-to-one]].|
+|Conversely, a single house contains multiple students—a [[one-to-many\|one-to-one\|many-to-one\|many-to-many]] relation.|Conversely, multiple classes contain multiple students—a [[many-to-many\|one-to-one\|many-to-one\|one-to-many]] relation.|Conversely, a single student has one wand—a [[one-to-one\|one-to-many\|many-to-one\|many-to-many]] relation.|
---
@@ -118,13 +119,15 @@ You can think of relations as an operation: you pass in an item from the __set__
| 6 | | even |
| -6 | | even |
-The input set for the first example is {14, 3, 11, 6, -6}. Note that these numbers don’t have to be in order from least to greatest. The output set for this example is {even, odd}. Notice the elements even and odd are not repeated.
+The input set for the first example is {14, 3, 11, 6, -6}. Note that these numbers don’t have to be in order from least to greatest. The output set for this example is {even, odd}. Notice that we only list even and odd *once* in our output set.
These input and output sets have special names. The complete set of all possible inputs is called [__domain__](gloss:domain). Similarly, [__range__](gloss:range) is the set of all possible output values. We use the same curly brace notation in the example above. Sometimes we use inequalities to communicate domain and range. We will look at this in more detail later. Use the tabs to answer the questions below.
TODO Interactive here
---
+> id: coordinate-plots
+> goals: p0 p1 p2
### Coordinate Systems
@@ -132,19 +135,76 @@ We have been using mapping diagrams, coordinate pairs, and tables to represent r
Let’s plot the relation {(0,0), (1,4), (-5,3), (-2,-1), (4, -3)} on the coordinate plane. Two points are already on the graph.
-TODO Interactive here
+ x-geopad(width=400 height=300 x-axis="-6,6,1" y-axis="-5,5,1" axes grid padding=8 snap): svg
+ circle.green(x="point(0, 0)")
+ circle.green(x="point(1, 4)")
-TODO Tutor prompts here
+---
+> id: vertical-line-test
Let’s look at the graphs of the relations we have been working with so far. Notice how the mapping diagram relates to the graph. The axes in the coordinate plane below take names other than x and y, the variables we’ve seen before. This graph, for instance, includes a name- and a house-axis.
-TODO Interactive here
+ x-geopad(width=500 height=350 x-axis="-1,6,1" y-axis="0,4,1" axes grid padding=25): svg
Using coordinate systems, it is also very easy to check whether a relation is many-to-many, or one-to-many. The many-to-one and many-to-many graphs have [[at least one | no]] points that share an x-value. The one-to-one and one-to-many graphs have [[no | at least one]] points that share an x-value.
-Go through the coordinate system from left to right, and check if there are any two points connected by a vertical line. This means that they share the same x-value, so the relation is not many-to-one. This is called the vertical line test. The graphs that share an x-value seem like they could have a vertical line that connects [[two or more | none]] of their points.
+Go through the coordinate system from left to right, and check if there are any two points connected by a vertical line. This means that they share the same x-value, so the relation is not many-to-one or many-to-many. This is called the vertical line test. The graphs that share an x-value seem like they could have a vertical line that connects [[two or more | none]] of their points.
-TODO Interactive here
+::: column(width=350)
+
+This relation [[passes | fails]] the vertical line test:
+
+ x-geopad.verticalLineTest(width=350 height=350 x-axis="-10,10,1" y-axis="-10,10,1" axes grid padding=8): svg
+ circle(x="point(-6, 9)")
+ circle(x="point(-3, 9)")
+ circle(x="point(-1, -3)")
+ circle(x="point(5, -8)")
+ circle(x="point(9, 1)")
+ circle(x="point(10, 1)")
+
+::: column(width=350)
+
+This relation [[fails | passes]] the vertical line test:
+
+ x-geopad.verticalLineTest(width=350 height=350 x-axis="-10,10,1" y-axis="-10,10,1" axes grid padding=8): svg
+ circle(x="point(-10, 0)")
+ circle(x="point(-6, -3)")
+ circle(x="point(-6, -7)")
+ circle(x="point(-3, -9)")
+ circle(x="point(-3, -10)")
+ circle(x="point(2, -9)")
+
+:::
+
+::: column(width=350)
+
+This relation [[passes | fails]] the vertical line test:
+
+ x-geopad.verticalLineTest(width=350 height=350 x-axis="-10,10,1" y-axis="-10,10,1" axes grid padding=8): svg
+ circle(x="point(-9, -3)")
+ circle(x="point(0, -4)")
+ circle(x="point(6, -6)")
+ circle(x="point(7, 10)")
+ circle(x="point(8, 1)")
+ circle(x="point(10, 1)")
+
+::: column(width=350)
+
+This relation [[fails | passes]] the vertical line test:
+
+ x-geopad.verticalLineTest(width=350 height=350 x-axis="-10,10,1" y-axis="-10,10,1" axes grid padding=8): svg
+ circle(x="point(-6, 5)")
+ circle(x="point(-6, -7)")
+ circle(x="point(0, -8)")
+ circle(x="point(0, 5)")
+ circle(x="point(3, -8)")
+ circle(x="point(3, -9)")
+ circle(x="point(9, 2)")
+ circle(x="point(9, -4)")
+ circle(x="point(10, -4)")
+ circle(x="point(10, -9)")
+
+:::
---
@@ -153,78 +213,280 @@ TODO Interactive here
In math, relations that are one-to-one or many-to-one are particularly important, and we will see many more examples in later chapters. That’s why they have a special name: Functions. A [__function__](gloss:function) is a rule that assigns each input to [[exactly one | at least one]] output.
---
+> id: select-functions
-Let’s return to the Hoctagon relations. Using the definition of function, select the relations that are functions.
+Let’s return to the Hoctagon relations from the very beginning of this chapter. Which of them are functions?
-TODO Interactive here
+**Students to Houses** is [[many-to-one | one-to-many | many-to-many | one-to-one]], which makes it [[a function | not a function]]:
----
+ x-relation
+ .item(slot="domain" name="a") **Current User**
+ .item(slot="domain" name="b") Phineas Lynch
+ .item(slot="domain" name="c") Sturgis Switch
+ .item(slot="domain" name="d") Dilys Derwent
+ .item(slot="domain" name="e") Demelza Zabini
+ .item(slot="domain" name="f") Bogod Clearwater
+ .item(slot="range") Lionpaw
+ .item(slot="range") Eaglewing
+ .item(slot="range") Badgerclaw
+ .item(slot="range") Serpentfang
-We can think of functions as machines. Let’s look at the sorting hat machine to see how it works.
+**Students to Classes** is [[many-to-many | many-to-one | one-to-many | one-to-one]], which makes it [[not a function | a function]]:
-TODO Interactive here
+ x-relation
+ .item(slot="domain" name="a") **Current User**
+ .item(slot="domain" name="b") Phineas Lynch
+ .item(slot="domain" name="c") Sturgis Switch
+ .item(slot="domain" name="d") Dilys Derwent
+ .item(slot="domain" name="e") Demelza Zabini
+ .item(slot="domain" name="f") Bogod Clearwater
+ .item(slot="range") Potions
+ .item(slot="range") Transfiguration
+ .item(slot="range") Magical Creatures
+ .item(slot="range") Broomstick Flying
+ .item(slot="range") Charms
+
+
+**Wands to Students** is [[one-to-one | many-to-one | many-to-many | one-to-many]], which makes it [[a function | not a function]]:
+
+ x-relation
+ .item(slot="domain")
+ img(src="images/wand-1.png" width=200 height=30)
+ span.caption Birch, phoenix feather, 5 3/4
+ .item(slot="domain")
+ img(src="images/wand-2.png" width=200 height=30)
+ span.caption Oak,dragon heartstring, 6 9/16
+ .item(slot="domain")
+ img(src="images/wand-3.png" width=200 height=30)
+ span.caption Oak, unicorn hair, 6 5/8
+ .item(slot="domain")
+ img(src="images/wand-4.png" width=200 height=30)
+ span.caption Yew, kneazle whicker, 4 9/16
+ .item(slot="domain")
+ img(src="images/wand-5.png" width=200 height=30)
+ span.caption Yew, unicorn hair, 5 7/8
+ .item(slot="range" name="a") **Current User**
+ .item(slot="range" name="b") Phineas Lynch
+ .item(slot="range" name="c") Sturgis Switch
+ .item(slot="range" name="d") Dilys Derwent
+ .item(slot="range" name="e") Demelza Zabini
+
+---
+> id: function-machines
+> goals: monkey fox smile
+
+Think of a function as a machine that accepts an object, changes it, and gives it back. Here is a function machine that puts hats on things:
+
+ x-function-machine(width=400 height=200 id="hat-machine"): svg
+ g.input(name="monkey" output="hat-monkey" id="monkey" hint="Woah, Hat Monkey!")
+ text 🙊
+ g.input(name="fox" output="hat-fox" hint="Hat Fox! Amazing!")
+ text 🦊
+ g.input(name="smile" output="hat-smile" hint="Hat Smiley?? YES!")
+ text 😃
+ g.operation
+ text 🎩
+ g.output(name="hat-monkey")
+ text 🙊
+ text(y=-6) 🎩
+ g.output(name="hat-fox")
+ text 🦊
+ text(y=-6) 🎩
+ g.output(name="hat-smile")
+ text 😃
+ text(y=-6) 🎩
+ x-gesture(target="#monkey" slide="300, 0")
---
+> id: function-notation
-Every function needs to have a name. Function notation tells us the name of the function, the input value, and the rule that gives us the output value.
+In math we have a simple way to write functions. This notation tells us the name, the input value, and the rule that provides the output value for a function.
-Let’s say a function, we’ll call f, put top hats on inputs.
+Let's use our top hat function as an example. If we call the function "f", and the input "x", we could write the function like this:
-IMAGE
+TODO: Emoji latex
+```latex
+f(x) = _{x}^{🎩}
+```
-If we are given, f(🙊), we know the output is INSERT IMAGE. Similarly, if we know f(x)= INSERT IMAGE , we know the x is 🦊.
+If x=🙊, we know that f(x)=. Similarly, if f(x)=, we know x=[[🦊|🙊|😃|🎩]].
-| Function Name | | Input Value | = | Output Value |
+| Function Name | | Input Value | | Output Value |
| :-----------: | |:---------: | :---:| :-------: |
-| _f_ | | _(x)_ | = | value or expression|
+| _f_ | | _(x)_ | = | _rule_ |
+
+Let’s use this pattern to describe the sorting hat function, which we'll name "sort". *sort* takes a *Student Name* as input and outputs a *House Name*:
+
+sort(Student Name) = House Name
+
+At the beginning of the chapter, you set up this relation for the Sorting Hat:
+
+TODO: Load state from earlier interactive
+
+ x-relation
+ .item(slot="domain" name="a") **Current User**
+ .item(slot="domain" name="b") Phineas Lynch
+ .item(slot="domain" name="c") Sturgis Switch
+ .item(slot="domain" name="d") Dilys Derwent
+ .item(slot="domain" name="e") Demelza Zabini
+ .item(slot="domain" name="f") Bogod Clearwater
+ .item(slot="range") Lionpaw
+ .item(slot="range") Eaglewing
+ .item(slot="range") Badgerclaw
+ .item(slot="range") Serpentfang
-Let’s use this pattern to describe the sorting hat function.
+According to your relation, what House Name does _sort_ return for each of these students?
-sortinghat(first year name) = house name
+sort({current user name})= [[{current user house} | not {current user house}1 | not {current user house}2 |not {current user house}3 ]]
-From the function notation above, we know that putting the sorting hat on a first year shows what house the first year is sorted into. This pattern is important to remember. The notation and expressions opposite the function name can sometimes distract us from remembering how to read function notation.
+sort(Sturgis Switch) = [[{Sturgis Switch’s house} | not {Sturgis Switch’s house}1 | not {Sturgis Switch’s house}2 | not {Sturgis Switch’s house}3]]
-sortinghat({current user name}= [[{current user house} | not {current user house}1 | not {current user house}2 |not {current user house}3 ]]
+sort(Demelza Zabini) = [[{Demela Zabini's house} | not {Demela Zabini's house}1 | not {Demela Zabini's house}2 | not {Demela Zabini's house}3]]
-sortinghat(Sturgis Switch) = [[{Sturgis Switch’s house} | not {Sturgis Switch’s house}1 | not {Sturgis Switch’s house}2 | not {Sturgis Switch’s house}3]]
+### Functions with Numbers
+> id: number-functions
-Now let’s try some examples with algebraic expressions.
-Let x=2 and f(x)=50-3x, find f(2). [[44]]
+A function maps a set of inputs to a set of outputs. That could be Emojis to Emojis With Hats, or Student Names to House Names… but naturally, in math our functions typically map Numbers to other Numbers.
-TODO optional text for incorrect answers
+Here's a function machine that adds +1 to a given number:
-Here are a few more function machines. Can you work out what the rule is in each case?
+ x-function-machine(width=400 height=200 id="plus-one-machine"): svg
+ g.input(name="two" output="three" hint="2+1=3")
+ text 2
+ g.input(name="five" output="six" hint="5+1=6")
+ text 5
+ g.input(name="eight" output="nine" hint="8+1=9")
+ text 8
+ g.operation
+ text x+1
+ g.output(name="three")
+ text 3
+ g.output(name="six")
+ text 6
+ g.output(name="nine")
+ text 9
-TODO Interactive here
+So for this function, we know that f(3)=[[4]]
-Just like before, we can visualise functions in a coordinate system. The value along the horizontal x-axis represents the input, and the value along the vertical y-axis represents the output.
+---
-TODO Interactive here
+Let's try another simple function, `f(x)=x^2`:
-A domain and range may include more elements than those listed. We have to think about what would make a complete list of all inputs or outputs. For each of these examples, drag the numbers into the groups “possible input” or “impossible input”.
+| _f(x)_ | _=_ | _x•x_ |
+| :-----------: | :---------: | :-------: |
+| f(2) | = | [[4]] |
+| f(3) | = | [[9]] |
+| f(-1) | = | [[1]] |
-TODO Interactive here
+---
+> id: numerical-coordinate-functions
+> goals: negative-two negative-one zero one two
-Look at the graphs of the functions. Determine what outputs make sense and why.
+Just like before, we can plot functions on a coordinate system. The horizontal x-axis represents the input, and the vertical y-axis represents the output. Let's plot `f(x)=x^2`:
::: column(width=300)
-TODO coordinate plane
+ x-function-machine(id="x-squared-machine" width=300 height=350): svg
+ g.input(name="negative-two" value=-2 output="four" hint="-2•-2=4")
+ text -2
+ g.input(name="negative-one" value=-1 output="one" hint="-1•-1=1")
+ text -1
+ g.input(name="zero" value=0 output="zero" hint="0•0=0")
+ text 0
+ g.input(name="one" value=1 output="one" hint="1•1=1")
+ text 1
+ g.input(name="two" value=2 output="four" hint="2•2=4")
+ text 2
+ g.operation
+ text x•x
+ g.output(name="four" value=4)
+ text 4
+ g.output(name="one" value=1)
+ text 1
+ g.output(name="zero" value=0)
+ text 0
+
+::: column(width=200)
+
+ x-geopad(id="x-squared-plot" width=200 height=350 x-axis="-3,3,1" y-axis="-5,5,1" axes grid padding=8): svg
-::: column.grow
+:::
+
+---
+> id: numerical-plot
+> goals: plotPoints
-The graph only appears in the [[1st]] quadrant. We can see that all output values must be [[positive | negative | zero]].
+With just a few input values, we can see `f(x)=x^2` tracing out a shape. Let's plot even more points here to get a better look at our function:
-::: column(width=300)
+ x-geopad(id="x-squared-plotter" width=400 height=400 x-axis="-4,4,1" y-axis="-1,8,1" axes grid padding="24 8 8 8" count=10): svg
-TODO coordinate plane
+ x-gesture(target="#x-squared-plotter")
-::: column.grow
+---
+> id: pick-domain
-The lowest y-value on the graph is about [[1250+-50 depending on scale]]. Let’s think about how we would figure out the highest y-value. If every student at the school bought a ticket, we would multiply [[student enrollment]] by $25 to calculate the highest possible y-value.
+### Finding Domain and Range
-:::
+Let's find the domain and range for this function. Remember, the domain is **all possible inputs** and the range is **all possible outputs**. Which of these are possible inputs for `f(x)=x^2`?
+
+ x-picker.numberPicker
+ .item#item1 -2
+ .item 4
+ .item 8.5
+ .item(data-error="invalid-domain-emoji") 🦊
+ .item -1.2
+ .item 0
+
+ x-gesture(target="#item1")
+
+---
+> id: input-domain
+
+All those numbers are valid because _any_ point on the number line works as input for `f(x)=x^2`. We could write the domain like this (remember that ∞ means infinity):
+
+**Domain:**
+
+`x` [[ > | < | = | ≤ | ≥ ]] `-∞`
+
+`x` [[ < | > | = | ≤ | ≥ ]] `∞`
+
+Here's a slightly simpler way to write the same thing:
+
+**Domain:** `-∞` [[ < | > | = | ≤ | ≥ ]] `x` [[ ≤ | > | = | < | ≥ ]] `∞`
+
+---
+> id: pick-range
+
+Now let's look at the range. Which are possible outputs for `f(x)=x^2`?
+
+ x-picker.numberPicker
+ .item(data-error="invalid-range-negative") -2
+ .item 4
+ .item 8.5
+ .item(data-error="invalid-range-emoji") 🦊
+ .item(data-error="invalid-range-negative") -1.2
+ .item 0
+
+---
+> id: input-range
+
+For `f(x)=x^2` every number is a possible input, but _not_ every number is a possible output. Specifically, the output can never be less than [[zero | one | negative one]]. So we could write the range like this:
+
+**Range:** [[0]] ≤ `f(x)` [[ < | > | = | ≤ | ≥ ]] `∞`
+
+---
+> id: find-domain-range-1
+Let's try some other functions, starting with `f(x)=-x^2+3`. Use the function plotter to help you find the domain and range:
+
+ x-geopad(id="domain-range-1-plot" width=400 height=400 x-axis="-4,4,1" y-axis="-5,4,1" axes grid padding="24 8 8 8"): svg
+
+ x-gesture(target="#domain-range-1-plot")
+
+
+**Domain:** `-∞` < `x` < `∞`
+
+**Range**: `-∞` < `f(x)` [[ < | > | = | ≤ | ≥ ]] [[3]]
--------------------------------------------------------------------------------
@@ -233,18 +495,13 @@ The lowest y-value on the graph is about [[1250+-50 depending on scale]]. Let’
> section: graphing
> sectionStatus: dev
-
- // NOTE
- // Local server trouble - not able to visualize design decisions. Followed the conventions I could find in terms of notes, fixme tags, image/ graph mock-ups, and targets. Targets do not have objects set, but the syntax should indicate where the target is intended to go. Worked last to first. Some of these conventions change as I learned more from other code.
-
- // EDITORIAL USE ONLY
- // [mock-up title image](https://drive.google.com/file/d/1P7d1Tfb7NwYLR5FM-e0zMbj5JKYgpJMJ/view?usp=sharing)
+> id: vault-graph
The Olympics is full of incredible athletic feats. It’s also full of interesting data. Graphs help us visualize that data. During our time together today, we will watch Olympic competitions and analyse their graphs for interesting information. Let’s head over to the gymnastics arena!
::: column(width=240)
-
+
::: column.grow
@@ -252,312 +509,356 @@ Ri Se-gwang of the People’s Republic of Korea is about to vault. He won the go
:::
- // NOTES
- // [citation](https://en.wikipedia.org/wiki/Gymnastics_at_the_2016_Summer_Olympics_%E2%80%93_Men%27s_vault)
- // Student presses play on a [video](https://www.youtube.com/watch?v=85v0Un19A94) (0:00-0:18) of Ri’s vault. Simultaneously, a distance-time graph populates in a card to the right of the animation.
+ x-video-graph
+ x-video(src="images/olympic_vault.mp4" poster="images/olympic_vault_poster.png" width=640 height=360 controls credit="©NBC")
+ x-coordinate-system(width=640 height=180 x-axis="0,29.1,1" y-axis="0,10,1" axis-names="Distance,Height")
-[vault mock-up](https://www.desmos.com/calculator/td3fynck7q)
+ // functions: https://www.desmos.com/calculator/td3fynck7q
+ // TODO: Realistic height/distance values. 30m is arbitrary, and there's no way he hits 8 meters.
There are several things going on here. Move the video back and forth to see how the graph lines up with the motion.
-First, we need to understand is what the axes represent. The x-axis in this graph is the horizontal distance Ri travels throughout his vault. It is measured in centimeters. The y-axis is the vertical distance in centimeters Ri travels. This gives us information about Ri’s position much like chess pieces on a board.
-
-The graph does not include any information about time. For example, we cannot tell _when_ Ri landed on the pit. Some of the graphs of later events will include time along the horizontal axis.
+First we need to understand what the axes represent. The X axis in this graph is the horizontal **Distance** Ri travels throughout his vault (measured in meters). The Y axis is the vertical **Height** Ri reaches (also in meters). Together these axes tell us every point along Ri's path.
-On this graph, we see the vault is at ([[3565+-5]], [[135+-5]])), which means Ri ran about 3.5 meters in his approach. The starting point on the runway is at the [__origin__](gloss:coordinate-system-origin). Ri lands at (3910, 30), which means the pit is about [[30]] centimeters tall.
+ // TODO: Implement +/- ranges for these
+Using this graph, we can see that Ri begins his vault at 25 meters, peaks at ([[27±0.5]], [[8±0.5]]), and lands at [[29±0.5]] meters.
---
-Let’s build some intuition for what graphs of different events look like. Match the graph to the event. Be sure to pay close attention to what the axes represent.
+> id: graph-match
+
+Let’s build some intuition for what graphs of different events look like. Match each event to a graph.
+
+ // TODO: Make completing this a required goal for this section
+
+ x-relation#graph-match-relation(randomize="true" requireMatch="true")
+ .item(slot="domain" name="vault" match="vault-graph") Vault
+ x-video(src="images/olympic_vault.mp4" poster="images/olympic_vault_poster.png" width=320 height=180 controls credit="©NBC")
+ .item(slot="domain" name="triple-jump" match="triple-jump-graph") Triple Jump
+ x-video(src="images/olympic_triple_jump.mp4" poster="images/olympic_triple_jump_poster.png" width=320 height=180 controls credit="©ESPN")
+ .item(slot="domain" name="dive" match="dive-graph") Diving
+ x-video(src="images/olympic_dive.mp4" poster="images/olympic_dive_poster.png" width=320 height=180 controls credit="©Fina")
+ .item(slot="domain" name="ski" match="ski-graph") Slalom Skiing
+ x-video(src="images/olympic_ski.mp4" poster="images/olympic_ski_poster.png" width=320 height=180 controls credit="©Olympic")
+ .item(slot="domain" name="hurdles" match="hurdles-graph") Hurdles
+ x-video(src="images/olympic_hurdles.mp4" poster="images/olympic_hurdles_poster.png" width=320 height=180 controls credit="©Olympic")
+ .item(slot="domain" name="swim" match="swim-graph") Swimming
+ x-video(src="images/olympic_swim.mp4" poster="images/olympic_swim_poster.png" width=320 height=180 controls credit="©Olympic")
+ .item(slot="range" name="vault-graph")
+ x-coordinate-system(width=300 height=150 x-axis="0,30,1" y-axis="0,15,1" axis-names="Distance,Height" grid="no" labels="no" crosshairs="no" fn="6/(1+((x-24)/1)^4)")
+ .item(slot="range" name="triple-jump-graph")
+ x-coordinate-system(width=300 height=150 x-axis="0,30,1" y-axis="0,15,1" axis-names="Distance,Height" grid="no" labels="no" crosshairs="no" fn="2/(1+((x-16)*1.5)^4)+2.5/(1+((x-20)*1.5)^4)+3/(1+((x-25)/1.5)^4)")
+ .item(slot="range" name="dive-graph")
+ x-coordinate-system(width=300 height=150 x-axis="0,30,1" y-axis="0,15,1" axis-names="Distance,Height" grid="no" labels="no" crosshairs="no" fn="12-((x-1)*1)^2")
+ .item(slot="range" name="ski-graph")
+ x-coordinate-system(width=300 height=150 x-axis="0,30,1" y-axis="0,15,1" axis-names="Distance,Height" grid="no" labels="no" crosshairs="no" fn="15+sin(x/2)-x/2")
+ .item(slot="range" name="hurdles-graph")
+ x-coordinate-system(width=300 height=150 x-axis="0,30,1" y-axis="0,15,1" axis-names="Distance,Height" grid="no" labels="no" crosshairs="no" fn="1/(1+((x-3)*1.5)^4)+1/(1+((x-6)*1.5)^4)+1/(1+((x-9)*1.5)^4)+1/(1+((x-12)*1.5)^4)+1/(1+((x-15)*1.5)^4)+1/(1+((x-18)*1.5)^4)+1/(1+((x-21)*1.5)^4)+1/(1+((x-24)*1.5)^4)+1/(1+((x-27)*1.5)^4)")
+ .item(slot="range" name="swim-graph")
+ x-coordinate-system(width=300 height=150 x-axis="0,30,1" y-axis="0,15,1" axis-names="Distance,Height" grid="no" labels="no" crosshairs="no" fn="1")
- // NOTE
- // Organized as a table in the Google Doc, so I (Dani) put it in a table here. The idea is to make it cards.
- // Match graphs to sports of time functions
+---
- // I'm having a tough time getting the shape to work with the scale. It might be easier to get the shape then layer the axes over - make this a static image. [sketch](https://drive.google.com/file/d/1uh9_0Abfs0lYIa8q6uZxUYFoRnHeLKtr/view?usp=sharing) disegard notes dotted graph
+> id: time-height-graph
-| Triple Jump | 50 M Freestyle | 100 M Hurdles | Vault | Diving | Skiing |
-| ----------- | -------------- | ------------- | ----- | ------ | ------ |
-| [Graph](https://www.desmos.com/calculator/qxfugm6xpi) | [Graph](https://www.desmos.com/calculator/z07xkap2bl) | [Graph](https://www.desmos.com/calculator/fwgpfln0ne) | [Graph](https://www.desmos.com/calculator/td3fynck7q) | [Graph](https://www.desmos.com/calculator/es8ugnvxeq) | [Graph](https://www.desmos.com/calculator/x58olmjtkl) |
-|  |  |  |  |  |  |
-| [Video](https://www.youtube.com/watch?v=wVqYjmK-T3w) | [Video](https://www.youtube.com/watch?v=qZvdhv9uhi0) | [Video](https://www.youtube.com/watch?v=AFNqbhJ3kmw) | [Video](https://www.youtube.com/watch?v=85v0Un19A94) | [Video](https://www.youtube.com/watch?v=wTX13JZFHd4) | [Video](https://www.youtube.com/watch?v=kU0a-kvvKW4) |
-| [Françoise Mbango Etone](https://en.wikipedia.org/wiki/List_of_Olympic_records_in_athletics) of Cameroon holds the Olympic record in women’s triple jump with a length of 15.39 meters. | [César Cielo](https://en.wikipedia.org/wiki/List_of_Olympic_records_in_athletics) of Brazil holds the Olympic record for the men’s 50 meter with a time of 21.47 seconds. | [Sally Pearson](https://en.wikipedia.org/wiki/List_of_Olympic_records_in_athletics) of Australia holds the Olympic record in the women’s 100 meter hurdles with a time of 12.35 seconds. | [Ri Se-gwang](https://en.wikipedia.org/wiki/Gymnastics_at_the_2016_Summer_Olympics_%E2%80%93_Men%27s_vault) of the People’s Republic of Korea won the gold medal for the vault in the 2016 Summer Olympic Games. | [Ren Qian](https://en.wikipedia.org/wiki/Diving_at_the_2016_Summer_Olympics_–_Women%27s_10_metre_platform) of China won the gold medal for diving in the 2016 Summer Olympic Games. |
-| Start: 985m | Finish: 805m | Vertical drop: 180m | Gates: 66 | Finish time: 48.33 |
+These graphs tell us everything about the athlete's position, but they say nothing about **Time**. For example, during Ri Se-Gwang's vault we cannot tell _when_ he lands on the mat. We only see Height and Distance.
-As you can see there are several different perspectives for graphing motion. One perspective is distance as a function of time. Select the events represented with this perspective.
+On our first graph, the X axis represented Distance (measured in meters). What if it represented Time (measured in seconds)?
- // NOTES
- // Multiple Select
+ x-video-graph
+ x-video(src="images/olympic_vault.mp4" poster="images/olympic_vault_poster.png" width=640 height=360 controls credit="©NBC")
+ x-coordinate-system(width=640 height=180 x-axis="0,9.1,1" y-axis="0,10,1" axis-names="Time,Height")
-We see the other two events are [[height | distance]] as a function of [[distance | height]]. We will look at two of these functions in more detail. Let’s head over to the pool.
+This looks similar to our first graph. This makes sense, because the number of meters that Ri runs is closely related to the number of seconds that pass. However, now we can measure new things about Ri's vault; he begins running about 1.5 seconds after the video begins. He hits the table at about [[5.9]] seconds, peaks at [[6.4]] seconds, and lands at [[7.3]] seconds.
---
-We are just in time for the men’s 50 meter freestyle finals. Keep a close eye on César Cielo Filho of Brazil. The graph of his swim will appear as the video plays.
+> id: time-distance-graph
- // NOTES
- // Student presses play on a [video](https://www.youtube.com/watch?v=qZvdhv9uhi0) (0:12-1:21) of Cielo’s record setting swim. Simultaneously, a distance-time graph populates in a card to the right of the animation.
+Our last graph plotted Height by Time. We can also plot Distance by Time. Note that Time is still on the X axis, but Distance is now on the Y axis:
-::: column.grow
+ x-video-graph
+ x-video(src="images/olympic_vault.mp4" poster="images/olympic_vault_poster.png" width=640 height=360 controls credit="©NBC")
+ x-coordinate-system(width=640 height=180 x-axis="0,9.1,1" y-axis="0,29.5,5" axis-names="Time,Distance")
+
+This graph looks different than the other two, because it tells us nothing about [[Height|Distance|Time]].
+
+But again, we can learn new things about Ri's vault. In the first few seconds, we can see him [[gaining|losing]] speed. He reaches his top speed after about [[4]] seconds. As he enters the vault, we see him [[losing|gaining]] speed.
+
+---
+
+> id: swim-graph
+
+Let's head over to the pool. We are just in time for the men’s 50 meter freestyle finals! Keep a close eye on César Cielo Filho of Brazil:
-César Cielo of Brazil holds the Olympic record for the men’s 50 meter with a time of 21.47 seconds.
+ x-video-graph.horizontal
+ x-video(src="images/olympic_swim.mp4" poster="images/olympic_swim_poster.png" width=640 height=360 controls credit="©NBC")
+ x-coordinate-system(width=180 height=400 x-axis="0,21.5,10" y-axis="0,51,10" axis-names="Time,Distance" style="margin-left: 20px;")
::: column(width=240)
-
+
-:::
::: column.grow
-What an emotional race for Cielo! He broke the world and olympic records with this swim. The graph shows Cielo’s record-breaking swim. The shape is one we have seen before in [graphing linear functions](/linear-functions/graphing-functions). Recall this graph represents all the ordered pairs matching inputs to outputs. We can use information from the graph to write the function describing Cielo’s swim. Let’s call the function f(t).
+What an emotional race for Cielo! He broke the world and olympic records with a time of 21.47 seconds. The graph shows Cielo’s record-breaking swim. The shape is one we have seen before in [graphing linear functions](/linear-functions/graphing-functions). Remember that every point on this graph represents an ordered pair: one input, and one output.
+
+:::
-Remember the general form for [__linear function__](gloss:linear-function) like this is y=mx+c, where m is the slope and c is the y-intercept. We replace y with the name of the function, and we replace x with the input variable for this function.
+We can use information from the graph to write the function describing Cielo’s swim. Let’s call the function f(t).
-{.text-center} `y=mx+c`
-`=> f(t)=m[[t]]+c`
+Remember the general form for a [__linear function__](gloss:linear-function) like this is y=mx+c, where m is the slope and c is the y-intercept. Our function f(t) has the same form, but we replace x with t:
-This means we need to find [[slope {.fixme} also accept "m"]] and [[y-intercept {.fixme} also accept "c"]] from the graph.
+{.text-center} `y=mx+c`
+⇓
+`f(t)=m`[[t]]`+c`
-Notice the [horizontal-axis](target:1_xAxis), in this graph shows [[time]] in seconds. The [vertical-axis](target:1_yAxis) is the distance from the starting block to the opposite end of the pool measured in [[meters]]. We see that the y-axis [__intercept__](gloss:intercept) is [[00]] meters, which represent the [[distance | time | number of laps]] of the race.
+This means we have two remaining unknown variables: [[m]] and [[c]].
-{.text-center} `y=mx+c`
-`=> f(t)=mt+[[0]]`
+---
-We are only missing slope. In this function, slope represents Cielo’s [[speed | distance | kick rate]]. How could we use the graph to see how fast he swims?
+> id: measure-slope-1
- // NOTES
- // This might work better as an animation to keep the stairs equal distances. We won't have to worry about reducing ratios.
- // Students can click two points on the graph. These coordinates appear in a x-y table. Dashed lines with the horizontal and vertical measurements appear one unit at a time as though counting the slope. Students can choose between one and five points to see a pattern (slope).
+::: column(width=180)
-As we move from left to right along the line, the vertical distances measure [[-2.35]] meters. The horizontal distances measure [[1]] second. Cielo’s speed [[stayed constant | increased | decreased]] for this race, which is a key feature of linear functions.
+ x-coordinate-system(width=170 height=350 x-axis="0,21.5,10" y-axis="0,51,10" axis-names="Time,Distance" fn="x*50/21.47")
-Recall that the [__slope__](gloss:line-slope) is the vertical change divided by the horizontal change (rise over run). The slope of f(t) is [[2.35]] meters per second.
-y=mx+c
+ // TODO: Fix targeting of swim-x-axis and swim-y-axis
-f(t)=[[2.35]]t+0
+::: column.grow
-::: column(width=240)
+Notice the [horizontal-axis](target:swim-x-axis) in this graph shows [[time | distance | speed]] in seconds. The [vertical-axis](target:swim-y-axis) is the distance travelled by Cielo, measured in [[meters]].
-[Cielo race mock-up](https://www.desmos.com/calculator/o3de2a7odh)
+We see that the graph [__intercepts__](gloss:intercept) the y-axis at [[0]] meters. Now we can fill in c, our y-intercept variable:
+
+{.text-center} `f(t)=mt+c`
+⇓
+`f(t)=mt+`[[0]]
:::
-Let’s say we want to know how long it took Cielo to swim the first 10 meters. Ten meters into the race, This means we are looking at [f(t)=10](target:1_cieloGraph). The phrase “how long” indicates we are solving for t.
+---
- // NOTE
- // Algebra Flow
+> id: measure-slope-2
-{.text-center} `f(t)=2.35t`
-`10=2.35t`
-`(10)/(2.35)=t`
-`4.25=t`
+Now the only variable we are missing is m, our [__slope__](gloss:line-slope). In this function, slope represents Cielo’s [[speed | distance | kick rate]]. How could we use the graph to measure this?
-Cielo swims the first 10 meters in just over [[4.1+-0.1]] seconds.
+::: column.grow
----
+Remember that slope is "Rise over Run", meaning Vertical Change (rise) divided by Horizontal Change (run). In this case, that means [[distance | time | speed]] divided by [[time | distance | speed]].
-Take a look at the top four finishers during this race:
+In this race, Cielo travels 50 meters in 21.47 seconds. So our slope is [[50]] divided by 21.47, or (roughly) 2.33. Now we can fill in our slope variable, m:
- // NOTES
- // Lines are labeled with the swimmer’s name and the function name. Moving the cursor along the active line show crosshairs extending to the axes. Students can also select a line, then select a value along one of the axes to lock the crosshairs to that value.
-| Swimmer | | Function Name | | Color |
-| :------ | | :------------ | | :---- |
-| Cesar Cielo Filho | | f(t) | | green |
-| Amaury Leveaux | | l(t) | | purple |
-| Alain Bernard | | b(t) | | blue |
-| Ashley Callus | | c(t) | | red |
+{.text-center} `f(t)=mt+0`
+⇓
+`f(t)=`[[2.33±0.01]]`t+0`
-::: column(width=240)
+::: column(width=180)
-[50 free mock-up](https://www.desmos.com/calculator/ahx1i7lkau)
+ x-coordinate-system(width=170 height=350 x-axis="0,21.5,10" y-axis="0,51,10" fn="x*50/21.47")
-::: column.grow
+ // TODO: Is there a way to reverse column order? I want this to display right of text on wide screens, but above text on narrow.
-All the lines cross the y-axis at [[0]] meters because this is the distance of the race. At first glance, we notice the graphs seem almost on top of each other. This must indicate that the swimmers’ speeds are similar.
+:::
-Say we want to figure out how many seconds Cielo is ahead of Leveaux after 10 meters. We already know Cielo swam this distance in 4.25 seconds. We don’t know the function rule for l(t), Leveaux’s swim, but we do have the graph. Find the time, t, where l(t) = [[10]].
+---
- // NOTES
- // Student clicks on l(t) to make it the active function. Student clicks on 10 on the y-axis. Dotted line from y-axis to l(t) appears. Dotted line from l(t) at 10 meters to corresponding t-value appears.
+> id: measure-slope-3
-Leveaux swims the first 10 meters in [[4.56+-.02]] seconds. That means Cielo was only [[0.46+-0.12]] seconds ahead of Leaveaux!
+::: column(width=180)
-Leveaux and Bernard trained together, and it shows. They stay neck-in-neck the whole race. They finish only [[0.4+-.01]] seconds apart.
+ x-coordinate-system#slope-graph-3(width=170 height=350 x-axis="0,21.5,10" y-axis="0,51,10" fn="x*50/21.47")
-Use the graph to find where everyone is at the 20 second mark.
-f(20)=[[47+-0.1]]
-l(20)=[[43+-0.1]]
-b(20)=[[43.8+-0.1]]
-c(20)=[[41.2+-0.1]]
+::: column.grow
-Callus is 6 meters behind [[Cielo | Leveaux | Bernard]] at the 20 second mark.
+ // TODO: Can I `align-items: center;` this column group?
-:::
+Another way to measure the slope is to ask "how many meters does Cielo swim in one second?"
+
+ // TODO: Fix targeting!
+Check the [{.red} graph](target:#slope-graph-3) yourself; in a single second, the graph "rises" [[2.33±0.03]] meters and "runs" [[1]] second. This gives us a slope of [[2.33±0.03]]
-Let’s head over to the diving pool for the women’s 10 meter platform competition.
+The slope is the same at every point on this graph, because Cielo’s speed [[stayed constant | increased | decreased]] for this race. This is a key feature of linear functions.
+
+:::
---
-::: column.grow
+> id: swim-algebra
-Meanwhile, on the other side of the Aquatics center, a diving competition is in progress.
-Ren Qian is among the youngest Olympic medalists. She is diving now - let’s [watch](https://www.youtube.com/watch?v=wTX13JZFHd4)
+Now we have a complete linear function: `f(t)=2.33t+0`
- // NOTES (0:00-0:12)!
+What can we do with this? Well, let’s say we want to know how long it took Cielo to swim the first 10 meters. `f(t)` is the [[distance | time | speed]] Cielo swims, so at 10 meters `f(t)=`[[10]]. We need to solve for t, the number of seconds it takes to reach that point:
-Ren Qian of China won the gold medal for diving in the 2016 Summer Olympic Games.
+{.text-center} `f(t)=2.33t+0`
+[[10]]`=2.33t`
+`10/(2.33)=`[[t]]
+[[4.3±0.01]]`=t`
-::: column(width=240)
+ // TODO: Ask if we should assume students have access to a calculator
-[Ren image mock-up](https://img.washingtonpost.com/rf/image_1484w/2010-2019/WashingtonPost/2016/08/19/Production/Daily/Style/Images/2016-08-18T192657Z_01_OLYGK111_RTRIDSP_3_OLYMPICS-RIO-DIVING-W-10MPLATFORM.jpg?uuid=BvCIjGYKEeaLJ7uLo5SXog)
+There you have it; Cielo swims the first 10 meters in about [[4.3±0.01]] seconds.
-::: column(width=240)
+---
- // NOTES
- // make sticky
-[dive mock-up](https://www.desmos.com/calculator/es8ugnvxeq)
+> id: swim-system
+
+Now let's take a look at the top four finishers during this race:
::: column.grow
-Let’s call the function representing Ren’s dive d(x). The input values, x, are horizontal distances from the platform. The output values, d(x), are Ren’s [[height]] throughout the dive. Immediately, we notice the shape of this graph is different from the swimming graphs above. This graph has [[2]] turning points compared to the linear functions [[0]] turning points.
+ // NOTES
+ // Lines are labeled with the swimmer’s name and the function name. Moving the cursor along the active line show crosshairs extending to the axes. Students can also select a line, then select a value along one of the axes to lock the crosshairs to that value.
+| Swimmer | | Function Name |
+| :------ | | :------------ |
+| Cesar Cielo Filho | | {.red#cielo-distance} `f(t)=0` |
+| Amaury Leveaux | | {.blue#leveaux-distance} `l(t)=0` |
+| Alain Bernard | | {.green#bernard-distance} `b(t)=0` |
+| Ashley Callus | | {.purple#callus-distance} `c(t)=0` |
-Graphs with this shape are called cubic functions. We can get important information from the graph even without knowing the function equation. Match the given statements to the graph.
+::: column(width=220)
+
+ x-coordinate-system#multi-swimmer-graph(width=200 height=260 x-axis="0,25,10" y-axis="0,51,10" axis-names="Time,Distance" crosshairs="no")
:::
- // NOTES
- // Students cards for all of the items below, and then drag them onto the corresponding point along the graph. Let’s show all the contextual statements, but talk about one key feature at a time.
+{.text-center#time-variable-text} `t=0`
+
+ x-slider#swim-slider(steps=500)
-| Place contextual statement cards on graph | | Target key feature appears when card is placed | |Function notation appears when card is placed |
-| :---: | | :---: | | :---: |
-| Ren’s takes her place on the platform. | | Vertical intercept | | |
-| Ren reaches the highest point of her dive. | | maximum | | |
-| Ren completes 3.5 somersaults. | | decreasing | | 0.335 id: dive-graph
+> goals: card1 card2 card3 card4 card5 card6
-Intuitively, we understand that the graph is increasing when Ren’s body is moving [[up | down]]. The notation for increasing is different from tuning points and intercepts. Since the graph increases for more than one point, we represent the section of the graph using an [__interval__](gloss:interval). The interval communicates the [[x | d(x)]] values corresponding to Ren’s increasing height. Note that there are many different ways to write intervals, we use inequalities in this chapter.
+Meanwhile, on the other side of the Aquatics center, a diving competition is in progress.
+Ren Qian is among the youngest Olympic medalists. She is diving now - let’s [watch](https://www.youtube.com/watch?v=wTX13JZFHd4)
+
+ x-video-graph.horizontal
+ x-video(src="images/olympic_dive.mp4" poster="images/olympic_dive_poster.png" width=640 height=360 controls credit="©Fina")
+ x-coordinate-system(width=180 height=400 x-axis="0,4.3,1" y-axis="-8,12.5,4" axis-names="Time,Height" style="margin-left: 20px;")
-Ren moves up during the intervals:
+::: column.grow
- // NOTES
- // Multiple selector (shuffle order)
+Ren Qian of China won the gold medal for diving in the 2016 Summer Olympic Games.
-0=.
+Recall that for any function, the y-intercept is where t=[[0]]. In function notation, this looks like d([[0]])=10.
-Recall [__range__](gloss:range) is the set of all heights Ren travels. Notice that Ren goes below the surface of the water. In fact, we can use the function’s [[minimum | maximum | horizontal intercept | vertical intercept]] to determine the lower bound on the range. The minimum d(x) is [[-1.623]] meters.
+Similarly, the x-intercepts are where [[d(t) | t]]=0. This graph has [[2]] x-intercepts. They represent the surface of the water.
-Similarly, the maximum d(x) gives us the upper bound on the range. Therefore, the range is [[-2]] <= d(x) <= [[10.941]].
+---
+
+When we talk about the maximum, we are really talking about the highest [[d(t) | t]] value. Ren’s greatest height is [[10.8±0.3]] meters, which she reaches after [[0.3±0.2]] seconds. In function notation, this looks like d([[0.3±0.2]])=[[10.8±0.3]].
+
+The minimum is Ren’s lowest height. In this graph her lowest point is underwater. Because the x-axis represents the surface of the water, the minimum d(t) is [[negative | positive | zero]]. She turns around [[3.2±0.3]] seconds after diving.
---
-Let’s head to the beach for the gold medal men’s volleyball match between Brazil and Italy. The teams engage in a beautiful [volley](https://www.youtube.com/watch?v=k4ux0jau_ws) (5:56-6:01). As you watch the video, notice the shape of the graph. Is it what you expect?
+> id: dive-intervals
+> goals: card0 card1 card2
- // NOTES
- // Students watch the video of the volley and the graph appears simultaneously.
- // I'm thinking students can use estimates for y-values. The times should be pretty close to the video, but the heights should just be correct relative to each other (e.g. the second relative max is higher than the first and lower than the third). We can have the net height on the graph for reference.
- // We make this graph, add discussion.
+Intuitively, we understand that the graph is increasing when Ren’s body is moving [[up | down]]. The notation for increasing is different from turning points and intercepts. Since the graph increases for more than one point, we represent the section of the graph using an [__interval__](gloss:interval). The interval specifies the [[t | d(t)]] values corresponding to Ren’s increasing height. Note that there are many different ways to write intervals; in this chapter, we will write them as [__inequalities__](gloss:inequality). We'll break this graph into three intervals:
+
+ x-card-graph
+ x-coordinate-system(width=500 height=300 x-axis="0,4.3,1" y-axis="-8.5,12.5,4" axis-names="Time,Height" crosshairs="no")
-[sketch](https://drive.google.com/file/d/1mrT-d6Xwunc6I6hC7y2U_38bWR1y-lF5/view?usp=sharing)
+Notice the [[maximum | minimum]] is where Ren’s path changes from increasing to decreasing heights. The [[minimum | maximum]] is where Ren’s path changes from decreasing to increasing.
-The graph shows the volley as a function of [[time | distance | height]]. That means for at each moment, the ball has a position marked by [[(time, height) | (height, time)]]. For example, the ball is [[9+-.5 ]] feet high at 2 seconds. We can find information about _when_ certain things happen.
+---
-For instance, the ball spends about [[3.5+-.5]] seconds above the net. Brazil scores a point after about [[4.75+-.5]] seconds. Italy places a beautiful set at about [[3]] seconds. The ball reaches its maximum height, about [[11+-1]] feet, off of the set.
+> id: dive-domain-range
-Notice that the graph does not show when the ball changes direction. This could happen if the graph were a function of [[distance | height | time]]. Such a graph would tell us _where_ certain things happen. We can’t tell by looking at this graph which side of the net the ball is on.
+Let’s think about the input and output values for d(t). Recall [__domain__](gloss:domain) is the set of all possible input values for d(t). One method for finding the domain is starting with the set of [Real numbers](gloss:real-numbers) and narrowing down to match the situation.
-___
+Ren’s dive begins at 0 seconds. She resurfaces at about t=[[4.2±0.2]] seconds. Therefore, we can write the domain as [[0]]≤x≤[[4.2±0.2]]. Note that we're using `≤`, which unlike `<` [[includes | excludes]] the minimum and maximum values.
-### Creating Graphs
+Recall [__range__](gloss:range) is the set of all heights Ren travels. We can use the function’s [[minimum | maximum | horizontal intercept | vertical intercept]] to determine the lower bound on the range. On this graph, that's about [[-7.6±0.5]] meters.
- // NOTES
- // From David re: diving graph. Capturing idea.
- // Great graph. I really like how you included the part under the water. That's nice to show. I wonder if before showing the graph, students could draw the shape they think the graph would be? Maybe have an image of a diving board at the 10 meter mark and they use like a "scribble" or "line" tool to draw in the line they think the graph will be. Then, when they are done, the graph you have gets superimposed on their graph and their line fades away.
- // Create videos similar to the ball bouncing activity here: https://curriculum.illustrativemathematics.org/HS/teachers/1/4/8/index.html
- // Students place a point on the coordinate plane and label it with a key feature name. The might also be able to place approximate points on the internals of increase and decrease. Students click “graph”, or some such button, to see a line connect the points according to their labels. Once students see the graph, they can choose to edit or submit for checking.
- // Alternate interactive ideas
- // Give components of this similar to the piecing it together activity. [This is the same idea](https://www.google.com/url?q=https://curriculum.illustrativemathematics.org/HS/teachers/1/4/12/index.html&sa=D&ust=1595249230079000&usg=AFQjCNFlsjZxKJ9PGN9cluHSZm-OAFBaOA) used in the next chapter for building the tri graph.
+Similarly, the maximum d(x) gives us the upper bound on the range. Therefore, the range is [[-7.6±0.5]] ≤ d(x) ≤ [[10.8±]].
-The women’s pole vault is just about to start. You will be drawing the graph for this event.
+___
- // NOTES
- // Allow scrubbing in video. Superimpose timer on the frames to make graphing easier. [1:00 - 1:15](https://www.youtube.com/watch?v=PPaUgaBor2I)
+> id: pole-vault
+> goals: submitCorrect
-We like to start graphing using a table. Fill in the table below. Note the landing pad, called the pit, is 0.81 meters tall.
+### Creating Graphs
- // NOTES
- // Students fill in the missing values.
+The women’s pole vault is just about to start. Ekaterini Stefanidi takes her position:
-| Time (s) | | Height (m) |
-| :------: | | :--------: |
-| 0 | | 0 |
-| 1 | | 0 |
-| 4 | | [[0]] |
-| 5.5 | | [[0]] |
-| 6 | | [[1.5+-.2]] |
-| 6.5 | | [[3.2+-.2]] |
-| 7 | | [[4.85]] |
-| 7.5 | | [[3.2+-.2]] |
-| 8 | | [[0.81]] |
+ x-video#pole-vault-video(src="images/olympic_pole_vault.mp4" poster="images/olympic_pole_vault_poster.png" width=640 height=360 controls credit="©Olympics")
-Plot these values on the coordinate plane.
-[sketch](https://drive.google.com/file/d/1iywz65_-0ySs5Sd6rSxWF9RN_zun7pLc/view?usp=sharing)
+You will be drawing the graph for this event. Here is everything you need to know:
-This graph is interesting because between [[0]] and about [[5.7+-0.2]] seconds, the graph is constant. Stefanidi’s maximum height is [[4.85]] meters. The last point on the graph is at ([[8]], [[0.81]]) because she lands on the pit, not the ground.
+- The runway is 40m long
+- The pole is 4.45m long
+- The landing pad is 5x5x0.8m
+- The bar is 4.85m high
----
+Good luck!
-### Systems of Functions/ Simultaneous Functions
+ x-draw-graph#pole-vault-graph
+ x-coordinate-system(width=600 height=200 x-axis="0,45.5,5" y-axis="0,5.1,1" axis-names="Distance,Height" crosshairs="no")
+ div.scoring-row
+ button.btn Submit
+ div.judge-text
+ div.scores
-Let’s head over to the track for the women’s 800 meter final. Looks like we arrive in time to catch the last 200 meters of the race. [3:22-4:00](https://www.youtube.com/watch?v=h83yS9gPkA8)
+ x-gesture(target="#pole-vault-graph")
- // NOTES
- // Information for the interactive:
+---
-| __Athlete__ | | Adelle Tracey | | Laila Boufaarirane | | Raevyn Rogers |
-| :---------: | | :-----------: | | :----------------: | | :------------:|
-| __Country__ | | GBR | | FRA | | USA |
-| __@ 90.63 s__ | | 600 | | 598 | | 590 |
-| __@ 800 m__ | | 121 | | 126 | | 120.2 |
-| __color__ | | black | | green | | orange |
-| __function__ | | g(t)=6.58545x+3.16101 | | f(t)=5.71105x+80.4071 | | u(t)=7.10179x-53.6354 |
+> id: running-graph
- // NOTES
- // Students see the graph populate in time with the video.
- // We'll need to think about where we want to place this in the coordinate plane. Floating axes are nice as seen here. We could also change it such that 90.63s is t=0, though I think that would be a higher barrier to understand than not seeing the origin on the coordinate plane.
+### Systems of Functions/ Simultaneous Functions
+
+Let’s head over to the track for the women’s 800 meter final. Looks like we arrive in time to catch the last 200 meters of the race:
+
+ x-video-graph#running-video-graph
+ x-video(src="images/olympic_running.mp4" poster="images/olympic_running_poster.png" width=640 height=360 controls credit="©RA")
+ div
+ div.runner-name-key
+ div
+ img(src="images/tracey_face.png")
+ div.red Adelle Tracey
+ div g(t)
+ div
+ img(src="images/boufaarirane_face.png")
+ div.green Laila Boufaarirane
+ div f(t)
+ div
+ img(src="images/rogers_face.png")
+ div.blue Raevyn Rogers
+ div u(t)
+ x-coordinate-system(width=400 height=320 x-axis="80,130,10" y-axis="550,800,50" axis-names="Time,Distance")
[800 M mock-up](https://www.desmos.com/calculator/msryjohuz9)
@@ -573,62 +874,90 @@ The graphs intersect when one runner passes another. When the runners have about
---
-Rogers passes Boufaarirane at about [[96.5+-.25]] seconds when they are both about [[630]] meters into the race. In function notation, this looks like f([[96.5+-.25]])=u([[96.5+-.25]])=[[630]].
+Rogers passes Boufaarirane at about [[97±3]] seconds when they are both about [[630±10]] meters into the race. In function notation, this looks like f([[97±3]])=u([[97±3]])=[[630±10]].
-About [[13.5+-.25]] seconds later, [[Rogers | Boufaarirane | Tracey]] overtakes [[Tracey | Rogers | Boufaarirane]]. They have about [[73+-1]] meters to the finish line.
+About [[13.5±1]] seconds later, Rogers overtakes [[Tracey | Rogers | Boufaarirane]]. They have about [[73±10]] meters to the finish line.
---
-The slopes of each function tell us each runner’s [[speed | distance | cadence]]. Rogers is running at about [[7.1+-0.2]] meters per second.
+> id: running-slope-rogers
- // NOTES
- // We looked at counting slope above. This is a review of calculating slope. They need to pull the values off of the graph.
- // Algebra Flow
+The slope of each function tells us that runner’s [[speed | distance | time]]. Let's focus on Rogers:
+
+ x-coordinate-system#rogers-slope-graph(width=400 height=200 x-axis="80,130,10" y-axis="550,800,50" axis-names="Time,Distance" fn="7.10179x-53.6354")
+
+Remember that slope (m) is change in y over change in x. Written out, it looks like this:
-{.text-center} `m=(y_2 - y_1)/(x_2 - x_1)`
-`m= (y_2 - 590)/(x_2 - 90.63)`
-`m= (800-590)/(120.2-90.63)`
-`m= (210)/(29.57)`
-`m=7.1`
+{.text-center} `m=(y_2 - y_1)/(x_2 - x_1)`
-Roger’s speed is [[0.6+-0.2]] meters per second faster than Boufaarirane and [[1.4+-0.2]] meters per second faster than Tracey.
+Let's calculate our change in x first. We'll use the period from `t=90` to `t=100`. This is a period of [[10]] seconds, which means:
+
+{.text-center} `x_2-x_1=`[[10]]
---
-In this system of functions, we can see who is ahead at any given time during the race. For example, we write f(t) > u(t) when [[Boufaarirane is ahead of Rogers | Rogers is ahead of Boufaarirane]].
+Now we need to calculate `y_2-y_1`. We know that `y_1=u(90)`, and `y_2=`u([[100]]).
- // NOTES
- // Used both < and > to show both. Would it be better for students to see a "readable" pattern than matches the sentences, which would only use > ?
+Using the graph above, we can see that `u(100)=`[[656.5]] and `u(90)=`[[585.5]]. So now we can calculate:
-| Students label with the given contextual statement cards. | | This information appears after the functions notation card is correctly placed. | | Extra information. Not cards. |
-| :---: | | :---: | | :---: |
-| Boufaarirane is ahead of Rogers. | | f(t) > u(t) | | 90.63= f(t) | | |
-| Rogers is ahead of Tracey. | | g(t) < u(t) | | 109.998 f(t) | g(t) < f(t)]]. We can think of this as “the range of time when Tracey has run a farther distance than Boufaarirane”. That range is [[90.63+-0.2]] <= t <= [[121]]. We can do a similar analysis for each pair of functions.
+---
-Looking at the three functions on the coordinate plane, we can see that the relationship between u(t) and g(t) changes from g(t)>u(t) to u(t)>g(t) when [[Rogers passes Tracey | Tracey passes Rogers | Rogers passes Boufaarirane | Boufaarirane passes Rogers]]. This means that the upper bound on g(t)>u(t) is where g(t) [[= | < | >]] u(t), which is t=[[110+-0.2]] seconds. This t-value, 110 seconds, is also the lower bound on u(t)>g(t).
+Now we have everything we need to finish our equation:
-----
+{.text-center} `m=(y_2 - y_1)/(x_2 - x_1)=`[[7.1]]
-Finally, here is one more function that represents a sport. Can you think of what it is, and write a short story that explains the different features of the chart?
+---
-TODO: draw chart
-<< free-form text input >>
+> id: running-slope-boufaarirane
+Let's follow the same process for Boufaarirane—but this time, you're on your own!
---------------------------------------------------------------------------------
+ x-coordinate-system#boufaarirane-slope-graph(width=400 height=200 x-axis="80,130,10" y-axis="550,800,50" axis-names="Time,Distance" fn="5.709x+80.55")
+It's simple, just fill in the formula:
-## Piecewise Functions
+{.text-center} `m=(y_2 - y_1)/(x_2 - x_1)=`[[5.71±0.2]]
+
+So Rogers could overtake Boufaarirane in the last 200 meters of the race because she was running [[1.4±0.2]] meters per second faster.
+
+---
+
+> id: running-cards
+> goals: card0 card1 card2 card3 card4
+
+In this system of functions, we can see who is ahead at any given time during the race. For example, we write f(t) > u(t) when [[Boufaarirane is ahead of Rogers | Rogers is ahead of Boufaarirane]].
+
+
+ x-card-graph#runner-card-graph
+ div
+ div
+ div.red Tracey
+ div g(t)
+ div
+ div.green Boufaarirane
+ div f(t)
+ div
+ div.blue Rogers
+ div u(t)
+ x-coordinate-system(width=600 height=400 x-axis="90,130,10" y-axis="600,800,50" axis-names="Time,Distance" crosshairs="no")
+
+---
+
+When we talk about one function being greater than another, we are using the [[output | input]] values to identify a range of [[input | output]] values. For example, we see Tracey is ahead of Boufaarirane for this entire stretch of the race. This is expressed as [[g(t) > f(t) | g(t) < f(t)]]. We can think of this as “the range of time when Tracey has run a farther distance than Boufaarirane”. That range is [[90.5±1]] ≤ t ≤ [[121±1]]. We can do a similar analysis for each pair of functions.
+
+Looking at the three functions on the coordinate plane, we can see that the relationship between u(t) and g(t) changes from g(t)>u(t) to u(t)>g(t) when [[Rogers passes Tracey | Tracey passes Rogers | Rogers passes Boufaarirane | Boufaarirane passes Rogers]]. This means that the upper bound on g(t)>u(t) is where g(t) [[= | < | >]] u(t), which is t=[[110±2]] seconds. This t-value is also the lower bound on u(t)>g(t).
+
+----
-> section: piecewise
-> sectionStatus: dev
> id: fn-sketch
+Finally, here is one more function that represents a sport. Can you think of what it is, and write a short story that explains the different features of the chart?
+
+TODO: draw chart
+<< free-form text input >>
+
Draw a Function:
x-coordinate-sketch(width=600 height=400 x-axis="-1,10,1" y-axis="-5,5,1")
@@ -638,215 +967,255 @@ Type some text:
x-free-text(placeholder="Your answer…")
- // NOTE
- // Local server trouble - not able to visualize design decisions. Followed the conventions I could find in terms of notes, fixme tags, image/ graph mock-ups, and targets. Targets do not have objects set, but the syntax should indicate where the target is intended to go. Worked last to first. Some of these conventions change as I learned more from other code.
+--------------------------------------------------------------------------------
-Multisport races test athletes endurance. Swimrun is a rather new multi sport competition that started in 2002 in Sweden. The story goes that the owner of the Utö Värdshus hotel, his friend, and two hotel staff challenged each other to a two-versus-two race from the Utö Värdshus hotel, across three islands, to Sandhamn. The losing team would pay for everyone’s post-race meals. How long do you think the race lasted?
+## Piecewise Functions
- // NOTES
- // Anders Malm - owner of Utö Värdshus, Janne Lindberg - friend, Andersson brothers - staff at Utö Värdshus
- // [citation](https://en.wikipedia.org/wiki/Swimrun)
+> section: piecewise
+> id: piecewise-intro
+
+::: column.grow
+
+Only the most elite athletes make it to the Olympics. However, similar athletic events are enjoyed by people all around the world—and we can graph those too. One popular choice is the triathlon, where athletes complete an epic long-distance race broken into three distinct events.
+
+Triathlons can be any combination of three sports—some involve canoeing, ice skating, and at least one even features rappelling—but the most common arrangement is **swim**, **bike**, **run** (in that order).
- // Map between these two locations. Include images from each place. I like David’s map zoom effect he has mentioned in a couple of chapters. While it wouldn’t necessarily add to the math directly, it would make the story more interesting and help student intuit distance.
- // EDITOR USE ONLY
- // [Utö Värdshus](https://www.utovardshus.se/wp-content/uploads/2019/03/Liggande_VH-fr%C3%A5n-Bastun_Copyright-Ut%C3%B6-V%C3%A4rdshus-1.jpg)
+::: column(width=200)
- // [map view](https://www.google.com/maps/dir/Sandhamn,+Sweden/Ut%C3%B6+V%C3%A4rdshus,+Pr%C3%A4stbacken+22,+130+56+Ut%C3%B6,+Sweden/@59.1054899,18.3165704,10z/data=!4m14!4m13!1m5!1m1!1s0x46f5741069214bbf:0xbfee8fb6ece8997c!2m2!1d18.9108304!2d59.2878703!1m5!1m1!1s0x46f58b4425a902e9:0xb792bc38be8de224!2m2!1d18.329336!2d58.967417!3e4)
+
-
+:::
+
+Since [[biking | swimming | running]] is the fastest and [[swimming | biking | running]] is the slowest, which of these graphs best represents such a race?
-The race ended up taking over 24 hours! The friends did the same race the next year, and the idea for the ÖtillÖ (island to island) was born.
+ x-picker.graphPicker
+ .item
+ x-coordinate-system#graph1(width=200 height=200 x-axis="0,65,10" y-axis="0,5.5,1" axis-names="Time,Distance" crosshairs="no")
+ .item(data-error="swimming-faster-running")
+ x-coordinate-system#graph2(width=200 height=200 x-axis="0,65,10" y-axis="0,5.5,1" axis-names="Time,Distance" crosshairs="no")
+ .item(data-error="wrong-distance")
+ x-coordinate-system#graph3(width=200 height=200 x-axis="0,65,10" y-axis="0,5.5,1" axis-names="Time,Distance" crosshairs="no")
+ .item(data-error="running-too-fast")
+ x-coordinate-system#graph4(width=200 height=200 x-axis="0,65,10" y-axis="0,5.5,1" axis-names="Time,Distance" crosshairs="no")
---
-::: column(width=240)
+> id: piecewise-definition
- // NOTES
- // Something like this would be awesome. Transition from water to run. Setting. Dressed for water in Sweden.
- // EDITORIAL ONLY
+::: column.grow#piecewise-description-column
-
+This is called a [__piecewise function__](gloss:piecewise), where different rules apply within different ranges of input values. Notice how the [{.red}first](target:line1) segment has a different slope than the [{.blue}second](target:line2) segment, and occupies a different range of inputs.
-::: column.grow
+For example, the [{.green}third](target:line3) segment has a slope of `1/20`, begins at `t=`[[40]], and ends at `t=`[[60]].
+
+::: column.grow#piecewise-graph-column
-We are training for the ÖtillÖ. We need to get used to the feeling of swimming then immediately running. We decide to swim for 500 meters then run for 5 kilometers. Choose the graph that represents the athlete’s distance as a function of time, d(t).
+ x-coordinate-system.piecewise-cases(width=300 height=300 x-axis="0,65,10" y-axis="0,5.5,1" axis-names="Time (min),Distance (km)" crosshairs="no")
:::
- // NOTES
- // Multiple selector with one choice
- // [option 1](https://drive.google.com/file/d/1UhFc87ir21UUNnQWmJ0UhvrXn6bZu6Lo/view?usp=sharing)
- // [option 2](https://drive.google.com/file/d/1T0q0btNyuiNaOVWk6NbM8dQydXthhwtv/view?usp=sharing)
- // [option 3](https://drive.google.com/file/d/18DMyDqbjmOq7Wdou-yTuMvI0NTBfSg-T/view?usp=sharing)
- // [option 4](https://drive.google.com/file/d/1zKhk2t_V00SSEsmyXWCvohxnOyzMk0Q6/view?usp=sharing)
- // Option 2 is the object of the targets below.
+---
-This is an example of a [__piecewise function__](gloss:piecewise) where different rules apply to different sets of input values. We can see the [first section](target:1_piecewise) of the graph has a different slope than the [second section](target:2_piecewise).
+> id: piecewise-cases
-One of the most common ways to write piecewise functions is by using cases.
+One of the most common ways to write piecewise functions is by using "cases":
- // NOTES
- // It would be great to target or color code the different components of the function. Input ranges point to x-axis, rules point to graph.
-`d(t)= {(1/20t,0<=t<10),(1/6 t-7/6,10<=t<=40):}`
+::: column.piecewise-function-left(style="width:60px; margin:0;")
-Each line in this function is a case. It includes the function rule and the input values where the rule is used. We read this function as “The function d has a value of (1/20)t when t is at least 0 and up to 10. Function d is (⅙)t-(7/6) when t is at least 10 and no more than 40.”
+`d(t)=`
-Let’s continue to get our feet wet in the world of piecewise functions.
+::: column(style="width:240px;")
+
+{div.red}(`(1/40 t, 0 ≤ t < 20)`,
+
+{div.blue}`(6/40 t, 20 ≤ t < 40)`,
+
+{div.green}`(3/40 t, 40 ≤ t ≤ 60)`:)
+
+:::
+
+Each case defines a function rule and a range of input values where the rule applies. So `t=44` falls into the [[third | second | first]] case, and `t=6` falls into the [[first | second | third]] case.
---
-::: column.grow
+> id: piecewise-endpoints
-The 400 meter medley relay in swimming includes four swimmers. Each athlete swims 100 meters of the relay in one of the four strokes. These 100 meter segments are called legs. This relay includes [backstroke](target:1_relayGraph), [breaststroke](target:2_relayGraph), [butterfly](https://www.desmos.com/calculator/y3zz7gphmo), and [freestyle](https://www.desmos.com/calculator/y3zz7gphmo) in that order. The graph below shows s(d). Notice the vertical axis is [[distance | speed]] in meters. The horizontal axis represents [[speed | distance]] in meters per second. Is s(d) a [__function__](gloss:function)?
+Let's look at the speed of each segment, which we'll call `s(t)`. Recall that our speed is equal to the [[slope | y-axis | area]] of the previous graph:
-::: column(width=240)
+ x-coordinate-system.piecewise-step(width=600 height=200 x-axis="0,65,10" y-axis="0,0.25,0.1" axis-names="Time (min),Speed (km/min)" crosshairs="no")
- // EDITORIAL USE ONLY
+Notice that each segment has two endpoints, but there are *two kinds* of endpoints: [__closed ●__](target:closed-endpoint) and [__open ◦__](target:open-endpoint). A closed endpoint means the segment includes that point. An open endpoint means the segment includes everything _until_ that point, but not the point itself. This means a closed endpoint represents [[≤ | < | =]] and an open enpoint represents [[< | ≤ | =]].
-
+---
- // NOTES
- // Add vertical line test tool.
- // [200 Medley Mock-up](https://www.desmos.com/calculator/y3zz7gphmo)
- // Graph above is the object of the targets in relay paragraph.
+> id: endpoints-1
+> goals: endpoint-puzzle
-:::
+Make the endpoints match the ranges. Remember that ● represents ≤, and ◦ represents <:
-Recall that functions cannot have one input going to [[more than one | only one]] output value. The [__vertical line test__](gloss:vertical-line-test) is a tool to test whether a relation is a function. Use the vertical line above to test this relation.
+ x-piecewise-endpoint-puzzle#endpoints-1
+ div
+ x-coordinate-system(width=600 height=200 x-axis="0,11,1" y-axis="0,6,1" axis-names="X,Y" crosshairs="no")
+ div.scoring-row
+ button.btn Submit
+ div.prompt-text
+ x-gesture(target="#endpoints-1 .endpoint")
-:::
+ // TODO: Make gesture successfully target dot
---
-We need to pay close attention to the endpoints of each section of the domain. The endpoints ensure each element of the domain is matched to only one element of the range. With this in mind, select the function that matches the graph above.
+> id: endpoints-2
+> goals: endpoint-puzzle
- // NOTES
- // Multiple selector with one correct answer
+Now try this one!
+
+ x-piecewise-endpoint-puzzle#endpoints-2
+ div
+ x-coordinate-system(width=600 height=200 x-axis="0,11,1" y-axis="0,6,1" axis-names="X,Y" crosshairs="no")
+ div.scoring-row
+ button.btn Submit
+ div.prompt-text
-[option 1](s(d)={(1.3, 0<=d<=100),(1.2, 100 id: endpoints-3
+> goals: endpoint-puzzle
+
+How about this one?
-Notice the graph shows “<” as an open circle - the same would be true for endpoint containing “>”. The closed circles inculcate “<=” and “>=”.
+ x-piecewise-endpoint-puzzle#endpoints-3
+ div
+ x-coordinate-system(width=600 height=200 x-axis="0,11,1" y-axis="0,6,1" axis-names="X,Y" crosshairs="no")
+ div.scoring-row
+ button.btn Submit
+ div.prompt-text
---
-The function s(d) is a special kind of piecewise function called a [__step function__](gloss:step-function). One major difference between s(d) and d(t) above is all the slopes in s(d) are [[0]].
+> id: endpoints-4
+> goals: endpoint-puzzle
-The fastest leg of the relay is [[freestyle | butterfly | breaststroke | backstroke]] with a speed of [[1.5+-0.025]] meters per second. The slowest leg was [[breaststroke | freestyle | butterfly | backstroke]] completed in [[1.2+-0.025]] meters per second. Each leg of the race was [[100]] meters long.
+Now make a function that covers the whole range of `1 ≤ x ≤ 10` (remember, a function must pass the [__Vertical Line Test__](gloss:vertical-line-test)—only one output per input!):
-We’re ready to dive into graphing.
+ x-piecewise-endpoint-puzzle#endpoints-3
+ div
+ x-coordinate-system(width=600 height=200 x-axis="0,11,1" y-axis="0,6,1" axis-names="X,Y" crosshairs="no")
+ div.scoring-row
+ button.btn Submit
+ div.prompt-text
---
-One of the most common multisport competitions is a triathlon where athletes swim, bike, and run. The function l(t), Lisa Laws’s race, is given below. Use the given line segments to draw l(t) on the coordinate plane.
+> id: piecewise-data
- // EDITORIAL USE ONLY
- // Fun, quick [video](https://tokyo2020.org/en/sports/triathlon/) of triathlon. Nod to 2020 Olympics, especially if they don't happen. Includes map of courses.
+Great! Now fill out the piecewise function for our original triathlon speed graph:
-`l(t)={(75x, 0<=t<=20),(50000, 20 | ≥ ]] d < 40),
- // TUTOR PROMPTS
- // What do you remember about slope?
- // Positive slope increases from left to right.
- // Negative slope decreases from left to right.
- // What does a linear function with zero slope look like?
+{div.green}(`3/40`, 40 ≤ d [[ ≤ | < | > | ≥ ]] 60):)
-Noticing that each slope is either constant or [[positive | negative]] helps us determine the orientation of each piece of the graph. Constant slope is a [[horizontal | vertical]] line. Positive slope moves [[up | down]] as we read from left to right.
+:::
-Slope can also help us determine the order of the pieces from left to right. For example, Law’s fastest leg of the race was [[cycling | swimming | running]]. The largest slope, [[615.4+-.1]] meters per minute, is the third case in the function. It runs between [[21]] and [[84]] minutes. We now know where on the x-axis to place the steepest piece of the graph.
+ x-coordinate-system.piecewise-step.piecewise-data(width=600 height=200 x-axis="0,65,10" y-axis="0,0.25,0.1" axis-names="Time (min),Speed (km/min)" crosshairs="no")
---
-Recall that a function’s key features give us insights into what’s going on during the race. For example, the starting line is represented by the {.FIXME} (multiple select) [[y-intercept | x-intercept | maximum | minimum]]. We can write this point in function notation as l([[0]])=[[0]]. Place the remaining statements on the graph.
-
- // NOTES
- // Students label with the given cards.
+> id: step-function
-| Place contextual statement cards on graph | | Target key feature appears when card is placed | |Function notation appears when card is placed |
-| :---: | | :---: | | :---: |
-| Law crosses the finish line. | | maximum | | l(123)=51500 |
-| Law is cycling toward the transition point. | | increasing | | 21 < t <= 86 |
-| Law is transitioning from swimming to cycling. | | constant | | 20 < t <= 21 |
-| Law is transitioning from cycling to running. | | constant | | 86 < t <= 87 |
+The function s(d) is a special kind of piecewise function called a [__step function__](gloss:step-function). One major difference between s(d) and d(t) above is all the slopes in s(d) are [[0]].
---
-Now you get to race Law. One of the exciting things about triathlons is that you don’t need to be the fastest at each of the three sports, you just need to cross the finish line first. Here you can adjust your graph, s(t), to see how the race changes. Let’s say your most challenging leg of this race is swimming. As you can see, this segment cannot be adjusted. Can you beat Law with a swim leg that’s [[4]] minutes slower?
+> id: triathlon-graph
+> goals: submitCorrect
-::: column.grow
+The data from our first graph, `d(t)`, is a little unrealistic for the sake of explanation. Let's draw a graph to represent a real-world race. Here's everything you need to know about the function for our new race, `l(t)`:
- // NOTES
- // Slider in __bold__ . Like slider graphs in https://mathigon.org/course/sequences/arithmetic-geometric. Or drag endpoints along the transition lines to change slopes.
- // Adjusts to match given input _italics_.
+- The swimming portion is 2km
+- The biking portion is 40km
+- The running portion is 10km
+- `l(t)` matches the following 3 cases—though we've hidden the rule for each case!
-s(t) = {(60t, 0<=t<=25),(1500, 25 id: triathlon-slopes
-[sundae image](https://depositphotos.com/4537530/stock-photo-ice-cream.html)
+Now that we have the graph, let's calculate the slope of each segment. Recall that [__slope__](gloss:slope) is equal to Rise over Run. We'll start with the swimming segment:
-::: column.grow
+{.text-center}**Rise** is the distance swam: [[2]]km
-Let’s go get some ice cream to celebrate you win! Ice-agon gives two pricing schemes. You can pay by the gram or buy one of three sizes. What do you want to order?
+{.text-center}**Run** is the time swimming: [[25]]min
-:::
+{.text-center}**Slope** is equal to `Rise/Run`: [[0.08]]km/min
- // NOTES
- // Students interact with the graph. Small, medium, and large are in orange. Price per gram is in purple.
+---
-[graph mock-up](https://www.desmos.com/calculator/i0iatpatrn)
+Now do the same for the other two segments:
-As you can see, your choice will depend on a few different factors. Fill out the table below to have a clearer understanding of your options.
+| | **Swimming** | **Biking** | **Running** |
+| **Rise** | 2km | [[40]]km | [[10]]km |
+| **Run** | 25min | [[60]]min | [[40]]min |
+| **Slope** | 0.08km/min | [[0.66±0.01]]km/min | [[0.25]]km/min |
+
+---
-| Weight | | Cost | | Cost |
-| :---: | | :---: | | :---: |
-| 50 | | [[ Not available ]] {.fixme} also accept NA, N/A, na, n/a, none, no | | 0.10 |
-| 75 | | [[2.75]] | | [[1.50+-0.1]] |
-| 125 | | [[2.75]] | | [[2.50+-0.1]] |
-| 150 | | [[Not available]] | | [[3+-0.1]] |
-| 175 | | [[4.25]] | | [[3.50+-0.1]] |
-| 225 | | [[4.25]] | | [[4.50+-0.1]] |
-| 275 | | [[Not available]] | | [[5.50+-0.1]] |
-| 325 | | [[5]] | | [[6.50+-0.1]] |
-| 335 | | [[Not available]] | | [[6.70+-0.1]] |
+> id: triathlon-adjustments
-The most ice cream you can buy with $5 is about [[300+-30]] grams with the large size container. The best buy if you want 200 grams of ice cream is [[pay per gram | medium container]], which costs [[4]].
+Triathlons are exciting because you don't need to be the fastest at every sport; you only need to cross the finish line first. Say you finish the swim segment a whole 15 minutes slower than the graph above. What speed—or slope—do you need during the biking segment to finish at the same time?
+(hint: you'll need to finish the biking segment 15 minutes faster!)
+
+| | **Swimming** | **Biking** | **Running** |
+| **Rise** | 2km | 40km | 10km |
+| **Run** | 40min | [[45]]min | 40min |
+| **Slope** | 0.05km/min | [[0.88±0.01]]km/min | 0.25km/min |
+
+---
+
+That's pretty fast. Say you can only bike at 0.8km/min; now how fast do you need to run to catch up?
+
+| | **Swimming** | **Biking** | **Running** |
+| **Rise** | 2km | 40km | 10km |
+| **Run** | 40min | [[50]]min | [[45]]min |
+| **Slope** | 0.05km/min | 0.8km/min | [[0.22]]km/min |
+
+---
+
+What an exhausting day. Let's go relax with some video games in the next chapter.
--------------------------------------------------------------------------------
@@ -878,6 +1247,9 @@ MathiPong pays homage to this titan in gaming history. The objective is to direc
:::
+ x-pong
+ x-coordinate-system(width=600 height=300 x-axis="-10,10,1" y-axis="0,10,1" crosshairs="no")
+
// NOTES
// onboard the game. User uses arrow keys to move platform. Platform is wider than the point shown.
// User plays several rounds.
diff --git a/content/functions/functions.ts b/content/functions/functions.ts
index 508f75d18..cfe9d294f 100644
--- a/content/functions/functions.ts
+++ b/content/functions/functions.ts
@@ -4,12 +4,815 @@
// =============================================================================
-import {Step} from '@mathigon/studio';
+import {Slider, Step} from '@mathigon/studio';
+import {Point} from '@mathigon/euclid';
+import {$N, animate, ease, ElementView, pointerOver, SVGParentView, svgPointerPosn} from '@mathigon/boost';
+import {last} from '@mathigon/core';
+import {lerp} from '@mathigon/fermat';
+import {shuffle} from '@mathigon/fermat/src/random';
+import {CoordinateSystem, Geopad, GeoPoint} from '../shared/types';
+
+import '../shared/components/burst';
+import {Burst} from '../shared/components/burst';
+
import '../shared/components/relation/relation';
+import {Relation} from '../shared/components/relation/relation';
+
+import './components/card-graph/card-graph';
+import {CardGraph} from './components/card-graph/card-graph';
+
+import './components/draw-graph/draw-graph';
+import {DrawGraph} from './components/draw-graph/draw-graph';
+
+import './components/piecewise-endpoint-puzzle/piecewise-endpoint-puzzle';
+import {PiecewiseEndpointPuzzle} from './components/piecewise-endpoint-puzzle/piecewise-endpoint-puzzle';
+
+import './components/function-machine/function-machine';
+import {FunctionMachine} from './components/function-machine/function-machine';
+
+import {VideoGraph} from './components/video-graph/video-graph';
+import './components/pong/pong';
+import {Pong} from './components/pong/pong';
export function fnSketch($step: Step) {
$step.$('.btn.clear')!.on('click', () => {
($step.$('x-coordinate-sketch') as any).clear();
});
}
+
+export function coordinatePlots($step: Step) {
+ const $geopad = $step.$('x-geopad') as Geopad;
+ $geopad.switchTool('point');
+
+ const targets = [new Point(-5, 3), new Point(-2, -1), new Point(4, -3)];
+
+ $geopad.on('add:point', (e) => {
+ const point = (e.point || e.path) as GeoPoint; // Messy, sorry!
+ const index = targets.findIndex(p => point.value?.equals(p));
+
+ let burstType: string;
+
+ if (index < 0) {
+ $step.addHint('incorrect');
+ point.$el.addClass('red');
+ point.lock();
+ burstType = 'burst incorrect';
+
+ animate((p) => {
+ const q = Math.sqrt(ease('bounce-in', 1 - p));
+ point.$el.css('transform', `scale(${q})`);
+ }, 1000).promise.then(() => {
+ point.delete();
+ });
+ } else {
+ $step.score('p' + index);
+ $step.addHint('correct');
+ point.$el.addClass('green');
+ point.lock();
+ burstType = 'burst correct';
+ }
+
+ const burstElement = $N('g', {class: burstType}, $geopad.$svg);
+ const burst = new Burst(burstElement as SVGParentView, 10);
+ burst.play(1000, [point.$el.center.x, point.$el.center.y], [5, 25]).then(() => {
+ burstElement.remove();
+ });
+ });
+
+ $step.onScore('p0 p1 p2', () => {
+ $geopad.switchTool('move');
+ });
+}
+
+export function verticalLineTest($step: Step) {
+ // Name/House Chart
+ const $labels = $step.$$('x-geopad svg .labels text')!;
+
+ // HACK: To get string axis labels I am simply replacing the contents of the axis label text elements.
+ // TODO: Make names interactively update to reflect mappings assigned in earlier interactive
+
+ const $xLabels = $labels.slice(0, 6);
+ const $yLabels = $labels.slice(6, 11);
+
+ const nameMappings: Record = {
+ '-1': '',
+ 0: '',
+ 1: 'Lynch',
+ 2: 'Switch',
+ 3: 'Derwent',
+ 4: 'Zabini',
+ 5: 'Clearwater'
+ };
+
+ const houseMappings: Record = {
+ 0: '',
+ 1: 'Lionpaw',
+ 2: 'Eaglewing',
+ 3: 'Badgerclaw',
+ 4: 'Serpentfang'
+ };
+
+ $xLabels.forEach(label => {
+ label.text = nameMappings[label.text.toString()];
+ });
+
+ $yLabels.forEach(label => {
+ label.text = houseMappings[label.text.toString()];
+ });
+
+ // Vertical Line Test Interactives
+ const $plots = $step.$$('.verticalLineTest');
+
+ for (const $plot of $plots) {
+ const $geopad = $plot as Geopad;
+ const $svg = $geopad.$svg;
+
+ $svg.css('overflow', 'visible');
+
+ const $paths = $plot.$('svg .paths')!;
+ const $verticalLine = $N('g', {class: 'verticalLine', transform: 'translate(50, 0)'}, $paths);
+ $N('line', {x1: 0, x2: 0, y1: 0, y2: $plot.height}, $verticalLine);
+
+ const $verticalLineLabel = $N('text', {x: 0, y: -2, 'text-anchor': 'middle'}, $verticalLine);
+ $verticalLine.hide();
+
+ // This odd selection is necessary because there are two SVG groups classed as "labels"
+ const $labels = last($plot.$$('.labels'));
+
+ type RelationValue = {
+ coord: Point,
+ $el: ElementView,
+ $label: ElementView,
+ }
+
+ const relationValues: RelationValue[] = Array.from($geopad.points).map(point => {
+ const coord = point.value!;
+ const position = $geopad.toViewportCoords(coord);
+
+ const $label = $N('text', {transform: `translate(${position.x + 10}, ${position.y + 10})`}, $labels);
+ $label.text = `(${Math.round(coord.x * 10) / 10}, ${Math.round(coord.y * 10) / 10})`;
+ $label.hide();
+
+ return {
+ coord,
+ $el: point.$el,
+ $label
+ };
+ });
+
+ pointerOver($svg, {
+ enter: () => $verticalLine.show(),
+ move: (point) => {
+ // Transform viewport-space point to geopad coordinate
+ const pointerCoord = $geopad.toPlotCoords(point);
+
+ // Sort relation values by proximity to pointer X, then grab all points with that X
+ relationValues.sort((a, b) => Math.abs(a.coord.x - pointerCoord.x) - Math.abs(b.coord.x - pointerCoord.x));
+ const closestPoints = relationValues.filter((value) => value.coord.x == relationValues[0].coord.x);
+
+ // Hide all labels
+ for (const value of relationValues) {
+ value.$label.hide();
+ }
+
+ // Are we close enough to snap to the closest point(s)?
+ let snapCoord = pointerCoord;
+ if (Math.abs(closestPoints[0].coord.x - pointerCoord.x) < 0.5) {
+ snapCoord = closestPoints[0].coord;
+
+ // Show labels for snapped points
+ for (const value of closestPoints) {
+ value.$label.show();
+ }
+ }
+
+ // Transform coordinate back to pixel space
+ const snapPoint = $geopad.toViewportCoords(snapCoord);
+
+ // Set position of our line and the text of our x-value label
+ $verticalLine.setAttr('transform', `translate(${snapPoint.x}, 0)`);
+ $verticalLineLabel.text = `x = ${Math.round(snapCoord.x * 10) / 10}`;
+ },
+ exit: () => {
+ $verticalLine.hide();
+
+ // Hide all labels, in case any are showing when pointer exits the SVG
+ for (const value of relationValues) {
+ value.$label.hide();
+ }
+ }
+ });
+ }
+}
+
+export function functionMachines($step: Step) {
+ const hatMachine = $step.$('#hat-machine') as FunctionMachine;
+ hatMachine.bindStep($step);
+}
+
+export function numberFunctions($step: Step) {
+ const $hatMachine = $step.$('#plus-one-machine') as FunctionMachine;
+ $hatMachine.bindStep($step);
+}
+
+export function numericalCoordinateFunctions($step: Step) {
+ const $xSquaredMachine = $step.$('#x-squared-machine')! as FunctionMachine;
+ const $xSquaredPlot = $step.$('#x-squared-plot')! as Geopad;
+
+ $xSquaredPlot.locked = true;
+
+ $xSquaredMachine.bindStep($step);
+ $xSquaredMachine.bindCallback((inputString: string, outputString: string) => {
+ const input = parseInt(inputString);
+ const output = parseInt(outputString);
+
+ const $point = $xSquaredPlot.drawPoint(new Point(input, output));
+
+ const burstElement = $N('g', {class: 'burst'}, $xSquaredPlot.$svg);
+ const burst = new Burst(burstElement as SVGParentView, 10);
+ burst.play(1000, [$point.$el.center.x, $point.$el.center.y], [5, 25]).then(() => {
+ burstElement.remove();
+ });
+ });
+}
+
+function clickPlotter($step: Step, $geopad: Geopad, plotFunction: (x: number) => number) {
+ const $svg = $geopad.$svg;
+ const $axes = $svg.$('.axes')!;
+
+ const requiredCount = parseInt($geopad.attr('count') || '0');
+ let pointCount = 0;
+
+ $geopad.locked = true;
+ const lineTop = $axes.positionTop - $svg.positionTop;
+
+ const $paths = $svg.$('.paths')!;
+ const $verticalLine = $N('g', {class: 'verticalLine', transform: `translate(50, ${lineTop})`}, $paths);
+ $N('line', {x1: 0, x2: 0, y1: 0, y2: $axes.height}, $verticalLine);
+
+ const $verticalLineLabel = $N('text', {x: 0, y: -8, 'text-anchor': 'middle'}, $verticalLine);
+ $verticalLine.hide();
+
+ $geopad.on('click', event => {
+ const clickPoint = $geopad.toPlotCoords(svgPointerPosn(event, $svg));
+ const point = new Point(clickPoint.x, plotFunction(clickPoint.x));
+ const $point = $geopad.drawPoint(point);
+
+ const burstElement = $N('g', {class: 'burst'}, $geopad.$svg);
+ const burst = new Burst(burstElement as SVGParentView, 10);
+ burst.play(1000, [$point.$el.center.x, $point.$el.center.y], [5, 25]).then(() => {
+ burstElement.remove();
+ });
+
+ pointCount++;
+ if (requiredCount > 0) {
+
+ if (pointCount == requiredCount) {
+ $step.score('plotPoints');
+ } else if (pointCount < requiredCount) {
+ $step.addHint('plot-points-more');
+ }
+ }
+ });
+
+ pointerOver($svg, {
+ enter: () => $verticalLine.show(),
+ move: (point) => {
+ const pointerCoord = $geopad.toPlotCoords(point);
+
+ $verticalLine.setAttr('transform', `translate(${point.x}, ${lineTop})`);
+ $verticalLineLabel.text = `x = ${Math.round(pointerCoord.x * 10) / 10}`;
+ },
+ exit: () => $verticalLine.hide()
+ });
+}
+
+export function numericalPlot($step: Step) {
+ const $geopad = $step.$('x-geopad')! as Geopad;
+ clickPlotter($step, $geopad, (x: number) => x * x);
+}
+
+export function findDomainRange1($step: Step) {
+ const $geopad = $step.$('x-geopad')! as Geopad;
+ clickPlotter($step, $geopad, (x: number) => -x * x + 3);
+}
+
+// Graphing and Interpreting Functions
+
+// Ri Se-Gwang's vault functions
+function vaultHeightDistance(x: number) {
+ return (1 / (1 + Math.pow((8 * (x - 25.13)), 2))) + (8 - Math.pow(((x - 27) * 1.5), 2)) / (1 + Math.pow(((x - 27) / 1.88), 128));
+}
+
+function vaultDistanceTime(t: number) {
+ // Pause before running begins
+ const pause = 1.6;
+ // Total duration of clip
+ const duration = 9.1;
+
+ if (t < pause) return 0;
+
+ return (1 - Math.pow((1 + (Math.cos((t - pause) * Math.PI / (duration - pause)))) / 2, 2)) * 29.5;
+}
+
+export function vaultGraph($step: Step) {
+ const $videoGraph = $step.$('x-video-graph')! as VideoGraph;
+
+ $videoGraph.addPlot(vaultDistanceTime, vaultHeightDistance, '/content/functions/images/ri_face.png');
+}
+
+export function graphMatch($step: Step) {
+ ($step.$('x-relation')! as Relation).bindStep($step);
+}
+
+export function timeHeightGraph($step: Step) {
+ const $videoGraph = $step.$('x-video-graph')! as VideoGraph;
+
+ $videoGraph.addPlot((t: number) => t, (t: number) => vaultHeightDistance(vaultDistanceTime(t)), '/content/functions/images/ri_face.png');
+}
+
+export function timeDistanceGraph($step: Step) {
+ const $videoGraph = $step.$('x-video-graph')! as VideoGraph;
+
+ $videoGraph.addPlot((t: number) => t, (t: number) => vaultDistanceTime(t), '/content/functions/images/ri_face.png');
+}
+
+export function swimGraph($step: Step) {
+ const $videoGraph = $step.$('x-video-graph')! as VideoGraph;
+
+ $videoGraph.addPlot((t: number) => t > 21.47 ? 21.47 : t, (t: number) => t > 21.47 ? 50 : 50 / 21.47 * t, '/content/functions/images/cielo_face.png');
+}
+
+export function measureSlope1($step: Step) {
+ const lines = $step.$$('x-coordinate-system svg .axes line');
+ lines.pop()!.setAttr('id', 'swim-y-axis');
+ lines.pop()!.setAttr('id', 'swim-x-axis');
+}
+
+function evaluateSwimGraph(t: number) {
+ return t * 50 / 21.47;
+}
+
+function drawSlopeMeasurements($graph: CoordinateSystem, x0: number, x1: number, showRiseRun = true) {
+ const $svg = $graph.$svg as SVGParentView;
+ const $overlay = $svg.$('.overlay')!;
+ const $labels = $svg.$('.labels')!;
+
+ $svg.css('overflow', 'visible');
+
+ const point0 = new Point(x0, evaluateSwimGraph(x0));
+ const point1 = new Point(x1, evaluateSwimGraph(x1));
+
+ const point2 = new Point(point1.x, point0.y);
+
+ const position0 = $graph.toViewportCoords(point0);
+ const position1 = $graph.toViewportCoords(point1);
+ const position2 = $graph.toViewportCoords(point2);
+
+ const _$circle0 = $N('circle', {class: 'slope-measurement', id: 'circle-0', cx: position0.x, cy: position0.y, r: 4}, $overlay);
+ const _$circle1 = $N('circle', {class: 'slope-measurement', id: 'circle-1', cx: position1.x, cy: position1.y, r: 4}, $overlay);
+ const _$circle2 = $N('circle', {class: 'slope-measurement', id: 'circle-2', cx: position2.x, cy: position2.y, r: 4}, $overlay);
+
+ const _$runLine = $N('line', {class: 'slope-measurement', id: 'run-line', x1: position0.x, x2: position2.x, y1: position0.y, y2: position2.y}, $overlay);
+ const _$riseLine = $N('line', {class: 'slope-measurement', id: 'rise-line', x1: position1.x, x2: position2.x, y1: position1.y, y2: position2.y}, $overlay);
+
+ if (showRiseRun) {
+ const runText = $N('text', {class: 'slope-measurement', x: (position0.x + position2.x) / 2, y: (position0.y + position2.y) / 2 - 8, 'text-anchor': 'middle', 'alignment-baseline': 'bottom'}, $labels);
+ runText.text = 'Run: ' + Math.round((point1.x - point0.x) * 10) / 10;
+
+ const riseText = $N('text', {class: 'slope-measurement', x: (position1.x + position2.x) / 2 + 8, y: (position1.y + position2.y) / 2, 'text-anchor': 'start', 'alignment-baseline': 'middle'}, $labels);
+ riseText.text = 'Rise: ' + Math.round((point1.y - point0.y) * 10) / 10;
+ }
+}
+
+export function measureSlope2($step: Step) {
+ const $graph = $step.$('x-coordinate-system')! as CoordinateSystem;
+
+ drawSlopeMeasurements($graph, 0, 21.47);
+}
+
+export function measureSlope3($step: Step) {
+ const $graph = $step.$('x-coordinate-system')! as CoordinateSystem;
+
+ drawSlopeMeasurements($graph, 0, 1, false);
+}
+
+export function swimSystem($step: Step) {
+ const $graph = $step.$('x-coordinate-system')! as CoordinateSystem;
+
+ const origin = $graph.toViewportCoords(new Point(0, 0));
+ const timeLine = $N('line', {id: 'time-line', x1: origin.x, x2: origin.x, y1: origin.y - 220, y2: origin.y}, $graph.$svg.$('.overlay'));
+
+ const swimmers = [{
+ name: 'cielo',
+ speed: 2.33,
+ $distance: $step.$('#cielo-distance .math mn')
+ }, {
+ name: 'leveaux',
+ speed: 2.19,
+ $distance: $step.$('#leveaux-distance .math mn')
+ }, {
+ name: 'bernard',
+ speed: 2.15,
+ $distance: $step.$('#bernard-distance .math mn')
+ }, {
+ name: 'callus',
+ speed: 2.06,
+ $distance: $step.$('#callus-distance .math mn')
+ }
+ ];
+
+ $graph.setFunctions(...swimmers.map((swimmer) => {
+ return (t: number) => swimmer.speed * t;
+ }));
+
+ const $slider = $step.$('x-slider')! as Slider;
+ const $timeText = $step.$('#time-variable-text .sentence .math mn')!;
+
+ $slider.on('move', (n: number) => {
+ const s = 25 * n / 500;
+ $timeText.text = (Math.round(s * 100) / 100).toString();
+
+ const p = $graph.toViewportCoords(new Point(s, 0));
+ timeLine.setAttr('x1', p.x);
+ timeLine.setAttr('x2', p.x);
+
+ for (const swimmer of swimmers) {
+ const d = Math.round(s * swimmer.speed * 10) / 10;
+ swimmer.$distance!.text = Math.min(50, d).toString();
+ }
+ });
+}
+
+function renDive(t: number) {
+ return 1.4 * Math.pow(t, 3) - 7.6 * Math.pow(t, 2) + 4.5 * t + 10;
+}
+
+export function diveGraph($step: Step) {
+ const $videoGraph = $step.$('x-video-graph')! as VideoGraph;
+
+ $videoGraph.addPlot((t: number) => t, renDive, '/content/functions/images/ren_face.png');
+
+ {
+ const cards = [{
+ description: 'Ren stands on the platform',
+ imagePath: '/content/functions/images/dive_card_1.png',
+ point: new Point(0, 10),
+ hint: 'This is the y-intercept.'
+ }, {
+ description: 'Ren reaches her peak',
+ imagePath: '/content/functions/images/dive_card_2.png',
+ point: new Point(0.34, 10.7),
+ hint: 'This is the maximal turning point.'
+ }, {
+ description: 'Ren hits the water',
+ imagePath: '/content/functions/images/dive_card_4.png',
+ point: new Point(1.98, 0),
+ hint: 'This is the first x-intercept.'
+ }, {
+ description: 'Ren turns underwater',
+ imagePath: '/content/functions/images/dive_card_5.png',
+ point: new Point(3.3, -7.6),
+ hint: 'This is the minimal turning point.'
+ }, {
+ description: 'Ren reaches the surface',
+ imagePath: '/content/functions/images/dive_card_6.png',
+ point: new Point(4.3, 0),
+ hint: 'This is the second x-intercept.'
+ }];
+
+ const $cardGraph = $step.$('x-card-graph') as CardGraph;
+ $cardGraph.bindStep($step);
+ $cardGraph.setPlots([{function: renDive, color: 'red'}]);
+ $cardGraph.setCards(cards);
+ }
+}
+
+export function diveIntervals($step: Step) {
+ const cards = [{
+ description: 'f(t) is Increasing',
+ label: '0 0.8 - 0.8 / (1 + Math.pow(Math.E, (x - 38) * 1)) + 4.05 / (1 + Math.pow((x - 40), 8)));
+ $drawGraph.setHintPoints([{
+ x: 0.01,
+ hint: 'Ekaterina starts at y=0m',
+ drawCircle: true
+ }, {
+ x: 39.5,
+ hint: 'The 4.85m crossbar ends the 40m runway',
+ relevanceThresholdDistance: 5,
+ id: 'hint-bar'
+ }, {
+ x: 38.2,
+ hint: 'Ekaterina shoots upward at about 38m',
+ drawCircle: true
+ }, {
+ x: 40.5,
+ hint: 'Ekaterina arches over the 4.85m crossbar',
+ drawCircle: true
+ }, {
+ x: 35,
+ hint: 'The pole strikes just past 35m',
+ drawLine: true
+ }, {
+ x: 41.5,
+ hint: 'The landing pad is 0.8m high',
+ id: 'hint-landing',
+ drawLine: true
+ }]);
+}
+
+export function runningGraph($step: Step) {
+ const $videoGraph = $step.$('x-video-graph')! as VideoGraph;
+
+ $videoGraph.addPlot((t: number) => Math.min(t + 88, 121), (t: number) => 6.58545 * t + 3.16101, '/content/functions/images/tracey_face.png');
+ $videoGraph.addPlot((t: number) => Math.min(t + 88, 126), (t: number) => 5.71105 * t + 80.4071, '/content/functions/images/boufaarirane_face.png', 'green');
+ $videoGraph.addPlot((t: number) => Math.min(t + 88, 120.2), (t: number) => 7.10179 * t - 53.6354, '/content/functions/images/rogers_face.png', 'blue');
+}
+
+export function runningCards($step: Step) {
+ const cards = [{
+ description: 'Boufaarirane is ahead of Rogers.',
+ label: 'f(t) > u(t)',
+ point: new Point(93, 650),
+ domain: [90.5, 96.4]
+ }, {
+ description: 'Rogers is ahead of Boufaarirane.',
+ label: 'f(t) < u(t)',
+ point: new Point(110, 750),
+ domain: [96.5, 126]
+ }, {
+ description: 'Tracey is ahead of Rogers.',
+ label: 'u(t) < g(t)',
+ point: new Point(100, 700),
+ domain: [90.5, 110]
+ }, {
+ description: 'Tracey is ahead of Boufaarirane.',
+ label: 'g(t) > f(t)',
+ point: new Point(88, 675),
+ domain: [90.5, 126]
+ }, {
+ description: 'Rogers is ahead of Tracey.',
+ label: 'g(t) < u(t)',
+ point: new Point(115, 790),
+ domain: [110, 121]
+ }];
+
+ const $cardGraph = $step.$('x-card-graph') as CardGraph;
+ $cardGraph.bindStep($step);
+ $cardGraph.setPlots([
+ {function: (t: number) => 6.58545 * t + 3.16101, color: 'red'},
+ {function: (t: number) => 5.71105 * t + 80.4071, color: 'green'},
+ {function: (t: number) => 7.10179 * t - 53.6354, color: 'blue'}
+ ]);
+ $cardGraph.setCards(cards);
+}
+
+export function piecewiseIntro($step: Step) {
+ ($step.$('#graph1')! as CoordinateSystem).drawLinePlot([new Point(0, 0), new Point(20, 0.5), new Point(40, 3.5), new Point(60, 5)]);
+ ($step.$('#graph2')! as CoordinateSystem).drawLinePlot([new Point(0, 0), new Point(5, 2), new Point(40, 3), new Point(60, 5)]);
+ ($step.$('#graph3')! as CoordinateSystem).drawLinePlot([new Point(0, 0), new Point(10, 2), new Point(35, 4), new Point(60, 5)]);
+ ($step.$('#graph4')! as CoordinateSystem).drawLinePlot([new Point(0, 0), new Point(30, 0.5), new Point(50, 2), new Point(60, 5)]);
+
+ const picker = $step.$('x-picker')!;
+ const children = shuffle(picker.children);
+ picker.removeChildren();
+ while (children.length) {
+ picker.append(children.pop()!);
+ }
+}
+
+export function piecewiseDefinition($step: Step) {
+ const $graph = $step.$('x-coordinate-system')! as CoordinateSystem;
+ const $plot = $step.$('.plot')!;
+
+ const a = $graph.toViewportCoords(new Point(0, 0));
+ const b = $graph.toViewportCoords(new Point(20, 0.5));
+ const c = $graph.toViewportCoords(new Point(40, 3.5));
+ const d = $graph.toViewportCoords(new Point(60, 5));
+
+ const _$line1 = $N('line', {id: 'line1', target: 'line1', x1: a.x, x2: b.x, y1: a.y, y2: b.y}, $plot);
+ const _$line2 = $N('line', {id: 'line2', target: 'line2', x1: b.x, x2: c.x, y1: b.y, y2: c.y}, $plot);
+ const _$line3 = $N('line', {id: 'line3', target: 'line3', x1: c.x, x2: d.x, y1: c.y, y2: d.y}, $plot);
+}
+
+export function piecewiseEndpoints($step: Step) {
+ const $graph = $step.$('x-coordinate-system') as CoordinateSystem;
+ const $plot = $N('g', {class: 'plot'}, $graph.$overlay);
+
+ {
+ const a = $graph.toViewportCoords(new Point(0, 1 / 40));
+ const b = $graph.toViewportCoords(new Point(20, 1 / 40));
+
+ const $g = $N('g', {class: 'red', id: 'swim'}, $plot);
+ $N('text', {x: (a.x + b.x) / 2, y: a.y - 10}, $g).text = 'Swim';
+ $N('line', {x1: a.x + 4, x2: b.x - 4, y1: a.y, y2: b.y}, $g);
+ $N('circle', {class: 'closed', target: 'closed', cx: a.x, cy: a.y, r: 4}, $g);
+ $N('circle', {class: 'open', target: 'open', cx: b.x, cy: b.y, r: 4}, $g);
+ }
+ {
+ const a = $graph.toViewportCoords(new Point(20, 6 / 40));
+ const b = $graph.toViewportCoords(new Point(40, 6 / 40));
+
+ const $g = $N('g', {class: 'blue', id: 'bike'}, $plot);
+ $N('text', {x: (a.x + b.x) / 2, y: a.y - 10}, $g).text = 'Bike';
+ $N('line', {x1: a.x + 4, x2: b.x - 4, y1: a.y, y2: b.y}, $g);
+ $N('circle', {class: 'closed', target: 'closed', cx: a.x, cy: a.y, r: 4}, $g);
+ $N('circle', {class: 'open', target: 'open', cx: b.x, cy: b.y, r: 4}, $g);
+ }
+ {
+ const a = $graph.toViewportCoords(new Point(40, 3 / 40));
+ const b = $graph.toViewportCoords(new Point(60, 3 / 40));
+
+ const $g = $N('g', {class: 'green', id: 'run'}, $plot);
+ $N('text', {x: (a.x + b.x) / 2, y: a.y - 10}, $g).text = 'Run';
+ $N('line', {x1: a.x + 4, x2: b.x - 4, y1: a.y, y2: b.y}, $g);
+ $N('circle', {class: 'closed', target: 'closed', cx: a.x, cy: a.y, r: 4}, $g);
+ $N('circle', {class: 'closed', target: 'closed', cx: b.x, cy: b.y, r: 4}, $g);
+ }
+
+ return;
+ // Disabling VLT for now
+
+ const $verticalLine = $N('line', {class: 'vertical-line', x1: 0, x2: 0, y1: $graph.viewportBounds.yMin, y2: $graph.viewportBounds.yMax}, $graph.$overlay);
+ $verticalLine.hide();
+
+ pointerOver($graph.$svg, {
+ enter: () => $verticalLine.show(),
+ move: (p) => {
+ $verticalLine.setAttr('x1', p.x);
+ $verticalLine.setAttr('x2', p.x);
+ },
+ exit: () => $verticalLine.hide()
+ });
+}
+
+export function endpoints1($step: Step) {
+ const $puzzle = $step.$('x-piecewise-endpoint-puzzle')! as PiecewiseEndpointPuzzle;
+
+ $puzzle.bindStep($step);
+
+ $puzzle.setSegments([{
+ point0: new Point(1, 3),
+ point1: new Point(10, 3),
+ include0: true,
+ include1: false
+ }]);
+}
+
+export function endpoints2($step: Step) {
+ const $puzzle = $step.$('x-piecewise-endpoint-puzzle')! as PiecewiseEndpointPuzzle;
+
+ $puzzle.bindStep($step);
+
+ $puzzle.setSegments([{
+ point0: new Point(1, 4),
+ point1: new Point(6, 4),
+ include0: false,
+ include1: true
+ }, {
+ point0: new Point(6, 2),
+ point1: new Point(10, 2),
+ include0: false,
+ include1: true
+ }]);
+}
+
+export function endpoints3($step: Step) {
+ const $puzzle = $step.$('x-piecewise-endpoint-puzzle')! as PiecewiseEndpointPuzzle;
+
+ $puzzle.bindStep($step);
+
+ $puzzle.setSegments([{
+ point0: new Point(1, 2),
+ point1: new Point(3, 2),
+ include0: false,
+ include1: true
+ }, {
+ point0: new Point(3, 5),
+ point1: new Point(7, 5),
+ include0: false,
+ include1: false
+ }, {
+ point0: new Point(7, 3),
+ point1: new Point(10, 3),
+ include0: false,
+ include1: true
+ }]);
+}
+
+export function endpoints4($step: Step) {
+ const $puzzle = $step.$('x-piecewise-endpoint-puzzle')! as PiecewiseEndpointPuzzle;
+
+ $puzzle.bindStep($step);
+
+ $puzzle.setSegments([{
+ point0: new Point(1, 3),
+ point1: new Point(3, 3)
+ }, {
+ point0: new Point(3, 1),
+ point1: new Point(5, 1)
+ }, {
+ point0: new Point(5, 5),
+ point1: new Point(8, 5)
+ }, {
+ point0: new Point(8, 4),
+ point1: new Point(10, 4)
+ }]);
+}
+
+export function piecewiseData($step: Step) {
+ piecewiseEndpoints($step);
+}
+
+export function triathlonGraph($step: Step) {
+ const $drawGraph = $step.$('x-draw-graph')! as DrawGraph;
+ const $graph = $drawGraph.$('x-coordinate-system')! as CoordinateSystem;
+
+ const bikeStart = $graph.toViewportCoords(new Point(0, 2));
+ const runStart = $graph.toViewportCoords(new Point(125, 42));
+ const raceEnd = $graph.toViewportCoords(new Point(125, 52));
+
+ const $lines = $N('g', {});
+ $graph.$('.grid')!.insertAfter($lines);
+
+ $N('line', {class: 'transition-line', x1: bikeStart.x, x2: runStart.x, y1: bikeStart.y, y2: bikeStart.y}, $lines);
+ $N('line', {class: 'transition-line', x1: bikeStart.x, x2: runStart.x, y1: runStart.y, y2: runStart.y}, $lines);
+ $N('line', {class: 'transition-line', x1: bikeStart.x, x2: runStart.x, y1: raceEnd.y, y2: raceEnd.y}, $lines);
+
+ $drawGraph.bindStep($step);
+
+ $drawGraph.setSolutionFunction((x: number) => {
+ if (x < 25) {
+ return 2 / 25 * x;
+ }
+ if (x < 85) {
+ const p = (x - 25) / (85 - 25);
+ return lerp(2, 42, p);
+ }
+ if (x < 125) {
+ const p = (x - 85) / (125 - 85);
+ return lerp(42, 52, p);
+ }
+ return 52;
+ });
+
+ $drawGraph.setHintPoints([{
+ x: 0.01,
+ hint: 'The race begins at (0, 0)',
+ relevanceThresholdDistance: 0,
+ drawCircle: true
+ }, {
+ x: 25,
+ hint: 'The transition to biking happens at t=25',
+ relevanceThresholdDistance: 0,
+ drawLine: true
+ }, {
+ x: 85,
+ hint: 'The transition to running happens at t=85',
+ drawLine: true
+ }, {
+ x: 125,
+ hint: 'The race ends at t=125',
+ drawLine: true
+ }]);
+}
+
+export function absoluteValue($step: Step) {
+ const $pong = $step.$('x-pong') as Pong;
+ $pong.bindStep($step);
+}
diff --git a/content/functions/hints.yaml b/content/functions/hints.yaml
new file mode 100644
index 000000000..c59812dd6
--- /dev/null
+++ b/content/functions/hints.yaml
@@ -0,0 +1,6 @@
+invalid-domain-emoji: You can't square an emoji!
+invalid-range-emoji: x^2 never equals an emoji!
+invalid-domain-negative: sqrt(x) is never negative!
+invalid-range-negative: x^2 is never negative!
+
+plot-points-more: Perfect! Add a few more
\ No newline at end of file
diff --git a/content/functions/images/boufaarirane_face.png b/content/functions/images/boufaarirane_face.png
new file mode 100644
index 000000000..52362ad37
Binary files /dev/null and b/content/functions/images/boufaarirane_face.png differ
diff --git a/content/functions/images/cielo_face.png b/content/functions/images/cielo_face.png
new file mode 100644
index 000000000..5caec0df0
Binary files /dev/null and b/content/functions/images/cielo_face.png differ
diff --git a/content/functions/images/cielo_portrait.jpg b/content/functions/images/cielo_portrait.jpg
new file mode 100644
index 000000000..aa0326a7c
Binary files /dev/null and b/content/functions/images/cielo_portrait.jpg differ
diff --git a/content/functions/images/dive_card_1.png b/content/functions/images/dive_card_1.png
new file mode 100644
index 000000000..80d398460
Binary files /dev/null and b/content/functions/images/dive_card_1.png differ
diff --git a/content/functions/images/dive_card_2.png b/content/functions/images/dive_card_2.png
new file mode 100644
index 000000000..3b851d2ff
Binary files /dev/null and b/content/functions/images/dive_card_2.png differ
diff --git a/content/functions/images/dive_card_3.png b/content/functions/images/dive_card_3.png
new file mode 100644
index 000000000..408fc5447
Binary files /dev/null and b/content/functions/images/dive_card_3.png differ
diff --git a/content/functions/images/dive_card_4.png b/content/functions/images/dive_card_4.png
new file mode 100644
index 000000000..2ef68c7b9
Binary files /dev/null and b/content/functions/images/dive_card_4.png differ
diff --git a/content/functions/images/dive_card_5.png b/content/functions/images/dive_card_5.png
new file mode 100644
index 000000000..53d7e8ad1
Binary files /dev/null and b/content/functions/images/dive_card_5.png differ
diff --git a/content/functions/images/dive_card_6.png b/content/functions/images/dive_card_6.png
new file mode 100644
index 000000000..b730d9471
Binary files /dev/null and b/content/functions/images/dive_card_6.png differ
diff --git a/content/functions/images/hat-fox.png b/content/functions/images/hat-fox.png
new file mode 100644
index 000000000..ad41b50d0
Binary files /dev/null and b/content/functions/images/hat-fox.png differ
diff --git a/content/functions/images/hat-monkey.png b/content/functions/images/hat-monkey.png
new file mode 100644
index 000000000..5ad5c0d09
Binary files /dev/null and b/content/functions/images/hat-monkey.png differ
diff --git a/content/functions/images/olympic_dive.mp4 b/content/functions/images/olympic_dive.mp4
new file mode 100644
index 000000000..ac53a456f
Binary files /dev/null and b/content/functions/images/olympic_dive.mp4 differ
diff --git a/content/functions/images/olympic_dive_poster.png b/content/functions/images/olympic_dive_poster.png
new file mode 100644
index 000000000..2dc73d922
Binary files /dev/null and b/content/functions/images/olympic_dive_poster.png differ
diff --git a/content/functions/images/olympic_hurdles.mp4 b/content/functions/images/olympic_hurdles.mp4
new file mode 100644
index 000000000..508ed7208
Binary files /dev/null and b/content/functions/images/olympic_hurdles.mp4 differ
diff --git a/content/functions/images/olympic_hurdles_poster.png b/content/functions/images/olympic_hurdles_poster.png
new file mode 100644
index 000000000..940c264ee
Binary files /dev/null and b/content/functions/images/olympic_hurdles_poster.png differ
diff --git a/content/functions/images/olympic_pole_vault.mp4 b/content/functions/images/olympic_pole_vault.mp4
new file mode 100644
index 000000000..90af01c57
Binary files /dev/null and b/content/functions/images/olympic_pole_vault.mp4 differ
diff --git a/content/functions/images/olympic_pole_vault_poster.png b/content/functions/images/olympic_pole_vault_poster.png
new file mode 100644
index 000000000..f99315cf9
Binary files /dev/null and b/content/functions/images/olympic_pole_vault_poster.png differ
diff --git a/content/functions/images/olympic_running.mp4 b/content/functions/images/olympic_running.mp4
new file mode 100644
index 000000000..34cb8bc90
Binary files /dev/null and b/content/functions/images/olympic_running.mp4 differ
diff --git a/content/functions/images/olympic_running_poster.png b/content/functions/images/olympic_running_poster.png
new file mode 100644
index 000000000..d39b82787
Binary files /dev/null and b/content/functions/images/olympic_running_poster.png differ
diff --git a/content/functions/images/olympic_ski.mp4 b/content/functions/images/olympic_ski.mp4
new file mode 100644
index 000000000..f055eff68
Binary files /dev/null and b/content/functions/images/olympic_ski.mp4 differ
diff --git a/content/functions/images/olympic_ski_poster.png b/content/functions/images/olympic_ski_poster.png
new file mode 100644
index 000000000..b4cbd179a
Binary files /dev/null and b/content/functions/images/olympic_ski_poster.png differ
diff --git a/content/functions/images/olympic_swim.mp4 b/content/functions/images/olympic_swim.mp4
new file mode 100644
index 000000000..530f4ebde
Binary files /dev/null and b/content/functions/images/olympic_swim.mp4 differ
diff --git a/content/functions/images/olympic_swim_poster.png b/content/functions/images/olympic_swim_poster.png
new file mode 100644
index 000000000..0f581f1e5
Binary files /dev/null and b/content/functions/images/olympic_swim_poster.png differ
diff --git a/content/functions/images/olympic_triple_jump.mp4 b/content/functions/images/olympic_triple_jump.mp4
new file mode 100644
index 000000000..21ba9f0cd
Binary files /dev/null and b/content/functions/images/olympic_triple_jump.mp4 differ
diff --git a/content/functions/images/olympic_triple_jump_poster.png b/content/functions/images/olympic_triple_jump_poster.png
new file mode 100644
index 000000000..c0fc9fbf6
Binary files /dev/null and b/content/functions/images/olympic_triple_jump_poster.png differ
diff --git a/content/functions/images/olympic_vault.mp4 b/content/functions/images/olympic_vault.mp4
new file mode 100644
index 000000000..ba2b97859
Binary files /dev/null and b/content/functions/images/olympic_vault.mp4 differ
diff --git a/content/functions/images/olympic_vault_poster.png b/content/functions/images/olympic_vault_poster.png
new file mode 100644
index 000000000..03db55683
Binary files /dev/null and b/content/functions/images/olympic_vault_poster.png differ
diff --git a/content/functions/images/ren_face.png b/content/functions/images/ren_face.png
new file mode 100644
index 000000000..e35755938
Binary files /dev/null and b/content/functions/images/ren_face.png differ
diff --git a/content/functions/images/ren_portrait.jpg b/content/functions/images/ren_portrait.jpg
new file mode 100644
index 000000000..e1fdbacce
Binary files /dev/null and b/content/functions/images/ren_portrait.jpg differ
diff --git a/content/functions/images/ri_face.png b/content/functions/images/ri_face.png
new file mode 100644
index 000000000..47c07fa0a
Binary files /dev/null and b/content/functions/images/ri_face.png differ
diff --git a/content/functions/images/ri_portrait.jpg b/content/functions/images/ri_portrait.jpg
new file mode 100644
index 000000000..97374d504
Binary files /dev/null and b/content/functions/images/ri_portrait.jpg differ
diff --git a/content/functions/images/rogers_face.png b/content/functions/images/rogers_face.png
new file mode 100644
index 000000000..0469fab5e
Binary files /dev/null and b/content/functions/images/rogers_face.png differ
diff --git a/content/functions/images/swim_bike_run.png b/content/functions/images/swim_bike_run.png
new file mode 100644
index 000000000..cc4fcd614
Binary files /dev/null and b/content/functions/images/swim_bike_run.png differ
diff --git a/content/functions/images/tracey_face.png b/content/functions/images/tracey_face.png
new file mode 100644
index 000000000..da7c67ab5
Binary files /dev/null and b/content/functions/images/tracey_face.png differ
diff --git a/content/functions/styles.scss b/content/functions/styles.scss
index 1bab7c644..63e829f4b 100755
--- a/content/functions/styles.scss
+++ b/content/functions/styles.scss
@@ -6,10 +6,322 @@
@import "../../node_modules/@mathigon/studio/frontend/styles/variables";
@import "../shared/components/relation/relation";
+@import "./components/function-machine/function-machine";
+@import "./components/video-graph/video-graph";
+@import "./components/card-graph/card-graph";
+@import "./components/draw-graph/draw-graph";
+@import "./components/pong/pong";
+@import "./components/piecewise-endpoint-puzzle/piecewise-endpoint-puzzle";
+x-coordinate-system {
+ line,path.red { stroke: $red; }
+ line,path.green { stroke: $green; }
+ line,path.blue { stroke: $blue; }
+ line,path.yellow { stroke: $yellow; }
+ line,path.purple { stroke: $purple; }
+ line,path.orange { stroke: $orange; }
+}
.btn.clear {
position: absolute;
bottom: 6px;
right: 6px;
}
+
+.burst { fill: none; stroke: $dark-grey; }
+.burst.correct { stroke: $green; }
+.burst.incorrect { stroke: $red; }
+.burst circle { stroke: none; }
+
+.verticalLine line {
+ stroke: $blue;
+ stroke-width: 4px;
+}
+
+.verticalLine text {
+ color: $blue;
+}
+
+.vault-graph .overlay circle {
+ stroke-width: 3px;
+ stroke: $green;
+ fill: none;
+}
+
+#graph-match-relation x-coordinate-system {
+ margin: 20px 0;
+}
+
+line.slope-measurement {
+ stroke: $blue;
+ stroke-width: 3;
+ stroke-dasharray: 2 3;
+}
+
+text.slope-measurement {
+ font-size: 18px;
+ text-anchor: center;
+}
+
+circle.slope-measurement {
+ stroke: none;
+ fill: $green;
+}
+
+#multi-swimmer-graph svg .plot {
+ g:nth-child(1) {
+ path {
+ stroke: $red;
+ }
+ }
+ g:nth-child(2) {
+ path {
+ stroke: $blue;
+ }
+ }
+ g:nth-child(3) {
+ path {
+ stroke: $green;
+ }
+ }
+ g:nth-child(4) {
+ path {
+ stroke: $purple;
+ }
+ }
+}
+
+#time-variable-text {
+ font-size: 24px;
+}
+
+#pole-vault-video {
+ margin-bottom: 2em;
+}
+
+x-draw-graph {
+ #hint-bar {
+ stroke: mix($dark-grey, $medium-grey, 50%);
+ stroke-width: 6px;
+ }
+
+ #hint-landing {
+ fill: mix($blue, $dark-grey, 50%);
+ }
+}
+
+line#time-line {
+ stroke: $medium-grey;
+ stroke-width: 2px;
+ stroke-dasharray: 4;
+}
+
+#running-video-graph>div {
+ display: flex;
+ justify-content: space-evenly;
+ margin: 20px 0 40px 0;
+}
+
+#running-video-graph x-coordinate-system {
+ margin: 0;
+}
+
+#running-video-graph x-coordinate-system svg {
+ overflow: visible;
+}
+
+.runner-name-key {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ margin-right: 20px;
+
+ >:not(:last-child) {
+ margin-bottom: 20px;
+ }
+
+ >div {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ :not(:last-child) {
+ margin-right: 10px;
+ }
+ }
+}
+
+.runner-name-key img {
+ width: 48px;
+ height: 48px;
+}
+
+#rogers-slope-graph {
+ .plot path {
+ stroke: $blue;
+ }
+ .crosshair {
+ circle {
+ stroke: $blue;
+ }
+ text {
+ fill: $blue;
+ }
+ }
+}
+
+#boufaarirane-slope-graph {
+ .plot path {
+ stroke: $green;
+ }
+ .crosshair {
+ circle {
+ stroke: $green;
+ }
+ text {
+ fill: $green;
+ }
+ }
+}
+
+#runner-card-graph {
+ >div {
+ margin-top: 2em;
+
+ display: flex;
+ justify-content: center;
+
+ >div {
+ display: flex;
+
+ >:last-child {
+ margin-left: 10px;
+ }
+ }
+
+ >div:not(:last-child) {
+ margin-right: 40px;
+ }
+ }
+
+ x-coordinate-system {
+ margin: 5px auto 2em auto;
+ }
+}
+
+.piecewise-function-left {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+x-picker.graphPicker {
+ .item {
+ width: auto;
+ }
+}
+
+#piecewise-description-column {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ min-width: 285px;
+}
+
+#piecewise-graph-column {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 2em 0;
+}
+
+x-coordinate-system.piecewise-cases {
+ .plot {
+ line {
+ stroke-width: 3px;
+ }
+ #line1 {
+ stroke: $red;
+ }
+ #line2 {
+ stroke: $blue;
+ }
+ #line3 {
+ stroke: $green;
+ }
+ }
+}
+
+x-coordinate-system.piecewise-step {
+ svg {
+ overflow: visible;
+ }
+
+ .overlay>line.vertical-line {
+ stroke-width: 2px;
+ stroke: black;
+ }
+
+ .overlay>.plot {
+ stroke-width: 2px;
+
+ text {
+ stroke-width: 0;
+ font-size: 16px;
+ text-anchor: middle;
+ }
+
+ .red {
+ stroke: $red;
+ fill: $red;
+ }
+ .orange {
+ stroke: $orange;
+ fill: $orange;
+ }
+ .blue {
+ stroke: $blue;
+ fill: $blue;
+ }
+ .green {
+ stroke: $green;
+ fill: $green;
+ }
+
+ circle.open {
+ fill: none;
+ }
+ }
+}
+
+x-step#piecewise-data {
+ .row.padded {
+ margin-bottom: 0;
+ }
+
+ x-coordinate-system {
+ margin-top: 0;
+ }
+}
+
+x-draw-graph#draw-triathlon-graph {
+ line.transition-line {
+ stroke: $light-grey;
+ stroke-width: 2px;
+ stroke-dasharray: 6 4;
+ }
+}
+
+x-picker x-coordinate-system {
+ margin: 0;
+}
+
+x-step#triathlon-graph {
+ .row.padded {
+ margin-bottom: 0;
+ }
+
+ x-draw-graph {
+ margin-top: 0;
+ }
+}
\ No newline at end of file
diff --git a/content/shared/components/relation/relation.ts b/content/shared/components/relation/relation.ts
index 4206357a1..295d58202 100644
--- a/content/shared/components/relation/relation.ts
+++ b/content/shared/components/relation/relation.ts
@@ -4,6 +4,7 @@
// =============================================================================
+import {Step} from '@mathigon/studio';
import {$N, CustomElementView, ElementView, hover, register, slide, SVGParentView, SVGView} from '@mathigon/boost';
import {Point} from '@mathigon/euclid';
import template from './relation.pug';
@@ -14,21 +15,46 @@ type Match = {
matched: boolean
}
+type Connection = {
+ input: number
+ output: number
+ line: SVGView
+}
+
@register('x-relation', {template})
export class Relation extends CustomElementView {
private $inputs!: ElementView[];
private $outputs!: ElementView[];
+ private $lines!: SVGView[];
+ private $step?: Step;
+ private requireMatch = false;
private lastWidth = 0;
private inputTargets: Point[] = [];
private outputTargets: Point[] = [];
private matches: Match[] = [];
+ private connections: Connection[] = [];
ready() {
const $svg = this.$('svg.connections') as SVGParentView;
this.$inputs = this.$$('.domain .item');
this.$outputs = this.$$('.range .item');
+ this.requireMatch = this.attr('requireMatch') == 'true';
+
+ function selectRandomElement(elements: ElementView[]) {
+ return elements[Math.floor(Math.random() * elements.length)];
+ }
+
+ if (this.attr('randomize') == 'true') {
+ for (const $input of this.$inputs) {
+ $input.insertAfter(selectRandomElement(this.$inputs));
+ }
+ for (const $output of this.$outputs) {
+ $output.insertAfter(selectRandomElement(this.$outputs));
+ }
+ }
+
this.resize();
let $currentLine: SVGView|undefined = undefined;
@@ -59,19 +85,57 @@ export class Relation extends CustomElementView {
const $target = this.$outputs[activeTarget];
$target.removeClass('active');
- if ($target.attr('name') == this.matches[i].name) {
+ const extantConnection = this.connections.find(connection => connection.input == i && connection.output == activeTarget);
- this.trigger('correct', comment);
- this.matches[i].matched = true;
+ if (extantConnection) {
+ this.connections.splice(this.connections.indexOf(extantConnection), 1);
+ extantConnection.line.remove();
+ $currentLine!.remove();
- if (this.matches.every(m => m.matched == true)) {
- this.trigger('complete');
+ if ($target.attr('name') == this.matches[i].name) {
+ this.matches[i].matched = false;
}
+ if (this.matches.every(m => m.matched == true)) {
+ if (this.$step) {
+ this.$step.complete();
+ }
+ }
} else {
- this.trigger('incorrect');
+ const connection = {
+ input: i,
+ output: activeTarget,
+ line: $currentLine!
+ };
+
+ if ($target.attr('name') == this.matches[i].name) {
+ if (this.$step) {
+ this.$step.addHint('correct');
+ }
+
+ this.matches[i].matched = true;
+
+ if (this.matches.every(m => m.matched == true)) {
+ if (this.$step) {
+ this.$step.complete();
+ }
+ }
+
+ this.connections.push(connection);
+ } else if (this.requireMatch) {
+ if (this.$step) {
+ this.$step.addHint('incorrect');
+ }
+
+ $currentLine!.exit('draw', 300, 0, true);
+ } else {
+ if (this.$step) {
+ this.$step.addHint('incorrect');
+ }
+
+ this.connections.push(connection);
+ }
}
-
} else {
$currentLine!.exit('draw', 300, 0, true);
}
@@ -119,4 +183,8 @@ export class Relation extends CustomElementView {
// TODO Update existing connections
}
+
+ bindStep($step: Step) {
+ this.$step = $step;
+ }
}