Magic Tricks with Houdini

What is Houdini?!

The objective of the CSS-TAG Houdini Task Force (CSS Houdini) is to jointly develop features that explain the “magic” of Styling and Layout on the web.

Magic

Practically, though, what does that mean?

Extending CSS via JS

so authors no longer have to wait a decade for standards bodies and browsers to do something new

But Wait! Can't We Do This Already!

Not Quite

  • It's currently not possible to extend CSS with JS, only write JS that mimics CSS
  • Actually polyfilling CSS, or introducing new features (like CSS Grids), is hard-to-impossible to do. Doubly so in a way that's not terrible for performance.
  • Houdini will let authors tap in to the actual CSS engine, finally allowing them to extend CSS, and do so at CSS Engine speeds

Much like Service Workers are a low-level JavaScript API for the browser's cache Houdini introduces low-level JavaScript APIs for the browser's render engine

That's Cool

Nope

This is very early days work in progress, nothing more. No compatible implementations exist. What little actually works only really does so in Chrome Canary. This deck will likely break if not viewed there. Syntax and semantics are likely to change. In fact, in the course of a year, one API has gone through 4ish incompatible API changes. Some of the examples provided are speculative based on previous and current working implementations and may not reflect final syntax. Terms and conditions apply. Not redeemable for cash. Your mileage may vary.

Worklets

Worklets are extension points for rendering engines

They're like Web Workers, but with a much smaller scope, can be parallelized, live on multiple threads, and most importantly, get called by the render engine, not us

// From https://drafts.css-houdini.org/worklets/ September 1, 2017 Editor's Draft

// From inside the browser's context
// Script gets loaded in the main thread, then sources are sent to Worklet threads
// Same script can be loaded in to multiple Worklet threads
window.demoWorklet.addModule('path/to/script.js');
// From https://drafts.css-houdini.org/worklets/ September 1, 2017 Editor's Draft

// worklet.addModule returns a Promise!
// Sometimes post-register work is done with loaded worklets, this makes it possible
Promise.all([
  window.demoWorklet1.addModule('path/to/script1.js'),
  window.demoWorklet2.addModule('path/to/script2.js'),
]).then(worklets => {
  // Worklets have loaded and can be worked on!
});
// From https://drafts.css-houdini.org/worklets/ September 1, 2017 Editor's Draft

// The kind of worklet it is
registerDemoWorklet('name', class { // The name we'll call this worklet

  // Each worklet can define different functions to be used
  // These will be called by the render engine as needed
  process(arg) {
    // Stuff happens in here! What happens depends on the worklet
    // Sometimes it'll return something
    // Other times it'll work directly on the arguments
    return !arg;
  }
});
Artboard 1 worklet.addModule process,

Worklets are the underlying foundation to which all of Houdini is based. They're the magic that makes it happen. They're Houdini's Secret Sauce

Secret Sauce

Typed OM

Typed OM exposes structure, beyond simple strings, for CSS Values. These then can be manipulated and retrieved in a more performant manner, and are part of the new CSSStyleValue class.

  • CSSKeywordValue - CSS Keywords and other identifiers (like inherit)
  • CSSPositionValue - Position (x and y) values
  • CSSTransformValue - A list of CSS transforms consisting of CSSTransformComponent including CSSTranslation, CSSRotation, CSSRotation, CSSScale, and CSSSkew
  • CSSUnitValue - Numeric values that can be expressed as a single unit (or a naked number or percentage)
  • CSSMathValue - and its subclasses CSSMathSum, CSSMathProduct, CSSMathMin, CSSMathMax, CSSMathNegate, and CSSMathInvert. Complicated numeric values (like calc, min, max, etc…)
.example {
  background-position: center bottom 10px;
}
// From https://drafts.css-houdini.org/css-typed-om/ March 7, 2018 Editor Draft

let map = document.querySelector('.example').computedStyleMap();

map.get('background-position').x;
// CSSUnitValue { value: 50, unit: 'percent' }

map.get('background-position').y;
// CSSMathSum {
//   operator: 'sum',
//   values: [ // CSSNumericArray
//     { value: -10, unit: 'px' }, // CSSUnitValue
//     { value: 100, unit: 'percent' }, // CSSUnitValue
//   ]
// }

The Typed OM is the glue to meaningfully connect our CSS and our worklets

Krazy Glue

Yah, But What Can I DO With This?

Rad

The Cool Custom Stuff

Please allow me to introduce you to ...

window.CSS

Custom Properties

Make Snozzberries Taste Like Snozzberries

Current Situation

.thing {
  --my-color: green;
  --my-color: url('not-a-color'); // It's just a variable! It doesn't know any better
  color: var(--my-color);
}

But Then

