It's cold. It's dark. There are loud bangs everywhere and I'm slightly terrified. But I found my crew, eventually. This should be a fun month.
My Fujifilm XT-3 stopped working right before I left for Europe and I didn't have time to fix it. Thankfully my friend Isaac lent me a Nikon D700. Shooting fireworks with it reminds me of my old D7000, which I guess is the baby version of this thing.
Note: this is a collab with classmate, homie and general legend Mishka. Being both design-oriented, we split tasks rather than roles, both doing a bit of design and a bit of coding. For Mishka's perspective, read her documentation.
These videos give a quick summary of what we did between Project 02 and 03—read on below for details!
Previously…
For Project 02, Mishka and I built about 65% of a functioning quiz game about our cohort. We started by collecting fun facts from our classmates via Google Forms, worked on a design in Figma and following the quiz example video, created a sockets-based quiz where multiple people could answer questions simultaneously.
Except this didn’t quite work. First of all, Glitch was glitching so we could only run the app locally. Because we had originally envisioned everybody playing along in a joint session, sockets initially made sense. Once we pivoted to having it so that each person could play individually, serving questions via sockets meant every time one player requested a new question, everybody got one. We also didn’t have the score updating properly (more on that later, I still don’t understand object notation). Finally, we hadn’t had time to design an end state for the game in Project 02.
TL;DR Here’s The To-Do List
[x] Get it working on Glitch!!!
[x] Move the database of fun facts to Mongo DB so that people can add questions directly to the game
[x] Create a submission form to do the above
[x] Switch from each question having an array of answers to dynamically generating “wrong” answers from the answers to other questions (this will scale better if more people get added)
[x] Figure out the async thing—each player should get a unique instance of the game without influencing others’ questions, BUT the scoreboard should be visible to other players
[x] Easiest way is to use express fetch for all the quiz stuff instead of sockets, and only use sockets to broadcast the user id and store whenever someone joins, leaves, or has a new score
[x] Get the score updating correctly (get the user object by id and update the score ++ on correct answer)
[x] Full-screen Menu
[ ] Add a leaderboard?
[x] End the game! Doesn’t quite work right now.
[x] Styling this screen
[x] Design a footer, about page or other context section
[x] Audio?
[ ] New Avatars for folks who submitted
[x] Find a way to use a mystery/silhouette avatar for anybody who doesn’t have one
[x] Environment variables locally and on Github
[x] Add question number before each question (to be updated to progress bar later?)
Migrating to Mongo
The first thing we did was move the database of questions and answers to MongoDB using the Insert Document feature. This would allow us to add an in-game feature where people could add their own fun facts to the game, replacing us hard-coding them.
Using QuickMongo, we connect to the database to pull our question list and add new questions. This was a pain to get working—eventually, it did work but made a whole new databse, ignoring the one we built. I was way too tired at this point to examine why. Trust the process and move on, I suppose.
db.connect();
console.log("db connected!");
// add route to get all questions from database
app.get('/getQuestions', async (req, res) => {
try {
// Fetch from the DB using await
const questions = await db.get("questions");
console.log(questions);
if (!questions) {
return res.status(404).json({ error: "No questions found" });
}
let obj = { questions };
res.json(obj);
} catch (error) {
console.error("Error fetching questions:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// Fetch request (type POST) to send the name and fact to the server
fetch('/funFact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
console.log(data);
All of this required a slight structural change (more on that below). Each question previously contained “wrong” answers to make the quiz multiple-choice:
But asking someone to submit four other names (or going in ourselves and doing that) felt unsustainable, so…
Dynamic Multiple Choice
After a chat with Dora, we came up with some logic for generating options dynamically:
Get all questions → shuffle all questions → create new array of only the answers → find and remove duplicates in this array → grab a question → add 4x random answers from the answers array
This is a good time to write a shuffle function as we’ll be using it for a few things:
// Function to shuffle an array
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
// Pick a random index from 0 to i
const j = Math.floor(Math.random() * (i + 1));
// Swap array[i] with array[j]
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
Now we’ve got that, we can go ahead and shuffle the questions as soon as we load them into the game.
window.addEventListener('load', () => {
// Get all questions from server
fetch('/getQuestions', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
.then(response => response.json())
.then(data => {
console.log(data);
// Shuffle the questions
shuffledQuestions = shuffle(data.questions);
Then we’ll create an array with just the answers and remove duplicates (some people have multiple facts in the game). Googling how to do this, I stumbled across the sets method:
// Grab the names from the questions and put them in an array
let answerPool = [];
for (let i = 0; i < shuffledQuestions.length; i++) {
answerPool.push(shuffledQuestions[i].answer);
}
// Check answerPool for duplicates and remove them
uniqueAnswers = [...new Set(answerPool)];
console.log(uniqueAnswers);
// console.log(shuffledQuestions);
})
Now we want to create an array of options for the question. It took a while to figure this out but if you add the correct answer to the array first, you can check against it when you start adding random options. This will also avoid duplicates within the randoms.
// create an array to store the options
let options = [];
// add the correct answer to the options array
options.push(answer);
// pick 4 other answers from the uniqueAnswers array, excluding the correct answer and not repeating any
while (options.length < 5) {
let randomAnswer = uniqueAnswers[Math.floor(Math.random() * uniqueAnswers.length)];
if (!options.includes(randomAnswer)) {
options.push(randomAnswer);
}
}
Then we use the shuffle function again to scramble the answers so the correct answer isn’t always the first one. Now we can go ahead and build out our question and answers in HTML as we did back in Project 02.
Avatars
We insert Mishka's lovely avatars into the game dynamically by giving them the exact same name as the answer. That way, we can use the answer string ("options[i]" in the loop below) to declare the name of the background image. With people able to add themselves to the game at any time, we needed a way to catch any names without a corresponding avatar. We made a “default” avatar (later expanded to six for variety) and used a conditional to specify the default if no match can be found. The internet suggested switching from background images to <img> elements so we could change innerHTML.
// create a button for each option
for (let i = 0; i < options.length; i++) {
let option = document.createElement('div');
option.id = "option" + i;
option.className = "option";
// create an image element for the avatar
let avatarImg = document.createElement('img');
avatarImg.src = "./avatars/" + options[i] + ".svg";
avatarImg.alt = "Avatar";
// if the image fails to load, use the default avatar
avatarImg.onerror = function () {
//pick a random number between 1 and 6
let randomAvatar = Math.floor(Math.random() * 6) + 1;
// set the image to one of six defaults
this.src = "./avatars/default" + randomAvatar + ".svg"; // Set the default image path
};
// append the image element to the option div
option.appendChild(avatarImg);
Live Scores
We previously had this effectively working in the code, I just couldn’t figure out how to display it because OBJECT NOTATION MAKES NO SENSE. Also, sockets stuff.
Every time someone connects, or every time a score is updated, our server is listening for the list of users and scores, and emits an updated list.
On the client side, we update the current score variable whenever they get an answer right.
io.on('connection', (socket) => { // when a new user connects
console.log("we have a new player: " + socket.id);
io.emit('user scores', users); // send an object with key "user scores" and value [all online players] to everyone
const userID = socket.id; // store the user's socket id in a constant
users[userID] = {}; // create an empty object for the user in the users object
socket.on('new user', (name) => {
console.log('Our new player is called: ' + name);
users[userID].name = name; // add the name to the user's object
});
// listen for user scores and update the users object
socket.on('user scores', (score) => {
console.log('User scores: ' + score);
users[userID].score = score; // add the score to the user's object
console.log(users);
io.emit('user scores', users); // Why do we do it both here and above?
});
socket.on('disconnect', () => {
delete users[userID]; // remove a player from the users object when they disconnect
console.log(users);
});
});
if (options[i] == answer) {
currentScore++; // add 1 to the score
console.log("current score: " + currentScore);
// emit the updated score to the server
socket.emit('user scores', currentScore);
}
I do not for the life of me remember this part but apparently we update the scoreboard like this?
// Function to update the scoreboard UI
function updateScoreboard(users) {
const scoreboard = document.getElementById('scoreboard');
scoreboard.innerHTML = "<span style='font-weight: 600;'>Live Scoreboard</span>";
Object.keys(users).forEach((userID) => {
const user = users[userID];
if (!user.name) {
return;
}
const scoreElement = document.createElement('div');
scoreElement.className = 'score';
scoreElement.textContent = `${user.name}: ${user.score || 0}`;
scoreboard.appendChild(scoreElement);
});
}
Ending the game
Our new end sequence displays your final score along with an assessment of your prowess. Ruta mentioned in playtesting that this would be a good moment to add a fact to the game, having just gone through it yourself.
Navigation
Full-screen overlay menus are surprisingly hard. The method we used involves a hidden overlay div that is set to display when the menu button is clicked.
<!-- Overlay Menu -->
<div id="myNav" class="overlay">
<!-- Button to close the overlay navigation -->
<img src="/img/icon_close.svg" alt="close menu" id="close-button" />
<!-- <a href="javascript:void(0)" class="closebtn" onclick="closeNav()">×</a> -->
<!-- Overlay content -->
<div class="overlay-content">
<a href="/">Play Now</a>
<a href="about.html">About the Game</a>
<a href="submit.html">Submit a Fact</a>
<p class="credits">✌️ by <a href="<https://www.mishka.design>">Mishka</a> & <a
href="<https://www.nuff.design>">Nuff</a></p>
</div>
</div>
<!-- Use any element to open/show the overlay navigation menu -->
<img src="/img/icon_menu.svg" alt="open menu" id="menu-button">
// Open Menu
document.getElementById('menu-button').addEventListener('click', () => {
openNav();
console.log("menu button clicked");
});
//Close Menu
document.getElementById('close-button').addEventListener('click', () => {
closeNav();
console.log("menu button clicked");
});
Music
Mishka found SFX files to add some character to the game. A future goal here would be a sound on/off toggle and maybe background music.
Review & Feedback
During our review with Julie and Pierre, a couple of things came up:
It was possible to keep clicking answers until you got the right one.
If you did this, multiple “next question” buttons appeared on screen and didn’t go away
We can solve both issues by disabling the buttons once you’ve selected—this also adds a little bit of feedback for the player.
Some people mentioned they would like to know the correct answers and maybe we’ll do that eventually but our position is that not revealing them keeps the game interesting for repeat play.
In Future…Stretch Goals
Leaderboard
Leaderboard sorts first by score then by speed
We’d need a timer
Animations
Interface motion
Microinteractions (right/wrong answer icons)
Avatars smile/frown?! (would need 3 states per avatar, unsustainable but funny)
Confetti if you get 5/5
A way to prioritise “fresher” questions as people play more and add more
In-game avatar builder (Ryan!)
Session mode (people play simultaneously, fastest correct answer gets the points)
“Build-your-own-quiz” mode where you play with a temporary database of only your group’s questions
Then we can charge $$$ to companies who want to do team bonding stuff
For Project 02, our brief is to “create a web application that allows a user to send and receive data” using NodeJS, Express and either a database library for storage, Socket.io for real-time multi-person data exchange or both.
The aim is to produce an experience where users can interact, either alone or with one another, and submit, store and/or exchange data.
We're working in pairs for this project. Mishka and I have been paired again for our first collab since the very first Low Res project. As two designers who aren't particularly strong at code, I predict an aesthetic experience. Hopefully it'll also be a functional one.
We decided to each do a bit of everything, as our skillsets are quite similar. We both enjoy the front-end, visual side of things so it was important we each did a little back-end. Also, we both tend to procrastiwork by doing things that might not be the most critical to avoid the things we'd rather not be doing. Knowing that up front, we were able to manage the process and only had one really late night.
Ideation
Tic tac toe
Connect 4
A game involving a grid of squares where players hover over each one to change its colour. The player with the most squares at the end of a time period wins (I might end up doing this for my late week 7/8 homework)
Pong
A trivia game where people have to guess a fun fact about members of the cohort (of course we ended up doing the hardest one)
In class, each team presented their idea and received feedback. The overwhelming class favourite was the quiz game, so we whipped up a submission form on the fact and got 12 responses—enough to build a database.
Early Questions
Should the game be multiplayer and synchronous, multiplayer and asynchronous or single-player?
How will we store the "questions" (facts) and "answers" (people connected to those facts)?
JSON file with “question”, “options” (array) and “answer” key-value pairs.
In the future, it could be interesting to have the code pick random “wrong” answers in real time and scramble them together with the correct answer. This might require a different database structure (especially combined with user submission as below)
How will we make use of Socket.io and/or MongoDB?
For now, following the Quiz example in our weekly lessons, we'll use Socket.io to serve questions to the client, receive the answer and check it against the correct answer. This way, the client never actually receives the correct answer data and can't cheat by inspecting the code.
MongoDB will come in later for storing scores and creating a leaderboard.
As a stretch goal, we could also use a database to enable adding more fun facts. The user would submit their fact, then either select their name from the database if it already exists or add it as a new entry. We'd need to keep track of the relationship between fact and person and be able to point multiple facts to the same person.
Essential
Nice To Have
Random Ideas
Match fact to person (JSON key-value pairs?)
Multiplayer (either first correct answer gets the point or all correct answers get a point)
Timer - either each question has a countdown or the whole thing has a countdown
Award a point for each correct answer; keep track of correct answers
Avatars for each answer (cohort member)
Ranking players by score
Display a points total
Post-answer feedback (show what you chose and what the right answer was)
Ability for players to pick their own name
End the game after a certain number of questions
Randomise the “wrong” options and shuffle the “right” one in the array (the alternative for this is to show every possible answer or manually pick the options)
Seeing who other players picked (after everybody has picked)
Randomise question order
Add your fact directly to the database (MongoDB)
Link to submit your fact (G Form)
SVG Avatars for each member of the cohort
Design
After a brief collaborative wireframing session, we whipped up a quick mockup in Figma to work through some key interface concepts:
Question and answer would display as the main part of the “app”
We'll display both a name and image for each answer—Mishka will make avatars
Question count, timer and scoreboard (featuring any live players)
(Because we ended up with an asynchronous game, we got rid of the timer)
Creating the Database
Our Google form links to a spreadsheet, from which we can technically export direct to JSON. However, it wouldn't be exactly formatted for our needs, so I manually made a file (not too hard with only 12 questions but if we wanted to expand the game, it'd be a nightmare).
"questions": [
{
"question": "I went to the same high school as Madonna.",
"options": [
"Ruta",
"Ryan",
"Anastasia",
"Amber",
"Elisabeth"
],
"answer": 1
},
First, we have a key-value pair where the key is "questions" and the value is the array of questions
Each item in that array has 3 further pairs
question: the value of which is the fun fact
options: the value of which is an array of multiple-choice answers
answer: the value of which is the position in the array of the correct answer (remember, coding numbers start from zero, so "1" in this case is the second answer, Ryan.
Organising the data like this allows us to manually adjust the difficulty of the question by choosing convincing "wrong" answers. For example, if you know where Madonna grew up, you might want to add other people who could conceivably have gone to school in the same area (or at least country).
Building
OK, so I don't really get sockets after a few weeks of trying and failing. Dora helped Mishka and I figure the setup out for our index.js (server) and public/app.js (client).
Let's start with index.js. First, we require express, node, socket.io and our JSON file, quiz.json.
Next, we initialise our HTTP Server, Socket.io and express.
//Initialise HTTP Server
const app = express();
const server = createServer(app);
const port = process.env.PORT || 3000;
server.listen(port, () => {
console.log("Server listening at port: " + port)
});
//Initialise Socket.io
const io = new Server(server);
app.use('/', express.static('public'));
const users = {} // object to store names and scores
Everything from here will require a connection, so we wrap it in an io.on('connection'):
io.on('connection', (socket) => { // when a new user connects
console.log("we have a new player: " + socket.id);
io.emit('user scores', users); // send an object with key "user scores" and value [all online players] to everyone
const userID = socket.id; // store the user's socket id in a constant
users[userID] = {}; // create an empty object for the user in the users object
On the client side, we want players to be able to choose their own names, so we get the value from an input field and emit it back to the server. We'll attach it to the User ID there. This also lets us show every player currently online.
let socket = io();
const submitButton = document.getElementById("submit-button");
submitButton.addEventListener("click", () => {
//get the user's name from input box
const playerName = document.getElementById('name-field').value; // set playerName to what was just typed in the field
socket.emit('new user', playerName);
})
Back in index.js, we'll grab the player's name and add it to their object.
socket.on('new user', (name) => {
console.log('Our new player is called: ' + name);
users[userID].name = name; // add the name to the user's object
Getting and Displaying Questions
To get a question, we'll emit a request for a question from the client
socket.emit('getquestion');
console.log("requesting a question");
On the server, we listen to this request and serve a random question from the array of questions. To do this, we need a questionNo variable.
// When client requests a question, send a random question
socket.on('getquestion', () => {
console.log("question requested");
// Get a random question from our array
questionNo = Math.floor(Math.random() * quiz.questions.length);
console.log(questionNo);
// send the question and options to client
io.emit('question', quiz.questions[questionNo].question, quiz.questions[questionNo].options);
// log what we sent
console.log(quiz.questions[questionNo].question);
console.log(quiz.questions[questionNo].options);
});
By only sending the question and options, we never reveal the correct answer.
We use an empty div (or section?) in the original HTML as a placeholder for the quiz content and append both the question and answers as children of quizDiv. For the answers, we create a for loop with the length of the array, options.length, to create a button for each answer.
Something that'll be super useful later: we create a URL to the image file for each answer by inserting the name stored in the options array of our JSON into an otherwise static link. This means all we have to do is name the images exactly the same as the answers and put them in the same folder, and we'll always have the right image for the right person.
Finally, we emit an "answer" event once the player has made a selection. For now, in the absence of a timer, we won't let players change their answer.
//create new div for question and add a paragraph with the question text
let quizDiv = document.getElementById('quiz');
let questionText = document.createElement('p');
questionText.id = "question";
questionText.className = 'question';
questionText.innerHTML = question;
quizDiv.appendChild(questionText);
//create new div for options
let optionsDiv = document.createElement('div');
optionsDiv.id = "options";
optionsDiv.className = "options";
quizDiv.appendChild(optionsDiv);
// create a button for each option
for (let i = 0; i < options.length; i++) {
let option = document.createElement('div');
option.id = "option" + i;
option.className = "option";
// add avatar to the button
let avatarURL = "./avatars/" + options[i] + ".png";
option.style.backgroundImage = `url(${avatarURL})`;
// add option text below the avatar
option.innerHTML = options[i];
optionsDiv.appendChild(option);
// when player selects answer
let optionButton = document.getElementById("option" + i);
optionButton.onclick = function () {
if (!isAnswered) {
optionButton.classList.add("selected");
socket.emit("answer", i);
console.log("Player answered: " + i);
isAnswered = true;
}
}
}
})
Styling Questions
Mishka made incredible avatars for each of the people who submitted a fact. As mentioned above, we're using a dynamic-ish (for us low-level coders at least!) URL variable to find the right image for the right person. and setting it as a background image for the button. Background images can be a bit of a pain to deal with and I may consider using an <img> tag in future. But this way lets us keep the button as one element, rather than a parent element with both an image and text inside.
In our CSS, we create a Flexbox row for our questions which lets us space them evenly on different size monitors. To make sure our buttons are always square (because our avatars are), we're currently using pixel values, which feels a bit...unresponsive. We use hover and selected states to send a little more information to the player about what they're doing.
Once the server receives the answer from the client (they've picked one of the buttons in an array from 0-4), we can check that number against the number stored in our JSON. Then we can send back an answer: true or answer: false results message.
Annoyingly, because we're using numbers to check, we need to write a little more code to pass the name of the person whodunnit back to the client (technically, we could also pull it up from the options using the number, but why do that?).
// When client submits an answer, check if it's correct
socket.on('answer', (answer) => {
console.log("answer submitted: " + answer);
console.log("correct answer: " + quiz.questions[questionNo].answer);
// get the NAME of the correct answer
let correctAnswer = quiz.questions[questionNo].options[quiz.questions[questionNo].answer];
if (answer == quiz.questions[questionNo].answer) {
console.log("correct!");
currentScore++;
console.log("current score: " + currentScore);
socket.emit("results", { answer: true, name: correctAnswer });
} else {
console.log("incorrect!");
socket.emit('results', { answer: false, name: correctAnswer });
}
Removing Asked Questions from the Array
We don't want to ask this question again, so we use quiz.questions.splice(questionNo, 1); to remove it (only for the duration of this active session).
Back on the client side, we display feedback on whether the player got the question right. For the longest time, this wasn't working properly (it would always say the answer was correct). I finally realised I needed a double equal sign for data.answer == true. A single equal sign would just check if an answer exists.
// Display whether the player got the answer right or wrong
socket.on('results', (data) => {
console.log(data);
let quizDiv = document.getElementById('quiz');
let results = document.createElement('p');
results.id = "results";
quizDiv.appendChild(results);
if (data.answer == true) {
results.innerHTML = "Correct! The answer was " + data.name + ".";
} else {
results.innerHTML = "Wrong! The answer was " + data.name + ".";
}
// add a button to request the next question
let nextButton = document.createElement('button');
nextButton.innerHTML = "Next Question";
nextButton.id = "next-button";
nextButton.className = "button";
quizDiv.appendChild(nextButton);
// When the player clicks the next question button, request a new question
nextButton.addEventListener("click", () => {
removeQuestion();
socket.emit('getquestion');
console.log("requesting next question");
})
})
Rules, Edge Cases & General Tidying Up
Removing the Intro
Once you've submitted your name, we need to clear that field for the duration of the game. We use a boolean, introVisible to stop the function more than once.
function removeIntro() {
if (introVisible) {
let intro = document.getElementById('intro');
intro.remove();
introVisible = false;
}
}
Ending the Game
Using a maxQuestions variable on the server side, we can continue the cycle of requesting a new question until we hit that ceiling, then emit a gameOver message and trigger an end scenario. This currently doesn't quite work as expected.
// check if we've reached the maximum number of questions
if (currentQuestion < maxQuestions) {
currentQuestion++;
console.log(currentQuestion + " out of " + maxQuestions + " questions asked");
} else {
console.log(currentQuestion + " out of " + maxQuestions + " questions asked");
console.log("game over!");
io.emit('gameOver', { score: currentScore, max: maxQuestions });
}
io.emit('user scores', users);
})
Next Steps
We had a week to build this project and couldn't get it running on Glitch in that time. That's our next mission. We also weren't able to get the score updating (I don't quite get the users object and how best to update it). There's a little hiccup in the multiplayer nature of the game that means one player asking for a new question gives everybody a new question. That would work well for a synchronous game but not an asynchronous one. What we'd like to do is effectively run the quiz part like a single-player game but keep the part that shows who is currently playing and what their score is.
For Project 01, our class was split into thematic groups and given both a group topic and a randomly selected individual topic to research. Our goal was to create a guide to our topic. My group topic was transgression; within that, my topic was puppets.
Puppetmaster is a semi-useful guide to puppetry. More accurately (spoilers ahead), it’s a guide to installing a puppet politician and ruling from the shadows, wrapped in a semi-useful guide to puppetry.
Two Versions
Originally, I pursued the format of a single-page, fold-out zine. The reverse would be a cut-out D.I.Y marionette (I never got round to designing one but prototyped using a found version from Hej hopp). Belatedly, I decided to try a different format: I laser-cut a book cover that could behave like a marionette and bound the entire book with fishing wire.
Intentions and Original Ideas
From our first cohort conversation onwards, I knew I wanted to make something that reflected our group theme not just in subject matter but in spirit. I wanted something a little surreptitious and slightly sinister, but in a funny way.
Two possible directions emerged. One is effectively what you see here. The other was really interesting and something that's been on my mind for a while, but not quite right for this project. My dad's side of the family were traditionally responsible for putting on the village masquerade but after the village converted to Christianity, there has been little mention of anything to do with that. I thought it might be interesting to find existing ethnographic research of similar villages with similar masquerades and pair that with my own digging into my family history. I'd still like to do that someday.
References and Research Trail
Systems
I've had a really hard time getting my head around any of the systems thinking readings we've done, going back to the summer. Maybe my brain doesn't work in terms of seeing interconnected nodes creating emergent behaviour. So rather than trying to see the systems in puppetry, I looked at puppets as systems themselves. After all, they are interconnected nodes creating emergent behaviour.
That lens didn't work so well when it came to thinking about stakeholders and the stakes they hold.
A systems map of puppets.
Puppets and Politics
This line of inquiry came largely out of my conversation with Margaret, the Tisch librarian. Having suggested I looked into Bread and Puppet Theatre, I was reminded of the UK's affinity for political satire via puppets (specifically Spitting Image). The idea of using puppetry to, in the words of Peter Schumann, “make the gods cry” was interesting, but also not quite the direction I wanted to take.
Then I looked into sock puppets—online accounts used to spread disinformation or otherwise manipulate a population. I thought about becoming a sock puppet myself and allowing anybody to spread any message via my online “reach”, such as it is. This idea made me laugh no end and also felt spiritually linked to Beth Fileti's thesis, which we'd seen in class. But I decided against it because a) I have no reach and b) it's not a guide (although maybe you could make the argument for it).
Tarnschriften, Capoeira and Trojan Horses
Last winter, I went to see the NYPL Treasures exhibit. It was the first time I'd learned about the Green Book. While thinking about this project, this idea of clandestine literature came back. Growing up, my dad had told me about how bibles were smuggled into China as a way to gratitude-scare me into reading mine.
I felt like there could be something here—at the NYPL, my friend had mentioned anti-Nazi literature needed to be disguised as mundane things to get past the prying eyes of the military. But I didn't remember what they were called. Three hours of Googling later...Tarnscriften.
This felt like the energy I was going for. Smuggling something dangerous inside something palatable to the powers that be. A literary trojan horse. It also felt linked to the practice of Capoeira. I'm not sure how much truth there is to the claim that original practitioners disguised their combat training as a dance to escape the attention of slave owners, but it's a beautiful thing either way.
Subliminal Imaging
People have found a way to hide subliminal messages in generated images. I thought this might be a fun way to illustrate the guide. My initial attempts failed but I did eventually figure out how to use Illusion Diffusion to do this—to an extent. Unfortunately, the images were both extremely creepy and not quite in the vintage illustration style I would have preferred. Squint to see the hidden text on the right.
Production
I prototyped a few different single-page, fold-out zine types—I most typically saw instructions for the version with a slit but I actually preferred the accordion fold. I tried both 11x17" and 18x24" starting sizes.11x17" felt like a good size for a pocket book but the various risograph printers I reached out to were either busy or couldn't do short runs. That meant I'd likely be printing at home.
Vellum Paper
Once I moved away from a single-sheet format, size restrictions became a little easier. I liked the idea of printing something deceitful on something transparent (or at least translucent), so I tried vellum paper.
Bookbinding
Our instructor shared some references with me that prompted me to think more about form. How could this object become part of its own conversation?
My classmate Elizabeth (with a friend) also led a bookbinding workshop. These two things in combination ended up providing the spark for the “puppet-binding” look and feel of the second version.
Cover
The front cover is laser cut in three pieces. The cutout from the outer layer becomes the controller.
Puppetworks: Nick Coppola
As part of the project, we were tasked with interviewing a subject matter expert on our topic. After my classmate Mishka sent me a photo of the puppet theatre in Brooklyn she happened to drive past, I had the great fortune of speaking with Nick Coppola, founder of Puppet Works. Nick's been a puppeteer since [year], founded puppetworks in 1980 and knew from the age of [age] he wanted to do that. By the end of our conversation, I had an immense sense of guilt for how cynical my project seemed. Not necessarily cynical about puppets, but still cynical.
Reflections
I really struggled with this project. In my working life, research often involves a lot of early discovery work run through a client or stakeholder. It mostly lives at the brief end of a project—the idea is once we have synthesised it, we are free to move away from it. Here, I hoped the answers might lie in the research itself, but that never quite worked out.
From the beginning, I think I stumbled on the idea of a “guide”, particularly one that would enable some sort of action. Wanting to play the double-entendre, it felt like I would end up neither enabling any literal puppetry or allowing someone to become the power behind the throne.
Puppetry is a pretty cool discipline, and getting into the mechanics, history and philosophy of the artform has been quite fun. I'm quite interested in learning some basic marionette techniques in terms of both building and manipulating, as well as looking more into shadow puppetry. An early idea for this project was to create an app that would turn hand gestures into shadow puppets using something like Posenet or Leap Motion. I could see that happening later in the year.
What feedback did you receive? Any reflections on critique itself?
Most people I spoke to were quite interested in the idea—although there seemed to be even more interest in the other direction I considered (family history/Yoruba masquerade). Nobody had a chance to interact with the physical object but classmates did give some helpful feedback on the digital spreads.
What might you do differently in terms of process or content?
Having put so much effort into thinking about systems and criticality without finding anything compelling to lean on there, I would probably resort to a form-oriented workflow from the start were I to redo this project.
Revisit the assignment prompts: how did your project relate to the original prompts, in terms of critical lens, audience, tone, etc…
I think I was able to identify a lens, tone and point of view fairly early. This is, in effect, a satirical product. I found it hard to create “meaningful” content within the framework of a guide for aspiring shadow dictators disguised as a puppetry manual—but that is what it is.
How did you balance research and experimentation? Which is easier for you? How can you focus more on the areas that you shy away from?
Because I have no real research “muscle”, I decided to lean fully into that. I think in the future I would start with some early experimentation, just to ground the project in something familiar.
This is a rough draft version of the guide—things are very much in flux!
What is your guide?
I am creating a tarnschriften-style guide to puppetry (spoiler: it’s actually about installing a puppet government and controlling the universe from behind the scenes).
The book will (hopefully) fold out into a paper puppet (still need to figure this out).
What are you titling your guide?
Puppet Master by Clivia Almeh
Who is your guide for?
Aspiring dictators (hopefully the rest of us will learn what not to do)
This is a GPT draft, rewriting the whole thing myself (yikes!)
Why did you choose this form for your guide?
I loved the idea of a clandestine text or literary trojan horse. I also wanted to actually help people make a literal puppet.
What are the affordances of your guide?
It’s small and light so it can be stored in pockets, stashed inside another book or otherwise hidden away
It looks like a regular book on puppetry so aspiring dictators don’t have to out themselves
It can be turned into a puppet
20th-century modernism gives the feeling of a "safe" book
Imagery
I'm trying to work out a way to generate these subliminal message illustrations—I like the idea of a single-colour, inky look which doesn't naturally come out of the Stable Diffusion instance I'm running. There might be another one I could run it through to create that effect.
What does your guide enable for your audience?
Some hopefully humorous, light reading related to a fairly dark topic
How do you know if this guide is successful?
More bland idiots in charge being manipulated by shadowy geniuses? More seriously, I'll go for laughs and/or smiles while reading.
What are two questions you have about your guide, as it currently stands?
How can I better blend the puppetry and puppet mastery themes?
What printed format will allow both the fold-out puppet and the book to co-exist?
Would you add/edit anything to the Critique Guidelines from the Summer Session? If not, why?
I love the Physical Computing playtesting guidelines we got from David Rios in the summer—I think the kind of acting-out-speaking-out method described there works really well.
For our first Connections Lab project, we're combining everything we've learned so far to create a site that interacts with data via an API. We're also incorporating P5 or any other library.
Ideas
I keep a list of project ideas so I first looked at that to see if anything would fit. There were a few interesting ones to which I added a bunch more. Dora shared a list of public APIs which led to even more ideas—16 in total (you can see them all on Notion).
Of these, I was really interested in doing something with an art database. I loved the Google Arts and Culture app that let you take a photo and find a piece of art that looked like it, but image recognition seemed a bridge too far.
What if you typed in your name and a related artwork showed up? This seems to keep the spirit of "find myself in the Met", but at a technical level I could maybe keep up with.
I quickly mocked this idea up in Figma—some notes on the design:
A P5 sketch would create a fullscreen background animation
Searching for a query would return a corresponding artwork from the Met's collection
In the case of multiple results, I would first want to filter out the "uninteresting" ones (at the time of designing, unavailable works or pieces with no images), then randomly select one of the remaining results
If no results, we show a message encouraging the viewer to make their own piece and get it into the Met
Landing page, results when on view, on loan or not in collection.
The Met's API
The Met has an open access API through which you can search for artworks (or "objects") by a query (the text we'll put in the search bar), or using a number of key/value pairs. The following are all booleans (true/false)
isHighlight - is it a “highlight” artwork (display these first)?
isOnView - is the artwork currently on view?
hasImages - does this object have an image?
title - does the search query match the title?
tags - does the search query match any tags?
My thinking is that an artwork that is a highlight, on view and has images is a "better" result to show than one that just has images, or is just a highlight, so I considered using nested conditional statements to trap for all of those conditions. Eric taught me a better way of doing this, by writing a helper function that accepts all of those keys as arguments, which you can then run in a series of true/false permutations.
async function searchArtworks(isHighlight, isOnView, hasImages, title, tags) {
const url = new URL('https://collectionapi.metmuseum.org/public/collection/v1/search?');
if (isHighlight) {
url.searchParams.append('isHighlight', 'true')
}
if (hasImages) {
url.searchParams.append('hasImages', 'true')
}
if (isOnView) {
url.searchParams.append('isOnView', 'true')
}
if (title) {
url.searchParams.append('title', 'true')
}
if (tags) {
url.searchParams.append('tags', 'true')
}
url.searchParams.append('q', searchName)
In this function, we can append a given parameter to the search only if it's set to true. We can then call the function from another function that starts by running the narrowest search (all true), and runs wider and wider searches if no results.
async function getObjectID(searchName) {
// Fetch all objects with isHighlight = true, isOnView = true, hasImages = true, title = true and searchName
const data = await searchArtworks(true, true, true, true);
if (data) return data;
// HIghlights and nothing else
const data1b = await searchArtworks(true);
if (data1b) return data1b;
// Fetch all objects with isHighlight = false, isOnView = true, hasImages = true, title = true and searchName
const data2 = await searchArtworks(false, true, true, true);
if (data2) return data2;
// Fetch all objects with isHighlight = false, isOnView = false, hasImages = true, title = true and searchName
const data3 = await searchArtworks(false, false, true, true);
if (data3) return data3;
// Introduce tags if nothing matches title
const data4 = await searchArtworks(true, true, true, false, true);
if (data4) return data4;
// Tags but no highlight
const data5 = await searchArtworks(false, true, true, false, true);
if (data5) return data5;
// Tags but no highlight or on view
const data6 = await searchArtworks(false, false, true, false, true);
if (data6) return data6;
// Title but no image
const data7 = await searchArtworks(false, false, false, true);
if (data7) return data7;
// Only tags
const data8 = await searchArtworks(false, false, false, false, true);
if (data8) return data8;
// Nothing but still a match?
const data9 = await searchArtworks();
if (data9) return data9;
I'm finding this search a little more restrictive—I think in an ideal world, what I'd like to do is pass on all the results with images but give more "weight" to the ones that have more attributes so they are more likely to be picked at random than the others. As it stands, if one highlight piece is a match, you will only ever see that piece. I may roll it back to a broader search in future.
(Update Oct 7: I'm using a slightly broader search now—I start by looking for a match in the artwork title, then in the artist name, and finally in the artwork tags. What I'd like to do in the future is maybe use OR instead of AND type logic to group different permutations of attributes that I consider equivalent and create a pool of results that would be equally "fun" to serve up).
Choosing an Object
At each step in the search sequence, we will either have zero, one or more than one result. We use a conditional to either pick a random result from the array, use the single result or pass on the fact there are no matches.
return fetch(url).then(res => res.json())
.then(data => {
console.log(data);
// if array length > 1, return random object ID from array
if (data.objectIDs.length > 1) {
let randomIndex = Math.floor(Math.random() * data.objectIDs.length);
return data.objectIDs[randomIndex];
}
// if array length = 1, return object ID
if (data.objectIDs.length === 1) {
return data.objectIDs[0];
}
// if array length = 0, return null
if (data.objectIDs.length === 0) {
return null;
}
})
Displaying an Object
Once we get an objectID, I need to grab the following:
primaryImage - URL for BG image
title - What’s it called?
artistPrefix (optional) - If the artist is a Dr. Ms, etc.
artistDisplayName - Artist’s name
artistDisplayBio (optional) - Place and date of birth, usually
objectDate (optional) - When was the work (said to be) created?
medium (optional) - What’s it made of?
Then, I need to create DOM elements to house each of these pieces.
I couldn't get this to work for the longest time, so I asked Jonny for help. He showed me his workflow for getting chat GPT to debug his code, and GPT suggested I use appendChild to append the new DOM elements I created to the existing HTML.
// Create a new Div for the metadata
let artworkInfo = document.createElement('div');
artworkInfo.setAttribute('class', 'artwork-info');
// Grab primaryImage and make it BG Image
console.log(data.primaryImage);
let bgImage = document.getElementById('bg-image');
bgImage.style.backgroundImage = `url(${data.primaryImage})`;
// Create a div element for artist name and bio
let artistInfo = document.createElement('div');
artistInfo.setAttribute('class', 'artist-info');
let artistName = document.createElement('h2');
artistName.innerHTML = data.artistDisplayName;
artistInfo.appendChild(artistName); // Append artistName to artistInfo div
Wiping and Unwiping
To move between search and results without a page transition, we have a removeSearch function that takes the container element for search away.
function removeSearch() {
const element = document.getElementById('main');
element.remove();
}
Refreshing the page restores the original HTML, so we don't need an “unwipe” function, just a refresh button.
//create button to refresh page
let refresh = document.createElement('button');
refresh.setAttribute('class', 'button');
refresh.setAttribute('onclick', 'refresh()');
refresh.innerHTML = '← Start Again'; // Set the button text
artworkInfo.appendChild(refresh); // Append refresh to artworkInfo div
function refresh() {
location.reload();
}
No Results
This was another thing that gave me a headache. For some reason, the "else" part of my stack of conditionals wasn't firing, and the function to display the no results message was never invoked. Again I went to Jonny/GPT to help, but this time, neither of the two solutions I got back were close to working. However, one of the hallucinated solutions included a .catch(error) at the end, something I hadn't included in the function I tried to write. I still can't quite understand why, but simply adding that was enough to fix the issue.
No Cheeseburgers in the Met. Footer needs fixing but we have it!
My original idea (see mockups above) was to serve up a random artwork when no results were found—I might still return to that but for now the P5 sketch continues in the background.
Next Steps
I'll definitely spend some more time on this project even though we're technically done. Here's what I'd like to fix:
P5 background (ideally this would be dynamic each time the page loads) (currently using a sketch written via Copilot)
Pressing return in the field runs the search rather than refreshing the page (I found many explanations that using the form's submit event should fix this, but it didn't)
Get the "no results" part working (it looks like returning null doesn't fail the if(id) conditional) (done!)
Get more results coming back per search query (done but can still improve!)
CSS animations to have things come in and out more smoothly
Footer design
Update October 10
“Undefined” Error
It looks like for certain objects I get the following:
{ "message": "Not a valid object" }
I should be able to consider this a "no matches" situation and run displayNoMatchMessage, which should hopefully get rid of undefined results.
Results Without Images
From clicking through to the Met’s page, it looks like the issue is that some results don’t have image rights. Not sure if I can do anything about this as the search params don’t seem to give me this.
The general thrust of the article seems to be that as different groups of people approach the issue of research into GMO from different perspectives, it can be hard to come to any kind of consensus about whether the protests are "a good thing" and what the ideal way to critique or challenge the research might be.
I’m not sure how lens/worldview/stakeholder == system in this context. Maybe I’m just dense or I’m getting stuck looking for the wrong thing in the wrong place, but I’m reading “system” as “network of connected nodes with collective and emergent behaviour”. The system of scientists doing research kinda makes sense in that I can see that as a closed loop/silo where they interact with each other to produce and refine knowledge. But as soon as we get to system 2 (ethics and research management), that just sounds like a concern about system 1. 5 and 6 kind of make sense to me but the rest don’t.
I don’t really have any understanding of GMOs but of all the listed systems, 2 (a system of research ethics and risk management) and 8 (a system of sustainable agriculture, with long time horizons) “make sense” to me—they seem to both be concerned with asking whether and why this research should happen at all.
The ethics/risk stakeholders are concerned with beneficence. They want to minimise risk and make sure the overall benefits of the research outweigh whatever risk does exist
The sustainable agriculture stakeholders want to make sure we don’t pursue good, short-term solutions in favour of better or more permanent ones. They look at the bigger picture over a longer time horizon.
This week, we're reading Metaphors We Live By by George Lakoff and Mark Johnson. The central thesis seems to be that metaphors are more than linguistic or rhetorical tools. Rather, they suggest metaphors are essential to the way we think ("most of our ordinary conceptual system is metaphorical in nature") and the way we act ("many of the things we do in arguing are partly structured by the concept of war"). While I found this argument pretty compelling, most of the examples in the reading ended up being about language, which I suppose is natural for a book, seeing as books are made of words.
The authors explain these types of metaphorical concepts as "understanding and experiencing one kind of thing in terms of another", a light and straightforward definition I would have struggled to arrive at but one that seems perfectly obvious after reading. On the argument-as-war topic, they note that this is not a flowery, slangy or otherwise contrived way of talking about arguments, it's the default way. They ask us to imagine a culture that used the metaphor of dancing instead of fighting—how might arguments be different in that case?
The last thing that struck me in the opening section of the reading was the idea that metaphors should not be total—"if it were total, one concept would actually be the other, not merely be understood in terms of it". I often find myself saying things like "that's where the metaphor falls apart", "not a perfect metaphor" or "it's not one-to-one". It never occurred to me that being incomplete might be the point.
Structural and Orientational
The authors make a distinction between a structural metaphor (mapping the structure of one concept onto another, such as arguments as war) and an orientational metaphor (mapping concepts to our understanding of physical space, such as happy as up). While reading this I went on a little side quest into the temporal/spatial metaphors of the Aymara:
The Aymara also feel time as motion, but for them, speakers face the past and have their backs to the future. The Aymara word for past is transcribed as nayra , which literally means eye, sight or front. The word for future is q"ipa , which translates as behind or the back.
Fascinating. But also kind of makes sense—we know the past and in that sense, can "see it".
Which sort of brings us to the last point of the reading. There are usually multiple possible metaphors for a given concept. You could think of the future as "forward", but also (in literate societies) as moving in the direction of reading in a given culture, as our video timelines now do. The authors suggest that the "winning" metaphor tends to be the most coherent with other conceptual metaphors. Happy is up and healthy is up reinforce each other.
My Metaphors
I tend to use metaphors much more loosely and illustratively. For example, in one of our first classes, I described what I would now call a procedural or multisequential medium (one that allows for nonlinear, multiple-choice input-output relationships) "a tango rather than a waltz". As far as metaphorical systems go, I certainly use the in-out spatial pairing to convey interest, willingness, participation or acceptance. Weirdly, I absolutely do not use it to describe trendiness, maybe because I'm offended by the idea that something trendy should be coherent with those other things.
I'm struggling to think of any metaphors or metaphorical systems in the world of puppetry but when it comes to our group theme (transgression), it's worth noting that the church often uses the language of flesh to discuss sin and sinfulness.
Interview
This is the part where I admit I haven't spoken to Puppet Works yet. BUT I have reached out to them and hope to interview them soon.
Project 01 Continues
I feel like I finally have the makings of a gameplan for this thing. Some updates:
I'm going to focus on the "how to puppet (but secretly how to puppet government)" idea. The Yoruba masquerade thing feels personally relevant and probably a thread I should pull on over the next few months, but not right now. It may even end up being what I do for the second CritEx project.
Digging the idea of making a tarnschriften style book(let). On one hand, I feel a bit iffy about co-opting Nazi resistance literature to make a ridiculous machiavellian tragicomedy. On the other hand, it is the perfect format.
Going back and forth over whether to start off with a straight-ahead puppetry guide and switch around page 6-7, as the classic tarnschriften did, or write a book that fully reads as a puppetry guide and only subliminally talks about overthrowing and installing a government
I want this to be funny. It's not meant to be serious either in a pro-authoritarianism or an activism kind of way. More...chaotic neutral?
It would be very, very cool indeed if somehow this guide could also become a puppet. Fold out into a big sheet, cut-and-fold kinda vibes?
I'd like to exercise some graphic design muscles here—risograph microtypography paperstock ipsum—but I'm worried all the other stuff will take too much time and energy for me to nudge a semicolon backwards and forwards for six hours.
I'm realising I actually don't know anything about puppet governments. More research, yay!
There are some interesting gen-AI subliminal hidden message things going on right now (the Obey one in that article is particularly appealing to me). I haven't been able to get Stable Diffusion to do this yet but I'm thinking the illustrations in the guide oculd all contain messages. Jonny (AI expert) thinks it's 5-10 hours of experimentation to get a good result.