From 9a270e59a4a7189dbe956c2585ca1bdce60fc406 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 6 Dec 2020 12:51:00 +0900
Subject: [PATCH] :secret:

---
 package.json                       |   2 +
 src/client/pages/about-misskey.vue | 132 +++++++++++++++--------
 src/client/scripts/physics.ts      | 168 +++++++++++++++++++++++++++++
 yarn.lock                          |  10 ++
 4 files changed, 266 insertions(+), 46 deletions(-)
 create mode 100644 src/client/scripts/physics.ts

diff --git a/package.json b/package.json
index 8d21e3484f..f825aa04ca 100644
--- a/package.json
+++ b/package.json
@@ -74,6 +74,7 @@
 		"@types/koa__multer": "2.0.2",
 		"@types/koa__router": "8.0.2",
 		"@types/markdown-it": "10.0.3",
+		"@types/matter-js": "0.14.7",
 		"@types/mocha": "7.0.2",
 		"@types/node": "14.0.22",
 		"@types/node-fetch": "2.5.7",
@@ -175,6 +176,7 @@
 		"lookup-dns-cache": "2.1.0",
 		"markdown-it": "11.0.1",
 		"markdown-it-anchor": "6.0.1",
+		"matter-js": "0.14.2",
 		"mocha": "8.2.1",
 		"moji": "0.5.1",
 		"ms": "2.1.2",
diff --git a/src/client/pages/about-misskey.vue b/src/client/pages/about-misskey.vue
index 4e0e74bfa1..77922fc966 100644
--- a/src/client/pages/about-misskey.vue
+++ b/src/client/pages/about-misskey.vue
@@ -1,50 +1,54 @@
 <template>
