diff --git a/public/Geometry.zip b/public/Geometry.zip
new file mode 100644
index 0000000..9e9eaf9
Binary files /dev/null and b/public/Geometry.zip differ
diff --git a/public/index.html b/public/index.html
index c638339..2b5424f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,6 +8,7 @@
Joyce Geometry Applet
WRL Files
diff --git a/public/inscribed-revived.html b/public/inscribed-revived.html
new file mode 100644
index 0000000..9d491f5
--- /dev/null
+++ b/public/inscribed-revived.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+An equilateral triangle inscribed in a rectangle
+
+
+
+
+
+
+
+An equilateral triangle inscribed in a rectangle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Slide the “@” up and down to change the geometry.
+Press “r” to reset the diagram to its initial state.
+Proposition: The blue area equals the sum of the two pink areas.
+
+
+
+Problem statement
+
+
+The diagram above shows an equilateral triangle inscribed in a rectangle
+in such a way that the two have a vertex in common. This subdivides the
+rectangle into four disjoint triangles.
+The original equilateral triangle is shown in white
+in the diagram; the other three are shown in color.
+
+
+Proposition
+
+The area of the blue triangle equals the sum
+of the areas of the two pink triangles.
+
+
+
+The trigonometric proof is quite straightforward. I don't
+know of a classical proof a la Euclid .
+(Well, actually I haven't tried much.)
+If you can think of a neat non-trigonometric proof, let me know. I will
+put it here with due credit.
+
+
+This problem appeared as a conjecture
+in an article
+in the geometry.puzzles
newsgroup on March 15, 1997.
+
+
+Note added January 8, 2017:
+Here is a
+clever solution
+that Peter Renz sent me a in December 2016. Thanks, Peter!
+
+
+
+This applet was created by
+Rouben Rostamian
+using
+David Joyce 's
+Geometry
+Applet
+on July 2, 2010.
+
+
+
+
+
+
+
+
diff --git a/src/adapptlet.civet b/src/adapptlet.civet
index 5fbc3fb..0cd01e4 100644
--- a/src/adapptlet.civet
+++ b/src/adapptlet.civet
@@ -16,6 +16,30 @@ $('applet[code="Geometry"]').before (i, html) ->
height: parseInt(this.getAttribute('height') ?? '200') }
`
`
+type Split
+ S extends `${infer W} ${infer R}` ? (W | Split) : S
+
+classes := 'point line circle polygon sector plane sphere polyhedron'
+type JoyceClass = Split
+function assertJoyceClass(s: string): asserts s is JoyceClass
+ unless classes.includes s then throw new Error `Oops ${s} slipped through`
+
+type JoyceName = string // we use this to indicate where the names
+ // from the Joyce commands (which are used as captions in the GeoGebra
+ // applet) go.
+type GeoName = string // and this to indicate where GeoGebra identifiers go
+type AnyName = GeoName | JoyceName // and this for slots that can be either
+
+type Description
+ otherName: AnyName
+ usesCaptions: JoyceName[]
+ klass: JoyceClass
+ ends?: [GeoName, GeoName]
+
+// We put both JoyceNames and GeoNames in here, pointing to each other
+// with the otherName property:
+type JoyceElements = Record
+
jQuery.getScript 'https://www.geogebra.org/apps/deployggb.js', =>
for each jApp of joyceApplets
params := {
@@ -23,100 +47,325 @@ jQuery.getScript 'https://www.geogebra.org/apps/deployggb.js', =>
jApp.width,
jApp.height,
appletOnLoad: (api: AppletObject) =>
+ elements: JoyceElements := {}
for child of jApp.children
- dispatchJcommand api, child
+ dispatchJcommand api, child, elements
api.setCoordSystem(-10, 10 + jApp.width, -10, 10 + jApp.height)
} as const
geoApp := new GGBApplet params
geoApp.inject jApp.id
-type GeogebraCallback = (api: AppletObject) => void
+type DimParts = [string[], string[], string[]] // Gives GeoNames
+ // or expressions for 0-, 1-, and 2-dimensional parts for coloring
+
+// need to pass the parts into the callbacks because sometimes the parts
+// are not generated until callback time
+type GeogebraCallback = (api: AppletObject, parts: DimParts) => void
type Commander
- command: string
+ commands: string[]
callbacks: GeogebraCallback[]
+ parts: DimParts
+ auxiliaries: GeoName[] // extra entities needed in GeoGebra
+ ends?: [GeoName, GeoName]
+
+function freshCommander(): Commander
+ commands: []
+ callbacks: []
+ parts: [[], [], []]
+ auxiliaries: []
+
+type JoyceArguments =
+ Partial & {scalar: number[]}>
type ClassHandler = (
- name: string, m: string, data: string, colors: string[]) => Commander
+ name: GeoName, m: string, args: JoyceArguments, index: number) => Commander
-function dispatchJcommand(api: AppletObject, param: Element): void
+// Executes the command corresponding to param against the GeoGebra applet
+// api, consulting and extending by side effect the elements that are
+// present in that applet
+function dispatchJcommand(
+ api: AppletObject, param: Element, elements: JoyceElements): void
val := param.getAttribute 'value'
unless val return
- switch param.getAttribute 'name'
+ attr := param.getAttribute 'name'
+ switch attr
'background'
api.setGraphicsOptions 1, bgColor: `#${val}`
'title'
api.evalCommand `TitlePoint = Corner(1,1)
Text("${val}", TitlePoint + (2,5))`
/e\[\d+\]/
- {command, callbacks} := jToG val
- if command
- if api.evalCommand command
- for each cb of callbacks
- cb(api)
- else
- console.log `Geogebra command '${command}' translated from`,
- val, 'failed.'
- else
- console.log `Could not parse command '${val}'`
- else
- console.log `Unkown param ${param}`
+ num := parseInt(attr.slice(2))
+ {commands, callbacks, parts} := jToG val, elements, num
+ if commands.length
+ lastTried .= 0
+ if commands.filter((&)).every (cmd) =>
+ api.evalCommand(cmd) and ++lastTried
+ callbacks.forEach &(api, parts)
+ else console.log
+ `Geogebra command '${commands[lastTried]}'
+ (part of translation of '${val}')
+ failed.`
+ else console.log `Could not parse command '${val}'`
+ else console.log `Unkown param ${param}`
-function jToG(jCom: string): Commander
- [name, klass, method, data, ...colors] := jCom.split(';')
- if klass in classHandler
- return classHandler[klass] name, method, data, colors
- console.log `Unknown entity class ${klass}`
- command: '', callbacks: []
+// function myListener(...args: unknown[]) {
+// console.log 'In my listener with', args
+// }
-classHandler: Record :=
- point: (name, method, data, colors) =>
- command .= ''
- callbacks: GeogebraCallback[] .= []
- args := data.split(',')
+// window.myListener = myListener
+
+// Parses a Joyce element-creating command, extending the elements
+// by side effect:
+function jToG(jCom: string, elements: JoyceElements, index: number): Commander
+ [jname, klass, method, data, ...colors] := jCom.split(';')
+ cmdr .= freshCommander()
+ unless klass in classHandler
+ console.log `Unknown entity class ${klass}`
+ return cmdr
+ assertJoyceClass klass // shouldn't need to do that :-/
+ name := if /^\p{L}\w*$/u.test jname then jname else geoname jname, elements
+ args: JoyceArguments := {}
+ usesCaptions := []
+ for each jdep of data.split ','
+ scalar := parseFloat jdep
+ if scalar is scalar // not NaN
+ (args.scalar ?= []).push scalar
+ continue
+ unless jdep in elements
+ console.log `Reference to unknown geometric entity ${jdep} in $jCom}`
+ return cmdr
+ usesCaptions.push jdep
+ {klass: depKlass, otherName: depGeo, ends} := elements[jdep]
+ (args[depKlass] ?= []).push depGeo
+ if depKlass is 'point'
+ (args.subpoints ?= []).push depGeo
+ else if depKlass is 'line'
+ (args.subpoints ?= []).push ...ends ?? []
+ cmdr = classHandler[klass] name, method, args, index
+ unless name is jname then cmdr.callbacks.push (api: AppletObject) =>
+ api.setCaption(name, jname)
+ api.setLabelStyle(name, 3) // style CAPTION = 3
+ if cmdr.auxiliaries.length
+ cmdr.callbacks.push (api: AppletObject) =>
+ for each aux of cmdr.auxiliaries
+ api.setAuxiliary(aux, true)
+ api.setVisible(aux,false)
+ // Create callback to assign colors
+ if colors.length is 4 and colors.every (color) =>
+ false and color is '0' or color is 'none'
+ cmdr.callbacks.push (api: AppletObject) =>
+ api.setVisible(name, false)
+// window[hideListener] = (arg) =>
+// console.log('Hello', arg, 'disappearing', name)
+// api.setVisible(name, false)
+ api.registerObjectUpdateListener(name, hideListener)
+ if cmdr.ends or klass is 'line'
+ elements[jname] =
+ {otherName: name, usesCaptions, klass: 'line', cmdr.ends}
+ elements[name] =
+ {otherName: jname, usesCaptions, klass: 'line', cmdr.ends}
+ else // any other geometry
+ elements[jname] = {otherName: name, usesCaptions, klass}
+ elements[name] = {otherName: jname, usesCaptions, klass}
+ cmdr
+
+function geoname(jname: JoyceName, elements: JoyceElements): GeoName
+ numCode .= 0n
+ numCode = numCode*128n + BigInt ch.codePointAt(0) ?? 1 for each ch of jname
+ return .= 'Geo' + numCode.toString(36);
+ return += '1' while return.value in elements
+
+// All of the detailed semantics of each available command lies in this
+// function.
+classHandler: Record :=
+ point: (name, method, args): Commander =>
+ return := freshCommander()
+ {commands, callbacks, parts, auxiliaries} := return.value
+ aux := name + 'aUx'
+ parts[0].push name
switch method
/free|fixed/
- command += `${name} = (${data})`
+ commands.push `${name} = (${args.scalar?.join ','})`
if method is 'fixed'
callbacks.push (api: AppletObject) => api.setFixed(name, true)
'perpendicular'
- [center, direction] := args
- command += `${name} = Rotate(${direction}, 3*pi/2, ${center})`
+ // Note only the two-point option implemented so far
+ unless args.subpoints return
+ [center, direction] := args.subpoints
+ // Note clockwise 90° rotation (3π/2) confirmed in Joyce source
+ commands.push `${name} = Rotate(${direction}, 3*pi/2, ${center})`
'angleDivider'
- n .= -1
- // use the fact that NaN doesn't equal itself:
- nLoc := args.findIndex((arg) => (n = parseInt arg) is n)
- if n >= 0
- args.splice(nLoc)
- [center, start, end] := args
- command += `${name}aUx1 = Segment(${start}, ${end})
- ${name}aUx2 = Angle(${start}, ${center}, ${end})
- ${name}aUx2a = If(${name}aUx2 > pi, ${name}aUx2 - 2*pi, ${name}aUx2)
- ${name}aUx3 = Rotate(${start}, ${name}aUx2a/${n}, ${center})
- ${name}aUx4 = Ray(${center}, ${name}aUx3)
- ${name} = Intersect(${name}aUx1, ${name}aUx4)`
+ // Note doesn't yet handle plane argument
+ unless args.subpoints return
+ [start, center, end] := args.subpoints
+ // see if we need to make the destination segment from start to end
+ destination .= ''
+ unless args.line?.length is 1 and args.point?[0] is center
+ destination = aux + '1'
+ auxiliaries.push destination
+ commands.push `${destination} = Segment(${start}, ${end})`
+ else destination = args.line[0]
+ n := args.scalar?[0]
+ commands.push
+ `${aux}2 = Angle(${start}, ${center}, ${end})`
+ `${aux}3 = If(${aux}2 > pi, ${aux}2 - 2*pi, ${aux}2)`
+ `${aux}4 = Rotate(${start}, ${aux}3/${n}, ${center})`
+ `${name} = Intersect(${destination}, Ray(${center}, ${aux}4))`
+ auxiliaries.push ...[2..4].map (i) => `${aux}${i}`
'intersection'
- command += `${name} = Intersect(${data})`
- return {command, callbacks}
+ // Checking Joyce source, means intersection of lines, not
+ // intersection of line segments
+ unless args.subpoints then return
+ l1 := `Line(${args.subpoints[0]},${args.subpoints[1]})`
+ l2 := `Line(${args.subpoints[2]},${args.subpoints[3]})`
+ commands.push `${name} = Intersect(${l1},${l2})`
+ 'lineSegmentSlider'
+ segment .= args.line?[0]
+ unless segment
+ unless args.point then return
+ commands.push `${aux} = Segment(${args.point.join ','})`
+ auxiliaries.push aux
+ segment = aux
+ commands.push `${name} = Point(${segment})`
+ if args.scalar and args.scalar.length
+ callbacks.push (api: AppletObject) =>
+ api.setCoords name,
+ ...args.scalar as [number, number, number]
+ 'first'
+ unless args.subpoints then return
+ commands.push `${name} = ${args.subpoints[0]}`
+ 'last'
+ unless args.subpoints then return
+ commands.push `${name} = ${args.subpoints.at(-1)}`
+ 'extend'
+ unless args.subpoints then return
+ sp := args.subpoints
+ direction .= `UnitVector(Vector(${sp[0]},${sp[1]}))`
+ if args.line and (
+ not args.point or args.point[0] !== args.subpoints[0])
+ direction = `UnitVector(${args.line[0]})`
+ displacement := `Distance(${sp[2]}, ${sp[3]})*${direction}`
+ commands.push `${name} = Translate(${sp[1]}, ${displacement})`
+ 'vertex'
+ commands.push
+ `${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})`
+ 'midpoint'
+ if args.line
+ commands.push `${name} = Midpoint(${args.line[0]})`
+ else
+ commands.push
+ `${name} = Midpoint(${args.point?[0]},${args.point?[1]})`
+ 'foot'
+ pt := args.subpoints
+ unless pt then return
+ commands.push
+ `${name} = ClosestPoint(Line(${pt[1]},${pt[2]}), ${pt[0]})`
- line: (name, method, data, colors) =>
- command .= ''
- callbacks: GeogebraCallback[] .= []
- args := data.split(',')
+ line: (name, method, args) =>
+ return := freshCommander()
+ return.value.ends = ['', '']
+ {commands, callbacks, parts, auxiliaries, ends} := return.value
+ aux := name + 'aUx'
+ parts[1].push name
+ madeSegment .= false
switch method
'connect'
- command += `${name} = Segment(${data})`
+ unless args.subpoints and args.subpoints.length is 2 then return
+ ends[0] = args.subpoints[0]
+ ends[1] = args.subpoints[1]
'parallel'
- [newStart, oldStart, oldEnd] := args
- command += `${name}aUx1 = Vector(${oldStart}, ${newStart})
- ${name}aUx2 = Translate(${oldEnd}, ${name}aUx1)
- ${name} = Segment(${newStart}, ${name}aUx2)`
- return {command, callbacks}
+ unless args.subpoints then return
+ [newStart, oldStart, oldEnd] := args.subpoints
+ commands.push `${aux}1 = Vector(${oldStart}, ${newStart})`
+ auxiliaries.push aux + 1, aux + 2
+ ends[0] = newStart
+ ends[1] = aux + 2
+ if args.line?.length is 1 and args.point?[0] is args.subpoints[0]
+ // In this case we are translating an existing segment
+ commands.push
+ `${name} = Translate(${args.line[0]}, ${aux}1)`
+ `${aux}2 = Vertex(${name}, 2)`
+ madeSegment = true
+ else
+ commands.push `${aux}2 = Translate(${oldEnd}, ${aux}1)`
+ 'chord'
+ // To match Joyce, we need to get the ordering here correct.
+ // ends[0] should be the one closer to args.subpoints[0]
+ unless args.subpoints and args.circle then return
+ line := `Line(${args.subpoints.join ','})`
+ pt := args.subpoints[0]
+ commands.push ...[1..2].map (n) =>
+ `${aux}${n} = Intersect(${args.circle}, ${line}, ${n})`
+ condition := `Distance(${aux}2,${pt}) < Distance(${aux}1,${pt})`
+ // NOTE: Joyce's code has special case for when pt is almost
+ // at midpoint of chord; in that case, it starts at endpoint
+ // closer to the second subpoint... postponing that nicety
+ commands.push
+ `${aux}3 = If(${condition}, ${aux}2, ${aux}1)`
+ `${aux}4 = If(${condition}, ${aux}1, ${aux}2)`
+ ends[0] = aux + 3
+ ends[1] = aux + 4
+ auxiliaries.push ...[1..4].map (n) => aux + n
+ unless madeSegment
+ commands.push `${name} = Segment(${ends[0]},${ends[1]})`
+ callbacks.push (api: AppletObject) =>
+ api.setLabelVisible(name, true)
+ parts[0].push ...ends
- circle: (name, method, data, colors) =>
- command .= ''
- callbacks: GeogebraCallback[] .= []
+ circle: (name, method, args) =>
+ return := freshCommander()
+ {commands, callbacks, parts, auxiliaries} := return.value
+ parts[2].push name
+ parts[1].push name
switch method
'radius'
- [center, point] := data.split(',')
- command += `${name} = Circle(${center}, ${point})`
- return {command, callbacks}
+ unless args.subpoints then return
+ [center, point] := args.subpoints
+ commands.push `${name} = Circle(${center}, ${point})`
+ callbacks.push (api: AppletObject) => api.setLabelVisible(name, true)
+
+ polygon: (name, method, args, index) =>
+ return := freshCommander()
+ {commands, callbacks, parts, auxiliaries} := return.value
+ parts[2].push name
+ // what to push for edges?
+ switch method
+ 'equilateralTriangle'
+ pt := args.subpoints
+ unless pt then return
+ commands.push '' // hack, make sure there is a command
+ parts[0].push pt[0], pt[1]
+ callbacks.push (api: AppletObject, moreParts: DimParts) =>
+ made:= api.evalCommandGetLabels
+ `${name} = Polygon(${pt[1]},${pt[0]}, 3)`
+ if not made return
+ for each obj of made.split ','
+ if obj is name continue
+ newObj := 'GeoAux' + index + obj
+ api.renameObject obj, newObj
+ switch api.getObjectType newObj
+ 'segment'
+ parts[1].push newObj
+ 'point'
+ parts[0].push newObj
+ api.setVisible(newObj, false)
+ /triangle|quadrilateral/
+ pt := args.subpoints
+ unless pt then return
+ commands.push ''
+ parts[0].push ...pt
+ callbacks.push (api: AppletObject, moreParts: DimParts) =>
+ made := api.evalCommandGetLabels
+ `${name} = Polygon(${pt.join ','})`
+ if not made return
+ for each obj of made.split ','
+ if obj is name continue
+ newObj := 'GeoAux' + index + obj
+ api.renameObject obj, newObj
+ parts[1].push newObj
+
+ sector: (name, method, args) => freshCommander()
+ plane: (name, method, args) => freshCommander()
+ sphere: (name, method, args) => freshCommander()
+ polyhedron: (name, method, args) => freshCommander()
diff --git a/src/giveAwrl.civet b/src/giveAwrl.civet
index 8799121..76ff036 100644
--- a/src/giveAwrl.civet
+++ b/src/giveAwrl.civet
@@ -47,7 +47,7 @@ function makeBrowser(url: string)
canvas
// Put eye icons after all of the eligible links
-links := $('a').filter -> !!@.getAttribute('href')?.match knownExtensions
+links := $('a').filter -> knownExtensions.test @.getAttribute('href') ?? ''
links.after ->
newSpan := $('👁 ')
newSpan.hover