The Gift Receipt Problem
About This Sketch
A generative visualization contrasting gifts with receipts (provisional, drifting, returnable) versus gifts without receipts (committed, rooted, permanent). Receipted gifts float as hollow outlines with visible "RETURN" slips, while committed gifts settle down and grow integration roots over time. The sketch explores how reversibility prevents depth and meaning—returnable gifts eventually fade away, while irrevocable gifts become solid and develop a warm glow of integration.
Watch as gifts appear: 60% come with receipts (reflecting our cultural default), drifting perpetually without commitment. The remaining 40% settle into place and slowly develop roots, representing how commitment creates meaning through irrevocability. The visualization tracks both types, showing the tension between our desire for optionality and our need for meaningful commitment.
Algorithm
Pseudocode
SETUP:
Initialize canvas (400x300)
Create empty gifts array
Set rectMode to CENTER for easier box drawing
GIFT CLASS (two types):
IF has receipt:
- Give random velocity (drifting motion)
- Make semi-transparent (100-180 alpha)
- Add rotation and rotation speed
- Integration level = 0 (never integrates)
- Eventually fade out (age 180-300)
ELSE (no receipt):
- No velocity initially
- Fully opaque (255 alpha)
- No rotation
- Integration level grows over time
- Permanent (never removed)
DRAW (every frame):
Clear background
Draw title
Every 60 frames:
Generate new gift (60% chance of receipt)
Start at top of canvas
FOR each gift:
UPDATE:
IF has receipt:
- Move according to velocity
- Rotate continuously
- Bounce off edges
- Wave receipt slip above
- Fade out after age 180
ELSE:
- Apply gravity until settled
- Once settled, grow integration level
- Develop roots growing downward
DISPLAY:
IF has receipt:
- Draw hollow box outline
- Draw decorative cross (bow)
- Draw receipt slip above with "RETURN" text
ELSE:
- Draw roots if integrated (bezier curves)
- Draw solid filled box
- Draw solid bow (cross)
- Draw glow if highly integrated
Count gifts with/without receipts
Display stats and legend at bottom
Source Code
let sketch = function(p) {
let gifts = [];
let maxGifts = 30;
class Gift {
constructor(x, y, hasReceipt) {
this.x = x;
this.y = y;
this.hasReceipt = hasReceipt;
this.age = 0;
this.size = p.random(15, 30);
if (hasReceipt) {
// Gifts with receipts: tentative, floating, uncommitted
this.vx = p.random(-0.3, 0.3);
this.vy = p.random(-0.3, 0.3);
this.opacity = p.random(100, 180);
this.rotation = p.random(p.TWO_PI);
this.rotationSpeed = p.random(-0.03, 0.03);
this.integrationLevel = 0; // Never integrates
this.finalAlpha = 150; // Stays transparent
this.weight = 0.5; // Light, ungrounded
} else {
// Gifts without receipts: grounded, settled, committed
this.vx = 0;
this.vy = 0;
this.opacity = 255;
this.rotation = 0;
this.rotationSpeed = 0;
this.integrationLevel = 0; // Will grow over time
this.finalAlpha = 255; // Becomes solid
this.weight = 2; // Heavy, grounded
this.settled = false;
this.settleSpeed = p.random(0.5, 1);
}
this.receiptOffset = 0;
this.receiptWave = p.random(p.TWO_PI);
}
update() {
this.age++;
if (this.hasReceipt) {
// Receipted gifts: drift around, never settle
this.x += this.vx;
this.y += this.vy;
this.rotation += this.rotationSpeed;
// Bounce off edges
if (this.x < this.size || this.x > 400 - this.size) this.vx *= -1;
if (this.y < this.size + 30 || this.y > 300 - this.size) this.vy *= -1;
// Receipt floats above, waving
this.receiptWave += 0.05;
this.receiptOffset = p.sin(this.receiptWave) * 3;
// Fade slightly over time (being returned)
if (this.age > 180) {
this.opacity = p.map(this.age, 180, 300, this.finalAlpha, 0);
}
return this.age < 300;
} else {
// Non-receipted gifts: settle down, integrate, become solid
if (!this.settled) {
// Gently settle to final position
this.vy += 0.1; // Gravity
this.y += this.vy;
// Settle on "ground"
let groundY = p.random(80, 250);
if (this.y >= groundY) {
this.y = groundY;
this.vy = 0;
this.settled = true;
}
}
// Integration: becomes more solid, rooted over time
if (this.settled && this.integrationLevel < 1) {
this.integrationLevel += 0.005;
}
return true; // Permanent
}
}
display(colors) {
p.push();
p.translate(this.x, this.y);
if (this.hasReceipt) {
// Receipted gift: outlined, transparent, floating
p.rotate(this.rotation);
p.noFill();
p.stroke(...colors.accent3, this.opacity);
p.strokeWeight(2);
p.rect(-this.size/2, -this.size/2, this.size, this.size);
// Bow (decorative but hollow)
p.line(-this.size/2, 0, this.size/2, 0);
p.line(0, -this.size/2, 0, this.size/2);
// Receipt sticking out top (visible)
p.push();
p.translate(0, -this.size/2 + this.receiptOffset);
p.fill(...colors.bg, 200);
p.stroke(...colors.accent3, this.opacity);
p.strokeWeight(1);
p.rect(-this.size/4, -10, this.size/2, 10);
// "Return" text on receipt
p.noStroke();
p.fill(...colors.accent2, this.opacity * 0.8);
p.textSize(5);
p.textAlign(p.CENTER, p.CENTER);
p.text('RETURN', 0, -5);
p.pop();
} else {
// Non-receipted gift: solid, grounded, integrated
// Integration roots (grow over time)
if (this.integrationLevel > 0) {
p.stroke(...colors.accent1, this.integrationLevel * 150);
p.strokeWeight(1 + this.integrationLevel);
let rootLength = this.size * 0.5 * this.integrationLevel;
// Roots growing down
for (let i = 0; i < 3; i++) {
let rootX = p.map(i, 0, 2, -this.size/3, this.size/3);
let rootCurve = p.sin(this.age * 0.02 + i) * 5 * this.integrationLevel;
p.noFill();
p.beginShape();
p.vertex(rootX, this.size/2);
p.bezierVertex(
rootX + rootCurve, this.size/2 + rootLength/2,
rootX - rootCurve, this.size/2 + rootLength/2,
rootX, this.size/2 + rootLength
);
p.endShape();
}
}
// Solid gift box
let solidAlpha = p.map(this.integrationLevel, 0, 1, 200, 255);
p.fill(...colors.accent1, solidAlpha);
p.noStroke();
p.rect(-this.size/2, -this.size/2, this.size, this.size);
// Bow (solid and real)
p.fill(...colors.accent2, solidAlpha);
p.rect(-this.size/2, -2, this.size, 4);
p.rect(-2, -this.size/2, 4, this.size);
// Glow of meaning (grows with integration)
if (this.integrationLevel > 0.3) {
let glowSize = this.size * (1 + this.integrationLevel * 0.3);
let glowAlpha = this.integrationLevel * 50;
p.fill(...colors.accent2, glowAlpha);
p.rect(-glowSize/2, -glowSize/2, glowSize, glowSize);
}
}
p.pop();
}
}
p.setup = function() {
p.createCanvas(400, 300);
p.rectMode(p.CENTER);
};
p.draw = function() {
const colors = getThemeColors();
p.background(...colors.bg);
// Title
p.fill(...colors.accent3);
p.noStroke();
p.textAlign(p.CENTER);
p.textSize(11);
p.text('The Gift Receipt Problem', 200, 20);
// Generate new gifts
// 60% with receipts (reflects cultural default)
if (p.frameCount % 60 === 0 && gifts.length < maxGifts) {
let hasReceipt = p.random() < 0.6;
let x = p.random(50, 350);
let y = 30;
gifts.push(new Gift(x, y, hasReceipt));
}
// Update and display
for (let i = gifts.length - 1; i >= 0; i--) {
if (!gifts[i].update()) {
gifts.splice(i, 1);
} else {
gifts[i].display(colors);
}
}
// Count committed vs returnable
let withReceipts = 0;
let withoutReceipts = 0;
let integratedCount = 0;
for (let gift of gifts) {
if (gift.hasReceipt) {
withReceipts++;
} else {
withoutReceipts++;
if (gift.integrationLevel > 0.5) integratedCount++;
}
}
// Stats
p.textAlign(p.LEFT);
p.textSize(8);
p.fill(...colors.accent3);
p.text('Returnable (floating):', 15, 270);
p.text('Committed (rooted):', 15, 285);
p.fill(...colors.accent3, 150);
p.noStroke();
let returnWidth = withReceipts * 8;
p.rect(120, 263, p.constrain(returnWidth, 0, 80), 8);
p.fill(...colors.accent1, 150);
let commitWidth = withoutReceipts * 8;
p.rect(120, 278, p.constrain(commitWidth, 0, 80), 8);
// Legend
p.textSize(7);
// Returnable
p.push();
p.translate(220, 268);
p.noFill();
p.stroke(...colors.accent3, 200);
p.strokeWeight(1.5);
p.rect(0, 0, 8, 8);
p.fill(...colors.bg);
p.stroke(...colors.accent3, 200);
p.strokeWeight(1);
p.rect(0, -7, 4, 3);
p.pop();
p.fill(...colors.accent3);
p.noStroke();
p.text('Returnable: Drifting, provisional, temporary', 230, 271);
// Committed
p.fill(...colors.accent1, 200);
p.rect(224, 283, 8, 8);
p.fill(...colors.accent2, 200);
p.rect(224, 283, 2, 8);
p.rect(220, 283, 8, 2);
p.fill(...colors.accent3);
p.text('Committed: Rooted, integrated, permanent', 230, 286);
// Bottom insight
p.fill(...colors.accent3, 130);
p.textAlign(p.CENTER);
p.textSize(7);
if (integratedCount > 0) {
p.text(`${integratedCount} gift${integratedCount === 1 ? '' : 's'} becoming deeply rooted`, 200, 297);
} else {
p.text('Watch how commitment creates roots', 200, 297);
}
};
};