implement font-family module
This commit is contained in:
parent
6f5efb6fbc
commit
cb9fd93e51
137 changed files with 1177 additions and 21 deletions
|
|
@ -57,10 +57,12 @@ export function usePreviewRenderer({
|
|||
}
|
||||
|
||||
// Render to the hidden frame
|
||||
// Font-face CSS is injected as a separate style block so PagedJS doesn't try to parse its URLs
|
||||
hiddenFrame.srcdoc = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style id="font-faces">${stylesheetStore.fontFaceCss}</style>
|
||||
<link rel="stylesheet" href="/assets/css/pagedjs-interface.css">
|
||||
<style id="dynamic-styles">${stylesheetStore.content}</style>
|
||||
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"><\/script>
|
||||
|
|
@ -136,6 +138,15 @@ export function usePreviewRenderer({
|
|||
}
|
||||
);
|
||||
|
||||
// Watch for font-face CSS changes (new font loaded) and re-render
|
||||
watch(
|
||||
() => stylesheetStore.fontFaceCss,
|
||||
() => {
|
||||
if (!initialized.value) return;
|
||||
renderPreview();
|
||||
}
|
||||
);
|
||||
|
||||
// Re-render when narrative data changes
|
||||
watch(
|
||||
() => narrativeStore.data,
|
||||
|
|
|
|||
153
src/composables/useProjectFonts.js
Normal file
153
src/composables/useProjectFonts.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { reactive, computed } from 'vue';
|
||||
import { useStylesheetStore } from '../stores/stylesheet';
|
||||
|
||||
const PROJECT_FONTS = [
|
||||
{
|
||||
name: 'Alegreya',
|
||||
category: 'serif',
|
||||
files: {
|
||||
variable: '/assets/fonts/project/Alegreya/Alegreya-VariableFont_wght.ttf',
|
||||
variableItalic: '/assets/fonts/project/Alegreya/Alegreya-Italic-VariableFont_wght.ttf',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EB Garamond',
|
||||
category: 'serif',
|
||||
files: {
|
||||
variable: '/assets/fonts/project/EB_Garamond/EBGaramond-VariableFont_wght.ttf',
|
||||
variableItalic: '/assets/fonts/project/EB_Garamond/EBGaramond-Italic-VariableFont_wght.ttf',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Fira Sans',
|
||||
category: 'sans-serif',
|
||||
files: {
|
||||
regular: '/assets/fonts/project/Fira_Sans/FiraSans-Regular.ttf',
|
||||
bold: '/assets/fonts/project/Fira_Sans/FiraSans-Bold.ttf',
|
||||
italic: '/assets/fonts/project/Fira_Sans/FiraSans-Italic.ttf',
|
||||
boldItalic: '/assets/fonts/project/Fira_Sans/FiraSans-BoldItalic.ttf',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Montserrat',
|
||||
category: 'sans-serif',
|
||||
files: {
|
||||
variable: '/assets/fonts/project/Montserrat/Montserrat-VariableFont_wght.ttf',
|
||||
variableItalic: '/assets/fonts/project/Montserrat/Montserrat-Italic-VariableFont_wght.ttf',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Open Sans',
|
||||
category: 'sans-serif',
|
||||
files: {
|
||||
variable: '/assets/fonts/project/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf',
|
||||
variableItalic: '/assets/fonts/project/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Source Code Pro',
|
||||
category: 'monospace',
|
||||
files: {
|
||||
variable: '/assets/fonts/project/Source_Code_Pro/SourceCodePro-VariableFont_wght.ttf',
|
||||
variableItalic: '/assets/fonts/project/Source_Code_Pro/SourceCodePro-Italic-VariableFont_wght.ttf',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Singleton state
|
||||
const fonts = reactive(
|
||||
PROJECT_FONTS.map((f) => ({ ...f, loaded: false }))
|
||||
);
|
||||
|
||||
const loadedSet = new Set();
|
||||
|
||||
function absoluteUrl(path) {
|
||||
return window.location.origin + path.replace(/,/g, '%2C');
|
||||
}
|
||||
|
||||
function generateFontFaceCss(font) {
|
||||
const rules = [];
|
||||
const { name, files } = font;
|
||||
|
||||
if (files.variable) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.variable)}") format("truetype");\n font-weight: 100 900;\n font-style: normal;\n}`
|
||||
);
|
||||
}
|
||||
if (files.variableItalic) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.variableItalic)}") format("truetype");\n font-weight: 100 900;\n font-style: italic;\n}`
|
||||
);
|
||||
}
|
||||
if (files.regular) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.regular)}") format("truetype");\n font-weight: 400;\n font-style: normal;\n}`
|
||||
);
|
||||
}
|
||||
if (files.bold) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.bold)}") format("truetype");\n font-weight: 700;\n font-style: normal;\n}`
|
||||
);
|
||||
}
|
||||
if (files.italic) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.italic)}") format("truetype");\n font-weight: 400;\n font-style: italic;\n}`
|
||||
);
|
||||
}
|
||||
if (files.boldItalic) {
|
||||
rules.push(
|
||||
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.boldItalic)}") format("truetype");\n font-weight: 700;\n font-style: italic;\n}`
|
||||
);
|
||||
}
|
||||
|
||||
return rules.join('\n\n');
|
||||
}
|
||||
|
||||
async function loadFontInDocument(font) {
|
||||
const file = font.files.variable || font.files.regular;
|
||||
if (!file) return;
|
||||
|
||||
const face = new FontFace(font.name, `url("${file}")`);
|
||||
await face.load();
|
||||
document.fonts.add(face);
|
||||
}
|
||||
|
||||
const loadedFontsCss = computed(() => {
|
||||
return fonts
|
||||
.filter((f) => f.loaded)
|
||||
.map((f) => generateFontFaceCss(f))
|
||||
.join('\n\n');
|
||||
});
|
||||
|
||||
async function loadFont(fontName) {
|
||||
if (!fontName || fontName === 'sans-serif' || loadedSet.has(fontName)) return;
|
||||
|
||||
const font = fonts.find((f) => f.name === fontName);
|
||||
if (!font) return;
|
||||
|
||||
await loadFontInDocument(font);
|
||||
font.loaded = true;
|
||||
loadedSet.add(fontName);
|
||||
|
||||
// Update stylesheet store font-face CSS (injected separately in iframe)
|
||||
const stylesheetStore = useStylesheetStore();
|
||||
stylesheetStore.setFontFaceCss(loadedFontsCss.value);
|
||||
}
|
||||
|
||||
async function loadAllFontPreviews() {
|
||||
// Only loads fonts into document.fonts for select preview — does NOT update the stylesheet store
|
||||
const promises = fonts
|
||||
.filter((f) => !loadedSet.has(f.name))
|
||||
.map((f) => loadFontInDocument(f).catch(() => {}));
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
export function useProjectFonts() {
|
||||
return {
|
||||
fonts,
|
||||
loadFont,
|
||||
loadAllFontPreviews,
|
||||
loadedFontsCss,
|
||||
};
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { reactive } from 'vue';
|
|||
const defaults = reactive({
|
||||
fontSize: { value: 16, unit: 'px' },
|
||||
lineHeight: { value: 20, unit: 'px' },
|
||||
fontFamily: 'Alegreya Sans',
|
||||
fontFamily: 'sans-serif',
|
||||
color: 'rgb(0, 0, 0)',
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue