Remotion LabRemotion Lab
建構應用建構 Google 字型選擇器

建構 Google 字型選擇器

學習如何使用 @remotion/google-fonts 建構字型選擇器組件,在影片中動態載入和套用 Google Fonts

要在字型選擇器中顯示各種 Google 字型並在使用者選擇時載入它們,首先請安裝 @remotion/google-fonts(至少 v3.3.64 版本)。

此功能僅在 @remotion/google-fonts 作為 ES 模組匯入時可用。如果以 CommonJS 模組方式匯入,載入字型將會拋出錯誤。

安裝

npm
npm install @remotion/google-fonts
pnpm
pnpm install @remotion/google-fonts
yarn
yarn add @remotion/google-fonts

基本概念

呼叫 getAvailableFonts() 來獲取 Google 字型列表,並呼叫 .load() 來載入字型的元數據。

之後,在你獲取的對象上:

  • 呼叫 getInfo() 來獲取可用的樣式和粗細。
  • 呼叫 loadFont() 來載入字型本身。

使用 fontFamily CSS 屬性來套用字型。

記住:如果你想要在渲染影片時使用該字型,你也需要在 Remotion 影片中載入字型。你可以用相同的方式,透過遍歷可用字型、找到你要載入的字型然後載入它。

在字型選擇器中顯示所有字型

以下程式碼片段渲染一個包含所有 Google 字型的下拉選單,並在選擇後載入字型。列表大約包含 1400 種字型。

FontPicker.tsx(所有字型)
import { getAvailableFonts } from '@remotion/google-fonts';
import React, { useCallback } from 'react';
 
export const FontPicker: React.FC = () => {
  const newFonts = getAvailableFonts();
 
  const onChange = useCallback(
    async (e: React.ChangeEvent<HTMLSelectElement>) => {
      const fonts = newFonts[e.target.selectedIndex];
 
      // 載入字型資訊
      const loaded = await fonts.load();
 
      // 載入字型本身
      const { fontFamily, ...otherInfo } = loaded.loadFont();
 
      // 或者獲取字型的元數據
      const info = loaded.getInfo();
      const styles = Object.keys(info.fonts);
      console.log('字型', info.fontFamily, '樣式', styles);
 
      for (const style of styles) {
        const weightObject = info.fonts[style as keyof typeof info.fonts];
        const weights = Object.keys(weightObject);
        console.log('- 樣式', style, '支援粗細', weights);
 
        for (const weight of weights) {
          const scripts = Object.keys(weightObject[weight]);
          console.log('-- 粗細', weight, '支援文字系統', scripts);
        }
      }
 
      // 套用字型到 document
      document.body.style.fontFamily = fontFamily;
    },
    [newFonts]
  );
 
  return (
    <div>
      <select onChange={onChange}>
        {newFonts.map((f) => {
          return (
            <option key={f.fontFamily} value={f.fontFamily}>
              {f.fontFamily}
            </option>
          );
        })}
      </select>
    </div>
  );
};

僅顯示 250 種最受歡迎的 Google 字型

為了減少套件大小,你可以限制選擇範圍。不要呼叫 getAvailableFonts(),而是建立一個包含以下內容的檔案,並將其用作字型陣列:

top250Fonts.ts
import type { GoogleFont } from '@remotion/google-fonts';
 
