diff --git a/client/src/components/world_tree.css b/client/src/components/world_tree.css
index 5cf54fb..28768dd 100644
--- a/client/src/components/world_tree.css
+++ b/client/src/components/world_tree.css
@@ -45,3 +45,9 @@
padding: 3px;
}
} */
+
+.world-label {
+ /* border: 2px solid purple; */
+ padding: .2em;
+ border-radius: .5em;
+}
diff --git a/client/src/components/world_tree.tsx b/client/src/components/world_tree.tsx
index c064cc6..5ea7036 100644
--- a/client/src/components/world_tree.tsx
+++ b/client/src/components/world_tree.tsx
@@ -21,37 +21,73 @@ import './world_tree.css'
// Settings for the world tree
cytoscape.use( klay )
-const N = 18 // max number of levels per world
-const R = 64 // radius of a world
-const r = 12 // radius of a level
-const s = 10 // global scale
-const padding = R + 2*r // padding of the graphic (on a different scale)
-const ds = .75 // scale the resulting svg image
+
+const r = 16 // radius of a level
+const s = 12 // global scale
+const lineWidth = 10 //
+const ds = .75 // scale the resulting svg image
+
+const NMIN = 5 // min. worldsize
+const NLABEL = 9 // max. world size to display label below the world
+const NMAX = 16 // max. world size. Level icons start spiraling out if the world has more levels.
// colours
const grey = '#999'
const lightgrey = '#bbb'
-const green = 'green'
+const green = 'green' // 118a11?
const lightgreen = '#139e13'
const blue = '#1976d2'
+const darkgrey = '#868686'
+const darkgreen = '#0e770e'
+const darkblue = '#1667b8'
+
/** svg object for a level in the game tree */
-export function LevelIcon({ world, level, position, completed, unlocked }:
+export function LevelIcon({ world, level, position, completed, unlocked, worldSize }:
{ world: string,
level: number,
position: cytoscape.Position,
completed: boolean,
unlocked: boolean,
+ worldSize: number
}) {
+
+ const N = Math.max(worldSize, NMIN)
+
+ // divide circle into `N+2` equal pieces
+ const beta = 2 * Math.PI / Math.min(N+2, (NMAX+1))
+
+ // We want distance between two level icons to be `2.2*r`, therefore:
+ // Sinus-Satz: (1.1*r) / sin(β/2) = R / sin(π/2)
+ let R = 1.1 * r / Math.sin(beta/2)
+
const gameId = React.useContext(GameIdContext)
const difficulty = useSelector(selectDifficulty(gameId))
- const x = s * position.x + Math.sin(level * 2 * Math.PI / N) * (R + 1.2*r + 2.4*r*Math.floor((level - 1)/N))
- const y = s * position.y - Math.cos(level * 2 * Math.PI / N) * (R + 1.2*r + 2.4*r*Math.floor((level - 1)/N))
const levelDisabled = (difficulty >= 2 && !(unlocked || completed))
+
+ /** In the spiral, the angle `β` should decrease to avoid big gaps between levels.
+ * This is a simplified function, which has little mathematical foundation, but
+ * works fine in tests up to `N=30`.
+ */
+ function betaSpiral(level) {
+ return 2 * Math.PI / ((NMAX+1) + Math.max(0, (level-2)) / (NMAX+1))
+ }
+
+ const x = N < (NMAX+1) ?
+ // normal case
+ s * position.x + Math.sin(level * beta) * R :
+ // spiraling case
+ s * position.x + Math.sin(level * betaSpiral(level)) * (R + 2*r*(level-1)/(NMAX+1))
+ const y = N < (NMAX+1) ?
+ // normal case
+ s * position.y - Math.cos(level * beta) * R :
+ // spiraling case
+ s * position.y - Math.cos(level * betaSpiral(level)) * (R + 2*r*(level-1)/(NMAX+1))
+
return (
-
+
@@ -65,12 +101,22 @@ export function LevelIcon({ world, level, position, completed, unlocked }:
}
/** svg object of one world in the game tree */
-export function WorldIcon({world, title, position, completedLevels, difficulty}:
+export function WorldIcon({world, title, position, completedLevels, difficulty, worldSize}:
{ world: string,
title: string,
position: cytoscape.Position,
completedLevels: any,
- difficulty: number }) {
+ difficulty: number,
+ worldSize: number
+ }) {
+
+ // See level icons. Match radius computed there minus `1.2*r`
+ const N = Math.max(worldSize, NMIN)
+ const betaHalf = Math.PI / Math.min(N+2, (NMAX + 1))
+ let R = 1.1 * r / Math.sin(betaHalf) - 1.2 * r
+
+ // Offset for the labels for small worlds
+ let labelOffset = R + 2.5 * r
// index `0` indicates that all prerequisites are completed
let unlocked = completedLevels[0]
@@ -84,7 +130,6 @@ export function WorldIcon({world, title, position, completedLevels, difficulty}:
nextLevel = 0
}
let playable = difficulty <= 1 || completed || unlocked
-
const gameId = React.useContext(GameIdContext)
return
-
-
-
- {title ? title : world}
-
-
-
+ {worldSize > NLABEL ?
+ // Label for large worlds
+
+
+
+ {title ? title : world}
+
+
+
+ :
+ // Label for small worlds
+
+
+
+ {title ? title : world}
+
+
+ }
}
@@ -107,7 +165,7 @@ export function WorldIcon({world, title, position, completedLevels, difficulty}:
export function WorldPath({source, target, unlocked} : {source: any, target: any, unlocked: boolean}) {
return
+ stroke={unlocked ? green : grey} strokeWidth={lineWidth} />
}
/** Download a file containing `data` */
@@ -286,7 +344,9 @@ export function computeWorldLayout(worlds) {
headless: true,
styleEnabled: false
})
- const layout = cy.layout({name: "klay", klay: {direction: "DOWN", nodePlacement: "LINEAR_SEGMENTS"}} as LayoutOptions).run()
+
+ cy.layout({name: "klay", klay: {direction: "DOWN", nodePlacement: "LINEAR_SEGMENTS"}} as LayoutOptions).run()
+
let nodes = {}
cy.nodes().forEach((node, id) => {
nodes[node.id()] = {
@@ -355,21 +415,32 @@ export function WorldTreePanel({worlds, worldSize}:
position={position}
completedLevels={completed[worldId]}
difficulty={difficulty}
- key={`${gameId}-${worldId}`} />
+ key={`${gameId}-${worldId}`}
+ worldSize={worldSize[worldId]}
+ />
)
+
for (let i = 1; i <= worldSize[worldId]; i++) {
svgElements.push(
+ completed={completed[worldId][i]}
+ unlocked={completed[worldId][i-1]}
+ key={`${gameId}-${worldId}-${i}`}
+ worldSize={worldSize[worldId]}
+ />
)
}
}
}
+ // See `LevelIcon` for calculation of the radius. Use the max. radius for calculating the padding
+ // TODO: Is there a way to determine padding according to the drawn objects?
+ let R = 1.1 * r / Math.sin(Math.PI / (NMAX+1))
+ const padding = R + 2.1*r
+
let dx = bounds ? s*(bounds.x2 - bounds.x1) + 2*padding : null
return