-<FormBase class="znqjceqz">
-	<section class="_formItem">
-		<div class="_formPanel" style="text-align: center; padding: 16px;">
-			<img src="/assets/icons/512.png" alt="" style="display: block; width: 100px; margin: 0 auto; border-radius: 16px;" ref="icon"/>
-			<div style="margin-top: 0.75em;">Misskey</div>
-			<div style="opacity: 0.5;">v{{ version }}</div>
-		</div>
-	</section>
-	<section class="_formItem" style="text-align: center; padding: 0 16px;">
-		{{ $t('_aboutMisskey.about') }}
-	</section>
-	<FormGroup>
-		<FormLink to="https://github.com/syuilo/misskey" external>
-			<template #icon><Fa :icon="faCode"/></template>
-			{{ $t('_aboutMisskey.source') }}
-			<template #suffix>GitHub</template>
-		</FormLink>
-		<FormLink to="https://crowdin.com/project/misskey" external>
-			<template #icon><Fa :icon="faLanguage"/></template>
-			{{ $t('_aboutMisskey.translation') }}
-			<template #suffix>Crowdin</template>
-		</FormLink>
-		<FormLink to="https://www.patreon.com/syuilo" external>
-			<template #icon><Fa :icon="faHandHoldingMedical"/></template>
-			{{ $t('_aboutMisskey.donate') }}
-			<template #suffix>Patreon</template>
-		</FormLink>
-	</FormGroup>
-	<FormGroup>
-		<template #label>{{ $t('_aboutMisskey.contributors') }}</template>
-		<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
-		<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
-		<FormLink to="https://github.com/mei23" external>@mei23</FormLink>
-		<FormLink to="https://github.com/acid-chicken" external>@acid-chicken</FormLink>
-		<FormLink to="https://github.com/tamaina" external>@tamaina</FormLink>
-		<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink>
-		<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink>
-		<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
-		<template #caption><MkLink url="https://github.com/syuilo/misskey/graphs/contributors">{{ $t('_aboutMisskey.allContributors') }}</MkLink></template>
-	</FormGroup>
-	<FormGroup>
-		<template #label><Mfm text="[jelly ❤]"/> {{ $t('_aboutMisskey.patrons') }}</template>
-		<FormKeyValueView v-for="patron in patrons" :key="patron"><template #key>{{ patron }}</template></FormKeyValueView>
-		<template #caption>{{ $t('_aboutMisskey.morePatrons') }}</template>
-	</FormGroup>
-</FormBase>
+<div style="overflow: hidden;">
+	<FormBase class="znqjceqz">
+		<div id="debug"></div>
+		<section class="_formItem">
+			<div class="_formPanel" style="text-align: center; padding: 16px;" ref="about">
+				<img src="/assets/icons/512.png" alt="" style="display: block; width: 100px; margin: 0 auto; border-radius: 16px;" ref="icon" @load="iconLoaded" draggable="false"/>
+				<div style="margin: 0.75em auto 0 auto; width: max-content;">Misskey</div>
+				<div style="margin: 0 auto; opacity: 0.5; width: max-content;">v{{ version }}</div>
+				<span v-for="emoji in easterEggEmojis" :key="emoji.emoji" class="_physics_circle_" :style="{ position: 'absolute', top: emoji.top, left: emoji.left, userSelect: 'none' }"><MkEmoji style="pointer-events: none; font-size: 24px; width: 24px;" :emoji="emoji.emoji" :custom-emojis="$store.state.instance.meta.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
+			</div>
+		</section>
+		<section class="_formItem" style="text-align: center; padding: 0 16px;" @click="gravity">
+			{{ $t('_aboutMisskey.about') }}
+		</section>
+		<FormGroup>
+			<FormLink to="https://github.com/syuilo/misskey" external>
+				<template #icon><Fa :icon="faCode"/></template>
+				{{ $t('_aboutMisskey.source') }}
+				<template #suffix>GitHub</template>
+			</FormLink>
+			<FormLink to="https://crowdin.com/project/misskey" external>
+				<template #icon><Fa :icon="faLanguage"/></template>
+				{{ $t('_aboutMisskey.translation') }}
+				<template #suffix>Crowdin</template>
+			</FormLink>
+			<FormLink to="https://www.patreon.com/syuilo" external>
+				<template #icon><Fa :icon="faHandHoldingMedical"/></template>
+				{{ $t('_aboutMisskey.donate') }}
+				<template #suffix>Patreon</template>
+			</FormLink>
+		</FormGroup>
+		<FormGroup>
+			<template #label>{{ $t('_aboutMisskey.contributors') }}</template>
+			<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
+			<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
+			<FormLink to="https://github.com/mei23" external>@mei23</FormLink>
+			<FormLink to="https://github.com/acid-chicken" external>@acid-chicken</FormLink>
+			<FormLink to="https://github.com/tamaina" external>@tamaina</FormLink>
+			<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink>
+			<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink>
+			<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
+			<template #caption><MkLink url="https://github.com/syuilo/misskey/graphs/contributors">{{ $t('_aboutMisskey.allContributors') }}</MkLink></template>
+		</FormGroup>
+		<FormGroup>
+			<template #label><Mfm text="[jelly ❤]"/> {{ $t('_aboutMisskey.patrons') }}</template>
+			<FormKeyValueView v-for="patron in patrons" :key="patron"><template #key>{{ patron }}</template></FormKeyValueView>
+			<template #caption>{{ $t('_aboutMisskey.morePatrons') }}</template>
+		</FormGroup>
+	</FormBase>
+</div>
 </template>
 
 <script lang="ts">
@@ -57,6 +61,7 @@ import FormBase from '@/components/form/base.vue';
 import FormGroup from '@/components/form/group.vue';
 import FormKeyValueView from '@/components/form/key-value-view.vue';
 import MkLink from '@/components/link.vue';
+import { physics } from '@/scripts/physics.ts';
 import * as os from '@/os';
 
 const patrons = [
@@ -115,10 +120,24 @@ export default defineComponent({
 			},
 			version,
 			patrons,
+			easterEggReady: false,
+			easterEggEmojis: [],
+			easterEggEngine: null,
 			faInfoCircle, faCode, faLanguage, faHandHoldingMedical,
 		}
 	},
 
