A simple greeting
My name
The project I’m here to talk about — a cross-platform library to help you precisely & programmatically do high-quality display typography with Python — kind of like DrawBot, but more oriented towards animation and an abbreviated programming style
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from coldtype import *
fatface = Font("~/Type/fonts/fonts/OhnoFatfaceVariable.ttf")
@animation(timeline=Timeline(100, storyboard=[100]), bg=0)
def render(f):
at = f.a.progress(f.i, loops=1, easefn="eeio")
c1, c2 = [r.inset(20, 5) for r in f.a.r.inset(0, 50).divide(0.15+at.e*0.7, "maxy")]
s = Style(fatface, t=-25, wdth=1, wght=1, ro=1, r=1)
stacked_and = StyledString("STACKED &", s.mod(fitHeight=c1.h, opsz=at.e)).fit(c1).pens()
justified = StyledString("JUSTIFIED", s.mod(fitHeight=c2.h, opsz=1-at.e)).fit(c2).pens()
return DATPenSet([
stacked_and.align(c1).trackToRect(c1, pullToEdges=1, r=1).f(1),
justified.align(c2).trackToRect(c2, pullToEdges=1, r=1).f(1)
]).understroke(s=0, sw=10)
|
Short-but-dense code and the resulting animation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | from coldtype import *
from coldtype.animation.midi import MidiReader
from coldtype.warping import warp_fn
import noise
# Load the designspace directly (no need for fonts)
obvs = Font("fonts/ColdtypeObviously.designspace")
# Load the MIDI before our render, since this won’t change between renders
drums = MidiReader("animations/media/808.mid", duration=120, bpm=120)[0]
# Load our logos ufo for branding-purposes
# (N.B. here we are loading a defcon.Font directly, since the logos.ufo
# isn’t really a font even in spirit, it’s just a collection of outlines
# keyed by some names)
logos = raw_ufo("fonts/logos.ufo")
@animation(duration=drums.duration, bg=0.2, storyboard=[105])
def render(f):
# Get the kick and cowbell values b/c we’re going to use
# these in the initial lockup
kick = drums.fv(f.i, [36], [5, 50])
cowbell = drums.fv(f.i, [47], [15, 75])
### INITIAL LOCKUP
# Use the `Graf` to set two StyledStrings vertically
# Adjust the tracking (via `tu` (a shortcut for (t)racking-in-font-(u)nits))
# using the cowbell midi note, using standard easing
# also rekern the T & Y together to be a little closer (via the (k)ern-(p)airs)
# so when they get stroked they don’t create an overly black visual mass
style = Style(obvs, 390, tu=-150+550*cowbell.ease(), wdth=1-cowbell.ease()*0.75, ro=1, r=1, kp={"T/Y":-25})
strings = [StyledString(t, style) for t in ["COLD", "TYPE"]]
pens = Graf(strings, f.a.r, leading=math.floor(kick.ease()*50)).pens().align(f.a.r)
### SNARE (+claps)
# Visualize the snare hits by shearing the line composition
# & rotating the two letters that correspond to where the snares hit in an 8-count
snare = drums.fv(f.i, [40], [10, 40])
if snare.count == 1:
pens[0].translate(-150*snare.ease(), 0)
pens[1].translate(150*snare.ease(), 0)
pens[0].ffg("L").rotate(snare.ease()*-270)
else:
pens[0].translate(150*snare.ease(), 0)
pens[1].translate(-150*snare.ease(), 0)
# Rotate the outer P shape w/o moving the counter
pens[1].ffg("P").mod_contour(0, lambda c: c.rotate(snare.ease()*270))
### RIMSHOT
# When the second rim hits (we ignore the first one b/c it’s in sync with a hat (see below)),
# let’s rotate the P’s counter
rim = drums.fv(f.i, [39], [5, 5])
if rim.count == 2:
pens[1].ffg("P").mod_contour(1, lambda c: c.rotate(rim.ease()*-270))
### BIG KICKS
# Use the kick signal to scale up some letters
line, glyph = (0, "C") if kick.count == 1 else (1, "Y")
pens[line].ffg(glyph).scale(1+0.5*kick.ease())
### HI-HATS
# Definitely the most complicated bit of all the code:
# Get the hat signal from the midi, with an even preverb-reverb (10, 10)
# To mimic the regular action of a drummer hitting a hi-hat
hat = drums.fv(f.i, [43], [10, 10])
# For the first hat, move the counter of the O in the first line
if hat.count == 1:
pens[0].ffg("O").mod_contour(1, lambda c: c.translate(80*hat.ease(), 0))
# For the second hat, move the counter of the D in the first line
# This time make it a little fancier, first translating it down
# & then rotating it as well, to make it seem like it's falling
# and then bouncing back up from the bottom of the outer shape
elif hat.count == 2:
pens[0].ffg("D").mod_contour(1, lambda c: c.translate(-30*hat.ease(), -100*hat.ease()).rotate(hat.ease()*110))
# And now for our most complicated trick...
# Move the T crossbar, first on the left-hand side (hat 3)
# And then on the right-hand side (hat 4)
elif hat.count in [3, 4]:
def move_t_top(idx, x, y):
if hat.count == 3 and 0 <= idx <= 6:
return x-150*hat.ease(), y
elif hat.count == 4 and 22 <= idx <= 30:
return x+150*hat.ease(), y
pens[1].ffg("T").map_points(move_t_top)
# Finally (for the hats), exaggerate the horizontality of the E counters
elif hat.count == 5:
def move_e_contour(idx, x, y):
if 9 <= idx <= 13 or 20 <= idx <= 25:
x -= 75*hat.ease()
#if 0 <= idx <= 33:
# y -= 50*hat.ease()
return x, y
pens[1].ffg("E").map_points(move_e_contour)#.translate(hat.ease()*150, 0)
### TOMTOM
# And lastly, use the tom signal to push up the outside of the O in the first line
tom = drums.fv(f.i, [50], [5, 10])
pens[0].ffg("O").mod_contour(0, lambda c: c.translate(0, -80*tom.ease()))
### BRANDING
fp = f.a.prg(f.i, easefn="linear").e
ghz_logo = DATPen().glyph(logos["goodhertz_logo_2019"])
ghz_logo.scale(0.2).align(f.a.r, y="mny").translate(0, 100).nonlinear_transform(warp_fn(speed=fp*3, rz=3, mult=10))
# return both elements to the renderer
# color elements with rgb primitives so we can
# channel separate them in after effects later
return [
ghz_logo.f(1, 0, 0).skew(cowbell.ease()*1),
pens.f(0, 1, 0).reversePens().understroke(s=(0, 0, 1), sw=15).translate(0, 100)
]
|
Some example Coldtype code and the resulting animation
A unique application of the Coldtype library (my favorite application)
Some lyric video animations I have done using Coldtype, many of them for friends who are musicians, or friends who run type foundries. (I am not friends with A Tribe Called Quest, I made that video for my brother’s 40th birthday because he loves Phife Dawg fan.)
A common reaction to my work
Another common reaction to my work
The two programs I use the most to make text animations
The main issue with all text animations: data-entry
The best known and most loved form of time-based data — which is why there are so many great ways to author time-based data that is music (hundreds of programs like Ableton Live and Logic and Reason, etc.)
The only “professional” way to enter time-based text data (that I know of)
All GUI programs I have used conflate these two very distinct operations
The only fun way to enter time-based text data (the key is to use blank footage + clip labeling and pretend you’re a video editor working with actual camera footage — this opens up a whole world of video editing technique that you can apply directly to manipulating text in the time dimension)
The essential ingredient for rendering time-based text data: rendering a frame independently of any loop
The essential technique for rendering callbacks for rendering time-based text data (you can render frames in a random order across all the cores of your computer, which can be very fast)
Pause to consider the question that haunts me: is it good to make videos in such an esoteric way?
Maybe? It’s certainly changed everything about the way I design
The point of the entire Coldtype library: to shorten the feedback loop and encourage experimentation — i.e. to remove the feeling of impossibility/endless labor that (for me) used to hover around the idea of making text-based animations
Hopefully Coldtype will be free and open very soon
Seriously! I’d love to know if this technique would benefit other people and their work
A big thank you to you for listening & watching, and to anyone who’s ever asked me to make a weird typographic video for them, and to all the type designers whose fonts are used in this presentation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | from coldtype import *
from coldtype.warping import warp_fn
from functools import partial
from random import randint, random
from pathlib import Path
import jinja2 as j2
import markdown
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import HtmlFormatter
Style.RegisterShorthandPrefix("≈", "~/Type/fonts/fonts")
hershey = Font("≈/Hershey-TriplexGothicGerman.ufo")
jenv = j2.Environment(loader=j2.FileSystemLoader("typelab"))
page_template = jenv.get_template("2020.j2")
slides = []
class slide(renderable):
def __init__(self, text, font, video=None, code=None, image=None, **kwargs):
self.text = text
self.font = font
self.video = video
self.image = image
if code:
self.code = highlight(code, PythonLexer(), HtmlFormatter(linenos=True))
else:
self.code = None
super().__init__(rect=(1200, 280), bg=1, **kwargs)
def __call__(self, func):
super().__call__(func)
html = markdown.markdown(func.__doc__)
if isinstance(self.font, Font):
font_name = self.font.path.name.replace("≈/", "")
else:
font_name = self.font.replace("≈/", "")
slides.append(dict(fname=func.__name__, bigtext=self.text, caption=html, video=self.video, code=self.code, font=font_name, image=self.image))
return self
def passes(self, action, layers, indices=[]):
return [RenderPass(self, self.func.__name__, [self.rect, self.text, self.font])]
warp = warp_fn(xa=0, ya=0, mult=55, base=randint(0, 100))
def simple_text(rect, string, f, onepen=1, ro=1, **kwargs):
pens = StyledString(string, Style(f, 150, ro=ro, **kwargs)).fit(1100).pens().align(rect).f(hsl(random()*0.7+0.3, s=0.5, l=0.55))
if onepen:
return pens.pen().flatten(3).nonlinear_transform(warp)
else:
return pens.pmap(lambda i,p: p.flatten(3).nonlinear_transform(warp))
@slide("Hello!", "≈/MeekDisplayv0.2-Super.otf")
def hello(*args):
"""A simple greeting"""
return simple_text(*args)
@slide("Rob Stenson", "≈/Taters0.2-Baked.otf")
def rob_stenson(*args):
"""My name"""
return simple_text(*args)
@slide("Goodhertz", "A custom logo by James Edmondson", image="https://static.goodhertz.co/statics/store/artifacts/screenshots/goodhertz-tone-ctrl-3.5.0-full-en.png")
def goodhertz(r, *args):
"""The company I work for ([we make audio software](https://goodhertz.co))"""
logos = raw_ufo("~/Goodhertz/coldtype-examples/fonts/logos.ufo")
ghz_logo = DATPen().glyph(logos["goodhertz_logo_2019"])
ghz_logo.scale(0.2).align(r).nonlinear_transform(warp)
return ghz_logo.f(hsl(random()*0.7+0.3, s=0.5, l=0.65))
@slide("COLDTYPE", "≈/ObviouslyVariable.ttf")
def coldtype(*args):
"""The project I’m here to talk about — a cross-platform library to help you precisely & programmatically do high-quality display typography with Python — kind of like DrawBot, but more oriented towards animation and an abbreviated programming style"""
return (
simple_text(*args, onepen=0, wdth=0.35, wght=1, slnt=1, tu=-150, r=1, ss01=1)
.understroke(s=0)
.scale(1.3))
@slide("Sample Code",
"≈/Biblio_0.1.otf",
video="385599032",
code=Path("typelab/stacked_and_justified.py").read_text())
def sample_code(*args):
"""Short-but-dense code and the resulting animation"""
return simple_text(*args)
@slide("Complex Code",
"≈/LCMarichiweu-Newen.otf",
video="408581790",
code=Path("typelab/808.py").read_text())
def code_example(*args):
"""Some example Coldtype code and the resulting animation"""
return simple_text(*args)
@slide("Lyric Videos", "≈/Margo_Condensed_v0.1-Medium.otf")
def lyric_videos(*args):
"""A unique application of the Coldtype library (my favorite application)"""
return simple_text(*args)
@slide("A Brief Reel", "≈/PufflingV02-Variable.ttf", video="430105805")
def reel(*args):
"""Some lyric video animations I have done using Coldtype, many of them for [friends](https://vulfpeck.com) [who](http://www.annaash.com/) are [musicians](https://www.theokatzman.com/), or [friends who run type foundries](https://ohnotype.co/). (I am not friends with A Tribe Called Quest, I made that video for my brother’s 40th birthday because he loves Phife Dawg fan.)"""
return simple_text(*args)
@slide("YIKES! Is that After Effects?", "≈/OhnoFatfaceVariable.ttf")
def yikes_ae(*args):
"""A common reaction to my work"""
return simple_text(*args, ro=1, opsz=0.85, wdth=1)
@slide("DrawBot???", "≈/Capra-RegularV0.1.otf")
def yikes_db(*args):
"""Another common reaction to my work"""
return simple_text(*args)
@slide("Premiere + Python".upper(), "≈/Megabase-Regular.otf")
def premiere_python(*args):
"""The two programs I use the most to make text animations"""
return simple_text(*args).scale(1.2)
@slide("Time-Based Data".upper(), "≈/HalunkeV0.2-Italic.otf")
def time_based(*args):
"""The main issue with all text animations: data-entry"""
return simple_text(*args)
@slide("“Music”", "≈/Pique.otf")
def aka_music(*args):
"""The best known and most loved form of time-based data — which is why there are so many great ways to author time-based data that is music (hundreds of programs like Ableton Live and Logic and Reason, etc.)"""
return simple_text(*args)
@slide("Keyframing", "≈/Beastly-18Point.otf")
def keyframing(*args):
"""The only “professional” way to enter time-based text data (that I know of)"""
return simple_text(*args)
@slide("Data-Entry vs. Design", "≈/MinSans-Bold_v0100.otf")
def data_vs_design(*args):
"""All GUI programs I have used conflate these two very distinct operations"""
return simple_text(*args).scale(0.8)
@slide("Not Keyframing", "≈/Gooper/Gooper5-BoldItalic.otf", video="430804932")
def not_keyframing(*args):
"""The only fun way to enter time-based text data (the key is to use blank footage + clip labeling and pretend you’re a video editor working with actual camera footage — this opens up a whole world of video editing technique that you can apply directly to manipulating text in the time dimension)"""
return simple_text(*args).scale(0.9).f(hsl(0.4))
@slide("“CALLBACK”", "≈/LoRes21OT-SerifRegular.ttf")
def callback(*args):
"""The essential ingredient for rendering time-based text data: rendering a frame independently of any loop"""
return simple_text(*args)
@slide("Out-of-order rendering".upper(), "≈/CoFo_Peshka_Variable_V0.3.ttf")
def out_of_order(*args):
"""The essential technique for rendering callbacks for rendering time-based text data (you can render frames in a random order across all the cores of your computer, which can be very fast)"""
return simple_text(*args)
@slide("A moment of introspection", "≈/NikolaiV0.4Narrow-SemiboldItalic.otf")
def introspection(*args):
"""Pause to consider the question that haunts me: is it good to make videos in such an esoteric way?"""
return simple_text(*args).scale(0.8)
@slide("Should I learn to code?", "≈/Oggle0.1-Regular.otf")
def learn_to_code(*args):
"""Maybe? It’s certainly changed everything about the way I design"""
return simple_text(*args)
@slide("The Feedback Loop".upper(), "≈/PappardelleParty-VF.ttf")
def feedback_loop(r, text, font):
"""The point of the entire Coldtype library: to shorten the feedback loop and encourage experimentation — i.e. to remove the feeling of impossibility/endless labor that (for me) used to hover around the idea of making text-based animations"""
return StyledString(text, Style(font, 250, palette=4)).pens().align(r).pmap(lambda i,p: p.flatten(3).nonlinear_transform(warp))
@slide("Open Source", hershey)
def open_source(r, text, font):
"""Hopefully Coldtype will be free and open very soon"""
return simple_text(r, text, font, ro=0).f(None).s(hsl(0.5)).sw(1)
@slide("Let’s Talk!", "≈/Nordvest-BlackItalic.otf")
def lets_talk(*args):
"""Seriously! I’d love to know if this technique would benefit other people and their work"""
return simple_text(*args)
@slide("“Shulie A Bop”".lower(), "≈/BAYARD-Regular.otf", video="383165505")
def shulie_a_bop(*args):
"""Sarah Vaughan’s ”[Shulie A Bop](https://en.wikipedia.org/wiki/Swingin%27_Easy)” (after Len Lye)"""
return simple_text(*args)
@slide("Thanks!", "≈/ObviouslyVariable.ttf")
def thanks(*args):
"""A big thank you to you for listening & watching, and to anyone who’s ever asked me to make a weird typographic video for them, and to all the type designers whose fonts are used in this presentation."""
return simple_text(*args, wdth=1, wght=1, slnt=1)
@slide("Source Code".upper(), "≈/LoRes9PlusOT-NarBold.ttf",
code=Path("typelab/2020.py").read_text())
def source(*args):
"""The source code for this presentation (uses quite a bit of Coldtype)"""
return simple_text(*args).scale(0.75)
(Path("typelab/2020.html")
.write_text(page_template
.render(dict(slides=slides, cache_bust=randint(0, 10000)))))
|
The source code for this presentation (uses quite a bit of Coldtype)