diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a0090b4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+# macOS
+.DS_Store
diff --git a/src/fencer.html b/src/fencer.html
new file mode 100644
index 0000000..8d16ed0
--- /dev/null
+++ b/src/fencer.html
@@ -0,0 +1,130 @@
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+
+
+
+
Fontinfo
+
+
+
+
+
Mappings visual
+
+
+
+
+
Mappings XML
+
+
+
+
+
+
+
Result
+
+
+
+
+ ABCdef123
+
+
+
+
+
+
+
+
diff --git a/src/fencer.js b/src/fencer.js
new file mode 100644
index 0000000..145fc35
--- /dev/null
+++ b/src/fencer.js
@@ -0,0 +1,77 @@
+"use strict"
+
+// import the samsa-core module
+//import { SamsaFont, SamsaInstance, SamsaBuffer, SAMSAGLOBAL } from "https://lorp.github.io/samsa-core/src/samsa-core.js";
+//import { * } from "./models.js";
+
+//export {SamsaFont, SamsaGlyph, SamsaInstance, SamsaBuffer, SAMSAGLOBAL} from "https://lorp.github.io/samsa-core/src/samsa-core.js";
+
+import { SamsaFont, SamsaInstance, SamsaBuffer, SAMSAGLOBAL } from "https://lorp.github.io/samsa-core/src/samsa-core.js";
+import { normalizeValue, piecewiseLinearMap } from "./models.js";
+
+
+
+//console.log(piecewiseLinearMap)
+
+
+
+function onDropFont (e) {
+ const el = e.target;
+
+ e.preventDefault();
+
+ // open font as SamsaFont
+
+
+
+
+ // get arrayBuffer from dropped object
+ const file = e.dataTransfer.files[0];
+ file.arrayBuffer().then(arrayBuffer => {
+ const font = new SamsaFont(new SamsaBuffer(arrayBuffer));
+ let str = "";
+
+ // set name
+ str += font.names[6] + "\n";
+ str += "-".repeat(font.names[6].length) + "\n";
+
+ // repeat string 6 times
+
+ // report axes
+ font.fvar.axes.forEach(axis => {
+ str += `${axis.axisTag} ${axis.minValue} ${axis.defaultValue} ${axis.maxValue}\n`;
+ });
+
+ // set the textarea content to the string
+ document.querySelector(".fontinfo textarea").value = str;
+
+ // set the font face to the arraybuffer
+ const fontFace = new FontFace(font.names[6], arrayBuffer);
+ fontFace.load().then(loadedFace => {
+ document.fonts.add(loadedFace);
+ document.querySelector(".render-native").style.fontFamily = font.names[6];
+ console.log("loaded font: " + font.names[6])
+ });
+ });
+
+}
+
+function initFencer() {
+
+ const fontinfo = document.querySelector(".fontinfo");
+ fontinfo.addEventListener("dragover", (event) => {
+ // prevent default to allow drop
+ event.preventDefault();
+ });
+
+ fontinfo.addEventListener("drop", onDropFont);
+
+
+
+
+
+}
+
+
+initFencer();
+
diff --git a/src/models.js b/src/models.js
new file mode 100644
index 0000000..a6feb60
--- /dev/null
+++ b/src/models.js
@@ -0,0 +1,544 @@
+// This is Behdad’s untested translation of the VariationModel class from fontTools.varLib.models
+// "Just a ChatGPT translation. Untested"
+// https://gist.github.com/behdad/3c5e6bd015b1a44f5f3b50774902eb52
+//
+// Minor changes to make it work in the browser:
+// - removed main() function
+// - removed require('process');
+
+function nonNone(lst) {
+ return lst.filter(l => l !== null);
+}
+
+function allNone(lst) {
+ return lst.every(l => l === null);
+}
+
+function allEqualTo(ref, lst, mapper = null) {
+ if (mapper === null) {
+ return lst.every(item => ref === item);
+ }
+ let mapped = mapper(ref);
+ return lst.every(item => mapped === mapper(item));
+}
+
+function allEqual(lst, mapper = null) {
+ if (lst.length === 0) {
+ return true;
+ }
+ let first = lst[0];
+ return allEqualTo(first, lst.slice(1), mapper);
+}
+
+function subList(truth, lst) {
+ if (truth.length !== lst.length) {
+ throw new Error("Lengths of truth and lst must be equal.");
+ }
+ return lst.filter((_, index) => truth[index]);
+}
+
+function normalizeValue(v, triple, extrapolate = false) {
+ let [lower, defaultValue, upper] = triple;
+ if (!(lower <= defaultValue && defaultValue <= upper)) {
+ throw new Error(`Invalid axis values, must be minimum, default, maximum: ${lower.toFixed(3)}, ${defaultValue.toFixed(3)}, ${upper.toFixed(3)}`);
+ }
+ if (!extrapolate) {
+ v = Math.max(Math.min(v, upper), lower);
+ }
+
+ if (v === defaultValue || lower === upper) {
+ return 0.0;
+ }
+
+ if ((v < defaultValue && lower !== defaultValue) || (v > defaultValue && upper === defaultValue)) {
+ return (v - defaultValue) / (defaultValue - lower);
+ } else {
+ if (!((v > defaultValue && upper !== defaultValue) || (v < defaultValue && lower === defaultValue))) {
+ throw new Error(`Oops... v=${v}, triple=(${lower}, ${defaultValue}, ${upper})`);
+ }
+ return (v - defaultValue) / (upper - defaultValue);
+ }
+}
+
+function normalizeLocation(location, axes, extrapolate = false, validate = false) {
+ if (validate) {
+ let locationKeys = new Set(Object.keys(location));
+ let axesKeys = new Set(Object.keys(axes));
+ if (!Array.from(locationKeys).every(key => axesKeys.has(key))) {
+ throw new Error(`Invalid keys: ${Array.from(new Set([...locationKeys].filter(x => !axesKeys.has(x))))}`);
+ }
+ }
+
+ let output = {};
+ for (let [tag, triple] of Object.entries(axes)) {
+ let v = location.hasOwnProperty(tag) ? location[tag] : triple[1];
+ output[tag] = normalizeValue(v, triple, extrapolate);
+ }
+ return output;
+}
+
+function supportScalar(location, support, ot = true, extrapolate = false, axisRanges = null) {
+ if (extrapolate && axisRanges === null) {
+ throw new TypeError("axisRanges must be passed when extrapolate is True");
+ }
+
+ let scalar = 1.0;
+ for (let axis in support) {
+ let [lower, peak, upper] = support[axis];
+
+ if (ot) {
+ if (peak === 0.0) {
+ continue;
+ }
+ if (lower > peak || peak > upper) {
+ continue;
+ }
+ if (lower < 0.0 && upper > 0.0) {
+ continue;
+ }
+ }
+
+ let v = location.hasOwnProperty(axis) ? location[axis] : (ot ? 0.0 : null);
+ if (!ot && v === null) {
+ throw new Error(`Axis ${axis} must be in location`);
+ }
+
+ if (v === peak) {
+ continue;
+ }
+
+ if (extrapolate) {
+ let [axisMin, axisMax] = axisRanges[axis];
+ if (v < axisMin && lower <= axisMin) {
+ if (peak <= axisMin && peak < upper) {
+ scalar *= (v - upper) / (peak - upper);
+ continue;
+ } else if (axisMin < peak) {
+ scalar *= (v - lower) / (peak - lower);
+ continue;
+ }
+ } else if (v > axisMax && axisMax <= upper) {
+ if (axisMax <= peak && lower < peak) {
+ scalar *= (v - lower) / (peak - lower);
+ continue;
+ } else if (peak < axisMax) {
+ scalar *= (v - upper) / (peak - upper);
+ continue;
+ }
+ }
+ }
+
+ if (v <= lower || v >= upper) {
+ scalar = 0.0;
+ break;
+ }
+
+ if (v < peak) {
+ scalar *= (v - lower) / (peak - lower);
+ } else {
+ scalar *= (v - upper) / (peak - upper);
+ }
+ }
+ return scalar;
+}
+
+class VariationModel {
+ constructor(locations, axisOrder = null, extrapolate = false) {
+ if (new Set(locations.map(l => JSON.stringify(Object.entries(l).sort()))).size !== locations.length) {
+ throw new Error("Locations must be unique.");
+ }
+
+ this.origLocations = locations;
+ this.axisOrder = axisOrder || [];
+ this.extrapolate = extrapolate;
+ this.axisRanges = extrapolate ? this.computeAxisRanges(locations) : null;
+
+ let nonZeroLocations = locations.map(loc =>
+ Object.fromEntries(Object.entries(loc).filter(([_, v]) => v !== 0))
+ );
+
+ let keyFunc = this.getMasterLocationsSortKeyFunc(nonZeroLocations, this.axisOrder);
+ this.locations = nonZeroLocations.sort((a, b) => keyFunc(a) - keyFunc(b));
+
+ this.mapping = this.locations.map(l => nonZeroLocations.indexOf(l));
+ this.reverseMapping = nonZeroLocations.map(l => this.locations.indexOf(l));
+
+ this._computeMasterSupports();
+ this._subModels = {};
+ }
+
+ getSubModel(items) {
+ if (!items.includes(null)) {
+ return [this, items];
+ }
+
+ let key = items.map(v => v !== null);
+ if (!this._subModels) {
+ this._subModels = {};
+ }
+ let subModelKey = JSON.stringify(key);
+ let subModel = this._subModels[subModelKey];
+
+ if (!subModel) {
+ subModel = new VariationModel(this.subList(key, this.origLocations), this.axisOrder);
+ this._subModels[subModelKey] = subModel;
+ }
+
+ return [subModel, this.subList(key, items)];
+ }
+
+ static computeAxisRanges(locations) {
+ let axisRanges = {};
+ let allAxes = new Set(locations.flatMap(loc => Object.keys(loc)));
+
+ for (let loc of locations) {
+ for (let axis of allAxes) {
+ let value = loc.hasOwnProperty(axis) ? loc[axis] : 0;
+ let [axisMin, axisMax] = axisRanges[axis] || [value, value];
+ axisRanges[axis] = [Math.min(value, axisMin), Math.max(value, axisMax)];
+ }
+ }
+
+ return axisRanges;
+ }
+
+ // Utility function used in getSubModel
+ subList(truth, lst) {
+ return lst.map((item, index) => truth[index] ? item : null).filter(item => item !== null);
+ }
+
+ static getMasterLocationsSortKeyFunc(locations, axisOrder = []) {
+ if (!locations.some(loc => Object.keys(loc).length === 0)) {
+ throw new Error("Base master not found.");
+ }
+
+ let axisPoints = {};
+ for (let loc of locations) {
+ if (Object.keys(loc).length !== 1) {
+ continue;
+ }
+ let axis = Object.keys(loc)[0];
+ let value = loc[axis];
+ if (!axisPoints[axis]) {
+ axisPoints[axis] = new Set([0.0]);
+ } else {
+ if (axisPoints[axis].has(value)) {
+ throw new Error(`Value "${value}" in axisPoints["${axis}"] --> ${JSON.stringify(Array.from(axisPoints[axis]))}`);
+ }
+ }
+ axisPoints[axis].add(value);
+ }
+
+ return function key(loc) {
+ let rank = Object.keys(loc).length;
+ let onPointAxes = Object.keys(loc).filter(axis => axisPoints[axis] && axisPoints[axis].has(loc[axis]));
+ let orderedAxes = axisOrder.filter(axis => loc.hasOwnProperty(axis));
+ orderedAxes.push(...Object.keys(loc).filter(axis => !axisOrder.includes(axis)).sort());
+
+ let axisOrderIndices = orderedAxes.map(axis => axisOrder.includes(axis) ? axisOrder.indexOf(axis) : 0x10000);
+ let axisSigns = orderedAxes.map(axis => Math.sign(loc[axis]));
+ let axisAbsValues = orderedAxes.map(axis => Math.abs(loc[axis]));
+
+ return VariationModel.compositeSortKey([
+ rank,
+ -onPointAxes.length,
+ axisOrderIndices,
+ orderedAxes,
+ axisSigns,
+ axisAbsValues
+ ]);
+ };
+ }
+
+ static compositeSortKey(values) {
+ return values.map(val => {
+ if (Array.isArray(val)) {
+ return `[${val.join(",")}]`;
+ } else {
+ return val.toString();
+ }
+ }).join("|");
+ }
+
+ reorderMasters(masterList, mapping) {
+ let newList = mapping.map(idx => masterList[idx]);
+ this.origLocations = mapping.map(idx => this.origLocations[idx]);
+
+ let locations = this.origLocations.map(loc =>
+ Object.fromEntries(Object.entries(loc).filter(([_, v]) => v !== 0))
+ );
+
+ this.mapping = locations.map(l => this.locations.findIndex(loc => JSON.stringify(loc) === JSON.stringify(l)));
+ this.reverseMapping = this.locations.map(l => locations.findIndex(loc => JSON.stringify(loc) === JSON.stringify(l)));
+ this._subModels = {};
+
+ return newList;
+ }
+
+ _computeMasterSupports() {
+ this.supports = [];
+ let regions = this._locationsToRegions();
+
+ for (let i = 0; i < regions.length; i++) {
+ let region = regions[i];
+ let locAxes = new Set(Object.keys(region));
+
+ for (let j = 0; j < i; j++) {
+ let prevRegion = regions[j];
+ if (new Set(Object.keys(prevRegion)).size !== locAxes.size) {
+ continue;
+ }
+
+ let relevant = true;
+ for (let axis in region) {
+ let [lower, peak, upper] = region[axis];
+ if (!(prevRegion[axis][1] === peak || (lower < prevRegion[axis][1] && prevRegion[axis][1] < upper))) {
+ relevant = false;
+ break;
+ }
+ }
+
+ if (!relevant) {
+ continue;
+ }
+
+ let bestAxes = {};
+ let bestRatio = -1;
+ for (let axis in prevRegion) {
+ let val = prevRegion[axis][1];
+ if (!(axis in region)) {
+ throw new Error("Axis missing in region");
+ }
+
+ let [lower, locV, upper] = region[axis];
+ let newLower = lower, newUpper = upper;
+ let ratio;
+ if (val < locV) {
+ newLower = val;
+ ratio = (val - locV) / (lower - locV);
+ } else if (locV < val) {
+ newUpper = val;
+ ratio = (val - locV) / (upper - locV);
+ } else {
+ continue;
+ }
+
+ if (ratio > bestRatio) {
+ bestAxes = {};
+ bestRatio = ratio;
+ }
+ if (ratio === bestRatio) {
+ bestAxes[axis] = [newLower, locV, newUpper];
+ }
+ }
+
+ for (let axis in bestAxes) {
+ region[axis] = bestAxes[axis];
+ }
+ }
+
+ this.supports.push(region);
+ }
+
+ this._computeDeltaWeights();
+ }
+
+ _locationsToRegions() {
+ let minV = {};
+ let maxV = {};
+ for (let loc of this.locations) {
+ for (let [k, v] of Object.entries(loc)) {
+ minV[k] = minV[k] !== undefined ? Math.min(v, minV[k]) : v;
+ maxV[k] = maxV[k] !== undefined ? Math.max(v, maxV[k]) : v;
+ }
+ }
+
+ let regions = [];
+ for (let loc of this.locations) {
+ let region = {};
+ for (let [axis, locV] of Object.entries(loc)) {
+ region[axis] = locV > 0 ? [0, locV, maxV[axis]] : [minV[axis], locV, 0];
+ }
+ regions.push(region);
+ }
+ return regions;
+ }
+
+ _computeDeltaWeights() {
+ this.deltaWeights = [];
+ for (let i = 0; i < this.locations.length; i++) {
+ let loc = this.locations[i];
+ let deltaWeight = {};
+ for (let j = 0; j < i; j++) {
+ let support = this.supports[j];
+ let scalar = this.supportScalar(loc, support); // Ensure supportScalar function is defined
+ if (scalar) {
+ deltaWeight[j] = scalar;
+ }
+ }
+ this.deltaWeights.push(deltaWeight);
+ }
+ }
+
+ getDeltas(masterValues, round = Math.round) {
+ if (masterValues.length !== this.deltaWeights.length) {
+ throw new Error(`Length mismatch: ${masterValues.length} vs ${this.deltaWeights.length}`);
+ }
+ let mapping = this.reverseMapping;
+ let out = [];
+ for (let i = 0; i < this.deltaWeights.length; i++) {
+ let weights = this.deltaWeights[i];
+ let delta = masterValues[mapping[i]];
+ for (let j in weights) {
+ let weight = weights[j];
+ delta -= weight === 1 ? out[j] : out[j] * weight;
+ }
+ out.push(round(delta));
+ }
+ return out;
+ }
+
+ getDeltasAndSupports(items, round = Math.round) {
+ let [model, newItems] = this.getSubModel(items);
+ return [model.getDeltas(newItems, round), model.supports];
+ }
+
+ getScalars(loc) {
+ return this.supports.map(support =>
+ this.supportScalar(loc, support, this.extrapolate, this.axisRanges)
+ );
+ }
+
+ getMasterScalars(targetLocation) {
+ let out = this.getScalars(targetLocation);
+ for (let i = this.deltaWeights.length - 1; i >= 0; i--) {
+ let weights = this.deltaWeights[i];
+ for (let j in weights) {
+ let weight = weights[j];
+ out[j] -= out[i] * weight;
+ }
+ }
+
+ return this.mapping.map(i => out[i]);
+ }
+
+ static interpolateFromValuesAndScalars(values, scalars) {
+ if (values.length !== scalars.length) {
+ throw new Error("Values and scalars arrays must be of the same length.");
+ }
+
+ let v = null;
+ for (let i = 0; i < values.length; i++) {
+ let value = values[i];
+ let scalar = scalars[i];
+ if (!scalar) {
+ continue;
+ }
+ let contribution = value * scalar;
+ v = v === null ? contribution : v + contribution;
+ }
+ return v;
+ }
+
+ static interpolateFromDeltasAndScalars(deltas, scalars) {
+ return VariationModel.interpolateFromValuesAndScalars(deltas, scalars);
+ }
+
+ interpolateFromDeltas(loc, deltas) {
+ let scalars = this.getScalars(loc);
+ return VariationModel.interpolateFromDeltasAndScalars(deltas, scalars);
+ }
+
+ interpolateFromMasters(loc, masterValues, round = Math.round) {
+ let scalars = this.getMasterScalars(loc);
+ return VariationModel.interpolateFromValuesAndScalars(masterValues, scalars).map(round);
+ }
+
+ interpolateFromMastersAndScalars(masterValues, scalars, round = Math.round) {
+ let deltas = this.getDeltas(masterValues, round);
+ return VariationModel.interpolateFromDeltasAndScalars(deltas, scalars);
+ }
+}
+
+function piecewiseLinearMap(v, mapping) {
+ let keys = Object.keys(mapping).map(Number).sort((a, b) => a - b);
+
+ if (keys.length === 0) {
+ return v;
+ }
+ if (mapping.hasOwnProperty(v)) {
+ return mapping[v];
+ }
+
+ let k = Math.min(...keys);
+ if (v < k) {
+ return v + mapping[k] - k;
+ }
+
+ k = Math.max(...keys);
+ if (v > k) {
+ return v + mapping[k] - k;
+ }
+
+ // Interpolate
+ let a = Math.max(...keys.filter(key => key < v));
+ let b = Math.min(...keys.filter(key => key > v));
+ let va = mapping[a];
+ let vb = mapping[b];
+
+ return va + (vb - va) * (v - a) / (b - a);
+}
+
+
+/*
+const process = require('process');
+
+function main(args) {
+ // Assuming `args` is an array of command-line arguments
+ let logLevel = "INFO";
+ let designSpaceFile = null;
+ let locations = null;
+
+ for (let i = 0; i < args.length; i++) {
+ if (args[i] === "--loglevel" && i + 1 < args.length) {
+ logLevel = args[i + 1];
+ i++; // Skip next argument since it's part of this option
+ } else if (args[i] === "-d" && i + 1 < args.length) {
+ designSpaceFile = args[i + 1];
+ i++;
+ } else if (args[i] === "-l") {
+ locations = [];
+ for (let j = i + 1; j < args.length; j++) {
+ if (args[j].startsWith("-")) {
+ break; // Next argument is a new option
+ }
+ locations.push(args[j]);
+ }
+ }
+ }
+
+ // Configure logger based on logLevel
+ // JavaScript equivalent of logging configuration goes here
+
+ if (designSpaceFile) {
+ // Logic to handle design space file
+ // Omitted in translation as JavaScript doesn't have a direct equivalent of fontTools
+ } else if (locations) {
+ const axes = Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i));
+ const locs = locations.map(s => Object.fromEntries(axes.map((a, i) => [a, parseFloat(s.split(',')[i] || "0")])));
+
+ const model = new VariationModel(locs); // Assuming VariationModel is defined
+ console.log("Sorted locations:");
+ console.log(model.locations);
+ console.log("Supports:");
+ console.log(model.supports);
+ }
+}
+
+if (require.main === module) {
+ main(process.argv.slice(2));
+}
+
+*/
+
+export { normalizeValue, piecewiseLinearMap };
\ No newline at end of file