(({ className, label }) => {
10 | const [isOpen, setIsOpen] = useState(false);
11 |
12 | const handleClick = useCallback(() => {
13 | setIsOpen((v) => !v);
14 | }, []);
15 |
16 | return (
17 | <>
18 |
19 | {label}
20 |
21 |
22 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos
23 | voluptates quae nostrum totam quaerat, commodi asperiores ullam.
24 | Neque enim beatae, cum, optio, tempora autem est a sint vitae ipsum
25 | debitis! Lorem ipsum dolor sit amet consectetur adipisicing elit.
26 | Quos voluptates quae nostrum totam quaerat, commodi asperiores
27 | ullam. Neque enim beatae, cum, optio, tempora autem est a sint vitae
28 | ipsum debitis!
29 |
30 |
31 |
32 | >
33 | );
34 | });
35 |
36 | export const Accordion = memo(() => {
37 | const dummyList = [
38 | 'Jacob Accordion One',
39 | 'Jacob Accordion Two',
40 | 'Jacob Accordion Three',
41 | 'Jacob Accordion Four',
42 | ];
43 |
44 | return (
45 |
46 | {dummyList.map((d) => (
47 |
48 | ))}
49 |
50 | );
51 | });
52 |
53 | const AccordionContainer = styled.div`
54 | max-width: 600px;
55 | `;
56 |
57 | const ContentSection = styled.div`
58 | position: relative;
59 | margin: 10px 20px;
60 | background: white;
61 | `;
62 |
63 | const Label = styled.div<{ open: boolean }>`
64 | position: relative;
65 | padding: 10px;
66 | background: cyan;
67 | font-weight: 600;
68 | cursor: pointer;
69 |
70 | &::before {
71 | content: ${(props) => (props.open ? '"-"' : '"+"')};
72 | position: absolute;
73 | top: 50%;
74 | right: 20px;
75 | transform: translateY(-50%);
76 | font-size: 1.5em;
77 | }
78 | `;
79 |
80 | const Contents = styled.div<{ open: boolean }>`
81 | position: relative;
82 | background: white;
83 | overflow-y: auto;
84 | transition: 0.5s;
85 | height: 0;
86 |
87 | ${(props) =>
88 | props.open &&
89 | css`
90 | height: 100px;
91 | `}
92 | `;
93 |
94 | const P = styled.p`
95 | padding: 0 8px;
96 | `;
97 |
--------------------------------------------------------------------------------
/src/stories/threejs/BasicScene.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import { BasicScene, BasicScene2, BasicScene3 } from '../../components';
4 |
5 | export default {
6 | title: 'threejs/BasicScene',
7 | component: BasicScene,
8 | parameters: {
9 | docs: {
10 | description: { component: 'threejs에서 제공하는 기본 예제' },
11 | },
12 | },
13 | } as Meta;
14 |
15 | const Template: Story = (args) => ;
16 |
17 | const Template2: Story = (args) => ;
18 |
19 | const Template3: Story = (args) => (
20 |
21 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Deleniti eaque nisi aliquid quisquam, quibusdam incidunt
22 | sint quis ad eos, atque, nesciunt ea! Earum pariatur ducimus beatae, consequatur quisquam dolore natus!
23 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Repellat repudiandae nesciunt eos
24 | necessitatibus, quod porro ad quam debitis placeat nihil, quia doloribus distinctio provident voluptate vel neque
25 | dolor deserunt consequatur. Reprehenderit, voluptatum tempore. Ducimus deleniti, qui labore natus molestiae
26 | similique pariatur veniam dicta dolores in, eveniet est molestias. Ex dolorem accusamus nisi deserunt ducimus
27 | quisquam modi optio, voluptatum distinctio soluta, ratione quas velit laborum voluptate suscipit aliquam dolores,
28 | doloremque quo repellat error labore autem? Distinctio, laborum, cumque reiciendis mollitia ex odio, unde
29 | consectetur quidem amet sit vero quo? Fugit pariatur laboriosam, provident magni placeat eos vero amet veniam
30 | temporibus cum eligendi expedita laudantium, necessitatibus commodi! Dolorem nesciunt officia quod facilis impedit
31 | ab molestias voluptas, pariatur assumenda doloribus atque qui quam dolorum laborum in quis sint eum! Corrupti sed
32 | accusamus id possimus delectus quod ea dolorem molestiae modi porro tempore repudiandae tempora veniam, repellat
33 | rerum autem blanditiis? Dolorum maxime molestiae, possimus blanditiis asperiores sunt, eos rem cum explicabo atque
34 | corrupti corporis, consectetur officia nemo incidunt porro amet iste! Incidunt veniam corrupti hic placeat nam,
35 | doloribus perferendis at? Cupiditate animi culpa molestiae rem officiis. Aperiam laudantium vel quas itaque velit
36 | aspernatur debitis, quasi quibusdam temporibus, dignissimos, repellat hic voluptate totam atque illum?
37 |
38 | );
39 |
40 | export const BasicCss = Template.bind({});
41 | export const Improve1BasicCss = Template2.bind({});
42 | export const Improve2BasicCss = Template3.bind({});
43 |
--------------------------------------------------------------------------------
/src/components/Drag/DragItemList.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
2 | import { DragItem } from './DragItem';
3 |
4 | export interface DragOptions {
5 | label?: ReactNode;
6 | value?: string;
7 | disabled?: boolean;
8 | }
9 |
10 | interface DragItemListProps {
11 | className?: string;
12 | options?: ReadonlyArray;
13 | isReset?: boolean;
14 | onChange?: (newOptions: ReadonlyArray) => void;
15 | }
16 |
17 | export const DragItemList = memo(({ className, options, onChange, isReset = false }) => {
18 | const [dargList, setDragList] = useState(options);
19 | const draggedValue = React.useRef();
20 | const resetInitialValue = useRef>();
21 |
22 | useEffect(() => {
23 | if (isReset && resetInitialValue.current !== undefined) {
24 | setDragList(resetInitialValue.current);
25 | }
26 | }, [isReset]);
27 |
28 | useEffect(() => {
29 | if (resetInitialValue.current === undefined) {
30 | resetInitialValue.current = options;
31 | }
32 | }, [options]);
33 |
34 | useEffect(() => {
35 | if (dargList && onChange !== undefined) {
36 | onChange(dargList);
37 | }
38 | }, [dargList, onChange]);
39 |
40 | const handleChange = useCallback((dragValue?: string, dropValue?: string) => {
41 | setDragList((preDragList) => {
42 | if (preDragList === undefined) {
43 | console.error('options is empty.');
44 | return;
45 | }
46 |
47 | if (dragValue !== undefined) {
48 | draggedValue.current = dragValue;
49 | }
50 |
51 | const defineList = preDragList.filter((l) => l.value !== draggedValue.current);
52 | const dropValueIndex = preDragList.findIndex((d) => d.value === dropValue);
53 | const draggedOption = preDragList.find((d) => d.value === draggedValue.current);
54 |
55 | if (draggedOption === undefined) {
56 | console.error('The dragged value could not be found.');
57 | return preDragList;
58 | }
59 |
60 | const forwardList = defineList.slice(0, dropValueIndex);
61 | const backwardList = defineList.slice(dropValueIndex);
62 | return [...forwardList, draggedOption, ...backwardList];
63 | });
64 | }, []);
65 |
66 | if (dargList === undefined) {
67 | return null;
68 | }
69 |
70 | return (
71 | <>
72 | {dargList.map(({ label, value, disabled }) => (
73 |
81 | ))}
82 | >
83 | );
84 | });
85 |
--------------------------------------------------------------------------------
/src/components/AnimatedEyesFollowMouseCursor/AnimatedEyesFollowMouseCursor.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useRef, useCallback } from 'react';
2 | import styled from 'styled-components';
3 | import throttle from 'lodash/throttle';
4 |
5 | export const AnimatedEyesFollowMouseCursor = memo(() => {
6 | const leftEyeRef = useRef(null);
7 | const rightEyeRef = useRef(null);
8 |
9 | const eyeball = useCallback((event: MouseEvent) => {
10 | if (
11 | leftEyeRef === null ||
12 | leftEyeRef.current === null ||
13 | rightEyeRef === null ||
14 | rightEyeRef.current === null
15 | ) {
16 | return;
17 | }
18 | const leftX =
19 | leftEyeRef.current.getBoundingClientRect().left +
20 | leftEyeRef.current.clientWidth / 2;
21 | const leftY =
22 | leftEyeRef.current.getBoundingClientRect().top +
23 | leftEyeRef.current.clientHeight / 2;
24 | const rightX =
25 | rightEyeRef.current.getBoundingClientRect().left +
26 | rightEyeRef.current.clientWidth / 2;
27 | const rightY =
28 | rightEyeRef.current.getBoundingClientRect().top +
29 | rightEyeRef.current.clientHeight / 2;
30 |
31 | const radianLeft = Math.atan2(event.pageX - leftX, event.pageY - leftY);
32 | const radianRight = Math.atan2(event.pageX - rightX, event.pageY - rightY);
33 |
34 | const rotationLeft = radianLeft * (180 / Math.PI) * -1 + 270;
35 | const rotationRight = radianRight * (180 / Math.PI) * -1 + 270;
36 |
37 | leftEyeRef.current.style.transform = `rotate(${rotationLeft}deg)`;
38 | rightEyeRef.current.style.transform = `rotate(${rotationRight}deg)`;
39 | }, []);
40 |
41 | useEffect(() => {
42 | document.body.addEventListener('mousemove', throttle(eyeball, 50));
43 | return () =>
44 | document.body.removeEventListener('mousemove', throttle(eyeball, 50));
45 | }, [eyeball]);
46 |
47 | return (
48 |
49 |
50 |
51 |
52 | );
53 | });
54 |
55 | const Box = styled.div`
56 | display: flex;
57 | justify-content: center;
58 | align-items: center;
59 | `;
60 |
61 | const Eye = styled.div`
62 | position: relative;
63 | width: 120px;
64 | height: 120px;
65 | display: block;
66 | background: white;
67 | margin: 0 20px;
68 | border-radius: 50%;
69 | box-shadow: 0 5px 45px rgba(0, 0, 0, 0.2), inset 0 0 15px black,
70 | inset 0 0 25px black;
71 |
72 | &::before {
73 | content: '';
74 | position: absolute;
75 | top: 50%;
76 | left: 35px;
77 | transform: translate(-50%, -50%);
78 | width: 45px;
79 | height: 45px;
80 | border-radius: 50%;
81 | background: white;
82 | border: 14px solid black;
83 | box-sizing: border-box;
84 | }
85 | `;
86 |
--------------------------------------------------------------------------------
/src/components/Canvas/MovingGradation/MovingGradation.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
2 | import useScreenSize from '../../../hooks/useScreenSize';
3 | import { DomUtils } from '../../../utils';
4 | import { COLORS, MAX_RADIUS, MIN_RADIUS, TOTAL_PARTICLES } from './constants';
5 | import { GlowParticle } from './GlowParticle';
6 |
7 | export const MovingGradation = memo(() => {
8 | const ref = useRef(null);
9 | const { stageWidth, stageHeight } = useScreenSize();
10 | const [particles, setParticles] = useState>([]);
11 |
12 | const createParticles = useCallback(() => {
13 | let curColor = 0;
14 | setParticles([]);
15 |
16 | for (let i = 0; i < TOTAL_PARTICLES; i++) {
17 | const item = new GlowParticle(
18 | Math.random() * stageWidth,
19 | Math.random() * stageHeight,
20 | Math.random() * (MAX_RADIUS - MIN_RADIUS) + MIN_RADIUS,
21 | COLORS[curColor]
22 | );
23 |
24 | if (++curColor >= COLORS.length) {
25 | curColor = 0;
26 | }
27 |
28 | setParticles((prev) => [...prev, item]);
29 | }
30 | }, [stageWidth, stageHeight]);
31 |
32 | const resize = useCallback(() => {
33 | if (ref.current === null) return;
34 |
35 | const ctx = ref.current.getContext('2d');
36 |
37 | if (ctx === null) return;
38 |
39 | const pixelRatio = window.devicePixelRatio > 1 ? 2 : 1;
40 |
41 | ref.current.width = stageWidth * pixelRatio;
42 | ref.current.height = stageHeight * pixelRatio;
43 | ctx.scale(pixelRatio, pixelRatio);
44 |
45 | // https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Compositing
46 | ctx.globalCompositeOperation = 'saturation';
47 |
48 | createParticles();
49 | }, [stageWidth, stageHeight, createParticles]);
50 |
51 | const animate = useCallback(
52 | (t: number) => {
53 | window.requestAnimationFrame(animate);
54 | if (ref.current === null) return;
55 | const ctx = ref.current.getContext('2d');
56 |
57 | ctx?.clearRect(0, 0, stageWidth, stageHeight);
58 |
59 | particles.forEach((p) => p.animate(ctx, stageWidth, stageHeight));
60 | },
61 | [stageWidth, stageHeight, particles]
62 | );
63 |
64 | useEffect(() => {
65 | if (!DomUtils.usableWindow()) return;
66 |
67 | window.addEventListener('resize', resize, false);
68 | resize();
69 |
70 | return () => window.removeEventListener('resize', resize, false);
71 | }, [resize]);
72 |
73 | useEffect(() => {
74 | if (!DomUtils.usableWindow()) return;
75 |
76 | const num = window.requestAnimationFrame(animate);
77 |
78 | return () => window.cancelAnimationFrame(num);
79 | }, [animate]);
80 |
81 | return ;
82 | });
83 |
--------------------------------------------------------------------------------
/src/components/FakeScrollSpy/FakeScrollSpy.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import styled, { css } from 'styled-components';
3 |
4 | export const FakeScrollSpy = memo(() => {
5 | return (
6 |
7 | Home Section
8 | About Section
9 | Services Section
10 | Portfolio Section
11 | Contact Section
12 |
13 |
14 | Home
15 | About
16 | Services
17 | Portfolio
18 | Contact
19 |
20 |
21 | );
22 | });
23 |
24 | const commonSectionStyle = css`
25 | position: relative;
26 | width: 100%;
27 | height: 100vh;
28 | color: white;
29 | display: flex;
30 | justify-content: center;
31 | align-items: center;
32 | font-size: 6em;
33 | `;
34 |
35 | const commonHoverStyle = css`
36 | background: black;
37 | color: white;
38 | `;
39 |
40 | const NavBar = styled.nav`
41 | position: fixed;
42 | top: 0;
43 | width: 100%;
44 | background: white;
45 | box-sizing: border-box;
46 | display: flex;
47 | justify-content: space-between;
48 | align-items: center;
49 | z-index: 1;
50 | `;
51 |
52 | const NavItem = styled.a`
53 | text-decoration: none;
54 | color: black;
55 | font-weight: 600;
56 | padding: 10px 20px;
57 | font-size: 1.6em;
58 | display: inline-block;
59 | width: 100%;
60 | text-align: center;
61 |
62 | &:hover {
63 | ${commonHoverStyle};
64 | }
65 | `;
66 |
67 | const Home = styled.section`
68 | ${commonSectionStyle};
69 |
70 | &:hover ~ ${NavBar} > ${NavItem}[href="#home"] {
71 | ${commonHoverStyle};
72 | }
73 | `;
74 |
75 | const About = styled.section`
76 | ${commonSectionStyle};
77 |
78 | &:hover ~ ${NavBar} > ${NavItem}[href="#about"] {
79 | ${commonHoverStyle};
80 | }
81 | `;
82 |
83 | const Services = styled.section`
84 | ${commonSectionStyle};
85 |
86 | &:hover ~ ${NavBar} > ${NavItem}[href="#services"] {
87 | ${commonHoverStyle};
88 | }
89 | `;
90 |
91 | const Portfolio = styled.section`
92 | ${commonSectionStyle};
93 |
94 | &:hover ~ ${NavBar} > ${NavItem}[href="#portfolio"] {
95 | ${commonHoverStyle};
96 | }
97 | `;
98 |
99 | const Contact = styled.section`
100 | ${commonSectionStyle};
101 |
102 | &:hover ~ ${NavBar} > ${NavItem}[href="#contact"] {
103 | ${commonHoverStyle};
104 | }
105 | `;
106 |
107 | const Container = styled.div`
108 | & > section:nth-child(even) {
109 | background: gray;
110 | }
111 | `;
112 |
--------------------------------------------------------------------------------
/src/stories/GlowingCheckbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, useCallback, useState } from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import { GlowingCheckbox, GlowingRadio } from '../components';
4 | import styled, { keyframes } from 'styled-components';
5 |
6 | export default {
7 | title: 'jacob-css/GlowingCheckbox',
8 | component: GlowingCheckbox,
9 | decorators: [
10 | (Story) => {
11 | const style = {
12 | display: 'flex',
13 | flexDirection: 'column',
14 | justifyContent: 'center',
15 | alignItems: 'center',
16 | height: '100vh',
17 | overflow: 'auto',
18 | boxSizing: 'border-box',
19 | } as CSSProperties;
20 |
21 | return (
22 |
23 |
24 |
25 | );
26 | },
27 | ],
28 | parameters: {
29 | docs: {
30 | description: {
31 | component:
32 | '(checkbox/radio)타입의 input과 스타일을 조작하는 기초를 제공합니다.',
33 | },
34 | },
35 | },
36 | } as Meta;
37 |
38 | const options = [
39 | { value: 'Jacob ...', name: 'test' },
40 | { value: 'Jacob is good!', name: 'test' },
41 | ];
42 |
43 | const Template: Story = () => {
44 | const [radioValue, setRadioValue] = useState('Jacob ...');
45 |
46 | const handleRadioChange = useCallback(
47 | (event: React.ChangeEvent) => {
48 | setRadioValue(event.target.value);
49 | },
50 | []
51 | );
52 |
53 | return (
54 | <>
55 | checkbox
56 |
57 |
58 |
63 | {radioValue}
64 | >
65 | );
66 | };
67 |
68 | const Title = styled.h2`
69 | color: white;
70 | font-weight: 600;
71 | font-size: 24px;
72 | text-transform: uppercase;
73 | `;
74 |
75 | const Divider = styled.hr`
76 | margin: 28px 0 16px;
77 | width: 32%;
78 | background: white;
79 | `;
80 |
81 | const animation = keyframes`
82 | 0% {
83 | transform: scaleX(0);
84 | }
85 | 100% {
86 | transform: scaleX(1);
87 | }
88 | `;
89 | const RadioValue = styled.span`
90 | color: whitesmoke;
91 | position: relative;
92 | margin-top: 8px;
93 |
94 | &::before {
95 | content: '';
96 | position: absolute;
97 | height: 2px;
98 | bottom: 0;
99 | left: 0;
100 | background: cyan;
101 | width: 100%;
102 | transform: scaleX(0);
103 | transform-origin: bottom left;
104 | animation: ${animation} 0.5s ease-out infinite;
105 | }
106 | `;
107 |
108 | export const BasicCss = Template.bind({});
109 |
--------------------------------------------------------------------------------
/src/components/WebGlAndThreeJS/BasicScene3.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useMemo, useRef } from 'react';
2 | import styled from 'styled-components';
3 | import { BoxGeometry, DirectionalLight, PerspectiveCamera, Scene, WebGLRenderer } from 'three';
4 | import { DomUtils, RefUtils } from '../../utils';
5 | import { WebGLUtils } from '../../utils/WebGLUtils';
6 |
7 | /**
8 | * 반응형 디자인 적용
9 | */
10 |
11 | export const BasicScene3 = memo(() => {
12 | const ref = useRef(null);
13 | const scene = useMemo(() => {
14 | return new Scene();
15 | }, []);
16 |
17 | const cubes = useMemo(() => {
18 | const boxWidth = 1;
19 | const boxHeight = 1;
20 | const boxDepth = 1;
21 | const geometry = new BoxGeometry(boxWidth, boxHeight, boxDepth);
22 |
23 | const instansA = WebGLUtils.makeInstance(scene, geometry, 0x44aa88, 0);
24 | const instansB = WebGLUtils.makeInstance(scene, geometry, 0x8844aa, -2);
25 | const instansC = WebGLUtils.makeInstance(scene, geometry, 0xaa8844, 2);
26 |
27 | return [instansA.cube, instansB.cube, instansC.cube];
28 | }, [scene]);
29 |
30 | useEffect(() => {
31 | if (!RefUtils.notNull(ref) || !DomUtils.usableWindow()) {
32 | return;
33 | }
34 |
35 | const canvas = ref.current!;
36 | const renderer = new WebGLRenderer({ canvas });
37 |
38 | const fov = 75;
39 | const aspect = 2; // the canvas default
40 | const near = 0.1;
41 | const far = 5;
42 | const camera = new PerspectiveCamera(fov, aspect, near, far);
43 | camera.position.z = 3;
44 |
45 | // 광원 추가
46 | const color = 0xffffff;
47 | const intensity = 1;
48 | const light = new DirectionalLight(color, intensity);
49 | light.position.set(-1, 2, 4);
50 | scene.add(light);
51 |
52 | const animate = (time: number) => {
53 | time *= 0.001; // convert time to seconds
54 |
55 | if (WebGLUtils.resizeRendererToDisplaySize(renderer)) {
56 | const canvas = renderer.domElement;
57 | camera.aspect = canvas.clientWidth / canvas.clientHeight;
58 | camera.updateProjectionMatrix();
59 | }
60 |
61 | cubes.forEach((cube, ndx) => {
62 | const speed = 1 + ndx * 0.1;
63 | const rot = time * speed;
64 | cube.rotation.x = rot;
65 | cube.rotation.y = rot;
66 | });
67 | renderer.render(scene, camera);
68 | requestAnimationFrame(animate);
69 | };
70 |
71 | if (WebGLUtils.isWebGLAvailable()) {
72 | requestAnimationFrame(animate);
73 | } else {
74 | const warning = WebGLUtils.getWebGLErrorMessage();
75 | ref.current!.appendChild(warning);
76 | }
77 | }, [cubes, scene]);
78 |
79 | return ;
80 | });
81 |
82 | const Canvas = styled.canvas`
83 | display: block;
84 | width: 200px;
85 | height: 120px;
86 | float: left;
87 | `;
88 |
--------------------------------------------------------------------------------
/src/components/GlowingRadio/GlowingRadio.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import styled from 'styled-components';
3 |
4 | export interface GlowingRadioProps {
5 | className?: string;
6 | onChange?: (event: React.ChangeEvent) => void;
7 | options: ReadonlyArray<{ label?: string; value: string; name: string }>;
8 | value?: string;
9 | }
10 |
11 | export const GlowingRadio = memo(
12 | ({ className, onChange, options, value }) => {
13 | return (
14 |
15 | radio
16 | {options.map((option) => (
17 |
18 |
25 | {option.label}
26 |
27 | ))}
28 |
29 | );
30 | },
31 | );
32 |
33 | const Fieldset = styled.fieldset`
34 | display: flex;
35 | flex-direction: column;
36 | align-items: center;
37 | border-radius: 4px;
38 | text-transform: uppercase;
39 | `;
40 |
41 | const Legend = styled.legend`
42 | color: white;
43 | padding: 16px 8px;
44 | font-weight: 600;
45 | font-size: 24px;
46 | margin: 0 auto;
47 | `;
48 |
49 | const Radio = styled.input`
50 | position: relative;
51 | width: 120px;
52 | height: 40px;
53 | margin: 10px;
54 | outline: none;
55 | background: black;
56 | cursor: pointer;
57 | border-radius: 20px;
58 | appearance: none;
59 | box-shadow: -5px -5px 20px rgba(255, 255, 255, 0.1),
60 | 5px 5px 20px rgba(0, 0, 0, 0.1),
61 | inset -2px -2px 5px rgba(255, 255, 255, 0.1),
62 | inset 2px 2px 5px rgba(0, 0, 0, 0.5), 0 0 0 2px #3e3e3e;
63 | transition: 0.5s;
64 |
65 | &:checked {
66 | background: cyan;
67 | }
68 |
69 | &::before {
70 | content: '';
71 | position: absolute;
72 | top: 0;
73 | left: 0;
74 | width: 80px;
75 | height: 40px;
76 | background: linear-gradient(to top, black, gray);
77 | border-radius: 20px;
78 | box-shadow: 0 0 0 1px dimgray;
79 | transform: scale(0.98, 0.96);
80 | transition: 0.5s;
81 | }
82 |
83 | &:checked::before {
84 | left: 40px;
85 | }
86 |
87 | &::after {
88 | content: '';
89 | position: absolute;
90 | top: calc(50% - 2px);
91 | left: 65px;
92 | width: 4px;
93 | height: 4px;
94 | background: gray;
95 | border-radius: 50%;
96 | transition: 0.5s;
97 | }
98 |
99 | &:checked::after {
100 | left: calc(65px + 40px);
101 | background: cyan;
102 | box-shadow: 0 0 5px cyan, 0 0 15px cyan, 0 0 30px cyan;
103 | }
104 | `;
105 |
--------------------------------------------------------------------------------
/src/components/NeonButton/NeonButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, ReactText } from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | export interface NeonButtonProps {
5 | label?: ReactText;
6 | }
7 |
8 | export const NeonButton = memo(({ label }) => {
9 | return (
10 |
11 | {label}
12 |
13 |
14 |
15 |
16 |
17 | );
18 | });
19 |
20 | const NeonButtonContainer = styled.a`
21 | position: relative;
22 | display: inline-block;
23 | color: cyan;
24 | padding: 20px 24px;
25 | font-size: 24px;
26 | text-decoration: none;
27 | text-transform: uppercase;
28 | transition: 0.5s;
29 | letter-spacing: 0.5px;
30 | overflow: hidden;
31 | -webkit-box-reflect: below 1px linear-gradient(transparent, #0004);
32 |
33 | &:hover {
34 | background-color: cyan;
35 | color: black;
36 | box-shadow: 0 0 5px cyan, 0 0 25px cyan, 0 0 50px cyan, 0 0 100px cyan;
37 | }
38 | `;
39 |
40 | const topAnimate = keyframes`
41 | 0% {
42 | left: -100%;
43 | }
44 | 50%, 100% {
45 | left: 100%;
46 | }
47 | `;
48 |
49 | const rightAnimate = keyframes`
50 | 0% {
51 | top: -100%;
52 | }
53 | 50%, 100% {
54 | top: 100%;
55 | }
56 | `;
57 |
58 | const bottomAnimate = keyframes`
59 | 0% {
60 | right: -100%;
61 | }
62 | 50%, 100% {
63 | right: 100%;
64 | }
65 | `;
66 |
67 | const leftAnimate = keyframes`
68 | 0% {
69 | bottom: -100%;
70 | }
71 | 50%, 100% {
72 | bottom: 100%;
73 | }
74 | `;
75 |
76 | const Border = styled.span`
77 | position: absolute;
78 | display: block;
79 |
80 | &:nth-child(1) {
81 | top: 0;
82 | left: -100%;
83 | width: 100%;
84 | height: 2px;
85 | background: linear-gradient(90deg, transparent, cyan);
86 | animation: ${topAnimate} 1s linear infinite;
87 | }
88 |
89 | &:nth-child(2) {
90 | top: -100%;
91 | right: 0;
92 | width: 2px;
93 | height: 100%;
94 | background: linear-gradient(180deg, transparent, cyan);
95 | animation: ${rightAnimate} 1s linear infinite;
96 | animation-delay: 0.25s;
97 | }
98 |
99 | &:nth-child(3) {
100 | right: -100%;
101 | bottom: 0;
102 | width: 100%;
103 | height: 2px;
104 | background: linear-gradient(270deg, transparent, cyan);
105 | animation: ${bottomAnimate} 1s linear infinite;
106 | animation-delay: 0.5s;
107 | }
108 |
109 | &:nth-child(4) {
110 | bottom: -100%;
111 | left: 0;
112 | width: 2px;
113 | height: 100%;
114 | background: linear-gradient(360deg, transparent, cyan);
115 | animation: ${leftAnimate} 1s linear infinite;
116 | animation-delay: 0.75s;
117 | }
118 | `;
119 |
--------------------------------------------------------------------------------
/src/components/Canvas/RotatingPolygon/RotatingPolygon.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2 | import useScreenSize from '../../../hooks/useScreenSize';
3 | import { DomUtils } from '../../../utils';
4 | import { Polygon } from './Polygon';
5 |
6 | export const RotatingPolygon = memo(() => {
7 | const { stageWidth, stageHeight } = useScreenSize();
8 | const ref = useRef(null);
9 | const [pointer, setPointer] = useState({ isDown: false, moveX: 0, offsetX: 0 });
10 |
11 | const polygon = useMemo(() => {
12 | return new Polygon(stageWidth / 2, stageHeight + stageHeight / 4, stageHeight / 1.5, 15);
13 | }, [stageWidth, stageHeight]);
14 |
15 | const handlePointerDown = useCallback((event: React.PointerEvent) => {
16 | setPointer((prev) => ({ ...prev, isDown: true, moveX: 0, offsetX: event.clientX }));
17 | }, []);
18 |
19 | const handlePointerMove = useCallback(
20 | (event: React.PointerEvent) => {
21 | if (pointer.isDown) {
22 | setPointer((prev) => ({ ...prev, moveX: event.clientX - prev.offsetX, offsetX: event.clientX }));
23 | }
24 | },
25 | [pointer]
26 | );
27 |
28 | const handlePointerUp = useCallback((event: React.PointerEvent) => {
29 | setPointer((prev) => ({ ...prev, isDown: false }));
30 | }, []);
31 |
32 | const resize = useCallback(() => {
33 | if (ref.current === null) return;
34 |
35 | const ctx = ref.current.getContext('2d');
36 | const pixelRatio = window.devicePixelRatio > 1 ? 2 : 1;
37 |
38 | ref.current.width = stageWidth * pixelRatio;
39 | ref.current.height = stageHeight * pixelRatio;
40 |
41 | ctx?.scale(pixelRatio, pixelRatio);
42 | }, [stageWidth, stageHeight]);
43 |
44 | const animate = useCallback(
45 | (t: number) => {
46 | if (ref.current === null) return;
47 |
48 | const ctx = ref.current.getContext('2d');
49 | ctx?.clearRect(0, 0, stageWidth, stageHeight);
50 |
51 | setPointer((prev) => ({ ...prev, moveX: (prev.moveX *= 0.92) }));
52 |
53 | polygon.animate(ctx, pointer.moveX);
54 | },
55 | [stageWidth, stageHeight, polygon, pointer.moveX]
56 | );
57 |
58 | useEffect(() => {
59 | if (!DomUtils.usableWindow()) return;
60 |
61 | window.addEventListener('resize', resize, false);
62 | resize();
63 |
64 | return () => window.removeEventListener('resize', resize, false);
65 | }, [resize]);
66 |
67 | useEffect(() => {
68 | if (!DomUtils.usableWindow()) return;
69 |
70 | const num = window.requestAnimationFrame(animate);
71 |
72 | return () => window.cancelAnimationFrame(num);
73 | }, [animate]);
74 |
75 | return (
76 |
82 | );
83 | });
84 |
--------------------------------------------------------------------------------
/src/components/ThreeDimensionBackground/ThreeDimensionBackground.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | export const ThreeDimensionBackground = memo(() => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | });
44 |
45 | const Background = styled.div`
46 | position: fixed;
47 | width: 100%;
48 | height: 100%;
49 | overflow: hidden;
50 | `;
51 |
52 | const animation = keyframes`
53 | 0% {
54 | transform: rotateX(0deg) rotateY(0deg);
55 | }
56 | 100% {
57 | transform: rotateX(360deg) rotateY(360deg);
58 | }
59 | `;
60 |
61 | const Rotate = styled.div`
62 | position: absolute;
63 | top: calc(50% - 200px);
64 | left: calc(50% - 200px);
65 | width: 400px;
66 | height: 400px;
67 | transform-style: preserve-3d;
68 | animation: ${animation} 20s linear infinite;
69 | zoom: 5;
70 | `;
71 |
72 | const Sphere = styled.div`
73 | position: absolute;
74 | top: 0;
75 | left: 0;
76 | width: 100%;
77 | height: 100%;
78 | transform-style: preserve-3d;
79 |
80 | &:nth-child(2) {
81 | transform: rotate(90deg);
82 | }
83 | &:nth-child(3) {
84 | transform: rotate(45deg);
85 | }
86 | &:nth-child(4) {
87 | transform: rotate(-45deg);
88 | }
89 | `;
90 |
91 | const SphereItem = styled.span`
92 | position: absolute;
93 | top: 0;
94 | left: 0;
95 | width: 100%;
96 | height: 100%;
97 | transform-style: preserve-3d;
98 | background: radial-gradient(lightgray, white);
99 | border-radius: 50%;
100 |
101 | &:nth-child(1) {
102 | transform: rotateY(0deg);
103 | }
104 | &:nth-child(2) {
105 | transform: rotateY(30deg);
106 | }
107 | &:nth-child(3) {
108 | transform: rotateY(60deg);
109 | }
110 | &:nth-child(4) {
111 | transform: rotateY(90deg);
112 | }
113 | &:nth-child(5) {
114 | transform: rotateY(120deg);
115 | }
116 | &:nth-child(6) {
117 | transform: rotateY(150deg);
118 | }
119 | `;
120 |
--------------------------------------------------------------------------------
/src/utils/WebGLUtils.ts:
--------------------------------------------------------------------------------
1 | import { ReactText } from 'react';
2 | import { BoxGeometry, Mesh, MeshPhongMaterial, Renderer, Scene } from 'three';
3 |
4 | // https://github.com/mrdoob/three.js/blob/master/examples/jsm/WebGL.js
5 | export class WebGLUtils {
6 | static isWebGLAvailable() {
7 | try {
8 | const canvas = document.createElement('canvas');
9 | return !!(
10 | window.WebGLRenderingContext &&
11 | (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
12 | );
13 | } catch (e) {
14 | return false;
15 | }
16 | }
17 |
18 | static isWebGL2Available() {
19 | try {
20 | const canvas = document.createElement('canvas');
21 | return !!(window.WebGL2RenderingContext && canvas.getContext('webgl2'));
22 | } catch (e) {
23 | return false;
24 | }
25 | }
26 |
27 | static getWebGLErrorMessage() {
28 | return this.getErrorMessage(1);
29 | }
30 |
31 | static getWebGL2ErrorMessage() {
32 | return this.getErrorMessage(2);
33 | }
34 |
35 | static getErrorMessage(version: number) {
36 | const names: { [key in number]: any } = {
37 | 1: 'WebGL',
38 | 2: 'WebGL 2',
39 | };
40 |
41 | const contexts: { [key in number]: any } = {
42 | 1: window.WebGLRenderingContext,
43 | 2: window.WebGL2RenderingContext,
44 | };
45 |
46 | let message =
47 | 'Your $0 does not seem to support $1 ';
48 |
49 | const element = document.createElement('div');
50 | element.id = 'webglmessage';
51 | element.style.fontFamily = 'monospace';
52 | element.style.fontSize = '13px';
53 | element.style.fontWeight = 'normal';
54 | element.style.textAlign = 'center';
55 | element.style.background = '#fff';
56 | element.style.color = '#000';
57 | element.style.padding = '1.5em';
58 | element.style.width = '400px';
59 | element.style.margin = '5em auto 0';
60 |
61 | if (contexts[version]) {
62 | message = message.replace('$0', 'graphics card');
63 | } else {
64 | message = message.replace('$0', 'browser');
65 | }
66 |
67 | message = message.replace('$1', names[version]);
68 | element.innerHTML = message;
69 |
70 | return element;
71 | }
72 |
73 | static makeInstance(scene: Scene, geometry: BoxGeometry, color: ReactText, x: number) {
74 | const material = new MeshPhongMaterial({ color });
75 | const cube = new Mesh(geometry, material);
76 | scene.add(cube);
77 |
78 | cube.position.x = x;
79 |
80 | return { scene, cube };
81 | }
82 |
83 | static resizeRendererToDisplaySize(renderer: Renderer) {
84 | const canvas = renderer.domElement;
85 | const width = canvas.clientWidth;
86 | const height = canvas.clientHeight;
87 | const needResize = canvas.width !== width || canvas.height !== height;
88 | if (needResize) {
89 | renderer.setSize(width, height, false);
90 | }
91 | return needResize;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/Canvas/MovingBox/Dialog.ts:
--------------------------------------------------------------------------------
1 | import { Point } from './Point';
2 |
3 | const FOLLOW_SPEED = 0.08;
4 | const ROTATE_SPEED = 0.12;
5 | const MAX_ANGLE = 30;
6 | const FPS = 1000 / 60;
7 | const WIDTH = 260;
8 | const HEIGHT = 260;
9 |
10 | export class Dialog {
11 | pos: Point;
12 | target: Point;
13 | prevPos: Point;
14 | downPos: Point;
15 | startPos: Point;
16 | mousePos: Point;
17 | centerPos: Point;
18 | origin: Point;
19 | rotation: number;
20 | sideValue: number;
21 | isDown: boolean;
22 |
23 | constructor() {
24 | this.pos = new Point();
25 | this.target = new Point();
26 | this.prevPos = new Point();
27 | this.downPos = new Point();
28 | this.startPos = new Point();
29 | this.mousePos = new Point();
30 | this.centerPos = new Point();
31 | this.origin = new Point();
32 | this.rotation = 0;
33 | this.sideValue = 0;
34 | this.isDown = false;
35 | }
36 |
37 | resize(stageWidth: number, stageHeight: number) {
38 | this.pos.x = Math.random() * (stageWidth - WIDTH);
39 | this.pos.y = Math.random() * (stageHeight - HEIGHT);
40 | this.target = this.pos.clone();
41 | this.prevPos = this.pos.clone();
42 | }
43 |
44 | animate(ctx: CanvasRenderingContext2D | null) {
45 | if (ctx === null) return;
46 |
47 | const move = this.target.clone().subtract(this.pos).reduce(FOLLOW_SPEED);
48 | this.pos.add(move);
49 |
50 | this.centerPos = this.pos.clone().add(this.mousePos);
51 |
52 | this.swingDrag(ctx);
53 |
54 | this.prevPos = this.pos.clone();
55 | }
56 |
57 | swingDrag(ctx: CanvasRenderingContext2D | null) {
58 | if (ctx === null) return;
59 |
60 | const dx = this.pos.x - this.prevPos.x;
61 | const speedX = Math.abs(dx) / FPS;
62 | const speed = Math.min(Math.max(speedX, 0), 1);
63 |
64 | let rotation = (MAX_ANGLE / 1) * speed;
65 | rotation = rotation * (dx > 0 ? 1 : -1) - this.sideValue;
66 |
67 | this.rotation += (rotation - this.rotation) * ROTATE_SPEED;
68 |
69 | const temPos = this.pos.clone().add(this.origin);
70 | ctx.save();
71 | ctx.translate(temPos.x, temPos.y);
72 | ctx.rotate((this.rotation * Math.PI) / 180);
73 | ctx.beginPath();
74 | ctx.fillStyle = '#f4e55a';
75 | ctx.fillRect(-this.origin.x, -this.origin.y, WIDTH, HEIGHT);
76 | ctx.restore();
77 | }
78 |
79 | down(point: Point) {
80 | if (point.collide(this.pos, WIDTH, HEIGHT)) {
81 | this.isDown = true;
82 | this.startPos = this.pos.clone();
83 | this.downPos = point.clone();
84 | this.mousePos = point.clone().subtract(this.pos);
85 |
86 | const xRatioValue = this.mousePos.x / WIDTH;
87 | this.origin.x = WIDTH * xRatioValue;
88 | this.origin.y = (HEIGHT * this.mousePos.y) / HEIGHT;
89 |
90 | this.sideValue = xRatioValue - 0.5;
91 |
92 | return this;
93 | }
94 | return null;
95 | }
96 |
97 | move(point: Point) {
98 | if (this.isDown) {
99 | this.target = this.startPos.clone().add(point).subtract(this.downPos);
100 | }
101 | }
102 |
103 | up() {
104 | this.isDown = false;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------