Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import { EventBus } from './eventbus';
let appLoaded = ref(false);
let showUndefined = ref(false);
let showPlaceholderDialog = ref(false);
let vfDisplayMode = ref<'animated' | 'list'>('animated');
let commitRef = ref('refs/heads/main');

function loadFromCommit() {
if (gf.value) {
gf.value.loadTaggings(commitRef.value);
}
}
let gf = ref<GF | null>(null);
// @ts-ignore
window.gf = gf; // For debugging
Expand Down Expand Up @@ -107,12 +115,17 @@ onBeforeMount(async () => {
<button @click="gf?.exportTaggings()">Save changes</button>
<button @click="showPlaceholderDialog = true">Placeholder Tags</button>
<input type="checkbox" v-model="showUndefined">Show all</input>
<button @click="vfDisplayMode = vfDisplayMode === 'animated' ? 'list' : 'animated'">
VF: {{ vfDisplayMode === 'animated' ? 'Animated' : 'List' }}
</button>
<label>Commit: <input type="text" v-model.lazy="commitRef" @change="loadFromCommit"
style="width: 280px; font-family: monospace;" /></label>
<placeholder-tag-adder v-if="showPlaceholderDialog" :gf="gf" @close="showPlaceholderDialog = false" />
<div style="display: flex; flex-direction: row; width: 100vw; min-height: 100vh;">
<div v-for="(panel, idx) in panels" :key="idx"
:style="{ flex: '1 1 0', minWidth: 0, borderRight: idx < panels.length - 1 ? '1px solid #eee' : 'none', height: '100vh', overflow: 'auto' }">
<panel :panel="panel" :gf="gf" :idx="idx" @remove-panel="removePanel(idx)" @shift-left="shiftLeft(idx)"
@shift-right="shiftRight(idx)" :showUndefined="showUndefined">
@shift-right="shiftRight(idx)" :showUndefined="showUndefined" :vfDisplayMode="vfDisplayMode">
</panel>
</div>
</div>
Expand Down
8 changes: 4 additions & 4 deletions src/components/AddTags.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function addTags() {
<input type="number" v-model="currentLowScore" placeholder="Score" />
<br>
<input type="text" v-model="currentLowAxis" placeholder="Axis name" />
<input type="number" v-model="currentLowPosition" placeholder="Position" />
<input type="number" v-model.lazy="currentLowPosition" placeholder="Position" />
<select v-model="currentLowOp">
<option value="<=">&lt;=</option>
<option value=">=">&gt;=</option>
Expand All @@ -110,10 +110,10 @@ function addTags() {

<h3>High Tag:</h3>
Score:
<input type="number" v-model="currentHighScore" placeholder="Score" />
<input type="number" v-model.lazy="currentHighScore" placeholder="Score" />
<br>
<input type="text" v-model="currentHighAxis" placeholder="Axis name" />
<input type="number" v-model="currentHighPosition" placeholder="Position" />
<input type="text" v-model.lazy="currentHighAxis" placeholder="Axis name" />
<input type="number" v-model.lazy="currentHighPosition" placeholder="Position" />
<select v-model="currentHighOp">
<option value="<=">&lt;=</option>
<option value=">=">&gt;=</option>
Expand Down
5 changes: 3 additions & 2 deletions src/components/Panel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const props = defineProps<{
panel: Panel,
gf: GF,
showUndefined: boolean,
vfDisplayMode: 'animated' | 'list',
}>();
const emit = defineEmits(["remove-panel", "shift-left", "shift-right"]);

Expand Down Expand Up @@ -52,9 +53,9 @@ onMounted(() => {
<button @click="emit('shift-left')" style="float:right">←</button>
<button @click="emit('shift-right')" style="float:right">→</button>
<tags-by-font v-if="panel.type === 'font'" :font="panel.font" :gf="gf"
:showUndefined="props.showUndefined"></tags-by-font>
:showUndefined="props.showUndefined" :vfDisplayMode="props.vfDisplayMode"></tags-by-font>
<tags-by-categories v-else-if="panel.type === 'categories'" :categories="panel.categories"
:gf="gf" :showUndefined="props.showUndefined"></tags-by-categories>
:gf="gf" :showUndefined="props.showUndefined" :vfDisplayMode="props.vfDisplayMode"></tags-by-categories>
<todo v-else-if="panel.type === 'todo'" :gf="gf"></todo>
</div>
</template>
24 changes: 17 additions & 7 deletions src/components/PlaceholderTagAdder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,11 @@ function submitVariable() {

<div v-if="mode === 'variable'">
<div v-for="(spec, idx) in axisSpecs" :key="idx" class="axis-spec-row">
<label>Axis: <input type="text" v-model="spec.axisName" placeholder="e.g. wght" style="width: 80px;" /></label>
<label>Min axis: <input type="number" v-model.number="spec.minAxis" style="width: 80px;" /></label>
<label>Max axis: <input type="number" v-model.number="spec.maxAxis" style="width: 80px;" /></label>
<label>Min score: <input type="number" v-model.number="spec.minScore" style="width: 60px;" /></label>
<label>Max score: <input type="number" v-model.number="spec.maxScore" style="width: 60px;" /></label>
<label>Axis:</label> <input type="text" v-model="spec.axisName" placeholder="e.g. wght" style="width: 80px;" />
<label>Min axis:</label> <input type="number" v-model.number="spec.minAxis" style="width: 80px;" />
<label>Max axis:</label> <input type="number" v-model.number="spec.maxAxis" style="width: 80px;" />
<label>Min score:</label> <input type="number" v-model.number="spec.minScore" style="width: 60px;" />
<label>Max score:</label> <input type="number" v-model.number="spec.maxScore" style="width: 60px;" />
<button @click="removeAxisSpec(idx)">Remove</button>
</div>
<button @click="addAxisSpec">Add axis</button>
Expand Down Expand Up @@ -156,10 +156,20 @@ function submitVariable() {
}

.axis-spec-row {
margin-bottom: 8px;
display: grid;
grid-template-columns: auto 1fr;
column-gap: 8px;
row-gap: 4px;
align-items: center;
margin-bottom: 12px;
}

.axis-spec-row label {
margin-right: 8px;
text-align: right;
}

.axis-spec-row button {
grid-column: 1 / -1;
justify-self: start;
}
</style>
42 changes: 37 additions & 5 deletions src/components/TagView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ import { EventBus } from '@/eventbus';
const props = defineProps({
tagging: Object as PropType<Tagging>,
location: Object as PropType<Location>,
vfDisplayMode: {
type: String as PropType<'animated' | 'list'>,
default: 'animated',
},
});

onBeforeMount(() => { EventBus.$emit('ensure-loaded', props.tagging?.font.name); });
const removeTagging = () => { props.tagging?.font.removeTagging(props.tagging) }
const inputValue = (e: Event) => Number((e.target as HTMLInputElement).value);

const currentLocationIndex = ref(0);
const editing = ref(false);
let animationInterval: ReturnType<typeof setInterval> | null = null;

// Build cross-product of per-axis values for animation.
Expand Down Expand Up @@ -61,10 +67,22 @@ const animatedStyle = computed(() => {
return style;
});

function styleForLocation(location: Location) {
if (!props.tagging) return '';
let style = `font-family: '${props.tagging.font.name}'; font-size: 32pt; font-variation-settings:`;
style += Object.entries(location).map(([tag, val]) => ` '${tag}' ${val}`).join(',');
style += ';';
return style;
}

function locationLabel(location: Location) {
return Object.entries(location).map(([axis, val]) => `${axis}=${val}`).join(', ');
}

onBeforeMount(() => {
if (animationFrames.value.length > 1) {
animationInterval = setInterval(() => {
currentLocationIndex.value++;
if (!editing.value) currentLocationIndex.value++;
}, 2000);
}
});
Expand Down Expand Up @@ -95,17 +113,31 @@ onBeforeUnmount(() => {
Variable tag
<div v-for="(entry, idx) in props.tagging.scores" :key="idx">
<span v-for="(val, axis) in entry.location" :key="axis">
{{ axis }}=<input type="number" v-model.number="entry.location[axis]" style="width: 70px;"
{{ axis }}=<input type="number" :value="entry.location[axis]" style="width: 70px;"
:min="props.tagging.font.axis(axis)?.min"
:max="props.tagging.font.axis(axis)?.max" />
:max="props.tagging.font.axis(axis)?.max"
@focus="editing = true"
@change="entry.location[axis] = inputValue($event)"
@blur="editing = false" />
</span>
score=<input type="number" v-model.number="entry.score" style="width: 60px;" />
score=<input type="number" :value="entry.score" style="width: 60px;"
@focus="editing = true"
@change="entry.score = inputValue($event)"
@blur="editing = false" />
</div>
</span>
<button @click="removeTagging" class="remove-tag-btn">Remove</button>
</div>
<div class="text-editor" contenteditable="true" :style="animatedStyle">
<div v-if="!props.tagging || !('scores' in props.tagging) || props.vfDisplayMode === 'animated'" class="text-editor" contenteditable="true" :style="animatedStyle">
Hello world
</div>
<div v-else class="location-list">
<div v-for="(entry, idx) in props.tagging.scores" :key="idx" class="location-entry">
<div class="location-label">{{ locationLabel(entry.location) }} (score: {{ entry.score }})</div>
<div class="text-editor" contenteditable="true" :style="styleForLocation(entry.location)">
Hello world
</div>
</div>
</div>
</div>
</template>
22 changes: 19 additions & 3 deletions src/components/TagsByCategories.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ const props = defineProps({
showUndefined: {
type: Boolean,
default: false
},
vfDisplayMode: {
type: String as () => 'animated' | 'list',
default: 'animated'
}
});

Expand All @@ -24,6 +28,7 @@ const sortBy = ref('family'); // Default sorting option
const tagFilter = ref('');
const reverseTags = ref(false);
const variableOnly = ref(false);
const staticOnly = ref(false);

const unappliedTaggings: ComputedRef<Tagging[]> = computed(() => {
if (!selectedCategories.value) return [];
Expand All @@ -45,7 +50,14 @@ const filteredTaggings: ComputedRef<Tagging[]> = computed(() => {
filtered = filtered.concat(unappliedTaggings.value);
}
if (sortBy.value === 'score') {
filtered = filtered.sort((a, b) => (b.score || 0) - (a.score || 0));
const sortScore = (t: Tagging) => {
if ('scores' in t) {
const scores = t.scores.map(s => s.score);
return reverseTags.value ? Math.min(...scores) : Math.max(...scores);
}
return t.score || 0;
};
filtered = filtered.sort((a, b) => sortScore(b) - sortScore(a));
}
if (sortBy.value === 'family') {
filtered = filtered.sort((a, b) => {
Expand All @@ -70,6 +82,9 @@ const filteredTaggings: ComputedRef<Tagging[]> = computed(() => {
if (variableOnly.value) {
filtered = filtered.filter(t => t instanceof VariableTagging);
}
if (staticOnly.value) {
filtered = filtered.filter(t => t instanceof StaticTagging);
}
if (tagFilter.value !== "") {
const myRegex = new RegExp(tagFilter.value, "i");
filtered = filtered.filter(tag => myRegex.test(tag.font.name));
Expand All @@ -96,10 +111,11 @@ const filteredTaggings: ComputedRef<Tagging[]> = computed(() => {
Reverse Order
</button>
<input type="text" v-model="tagFilter" placeholder="Filter tags by name" />
<label><input type="checkbox" v-model="variableOnly" /> Variable only</label>
<label><input type="checkbox" v-model="variableOnly" :disabled="staticOnly" /> Variable only</label>
<label><input type="checkbox" v-model="staticOnly" :disabled="variableOnly" /> Static only</label>
</div>
<div v-for="tagging in filteredTaggings" :key="tagging.font.name + tagging.tag.name + tagging.score">
<tag-view :tagging="tagging"></tag-view>
<tag-view :tagging="tagging" :vfDisplayMode="props.vfDisplayMode"></tag-view>
</div>
</div>
</template>
30 changes: 28 additions & 2 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,10 +419,18 @@ export class GF {
return Object.keys(this.tags).sort();
}

clearTaggings() {
for (const family of this.families) {
family.taggings = [];
}
}

loadTaggings(commit?: string) {
if (commit === undefined) {
commit = "refs/heads/main"; // Default to main branch if no commit is specified
}
this.commit = commit;
this.clearTaggings();
const tagsUrl = `https://raw.githubusercontent.com/google/fonts/${commit}/tags/all/families.csv`;
// TODO this approach only works for static tags for now
fetch(tagsUrl)
Expand All @@ -434,8 +442,10 @@ export class GF {
})
.then((csvText) => {
const lines = csvText.split("\n");
// Collect variable tag entries keyed by "familyName,tagName"
const variableEntries: Record<string, { family: Font; tag: Tag; scores: { location: Location; score: number }[] }> = {};
for (let line of lines) {
const [familyName, , tagName, scoreStr] = line.split(",");
const [familyName, axisStr, tagName, scoreStr] = line.split(",");
let score: number = parseFloat(scoreStr);
if (!familyName || !tagName) {
console.warn(
Expand All @@ -453,7 +463,23 @@ export class GF {
console.warn("Unknown tag:", tagName, "for family:", familyName);
continue;
}
family.taggings.push(new StaticTagging(family, tag, score));
if (axisStr && axisStr.includes("@")) {
const [axisTag, axisVal] = axisStr.split("@");
const key = `${familyName},${tagName}`;
if (!variableEntries[key]) {
variableEntries[key] = { family, tag, scores: [] };
}
variableEntries[key].scores.push({
location: { [axisTag]: parseFloat(axisVal) },
score,
});
} else {
family.taggings.push(new StaticTagging(family, tag, score));
}
}
// Create VariableTaggings from grouped entries
for (const entry of Object.values(variableEntries)) {
entry.family.taggings.push(new VariableTagging(entry.family, entry.tag, entry.scores));
}
})
.catch((error) => {
Expand Down
Loading