Skip to content
embody
collectiontransparent TDN networks to inspect, copy, and remix.manifestowhy TouchDesigner should be transparent and diffable.docsguides and API reference for Embody and Envoy.sign in

oCollection / typography, animation

textTOP example

by @rosco

starter13 operatorsCC0-1.0
report

Recreated from one reference screenshot and the prompt: "recreate this imagein TouchDesigner, exactly the same layout."

Rather than eyeball it, the reference was measured programmatically — per-row baselines, cap heights and ink extents — which revealed that each line is centered on a smoothly drifting point, not ragged-left as it appears. Those numbers became the table.

Read left to right: table_lineup (name | code | centre) plus a Noise CHOP feed script_spec, which emits a Text TOP Specification DAT — a table where each row is one string with its own x/y — sizing each name via evalTextSize() so the superscripts land correctly. select_names / select_codes split that into two Text TOPs, because the spec DAT positions per row but ignores per-row font size. They composite into out1.

Verified by capturing the render and re-measuring it against the source: line centres match within 0.6 px RMS.

Noise CHOP Amplitude drives the stagger — at 0 it's the exact reference frame.

All geometry is stored as canvas fractions, so it's resolution independent.

properties

Select an operator in the graph to inspect its parameters.

single click selects · double click enters a COMP

rendered result

textTOP example rendered result

tdn source

