Recently, CDG's Vi Hart and web programmer Niky Case relased a highly cited and praised web page called Parables of the Polygons. Not only beautifully designed, the page explains a mathematics concept in an explorable way and leads to conveying a positive social lesson for tolerance of all. We took the task of remaking the web page in Sketchpad14 and the CDP model to understand the advantages and limitations of our approach. This chapter summarizes what it took to make the demo.
Figure above is a snapshot of the demo done in Sketchpad14. Though this demo does not fully implement the original page, most interesting and interactive behaviors are present. Surprisingly, only three new constraint types had to be defined, two of which can be seen in the constraint list view in the figure above. The other five type of constraints were already part of our predefined set of constraints or present in previous demos.
You could find the source code for this demo at "../2d/demo/polygons/example-polygons.js" or scroll down to the bottom of this page to play it. Let's examine it. It should be possible to develop it all within the tool itself, but at the moment the UI just isn't usable yet.
All CDP programs are conventionally organized in the same way. Data classes are defined first, then constraint classes. Afterwards data objects are created and then constraints are instantiated to operate on them. Finally, should this be an interactive program (all Sketchpad14 programs are because objects are draggable), events are registered, whose job is to modify the data and constraint stores.
denotes each polygon shape in the page.
Note that each data class, such as this one, has defined a
denotes each board in the page.
We will see later than one the constraint types will be affecting the
// Dictionary add merge fn:
dictionaryAddMergeFn = function(curr, solutions) {
solutions.forEach(function(v) { for (var k in v) curr[k] = v[k] })
return curr
}
where each solution is a dictionary of key/value pairs that include keys (in this case indices in an array) that it wants to change. So, here is how the class defines its
Board.prototype.merge = function() {
return {cells: dictionaryAddMergeSolutions}
}
Now the reason the above works is that we happen to know there will be exactly one solution and no chance of conflicts, because there will be exactly one constraint that will want to set the
This class also has a few important functions related to the logic of the game, implemented in JS code:
states that
So let's go over the functions that must be defined for a constraint type:
Definitions related to property type checking and summary generation:
// property types:
ShapeSwingConstraint.prototype.propertyTypes =
{shape: 'Shape', swingSpeed: 'Number', dangling: 'Boolean'}
// class summary:
ShapeSwingConstraint.description = function() {
return "ShapeSwingConstraint(Shape S, Number R, Boolean D) causes "
+ "image of S to swing at rate R. When D is true this is simulating dangling "
+ "and will gradually subside."
}
// instance-specific summary:
ShapeSwingConstraint.prototype.description = function() {
return "image of shape" + this.shape
+ " should " + (this.dangle ? "dangle" : "swing") + " at an initial "
+ "rate of " + this.swingSpeed
}
And now things related to constraint solving:
ShapeSwingConstraint.prototype.computeError = function(pseudoTime) {
this.targetRotation = this.image.origRotation
+ (Math.sin(this.image.swingSpeed * pseudoTime + this.rotationOffset)
* Math.PI / 10)
return this.targetRotation - this.image.rotation
}
ShapeSwingConstraint.prototype.solve = function(pseudoTime) {
return {image: {rotation: this.targetRotation}}
}
Note that the previous constraint was temporal, i.e., it's a function of time, or as we have here:
states that the URL of the image belonging to the
We'll skip the functions related to type checking and summary generation, as they are similar to the previous constraint. So let's go over the constraint solving parts:
ShapeMoodConstraint.prototype.computeError = function(pseudoTime) {
this.targetMood = this.shape.board.getMood(this.shape)
return (shape.mood == this.targetMood
&& this.shapeImage.url === this.shape.getUrl(this.targetMood)) ? 0 : 1
}
ShapeMoodConstraint.prototype.solve = function(pseudoTime) {
return {shape: {mood: this.targetMood},
shapeImage: {url: this.shape.getUrl(this.targetMood)}}
}
Behavior of |
states if
So let's go over the functions that must be defined for a constraint type:
ShapePlacementConstraint.prototype.computeError = function(pseudoTime) {
var shapeCurrPos = this.shapePos, board = this.shape.board
this.shapeCoord = board.getCoord(shapeCurrPos)
var inside = board.containsPoint(shapeCurrPos)
if (inside && board.fits(shapeCoord)) {
this.placing = true
this.targetPos = plus(board.position,
{x: shapeCoord.j * board.cellLength,
y: shapeCoord.i * board.cellLength})
} else {
this.placing = false
this.targetPos = this.shapeOrigPos
}
return magnitude(minus(this.targetPos, shapeCurrPos))
}
ShapePlacementConstraint.prototype.solve = function(pseudoTime) {
var board = this.board, shape = this.shape
var sol = {shapePos: {x: this.targetPos.x, y: this.targetPos.y}}
if (this.placing) {
var shapeOldCoord = shape.boardPos
var dict = {}
dict[(shapeOldCoord.i * board.width) + shapeOldCoord.j] = 0
dict[(this.shapeCoord.i * board.width) + this.shapeCoord.j] = shape
sol.board = {cells: dict}
sol.shapeBoardPos = {i: this.shapeCoord.i, j: this.shapeCoord.j}
}
return sol
}
Now that we have all the constraint types we need, we can worry about the interactive/reactive part of the demo.
As with all demos, the default dragging-related events allowing things to be moved are in place. We saw these in Part III.
We could have this constraint be a continuous requirement on all pieces that are part of a board. However, since it applies only when a piece that was picked up dropped by the user, it makes sense to only add this requirement then and remove it the next time another piece is picked up (since by that time that original piece is already placed nicely in place).
registerEvent('mouseup', function(e) {
var thing = e.pointedObject
if (thing instanceof Shape && thing.board !== undefined)
placementConstraint = addConstraint(
new ShapePlacementConstraint(thing))
})
registerEvent('mousedown', function(e) {
if (placementConstraint !== undefined) {
removeConstraint(placementConstraint)
placementConstraint = undefined
}
})
The pieces on the header of the page (stored in the
registerEvent('mousemove', function(e) {
swingingShapes.forEach(function(shape) {
var dist = distance(e.mousePosition, shape.position)
// kept a reference of the constraint for convenience:
shape.swingConstraint.swingSpeed =
2 + (dist < 200 ? ((200-dist)/200*10) : 0)
}
})
Dangling effect |
We'll add an instance of
There is a feature we haven't talked about before, but we'll now discuss and use it. Each class (be it a data or constraint type) can define a
Thus we define a
ShapeSwingConstraint.prototype.onEachTimeStep = function(pseudoTime) {
if (this.dangle) {
var movement = this.shape.position.x - this.shap.lastPosition.x
if (Math.abs(movement) > 0)
this.swingSpeed += (movement / 200)
else {
this.swingSpeed /= 1.05
if (this.image.swingSpeed < 0.001)
this.swingSpeed = 0
}
this.shape.lastPosition = this.shape.position.copy()
}
}
Now all we need to do to get dangling working is to add a
registerEvent('mousedown', function(e) {
var thing = e.pointedObject
if (thing instanceof Shape) {
danglingConstraint = addConstraint(
new ShapeSwingConstraint(thing, 2, true))
}
})
Similarly, the constraint gets removed when a shape is dropped down.
By now all kinds of data, and continuous and reactive behaviors have been defined. All that's left to do is layout the page and add constraints.
To get the temporal constraints going, don't forget to add a
addConstraint(new TimerConstraint(new Timer(1))) // steps pseudo-time by 1 unit at each frame
Now we'll instantiate as many
That's all folks. We omitted some parts of the demo (e.g., In the tool see the slider demo, which was implemented separately and reused here), but you should have gotten a good overview of things by now!
Read Part VI: Comparison of CDP and Imperative Programming
Back to Table of Contents