본문 바로가기
css

사용자 지정 속성이 있는 CSS 페인트 API의 선호 색상 스키마

by code-box 2021. 12. 29.
One of the coolest things I’ve been messing with in the last couple years is the CSS Paint API. I love it. I did a talk on it, and made a little gallery of my own paint worklets. The other cool thing is the `prefers-color-scheme` media query and how you can use it to adapt to a user’s preference for light or dark modes.

최근에, 저는 페인트 작업대의 외관을 사용자가 선호하는 색상표에 맞게 조정할 수 있도록 이 두 가지 멋진 것들을 CSS 맞춤 특성과 결합할 수 있다는 것을 알게 되었습니다!

스테이지 설정

I’ve been overdue for a website overhaul, and I decided to go with a Final Fantasy II theme. My first order of business was to make a paint worklet that was a randomly generated Final Fantasy-style landscape I named `overworld.js`:

 

옷을 좀 더 차려입어야 할 것 같은데, 그건 분명 안건이야. 하지만 이건 아주 좋은 시작이야!

페인트 워크렛을 마친 후, 저는 계속해서 밝고 어두운 모드를 위한 테마 스위처와 같은 웹사이트의 다른 부분에서 작업을 진행했습니다. 그때 저는 페인트 작업대가 이러한 선호도에 적응하지 못하고 있다는 것을 깨달았습니다. 이것은 보통 큰 고통일 수 있지만, 저는 CSS 사용자 지정 속성을 통해 페인트 작업자의 렌더링 논리를 사용자가 선호하는 색상 체계에 비교적 쉽게 적용할 수 있다는 것을 깨달았습니다!

페인트 워크렛에 대한 사용자 정의 특성 설정

요즘 CSS의 상태는 꽤 괜찮고, CSS 커스텀 속성은 앞서 언급한 dopeness의 한 예입니다. 그림판 API 및 사용자 지정 특성 기능이 모두 지원되는지 확인하려면 다음과 같이 약간의 기능 검사를 수행합니다.

const paintAPISupported = "registerProperty" in window.CSS && "paintWorklet" in window.CSS`
 
const paintAPISupported = "registerProperty" in window.CSS && "paintWorklet" in window.CSS`
The first step is to define your custom properties, which involves the `CSS.registerProperty` method. That looks something like this:
CSS.registerProperty({
    name,             // The name of the property
    syntax,           // The syntax (e.g., <number>, <color>, etc.)
    inherits,         // Whether the value can be inherited by other properties
    initialValue      // The default value
});
CSS.registerProperty({
    name,             // The name of the property
    syntax,           // The syntax (e.g., <number>, <color>, etc.)
    inherits,         // Whether the value can be inherited by other properties
    initialValue      // The default value
});

사용자 지정 속성은 CSS에 지정되지만 페인트 워크렛 컨텍스트에서 읽을 수 있으므로 그림판 API 사용의 가장 좋은 부분입니다. 이를 통해 개발자는 페인트 워크렛을 CSS로 렌더링하는 방법을 제어할 수 있습니다.

 
For the `overworld.js` paint worklet, the custom properties are used to define the colors for various parts of the randomly generated landscape—the grass and trees, the river, the river banks, and so on. Those color defaults are for the light mode color scheme.
The way I register these properties is to set up everything in an object that I call with `Object.entries` and then loop over the entries. In the case of my `overworld.js` paint worklet, that looked like this:
// Specify the paint worklet's custom properties
const properties = {
    "--overworld-grass-green-color": {
          syntax: "<color>",
          initialValue: "#58ab1d"
    },
    "--overworld-dark-rock-color": {
          syntax: "<color>",
          initialValue: "#a15d14"
    },
    "--overworld-light-rock-color": {
          syntax: "<color>",
          initialValue: "#eba640"
    },
    "--overworld-river-blue-color": {
          syntax: "<color>",
          initialValue: "#75b9fd"
    },
    "--overworld-light-river-blue-color": {
          syntax: "<color>",
          initialValue: "#c8e3fe"
    }
};

// Register the properties
Object.entries(properties).forEach(([name, { syntax, initialValue }]) => {
    CSS.registerProperty({
          name,
          syntax,
          inherits: false,
          initialValue
    });
});

// Register the paint worklet
CSS.paintWorklet.addModule("/worklets/overworld.js");
// Specify the paint worklet's custom properties
const properties = {
    "--overworld-grass-green-color": {
          syntax: "<color>",
          initialValue: "#58ab1d"
    },
    "--overworld-dark-rock-color": {
          syntax: "<color>",
          initialValue: "#a15d14"
    },
    "--overworld-light-rock-color": {
          syntax: "<color>",
          initialValue: "#eba640"
    },
    "--overworld-river-blue-color": {
          syntax: "<color>",
          initialValue: "#75b9fd"
    },
    "--overworld-light-river-blue-color": {
          syntax: "<color>",
          initialValue: "#c8e3fe"
    }
};

