이 페이지는 아직 번역되지 않았습니다 — 영어 버전을 표시합니다.

Roll

The Roll tab shows every developed frame in the current folder as a contact sheet. From here you can apply a unified look across the whole roll — tone, color, crop, and film base — with a live two-pass preview that shows the result on every frame as you move a slider, then persists it automatically once you stop. You can also copy develop settings from a single frame and apply selected groups to the whole roll from the Basic panel.

The contact sheet

The contact sheet fills the main area of the Roll tab. Frames are laid out in horizontal rows of six — a strip — matching the physical strip length of a 35 mm roll. The tile aspect ratio adapts to the roll's actual frame shape: the median aspect of all landscape frames is computed via pickTileAspect() in contactSheet.ts, so that landscape frames fill their tile edge-to-edge. Portrait frames letterbox inside the same tile height, keeping every row the same height regardless of orientation.

Clicking any frame opens it in a full-screen frame preview overlay (rendered by FramePreview.svelte) — a full-resolution GPU viewport showing the frame's committed develop settings and crop. Press Esc or click the background to dismiss. The overlay is read-only: it shows the frame as-is and does not open the Frame editor.

The toolbar above the sheet has two controls. The Film edge toggle (label roll.filmEdge) switches the sheet between film-strip mode and plain proof-grid mode — see Film-edge rendering below. The Export contact sheet button (label roll.export.button) saves the sheet as a PNG file via an OS save dialog — see Export →.

Mirror mode and the entry gate

When you open the Roll tab, OpenEnlarge checks whether any frame in the folder already has develop edits (tone/color, crop, film base, or white point). If one or more frames do, a confirmation dialog appears — "Overwrite N edited frames?" (label confirmOverwrite.title) — explaining that applying roll-wide settings will replace per-frame edits. You can click Overwrite all (confirmOverwrite.overwrite) to proceed, or Cancel (confirmOverwrite.cancel) to return to the Library. A Don't ask me again checkbox (confirmOverwrite.dontAsk) suppresses the dialog for future visits to this roll. If no frames have edits, mirroring is enabled immediately without showing the dialog.

Once confirmed, the roll enters mirror mode: every change to the roll-wide controls in the right panel is reflected live across all frames in the contact sheet. If there are no conflicts, mirror mode is active from the moment the tab opens.

Live preview and persist (the two-pass model)

Mirror mode runs two asynchronous passes:

Pass 1 — Preview fires 120 ms after the last slider movement (debounced). It re-renders every frame's thumbnail using the draft look and updates the contact sheet immediately, with up to five frames rendering in parallel. The thumbnails shown in the sheet are replaced as results arrive; frames not yet updated keep their previous preview. This pass does not write to disk or update any stored edits.

Pass 2 — Persist fires 600 ms after the last change. It writes the draft look into every frame's stored edits (editsById), saves the crop geometry to all frames when a roll-wide crop is set, and updates the catalog thumbnails by reusing the previews already rendered in Pass 1 — no second render. Both passes are suppressed until you actually touch a control (rollDraftTouched), so simply opening the Roll tab never reverts per-frame edits you made in Frame.

The draft state is kept alive across re-entries to the same roll (so slider positions are not lost when you switch tabs), but a fresh default draft is started whenever the active folder changes.

Roll-wide controls

The right panel (rendered by RollAdjust.svelte) is headed "Roll look" (roll.adjust.heading) and contains the same sliders as the Frame Basic panel: Temperature, Tint, Exposure, Contrast, Highlights, Shadows, Whites, Blacks, Vibrance, and Saturation, plus a tonal curve. A Reset button (basic.reset) clears the look back to defaults while leaving the film base and white point in the draft untouched.

Below the heading and above the sliders, two icon tool buttons open reference-edit modes:

  • Crop (roll.crop.tool) — opens the full crop editor on a reference frame; the result is applied to all frames.
  • Film base (roll.base.heading) — opens base-sampling mode on a reference frame and recalibrates the whole-roll base from the pick; the new base is set as the folder default and reseeded across all frames. Use this to override the automatic roll base (see below).

Automatic roll base: when you develop a roll, OpenEnlarge automatically computes a single roll-wide film base — the confidence-filtered per-channel median of each frame's auto-detected rebate — and stores it as the folder default. Every frame is then seeded against this shared base. Frames with a per-image base_override or a manually picked white balance are left untouched. Existing rolls that have no stored base are migrated automatically on first open. The Film base reference-edit mode above is a manual recalibrate for when the automatic base needs correction; it replaces the stored folder base and reseeds the roll.

Both modes use the same reference-edit layout: the chosen frame fills the main area, a horizontal filmstrip of all frames runs along the bottom so you can switch the reference without leaving the mode, and the right panel shows the relevant controls or remains empty (base mode auto-dismisses on a single pick). In crop mode, pressing Enter or clicking the Crop button commits the geometry; pressing Esc discards it.

The scalar sliders are relative. Temperature, Tint, Exposure, Contrast, Highlights, Shadows, Whites, Blacks, Vibrance and Saturation apply as an offset on top of each frame's own value rather than overwriting it — so per-frame differences survive (each frame keeps its own Auto-Brightness exposure and white balance; a punchier frame stays relatively punchier). Moving a roll slider shifts every frame by the same amount, and opening a frame shows its shifted value. The Tonal Curve and the film base apply identically to every frame.

