cssreactjselectroncss-houdinicss-paint-api

Can't import css houdini paint js file


I have a React/Electron app, where I'm trying to use the new CSS Houdini paint() function (as demonstrated in this page). In my project's index.html file I have added a script tag with the paintWorklet addModule() function as shown:

<script>
  CSS.paintWorklet.addModule('../src/ResultDisplay/DefaultResultDisplay/testPaint.js');
</script>

Then in that testPaint.js file I have essentially a copy of what's shown in that blog post:

registerPaint(
  "testPaint",
  class {
    paint(ctx, geom) {
      console.log("painting!!!");
      const circleSize = 10;
      const bodyWidth = geom.width;
      const bodyHeight = geom.height;

      const maxX = Math.floor(bodyWidth / circleSize);
      const maxY = Math.floor(bodyHeight / circleSize);

      for (let y = 0; y < maxY; y++) {
        for (let x = 0; x < maxX; x++) {
          ctx.fillStyle = "blue";
          ctx.beginPath();
          ctx.arc(
            x * circleSize * 2 + circleSize,
            y * circleSize * 2 + circleSize,
            circleSize,
            0,
            2 * Math.PI,
            true
          );
          ctx.closePath();
          ctx.fill();
        }
      }
    }
  }
);

And finally my css file:

.container {
  background-image: paint(testPaint);
  display: flex;
  margin: 4px;
  border-radius: 12px;
  height: 75px;
}

I should point out I am using CSS Modules, so this file is defaultResultStyles.module.scss; not sure if that affects anything. When I bring up the component that's meant to have these styles in my app, it has no styles, though inspecting it, it does display background-image: paint(testPaint). The console.log that I added to the testPaint.js` file is never shown.

I have tried multiple variations of the filepath for addModule; I've tried just testPaint.js, starting it with ./src and src both, but nothing seems to work; is this possible in an Electron/React app?


Solution

  • The addModule function will not work through webpack or other bundlers, it instead works through the native module system of browsers. You have to put the testPaint.js file in the public directory, otherwise it would get bundled with everything else.

    Here's what I added to the index.html to get it to run from the public directory on a local Create React App project:

        <script>
          CSS.paintWorklet.addModule('%PUBLIC_URL%/testPaint.js');
        </script>
    

    I didn't actuallly set up any of the markup and just added the container class to test it out: enter image description here


    If you want to use this without going through the public folder (at all, because normally you'd have to add the paint module in the index.html which is changing the public directory), then I'd suggest using React Helmet to set up the script tags. As a note with this, the hot-reload features of CRA seems to prevent the scripts from updating, so every time you change the script tags, you'll need to refresh the page manually.

    import React from 'react';
    import { render } from 'react-dom';
    import { Helmet } from 'react-helmet';
    import styled from 'styled-components';
    
    // Render these styled components like normal react components.
    // They will pass on all props and work
    // like normal react components – except they're styled!
    const Demo = styled.div`
      background: #1108a0;
      padding: 50px 0;
    `;
    const Test = styled.div`
      --color: cyan;
      --multiplier: 0.24;
      --pad: 30;
      --slant: 20;
      background: paint(background-canvas);
      transition: --multiplier 0.4s;
      font: bold 6em sans-serif;
      color: yellow;
      text-shadow: 0 3px 1px cyan;
      line-height: 1.5em;
      width: max-content;
      padding-left: 30px;
      padding-right: 50px;
      isolation: isolate;
      &:hover {
        --multiplier: 1;
      }
      & span {
        mix-blend-mode: exclusion;
      }
    `;
    export const App = () => (
      <Demo>
        <Test className="el" right={'right'}>
          <span>JS-in-CSS</span>
        </Test>
      </Demo>
    );
    export const Helm = () => (
      <Helmet>
        <script language="javascript+paint">{`
      registerPaint('background-canvas', class {
      static get inputProperties() {
          return ['--multiplier', '--color', '--pad', '--slant'];
      }
      paint(ctx, geom, properties) {
        let multiplier = +properties.get('--multiplier').toString();
        let c = properties.get('--color').toString();
        let pad = +properties.get('--pad').toString();
        let slant = +properties.get('--slant').toString();
        ctx.moveTo(0, 0);
        ctx.lineTo(pad + (geom.width - slant - pad) * multiplier, 0);
        ctx.lineTo(pad + (geom.width - slant - pad) * multiplier + slant, geom.height);
        ctx.lineTo(0, geom.height);
        ctx.fillStyle = c;
        ctx.fill();
      }
    })
    `}</script>
    
        <script>{`
      if ("paintWorklet" in CSS) {
      const src = document.querySelector('script[language$="paint"]').innerHTML;
      const blob = new Blob([src], {
        type: 'text/javascript'
      });
      CSS.paintWorklet.addModule(URL.createObjectURL(blob));
    }
    `}</script>
      </Helmet>
    );
    render(
      <div>
        <Helm />
        <App />
      </div>,
      document.getElementById('root')
    );