Star Power

Reach out into space. Your hands must be visible in the camera frame. ML models are loaded via a third party website.

This sketch needs permission to use your camera.

Source Code for this Sketch

class StepToHand {
    x = 0
    y = 0

    static def(input, defaultValue) {
        if (typeof input === "number") {
            return input
        }
        return defaultValue
    }

    constructor(step, minX, minY, maxX, maxY) {
        // step determines how quickly we orient towards
        // the hand position. Each frame, we move at
        // most this many units in any direction.
        this.step = StepToHand.def(step, 3)

        // Set the bounds for valid input values
        this.minX = StepToHand.def(minX, 0)
        this.minY = StepToHand.def(minY, 0)
        this.maxX = StepToHand.def(maxX, width)
        this.maxY = StepToHand.def(maxY, height)
    }

    update(x, y) {
        let deltaX = x-this.x
        let deltaY = y-this.y

        // Calculate the hypotenuse of our delta triangle
        // so we can limit angular movement by this.step
        let c = sqrt(pow(deltaX, 2)+pow(deltaY, 2))

        // Multiply our x & y movement by a ratio of their
        // length to the hypotenuse. For lateral or
        // horizonal movement we'll have 1:0 or 0:1.
        let ratioX = abs(deltaX) / c || 0
        let ratioY = abs(deltaY) / c || 0

        this.x += max(min(deltaX, this.step), -this.step)*ratioX
        this.y += max(min(deltaY, this.step), -this.step)*ratioY

        // Constrain x and y to valid area
        this.x = min(this.x, this.maxX)
        this.x = max(this.x, this.minX)
        this.y = min(this.y, this.maxY)
        this.y = max(this.y, this.minY)
    }
}

class Timer{
    constructor(delay) {
        this.count = 0
        this.delta = 0
        this.delay = delay
    }

    update() {
        this.delta += deltaTime
        if(this.delta > this.delay) {
            this.count++
            this.delta %= this.delay
        }
    }
}

class StepToMouse{
    // x and y are measured from the canvas origin (top left)
    x = 0
    y = 0
    // cx and cy are centered coordinates; i.e. 0,0 is
    // at the midpoint in the canvas
    cx = 0
    cy = 0

    static def(input, defaultValue) {
        if (typeof input === "number") {
            return input
        }
        return defaultValue
    }

    constructor(step, minX, minY, maxX, maxY) {
        // step determines how quickly we orient towards
        // the mouse position. Each frame, we move at
        // most this many units in any direction.
        this.step = StepToMouse.def(step, 3)

        // Set the bounds for valid input values
        this.minX = StepToMouse.def(minX, 0)
        this.minY = StepToMouse.def(minY, 0)
        this.maxX = StepToMouse.def(maxX, width)
        this.maxY = StepToMouse.def(maxY, height)
    }

    update() {
        let deltaX = mouseX-this.x
        let deltaY = mouseY-this.y

        // Calculate the hypotenuse of our delta triangle
        // so we can limit angular movement by this.step
        let c = sqrt(pow(deltaX, 2)+pow(deltaY, 2))

        // Multiply our x & y movement by a ratio of their
        // length to the hypotenuse. For lateral or
        // horizonal movement we'll have 1:0 or 0:1.
        let ratioX = abs(deltaX) / c || 0
        let ratioY = abs(deltaY) / c || 0

        this.x += max(min(deltaX, this.step), -this.step)*ratioX
        this.y += max(min(deltaY, this.step), -this.step)*ratioY

        // Constrain x and y to valid area
        this.x = min(this.x, this.maxX)
        this.x = max(this.x, this.minX)
        this.y = min(this.y, this.maxY)
        this.y = max(this.y, this.minY)

        this.cx = this.x - width/2
        this.cy = this.y - height/2
    }
}

class Star {
    static minSize = 3
    static maxSize = 6

    // speedFactor controls how quickly stars approach
    static speedFactor = 1

