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 | 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) } |