Epic 2.5.1: basis for framing layer
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
// How a shot is FRAMED, as opposed to what it contains.
|
||||
//
|
||||
// Every scene in the library is a locked-off, full-frame wide, and always has
|
||||
// been. That is one shot type, held for the length of a song. Cutting between
|
||||
// two scenes therefore changes the subject and never the framing — and framing
|
||||
// is at least half of how a real edit holds attention. A wide answered by a
|
||||
// close reads as two shots of one thing; two wides read as two things.
|
||||
//
|
||||
// So a shot now carries a scale and a recentre, applied in SCENE coordinates
|
||||
// inside sigCamera. That distinction matters: a close-up is rendered close
|
||||
// rather than being a magnified 720p frame, which is why this is a coordinate
|
||||
// transform and not a post pass. It is also why it costs nothing at 4K.
|
||||
//
|
||||
// Framing is per shot and constant within it. A zoom that moves during a shot
|
||||
// is a different device — one that would fight the drift LFO and the slow axis,
|
||||
// both of which already own continuous motion.
|
||||
|
||||
/**
|
||||
* The shot sizes, as multipliers on the scene's coordinate scale.
|
||||
*
|
||||
* Bounded much more tightly than a real camera would be. Past about 2.2 most
|
||||
* scenes in this library run out of detail and a close-up is just a blurry
|
||||
* wide; below about 0.55 the subject is a speck in an empty frame. Both were
|
||||
* measured by pushing until the image stopped being worth looking at.
|
||||
*/
|
||||
export const SHOT_SIZES = {
|
||||
wide: { scale: 0.62, drift: 0.06 },
|
||||
normal: { scale: 1.0, drift: 0.05 },
|
||||
close: { scale: 1.7, drift: 0.10 },
|
||||
};
|
||||
|
||||
export const SHOT_SIZE_NAMES = Object.keys(SHOT_SIZES);
|
||||
|
||||
/**
|
||||
* Whether this track uses framing at all, and how boldly.
|
||||
*
|
||||
* A track that never changes size is a legitimate look — locked-off is a style,
|
||||
* and it is the one the whole library was built in — so it stays reachable.
|
||||
* What is not acceptable is it being the only option, which is what it was.
|
||||
*/
|
||||
export function deriveFramingStyle(summary, rng) {
|
||||
const mode = rng.pickWeighted(['locked', 'gentle', 'edited'], [1, 2.5, 3]);
|
||||
return {
|
||||
mode,
|
||||
// How far from `normal` this track is willing to go.
|
||||
range: mode === 'locked' ? 0 : mode === 'gentle' ? 0.45 : 1,
|
||||
// Chance a cut also changes the shot size, rather than only the image.
|
||||
changeChance: mode === 'locked' ? 0 : mode === 'gentle' ? 0.35 : 0.6,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the framing for one shot.
|
||||
*
|
||||
* `previous` is the framing of the shot before it, and it is the whole point:
|
||||
* a size only means something relative to the size before it. The rule is that
|
||||
* a change of size must be a real change — a wide answered by a slightly less
|
||||
* wide is not a cut, it is a mistake — so sizes step rather than slide.
|
||||
*
|
||||
* @param {object} style from deriveFramingStyle
|
||||
* @param {object|null} previous the previous shot's framing
|
||||
* @param {number} energy section energy, 0..1
|
||||
* @param {Rng} rng
|
||||
*/
|
||||
export function frameShot(style, previous, energy, rng) {
|
||||
if (style.mode === 'locked') return neutralFraming();
|
||||
|
||||
const keep = previous && !rng.bool(style.changeChance);
|
||||
if (keep) return { ...previous };
|
||||
|
||||
// Loud material earns the close-ups; quiet material earns the wides. This
|
||||
// is a lean rather than a rule, so an intro can still land on a close and
|
||||
// read as intimate instead of empty.
|
||||
const weights = [
|
||||
1 + (1 - energy) * 2.5, // wide
|
||||
2, // normal
|
||||
1 + energy * 2.5, // close
|
||||
];
|
||||
let size = rng.pickWeighted(SHOT_SIZE_NAMES, weights);
|
||||
|
||||
// Never repeat the previous size when we have decided to change: repeating
|
||||
// it is what "no change" already means.
|
||||
if (previous && size === previous.size) {
|
||||
const others = SHOT_SIZE_NAMES.filter((n) => n !== size);
|
||||
size = rng.pick(others);
|
||||
}
|
||||
|
||||
const spec = SHOT_SIZES[size];
|
||||
// Scale toward 1 for a timid track, so `gentle` is genuinely gentle rather
|
||||
// than the same sizes drawn less often.
|
||||
const scale = 1 + (spec.scale - 1) * style.range;
|
||||
|
||||
// Recentring is what stops a close-up being a centre crop of the wide. Held
|
||||
// small: the scenes are centred compositions and pushing far off centre
|
||||
// finds their empty corners.
|
||||
const angle = rng.range(0, Math.PI * 2);
|
||||
const amount = spec.drift * style.range * rng.range(0.3, 1);
|
||||
|
||||
return {
|
||||
size,
|
||||
scale,
|
||||
shift: [Math.cos(angle) * amount, Math.sin(angle) * amount],
|
||||
};
|
||||
}
|
||||
|
||||
export function neutralFraming() {
|
||||
return { size: 'normal', scale: 1, shift: [0, 0] };
|
||||
}
|
||||
|
||||
/** One line for the HUD and check output. */
|
||||
export function describeFraming(style) {
|
||||
if (!style || style.mode === 'locked') return 'framing: locked off';
|
||||
return `framing: ${style.mode}`;
|
||||
}
|
||||
Reference in New Issue
Block a user