Vue

by joplomacedo

HTML

<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">

<div id="app">
  <div class="scoreBar">
    <p>Score: {{score}}</p>
  </div>
  
  <div class="field" ref="field">
    <img
      @click="onTargetClick"
      @load="onImgLoad"
      class="target"
      ref="target"
      src="https://upload.wikimedia.org/wikipedia/commons/7/78/Jos%C3%A9_S%C3%B3crates_cropped.png"
      :style="`--coord-x:${targetX}px; --coord-y:${targetY}px`">
  </div>
</div>

CSS

* {
  padding: 0;
  margin: 0;
  border: 0;
}

html,
body,
#app {
  height: 100%;
}

#app {
  display: flex;
  flex-direction: column;
  font-family: 'Press Start 2P', cursive;
}

.scoreBar {
  padding: .5em;
  text-align: right;
}

.field {
  flex-grow: 1;
}

.target {
  width: 150px;
  transition: .5s transform, .1s outline;
  transform: translateX(var(--coord-x)) translateY(var(--coord-y));
  user-drag: none;
  user-select: none;
  -moz-user-select: none;
  -webkit-user-drag: none;
  -webkit-user-select: none;
  -ms-user-select: none;
  cursor: crosshair;
}

.target.is-clicked{
  filter: hue-rotate(296deg);
}

Vue

new Vue({
  el: "#app",
  data: {
	  targetWidth: undefined,
    targetHeight: undefined,
    
    fieldWidth: undefined,
    fieldHeight: undefined,
    
		targetX: 40,
    targetY: 40,
    
    maxTargetX: undefined,
    maxTargetY: undefined,
    
    score: 0,
    
    targetMoveInterval: undefined,
    targetClickSequenceTimeout: undefined,
    
    currentLevel: 0,
    levelOptions: [
    	{
      	targetRestDuration: 1000,
      },
      {
      	targetRestDuration: 700,
      },
      {
      	targetRestDuration: 500,
      },
      {
      	targetRestDuration: 300,
      },
      {
      	targetRestDuration: 200,
      }
    ]
  },
  
  computed: {
  	currentLevelOptions() {
    	return this.levelOptions[this.currentLevel];
    }
  },
  watch: {
  	score( score ) {
    	if ( score%5 === 0) {
      	this.currentLevel++;
      }
    },
    currentLevel( currentLevel ) {
    	clearInterval(this.targetMoveInterval);
      
      this.targetMoveInterval = setInterval( () => {
        this.moveTargetRandomly();
      }, this.currentLevelOptions.targetRestDuration)
    }
  },
  methods: {
  	getRandomNumBetween( num1, num2) {
    	return Math.floor(Math.random() * num2) + num1;
    },
    
  	moveTargetRandomly() {
      this.targetX = this.getRandomNumBetween(0,this.maxTargetX);
    	this.targetY = this.getRandomNumBetween(0,this.maxTargetY);
    },
    
    onImgLoad() {
    	this.targetWidth = this.$refs.target.offsetWidth;
      this.targetHeight = this.$refs.target.offsetHeight;

      this.maxTargetX = this.fieldWidth - this.targetWidth;
      this.maxTargetY = this.fieldHeight - this.targetHeight;

      this.targetMoveInterval = setInterval( () => {
        this.moveTargetRandomly();
      }, this.currentLevelOptions.targetRestDuration)
    },
    
    onPageResize() {
    	if ( this.targetWidth && this.targetHeight ) {
  	    this.maxTargetX = this.fieldWidth - this.targetWidth;
	      this.maxTargetY = this.fieldHeight - this.targetHeight;
      }
   ...