Typoglycemia Generator

A conversation I had recently reminded me of some text I saw years ago where the letters of each word were scrambled except that the first and last letters were kept the same. The idea is that our brains are able to read it easily. I wondered if I could code that up in Typst during the conversation to test how true the claim is (spoiler: it’s not true). I finished just as my conversation partners were losing interest so I wasn’t as fast as I had hoped.
Since it works I thought I would share the results here.

#import "@preview/t4t:0.4.3": *
#import "@preview/suiji:0.5.1": *

#let scramble-word(word) = {
  let txt-original = get.text(word)
  let clusters = txt-original.clusters()

  //Edge cases
  if clusters.len() < 4 {
    //3 letters can't be randomized
    return word
  } else if clusters.len() == 4 {
    return {
      //Just swap middle two characters
      clusters.at(0)
      clusters.at(2)
      clusters.at(1)
      clusters.at(3)
    }
  }

  //Given a scrambled array of characters, create the scrambled word
  let get-result(shuffled-arr) = {
    clusters.first()
    shuffled-arr.join("")
    clusters.last()
  }

  //Change seed while looping in order to guarantee it's scrambled
  let seed = 21
  for i in range(10) {
    let rng = gen-rng-f(seed + i)
    let (_, a) = shuffle(rng, clusters.slice(1, -1))
    let txt-scrambled = get-result(a)
    
    if txt-scrambled != txt-original {
      return txt-scrambled
    }
  }

  //Didn't end up scrambling, highlight this word
  return text(red, txt-scrambled)
}

#show regex(`\w+`.text): scramble-word

According to a research at Cambridge University, it doesn't matter in what order the letters in a word are, the only important thing is that the first and last letter be at the right place.  The rest can be a total mess and you can still read it without problem.  This is because the human mind does not read every letter by itself, but the word as a whole.

I’m mis-using suiji simply to have an easy way to shuffle an array. I couldn’t think of a good way to continuously update the seed and in this case I don’t think it matters.
t4t is used to convert content into strings.


As for the text, it was certainly harder to read than the example I had seen. Since most of the letters in the original are not far from their starting point it seems like the original author iterated through each word a couple times randomly deciding to swap pairs of letters[1]. Or they wrote it by hand in a way that was mostly legible.


  1. Could be another small coding challenge ↩︎

9 Likes

As a native English speaker, I find that text completely easy to read… I only paused for a fraction of a second on two words, largely because of an environmental sound taking my attention.

:slight_smile:
Mark

2 Likes

As a native German speaker, I also was able to read the whole text without any problems very fluently.

Maybe it depends on personal skill. I don’t know.

Nonetheless a very cool project! Thank’s for sharing! <3

You could use this:

#let scramble-seed = counter("scramble-seed")

#let scramble-word(word) = {
  ...
  scramble-seed.step()
  context {
    let seed = scramble-seed.get().first()
    ...
  }
}

Not necessary here: you use a regex/text show rule, so a simple let txt-original = word.text will work reliably.


The result, by the way, was pretty unhinged:

Adnrcoicg to a rsreeach at Cagmrbdie Utirsvneiy, it dseon’t mtaetr in waht oedrr the lteetrs in a wrod are, the olny inpotramt tinhg is taht the fsirt and lsat letetr be at the rgiht palce. The rset can be a toatl mses and you can sitll raed it wouhtit peroblm. Tihs is buasece the huamn mnid deos not raed evrey letetr by iltesf, but the wrod as a wohle.

So I agree with your assessment, that the scrambling was possibly not a completely fair shuffle.

I should have included the original scrambled text since I was making a comparison. I’ve now (painfully :sweat_smile:) typed it:

Aoccdrnig to a rscheearch at Cmabrigde Uinervtisy, it deosn’t mttaer in waht oredr the ltteers in a wrod are, the olny iprmoetnt tihng is taht the frist and lsat ltteer be at the rghit pclae. The rset can be a toatl mses and you can sitll raed it wouthit porbelm. Tihs is bcuseae the huamn mnid deos not raed ervey lteter by istlef, but the wrod as a wlohe.
Source