window.CSS.registerProperty({
  name: '--my-color',
  syntax: '<color>', // Now it's def a color. That `url` junk is skipped!
});
Structure of a Registered Property
// From https://drafts.css-houdini.org/css-properties-values-api/ July 19, 2017 Editor's Draft

window.CSS.registerProperty({
  name: '--foo', // String, name of the custom property
  syntax: '<color>', // String, how to parse this property. Defaults to *
  inherits: false, // Boolean, if true should inherit down the DOM tree
  initialValue: 'black', // String, initial value of this property
})

The following are valid types for syntax

  • <length> - Any valid length value
  • <number> - Number values
  • <percentage> - Any valid percentage
  • <length-percentage> - Any valid length or percentage value, any valid calc() expression combining length and percentage
  • <color> - Any valid color value
  • <image> - Any valid image value
  • <url> - Any valid url value
  • <integer> - Any valid integer value
  • <angle> - Any valid angle value
  • <time> - Any valid time value
  • <resolution> - Any valid resolution value
  • <transform-list> - A list of valid transform-function values
  • <custom-ident> - Any valid custom-ident value

syntax also allows for combiners

  • <length> - A single length value
  • <image> | <url> - Accepts a single image or a single URL
  • big | bigger | BIGGER - Accepts the ident "big", the ident "bigger", or the ident "BIGGER"
  • <length>+ - Accepts a list of length values

Paint API

Pain, in CSS, but, like, for real

  • Ever wanted to use canvas* as a background, a mask, or a border in CSS?
  • With the styling flexibility of an element?
  • And the scalability of SVG?

That's what the Paint API does

*2D drawing without access to window

paintWorklet Class
// From https://drafts.css-houdini.org/css-paint-api/ January 28, 2018 Editor's Draft

class myPaint {
  // Input properties from element to look for
  static get inputProperties() { return ['--foo']; }
  // Input arguments that can be passed to `paint` function
  static get inputArguments() { return ['<color>']; }
  // Alpha allowed?
  static get alpha() { return true; }

  paint(ctx, size, props, args) {
    // ctx - drawing context
    // size - size of the box being painted
    // props - inputProperties
    // args - array of passed-in arguments

    // Paint code goes here.
  }
}

Define - js/circle.js

registerPaint('circle', class {
    static get inputProperties() { return ['--circle-color']; }
    paint(ctx, size, props) {
      // Change the fill color.
      const circle = props.get('--circle-color'); // This is a CSSStyleValue!

      // Determine the center point and radius.
      const xCircle = size.width / 2;
      const yCircle = size.height / 2;
      const radiusCircle = Math.min(xCircle, yCircle) - 2.5;

      // Draw the circle \o/
      ctx.beginPath();
      ctx.arc(xCircle, yCircle, radiusCircle, 0, 2 * Math.PI);
      ctx.fillStyle = circle;
      ctx.fill();
    }
});

Import

window.CSS.paintWorklet.addModule('js/circle.js');

Now things get a little fuzzy

Animation API

Yo Dawg, I heard you like parallax

  • Listen for user input, like scroll events!
  • Style elements, based on that user input
  • Do it all off of the main thread!

Animation API: Make Parallax Perform

animationWorklet Class
// https://wicg.github.io/animation-worklet/ August 1, 2017 Draft Community Group Report

class myAnimator {
  constructor(options) {
    // Called when a new animator is instantiated
    // Used to set stuff up for each use of an animator
  }
  animate(currentTime, effect) {
    // currentTime - The current time from the defined timeline
    // effect - Group of effects that this animation is working on

    // Animation frame logic goes here.
  }
}
// From https://wicg.github.io/animation-worklet/ August 1, 2017 Draft Community Group Report

registerAnimator('twitter-header', class {
  // Called when new animator is instantiated
  constructor(options) {
    this.timing_ = new CubicBezier('ease-out');
  }

  // Internal function to perform effect
  clamp(value, min, max) {
    return Math.min(Math.max(value, min), max);
  }

  // Animation Frame Logic
  animate(currentTime, effect) {
    const scroll = currentTime; // scroll is in [0, 1] range

    // Drive the output group effect by setting its children local times
    effect.children[0].localTime = scroll;
    effect.children[1].localTime = this.timing_(clamp(scroll, 0, .5));
  }
});
window.animationWorklet.addModule('twitter-header.js').then(_ => {
  const workletAnim = new WorkletAnimation('twitter-header'),
    // Worklet `effects`
    [
      new KeyFrameEffect( // effect.children[0] in worklet
        $avatar, // Element to work on
        // Scales down as we scroll up
        [ { transform: 'scale(1)' }, { transform: 'scale(0.5)' } ], // Keyframes
        { duration: 1, iterations: 1 }, // Only want one frame per timeline tick
      ),
      new KeyFrameEffect( // effect.children[1] in worklet
        $header,
        // Loses transparency as we scroll up
        [ { opacity: 0 }, { opacity: 0.8 } ],
        { duration: 1, iterations: 1 },
      ),
    ],
    // Worklet `curretTime` timeline
    // New! Defines animation timeline whose time value depends on scroll position of a scroll container
    // Work still being done for other input types, like touch and pointer input
    new ScrollTimeline($scrollingContainer, {
      timeRange: 1,
      startScrollOffset: 0,
      endScrollOffset: $header.clientHeight,
    },
  );
});

