Skip to content

Commit

Permalink
Make antichiral sequences’ inflections less abrupt
Browse files Browse the repository at this point in the history
  • Loading branch information
dscorbett committed Sep 2, 2024
1 parent 52f6067 commit 3d5b2c6
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 28 deletions.
94 changes: 94 additions & 0 deletions sources/phases/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1762,6 +1762,99 @@ def rotate_diacritics(
return [lookup]


def avoid_abrupt_inflections(
builder: Builder,
original_schemas: OrderedSet[Schema],
schemas: OrderedSet[Schema],
new_schemas: OrderedSet[Schema],
classes: PrefixView[FreezableList[Schema]],
named_lookups: PrefixView[Lookup],
add_rule: AddRule,
) -> Sequence[Lookup]:
lookup = Lookup(
'rclt',
'dflt',
flags=fontTools.otlLib.builder.LOOKUP_FLAG_IGNORE_MARKS,
)
if len(original_schemas) != len(schemas):
return [lookup]
named_lookups['smooth_1'] = Lookup(None, None)
named_lookups['smooth_2'] = Lookup(None, None)
to_do_i_first = collections.defaultdict(list)
to_do_a_first = collections.defaultdict(list)
to_do_i_second = collections.defaultdict(list)
to_do_a_second = collections.defaultdict(list)
for schema in new_schemas:
if (schema.glyph_class != GlyphClass.JOINER or not isinstance(schema.path, Curve) or schema.path.angle_in % 90 != 0 or schema.path.angle_out % 90 != 0
or schema.path.reversed_circle or schema.path.stretch != 0 or schema.path.entry_position != 1 or schema.path.exit_position != 1
):
continue
if (schema.path.angle_in - schema.path.angle_out) % 360 == 180:
if not schema.diphthong_1:
to_do_i_first[schema.path.clockwise, float(schema.path.angle_out)].append(schema)
if not schema.diphthong_2:
to_do_i_second[not schema.path.clockwise, float(schema.path.angle_in)].append(schema)
if not schema.diphthong_1 and not schema.diphthong_2 and not issubclass(schema.original_shape, Curve):
da = schema.path.angle_out - schema.path.angle_in
if schema.path.clockwise:
da *= -1
da %= 360
if 270 <= da < 315:
to_do_a_first[schema.path.clockwise, float(schema.path.angle_out)].append(schema)
to_do_a_second[not schema.path.clockwise, float(schema.path.angle_in)].append(schema)
for (c_i, a_i), schemas_i in to_do_i_first.items():
for (c_a, a_a), schemas_a in to_do_a_second.items():
if c_i != c_a or a_i != a_a:
continue
class_name_i = f'IA_i_{c_i}_{a_i}'
class_name_a = f'IA_a_{c_a}_{a_a}'
class_name_s2_input = f'i_{class_name_i}_s2'
class_name_s1_input = f'i_{class_name_i}_s1'
class_name_s2_output = f'o_{class_name_i}_s2'
class_name_s1_output = f'o_{class_name_i}_s1'
classes[class_name_i].extend(schemas_i)
classes[class_name_a].extend(schemas_a)
for schema_i in schemas_i:
assert isinstance(schema_i.path, Curve)
schema_i_2 = schema_i.clone(cmap=None, path=schema_i.path.clone(smooth_1=True))
classes[class_name_s2_input].append(schema_i)
classes[class_name_s2_output].append(schema_i_2)
add_rule(named_lookups['smooth_1'], Rule(class_name_s2_input, class_name_s2_output))
for schema_a in schemas_a:
assert isinstance(schema_a.path, Curve)
schema_a_1 = schema_a.clone(cmap=None, path=schema_a.path.clone(smooth_2=True))
classes[class_name_s1_input].append(schema_a)
classes[class_name_s1_output].append(schema_a_1)
add_rule(named_lookups['smooth_2'], Rule(class_name_s1_input, class_name_s1_output))
add_rule(lookup, Rule([], [class_name_i, class_name_a], [], lookups=['smooth_1', 'smooth_2']))
for (c_a, a_a), schemas_a in to_do_a_first.items():
for (c_i, a_i), schemas_i in to_do_i_second.items():
if c_i != c_a or a_i != a_a:
continue
class_name_i = f'AI_i_{c_i}_{a_i}'
class_name_a = f'AI_a_{c_a}_{a_a}'
class_name_s2_input = f'i_{class_name_i}_s2'
class_name_s1_input = f'i_{class_name_i}_s1'
class_name_s2_output = f'o_{class_name_i}_s2'
class_name_s1_output = f'o_{class_name_i}_s1'
classes[class_name_i].extend(schemas_i)
classes[class_name_a].extend(schemas_a)
for schema_i in schemas_i:
assert isinstance(schema_i.path, Curve)
schema_i_1 = schema_i.clone(cmap=None, path=schema_i.path.clone(smooth_2=True))
classes[class_name_s1_input].append(schema_i)
classes[class_name_s1_output].append(schema_i_1)
add_rule(named_lookups['smooth_2'], Rule(class_name_s1_input, class_name_s1_output))
for schema_a in schemas_a:
assert isinstance(schema_a.path, Curve)
schema_a_2 = schema_a.clone(cmap=None, path=schema_a.path.clone(smooth_1=True))
classes[class_name_s2_input].append(schema_a)
classes[class_name_s2_output].append(schema_a_2)
add_rule(named_lookups['smooth_1'], Rule(class_name_s2_input, class_name_s2_output))
add_rule(lookup, Rule([], [class_name_a, class_name_i], [], lookups=['smooth_1', 'smooth_2']))
return [lookup]


def shade(
builder: Builder,
original_schemas: OrderedSet[Schema],
Expand Down Expand Up @@ -1943,6 +2036,7 @@ def classify_marks_for_trees(
join_double_marks,
separate_subantiparallel_lines,
rotate_diacritics,
avoid_abrupt_inflections,
shade,
create_superscripts_and_subscripts,
make_widthless_variants_of_marks,
Expand Down
87 changes: 61 additions & 26 deletions sources/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1921,6 +1921,10 @@ class Curve(Shape):
curve letter is in a context where it would look confusingly
like a loop, i.e. like a circle letter, it can be clearer to
shift the exit point earlier.
smooth_1: Whether the exit part of this curve is modified to
create a more gradual inflection with the following shape.
smooth_2: Whether the entry part of this curve is modified to
create a more gradual inflection with the preceding shape.
"""

@override
Expand All @@ -1940,6 +1944,8 @@ def __init__(
may_reposition_cursive_endpoints: bool = False,
entry_position: float = 1,
exit_position: float = 1,
smooth_1: bool = False,
smooth_2: bool = False,
) -> None:
"""Initializes this `Curve`.
Expand All @@ -1958,13 +1964,16 @@ def __init__(
may_reposition_cursive_endpoints: The ``may_reposition_cursive_endpoints`` attribute.
entry_position: The ``entry_position`` attribute.
exit_position: The ``exit_position`` attribute.
smooth_1: The ``smooth_1`` attribute.
smooth_2: The ``smooth_2`` attribute.
"""
assert overlap_angle is None or abs(angle_out - angle_in) == 180, 'Only a semicircle may have an overlap angle'
assert stretch > -1
assert entry_position == 1 or may_reposition_cursive_endpoints, f'{entry_position=}'
assert exit_position == 1 or may_reposition_cursive_endpoints, f'{exit_position=}'
assert 0 <= entry_position <= 1
assert 0 <= exit_position <= 1
assert not (smooth_1 and smooth_2)
self.angle_in: Final = angle_in
self.angle_out: Final = angle_out
self.clockwise: Final = clockwise
Expand All @@ -1978,6 +1987,8 @@ def __init__(
self.may_reposition_cursive_endpoints: Final = may_reposition_cursive_endpoints
self.entry_position: Final = entry_position
self.exit_position: Final = exit_position
self.smooth_1: Final = smooth_1
self.smooth_2: Final = smooth_2

@override
def clone(
Expand All @@ -1996,6 +2007,8 @@ def clone(
may_reposition_cursive_endpoints: bool | CloneDefault = CLONE_DEFAULT,
entry_position: float | CloneDefault = CLONE_DEFAULT,
exit_position: float | CloneDefault = CLONE_DEFAULT,
smooth_1: bool | CloneDefault = CLONE_DEFAULT,
smooth_2: bool | CloneDefault = CLONE_DEFAULT,
) -> Self:
return type(self)(
self.angle_in if angle_in is CLONE_DEFAULT else angle_in,
Expand All @@ -2013,25 +2026,37 @@ def clone(
),
entry_position=self.entry_position if entry_position is CLONE_DEFAULT else entry_position,
exit_position=self.exit_position if exit_position is CLONE_DEFAULT else exit_position,
smooth_1=self.smooth_1 if smooth_1 is CLONE_DEFAULT else smooth_1,
smooth_2=self.smooth_2 if smooth_2 is CLONE_DEFAULT else smooth_2,
)

@override
def get_name(self, size: float, joining_type: Type) -> str:
if self.overlap_angle is not None:
return f'{int(self.overlap_angle)}'
if joining_type != Type.ORIENTING:
return ''
return f'''{
int(self.angle_in)
}{
'n' if self.clockwise else 'p'
}{
int(self.angle_out)
}{
'r' if self.reversed_circle else ''
}{
'.ee' if not self.entry_position == self.exit_position == 1 else ''
}'''
name = f'{int(self.overlap_angle)}'
elif joining_type == Type.ORIENTING:
name = f'''{
int(self.angle_in)
}{
'n' if self.clockwise else 'p'
}{
int(self.angle_out)
}{
'r' if self.reversed_circle else ''
}{
'.ee' if not self.entry_position == self.exit_position == 1 else ''
}'''
else:
name = ''
if self.smooth_1 or self.smooth_2:
name += f'''{
'.' if name else ''
}s{
'1' if self.smooth_1 else ''
}{
'2' if self.smooth_2 else ''
}'''
return name

@override
def group(self) -> Hashable:
Expand Down Expand Up @@ -2060,6 +2085,8 @@ def group(self) -> Hashable:
self.overlap_angle,
self.entry_position,
self.exit_position,
self.smooth_1,
self.smooth_2,
)

@override
Expand Down Expand Up @@ -2118,19 +2145,17 @@ def _pre_stretch_values(self) -> tuple[float, float, float, float, float]:

def get_normalized_angles(
self,
diphthong_1: bool = False,
diphthong_2: bool = False,
offset_1: float = 0,
offset_2: float = 0,
angle_in: float | None = None,
angle_out: float | None = None,
) -> tuple[float, float]:
if angle_in is None:
angle_in = self.angle_in
if angle_out is None:
angle_out = self.angle_out
if diphthong_1:
angle_out = (angle_out + 90 * (1 if self.clockwise else -1)) % 360
if diphthong_2:
angle_in = (angle_in - 90 * (1 if self.clockwise else -1)) % 360
angle_out = (angle_out + offset_1 * (1 if self.clockwise else -1)) % 360
angle_in = (angle_in - offset_2 * (1 if self.clockwise else -1)) % 360
if self.clockwise and angle_out > angle_in:
angle_out -= 360
elif not self.clockwise and angle_out < angle_in:
Expand All @@ -2141,14 +2166,14 @@ def get_normalized_angles(

def _get_normalized_angles_and_da(
self,
diphthong_1: bool,
diphthong_2: bool,
offset_1: float,
offset_2: float,
final_circle_diphthong: bool,
initial_circle_diphthong: bool,
angle_in: float | None = None,
angle_out: float | None = None,
) -> tuple[float, float, float]:
a1, a2 = self.get_normalized_angles(diphthong_1, diphthong_2, angle_in, angle_out)
a1, a2 = self.get_normalized_angles(offset_1, offset_2, angle_in, angle_out)
if final_circle_diphthong:
a2 = a1
elif initial_circle_diphthong:
Expand All @@ -2172,7 +2197,7 @@ def get_da(
The difference between this curve’s entry angle and exit
angle in the range (0, 360].
"""
return self._get_normalized_angles_and_da(False, False, False, False, angle_in, angle_out)[2]
return self._get_normalized_angles_and_da(0, 0, False, False, angle_in, angle_out)[2]

def _get_angle_to_overlap_point(
self,
Expand Down Expand Up @@ -2234,9 +2259,10 @@ def draw(
) -> tuple[float, float, float, float] | None:
pen = glyph.glyphPen()
pre_stretch_angle_in, pre_stretch_angle_out, stretch_axis_angle, scale_x, scale_y = self._pre_stretch_values
smooth_delta = 45
a1, a2, da = self._get_normalized_angles_and_da(
diphthong_1,
diphthong_2,
90 if diphthong_1 else smooth_delta if self.smooth_1 else 0,
90 if diphthong_2 else smooth_delta if self.smooth_2 else 0,
final_circle_diphthong,
initial_circle_diphthong,
pre_stretch_angle_in,
Expand All @@ -2254,6 +2280,11 @@ def draw(
entry = (p0[0] + entry_delta[0], p0[1] + entry_delta[1])
pen.moveTo(entry)
pen.lineTo(p0)
elif self.smooth_2:
entry_delta = _rect(-r, math.radians((self.angle_in - smooth_delta * (1 if self.clockwise else -1)) % 360))
entry = (p0[0] + entry_delta[0], p0[1] + entry_delta[1])
pen.moveTo(entry)
pen.lineTo(p0)
else:
entry = p0
pen.moveTo(entry)
Expand Down Expand Up @@ -2305,6 +2336,10 @@ def draw(
exit_delta = _rect(r, math.radians((a2 - 90 * (1 if self.clockwise else -1)) % 360))
exit = (exit[0] + exit_delta[0], exit[1] + exit_delta[1])
pen.lineTo(exit)
elif self.smooth_1:
exit_delta = _rect(r, math.radians((self.angle_out + smooth_delta * (1 if self.clockwise else -1)) % 360))
exit = (exit[0] + exit_delta[0], exit[1] + exit_delta[1])
pen.lineTo(exit)
pen.endPath()
relative_mark_angle = (a1 + a2) / 2
if not anchor and joining_type != Type.NON_JOINING:
Expand Down
9 changes: 7 additions & 2 deletions tests/antichiral-sequences.test
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,10 @@
# limitations under the License.

# Semicircle letter before circle letter
1BC4A 1BC41 1BC19::[u1BC4A.ui@0,620|u1BC41.a.270p180@200,520|u1BC19.m@0,0|_@640,0]
1BC4A 1BC44 1BC19::[u1BC4A.ui@0,570|u1BC44.o.270p180@200,420|u1BC19.m@50,0|_@740,0]
1BC4A 1BC41 1BC19::[u1BC4A.ui.s1@0,620|u1BC41.a.270p180.s2@241,520|u1BC19.m@82,0|_@722,0]
1BC4A 1BC44 1BC19::[u1BC4A.ui.s1@0,570|u1BC44.o.270p180.s2@241,420|u1BC19.m@153,0|_@843,0]

# Circle letter before semicircle letter
1BC1A 1BC41 1BC4B::[u1BC1A.n@100,0|u1BC41.a.180n270.s1@0,0|u1BC4B.ee.s2@241,0|_@722,0]
1BC1A 1BC44 1BC4B::[u1BC1A.n@150,0|u1BC44.o.180n270.s1@0,0|u1BC4B.ee.s2@362,50|_@843,0]
1BC1A 1BC56 1BC4B::[u1BC1A.n@150,0|u1BC44.o.180n270.s1@0,0|dupl_.Dot.0.rel1@150,150|u1BC4B.ee.s2@362,50|_@843,0]

0 comments on commit 3d5b2c6

Please sign in to comment.