world size varies with number of levels #95
This commit is contained in:
@@ -45,3 +45,9 @@
|
||||
padding: 3px;
|
||||
}
|
||||
} */
|
||||
|
||||
.world-label {
|
||||
/* border: 2px solid purple; */
|
||||
padding: .2em;
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Link to={levelDisabled ? '' : `/${gameId}/world/${world}/level/${level}`}
|
||||
className={`level${levelDisabled ? ' disabled' : ''}`}>
|
||||
<circle fill={completed ? lightgreen : unlocked? blue : grey} cx={x} cy={y} r={r} />
|
||||
<circle fill={completed ? lightgreen : unlocked? blue : lightgrey} cx={x} cy={y} r={r} />
|
||||
<foreignObject className="level-title-wrapper" x={x} y={y}
|
||||
width={1.42*r} height={1.42*r} transform={"translate("+ -.71*r +","+ -.71*r +")"}>
|
||||
<div>
|
||||
@@ -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 <Link
|
||||
@@ -92,14 +137,27 @@ export function WorldIcon({world, title, position, completedLevels, difficulty}:
|
||||
className={playable ? '' : 'disabled'}>
|
||||
<circle className="world-circle" cx={s*position.x} cy={s*position.y} r={R}
|
||||
fill={completed ? green : unlocked ? blue : grey}/>
|
||||
<foreignObject className="world-title-wrapper" x={s*position.x} y={s*position.y}
|
||||
width={1.42*R} height={1.42*R} transform={"translate("+ -.71*R +","+ -.71*R +")"}>
|
||||
<div className={unlocked && !completed ? "playable-world" : ''}>
|
||||
<p className="world-title" style={{fontSize: Math.floor(R/4) + "px"}}>
|
||||
{title ? title : world}
|
||||
</p>
|
||||
</div>
|
||||
</foreignObject>
|
||||
{worldSize > NLABEL ?
|
||||
// Label for large worlds
|
||||
<foreignObject className="world-title-wrapper" x={s*position.x} y={s*position.y}
|
||||
width={1.42*R} height={1.42*R} transform={"translate("+ -.71*R +","+ -.71*R +")"}>
|
||||
<div className={unlocked && !completed ? "playable-world" : ''}>
|
||||
<p className="world-title" style={{fontSize: Math.floor(R/4) + "px"}}>
|
||||
{title ? title : world}
|
||||
</p>
|
||||
</div>
|
||||
</foreignObject>
|
||||
:
|
||||
// Label for small worlds
|
||||
<foreignObject x={s*position.x - 75} y={s*position.y + labelOffset}
|
||||
width='150px' height='2em' style={{overflow: 'visible'}}
|
||||
>
|
||||
<div className='world-label' style={{backgroundColor: completed ? darkgreen : unlocked ? darkblue : darkgrey}}>
|
||||
<p className='world-title'>
|
||||
{title ? title : world}
|
||||
</p>
|
||||
</div>
|
||||
</foreignObject>}
|
||||
</Link>
|
||||
}
|
||||
|
||||
@@ -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 <line x1={s*source.position.x} y1={s*source.position.y}
|
||||
x2={s*target.position.x} y2={s*target.position.y}
|
||||
stroke={unlocked ? green : lightgrey} strokeWidth={s}/>
|
||||
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(
|
||||
<LevelIcon
|
||||
world={worldId}
|
||||
level={i}
|
||||
position={position}
|
||||
completed={completed[worldId][i]} unlocked={completed[worldId][i-1]}
|
||||
key={`${gameId}-${worldId}-${i}`} />
|
||||
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 <div className="column">
|
||||
|
||||
Reference in New Issue
Block a user