CSS Houdini C’est plus que de la magie !

Vincent De Oliveira · @iamvdo

Allô, c’est @iamvdo

Screenshot of extensiblewebmanifesto.org

Pour CSS ?

Créer des polyfills CSS

Il est aujourd’hui difficile impossible de polyfiller CSS, même avec JS.

https://philipwalton.com/articles/the-dark-side-of-polyfilling-css/

Créer ses propres effets graphiques

Créer ses propres extensions au langage

En un mot

Innover

Bienvenue Houdini!

Rendu navigateur*

JS HTML / CSS Parsing Parsing DOM DOM CSSOM CSSOM Typed OM Render Tree Render Tree Layout Layout Paint Paint Composite Composite Rendu écran

* Plus ou moins (en fonction des navigateurs)

Nombreuses APIs

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API

Houdini c’est du
JS-in-CSS

Ou plutôt du CSS-by-JS

Peut-on utiliser Houdini ?

Amélioration progressive FTW

Attention, peinture fraiche !

Tout ce qui est présenté ici peut ne plus fonctionner du jour au lendemain ¯\_(ツ)_/¯

CSS Typed OM

CSSOM++

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API
  • API pour interagir avec CSS
  • Remplaçant de CSSOM
  • Fini les concaténations hasardeuses
  • Optimisations des perfs par le navigateur
  • Parsing simplifié
// CSSOM
el.style.width = '50px'
el.style.setProperty('width', '50px')
el.style.setProperty('transform', 'translate(' + x + 'px, ' + y + 'px)')
// Typed OM
el.attributeStyleMap.set('width', '50px')
el.attributeStyleMap.set('width', CSS.px(50))
el.attributeStyleMap.set('transform', new CSSTranslate(CSS.px(x), CSS.px(y)))

el.computedStyleMap().get('width') // CSSUnitValue {value: 50, unit: 'px'}

let [x, y] = [10, 10];
let transform = new CSSTranslate(CSS.px(x), CSS.px(y))
transform.x.value = 50
transform.toString() // "translate(50px, 10px)"
// Parse CSS
let css = CSSNumericValue.parse('42.0px') // {value: 42, unit: 'px'}

let css = CSSStyleValue.parse('transform', 'translate3d(10px,10px,0) scale(0.5)');
/*
CSSTranslateValue {
  0: CSSTranslate {
    is2D: false
    x: CSSUnitValue {value: 10, unit: 'px'}
    y: CSSUnitValue {value: 10, unit: 'px'}
    z: CSSUnitValue {value: 10, unit: 'px'}
  }
  1: CSSScale {
    is2D: true
    x: CSSUnitValue {value: 0.5, unit: 'number'}
    y: CSSUnitValue {value: 0.5, unit: 'number'}
    z: CSSUnitValue {value: 1, unit: 'number'}
  }
  is2D: false
}
*/

Typed OM == Le socle pour Houdini !

CSS Custom Properties

Pas spécifique à Houdini

.el {
  box-shadow: 0 3px 3px rgba(0,0,0,.75);
}
.el:hover {
  box-shadow: 0 15px 10px rgba(0,0,0,.75);
}
/* Étendre CSS: propriétés raccourcies */
.el {
  box-shadow: var(--box-shadow-x, 0) var(--box-shadow-y, 3px)
              var(--box-shadow-blur, 3px)
              var(--box-shadow-color, rgba(0,0,0,.75));
}
.el:hover {
  --box-shadow-y: 15px;
  --box-shadow-blur: 10px;
}
HELLO CSS!
.el {
  text-shadow: 
    var(--x, 0)
    var(--y, 3px)
    var(--blur, 3px)
    var(--color, rgba(0,0,0,.75));
}
// Modifier en JS
el.addEventListener('mousemove', e => {
  el.attributeStyleMap.set('--x', e.offsetX)
  el.attributeStyleMap.set('--y', e.offsetY)
  el.attributeStyleMap.set('--blur', blur)
})

