A Logo that Mutates

Encounter

One day in 2020, I was searching for and browsing among a list of graduate programs. I stumbled across the official website of Koninklijke Academie van Beeldende Kunsten (Royal Academy of Art), Den Haag, where the logo on the webpage captured me. The institution’s logo is a minimalistic crown figure connected by seven discrete points. It is unique: every time I refresh the page, the connection pattern between the dots randomly alters.

Figure 1. The official website of KABK(kabk.nl).

Since then, the conception of variable logos came into my sight. Some people call them dynamic logos. Probably there isn’t a unified term, but they do seem increasingly prevalent in the new design trends.

Later in September 2020, I was invited to the NkHistory Project as the lead designer and web dev engineer. The project itself is initiated by a group of Nankai alumni and was relatively small in scale, but I was granted the maximum creative freedom. A perfect chance to try something different! I joined the team and started to plan on a branding identity with an experimental variable logo present.

Sketch

NkHistory is designed as a platform for Nankai alumni to share their campus memories. All memories submitted will have corresponding time and location information. As we collected a sufficient amount of memories, users will be able to browse them in a timeline view or a spatial view. The essential idea is kind of like i remember, but different.

With that concept, inspirations of the logo came quite naturally. We brainstormed several linkable visual elements and settled on an ✳ shaped iteration. This logo put together two things: The Nankai Octogram, which is the official logo of the Nankai University and Nankai High School; as well as the asterisk symbol, which represents “any”.

Figure 2. A screenshot from the NkHistory VI system booklet. The original project name in Chinese is 南开通鉴 (lit. Miscellaneous Historical Events in Nankai), and the slogan was "the history that anyone can write" in its early iterations.

That being said, a strictly upright and regular eight-spoked asterisk is inevitably dull. Here, What we are going to do is to randomly disorganize the strokes in it, endowing it with a certain level of casualness and a nonchalant attitude.

Figure 3. In an ideal result, all of the figures shown above are correct logos for this project. A random one will be shown on the webpage every time the user opens it.

Implement

To draw logos like this on the webpage, there are several routes to choose from. In the following section, we’ll discuss three of them.

Div + CSS: Simple & Brutal

Considering the logo essentially consists of a group of rectangles, one option is to use a lot of divs and transform them using CSS. In fact, that’s the way we chose when experimenting with our initial demo posters:

Figure 4. Writing Freewill, the poster of Project NkHistory

In this poster, randomly generated logos are arranged in 17 rows and 13 columns. The first logo is a strictly standard eight-spoked asterisk, and as the index increments, the randomness increases along. To implement this, we could simply define a Vue Component box, specifying a prop randomi as a quantified “randomness factor”:

1
2
3
4
Vue.component('box', {
props: ['randomi'],
template: `<div class="box">${bars}</div>`,
})

In each black box, there are four bars, all of which are made up of four rectangles with two layers of transformation.

  • The first layer of transformation is solely rotation, with the i-th bar rotated to a degree of 45i, so the bars could be at 0, 45, 90, and 135 degrees, respectively, to constitute an asterisk;
  • The second layer is all about randomization, including randomized translation and further rotation.

It is known that the current CSS standard lacks support for multilayered transformations applied on one single element. We could manually calculate the transformation matrices, but that would unnecessarily complicate our codebase. We decided to use wrappers.

The following code only suggests the second layer transformation because the first layer is “static” and could be defined in the stylesheets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Generate r by randomness factor
const r = randomi => (Math.random() - 0.5) * randomi
// User r in the randomize transforms (translateX + translateY + rotate) for each bar
let genStyleObj = randomi => {return {transform: `
translateX(${r(randomi)}px) translateY(${r(randomi)}px) rotate(${r(randomi)}deg)
`}}

// Define the bars
let bars = ''
for (let i = 0; i < 4; i++) bars += `
<div class="bar bar-${i}">
<div :style="styles[${i}]"/>
</div>
`

After setting the container element, we then iterate this process 221 times. Each iteration would push a box with an incremented randomness factor. Note that overflow: visible is required to achieve the effect of boxes travelling out of their bounds.

1
2
3
4
5
6
7
8
9
let boxes = []
for (let i = 0; i < 221; i++) {
boxes.push({randomi: i * 0.2}) // The i-th box with a randomness factor of 0.2i
}

