All files / src/edit paint.ts

50.46% Statements 54/107
66.66% Branches 12/18
50% Functions 3/6
50.46% Lines 54/107

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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202                          2x 2x                                                       2x 24x 24x 24x 24x 6120x 6120x 6120x 6120x 6120x 24x 24x     2x                                                                 2x                                                                                                                                     2x 3x 3x 3x 3x 43x 43x 43x 43x 43x 43x 43x 43x 43x 3x 3x               2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 6x 6x 4x 4x 2x 1x  
/**
 * Draw the edited map from the picture, through the lens (SPECS §27.9).
 *
 * The picture is in the file's order and the screen is in the edit's, so every
 * pixel asks the lens which cell of the picture it shows: the edited cell under
 * the pixel's centre, then {@link LensGrid.cellMap}, then the level of the
 * pyramid whose cell is about a pixel. That is two lookups a pixel on top of
 * the colour, and no picture is built between the two — so an edit, a pan and a
 * piece dragged under the pointer all cost one paint, the same paint.
 *
 * Pure: typed arrays in, typed arrays out, so it is tested without a canvas.
 */
 
import { applyColormap, colormapIndex } from '../render/colormaps'
import { encode, logOf, type Quantizer } from '../metacooler/picture'
import type { LensGrid } from './grid'
 
/** One level of the picture: an upper triangle `n` a side. */
export interface PaintLevel {
  n: number
  cells: Uint8Array
}
 
/** The window to draw, in units of the edited axis, and the pixels it fills. */
export interface PaintView {
  /** Columns: the units at the left and right edges. */
  x0: number
  x1: number
  /** Rows: the units at the top and bottom edges. */
  y0: number
  y1: number
  width: number
  height: number
}
 
/**
 * Every byte's colour, as the 32-bit pixel `ImageData` holds — red in the low
 * byte, on the little-endian machines browsers run on. Byte `0`, a cell with
 * nothing in it, is transparent, as a blank cell is on every map (§6.4).
 *
 * `vmin` and `vmax` are base-10 logs, as the picture's own range is.
 */
export function pictureColours(colormap: string, q: Quantizer, vmin: number, vmax: number): Uint32Array {
  const table = applyColormap(colormap)
  const out = new Uint32Array(256)
  const span = vmax > vmin ? vmax - vmin : 1
  for (let byte = 1; byte < 256; byte++) {
    const index = colormapIndex((logOf(byte, q) - vmin) / span)
    if (index < 0) continue
    const at = index * 4
    out[byte] = ((table[at + 3] << 24) | (table[at + 2] << 16) | (table[at + 1] << 8) | table[at]) >>> 0
  }
  return out
}
 
/** The level of the pyramid whose cell is nearest a pixel at this zoom. */
export function levelFor(unitsPerPixel: number, levels: number): number {
  if (!(unitsPerPixel > 1)) return 0
  return Math.max(0, Math.min(levels - 1, Math.round(Math.log2(unitsPerPixel))))
}
 
/**
 * The picture cell each pixel along one side shows, at a level — `-1` off the
 * axis or in a gap.
 */
function sideCells(
  cellMap: Int32Array,
  from: number,
  to: number,
  pixels: number,
  shift: number,
): Int32Array {
  const out = new Int32Array(pixels)
  const per = (to - from) / pixels
  for (let p = 0; p < pixels; p++) {
    const unit = Math.floor(from + (p + 0.5) * per)
    const cell = unit >= 0 && unit < cellMap.length ? cellMap[unit] : -1
    out[p] = cell < 0 ? -1 : cell >> shift
  }
  return out
}
 
/**
 * Paint a window of the edited map into `target`, `view.width × view.height`
 * pixels, row-major.
 *
 * @param rows The lens for the rows, `cols` for the columns: the same one on the
 *   stage's square map, two while a stretch is dragged between them.
 */
