web: shoot through the live camera
The one path where the look is chosen before the picture exists: OPEN CAMERA grades the camera's own feed with the recipe in force, many times a second, and the shutter hands the studio the sensor's still under that same recipe. Preview and file differ in resolution only — the still is `takePhoto`'s own frame, not a copy of the small preview video, with `grabFrame` and a 2d copy of the element behind it for the browsers that ship no ImageCapture. The renderer gains two inputs for it: `sourceImage`, a picture the caller already decoded (re-encoding the camera's frame to JPEG only to decode it again would cost more than the whole render), and `drawTo`, which paints the finished picture instead of encoding it. One render is in flight at a time; a frame that arrives during one is dropped, so a slow device shows a lower frame rate rather than a queue of moments that have passed. The view flashed black on a phone. Setting width/height on a canvas resets its bitmap: measured on the preview, a resize leaves mean 0 until the next render lands, which on this box is 0.5s and on a phone more. The buffer was sized from every incoming frame, and a capture that renegotiates its resolution — which Chromium does when the page is too slow to consume its frames, and this pipeline runs ~2 fps at 720p under software GL — strobed black/picture at every switch. The buffer is now sized on the first frame and after that only when the frame's aspect changes: a same-aspect frame is scaled into it. Swapping a 1280x720 stream for a 640x360 one mid-view now leaves the buffer at 1280x720 with no black frame, and 640x360 renders at 6-13 fps instead of 2. The frames are read from a <video>, which is now IN the document (1px, behind the black backdrop) rather than detached: Safari draws blank frames from a detached video, which is the same black-between-pictures. It leaves the document with the view, and the tracks are stopped, so the camera light goes out. Probes: cam-smoke (feed painted, resolution, frame rate, a monochrome sim reaching the live frames, shutter into the studio, close, console clean), cam-renegotiate (no resize, no blank frame, status line on the frames), cam-close-flip (flip returns a picture; video gone on close).
This commit is contained in:
@@ -278,6 +278,10 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
|
||||
}
|
||||
.dropzone.over { border-color: var(--accent); color: var(--text); background: var(--accent-soft); }
|
||||
.dropzone strong { display: block; color: var(--text); margin-bottom: 6px; font-size: 16px; }
|
||||
/* The camera sits beside the picker's own hint, so it needs a line of its own
|
||||
rather than riding on the sentence above it. The click on this button must
|
||||
not also open the file dialog the dropzone itself listens for. */
|
||||
.dropzone .btn { margin-top: 14px; }
|
||||
.canvas-wrap { position: relative; display: flex; align-items: center; justify-content: center; min-height: 0; flex: 1 1 auto; width: 100%; overflow: hidden; }
|
||||
.canvas-wrap img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: var(--radius-sm); box-shadow: var(--shadow); }
|
||||
/* Crop editor: the layer is the photo's own box, the rect is the framing and
|
||||
@@ -713,6 +717,73 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
|
||||
.stage .toolbar .dims { font-family: var(--mono); white-space: nowrap; }
|
||||
.err { color: var(--danger); font-size: 12px; max-width: 520px; text-align: center; }
|
||||
|
||||
/* --- live viewfinder ---------------------------------------------------- */
|
||||
/* Full screen and black whatever the theme: a viewfinder is something the user
|
||||
looks through, not a panel in the page. */
|
||||
.camera-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 70;
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
/* The graded frame itself. `object-fit: contain` keeps the whole picture on
|
||||
screen at any window shape, which is the composition the export writes. */
|
||||
.camera-feed {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
.camera-bar,
|
||||
.camera-deck {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
}
|
||||
.camera-bar .hint { color: #a9a9a9; font-family: var(--mono); }
|
||||
.camera-bar .spacer { flex: 1 1 auto; }
|
||||
.camera-deck .camera-spacer { width: 44px; }
|
||||
.camera-err { max-width: 900px; }
|
||||
/* The <video> the live frames are read from. It has to be IN the document —
|
||||
Safari paints blank frames from a detached video — but never seen: 1px, faint
|
||||
and out of the way, behind the black backdrop. Not `display: none`: that stops
|
||||
the frames as surely as detaching it. */
|
||||
.camera-source-host {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0.01;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.camera-source-host video { width: 1px; height: 1px; }
|
||||
/* The shutter: the app's one round control, because it is the one control whose
|
||||
shape says what it does. */
|
||||
.shutter {
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.65);
|
||||
cursor: pointer;
|
||||
}
|
||||
.shutter:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* --- auth dialog -------------------------------------------------------- */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
|
||||
Reference in New Issue
Block a user