g | x | w | all
Bytes Lang Time Link
085Wolfram Language Mathematica200902T045629Zatt
021Charcoal200902T000041ZNeil
225R200902T130620ZDominic
126R200903T171959Zatt
163Perl 5200906T035724ZKjetil S
137JavaScript ES8200902T120927ZArnauld
01305AB1E200902T084849ZKevin Cr
060J200903T065429ZBubbler
070Pip l200903T023139ZDLosc
318Scala200902T183306Zuser
222JavaScript V8200902T000438ZMatthew

Wolfram Language (Mathematica), 123 122 109 107 87 85 bytes

Print@@@SparseArray[i=p=0;p-=ReIm[++I/I^⌈2√i++⌉/.I+1|I-1->I]&/@#+2i->#,3i," "]&

Try it online!

Input a list of characters.

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


alternative approach, 99 93 bytes

sPrint@@@Array[" "&@@s[[4# #-2#+1-#2&@@If[(a=Abs@#2)<2#,!##,#-a|-#2]]]&,2{L=Tr[1^s],L},-L]

Try it online!

Directly computes the index of each position: in Cartesian coordinates (zero-indexed), \$\operatorname{index}(x,y)=\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}.\$

Charcoal, 37 36 21 bytes

GH✳✳E⊖LθI§4174⌈⊗₂⊕ι²θ

Attempt This Online! Link is to verbose version of code. Explanation: Inspired by @KevinCruijssen's 05AB1E solution, but then using @att's formula to generate the directions.

       θ                Input string
      L                 Length
     ⊖                  Decremented
    E                   Map over implicit range
                  ι     Current index (0-indexed)
                 ⊕      Incremented (i.e. 1-indexed)
                ₂       Square rooted
               ⊗        Doubled
              ⌈         Ceiling
         §4174          Cyclically index to find direction
        I               Cast to integer
  ✳✳                    Convert to directions
GH                 ²θ   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:

≔⮌⪪S¹θFLθF³F§⟦⊕⊗ι⁺³×⁴ι⊗⊕ι⟧κ¿θ✳⁻⁷׳κ⊟θ

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

≔⮌⪪S¹θ

Split the input into characters and reverse the list.

FLθ

Loop a large enough number of times.

F³

Loop for each side of the triangle.

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

Loop for the size of the side.

¿θ

Check whether there is still anything left to print.

✳⁻⁷׳κ⊟θ

Print the next character in the appropriate direction.

R, 270 265 252 243 232 227 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
")}

R, 205 153 147 136 132 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!

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!

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

05AB1E, 24 20 15 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:

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.


Original 24 20 bytes answer:

ā·Ð·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:

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.

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 }

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.

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)

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