CSS Custom Properties ++

Properties & Values API

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API
// Déclarer notre propre propriété animable
CSS.registerProperty({
  name: '--box-shadow-blur',
  syntax: '<length>',
  inherits: false,
  initialValue: '0px'
})
.el {
  transition-property: --box-shadow-blur, --box-shadow-y;
  transition-duration: .45s;
}
.el:hover {
  --box-shadow-y: 15px;
  --box-shadow-blur: 10px;
}

Créatif Animer un dégradé

Worklets

  • Environnement d’éxécution isolé
  • Similaire aux Web Workers
  • Léger, réutilisable, jetable
  • Uniquement HTTPS

Paint API

Worklet

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API
  • Dessiner un canvas en arrière-plan
  • Automatiquement redessiné, redimensionné
  • Hors du thread principal
  • Pas besoin de DOM supplémentaire
  • CSS.paintWorklet.addModule('paint.js')
  • Worklet: registerPaint()
  • CSS: paint()
CSS!
registerPaint('circle', class {
  paint(ctx, geom, props, args) {
    // Get the center point and radius
    const x = geom.width / 2;
    const y = geom.height / 2;
    const radius = Math.min(x, y);

    // Draw the circle
    ctx.fillStyle = 'deeppink';
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI);
    ctx.fill();
  }
}
.el {
  background-image: paint(circle);
}
CSS!
registerPaint('circle-props', class {
  static get inputProperties() { return ['--circle-color']; }
  paint(ctx, geom, props, args) {
    // Determine the center point and radius.
    const x = geom.width / 2;
    const y = geom.height / 2;
    const radius = Math.min(x, y);

    // Draw the circle
    ctx.fillStyle = props.get('--circle-color').value;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI);
    ctx.fill();
  }
}
.el {
  --circle-color: deepskyblue;
  background-image: paint(circle-props);
}
Click!
registerPaint('circle-ripple', class {
  static get inputProperties() { return [ '--circle-color',
    '--circle-radius', '--circle-x', '--circle-y'
  ]}
  paint(ctx, geom, props, args) {
    const x = props.get('--circle-x').value;
    const y = props.get('--circle-y').value;
    const radius = props.get('--circle-radius').value;
  }
}el.addEventListener('click', e => {
  el.classList.add('animating');
  el.attributeStyleMap.set('--circle-x', e.offsetX);
  el.attributeStyleMap.set('--circle-y', e.offsetY);
});
.el {
  --circle-radius: 0;
  --circle-color: deepskyblue;
  background-image: paint(circle-ripple);
}
.el.animating {
  transition: --circle-radius 1s,
              --circle-color 1s;
  --circle-radius: 300;
  --circle-color: transparent;
}

Créatif Dessin artistique

Créatif Arrière-plans Dégradé des coins

Polyfill Arrière-plans corner-shape

Polyfill Créatif Arrière-plans background-filter / opacity / rotate

Créatif Arrière-plans Surligneur

Créatif Bordures Flèche d’infobulle

Créatif Bordures Bordures dessinées

Worklet réutilisable et jetable ?

Légèrement contournable: graine aléatoire (seed random)

Créatif Masques Bulles aléatoires

Créatif Masques Grille irrégulière

Créatif 🤯 JS-in-CSS

Layout API

Worklet

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API
  • Créer son propre layout
  • À la mode Flexbox / Grid
  • Seulement pour les enfants
  • Hors du thread principal*
  • Très complexe
  • CSS.layoutWorklet.addModule('layout.js')
  • Worklet: registerLayout()
  • CSS: layout()
CSS!
registerLayout('center', class {
  *layout(children, edges, constraintSpace, props) {
    let childFragments = [];
    for(let child of children) {
      let childFragment = yield child.layoutNextFragment();
      let childHalfSize = childFragment.inlineSize / 2;
      let parentHalfSize = constraintSpace.fixedInlineSize / 2;
      childFragment.inlineOffset = parentHalfSize - childHalfSize;
      childFragments.push(childFragment);
    }
    return { childFragments };
  }
});
.parent {
  display: layout(center);
}
CSS!
registerLayout('position', class {
  static get inputProperties() { return ['--position-x']; }
  *layout(children, edges, constraintSpace, props) {
    let posx = props.get('--position-x').value;
    let pos = constraintSpace.fixedInlineSize / (100 / posx);
  }
});
.parent {
  display: layout(position);
  --position-x: 50%;
}
Click outside!
registerLayout('position', class {
  static get inputProperties() { }
  *layout(children, edges, constraintSpace, props) { }
});parent.addEventListener('click', e => {
  parent.classList.add('animating');
  let pos = 100 * e.offsetX / parentWidth;
  parent.attributeStyleMap.set('--position-x', CSS.percent(pos));
});
.parent {
  display: layout(position);
  --position-x: 50%;
}
.parent.animating {
  transition: --position-x 1s;
}

Créatif Masonry

Polyfill Android RelativeLayout

Créatif SVG Path

Animation API

Worklet

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API
  • Animations hors du thread principal
  • Basée sur l’API Web Animations
  • Timeline de temps OU au scroll
  • Surtout pour la performance
  • Que du JavaScript
  • CSS.animationWorklet.addModule('anim.js')
  • registerAnimator()
  • new WorkletAnimation()

Basé sur une idée de Scott Kellum

registerAnimator('simple', class {
  animate(currentTime, effect) {
    effect.localTime = currentTime;
  }
});.cube {
  --angle: 0;
  transform: rotateX(var(--angle)) rotateZ(45deg) rotateY(-45deg);
}
new WorkletAnimation('simple',
  new KeyframeEffect(el, [
      { '--angle': 0 },
      { '--angle': '1turn' }
    ],
    { duration: 1 }
  ),
  new ScrollTimeline({
    scrollSource: scrollElement,
    timeRange: 1
  }),
).play();
Scroll!
Scroll!
Scroll!
registerAnimator('parallax', class {
  constructor(options = {}) {
    this.factor = options.factor || 1;
  }
  animate(currentTime, effect) {
    effect.localTime = currentTime * this.factor;
  }
});
for (let i = 0; i < els.length; i++) {
  new WorkletAnimation('parallax',
    new KeyframeEffect(els[i], [
        { transform: new CSSTranslate(0, 0) },
        { transform: new CSSTranslate(0, CSS.px(scrollHeight)) }
      ], { duration: 1 }
    ),
    new ScrollTimeline({
      scrollSource: scrollElement,
      timeRange: 1
    }),
    { factor: (i / 2) * (1.2 - 1) + 1 }
  ).play();
}

CSS Parser, Render Tree, Font Metrics

Le futur ?
CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API

Tout devient possible

Enfin, peut-être…

Questions

Nouvelles possibilités offertes ?

  • Augmente la créativité
  • Tout en JavaScript
  • Plutôt complexe à mettre en oeuvre

Performance ?

  • Gain de perf, surtout animations
  • Pas toujours vrai (actuellement)

Standardisation ?

  • Plus besoin d’attendre les standards
  • Tout doit être créé depuis zéro (prédiction: écosystème hétérogène)
  • Évolution possible des standards

Polyfill ?

  • Plus performant, plus facilement
  • Vite limité (actuellement ?)
  • Pleins de choses impossibles: element(), backdrop-filter, CSS Shaders 😍, etc.

Innovation des moteurs de rendu ?

  • Optimisation des performances par le navigateur
  • Houdini basé sur un consensus de rendu
  • Firefox et son nouveau moteur WebRender

Sécurité ?

  • Complexe. Ex: paint() pas sur les liens
  • Seulement HTTPS
  • Même sort que les CSS Shaders ?

Merci !

https://slides.iamvdo.me/waq19

https://css-houdini.rocks

Vincent De Oliveira · @iamvdo · iamvdo.me

@iamvdo EN Version