// Register the properties
Object.entries(properties).forEach(([name, { syntax, initialValue }]) => {
    CSS.registerProperty({
          name,
          syntax,
          inherits: false,
          initialValue
    });
});

// Register the paint worklet
CSS.paintWorklet.addModule("/worklets/overworld.js");

모든 특성은 초기 값을 설정하므로 나중에 페인트 워크렛을 호출할 때 사용자 지정 특성을 지정할 필요가 없습니다. 그러나 이러한 특성의 기본값은 재지정될 수 있으므로 사용자가 색상표에 대한 기본 설정을 나타낼 때 조정할 수 있습니다.

 

사용자가 선호하는 색상표에 적응

작업 중인 웹 사이트 새로 고침에는 사이트의 기본 탐색에서 액세스할 수 있는 설정 메뉴가 있습니다. 여기서 사용자는 원하는 색상표를 포함하여 다양한 환경설정을 조정할 수 있습니다.

색상표 설정은 다음 세 가지 옵션으로 순환됩니다.

  • 시스템
  • 어둡다
“System” defaults to whatever the user has specified in their operating system’s settings. The last two options override the user’s operating system-level setting by setting a `light` or `dark` class on the `<html>` element, but in the absence of an explicit, the “System” setting relies on whatever is specified in the `prefers-color-scheme` media queries.
 

이 재정의의 힌지는 CSS 변수에 따라 달라진다.

/* Kicks in if the user's site-level setting is dark mode */
html.dark { 
    /* (I'm so good at naming colors) */
    --pink: #cb86fc;
    --firion-red: #bb4135;
    --firion-blue: #5357fb;
    --grass-green: #3a6b1a;
    --light-rock: #ce9141;
    --dark-rock: #784517;
    --river-blue: #69a3dc;
    --light-river-blue: #b1c7dd;
    --menu-blue: #1c1f82;
    --black: #000;
    --white: #dedede;
    --true-black: #000;
    --grey: #959595;
}

/* Kicks in if the user's system setting is dark mode */
@media screen and (prefers-color-scheme: dark) {
    html {
          --pink: #cb86fc;
          --firion-red: #bb4135;
          --firion-blue: #5357fb;
          --grass-green: #3a6b1a;
          --light-rock: #ce9141;
          --dark-rock: #784517;
          --river-blue: #69a3dc;
          --light-river-blue: #b1c7dd;
          --menu-blue: #1c1f82;
          --black: #000;
          --white: #dedede;
          --true-black: #000;
          --grey: #959595;
  }
}

/* Kicks in if the user's site-level setting is light mode */
html.light {
    --pink: #fd7ed0;
    --firion-red: #bb4135;
    --firion-blue: #5357fb;
    --grass-green: #58ab1d;
    --dark-rock: #a15d14;
    --light-rock: #eba640;
    --river-blue: #75b9fd;
    --light-river-blue: #c8e3fe;
    --menu-blue: #252aad;
    --black: #0d1b2a;
    --white: #fff;
    --true-black: #000;
    --grey: #959595;
}

/* Kicks in if the user's system setting is light mode */
@media screen and (prefers-color-scheme: light) {
    html {
          --pink: #fd7ed0;
          --firion-red: #bb4135;
          --firion-blue: #5357fb;
          --grass-green: #58ab1d;
          --dark-rock: #a15d14;
          --light-rock: #eba640;
          --river-blue: #75b9fd;
          --light-river-blue: #c8e3fe;
          --menu-blue: #252aad;
          --black: #0d1b2a;
          --white: #fff;
          --true-black: #000;
          --grey: #959595;
  }
}
/* Kicks in if the user's site-level setting is dark mode */
html.dark { 
    /* (I'm so good at naming colors) */
    --pink: #cb86fc;
    --firion-red: #bb4135;
    --firion-blue: #5357fb;
    --grass-green: #3a6b1a;
    --light-rock: #ce9141;
    --dark-rock: #784517;
    --river-blue: #69a3dc;
    --light-river-blue: #b1c7dd;
    --menu-blue: #1c1f82;
    --black: #000;
    --white: #dedede;
    --true-black: #000;
    --grey: #959595;
}

/* Kicks in if the user's system setting is dark mode */
@media screen and (prefers-color-scheme: dark) {
    html {
          --pink: #cb86fc;
          --firion-red: #bb4135;
          --firion-blue: #5357fb;
          --grass-green: #3a6b1a;
          --light-rock: #ce9141;
          --dark-rock: #784517;
          --river-blue: #69a3dc;
          --light-river-blue: #b1c7dd;
          --menu-blue: #1c1f82;
          --black: #000;
          --white: #dedede;
          --true-black: #000;
          --grey: #959595;
    }
}

