2D Run-and-gun shooter inspired by One Man's Doomsday, Counter-Strike, and Metal Slug.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

645 lines
20 KiB

  1. import {
  2. ChangeEventHandler, FocusEventHandler,
  3. FormEventHandler,
  4. MouseEventHandler,
  5. useEffect,
  6. useId,
  7. useRef,
  8. useState,
  9. } from 'react';
  10. import { getFormValues, setFormValues } from '@theoryofnekomata/formxtra';
  11. import { Button } from '~/components/Button';
  12. import { FileButton } from '~/components/FileButton';
  13. import * as FileModule from '~/modules/file';
  14. import {ComboBox} from '~/components/ComboBox';
  15. type SvgGroup = {
  16. id: string;
  17. name: string;
  18. }
  19. type KeyframeAttribute = {
  20. group: SvgGroup;
  21. translateX: number;
  22. translateY: number;
  23. rotate: number;
  24. }
  25. type Keyframe = {
  26. time: number;
  27. attributes: KeyframeAttribute[];
  28. }
  29. const useAnimationForm = () => {
  30. const [loadedFileName, setLoadedFileName] = useState<string>();
  31. const [loadedFileContents, setLoadedFileContents] = useState<string>();
  32. const [svgAnimations, setSvgAnimations] = useState<string[]>();
  33. const [svgGroups, setSvgGroups] = useState<SvgGroup[]>();
  34. const [isNewAnimationId, setIsNewAnimationId] = useState<boolean>();
  35. const [currentAnimationId, setCurrentAnimationId] = useState<string>();
  36. const [enabledGroupIds, setEnabledGroupIds] = useState<string[]>();
  37. const [savedSvgKeyframes, setSavedSvgKeyframes] = useState<Keyframe[]>();
  38. const svgAnimationIdsListId = useId();
  39. const svgPreviewRef = useRef<HTMLDivElement>(null);
  40. const loadSvgFile: ChangeEventHandler<HTMLInputElement> = async (e) => {
  41. const filePicker = e.currentTarget;
  42. if (!filePicker.files) {
  43. return;
  44. }
  45. const [file] = Array.from(filePicker.files);
  46. if (!file) {
  47. return;
  48. }
  49. setLoadedFileName(file.name);
  50. setIsNewAnimationId(false);
  51. const result = await FileModule.readFile(file);
  52. setLoadedFileContents(result);
  53. filePicker.value = ''
  54. }
  55. const selectNewAnimationId: FormEventHandler<HTMLInputElement> = (e) => {
  56. const target = e.currentTarget;
  57. setIsNewAnimationId(false);
  58. setCurrentAnimationId(e.currentTarget.value);
  59. setTimeout(() => {
  60. target.blur();
  61. });
  62. }
  63. const proposeNewAnimationId: FormEventHandler<HTMLInputElement> = (e) => {
  64. setIsNewAnimationId(true);
  65. }
  66. const cancelAnimationIdChange: MouseEventHandler<HTMLButtonElement> = (e) => {
  67. e.preventDefault();
  68. setIsNewAnimationId(false);
  69. setFormValues(e.currentTarget.form!, {
  70. animationId: currentAnimationId,
  71. })
  72. }
  73. useEffect(() => {
  74. if (!loadedFileContents) {
  75. return
  76. }
  77. if (!svgPreviewRef.current) {
  78. return
  79. }
  80. const svgPreviewRoot = svgPreviewRef.current.children[0];
  81. setSvgAnimations(['default']);
  82. setSvgGroups(
  83. Array
  84. .from(svgPreviewRoot.children)
  85. .map((g) => ({
  86. id: g.id,
  87. name: g.id,
  88. }))
  89. );
  90. setCurrentAnimationId('default');
  91. setEnabledGroupIds([]); // TODO - compute enabled groupIds based on current animation id
  92. setSavedSvgKeyframes([]); // TODO - load keyframes from file
  93. }, [loadedFileContents, svgPreviewRef.current]);
  94. const focusAnimationIdInput: FocusEventHandler<HTMLInputElement> = (e) => {
  95. e.currentTarget.value = '';
  96. }
  97. const blurAnimationIdInput: FocusEventHandler<HTMLInputElement> = (e) => {
  98. e.currentTarget.value = currentAnimationId as string;
  99. setIsNewAnimationId(false);
  100. }
  101. const addKeyframeAttribute: ChangeEventHandler<HTMLInputElement> = (e) => {
  102. const { currentTarget } = e;
  103. if (currentTarget.checked) {
  104. setEnabledGroupIds((oldEnabledGroupIds = []) => [...oldEnabledGroupIds, currentTarget.value]);
  105. return
  106. }
  107. setEnabledGroupIds((oldEnabledGroupIds = []) => oldEnabledGroupIds.filter((i) => i !== currentTarget.value));
  108. }
  109. const translateXGroup = (group: SvgGroup): ChangeEventHandler<HTMLInputElement> => (e) => {
  110. const { current: svgPreviewWrapper } = svgPreviewRef;
  111. if (!svgPreviewWrapper) {
  112. return;
  113. }
  114. const svgPreviewRoot = svgPreviewWrapper.children[0] as SVGSVGElement | null;
  115. if (!svgPreviewRoot) {
  116. return;
  117. }
  118. const targetGroup = Array.from(svgPreviewRoot.children).find(g => g.id === group.id) as SVGGraphicsElement;
  119. if (!targetGroup) {
  120. return;
  121. }
  122. const rotationCenterReference = Array.from(targetGroup.children).find(e => e.tagName === 'circle') as SVGCircleElement | undefined
  123. const rotationCenter = {
  124. cx: rotationCenterReference?.getAttribute?.('cx') ?? 0,
  125. cy: rotationCenterReference?.getAttribute?.('cy') ?? 0,
  126. };
  127. const values = getFormValues(e.currentTarget.form!, { forceNumberValues: true });
  128. const changed = Array.isArray(values.changed) ? values.changed : [values.changed];
  129. const changedIndex = changed.indexOf(group.id);
  130. const rotate = Array.isArray(values.rotate) ? values.rotate : [values.rotate];
  131. const translateY = Array.isArray(values.translateY) ? values.translateY : [values.translateY];
  132. const currentRotate = rotate[changedIndex];
  133. const currentTranslateX = e.currentTarget.value;
  134. const currentTranslateY = translateY[changedIndex];
  135. targetGroup.setAttribute('transform', `translate(${currentTranslateX} ${currentTranslateY}) rotate(${currentRotate} ${rotationCenter.cx} ${rotationCenter.cy})`);
  136. }
  137. const translateYGroup = (group: SvgGroup): ChangeEventHandler<HTMLInputElement> => (e) => {
  138. const { current: svgPreviewWrapper } = svgPreviewRef;
  139. if (!svgPreviewWrapper) {
  140. return;
  141. }
  142. const svgPreviewRoot = svgPreviewWrapper.children[0] as SVGSVGElement | null;
  143. if (!svgPreviewRoot) {
  144. return;
  145. }
  146. const targetGroup = Array.from(svgPreviewRoot.children).find(g => g.id === group.id) as SVGGraphicsElement;
  147. if (!targetGroup) {
  148. return;
  149. }
  150. const rotationCenterReference = Array.from(targetGroup.children).find(e => e.tagName === 'circle') as SVGCircleElement | undefined
  151. const rotationCenter = {
  152. cx: rotationCenterReference?.getAttribute?.('cx') ?? 0,
  153. cy: rotationCenterReference?.getAttribute?.('cy') ?? 0,
  154. };
  155. const values = getFormValues(e.currentTarget.form!, { forceNumberValues: true });
  156. const changed = Array.isArray(values.changed) ? values.changed : [values.changed];
  157. const changedIndex = changed.indexOf(group.id);
  158. const rotate = Array.isArray(values.rotate) ? values.rotate : [values.rotate];
  159. const translateX = Array.isArray(values.translateX) ? values.translateX : [values.translateX];
  160. const currentRotate = rotate[changedIndex];
  161. const currentTranslateX = translateX[changedIndex];
  162. const currentTranslateY = e.currentTarget.value;
  163. targetGroup.setAttribute('transform', `translate(${currentTranslateX} ${currentTranslateY}) rotate(${currentRotate} ${rotationCenter.cx} ${rotationCenter.cy})`);
  164. }
  165. const rotateGroup = (group: SvgGroup): ChangeEventHandler<HTMLInputElement> => (e) => {
  166. const { current: svgPreviewWrapper } = svgPreviewRef;
  167. if (!svgPreviewWrapper) {
  168. return;
  169. }
  170. const svgPreviewRoot = svgPreviewWrapper.children[0] as SVGSVGElement | null;
  171. if (!svgPreviewRoot) {
  172. return;
  173. }
  174. const targetGroup = Array.from(svgPreviewRoot.children).find(g => g.id === group.id) as SVGGraphicsElement;
  175. if (!targetGroup) {
  176. return;
  177. }
  178. const rotationCenterReference = Array.from(targetGroup.children).find(e => e.tagName === 'circle') as SVGCircleElement | undefined
  179. const rotationCenter = {
  180. cx: rotationCenterReference?.getAttribute?.('cx') ?? 0,
  181. cy: rotationCenterReference?.getAttribute?.('cy') ?? 0,
  182. };
  183. const values = getFormValues(e.currentTarget.form!, { forceNumberValues: true });
  184. setFormValues(e.currentTarget.form!, { rotateNumber: values.rotate });
  185. const changed = Array.isArray(values.changed) ? values.changed : [values.changed];
  186. const changedIndex = changed.indexOf(group.id);
  187. const translateX = Array.isArray(values.translateX) ? values.translateX : [values.translateX];
  188. const translateY = Array.isArray(values.translateY) ? values.translateY : [values.translateY];
  189. const currentRotate = e.currentTarget.value;
  190. const currentTranslateX = translateX[changedIndex];
  191. const currentTranslateY = translateY[changedIndex];
  192. targetGroup.setAttribute('transform', `translate(${currentTranslateX} ${currentTranslateY}) rotate(${currentRotate} ${rotationCenter.cx} ${rotationCenter.cy})`);
  193. }
  194. const rotateGroupThroughNumber = (group: SvgGroup): ChangeEventHandler<HTMLInputElement> => (e) => {
  195. const { current: svgPreviewWrapper } = svgPreviewRef;
  196. if (!svgPreviewWrapper) {
  197. return;
  198. }
  199. const svgPreviewRoot = svgPreviewWrapper.children[0] as SVGSVGElement | null;
  200. if (!svgPreviewRoot) {
  201. return;
  202. }
  203. const targetGroup = Array.from(svgPreviewRoot.children).find(g => g.id === group.id) as SVGGraphicsElement;
  204. if (!targetGroup) {
  205. return;
  206. }
  207. const rotationCenterReference = Array.from(targetGroup.children).find(e => e.tagName === 'circle') as SVGCircleElement | undefined
  208. const rotationCenter = {
  209. cx: rotationCenterReference?.getAttribute?.('cx') ?? 0,
  210. cy: rotationCenterReference?.getAttribute?.('cy') ?? 0,
  211. };
  212. const values = getFormValues(e.currentTarget.form!, { forceNumberValues: true });
  213. setFormValues(e.currentTarget.form!, { rotate: values.rotateNumber });
  214. const changed = Array.isArray(values.changed) ? values.changed : [values.changed];
  215. const changedIndex = changed.indexOf(group.id);
  216. const translateX = Array.isArray(values.translateX) ? values.translateX : [values.translateX];
  217. const translateY = Array.isArray(values.translateY) ? values.translateY : [values.translateY];
  218. const currentRotate = e.currentTarget.value;
  219. const currentTranslateX = translateX[changedIndex];
  220. const currentTranslateY = translateY[changedIndex];
  221. targetGroup.setAttribute('transform', `translate(${currentTranslateX} ${currentTranslateY}) rotate(${currentRotate} ${rotationCenter.cx} ${rotationCenter.cy})`);
  222. }
  223. const handleFormSubmit: FormEventHandler<HTMLFormElement> = (e) => {
  224. e.preventDefault();
  225. const { submitter } = e.nativeEvent as unknown as { submitter: HTMLButtonElement };
  226. const values = getFormValues(e.currentTarget, { submitter, forceNumberValues: true });
  227. const changed = Array.isArray(values.changed) ? values.changed : [values.changed];
  228. const translateX = Array.isArray(values.translateX) ? values.translateX : [values.translateX];
  229. const translateY = Array.isArray(values.translateY) ? values.translateY : [values.translateY];
  230. const rotate = Array.isArray(values.rotate) ? values.rotate : [values.rotate];
  231. switch (values.action) {
  232. case 'updateTimeKeyframes':
  233. if ('changed' in values) {
  234. // TODO handle keyframes on different animations
  235. // TODO update SVG preview when updating current time
  236. setSavedSvgKeyframes((oldSavedSvgKeyframes = []) => {
  237. const newKeyframe = {
  238. time: values.time as number,
  239. attributes: changed.map((attribute, i) => ({
  240. group: {
  241. id: attribute,
  242. name: attribute,
  243. },
  244. translateX: translateX[i],
  245. translateY: translateY[i],
  246. rotate: rotate[i],
  247. })),
  248. };
  249. if (oldSavedSvgKeyframes.some((k) => k.time === values.time as number)) {
  250. return oldSavedSvgKeyframes.map((k) => k.time === values.time ? newKeyframe : k);
  251. }
  252. return [
  253. ...oldSavedSvgKeyframes,
  254. newKeyframe,
  255. ]
  256. })
  257. return;
  258. }
  259. setSavedSvgKeyframes((oldSavedSvgKeyframes = []) => oldSavedSvgKeyframes.filter(k => k.time !== values.time as number));
  260. return;
  261. default:
  262. break;
  263. }
  264. console.log(values);
  265. }
  266. const updateTime: ChangeEventHandler<HTMLInputElement> = (e) => {
  267. const value = e.currentTarget.valueAsNumber;
  268. const form = e.currentTarget.form as HTMLFormElement
  269. const keyframe = savedSvgKeyframes?.find((k) => k.time === value);
  270. if (!keyframe) {
  271. setEnabledGroupIds([]);
  272. setFormValues(form, {
  273. changed: svgGroups?.map(() => false),
  274. translateX: svgGroups?.map(() => 0),
  275. translateY: svgGroups?.map(() => 0),
  276. rotate: svgGroups?.map(() => 0),
  277. rotateNumber: svgGroups?.map(() => 0),
  278. })
  279. return;
  280. }
  281. const changed = keyframe.attributes.map((a) => a.group.id);
  282. setEnabledGroupIds(changed);
  283. setTimeout(() => {
  284. setFormValues(form, {
  285. changed: changed,
  286. translateX: keyframe.attributes.map((a) => a.translateX),
  287. translateY: keyframe.attributes.map((a) => a.translateY),
  288. rotate: keyframe.attributes.map((a) => a.rotate),
  289. rotateNumber: keyframe.attributes.map((a) => a.rotate),
  290. });
  291. });
  292. }
  293. return {
  294. isNewAnimationId,
  295. svgAnimations,
  296. loadedFileName,
  297. loadedFileContents,
  298. loadSvgFile,
  299. svgPreviewRef,
  300. svgGroups,
  301. svgAnimationIdsListId,
  302. selectNewAnimationId,
  303. proposeNewAnimationId,
  304. cancelAnimationIdChange,
  305. focusAnimationIdInput,
  306. blurAnimationIdInput,
  307. addKeyframeAttribute,
  308. enabledGroupIds,
  309. rotateGroup,
  310. rotateGroupThroughNumber,
  311. translateXGroup,
  312. translateYGroup,
  313. handleFormSubmit,
  314. savedSvgKeyframes,
  315. updateTime,
  316. }
  317. }
  318. const IndexPage = () => {
  319. const {
  320. loadedFileName,
  321. loadedFileContents,
  322. loadSvgFile,
  323. svgPreviewRef,
  324. svgGroups,
  325. svgAnimations,
  326. svgAnimationIdsListId,
  327. selectNewAnimationId,
  328. proposeNewAnimationId,
  329. isNewAnimationId,
  330. cancelAnimationIdChange,
  331. focusAnimationIdInput,
  332. blurAnimationIdInput,
  333. addKeyframeAttribute,
  334. enabledGroupIds,
  335. rotateGroup,
  336. rotateGroupThroughNumber,
  337. translateXGroup,
  338. translateYGroup,
  339. handleFormSubmit,
  340. savedSvgKeyframes,
  341. updateTime,
  342. } = useAnimationForm();
  343. return (
  344. <form className="lg:fixed lg:w-full lg:h-full flex flex-col" onSubmit={handleFormSubmit}>
  345. <datalist id="translationTickMarks">
  346. <option value="0">0</option>
  347. </datalist>
  348. <datalist id="rotationTickMarks">
  349. <option value="-360">-360</option>
  350. <option value="-270">-270</option>
  351. <option value="-180">-180</option>
  352. <option value="-90">-90</option>
  353. <option value="0">0</option>
  354. <option value="90">90</option>
  355. <option value="180">180</option>
  356. <option value="270">270</option>
  357. <option value="360">360</option>
  358. </datalist>
  359. <datalist id="timeTickMarks">
  360. {savedSvgKeyframes?.map((k) => (
  361. <option
  362. key={k.time}
  363. value={k.time}
  364. >
  365. {k.time}
  366. </option>
  367. ))}
  368. </datalist>
  369. <div className="flex gap-4 px-4 py-2 items-center">
  370. <FileButton
  371. name="svgFile"
  372. accept="*.svg"
  373. onChange={loadSvgFile}
  374. className="whitespace-nowrap"
  375. >
  376. Load SVG File
  377. </FileButton>
  378. {
  379. loadedFileContents
  380. && (
  381. <>
  382. <Button
  383. type="submit"
  384. className="whitespace-nowrap"
  385. name="action"
  386. value="saveSvgFile"
  387. >
  388. Save SVG File
  389. </Button>
  390. <div className="text-center w-full">
  391. {loadedFileName}
  392. </div>
  393. </>
  394. )
  395. }
  396. </div>
  397. {
  398. loadedFileContents
  399. && (
  400. <div className="h-full flex flex-col">
  401. <div className="flex gap-4">
  402. <label className="block w-full">
  403. <span className="sr-only">
  404. Animation ID
  405. </span>
  406. <ComboBox
  407. name="animationId"
  408. defaultValue={svgAnimations?.[0] ?? ''}
  409. onOptionSelect={selectNewAnimationId}
  410. onCustomValueInput={proposeNewAnimationId}
  411. onFocus={focusAnimationIdInput}
  412. onBlur={blurAnimationIdInput}
  413. autoComplete="off"
  414. >
  415. {svgAnimations?.map(s => (
  416. <option key={s}>{s}</option>
  417. ))}
  418. </ComboBox>
  419. </label>
  420. {
  421. isNewAnimationId && (
  422. <>
  423. <Button
  424. type="submit"
  425. name="action"
  426. value="renameCurrentAnimationId"
  427. className="whitespace-nowrap"
  428. >
  429. Rename
  430. </Button>
  431. <Button
  432. type="submit"
  433. name="action"
  434. value="createNewAnimation"
  435. className="whitespace-nowrap"
  436. >
  437. Create New
  438. </Button>
  439. <Button
  440. type="reset"
  441. className="whitespace-nowrap"
  442. onClick={cancelAnimationIdChange}
  443. >
  444. Cancel
  445. </Button>
  446. </>
  447. )
  448. }
  449. </div>
  450. <div
  451. className="flex gap-4 h-full"
  452. >
  453. <div
  454. className="overflow-auto bg-[black] relative w-full"
  455. >
  456. <div
  457. className="absolute p-4"
  458. >
  459. <div
  460. className="bg-[white] p-4"
  461. ref={svgPreviewRef}
  462. dangerouslySetInnerHTML={{ __html: loadedFileContents ?? '' }}
  463. />
  464. </div>
  465. </div>
  466. <div
  467. style={{
  468. width: 560
  469. }}
  470. >
  471. {
  472. Array.isArray(svgGroups) && (
  473. <>
  474. {svgGroups.map((group) => {
  475. return (
  476. <fieldset
  477. key={group.id}
  478. className="contents"
  479. >
  480. <legend className="sr-only">
  481. {group.name}
  482. </legend>
  483. <div className="flex">
  484. <div className="flex-auto">
  485. <label
  486. className="whitespace-nowrap"
  487. >
  488. <input type="checkbox" name="changed" value={group.id} onChange={addKeyframeAttribute} />
  489. {' '}
  490. <span>
  491. {group.name}
  492. </span>
  493. </label>
  494. </div>
  495. <label
  496. className="w-16 block"
  497. >
  498. <span className="sr-only">Translate X</span>
  499. <input
  500. className="w-full block text-right"
  501. type="number"
  502. name="translateX"
  503. min="-360" max="360"
  504. defaultValue="0"
  505. step="any"
  506. disabled={!enabledGroupIds?.includes(group.name)}
  507. onChange={translateXGroup(group)}
  508. list="translationTickMarks"
  509. />
  510. </label>
  511. <label
  512. className="w-16 block"
  513. >
  514. <span className="sr-only">Translate Y</span>
  515. <input
  516. className="w-full block text-right"
  517. type="number"
  518. name="translateY"
  519. min="-360" max="360"
  520. defaultValue="0"
  521. step="any"
  522. disabled={!enabledGroupIds?.includes(group.name)}
  523. onChange={translateYGroup(group)}
  524. list="translationTickMarks"
  525. />
  526. </label>
  527. <label
  528. className="w-40 block"
  529. >
  530. <span className="sr-only">Rotate</span>
  531. <input
  532. className="w-full block"
  533. type="range"
  534. name="rotate"
  535. min="-360"
  536. max="360"
  537. defaultValue="0"
  538. step="5"
  539. disabled={!enabledGroupIds?.includes(group.name)}
  540. onChange={rotateGroup(group)}
  541. list="rotationTickMarks"
  542. />
  543. </label>
  544. <label
  545. className="w-12 block"
  546. >
  547. <span className="sr-only">Rotate</span>
  548. <input
  549. className="w-full block text-right"
  550. type="number"
  551. name="rotateNumber"
  552. min="-360"
  553. max="360"
  554. defaultValue="0"
  555. step="5"
  556. disabled={!enabledGroupIds?.includes(group.name)}
  557. onChange={rotateGroupThroughNumber(group)}
  558. />
  559. </label>
  560. </div>
  561. </fieldset>
  562. )
  563. })}
  564. </>
  565. )
  566. }
  567. </div>
  568. </div>
  569. <div className="flex gap-4 items-center px-4 py-2">
  570. <div
  571. className="w-16"
  572. >
  573. <input type="number" name="length" min="1" defaultValue="10" step="1" className="block w-full" />
  574. </div>
  575. <input
  576. className="w-full"
  577. list="timeTickMarks"
  578. type="range"
  579. name="time"
  580. min="0"
  581. max="10"
  582. defaultValue="0"
  583. step="1"
  584. onChange={updateTime}
  585. />
  586. <Button
  587. type="submit"
  588. name="action"
  589. value="updateTimeKeyframes"
  590. className="whitespace-nowrap"
  591. >
  592. Update Current Time
  593. </Button>
  594. </div>
  595. </div>
  596. )
  597. }
  598. </form>
  599. );
  600. }
  601. export default IndexPage;