# Spiralize a Word, Triangularly!

## Challenge

Given a string of any length, write it as a triangle, spiraling out from the center. For example, abcdefghijklmnop becomes:

   g
fah
edcbi
ponmlkj


Or more explicitly:

If you like, you can spiral counter-clockwise instead:

   g
haf
ibcde
jklmnop


      g

f a h

e d c b i

p o n m l k j


The input characters will be ascii, but may include spaces. Also, the number of characters may not be a perfect square (Hello World!):

  W
Ho
oller
!dl


A couple more edge cases. 2 letter input ab:

 a
b


And 3 letter input abc:

 a
cb


## Procedural Description

In case the above examples aren't clear, here's a procedural description of the process:

1. Put down your initial letter.
2. Move diagonally down and to the right (i.e., this direction \). So if you started at (0,0), you will now be at (1,-1). Put down your second letter.
3. Move left one space a time, dropping a letter on each space, for a total of 3 spaces. That is, drop letters on (0,-1), (-1,-1), and (-2, -1).
4. Next move diagonally up and to the right / two spaces, dropping letters on (-1,0) and (0,1).
5. Now cycle back to moving diagonally down and to the right, continuing to step and drop letters as long as your current position is left-right adjacent to an existing letter.
6. Next move left again, continuing to step and drop letters as long as you are diagonally adjacent / to an existing letter.
7. Move diagonally up and to the right again /, stepping and dropping letters as long as your current position is left-right adjacent to an existing letter.
8. Repeat steps 5-7 until all letters are used up.

## Rules

• Code golf, standard rules apply.
• Trailing spaces or newlines are ok.
• Consistent leading spaces or newlines are also ok, as long as the shape of the triangle is preserved.
• It isn't immediately clear to me how to scale this properly. What does a 3 letter input look like? Is it the afg portion? Or would it be the gha? – FryAmTheEggman Sep 1 '20 at 22:48
• I think you would be much better off actually explaining the process, not just giving examples. I could still see someone being confused for, e.g. the case with 5 characters. I believe I get it, in that we essentially fill up the smallest "double triangle" possible, starting from the top of the middle when there's a even number of rows - but that is a lot to have to parse out of some examples. – FryAmTheEggman Sep 1 '20 at 23:22
• What about (consistent) leading spaces and newlines? – att Sep 2 '20 at 2:30
• "Also, the number of characters may not be a triangular number" - nitpicking but you need a square number of characters, not a triangular number of characters, to make a 'perfect' triangle via this algorithm :) – boboquack Sep 2 '20 at 4:46
• Sure, that's fine. – Jonah Sep 2 '20 at 19:50

# 05AB1E, 242015 13 bytes

2Iā¨t·îŽOGsèΛ


-7 bytes by porting @Neil's Charcoal answer, using @att's formula, so make sure to upvote both of them as well!

Try it online. No test suite, because the .Λ builtin will keep its previous contents and there isn't any way to reset it (this is what it would look like.

Explanation:

2              # Push a 2
I             # Push the input-string
ā            # Push a list in the range [1,length] (without popping)
¨           # Remove the last value to change the range to [1,length)
t          # Take the square-root of each value
·         # Double each
î        # Ceil each
ŽOG     # Push compressed integer 6136
s    # Swap so the list is at the top of the stack again
è   # Index each value (0-based and modulair) into the 6136
Λ  # Pop all three and use the Canvas builtin,
# after which the result is implicitly output immediately afterwards


See this 05AB1E tip of mine (section How to compress large integers?) to understand why ŽOG is 6136.

The Canvas builtin uses three arguments to draw a shape:

• Character/string to draw: the input in this case
• Length of the lines we'll draw: 2 in this case
• The direction to draw in: [3,6,6,6,1,1,3,3,3,6,6,6,6,6,6,6,1,1,1,1,3,...].

See the original answer below for an explanation of the Canvas builtin. Unlike the program below where the list of lengths are leading, here the list of directions are leading because we use a single length of 2.

ā·Ð·s>ø.ι˜DŠOð×«₆1ªΛ


Contains leading/trailing spaces and newlines (the longer the input, the more spaces/newlines)

Try it online. No test suite, because the .Λ builtin will keep its previous contents and there isn't any way to reset it (this is what it would look like, where the test cases are drawn on top of one another).

Explanation:

ā           # Push a list in the range [1, length] of the (implicit) input (without popping)
#  i.e. "Hello World!" → "Hello World!" and [1,2,3,4,5,6,7,8,9,10,11,12]
·          # Double each value in this list
#  → [2,4,6,8,10,12,14,16,18,20,22,24]
Ð         # Triplicate it
·        # Double each value of the top copy
#  → [4,8,12,16,20,24,28,32,36,40,44,48]
s       # Swap to get the other copy
>      # Increase each by 1
#  → [3,5,6,9,11,13,15,17,19,21,23,25]
ø     # Create pairs of the top two lists
#  → [[4,3],[8,5],[12,7],[16,9],[20,11],[24,13],[28,15],[32,17],[36,19],[40,21],[44,23],[48,25]]
.ι   # Interleave it with the third list
#  → [2,[4,3],4,[8,5],6,[12,7],8,[16,9],10,[20,11],12,[24,13],14,[28,15],16,[32,17],18,[36,19],20,[40,21],22,[44,23],24,[48,25]]
˜  # Flatten
#  → [2,4,3,4,8,5,6,12,7,8,16,9,10,20,11,12,24,13,14,28,15,16,32,17,18,36,19,20,40,21,22,44,23,24,48,25]
D           # Duplicate this list of integers
Š          # Triple-swap, so the stack order is list,input,list
O         # Pop and sum the top list
#  → 636
ð×       # Create a string of that many spaces
«      # And append it to the string
₆           # Push builtin 36
1ª         # Convert it to a list of digits, and append 1: [3,6,1]
Λ           # Use the Canvas builtin with these three arguments,
# after which the result is implicitly output immediately afterwards


The Canvas builtin uses three arguments to draw a shape:

• Character/string to draw: the input in this case, appended with trailing spaces
• Length of the lines we'll draw: the list [2,4,3,4,8,5,6,12,7,8,16,9,10,20,11,...]
• The direction to draw in: [3,6,1]. The digits in the range $$\[0,7]\$$ each represent a certain direction:
7   0   1
↖ ↑ ↗
6 ← X → 2
↙ ↓ ↘
5   4   3


So the [3,6,1] in this case translate to the directions $$\[↘,←,↗]\$$.

