chromium/chrome/browser/resources/lens/overlay/shimmer_circle_paint_worklet.ts

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert, assertInstanceof} from '//resources/js/assert.js';

interface ShimmerCircleProperties {
  circleColor: string;
  circleRadius: CSSUnitValue;
  circleBlur: CSSUnitValue;
  circleCenterX: CSSUnitValue;
  circleCenterY: CSSUnitValue;
  circleRadiusAmplitude: CSSUnitValue;
  circleCenterXAmplitude: CSSUnitValue;
  circleCenterYAmplitude: CSSUnitValue;
  circleRadiusWiggle: CSSUnitValue;
  circleCenterXWiggle: CSSUnitValue;
  circleCenterYWiggle: CSSUnitValue;
}

class ShimmerCircleWorklet {
  static get inputProperties() {
    return [
      '--shimmer-circle-color',
      '--shimmer-circle-radius',
      '--shimmer-circle-blur',
      '--shimmer-circle-center-x',
      '--shimmer-circle-center-y',
      '--shimmer-circle-radius-amplitude',
      '--shimmer-circle-center-x-amplitude',
      '--shimmer-circle-center-y-amplitude',
      '--shimmer-circle-radius-wiggle',
      '--shimmer-circle-center-x-wiggle',
      '--shimmer-circle-center-y-wiggle',
    ];
  }

  paint(
      ctx: PaintRenderingContext2D, size: PaintSize,
      properties: StylePropertyMapReadOnly) {
    const props = this.getAndAssertProperties(properties);

    // Get the base values from percentages to pixels.
    // We double the radius to increase the blurriness.
    const baseRadius =
        (props.circleRadius.value / 100 * Math.max(size.width, size.height)) *
        props.circleBlur.value;
    const baseCircleX = props.circleCenterX.value / 100 * size.width;
    const baseCircleY = props.circleCenterY.value / 100 * size.height;

    // Get the amplitude values from percentages to pixels.
    const radiusAmp = props.circleRadiusAmplitude.value / 100 *
        Math.max(size.width, size.height);
    const centerXAmp = props.circleCenterXAmplitude.value / 100 * size.width;
    const centerYAmp = props.circleCenterYAmplitude.value / 100 * size.height;

    // Get the actual values as they should be rendered on the screen.
    const radius = baseRadius + radiusAmp * props.circleRadiusWiggle.value;
    const centerX = baseCircleX + centerXAmp * props.circleCenterXWiggle.value;
    const centerY = baseCircleY + centerYAmp * props.circleCenterYWiggle.value;

    // Render the circle on the canvas.
    ctx.clearRect(0, 0, size.width, size.height);
    ctx.fillStyle = this.createCircleGradient(
        ctx, centerX, centerY, radius, props.circleColor);
    ctx.beginPath();
    ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
    ctx.fill();
  }

  private createCircleGradient(
      ctx: PaintRenderingContext2D, centerX: number, centerY: number,
      radius: number, colorRgb: string) {
    // Centered radial gradient.
    const radialGradient = ctx.createRadialGradient(
        centerX,
        centerY,
        0,
        centerX,
        centerY,
        radius,
    );
    // Simulate a Gaussian full page blur by mimicking a smooth step alpha
    // change through the circle radius.
    radialGradient.addColorStop(0, this.rgbToRgba(colorRgb, 1));
    radialGradient.addColorStop(0.125, this.rgbToRgba(colorRgb, 0.957));
    radialGradient.addColorStop(0.25, this.rgbToRgba(colorRgb, 0.84375));
    radialGradient.addColorStop(0.375, this.rgbToRgba(colorRgb, 0.68359));
    radialGradient.addColorStop(0.5, this.rgbToRgba(colorRgb, 0.5));
    radialGradient.addColorStop(0.625, this.rgbToRgba(colorRgb, 0.3164));
    radialGradient.addColorStop(0.75, this.rgbToRgba(colorRgb, 0.15625));
    radialGradient.addColorStop(0.875, this.rgbToRgba(colorRgb, 0.04297));
    radialGradient.addColorStop(1, this.rgbToRgba(colorRgb, 0));
    return radialGradient;
  }

