Where ideas percolate and thoughts brew

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

This sketch visualizes the contrast between gifts with receipts (returnable, provisional) and gifts without receipts (committed, permanent). Gifts with receipts are rendered as hollow outlines that drift around the canvas, rotating and never settling. They have visible receipt slips waving above them with "RETURN" text. These gifts eventually fade away, representing how returnable items never truly become yours. Gifts without receipts fall gently and settle into permanent positions. Once settled, they grow roots downward over time, representing integration and commitment. They become more solid and develop a warm glow as they integrate, showing how irrevocable choices create meaning through commitment. The visualization tracks and displays the count of returnable versus committed gifts, showing how most gifts in our culture come with receipts (60%), reflecting our default toward reversibility. This sketch accompanies the blog post "The Gift Receipt Problem" and explores how optionality and reversibility prevent depth and meaning in our choices.

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