12 operators · 7 connections · 3 annotations · 423 lines
1format: tdn
2version: "2.0"
3build: 5
4generator: Embody/6.0.168
5td_build: 099.2025.33070
6source_file: embody-discord.toe
7exported_at: 2026-07-31T18:50:55Z
8network_path: /project1/lineup
9options:
10 include_dat_content: true
11 include_storage: true
12type_defaults:
13 textTOP:
14 parameters:
15 fontsizexunit: pixels
16 fontsizeyunit: pixels
17 outputresolution: custom
18 size:
19 - 130
20 - 90
21 color:
22 - 0.67
23 - 0.67
24 - 0.67
25 selectDAT:
26 parameters:
27 firstrow: true
28 extractrows: byindex
29 rowexpr: =re.match('.*',me.inputCell.val) != None
30 colindexend: =me.inputTable.numCols - 1
31 colexpr: =re.match('.*',me.inputCell.val) != None
32 size:
33 - 130
34 - 90
35 color:
36 - 0.67
37 - 0.67
38 - 0.67
39type: baseCOMP
40color:
41 - 0.3
42 - 0.5
43 - 0.9
44tags:
45 - tdn
46operators:
47 - name: out1
48 type: outTOP
49 parameters:
50 label: =me.name
51 flags:
52 - display
53 - viewer
54 position:
55 - 3200
56 - 0
57 size:
58 - 130
59 - 90
60 color:
61 - 0.67
62 - 0.67
63 - 0.67
64 inputs:
65 - over_lineup
66 - name: text_codes
67 type: textTOP
68 parameters:
69 specdat: select_codes
70 fontfile: =op('text_names').par.fontfile.eval()
71 dispmethod: =op('text_names').par.dispmethod.eval()
72 fontsizex: =float(op('select_codes')[1, 'fontsizex'])
73 fontsizey: =float(op('select_codes')[1, 'fontsizex'])
74 alignx: left
75 aligny: bottom
76 fontcolorr: =op('text_names').par.fontcolorr.eval()
77 fontcolorg: =op('text_names').par.fontcolorg.eval()
78 fontcolorb: =op('text_names').par.fontcolorb.eval()
79 resolutionw: =op('text_names').par.resolutionw.eval()
80 resolutionh: =op('text_names').par.resolutionh.eval()
81 position:
82 - 2400
83 - -400
84 - name: text_names
85 type: textTOP
86 parameters:
87 specdat: select_names
88 font: TeleNeo
89 fontfile: /Users/rosco/Library/Fonts/TeleNeo-Bold.otf
90 dispmethod: scalable
91 fontsizex: =float(op('select_names')[1, 'fontsizex'])
92 fontsizey: =float(op('select_names')[1, 'fontsizex'])
93 alignx: left
94 aligny: bottom
95 fontcolorr: 0.812
96 fontcolorg: 0.812
97 fontcolorb: 0.812
98 bgalpha: 1
99 resolutionw: 990
100 resolutionh: 1280
101 position:
102 - 2400
103 - 0
104 - name: over_lineup
105 type: overTOP
106 position:
107 - 2800
108 - 0
109 size:
110 - 130
111 - 90
112 color:
113 - 0.67
114 - 0.67
115 - 0.67
116 inputs:
117 - text_codes
118 - text_names
119 - name: script_spec
120 type: scriptDAT
121 parameters:
122 callbacks: script_spec_callbacks
123 custom_pars:
124 Custom:
125 - name: Valuea
126 style: Float
127 label: Value A
128 - name: Valueb
129 style: Float
130 label: Value B
131 position:
132 - 1200
133 - 0
134 size:
135 - 130
136 - 90
137 color:
138 - 0.67
139 - 0.67
140 - 0.67
141 inputs:
142 - table_lineup
143 - chopto_stagger
144 dat_read_only: true
145 - name: select_codes
146 type: selectDAT
147 parameters:
148 rowindexstart: =(op('script_spec').numRows - 1) // 2 + 1
149 rowindexend: =op('script_spec').numRows - 1
150 position:
151 - 1600
152 - -400
153 inputs:
154 - script_spec
155 dat_read_only: true
156 - name: select_names
157 type: selectDAT
158 parameters:
159 rowindexstart: 1
160 rowindexend: =(op('script_spec').numRows - 1) // 2
161 position:
162 - 1600
163 - 0
164 inputs:
165 - script_spec
166 dat_read_only: true
167 - name: table_lineup
168 type: tableDAT
169 parameters:
170 file: project1/lineup/table_lineup.tsv
171 syncfile: true
172 rows: 16
173 cols: 3
174 size:
175 - 130
176 - 90
177 color:
178 - 0.9
179 - 0.3
180 - 0.4
181 tags:
182 - tsv
183 dat_content:
184 - - name
185 - code
186 - center
187 - - Cynthia Spiering
188 - NL
189 - "0.329879"
190 - - Daria Kolosova
191 - UA
192 - "0.329879"
193 - - David Guetta
194 - FR
195 - "0.360967"
196 - - Davyboi
197 - DE
198 - "0.422280"
199 - - DJ AYA
200 - CH
201 - "0.501727"
202 - - DJ BORING
203 - AU
204 - "0.570812"
205 - - DJ EZ
206 - GB
207 - "0.610535"
208 - - DJ Heartstring
209 - DE
210 - "0.613126"
211 - - DJ Nobu
212 - JP
213 - "0.630397"
214 - - Eris Drew
215 - US
216 - "0.644214"
217 - - Fafi Abdel Nour
218 - NL
219 - "0.677030"
220 - - Faster Horses
221 - GB
222 - "0.703800"
223 - - Fenrick
224 - FR
225 - "0.720207"
226 - - FJAAK
227 - DE
228 - "0.720207"
229 - - Folamour
230 - FR
231 - "0.719344"
232 dat_content_format: table
233 - name: text_metrics
234 type: textTOP
235 parameters:
236 font: TeleNeo
237 fontfile: =op('text_names').par.fontfile.eval()
238 dispmethod: =op('text_names').par.dispmethod.eval()
239 fontsizex: 100
240 fontsizey: 100
241 resolutionw: 16
242 resolutionh: 16
243 position:
244 - 1200
245 - -800
246 - name: noise_stagger
247 type: noiseCHOP
248 parameters:
249 period: 5
250 periodunit: samples
251 amp: 0
252 tz: =absTime.seconds * 0.12 if me.par.amp.eval() else 0.0
253 channelname: stagger
254 startunit: samples
255 end: 14
256 endunit: samples
257 rate: =me.time.rate
258 autoexportroot: =parent()
259 position:
260 - 0
261 - -400
262 size:
263 - 130
264 - 90
265 color:
266 - 0.67
267 - 0.67
268 - 0.67
269 - name: chopto_stagger
270 type: choptoDAT
271 parameters:
272 chop: noise_stagger
273 position:
274 - 400
275 - -400
276 size:
277 - 130
278 - 90
279 color:
280 - 0.67
281 - 0.67
282 - 0.67
283 dat_read_only: true
284 - name: script_spec_callbacks
285 type: textDAT
286 parameters:
287 file: project1/lineup/script_spec_callbacks.py
288 syncfile: true
289 language: python
290 flags:
291 - viewer
292 position:
293 - 1200
294 - -120
295 size:
296 - 130
297 - 90
298 color:
299 - 0.9
300 - 0.3
301 - 0.4
302 tags:
303 - py
304 dock: script_spec
305 dat_content: |
306 # Builds the Specification table that positions every string in the lineup.
307 #
308 # in0 table_lineup name | code | center (center = 0-1 across the canvas)
309 # in1 chopto_stagger one animated horizontal offset per line
310 #
311 # Output is 'x y text fontsizex': the 15 NAME rows first, then the 15 CODE
312 # rows. select_names / select_codes split that by row index and feed the two
313 # Text TOPs. Two TOPs are required because a Text TOP renders one font size --
314 # the Specification DAT positions text per row but ignores per-row font size.
315 # The fontsizex column is inert to the Text TOP; the two TOPs read it by
316 # expression to pick up their own size, which keeps this script the single
317 # source of truth for the geometry.
318 #
319 # Two behaviours here were established by measuring rendered output, not from
320 # the docs:
321 # * With aligny = bottom, the spec table's y IS the text baseline.
322 # * evalTextSize returns the ADVANCE width, which overshoots the inked
323 # width by roughly 0.10 of the cap height. CODE_GAP is calibrated against
324 # rendered pixels, so it is the visible gap net of that overshoot.
325 #
326 # Every measurement below is a FRACTION of the canvas, so the layout is
327 # resolution independent. The ratios were measured off the reference frame.
328
329 CAP_FRAC = 0.035380 # name cap height / canvas height
330 LINE_FRAC = 0.055402 # baseline-to-baseline / canvas height
331 FIRST_BASE = 0.134045 # first baseline from top / canvas height
332 CODE_SIZE = 0.471 # code font size / name font size
333 CODE_RISE = 0.362 # code baseline lift / name cap height
334 CODE_GAP = 0.063 # name-to-code gap / name cap height (see note above)
335 SWING = 0.25 # stagger travel / canvas width, at CHOP value 1.0
336
337
338 def onCook(dat):
339 src, stagger = dat.inputs[0], dat.inputs[1]
340 top, metrics = op('text_names'), op('text_metrics')
341
342 # Read the canvas from text_names' PARAMETERS, not its cooked texture --
343 # text_names consumes this DAT, so touching its output would be a cycle.
344 W = int(top.par.resolutionw.eval())
345 H = int(top.par.resolutionh.eval())
346
347 # text_metrics is a stand-in rendered at a fixed reference size. Cap height
348 # and advance widths both scale linearly with font size, so one measurement
349 # at the reference size gives us every size we need.
350 ref = metrics.par.fontsizey.eval()
351 cap = CAP_FRAC * H
352 size = cap * ref / metrics.capHeight # size that yields the target cap height
353 scale = size / ref # advance-width scale factor
354 code_size = size * CODE_SIZE
355 gap = CODE_GAP * cap
356
357 names, codes = [], []
358 for i, row in enumerate(src.rows()[1:]):
359 name, code = row[0].val, row[1].val
360 offset = float(stagger[i, 0].val) if i < stagger.numRows else 0.0
361 center = float(row[2].val) + offset * SWING
362
363 name_w = metrics.evalTextSize(name)[0] * scale
364 code_w = metrics.evalTextSize(code)[0] * scale * CODE_SIZE
365
366 # Center the whole line -- name, gap and superscript together.
367 x = center * W - (name_w + gap + code_w) / 2.0
368 baseline = H - (FIRST_BASE + i * LINE_FRAC) * H # spec origin is bottom-left
369
370 names.append([round(x, 2), round(baseline, 2), name, round(size, 3)])
371 codes.append([round(x + name_w + gap, 2),
372 round(baseline + CODE_RISE * cap, 2),
373 code, round(code_size, 3)])
374
375 dat.clear()
376 dat.appendRow(['x', 'y', 'text', 'fontsizex'])
377 for row in names + codes:
378 dat.appendRow(row)
379 return
380 dat_content_format: text
381annotations:
382 - name: annotate1
383 mode: annotate
384 title: Lineup Data
385 text: "Edit here. table_lineup is one row per artist: name, country code, and the line's centre as a 0-1 fraction of canvas width. noise_stagger animates that centre -- raise its Amplitude to make the lines drift. At Amplitude 0 the frame matches the reference exactly."
386 position:
387 - -70
388 - -470
389 size:
390 - 670
391 - 730
392 color:
393 - 0.45
394 - 0.45
395 - 0.45
396 - name: annotate2
397 mode: annotate
398 title: Layout Solver
399 text: script_spec turns the data into a Text TOP Specification table (x, y, text per string), measuring every name against text_metrics so superscripts sit correctly and lines stay centred. select_names / select_codes split the result into the two type sizes. All geometry is fractions of the canvas, so it is resolution independent.
400 position:
401 - 1130
402 - -870
403 size:
404 - 670
405 - 1130
406 color:
407 - 0.45
408 - 0.45
409 - 0.45
410 - name: annotate3
411 mode: annotate
412 title: Render
413 text: Two Text TOPs, because a Text TOP renders exactly one font size -- the Specification DAT positions text per row but ignores per-row font size. text_codes (transparent) composites over text_names (opaque black).
414 position:
415 - 2330
416 - -470
417 size:
418 - 1070
419 - 730
420 color:
421 - 0.45
422 - 0.45
423 - 0.45
embody

natural language tools for TouchDesigner

Explore

  • collection
  • contribute
  • github
  • for AI agents

Reference

  • documentation
  • TDN spec
  • manifesto
  • changelog
  • privacy
  • terms
  • cookies
  • copyright

© 2026 TEC · MIT License

made with in idyllwild california