Skip to content

Instantly share code, notes, and snippets.

@queerviolet
Created December 22, 2020 17:35

Revisions

  1. queerviolet renamed this gist Dec 22, 2020. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. queerviolet created this gist Dec 22, 2020.
    149 changes: 149 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,149 @@
    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])
    )