You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			491 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			491 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
"""GlyphSets returned by a TTFont."""
 | 
						|
 | 
						|
from abc import ABC, abstractmethod
 | 
						|
from collections.abc import Mapping
 | 
						|
from contextlib import contextmanager
 | 
						|
from copy import copy, deepcopy
 | 
						|
from types import SimpleNamespace
 | 
						|
from fontTools.misc.vector import Vector
 | 
						|
from fontTools.misc.fixedTools import otRound, fixedToFloat as fi2fl
 | 
						|
from fontTools.misc.loggingTools import deprecateFunction
 | 
						|
from fontTools.misc.transform import Transform, DecomposedTransform
 | 
						|
from fontTools.pens.transformPen import TransformPen, TransformPointPen
 | 
						|
from fontTools.pens.recordingPen import (
 | 
						|
    DecomposingRecordingPen,
 | 
						|
    lerpRecordings,
 | 
						|
    replayRecording,
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
class _TTGlyphSet(Mapping):
 | 
						|
    """Generic dict-like GlyphSet class that pulls metrics from hmtx and
 | 
						|
    glyph shape from TrueType or CFF.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, font, location, glyphsMapping, *, recalcBounds=True):
 | 
						|
        self.recalcBounds = recalcBounds
 | 
						|
        self.font = font
 | 
						|
        self.defaultLocationNormalized = (
 | 
						|
            {axis.axisTag: 0 for axis in self.font["fvar"].axes}
 | 
						|
            if "fvar" in self.font
 | 
						|
            else {}
 | 
						|
        )
 | 
						|
        self.location = location if location is not None else {}
 | 
						|
        self.rawLocation = {}  # VarComponent-only location
 | 
						|
        self.originalLocation = location if location is not None else {}
 | 
						|
        self.depth = 0
 | 
						|
        self.locationStack = []
 | 
						|
        self.rawLocationStack = []
 | 
						|
        self.glyphsMapping = glyphsMapping
 | 
						|
        self.hMetrics = font["hmtx"].metrics
 | 
						|
        self.vMetrics = getattr(font.get("vmtx"), "metrics", None)
 | 
						|
        self.hvarTable = None
 | 
						|
        if location:
 | 
						|
            from fontTools.varLib.varStore import VarStoreInstancer
 | 
						|
 | 
						|
            self.hvarTable = getattr(font.get("HVAR"), "table", None)
 | 
						|
            if self.hvarTable is not None:
 | 
						|
                self.hvarInstancer = VarStoreInstancer(
 | 
						|
                    self.hvarTable.VarStore, font["fvar"].axes, location
 | 
						|
                )
 | 
						|
            # TODO VVAR, VORG
 | 
						|
 | 
						|
    @contextmanager
 | 
						|
    def pushLocation(self, location, reset: bool):
 | 
						|
        self.locationStack.append(self.location)
 | 
						|
        self.rawLocationStack.append(self.rawLocation)
 | 
						|
        if reset:
 | 
						|
            self.location = self.originalLocation.copy()
 | 
						|
            self.rawLocation = self.defaultLocationNormalized.copy()
 | 
						|
        else:
 | 
						|
            self.location = self.location.copy()
 | 
						|
            self.rawLocation = {}
 | 
						|
        self.location.update(location)
 | 
						|
        self.rawLocation.update(location)
 | 
						|
 | 
						|
        try:
 | 
						|
            yield None
 | 
						|
        finally:
 | 
						|
            self.location = self.locationStack.pop()
 | 
						|
            self.rawLocation = self.rawLocationStack.pop()
 | 
						|
 | 
						|
    @contextmanager
 | 
						|
    def pushDepth(self):
 | 
						|
        try:
 | 
						|
            depth = self.depth
 | 
						|
            self.depth += 1
 | 
						|
            yield depth
 | 
						|
        finally:
 | 
						|
            self.depth -= 1
 | 
						|
 | 
						|
    def __contains__(self, glyphName):
 | 
						|
        return glyphName in self.glyphsMapping
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        return iter(self.glyphsMapping.keys())
 | 
						|
 | 
						|
    def __len__(self):
 | 
						|
        return len(self.glyphsMapping)
 | 
						|
 | 
						|
    @deprecateFunction(
 | 
						|
        "use 'glyphName in glyphSet' instead", category=DeprecationWarning
 | 
						|
    )
 | 
						|
    def has_key(self, glyphName):
 | 
						|
        return glyphName in self.glyphsMapping
 | 
						|
 | 
						|
 | 
						|
class _TTGlyphSetGlyf(_TTGlyphSet):
 | 
						|
    def __init__(self, font, location, recalcBounds=True):
 | 
						|
        self.glyfTable = font["glyf"]
 | 
						|
        super().__init__(font, location, self.glyfTable, recalcBounds=recalcBounds)
 | 
						|
        self.gvarTable = font.get("gvar")
 | 
						|
 | 
						|
    def __getitem__(self, glyphName):
 | 
						|
        return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds)
 | 
						|
 | 
						|
 | 
						|
class _TTGlyphSetCFF(_TTGlyphSet):
 | 
						|
    def __init__(self, font, location):
 | 
						|
        tableTag = "CFF2" if "CFF2" in font else "CFF "
 | 
						|
        self.charStrings = list(font[tableTag].cff.values())[0].CharStrings
 | 
						|
        super().__init__(font, location, self.charStrings)
 | 
						|
        self.setLocation(location)
 | 
						|
 | 
						|
    def __getitem__(self, glyphName):
 | 
						|
        return _TTGlyphCFF(self, glyphName)
 | 
						|
 | 
						|
    def setLocation(self, location):
 | 
						|
        self.blender = None
 | 
						|
        if location:
 | 
						|
            # TODO Optimize by using instancer.setLocation()
 | 
						|
 | 
						|
            from fontTools.varLib.varStore import VarStoreInstancer
 | 
						|
 | 
						|
            varStore = getattr(self.charStrings, "varStore", None)
 | 
						|
            if varStore is not None:
 | 
						|
                instancer = VarStoreInstancer(
 | 
						|
                    varStore.otVarStore, self.font["fvar"].axes, location
 | 
						|
                )
 | 
						|
                self.blender = instancer.interpolateFromDeltas
 | 
						|
        else:
 | 
						|
            self.blender = None
 | 
						|
 | 
						|
    @contextmanager
 | 
						|
    def pushLocation(self, location, reset: bool):
 | 
						|
        self.setLocation(location)
 | 
						|
        with _TTGlyphSet.pushLocation(self, location, reset) as value:
 | 
						|
            try:
 | 
						|
                yield value
 | 
						|
            finally:
 | 
						|
                self.setLocation(self.location)
 | 
						|
 | 
						|
 | 
						|
class _TTGlyphSetVARC(_TTGlyphSet):
 | 
						|
    def __init__(self, font, location, glyphSet):
 | 
						|
        self.glyphSet = glyphSet
 | 
						|
        super().__init__(font, location, glyphSet)
 | 
						|
        self.varcTable = font["VARC"].table
 | 
						|
 | 
						|
    def __getitem__(self, glyphName):
 | 
						|
        varc = self.varcTable
 | 
						|
        if glyphName not in varc.Coverage.glyphs:
 | 
						|
            return self.glyphSet[glyphName]
 | 
						|
        return _TTGlyphVARC(self, glyphName)
 | 
						|
 | 
						|
 | 
						|
class _TTGlyph(ABC):
 | 
						|
    """Glyph object that supports the Pen protocol, meaning that it has
 | 
						|
    .draw() and .drawPoints() methods that take a pen object as their only
 | 
						|
    argument. Additionally there are 'width' and 'lsb' attributes, read from
 | 
						|
    the 'hmtx' table.
 | 
						|
 | 
						|
    If the font contains a 'vmtx' table, there will also be 'height' and 'tsb'
 | 
						|
    attributes.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, glyphSet, glyphName, *, recalcBounds=True):
 | 
						|
        self.glyphSet = glyphSet
 | 
						|
        self.name = glyphName
 | 
						|
        self.recalcBounds = recalcBounds
 | 
						|
        self.width, self.lsb = glyphSet.hMetrics[glyphName]
 | 
						|
        if glyphSet.vMetrics is not None:
 | 
						|
            self.height, self.tsb = glyphSet.vMetrics[glyphName]
 | 
						|
        else:
 | 
						|
            self.height, self.tsb = None, None
 | 
						|
        if glyphSet.location and glyphSet.hvarTable is not None:
 | 
						|
            varidx = (
 | 
						|
                glyphSet.font.getGlyphID(glyphName)
 | 
						|
                if glyphSet.hvarTable.AdvWidthMap is None
 | 
						|
                else glyphSet.hvarTable.AdvWidthMap.mapping[glyphName]
 | 
						|
            )
 | 
						|
            self.width += glyphSet.hvarInstancer[varidx]
 | 
						|
        # TODO: VVAR/VORG
 | 
						|
 | 
						|
    @abstractmethod
 | 
						|
    def draw(self, pen):
 | 
						|
        """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
 | 
						|
        how that works.
 | 
						|
        """
 | 
						|
        raise NotImplementedError
 | 
						|
 | 
						|
    def drawPoints(self, pen):
 | 
						|
        """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details
 | 
						|
        how that works.
 | 
						|
        """
 | 
						|
        from fontTools.pens.pointPen import SegmentToPointPen
 | 
						|
 | 
						|
        self.draw(SegmentToPointPen(pen))
 | 
						|
 | 
						|
 | 
						|
