Created
December 22, 2020 17:35
-
-
Save queerviolet/14e14b3467ff807b32222d8deecaf931 to your computer and use it in GitHub Desktop.
letterbox.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Ratio = [number, number] | |
type Size = {width: number, height: number} | |
type Position = {top: number, left: number} | |
type Frame = Position & {bottom: number, right: number} | |
export type Box = Frame & Size | |
export type Letterbox = Box & { bars: Box[], su: number } | |
/** | |
* Compute a letterbox and apply its measurements as CSS properties to document.body. | |
* | |
* This does not create any DOM elements or crop the page, but gives you the tools to do so in CSS. | |
* | |
* Applies the following CSS vars and keeps them up to date (the actual numbers here are just an example): | |
* | |
* --letterbox-top:26.3125px; | |
* --letterbox-left:0px; | |
* --letterbox-width:1590px; | |
* --letterbox-height:894.375px; | |
* --letterbox-bottom:26.3125px; | |
* --letterbox-right:0px | |
* --letterbox-bars-0-top:0px | |
* --letterbox-bars-0-left:0px | |
* --letterbox-bars-0-width:1590px | |
* --letterbox-bars-0-height:26.3125px | |
* --letterbox-bars-0-bottom:920.688px | |
* --letterbox-bars-0-right:0px | |
* --letterbox-bars-1-top:920.688px | |
* --letterbox-bars-1-left:0px | |
* --letterbox-bars-1-width:1590px | |
* --letterbox-bars-1-height:26.3125px | |
* --letterbox-bars-1-bottom:0px | |
* --letterbox-bars-1-right:0px | |
* --su:99.375px; | |
* | |
* `--letterbox-{top, left, width, height, bottom, right}` describes the stage box, where drawing should occur. | |
* | |
* `--letterbox-bars-{0,1}-{top, left, width, height, bottom, right}` describes the dead space (the "bars"), which | |
* may be at the top and bottom (for landscape letterboxing) or left and right (for portait) of the stage. You might | |
* use these measurements to position masks which sit over the page and decisively block any overdraw. The bars will | |
* always be specified, but they might be zero width or height (if the page's aspect is exactly the letterbox aspect). | |
* | |
* `--su` is a new unit, "stage units", derived from the aspect ratio. Specifying an aspect of `[16, 9]` | |
* results in `--su` being defined as `stageWidth / 16` (or as `stageHeight / 9`—they are the same number | |
* by definition). | |
* | |
* `--su` can be used in CSS via `calc()`, e.g. `left: calc(var(--su) * 2)` | |
* | |
* @param aspect width and height in stage units | |
* @param onReshape called with new bounds whenever the stage is reshaped | |
* @returns a destroy function | |
*/ | |
export default function applyLetterbox(aspect: Ratio = [16, 9], onReshape: (box: Box) => void = None) { | |
function onResize() { | |
const box = | |
letterbox(aspect, {width: innerWidth, height: innerHeight}) | |
setCSSPropertiesFrom(box) | |
onReshape(box) | |
} | |
window.addEventListener('resize', onResize) | |
onResize() | |
return () => window.removeEventListener('resize', onResize) | |
} | |
export function letterbox(ratio: Ratio, container: Size): Letterbox { | |
const [w, h] = ratio | |
const aspect = w / h | |
const containerAspect = container.width / container.height | |
if (containerAspect > aspect) { | |
// Container is flatter than content, lock to container | |
// height and letterbox on left and right | |
const width = aspect * container.height | |
const left = (container.width - width) / 2 | |
const height = container.height | |
const top = 0 | |
return letterboxFrom(ratio, container, { | |
top, | |
left, | |
width, | |
height, | |
}) | |
} | |
// Container is taller than content, lock to container | |
// width and letterbox on top and bottom | |
const height = container.width / aspect | |
const top = (container.height - height) / 2 | |
const width = container.width | |
const left = 0 | |
return letterboxFrom(ratio, container, {top, left, width, height}) | |
} | |
const letterboxFrom = (ratio: Ratio, container: Size, box: Size & Position): Letterbox => ({ | |
...boxFrom(container, box), | |
bars: subtract(container, box), | |
su: box.width / ratio[0], | |
}) | |
const boxFrom = (container: Size, box: Size & Position): Box => ({ | |
...box, | |
bottom: container.height - (box.top + box.height), | |
right: container.width - (box.left + box.width), | |
}) | |
const subtract = (container: Size, box: Size & Position): Box[] => { | |
if (!box.top) { | |
const width = (container.width - box.width) / 2 | |
return [ | |
boxFrom(container, { | |
top: 0, | |
left: 0, | |
width, | |
height: container.height | |
}), | |
boxFrom(container, { | |
top: 0, | |
left: container.width - width, | |
width, | |
height: container.height | |
}), | |
] | |
} | |
const height = (container.height - box.height) / 2 | |
return [ | |
boxFrom(container, { | |
top: 0, left: 0, | |
width: container.width, | |
height, | |
}), | |
boxFrom(container, { | |
top: container.height - height, | |
left: 0, | |
width: container.width, | |
height, | |
}) | |
] | |
} | |
const px = (px: number) => `${px}px` | |
const None = () => {} | |
const setCSSPropertiesFrom = (src: any, prefix='--letterbox-', element=document.body) => | |
Object.keys(src).forEach(k => | |
typeof src[k] === 'object' | |
? setCSSPropertiesFrom(src[k], prefix + k + '-', element) | |
: | |
typeof src[k] === 'number' | |
? element.style.setProperty(k !== 'su' ? prefix + k : `--${k}`, px(src[k])) | |
: | |
element.style.setProperty(prefix + k, src[k]) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment