├── .gitignore ├── templates ├── exports │ └── simple-a.gif └── simple-a.py ├── variable-fonts-blog-post ├── exports │ ├── .DS_Store │ ├── grid.gif │ ├── GimmeGeo.gif │ ├── CheeeMood.gif │ ├── TweakDynamic.gif │ └── tweak-dance.mp4 ├── links │ ├── variable-FUNTIMES-04.png │ └── variable-FUNTIMES-04-b.png ├── gimme-fun.py ├── tweak-dynamic.py ├── cheee-mood.py ├── tweak-dance.py └── grid.py ├── utils ├── info.py ├── hexToRgb.py └── easing.py ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | fonts/* 2 | explore/* 3 | .DS_Store 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /templates/exports/simple-a.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurefonts/type-animations/HEAD/templates/exports/simple-a.gif -------------------------------------------------------------------------------- /variable-fonts-blog-post/exports/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurefonts/type-animations/HEAD/variable-fonts-blog-post/exports/.DS_Store -------------------------------------------------------------------------------- /variable-fonts-blog-post/exports/grid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurefonts/type-animations/HEAD/variable-fonts-blog-post/exports/grid.gif -------------------------------------------------------------------------------- /variable-fonts-blog-post/exports/GimmeGeo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurefonts/type-animations/HEAD/variable-fonts-blog-post/exports/GimmeGeo.gif -------------------------------------------------------------------------------- /variable-fonts-blog-post/exports/CheeeMood.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurefonts/type-animations/HEAD/variable-fonts-blog-post/exports/CheeeMood.gif -------------------------------------------------------------------------------- /variable-fonts-blog-post/exports/TweakDynamic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurefonts/type-animations/HEAD/variable-fonts-blog-post/exports/TweakDynamic.gif -------------------------------------------------------------------------------- /variable-fonts-blog-post/exports/tweak-dance.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurefonts/type-animations/HEAD/variable-fonts-blog-post/exports/tweak-dance.mp4 -------------------------------------------------------------------------------- /variable-fonts-blog-post/links/variable-FUNTIMES-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurefonts/type-animations/HEAD/variable-fonts-blog-post/links/variable-FUNTIMES-04.png -------------------------------------------------------------------------------- /variable-fonts-blog-post/links/variable-FUNTIMES-04-b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/futurefonts/type-animations/HEAD/variable-fonts-blog-post/links/variable-FUNTIMES-04-b.png -------------------------------------------------------------------------------- /utils/info.py: -------------------------------------------------------------------------------- 1 | import drawbot 2 | 3 | def variableFontInfo(fontPath): 4 | for axis, data in listFontVariations(fontPath).items(): 5 | print((axis, data)) 6 | # print('hello') -------------------------------------------------------------------------------- /utils/hexToRgb.py: -------------------------------------------------------------------------------- 1 | ### simple method to convert hex color to Drawbot-compatible, 0–1 color tuple 2 | ### with help from https://stackoverflow.com/questions/29643352/converting-hex-to-rgb-value-in-python 3 | 4 | def RGBfromHex(hex): 5 | h = hex.lstrip('#') 6 | RGB = tuple(int(h[i:i+2], 16) for i in (0, 2 ,4)) 7 | r1, g1, b1 = RGB[0] / 255, RGB[1] / 255, RGB[2] / 255 8 | return(r1, g1, b1) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Type Animations 2 | 3 | Drawbot scripts used for various animations in [Future Fonts](https://www.futurefonts.xyz) marketing. These aren’t super elegantly written, but feel free to use these as a starting point for your own projects. 4 | 5 | Fonts not included, but can be licensed at [Future Fonts](https://www.futurefonts.xyz). 6 | 7 | ### Python/Drawbot Resources 8 | * [Python for Designers](https://pythonfordesigners.com) 9 | * [Drawbot](https://www.drawbot.com) 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Future Fonts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/easing.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | def easeInOutQuad(t, b, c, d): 4 | # t is the current time (or position) of the tween. 5 | # b is the beginning value. 6 | # c is the change between the beginning and destination value 7 | # d is the total time of the tween. 8 | t /= d/2 9 | if t < 1: 10 | return c/2*t*t + b 11 | t-=1 12 | return -c/2 * (t*(t-2) - 1) + b 13 | 14 | def easeInQuad(t, b, c, d): 15 | t /= d 16 | return c*t*t + b 17 | 18 | def easeOutQuad(t, b, c, d): 19 | t /= d 20 | return c * (1 - (1 - t) * (1 - t)) + b; 21 | 22 | 23 | 24 | def easeInOutQuart(t, b, c, d): 25 | t /= d/2 26 | if t < 1: 27 | return c/2*t*t*t*t + b 28 | t -= 2 29 | return -c/2 * (t*t*t*t - 2) + b 30 | 31 | def easeInQuart(t, b, c, d): 32 | t /= d 33 | return c*t*t*t*t + b 34 | 35 | def easeOutQuart(t, b, c, d): 36 | t /= d 37 | return c * ((t-1)*(t-1)*(t-1)*(1-t)+1) + b 38 | 39 | 40 | def easeInOutExp(t, b, c, d): 41 | #t /= d/2 42 | t /= d 43 | 44 | if t == 0 or t == 1: 45 | return t 46 | 47 | if t < 0.5: 48 | return 0.5 * math.pow(2, (20 * t) - 10) 49 | 50 | return -0.5 * math.pow(2, (-20 * t) + 10) + 1 51 | 52 | def easeInExp(t, b, c, d): 53 | t /= d 54 | if t == 0: 55 | return 0 56 | return c * (math.pow(2, 10 * (t - 1))) + b 57 | 58 | def easeOutExp(t, b, c, d): 59 | t /= d 60 | if t == 1: 61 | return 1 62 | return c * (1 - math.pow(2, -10 * t)) + b 63 | 64 | # y = easeInExp(9,0,1,10) 65 | # print(y) -------------------------------------------------------------------------------- /variable-fonts-blog-post/gimme-fun.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') # this lets us import utils 3 | from utils.hexToRgb import RGBfromHex 4 | 5 | 6 | 7 | fontPath = "../fonts/GimmeConstructoVariable_02.ttf" 8 | 9 | exportPath = "./exports/GimmeGeo.gif" 10 | 11 | docWidth=800 12 | docHeight=int(docWidth * 0.480814) 13 | 14 | saveEnabled = True 15 | 16 | docColor = RGBfromHex('#ffffff') 17 | defaultTextColor = RGBfromHex('#000000') 18 | 19 | numFrames = 40 20 | defaultFrameDuration = 0.10 21 | 22 | textSize = 260 23 | leading = textSize * 1.2 24 | 25 | textBlocks = [] 26 | 27 | textBlocks.append({ 28 | "text": 'GEO', 29 | "textSize": textSize, 30 | "textColor": defaultTextColor, 31 | "lineHeightOffset": 0, 32 | "keyframes": [ 33 | { 34 | "pct": 0, 35 | "axes": { 36 | "wght": 400 37 | } 38 | }, 39 | { 40 | "pct": .5, 41 | "axes": { 42 | "wght": 700 43 | } 44 | }, 45 | { 46 | "pct": 1, 47 | "axes": { 48 | "wght": 400 49 | } 50 | }, 51 | ] 52 | }) 53 | 54 | shouldEqualizeKeyframePct = False 55 | 56 | spaceBetweenBlocks = leading - textSize 57 | 58 | textBoxVerticalOffset = -20 59 | 60 | # Change this for each font. It's used calculate the height of the text line, so it can be spaced evenly vertically. When displayHelpers are visible, the yellow box should extend from the baseline to the top of the main mass of your letterforms. 61 | heightOffsetPct = 0.73 62 | 63 | # show this to help set theHeight offset. 64 | displayHelpers = False 65 | 66 | def main(): 67 | setup() 68 | drawFrames() 69 | 70 | if saveEnabled: 71 | saveImage(exportPath) 72 | 73 | def setup(): 74 | variableFontInfo() 75 | newDrawing() 76 | #setCurrentLetter(letter) 77 | prepKeyframes() 78 | setBlockHeights() 79 | setBlockYpositions() 80 | 81 | def setCurrentLetter(letter): 82 | for textBlock in textBlocks: 83 | textBlock['text'] = letter 84 | 85 | 86 | def prepKeyframes(): 87 | if shouldEqualizeKeyframePct: 88 | equalizeKeyframePct() 89 | addIndexesToTextBlocks() 90 | 91 | def addIndexesToTextBlocks(): 92 | for textBlock in textBlocks: 93 | for keyframe in textBlock['keyframes']: 94 | keyframe['frameIndex'] = int((numFrames-1) * keyframe['pct']) 95 | 96 | def equalizeKeyframePct(): 97 | for textBlock in textBlocks: 98 | i = 0 99 | for keyframe in textBlock['keyframes']: 100 | keyframe['pct'] = i / (len(textBlock['keyframes']) - 1) 101 | i += 1 102 | 103 | def drawFrames(): 104 | for frameIndex in range(numFrames): 105 | drawFrame(frameIndex) 106 | 107 | def addBackground(): 108 | fill(*docColor) 109 | rect(0,0, docWidth, docHeight) 110 | 111 | def drawFrame(frameIndex): 112 | newPage(docWidth, docHeight) 113 | 114 | addBackground() 115 | frameDuration(defaultFrameDuration) 116 | 117 | i = 1 118 | for textBlock in textBlocks: 119 | drawHelper(textBlock) 120 | axesVals = frameAxesVals(frameIndex, textBlock) 121 | setMainText(textBlock, docWidth/2, textBlock['yPos'], axesVals) 122 | i += 1 123 | 124 | 125 | def frameAxesVals(frameIndex, textBlock): 126 | keyframes = relevantKeyframes(frameIndex, textBlock) 127 | pct = pctBetweenKeyframes(frameIndex, keyframes) 128 | easedPct = easeInOutQuad(pct, 0, 1, 1) 129 | # t is the current time (or position) of the tween. 130 | # b is the beginning value of the property. 131 | # c is the change between the beginning and destination value of the property. 132 | # d is the total time of the tween. 133 | 134 | axes = axisValsAtPct(easedPct, keyframes) 135 | return axes 136 | 137 | def axisValsAtPct(pct, keyframes): 138 | axes = [] 139 | for axis in keyframes[0]['axes']: 140 | a = {} 141 | minVal = keyframes[0]['axes'][axis] 142 | maxVal = keyframes[1]['axes'][axis] 143 | axes.append({axis: float((maxVal - minVal) * pct + minVal) 144 | }) 145 | return axes 146 | 147 | def easeInOutQuart(t, b, c, d): 148 | # t is the current time (or position) of the tween. 149 | # b is the beginning value of the property. 150 | # c is the change between the beginning and destination value of the property. 151 | # d is the total time of the tween. 152 | t /= d/2 153 | if t < 1: 154 | return c/2*t*t*t*t + b 155 | t -= 2 156 | return -c/2 * (t*t*t*t - 2) + b 157 | 158 | def easeInOutQuad(t, b, c, d): 159 | t /= d/2 160 | if t < 1: 161 | return c/2*t*t + b 162 | t-=1 163 | return -c/2 * (t*(t-2) - 1) + b 164 | 165 | def pctBetweenKeyframes(frameIndex, keyframes): 166 | frameSpan = keyframes[1]['frameIndex'] - keyframes[0]['frameIndex'] 167 | if frameSpan == 0: 168 | frameSpan = 1 169 | framesIntoKeyframe = frameIndex - keyframes[0]['frameIndex'] 170 | pctComplete = framesIntoKeyframe / frameSpan 171 | return pctComplete 172 | 173 | def relevantKeyframes(frameIndex, textBlock): 174 | minKeyframe = list(filter(lambda x: x['frameIndex'] <= frameIndex, textBlock['keyframes'])) 175 | maxKeyframe = list(filter(lambda x: x['frameIndex'] >= frameIndex, textBlock['keyframes'])) 176 | return minKeyframe[len(minKeyframe)-1], maxKeyframe[0] 177 | 178 | def setBlockHeights(): 179 | # calculate height of text 180 | for textBlock in textBlocks: 181 | tbHeight = (textBlock['textSize'] * heightOffsetPct) 182 | textBlock['height'] = tbHeight 183 | 184 | def setBlockYpositions(): 185 | # calculate y position of line 186 | textBlockHeightTotal = sum(textBlock['height'] for textBlock in textBlocks) 187 | spacerHeightTotal = len(textBlocks) * spaceBetweenBlocks 188 | totalHeight = textBlockHeightTotal + spacerHeightTotal 189 | top = docHeight - ((docHeight - totalHeight) / 2) + textBoxVerticalOffset 190 | 191 | for textBlock in textBlocks: 192 | textBlock['yPos'] = top - textBlock['height'] 193 | top = top - textBlock['height'] - spaceBetweenBlocks 194 | 195 | def drawHelper(textBlock): 196 | # draws helper bg 197 | if displayHelpers: 198 | strokeWidth(0) 199 | fill(255, 255, 0) 200 | rect(0, textBlock['yPos'], docWidth, textBlock['height']) 201 | 202 | strokeWidth(1) 203 | stroke(255,0,0) 204 | yMiddle = textBlock['yPos'] + (textBlock['height'] / 2) 205 | 206 | line((0, yMiddle), (docWidth, yMiddle)) 207 | 208 | strokeWidth(0) 209 | 210 | def variableFontInfo(): 211 | for axis, data in listFontVariations(fontPath).items(): 212 | print((axis, data)) 213 | 214 | def setMainText(textBlock, xPos, yPos, axes): 215 | c = defaultTextColor 216 | txt = FormattedString() 217 | 218 | for axis in axes: 219 | txt.fontVariations(**axis) 220 | 221 | txt.font(fontPath) 222 | 223 | 224 | txt.append(textBlock['text'], 225 | fontSize=textBlock['textSize'], 226 | lineHeight=textBlock['textSize'], 227 | fill=(c)) 228 | text(txt, (xPos,yPos), align="center") 229 | 230 | 231 | main() -------------------------------------------------------------------------------- /variable-fonts-blog-post/tweak-dynamic.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') # this lets us import utils 3 | from utils.hexToRgb import RGBfromHex 4 | 5 | 6 | 7 | fontPath = "../fonts/TweakDisplayv0.3.ttf" 8 | exportPath = "./exports/TweakDynamic.gif" 9 | 10 | docWidth=800 11 | docHeight=int(docWidth * 0.480814) 12 | 13 | saveEnabled = True 14 | 15 | docColor = RGBfromHex('#ffffff') 16 | defaultTextColor = RGBfromHex('#000000') 17 | 18 | numFrames = 50 19 | defaultFrameDuration = 0.04 20 | 21 | textSize = 128 22 | leading = textSize * 1 23 | 24 | textBlocks = [] 25 | 26 | textBlocks.append({ 27 | "text": 'Dynamic', 28 | "textSize": textSize, 29 | "textColor": defaultTextColor, 30 | "lineHeightOffset": 0, 31 | "keyframes": [ 32 | { 33 | "pct": 0, 34 | "axes": { 35 | "DIST": 0 36 | } 37 | }, 38 | { 39 | "pct": .5, 40 | "axes": { 41 | "DIST": 1000 42 | } 43 | }, 44 | { 45 | "pct": 1, 46 | "axes": { 47 | "DIST": 0 48 | } 49 | }, 50 | ] 51 | }) 52 | 53 | shouldEqualizeKeyframePct = False 54 | 55 | spaceBetweenBlocks = leading - textSize 56 | 57 | textBoxVerticalOffset = 10 58 | 59 | # Change this for each font. It's used calculate the height of the text line, so it can be spaced evenly vertically. When displayHelpers are visible, the yellow box should extend from the baseline to the top of the main mass of your letterforms. 60 | heightOffsetPct = 0.8 61 | 62 | # show this to help set theHeight offset. 63 | displayHelpers = False 64 | 65 | def main(): 66 | setup() 67 | drawFrames() 68 | 69 | if saveEnabled: 70 | saveImage(exportPath) 71 | 72 | def setup(): 73 | variableFontInfo() 74 | newDrawing() 75 | #setCurrentLetter(letter) 76 | prepKeyframes() 77 | setBlockHeights() 78 | setBlockYpositions() 79 | 80 | def setCurrentLetter(letter): 81 | for textBlock in textBlocks: 82 | textBlock['text'] = letter 83 | 84 | 85 | def prepKeyframes(): 86 | if shouldEqualizeKeyframePct: 87 | equalizeKeyframePct() 88 | addIndexesToTextBlocks() 89 | 90 | def addIndexesToTextBlocks(): 91 | for textBlock in textBlocks: 92 | for keyframe in textBlock['keyframes']: 93 | keyframe['frameIndex'] = int((numFrames-1) * keyframe['pct']) 94 | 95 | def equalizeKeyframePct(): 96 | for textBlock in textBlocks: 97 | i = 0 98 | for keyframe in textBlock['keyframes']: 99 | keyframe['pct'] = i / (len(textBlock['keyframes']) - 1) 100 | i += 1 101 | 102 | def drawFrames(): 103 | for frameIndex in range(numFrames): 104 | drawFrame(frameIndex) 105 | 106 | def addBackground(): 107 | fill(*docColor) 108 | rect(0,0, docWidth, docHeight) 109 | image('links/variable-FUNTIMES-04-b.png', (0, 0)) 110 | 111 | def drawFrame(frameIndex): 112 | newPage(docWidth, docHeight) 113 | 114 | addBackground() 115 | frameDuration(defaultFrameDuration) 116 | 117 | i = 1 118 | for textBlock in textBlocks: 119 | drawHelper(textBlock) 120 | axesVals = frameAxesVals(frameIndex, textBlock) 121 | setMainText(textBlock, docWidth/2, textBlock['yPos'], axesVals) 122 | i += 1 123 | 124 | 125 | def frameAxesVals(frameIndex, textBlock): 126 | keyframes = relevantKeyframes(frameIndex, textBlock) 127 | pct = pctBetweenKeyframes(frameIndex, keyframes) 128 | easedPct = easeInOutQuad(pct, 0, 1, 1) 129 | # t is the current time (or position) of the tween. 130 | # b is the beginning value of the property. 131 | # c is the change between the beginning and destination value of the property. 132 | # d is the total time of the tween. 133 | 134 | axes = axisValsAtPct(easedPct, keyframes) 135 | return axes 136 | 137 | def axisValsAtPct(pct, keyframes): 138 | axes = [] 139 | for axis in keyframes[0]['axes']: 140 | a = {} 141 | minVal = keyframes[0]['axes'][axis] 142 | maxVal = keyframes[1]['axes'][axis] 143 | axes.append({axis: float((maxVal - minVal) * pct + minVal) 144 | }) 145 | return axes 146 | 147 | def easeInOutQuart(t, b, c, d): 148 | # t is the current time (or position) of the tween. 149 | # b is the beginning value of the property. 150 | # c is the change between the beginning and destination value of the property. 151 | # d is the total time of the tween. 152 | t /= d/2 153 | if t < 1: 154 | return c/2*t*t*t*t + b 155 | t -= 2 156 | return -c/2 * (t*t*t*t - 2) + b 157 | 158 | def easeInOutQuad(t, b, c, d): 159 | t /= d/2 160 | if t < 1: 161 | return c/2*t*t + b 162 | t-=1 163 | return -c/2 * (t*(t-2) - 1) + b 164 | 165 | def pctBetweenKeyframes(frameIndex, keyframes): 166 | frameSpan = keyframes[1]['frameIndex'] - keyframes[0]['frameIndex'] 167 | if frameSpan == 0: 168 | frameSpan = 1 169 | framesIntoKeyframe = frameIndex - keyframes[0]['frameIndex'] 170 | pctComplete = framesIntoKeyframe / frameSpan 171 | return pctComplete 172 | 173 | def relevantKeyframes(frameIndex, textBlock): 174 | minKeyframe = list(filter(lambda x: x['frameIndex'] <= frameIndex, textBlock['keyframes'])) 175 | maxKeyframe = list(filter(lambda x: x['frameIndex'] >= frameIndex, textBlock['keyframes'])) 176 | return minKeyframe[len(minKeyframe)-1], maxKeyframe[0] 177 | 178 | def setBlockHeights(): 179 | # calculate height of text 180 | for textBlock in textBlocks: 181 | tbHeight = (textBlock['textSize'] * heightOffsetPct) 182 | textBlock['height'] = tbHeight 183 | 184 | def setBlockYpositions(): 185 | # calculate y position of line 186 | textBlockHeightTotal = sum(textBlock['height'] for textBlock in textBlocks) 187 | spacerHeightTotal = len(textBlocks) * spaceBetweenBlocks 188 | totalHeight = textBlockHeightTotal + spacerHeightTotal 189 | top = docHeight - ((docHeight - totalHeight) / 2) + textBoxVerticalOffset 190 | 191 | for textBlock in textBlocks: 192 | textBlock['yPos'] = top - textBlock['height'] 193 | top = top - textBlock['height'] - spaceBetweenBlocks 194 | 195 | def drawHelper(textBlock): 196 | # draws helper bg 197 | if displayHelpers: 198 | strokeWidth(0) 199 | fill(255, 255, 0) 200 | rect(0, textBlock['yPos'], docWidth, textBlock['height']) 201 | 202 | strokeWidth(1) 203 | stroke(255,0,0) 204 | yMiddle = textBlock['yPos'] + (textBlock['height'] / 2) 205 | 206 | line((0, yMiddle), (docWidth, yMiddle)) 207 | 208 | strokeWidth(0) 209 | 210 | def variableFontInfo(): 211 | for axis, data in listFontVariations(fontPath).items(): 212 | print((axis, data)) 213 | 214 | def setMainText(textBlock, xPos, yPos, axes): 215 | c = defaultTextColor 216 | txt = FormattedString() 217 | 218 | for axis in axes: 219 | txt.fontVariations(**axis) 220 | 221 | txt.font(fontPath) 222 | 223 | 224 | txt.append(textBlock['text'], 225 | fontSize=textBlock['textSize'], 226 | lineHeight=textBlock['textSize'], 227 | fill=(c)) 228 | text(txt, (xPos,yPos), align="center") 229 | 230 | 231 | main() -------------------------------------------------------------------------------- /variable-fonts-blog-post/cheee-mood.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') # this lets us import utils 3 | from utils.hexToRgb import RGBfromHex 4 | 5 | 6 | 7 | fontPath = "../fonts/CheeeVariable.ttf" 8 | 9 | exportPath = "./exports/CheeeMood.mp4" 10 | 11 | docWidth=800 12 | docHeight=int(docWidth * 0.480814) 13 | 14 | saveEnabled = True 15 | 16 | docColor = RGBfromHex('#ffffff') 17 | defaultTextColor = RGBfromHex('#000000') 18 | 19 | numFrames = 50 20 | defaultFrameDuration = 1/30 21 | 22 | textSize = 220 23 | leading = textSize * 1.2 24 | 25 | textBlocks = [] 26 | 27 | textBlocks.append({ 28 | "text": 'MOOD', 29 | "textSize": textSize, 30 | "textColor": defaultTextColor, 31 | "lineHeightOffset": 0, 32 | "keyframes": [ 33 | { 34 | "pct": 0, 35 | "axes": { 36 | "yest": 0, 37 | "grvt": 0 38 | } 39 | }, 40 | { 41 | "pct": .25, 42 | "axes": { 43 | "yest": 0, 44 | "grvt": 1000 45 | } 46 | }, 47 | { 48 | "pct": .5, 49 | "axes": { 50 | "yest": 1000, 51 | "grvt": 1000 52 | } 53 | }, 54 | { 55 | "pct": .75, 56 | "axes": { 57 | "yest": 1000, 58 | "grvt": 0 59 | } 60 | }, 61 | { 62 | "pct": 1, 63 | "axes": { 64 | "yest": 0, 65 | "grvt": 0 66 | } 67 | } 68 | ] 69 | }) 70 | 71 | shouldEqualizeKeyframePct = False 72 | 73 | spaceBetweenBlocks = leading - textSize 74 | 75 | textBoxVerticalOffset = -15 76 | 77 | # Change this for each font. It's used calculate the height of the text line, so it can be spaced evenly vertically. When displayHelpers are visible, the yellow box should extend from the baseline to the top of the main mass of your letterforms. 78 | heightOffsetPct = 0.65 79 | 80 | # show this to help set theHeight offset. 81 | displayHelpers = False 82 | 83 | def main(): 84 | setup() 85 | drawFrames() 86 | 87 | if saveEnabled: 88 | saveImage(exportPath) 89 | 90 | def setup(): 91 | variableFontInfo() 92 | newDrawing() 93 | #setCurrentLetter(letter) 94 | prepKeyframes() 95 | setBlockHeights() 96 | setBlockYpositions() 97 | 98 | def setCurrentLetter(letter): 99 | for textBlock in textBlocks: 100 | textBlock['text'] = letter 101 | 102 | 103 | def prepKeyframes(): 104 | if shouldEqualizeKeyframePct: 105 | equalizeKeyframePct() 106 | addIndexesToTextBlocks() 107 | 108 | def addIndexesToTextBlocks(): 109 | for textBlock in textBlocks: 110 | for keyframe in textBlock['keyframes']: 111 | keyframe['frameIndex'] = int((numFrames-1) * keyframe['pct']) 112 | 113 | def equalizeKeyframePct(): 114 | for textBlock in textBlocks: 115 | i = 0 116 | for keyframe in textBlock['keyframes']: 117 | keyframe['pct'] = i / (len(textBlock['keyframes']) - 1) 118 | i += 1 119 | 120 | def drawFrames(): 121 | for frameIndex in range(numFrames): 122 | drawFrame(frameIndex) 123 | 124 | def addBackground(): 125 | fill(*docColor) 126 | rect(0,0, docWidth, docHeight) 127 | 128 | def drawFrame(frameIndex): 129 | newPage(docWidth, docHeight) 130 | 131 | addBackground() 132 | frameDuration(defaultFrameDuration) 133 | 134 | i = 1 135 | for textBlock in textBlocks: 136 | drawHelper(textBlock) 137 | axesVals = frameAxesVals(frameIndex, textBlock) 138 | setMainText(textBlock, docWidth/2, textBlock['yPos'], axesVals) 139 | i += 1 140 | 141 | 142 | def frameAxesVals(frameIndex, textBlock): 143 | keyframes = relevantKeyframes(frameIndex, textBlock) 144 | pct = pctBetweenKeyframes(frameIndex, keyframes) 145 | easedPct = easeInOutQuad(pct, 0, 1, 1) 146 | # t is the current time (or position) of the tween. 147 | # b is the beginning value of the property. 148 | # c is the change between the beginning and destination value of the property. 149 | # d is the total time of the tween. 150 | 151 | axes = axisValsAtPct(easedPct, keyframes) 152 | return axes 153 | 154 | def axisValsAtPct(pct, keyframes): 155 | axes = [] 156 | for axis in keyframes[0]['axes']: 157 | a = {} 158 | minVal = keyframes[0]['axes'][axis] 159 | maxVal = keyframes[1]['axes'][axis] 160 | axes.append({axis: float((maxVal - minVal) * pct + minVal) 161 | }) 162 | return axes 163 | 164 | def easeInOutQuart(t, b, c, d): 165 | # t is the current time (or position) of the tween. 166 | # b is the beginning value of the property. 167 | # c is the change between the beginning and destination value of the property. 168 | # d is the total time of the tween. 169 | t /= d/2 170 | if t < 1: 171 | return c/2*t*t*t*t + b 172 | t -= 2 173 | return -c/2 * (t*t*t*t - 2) + b 174 | 175 | def easeInOutQuad(t, b, c, d): 176 | t /= d/2 177 | if t < 1: 178 | return c/2*t*t + b 179 | t-=1 180 | return -c/2 * (t*(t-2) - 1) + b 181 | 182 | def pctBetweenKeyframes(frameIndex, keyframes): 183 | frameSpan = keyframes[1]['frameIndex'] - keyframes[0]['frameIndex'] 184 | if frameSpan == 0: 185 | frameSpan = 1 186 | framesIntoKeyframe = frameIndex - keyframes[0]['frameIndex'] 187 | pctComplete = framesIntoKeyframe / frameSpan 188 | return pctComplete 189 | 190 | def relevantKeyframes(frameIndex, textBlock): 191 | minKeyframe = list(filter(lambda x: x['frameIndex'] <= frameIndex, textBlock['keyframes'])) 192 | maxKeyframe = list(filter(lambda x: x['frameIndex'] >= frameIndex, textBlock['keyframes'])) 193 | return minKeyframe[len(minKeyframe)-1], maxKeyframe[0] 194 | 195 | def setBlockHeights(): 196 | # calculate height of text 197 | for textBlock in textBlocks: 198 | tbHeight = (textBlock['textSize'] * heightOffsetPct) 199 | textBlock['height'] = tbHeight 200 | 201 | def setBlockYpositions(): 202 | # calculate y position of line 203 | textBlockHeightTotal = sum(textBlock['height'] for textBlock in textBlocks) 204 | spacerHeightTotal = len(textBlocks) * spaceBetweenBlocks 205 | totalHeight = textBlockHeightTotal + spacerHeightTotal 206 | top = docHeight - ((docHeight - totalHeight) / 2) + textBoxVerticalOffset 207 | 208 | for textBlock in textBlocks: 209 | textBlock['yPos'] = top - textBlock['height'] 210 | top = top - textBlock['height'] - spaceBetweenBlocks 211 | 212 | def drawHelper(textBlock): 213 | # draws helper bg 214 | if displayHelpers: 215 | strokeWidth(0) 216 | fill(255, 255, 0) 217 | rect(0, textBlock['yPos'], docWidth, textBlock['height']) 218 | 219 | strokeWidth(1) 220 | stroke(255,0,0) 221 | yMiddle = textBlock['yPos'] + (textBlock['height'] / 2) 222 | 223 | line((0, yMiddle), (docWidth, yMiddle)) 224 | 225 | strokeWidth(0) 226 | 227 | def variableFontInfo(): 228 | for axis, data in listFontVariations(fontPath).items(): 229 | print((axis, data)) 230 | 231 | def setMainText(textBlock, xPos, yPos, axes): 232 | c = defaultTextColor 233 | txt = FormattedString() 234 | 235 | for axis in axes: 236 | txt.fontVariations(**axis) 237 | 238 | txt.font(fontPath) 239 | 240 | 241 | txt.append(textBlock['text'], 242 | fontSize=textBlock['textSize'], 243 | lineHeight=textBlock['textSize'], 244 | fill=(c)) 245 | text(txt, (xPos,yPos), align="center") 246 | 247 | 248 | main() -------------------------------------------------------------------------------- /templates/simple-a.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') # this lets us import utils 3 | from utils.hexToRgb import RGBfromHex 4 | 5 | 6 | 7 | fontPath = "../fonts/TweakDisplayv0.3.ttf" 8 | 9 | exportPath = "./exports/simple-a.gif" 10 | 11 | docWidth=1000 12 | docHeight=1000 13 | 14 | saveEnabled = True 15 | 16 | docColor = RGBfromHex('#ffffff') 17 | defaultTextColor = RGBfromHex('#000000') 18 | 19 | numFrames = 100 20 | defaultFrameDuration = 0.01 21 | 22 | textSize = 128 23 | leading = textSize * 1.2 24 | 25 | textBlocks = [] 26 | 27 | textBlocks.append({ 28 | "text": 'Dynamic', 29 | "textSize": textSize, 30 | "textColor": defaultTextColor, 31 | "lineHeightOffset": 0, 32 | "keyframes": [ 33 | { 34 | "pct": 0, 35 | "axes": { 36 | "DIST": 0, 37 | "BULK": 0 38 | } 39 | }, 40 | { 41 | "pct": .25, 42 | "axes": { 43 | "DIST": 1000, 44 | "BULK": 0 45 | } 46 | }, 47 | { 48 | "pct": .5, 49 | "axes": { 50 | "DIST": 1000, 51 | "BULK": 1000 52 | } 53 | }, 54 | { 55 | "pct": .75, 56 | "axes": { 57 | "DIST": 0, 58 | "BULK": 1000 59 | } 60 | }, 61 | { 62 | "pct": 1, 63 | "axes": { 64 | "DIST": 0, 65 | "BULK": 0 66 | } 67 | } 68 | ] 69 | }) 70 | textBlocks.append({ 71 | "text": 'Flex', 72 | "textSize": textSize * 1.5, 73 | "textColor": defaultTextColor, 74 | "lineHeightOffset": 0, 75 | "keyframes": [ 76 | { 77 | "pct": 0, 78 | "axes": { 79 | "DIST": 0, 80 | "BULK": 1000 81 | } 82 | }, 83 | { 84 | "pct": .5, 85 | "axes": { 86 | "DIST": 1000, 87 | "BULK": 0 88 | } 89 | }, 90 | { 91 | "pct": 1, 92 | "axes": { 93 | "DIST": 0, 94 | "BULK": 1000 95 | } 96 | } 97 | ] 98 | }) 99 | 100 | shouldEqualizeKeyframePct = False 101 | 102 | spaceBetweenBlocks = leading - textSize 103 | 104 | textBoxVerticalOffset = 10 105 | 106 | # Change this for each font. It's used calculate the height of the text line, so it can be spaced evenly vertically. When displayHelpers are visible, the yellow box should extend from the baseline to the top of the main mass of your letterforms. 107 | heightOffsetPct = 0.8 108 | 109 | # show this to help set theHeight offset. 110 | displayHelpers = False 111 | 112 | def main(): 113 | setup() 114 | drawFrames() 115 | 116 | if saveEnabled: 117 | saveImage(exportPath) 118 | 119 | def setup(): 120 | variableFontInfo() 121 | newDrawing() 122 | #setCurrentLetter(letter) 123 | prepKeyframes() 124 | setBlockHeights() 125 | setBlockYpositions() 126 | 127 | def setCurrentLetter(letter): 128 | for textBlock in textBlocks: 129 | textBlock['text'] = letter 130 | 131 | 132 | def prepKeyframes(): 133 | if shouldEqualizeKeyframePct: 134 | equalizeKeyframePct() 135 | addIndexesToTextBlocks() 136 | 137 | def addIndexesToTextBlocks(): 138 | for textBlock in textBlocks: 139 | for keyframe in textBlock['keyframes']: 140 | keyframe['frameIndex'] = int((numFrames-1) * keyframe['pct']) 141 | 142 | def equalizeKeyframePct(): 143 | for textBlock in textBlocks: 144 | i = 0 145 | for keyframe in textBlock['keyframes']: 146 | keyframe['pct'] = i / (len(textBlock['keyframes']) - 1) 147 | i += 1 148 | 149 | def drawFrames(): 150 | for frameIndex in range(numFrames): 151 | drawFrame(frameIndex) 152 | 153 | def addBackground(): 154 | fill(*docColor) 155 | rect(0,0, docWidth, docHeight) 156 | 157 | def drawFrame(frameIndex): 158 | newPage(docWidth, docHeight) 159 | 160 | addBackground() 161 | frameDuration(defaultFrameDuration) 162 | 163 | i = 1 164 | for textBlock in textBlocks: 165 | drawHelper(textBlock) 166 | axesVals = frameAxesVals(frameIndex, textBlock) 167 | setMainText(textBlock, docWidth/2, textBlock['yPos'], axesVals) 168 | i += 1 169 | 170 | 171 | def frameAxesVals(frameIndex, textBlock): 172 | keyframes = relevantKeyframes(frameIndex, textBlock) 173 | pct = pctBetweenKeyframes(frameIndex, keyframes) 174 | easedPct = easeInOutQuad(pct, 0, 1, 1) 175 | # t is the current time (or position) of the tween. 176 | # b is the beginning value of the property. 177 | # c is the change between the beginning and destination value of the property. 178 | # d is the total time of the tween. 179 | 180 | axes = axisValsAtPct(easedPct, keyframes) 181 | return axes 182 | 183 | def axisValsAtPct(pct, keyframes): 184 | axes = [] 185 | for axis in keyframes[0]['axes']: 186 | a = {} 187 | minVal = keyframes[0]['axes'][axis] 188 | maxVal = keyframes[1]['axes'][axis] 189 | axes.append({axis: float((maxVal - minVal) * pct + minVal) 190 | }) 191 | return axes 192 | 193 | def easeInOutQuart(t, b, c, d): 194 | # t is the current time (or position) of the tween. 195 | # b is the beginning value of the property. 196 | # c is the change between the beginning and destination value of the property. 197 | # d is the total time of the tween. 198 | t /= d/2 199 | if t < 1: 200 | return c/2*t*t*t*t + b 201 | t -= 2 202 | return -c/2 * (t*t*t*t - 2) + b 203 | 204 | def easeInOutQuad(t, b, c, d): 205 | t /= d/2 206 | if t < 1: 207 | return c/2*t*t + b 208 | t-=1 209 | return -c/2 * (t*(t-2) - 1) + b 210 | 211 | def pctBetweenKeyframes(frameIndex, keyframes): 212 | frameSpan = keyframes[1]['frameIndex'] - keyframes[0]['frameIndex'] 213 | if frameSpan == 0: 214 | frameSpan = 1 215 | framesIntoKeyframe = frameIndex - keyframes[0]['frameIndex'] 216 | pctComplete = framesIntoKeyframe / frameSpan 217 | return pctComplete 218 | 219 | def relevantKeyframes(frameIndex, textBlock): 220 | minKeyframe = list(filter(lambda x: x['frameIndex'] <= frameIndex, textBlock['keyframes'])) 221 | maxKeyframe = list(filter(lambda x: x['frameIndex'] >= frameIndex, textBlock['keyframes'])) 222 | return minKeyframe[len(minKeyframe)-1], maxKeyframe[0] 223 | 224 | def setBlockHeights(): 225 | # calculate height of text 226 | for textBlock in textBlocks: 227 | tbHeight = (textBlock['textSize'] * heightOffsetPct) 228 | textBlock['height'] = tbHeight 229 | 230 | def setBlockYpositions(): 231 | # calculate y position of line 232 | textBlockHeightTotal = sum(textBlock['height'] for textBlock in textBlocks) 233 | spacerHeightTotal = len(textBlocks) * spaceBetweenBlocks 234 | totalHeight = textBlockHeightTotal + spacerHeightTotal 235 | top = docHeight - ((docHeight - totalHeight) / 2) + textBoxVerticalOffset 236 | 237 | for textBlock in textBlocks: 238 | textBlock['yPos'] = top - textBlock['height'] 239 | top = top - textBlock['height'] - spaceBetweenBlocks 240 | 241 | def drawHelper(textBlock): 242 | # draws helper bg 243 | if displayHelpers: 244 | strokeWidth(0) 245 | fill(255, 255, 0) 246 | rect(0, textBlock['yPos'], docWidth, textBlock['height']) 247 | 248 | strokeWidth(1) 249 | stroke(255,0,0) 250 | yMiddle = textBlock['yPos'] + (textBlock['height'] / 2) 251 | 252 | line((0, yMiddle), (docWidth, yMiddle)) 253 | 254 | strokeWidth(0) 255 | 256 | def variableFontInfo(): 257 | for axis, data in listFontVariations(fontPath).items(): 258 | print((axis, data)) 259 | 260 | def setMainText(textBlock, xPos, yPos, axes): 261 | c = defaultTextColor 262 | txt = FormattedString() 263 | 264 | for axis in axes: 265 | txt.fontVariations(**axis) 266 | 267 | txt.font(fontPath) 268 | 269 | 270 | txt.append(textBlock['text'], 271 | fontSize=textBlock['textSize'], 272 | lineHeight=textBlock['textSize'], 273 | fill=(c)) 274 | text(txt, (xPos,yPos), align="center") 275 | 276 | 277 | main() -------------------------------------------------------------------------------- /variable-fonts-blog-post/tweak-dance.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') # this lets us import utils 3 | from utils.hexToRgb import RGBfromHex 4 | 5 | 6 | 7 | fontPath = "../fonts/TweakDisplayv0.3.ttf" 8 | 9 | exportPath = "./exports/tweak-dance.mp4" 10 | 11 | docWidth=1080 12 | docHeight=1018 13 | 14 | saveEnabled = True 15 | 16 | docColor = RGBfromHex('#FFE7DA') 17 | defaultTextColor = RGBfromHex('#000000') 18 | 19 | numFrames = 100 20 | defaultFrameDuration = 0.01 21 | 22 | textSize = 275 23 | leading = textSize * 1.2 24 | 25 | textBlocks = [] 26 | 27 | textBlocks.append({ 28 | "text": 'Dance', 29 | "textSize": textSize, 30 | "textColor": defaultTextColor, 31 | "lineHeightOffset": 0, 32 | "keyframes": [ 33 | { 34 | "pct": 0, 35 | "axes": { 36 | "DIST": 0, 37 | "BULK": 0 38 | } 39 | }, 40 | { 41 | "pct": .5, 42 | "axes": { 43 | "DIST": 1000, 44 | "BULK": 0 45 | } 46 | }, 47 | { 48 | "pct": 1, 49 | "axes": { 50 | "DIST": 0, 51 | "BULK": 0 52 | } 53 | } 54 | ] 55 | }) 56 | textBlocks.append({ 57 | "text": 'Dance', 58 | "textSize": textSize, 59 | "textColor": defaultTextColor, 60 | "lineHeightOffset": 0, 61 | "keyframes": [ 62 | { 63 | "pct": 0, 64 | "axes": { 65 | "DIST": 1000, 66 | "BULK": 0 67 | } 68 | }, 69 | { 70 | "pct": .5, 71 | "axes": { 72 | "DIST": 0, 73 | "BULK": 0 74 | } 75 | }, 76 | { 77 | "pct": 1, 78 | "axes": { 79 | "DIST": 1000, 80 | "BULK": 0 81 | } 82 | } 83 | ] 84 | }) 85 | textBlocks.append({ 86 | "text": 'Dance', 87 | "textSize": textSize, 88 | "textColor": defaultTextColor, 89 | "lineHeightOffset": 0, 90 | "keyframes": [ 91 | { 92 | "pct": 0, 93 | "axes": { 94 | "DIST": 0, 95 | "BULK": 0 96 | } 97 | }, 98 | { 99 | "pct": .5, 100 | "axes": { 101 | "DIST": 1000, 102 | "BULK": 0 103 | } 104 | }, 105 | { 106 | "pct": 1, 107 | "axes": { 108 | "DIST": 0, 109 | "BULK": 0 110 | } 111 | } 112 | ] 113 | }) 114 | 115 | shouldEqualizeKeyframePct = False 116 | 117 | spaceBetweenBlocks = leading - textSize 118 | 119 | textBoxVerticalOffset = 10 120 | 121 | # Change this for each font. It's used calculate the height of the text line, so it can be spaced evenly vertically. When displayHelpers are visible, the yellow box should extend from the baseline to the top of the main mass of your letterforms. 122 | heightOffsetPct = 0.8 123 | 124 | # show this to help set theHeight offset. 125 | displayHelpers = False 126 | 127 | def main(): 128 | setup() 129 | drawFrames() 130 | 131 | if saveEnabled: 132 | saveImage(exportPath) 133 | 134 | def setup(): 135 | variableFontInfo() 136 | newDrawing() 137 | #setCurrentLetter(letter) 138 | prepKeyframes() 139 | setBlockHeights() 140 | setBlockYpositions() 141 | 142 | def setCurrentLetter(letter): 143 | for textBlock in textBlocks: 144 | textBlock['text'] = letter 145 | 146 | 147 | def prepKeyframes(): 148 | if shouldEqualizeKeyframePct: 149 | equalizeKeyframePct() 150 | addIndexesToTextBlocks() 151 | 152 | def addIndexesToTextBlocks(): 153 | for textBlock in textBlocks: 154 | for keyframe in textBlock['keyframes']: 155 | keyframe['frameIndex'] = int((numFrames-1) * keyframe['pct']) 156 | 157 | def equalizeKeyframePct(): 158 | for textBlock in textBlocks: 159 | i = 0 160 | for keyframe in textBlock['keyframes']: 161 | keyframe['pct'] = i / (len(textBlock['keyframes']) - 1) 162 | i += 1 163 | 164 | def drawFrames(): 165 | for frameIndex in range(numFrames): 166 | drawFrame(frameIndex) 167 | 168 | def addBackground(): 169 | fill(*docColor) 170 | rect(0,0, docWidth, docHeight) 171 | 172 | def drawFrame(frameIndex): 173 | newPage(docWidth, docHeight) 174 | 175 | addBackground() 176 | frameDuration(defaultFrameDuration) 177 | 178 | i = 1 179 | for textBlock in textBlocks: 180 | drawHelper(textBlock) 181 | axesVals = frameAxesVals(frameIndex, textBlock) 182 | setMainText(textBlock, docWidth/2, textBlock['yPos'], axesVals) 183 | i += 1 184 | 185 | 186 | def frameAxesVals(frameIndex, textBlock): 187 | keyframes = relevantKeyframes(frameIndex, textBlock) 188 | pct = pctBetweenKeyframes(frameIndex, keyframes) 189 | easedPct = easeInOutQuad(pct, 0, 1, 1) 190 | # t is the current time (or position) of the tween. 191 | # b is the beginning value of the property. 192 | # c is the change between the beginning and destination value of the property. 193 | # d is the total time of the tween. 194 | 195 | axes = axisValsAtPct(easedPct, keyframes) 196 | return axes 197 | 198 | def axisValsAtPct(pct, keyframes): 199 | axes = [] 200 | for axis in keyframes[0]['axes']: 201 | a = {} 202 | minVal = keyframes[0]['axes'][axis] 203 | maxVal = keyframes[1]['axes'][axis] 204 | axes.append({axis: float((maxVal - minVal) * pct + minVal) 205 | }) 206 | return axes 207 | 208 | def easeInOutQuart(t, b, c, d): 209 | # t is the current time (or position) of the tween. 210 | # b is the beginning value of the property. 211 | # c is the change between the beginning and destination value of the property. 212 | # d is the total time of the tween. 213 | t /= d/2 214 | if t < 1: 215 | return c/2*t*t*t*t + b 216 | t -= 2 217 | return -c/2 * (t*t*t*t - 2) + b 218 | 219 | def easeInOutQuad(t, b, c, d): 220 | t /= d/2 221 | if t < 1: 222 | return c/2*t*t + b 223 | t-=1 224 | return -c/2 * (t*(t-2) - 1) + b 225 | 226 | def pctBetweenKeyframes(frameIndex, keyframes): 227 | frameSpan = keyframes[1]['frameIndex'] - keyframes[0]['frameIndex'] 228 | if frameSpan == 0: 229 | frameSpan = 1 230 | framesIntoKeyframe = frameIndex - keyframes[0]['frameIndex'] 231 | pctComplete = framesIntoKeyframe / frameSpan 232 | return pctComplete 233 | 234 | def relevantKeyframes(frameIndex, textBlock): 235 | minKeyframe = list(filter(lambda x: x['frameIndex'] <= frameIndex, textBlock['keyframes'])) 236 | maxKeyframe = list(filter(lambda x: x['frameIndex'] >= frameIndex, textBlock['keyframes'])) 237 | return minKeyframe[len(minKeyframe)-1], maxKeyframe[0] 238 | 239 | def setBlockHeights(): 240 | # calculate height of text 241 | for textBlock in textBlocks: 242 | tbHeight = (textBlock['textSize'] * heightOffsetPct) 243 | textBlock['height'] = tbHeight 244 | 245 | def setBlockYpositions(): 246 | # calculate y position of line 247 | textBlockHeightTotal = sum(textBlock['height'] for textBlock in textBlocks) 248 | spacerHeightTotal = len(textBlocks) * spaceBetweenBlocks 249 | totalHeight = textBlockHeightTotal + spacerHeightTotal 250 | top = docHeight - ((docHeight - totalHeight) / 2) + textBoxVerticalOffset 251 | 252 | for textBlock in textBlocks: 253 | textBlock['yPos'] = top - textBlock['height'] 254 | top = top - textBlock['height'] - spaceBetweenBlocks 255 | 256 | def drawHelper(textBlock): 257 | # draws helper bg 258 | if displayHelpers: 259 | strokeWidth(0) 260 | fill(255, 255, 0) 261 | rect(0, textBlock['yPos'], docWidth, textBlock['height']) 262 | 263 | strokeWidth(1) 264 | stroke(255,0,0) 265 | yMiddle = textBlock['yPos'] + (textBlock['height'] / 2) 266 | 267 | line((0, yMiddle), (docWidth, yMiddle)) 268 | 269 | strokeWidth(0) 270 | 271 | def variableFontInfo(): 272 | for axis, data in listFontVariations(fontPath).items(): 273 | print((axis, data)) 274 | 275 | def setMainText(textBlock, xPos, yPos, axes): 276 | c = defaultTextColor 277 | txt = FormattedString() 278 | 279 | for axis in axes: 280 | txt.fontVariations(**axis) 281 | 282 | txt.font(fontPath) 283 | 284 | 285 | txt.append(textBlock['text'], 286 | fontSize=textBlock['textSize'], 287 | lineHeight=textBlock['textSize'], 288 | fill=(c)) 289 | text(txt, (xPos,yPos), align="center") 290 | 291 | 292 | main() -------------------------------------------------------------------------------- /variable-fonts-blog-post/grid.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') # this lets us import utils 3 | from utils.hexToRgb import RGBfromHex 4 | import math 5 | 6 | 7 | fontPath = "../fonts/rotor-VF.ttf" 8 | 9 | exportPath = "./exports/grid.gif" 10 | 11 | 12 | 13 | 14 | docWidth=800 15 | #docHeight=int(docWidth * 0.480814) 16 | docHeight=470 17 | 18 | numCols = 3.0 19 | numRows = 3.0 20 | 21 | colWidth = (docWidth / numCols) 22 | rowHeight = (docHeight / numRows) 23 | 24 | saveEnabled = True 25 | 26 | docColor = RGBfromHex('#ffffff') 27 | defaultTextColor = RGBfromHex('#000000') 28 | 29 | numFrames = 40 30 | defaultFrameDuration = 0.08 31 | 32 | textSize = 90 33 | leading = textSize * 1 34 | 35 | textBlocks = [] 36 | 37 | textBlocks.append({ 38 | "fontPath": "../fonts/rotor-VF.ttf", 39 | "text": 'LOL', 40 | "textSize": textSize*1.1, 41 | "textColor": RGBfromHex('#2d63e9'), 42 | "lineHeightOffset": -10, 43 | "xOffset": 0, 44 | "keyframes": [ 45 | { 46 | "pct": 0, 47 | "axes": { 48 | "rttx": 0 49 | } 50 | }, 51 | { 52 | "pct": 1, 53 | "axes": { 54 | "rttx": 360 55 | } 56 | } 57 | ] 58 | }) 59 | textBlocks.append({ 60 | "fontPath": "../fonts/SeraphsVAR-V3.ttf", 61 | "text": '8RO', 62 | "textSize": textSize * 1.2, 63 | "textColor": RGBfromHex('#1be378'), 64 | "lineHeightOffset": -10, 65 | "xOffset": 6, 66 | "keyframes": [ 67 | { 68 | "pct": 0, 69 | "axes": { 70 | "SRFS": 3, 71 | "wght": 200 72 | } 73 | }, 74 | { 75 | "pct": .5, 76 | "axes": { 77 | "SRFS": 0, 78 | "wght": 200 79 | } 80 | }, 81 | { 82 | "pct": 1, 83 | "axes": { 84 | "SRFS": 3, 85 | "wght": 200 86 | } 87 | } 88 | ] 89 | }) 90 | 91 | textBlocks.append({ 92 | "fontPath": "../fonts/TXT25-VRBL-0.7.ttf", 93 | "text": 'brb', 94 | "textSize": textSize*1.3, 95 | "textColor": RGBfromHex("#e59f3b"), 96 | "lineHeightOffset": -12, 97 | "xOffset": 0, 98 | "keyframes": [ 99 | { 100 | "pct": 0, 101 | "axes": { 102 | "wght": 1 103 | } 104 | }, 105 | { 106 | "pct": .5, 107 | "axes": { 108 | "wght": 1000 109 | } 110 | }, 111 | { 112 | "pct": 1, 113 | "axes": { 114 | "wght": 1 115 | } 116 | } 117 | ] 118 | }) 119 | textBlocks.append({ 120 | "fontPath": "../fonts/ShrillVariablev0.3.ttf", 121 | "text": 'yay', 122 | "textSize": textSize*1.2, 123 | "textColor": RGBfromHex("#964b00"), 124 | "lineHeightOffset": 3, 125 | "xOffset": 0, 126 | "keyframes": [ 127 | { 128 | "pct": 0, 129 | "axes": { 130 | "wght": 100 131 | } 132 | }, 133 | { 134 | "pct": .5, 135 | "axes": { 136 | "wght": 900 137 | } 138 | }, 139 | { 140 | "pct": 1, 141 | "axes": { 142 | "wght": 100 143 | } 144 | } 145 | ] 146 | }) 147 | textBlocks.append({ 148 | "fontPath": "../fonts/Dunkelsansv0.63GX.ttf", 149 | "text": '우주', 150 | "textSize": textSize*1.2, 151 | "textColor": RGBfromHex("#000000"), 152 | "lineHeightOffset": -8, 153 | "xOffset": 0, 154 | "keyframes": [ 155 | { 156 | "pct": 0, 157 | "axes": { 158 | "wdth": 700 159 | } 160 | }, 161 | { 162 | "pct": .5, 163 | "axes": { 164 | "wdth": 1000 165 | } 166 | }, 167 | { 168 | "pct": 1, 169 | "axes": { 170 | "wdth": 700 171 | } 172 | } 173 | ] 174 | }) 175 | textBlocks.append({ 176 | "fontPath": "../fonts/CSTMXprmntl03-VF.ttf", 177 | "text": 'Oh', 178 | "textSize": textSize*1.3, 179 | "textColor": RGBfromHex("#ff6bb2"), 180 | "lineHeightOffset": -20, 181 | "xOffset": 0, 182 | "keyframes": [ 183 | { 184 | "pct": 0, 185 | "axes": { 186 | "wght": 950 187 | } 188 | }, 189 | { 190 | "pct": .5, 191 | "axes": { 192 | "wght": 400 193 | } 194 | }, 195 | { 196 | "pct": 1, 197 | "axes": { 198 | "wght": 950 199 | } 200 | } 201 | ] 202 | }) 203 | textBlocks.append({ 204 | "fontPath": "../fonts/GoitersGX.ttf", 205 | "text": 'idk', 206 | "textSize": textSize * .85, 207 | "textColor": RGBfromHex("#ff0000"), 208 | "lineHeightOffset": -3, 209 | "xOffset": 0, 210 | "keyframes": [ 211 | { 212 | "pct": 0, 213 | "axes": { 214 | "wght": 900 215 | } 216 | }, 217 | { 218 | "pct": .5, 219 | "axes": { 220 | "wght": 100 221 | } 222 | }, 223 | { 224 | "pct": 1, 225 | "axes": { 226 | "wght": 900 227 | } 228 | } 229 | ] 230 | }) 231 | textBlocks.append({ 232 | "fontPath": "../fonts/ClaretteGX.ttf", 233 | "text": 'yum', 234 | "textSize": textSize*1.1, 235 | "textColor": RGBfromHex("#2d3fe9"), 236 | "lineHeightOffset": 0, 237 | "xOffset": 5, 238 | "keyframes": [ 239 | { 240 | "pct": 0, 241 | "axes": { 242 | "wdth": 90, 243 | "ital": 0 244 | } 245 | }, 246 | { 247 | "pct": .5, 248 | "axes": { 249 | "wdth": 90, 250 | "ital": 100 251 | } 252 | }, 253 | { 254 | "pct": 1, 255 | "axes": { 256 | "wdth": 90, 257 | "ital": 0 258 | } 259 | } 260 | ] 261 | }) 262 | textBlocks.append({ 263 | "fontPath": "../fonts/CoFoPeshkaV0.4_Variable.ttf", 264 | "text": 'HAHA', 265 | "textSize": textSize*1.1, 266 | "textColor": RGBfromHex("#aaaaaa"), 267 | "lineHeightOffset": -5, 268 | "xOffset": 0, 269 | "keyframes": [ 270 | { 271 | "pct": 0, 272 | "axes": { 273 | "wdth": 200, 274 | "wght": 1000 275 | } 276 | }, 277 | { 278 | "pct": .5, 279 | "axes": { 280 | "wdth": 400, 281 | "wght": 0 282 | } 283 | }, 284 | { 285 | "pct": 1, 286 | "axes": { 287 | "wdth": 200, 288 | "wght": 1000 289 | } 290 | } 291 | ] 292 | }) 293 | 294 | shouldEqualizeKeyframePct = False 295 | 296 | spaceBetweenBlocks = leading - textSize 297 | 298 | textBoxVerticalOffset = -25 299 | 300 | # Change this for each font. It's used calculate the height of the text line, so it can be spaced evenly vertically. When displayHelpers are visible, the yellow box should extend from the baseline to the top of the main mass of your letterforms. 301 | heightOffsetPct = 0.73 302 | 303 | # show this to help set theHeight offset. 304 | displayHelpers = False 305 | 306 | def main(): 307 | variableFontInfo() 308 | setup() 309 | drawFrames() 310 | 311 | if saveEnabled: 312 | saveImage(exportPath) 313 | 314 | def setup(): 315 | newDrawing() 316 | prepKeyframes() 317 | setBlockHeights() 318 | setBlockYpositions() 319 | 320 | 321 | def variableFontInfo(): 322 | for textBlock in textBlocks: 323 | print('---') 324 | print(textBlock['text']) 325 | for axis, data in listFontVariations(textBlock['fontPath']).items(): 326 | print((axis, data)) 327 | 328 | def prepKeyframes(): 329 | if shouldEqualizeKeyframePct: 330 | equalizeKeyframePct() 331 | addIndexesToTextBlocks() 332 | 333 | def addIndexesToTextBlocks(): 334 | for textBlock in textBlocks: 335 | for keyframe in textBlock['keyframes']: 336 | keyframe['frameIndex'] = int((numFrames-1) * keyframe['pct']) 337 | 338 | def equalizeKeyframePct(): 339 | for textBlock in textBlocks: 340 | i = 0 341 | for keyframe in textBlock['keyframes']: 342 | keyframe['pct'] = i / (len(textBlock['keyframes']) - 1) 343 | i += 1 344 | 345 | def drawFrames(): 346 | for frameIndex in range(numFrames): 347 | drawFrame(frameIndex) 348 | 349 | def addBackground(): 350 | fill(*docColor) 351 | rect(0,0, docWidth, docHeight) 352 | drawGrid() 353 | 354 | def drawGrid(): 355 | stroke(0) 356 | sWidth = 4 357 | strokeWidth(sWidth) 358 | fill(*docColor) 359 | rect(sWidth * .5, sWidth * .5, docWidth - sWidth, docHeight - sWidth) 360 | for row in range(int(numRows)): 361 | if row > 0: 362 | y = rowHeight * row 363 | line((0, y), (docWidth, y)) 364 | 365 | for col in range(int(numCols)): 366 | if col > 0: 367 | x = colWidth * col 368 | line((x, 0), (x, docHeight)) 369 | 370 | def getCenterPos(index): 371 | row = math.ceil((index / numRows)) 372 | col = (((index - 1) % numCols)) 373 | x = (colWidth * col) + (colWidth / 2) 374 | y = docHeight - (rowHeight * row) + (rowHeight / 2) 375 | 376 | return(x, y) 377 | 378 | def drawFrame(frameIndex): 379 | newPage(docWidth, docHeight) 380 | 381 | addBackground() 382 | frameDuration(defaultFrameDuration) 383 | 384 | i = 1 385 | for textBlock in textBlocks: 386 | drawHelper(textBlock) 387 | axesVals = frameAxesVals(frameIndex, textBlock) 388 | x,y = getCenterPos(i) 389 | setMainText(textBlock, (x + textBlock['xOffset']), (y + textBoxVerticalOffset + textBlock['lineHeightOffset']), axesVals) 390 | i += 1 391 | 392 | 393 | def frameAxesVals(frameIndex, textBlock): 394 | keyframes = relevantKeyframes(frameIndex, textBlock) 395 | pct = pctBetweenKeyframes(frameIndex, keyframes) 396 | easedPct = easeInOutQuad(pct, 0, 1, 1) 397 | # t is the current time (or position) of the tween. 398 | # b is the beginning value of the property. 399 | # c is the change between the beginning and destination value of the property. 400 | # d is the total time of the tween. 401 | 402 | axes = axisValsAtPct(easedPct, keyframes) 403 | return axes 404 | 405 | def axisValsAtPct(pct, keyframes): 406 | axes = [] 407 | for axis in keyframes[0]['axes']: 408 | a = {} 409 | minVal = keyframes[0]['axes'][axis] 410 | maxVal = keyframes[1]['axes'][axis] 411 | axes.append({axis: float((maxVal - minVal) * pct + minVal) 412 | }) 413 | return axes 414 | 415 | def easeInOutQuart(t, b, c, d): 416 | # t is the current time (or position) of the tween. 417 | # b is the beginning value of the property. 418 | # c is the change between the beginning and destination value of the property. 419 | # d is the total time of the tween. 420 | t /= d/2 421 | if t < 1: 422 | return c/2*t*t*t*t + b 423 | t -= 2 424 | return -c/2 * (t*t*t*t - 2) + b 425 | 426 | def easeInOutQuad(t, b, c, d): 427 | t /= d/2 428 | if t < 1: 429 | return c/2*t*t + b 430 | t-=1 431 | return -c/2 * (t*(t-2) - 1) + b 432 | 433 | def pctBetweenKeyframes(frameIndex, keyframes): 434 | frameSpan = keyframes[1]['frameIndex'] - keyframes[0]['frameIndex'] 435 | if frameSpan == 0: 436 | frameSpan = 1 437 | framesIntoKeyframe = frameIndex - keyframes[0]['frameIndex'] 438 | pctComplete = framesIntoKeyframe / frameSpan 439 | return pctComplete 440 | 441 | def relevantKeyframes(frameIndex, textBlock): 442 | minKeyframe = list(filter(lambda x: x['frameIndex'] <= frameIndex, textBlock['keyframes'])) 443 | maxKeyframe = list(filter(lambda x: x['frameIndex'] >= frameIndex, textBlock['keyframes'])) 444 | return minKeyframe[len(minKeyframe)-1], maxKeyframe[0] 445 | 446 | def setBlockHeights(): 447 | # calculate height of text 448 | for textBlock in textBlocks: 449 | tbHeight = (textBlock['textSize'] * heightOffsetPct) 450 | textBlock['height'] = tbHeight 451 | 452 | def setBlockYpositions(): 453 | # calculate y position of line 454 | textBlockHeightTotal = sum(textBlock['height'] for textBlock in textBlocks) 455 | spacerHeightTotal = len(textBlocks) * spaceBetweenBlocks 456 | totalHeight = textBlockHeightTotal + spacerHeightTotal 457 | top = docHeight - ((docHeight - totalHeight) / 2) + textBoxVerticalOffset 458 | 459 | for textBlock in textBlocks: 460 | textBlock['yPos'] = top - textBlock['height'] 461 | top = top - textBlock['height'] - spaceBetweenBlocks 462 | 463 | def drawHelper(textBlock): 464 | # draws helper bg 465 | if displayHelpers: 466 | strokeWidth(0) 467 | fill(255, 255, 0) 468 | rect(0, textBlock['yPos'], docWidth, textBlock['height']) 469 | 470 | strokeWidth(1) 471 | stroke(255,0,0) 472 | yMiddle = textBlock['yPos'] + (textBlock['height'] / 2) 473 | 474 | line((0, yMiddle), (docWidth, yMiddle)) 475 | 476 | strokeWidth(0) 477 | 478 | def setMainText(textBlock, xPos, yPos, axes): 479 | c = textBlock['textColor'] 480 | txt = FormattedString() 481 | 482 | for axis in axes: 483 | txt.fontVariations(**axis) 484 | 485 | txt.font(textBlock['fontPath']) 486 | 487 | 488 | txt.append(textBlock['text'], 489 | fontSize=textBlock['textSize'], 490 | lineHeight=textBlock['textSize'], 491 | fill=(c)) 492 | text(txt, (xPos,yPos), align="center") 493 | 494 | 495 | main() --------------------------------------------------------------------------------