joxin-clock
Burning Incense (一炷香)
Daily rituals–burning incense, drinking a cup of tea, finishing a meal–can be ways of measuring time.
![](/2018/60212s/wp-content/uploads/2018/02/IMG_8904.jpg)
When I was little, my mom used to burn incense at home. It burned very slow, and the smell would linger all afternoon. In Chinese, there is an old phrase called “yi zhu xiang” (一炷香). It literally means “the burning of an incense stick,” but it is actually a unit of time. People don’t say it anymore, but in old books, you could see sentences like “after the burning of an incense, she still hasn’t arrived,” or “he figured it out in the burning of an incense.” The burning of an incense signifies an innumerable duration. It originated from a common activity, and has become a shared notion of time in culture.
In my life today, time is most often measured numerically, which I always find stressful. I think the burning of an incense stick embodies the personal, poetic, and leisurely possibility of time-keeping. Therefore, I made this virtual incense time-keeper, as a reminiscence of the days when I didn’t have to be so obsessed with numerical time.
Here is how it works: You can burn a short incense and a long incense. The short incense will keep a relatively short period of time, and the long incense will keep a relatively long period of time. The exactly duration is pseudo-random. You wouldn’t know how long it is. All you can see is the incense slowly shortening.
![](/2018/60212s/wp-content/uploads/2018/02/incense_long-1.gif)
![](/2018/60212s/wp-content/uploads/2018/02/incense_short-1.gif)
I envision using it in my free time. Maybe I want to read a book, or take a nap. Then I would burn a long incense. Maybe I want to do a few jumping jacks, then I would burn the short incense. Using this time-keeper, I wouldn’t get bothered by numbers. I will simply get a vague idea of time and focus on what I am doing instead.
The challenging parts of the process were story-boarding and developing a visual language. Since I was essentially making an application for people to play, I thought it was important to design an experience that doesn’t confuse anybody. I didn’t want to include text instructions, which made it even harder. The visual language was tricky because I wanted to go for a hyper-realistic feel. I tried to combine the geometric features of p5.js and photos I took, while maintaining a consistent aesthetic.
Process pics:
My personal critique of this project: Why does this need to be on the screen? We have incense in real life, so why don’t we just burn an actual incense stick? I guess I could argue this is an attempt to find a peace of mind on the screen. But I don’t totally buy it. I have to personally use it more to judge whether it actually accomplishes that.
You can play with it here. OpenProcessing runs super slow for my sketch, because the particle system algorithm is very costly.
Code:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 | var holder; var incense_down; var incense_down_short; var incense_down_long; var arm_empty; var arm_with_match; var arm_with_incense_long; var arm_with_incense_short; var shortSpan = 20000; var longSpan = 40000; var p_texture; var p_grey_texture; var fire; var smoke; var longTip = {x:289, y:156}; var shortTip = {x:276, y:218}; var centerTip = {x:254, y:338}; var startTime; var timeSpan; function setup() { createCanvas(500, 600); frameRate(20); smooth(); textAlign(CENTER); holder = loadImage('assets/holder.png'); incense_down = loadImage('assets/incense_down.png'); incense_down_short = loadImage('assets/incense_down_short.png'); incense_down_long = loadImage('assets/incense_down_long.png'); arm_empty = loadImage('assets/arm_empty.png'); arm_with_match = loadImage('assets/arm_with_match.png'); arm_with_incense_long = loadImage('assets/arm_with_incense_long.png'); arm_with_incense_short = loadImage('assets/arm_with_incense_short.png'); p_texture = loadImage('assets/particle.png'); p_grey_texture = loadImage('assets/particle_grey.png'); state = "none"; } function draw() { background(255); drawShadow(); drawHolder(); switch(state) { case "none": // start up drawIncenseDown(); drawEmptyHand(); if (mouseIsPressed){ if (onLongIncense(mouseX, mouseY)) { state = "holdingLong"; } else if (onShortIncense(mouseX, mouseY)) { state = "holdingShort"; } } break; case "holdingLong": drawShortIncenseDown(); drawHandWithLongIncense(); if (mouseIsPressed) { if (onHolder(mouseX, mouseY)) { state = "lightingLong"; fire = new ParticleSystem(0,createVector(0, 0), p_texture); } else { state = "none"; } } break; case "holdingShort": drawLongIncenseDown(); drawHandWithShortIncense(); if (mouseIsPressed) { if (onHolder(mouseX, mouseY)) { state = "lightingShort"; fire = new ParticleSystem(0,createVector(0, 0), p_texture); } else { state = "none"; } } break; case "lightingLong": drawShortIncenseDown(); drawLongIncense(0, false); drawHolder(); drawHandWithMatch(); updateFire(); if (abs(mouseX-longTip.x) < 3 && abs(mouseY-longTip.y) < 3) { state = "timingLong"; startTime = millis(); timeSpan = random(shortSpan, longSpan); smoke = new ParticleSystem(0,createVector(0, 0), p_grey_texture); } break; case "lightingShort": drawLongIncenseDown(); drawShortIncense(0, false); drawHolder(); drawHandWithMatch(); updateFire(); if (abs(mouseX-shortTip.x) < 3 && abs(mouseY-shortTip.y) < 3) { state = "timingShort"; startTime = millis(); timeSpan = random(10000, shortSpan); smoke = new ParticleSystem(0,createVector(0, 0), p_grey_texture); } break; case "timingLong": drawShortIncenseDown(); drawLongIncense((map(millis() - startTime, 0, timeSpan+1, 0, 1)), true); if (millis() - startTime > timeSpan) { state = "none"; } drawHolder(); break; case "timingShort": drawLongIncenseDown(); drawShortIncense((map(millis() - startTime, 0, timeSpan+1, 0, 1)), true); if (millis() - startTime > timeSpan) { state = "none"; } drawHolder(); break; default: break; } } function updateFire() { var dx = map(mouseX,0,width,-0.2,0.2); var wind = createVector(dx,0); fire.applyForce(wind); fire.run(mouseX-5, mouseY); for (var i = 0; i < 2; i++) { fire.addParticle(); } } function onLongIncense(x, y) { if (abs(x * -0.1712 + 507.71 - y) < 10 && x > 227 && x < 460) { return true; } else { return false; } } function onShortIncense(x, y) { if (abs(x * -0.287 + 579 - y) < 10 && x > 317 && x < 500) { return true; } else { return false; } } function onHolder(x, y) { if (abs(x - 250) < 30 && abs(y - 330) < 50) { return true; } else { return false; } } function drawShadow() { noStroke(); fill(255, 232, 245, 200); ellipse(width/2, height/2 + 115, 180, 40); fill(255, 244, 244, 100); ellipse(width/2-30, height/2 + 135, 300, 100); } function drawHolder() { image(holder, width/2-holder.width/6, height/2-40, holder.width/3, holder.height/3); } function drawIncenseDown() { image(incense_down, width/2-30, height/2+110, incense_down.width/1.5, incense_down.height/1.2); } function drawLongIncense(n, isSmoking) { var x = map(n, 1, 0, centerTip.x, longTip.x); var y = map(n, 1, 0, centerTip.y, longTip.y); strokeWeight(3.5); stroke(0); line(x, y, centerTip.x, centerTip.y); stroke(255, 71, 71); point(x, y); if (isSmoking) { var dx = map(mouseX,0,width,-0.2,0.2); var wind = createVector(dx,0); smoke.applyForce(wind); smoke.run(x, y); for (var i = 0; i < 2; i++) { smoke.addParticle(); } } } function drawShortIncense(n, isSmoking) { var x = map(n, 1, 0, centerTip.x, shortTip.x); var y = map(n, 1, 0, centerTip.y, shortTip.y); strokeWeight(3.5); stroke(0); line(x, y, centerTip.x, centerTip.y); stroke(255, 71, 71); point(x, y); if (isSmoking) { var dx = map(mouseX,0,width,-0.2,0.2); var wind = createVector(dx,0); smoke.applyForce(wind); smoke.run(x, y); for (var i = 0; i < 2; i++) { smoke.addParticle(); } } } function drawEmptyHand() { var x = constrain(mouseX, width/2, width); image(arm_empty, x-50, mouseY-50, arm_empty.width/4, arm_empty.height/4); } function drawHandWithLongIncense() { var x = constrain(mouseX, width/2, width); image(arm_with_incense_long, x-50, mouseY-210, arm_with_incense_long.width/4, arm_with_incense_long.height/4); } function drawHandWithShortIncense() { var x = constrain(mouseX, width/2, width); image(arm_with_incense_short, x-50, mouseY-210, arm_with_incense_short.width/4, arm_with_incense_short.height/4); } function drawHandWithMatch() { var x = constrain(mouseX, width/2, width); image(arm_with_match, x-10, mouseY-65, arm_empty.width/4, arm_empty.height/4); } function drawLongIncenseDown() { image(incense_down_long, width/2-30, height/2+110, incense_down_long.width/1.5, incense_down_long.height/1.2); } function drawShortIncenseDown() { image(incense_down_short, width/2-30, height/2+110, incense_down_short.width/1.5, incense_down_short.height/1.2); } // *** the code below is adapted from https://p5js.org/examples/simulate-smokeparticles.html *** // //========= PARTICLE SYSTEM =========== /** * A basic particle system class * @param num the number of particles * @param v the origin of the particle system * @param img_ a texture for each particle in the system * @constructor */ var ParticleSystem = function(num,v,img_) { this.particles = []; this.origin = v.copy(); // we make sure to copy the vector value in case we accidentally mutate the original by accident this.img = img_ for(var i = 0; i < num; ++i){ this.particles.push(new Particle(this.origin,this.img)); } }; /** * This function runs the entire particle system. */ ParticleSystem.prototype.run = function(x, y) { // cache length of the array we're going to loop into a variable // You may see .length in a for loop, from time to time but // we cache it here because otherwise the length is re-calculated for each iteration of a loop var len = this.particles.length; //loop through and run particles for (var i = len - 1; i >= 0; i--) { var particle = this.particles[i]; particle.run(x, y); // if the particle is dead, we remove it. // javascript arrays don't have a "remove" function but "splice" works just as well. // we feed it an index to start at, then how many numbers from that point to remove. if (particle.isDead()) { this.particles.splice(i,1); } } } /** * Method to add a force vector to all particles currently in the system * @param dir a p5.Vector describing the direction of the force. */ ParticleSystem.prototype.applyForce = function(dir) { var len = this.particles.length; for(var i = 0; i < len; ++i){ this.particles[i].applyForce(dir); } } /** * Adds a new particle to the system at the origin of the system and with * the originally set texture. */ ParticleSystem.prototype.addParticle = function() { this.particles.push(new Particle(this.origin,this.img)); } //========= PARTICLE =========== /** * A simple Particle class, renders the particle as an image */ var Particle = function (pos, img_) { this.loc = pos.copy(); var vx = randomGaussian() * 0.3; var vy = randomGaussian() * 0.3 - 1.0; this.vel = createVector(vx,vy); this.acc = createVector(); this.lifespan = 100.0; this.texture = img_; } /** * Simulataneously updates and displays a particle. */ Particle.prototype.run = function(x, y) { this.update(); this.render(x, y); } /** * A function to display a particle */ Particle.prototype.render = function(x, y) { //print(x + ":" + y); imageMode(CENTER); tint(255,this.lifespan); image(this.texture, x+this.loc.x, y+this.loc.y, this.texture.width, this.texture.height); tint(255,255); imageMode(CORNER); } /** * A method to apply a force vector to a particle. */ Particle.prototype.applyForce = function(f) { this.acc.add(f); } /** * This method checks to see if the particle has reached the end of it's lifespan, * if it has, return true, otherwise return false. */ Particle.prototype.isDead = function () { if (this.lifespan <= 0.0) { return true; } else { return false; } } /** * This method updates the position of the particle. */ Particle.prototype.update = function() { this.vel.add(this.acc); this.loc.add(this.vel); this.lifespan -= 2.5; this.acc.mult(0); } |