분류 css

CSS Paint API로 그래픽 그리기

컨텐츠 정보

  • 조회 332 (작성일 )

본문

실습 예제와 함께 CSS Paint API에 대한 실용적인 소개입니다.


CSS Paint는 개발자가 CSS에 이미지가 필요한 그래픽을 프로그래밍 방식으로 생성하고 그릴 수 있게 해주는 API입니다.


CSS 엔진의 다른 부분을 노출하고 개발자가 브라우저 렌더링 엔진의 스타일링 및 레이아웃 프로세스에 연결하여 CSS를 확장 할 수 있도록 하는 7 개의 새로운 저수준 API에 대한 포괄적 인 용어 인 CSS Houdini의 일부입니다.


이를 통해 개발자는 브라우저에서 CSS로 구문 분석 할 수 있는 코드를 작성할 수 있으므로 브라우저에서 기본적으로 구현 될 때까지 기다리지 않고 새로운 CSS 기능을 만들 수 있습니다.


오늘 우리는 CSS Houdini 우산의 일부인 두 가지 특정 API를 살펴볼 것입니다.


  1. 이 기사를 쓰는 시점에서 CSS Paint는 Chrome, Opera 및 Edge에서 완전히 구현되었으며 폴리 필을 통해 Firefox 및 Safari에서 사용할 수 있습니다.
  2. CSS 속성 및 값 API를 사용하면 CSS 변수, 초기 값, 지원하는 값 유형 및 이러한 변수를 상속 할 수 있는지 여부를 명시 적으로 정의 할 수 있습니다.

CSS Paint는 CanvasRenderingContext2D의 제거 된 버전 인 PaintWorklet을 사용하여 그래픽을 렌더링하는 기능을 제공합니다. 주요 차이점은 다음과 같습니다.


  • 텍스트 렌더링을 지원하지 않습니다.
  • 직접적인 픽셀 액세스 / 조작 없음

이 두 가지 누락을 염두에 두고 canvas2d를 사용하여 그릴 수 있는 모든 것을 CSS Paint API를 사용하여 일반 DOM 요소에 그릴 수 있습니다. canvas2d를 사용하여 그래픽 작업을 해본 적이 있는 분들은 집에 있을 것입니다.


또한 개발자로서 CSS 변수를 PaintWorklet에 대한 입력으로 전달하고 사용자 정의 사전 정의 속성을 사용하여 프레젠테이션을 제어 할 수 있습니다.


이를 통해 자바 스크립트에 반드시 익숙하지 않은 설계자라도 높은 수준의 사용자 정의가 가능합니다.


여기여기에서 더 많은 예를 볼 수 있습니다. 그리고 그 과정에서 코딩을 시작합시다!


가장 간단한 예 : 두 개의 대각선 


일단 로드되면 적용 할 DOM 요소의 표면에 두 개의 대각선을 그릴 CSS 페인트 렛을 만들어 보겠습니다. paintlet 그리기 표면 크기는 DOM 요소의 너비와 높이에 맞게 조정되며 CSS 변수를 전달하여 대각선 두께를 제어 할 수 있습니다.


PaintWorklet 만들기 


PaintWorklet을로드하려면 별도의 Javascript 파일 (diagonal-lines.js)로 생성해야 합니다.


const PAINTLET_NAME = 'diagonal-lines'

class CSSPaintlet {

  // ? Define the names of the input CSS variables we will support
  static get inputProperties() {
    return [
      `--${PAINTLET_NAME}-line-width`,
    ]
  }

  // ? Define names for input CSS arguments supported in paint()
  // ⚠️ This part of the API is still experimental and hidden
  //    behind a flag.
  static get inputArguments () {
    return []
  }

  // ? paint() will be executed every time:
  //  - any input property changes
  //  - the DOM element we apply our paintlet to changes its dimensions
  paint(ctx, paintSize, props) {
    // ? Obtain the numeric value of our line width that is passed
    //    as a CSS variable
    const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))

    ctx.lineWidth = lineWidth

    // ? Draw diagonal line #1
    ctx.beginPath()
    ctx.moveTo(0, 0)
    ctx.lineTo(paintSize.width, paintSize.height)
    ctx.stroke()

    // ? Draw diagonal line #2
    ctx.beginPath()
    ctx.moveTo(0, paintSize.height)
    ctx.lineTo(paintSize.width, 0)
    ctx.stroke()
  }
}