LAYOUT API

Turn Layout In To Tetris

  • Literally, make your own display properties
  • Polyfill that awesome new layout spec you love!
  • Everyone likes a good Masonry layout, add one without a performance hit!

this spec is crazy complicated and I don't quite understand the whole thing yet

Overview of layout terminology
layoutWorklet Class
// From https://drafts.css-houdini.org/css-layout-api/ August 29, 2017 Collection of Interesting Ideas
// Intrinsic Sizes https://www.w3.org/TR/css-sizing-3/

class myLayout {
  // Properties to look for on calling element
  static get inputProperties() { return ['--foo']; }
  // Properties to look for on direct child elements
  static get childrenInputProperties() { return ['--bar']; }
  // How children are displayed; either "block" to blockify like Flexbox and Grid
  // or "normal" to let elements dictate
  static get childDisplay() { return 'normal'; }

  // Generator functions instead of normal functions to support async/parallel layout engines
  // Determines how a box fits its content or fits in to our layout context
  *intrinsicSizes(children, styleMap) {
    // children - Child elements of box being laid out
    // styleMap - Typed OM style map of box being laid out

    // Intrinsic sizes code goes here.
  }

  *layout(space, children, styleMap, edges, breakToken) {
    // space - `ConstraintSpace` for the box being laid out
    // children - Child elements of the box being laid out
    // styleMap - Typed OM style map of box being laid out
    // edges - `LayoutEdges` of box being laid out
    // breakToken - Token (if paginating for printing for example) to resume layout at

    // Layout code goes here.
  }
}
// From https://drafts.css-houdini.org/css-layout-api/#example-13a91ee5 August 29, 2017 Collection of Interesting Ideas

registerLayout('centered-stacked', class {
  *intrinsicSizes(children, styleMap) {
    // Get all the sizes!
    const childrenSizes = yield children.map((child) => {
      return child.intrinsicSizes();
    });

    // How large the box can be given unlimited space
    //  in order to fit its content with minimum unused
    //  space
    const maxContentSize = childrenSizes.reduce((max, childSizes) => {
      return Math.max(max, childSizes.maxContentContribution);
    }, 0);

    // How small the box can be so that its content
    //  doesn't overflow
    const minContentSize = childrenSizes.reduce((max, childSizes) => {
      return Math.max(max, childSizes.minContentContribution);
    }, 0);

    return {maxContentSize, minContentSize};
  }

  // ...
  // ...
  *layout(space, children, styleMap, edges) {
    const inlineSize = resolveInlineSize(space, styleMap);

    // Get content area inside edges
    const availableInlineSize = inlineSize - edges.all.inline;
    const availableBlockSize = resolveBlockSize(space, styleMap) - edges.all.block;

    const childConstraintSpace = new ConstraintSpace({
      inlineSize: availableInlineSize,
      blockSize: availableBlockSize,
    });

    // Build fragments inside the content area
    const childFragments = yield children.map((child) => {
      return child.layoutNextFragment(childConstraintSpace);
    });

    // ...
    // ...
    // Start counting block positioning from the start of block edges
    let blockOffset = edges.all.blockStart;
    for (let fragment of childFragments) {
      // Set fragment's block offset
      fragment.blockOffset = blockOffset;
      // Center the block inline
      fragment.inlineOffset = Math.max(
        edges.all.inlineStart,
        (availableInlineSize - fragment.inlineSize) / 2
      );

      // Add the fragment's block size to the offset to set the next below this one
      blockOffset += fragment.blockSize;
    }
    // ...
    // ...
    // Close off block size by adding edges
    const autoBlockSize = blockOffset + edges.all.blockEnd;
    // Resolve total block size
    const blockSize = resolveBlockSize(
      constraintSpace,
      styleMap,
      autoBlockSize
    );

    // Return the element's inlineSize, new blockSize, and child fragments
    return {
      inlineSize: inlineSize,
      blockSize: blockSize,
      childFragments: childFragments,
    };
  }
});

Representation of final block-like layout

Here come the fireworks, a working masonry layout using Layout API

The Future Is Bright
Get Excited
Magic is coming to the browser

👍

http://snugug.github.io/magic-tricks-with-houdini