class _TTGlyphGlyf(_TTGlyph):
 | 
						|
    def draw(self, pen):
 | 
						|
        """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
 | 
						|
        how that works.
 | 
						|
        """
 | 
						|
        glyph, offset = self._getGlyphAndOffset()
 | 
						|
 | 
						|
        with self.glyphSet.pushDepth() as depth:
 | 
						|
            if depth:
 | 
						|
                offset = 0  # Offset should only apply at top-level
 | 
						|
 | 
						|
            glyph.draw(pen, self.glyphSet.glyfTable, offset)
 | 
						|
 | 
						|
    def drawPoints(self, pen):
 | 
						|
        """Draw the glyph onto ``pen``. See fontTools.pens.pointPen for details
 | 
						|
        how that works.
 | 
						|
        """
 | 
						|
        glyph, offset = self._getGlyphAndOffset()
 | 
						|
 | 
						|
        with self.glyphSet.pushDepth() as depth:
 | 
						|
            if depth:
 | 
						|
                offset = 0  # Offset should only apply at top-level
 | 
						|
 | 
						|
            glyph.drawPoints(pen, self.glyphSet.glyfTable, offset)
 | 
						|
 | 
						|
    def _getGlyphAndOffset(self):
 | 
						|
        if self.glyphSet.location and self.glyphSet.gvarTable is not None:
 | 
						|
            glyph = self._getGlyphInstance()
 | 
						|
        else:
 | 
						|
            glyph = self.glyphSet.glyfTable[self.name]
 | 
						|
 | 
						|
        offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
 | 
						|
        return glyph, offset
 | 
						|
 | 
						|
    def _getGlyphInstance(self):
 | 
						|
        from fontTools.varLib.iup import iup_delta
 | 
						|
        from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
 | 
						|
        from fontTools.varLib.models import supportScalar
 | 
						|
 | 
						|
        glyphSet = self.glyphSet
 | 
						|
        glyfTable = glyphSet.glyfTable
 | 
						|
        variations = glyphSet.gvarTable.variations[self.name]
 | 
						|
        hMetrics = glyphSet.hMetrics
 | 
						|
        vMetrics = glyphSet.vMetrics
 | 
						|
        coordinates, _ = glyfTable._getCoordinatesAndControls(
 | 
						|
            self.name, hMetrics, vMetrics
 | 
						|
        )
 | 
						|
        origCoords, endPts = None, None
 | 
						|
        for var in variations:
 | 
						|
            scalar = supportScalar(glyphSet.location, var.axes)
 | 
						|
            if not scalar:
 | 
						|
                continue
 | 
						|
            delta = var.coordinates
 | 
						|
            if None in delta:
 | 
						|
                if origCoords is None:
 | 
						|
                    origCoords, control = glyfTable._getCoordinatesAndControls(
 | 
						|
                        self.name, hMetrics, vMetrics
 | 
						|
                    )
 | 
						|
                    endPts = (
 | 
						|
                        control[1] if control[0] >= 1 else list(range(len(control[1])))
 | 
						|
                    )
 | 
						|
                delta = iup_delta(delta, origCoords, endPts)
 | 
						|
            coordinates += GlyphCoordinates(delta) * scalar
 | 
						|
 | 
						|
        glyph = copy(glyfTable[self.name])  # Shallow copy
 | 
						|
        width, lsb, height, tsb = _setCoordinates(
 | 
						|
            glyph, coordinates, glyfTable, recalcBounds=self.recalcBounds
 | 
						|
        )
 | 
						|
        self.lsb = lsb
 | 
						|
        self.tsb = tsb
 | 
						|
        if glyphSet.hvarTable is None:
 | 
						|
            # no HVAR: let's set metrics from the phantom points
 | 
						|
            self.width = width
 | 
						|
            self.height = height
 | 
						|
        return glyph
 | 
						|
 | 
						|
 | 
						|
