Cards in hands
You can create a UI that responds to user input using two axes.
- JavaScript
- React
- Vue@2
- Vue@3
- Svelte
<div>
<p>You can create a UI that responds to user input using two axes.</p>
<div id="showcase">
<div class="showcase-item">
<div class="showcase-content">
<div id="movableCoordWrapper">
<div class="hand">
<div class="handcard" style=""><img class="logo_mono" src="../../common/image/logo_mono.svg"></div>
<div class="handcard" style=""><img class="logo_mono" src="../../common/image/logo_mono.svg"></div>
<div class="handcard" style=""><img class="logo_mono" src="../../common/image/logo_mono.svg"></div>
<div class="handcard" style=""><img class="logo_mono" src="../../common/image/logo_mono.svg"></div>
<div class="handcard" style=""><img class="logo_mono" src="../../common/image/logo_mono.svg"></div>
<div class="handcard" style=""><img class="logo_mono" src="../../common/image/logo_mono.svg"></div>
<div class="handcard" style=""><img class="logo_mono" src="../../common/image/logo_mono.svg"></div>
</div>
</div>
<div id="dot" class="movableDot"></div>
</div>
</div>
<div class="codepen"></div>
</div>
</div>
const transform = eg.Axes.TRANSFORM;
const dot = document.getElementById("dot");
const hand = document.querySelector(".hand");
const cards = Array.prototype.slice.apply(document.querySelectorAll(".handcard"));
const HAND_RADIUS = parseInt(window.getComputedStyle(hand).width) / 2;
const CARD_OFFSET = -300;
let handRotMin = null;
let handRotMax = null;
function getCardDistance(card, hand) {
const handRect = hand.getBoundingClientRect();
const handCenterY = (handRect.top + handRect.bottom) / 2;
const handCenterX = (handRect.left + handRect.right) / 2
const cardRect = card.getBoundingClientRect();
const cardCenterY = (cardRect.top + cardRect.bottom) / 2;
const cardCenterX = (cardRect.left + cardRect.right) / 2;
const deltaX = handCenterX - cardCenterX;
const deltaY = handCenterY - cardCenterY;
const cardDistance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
const cardTilt = (Math.atan(deltaX / deltaY) * -1)/Math.PI * 180;
return {
distance: cardDistance,
tilt: cardTilt
};
}
function setCardOnHand(card) {
const distance = getCardDistance(card, hand);
const cardTilt = distance.tilt;
const cardDistance = distance.distance;
const cardOffset = cardDistance - CARD_OFFSET - HAND_RADIUS;
if (handRotMin === null) {
handRotMin = cardTilt;
} else if (cardTilt < handRotMin) {
handRotMin = cardTilt;
}
if (handRotMax === null) {
handRotMax = cardTilt;
} else if (cardTilt > handRotMax) {
handRotMax = cardTilt;
}
card.style[eg.Axes.TRANSFORM] =
`rotateZ(${cardTilt}deg) translateY(${cardOffset}px)`;
card.setAttribute("data-cardOffset", cardOffset);
}
cards.forEach(function (v) {
setCardOnHand(v);
});
// 1. Initialize eg.Axes
const axes = new eg.Axes({
hand: {
range: [handRotMin, handRotMax],
bounce: 15
},
top: {
range: [0, 0],
bounce: [100, 160]
},
}, {
deceleration: 0.00034
});
// 2. attach event handler
axes.on("change", ({pos}) => {
dot.style["bottom"] = `${-1.4 * pos.top}px`;
dot.style[transform] = `translateX(${pos.hand * 2.3}px)`;
hand.style[transform] = `rotateZ(${pos.hand}deg)`;
cards.forEach((v) => {
v.style[transform] =
`${v.style[transform].split("translateY")[0]} translateY(${parseFloat(v.getAttribute("data-cardOffset")) + pos.top}px)`;
});
});
// 3. Initialize a inputType and connect it
axes.connect("hand top", new eg.Axes.PanInput(hand, {
scale: [0.3, 0.8]
}));
import React, { useState, useEffect } from "react";
import { useAxes, PanInput } from "@egjs/react-axes";
import Icon from "../../../static/img/demos/logo_mono.svg";
import "../../css/demos/cardinhand.css";
export default function CardInHand() {
const CARD_OFFSET = -300;
const [cards, setCards] = useState(new Array(7).fill({ tilt: 0, offset: 0 }));
const { connect, setAxis, hand, top } = useAxes(
{
hand: {
range: [0, 0],
bounce: 15,
},
top: {
range: [0, 0],
bounce: [100, 160],
},
},
{
deceleration: 0.00034,
},
);
useEffect(() => {
const hand = document.querySelector(".hand");
const handcards = Array.prototype.slice.apply(
document.querySelectorAll(".handcard")
);
const HAND_RADIUS = parseInt(window.getComputedStyle(hand).width) / 2;
let handRotMin = null;
let handRotMax = null;
const getCardDistance = (card, hand) => {
const handRect = hand.getBoundingClientRect();
const handCenterY = (handRect.top + handRect.bottom) / 2;
const handCenterX = (handRect.left + handRect.right) / 2;
const cardRect = card.getBoundingClientRect();
const cardCenterY = (cardRect.top + cardRect.bottom) / 2;
const cardCenterX = (cardRect.left + cardRect.right) / 2;
const deltaX = handCenterX - cardCenterX;
const deltaY = handCenterY - cardCenterY;
const cardDistance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
const cardTilt = ((Math.atan(deltaX / deltaY) * -1) / Math.PI) * 180;
return {
distance: cardDistance,
tilt: cardTilt,
};
};
const getCards = (card) => {
const distance = getCardDistance(card, hand);
const cardTilt = distance.tilt;
const cardDistance = distance.distance;
const cardOffset = cardDistance - CARD_OFFSET - HAND_RADIUS;
if (handRotMin === null) {
handRotMin = cardTilt;
} else if (cardTilt < handRotMin) {
handRotMin = cardTilt;
}
if (handRotMax === null) {
handRotMax = cardTilt;
} else if (cardTilt > handRotMax) {
handRotMax = cardTilt;
}
return { tilt: cardTilt, offset: cardOffset };
};
setCards(handcards.map(card => getCards(card)));
setAxis({
hand: {
range: [handRotMin, handRotMax],
},
});
connect("hand top", new PanInput(".hand", { scale: [0.3, 0.8] }));
}, []);
return (
<div>
<p>You can create a UI that responds to user input using two axes.</p>
<div id="showcase">
<div className="showcase-item">
<div className="showcase-content">
<div id="movableCoordWrapper">
<div className="hand" style={{ transform: `rotateZ(${hand}deg)` }}>
{cards.map((card, i) => (
<div className="handcard" key={i} style={{ transform: `rotateZ(${card.tilt}deg) translateY(${card.offset + top}px)` }}><Icon /></div>
))}
</div>
</div>
<div id="dot" className="movableDot" style={{ bottom: `${-1.4 * top}px`, transform: `translateX(${hand * 2.3}px)` }}></div>
</div>
</div>
</div>
</div>
);
};
<template>
<div class="App">
<p>You can create a UI that responds to user input using two axes.</p>
<div id="showcase">
<div class="showcase-item">
<div class="showcase-content">
<div id="movableCoordWrapper">
<div class="hand" :style="{ transform: `rotateZ(${hand}deg)` }">
<div v-for="card in cards" class="handcard" v-bind:key="card" :style="{ transform: `rotateZ(${card.tilt}deg) translateY(${card.offset + top}px)` }">
<img :src="`${require('../../../static/img/demos/logo_mono.svg')}`">
</div>
</div>
</div>
<div id="dot" class="movableDot" :style="{ bottom: `${-1.4 * top}px`, transform: `translateX(${hand * 2.3}px)` }"></div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { ref, defineComponent, onMounted } from "@vue/composition-api";
import { useAxes, PanInput } from "@egjs/vue2-axes";
export default defineComponent({
name: "App",
setup() {
const CARD_OFFSET = -300;
const cards = ref(new Array(7).fill({ tilt: 0, offset: 0 }));
const { connect, setAxis, hand, top } = useAxes(
{
hand: {
range: [0, 0],
bounce: 15,
},
top: {
range: [0, 0],
bounce: [100, 160],
},
},
{
deceleration: 0.00034,
},
);
onMounted(() => {
const hand = document.querySelector(".hand");
const handcards = Array.prototype.slice.apply(
document.querySelectorAll(".handcard")
);
const HAND_RADIUS = parseInt(window.getComputedStyle(hand).width) / 2;
let handRotMin = null;
let handRotMax = null;
const getCardDistance = (card, hand) => {
const handRect = hand.getBoundingClientRect();
const handCenterY = (handRect.top + handRect.bottom) / 2;
const handCenterX = (handRect.left + handRect.right) / 2;
const cardRect = card.getBoundingClientRect();
const cardCenterY = (cardRect.top + cardRect.bottom) / 2;
const cardCenterX = (cardRect.left + cardRect.right) / 2;
const deltaX = handCenterX - cardCenterX;
const deltaY = handCenterY - cardCenterY;
const cardDistance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
const cardTilt = ((Math.atan(deltaX / deltaY) * -1) / Math.PI) * 180;
return {
distance: cardDistance,
tilt: cardTilt,
};
};
const getCards = (card) => {
const distance = getCardDistance(card, hand);
const cardTilt = distance.tilt;
const cardDistance = distance.distance;
const cardOffset = cardDistance - CARD_OFFSET - HAND_RADIUS;
if (handRotMin === null) {
handRotMin = cardTilt;
} else if (cardTilt < handRotMin) {
handRotMin = cardTilt;
}
if (handRotMax === null) {
handRotMax = cardTilt;
} else if (cardTilt > handRotMax) {
handRotMax = cardTilt;
}
return { tilt: cardTilt, offset: cardOffset };
};
cards.value = handcards.map(card => getCards(card));
setAxis({
hand: {
range: [handRotMin, handRotMax],
},
});
connect("hand top", new PanInput(".hand", { scale: [0.3, 0.8] }));
});
return {
cards,
hand,
top,
};
},
});
</script>
<template>
<div class="App">
<p>You can create a UI that responds to user input using two axes.</p>
<div id="showcase">
<div class="showcase-item">
<div class="showcase-content">
<div id="movableCoordWrapper">
<div class="hand" :style="{ transform: `rotateZ(${hand}deg)` }">
<div v-for="card in cards" class="handcard" v-bind:key="card" :style="{ transform: `rotateZ(${card.tilt}deg) translateY(${card.offset + top}px)` }">
<img :src="`${require('../../../static/img/demos/logo_mono.svg')}`">
</div>
</div>
</div>
<div id="dot" class="movableDot" :style="{ bottom: `${-1.4 * top}px`, transform: `translateX(${hand * 2.3}px)` }"></div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { ref, defineComponent, onMounted } from "vue";
import { useAxes, PanInput } from "@egjs/vue-axes";
export default defineComponent({
name: "App",
setup() {
const CARD_OFFSET = -300;
const cards = ref(new Array(7).fill({ tilt: 0, offset: 0 }));
const { connect, setAxis, hand, top } = useAxes(
{
hand: {
range: [0, 0],
bounce: 15,
},
top: {
range: [0, 0],
bounce: [100, 160],
},
},
{
deceleration: 0.00034,
},
);
onMounted(() => {
const hand = document.querySelector(".hand");
const handcards = Array.prototype.slice.apply(
document.querySelectorAll(".handcard")
);
const HAND_RADIUS = parseInt(window.getComputedStyle(hand).width) / 2;
let handRotMin = null;
let handRotMax = null;
const getCardDistance = (card, hand) => {
const handRect = hand.getBoundingClientRect();
const handCenterY = (handRect.top + handRect.bottom) / 2;
const handCenterX = (handRect.left + handRect.right) / 2;
const cardRect = card.getBoundingClientRect();
const cardCenterY = (cardRect.top + cardRect.bottom) / 2;
const cardCenterX = (cardRect.left + cardRect.right) / 2;
const deltaX = handCenterX - cardCenterX;
const deltaY = handCenterY - cardCenterY;
const cardDistance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
const cardTilt = ((Math.atan(deltaX / deltaY) * -1) / Math.PI) * 180;
return {
distance: cardDistance,
tilt: cardTilt,
};
};
const getCards = (card) => {
const distance = getCardDistance(card, hand);
const cardTilt = distance.tilt;
const cardDistance = distance.distance;
const cardOffset = cardDistance - CARD_OFFSET - HAND_RADIUS;
if (handRotMin === null) {
handRotMin = cardTilt;
} else if (cardTilt < handRotMin) {
handRotMin = cardTilt;
}
if (handRotMax === null) {
handRotMax = cardTilt;
} else if (cardTilt > handRotMax) {
handRotMax = cardTilt;
}
return { tilt: cardTilt, offset: cardOffset };
};
cards.value = handcards.map(card => getCards(card));
setAxis({
hand: {
range: [handRotMin, handRotMax],
},
});
connect("hand top", new PanInput(".hand", { scale: [0.3, 0.8] }));
});
return {
cards,
hand,
top,
};
},
});
</script>
<script>
import { onMount } from "svelte";
import { useAxes, PanInput } from "@egjs/svelte-axes";
const CARD_OFFSET = -300;
let cards = new Array(7).fill({ tilt: 0, offset: 0 });
const { connect, setAxis, hand, top } = useAxes(
{
hand: {
range: [0, 0],
bounce: 15,
},
top: {
range: [0, 0],
bounce: [100, 160],
},
},
{
deceleration: 0.00034,
},
);
onMount(() => {
const hand = document.querySelector(".hand");
const handcards = Array.prototype.slice.apply(
document.querySelectorAll(".handcard")
);
const HAND_RADIUS = parseInt(window.getComputedStyle(hand).width) / 2;
let handRotMin = null;
let handRotMax = null;
const getCardDistance = (card, hand) => {
const handRect = hand.getBoundingClientRect();
const handCenterY = (handRect.top + handRect.bottom) / 2;
const handCenterX = (handRect.left + handRect.right) / 2;
const cardRect = card.getBoundingClientRect();
const cardCenterY = (cardRect.top + cardRect.bottom) / 2;
const cardCenterX = (cardRect.left + cardRect.right) / 2;
const deltaX = handCenterX - cardCenterX;
const deltaY = handCenterY - cardCenterY;
const cardDistance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
const cardTilt = ((Math.atan(deltaX / deltaY) * -1) / Math.PI) * 180;
return {
distance: cardDistance,
tilt: cardTilt,
};
};
const getCards = (card) => {
const distance = getCardDistance(card, hand);
const cardTilt = distance.tilt;
const cardDistance = distance.distance;
const cardOffset = cardDistance - CARD_OFFSET - HAND_RADIUS;
if (handRotMin === null) {
handRotMin = cardTilt;
} else if (cardTilt < handRotMin) {
handRotMin = cardTilt;
}
if (handRotMax === null) {
handRotMax = cardTilt;
} else if (cardTilt > handRotMax) {
handRotMax = cardTilt;
}
return { tilt: cardTilt, offset: cardOffset };
};
cards = handcards.map(card => getCards(card));
setAxis({
hand: {
range: [handRotMin, handRotMax],
},
});
connect("hand top", new PanInput(".hand", { scale: [0.3, 0.8] }));
});
</script>
<div>
<p>You can create a UI that responds to user input using two axes.</p>
<div id="showcase">
<div class="showcase-item">
<div class="showcase-content">
<div id="movableCoordWrapper">
<div class="hand" style="transform: rotateZ({$hand}deg)">
{#each cards as card}
<div class="handcard" style="transform: rotateZ({card.tilt}deg) translateY({card.offset + $top}px)"><img class="logo_mono" src="../../common/image/logo_mono.svg"></div>
{/each}
</div>
</div>
<div id="dot" class="movableDot" style="bottom: {-1.4 * $top}px; transform: translateX({$hand * 2.3}px)"></div>
</div>
</div>
</div>
</div>