245 Views
June 30, 23
スライド概要
IMPORTANT: THIS SLIDE HAS BEEN MACHINE TRANSLATED.
original slide: https://www.docswell.com/s/korosuke613/ZLLQJQ-2023-06-30-rotate-my-icon
---
This slide was presented at an internal Cybozu Frontend Day event on Friday, June 30, 2023.
Click on my image on the top page of https://korosuke613.dev/ to rotate it randomly (X-axis, Y-axis, Z-axis, X & Y-axis, X & Z-axis, Y & Z-axis).
We will teach you how to do it with Astro (MPA) using React.
I'm not familiar with front-end technologies, so my presentation may not be correct in terminology, etc. Please understand.
[keywords]
CSS, JavaScript, React, Astro, Browser
サイボウズ株式会社 開発本部 生産性向上チームで働いています。
How to make myself rotate randomly every time I click with Astro (Translated by DeepL) Jun 30, 2023 (Fri) Cybozu Frontend Day 平木場 風太> Futa Hirakoba <
Futa Hirakoba - 平木場 風太 🌋 Hometown - Kagoshima 🏢 Where I work - Cybozu K.K. / Development Division / Productivity Team 🧑💻 役割 - Engineering Productivity 🍣 Favorite Food - Chicken Nanban, spicy noodles . 💪 Often touches AWS, CI/CD tools, Terraform, etc. 💀 I don’t know anything about the front end. [1] @korosuke613 @shitimi_613 1. The photo shows Masumoto’s spicy noodles (tomato 5 spicy Chinese noodles topped with cheese). I want to eat it every day. [ 2 / 46 ]
https://korosuke613.dev [ 3 / 46 ]
korosuke613.dev | Z 軸の回転 共有 見る https://korosuke613.dev [ 4 / 46 ]
https://korosuke613.dev [ 5 / 46 ]
Click! https://korosuke613.dev [ 5 / 46 ]
Rotation! https://korosuke613.dev [ 6 / 46 ]
Rotation! https://korosuke613.dev [ 7 / 46 ]
I'll do the spinning 🙃. [ 8 / 46 ]
Furthermore...? [ 9 / 46 ]
korosuke613.dev | ランダムな軸の回転 共有 見る https://korosuke613.dev [ 10 / 46 ]
The direction of rotation is random. 😵💫 [ 11 / 46 ]
It changes with each click. [ 12 / 46 ]
https://www.geogebra.org/3d/czwmzrfr [ 13 / 46 ]
X-axis Y axis Z axis X-axis & Y-axis X-axis & Z-axis Y-axis & Z-axis [ 14 / 46 ]
korosuke613.dev Hirakiba website I created this as a place to organize my output, links to SNS, and other information. Composed of Astro + React (second generation) (aside) The first generation was configured with Nuxt + Vue (Aside) Migration to Nuxt3 was too hard, so we had to rebuild with a newer framework + React, which we use in our work. What is Astro? Astro is an all-in-one web framework for building fast, content-focused websites. https://docs.astro.build/ja/getting-started/ より Adopt MPA (Multi Page Application) High speed because it is designed to reduce JS execution on the client as much as possible Can use React as a UI framework [ 15 / 46 ]
I've got a website, but it's boring 😑. [ 16 / 46 ]
🤩 Yes, I know. Let's spin ourselves around . [ 17 / 46 ]
How to rotate
Do your best with CSS
And while we’re at it, we’ll make it zoom in and out (`scale`).
Rotate only once, because infinite rotation is too much work.
Add the following CSS class to the icon element
Example: CSS for an animation that rotates around the Z axis
.rotate-animation-z {
animation: rotate-anime-z 1.5s linear 1 /* animation name time curve iteration count */
}
/* Define an animation that rotates around the Z-axis */
/* Specify between 0% and 100%, respectively */
/* rotate is the rotation angle, scale is the scaling */
@keyframes rotate-anime-z {
0% {transform: rotate(0deg) scale(1);}
25% {transform: rotate(90deg) scale(1.5);}
50% {transform: rotate(180deg) scale(1);}
75% {transform: rotate(270deg) scale(1.5);}
100% {transform: rotate(360deg) scale(1);}
}
[ 18 / 46 ]
I want to rotate when I click. As it is, the page ends up rotating one revolution when it loads. It makes no sense to the viewer when it suddenly turns (and stops after one rotation). Rotate when clicked. Add CSS class on click (`click`), remove CSS class when animation ends (`animationend`, `animationcancel`) If you don’t remove the CSS class, it will only rotate on the first click. Use React’s `useEffect` to register event listeners at load time [ 19 / 46 ]
I want to rotate when I click.
Browser
My icon
User
page loading
register event handler
const cssClassName = 'rotate-animation-z';
export const MyIcon = (props: IMyIconProps) => {
useEffect(() => {
const thisImg = document.getElementById(props.iconId);
if (thisImg === null) return;
// Give animation class on click
thisImg.addEventListener('click', () => {
thisImg.classList.add(cssClassName);
});
loop
click my icon
(click event)
add animation class
(handle click event)
Rotate
animation end
(animationend event)
remove animation class
(handle animationend event)
// Delete the animation class when the animation is finished
const removeClass = () => {
thisImg.classList.remove(cssClassName);
};
thisImg.addEventListener('animationend', removeClass);
thisImg.addEventListener('animationcancel', removeClass);
});
return (<img /*
*/ />);
};
省略
Browser
My icon
User
[ 20 / 46 ]
Rotated! [ 21 / 46 ]
Something is missing 😕 [ 22 / 46 ]
🤣 Let's randomize the direction of rotation every time . [ 23 / 46 ]
I want to change the direction of rotation with
each click.
At the moment, there is only one
rotation on the Z-axis.
Randomly rotate X, Y, Z, XZ, XY,
and YZ axes
When deleting animations, it is not
known which animation class is
granted, so delete them all.
const cssClassNames = [
'rotate-animation-x',
'rotate-animation-y',
'rotate-animation-z',
'rotate-animation-xy',
'rotate-animation-xz',
'rotate-animation-yz',
];
export const MyIcon = (props: IMyIconProps) => {
useEffect(() => {
const thisImg = document.getElementById(props.iconId);
if (thisImg === null) return;
// Randomly assign animation class on click
thisImg.addEventListener('click', () => {
const randNum = Math.floor(Math.random() * cssClassNames.length);
const cssClassName = cssClassNames[randNum] || 'rotate-animation-z';
thisImg.classList.add(cssClassName);
});
// Delete the animation class when the animation is finished
const removeClass = () => {
cssClassNames.forEach((cssClassName) => {
// I don't know which classes are granted, so I delete them all.
thisImg.classList.remove(cssClassName);
});
};
thisImg.addEventListener('animationend', removeClass);
thisImg.addEventListener('animationcancel', removeClass);
});
[ 24 / 46 ]
X-axis Y axis Z axis Rotated! X-axis & Y-axis X-axis & Z-axis Y-axis & Z-axis [ 25 / 46 ]
desktop Actually, this website is responsive. smart phone [ 26 / 46 ]
The display of the two icons is switched by screen
width
Because the icons are positioned differently on the desktop and on the phone
<Section>
<div className="flex flex-col items-center md:flex-row md:justify-between md:gap-x-24">
<div>
<h1 className="hidden text-3xl font-bold md:block"> {/* Greetings */} </h1>
<div className="flex flex-row justify-between md:hidden md:gap-x-24">
{/* For mobile phone display */}
<h1 className="text-3xl font-bold"> {/* Greetings */} </h1>
<div className="h-20 w-20" id="my-icon-small">
<MyIcon /> {/* Small icon */}
</div>
</div>
<p className="mt-6 text-xl leading-9"> {/* Self-introduction */} </p>
<div className="mt-3 flex flex-wrap gap-1"> {/* SNS Links */} </div>
</div>
<div className="hidden shrink-0 md:block">
<div className="h-72 w-72" id="my-icon-large">
<MyIcon /> {/* Big icon */}
</div>
</div>
</div>
/Section>
[ 27 / 46 ]
The display of the two icons is switched by screen
width
Because the icons are positioned differently on the desktop and on the phone
<Section>
<div className="flex flex-col items-center md:flex-row md:justify-between md:gap-x-24">
<div>
<h1 className="hidden text-3xl font-bold md:block"> {/* Greetings */} </h1>
<div className="flex flex-row justify-between md:hidden md:gap-x-24">
{/* For mobile phone display */}
<h1 className="text-3xl font-bold"> {/* Greetings */} </h1>
<div className="h-20 w-20" id="my-icon-small">
<MyIcon /> {/* Small icon */}
</div>
</div>
<p className="mt-6 text-xl leading-9"> {/* Self-introduction */} </p>
<div className="mt-3 flex flex-wrap gap-1"> {/* SNS Links */} </div>
</div>
<div className="hidden shrink-0 md:block">
<div className="h-72 w-72" id="my-icon-large">
<MyIcon /> {/* Big icon */}
</div>
</div>
</div>
/Section>
[ 27 / 46 ]
Add animation to each icon
Get icon elements by class name
instead of Id
Assign a handler to each element
<div className="h-20 w-20 my-icon">
<MyIcon /> {/* Small icon */}
</div>
<div className="h-72 w-72 my-icon">
<MyIcon /> {/* Big icon */}
</div>
export const MyIcon = (props: IMyIconProps) => {
useEffect(() => {
const icons = document.getElementsByClassName(props.iconClass);
Array.from(icons).forEach((icon) => {
// Randomly assign animation class on click
icon.addEventListener('click', () => {
const randNum = Math.floor(Math.random() * cssClassNames.length);
const pickedCssClassName =
cssClassNames[randNum] || 'rotate-animation-z';
icon.classList.add(pickedCssClassName);
});
// Delete the animation class when the animation is finished
const removeClass = () => {
cssClassNames.forEach((cssClassName) => {
// I don't know which classes are granted, so I delete them all.
icon.classList.remove(cssClassName);
});
};
icon.addEventListener('animationend', removeClass);
icon.addEventListener('animationcancel', removeClass);
});
});
[ 28 / 46 ]
Multiple animation classes are assigned to a single element. Therefore, the probability of the animation coming out is not uniform. [ 29 / 46 ]
page load `useEffect` of large icon grant random animation class grant a random animation class small icon `useEffect` Grant random animation class grant a random animation class element of large icon small icon element ex: 'rotate-animation-z', 'rotate-animation-xy' ex: 'rotate-animation-x', 'rotate-animation-y' Each of the `MyIcon` components is a For elements with `.my-icon`, . To randomly assign animation classes. [ 30 / 46 ]
Why not just handle one element in one component → There's a reason it's not easy... 🧐. [ 31 / 46 ]
How to call the current component
It calls `MyIcon` with the following structure
Astro (`index.astro`)
├── TSX (`SelfIntroduction.tsx`)
└── TSX (`MyIcon.tsx`)
`MyIcon` is called from Astro and passed as a small element to the `SelfIntroduction` component
The reason for not calling `MyIcon.tsx` from `SelfIntroduction.tsx` has to do with Astro properties
<SelfIntroduction>
<MyIcon
client:idle
iconPath={/* PATH to icon image */}
iconClass="my-icon"
/>
</SelfIntroduction>
[ 32 / 46 ]
Astro’s philosophy is to minimize JS on the client
Astro is a framework that does as much server-side rendering as possible, so it is designed to minimize JS
execution on the client [1].
Thus, to get JS to run on the client, you must declare a `client:*` directive
`client:idle`: load and hydrate the component’s JavaScript after the page has initially loaded and the
`requestIdleCallback` event has occurred [2].
Example (`index.astro`)
<SelfIntroduction>
<MyIcon
client:idle {/* it won't work without it */}
iconPath={/* PATH to icon image */}
iconClass="my-icon"
/>
</SelfIntroduction>
1. https://docs.astro.build/en/concepts/why-astro/#server-first
2. https://docs.astro.build/en/reference/directives-reference/#clientidle
[ 33 / 46 ]
You have to apply client:* to MyIcon . `MyIcon` uses gory JS and requires `client:*` configuration `client:*` can only be called from `*.astro`. The effect of `client:*` extends to child components of the given component. Method 1. grant `client:idle` per `SelfIntroduction`. Astro (`index.astro`) └── TSX (`SelfIntroduction.tsx`) `client:idle` add └── TSX (`MyIcon.tsx`) Give `client:idle` to `MyIcon` only (current method) Astro (`index.astro`) ├── TSX (`SelfIntroduction.tsx`) └── TSX (`MyIcon.tsx`) `client:idle` add pass to `SelfIntroduction` 。 [ 34 / 46 ]
The introductory section comes up for a moment but disappears quickly... 🫨 1. grant `client:idle` per `SelfIntroduction`. method somehow didn't work. (We have not been able to identify the cause of the problem...) [ 35 / 46 ]
Organizing so far Currently calling `MyIcon` from Astro and passing it to `SelfIntroduction`. This method does not give a uniform probability of animation, so we want to use one component and one element. Easier to make it 1 component 1 element by calling `MyIcon` from `SelfIntroduction`. but this method makes the drawing look strange. What to do [ 36 / 46 ]
Organizing so far Currently calling `MyIcon` from Astro and passing it to `SelfIntroduction`. This method does not give a uniform probability of animation, so we want to use one component and one element. Easier to make it 1 component 1 element by calling `MyIcon` from `SelfIntroduction`. but this method makes the drawing look strange. What to do Using React’s `useState` to make it manageable for multiple `MyIcon`s to handle multiple elements Convert `SelfIntroduction.tsx` containing `MyIcon` calls to `.astro` files. [ 36 / 46 ]
Use React’s useState to make it manageable for multiple MyIcon s to handle multiple elements. I used to use this method. I stopped while I was working on the documents. useState`and`useRef` to manage the current animation state of large and small icons respectively Only one animation class is now added for each large and small icon Complex structure (I was trying out some stuff at the time, but I can’t remember why I needed `useRef`.) [ 37 / 46 ]
`MyIcon.tsx`
export const MyIcon = (props: IMyIconProps) => {
const [largeMode, setLargeMode] = useState('rotate-animation-z');
const largeModeRef = useRef<string>(null!);
largeModeRef.current = largeMode;
const [smallMode, setSmallMode] = useState('rotate-animation-z');
const smallModeRef = useRef<string>(null!);
smallModeRef.current = smallMode;
useEffect(makeEffect('my-icon-large', setLargeMode, largeModeRef));
useEffect(makeEffect('my-icon-small', setSmallMode, smallModeRef));
return (
<img
src={props.iconPath}
style={{ width: '100%' }}
alt="Avatar image"
loading="lazy"
/>
);
};
Memorial service
🪦.
[ 38 / 46 ]
`MyIcon.tsx`
const makeEffect = (
id: string,
setMode: React.Dispatch<React.SetStateAction<string>>,
modeRef: React.MutableRefObject<string>
) => {
return () => {
const thisImg = document.getElementById(id);
if (thisImg === null) return () => {};
const start = () => {
cssNames.forEach((c) => {
thisImg.classList.remove(c);
});
const randNum = Math.floor(Math.random() * cssNames.
const cssName = cssNames[randNum] || 'rotate-animati
setMode(cssName);
thisImg.classList.add(modeRef.current);
};
thisImg.addEventListener('click', start);
thisImg.addEventListener('animationend', end);
thisImg.addEventListener('animationcancel', end);
return () => {
thisImg.removeEventListener('click', start);
thisImg.removeEventListener('animationend', end);
thisImg.removeEventListener('animationcancel', end);
};
};
};
Memorial service
🪦.
const end = () => {
cssNames.forEach((cssName) => {
thisImg.classList.remove(cssName);
});
};
[ 39 / 46 ]
Convert SelfIntroduction.tsx containing MyIcon calls to .astro files. As we were making the materials, we realized that this way was simpler and easier to explain, so we adopted it. Fixed `MyIcon` to be one component, one element. Changed to a format where Id is passed from outside (initial contents of large icon looking around) Rewrite `SelfIntroduction.tsx` in `.astro` format Call `SelfIntroduction.astro` from `index.astro`. Call `MyIcon` from the newly created `SelfIntroduction.astro` and give `MyIcon` `client:idle [ 40 / 46 ]
`SelfIntroduction.astro`
--// <Omit: various imports>.
import { MyIcon } from '@/components/MyIconRandomLargeOnly';
const iconPath = /* icon path */;
// <Omit: Other processing>.
--<Section>
<div className="flex flex-col items-center md:flex-row md:justify-between md:gap-x-24">
<div>
<h1 className="hidden text-3xl font-bold md:block"> {/* Greetings */} </h1>
<div className="flex flex-row justify-between md:hidden md:gap-x-24">
{/* For mobile phone display */}
<h1 className="text-3xl font-bold"> {/* Greetings */} </h1>
<div class="my-icon h-20 w-20" id="my-icon-small">
<MyIcon client:idle iconPath={iconPath} iconId="my-icon-small" /> {/* Small icon */}
</div>
</div>
<p className="mt-6 text-xl leading-9"> {/* Self-introduction */} </p>
<div className="mt-3 flex flex-wrap gap-1"> {/* SNS Links */} </div>
</div>
<div className="hidden shrink-0 md:block">
<div class="my-icon h-72 w-72" id="my-icon-large">
<MyIcon client:idle iconPath={iconPath} iconId="my-icon-large" /> {/* Big icon */}
</div>
</div>
</div>
</Section>
[ 41 / 46 ]
`SelfIntroduction.astro`
--// <Omit: various imports>.
import { MyIcon } from '@/components/MyIconRandomLargeOnly';
const iconPath = /* icon path */;
// <Omit: Other processing>.
--<Section>
<div className="flex flex-col items-center md:flex-row md:justify-between md:gap-x-24">
<div>
<h1 className="hidden text-3xl font-bold md:block"> {/* Greetings */} </h1>
<div className="flex flex-row justify-between md:hidden md:gap-x-24">
{/* For mobile phone display */}
<h1 className="text-3xl font-bold"> {/* Greetings */} </h1>
<div class="my-icon h-20 w-20" id="my-icon-small">
<MyIcon client:idle iconPath={iconPath} iconId="my-icon-small" /> {/* Small icon */}
</div>
</div>
<p className="mt-6 text-xl leading-9"> {/* Self-introduction */} </p>
<div className="mt-3 flex flex-wrap gap-1"> {/* SNS Links */} </div>
</div>
<div className="hidden shrink-0 md:block">
<div class="my-icon h-72 w-72" id="my-icon-large">
<MyIcon client:idle iconPath={iconPath} iconId="my-icon-large" /> {/* Big icon */}
</div>
</div>
</div>
</Section>
[ 41 / 46 ]
index.astro
--// <Omit: various imports>.
import SelfIntroduction from '@/components/SelfIntroduction.astro';
// <Omit: Other processing>.
--...
<SelfIntroduction />
...
[ 42 / 46 ]
One icon no longer has multiple animation classes 🥳. [ 43 / 46 ]
Try turning it into a .astro file. What was good Replacing `.tsx` with `.astro` was easy. The `.astro` makes it simpler to implement if you want to dynamically execute JS. What’s not so good Cannot call `.astro` from `.tsx`. You may have trouble in the future. It would be good to keep only the parts that you actually want to be dynamic in `.tsx`, and wrap them in `.astro` for placement. [ 44 / 46 ]
Summary Can rotate images using CSS. If you want the animation to rotate with each click, you can add rotation CSS on click and remove it when the animation ends. If you want to rotate randomly, and for some reason want to place multiple components of the same image, and you are using Astro, you will have to twist your head. If you want to execute JS dynamically in Astro, be careful where you write `client:*`. It would be nice to have `.astro` for the part that calls the dynamic component If you know of a better way, please let me know! [ 45 / 46 ]
Thank you for your attention! By the way, this slide was created by Slidev[1][2]. 1. https://sli.dev/ 2. https://github.com/korosuke613/zenn-articles/pull/376 [ 46 / 46 ]