// ? Register our CSS Paintlet with the correct name
//    so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)

CSS paintlet을 독립형 클래스로 정의합니다. 이 클래스는 CSS paintlet을 할당 한 표면 위에 그래픽을 그리는 paint() 메소드 만 필요합니다. paintlet이 사용하는 CSS 변수를 변경하거나 DOM 요소가 크기를 변경할 때 실행됩니다.


다른 정적 메소드 inputProperties()는 선택 사항입니다. 입력 CSS 변수가 정확히 지원하는 CSS paintlet을 알려줍니다. 우리의 경우에는 --diagonal-lines-line-width가 됩니다. 입력 속성으로 선언하고 paint() 메서드에서 사용하기 위해 사용합니다. 브라우저 간 지원을 보장하기 위해 숫자를 숫자에 넣어 숫자로 캐스팅 하는 것이 중요합니다.


지원되는 또 다른 선택적 정적 메서드 인 inputArguments가 있습니다. 다음과 같이 paint() 메소드에 인수를 노출합니다.


#myImage {
  background-image: paint(myWorklet, 30px, red, 10deg);
}

그러나 CSS paintlet API의이 부분은 여전히 ​​플래그 뒤에 숨겨져 있으며 실험적인 것으로 간주됩니다. 사용의 용이성과 호환성을 위해 이 기사에서는 다루지 않을 것이지만 직접 읽어 보시기 바랍니다. 대신 inputProperties() 메서드를 사용하여 CSS 변수를 사용하여 페인트 렛에 대한 모든 입력을 제어합니다.


CSS PaintWorklet 등록 


그 후에 CSS paintlet을 참조하여 메인 페이지에 등록해야 합니다. 멋진 css-paint-polyfill 패키지를 조건부로 로드하여 Paintlet이 Firefox 및 Safari에서 작동하는지 확인하는 것이 중요합니다.


CSS paintlet과 함께 Houdini 우산의 일부인 새로운 CSS 속성 및 값 API를 사용하여 CSS.registerProperty()를 통해 CSS 변수 입력을 명시 적으로 정의 할 수 있습니다. 다음과 같이 CSS 변수를 제어 할 수 있습니다.


  • 유형 및 구문
  • 이 CSS 변수가 부모 요소에서 상속되는지 여부
  • 사용자가 지정하지 않으면 초기 값은 얼마입니까?

이 API는 Firefox 및 Safari에서도 지원되지 않지만 Chromium 브라우저에서는 계속 사용할 수 있습니다. 이렇게 하면 데모와 이를 지원하지 않는 브라우저는 이를 무시할 수 있습니다.


;(async function() {
  // ⚠️ Handle Firefox and Safari by importing a polyfill for CSS Pain    
  if (CSS['paintWorklet'] === undefined) {
    await import('https://unpkg.com/css-paint-polyfill')
  }

  // ? Explicitly define our custom CSS variable
  //    This is not supported in Safari and Firefox, so they will
  //    ignore it, but we can optionally use it in browsers that 
  //    support it. 
  //    This way we will future-proof our applications so once Safari
  //    and Firefox support it, they will benefit from these
  //    definitions too.
  //
  //    Make sure that the browser treats it as a number
  //    It does not inherit it's value
  //    It's initial value defaults to 1
  if ('registerProperty' in CSS) {
    CSS.registerProperty({
      name: '--diagonal-lines-line-width',
      syntax: '<number>',
      inherits: false,
      initialValue: 1
    })
  }

  // ? Include our separate paintlet file
  CSS.paintWorklet.addModule('path/to/our/external/worklet/diagonal-files.js')
})()

Paintlet을 CSS 배경으로 참조 


paintlet을 JS 파일로 포함하면 사용이 매우 간단합니다. CSS에서 스타일을 지정할 대상 DOM 요소를 선택하고 paint() CSS 명령을 통해 paintlet을 적용합니다.