let app = new Vue({
el: '#app',
data: { boxes }
})

SVG: Better Performance

Let alone the demo code above, recall that these variable logos need to be present in large numbers on an actual webpage. The first thing is to determine a more reliable approach for a production environment.

Experience has shown that when a large quantity of simple geometric figures recur, compared to using <div>, SVG is obviously a more adaptable and performant solution. Secondly, considering all possible use cases for this logo, we need it better designed and encapsulated. E.g. configurable parameters such as size, colors and so on, should be exposed.

Yep - again, we’ve come to the transition zone between research and engineering. Time for a change.

This time, we use Vue’s Single File Components. Since the logo is apparently important and reused all across the project, we need to specifically give it a name. How about “Wanderer”?

When designing the inner layout structure of the Wanderer Component, we encountered a problem. Unlike div, which could be defined an overflow: visible CSS property, elements inside SVG cannot be drawn outside the box, at least in some browsers. We could use another pattern to circumvent this: wrapping the SVG in a smaller div centrally. This way, we could allow the elements inside those SVG to wander out of the outer div, but still, on the SVG render area.

Figure 5. Relevant values are calculated and used in the layouts for this pattern. Note how the size of the content is larger than the size of the container. More generally, when some elements may position out of bound due to random transformations, but we still need the container to have a "semantical correct size" exposed, this layout pattern could be utilized.

In the design draft, a large portion of use cases for this logo involve a background box accompanied (See Figure 4). Hence we wish the box could be a configurable option for this component. Meanwhile, the box is not necessarily a regular square; it could be randomly distorted as well.

The main render process is written as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Code inside mounted():

let container = this.$el.querySelector(".wanderer");
let draw = SVG()
.addTo(container)
.size(size * (bleedCoefficient + 1), size * (bleedCoefficient + 1));

// Because the background box is randomized as well,
// We may use SVG polygons with translated vertices in place of SVG rects

draw
.polygon(this.randomizeArray([0, 0, 0, size, size, size, size, 0]))
.attr({ class: "w-polygon", fill: palette[1] })
.transform({
translateX: size * (bleedCoefficient / 2),
translateY: size * (bleedCoefficient / 2),
scale: polygonScale,
});

where randomizeArray is a function placed in the methods() block. We use it to randomize every value inside an array.

1
2
3
randomizeArray: function (arr) {
return arr.map((x) => x + this.r() * 0.3);
},

After taking care of the outside trapezoid, the inner bars (rectangles) could be implemented as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let groups = new Array(4).fill(draw.group());

// draw the 4 bars separately
for (let i = 0; i < 4; i++) {
let group = groups[i];
group
.rect(barThickness, size)
.attr({
class: "w-bar",
x: offsetX,
y: offsetY,
fill: palette[0],
})
// Transform layer 1: rotate each bar to 0, 45, 90 and 135 degrees
.transform({
rotate: i * 45,
});

// Transform layer 2: randomize each bar
group.transform({
translateX: r(),
translateY: r(),
rotate: r() * 0.3,
});
}

During the render process, the values in Figure 5 are used to calculate the correct element sizes and offset values. We also utilized SVG.js to manipulate SVGs inside the element more concisely and clearly.

Just for fun, we could add an animation option to let it swag:

1
2
3
4
5
6
7
8
9
10
11
if (animate) {
setInterval(() => {
groups.forEach((group) =>
group.animate(t).transform({
translateX: r(),
translateY: r(),
rotate: r() * 0.3,
})
);
}, t);
}

Canvas: Lack of Interactivity

When doing graphics on the webpage, SVG is always brought up along with HTML5 Canvas. From a pure performance first perspective, Canvas provides more low-level APIs together with pixel-wide graphic controls and should be more performant. The primary concern here is that Canvas cannot provide straightforward and easy-to-use interactivity functionalities as SVG, nor could it adapt stylesheets and embed in HTML docs seamlessly. At this point, therefore, we do not consider using Canvas.

The phase II of NkHistory might introduce recording memories by precise spatial location, allowing users to specify where their memories occurred on a 3D model of campus buildings. That’s probably when Canvas would come in handy.

Run

