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
3 changes: 3 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SCRIPT_TAGGING, LINT_RULES } from './config';
let appLoaded = ref(false);
let showUndefined = ref(false);
let showPlaceholderDialog = ref(false);
let showScriptStarter = ref(false);
let vfDisplayMode = ref<'animated' | 'list'>('animated');
let commitRef = ref('refs/heads/main');

Expand Down Expand Up @@ -110,6 +111,7 @@ onBeforeMount(async () => {
<button @click="addUntaggedAxesPanel()">Untagged axes</button>
<button @click="gf?.exportTaggings()">Save changes</button>
<button @click="showPlaceholderDialog = true">Placeholder Tags</button>
<button v-if="SCRIPT_TAGGING" @click="showScriptStarter = true">Start script tagging</button>
<input type="checkbox" v-model="showUndefined">Show all</input>
<button @click="vfDisplayMode = vfDisplayMode === 'animated' ? 'list' : 'animated'">
VF: {{ vfDisplayMode === 'animated' ? 'Animated' : 'List' }}
Expand All @@ -118,6 +120,7 @@ onBeforeMount(async () => {
style="width: 280px; font-family: monospace;" /></label>
<span v-if="SCRIPT_TAGGING" class="script-mode">Script tagging: saves to families-scripts.csv</span>
<placeholder-tag-adder v-if="showPlaceholderDialog" :gf="gf" @close="showPlaceholderDialog = false" />
<script-starter v-if="showScriptStarter" :gf="gf" @close="showScriptStarter = 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' }">
Expand Down
191 changes: 191 additions & 0 deletions src/components/ScriptStarter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { GF, StaticTagging, VariableTagging } from '../models';
import type { Font, Location, Tagging } from '../models';

// Scripts build only. One dialog to set up a tagger for a script: every family
// that supports the script gets a 0-score placeholder for each chosen category,
// tagged for that script. Families with a wght axis get a variable placeholder
// at the wght extremes, crossed with the wdth extremes when the family has wdth.
// Everything else gets a static placeholder. A family that already has any
// tagging for the category, for all scripts or for this one, gets nothing: the
// tagger reviews those on a second pass and uses Duplicate where the script
// needs its own scores.

const props = defineProps({
gf: {
type: GF,
required: true
}
});

const emit = defineEmits(['close']);

// The categories a script tagger starts with. Change the prefixes to widen the set.
const DEFAULT_CATEGORY_PREFIXES = ['/Expressive/', '/Quality/'];

const script = ref<string | null>(null);
const categories = ref<string[]>(
props.gf.uniqueTagNames().filter(name => DEFAULT_CATEGORY_PREFIXES.some(p => name.startsWith(p)))
);

const scriptOptions = computed(() => {
const all = new Set<string>();
for (const family of props.gf.families) {
for (const s of family.subsets) all.add(s);
}
return [...all].sort();
});

const families = computed<Font[]>(() => {
if (!script.value) return [];
return props.gf.families.filter(f => f.subsets.includes(script.value!));
});

// The grid axes a family is scored on: wght, plus wdth when present.
function gridAxes(family: Font): string[] {
if (!family.axis('wght')) return [];
return family.axis('wdth') ? ['wght', 'wdth'] : ['wght'];
}

// One location per corner of the grid, each axis at its min and max.
function cornerLocations(family: Font, axes: string[]): Location[] {
let locations: Location[] = [{}];
for (const axisName of axes) {
const axis = family.axis(axisName)!;
const next: Location[] = [];
for (const location of locations) {
next.push({ ...location, [axisName]: axis.min });
next.push({ ...location, [axisName]: axis.max });
}
locations = next;
}
return locations;
}

interface Plan {
staticFamilies: number;
wghtFamilies: number;
wghtWdthFamilies: number;
newTaggings: number;
newRows: number;
skipped: number;
}

// What OK will do, so the tagger can see the size of it first.
const plan = computed<Plan>(() => {
const p: Plan = { staticFamilies: 0, wghtFamilies: 0, wghtWdthFamilies: 0, newTaggings: 0, newRows: 0, skipped: 0 };
if (!script.value) return p;
for (const family of families.value) {
const axes = gridAxes(family);
if (axes.length === 0) p.staticFamilies++;
else if (axes.length === 1) p.wghtFamilies++;
else p.wghtWdthFamilies++;
const rows = axes.length === 0 ? 1 : 2 ** axes.length;
for (const categoryName of categories.value) {
if (family.hasTagging(categoryName)) {
p.skipped++;
} else {
p.newTaggings++;
p.newRows += rows;
}
}
}
return p;
});

function submit() {
if (!script.value) return;
const scripts = [script.value];
for (const family of families.value) {
const axes = gridAxes(family);
for (const categoryName of categories.value) {
const tag = props.gf.tags[categoryName];
if (!tag) continue;
// Any tagging for the category, even one scored 0, means a person already
// looked at it. Leave it alone.
if (family.hasTagging(categoryName)) continue;
let tagging: Tagging;
if (axes.length === 0) {
tagging = new StaticTagging(family, tag, 0, [...scripts]);
} else {
const scores = cornerLocations(family, axes).map(location => ({ location, score: 0 }));
tagging = new VariableTagging(family, tag, scores, [...scripts]);
}
tagging.placeholder = true;
family.taggings.push(tagging);
}
}
emit('close');
}
</script>

<template>
<div class="dialog-overlay" @click.self="emit('close')">
<div class="dialog">
<h3>Start script tagging</h3>
<p class="hint">
Adds a 0-score placeholder for every family that supports the script and every
category below. Families with a wght axis get a variable placeholder at the wght
extremes, and at the wdth extremes too when they have wdth. Families that already
have any tagging for a category, for all scripts or for this script, are skipped.
</p>

<h4>Script</h4>
<v-select v-model="script" :options="scriptOptions" placeholder="Choose one script"></v-select>

<h4>Categories</h4>
<v-select v-model="categories" multiple :options="gf.uniqueTagNames()"></v-select>

<div v-if="script" class="plan">
<div><b>{{ families.length }}</b> families support {{ script }}:
{{ plan.staticFamilies }} static, {{ plan.wghtFamilies }} with wght, {{ plan.wghtWdthFamilies }} with wght and wdth.</div>
<div>OK adds <b>{{ plan.newTaggings }}</b> placeholders ({{ plan.newRows }} CSV rows)
and skips {{ plan.skipped }} that already exist.</div>
</div>

<hr />
<button @click="submit" :disabled="!script || categories.length === 0 || plan.newTaggings === 0">OK</button>
<button @click="emit('close')">Cancel</button>
</div>
</div>
</template>

<style scoped>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 100;
display: flex;
align-items: flex-start;
justify-content: center;
}

.dialog {
background: white;
padding: 20px;
border-radius: 8px;
max-width: 600px;
width: 100%;
margin-top: 50px;
max-height: 80vh;
overflow: auto;
}

.hint {
margin: 4px 0 8px;
font-size: 0.85em;
color: #555;
}

.plan {
margin-top: 12px;
padding: 8px 12px;
background: #f3f4f6;
border-radius: 6px;
}
</style>
5 changes: 5 additions & 0 deletions src/components/TagsByCategories.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const reverseTags = ref(false);
const variableOnly = ref(false);
const staticOnly = ref(false);
const scriptFilter = ref<string[]>([]);
const placeholdersOnly = ref(false);
const sampleText = ref('');

// Every script any font supports, from the family metadata subsets
Expand Down Expand Up @@ -109,6 +110,9 @@ const filteredTaggings: ComputedRef<Tagging[]> = computed(() => {
const axes = axisFilter.value.split(",").map(a => a.trim().toLowerCase()).filter(Boolean);
filtered = filtered.filter(t => axes.every(a => t.font.axes.some(ax => ax.tag.toLowerCase() === a)));
}
if (placeholdersOnly.value) {
filtered = filtered.filter(t => t.placeholder);
}
if (scriptFilter.value.length > 0) {
// Keep fonts that support any of the chosen scripts
filtered = filtered.filter(t => scriptFilter.value.some(s => t.font.subsets.includes(s)));
Expand Down Expand Up @@ -140,6 +144,7 @@ const filteredTaggings: ComputedRef<Tagging[]> = computed(() => {
class="inline-block script-select"></v-select>
<label><input type="checkbox" v-model="variableOnly" :disabled="staticOnly" /> Variable only</label>
<label><input type="checkbox" v-model="staticOnly" :disabled="variableOnly" /> Static only</label>
<label><input type="checkbox" v-model="placeholdersOnly" /> Placeholders only</label>
<input type="text" v-model="sampleText" placeholder="Sample text for all previews" class="sample-text-input" />
</div>
<div v-for="tagging in filteredTaggings" :key="tagging.font.name + tagging.tag.name + tagging.scripts.join('|')">
Expand Down
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Todo from "./components/Todo.vue";
import UntaggedFamilies from "./components/UntaggedFamilies.vue";
import UntaggedAxes from "./components/UntaggedAxes.vue";
import PlaceholderTagAdder from "./components/PlaceholderTagAdder.vue";
import ScriptStarter from "./components/ScriptStarter.vue";
import vSelect from "vue-select";

Vue.component("v-select", vSelect);
Expand All @@ -32,6 +33,7 @@ Vue.component("untagged-families", UntaggedFamilies);
Vue.component("untagged-axes", UntaggedAxes);
Vue.component("sample-text", SampleText);
Vue.component("placeholder-tag-adder", PlaceholderTagAdder);
Vue.component("script-starter", ScriptStarter);

var app = new Vue({
el: "#app",
Expand Down
Loading