The page goes on to give other example with longer or less common words. They are harder to parse than this Cambridge related one.


@Mark_Hilton I agree that my version is still legible, but I would say it is still harder to read than this original version. I wonder if how well an individual is able to read these texts could be used as an indicator of their reading skill.

@ensko I knew a solution would involve counters or states but … didn’t try. Thanks for doing the work I was too lazy to do :smiley:. And thanks for the word.text hint.

I think that’s a fair point. As a former lecturer in linguistics and translation, I would rate my reading skill as high, and also I have a very extensive, active vocabulary and I think that that is material to the question. I might copy the text and send it to my Chinese collaborator-in-translation, whose English is excellent, to see how she gets on with it.

:slight_smile:
Mark

PS Oh, and as a family we enjoy doing the Times Cryptic Crossword, but my wife and daughter complain that I solve anagrams in my head before they’ve even finished writing out the letters involved… that ability also makes reading those texts easy.

1 Like

I decided to try this one too. After seeing the results I don’t know if it was worth it :sweat_smile:.

Full Code
#import "@preview/suiji:0.5.1": *
#let scramble-seed = counter("scramble-seed")

#let scramble-middle(clusters, seed-increment-outer, chunk-scrambles: 4) = {
  let seed-original = clusters.fold(1, (acc, v) => acc + v.to-unicode())
  
  let assembled = clusters.slice(1, -1)

  //Swap letters a few times so that letters are able to move a bit further away than 1 place
  //Increasing this makes the words _more_ scrambled
  for seed-increment-inner in range(chunk-scrambles) {
    let chunks = none

    //Break the clusters into chunks of 2 and prepare a variable to store randomized characters
    if calc.rem(clusters.len(), 2) != 0 and calc.rem(seed-increment-inner, 2) != 0 {
      //Chunking an odd-length word leaves a remaining (un-chunked) letter
      //Skipping an additional starting letter makes the "middle" even in length
      chunks = assembled.slice(1).chunks(2)
      assembled = (assembled.first(), )
    } else {
      chunks = assembled.chunks(2)
      assembled = ()
    }

    //Iterate over chunks and randomly decide to swap characters
    for (seed-increment-chunks, chunk) in chunks.enumerate() {
      if chunk.len() == 1 {
        assembled += chunk
        continue
      }
      let seed-final = seed-original + seed-increment-outer + seed-increment-inner + seed-increment-chunks
  
      let (_, random-value) = random-f(gen-rng-f(seed-final))
      let swap-chunk = random-value < 0.5
      
      if swap-chunk {
        assembled += (chunk.at(1), chunk.at(0))
      } else {
        assembled += chunk
      }
    }
  }
  
  clusters.first()
  assembled.join("")
  clusters.last()
}

#let scramble-word(it) = context {
  let txt-original = it.text
  let clusters = txt-original.clusters()
  scramble-seed.step()
  let result = text(red, it)

  if clusters.len() < 4 {
    result = txt-original
  } else {
    let seed = scramble-seed.get().first()
    //There's a chance the scrambled word is identical to the original
    //try a few times to avoid this
    for seed-increment in range(10) {
      let txt-scrambled = scramble-middle(clusters, seed + seed-increment)
      if txt-scrambled != txt-original {
        result = txt-scrambled
        break
      }
    }
  }
  result
}

#set page(width: 30em)
#set page(height: auto)
#set page(margin: 1em)

#show regex(`\w+`.text): scramble-word

According to a research at Cambridge University, it doesn't matter in what order the letters in a word are, the only important thing is that the first and last letter be at the right place.  The rest can be a total mess and you can still read it without problem.  This is because the human mind does not read every letter by itself, but the word as a whole.

For swapping letters I went with first chunking the array of clusters then returning either (first, second) or (second, first). This has the downside that it doesn’t handle words that have an odd length very well. I added a mechanism for dealing with it.

It also uses a counter as suggested by @ensko to create a different seed for each word. Originally this wasn’t in there so there are a lot of places that add an increment to the final seed used.