Copying Temperature with the Tone & Color group: Apply to whole roll… (basic.applyToRoll) carries Temperature/Tint with the Tone & Color group. (A per-frame-relative offset for copy/roll-apply — done in mired so the shift is consistent across base temperatures — is planned; until then Temperature copies as its slider value.)

Copy and apply to the whole roll

You can also push a single frame's develop settings across the whole roll from outside the Roll tab. In the Basic panel, the Apply to whole roll… button (basic.applyToRoll) opens a group-picker dialog — "Apply to all N frames in this roll?" (confirmApply.rollTitle) — with five checkboxes:

  • Tone & Color (applyGroups.toneColor) — all develop parameters except film base, white point, exposure, mode, stock, HDR, and positive flag.
  • Crop (applyGroups.crop) — the frame's full CropRect (aspect, orientation, rot90, flipH, flipV, angle, rect) applied identically to every frame.
  • Film base (applyGroups.base) — the frame's base_override RGB triplet applied to all frames.
  • Exposure (applyGroups.exposure) — the frame's absolute exposure value applied to all frames.
  • White point (applyGroups.whitePoint) — the frame's d_max_override applied to all frames.

The same group-picker appears when pasting copied settings (⌘C / ⌘V) onto a multi-image selection in the Library. For a single-image paste, only the Tone & Color group is applied by default (no dialog). The apply helpers are implemented in roll/apply.ts (applyToneColorToAll, applyCropToAll, applyBaseToAll, applyExposureToAll, applyWhitePointToAll) and are shared between the roll mirror path and the copy/paste path.

Film-edge rendering

The Film edge toggle switches between two sheet layouts, with a short fade transition between them:

Film-strip mode (Film edge on): each strip of six frames is wrapped in a film rebate. Above the frames: a row of CSS-rendered sprocket holes followed by monospaced sequential frame numbers. Below the frames: a barcode graphic, an editable text marking repeated three times across the strip width, and a direction arrow. The text marking is editable live via the Edge text field (roll.edgeText) that appears in the toolbar when film-edge is enabled; its placeholder reads "Film edge marking" (roll.edgeTextPlaceholder). The rebate background is dark (#131210); the frame area sits on a black row.

Proof-grid mode (Film edge off): each strip of six is a plain row. Each frame is shown on a warm-white background (#d8d3c4) with a 3 px padding and a drop shadow, and its sequential number is printed below in monospace. This mirrors the classic darkroom proof print.

The Export contact sheet button renders whichever layout is currently active — including the edge text — to a PNG file at a fixed resolution (frame width 260 px per frame tile, 6 frames per strip) via exportSheet.ts. The export faithfully replicates the sprocket, frame-number, barcode, edge-text, and arrow elements from the on-screen layout using the Canvas 2D API.

Under the hood

Draft state (roll/draft.ts): rollDraft (a Svelte writable store of type RollDraft) holds params: InvertParams and crop: CropRect | null. rollDraftTouched is a boolean store that starts false on each Roll tab entry; it is set to true the first time any slider, crop, base sample, or white-point pick fires. The two-pass preview/persist reactive chain ($: if (mirrorEnabled) { schedulePreview($rollDraft); schedulePersist($rollDraft); } in Roll.svelte) is inert while rollDraftTouched is false. enterRollDraft(folder) resets the draft only when the folder changes; re-entering the same roll preserves the slider positions.

Live preview concurrency (Roll.svelte): preview batches run with a pool of 5 concurrent api.thumbnail() calls. A monotonic previewToken discards stale batches so a slow render started before a fast slider drag cannot overwrite newer results. A requestAnimationFrame coalescing loop commits the accumulating previewMap at most ~60 times per second. If a new draft arrives while a batch is in flight, it is queued in previewPending and started immediately when the in-flight batch ends.

Tone/color subset (roll/apply.ts): toneColorOf(p) strips the EXCLUDE set (mode, stock, base_override, d_max_override, hdr, positive, exposure) from an InvertParams object. This is the "shared look" that mirrors across all frames; per-frame calibration fields (base, white point, exposure) travel only when explicitly included. The roll panel's scalar sliders apply relatively: applyRollDelta / rollPreviewFrame in apply.ts add the slider's offset (the delta since the last applied offset, tracked in rollApplied) on top of each frame's own value, so per-frame differences survive; the structured look (tonal curve, color grade) is still broadcast absolutely.

Tile aspect (contactSheet.ts): pickTileAspect(aspects) takes the median of all landscape-aspect (≥ 1) frame ratios, falling back to all frames or to the default 3:2 if the roll is empty or all-portrait. Each frame's effective aspect is computed in Roll.svelte by combining the raw metadata dimensions with orientDims(w, h, rot90) and the normalized crop rect dimensions (ow*rw / oh*rh).

D-max re-analysis after crop (Roll.svelte applyCropAndExit()): when the user commits a roll-wide crop, the function re-analyzes D-max from the reference frame's new crop area via api.analyze() and writes the result into the draft. If analysis fails (e.g. the frame is not yet developed) the existing value is preserved.

Next: Export →

OpenEnlarge — 오픈소스 필름 스캔 편집기 · MIT 라이선스