    constructor() {
        this.reset()

        // More evenly distribute stars in their initial
        // starting positions
        this.r = pow(random(0, sqrt(this.limit)), 1.8)
    }

    reset() {
        this.r = 0
        this.x = 0
        this.y = 0
        this.speed = sqrt(random(.1, 8)) * Star.speedFactor
        this.size = random(Star.minSize, Star.maxSize)
        this.deg = random(0, 360)
        this.hue = random(0, 360)
        this.limit = max(width, height)
    }

    // multiplier returns a ratio of this.r to this.limit
    // with the given minimum value. min should be between
    // 0 and 1
    multiplier(min) {
        if (typeof min !== 'number') {
            min = 0
        }
        return min + (this.r / this.limit * (1 - min))
    }

    draw() {
        // Position from center
        this.r += this.speed * this.multiplier(.2)

        fill(this.hue, 70, 90)
        this.x = cos(this.deg - 90) * this.r * width / this.limit
        this.y = sin(this.deg - 90) * this.r * height / this.limit
        circle(this.x, this.y, this.size * this.multiplier())

        // detect when stars are outside the visible area and
        // reset. note: assumes 0,0 coordinates are centered
        if (this.x < -(width + this.size) ||
            this.x > (width + this.size)) {
            this.reset()
        }
        if (this.y < -(height + this.size) *.75 ||
            this.y > (height + this.size) *.75) {
            this.reset()
        }
    }
}

class Alien{
    constructor() {
        this.lights = new Timer(250)
    }

    radX(deg, r) {
        return cos(deg-90)*r
    }
    radY(deg, r) {
        return sin(deg-90)*r
    }

    eye(x, y) {
        let deg = round(abs(atan2(mouseX-width/2, mouseY-height/2+30)-180))

        // Stalks
        stroke(80, 100, 50)
        noFill()
        bezier(x, 0, x+this.radX(deg, 5), this.radY(deg, 10), x-this.radX(deg, 15), y-this.radY(deg, 20), x, y)

        // Eyeballs
        noStroke()
        fill(80, 100, 50)
        circle(x+this.radX(deg, 2), y+this.radY(deg, 2), 10)
        fill( 100)
        circle(x+this.radX(deg, 3), y+this.radY(deg, 3), 6)
        fill( 0)
        circle(x+this.radX(deg, 4), y+this.radY(deg, 4), 3)
    }

    draw() {
        this.lights.update()

        push()
        rectMode(CENTER)

        // Position the ship
        translate(width/2, height*.9)

        // Cockpit
        strokeWeight(1)
        stroke(200, 100, 50, .7)
        fill(200, 100, 50, .3)
        arc(0, -10, 100, 100, 170, 10)

        // Pilot
        let alienElevation = 16
        noStroke()
        fill(80, 100, 50)
        arc(0, -alienElevation, 40, 20, 180, 0)
        rect(0, -alienElevation, 40, 5, 0, 0, 5, 5)

        this.eye(-10, -45)
        this.eye(10, -45)

        // Hull
        strokeWeight(1)
        stroke(20)
        fill(40)
        for(let i = 0; i < 6; i++) {
            ellipse(0, 0, 120-i*20, 30)
        }
        rect(0, 0, 120, 10, 5, 5, 5, 5)

        // Hull lights
        for(let i = 0; i < 11; i++) {
            if(i%4 === this.lights.count) {
                fill(65, 100, 50)
            } else if ((i+1)%4 === this.lights.count) {
                fill(310, 100, 50)
            } else {
                fill(200, 100, 50)
            }
            circle(-50+i*10, 0, 5)

            if(this.lights.count >=4) {
                this.lights.count = 0
            }
        }
        pop()
    }
}

let stars = []
let alien
let handpose
let video
let hands = []
let leftHand
let rightHand

function preload() {
    handpose = ml5.handPose()
}

