project title: The Touch of a Dragon
artist: Melissa Miao & Xuer Peng
instructor: Viola
Project Concept and Development:
The project aims to create a realistic experience of bringing the Chinese folklore character, the Loong, to life, promoting the mysterious aspects of Chinese culture. Through extensive research into cultural symbolism, the use of a black-and-white painted laser-cut wooden box represents the concept of Earth and Heaven in Chinese folklore, with symbolic clouds and fog featuring prominently in the design. Insights from origami and dragon puppets informed how the Loong’s body would move, adding a dynamic element to its portrayal.
User Interaction and Design Decisions:
Users can engage with the project using Processing software on an attached computer or by interacting directly with an ultrasonic sensor inside the box. If users touch the dragon's body or point at its face for too long, the dragon reacts by becoming angry—breathing fire on the screen, wiggling wildly with servos, and blowing hot air from a heater. To enhance this interaction, red and white LEDs were inserted inside the clouds, making the dragon’s emotional shifts more visible and specific.
User Testing and Feedback:
During user testing, challenges with the Processing code arose, but the project was ultimately completed. Peer feedback led to the decision to have the dragon breathe fire on the screen. This suggestion, along with the addition of more delicate and sophisticated facial animations, greatly improved the user experience. These adaptations were effective, and users responded positively to the enhancements, particularly the more dynamic and expressive dragon reactions.
#define NUM_OF_VALUES_FROM_PROCESSING 1 /* CHANGE THIS ACCORDING TO YOUR PROJECT */
/* This array stores values from Processing */
int processing_values[NUM_OF_VALUES_FROM_PROCESSING];
int Servo_output = 6;
int Servo_input = 7;
long distance;
#include <Servo.h>
Servo myservo;
int relay_1 = 5;
int LED = 3;
int RED_LED = 10;
int brightness = 0; // how bright the LED is
int fadeAmount = 5; // how many points to fade the LED by
int mode = 0;
int trigger_time;
bool triggered = false;
void setup() {
Serial.begin(9600);
pinMode(relay_1, OUTPUT);
pinMode(Servo_output, OUTPUT);
pinMode(Servo_input, INPUT);
myservo.attach(9);
pinMode(LED,OUTPUT);
pinMode(RED_LED,OUTPUT);
}
void getSerialData() {
static int tempValue = 0; // the "static" makes the local variable retain its value between calls of this function
static int tempSign = 1;
static int valueIndex = 0;
while (Serial.available()) {
char c = Serial.read();
if (c >= '0' && c <= '9') {
// received a digit:
// multiply the current value by 10, and add the character (converted to a number) as the last digit
tempValue = tempValue * 10 + (c - '0');
} else if (c == '-') {
// received a minus sign:
// make a note to multiply the final value by -1
tempSign = -1;
} else if (c == ',' || c == '\n') {
// received a comma, or the newline character at the end of the line:
// update the processing_values array with the temporary value
if (valueIndex < NUM_OF_VALUES_FROM_PROCESSING) { // should always be the case, but double-check
processing_values[valueIndex] = tempValue * tempSign;
}
// get ready for the new data by resetting the temporary value and sign
tempValue = 0;
tempSign = 1;
if (c == ',') {
// move to dealing with the next entry in the processing_values array
valueIndex = valueIndex + 1;
} else {
// except when we reach the end of the line
// go back to the first entry in this case
valueIndex = 0;
}
}
}
}
void RestMode(){
Serial.print("resting distance: ");
// Serial.print("distance: ");
Serial.println(distance);
// triggered = false;
//breathing
for (int fadeValue = 0; fadeValue <= 255; fadeValue += 5) {
// sets the value (range from 0 to 255):
analogWrite(LED, fadeValue);
// wait for 30 milliseconds to see the dimming effect
delay(30);
}
// fade out from max to min in increments of 5 points:
for (int fadeValue = 255; fadeValue >= 0; fadeValue -= 5) {
// sets the value (range from 0 to 255):
analogWrite(LED, fadeValue);
// wait for 30 milliseconds to see the dimming effect
delay(30);
}
}
void TriggerMode(){
Serial.println("triggered");
Serial.print("distance: ");
Serial.println(distance);
//trigger servo
digitalWrite(Servo_output, LOW);
delayMicroseconds(2);
digitalWrite(Servo_output, HIGH);
delayMicroseconds(10);
digitalWrite(Servo_output, LOW);
// pulseIn waits for signal to go from HIGH to LOW,
// timeout according to max range of sensor
long duration = pulseIn(Servo_input, HIGH, 17400);
// sound travels roughly 29cm per microsecond so we divide by 29,
// then by 2 since we recorded sound both going forth and back
distance = duration / 29 / 2;
Serial.println(distance);
myservo.write(60);
delay(300);
myservo.write(120);
delay(300);
}
void AngerMode(){
Serial.println("angry");
triggered = false;
//trigger red light
digitalWrite(RED_LED, HIGH);
delay(2000);
digitalWrite(RED_LED, LOW);
//trigger servo
myservo.write(80);
delay(200);
myservo.write(140);
delay(200);
myservo.write(90);
delay(200);
myservo.write(140);
delay(200);
//trigger relay
digitalWrite(relay_1, HIGH);
Serial.println("All relays ON");
delay(15000);
// turning off
digitalWrite(relay_1, LOW);
Serial.println("All relays OFF");
}
void loop() {
getSerialData();
float val = processing_values[0];
// measuring distance
// additional 2 microsecond delay to ensure pulse clarity
digitalWrite(Servo_output, LOW);
delayMicroseconds(2);
digitalWrite(Servo_output, HIGH);
delayMicroseconds(10);
digitalWrite(Servo_output, LOW);
long duration = pulseIn(Servo_input, HIGH, 17400);
// sound travels roughly 29cm per microsecond so we divide by 29,
// then by 2 since we recorded sound both going forth and back
distance = duration / 29 / 2;
if(distance < 10 && distance > 0 && triggered == false){
TriggerMode();
triggered = true;
//take current time
trigger_time = millis();
Serial.print("trigger_time: ");
Serial.println(trigger_time);
}
if ((millis()-trigger_time) >= 5000 && triggered == true) {
if(distance < 10 && distance > 0){
AngerMode();
} else {
RestMode();
triggered = false;
}
}
if (triggered == false) {
if (distance == 0 || distance > 10) {
RestMode();
}
}
if (processing_values[0] == 1) {
TriggerMode();
}
if (processing_values[0] == 2) {
AngerMode();
}
}
Serial serialPort;
int hoverTime = 0; // 鼠标停留时间
boolean isHovering = false;
import processing.serial.*;
import processing.sound.*;// declare a SoundFile object
SoundFile sound;
int NUM_OF_VALUES_FROM_PROCESSING = 2; /* CHANGE THIS ACCORDING TO YOUR PROJECT */
/* This array stores values you might want to send to Arduino */
int processing_values[] = new int[NUM_OF_VALUES_FROM_PROCESSING];
void setup() {
size(600, 600);
serialPort = new Serial(this, "/dev/cu.usbmodem11101", 9600);
sound = new SoundFile(this, "roar.mp3");
}
void draw() {
background(255);
// 检测鼠标是否在龙脸范围
if (mouseX > 150 && mouseX < 450 && mouseY > 100 && mouseY < 350) {
isHovering = true;
processing_values[0] = 1;
processing_values[1] = 1;
} else {
isHovering = false;
hoverTime = 0; // 重置计时
}
if (isHovering) {
hoverTime++;
}
// 动态参数
float eyebrowRotation = isHovering && hoverTime < 180 ? PI / 6 : 0; // 新眉毛旋转角度
float eyeWidth = isHovering ? 45 : 33; // 眼睛宽度动态变化
float eyeHeight = isHovering ? 40 : 55; // 眼睛高度动态变化
float eyeTilt = isHovering ? -10 : 0; // 眼睛上扬
// 外轮廓固定
noFill();
stroke(243, 247, 130);
strokeWeight(1); // 外轮廓线宽
fill(244,221,95);
bezier(395, 140, 319, 90, 224, 125, 213, 141); // 左耳朵
bezier(177, 157, -36, 90, 76, 271, 155, 207); // 右耳朵
bezier(431, 160, 464, 89, 705, 188, 446, 206);
bezier(452, 205, 467, 221, 469, 307, 461, 328);
bezier(158, 204, 127, 253, 138, 317, 133, 328);
bezier(461, 327, 594, 422, 29, 411, 132, 328);
bezier(466,334,529,39,75,36,133,328); //哦哦啊啊诶诶啊啊哦哦得个得个得个得个
noFill();
bezier(361, 305, 481, 392, 468, 241, 524, 343);
bezier(79, 324, 60, 212, 257, 379, 258, 307);
fill(200,150,21);
bezier(387, 136, 394, -134, 453, 81, 430, 169);
bezier(171, 162, 169, -123, 244, 50, 214, 136);
// 新眉毛
pushMatrix();
stroke(0);
translate(252, 190); // 左眉毛基点
rotate(eyebrowRotation);
strokeWeight(3);
line(-20, -10, 20, 10); // 左眉毛线条
popMatrix();
pushMatrix();
translate(353, 190); // 右眉毛基点
rotate(-eyebrowRotation);
line(-20, 10, 20, -10); // 右眉毛线条
popMatrix();
// 胡须动态(轻微弯曲)
noFill();
strokeWeight(2); // 加粗胡须
bezier(361, 305, 481, 392, 468, 241, 524, 343);
bezier(79, 324, 60, 212, 257, 379, 258, 307);
// 调整眼睛形状
fill(55);
stroke(0);
ellipse(252, 263 + eyeTilt, eyeWidth, eyeHeight); // 左眼
ellipse(353, 264 + eyeTilt, eyeWidth, eyeHeight); // 右眼
// 鼻子固定
fill(132, 102, 32);
ellipse(303, 290, 31, 8);
// 判断是否需要喷火
if (hoverTime > 120) {
sound.play();
drawFire();// 喷火效果
processing_values[0] = 2;
sendSerialData();
} else {
// 深红色嘴巴动态
fill(202, 29, 29);
bezier(273, 311, 268, 353, 347, 418, 343, 310);
}
sendSerialData();
}
// 喷火函数
void drawFire() {
noStroke();
for (int i = 0; i < 5; i++) {
fill(random(200, 255), random(50, 150), 0, 150);
ellipse(303 + random(-15, 15), 330 + random(-15, 15), random(20, 40), random(30, 50));
}
}
void sendSerialData() {
String data = "";
for (int i=0; i<processing_values.length; i++) {
data += processing_values[i];
// if i is less than the index number of the last element in the values array
if (i < processing_values.length-1) {
data += ","; // add splitter character "," between each values element
}
// if it is the last element in the values array
else {
data += "\n"; // add the end of data character linefeed "\n"
}
}
// write to Arduino
serialPort.write(data);
print("To Arduino: " + data); // this prints to the console the values going to arduino
}
The fabrication of the Loong project involved key decisions in material selection, sensor integration, and actuator choices to create an interactive, culturally immersive experience. A laser-cut wooden box, painted in black and white, symbolized Earth and Heaven in Chinese folklore, while ultrasonic sensors and mouse input allowed users to trigger the dragon’s reactions. Servo motors and a heater animated the dragon's movements and simulated fire breathing, and LEDs inside the clouds enhanced the emotional shifts of the dragon. We used a relay to connect the heater with the entire circuit and developed a brand new method of coding, which was successfully integrated into the project. Despite challenges with Processing code and sensor calibration, these production choices successfully brought the Loong to life, blending traditional aesthetics with modern technology to achieve the project’s cultural and interactive goals. Another interesting point is that we combined origami skills with the body construction of our dragon. The metal squama were made out of lazer-cut wood and metal stickers from the soildering station, which turns out to be one of the most creative ideas on fabrication throughout our project.
The Loong project successfully achieved its goal of creating an immersive, interactive experience that blends Chinese folklore with modern technology. Users engaged with the project through both physical and virtual interactions, triggering the dragon's reactions and enhancing the experience with visual and sensory elements. The project aligned with the definition of interaction, where users’ actions directly influenced the dragon's behavior. Given more time, I would refine the sensor responsiveness and explore additional sensory effects. Through setbacks like coding and calibration issues, I learned the importance of persistence, iteration, and early testing. Ultimately, this project reinforced how technology can enhance cultural storytelling and deepen user engagement.
Thanks to all the instructors, fellows, LAs and classmates 🥹It was a wonderful and magical semester spent in ima studio, which i would never imagine to do so in the past since i'm not from this major. yet the passion flowing between North building 3rd and 4th floor's studio has really made a difference on my understanding towards art and interaction. This is truly a life-long worth memory 🧡Hope to see you all in the future around the campus!