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. + +

+ + + + + +
Go to Geometry Problems and Puzzles + +Valid HTML + +Valid CSS +
+ + + + 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