Now the components are all set, to quickly wrap up all the exposed props:

  • size - Semantical size of the Wanderer component.
  • randomness - Irregularity of Wanderers’ shape.
  • barThicknessCoefficient - Default to 0.125, which means the length of the bars will be 8 times their width.
  • bleedCoefficient - Default to 0.5, which means the length of the SVG area (inner container) will be 1.5 times the actual component size (outer container) exposed to the parent. Note that for Wanderers with higher randomness, you probably would want to set a higher bleed coefficient as well to prevent drawing out of SVG bound.
  • palette - An array with two color strings, acting as the foreground and the background colors, respectively.
  • polygonScale - Size of the background polygon.
  • polygonRandomness - Irregularity of the background polygon.
  • animate - The animation playback cycle. Setting this to 0 disables the animation.

Let’s experiment with different combinations of props, see what possibilities we can expect.

First, we tried using v-for to render Wanderer 50 times with incremental barThicknessCoefficients:

1
2
3
4
5
6
<Wanderer
v-for="(record, i) in blocks"
:randomness="0"
:size="50"
:barThicknessCoefficient="i / 400"
/>
Figure T1 - Sequence of heavier & heavier Wanderers

Hmm. Looks like it does the job. Next, we’ll try to reimplement the poster given in Figure 4. To do this, we need to specify the correct colors, utilize the background polygon, and then use a sequence of increasing randomness. This time, however, Math.pow() is used instead of an arithmetic sequence. I can already feel how scary exponential growth is.

1
2
3
4
5
6
7
<Wanderer
v-for="(record, i) in blocks"
:randomness="Math.pow(i, 1.3)"
:size="50"
:palette="['white', 'black']"
:polygonScale="1.5"
/>
Figure T2 - The Poster Writing Freewill with excessive freedom

I’m quite curious about randomizing as many configurable metrics as we could. This is what we’d get if we insert Math.random() in the colors, size, thickness coefficient, and even in the randomness prop itself.

It’s a pity that NkHistory is only a small project. If it’s a Silicon Valley tech giant, this is an excellent chance to embed its multicultural inclusion & diversity values.

1
2
3
4
5
6
7
8
9
<Wanderer
v-for="(record, i) in blocks"
:key="i"
:randomness="Math.random()*20"
:polygonRandomness="11"
:size="Math.random() * 50 + 20"
:palette="[randomColor(), '#222']"
:barThicknessCoefficient="Math.random() * 0.33"
/>
Figure T3 - Celebrating Diversity!

If you’re sufficiently imaginative, you could even tweak the Wanderer into something with an unique artistic flavour that you can’t recognize:

1
2
3
4
5
6
7
8
<Wanderer
v-for="(record, i) in blocks"
:randomness="50"
:polygonRandomness="50"
:size="50"
:palette="i % 2 === 0 ? ['white', '#222'] : ['#222', 'white']"
:barThicknessCoefficient="0.01"
/>
Figure T4 - Is it you, Mr. Kandinsky?

Above are static images only. To see it animated, just pass a number to animate. With a few lines of CSS, you could apply some interactivity to it as well:

Figure T5 - Using animation and extra CSS

Random Thoughts

Looks like we’ve gone pretty far, but in fact, merely a total of 200 lines of code were written. No complex logic or design patterns were involved. It’s just that experimenting with interactive graphics is fun. That’s why I’m writing this article.

I would like to end it by pointing out that this is not quite a helpful technical blog post. Usually, we are not encouraged to consume so much time and energy in these perceptual, “uncertain” aspects in a typical project. For most software engineers working for a large company, the plan for each project is rather fixed, and the works of people from different backgrounds are highly decoupled. It’s already rare for a software engineer and a UI/UX designer to work closely and carry out in-depth communications, let alone the less relevant branding division.

Having said that, participating in small projects like this with a small group of people - or only myself - is genuinely an enjoyable experience. Not having a fixed plan makes it possible for new ideas to constantly come out even in the development process. For example, after discovering that randomizing all possible props outputs so many distinct and characteristic graphics, we started to consider assigning a unique random Wanderer to each memory. This way, users will identify it more easily when later browsing among all memories through either the timeline or the spatial view. They may also specify the Wanderer’s appearance associated with their memories. Of course, this might mean that we need to destructure and isolate the randomness prop, injecting new props such as the exact positioning data of the bars.

Soon I’ll be back as a software engineer, again - working on boring business logic on a daily basis. Nevertheless, I’ll be actively looking for the next escape.

+