class _TTGlyphCFF(_TTGlyph):
 | 
						|
    def draw(self, pen):
 | 
						|
        """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
 | 
						|
        how that works.
 | 
						|
        """
 | 
						|
        self.glyphSet.charStrings[self.name].draw(pen, self.glyphSet.blender)
 | 
						|
 | 
						|
 | 
						|
def _evaluateCondition(condition, fvarAxes, location, instancer):
 | 
						|
    if condition.Format == 1:
 | 
						|
        # ConditionAxisRange
 | 
						|
        axisIndex = condition.AxisIndex
 | 
						|
        axisTag = fvarAxes[axisIndex].axisTag
 | 
						|
        axisValue = location.get(axisTag, 0)
 | 
						|
        minValue = condition.FilterRangeMinValue
 | 
						|
        maxValue = condition.FilterRangeMaxValue
 | 
						|
        return minValue <= axisValue <= maxValue
 | 
						|
    elif condition.Format == 2:
 | 
						|
        # ConditionValue
 | 
						|
        value = condition.DefaultValue
 | 
						|
        value += instancer[condition.VarIdx][0]
 | 
						|
        return value > 0
 | 
						|
    elif condition.Format == 3:
 | 
						|
        # ConditionAnd
 | 
						|
        for subcondition in condition.ConditionTable:
 | 
						|
            if not _evaluateCondition(subcondition, fvarAxes, location, instancer):
 | 
						|
                return False
 | 
						|
        return True
 | 
						|
    elif condition.Format == 4:
 | 
						|
        # ConditionOr
 | 
						|
        for subcondition in condition.ConditionTable:
 | 
						|
            if _evaluateCondition(subcondition, fvarAxes, location, instancer):
 | 
						|
                return True
 | 
						|
        return False
 | 
						|
    elif condition.Format == 5:
 | 
						|
        # ConditionNegate
 | 
						|
        return not _evaluateCondition(
 | 
						|
            condition.conditionTable, fvarAxes, location, instancer
 | 
						|
        )
 | 
						|
    else:
 | 
						|
        return False  # Unkonwn condition format
 | 
						|
 | 
						|
 | 
						|