export const top250: GoogleFont[] = [
  {
    family: 'ABeeZee',
    load: () => import('@remotion/google-fonts/ABeeZee') as Promise<GoogleFont>,
  },
  {
    family: 'Abel',
    load: () => import('@remotion/google-fonts/Abel') as Promise<GoogleFont>,
  },
  {
    family: 'Abril Fatface',
    load: () =>
      import('@remotion/google-fonts/AbrilFatface') as Promise<GoogleFont>,
  },
  {
    family: 'Acme',
    load: () => import('@remotion/google-fonts/Acme') as Promise<GoogleFont>,
  },
  {
    family: 'Roboto',
    load: () => import('@remotion/google-fonts/Roboto') as Promise<GoogleFont>,
  },
  {
    family: 'Open Sans',
    load: () =>
      import('@remotion/google-fonts/OpenSans') as Promise<GoogleFont>,
  },
  {
    family: 'Lato',
    load: () => import('@remotion/google-fonts/Lato') as Promise<GoogleFont>,
  },
  {
    family: 'Montserrat',
    load: () =>
      import('@remotion/google-fonts/Montserrat') as Promise<GoogleFont>,
  },
  {
    family: 'Noto Sans TC',
    load: () =>
      import('@remotion/google-fonts/NotoSansTC') as Promise<GoogleFont>,
  },
  // ... 更多字型
];

完整的字型選擇器組件

以下是一個功能完整的字型選擇器,包含樣式和粗細選擇:

FullFontPicker.tsx
import { getAvailableFonts } from '@remotion/google-fonts';
import React, { useCallback, useEffect, useState } from 'react';
 
interface FontInfo {
  fontFamily: string;
  styles: string[];
  weights: Record<string, string[]>;
}
 
interface FontPickerProps {
  onFontChange?: (fontFamily: string, style: string, weight: string) => void;
}
 
export const FullFontPicker: React.FC<FontPickerProps> = ({ onFontChange }) => {
  const allFonts = getAvailableFonts();
  const [selectedFont, setSelectedFont] = useState<string>('');
  const [fontInfo, setFontInfo] = useState<FontInfo | null>(null);
  const [selectedStyle, setSelectedStyle] = useState<string>('normal');
  const [selectedWeight, setSelectedWeight] = useState<string>('400');
  const [isLoading, setIsLoading] = useState(false);
  const [previewText, setPreviewText] = useState('AaBbCc 1234 中文預覽');
 
  const loadFont = useCallback(
    async (fontFamily: string) => {
      const fontEntry = allFonts.find((f) => f.fontFamily === fontFamily);
      if (!fontEntry) return;
 
      setIsLoading(true);
 
      try {
        const loaded = await fontEntry.load();
        const info = loaded.getInfo();
        const styles = Object.keys(info.fonts);
 
        const weights: Record<string, string[]> = {};
        for (const style of styles) {
          const weightObject = info.fonts[style as keyof typeof info.fonts];
          weights[style] = Object.keys(weightObject);
        }
 
        setFontInfo({
          fontFamily: info.fontFamily,
          styles,
          weights,
        });
 
        // 載入預設樣式(normal/400)
        const defaultStyle = styles.includes('normal') ? 'normal' : styles[0];
        const defaultWeights = weights[defaultStyle];
        const defaultWeight = defaultWeights.includes('400')
          ? '400'
          : defaultWeights[0];
 
        setSelectedStyle(defaultStyle);
        setSelectedWeight(defaultWeight);
 
        // 載入字型
        const { fontFamily: loadedFamily } = loaded.loadFont();
        onFontChange?.(loadedFamily, defaultStyle, defaultWeight);
      } catch (error) {
        console.error('載入字型失敗:', error);
      } finally {
        setIsLoading(false);
      }
    },
    [allFonts, onFontChange]
  );
 
  const handleFontChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      const family = e.target.value;
      setSelectedFont(family);
      loadFont(family);
    },
    [loadFont]
  );
 
  const handleStyleChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      const style = e.target.value;
      setSelectedStyle(style);
      onFontChange?.(selectedFont, style, selectedWeight);
    },
    [selectedFont, selectedWeight, onFontChange]
  );
 
  const handleWeightChange = useCallback(
    (e: React.ChangeEvent<HTMLSelectElement>) => {
      const weight = e.target.value;
      setSelectedWeight(weight);
      onFontChange?.(selectedFont, selectedStyle, weight);
    },
    [selectedFont, selectedStyle, onFontChange]
  );
 
  const containerStyle: React.CSSProperties = {
    display: 'flex',
    flexDirection: 'column',
    gap: '12px',
    padding: '16px',
    border: '1px solid #ddd',
    borderRadius: '8px',
    maxWidth: '400px',
  };
 
  const selectStyle: React.CSSProperties = {
    padding: '8px',
    borderRadius: '4px',
    border: '1px solid #ccc',
    fontSize: '14px',
  };
 
  const previewStyle: React.CSSProperties = {
    fontFamily: selectedFont || 'inherit',
    fontStyle: selectedStyle,
    fontWeight: selectedWeight as any,
    fontSize: '24px',
    padding: '16px',
    background: '#f9f9f9',
    borderRadius: '4px',
    minHeight: '60px',
  };
 
  return (
    <div style={containerStyle}>
      <h3 style={{ margin: 0 }}>字型選擇器</h3>
 
      {/* 字型家族選擇 */}
      <div>
        <label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>
          字型家族
        </label>
        <select
          value={selectedFont}
          onChange={handleFontChange}
          style={{ ...selectStyle, width: '100%' }}
        >
          <option value="">請選擇字型...</option>
          {allFonts.map((f) => (
            <option key={f.fontFamily} value={f.fontFamily}>
              {f.fontFamily}
            </option>
          ))}
        </select>
      </div>
 
      {/* 樣式和粗細選擇(僅在選擇字型後顯示) */}
      {fontInfo && (
        <>
          <div style={{ display: 'flex', gap: '8px' }}>
            <div style={{ flex: 1 }}>
              <label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>
                樣式
              </label>
              <select
                value={selectedStyle}
                onChange={handleStyleChange}
                style={selectStyle}
              >
                {fontInfo.styles.map((style) => (
                  <option key={style} value={style}>
                    {style}
                  </option>
                ))}
              </select>
            </div>
 
            <div style={{ flex: 1 }}>
              <label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>
                粗細
              </label>
              <select
                value={selectedWeight}
                onChange={handleWeightChange}
                style={selectStyle}
              >
                {(fontInfo.weights[selectedStyle] || []).map((weight) => (
                  <option key={weight} value={weight}>
                    {weight}
                  </option>
                ))}
              </select>
            </div>
          </div>
 
          {/* 預覽文字 */}
          <div>
            <label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>
              預覽
            </label>
            <input
              value={previewText}
              onChange={(e) => setPreviewText(e.target.value)}
              style={{ ...selectStyle, width: '100%', marginBottom: '8px' }}
              placeholder="輸入預覽文字..."
            />
            <div style={previewStyle}>
              {isLoading ? '載入中...' : previewText}
            </div>
          </div>
        </>
      )}
    </div>
  );
};

