Repository: motion-canvas/motion-canvas
File: packages/2d/src/components/Icon.ts (TypeScript)

Code before:
    super(props);
  }

  /**
   * This creates the URL that will be used as the Image source
   * @returns Address to Iconify API for the requested Icon.
   */
  @computed()
  protected svgUrl(): string {
    const iconPathSegment = this.icon().replace(':', '/');

    // iconify API is documented here: https://docs.iconify.design/api/svg.html#color
    const url = `https://api.iconify.design/${iconPathSegment}.svg?color=${Icon.normalizeColorInput(
      this.color(),
    )}`;
    return url;
  }

  private static normalizeColorInput(colorString: string) {
    // This assumes that no HTML named color is a valid hexadecimal string.
    // First, check if a # is present. If yes, we can assume that this is not a
    // named color.
    const hasHashtag = colorString.trimStart().startsWith('#');

    // Does no longer contain a #. Also no trailing / leading whitespace characters
    const withoutHashTag = hasHashtag
      ? colorString.split('#')[1].trim()
      : colorString.trim();

    function canBeInterpretedAsHexadecimal(str: string) {
      if (str.length !== 6 && str.length !== 8) {
        return false;
      }
      // If we do not have a hexadecimal string here, parseInt will return Not a Number (NaN)
      // so we have a valid hexadecimal string if this returns false
      return !Number.isNaN(parseInt(str, 16));
    }

    const colorName = canBeInterpretedAsHexadecimal(withoutHashTag)
      ? '%23' + withoutHashTag // as hexadecimal value
      : withoutHashTag; // as named color

    return colorName;
  }

  protected override image(): HTMLImageElement {
    const src = this.svgUrl();

    if (!Icon.iconContentsPool[src]) {
      // Not in Cache yet.
      const image = document.createElement('img');

Review comment (suggestion):
Looking at this again, we probably can condense this into two steps:
1. Check if there's a hash, and if there is, make sure that it's a valid hex number of 6 or 8 digits.
    * If there's not, warn the user that they used an invalid hex using the logger and don't bother updating the value.
2. Send the result to the `encodeURIComponent` function. If there's a `#`, it'll get converted. If not, it stays as the input string.

Code after:
import {Image, ImageProps} from '../components/Image';
import {colorSignal, computed, initial, signal} from '../decorators';
import {SignalValue, SimpleSignal} from '@motion-canvas/core/lib/signals';
import {useLogger} from '@motion-canvas/core/lib/utils';
import {ColorSignal, PossibleColor} from '@motion-canvas/core/lib/types';

export interface IconProps extends ImageProps {
  /**
   * {@inheritDoc Icon.icon}
   */
  icon: SignalValue<string>;

  /**
   * {@inheritDoc Icon.color}
   */
  color?: SignalValue<PossibleColor>;
}

/**
 * An Icon Component that provides an easy access to over 150k icons.
 * See https://icones.js.org/collection/all for all available Icons.
 */
export class Icon extends Image {
  /**
   * The identifier of the icon.
   *
   * @remarks
   * You can find identifiers on [Icônes](https://icones.js.org).
   * They can look like this:
   * * `mdi:language-typescript`
   * * `ph:anchor-simple-bold`
   * * `ph:activity-bold`
   */
  @signal()
  public declare icon: SimpleSignal<string, this>;

  /**
   * The color of the icon
   *
   * @remarks
   * Provide the color in one of the following formats:
   * * named color like `red`, `darkgray`, …
   * * hexadecimal string with # like `#bada55`, `#141414`
   *   Value can be either RGB or RGBA: `#bada55`, `#bada55aa` (latter is partially transparent)
   *   The shorthand version (e.g. `#abc` for `#aabbcc` is also possible.)
   *
   * @default 'white'
   */
  @initial('white')
  @colorSignal()
  public declare color: ColorSignal<this>;

  public constructor(props: IconProps) {
    super(props);
  }

  /**
   * Create the URL that will be used as the Image source
   * @returns Address to Iconify API for the requested Icon.
   */
  @computed()
  protected svgUrl(): string {
    const iconPathSegment = this.icon().replace(':', '/');
    const encodedColorValue = encodeURIComponent(this.color().hex());
    // Iconify API is documented here: https://docs.iconify.design/api/svg.html#color
    return `https://api.iconify.design/${iconPathSegment}.svg?color=${encodedColorValue}`;
  }

  /**
   * overrides `Image.src` getter
   */
  protected getSrc(): string {
    return this.svgUrl();
  }

  /**
   * overrides `Image.src` setter to warn the user that the value
   * is not used
   */
  protected setSrc() {
    useLogger().warn(
      "The Icon Component does not accept setting the `src`. If you need access to `src`, use '<Image/>` instead.",
    );
  }
}