Here a step-by-step explanation of the output (we'll use input "Hello_World!" as example here):

Step 1: Draw 2 characters ("He") in direction 3↘:

H
e


Step 2: Draw 4-1 characters ("llo") in direction 6←:

  H
olle


Step 3: Draw 3-1 characters ("_W") in direction 1↗:

  W
_H
olle


Step 4: Draw 4-1 characters ("orl") in direction 3↘:

  W
_Ho
oller
l


Step 5: Draw 8-1 characters ("d! ") in direction 6←:

   W
_Ho
oller
!dl


Et cetera for all other trailing spaces.

See this 05AB1E tip of mine for an in-depth explanation of the Canvas builtin.

• @att's formula saved me 15 bytes! I wonder whether it would be shorter or longer in 05AB1E... – Neil Sep 2 '20 at 22:33
• @Neil I'm not sure I can use att's formula tbh. The Canvas builtin in 05AB1E simply requires three parameters before drawing everything at once. Those steps I mentioned in the explanation are thus all done at once after providing the arguments. For directions I now simply use ₆1ª, which is [3,6,1] / [↘,←,↗]. The main two things that costs bytes in my program is generating the list of lengths: ā·Ð·s>ø.ι˜, as well as adding trailing spaces to the input to prevent wraparound of the input-string: DŠOð×«. The directions and actually drawing the output are both very short ₆1ª and Λ. – Kevin Cruijssen Sep 3 '20 at 7:04
• Ah, what I maybe didn't explain was that my idea was to use the single length option of the canvas builtin, using a length of 2 so that each direction in the list causes another character to be drawn in that direction. In your case, given that you won't need to pad the input string, you would just need to generate the list of directions in fewer than 18 bytes. – Neil Sep 3 '20 at 9:14
• @Neil Yeah, sorry about that. I made 3 comments or so, and after that I discovered a bug which increased the byte-count by 1 again. I figured I'd just delete my comments instead of adding a fourth, since you would have still gotten the notification for those deleted comments. But in conclusion of those four now deleted comments: thanks for the suggestion, it saved 7 bytes. Funny how you've been inspired by my answer, and I've been inspired by yours in return. :) – Kevin Cruijssen Sep 3 '20 at 12:06
• I would have bet against the possibility of any answer this short, even with Jelly. Well done! – Jonah Sep 3 '20 at 12:27

# Wolfram Language (Mathematica), 99 bytes

sPrint@@@Array[s[[4# #-2#+1-#2&@@If[Abs@#2<2#,!##,#-Abs@#2|-#2]]]/._@__->" "&,2{L=Tr[1^s],L},-L]


Try it online!

Directly computes the index of each position: in Cartesian coordinates, $$\\operatorname{index}(x,y)=\textit{offset}+\begin{cases}2y(2y+1)-x,&|x|<-2y\\ 2\left(y+|x|\right)\left(2\left(y+|x|\right)+1\right)+x,&\text{else}\end{cases}\$$

where $$\\textit{offset}\$$ is the index of the "first" character (1, in Mathematica).

Takes a list of characters as input.

### Older approach, 123122109 107 bytes

Print@@@Normal@SparseArray[i=0;p=2Length@#;(p+=ReIm[I[2+I,1-I][[⌈2√i++⌉~Mod~4-1]]-1])->#&/@#,2p," "]&


Try it online!

The direction of the (1-indexed) ith character relative to the previous character can be computed with $$\\Big\lceil2\sqrt i\Big\rceil\bmod 4\$$:

• 1: ↗
• 2: ↘
• 3,0: ←

# Charcoal, 3736 21 bytes

ＧＨ✳✳Ｅ⊖ＬθＩ§4174⌈⊗₂⊕ι²θ


Try it online! No verbose link because the deverbosifier outputs ✳σ instead of ✳✳, generating invalid succinct code that doesn't execute correctly, but if it did work then it would be PolygonHollow(Directions(Map(Decremented(Length(q)), Cast(AtIndex("4174", Ceiling(Doubled(SquareRoot(Incremented(i)))))))), 2, q);. Inspired by @KevinCruijssen's 05AB1E solution, but then using @att's formula to generate the directions. Explanation:

       θ                Input string
Ｌ                 Length
⊖                  Decremented
Ｅ                   Map over implicit range
ι     Current index (0-indexed)
⊕      Incremented (i.e. 1-indexed)
₂       Square rooted
⊗        Doubled
⌈         Ceiling
§4174          Cyclically index to find direction
Ｉ               Cast to integer
✳✳                    Convert to directions
ＧＨ                 ²θ   Draw path using input string


The path drawing command draws one character for the start and then n-1 characters for each direction in the array. Unfortunately there aren't any single character strings that represent diagonal directions so I have to use integers instead; these start at 0 for right and increment for each 45° clockwise.

Previous 37 byte solution:

≔⮌⪪Ｓ¹θＦＬθＦ³Ｆ§⟦⊕⊗ι⁺³×⁴ι⊗⊕ι⟧κ¿θ✳⁻⁷×³κ⊟θ


Try it online! Link is to verbose version of code. Explanation:

≔⮌⪪Ｓ¹θ


Split the input into characters and reverse the list.

ＦＬθ


Loop a large enough number of times.

Ｆ³


Loop for each side of the triangle.

Ｆ§⟦⊕⊗ι⁺³×⁴ι⊗⊕ι⟧κ


Loop for the size of the side.

¿θ


Check whether there is still anything left to print.

✳⁻⁷×³κ⊟θ


Print the next character in the appropriate direction.

• Right tool for the job... – Jonah Sep 2 '20 at 0:03
• What's a good place to learn Charcoal? – Razetime Sep 2 '20 at 4:33
• @Razetime I just learned it from reading its github wiki and writing nearly all of my programs using verbose mode. – Neil Sep 2 '20 at 9:02
• I've read through the tutorial and things, but was unable to find any extensive docs for verbose mode. Maybe I should go through it again. – Razetime Sep 2 '20 at 9:06
• @Razetime I see what you mean, but fortunately verbose mode mostly looks like a traditional programming language, so all you usually need is the list of commands and operators. – Neil Sep 2 '20 at 9:13

# JavaScript (V8), 222 bytes

s=>(g=(a,b,n=1)=>b?g([(c=(b+' '.repeat(n*8)).slice(0,n*8))[n*6-1],...[...a,c.slice(0,n*4-1)].map((l,i)=>c[n*6+i]+l+c[n*6-2-i])],b.slice(n*8),n+1):a)([s[0]],s.slice(1)).reduce((p,l,i,a)=>p+' '.repeat(a.length-i-1)+l+
,'')


Try it online!

It most definitely can be golfed more.
I use a recursive algorithm, splitting the output into triangle 'layers', where each layer is a complete wrap (three sides) of the previous triangle.

Ungolfed

s=>(g=(a,b,n=1)=> // g is a recursive function; a: previous; b: rest; n: increment
b ? // if there is more string to wrap
g([ // wrap b around a as a triangle and recurse
(c=(b+' '.repeat(n*8)).slice(0,n*8))[n*6-1],
...[...a,c.slice(0,n*4-1)].map((l,i)=>c[n*6+i]+l+c[n*6-2-i])
],
b.slice(n*8),
n+1)
:a // otherwise return the triangle
)
([s[0]],s.slice(1)) // run the function with the first letter and the rest
.reduce((p,l,i,a)=>p+' '.repeat(a.length-i-1)+l+'\n','') // step the triangle to make it look like it is meant to

• I think you can remove the first .slice(0,n*8), since leading/trailing spaces/newlines are allowed. – Kevin Cruijssen Sep 2 '20 at 12:17

# JavaScript (ES8), 137 bytes

Expects an array of characters. Returns a string.

This version is based on the formula used by @att, modified to be more golf-friendly in JS.

a=>a.map((c,n)=>(m[y+=~(d=2*n**.5-1/n)%4%3?d&++x/x||-1:!x--]=m[y]||[...''.padEnd(x)])[x]=c,m=[],x=y=a.length)&&m.map(r=>r.join).join



Try it online! (raw output)

Try it online! (with extra whitespace removed)

### How?

Given the position $$\n\$$ of the character, the direction $$\0\le d\le 2\$$ can be computed with:

$$d=\left(\left\lfloor2\sqrt{n}+1-\frac{1}{n}\right\rfloor\bmod 4\right)\bmod 3$$

The actual JS implementation is:

~(2 * n ** 0.5 - 1 / n) % 4 % 3


which evaluates to $$\0\$$, $$\-1\$$ or $$\-2\$$.

# JavaScript (ES8),  163  157 bytes

Expects an array of characters. Returns a string.

a=>a.map(c=>((m[y]=m[y]||[...''.padEnd(x)])[x]=c,j%3%2?x--:y+=!!++x-j%3,k?k--:k=(n=j/3<<1)+(j++%3||n+2)),m=[],j=k=0,x=y=a.length)&&m.map(r=>r.join).join



Try it online! (raw output)

Try it online! (with extra whitespace removed)

### How?

This is a rather straightforward algorithm that draws the output character by character in a matrix $$\m[\:]\$$, keeping track of the position $$\(x,y)\$$ of the pen, a direction in $$\\{0,1,2\}\$$ and the number $$\k\$$ of characters to draw before the next direction change.

We move according to the following table:

 direction | moving towards | distance
-----------+----------------+----------
0     | South-East     |  2t + 1       (t = turn number)
1     | West           |  4t + 3
2     | North-East     |  2t + 2


Which gives:

t = 0     t = 1        t = 2            t = 3

2
2               2.
2           2.              2..0
2        2.          2..0            2....0
2X       2.X0        2..X.0          2...X..0
1110     2....0      2......0        2........0
11111110    2........0      2..........0
111111111110    2............0
1111111111111110


In the JS implementation, we do not store the direction explicitly. Instead, we use a counter $$\j\$$ going from $$\0\$$ to $$\+\infty\$$ and use $$\j\bmod 3\$$ to figure out the current direction. We also do not store the turn number but compute $$\n=2\cdot\lfloor j/3\rfloor\$$, using the value of $$\j\$$ before it's incremented to account for the direction change (which means that $$\n\$$ is equal to $$\2(t-1)\$$ rather than $$\2t\$$ when the direction wraps to $$\0\$$).

Hence the following table:

     j mod 3     |  (j + 1) mod 3  |                     | new starting
(old direction) | (new direction) |    new distance     | value for k
-----------------+-----------------+---------------------+--------------
2        |        0        | (n + 2) + 1 = n + 3 |     n + 2
0        |        1        |      2n + 3         |    2n + 2
1        |        2        |       n + 2         |     n + 1


And the corresponding expression to update $$\k\$$:

k = (n = j / 3 << 1) + (j++ % 3 || n + 2)


The coordinates are updated with:

j % 3 % 2 ?          // if the direction is 1:
x--                //   decrement x
:                    // else:
y += !!++x - j % 3 //   increment y if the direction is 0
//   or decrement y if it's 2
//   increment x in both cases

• Related: oeis.org/A332057 – Jonah Sep 2 '20 at 18:00
• Do you precompute the needed size of the matrix or expand it as you go? – Jonah Sep 2 '20 at 18:04
• @Jonah I start far away enough from the origin to avoid negative coordinates and expand the matrix as I go. More precisely: each row is created when it's visited for the 1st time, with as many leading spaces as needed. – Arnauld Sep 2 '20 at 18:33

# R, 205153147136132 126 bytes

-52 from Dominic van Essen.
-4 from Giuseppe.
-4 again thanks to Giuseppe.
-5 further thanks to Dominic van Essen

function(s,n=nchar(s))for(y in(x=-n:n)*2)cat(ifelse((i=(t=y-2*(r=abs(x))*!r<y)*t-t-2*(r<y)*x+x+1)>n," ",substring(s,i,i)),"
")


Try it online!

• 149 bytes. I'll give this a bounty once the question is eligible. Very nice work! – Giuseppe Sep 4 '20 at 0:24
• @Giuseppe Nice! I didn't know about write (rather than write.table) before. Any idea why the argument 1 works instead of "" to STDOUT? – Dominic van Essen Sep 4 '20 at 6:34
• @DominicvanEssen I presume because 1 represents stdout as in the shell: Try it online! – Giuseppe Sep 4 '20 at 14:07
• 132 bytes good grief I'm going to have to wait a week just to see how low this goes before picking the bounty amount! – Giuseppe Sep 4 '20 at 20:09
• This is incredible, and I've now lost track of how it works at all, but anyway 131 bytes... – Dominic van Essen Sep 6 '20 at 8:43

# Pip-l, 70 bytes

sMC:Y#ax:w:y/2-/2Ly*2L++i/2{I++v<ys@w@x:a@vi%4%3?++x&i%4=1?--w++w--x}s


Try it online!

... I'm not sure I want to try explaining this monstrosity in detail. The basic idea is to construct an over-big 2D array of spaces (sMC:#a) and then put the characters from the input string into the array at the proper indices (s@w@x:a@v). The rest of the code figures out what the "proper indices" are.

Alternate approach, 77 bytes:

a.:sX#aW<|a{UpaWa^@YxNl?v+1++v%2?v*2+1vl:xNl?RV^p.:lv%2?lPEpl.^pAEx}RVsX,#l.l


Try it online!

Builds the triangle as a list of lines, alternating between adding lines to the front/end of the list and adding characters to the front/end of each line. I was hoping this way might be shorter, but so far it seems it's not.

• I think this is the approach I'd have to take to solve this in J as well. – Jonah Sep 3 '20 at 2:48

# J, 60 bytes

4 :'x(<"1(#x)++/\(3|4|>.2*%:i.#x){<:3 3#:3 2 8)}y',~' '"0/,~


Try it online!

Obligatory J answer because it's Jonah's challenge.

Because "replace a certain position inside an array with a value" is not a verb but an adverb, it can't be used in a train as-is, so it is wrapped inside an explicit inline verb.

Uses att's formula to construct the directions.

### How it works

NB. input: a string (character vector) of length n

,~' '"0/,~  NB. create a large enough canvas (blank matrix of size 2n*2n)
,~  NB. concatenate two copies of self
/    NB. outer product by...
' '"0     NB.   a constant function that returns blank per character
,~          NB. concatenate two copies of self

4 :'...'  NB. a dyadic explicit verb, where x is the input string and
NB. y is the canvas generated above
x(...)}y    NB. replace some places of y by contents of x...
3|4|>.2*%:i.#x  NB. formula by att (gives 0, 1, or 2 per index)
(...){          NB. select the directions based on the above...
<:3 3#:3 2 8    NB. the matrix (0 -1)(-1 1)(1 1) i.e. L/RU/RD
(#x)++/\        NB. take cumulative sum (giving coords to place each char)
NB. and add n to all elements
<"1             NB. enclose each row to satisfy the input format of }

• It can be made as a train as { takes gerunds but it is … one byte longer. – xash Sep 3 '20 at 8:35
• Very nice work! – Jonah Sep 3 '20 at 12:22

# R, 270265252243232227 225 bytes

I finally managed to remove 2 more characters to bring the total to a number that can be represented in triangular form (as shown here). Code should be formatted conventionally to be run (as in the example on TIO); the '•' represents a newline (\n) character.

              f
unc
tion(
s,~=c
bind,m=ma
trix){n=nch
ar(s)+1;p=m(,
n^2,2);while(T<
n){a=4*F;p[T+0:a,
]=c(F:-F,(-F:F)[-1]
)~0:a-2*F;p[T+a+-2:a+
3,]=(F=F+1)~(b=2*F-1):-
b;T=T+2*a+4};m=m(" ",n,n)
;m[p[2:n-1,]+b+1]=el(strspl
it(s,''));apply(m,1,cat,"•")}


Try it online!

Note that this approach has been comprehensively outgolfed by att's approach, although as consolation neither that nor any of the other current answers can be represented as a triangle...

Works by constructing the coordinates for each letter, then using this to put the letters into an empty matrix.

Commented:

triangle=
function(s){n=nchar(s)          # n is the number of letters
s=el(strsplit(s,''))            # first split the string into individual letters
p=matrix(,2,n^2)                # initialize p as a 2-row matrix to hold the coordinates
# (with plenty of columns so that we've enough to go all
# the way round the outermost triangle)
# now, F is the current loop, starting at 0
while(T<=n){                    # T is the current letter index
a=4*F+1                         # a=the size of the 'arch' (number of letters going up & over)
p[,T+1:a-1]=                    # set the coordinates for the arch letters...
rbind(                        # ...(rbind combines rows for y & x coordinates)...
c(F:-F,(-F:F)[-1]),         # ...to y = F..-F, and then -F+1..F (so: up & then down again)
1:a-2*F-1)                  # ...and x = across the arch from -2*F to +2*F
a=a+2                           # a=now the width of the base = size of arch + 2
p[,T+a+1:a-3]=                  # now set the coordinates of the base letters...
rbind(                        #
F+1,                        # ... base y = row F+1
(b=2*F+1):-b)               # ... and x = goes (backwards) from 2*F+1..-2*F-1
T=T+2*a-2                       # update the current letter index
F=F+1}                          # increment the loop
p=p[,1:n]                       # delete any excess coordinates
p=p-min(p)+1                    # re-zero the coordinates to remove negatives
m=matrix(" ",b<-max(p),b)       # create a new matrix filled with " "
m[t(p)]=s                       # and fill it with the letters at the right positions
n=apply(m,1,cat,"               # finally, print each row
")}

• 243. This challenge is brutal for R. I'll give a bounty of at least 150 for an answer less than 200 bytes (and I'll happily help golf it, too) – Giuseppe Sep 2 '20 at 16:54
• Thanks for the golfs! I really ought to proof-read my submissions! p was defined in that contorted way because I'd been subtracting the row & col coordinate mins separately to (unneccessarily) minimize whitespace, but finally gave-up and forgot to switch it back to the sensible way around! – Dominic van Essen Sep 2 '20 at 17:00
• I'm not familiar with R, so you can probably improve from 205 bytes – att Sep 2 '20 at 17:55
• @att - I think it's down to 197 bytes now, but since the approach is completely different I think you should post this yourself. Well done! – Dominic van Essen Sep 2 '20 at 22:45
• @att post it! R's the language of the month, and you can claim my bounty now that it's under 200 bytes :-) – Giuseppe Sep 2 '20 at 23:49

# Scala, 322 318 bytes

s=>((s zip Seq.unfold((0,0,0->0)){case(r,n,y->x)=>Option.when(n<s.size){val(c,t)=(math.sqrt(n).toInt%2,r+1-math.abs(x.sign))
(y->x,(t,n+1,(y+(c-1)*(1-t%2*2),x+1-c*2)))}}groupBy(_._2._1)toSeq)sortBy(_._1)map(_._2.sortBy(_._2._2)map(_._1)mkString)zipWithIndex)map{t=>" "*(math.sqrt(s.size).toInt-t._2)+t._1}mkString "\n"


Try it in Scastie (doesn't work in TIO)

# Perl 5, 163 bytes

sub f{                    #newlines and indentation added here for readability.
$_=' 'x1e3; @L=(51,$a=-1,-49)x($p=225); for$c(pop=~/./g){
$P=$p+$L[1];$a++>0&&s/^(.{$P}) /$1$c/s&&($p=$P,$a=0,shift@L)||substr$_,$p+=$L[0],1,$c
}
s/.{50}/\$&\n/gr
}


In short, it adds the next char from the input in the current direction unless it discovers it's time to change direction.

Try it online!

• Won't this fail if the string gets long enough? – Jo King Sep 7 '20 at 0:31
• It will. I thought about changing the constant numbers into numbers calculated from the length of the input but never got to it :-/ – Kjetil S. Sep 7 '20 at 9:44