OBS配信用テロッパー

<iframe width="560" height="400" src="https://www.youtube.com/embed/FxjWWtdw4Hw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

```html:Browser.html
<!DOCTYPE html>
<html lang="ja"><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>


$(e=>{
	var b = new OBS.Browser().addLayer(
		OBS.layer.map(a=>{
			return new OBS.Layer(...a)
		})
	).addControl(
		new Controller.Status()
	)
})


var OBS = {
	layer:`

		RB,		NHK.png
		LB,		Cover.png
		B,		Shanghai.png
		LEFT,	Zombie.png
		START,	Shuchu2.svg
		X,		Zawa.png
		Y,		Pumpkin.png
		A,		Shuchu1.svg
		RIGHT,	NameCard.png
		UP,		Cloud.png
		DOWN,	Frame.png

	`.trim().split(/\s*\n+\s*/).map(s=>{
		return s.trim().split(/\s*,\s*/)
	})
}


function _html(...a){
	var b = a.shift().split('.')
	var q = $('<'+(b.shift()||'div')+'>').addClass(b)
	return q.append(a)
}


OBS.Browser = class{
	constructor(...a){
		this.layer = {}
		$('body').append(
			this.view = _html('.Browser',
				this.$Layers = _html('.Layers')
			)
		)
		this.loop = []
		setInterval(e=>{
			this.loop.forEach(c=>c.loop())
		},1000/30)
	}
	addLayer(a){
		a.forEach(v=>{
			this.$Layers.prepend(v.view||null)
			this.layer[v.type] = v
		})
		return this
	}
	addControl(...a){
		a.forEach(c=>{
			if(c.loop) this.loop.push(c)
			c.on(e=>{this.on(e)})
		})
		return this
	}
	on(e){
		if(e.type=='down'){
			var v = this.layer[e.key]
			if(v) v.view.toggle()
		}
		console.log(e)
	}
}

OBS.Layer = class{
	constructor(k,v){
		this.type = k
		this.view = _html('.Layer.'+v).hide()
		this.view.css('background-image',`url(img/${v})`)
	}
}


var Controller = {}

Controller.Data = class{
	constructor(){
		this.buttons = this.buttons()
		this.axes	= this.axes()
		this.count	= 0
		this.connection	= {}
		this.listener	= console.log
		this.dummy	= {
			buttons:this.buttons.map(k=>{return {pressed:false}}),
			axes:this.axes.map(k=>{return 0})
		}
	}
	buttons(){return 'A,B,X,Y,LB,RB,LT,RT,SELECT,START,LS,RS,UP,DOWN,LEFT,RIGHT'.split(',')}
	axes(){return 'LX,LY,RX,RY'.split(',')}
	button(a,b,i){
		if(a.pressed==b.pressed) return null
		switch(b.pressed){
			case(true):	this.listener({type:'down',	key:this.buttons[i]});break
			default:	this.listener({type:'up',	key:this.buttons[i]})
		}
	}
	stick(a,b,i){
		if(a==b) return null
		var p = this.axes[i]+'_POSITIVE'
		var n = this.axes[i]+'_NEGATIVE'
		switch(true){
			case(a>0):	this.listener({type:'up',	key:p});break
			case(a<0):	this.listener({type:'up',	key:n});break
		}
		switch(true){
			case(b>0):	this.listener({type:'down',	key:p});break
			case(b<0):	this.listener({type:'down',	key:n});break
		}
	}
	move(axes,s){
		if(axes.filter(v=>Math.abs(v)>0.2).length==0) return null
		this.listener({type:'move',axes:axes,stick:s})
	}
}

Controller.Status = class extends Controller.Data {
	constructor(){super()
		window.addEventListener("gamepadconnected",e=>{
			console.log(e)
			this.count++
			this.status(e.gamepad)
		});
		window.addEventListener("gamepaddisconnected",e=>{
			console.log(e)
			this.connection[e.gamepad.index] = null
			this.count--
		});
		this.gamepadAxes = []
	}
	on(f){
		this.listener = f
		return this
	}
	loop(){
		if(this.count==0) return
		this.item = Array.prototype.slice.call(navigator.getGamepads())
		this.item.forEach(g=>this.status(g))
		return this
	}
	status(gamepad){
		if(gamepad == null) return
		var g = this.connection[gamepad.index]||this.dummy
		gamepad.buttons.slice(0,16).forEach((v,i)=>this.button(g.buttons[i],v,i))
		gamepad.axes.forEach((v,i)=>this.stick(Math.round(g.axes[i]),Math.round(v),i))
		this.move(gamepad.axes.slice(0,2),'LeftStick')
		this.move(gamepad.axes.slice(2,4),'RightStick')
		this.connection[gamepad.index] = gamepad
		this.gamepadAxes = gamepad.axes
	}
}


</script>
<style>


*{margin:0;padding:0;}

.Layer{
	width:1920px;height:1080px;
	position:absolute;top:0;
	background-size:cover;
	background-repeat: no-repeat;
	background-position: 50% 50%;
}

/*
thx
https://qiita.com/seahorse_saki/items/e064760f82832ff766bc
*/

@keyframes pyon01 {
	0%,
	100% {transform: translate(0, 0);}
	50% {transform: translate(0, -100px);}
}
@keyframes bikkuri01 {
	100%,60%,35%,25%,0% {transform: translateY(0);}
	30% {transform: translateY(-26px);}
	40% {transform: translateY(-15px);}
}
@keyframes buruburu01 {
	0% {transform: translate(0, 2px) rotateZ(0deg);}
	25% {transform: translate(2px, 0) rotateZ(1deg);}
	50% {transform: translate(2px, 2px) rotateZ(0deg);}
	75% {transform: translate(2px, 0) rotateZ(-1deg);}
	100% {transform: translate(0, 0) rotateZ(0deg);}
}

.Layer.Zawa{
	animation:buruburu01 .1s infinite;
}
.Layer.Pumpkin{
	animation:pyon01 2s ease-in-out infinite;
}
.Layer.Zombie{
	top:200px;
	animation:pyon01 3s ease-in-out infinite;
}
.Layer.NameCard{
  animation: bikkuri01 3s both infinite;
}


</style>
</head>
</html>
```