class _TTGlyphVARC(_TTGlyph):
 | 
						|
    def _draw(self, pen, isPointPen):
 | 
						|
        """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
 | 
						|
        how that works.
 | 
						|
        """
 | 
						|
        from fontTools.ttLib.tables.otTables import (
 | 
						|
            VarComponentFlags,
 | 
						|
            NO_VARIATION_INDEX,
 | 
						|
        )
 | 
						|
 | 
						|
        glyphSet = self.glyphSet
 | 
						|
        varc = glyphSet.varcTable
 | 
						|
        idx = varc.Coverage.glyphs.index(self.name)
 | 
						|
        glyph = varc.VarCompositeGlyphs.VarCompositeGlyph[idx]
 | 
						|
 | 
						|
        from fontTools.varLib.multiVarStore import MultiVarStoreInstancer
 | 
						|
        from fontTools.varLib.varStore import VarStoreInstancer
 | 
						|
 | 
						|
        fvarAxes = glyphSet.font["fvar"].axes
 | 
						|
        instancer = MultiVarStoreInstancer(
 | 
						|
            varc.MultiVarStore, fvarAxes, self.glyphSet.location
 | 
						|
        )
 | 
						|
 | 
						|
        for comp in glyph.components:
 | 
						|
            if comp.flags & VarComponentFlags.HAVE_CONDITION:
 | 
						|
                condition = varc.ConditionList.ConditionTable[comp.conditionIndex]
 | 
						|
                if not _evaluateCondition(
 | 
						|
                    condition, fvarAxes, self.glyphSet.location, instancer
 | 
						|
                ):
 | 
						|
                    continue
 | 
						|
 | 
						|
            location = {}
 | 
						|
            if comp.axisIndicesIndex is not None:
 | 
						|
                axisIndices = varc.AxisIndicesList.Item[comp.axisIndicesIndex]
 | 
						|
                axisValues = Vector(comp.axisValues)
 | 
						|
                if comp.axisValuesVarIndex != NO_VARIATION_INDEX:
 | 
						|
                    axisValues += fi2fl(instancer[comp.axisValuesVarIndex], 14)
 | 
						|
                assert len(axisIndices) == len(axisValues), (
 | 
						|
                    len(axisIndices),
 | 
						|
                    len(axisValues),
 | 
						|
                )
 | 
						|
                location = {
 | 
						|
                    fvarAxes[i].axisTag: v for i, v in zip(axisIndices, axisValues)
 | 
						|
                }
 | 
						|
 | 
						|
            if comp.transformVarIndex != NO_VARIATION_INDEX:
 | 
						|
                deltas = instancer[comp.transformVarIndex]
 | 
						|
                comp = deepcopy(comp)
 | 
						|
                comp.applyTransformDeltas(deltas)
 | 
						|
            transform = comp.transform
 | 
						|
 | 
						|
            reset = comp.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES
 | 
						|
            with self.glyphSet.glyphSet.pushLocation(location, reset):
 | 
						|
                with self.glyphSet.pushLocation(location, reset):
 | 
						|
                    shouldDecompose = self.name == comp.glyphName
 | 
						|
 | 
						|
                    if not shouldDecompose:
 | 
						|
                        try:
 | 
						|
                            pen.addVarComponent(
 | 
						|
                                comp.glyphName, transform, self.glyphSet.rawLocation
 | 
						|
                            )
 | 
						|
                        except AttributeError:
 | 
						|
                            shouldDecompose = True
 | 
						|
 | 
						|
                    if shouldDecompose:
 | 
						|
                        t = transform.toTransform()
 | 
						|
                        compGlyphSet = (
 | 
						|
                            self.glyphSet
 | 
						|
                            if comp.glyphName != self.name
 | 
						|
                            else glyphSet.glyphSet
 | 
						|
                        )
 | 
						|
                        g = compGlyphSet[comp.glyphName]
 | 
						|
                        if isPointPen:
 | 
						|
                            tPen = TransformPointPen(pen, t)
 | 
						|
                            g.drawPoints(tPen)
 | 
						|
                        else:
 | 
						|
                            tPen = TransformPen(pen, t)
 | 
						|
                            g.draw(tPen)
 | 
						|
 | 
						|
    def draw(self, pen):
 | 
						|
        self._draw(pen, False)
 | 
						|
 | 
						|
    def drawPoints(self, pen):
 | 
						|
        self._draw(pen, True)
 | 
						|
 | 
						|
 | 
						|
