Toggle menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Common.js

MediaWiki interface page
Revision as of 20:43, 11 April 2023 by Sha (talk | contribs) (Created page with "→‎Any JavaScript here will be loaded for all users on every page load.: →‎quickly hacked up buddy for use outside of the main site, may have vestigial parts: class Buddy { constructor(settings = { element: { entity, // examine entity (if any) id, //unique ID across entire site (if global), otherwise whatever classes, // CSS classes added on creation img, // image for the default figure pseudoelement to us...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* Any JavaScript here will be loaded for all users on every page load. */
/* quickly hacked up buddy for use outside of the main site, may have vestigial parts */

class Buddy {
    constructor(settings = {
        element: {
            entity, // examine entity (if any)
            id, //unique ID across entire site (if global), otherwise whatever
            classes, // CSS classes added on creation
            img, // image for the default figure pseudoelement to use
            size, // size for the entity - uses default 75px
            figure, // if the figure should have any contents
            actor //if the element also has a dialogueActor - used in chatter
        },
        activity: {
            behavior: {
                type, //follow, wander, wander_element, element
                rate, //rate at which the buddy repositions
                drift, //pixel amount the element can 'drift' around its target
                limit, //(wander only) pixel amount the element can travel at maximum per tick
                threshold, //(follow only) picks a spot within this pixel range of the element (x and y)
                element, //element gravitating to if wander-element or element
                muiPause, // if true, pauses movement when MUI is open
            },
            events: {
                onRender, //called when created - even before the user has entered the page, but after content has been loaded
                screenEnter, //called when appears visually within window
                pageEnter, //called when user enters the page (i.e. either loads in, or clicks the 'continue' button)
                mouseEnter, //self explanatory
                leaving, //called when leaving page
            }
        },
        location: { //used only if "global", meaning pages should be checking for them
            path, //string or array of paths
            exploredOnly // i.e. won't select a path that hasn't been explored. if none explored, not created
        }
    } = {}) { //constructor start
        //store all critical data
        this.html = `<a href="https://corru.observer/" target="_blank" id="${settings.element.id}" 
        class="buddy ${settings.element.classes ? settings.element.classes : ""}" 
        style="
            ${settings.element.img ? `--buddyimg: url(${settings.element.img});` : ""}
            ${settings.element.size ? `--size: ${settings.element.size};` : ""}
            --speed: ${settings.activity.behavior.speed ? settings.activity.behavior.speed : "1000" }ms;
        ">
            <figure>${settings.element.figure ? settings.element.figure : ""}</figure>
        </a>`
        this.elementData = settings.element
        this.activityData = settings.activity

        //determine globalness, where it should be if so
        if(typeof settings.location != 'undefined') {
            if(Buddy.globalBuddies.find(buddy => buddy.id == settings.element.id)) throw 'buddy exists'

            this.global = true
            this.locationData = settings.location
            Buddy.globalBuddies.push(this)

            this.setNewLocation()
            if(!this.shouldBeOnPage()) { //we don't want to continue with render if its not on this page
                return this
            }
        }

        //continue with render
        this.render()
        return this        
    }

    /* PUBLIC - everything to do with the individual buddy */
    // NEWLOCATION - rerolls buddy location from list of specified paths, used by globals only
    // can also specify a specific one
    setNewLocation = (specificLocation) => {
        if(typeof this.locationData == "undefined" || this.global == false) return false

        //specific set
        if(specificLocation) {
            this.currentLocation = specificLocation
            return this.currentLocation
        }

        //resets to original path - may not use, but didn't want to leave a gap in coverage
        if(typeof this.locationData.path == "string") {
            this.currentLocation = this.locationData.path
            return this.currentLocation
        }

        //otherwise, using locationData.path array
        switch(this.locationData.exploredOnly) {
            case true: //we can reference detectedEntities for pages the user has been to
                let visitedAreas = []
                for (const areaName in flags.detectedEntities) {
                    const area = flags.detectedEntities[areaName];
                    visitedAreas.push(area.path)
                }

                const possibleAreas = visitedAreas.filter(area => this.locationData.path.includes(area));
                let selectedArea
                if(possibleAreas.length) {
                    selectedArea = possibleAreas.sample()
                    this.currentLocation = selectedArea
                    return this.currentLocation
                } else {
                    throw 'no possible areas'
                }

            case false:
                this.currentLocation = this.locationData.path.sample()
                return this.currentLocation
        }
    }

    //what it sounds like - returns true if they should show on this page, otherwise false
    shouldBeOnPage = () => {
        if(location.pathname == this.currentLocation) return true;
        else return false
    }

    // RENDER - creates the buddy within the current page's context
    render = () => {
        document.body.insertAdjacentHTML('beforeEnd', this.html)
        this.el = document.body.querySelector(`#${this.elementData.id}`)
        console.log('buddy', this.el)

        //constructor events
        try {
            if(typeof this.activityData.events.onRender == "function") setTimeout(()=>{
                this.activityData.events.onRender()
                this.el.classList.add('instant')
                setTimeout(()=>this.el.classList.remove('instant'), 50)
            }, 10)

            if(typeof this.activityData.events.mouseEnter == "function") this.el.addEventListener('mouseenter', this.activityData.events.mouseEnter)
        } catch(e) { /* no events specified */ }

        Buddy.currentPageBuddies.push(this)
        this.xy = {x: 0, y: 0}
        setTimeout(this.activateBehavior, 20)
    }

    /* BEHAVIORS - movement around the page, events, etc */
    behaviorTimeout = null // created/cleared on page enter/leave
    behaving = false
    paused = false
    clearBehavior = () => {
        clearTimeout(this.behaviorTimeout)
        this.behaviorTimeout = null
        this.behaving = false
    }

    //behavior is checked and acted upon every 200ms
    //behavior can change, (i.e. being set from follow to element)!
    activateBehavior = () => {
        if(this.behaviorTimeout || this.behaving) throw 'already behaving'
        this.behaving = true
        this.runBehavior() //do initial one too
    }

    runBehavior = () => {
        clearTimeout(this.behaviorTimeout)
        this.behaviorTimeout = setTimeout(this.runBehavior, this.activityData.behavior.rate || 1000)

        if(typeof this.activityData.behavior.muiPause == 'undefined' || this.activityData.behavior.muiPause === true ) 
            if(document.body.classList.contains('in-menu') || document.body.classList.contains('mui-active')) return 
        if(this.paused) return

        let behavior = this.activityData.behavior
        switch(behavior.type) {
            case "follow": this.behavior_follow(); break
            case "wander": this.behavior_wander(); break
        }
    }

    //shortcuts for updating travel rate/anim speed
    changeRate = (rate) => { this.activityData.behavior.rate = rate }
    changeSpeed = (speed) => { this.el.style.setProperty('--speed', `${speed}ms`) }

    //used by mouse follow
    behavior_follow = () => { //if the position of the buddy is further than its threshold, move it within its threshold
        let buddyEl = this.el
        let behavior = this.activityData.behavior
        if(buddyEl == null) this.clearBehavior()

        let buddyPosition = buddyEl.getBoundingClientRect()
        let newPosition = {x: 0, y: 0}

        let desiredX = env.pageCursor.x
        let desiredY = env.pageCursor.y

        let threshold = behavior.threshold ? behavior.threshold : 150 //default 150 px

        if(behavior.element) { // we want it to follow the element instead, assuming center for now - use drift for organicness
            let elPos = behavior.element.getBoundingClientRect()
            desiredX = elPos.x + window.scrollX + elPos.width / 2
            desiredY = elPos.y + window.scrollY + elPos.height / 2
        }

        let contentPosition = document.body.getBoundingClientRect() // since buddies are within the content, they need to be offset the same
        desiredX = (desiredX - contentPosition.x) + window.pageXOffset

        let drift = behavior.drift || 0
        let randX = ((Math.random() * drift) - drift * 0.5)
        if(buddyPosition.left < (desiredX - threshold)) newPosition.x = desiredX - threshold + randX
        else if(buddyPosition.right > (desiredX + threshold + buddyPosition.width)) newPosition.x = desiredX + threshold + buddyPosition.width + randX
        else newPosition.x = this.xy.x + randX

        let randY = ((Math.random() * drift) - drift * 0.5)
        if(buddyPosition.top + window.scrollY > (desiredY + threshold)) newPosition.y = desiredY + threshold + randY
        else if(buddyPosition.bottom + window.scrollY < (desiredY - threshold - + buddyPosition.height)) newPosition.y = desiredY - threshold - buddyPosition.height + randY
        else newPosition.y = this.xy.y + randY

        this.setPosition(newPosition)
    }

    //gets a random position in the page - beware on large pages, may be hard to track!
    //ignores MUI being open - not concerned about player
    behavior_wander = () => {
        let buddyEl = this.el
        let behavior = this.activityData.behavior
        if(buddyEl == null) this.clearBehavior()
        
        let buddyPosition = buddyEl.getBoundingClientRect()
        let newPosition = {x: 0, y: 0}
        let targetEl
        let targetDimensions

        if(behavior.element) {
            targetEl = behavior.element
            targetDimensions = behavior.element.getBoundingClientRect()
        } else {
            targetEl = document.body
            targetDimensions = document.body.getBoundingClientRect();
        }

        if(behavior.limit) { //limited - will go in pixel increments across the page, but not beyond boundaries
            //limit only works with page-level wander - wandering with a limit within arbitrary elements while also accounting for whether it's the content element is a pain soo i shelfed that for now.
            //maybe u code crawlers could write the math better than i can >:) hmu

            //get current position of the element (based on top-left point)
            var currentX = buddyPosition.left + window.pageXOffset;
            var currentY = buddyPosition.top + window.pageYOffset;

            //calculate maximum distance the element can move in each direction
            var maxX = Math.clamp(currentX + behavior.limit, currentX, targetEl.offsetWidth - this.el.offsetWidth) - currentX;
            var maxY = Math.clamp(currentY + behavior.limit, currentY, targetEl.offsetHeight - this.el.offsetHeight) - currentY;
            var minX = Math.clamp(currentX - behavior.limit, 0, currentX) - currentX;
            var minY = Math.clamp(currentY - behavior.limit, 0, currentY) - currentY;       

            //generate random offset within the maximum distances
            var offsetX = Math.random() * (maxX - minX) + minX;
            var offsetY = Math.random() * (maxY - minY) + minY;

            //calculate new position within container bounds and container position
            newPosition.x = Math.clamp(currentX + offsetX, 0, targetEl.offsetWidth - this.el.offsetWidth);
            newPosition.y = Math.clamp(currentY + offsetY, 0, targetEl.offsetHeight - this.el.offsetHeight);

        } else { //unlimited - good luck catching this freak on content level! but in a specified element, it's not so bad
            if(targetEl != document.body) {
                newPosition.x = corruRand(targetDimensions.x + window.scrollX, targetDimensions.x + window.scrollX + targetDimensions.width)
                newPosition.y = corruRand(targetDimensions.y + window.scrollY, targetDimensions.y + window.scrollY + targetDimensions.height)
            } else {
                newPosition.x = corruRand(buddyPosition.width, targetDimensions.width - buddyPosition.width)
                newPosition.y = corruRand(buddyPosition.height, targetDimensions.height - buddyPosition.height)                
            }
        }

        let contentPosition = document.body.getBoundingClientRect() // since buddies are within the content, they need to be offset the same
        newPosition.x = (newPosition.x - contentPosition.x) - window.pageXOffset

        this.setPosition(newPosition)
    }

    setPosition = (xy = {x: null, y: null}) => {
        let buddyEl = this.el
        if(buddyEl == null) return

        if(xy.x !== null) { buddyEl.style.setProperty('--offsetX', xy.x); this.xy.x = xy.x }
        if(xy.y !== null) { buddyEl.style.setProperty('--offsetY', xy.y); this.xy.y = xy.y }
    }

    /* STATIC - for tracking, checking if anything needs to be rendered on present page, etc. */
    static globalBuddies = []
    static currentPageBuddies = []
}

//basic rand
function corruRand(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
}

window.addEventListener("load", (event) => {
  let CORRU_FUNFRIEND = new Buddy({
      element: {
          id: "funfriend",
          actor: "proxyfriend",
          size: "75px",
          img: "https://corru.observer/img/sprites/funfriend/funfriend.gif",
          classes: "funfriend",
      },
  
      activity: {
          behavior: {type: 'wander', drift: 50, rate: 10000, speed: 5000}
      }
  })
  
  document.body.insertAdjacentHTML('beforeend', `
  <style>
  .buddy {
      --buddyimg: url(https://corru.observer/img/sprites/funfriend/funfriend.gif);
      --offsetX: 50vw;
      --offsetY: 50vh;
      --size: 75px;
      --speed: 1000ms;
      position: absolute;
      top: 0;
      left: 0;
      width: var(--size);
      height: var(--size);
      transform-origin: center;
      transform: translate(-50%, -50%) translate(calc(var(--offsetX) * 1px), calc(var(--offsetY) * 1px)); 
      transition: transform var(--speed) ease-in-out;
      z-index: 10;
  
      -ms-interpolation-mode: nearest-neighbor;
      image-rendering: -webkit-optimize-contrast;
      image-rendering: -moz-crisp-edges;
      image-rendering: -o-pixelated;
      image-rendering: pixelated;
  }
  
  .buddy.instant {
      transition: none;
  }
  
  .buddy figure {
      width: 100%;
      height: 100%;
      animation: BUDDYBOUNCE-Y 1s ease-in-out infinite alternate
  }
  
  .buddy figure::after {
      content: "";
      background-image: var(--buddyimg);
      background-size: contain;
      background-repeat: no-repeat;
      width: 100%;
      height: 100%;
      display: block;
      animation: BUDDYBOUNCE-X 2s ease-in-out infinite alternate;
      transform-origin: bottom;
  }
  
  .buddy .target {
      background-size: auto;
  }
  
  .buddy .target::after {
      font-size: 20px;
      background: #0000007a;
  }
  
  .buddy.hidden {
      opacity: 0;
  }
  
  @keyframes BUDDYBOUNCE-Y {
      0% { transform: translateY(5%) rotateY(20deg)}
      100% { transform: translateY(-5%) rotateY(-20deg)}
  }
  
  @keyframes BUDDYBOUNCE-X {
      0% { transform: translateX(5%) rotate(5deg) }
      100% { transform: translateX(-5%) rotate(-5deg) }
  }
  </style>`)
})
Cookies help us deliver our services. By using our services, you agree to our use of cookies.