포탈 키트
메타모아 공간끼리 서로 매치되면 내 사이트에 상대 공간으로 가는 포탈을 넣을 수 있어요. 포탈 안에는 상대 공간의 메타모아 썸네일이 보이고, 보는 각도(마우스·폰 기울기·3D 카메라)에 따라 문 너머 다른 세상처럼 움직여요.
전부 받기 (zip) 포탈 API 운영자 커뮤니티 · 공간연동
움직여 보기
마우스를 움직이거나 폰을 기울여 보세요. 문 안의 그림이 각도에 따라 달리 보여요.
1. 메타모아 문 스타일
메타모아 로고의 문 모양 그대로예요. 두 가지 느낌이 있어요.
2. 모아 야영지 스타일
모아 야영지에서 고블린이 여는 포탈 그대로예요. 메타모아 색(고블린 주황)으로 빛나요.
3. 커스텀 (링크로 만들기)
주소의 값만 바꿔서 색·크기·테두리·글자를 바꾼 포탈을 받아요. from·to에 서로 매치된 두 공간 id를 넣으면 문 안에 상대 공간의 썸네일이 들어가요(매치 전이거나 목록에 없는 공간이면 기본 그림).
| 값 | 뜻 |
|---|---|
style | metamoa-real(기본) · metamoa-cute · camp |
color·color2 | 16진수 색(# 없이, 예: e2663a). 기본은 메타모아 색(고블린 주황 e2663a→f2a65a) |
border·border_color | 테두리 굵기 0~3, 테두리 색 |
size | 가로 px 32~1024 (기본 256) |
label | 문 아래 글자, 20자 한 줄, 링크·금지어 안 됨 |
part | full(기본) · frame(안쪽 투명: 움직이는 그림 위에 겹치기) · mask(안쪽 모양) |
from·to | 서로 매치된 두 공간 id → 문 안에 to의 썸네일 |
예시 코드
웹 페이지 (2줄)
<div data-metamoa-portals="{내 공간 id}" data-style="metamoa-real"></div>
<script src="https://metamoa.com/portal/portal.js" async></script>
data-style은 metamoa-real·metamoa-cute·camp, data-color·data-color2·data-border·data-border-color·data-size로 커스텀 값도 넣어요. 매치된 포탈만 나오고, ?mm_portal=로 들어온 사람에게는 「돌아가기」 문이 먼저 보여요. 마우스·기울기로 안쪽 그림이 움직여요(움직임 줄이기 설정이면 멈춰요).
직접 그리기 (HTML·CSS·JS)
<a class="door" href="{url}"><span class="win"><span class="pic" style="background-image:url({thumb})"></span></span><img src="https://metamoa.com/portal.svg?style=metamoa-real&part=frame" alt=""></a>
.door { position: relative; display: inline-block; width: 96px; height: 108px; }
.door .win { position: absolute; inset: 0; overflow: hidden;
-webkit-mask: url(https://metamoa.com/portal.svg?style=metamoa-real&part=mask) 0 0 / 100% 100%;
mask: url(https://metamoa.com/portal.svg?style=metamoa-real&part=mask) 0 0 / 100% 100%; }
.door .pic { position: absolute; inset: -14%; background: center / cover; transition: transform .25s ease-out; }
.door img { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .door .pic { transition: none; } }
// 마우스·기울기에 따라 그림을 반대로 밀어요(창문 너머를 보는 느낌)
addEventListener('pointermove', (e) => {
const x = e.clientX / innerWidth - 0.5, y = e.clientY / innerHeight - 0.5;
for (const pic of document.querySelectorAll('.door .pic')) pic.style.transform = `translate(${-x * 18}px, ${-y * 18}px)`;
});
three.js (각도 효과 셰이더)
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const { scene: door } = await new GLTFLoader().loadAsync('https://metamoa.com/portal/portal-metamoa-real.gltf'); // 또는 -cute, portal-camp.gltf
const map = await new THREE.TextureLoader().setCrossOrigin('anonymous').loadAsync(p.thumb); // GET /api/portals/{id}의 thumb
map.colorSpace = THREE.SRGBColorSpace;
// 문 너머 다른 세상: 그림이 문 뒤 depth만큼 들어가 있는 것처럼, 보는 각도에 따라 보이는 부분이 달라져요.
door.getObjectByName('portal-inner').material = new THREE.ShaderMaterial({
uniforms: { map: { value: map }, depth: { value: 0.35 } },
side: THREE.DoubleSide,
vertexShader: `varying vec2 vUv; varying vec3 vDir;
void main() {
vUv = uv;
vec3 cam = (inverse(modelMatrix) * vec4(cameraPosition, 1.0)).xyz;
vDir = position - cam;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`,
fragmentShader: `uniform sampler2D map; uniform float depth; varying vec2 vUv; varying vec3 vDir;
void main() {
vec3 d = normalize(vDir);
vec2 shift = d.xy / max(abs(d.z), 0.2) * depth;
vec2 uv = (vUv - 0.5) * 0.8 + 0.5 + shift * 0.5; // 그림은 문보다 크게(0.8): 가장자리가 안 보여요
gl_FragColor = texture2D(map, clamp(uv, 0.0, 1.0));
#include <colorspace_fragment>
}`,
});
door.userData.href = p.url; // 클릭(raycast)하면 location.href = door.userData.href
scene.add(door);