export function paintPicture(
  target: Uint32Array,
  view: PaintView,
  rows: Int32Array,
  cols: Int32Array,
  levels: readonly PaintLevel[],
  colours: Uint32Array,
): void {
  const { width, height } = view
  const upp = Math.max((view.x1 - view.x0) / width, (view.y1 - view.y0) / height)
  const level = levelFor(upp, levels.length)
  const { n, cells } = levels[level]
  const rowCell = sideCells(rows, view.y0, view.y1, height, level)
  const colCell = sideCells(cols, view.x0, view.x1, width, level)
  // Where each row's and each column's own run of the triangle begins, so the
  // inner loop is a subtraction and a lookup whichever half a pixel is in.
  const base = (a: number): number => a * n - (a * (a - 1)) / 2
  const colBase = new Float64Array(width)
  for (let x = 0; x < width; x++) colBase[x] = colCell[x] < 0 ? -1 : base(colCell[x])
  for (let y = 0; y < height; y++) {
    const a = rowCell[y]
    const row = y * width
    if (a < 0) {
      target.fill(0, row, row + width)
      continue
    }
    const rowBase = base(a)
    for (let x = 0; x < width; x++) {
      const b = colCell[x]
      if (b < 0) {
        target[row + x] = 0
        continue
      }
      target[row + x] = colours[cells[a <= b ? rowBase + (b - a) : colBase[x] + (a - b)]]
    }
  }
}
 
/**
 * A window of the edited map read finer than the picture (§27.9): a rung's
 * bins through the lens, each side a run of bins per edited sequence.
 */
export interface WindowRead {
  /** Base pairs a bin. */
  binsize: number
  rows: readonly WindowRun[]
  cols: readonly WindowRun[]
  /** Row-major, `rows × cols` long; `NaN` for a bin the balancer left empty. */
  values: Float32Array
  ncols: number
}
 
/** The bins of one edited sequence a side of a window covers. */
export interface WindowRun {
  sequence: string
  /** Its first bin in the window, counted from the sequence's start. */
  firstBin: number
  count: number
  /** Where that bin is in the window's own rows or columns. */
  at: number
}
 
/**
 * The window's row (or column) each pixel along one side shows — `-1` off it,
 * or in a gap: the unit under the pixel's centre, its place in the edited
 * sequence, the bin there.
 */
export function windowSide(grid: LensGrid, from: number, to: number, pixels: number, runs: readonly WindowRun[], binsize: number): Int32Array {
  const out = new Int32Array(pixels).fill(-1)
  const byName = new Map(runs.map((run) => [run.sequence, run]))
  const per = (to - from) / pixels
  for (let p = 0; p < pixels; p++) {
    const unit = from + (p + 0.5) * per
    if (!(unit >= 0 && unit < grid.n)) continue
    const place = grid.place(unit)
    if (place === null || place.part.piece === null) continue
    const run = byName.get(place.sequence)
    if (run === undefined) continue
    const k = Math.floor(place.bp / binsize) - run.firstBin
    if (k >= 0 && k < run.count) out[p] = run.at + k
  }
  return out
}
 
/**
 * Paint a window over the picture where it reaches, and leave the picture's
 * pixels where it does not (§27.9). A value is scaled to a picture cell's
 * worth — `scale`, the bins of the window a cell holds — and coloured on the
 * picture's own quantizer, so a finer read is the same map, closer.
 */
export function paintWindow(
  target: Uint32Array,
  width: number,
  rows: Int32Array,
  cols: Int32Array,
  read: Pick<WindowRead, 'values' | 'ncols'>,
  scale: number,
  q: Quantizer,
  colours: Uint32Array,
): void {
  for (let y = 0; y < rows.length; y++) {
    const r = rows[y]
    if (r < 0) continue
    const at = y * width
    const base = r * read.ncols
    for (let x = 0; x < cols.length; x++) {
      const c = cols[x]
      if (c < 0) continue
      target[at + x] = colours[encode(read.values[base + c] * scale, q)]
    }
  }
}