+	created() {
+		const emojis = this.$store.state.settings.reactions;
+		for (let i = 0; i < 32; i++) {
+			this.easterEggEmojis.push({
+				top: -(32 + (Math.random() * 256)) + 'px',
+				left: (Math.random() * 99) + '%',
+				emoji: emojis[Math.floor(Math.random() * emojis.length)],
+			});
+		}
+	},
+
 	mounted() {
 		VanillaTilt.init(this.$refs.icon, {
 			max: 30,
@@ -127,6 +146,27 @@ export default defineComponent({
 			speed: 1000,
 		});
 	},
+
+	beforeUnmount() {
+		if (this.easterEggEngine) {
+			this.easterEggEngine.stop();
+		}
+	},
+
+	methods: {
+		iconLoaded() {
+			this.$nextTick(() => {
+				this.easterEggReady = true;
+			});
+		},
+
+		gravity() {
+			if (!this.easterEggReady) return;
+			this.easterEggReady = false;
+			this.$refs.icon.vanillaTilt.destroy();
+			this.easterEggEngine = physics(this.$refs.about);
+		}
+	}
 });
 </script>
 
diff --git a/src/client/scripts/physics.ts b/src/client/scripts/physics.ts
new file mode 100644
index 0000000000..c7f6b44a9f
--- /dev/null
+++ b/src/client/scripts/physics.ts
@@ -0,0 +1,168 @@
+import Matter from 'matter-js';
+
+export function physics(container: HTMLElement) {
+	const containerWidth = container.offsetWidth;
+	const containerHeight = container.offsetHeight;
+	const containerCenterX = containerWidth / 2;
+
+	// サイズ固定化(要らないかも?)
+	container.style.position = 'relative';
+	container.style.boxSizing = 'border-box';
+	container.style.width = `${containerWidth}px`;
+	container.style.height = `${containerHeight}px`;
+
+	// create engine
+	const engine    = Matter.Engine.create();
+	const world     = engine.world;
+
+	// create renderer
+	const render = Matter.Render.create({
+		engine: engine,
+		//element: document.getElementById('debug'),
+		options: {
+			width: containerWidth,
+			height: containerHeight,
+			background: 'transparent', // transparent to hide
+			wireframeBackground: 'transparent', // transparent to hide
+			hasBounds: false,
+			enabled: true,
+			wireframes: false,
+			showSleeping: true,
+			showDebug: false,
+			showBroadphase: false,
+			showBounds: false,
+			showVelocity: false,
+			showCollisions: false,
+			showAxes: false,
+			showPositions: false,
+			showAngleIndicator: false,
+			showIds: false,
+			showShadows: false
+		}
+	});
+
+	// Disable to hide debug
+	Matter.Render.run(render);
+
+	// create runner
+	const runner = Matter.Runner.create();
+	Matter.Runner.run(runner, engine);
+
+	// add walls
+	const wallopts = {
+		isStatic:     true,
+		restitution:  0.2,
+		friction:     1
+	};
+	const groundopts = {
+		isStatic:     true,
+		restitution:  0.1,
+		friction:     2
+	};
+
+	const groundThickness = 100;
+	const ground = Matter.Bodies.rectangle(containerCenterX, containerHeight + (groundThickness / 2), containerWidth, groundThickness, groundopts);
+	//const wallRight = Matter.Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, wallopts);
+	//const wallLeft = Matter.Bodies.rectangle(-50, window.innerHeight/2, 100, window.innerHeight, wallopts);
+
+	Matter.World.add(world, [
+		ground,
+		//wallRight,
+		//wallLeft,
+	]);
+
+	const objEls = Array.from(container.children);
+	const objs = [];
+	for (const objEl of objEls) {
+		let obj;
+		if (objEl.classList.contains('_physics_circle_')) {
+			obj = Matter.Bodies.circle(
+				objEl.offsetLeft + (objEl.offsetWidth / 2),
+				objEl.offsetTop + (objEl.offsetHeight / 2),
+				Math.max(objEl.offsetWidth, objEl.offsetHeight) / 2,
+				{
+					restitution:      0.1,
+					friction:         4,
+					frictionAir:      0,
+					frictionStatic:   50,
+					density:          100,
+				}
+			);
+		} else {
+			const style = window.getComputedStyle(objEl);
+			obj = Matter.Bodies.rectangle(
+				objEl.offsetLeft + (objEl.offsetWidth / 2),
+				objEl.offsetTop + (objEl.offsetHeight / 2),
+				objEl.offsetWidth,
+				objEl.offsetHeight,
+				{
+					restitution:      0.1,
+					friction:         4,
+					frictionAir:      0,
+					frictionStatic:   50,
+					density:          100,
+					chamfer:          { radius: parseInt(style.borderRadius, 10) },
+				}
+			);
+		}
+		objEl.id = obj.id;
+		objs.push(obj);
+	}
+
+	Matter.World.add(engine.world, objs);
+
+	// Add mouse control
+
+	const mouse = Matter.Mouse.create(container);
+	const mouseConstraint = Matter.MouseConstraint.create(engine, {
+		mouse: mouse,
+		constraint: {
+			stiffness: 1,
+			render: {
+				visible: false
+			}
+		}
+	});
+
+	Matter.World.add(engine.world, mouseConstraint);
+
+	// keep the mouse in sync with rendering
+	render.mouse = mouse;
+
+	for (const objEl of objEls) {
+		objEl.style.position = `absolute`;
+		objEl.style.top = 0;
+		objEl.style.left = 0;
+		objEl.style.margin = 0;
+		objEl.style.userSelect = 'none';
+		objEl.style.willChange = 'transform';
+	}
+
+	window.requestAnimationFrame(update);
+
+	let stop = false;
+
+	function update() {
+		for (const objEl of objEls) {
+			const obj = objs.find(obj => obj.id.toString() === objEl.id.toString());
+			if (obj == null) continue;
+
+			const x = (obj.position.x - objEl.offsetWidth / 2);
+			const y = (obj.position.y - objEl.offsetHeight / 2);
+			const angle = obj.angle;
+
+			objEl.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`;
+		}
+
+		if (!stop) {
+			window.requestAnimationFrame(update);
+		}
+	}
+
+	return {
+		stop: () => {
+			stop = true;
+			Matter.Runner.stop(runner);
+		}
+	};
+}
diff --git a/yarn.lock b/yarn.lock
index e56214ff75..6919719791 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -661,6 +661,11 @@
     "@types/mdurl" "*"
     highlight.js "^9.7.0"
 
+"@types/matter-js@0.14.7":
+  version "0.14.7"
+  resolved "https://registry.yarnpkg.com/@types/matter-js/-/matter-js-0.14.7.tgz#b816f1e7b441ee7499027f9566e4fb5baea637b3"
+  integrity sha512-HLUhVTUoKsibpPZ2tCzoCC/f/UYRWPP9WCOUh5F61BlrUESFV5fE7eKq/CmdoEGkNrLW9v407zYlfrTc9hnGIw==
+
 "@types/mdurl@*":
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
@@ -6116,6 +6121,11 @@ material-colors@^1.0.0:
   resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
   integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
 
+matter-js@0.14.2:
+  version "0.14.2"
+  resolved "https://registry.yarnpkg.com/matter-js/-/matter-js-0.14.2.tgz#8169af9e06fdc356ba9e72b49624eb329839883b"
+  integrity sha512-3ttVT8cJlQnGRjBa8MyVrGyvGmnmOkZ3YsyemIw+KwEEdVi70mo32FH1Eta2b3GfdDJFbMDRqyMQt4heNKBUEA==
+
 mdn-data@2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"