#myElement {
   // ? Reference our CSS paintlet
   background-image: paint('--diagonal-lines');

   // ? Pass in custom CSS variable to be used in our CSS paintlet
   --diagonal-lines-line-width: 10;

   // ? Remember - the browser treats this as a regular image
   // referenced in CSS. We can control it's repeat, size, position
   // and any other background related property available
   background-repeat: no-repeat;
   background-size: cover;
   background-position: 50% 50%;

   // Some more styles to make sure we can see our element on the page
   border: 1px solid red;
   width: 200px;
   height: 200px;
   margin: 0 auto;
}

그리고 이 코드를 사용하면 다음과 같은 결과를 얻을 수 있습니다.


https://codepen.io/gbnikolov/pen/JjWeVzZ


이 CSS paintlet을 모든 차원의 DOM 요소에 배경으로 적용 할 수 있습니다. DOM 요소를 전체 화면으로 확장하고 배경 크기 x 및 y 값을 낮추고 배경 반복을 반복하도록 설정하겠습니다. 다음은 업데이트 된 예입니다.


https://codepen.io/gbnikolov/pen/QWpJXVY


이전 예제와 동일한 CSS 페인트 렛을 사용하고 있지만 이제 전체 데모 페이지를 포함하도록 확장했습니다.


이제 기본 예제를 다루고 코드를 구성하는 방법을 확인 했으므로 더 멋진 데모를 작성해 보겠습니다!


Particle Connections 


https://codepen.io/gbnikolov/pen/GRWwbed


이 그림판은 @nucliweb의 멋진 데모에서 영감을 받았습니다.


다시 말하지만, 과거에 canvas2d API를 사용하여 그래픽을 그렸던 분들에게는 매우 간단합니다.


`–dots-connections-count` CSS 변수를 통해 렌더링 할 포인트 수를 제어합니다. 페인트 렛에서 숫자 값을 얻으면 적절한 크기의 배열을 만들고 임의의 x, y 및 반경 속성을 가진 객체로 채 웁니다.


그런 다음 배열의 각 항목을 반복하고 좌표에 구를 그리고 가장 가까운 이웃을 찾아서 연결합니다 (최소 거리는`–dots-connections-connection-min-dist` CSS 변수를 통해 제어 됨). 라인.


또한 각각`–dots-connections-fill-color` 및 --dots-connections-stroke-color CSS 변수를 통해 구체 채우기 색상과 선 획 색상을 제어합니다.


다음은 완전한 작업 코드입니다.


const PAINTLET_NAME = 'dots-connections'

class CSSPaintlet {
  // ? Define names for input CSS variables we will support
  static get inputProperties() {
    return [
      `--${PAINTLET_NAME}-line-width`,
      `--${PAINTLET_NAME}-stroke-color`,
      `--${PAINTLET_NAME}-fill-color`,
      `--${PAINTLET_NAME}-connection-min-dist`,
      `--${PAINTLET_NAME}-count`,
    ]
  }

