建構 Google 字型選擇器
學習如何使用 @remotion/google-fonts 建構字型選擇器組件,在影片中動態載入和套用 Google Fonts
要在字型選擇器中顯示各種 Google 字型並在使用者選擇時載入它們,首先請安裝 @remotion/google-fonts(至少 v3.3.64 版本)。
此功能僅在 @remotion/google-fonts 作為 ES 模組匯入時可用。如果以 CommonJS 模組方式匯入,載入字型將會拋出錯誤。
安裝
npm install @remotion/google-fontspnpm install @remotion/google-fontsyarn add @remotion/google-fonts基本概念
呼叫 getAvailableFonts() 來獲取 Google 字型列表,並呼叫 .load() 來載入字型的元數據。
之後,在你獲取的對象上:
- 呼叫
getInfo()來獲取可用的樣式和粗細。 - 呼叫
loadFont()來載入字型本身。
使用 fontFamily CSS 屬性來套用字型。
記住:如果你想要在渲染影片時使用該字型,你也需要在 Remotion 影片中載入字型。你可以用相同的方式,透過遍歷可用字型、找到你要載入的字型然後載入它。
在字型選擇器中顯示所有字型
以下程式碼片段渲染一個包含所有 Google 字型的下拉選單,並在選擇後載入字型。列表大約包含 1400 種字型。
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(),而是建立一個包含以下內容的檔案,並將其用作字型陣列:
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>,
},
// ... 更多字型
];完整的字型選擇器組件
以下是一個功能完整的字型選擇器,包含樣式和粗細選擇:
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 組件中也載入它:
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>
);
};