  private rgbToRgba(rgb: string, alpha: number) {
    const colorsString: string = rgb.substring(4, rgb.length - 1);
    const [red, green, blue] = colorsString.split(',');
    return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
  }

  private getAndAssertProperties(properties: StylePropertyMapReadOnly):
      ShimmerCircleProperties {
    const circleColor = properties.get('--shimmer-circle-color');
    const circleRadius = properties.get('--shimmer-circle-radius');
    const circleBlur = properties.get('--shimmer-circle-blur');
    const circleCenterX = properties.get('--shimmer-circle-center-x');
    const circleCenterY = properties.get('--shimmer-circle-center-y');
    const circleRadiusAmplitude =
        properties.get('--shimmer-circle-radius-amplitude');
    const circleCenterXAmplitude =
        properties.get('--shimmer-circle-center-x-amplitude');
    const circleCenterYAmplitude =
        properties.get('--shimmer-circle-center-y-amplitude');
    const circleRadiusWiggle = properties.get('--shimmer-circle-radius-wiggle');
    const circleCenterXWiggle =
        properties.get('--shimmer-circle-center-x-wiggle');
    const circleCenterYWiggle =
        properties.get('--shimmer-circle-center-y-wiggle');

    assert(circleColor);
    assertInstanceof(circleRadius, CSSUnitValue);
    assertInstanceof(circleBlur, CSSUnitValue);
    assertInstanceof(circleCenterX, CSSUnitValue);
    assertInstanceof(circleCenterY, CSSUnitValue);
    assertInstanceof(circleRadiusAmplitude, CSSUnitValue);
    assertInstanceof(circleCenterXAmplitude, CSSUnitValue);
    assertInstanceof(circleCenterYAmplitude, CSSUnitValue);
    assertInstanceof(circleRadiusWiggle, CSSUnitValue);
    assertInstanceof(circleCenterXWiggle, CSSUnitValue);
    assertInstanceof(circleCenterYWiggle, CSSUnitValue);
    assert(
        circleRadius.unit === 'percent',
        '--shimmer-circle-radius must be a percent value');
    assert(
        circleBlur.unit === 'number',
        '--shimmer-circle-blur must be a number value');
    assert(
        circleCenterX.unit === 'percent',
        '--shimmer-circle-center-x must be a percent value');
    assert(
        circleCenterY.unit === 'percent',
        '--shimmer-circle-center-y must be a percent value');
    assert(
        circleRadiusAmplitude.unit === 'percent',
        '--shimmer-circle-radius-amplitude must be a percent value');
    assert(
        circleCenterXAmplitude.unit === 'percent',
        '--shimmer-circle-center-x-amplitude must be a percent value');
    assert(
        circleCenterYAmplitude.unit === 'percent',
        '--shimmer-circle-center-y-amplitude must be a percent value');
    assert(
        circleRadiusWiggle.unit === 'number',
        '--shimmer-circle-radius-wiggle must be a number value');
    assert(
        circleCenterXWiggle.unit === 'number',
        '--shimmer-circle-center-x-wiggle must be a number value');
    assert(
        circleCenterYWiggle.unit === 'number',
        '--shimmer-circle-center-y-wiggle must be a number value');

    return {
      circleColor: circleColor.toString(),
      circleRadius,
      circleBlur,
      circleCenterX,
      circleCenterY,
      circleRadiusAmplitude,
      circleCenterXAmplitude,
      circleCenterYAmplitude,
      circleRadiusWiggle,
      circleCenterXWiggle,
      circleCenterYWiggle,
    };
  }
}

registerPaint('shimmer-circle', ShimmerCircleWorklet);