  // ? Our paint method to be executed when CSS vars change
  paint(ctx, paintSize, props, args) {
    const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))
    const minDist = Number(props.get(`--${PAINTLET_NAME}-connection-min-dist`))
    const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`)
    const fillColor = props.get(`--${PAINTLET_NAME}-fill-color`)
    const numParticles = Number(props.get(`--${PAINTLET_NAME}-count`))
    
    // ? Generate particles at random positions
    //    across our DOM element surface
    const particles = new Array(numParticles).fill(null).map(_ => ({
      x: Math.random() * paintSize.width,
      y: Math.random() * paintSize.height,
      radius: 2 + Math.random() * 2,
    }))
    
    // ? Assign lineWidth coming from CSS variables and make sure
    //    lineCap and lineWidth are round
    ctx.lineWidth = lineWidth
    ctx.lineJoin = 'round'
    ctx.lineCap = 'round'
    
    // ? Loop over the particles with nested loops - O(n^2)
    for (let i = 0; i < numParticles; i++) {
      const particle = particles[i]
      // ? Loop second time 
      for (let n = 0; n < numParticles; n++) {
        if (i === n) {
          continue
        }
        const nextParticle = particles[n]
        // ? Calculate distance between the current particle
        //    and the particle from the previous loop iteration
        const dx = nextParticle.x - particle.x
        const dy = nextParticle.y - particle.y
        const dist = Math.sqrt(dx * dx + dy * dy)
        // ? If the dist is smaller then the minDist specified via
        //    CSS variable, then we will connect them with a line
        if (dist < minDist) {
          ctx.strokeStyle = strokeColor
          ctx.beginPath()
          ctx.moveTo(nextParticle.x, nextParticle.y)
          ctx.lineTo(particle.x, particle.y)
          // ? Draw the connecting line
          ctx.stroke()
        }
      }
      // Finally draw the particle at the right position
      ctx.fillStyle = fillColor
      ctx.beginPath()
      ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2)
      ctx.closePath()
      ctx.fill()
    }
    
  }
}

// ? Register our CSS paintlet with a unique name
//    so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)

Line Loop 


다음은 다음 예입니다. Paintlet에 대한 입력으로 다음 CSS 변수가 필요합니다.


--loop-line-width
--loop-stroke-color
--loop-sides
--loop-scale
--loop-rotation 



전체 원 (PI * 2)을 순환하고 --loop-sides count CSS 변수를 기반으로 둘레를 따라 배치합니다. 각 위치에 대해 전체 원 주위를 다시 반복하고 ctx.lineTo() 명령을 통해 다른 모든 위치에 연결합니다.


https://codepen.io/gbnikolov/pen/BaWvyyv


const PAINTLET_NAME = 'loop'

class CSSPaintlet {
  // ? Define names for input CSS variables we will support
  static get inputProperties() {
    return [
      `--${PAINTLET_NAME}-line-width`,
      `--${PAINTLET_NAME}-stroke-color`,
      `--${PAINTLET_NAME}-sides`,
      `--${PAINTLET_NAME}-scale`,
      `--${PAINTLET_NAME}-rotation`,
    ]
  }
  // ? Our paint method to be executed when CSS vars change
  paint(ctx, paintSize, props, args) {
    const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))
    const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`)
    const numSides = Number(props.get(`--${PAINTLET_NAME}-sides`))
    const scale = Number(props.get(`--${PAINTLET_NAME}-scale`))
    const rotation = Number(props.get(`--${PAINTLET_NAME}-rotation`))
    
    const angle = Math.PI * 2 / numSides
    const radius = paintSize.width / 2
    ctx.save()
    ctx.lineWidth = lineWidth
    ctx.lineJoin = 'round'
    ctx.lineCap = 'round'
    ctx.strokeStyle = strokeColor
    ctx.translate(paintSize.width / 2, paintSize.height / 2)
    ctx.rotate(rotation * (Math.PI / 180))
    ctx.scale(scale / 100, scale / 100)
    ctx.moveTo(0, radius)

    // ? Loop over the numsides twice in nested loop - O(n^2)
    //    Connect each corner with all other corners
    for (let i = 0; i < numSides; i++) {
      const x = Math.sin(i * angle) * radius
      const y = Math.cos(i * angle) * radius
      for (let n = i; n < numSides; n++) {
        const x2 = Math.sin(n * angle) * radius
        const y2 = Math.cos(n * angle) * radius
        ctx.lineTo(x, y)
        ctx.lineTo(x2, y2);
      }
    }
    ctx.closePath()
    ctx.stroke()
    ctx.restore()
  }   
}

// ? Register our CSS paintlet with a unique name
//    so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)

https://codepen.io/gbnikolov/pen/bGqORea


다음은 다음 예입니다. Jhey Tompkins의 다른 멋진 CSS Paintlet에서 영감을 얻었습니다. Paintlet에 대한 입력으로 다음 CSS 변수가 필요합니다.


--grid-size
--grid-color
--grid-noise-scale 


paintlet 자체는 펄린 노이즈 (joeiddon의 코드 제공)를 사용하여 각 개별 셀의 불투명도를 제어합니다.