/* Kicks in if the user's site-level setting is light mode */
html.light {
    --pink: #fd7ed0;
    --firion-red: #bb4135;
    --firion-blue: #5357fb;
    --grass-green: #58ab1d;
    --dark-rock: #a15d14;
    --light-rock: #eba640;
    --river-blue: #75b9fd;
    --light-river-blue: #c8e3fe;
    --menu-blue: #252aad;
    --black: #0d1b2a;
    --white: #fff;
    --true-black: #000;
    --grey: #959595;
}

/* Kicks in if the user's system setting is light mode */
@media screen and (prefers-color-scheme: light) {
    html {
          --pink: #fd7ed0;
          --firion-red: #bb4135;
          --firion-blue: #5357fb;
          --grass-green: #58ab1d;
          --dark-rock: #a15d14;
          --light-rock: #eba640;
          --river-blue: #75b9fd;
          --light-river-blue: #c8e3fe;
          --menu-blue: #252aad;
          --black: #0d1b2a;
          --white: #fff;
          --true-black: #000;
          --grey: #959595;
    }
}

반복적인 방법이지만, 분명 누군가는 더 나은 방법을 알고 있을 겁니다. 하지만 이 방법은 잘 해낼 수 있습니다. 사용자의 명시적인 사이트 수준 환경설정 또는 기본 시스템 환경설정에 상관없이 페이지는 적절한 색상표로 신뢰성 있게 렌더링됩니다.

페인트 워크렛에서 사용자 정의 특성 설정

 
If the Paint API is supported, a tiny inline script in the document `<head>` applies a `paint-api` class to the `<html>` element.
/* The main content backdrop rendered at a max-width of 64rem.
   We don't want to waste CPU time if users can't see the
   background behind the content area, so we only allow it to
   render when the screen is 64rem (1024px) or wider. */
@media screen and (min-width: 64rem) {
    .paint-api .backdrop {
          background-image: paint(overworld);
          position: fixed;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          z-index: -1;

          /* These oh-so-well-chosen property names refer to the
             theme-driven CSS variables that vary according to
             the user's preferred color scheme! */
          --overworld-grass-green-color: var(--grass-green);
          --overworld-dark-rock-color: var(--dark-rock);
          --overworld-light-rock-color: var(--light-rock);
          --overworld-river-blue-color: var(--river-blue);
          --overworld-light-river-blue-color: var(--light-river-blue);
  }
}
/* The main content backdrop rendered at a max-width of 64rem.
   We don't want to waste CPU time if users can't see the
      background behind the content area, so we only allow it to
         render when the screen is 64rem (1024px) or wider. */
@media screen and (min-width: 64rem) {
    .paint-api .backdrop {
          background-image: paint(overworld);
          position: fixed;
          top: 0;
          left: 0;
          width: 100%;
              height: 100%;
                  z-index: -1;

          /* These oh-so-well-chosen property names refer to the
                 theme-driven CSS variables that vary according to
                        the user's preferred color scheme! */
          --overworld-grass-green-color: var(--grass-green);
          --overworld-dark-rock-color: var(--dark-rock);
          --overworld-light-rock-color: var(--light-rock);
          --overworld-river-blue-color: var(--river-blue);
          --overworld-light-river-blue-color: var(--light-river-blue);
    }
}
There’s some weirdness here for sure. For some reason, that may or may not be the case later on—but is at least the case as I write this—you can’t render a paint worklet’s output directly on the `<body>` element.

또한 일부 페이지는 상당히 높을 수 있기 때문에 전체 페이지의 배경이 무작위로 생성된(따라서 잠재적으로 비싼) 예술 작품으로 채워지는 것을 원하지 않습니다. 이 문제를 해결하기 위해 사용자가 아래로 스크롤할 때 따라가는 고정 배치를 사용하고 전체 뷰포트를 차지하는 요소에 페인트 워크렛을 렌더링합니다.

 
All quirks aside, the magic here is that the custom properties for the paint worklet are based on the user’s system—or site-level—color scheme preference because the CSS variables align with that preference. In the case of the `overworld` paint worklet, that means I can adjust its output to align with the user’s preferred color scheme!

나쁘지 않아! 하지만 이것은 심지어 페인트 작업대가 어떻게 만드는지 통제하는 방법의 창의적이지도 않습니다. 내가 원한다면, 특정한 색 구성표에서만 나타나는 약간의 디테일을 추가할 수도 있고, 렌더링을 근본적으로 바꾸거나 작은 부활절 달걀을 추가하기 위해 다른 것들을 할 수도 있다. 올해 많은 것을 배웠지만, 이 API의 교차점이 제가 가장 좋아하는 것 중 하나였던 것 같습니다.

반응형

 

댓글