Skip to content

Instantly share code, notes, and snippets.

@queerviolet
Created December 22, 2020 17:35
Show Gist options
  • Save queerviolet/14e14b3467ff807b32222d8deecaf931 to your computer and use it in GitHub Desktop.
Save queerviolet/14e14b3467ff807b32222d8deecaf931 to your computer and use it in GitHub Desktop.
letterbox.ts
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