const PAINTLET_NAME = 'grid'

class CSSPaintlet {
  // ? Define names for input CSS variables we will support
  static get inputProperties() {
    return [
      `--${PAINTLET_NAME}-size`,
      `--${PAINTLET_NAME}-color`,
      `--${PAINTLET_NAME}-noise-scale`
    ]
  }

  // ? Our paint method to be executed when CSS vars change
  paint(ctx, paintSize, props, args) {
    const gridSize = Number(props.get(`--${PAINTLET_NAME}-size`))
    const color = props.get(`--${PAINTLET_NAME}-color`)
    const noiseScale = Number(props.get(`--${PAINTLET_NAME}-noise-scale`))

    ctx.fillStyle = color
    for (let x = 0; x < paintSize.width; x += gridSize) {
      for (let y = 0; y < paintSize.height; y += gridSize) {
        // ? Use perlin noise to determine the cell opacity
        ctx.globalAlpha = mapRange(perlin.get(x * noiseScale, y * noiseScale), -1, 1, 0.5, 1)
        ctx.fillRect(x, y, gridSize, gridSize)
      }
    }
  }
}

// ? Register our CSS paintlet with a unique name
//    so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)

Curvy dividers 


마지막으로 좀 더 유용한 작업을 해보겠습니다. 프로그래밍 방식으로 구분선을 그려 페이지의 텍스트 콘텐츠를 구분합니다.


https://codepen.io/gbnikolov/pen/XWMoaJd


그리고 평소처럼 다음은 CSS 페인트 렛 코드입니다.


const PAINTLET_NAME = 'curvy-dividor'

class CSSPaintlet {
  // ? Define names for input CSS variables we will support
  static get inputProperties() {
    return [
      `--${PAINTLET_NAME}-points-count`,
      `--${PAINTLET_NAME}-line-width`,
      `--${PAINTLET_NAME}-stroke-color`
    ]
  }
  // ? Our paint method to be executed when CSS vars change
  paint(ctx, paintSize, props, args) {
    const pointsCount = Number(props.get(`--${PAINTLET_NAME}-points-count`))
    const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`))
    const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`)
    
    const stepX = paintSize.width / pointsCount
    
    ctx.lineWidth = lineWidth
    ctx.lineJoin = 'round'
    ctx.lineCap = 'round'
    
    ctx.strokeStyle = strokeColor
    
    const offsetUpBound = -paintSize.height / 2
    const offsetDownBound = paintSize.height / 2
    
    // ? Draw quadratic bezier curves across the horizontal axies
    //    of our dividers:
    ctx.moveTo(-stepX / 2, paintSize.height / 2)
    for (let i = 0; i < pointsCount; i++) {
      const x = (i + 1) * stepX - stepX / 2
      const y = paintSize.height / 2 + (i % 2 === 0 ? offsetDownBound : offsetUpBound)
      const nextx = (i + 2) * stepX - stepX / 2
      const nexty = paintSize.height / 2 + (i % 2 === 0 ? offsetUpBound : offsetDownBound)
      const ctrlx = (x + nextx) / 2
      const ctrly = (y + nexty) / 2
      ctx.quadraticCurveTo(x, y, ctrlx, ctrly)
    }
    ctx.stroke()
  }
}

// ? Register our CSS paintlet with a unique name
//    so we can reference it from our CSS
registerPaint(PAINTLET_NAME, CSSPaintlet)

결론 


이 기사에서는 CSS Paint API의 모든 주요 구성 요소와 방법을 살펴 보았습니다. 설정이 매우 쉽고 CSS가 기본적으로 지원하지 않는 고급 그래픽을 그리려는 경우 매우 유용합니다.


이러한 CSS paintlet에서 라이브러리를 쉽게 만들고 필요한 최소한의 설정으로 프로젝트 전체에서 재사용 할 수 있습니다.


좋은 방법으로 멋진 canvas2d 데모를 찾아 새로운 CSS Paint API로 포팅하는 것이 좋습니다.


https://tympanus.net/codrops/2021/06/18/drawing-graphics-with-the-css-paint-api/