Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | import type { NumberLineState, TickMark, TickThresholds } from './types'
import { DEFAULT_TICK_THRESHOLDS } from './types'
import { computeProminence } from '../shared/tickMath'
import { worldToScreen, screenToWorld } from '../shared/coordinateConversions'
/**
* Compute all visible tick marks for the current viewport.
*
* For each power of 10, we compute how many ticks of that spacing fit on screen
* and assign a continuous prominence value based on configurable thresholds.
* Prominence drives smooth interpolation of all visual properties (height,
* line width, font, color) — no abrupt jumps at threshold boundaries.
*
* A tick at value N is only emitted at its coarsest applicable power so that,
* for example, 100 is drawn as a power-2 tick and not also as power-1 and power-0.
*/
export function computeTickMarks(
state: NumberLineState,
canvasWidth: number,
thresholds: TickThresholds = DEFAULT_TICK_THRESHOLDS
): TickMark[] {
const { center, pixelsPerUnit } = state
if (pixelsPerUnit <= 0 || canvasWidth <= 0) return []
const { anchorMax, mediumMax } = thresholds
const halfWidth = canvasWidth / 2
const leftValue = center - halfWidth / pixelsPerUnit
const rightValue = center + halfWidth / pixelsPerUnit
// Determine the range of powers to consider.
// Smallest power where tick spacing in px >= 2
const minPower = Math.floor(Math.log10(2 / pixelsPerUnit))
// Largest power where at least ~1 tick fits on screen
const maxPower = Math.ceil(Math.log10((rightValue - leftValue) * 2))
// First pass: determine prominence and opacity for each power
const powerInfo: { power: number; prominence: number; opacity: number }[] = []
for (let power = maxPower; power >= minPower; power--) {
const spacing = 10 ** power
const tickSpacingPx = spacing * pixelsPerUnit
// Skip if too dense (more than ~130 ticks) or too small (< 2px apart)
if (tickSpacingPx < 2) continue
const numTicks = canvasWidth / tickSpacingPx
if (numTicks > 130) continue
const prominence = computeProminence(numTicks, anchorMax, mediumMax)
// Opacity: fully visible until prominence < 0.15, then fades
const opacity = Math.min(1, prominence / 0.15)
// Skip fully invisible ticks
if (opacity <= 0) continue
powerInfo.push({ power, prominence, opacity })
}
// Second pass: generate ticks, deduplicating so each value appears only at
// its coarsest power. We process from coarsest to finest.
// Use string keys "power:index" for dedup to avoid floating-point collisions.
const seen = new Set<string>()
const ticks: TickMark[] = []
// powerInfo is already ordered from coarsest (highest power) to finest
for (const { power, prominence, opacity } of powerInfo) {
const spacing = 10 ** power
const firstIndex = Math.ceil(leftValue / spacing)
const lastIndex = Math.floor(rightValue / spacing)
for (let i = firstIndex; i <= lastIndex; i++) {
// Compute value from index * spacing (avoids accumulated addition drift)
const value = i * spacing
// Dedup: a tick at value V was already emitted at a coarser power P'
// if V is a multiple of 10^P'. Check all coarser powers.
let dominated = false
for (const coarser of powerInfo) {
if (coarser.power <= power) break // only check strictly coarser
const coarserSpacing = 10 ** coarser.power
// V is a multiple of coarserSpacing if i is a multiple of 10^(P'-P)
const ratio = Math.round(coarserSpacing / spacing)
if (i % ratio === 0) {
dominated = true
break
}
}
if (dominated) continue
// Additional string-key dedup for safety
const key = `${power}:${i}`
if (seen.has(key)) continue
seen.add(key)
ticks.push({ value, power, prominence, opacity })
}
}
return ticks
}
/** Convert a number-line value to a screen X coordinate */
export function numberToScreenX(
value: number,
center: number,
pixelsPerUnit: number,
canvasWidth: number
): number {
return worldToScreen(value, center, pixelsPerUnit, canvasWidth)
}
/** Convert a screen X coordinate to a number-line value */
export function screenXToNumber(
screenX: number,
center: number,
pixelsPerUnit: number,
canvasWidth: number
): number {
return screenToWorld(screenX, center, pixelsPerUnit, canvasWidth)
}
|