joxin-clock

Burning Incense (一炷香)

Daily rituals–burning incense, drinking a cup of tea, finishing a meal–can be ways of measuring time.

(smoking a cigarette; burning an incense stick; drinking a cup of tea; enjoying a meal)

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.

Burning a long incense stick -> measuring a randomly determined, relatively long period of time
Burning a short incense stick -> measuring a randomly determined, relatively short period of time

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);
}