在 Remotion 影片中使用字型

如果你要在渲染時使用選定的字型,需要在 Remotion 組件中也載入它:

remotion/TextComposition.tsx
import { getAvailableFonts } from '@remotion/google-fonts';
import React, { useEffect, useState } from 'react';
import { AbsoluteFill } from 'remotion';
 
interface TextProps {
  text: string;
  fontFamily: string;
  fontWeight?: string;
}
 
export const TextComposition: React.FC<TextProps> = ({
  text,
  fontFamily,
  fontWeight = '400',
}) => {
  const [isFontLoaded, setIsFontLoaded] = useState(false);
 
  useEffect(() => {
    const loadFont = async () => {
      const allFonts = getAvailableFonts();
      const fontEntry = allFonts.find((f) => f.fontFamily === fontFamily);
 
      if (!fontEntry) return;
 
      const loaded = await fontEntry.load();
      loaded.loadFont();
      setIsFontLoaded(true);
    };
 
    loadFont();
  }, [fontFamily]);
 
  if (!isFontLoaded) {
    return null; // 等待字型載入
  }
 
  return (
    <AbsoluteFill
      style={{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        background: 'white',
      }}
    >
      <div
        style={{
          fontFamily,
          fontWeight,
          fontSize: '72px',
          color: '#333',
        }}
      >
        {text}
      </div>
    </AbsoluteFill>
  );
};

另請參閱