def _setCoordinates(glyph, coord, glyfTable, *, recalcBounds=True):
 | 
						|
    # Handle phantom points for (left, right, top, bottom) positions.
 | 
						|
    assert len(coord) >= 4
 | 
						|
    leftSideX = coord[-4][0]
 | 
						|
    rightSideX = coord[-3][0]
 | 
						|
    topSideY = coord[-2][1]
 | 
						|
    bottomSideY = coord[-1][1]
 | 
						|
 | 
						|
    for _ in range(4):
 | 
						|
        del coord[-1]
 | 
						|
 | 
						|
    if glyph.isComposite():
 | 
						|
        assert len(coord) == len(glyph.components)
 | 
						|
        glyph.components = [copy(comp) for comp in glyph.components]  # Shallow copy
 | 
						|
        for p, comp in zip(coord, glyph.components):
 | 
						|
            if hasattr(comp, "x"):
 | 
						|
                comp.x, comp.y = p
 | 
						|
    elif glyph.numberOfContours == 0:
 | 
						|
        assert len(coord) == 0
 | 
						|
    else:
 | 
						|
        assert len(coord) == len(glyph.coordinates)
 | 
						|
        glyph.coordinates = coord
 | 
						|
 | 
						|
    if recalcBounds:
 | 
						|
        glyph.recalcBounds(glyfTable)
 | 
						|
 | 
						|
    horizontalAdvanceWidth = otRound(rightSideX - leftSideX)
 | 
						|
    verticalAdvanceWidth = otRound(topSideY - bottomSideY)
 | 
						|
    leftSideBearing = otRound(glyph.xMin - leftSideX)
 | 
						|
    topSideBearing = otRound(topSideY - glyph.yMax)
 | 
						|
    return (
 | 
						|
        horizontalAdvanceWidth,
 | 
						|
        leftSideBearing,
 | 
						|
        verticalAdvanceWidth,
 | 
						|
        topSideBearing,
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
class LerpGlyphSet(Mapping):
 | 
						|
    """A glyphset that interpolates between two other glyphsets.
 | 
						|
 | 
						|
    Factor is typically between 0 and 1. 0 means the first glyphset,
 | 
						|
    1 means the second glyphset, and 0.5 means the average of the
 | 
						|
    two glyphsets. Other values are possible, and can be useful to
 | 
						|
    extrapolate. Defaults to 0.5.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, glyphset1, glyphset2, factor=0.5):
 | 
						|
        self.glyphset1 = glyphset1
 | 
						|
        self.glyphset2 = glyphset2
 | 
						|
        self.factor = factor
 | 
						|
 | 
						|
    def __getitem__(self, glyphname):
 | 
						|
        if glyphname in self.glyphset1 and glyphname in self.glyphset2:
 | 
						|
            return LerpGlyph(glyphname, self)
 | 
						|
        raise KeyError(glyphname)
 | 
						|
 | 
						|
    def __contains__(self, glyphname):
 | 
						|
        return glyphname in self.glyphset1 and glyphname in self.glyphset2
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        set1 = set(self.glyphset1)
 | 
						|
        set2 = set(self.glyphset2)
 | 
						|
        return iter(set1.intersection(set2))
 | 
						|
 | 
						|
    def __len__(self):
 | 
						|
        set1 = set(self.glyphset1)
 | 
						|
        set2 = set(self.glyphset2)
 | 
						|
        return len(set1.intersection(set2))
 | 
						|
 | 
						|
 | 
						|
class LerpGlyph:
 | 
						|
    def __init__(self, glyphname, glyphset):
 | 
						|
        self.glyphset = glyphset
 | 
						|
        self.glyphname = glyphname
 | 
						|
 | 
						|
    def draw(self, pen):
 | 
						|
        recording1 = DecomposingRecordingPen(self.glyphset.glyphset1)
 | 
						|
        self.glyphset.glyphset1[self.glyphname].draw(recording1)
 | 
						|
        recording2 = DecomposingRecordingPen(self.glyphset.glyphset2)
 | 
						|
        self.glyphset.glyphset2[self.glyphname].draw(recording2)
 | 
						|
 | 
						|
        factor = self.glyphset.factor
 | 
						|
 | 
						|
        replayRecording(lerpRecordings(recording1.value, recording2.value, factor), pen)
 |