function setup() {
    createCanvas(640, 400, P2D, canvas)
    colorMode(HSL)
    angleMode(DEGREES)
    ellipseMode(CENTER)
    noStroke()

    for (const x of Array(100).keys()) {
        stars.push(new Star())
    }

    alien = new Alien()

    video = createCapture(VIDEO)
    // mirror video for selfie purposes
    video.flipped = true
    video.size(640, 480)
    // video.hide()

    handpose.detectStart(video, (e) => {
        hands = e
    })

    fill(0, 100, 50);
    noStroke();

    leftHand = new StepToHand(30)
    leftHand.x = width*.25
    leftHand.y = height/2

    rightHand = new StepToHand(30)
    rightHand.x = width*.75
    rightHand.y = height/2
}

function draw() {
    background(0)
    push()
    translate(width/2, height/2)
    noStroke()
    stars.map((star) => { star.draw() })
    pop()

    noStroke()
    for (let i = 0; i < hands.length; i++) {
        let c = color(0)

        // Note: handedness seems to be flipped in the model.
        if (hands[i].handedness === "Left") {
            c = color(0, 100, 50)
        }
        if (hands[i].handedness === "Right") {
            c = color(240, 100, 50)
        }
        fill(c)

        let averageX = 0
        let countX = 0
        let averageY = 0
        let countY = 0

        for(let j = 0; j < hands[i].keypoints.length; j++) {
            let keypoint = hands[i].keypoints[j]

            // ignore the wrist and thumb to make gestures
            // a bit more responsive
            if (keypoint.name.indexOf("wrist") >= 0 ||
                keypoint.name.indexOf("thumb") >= 0) {
                continue
            }

            averageX += (width-keypoint.x)
            averageY += (keypoint.y)
            countX ++
            countY ++
        }

        // Note: handedness seems to be flipped in the model.
        if (hands[i].handedness === "Left") {
            rightHand.update(averageX/countX, averageY/countY)
        }
        if (hands[i].handedness === "Right") {
            leftHand.update(averageX/countX, averageY/countY)
        }
    }

    noFill()

    strokeWeight(8)
    stroke(80, 100, 50)

    bezier(width/2, height*.9, width/2-150, height*.9, leftHand.x, leftHand.y+150, leftHand.x+24, leftHand.y +10)
    bezier(width/2, height*.9, width/2-150, height*.9, leftHand.x, leftHand.y+150, leftHand.x+12, leftHand.y +2)
    bezier(width/2, height*.9, width/2-150, height*.9, leftHand.x, leftHand.y+150, leftHand.x, leftHand.y)
    bezier(width/2, height*.9, width/2-150, height*.9, leftHand.x, leftHand.y+150, leftHand.x-12, leftHand.y +3)
    bezier(width/2, height*.9, width/2-150, height*.9, leftHand.x, leftHand.y+150, leftHand.x-24, leftHand.y+6)

    bezier(width/2, height*.9, width/2+150, height*.9, rightHand.x, rightHand.y+150, rightHand.x+24, rightHand.y+6)
    bezier(width/2, height*.9, width/2+150, height*.9, rightHand.x, rightHand.y+150, rightHand.x+12, rightHand.y+3)
    bezier(width/2, height*.9, width/2+150, height*.9, rightHand.x, rightHand.y+150, rightHand.x, rightHand.y)
    bezier(width/2, height*.9, width/2+150, height*.9, rightHand.x, rightHand.y+150, rightHand.x-12, rightHand.y +2)
    bezier(width/2, height*.9, width/2+150, height*.9, rightHand.x, rightHand.y+150, rightHand.x-24, rightHand.y+10)

    alien.draw(width/2, height/5)
}

You may use all or part of the p5 code seen on this page for your own creations, without restriction. Attribution is appreciated but not required.

This sketch also includes the following sources: p5common.js ml5.js . Sources are available under their respective licenses.

See all p5.js sketches