円を描くように要素を動かす(HTML/CSS/JS)
前書き
円を描くように要素を動かす方法を掲載しています。
目次
- JavaScriptで動かす(平面)
- CSSで動かす(平面)
- SVGで動かす(平面)
- Canvasで動かす(平面)
- Canvasで動かす(立体)
- 参考
JavaScriptで動かす(平面)
下記の方法で動かしています。
- CSSで要素を中央に寄せる
- 三角関数を使って要素の座標を決める
- 要素のstyle属性を変更する
- 一定時間毎に繰り返す
円座標を親要素の大きさに合わせて調整しているので、親要素が正方形であれば真円、長方形であれば楕円を描くように動きます。
また、Intersection Observer APIを使って、見えている時だけ動くようにしています。
い
ろ
は
<div class="wrap js-wrap">
<div class="item item-01 js-item" data-opt="0.3, 0, 0.25">
<div class="interior interior-01">い</div>
</div>
<div class="item item-02 js-item" data-opt="0.6, 90, 0.35">
<div class="interior interior-02">ろ</div>
</div>
<div class="item item-03 js-item" data-opt="1.0, 180, -0.5">
<div class="interior interior-03">は</div>
</div>
</div>
<!-- data-opt="中心からの距離,初めの角度,一回に進む角度" -->
.wrap{
position: relative; overflow: hidden;
}
.item{
position: absolute; top: 50%; left: 50%;
transition: all .2s; //.2sかけて移動させる
}
.interior{
transform: translate(-50%, -50%); //中央に寄せる
//装飾
color: #fff; background: #845080; border-radius: 50%;
&-01{
width: 9em; height: 9em;
}
&-02{
width: 6em; height: 6em;
}
&-03{
width: 3em; height: 3em;
}
//親要素
const wrap = document.querySelector(".js-wrap");
//要素
const itemNode = document.querySelectorAll('.js-item');
const itemArray = Array.from(itemNode);
//設定
const itemOpt = itemArray.map((item) => {
const opt = item.dataset.opt.split(',');
const dist = Number(opt[0]); //中心からの距離 0~1
const first = Number(opt[1]); //初めの角度
const speed = Number(opt[2]); //一回に進む角度
return { dist, first, speed };
});
//要素の移動------------------
const moveItem = (count) => {
itemNode.forEach((item, index) => {
//要素の座標
const deg = itemOpt[index].speed * count + itemOpt[index].first; //進む角度 × カウント + 初めの角度
const rad = deg * Math.PI / 180; // 角度をラジアンに変換
const x = Math.cos(rad);
const y = Math.sin(rad);
//要素のstyle属性(座標 × 親要素の半分サイズ × 中心からの距離)
const itemTsX = x * wrap.clientWidth / 2 * itemOpt[index].dist + "px";
const itemTsY = y * wrap.clientHeight / 2 * itemOpt[index].dist + "px";
item.style.transform = "translateX(" + itemTsX + ") translateY(" + itemTsY + ")";
});
}
//一定時間毎に処理------------------
let tick;
let count = 0;
const switching = (e) => {
if( e[0].isIntersecting ){ //見えてる時
tick = () => {
moveItem( count );
requestAnimationFrame( tick );
count++;
}
requestAnimationFrame( tick );
}else{ //見えてない時
tick = () => {
cancelAnimationFrame( tick );
}
}
}
//見えているかどうか(Intersection Observer API)------------------
const createObserver = () => {
let observer;
const options = { root: null, rootMargin: "0%", threshold: 0 };
observer = new IntersectionObserver(switching, options); //コールバック関数とオプションを渡す
observer.observe(wrap); //要素の監視を開始
}
createObserver();
CSSで動かす(平面)
下記ページを参考にしています。
CSS3 のみで円周上を周回運動(circular motion)するアニメーション
当ページでは、下記の方法で動かしています。
また、見えている時だけ動くように、JavaScriptで制御しています。
- 要素(.item)の左上の角が、表示場所の中心に来るように配置
- 子要素(.interior)の上側にスペースを空ける(空ければ空ける程、大きく回る)
- 要素を、要素の左上の角を起点に回転させる。
- 子要素を、子要素の中心を起点に、要素と同じ速度で逆向きに回転させる。
「ろ」と書いてある部分だけに、点線の枠を付けています。
要素(.item)は点線の枠のある四角です。子要素(.interior)は紫色の円です。
見た目上は、子要素の位置だけが変わっているように見えます。
い
ろ
は
<div class="wrap js-wrap-b">
<div class="item item-01">
<div class="interior interior-01">い</div>
</div>
<div class="item item-02">
<div class="interior interior-02">ろ</div>
</div>
<div class="item item-03">
<div class="interior interior-03">は</div>
</div>
</div>
.wrap{
position: relative; overflow: hidden;
}
.item{
//位置
position: absolute; top: 50%; left: 50%; width: 50%; height: 50%;
//アニメーション
transform-origin: 0 0; //左上の角を起点に回転させる
animation-timing-function: linear;
animation-iteration-count: infinite;
&-01{
animation-duration: 18s;
}
&-02{
animation-duration: 12s; border: 1px dashed #a675a5;
}
&-03{
animation-duration: 9s; animation-direction: reverse;
}
}
//見えている時だけ動かす
.active .item{
animation-name: rotate-item;
}
.interior{
//位置、装飾
position: relative; color: #fff; background: #845080; border-radius: 50%;
//アニメーション
transform-origin: 0% 0%; //※1
animation-name: rotate-interior;
animation-timing-function: linear;
animation-iteration-count: infinite;
&-01{
top: 20%; width: 9em; height: 9em;
animation-duration: 18s; animation-direction: reverse;
}
&-02{
top: 60%; width: 6em; height: 6em;
animation-duration: 12s; animation-direction: reverse;
}
&-03{
top: 100%; width: 3em; height: 3em;
animation-duration: 9s;
}
}
//見えている時だけ動かす
.active .interior{
animation-name: rotate-interior;
}
@keyframes rotate-item {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes rotate-interior {
0% { transform: rotate(0deg) translate(-50%, -50%); } //※2
100% { transform: rotate(360deg) translate(-50%, -50%); } //※2
}
//※2 中央に寄せる為、translateも記述
//※1 中心を起点に回転させるのに(50%, 50%)でないのは、※2でtranslate(-50%, -50%) を指定している為。
(0%, 0%)が円の中心になる。
//見えているかどうか(Intersection Observer API)------------------
const wrap = document.querySelector(".js-wrap-b"); //親要素
const switching = (e) => {
if( e[0].isIntersecting ){ //見えてる時
wrap.classList.add("active");
}else{ //見えてない時
wrap.classList.remove("active");
}
}
const createObserver = () => {
let observer;
const options = { root: null, rootMargin: "0%", threshold: 0 };
observer = new IntersectionObserver(switching, options); //コールバック関数とオプションを渡す
observer.observe(wrap); //要素の監視を開始
}
createObserver();
SVGで動かす(平面)
SVGのアニメーション要素で動かしています。
パスで楕円を描き、そのパスの上を要素が動くようにしています。
また、見えている時だけ動くように、JavaScriptで制御しています。
<div class="wrap js-wrap-c">
<svg viewbox="0 0 720 360">
<!-- 開始、停止に利用する要素 -->
<rect id="start" x="0" y="0" width="0" height="0" />
<rect id="stop" x="0" y="0" width="0" height="0" />
<!-- パス -->
<path id="path01" class="path" d="M 240 180 A 120 60 0 0 1 480 180 A 120 60 0 0 1 240 180" />
<path id="path02" class="path" d="M 120 180 A 240 120 0 0 1 600 180 A 240 120 0 0 1 120 180" />
<path id="path03" class="path" d="M 0 180 A 360 180 0 1 0 720 180 A 360 180 0 1 0 0 180" />
<!-- 紫色の円、文字、アニメーション -->
<g>
<circle class="circle" cx="0" cy="0" r="4.5em" />
<text class="text" x="0" y="0">い</text>
<animateMotion dur="18s" repeatCount="indefinite" begin="start.click" end="stop.click">
<mpath href="#path01" />
</animateMotion>
</g>
<g>
<circle class="circle" cx="0" cy="0" r="3em" />
<text class="text" x="0" y="0">ろ</text>
<animateMotion dur="12s" repeatCount="indefinite" begin="start.click" end="stop.click">
<mpath href="#path02" />
</animateMotion>
</g>
<g>
<circle class="circle" cx="0" cy="0" r="1.5em" />
<text class="text" x="0" y="0">は</text>
<animateMotion dur="9s" repeatCount="indefinite" begin="start.click" end="stop.click">
<mpath href="#path03" />
</animateMotion>
</g>
</svg>
</div>
.path{
stroke: #a675a5; stroke-width: 1px; stroke-dasharray: 2 2; fill: none;
}
.circle{
fill: #845080; mix-blend-mode: multiply;
}
.text{
fill: #fff; text-anchor: middle; dominant-baseline: central;
}
//見えているかどうか(Intersection Observer API)------------------
const wrap = document.querySelector(".js-wrap-c"); //親要素
const startNode = document.querySelector('#start'); //開始に利用する要素
const stopNode = document.querySelector('#stop'); //停止に利用する要素
const clickEvent = new CustomEvent('click'); //カスタムイベント
const switching = (e) => {
if( e[0].isIntersecting ){ //見えてる時
startNode.dispatchEvent(clickEvent);
}else{ //見えてない時
stopNode.dispatchEvent(clickEvent);
}
}
const createObserver = () => {
let observer;
const options = { root: null, rootMargin: "0%", threshold: 0 };
observer = new IntersectionObserver(switching, options); //コールバック関数とオプションを渡す
observer.observe(wrap); //要素の監視を開始
}
createObserver();
Canvasで動かす(平面)
Canvasで動かしています。
見えている時だけ動くようにしています。
<div class="wrap js-wrap-d">
<canvas class="canvas js-canvas"></canvas>
</div>
.wrap{
position: relative; padding: 50% 0 0;
}
.canvas{
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}
//ディスプレイのデバイスピクセル比
const dpr = window.devicePixelRatio;
//親要素
const wrap = document.querySelector(".js-wrap-d");
const wrapStyles = window.getComputedStyle(wrap); //親要素のスタイル
const wrapFont = wrapStyles.getPropertyValue("font-family"); //親要素のフォント
//キャンバス
const canvas = document.querySelector(".js-canvas");
const ctx = canvas.getContext("2d");
const canvasFont = "bold " + dpr + "em " + wrapFont;
//設定
let itemOpt =[
{ text: "い", size: 77, dist: 0.3, first: 20, speed: 0.25, },
{ text: "ろ", size: 48, dist: 0.6, first: 0, speed: 0.35, },
{ text: "は", size: 24, dist: 1.0, first: 180, speed: -0.5, },
//文字、円の半径、中心からの距離、初めの角度、一回に進む角度
];
//要素の移動------------------
const moveItem = (count) => {
//キャンバスサイズ
canvas.width = wrap.clientWidth * dpr;
canvas.height = wrap.clientHeight * dpr;
itemOpt.forEach((item) => {
//要素の座標
const deg = item.speed * count + item.first; //進む角度 × カウント + 初めの角度
const rad = deg * Math.PI / 180; // 角度をラジアンに変換
const x = Math.cos(rad);
const y = Math.sin(rad);
//要素の位置(キャンバスの中心 + 座標 x キャンバスの半分 x 中心からの距離)
const itemX = canvas.width / 2 + x * canvas.width / 2 * item.dist;
const itemY = canvas.height / 2 + y * canvas.height / 2 * item.dist;
//円
ctx.fillStyle = "#845080";
ctx.globalCompositeOperation = "multiply"; //乗算
ctx.beginPath();
ctx.arc(itemX, itemY, item.size * dpr , 0, Math.PI*2);
ctx.closePath();
ctx.fill();
//テキスト
ctx.font = canvasFont;
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.globalCompositeOperation = "normal"; //乗算取り消し
ctx.fillText( item.text, itemX, itemY);
});
}
//一定時間毎に処理------------------
let tick;
let count = 0;
const switching = (e) => {
if( e[0].isIntersecting ){ //見えてる時
tick = () => {
moveItem( count );
requestAnimationFrame( tick );
count++;
}
requestAnimationFrame( tick );
}else{ //見えてない時
tick = () => {
cancelAnimationFrame( tick );
}
}
}
//見えているかどうか(Intersection Observer API)------------------
const createObserver = () => {
let observer;
const options = { root: null, rootMargin: "0%", threshold: 0 };
observer = new IntersectionObserver(switching, options); //コールバック関数とオプションを渡す
observer.observe(wrap); //要素の監視を開始
}
createObserver();
Canvasで動かす(立体)
下記ページを参考にthree.jsを使って動かしています。
Three.jsで入れ子構造
- 球体を作成して、グループに追加する
- グループを回転させる
<div class="wrap js-wrap-e">
<canvas class="canvas js-canvas02"></canvas>
</div>
.wrap{
position: relative; padding: 50% 0 0; overflow: hidden;
}
.canvas{
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}
import * as THREE from './three_r127/build/three.module.js';
import { OrbitControls } from './three_r127/examples/jsm/controls/OrbitControls.js';
// サイズ------------------
const wrap = document.querySelector(".js-wrap-e");
let wrapWidth = wrap.clientWidth;
let wrapHeight = wrap.clientHeight;
// レンダラー------------------
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector(".js-canvas02"),
antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(wrapWidth, wrapHeight);
// シーン------------------
const scene = new THREE.Scene();
scene.background = new THREE.Color( 0xf5edf3 ); //背景色
scene.fog = new THREE.Fog(0xf5edf3, 500, 2200); //フォグ
// カメラ------------------
const camera = new THREE.PerspectiveCamera(45, wrapWidth / wrapHeight);
camera.position.set(-100, 150, 500);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// カメラコントローラーを作成
const controls = new OrbitControls( camera, renderer.domElement );
// 光源------------------
const dLight = new THREE.DirectionalLight(0xffffff, 1); // 平行光源
dLight.position.set(1, 1, 1);
scene.add(dLight);
const aLight = new THREE.AmbientLight(0xffffff, 0.5); // 環境光源
scene.add(aLight);
// 物体------------------
//地面
const ground = new THREE.LineSegments(
new THREE.WireframeGeometry(
new THREE.PlaneGeometry(4000, 4000, 75, 75),
),
new THREE.LineBasicMaterial({color: 0x845080, transparent:true, opacity:0.25}),
);
ground.rotation.x = Math.PI / -2;
scene.add( ground );
//球体
const ballGroup = new THREE.Group(); //グループ作成
scene.add(ballGroup);
for (let i = 0; i < 10; i++) {
const ballTexture = new THREE.TextureLoader().load( i + '.svg' ); //画像
const ball = new THREE.Mesh(
new THREE.SphereGeometry(30, 30, 30),
new THREE.MeshPhongMaterial( { map: ballTexture } ),
);
// 座標
const rad = (i / 10) * Math.PI * 2;
const x = 250 * Math.cos(rad);
const y = 30;
const z = 250 * Math.sin(rad);
//配置
ball.position.set( x, y, z );
ball.rotation.y = -rad; // 向き
ballGroup.add(ball); //グループに追加
}
//リサイズ------------------
const wrapResize = () =>{
wrapWidth = wrap.clientWidth;
wrapHeight = wrap.clientHeight;
renderer.setSize(wrapWidth, wrapHeight);
// camera.aspect = wrapWidth / wrapHeight;
// camera.updateProjectionMatrix();
}
//一定時間毎に処理------------------
let tick;
const switching = (e) => {
if( e[0].isIntersecting ){ //見えてる時
tick = () => {
wrapResize(); //リサイズ
ballGroup.rotation.y += 0.01; //グループを回転
renderer.render(scene, camera); //レンダリング
requestAnimationFrame( tick );
}
requestAnimationFrame( tick );
}else{ //見えてない時
tick = () => {
cancelAnimationFrame( tick )
}
}
}
//見えているかどうか(Intersection Observer API)------------------
const createObserver = () => {
let observer;
const options = { root: null, rootMargin: "0%", threshold: 0 };
observer = new IntersectionObserver(switching, options); //コールバック関数とオプションを渡す
observer.observe(wrap); //要素の監視を開始
}
createObserver();
参考
WebGL開発に役立つ重要な三角関数の数式・概念まとめ (Three.js編)
君は使い分けられるか?CSS/SVG/Canvasのビジュアル表現でできること・できないこと
関連記事
マウスの位置に合わせて要素を動かす(HTML/CSS/JS)
この記事は役に立ちましたか?
送信中