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