import processing.sound.*; import processing.serial.*; PImage[] lotusFrames; PImage lotus; PImage flower; int totalFrames = 38; int currentIndex = 0; int gestureBoost = 0; boolean sprinkleElements = false; boolean showFallAnimation = false; ArrayList fallingLotuses = new ArrayList<>(); float animProgress = 0; // Sound SoundFile ambientSound; float soundVolume = 0; float maxVolume = 0.6; boolean startedSound = false; // Rotation + pulsing for final frame float rotationAngle = 0; float pulseScale = 1.0; // Wave background parameters float waveOffset = 0; // Floating flowers ArrayList floatingFlowers = new ArrayList<>(); int flowerCount = 12; boolean triggerLotusAnimation = false; boolean animationComplete = false; // ---- Conch Animation variables ---- boolean triggerConchAnimation = false; PImage[] conchFrames; int totalConchFrames = 32; int currentConchIndex = 0; int conchFrameCounter = 0; int conchFrameDuration = 4; float conchRotationAngle = 0; PImage smoke1, smoke2; float ambientSoundVolume = 0; float ambientMaxVolume = 0.6; boolean ambientSoundStarted = false; ArrayList clouds = new ArrayList<>(); int cloudCount = 20; // ---- Po Kolam variables ---- PImage[] kolamFrames; int totalKolamFrames = 27; int currentKolamFrame = 0; int kolamFrameDelay = 10; int kolamFrameCounter = 0; float kolamRotationAngle = 0; Smoke[] kolamSmokes; PImage kolamChalkBackground; boolean showConchAnimation = false; boolean showPoKolamAnimation = false; // --- Serial and RFID variables --- Serial myPort; String inputString = ""; String detectedRFID = ""; // --- Hall Effect Sensor variables (6 sensors A0-A5) --- int NUM_HALL_SENSORS = 6; int[] hallValues = new int[6]; int hallThreshold = 100; // Small threshold - trigger when reading drops below this int baselineThreshold = 400; // Normal baseline value when no magnet present // Sensor mapping: Physical order A0→A1→A3→A4→A5→A2 mapped to logical positions 0→1→2→3→4→5 // physicalToLogical[arduino_pin_index] = logical_position int[] physicalToLogical = {0, 1, 5, 2, 3, 4}; // A0=0, A1=1, A2=5, A3=2, A4=3, A5=4 int[] logicalToPhysical = {0, 1, 3, 4, 5, 2}; // position 0=A0, 1=A1, 2=A3, 3=A4, 4=A5, 5=A2 // Direction detection int lastTriggeredSensor = -1; int secondLastTriggeredSensor = -1; long lastTriggerTime = 0; int triggerDebounceMs = 150; // Minimum time between triggers void setup() { size(800, 800); // Hide the window frame/menu bar surface.setResizable(false); surface.setLocation(0, 0); // Load smoke images FIRST smoke1 = loadImage("smoke1.png"); smoke2 = loadImage("smoke2.png"); // Load Po Kolam frames kolamFrames = new PImage[totalKolamFrames]; for (int i = 0; i < totalKolamFrames; i++) { kolamFrames[i] = loadImage("Po Kolam " + (i+1) + ".png"); } kolamSmokes = new Smoke[6]; for (int i = 0; i < kolamSmokes.length; i++) { kolamSmokes[i] = new Smoke(random(width), random(height), random(1) < 0.5 ? smoke1 : smoke2); } kolamChalkBackground = loadImage("chalking.jpg"); conchFrames = new PImage[totalConchFrames]; for (int i = 0; i < totalConchFrames; i++) { conchFrames[i] = loadImage("Conch " + (i + 1) + ".png"); } for (int i = 0; i < cloudCount; i++) { clouds.add(new SmokeCloud()); } // Setup serial println("Available serial ports:"); for (int i = 0; i < Serial.list().length; i++) { println(i + ": " + Serial.list()[i]); } String portName = "/dev/tty.usbmodem1101"; myPort = new Serial(this, portName, 9600); myPort.bufferUntil('\n'); println("Connected to: " + portName); noCursor(); // Load images lotusFrames = new PImage[totalFrames]; for (int i = 0; i < totalFrames; i++) { String filename = "Lotus " + (i + 1) + ".png"; lotusFrames[i] = loadImage(filename); } lotus = loadImage("Lotus.png"); flower = loadImage("flower.png"); imageMode(CENTER); frameRate(60); for (int i = 0; i < flowerCount; i++) { floatingFlowers.add(new FloatingFlower(random(width), random(height), random(TWO_PI), random(0.2f, 0.6f))); } ambientSound = new SoundFile(this, "meditation.mp3"); ambientSound.amp(0); ambientSound.loop(); } void draw() { drawWaveBackground(); // Read serial input continuously readSerialInput(); if (triggerLotusAnimation) { if (!startedSound && currentIndex == 0 && animProgress > 0) { startedSound = true; } if (startedSound && soundVolume < maxVolume) { soundVolume += 0.0005; ambientSound.amp(constrain(soundVolume, 0, maxVolume)); } float opacityProgress = 0; if (currentIndex >= 5 && currentIndex <= 15) { opacityProgress = map(currentIndex, 5, 15, 0, 255); } else if (currentIndex > 15) { opacityProgress = 255; } for (FloatingFlower ff : floatingFlowers) { ff.update(); ff.display(opacityProgress); } if (!showFallAnimation) { for (int i = 0; i < currentIndex; i++) { PImage img = lotusFrames[i]; image(img, width / 2, height / 2, img.width * 1.5, img.height * 1.5); } if (currentIndex < totalFrames) { PImage img = lotusFrames[currentIndex]; float fadeDuration = getFadeDurationForFrame(currentIndex + 1); float alpha = map(animProgress, 0, fadeDuration, 0, 255); alpha = constrain(alpha, 0, 255); if (alpha > 10) { pushMatrix(); translate(width / 2, height / 2); tint(255, alpha); image(img, 0, 0, img.width * 1.5, img.height * 1.5); popMatrix(); noTint(); } animProgress++; if (animProgress > fadeDuration) { animProgress = 0; currentIndex++; } } else { showFallAnimation = true; spawnFallingLotuses(12); } } else { PImage lastLotus = lotusFrames[totalFrames - 1]; float pulse = sin(frameCount * 0.02) * 0.05; pulseScale = 1.5 + pulse; pushMatrix(); translate(width / 2, height / 2); rotate(rotationAngle); image(lastLotus, 0, 0, lastLotus.width * pulseScale, lastLotus.height * pulseScale); popMatrix(); rotationAngle += radians(0.35); for (FallingLotus fl : fallingLotuses) { fl.update(); fl.display(); } if (frameCount % 600 == 0) { animationComplete = true; } } } else if (triggerConchAnimation) { runConchAnimation(); } else if (showPoKolamAnimation) { runPoKolamAnimation(); } else { // Idle state - just floating flowers for (FloatingFlower ff : floatingFlowers) { ff.update(); ff.display(100); } } waveOffset += 0.5; // Apply circular mask at the end - makes outside transparent applyCircularMask(); } void applyCircularMask() { // Draw a clean black border outside the circle noStroke(); fill(0); // Black outside the circle int radius = 360; // Back to the smaller size int centerX = width / 2; int centerY = height / 2; // Draw black squares in corners int cornerSize = (int)(width - radius * sqrt(2)); // Use pixel manipulation for smooth circular edge loadPixels(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int index = x + y * width; float distance = dist(x, y, centerX, centerY); // Black outside circle if (distance > radius) { pixels[index] = color(0); } // Smooth anti-aliased edge (1-2 pixel transition) else if (distance > radius - 2) { float alpha = map(distance, radius - 2, radius, 255, 0); color original = pixels[index]; pixels[index] = lerpColor(original, color(0), (255 - alpha) / 255.0); } } } updatePixels(); } // Po Kolam Smoke class class Smoke { float x, y; float alpha; float speed; float size; PImage img; Smoke(float x, float y, PImage img) { this.x = x; this.y = y; this.img = img; this.alpha = random(50, 120); this.speed = random(0.2, 0.6); this.size = random(150, 300); } void update() { alpha -= speed; if (alpha <= 0) { x = random(width); y = random(height); alpha = random(50, 120); speed = random(0.2, 0.6); size = random(150, 300); img = random(1) < 0.5 ? smoke1 : smoke2; } } void display() { if (img != null) { tint(255, alpha); image(img, x, y, size, size); noTint(); } } } class SmokeCloud { float x, y; float scale; float alpha; float fadeSpeed; float maxAlpha; int state; PImage cloudImg; int delay; SmokeCloud() { reset(true); } void reset(boolean firstTime) { x = random(width); y = random(height); scale = random(0.3, 0.6); fadeSpeed = random(0.3, 0.6); maxAlpha = random(20, 60); cloudImg = random(1) < 0.5 ? smoke1 : smoke2; alpha = 0; state = 0; delay = int(random(60, 300)); } void update() { if (delay > 0) { delay--; return; } if (state == 0) { alpha += fadeSpeed; if (alpha >= maxAlpha) state = 1; } else { alpha -= fadeSpeed; if (alpha <= 0) reset(false); } } void display() { if (cloudImg != null) { tint(255, constrain(alpha, 0, 255)); image(cloudImg, x, y, cloudImg.width * scale, cloudImg.height * scale); noTint(); } } } void runConchAnimation() { if (!ambientSoundStarted) { ambientSound.amp(ambientMaxVolume); ambientSoundStarted = true; } background(0); if (conchFrameCounter++ >= conchFrameDuration) { currentConchIndex = (currentConchIndex + 1) % totalConchFrames; conchFrameCounter = 0; } pushMatrix(); translate(width/2, height/2); rotate(radians(conchRotationAngle)); if (conchFrames != null && conchFrames[currentConchIndex] != null) { PImage conchImg = conchFrames[currentConchIndex]; image(conchImg, 0, 0, conchImg.width * 0.8, conchImg.height * 0.8); } popMatrix(); conchRotationAngle += 0.1; for (SmokeCloud cloud : clouds) { cloud.update(); cloud.display(); } } void runPoKolamAnimation() { // Start meditation sound if not already playing if (!ambientSoundStarted) { ambientSound.amp(ambientMaxVolume); ambientSoundStarted = true; } // Background chalkboard background(0); if (kolamChalkBackground != null) { image(kolamChalkBackground, width/2, height/2); } // Draw hazy smoke effect for (Smoke s : kolamSmokes) { s.update(); s.display(); } // Animate Po Kolam mandala pushMatrix(); translate(width / 2, height / 2); rotate(radians(kolamRotationAngle)); if (kolamFrames != null && kolamFrames[currentKolamFrame] != null) { PImage kolamImg = kolamFrames[currentKolamFrame]; image(kolamImg, 0, 0, kolamImg.width * 0.9, kolamImg.height * 0.9); } popMatrix(); // Rotate and switch frames kolamRotationAngle += 0.2; kolamFrameCounter++; if (kolamFrameCounter >= kolamFrameDelay) { currentKolamFrame = (currentKolamFrame + 1) % totalKolamFrames; kolamFrameCounter = 0; } } void readSerialInput() { while (myPort.available() > 0) { char inChar = (char) myPort.read(); if (inChar == '\n') { inputString = inputString.trim(); println("Received: " + inputString); // DEBUG if (inputString.startsWith("HALL:")) { handleHallSensor(inputString); } else if (inputString.length() > 0) { handleRFID(inputString); } inputString = ""; } else { inputString += inChar; } } } void handleHallSensor(String hallData) { String valueStr = hallData.substring(5).trim(); try { // Parse all six sensor values separated by commas String[] parts = split(valueStr, ','); if (parts.length == NUM_HALL_SENSORS) { int minValue = 1023; int minSensorPhysical = -1; // Physical Arduino pin index (0-5 for A0-A5) // Read all sensor values and find the LOWEST reading (strongest magnet detection) for (int i = 0; i < NUM_HALL_SENSORS; i++) { hallValues[i] = int(parts[i].trim()); // Find sensor with lowest value (magnet nearby pulls value down) if (hallValues[i] < minValue) { minValue = hallValues[i]; minSensorPhysical = i; // This is A0, A1, A2, A3, A4, or A5 } } // Convert physical pin to logical position in circular sequence int minSensorLogical = physicalToLogical[minSensorPhysical]; println("Hall sensors: [A0=" + hallValues[0] + ", A1=" + hallValues[1] + ", A2=" + hallValues[2] + ", " + "A3=" + hallValues[3] + ", A4=" + hallValues[4] + ", A5=" + hallValues[5] + "] Min: " + minValue + " from A" + minSensorPhysical + " (logical pos " + minSensorLogical + ")"); // Check if debounce time has passed long currentTime = millis(); boolean canTrigger = (currentTime - lastTriggerTime) > triggerDebounceMs; // Trigger only when value drops below threshold (magnet nearby) if (minValue < hallThreshold && canTrigger && minSensorLogical != lastTriggeredSensor) { // Determine direction based on sensor sequence if (lastTriggeredSensor != -1) { boolean isClockwise = isClockwiseMovement(lastTriggeredSensor, minSensorLogical); String fromPin = "A" + logicalToPhysical[lastTriggeredSensor]; String toPin = "A" + minSensorPhysical; println("Movement detected: " + (isClockwise ? "CLOCKWISE" : "COUNTER-CLOCKWISE") + " (" + fromPin + "→" + toPin + ", logical: " + lastTriggeredSensor + "→" + minSensorLogical + ")"); if (isClockwise) { advanceAnimationLayer(); } else { reverseAnimationLayer(); } } // Update sensor history (using logical positions) secondLastTriggeredSensor = lastTriggeredSensor; lastTriggeredSensor = minSensorLogical; lastTriggerTime = currentTime; } else if (minValue > baselineThreshold) { // Reset when magnet is far away (all sensors back to baseline) // Don't reset lastTriggeredSensor to allow direction detection } } else { println("Error: Expected " + NUM_HALL_SENSORS + " values, got " + parts.length); } } catch (Exception e) { println("Error parsing hall sensor values: " + valueStr); e.printStackTrace(); } } // Determine if movement from sensor A to sensor B is clockwise // Sensors arranged in circle: 0, 1, 2, 3, 4, 5 boolean isClockwiseMovement(int fromSensor, int toSensor) { // Calculate the shortest distance between sensors int diff = toSensor - fromSensor; // Normalize to handle wraparound (e.g., 5 -> 0) if (diff > NUM_HALL_SENSORS / 2) { diff -= NUM_HALL_SENSORS; } else if (diff < -NUM_HALL_SENSORS / 2) { diff += NUM_HALL_SENSORS; } // Positive difference = clockwise, negative = counter-clockwise return diff > 0; } void advanceAnimationLayer() { // -------- LOTUS -------- if (triggerLotusAnimation) { println("Advancing Lotus animation"); currentIndex = min(currentIndex + 1, totalFrames - 1); animProgress = 0; // Reset fade progress spawnFallingLotuses(3); } // -------- CONCH -------- if (triggerConchAnimation) { println("Advancing Conch animation"); currentConchIndex = (currentConchIndex + 1) % totalConchFrames; for (int i = 0; i < 2; i++) { clouds.add(new SmokeCloud()); } } // -------- PO KOLAM -------- if (showPoKolamAnimation) { println("Advancing Po Kolam animation"); currentKolamFrame = (currentKolamFrame + 1) % totalKolamFrames; // Add more smoke int idx = int(random(kolamSmokes.length)); kolamSmokes[idx] = new Smoke(random(width), random(height), random(1) < 0.5 ? smoke1 : smoke2); } } void reverseAnimationLayer() { // -------- LOTUS -------- if (triggerLotusAnimation) { println("Reversing Lotus animation"); currentIndex = max(currentIndex - 1, 0); animProgress = 0; // Reset fade progress if (showFallAnimation && currentIndex < totalFrames - 1) { showFallAnimation = false; } } // -------- CONCH -------- if (triggerConchAnimation) { println("Reversing Conch animation"); currentConchIndex = (currentConchIndex - 1 + totalConchFrames) % totalConchFrames; } // -------- PO KOLAM -------- if (showPoKolamAnimation) { println("Reversing Po Kolam animation"); currentKolamFrame = (currentKolamFrame - 1 + totalKolamFrames) % totalKolamFrames; } } void handleRFID(String tagID) { println("RFID Tag detected: '" + tagID + "'"); // Reset all animations triggerLotusAnimation = false; triggerConchAnimation = false; showPoKolamAnimation = false; // Reset sound state for new animation ambientSoundStarted = false; // Remove spaces from tag ID for comparison tagID = tagID.replaceAll("\\s+", ""); if (tagID.equalsIgnoreCase("8BC71505")) { println("Starting LOTUS animation"); triggerLotusStart(); } else if (tagID.equalsIgnoreCase("8934812F")) { println("Starting CONCH animation"); triggerConchAnimation = true; currentConchIndex = 0; conchFrameCounter = 0; conchRotationAngle = 0; } else if (tagID.equalsIgnoreCase("EC21752F")) { println("Starting PO KOLAM animation"); showPoKolamAnimation = true; currentKolamFrame = 0; } else { println("Unsupported tag: '" + tagID + "'"); } } void triggerLotusStart() { triggerLotusAnimation = true; currentIndex = 0; animProgress = 0; showFallAnimation = false; startedSound = false; soundVolume = 0; ambientSound.amp(0); animationComplete = false; fallingLotuses.clear(); lastTriggeredSensor = -1; secondLastTriggeredSensor = -1; } float getFadeDurationForFrame(int frameNum) { return 45; } void spawnFallingLotuses(int count) { for (int i = 0; i < count; i++) { float x; if (random(1) < 0.5) { x = random(100, 400); } else { x = random(width - 400, width - 100); } float y = random(-height, 0); fallingLotuses.add(new FallingLotus(x, y)); } } class FallingLotus { float x, y; float speed; float alpha; float scale; FallingLotus(float x, float y) { this.x = x; this.y = y; this.speed = random(2.0, 4.0); this.alpha = 0; this.scale = random(0.8, 1.5); } void update() { y += speed; if (alpha < 255) alpha += 1; } void display() { tint(255, alpha); image(lotus, x, y, lotus.width * scale, lotus.height * scale); noTint(); } } class FloatingFlower { float x, y; float angle; float rotationSpeed; float driftSpeedX, driftSpeedY; FloatingFlower(float x, float y, float angle, float rotationSpeed) { this.x = x; this.y = y; this.angle = angle; this.rotationSpeed = rotationSpeed; this.driftSpeedX = random(-0.3, 0.3); this.driftSpeedY = random(-0.1, 0.1); } void update() { x += driftSpeedX; y += driftSpeedY; angle += rotationSpeed; if (x < -flower.width) x = width + flower.width; else if (x > width + flower.width) x = -flower.width; if (y < -flower.height) y = height + flower.height; else if (y > height + flower.height) y = -flower.height; } void display(float alpha) { pushMatrix(); translate(x, y); rotate(angle); tint(255, alpha); image(flower, 0, 0, flower.width * 0.7, flower.height * 0.7); noTint(); popMatrix(); } } void drawWaveBackground() { background(220, 240, 255); noFill(); stroke(100, 150, 200, 50); strokeWeight(1); float spacing = 20; float time = frameCount * 0.07; for (float y = 0; y <= height; y += spacing) { beginShape(); for (float x = 0; x <= width; x += 2) { float dx = x - width / 2; float dy = y - height / 2; float dist = sqrt(dx * dx + dy * dy); float wave = sin(dist * 0.1 - time) * 10; vertex(x, y + wave); } endShape(); } for (float x = 0; x <= width; x += spacing) { beginShape(); for (float y = 0; y <= height; y += 2) { float dx = x - width / 2; float dy = y - height / 2; float dist = sqrt(dx * dx + dy * dy); float wave = sin(dist * 0.1 - time) * 10; vertex(x + wave, y); } endShape(); } }