├── .gitignore ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── components ├── Backpack.js ├── ButtonGroup.js ├── CSSForm.js ├── CodeForm.js ├── ErrorMessage.js ├── FullPrompt.js ├── IterationComponent.js ├── Multiples │ ├── MultiplesOutput.js │ ├── MultiplesRendering.js │ ├── Prompt4Multiples.js │ └── PromptHelpers.js ├── OutputEditor.js ├── PropertiesForm.js ├── PropertyFormElements.js ├── PropertyFormElements │ ├── BezierSelect.js │ ├── ColorPicker.js │ └── TimingFunctionSelect.js ├── SVGForm.js ├── SVGInput.js ├── SVGOutput.js ├── SegmentedControl.js ├── TranslateHelpers.js └── defaultSVGs.js ├── figs ├── ids.png ├── input.png └── keyframer.png ├── package-lock.json ├── package.json ├── pages ├── api │ └── generate.js ├── index.js └── index.module.css └── public ├── favicon.ico └── paint.png /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # env files 27 | .env 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Tseng" 5 | given-names: "Tiffany" 6 | orcid: "https://orcid.org/0000-0003-4972-4061" 7 | - family-names: "Cheng" 8 | given-names: "Ruijia" 9 | orcid: "https://orcid.org/0000-0002-2377-9550" 10 | - family-names: "Nichols" 11 | given-names: "Jeffrey" 12 | orcid: "https://orcid.org/0000-0002-9290-1409" 13 | title: "Keyframer: Empowering Animation Design using Large Language Models" 14 | version: 0.1.0 15 | doi: 10.48550/arXiv.2402.06071 16 | date-released: 2024-05-20 17 | url: "github_url_placeholder" -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the open source team at [opensource-conduct@group.apple.com](mailto:opensource-conduct@group.apple.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, 71 | available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | Thanks for your interest in contributing. This project was released to accompany a research paper for purposes of reproducibility, and beyond its publication there are limited plans for future development of the repository. 4 | 5 | While we welcome new pull requests and issues please note that our response may be limited. Forks and out-of-tree improvements are strongly encouraged. 6 | 7 | ## Before you get started 8 | 9 | By submitting a pull request, you represent that you have the right to license your contribution to Apple and the community, and agree by submitting the patch that your contributions are licensed under the [LICENSE](LICENSE). 10 | 11 | We ask that all community members read and observe our [Code of Conduct](CODE_OF_CONDUCT.md). 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 2 | 3 | IMPORTANT: This Apple software is supplied to you by Apple 4 | Inc. ("Apple") in consideration of your agreement to the following 5 | terms, and your use, installation, modification or redistribution of 6 | this Apple software constitutes acceptance of these terms. If you do 7 | not agree with these terms, please do not use, install, modify or 8 | redistribute this Apple software. 9 | 10 | In consideration of your agreement to abide by the following terms, and 11 | subject to these terms, Apple grants you a personal, non-exclusive 12 | license, under Apple's copyrights in this original Apple software (the 13 | "Apple Software"), to use, reproduce, modify and redistribute the Apple 14 | Software, with or without modifications, in source and/or binary forms; 15 | provided that if you redistribute the Apple Software in its entirety and 16 | without modifications, you must retain this notice and the following 17 | text and disclaimers in all such redistributions of the Apple Software. 18 | Neither the name, trademarks, service marks or logos of Apple Inc. may 19 | be used to endorse or promote products derived from the Apple Software 20 | without specific prior written permission from Apple. Except as 21 | expressly stated in this notice, no other rights or licenses, express or 22 | implied, are granted by Apple herein, including but not limited to any 23 | patent rights that may be infringed by your derivative works or by other 24 | works in which the Apple Software may be incorporated. 25 | 26 | The Apple Software is provided by Apple on an "AS IS" basis. APPLE 27 | MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 28 | THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 29 | FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 30 | OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 31 | 32 | IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 33 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 34 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 35 | INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 36 | MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 37 | AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 38 | STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 39 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keyframer: Empowering Animation Design using Large Language Models 2 | 3 | Keyframer is a design tool for animating static images using natural language prompts. It enables users to refine animations by directly editing generated designs and explore alternatives by requesting variants. [[Paper](https://arxiv.org/abs/2402.06071), [Video](https://machinelearning.apple.com/research/keyframer)] 4 | 5 | Tiffany Tseng, Ruijia Cheng, Jeffrey Nichols 6 | 7 | ![](./figs/keyframer.png) 8 | 9 | ## Setup 10 | 11 | This application utilizes GPT-4 and requires a valid Open AI API key. 12 | 13 | 1. Clone this repository 14 | 15 | 2. Navigate to the project directory 16 | ```bash 17 | $ cd ml-keyframer 18 | ``` 19 | 3. Install the requirements 20 | ```bash 21 | $ npm install 22 | ``` 23 | 24 | 4. Edit your bash profile to add your OpenAI API key. 25 | 26 | For example, on Mac, open your bash profile with the nano editor 27 | ```bash 28 | $ nano ~/.bash_profile 29 | ``` 30 | or for newer Macs 31 | ```bash 32 | $ nano ~/.zshrc 33 | ``` 34 | 35 | and add your Open AI API, replacing `your-api-key-here` with your API key 36 | ```bash 37 | export OPENAI_API_KEY='your-api-key-here' 38 | ``` 39 | 40 | Save and exit (nano editor: Ctrl+O to write changes and Ctrl+X to close the editor) 41 | 42 | Load your updated profile with 43 | ```bash 44 | source ~/.bash_profile 45 | ``` 46 | or 47 | ```bash 48 | source ~/.zshrc 49 | ``` 50 | 51 | Verify your setup by typing `echo $OPENAI_API_KEY` in your terminal and check that it displays your API key. 52 | 53 | If you are using Windows, you can find instructions for setting up your API key [here](https://platform.openai.com/docs/quickstart) 54 | 55 | 5. Run the app 56 | 57 | ```bash 58 | $ npm run dev 59 | ``` 60 | 61 | You should now be able to access the app at [http://localhost:3000](http://localhost:3000) 62 | 63 | ## Using Keyframer 64 | 65 | Keyframer takes SVG image input and uses GPT-4 to generate CSS code to animate the image. 66 | 67 | 1. Select one of the default images under `Input`, or paste in code for an SVG image in the Input field. (Please refer to the instructions below for preparing custom SVGs to use in Keyframer.) 68 | 69 | ![](./figs/input.png) 70 | 71 | 2. Enter in a prompt for animating the image. When writing your prompt, it can helpful to incorporate the identifiers for elements in the SVG (specified with `id=ITEM_NAME` in the code, see figure below for more detail). You can request multiple designs to compare, e.g. `Create 3 animations of `. 72 | 73 | ![](./figs/ids.png) 74 | 75 | 3. Click the "Generate Animations" button. 76 | 77 | 4. The LLM returns CSS code followed by the generated animation once the request is complete. 78 | 79 | 5. You can edit the response by 1) editing the CSS code using the Code Editors, or 2) editing the CSS properties using the Properties Editor. The Properties Editor will have some helpful UI for editing certain types of properties, such as a color picker for editing hex color values. 80 | 81 | 6. To extend your design, click "+ Add New Prompt" for any selected design to add a new prompt. Any new requests will be added on top of your existing design so you can iteratively build your animation. 82 | 83 | ## Preparing Custom SVG Images 84 | Keyframer takes image input in the form of an SVG. You can create your own SVG images in your graphics editor of choice before bringing it into Keyframer. 85 | 86 | When creating custom images, it's important to add relevant names to your layers so the large language model has context of the elements in the image. When you name your layers, those names will be exported in the SVG code as `id` properties on the different SVG elements. 87 | 88 | For step by step tips for preparing SVGs to use in Keyframer (including naming and grouping layers and minimizing the SVG size), please refer to Section A.2 in the [paper](https://arxiv.org/abs/2402.06071). 89 | 90 | ## BibTeX 91 | 92 | To cite our paper, please use 93 | 94 | ```bibtex 95 | @article{tseng2024keyframer, 96 | title={Keyframer: Empowering Animation Design using Large Language Models}, 97 | author={Tseng, Tiffany and Cheng, Ruijia and Nichols, Jeffrey}, 98 | journal={arXiv preprint arXiv:2402.06071}, 99 | year={2024} 100 | } 101 | ``` 102 | 103 | ## Acknowledgments 104 | * This application builds upon the [Open AI Quickstart Tutorial](https://github.com/openai/openai-quickstart-node) 105 | -------------------------------------------------------------------------------- /components/Backpack.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useState, useEffect } from 'react'; 6 | import styles from "../pages/index.module.css"; 7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 8 | import { faAngleDown, faAngleUp } from "@fortawesome/free-solid-svg-icons"; 9 | 10 | export default function Backpack({ savedDesigns, svg }) { 11 | const [isVisible, setIsVisible] = useState(false); 12 | 13 | useEffect(() => { 14 | if (!isVisible) { 15 | setIsVisible(true); 16 | }; 17 | }, [savedDesigns]); 18 | 19 | function goToDesign(key) { 20 | let designContainer = document.getElementsByClassName(key)[0] // assume it's the first instance of it on the page, since the backpack item and the design option have the same class name 21 | designContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); 22 | // add temporary styling to show that it's been selected 23 | let parent = designContainer.parentElement; 24 | if (parent) { 25 | parent.style.background = '#d0edff'; 26 | setTimeout(function () { 27 | parent.style.background = 'white'; 28 | }, 1500); 29 | } 30 | } 31 | 32 | function toggleVisibility() { 33 | setIsVisible(!isVisible); 34 | } 35 | 36 | return ( 37 |
38 |
39 | Saved Designs   40 | {isVisible ? 41 | 42 | : 43 | } 44 | 45 |
46 | 47 | {Object.keys(savedDesigns).map((key, index) => { 48 | return ( 49 |
goToDesign(key)}> 50 |
51 |
{key}
52 |
53 | ) 54 | })} 55 |
56 | ) 57 | } -------------------------------------------------------------------------------- /components/ButtonGroup.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useState } from "react"; 6 | import styles from "../pages/index.module.css"; 7 | import { defaultSVGMinis } from '../components/defaultSVGs' 8 | 9 | const ButtonGroup = ({ buttons, doSomethingAfterClick }) => { 10 | const [clickedId, setClickedId] = useState(-1); 11 | const handleClick = (event, id) => { 12 | setClickedId(id); 13 | doSomethingAfterClick(event); 14 | }; 15 | 16 | return ( 17 | <> 18 | {buttons.map((buttonLabel, i) => ( 19 | 27 | ))} 28 | 29 | ); 30 | }; 31 | 32 | export default ButtonGroup; -------------------------------------------------------------------------------- /components/CSSForm.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import { toCSS, toJSON } from 'cssjson'; 6 | import React, { useEffect, useState } from 'react'; 7 | import _ from "lodash"; 8 | import styles from "../pages/index.module.css"; 9 | 10 | export default function CSSForm({ css, setCSS }) { 11 | const [cssJSON, setCSSJson] = useState({}); 12 | 13 | useEffect(() => { 14 | window.css = css; 15 | let json = toJSON(css); 16 | setCSSJson(json); 17 | window.parsed = json; 18 | }, []); 19 | 20 | function updateCSS(newJSON) { 21 | setCSSJson(newJSON); 22 | let newCSS = toCSS(newJSON); 23 | setCSS(``); 26 | } 27 | 28 | return ( 29 | <> 30 |
31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | export function RecursiveComponent(item) { 38 | const { children, json, updateCSS } = item; 39 | 40 | const onInputChange = (event) => { 41 | let target = event.target; 42 | let value = target.value; 43 | 44 | let pathArray = []; 45 | parent = target.parentElement; 46 | while (parent) { 47 | if (parent.id == '') { break } 48 | let id = parent.id.replace('_', ''); 49 | pathArray.unshift(id); 50 | parent = parent.parentElement; 51 | } 52 | reviseCSS(pathArray, value); 53 | } 54 | 55 | function reviseCSS(pathArray, value) { 56 | let path = '' 57 | for (var i = 0; i < pathArray.length; i++) { 58 | let pathItem = `['children']`; 59 | if (i == pathArray.length - 1) { 60 | pathItem = `['attributes']`; 61 | } 62 | 63 | path += pathItem + `['${pathArray[i]}']`; 64 | } 65 | let newJSON = _.set(json, path, value) 66 | updateCSS(newJSON); 67 | } 68 | 69 | return ( 70 | <> 71 | {children && Object.keys(children).map((child) => { 72 | return ( 73 |
= 0 ? "_" + child : child}> 74 | {child} {"{"} 75 | { children[child].children && 76 | 77 | } 78 | 79 | { !_.isEmpty(children[child].attributes) && children[child].attributes && 80 | Object.entries(children[child].attributes).map(([name, value]) => { 81 | return ( 82 | 87 | ) 88 | }) 89 | 90 | } 91 |
{"}"}
92 |
93 | ) 94 | })} 95 | 96 | ) 97 | }; 98 | 99 | export function PropertyContainer({ name, value, onChange }) { 100 | return ( 101 |
102 | 103 | : 104 | 112 |
113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /components/CodeForm.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useEffect, useState } from 'react'; 6 | import { less } from '@codemirror/lang-less'; 7 | import CodeMirror from '@uiw/react-codemirror'; 8 | import { tokyoNight } from '@uiw/codemirror-theme-tokyo-night'; 9 | 10 | export default function CodeForm() { 11 | let sunsetExample = ` 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | `; 26 | 27 | const [outputCode, setOutputCode] = useState(sunsetExample); // default to response from ChatGPT 28 | 29 | const onSVGOutputChange = React.useCallback((value, viewUpdate) => { 30 | setOutputCode(value); 31 | }, []); 32 | 33 | 34 | return ( 35 | <> 36 |
37 | 45 | 46 | ) 47 | } -------------------------------------------------------------------------------- /components/ErrorMessage.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import styles from "../pages/index.module.css"; 6 | 7 | export default function ErrorMessage({ message }) { 8 | return ( 9 |
10 | {message} 11 |
12 | ) 13 | } -------------------------------------------------------------------------------- /components/FullPrompt.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React from 'react'; 6 | import styles from "../pages/index.module.css"; 7 | 8 | export default function FullPrompt({ prompt }) { 9 | return ( 10 |
11 |

Full Prompt:

12 |
{prompt}
13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /components/IterationComponent.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useState } from 'react'; 6 | import styles from "../pages/index.module.css"; 7 | import SVGInput from './SVGInput'; 8 | import FullPrompt from './Fullprompt'; 9 | import MultiplesOutput from './Multiples/MultiplesOutput'; 10 | import { Prompt4Multiples } from './Multiples/Prompt4Multiples'; 11 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 12 | import { faCircleXmark } from '@fortawesome/free-solid-svg-icons'; 13 | import _ from "lodash"; 14 | 15 | export default function IterationComponent({ index, inputCSS, addIteration, designClassName, numDesigns, setNumDesigns, updateSavedDesigns, selectedSVG, setSelectedSVG, svgInput, setSVGInput, isInSummaryMode, lastPrompt, setLastPrompt, resetDesigns, removeIteration }) { 16 | 17 | const [promptInput, setPromptInput] = useState(""); 18 | const [result, setResult] = useState(""); 19 | const [fullPrompt, setFullPrompt] = useState(""); 20 | const [loading, setLoading] = useState(false); 21 | const [timeToComplete, setTimeToComplete] = useState(0); 22 | const [errorMessage, setErrorMessage] = useState(null); 23 | const [css, setCSS] = useState(inputCSS); // for storing any modified CSS 24 | 25 | async function onSubmit(event) { 26 | event.preventDefault(); 27 | 28 | // clear previous values 29 | setResult(""); 30 | setLoading(true); 31 | 32 | // store last prompt 33 | setLastPrompt(promptInput); 34 | 35 | // generate full prompt 36 | let generatedPrompt = ""; 37 | generatedPrompt = Prompt4Multiples(promptInput, css, svgInput, numDesigns); 38 | 39 | setFullPrompt(generatedPrompt); 40 | sendGPTrequest(generatedPrompt); 41 | } 42 | 43 | const sendGPTrequest = async (message) => { 44 | // start timer 45 | const start = Date.now(); 46 | 47 | const response = await fetch("/api/generate?endpoint=chat", { 48 | method: "POST", 49 | headers: { 50 | 'Content-Type': "application/json" 51 | }, 52 | body: JSON.stringify({ message }) 53 | }); 54 | 55 | const data = await response.json(); 56 | 57 | if (data.success) { 58 | // Open a connection to receive streamed responses 59 | const eventSource = new EventSource("/api/generate?endpoint=stream"); 60 | 61 | eventSource.onmessage = function (event) { 62 | // Parse the event data, which is a JSON string 63 | const parsedData = JSON.parse(event.data); 64 | if (parsedData == "DONE") { 65 | const end = Date.now(); 66 | const requestTime = (end - start) / 1000 67 | setTimeToComplete(requestTime); 68 | setLoading(false); 69 | window.scrollTo(0, document.body.scrollHeight); 70 | } else { 71 | setResult((prev) => prev.concat(parsedData)); 72 | } 73 | }; 74 | eventSource.onerror = function () { 75 | eventSource.close(); 76 | }; 77 | } 78 | } 79 | 80 | function clearResult() { 81 | setFullPrompt(""); 82 | setResult(""); 83 | setPromptInput(""); 84 | } 85 | 86 | function closeIteration() { 87 | removeIteration(designClassName); 88 | clearResult() 89 | } 90 | 91 | return ( 92 |
93 | {index > 0 && result.length == 0 && 94 |
95 |   Close 96 |
97 | } 98 |
{`Iteration ${index + 1}`}
99 | 116 | 117 |
118 |

GPT Prompt

119 |
120 |
122 | {isInSummaryMode ? 123 |
{promptInput}
124 | : 125 | 136 | } 137 | 138 | 139 | {fullPrompt && fullPrompt.length > 0 && 140 | 141 | } 142 | 143 |
144 |
145 | {result && result.length > 0 && 146 | 157 | } 158 |
159 | ) 160 | } -------------------------------------------------------------------------------- /components/Multiples/MultiplesOutput.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useEffect, useState, useRef } from 'react'; 6 | import styles from "../../pages/index.module.css"; 7 | import { EditorView } from "codemirror"; 8 | import CodeMirror from '@uiw/react-codemirror'; 9 | import { less } from '@codemirror/lang-less'; 10 | import { tokyoNight } from '@uiw/codemirror-theme-tokyo-night'; 11 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 12 | import { faAngleDown, faAngleUp } from "@fortawesome/free-solid-svg-icons"; 13 | import MultiplesRendering from './MultiplesRendering'; 14 | import _ from "lodash"; 15 | 16 | export default function MultiplesOutput({ svgInput, gptResult, isLoading, timeToComplete, setCSS, updateSavedDesigns, addIteration, numDesigns, setNumDesigns }) { 17 | 18 | const [snippets, setSnippets] = useState([]); 19 | const [selectedOption, setSelectedOption] = useState(0); // the currently selected design option 20 | const [isGPTOutputVisible, setIsGPTOutputVisible] = useState(0); 21 | const [snippetCount, setSnippetCount] = useState(0); 22 | const [firstLoad, setFirstLoad] = useState(false); 23 | 24 | const delimeter = '-----'; // this splits the different code snippets 25 | const gptOutputScrollRef = useRef(null); 26 | 27 | useEffect(() => { 28 | if (!firstLoad) { 29 | window.scrollTo(0, document.body.scrollHeight); 30 | setFirstLoad(!firstLoad); 31 | } 32 | 33 | }, [isLoading]) 34 | 35 | useEffect(() => { 36 | // scroll to bottom of container 37 | let allGptOutputContainer = document.getElementsByClassName('gptOutputContainer'); 38 | let lastGPTOutputContainer = allGptOutputContainer[allGptOutputContainer.length - 1] 39 | lastGPTOutputContainer.scrollTop = lastGPTOutputContainer.scrollHeight; 40 | 41 | // check if snippet count has increased 42 | let count = (gptResult.match(/-----/g) || []).length; // maybe there's a way to refer to delimeter variable 43 | 44 | if (count > snippetCount) { 45 | let currentResponse = _.cloneDeep(gptResult); 46 | setSnippetCount(count); 47 | 48 | // increment number of designs 49 | setNumDesigns(numDesigns + 1); 50 | // add the snippet to the page 51 | let styleSnippets = currentResponse.split(delimeter); 52 | let currentSnippet = styleSnippets[count - 1]; 53 | // handle if there is extra explanation before the snippet 54 | let snippetStartTag = ''; 25 | 26 | let start = css.indexOf(cssStart) + cssStart.length; 27 | let end = css.indexOf(cssEnd); 28 | return input.substring(start, end); 29 | } 30 | 31 | function onEditorTypeChange(newEditorType) { 32 | if (newEditorType == 'Code') { 33 | setEditorType(0); 34 | } else if (newEditorType == 'Properties') { 35 | setEditorType(1); 36 | } 37 | }; 38 | 39 | const onCodeEditorChange = React.useCallback((value, viewUpdate) => { 40 | setCSS(value); 41 | }, []); 42 | 43 | return ( 44 |
45 | {/* TODO: Should revise outputCode from nested editors */} 46 | {shouldShowSVG && 47 | <> 48 |
49 |
50 | 51 | } 52 | 53 |
54 | onEditorTypeChange(value)} 57 | className={styles.segmentedControls} 58 | /> 59 | {editorType == 0 && 60 |
61 | 69 |
70 | } 71 | {editorType == 1 && 72 | 73 | } 74 |
75 |
76 | ); 77 | } 78 | 79 | OutputEditor.defaultProps = { 80 | shouldShowSVG: true 81 | } -------------------------------------------------------------------------------- /components/PropertiesForm.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import { toCSS, toJSON } from 'cssjson'; 6 | import React, { useEffect, useState } from 'react'; 7 | import _ from "lodash"; 8 | import styles from "../pages/index.module.css"; 9 | import { KeyframePropertiesComponent, PropertyComponent } from './PropertyFormElements'; 10 | import { getX, getY, getUnit } from './TranslateHelpers'; 11 | 12 | export default function PropertiesForm({ css, setCSS }) { 13 | const [cssJSON, setCSSJson] = useState({}); 14 | const [elements, setElements] = useState({}); // these are css selectors that are not keyframes (e.g. #sky) 15 | const [keyframes, setKeyframes] = useState({}); // css selectors that are keyframes (e.g. @keyframes sky) 16 | 17 | useEffect(() => { 18 | window.css = css; 19 | let json = toJSON(css); 20 | setCSSJson(json); 21 | window.parsed = json; 22 | if (json && json.children) { 23 | let currentElements = getElements(json.children); 24 | let currentKeyframes = getKeyframes(json.children); 25 | setElements(currentElements); 26 | setKeyframes(currentKeyframes); 27 | window.elements = currentElements; 28 | window.keyframes = currentKeyframes; 29 | } 30 | }, [css]); 31 | 32 | function getElements(input) { 33 | return Object.keys(input) 34 | .filter(item => item.indexOf('#') >= 0 || item.indexOf('.') >= 0) 35 | .reduce((obj, key) => { 36 | obj[key] = input[key]; 37 | return obj; 38 | }, {}); 39 | } 40 | 41 | function getKeyframes(input) { 42 | return Object.keys(input) 43 | .filter(item => item.indexOf('#') < 0 && item.indexOf('.') < 0) 44 | .reduce((obj, key) => { 45 | obj[key.replace('@keyframes ', '')] = input[key]; 46 | return obj; 47 | }, {}); 48 | } 49 | 50 | function updateCSS(selector, propertyName, keyframe, translateType, newPropertyValue) { 51 | let path = ''; 52 | let timingProperties = ['animation-duration', 'animation-delay']; 53 | 54 | // parse through cssJSON to set the new value 55 | if (keyframe == null) { 56 | // handle non keyframe update 57 | path = `['children']['${selector}']['attributes']['${propertyName}']`; 58 | if (timingProperties.includes(propertyName) && newPropertyValue && newPropertyValue.includes('s') == false) { 59 | newPropertyValue += 's'; 60 | } 61 | } else { 62 | // handle keyframe update 63 | path = `['children']['${selector}']['children']['${keyframe}']['attributes']['${propertyName}']`; 64 | } 65 | 66 | if (translateType) { 67 | // handle translate value 68 | let currentValue = _.get(cssJSON, path); 69 | if (translateType == "X") { 70 | let currentY = getY(currentValue); 71 | let currentYValue = parseInt(currentY); 72 | let currentYUnit = getUnit(currentY); 73 | let currentXUnit = getUnit(getX(currentValue)); 74 | if (currentXUnit.length == 0) { 75 | currentXUnit = "px"; // default to pixels 76 | } 77 | 78 | if (isNaN(currentYValue)) { 79 | newPropertyValue = `translateX(${newPropertyValue}${currentXUnit})`; 80 | } else { 81 | newPropertyValue = `translate(${newPropertyValue}${currentXUnit}, ${currentYValue}${currentYUnit})`; 82 | } 83 | 84 | } else if (translateType == "Y") { 85 | let currentX = getX(currentValue); 86 | let currentXValue = parseInt(currentX); 87 | let currentXUnit = getUnit(currentX); 88 | let currentYUnit = getUnit(getX(currentValue)); 89 | if (currentYUnit.length == 0) { 90 | currentYUnit = "px"; 91 | } 92 | 93 | if (isNaN(currentXValue)) { 94 | newPropertyValue = `translateY(${newPropertyValue}${currentYUnit})`; 95 | } else { 96 | newPropertyValue = `translate(${currentXValue}${currentXUnit}, ${newPropertyValue}${currentYUnit})`; 97 | } 98 | 99 | } 100 | } 101 | 102 | let newJSON = _.set(cssJSON, path, newPropertyValue); 103 | 104 | // update CSS 105 | setCSSJson(newJSON); 106 | let newCSS = toCSS(newJSON); 107 | setCSS(``); 110 | } 111 | 112 | let ignoreList = ['animation-name', 'animation-iteration-count', 'transform-box', 'transform-origin', 'animation-direction', 'animation-fill-mode', 'animation-play-state']; 113 | 114 | return ( 115 |
116 |
117 | {elements && Object.entries(elements).map(([selectorName, selectorValue]) => { 118 | return ( 119 |
120 |
{selectorName.replace('#', '')}
121 | 122 | {/* only handle classes with animation-name properties */} 123 | {_.get(elements, `['${selectorName}']['attributes']['animation-name']`) && 124 |
125 | {/* display keyframe properties */} 126 | 132 |
133 | } 134 | 135 |
136 | {/* display animation properties */} 137 | {Object.entries(selectorValue.attributes).map(([propertyName, propertyValue]) => { 138 | if (!ignoreList.includes(propertyName)) { 139 | return ( 140 | 145 | ) 146 | } 147 | })} 148 |
149 |
150 | ) 151 | })} 152 |
153 |
154 | ) 155 | } -------------------------------------------------------------------------------- /components/PropertyFormElements.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useEffect } from 'react'; 6 | import styles from "../pages/index.module.css"; 7 | import { getX, getY, getPos, getUnit } from './TranslateHelpers'; 8 | import ColorPicker from './PropertyFormElements/ColorPicker'; 9 | import TimingFunctionSelect from './PropertyFormElements/TimingFunctionSelect'; 10 | 11 | export function KeyframePropertiesComponent({ name, keyframes, onChange }) { 12 | 13 | function getPoints() { 14 | let shortName = name; 15 | if (name.indexOf('#') >= 0) { 16 | shortName = name.replace('#', ''); 17 | } else if (name.indexOf('.') >= 0) { 18 | shortName = name.replace('.', ''); 19 | } 20 | let elKeyframes = keyframes[shortName]; 21 | if (elKeyframes) { 22 | children = elKeyframes['children']; 23 | } 24 | return Object.keys(children); // array of keyframe % points 25 | } 26 | 27 | function getAttribute() { 28 | // returns name of attribute 29 | let attributes = []; 30 | Object.values(children).map(child => { 31 | let names = Object.keys(child.attributes); 32 | names.forEach(name => { 33 | if (attributes.includes(name) == false) { 34 | attributes.push(name); 35 | } 36 | }) 37 | }); 38 | return attributes; 39 | } 40 | 41 | function getValues(attribute) { 42 | // returns array of values 43 | let values = []; 44 | Object.values(children).map(child => { 45 | let value = child['attributes'][attribute]; 46 | values.push(value); 47 | }); 48 | return values; 49 | } 50 | 51 | var children = {}; 52 | var points = getPoints(); 53 | 54 | let attributes = getAttribute(); // an array of unique attributes 55 | 56 | return ( 57 | <> 58 | {attributes.map(attribute => { 59 | let values = getValues(attribute); 60 | 61 | // for now, just handle the translate types 62 | if (values.filter(value => value && value.includes('translate')).length > 0) { 63 | return ( 64 | 65 | ) 66 | } else { 67 | return ( 68 |
69 | 70 | {points.map((point, index) => 71 | <> 72 | 78 | 79 | 80 | )} 81 |
82 | ) 83 | } 84 | }) 85 | } 86 | 87 | ) 88 | } 89 | 90 | export function TranslationPropertiesComponent({ attribute, points, values, onChange, selector }) { 91 | 92 | if (values[0].includes("X") || values[0].includes("Y")) { 93 | // handle transformX or transformY such as transformX(100) 94 | // note this assumes that there is only one transform type being applied to the element 95 | let pixelValues = values.map(value => value ? getPos(value) : []); 96 | return ( 97 |
98 | 99 | {points.map((point, index) => 100 | <> 101 | 109 | 110 | 111 | 112 | )} 113 |
114 | ) 115 | } else { 116 | // handle full transform, e.g. transform(0, 100) 117 | let valuesX = values.map(value => value ? getX(value) : []); 118 | let valuesY = values.map(value => value ? getY(value) : []); 119 | return ( 120 | <> 121 |
{attribute}
122 |
123 | 124 | {points.map((point, index) => 125 | <> 126 | 134 | 135 | 136 | 137 | )} 138 |
139 |
140 | 141 | {points.map((point, index) => 142 | <> 143 | 151 | 152 | 153 | 154 | )} 155 |
156 | 157 | ) 158 | } 159 | } 160 | 161 | // KeyframeAttributeLabel - the label for the attribute (fill, transform, etc) 162 | export function KeyframeAttributeLabel({ name, value }) { 163 | return ( 164 | 165 | ) 166 | } 167 | 168 | // KeyframePointEl - label and input field for the specific attribute value (e.g., 0% = white) 169 | export function KeyframePointComponent({ point, value, onChange, propertyName, selector, translateType }) { 170 | let ignoreList = ['transform-box', 'transform-origin']; 171 | 172 | const onInputChange = (event) => { 173 | let target = event.target; 174 | let selector = '@keyframes ' + target.getAttribute('data-selector').replace('#', ''); 175 | let keyframe = target.getAttribute('data-point'); 176 | let propertyName = target.name; 177 | let newPropertyValue = target.value; 178 | let translateType = target.getAttribute('data-translate-type') || null; 179 | onChange(selector, propertyName, keyframe, translateType, newPropertyValue) 180 | } 181 | 182 | function handleColorChange(color) { 183 | let fullSelector = '@keyframes ' + selector.replace('#', ''); 184 | onChange(fullSelector, propertyName, point, null, color.hex); 185 | } 186 | 187 | if (isColor(value)) { 188 | return ( 189 |
190 | 191 | 192 |
193 | ) 194 | } else if (ignoreList.includes(propertyName)) { 195 | return (<>) 196 | } 197 | else { 198 | return ( 199 |
200 | 3 ? String(value).length + 3 : 3} 204 | onChange={onInputChange} 205 | data-selector={selector} 206 | name={propertyName} 207 | data-point={point} 208 | data-translate-type={translateType} 209 | /> 210 | 211 |
212 | ) 213 | } 214 | } 215 | 216 | export function PropertyComponent({ name, value, onChange, selector }) { 217 | 218 | let propertyDisplayNames = { 219 | 'animation-duration': 'duration', 220 | 'animation-delay': 'delay', 221 | 'animation-timing-function': 'timing-function' 222 | } 223 | 224 | let displayName = (propertyDisplayNames[name] && propertyDisplayNames[name].length > 0) ? propertyDisplayNames[name] : name; 225 | 226 | const onInputChange = (event) => { 227 | let newPropertyValue = event.target.value; 228 | // set null for the keyframe and selectorType values 229 | onChange(selector, name, null, null, newPropertyValue); 230 | } 231 | 232 | const onTimingInputChange = (select) => { 233 | onChange(selector, name, null, null, select.value); 234 | } 235 | 236 | const onBezierInputChange = (value) => { 237 | onChange(selector, name, null, null, value); 238 | } 239 | 240 | function handleColorChange(color) { 241 | onChange(selector, name, null, null, color.hex); 242 | } 243 | 244 | if (isColor(value)) { 245 | return ( 246 |
247 | 248 | 249 |
250 | ) 251 | } else { 252 | return ( 253 |
254 | 255 | 256 | {name == 'animation-timing-function' && 257 | 263 | } 264 | {name !== 'animation-timing-function' && 265 | <> 266 | = 0) ? value.replace('s', '') : value} 270 | placeholder={value} 271 | onChange={onInputChange} 272 | /> 273 | {propertyDisplayNames[name] && s} 274 | 275 | } 276 |
277 | ) 278 | } 279 | } 280 | 281 | export function UnitLabel({ unit }) { 282 | return ( 283 | 284 | {unit} 285 | 286 | ) 287 | } 288 | 289 | export function RightArrow({ componentIsLast }) { 290 | if (componentIsLast) { 291 | return (<>) 292 | } else { 293 | return ( 294 | {" → "} 295 | ) 296 | } 297 | } 298 | 299 | function isColor(value) { 300 | const style = new Option().style; 301 | style.color = value; 302 | return style.color !== ''; 303 | } 304 | 305 | -------------------------------------------------------------------------------- /components/PropertyFormElements/BezierSelect.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useEffect, useRef } from 'react'; 6 | import styles from "../../pages/index.module.css"; 7 | import { BezierCurveEditor } from 'react-bezier-curve-editor'; 8 | 9 | export default function BezierSelect({ onChange, bezierValue, setBezierValue, setDisplayBezierEditor, displayBezierEditor }) { 10 | 11 | const wrapperRef = useRef(null); 12 | useOutsideAlerter(wrapperRef); 13 | 14 | const onInputChange = (value) => { 15 | setBezierValue(value); 16 | onChange(bezierString(value)); 17 | } 18 | 19 | // handle clicking outside editor 20 | function useOutsideAlerter(ref) { 21 | useEffect(() => { 22 | /** 23 | * Alert if clicked on outside of element 24 | */ 25 | function handleClickOutside(event) { 26 | if (ref.current && !ref.current.contains(event.target)) { 27 | setDisplayBezierEditor(false); 28 | } 29 | } 30 | // Bind the event listener 31 | document.addEventListener("mousedown", handleClickOutside); 32 | return () => { 33 | // Unbind the event listener on clean up 34 | document.removeEventListener("mousedown", handleClickOutside); 35 | }; 36 | }, [ref]); 37 | } 38 | 39 | return ( 40 |
41 |
42 |
43 | 0 ? bezierValue : [.4, 0, 1, .6]} 45 | onChange={onInputChange} 46 | startHandleColor={"#027AFF"} 47 | endHandleColor={"#26CD41"} 48 | rowColor={"#fafafa"} 49 | outerAreaColor={"white"} 50 | innerAreaColor={"#fafafa"} 51 | outerAreaSize={10} 52 | /> 53 | 58 |
59 |
60 |
61 | ) 62 | } 63 | 64 | function roundValues(values) { 65 | let roundedValues; 66 | if (values.length > 0) { 67 | roundedValues = values.map(e => Number(e.toFixed(2))); 68 | } else { 69 | roundedValues = ""; 70 | } 71 | return roundedValues; 72 | } 73 | 74 | export function bezierString(values) { 75 | let roundedValues = roundValues(values); 76 | let bezierString = `cubic-bezier(${roundedValues})`; 77 | return bezierString; 78 | } 79 | 80 | export function BezierValue({ value, setDisplayBezierEditor, displayBezierEditor }) { 81 | 82 | const onClick = () => { 83 | setDisplayBezierEditor(true); 84 | } 85 | 86 | return ( 87 | 88 | {value} 89 | 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /components/PropertyFormElements/ColorPicker.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useState } from 'react'; 6 | import reactCSS from 'reactcss' 7 | import styles from "../../pages/index.module.css"; 8 | import { SketchPicker } from 'react-color'; 9 | 10 | export default function ColorPicker({ inputColor, onChange }) { 11 | const [color, setColor] = useState(inputColor); 12 | const [displayColorPicker, setDisplayColorPicker] = useState(false); 13 | 14 | const handleClick = () => { 15 | setDisplayColorPicker(!displayColorPicker) 16 | }; 17 | 18 | const handleClose = () => { 19 | setDisplayColorPicker(false) 20 | }; 21 | 22 | const handleChange = (color) => { 23 | setColor(color.hex); 24 | onChange(color); 25 | }; 26 | 27 | const colorPickerStyles = reactCSS({ 28 | 'default': { 29 | color: { 30 | background: `${color}`, 31 | }, 32 | popover: { 33 | position: 'absolute', 34 | zIndex: '2', 35 | }, 36 | cover: { 37 | position: 'fixed', 38 | top: '0px', 39 | right: '0px', 40 | bottom: '0px', 41 | left: '0px', 42 | }, 43 | }, 44 | }); 45 | 46 | return ( 47 |
48 |
49 |
50 |
{color}
51 |
52 | {displayColorPicker ?
53 |
54 | 55 |
: null} 56 | 57 |
58 | ) 59 | 60 | } -------------------------------------------------------------------------------- /components/PropertyFormElements/TimingFunctionSelect.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useEffect, useState } from 'react'; 6 | import Select from 'react-select'; 7 | import styles from "../../pages/index.module.css"; 8 | import BezierSelect, { BezierValue, bezierString } from './BezierSelect'; 9 | 10 | const options = [ 11 | { value: 'linear', label: 'linear' }, 12 | { value: 'ease', label: 'ease' }, 13 | { value: 'ease-in', label: 'ease-in' }, 14 | { value: 'ease-out', label: 'ease-out' }, 15 | { value: 'ease-in-out', label: 'ease-in-out' }, 16 | { value: 'custom', label: 'custom...' } 17 | ] 18 | 19 | export default function TimingFunctionSelect({ value, onChange, onBezierInputChange }) { 20 | 21 | const [selectedOption, setSelectedOption] = useState(setDefaultSelectValue); 22 | const [displayBezierEditor, setDisplayBezierEditor] = useState(false); 23 | const [bezierValue, setBezierValue] = useState([]); // array of bezier values 24 | 25 | function setDefaultSelectValue() { 26 | let option = options.filter(option => option.value == value); 27 | 28 | if (option.length > 0) { 29 | return (option[0]); 30 | } else if (value.indexOf('cubic') >= 0) { 31 | // handle custom 32 | return (options[options.length - 1]); 33 | } else { 34 | return options[0]; 35 | } 36 | } 37 | 38 | function initializeBezierValue() { 39 | let start = value.indexOf('(') + 1; 40 | let end = value.indexOf(')'); 41 | let points = value.substring(start, end); 42 | if (points) { 43 | let pointsStringArray = points.split(','); 44 | let pointsArray = []; 45 | for (var i = 0; i < pointsStringArray.length; i++) { 46 | pointsArray.push(parseFloat(pointsStringArray[i])); 47 | } 48 | setBezierValue(pointsArray); 49 | } 50 | } 51 | 52 | const onSelectChange = (selector) => { 53 | if (selector.value == 'custom') { 54 | setDisplayBezierEditor(true); 55 | } else { 56 | setBezierValue([]); 57 | onChange(selector); 58 | } 59 | } 60 | 61 | useEffect(() => { 62 | setDefaultSelectValue(); 63 | initializeBezierValue(); 64 | }, [value]); 65 | 66 | return ( 67 | <> 68 | 0 ? value.length : 1} 163 | placeholder={value} 164 | onChange={onChange} 165 | /> 166 | ) 167 | } 168 | 169 | export function LeftBracket({ name }) { 170 | return ( 171 | {"<"} 172 | 173 | {name} 174 | {" "} 175 | 176 | ) 177 | } 178 | 179 | export function RightBracket() { 180 | return ( 181 | {">"} 182 | ) 183 | } 184 | 185 | export function CloseBracket() { 186 | return ( 187 | {"/>"} 188 | ) 189 | } -------------------------------------------------------------------------------- /components/SVGInput.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useState, useEffect, useRef } from 'react'; 6 | import styles from "../pages/index.module.css"; 7 | import CodeMirror from '@uiw/react-codemirror'; 8 | import { less } from '@codemirror/lang-less'; 9 | import { tokyoNight } from '@uiw/codemirror-theme-tokyo-night'; 10 | import ButtonGroup from '../components/ButtonGroup'; 11 | import { defaultSVGs } from '../components/defaultSVGs' 12 | import Segmented from 'rc-segmented'; 13 | 14 | export default function SVGInput({ svgInput, setSVGInput, css, setCSS, setSelectedSVG, clearResult, shouldShowSegments, setTimeToComplete, setErrorMessage, shouldShowButtons, designClassName, buttonsToShow, numDesigns, resetDesigns }) { 15 | const [editorType, setEditorType] = useState(0); 16 | 17 | const scrollRef = useRef(null); 18 | 19 | const editorTypesMap = { 20 | 0: 'svg', 21 | 1: 'css' 22 | } 23 | 24 | function onEditorTypeChange(editorType) { 25 | let newEditorType = Object.values(editorTypesMap).indexOf(editorType.toLowerCase()); 26 | setEditorType(newEditorType); 27 | } 28 | 29 | const onSVGInputChange = React.useCallback((value, viewUpdate) => { 30 | setSelectedSVG("null"); 31 | setSVGInput(value); 32 | }, []); 33 | 34 | const onCSSChange = React.useCallback((value, viewUpdate) => { 35 | setCSS(value); 36 | }, []); 37 | 38 | function resetPage() { 39 | setSVGInput(defaultSVGs[event.target.name]); 40 | setSelectedSVG(event.target.name.replace("Mini", "")) //TODO - this is super brittle... 41 | 42 | // remove the previous results 43 | clearResult(); 44 | setTimeToComplete(0); 45 | setEditorType(0); 46 | setCSS(""); 47 | setErrorMessage(null); 48 | resetDesigns(); 49 | } 50 | 51 | const onButtonClick = (event) => { 52 | // show alert if selecting new SVG will clear content 53 | if (numDesigns == 0) { 54 | resetPage(); 55 | } else if (css.length == 0 && numDesigns > 0) { 56 | // user has clicked on the button select for iteration 0 57 | if (confirm("Changing the SVG will clear all your designs. Are you sure you want to continue?")) { 58 | // toggle the SVG content 59 | resetPage(); 60 | } else { 61 | // do nothing... 62 | } 63 | } 64 | }; 65 | 66 | useEffect(() => { 67 | scrollRef.current?.scrollIntoView({ 68 | behavior: "smooth" 69 | }) 70 | }, []); 71 | 72 | return ( 73 |
74 | 75 |

Input

76 | 77 | {shouldShowButtons && 78 | <> 79 |
80 | 84 |
85 | 86 | } 87 | 88 |
89 |
90 |
91 |
92 |
93 | 94 |
95 |
96 | onEditorTypeChange(value)} 99 | className={styles.segmentedControls} 100 | /> 101 |
102 | 103 | 104 | {editorType == 0 && 105 |
106 | 114 |
115 | } 116 | {editorType == 1 && 117 | 125 | } 126 |
127 |
128 |
129 |
130 | ) 131 | 132 | } 133 | 134 | SVGInput.defaultProps = { 135 | shouldShowButtons: true, 136 | modelType: 'gpt-4', 137 | shouldShowModelSelector: false, 138 | buttonsToShow: "default" 139 | }; -------------------------------------------------------------------------------- /components/SVGOutput.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useEffect, useState } from 'react'; 6 | import styles from "../pages/index.module.css"; 7 | import { EditorView } from "codemirror"; 8 | import CodeMirror from '@uiw/react-codemirror'; 9 | import { less } from '@codemirror/lang-less'; 10 | import { tokyoNight } from '@uiw/codemirror-theme-tokyo-night'; 11 | import OutputEditor from './OutputEditor'; 12 | 13 | export default function SVGOutput({ svgInput, gptResult, isLoading, hasExplanation, timeToComplete, css, setCSS }) { 14 | 15 | const [helpText, setHelpText] = useState(""); // instruction text about how to edit the parameters provided by GPT output 16 | 17 | const delimeterStart = ''; 19 | 20 | useEffect(() => { 21 | if (!isLoading) { 22 | if (!hasExplanation) { 23 | setCSS(gptResult); 24 | } else { 25 | // separate the code from the instructional text 26 | let code = gptResult.substring(gptResult.indexOf(delimeterStart), 27 | gptResult.indexOf(delimeterEnd) + delimeterEnd.length); 28 | setCSS(code); 29 | getHelpText(gptResult); 30 | } 31 | } 32 | }, [isLoading]); 33 | 34 | function getHelpText(gptResult) { 35 | let explainStart = "Explanation: "; 36 | let helpText = gptResult.substring( 37 | gptResult.indexOf(explainStart) + explainStart.length, 38 | gptResult.length - 1 39 | ); 40 | // replace backticks with code brackets 41 | helpText = helpText.replace(/`(.*?)`/g, '$1'); 42 | // replace quotes with code brackets 43 | helpText = helpText.replace(/"(.*?)"/g, '$1'); 44 | helpText = helpText.replace(/'(.*?)'/g, '$1'); 45 | setHelpText(helpText); 46 | } 47 | 48 | return ( 49 |
50 |
51 | 52 |

GPT Output

53 | 54 |
55 |
56 | 62 |
63 |
64 | {!isLoading &&
Time to complete: {timeToComplete} seconds
} 65 |
66 | 67 | 68 | {!isLoading && 69 |
70 | 71 |

Rendering

72 | 73 |
74 | 75 | 76 |
77 | 78 |
79 |
80 |
81 | } 82 |
83 | ) 84 | 85 | } -------------------------------------------------------------------------------- /components/SegmentedControl.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import { useState, useEffect, useRef } from 'react'; 6 | import styles from "../pages/index.module.css"; 7 | import { DefaultEditor } from './defaultSVGs'; 8 | 9 | const SegmentedControl = ({ 10 | name, 11 | segments, 12 | callback, 13 | controlRef 14 | }) => { 15 | const [activeIndex, setActiveIndex] = useState(DefaultEditor); 16 | 17 | const onInputChange = (value, index) => { 18 | setActiveIndex(index); 19 | callback(value, index); 20 | } 21 | 22 | return ( 23 |
24 |
25 | {segments.map((item, i) => ( 26 |
31 | onInputChange(item.value, i)} 37 | checked={i === activeIndex} 38 | /> 39 | 42 |
43 | ))} 44 |
45 |
46 | ); 47 | } 48 | 49 | export default SegmentedControl; -------------------------------------------------------------------------------- /components/TranslateHelpers.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | // functions for getting X Y coordinate properties from a translate property (e.g., translate(0,100)) 6 | // get the X value from a translate property 7 | export function getX(translate) { 8 | let start = translate.indexOf('(') + 1; 9 | let end = translate.indexOf(','); 10 | return translate.substring(start, end); 11 | } 12 | 13 | // get the Y value from a translate property 14 | export function getY(translate) { 15 | let start = translate.indexOf(',') + 1; 16 | let end = translate.indexOf(')'); 17 | return translate.substring(start, end); 18 | } 19 | 20 | // get position from a translateX or translateY property 21 | export function getPos(translate) { 22 | let start = translate.indexOf('(') + 1; 23 | let end = translate.indexOf(')'); 24 | return translate.substring(start, end); 25 | } 26 | 27 | export function getUnit(value) { 28 | if (value.indexOf('px') > 0) { 29 | return "px"; 30 | } else if (value.indexOf('%') > 0) { 31 | return "%"; 32 | } else { 33 | return ""; 34 | } 35 | } -------------------------------------------------------------------------------- /components/defaultSVGs.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | export const DefaultEditor = 0; 6 | export const illustration = ` 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | `; 20 | 21 | export const illustrationMini = ` 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | `; 35 | 36 | export const galaxy = ` 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | `; 78 | 79 | export const galaxyMini = ` 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | `; 122 | 123 | export const rocket = ` 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | Thank 150 | you! 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | `; 164 | 165 | export const rocketMini = ` 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | Thank 191 | you! 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | `; 206 | 207 | export const astronaut = ` 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | `; 260 | 261 | export const astronautMini = ` 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | `; 314 | 315 | // mapping between svg name and illustration name 316 | export const defaultSVGs = { 317 | "illustrationMini": illustration, 318 | "galaxyMini": galaxy, 319 | "rocketMini": rocket, 320 | "astronautMini": astronaut, 321 | }; 322 | 323 | export const defaultSVGMinis = { 324 | "illustrationMini": illustrationMini, 325 | "galaxyMini": galaxyMini, 326 | "rocketMini": rocketMini, 327 | "astronautMini": astronautMini, 328 | } -------------------------------------------------------------------------------- /figs/ids.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-keyframer/30cc5c1c835154139b0fd8c3c152217780627192/figs/ids.png -------------------------------------------------------------------------------- /figs/input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-keyframer/30cc5c1c835154139b0fd8c3c152217780627192/figs/input.png -------------------------------------------------------------------------------- /figs/keyframer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-keyframer/30cc5c1c835154139b0fd8c3c152217780627192/figs/keyframer.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ml-keyframer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "test": "true" 10 | }, 11 | "dependencies": { 12 | "@babel/parser": "7.9.4", 13 | "@codemirror/lang-html": "^6.4.5", 14 | "@codemirror/lang-javascript": "^6.1.9", 15 | "@codemirror/lang-less": "^6.0.1", 16 | "@fortawesome/fontawesome-svg-core": "^6.4.2", 17 | "@fortawesome/free-solid-svg-icons": "^6.4.2", 18 | "@fortawesome/react-fontawesome": "^0.2.0", 19 | "@uiw/codemirror-theme-tokyo-night": "^4.21.7", 20 | "@uiw/react-codemirror": "^4.21.7", 21 | "codemirror": "^6.0.1", 22 | "cssjson": "^2.1.3", 23 | "install": "^0.13.0", 24 | "lodash": "^4.17.21", 25 | "next": ">=14.1.1", 26 | "npm": "^10.5.2", 27 | "openai": "^4.38.0", 28 | "prettier": "2.0.4", 29 | "prism-react-renderer": "^2.0.6", 30 | "rc-segmented": "^2.2.2", 31 | "react": "^18.3.1", 32 | "react-bezier-curve-editor": "^1.1.2", 33 | "react-color": "^2.19.3", 34 | "react-dom": "^18.3.1", 35 | "react-select": "^5.7.4", 36 | "react-simple-code-editor": "^0.13.1", 37 | "react-syntax-highlighter": "^15.5.0", 38 | "reactcss": "^1.2.3", 39 | "svg-parser": "^2.0.4", 40 | "svgson": "^5.3.1" 41 | }, 42 | "engines": { 43 | "node": "^18.17.0 || >=20.5.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pages/api/generate.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import OpenAI from "openai"; 6 | import _ from "lodash"; 7 | 8 | const openai = new OpenAI(); 9 | 10 | let chatHistory = []; 11 | 12 | export default async function handler(req, res) { 13 | const { method } = req; 14 | 15 | switch (method) { 16 | case "POST": 17 | if (req.query.endpoint === "chat") { 18 | console.log("====CHAT===="); 19 | // reset the chat history 20 | chatHistory = []; 21 | // Handle POST to /api/generate?endpoint=chat 22 | const content = req.body.message; 23 | console.log(content); 24 | chatHistory.push({ role: "user", content: content }); 25 | res.status(200).json({ success: true }); 26 | } else if (req.query.endpoint === "reset") { 27 | console.log("====RESET====") 28 | // Handle POST to /api/generate?endpoint=reset 29 | chatHistory = []; 30 | res.status(200).json({ success: true }); 31 | } else { 32 | res.status(404).json({ error: "Not Found" }); 33 | } 34 | break; 35 | case "GET": 36 | if (req.query.endpoint === "history") { 37 | res.status(200).json(chatHistory); 38 | } else if (req.query.endpoint === "stream") { 39 | // Set headers for Server-Sent Events 40 | res.setHeader("Content-Type", "text/event-stream"); 41 | res.setHeader("Cache-Control", "no-cache"); 42 | res.setHeader("Connection", "keep-alive"); 43 | res.setHeader('Content-Encoding', 'none'); 44 | 45 | try { 46 | const stream = await openai.beta.chat.completions.stream({ 47 | model: "gpt-4", 48 | messages: chatHistory, 49 | stream: true, 50 | }); 51 | 52 | for await (const chunk of stream) { 53 | const message = chunk.choices[0]?.delta?.content || ""; 54 | res.write(`data: ${JSON.stringify(message)}\n\n`); 55 | } 56 | 57 | // After the stream ends, get the final chat completion 58 | const chatCompletion = await stream.finalChatCompletion(); 59 | res.write(`data: ${JSON.stringify("DONE")}\n\n`); 60 | } catch (error) { 61 | res.write( 62 | "event: error\ndata: " + 63 | JSON.stringify({ message: "Stream encountered an error" }) + 64 | "\n\n" 65 | ); 66 | } 67 | 68 | // When the client closes the connection, we stop the stream 69 | return new Promise((resolve) => { 70 | req.on("close", () => { 71 | resolve(); 72 | }); 73 | }); 74 | } else { 75 | res.status(404).json({ error: "Not Found" }); 76 | } 77 | break; 78 | default: 79 | res.setHeader("Allow", ["GET", "POST"]); 80 | res.status(405).end(`Method ${method} Not Allowed`); 81 | } 82 | } -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | import React, { useState, useEffect } from 'react'; 6 | import styles from "./index.module.css"; 7 | import IterationComponent from '../components/IterationComponent'; 8 | import Backpack from '../components/Backpack'; 9 | import { astronaut } from '../components/defaultSVGs'; 10 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 11 | import { faRectangleList } from "@fortawesome/free-solid-svg-icons"; 12 | 13 | export default function Home() { 14 | const [iterationCSS, setIterationCSS] = useState({ default: "" }); // for passing the new CSS from a selected design, indexed by iteration version 15 | const [numDesigns, setNumDesigns] = useState(0); 16 | const [savedDesigns, setSavedDesigns] = useState({}); // contains design options saved to the backpack with the format {design-n: cssAndSVGCode, ...} 17 | const [isInSummaryMode, setIsInSummaryMode] = useState(false); 18 | const [lastPrompt, setLastPrompt] = useState(""); 19 | 20 | const defaultSVGNames = ['illustration', 'loadingIcon', 'galaxy', 'rocket', 'astronuat']; 21 | 22 | const [svgInput, setSVGInput] = useState(astronaut); // the input SVG code 23 | const [selectedSVG, setSelectedSVG] = useState(defaultSVGNames[2]); // user selects a default SVG 24 | 25 | useEffect(() => { 26 | document.body.style = 'background: #343541;' 27 | document.title = 'Keyframer' 28 | }, []); 29 | 30 | function toggleSummaryMode() { 31 | setIsInSummaryMode(!isInSummaryMode); 32 | } 33 | 34 | // TODO - need to figure out how to supply class to new design without overriding - maybe add iteration class to the svg? 35 | 36 | function addIteration(className, css) { 37 | let newIterationCSS = { ...iterationCSS }; 38 | newIterationCSS[className] = css; 39 | setIterationCSS(newIterationCSS); 40 | window.scrollTo(0, document.body.scrollHeight); 41 | } 42 | 43 | function removeIteration(className) { 44 | let newIterationCSS = { ...iterationCSS }; 45 | delete newIterationCSS[className]; 46 | setIterationCSS(newIterationCSS); 47 | } 48 | 49 | // updateSavedOptions - for saving selected designs to the "backpack" 50 | function updateSavedDesigns(selectionCode, designOption) { 51 | let newOptions = { ...savedDesigns }; 52 | 53 | if (newOptions[designOption]) { 54 | delete newOptions[designOption]; 55 | } else { 56 | // add option 57 | newOptions[designOption] = selectionCode; 58 | } 59 | setSavedDesigns(newOptions); 60 | } 61 | 62 | function resetDesigns() { 63 | setSavedDesigns({}); 64 | setNumDesigns(0); 65 | setIterationCSS({ default: "" }); 66 | setLastPrompt("") 67 | } 68 | 69 | return ( 70 |
71 | {Object.entries(iterationCSS).map(([designClassName, css], index) => { 72 | return ( 73 | 92 | ) 93 | })} 94 | {Object.entries(savedDesigns).length > 0 && 95 | 99 | } 100 | {numDesigns > 0 && 101 |
102 | 103 |
104 | } 105 |
106 | ) 107 | } -------------------------------------------------------------------------------- /pages/index.module.css: -------------------------------------------------------------------------------- 1 | /* For licensing see accompanying LICENSE file. 2 | Copyright (C) 2024 Apple Inc. All Rights Reserved. 3 | */ 4 | 5 | @font-face { 6 | font-family: "ColfaxAI"; 7 | src: url(https://cdn.openai.com/API/fonts/ColfaxAIRegular.woff2) format("woff2"), 8 | url(https://cdn.openai.com/API/fonts/ColfaxAIRegular.woff) format("woff"); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: "ColfaxAI"; 15 | src: url(https://cdn.openai.com/API/fonts/ColfaxAIBold.woff2) format("woff2"), 16 | url(https://cdn.openai.com/API/fonts/ColfaxAIBold.woff) format("woff"); 17 | font-weight: bold; 18 | font-style: normal; 19 | } 20 | 21 | .main, 22 | .main input, 23 | .main { 24 | font-size: 16px; 25 | line-height: 24px; 26 | font-family: "ColfaxAI", Helvetica, sans-serif; 27 | position: relative; 28 | } 29 | 30 | .main { 31 | padding-top: 20px; 32 | display: flex; 33 | background: #343541; 34 | color: white; 35 | flex-direction: column; 36 | align-items: center; 37 | margin: -8px; 38 | } 39 | 40 | .main .icon { 41 | width: 34px; 42 | } 43 | 44 | .main h3 { 45 | font-size: 32px; 46 | line-height: 40px; 47 | font-weight: bold; 48 | color: #202123; 49 | margin: 16px 0 40px; 50 | } 51 | 52 | .main h4 { 53 | font-size: 20px; 54 | line-height: 0px; 55 | } 56 | 57 | .main form { 58 | display: flex; 59 | flex-direction: column; 60 | width: 100%; 61 | position: relative; 62 | } 63 | 64 | .main input[type="text"], 65 | .main textarea, 66 | .main pre { 67 | border-radius: 4px; 68 | } 69 | 70 | .formContainer { 71 | color: black; 72 | } 73 | 74 | .formContainer .userPromptInput { 75 | padding: 12px 16px; 76 | border: 1px solid #10a37f !important; 77 | } 78 | 79 | .formContainer .modelSelect { 80 | width: 140px; 81 | position: absolute; 82 | right: 10px; 83 | top: -68px; 84 | display: flex; 85 | align-items: center; 86 | } 87 | 88 | .formContainer .modelSelect label { 89 | margin-right: 7px; 90 | color: white; 91 | } 92 | 93 | .formContainer .modelSelect select { 94 | padding: 8px 10px; 95 | border-radius: 4px; 96 | } 97 | 98 | .main ::placeholder { 99 | color: #8e8ea0; 100 | opacity: 1; 101 | } 102 | 103 | .main input[type="submit"], 104 | .submitButton { 105 | padding: 12px 0; 106 | color: #fff; 107 | background-color: #10a37f; 108 | border: none; 109 | border-radius: 4px; 110 | text-align: center; 111 | cursor: pointer; 112 | } 113 | 114 | .main .result { 115 | font-weight: bold; 116 | margin-top: 40px; 117 | } 118 | 119 | .main .inputContainer, 120 | .main .outputContainer { 121 | width: 900px; 122 | margin: 0 auto 45px; 123 | } 124 | 125 | .inputContainer .tabs { 126 | display: flex; 127 | gap: 10px; 128 | } 129 | 130 | .inputContainer button { 131 | border: 1px solid #ccc; 132 | border-radius: 3px; 133 | } 134 | 135 | .inputContainer button:hover { 136 | cursor: pointer; 137 | } 138 | 139 | .inputContainer button.active { 140 | background: #e2f8ff !important; 141 | } 142 | 143 | .main .hint { 144 | color: #939292; 145 | margin-bottom: 0; 146 | font-size: small; 147 | } 148 | 149 | .main .svgInputContainer { 150 | display: flex; 151 | gap: 15px; 152 | } 153 | 154 | .svgInputContainer, 155 | .formContainer, 156 | .svgOutputContainer { 157 | background: #F2F2F7; 158 | border-radius: 5px; 159 | padding: 25px; 160 | margin: 25px 0; 161 | } 162 | 163 | .outputContainerLabel { 164 | display: flex; 165 | column-gap: 10px; 166 | align-items: center; 167 | } 168 | 169 | .outputContainerLabel:hover { 170 | cursor: pointer; 171 | } 172 | 173 | .gptOutputContainer { 174 | background: #F2F2F7; 175 | border-radius: 5px; 176 | padding: 25px; 177 | margin: 0 0 25px; 178 | max-height: 300px; 179 | overflow-y: scroll; 180 | transition: all 500ms ease-out; 181 | resize: vertical; 182 | } 183 | 184 | .completeNote { 185 | margin-left: auto; 186 | color: white; 187 | } 188 | 189 | .prompt { 190 | height: 100px; 191 | font-family: monospace; 192 | margin: 10px 0; 193 | background: black; 194 | color: white; 195 | padding: 10px; 196 | overflow: scroll; 197 | white-space: pre-wrap; 198 | resize: vertical; 199 | } 200 | 201 | .note { 202 | color: black; 203 | margin: 15px 0 0; 204 | } 205 | 206 | .break { 207 | flex-basis: 100%; 208 | height: 0; 209 | } 210 | 211 | .main code { 212 | background: black; 213 | color: yellowgreen; 214 | padding: 2px 3px; 215 | border-radius: 3px; 216 | } 217 | 218 | .sliderOutputForm { 219 | height: 475px; 220 | overflow: scroll; 221 | background: #1a1b26; 222 | color: white; 223 | padding: 0px 25px 10px; 224 | border-radius: 5px; 225 | font-family: monospace; 226 | } 227 | 228 | .elName { 229 | color: #4d9cff; 230 | } 231 | 232 | .elChild { 233 | margin-left: 20px; 234 | } 235 | 236 | .formEl { 237 | margin: 5px 0; 238 | } 239 | 240 | .formEl div:not(.formElClose) { 241 | margin-left: 25px; 242 | margin-top: 5px; 243 | } 244 | 245 | .propertyName { 246 | color: #35ffbc; 247 | } 248 | 249 | .propertyID { 250 | color: #af88ff; 251 | } 252 | 253 | .sliderOutputForm input { 254 | background-color: white; 255 | margin-right: 10px; 256 | font-family: monospace; 257 | border: 2px solid white; 258 | border-radius: 5px; 259 | } 260 | 261 | .sliderOutputCode { 262 | white-space: pre-wrap; 263 | background: black; 264 | color: #0db9d7; 265 | font-family: monospace; 266 | padding: 25px; 267 | } 268 | 269 | .svgOutputContainer form { 270 | display: flex; 271 | flex-flow: row wrap; 272 | margin: 20px 0; 273 | flex-direction: column-reverse; 274 | } 275 | 276 | .cssForm { 277 | display: block !important; 278 | } 279 | 280 | .svgOutputContainer form label { 281 | margin-right: 5px; 282 | } 283 | 284 | .svgOutputContainer form input { 285 | padding: 4px 8px; 286 | margin: 0 3px; 287 | border-radius: 5px; 288 | box-shadow: 0 0 2px 2px #9090901f; 289 | text-align: center; 290 | } 291 | 292 | .svgOutputContainer form label:not(:first-of-type) { 293 | margin-left: 5px; 294 | } 295 | 296 | .main .helpText { 297 | margin: 10px 0px; 298 | white-space: pre-wrap; 299 | } 300 | 301 | .main .promptHint { 302 | font-size: small; 303 | margin-top: 15px; 304 | margin-bottom: 10px; 305 | color: #8a8a8a; 306 | } 307 | 308 | .segControlsContainer { 309 | --highlight-width: auto; 310 | --highlight-x-pos: 0; 311 | display: flex; 312 | margin-bottom: 10px; 313 | } 314 | 315 | .segControls { 316 | display: inline-flex; 317 | justify-content: space-between; 318 | background: #51516b; 319 | border: 1px solid #e1e1e1; 320 | border-radius: 10px; 321 | max-width: 500px; 322 | padding: 0px; 323 | overflow: hidden; 324 | position: relative; 325 | } 326 | 327 | .segControls input { 328 | opacity: 0; 329 | margin: 0; 330 | top: 0; 331 | right: 0; 332 | bottom: 0; 333 | left: 0; 334 | position: absolute; 335 | width: 100%; 336 | cursor: pointer; 337 | height: 100%; 338 | } 339 | 340 | .segControls .segment:first-of-type { 341 | border: none; 342 | } 343 | 344 | .segControls .segment:last-of-type { 345 | border: none; 346 | } 347 | 348 | .segControls .segment { 349 | border-left: 1px solid #9999c0; 350 | border-right: 1px solid #9999c0; 351 | } 352 | 353 | .segment { 354 | min-width: 100px; 355 | position: relative; 356 | text-align: center; 357 | z-index: 1; 358 | } 359 | 360 | .segment label { 361 | cursor: pointer; 362 | display: block; 363 | font-weight: bold; 364 | font-size: 15px; 365 | padding: 2px 20px; 366 | position: relative; 367 | } 368 | 369 | .segment label:hover { 370 | cursor: pointer; 371 | } 372 | 373 | .segment:first-of-type label { 374 | border-radius: 10px 0 0 10px; 375 | } 376 | 377 | .segment:last-of-type label { 378 | border-radius: 0 10px 10px 0; 379 | } 380 | 381 | .segment.active label { 382 | color: #fff; 383 | background-color: #0A84FF; 384 | } 385 | 386 | .segment label svg { 387 | width: 20px; 388 | height: 20px; 389 | margin-top: 5px; 390 | } 391 | 392 | .checkbox { 393 | text-align: right; 394 | margin-top: -60px; 395 | margin-bottom: 15px; 396 | padding-bottom: 15px; 397 | color: white; 398 | } 399 | 400 | .checkboxExtendCSS { 401 | margin-top: -54px; 402 | margin-right: 350px; 403 | } 404 | 405 | .checkbox input { 406 | margin-right: 5px; 407 | } 408 | 409 | .propertyFormContainer { 410 | border-radius: 3px; 411 | } 412 | 413 | .propertyForm { 414 | display: flex; 415 | color: black; 416 | } 417 | 418 | .propertyForm .elChild { 419 | margin: 10px 0; 420 | 421 | } 422 | 423 | .propertyForm .propertyName { 424 | color: initial; 425 | } 426 | 427 | .propertyForm input { 428 | border-radius: 3px; 429 | border: 1px solid #e2e2e2; 430 | box-shadow: 0 0 2px 2px #7b7b7b1a 431 | /* padding: 0px 15px !important; */ 432 | } 433 | 434 | .propertyForm input[type='number'] { 435 | width: 30px; 436 | } 437 | 438 | .propertyForm input::-webkit-outer-spin-button, 439 | .propertyForm input::-webkit-inner-spin-button { 440 | -webkit-appearance: none; 441 | margin: 0; 442 | } 443 | 444 | .propertyForm .transformLabel { 445 | margin-bottom: 10px; 446 | } 447 | 448 | .propertyForm .selectorDiv { 449 | margin-bottom: 30px; 450 | } 451 | 452 | .propertyForm .paramElDiv { 453 | font-weight: bold; 454 | background: #b9dcff !important; 455 | border-radius: 5px 5px 0 0; 456 | padding: 5px 15px !important; 457 | } 458 | 459 | .propertyForm .selectorDiv>div { 460 | padding: 10px 15px; 461 | background: white; 462 | } 463 | 464 | .propertyForm .keyframePropertiesContainer { 465 | border-bottom: 1px solid #e2e2e2; 466 | } 467 | 468 | .propertyForm .propertiesContainer { 469 | border-radius: 0 0 5px 5px; 470 | } 471 | 472 | .propertiesContainer>div { 473 | margin-bottom: 10px; 474 | } 475 | 476 | .outputEditorContainer { 477 | display: flex; 478 | column-gap: 10px; 479 | width: 100%; 480 | } 481 | 482 | .outputEditor { 483 | width: 75%; 484 | } 485 | 486 | .keyframeProperties { 487 | display: flex; 488 | flex-wrap: wrap; 489 | margin-bottom: 10px; 490 | align-items: baseline; 491 | } 492 | 493 | .keyframeProperties .keyframeEl { 494 | display: flex; 495 | flex-direction: column; 496 | text-align: center; 497 | } 498 | 499 | .keyframeEl label { 500 | color: #818181; 501 | margin-left: 10px; 502 | } 503 | 504 | .sketch-picker input { 505 | width: 10px !important; 506 | } 507 | 508 | .colorPickerContainer { 509 | 510 | width: fit-content; 511 | padding: 5px 10px; 512 | background: #fff; 513 | border-radius: 3px; 514 | box-shadow: 0 0 2px 2px rgba(0, 0, 0, .1); 515 | display: inline-flex; 516 | column-gap: 5px; 517 | cursor: pointer; 518 | margin: 0px 5px; 519 | } 520 | 521 | .colorPickerContainer .swatch { 522 | width: 20px; 523 | height: 20px; 524 | border-radius: 5px; 525 | border: 0.5px solid #818181; 526 | } 527 | 528 | .submitButtonContainer { 529 | text-align: center; 530 | } 531 | 532 | .submitButton { 533 | width: 500px; 534 | } 535 | 536 | .dropdown { 537 | display: inline-block; 538 | width: fit-content; 539 | box-shadow: 0 0 2px 2px rgba(0, 0, 0, .1); 540 | border-radius: 3px; 541 | } 542 | 543 | .hiddenSegmentedControls { 544 | display: none; 545 | } 546 | 547 | .hidden { 548 | display: none; 549 | } 550 | 551 | .unit { 552 | color: #818181; 553 | } 554 | 555 | .rightArrow { 556 | margin: 0 5px; 557 | } 558 | 559 | .bezierButton { 560 | margin: 0 10px !important; 561 | } 562 | 563 | .bezierButton .placeholder { 564 | color: #0A84FF; 565 | text-decoration: underline; 566 | } 567 | 568 | .bezierButton .placeholder:hover { 569 | cursor: pointer; 570 | } 571 | 572 | .bezierEditorPopover .bezierCurveEditor { 573 | position: absolute; 574 | z-index: 2; 575 | background: white; 576 | box-shadow: 0 0 2px 2px rgba(0, 0, 0, .1); 577 | padding: 10px; 578 | width: 275px; 579 | } 580 | 581 | .bezierValue { 582 | font-family: monospace; 583 | color: #818181; 584 | } 585 | 586 | .bezierValue:hover, 587 | .bezierValue.inactive { 588 | color: #0A84FF; 589 | } 590 | 591 | .bezierValue:hover { 592 | cursor: pointer; 593 | } 594 | 595 | .bezierInfo { 596 | margin-left: 10px; 597 | } 598 | 599 | .errorMessage { 600 | background: black; 601 | color: #ff3e3e; 602 | font-family: monospace; 603 | padding: 15px; 604 | margin: 10px 0; 605 | } 606 | 607 | /* multiples editor */ 608 | .promptTextArea { 609 | white-space: pre-wrap; 610 | font-size: 16px; 611 | line-height: 24px; 612 | font-family: "ColfaxAI", Helvetica, sans-serif; 613 | padding: 12px 16px; 614 | border: 1px solid #10a37f !important; 615 | margin-bottom: 10px; 616 | } 617 | 618 | .multipleRenderingContainer { 619 | display: flex; 620 | column-gap: 20px; 621 | margin-bottom: 10px; 622 | flex-flow: wrap; 623 | } 624 | 625 | .editorContainer { 626 | width: 100%; 627 | order: 100; 628 | } 629 | 630 | .optionContainer { 631 | position: relative; 632 | display: inline; 633 | padding: 10px; 634 | border-radius: 8px; 635 | text-align: center; 636 | border: 3px solid transparent; 637 | margin-bottom: 20px; 638 | color: black; 639 | background: white; 640 | transition: all 500ms ease-out; 641 | } 642 | 643 | .optionContainer button { 644 | background: #10a37f; 645 | padding: 12px 20px; 646 | color: white; 647 | border: none; 648 | margin: 10px 0; 649 | border-radius: 4px; 650 | font-size: 14px; 651 | font-family: "ColfaxAI", Helvetica, sans-serif; 652 | } 653 | 654 | .optionContainer:hover { 655 | cursor: pointer; 656 | border: 3px solid #dcdcdc; 657 | } 658 | 659 | .optionContainer.active { 660 | border: 3px solid #0A84FF; 661 | } 662 | 663 | .optionContainer.highlight { 664 | background: #dfeeff; 665 | } 666 | 667 | .explanation { 668 | max-width: 230px; 669 | font-size: small; 670 | line-height: 20px; 671 | margin-top: 10px; 672 | color: #676767; 673 | } 674 | 675 | .inline { 676 | display: inline; 677 | } 678 | 679 | .link { 680 | position: absolute; 681 | right: 100px; 682 | top: 25px; 683 | color: white; 684 | } 685 | 686 | .starContainer { 687 | position: absolute; 688 | top: 10px; 689 | right: 10px; 690 | } 691 | 692 | .starContainer svg { 693 | border: none; 694 | } 695 | 696 | .savedDesigns { 697 | display: flex; 698 | position: fixed; 699 | right: 0px; 700 | width: 160px; 701 | background: white; 702 | z-index: 100; 703 | flex-direction: column; 704 | top: 0; 705 | padding: 10px; 706 | height: 100%; 707 | overflow-y: scroll; 708 | color: black; 709 | text-align: center; 710 | transition: all 250ms ease-out; 711 | } 712 | 713 | .savedDesignsMin { 714 | height: 9px; 715 | overflow: hidden; 716 | } 717 | 718 | .savedDesigns .savedDesignsLabel { 719 | background: black; 720 | color: white; 721 | margin: -15px -15px 10px; 722 | padding-top: 10px; 723 | 724 | } 725 | 726 | .savedDesigns .savedDesignsLabel:hover { 727 | cursor: pointer; 728 | } 729 | 730 | .savedDesigns .backpackItem:hover { 731 | cursor: pointer; 732 | transition: all 500ms ease-out; 733 | } 734 | 735 | .savedDesigns .backpackItem { 736 | border-bottom: 1px solid #eee; 737 | margin: 10px 0; 738 | padding-bottom: 10px; 739 | } 740 | 741 | .savedDesigns .backpackItemLabel { 742 | color: #8a8a8a; 743 | text-align: center; 744 | font-size: smaller; 745 | } 746 | 747 | .iterationContainer { 748 | width: 100%; 749 | padding-top: 20px; 750 | background: #343541; 751 | color: white; 752 | } 753 | 754 | .iterationContainer .svgInputContainer { 755 | background: white; 756 | color: black; 757 | } 758 | 759 | .iterationContainer.alternate { 760 | /* background: #F2F2F7; */ 761 | background: #454654; 762 | } 763 | 764 | .iterationContainer.alternate .svgInputContainer, 765 | .iterationContainer.alternate .formContainer { 766 | background: white; 767 | color: black; 768 | } 769 | 770 | /* Segmented Controls */ 771 | .segmentedControls { 772 | display: inline-block; 773 | background: #51516b; 774 | color: white; 775 | margin: 0 0 10px; 776 | } 777 | 778 | .segmentedControls :global(.rc-segmented-group) { 779 | position: relative; 780 | display: flex; 781 | align-items: stretch; 782 | justify-items: flex-start; 783 | width: 100%; 784 | border-radius: 2px; 785 | } 786 | 787 | .segmentedControls :global(.rc-segmented-item) { 788 | position: relative; 789 | min-height: 28px; 790 | padding: 5px 10px 0px; 791 | color: white; 792 | text-align: center; 793 | cursor: pointer; 794 | border-left: 1px solid #9393c4; 795 | border-right: 1px solid #9393c4; 796 | } 797 | 798 | .segmentedControls :global(.rc-segmented-item-selected) { 799 | background-color: #0A84FF; 800 | color: white; 801 | } 802 | 803 | .segmentedControls :global(.rc-segmented-item-disabled), 804 | .segmentedControls :global(.rc-segmented-item-disabled:hover), 805 | .segmentedControls :global(.rc-segmented-item-disabled:focus) { 806 | color: white; 807 | cursor: not-allowed; 808 | } 809 | 810 | .segmentedControls :global(.rc-segmented-item-label) { 811 | z-index: 2; 812 | line-height: 24px; 813 | } 814 | 815 | .segmentedControls :global(.rc-segmented-item-input) { 816 | position: absolute; 817 | top: 0; 818 | left: 0; 819 | width: 0; 820 | height: 0; 821 | opacity: 0; 822 | pointer-events: none; 823 | display: none; 824 | } 825 | 826 | .segmentedControls :global(.rc-segmented-thumb) { 827 | background-color: #0A84FF; 828 | position: absolute; 829 | width: 0; 830 | height: 100%; 831 | } 832 | 833 | .summaryTab { 834 | position: fixed; 835 | bottom: 0; 836 | left: 10px; 837 | background: black; 838 | color: white; 839 | padding: 8px 15px; 840 | border-radius: 10px 10px 0 0; 841 | } 842 | 843 | .summary :global h4 { 844 | display: none; 845 | } 846 | 847 | .summary .editorContainer, 848 | .summary .toggle, 849 | .summary .modelSelect, 850 | .summary .generateBtn, 851 | .summary .prompt, 852 | .summary .svgInputEditor, 853 | .summary .refineBtn, 854 | .summary .tabs, 855 | .summary .segmentedControls, 856 | .summary .outputContainer { 857 | display: none; 858 | opacity: 0; 859 | transition: all 500ms ease-in-out; 860 | } 861 | 862 | .summary .svgInputContainer { 863 | width: fit-content; 864 | column-gap: 0; 865 | } 866 | 867 | .summary .inputContainer, 868 | .summary .formContainer, 869 | .summary .outputContainer { 870 | margin-bottom: 0; 871 | } 872 | 873 | .summary .promptTextArea { 874 | font-family: monospace; 875 | background: #4361cb; 876 | color: white; 877 | height: 124px; 878 | margin-bottom: 0px; 879 | border-radius: 3px; 880 | border: none !important; 881 | width: 871px; 882 | height: auto; 883 | } 884 | 885 | .summary .formContainer { 886 | padding: 0; 887 | background: transparent; 888 | } 889 | 890 | .iterationLabel { 891 | text-align: right; 892 | display: none; 893 | } 894 | 895 | .closeIteration { 896 | width: 900px; 897 | margin: 0 auto -40px; 898 | text-align: right; 899 | } 900 | 901 | .closeIteration:hover { 902 | cursor: pointer; 903 | } 904 | 905 | .summary .iterationContainer .iterationLabel { 906 | display: block; 907 | width: 900px; 908 | margin: 0 auto -10px; 909 | text-align: right; 910 | color: #ccc; 911 | } 912 | 913 | .exportLink { 914 | position: fixed; 915 | bottom: 10px; 916 | right: 50px; 917 | background: #4e4e4e; 918 | color: white; 919 | padding: 10px 20px; 920 | border-radius: 5px; 921 | z-index: 100; 922 | } 923 | 924 | .exportLink:hover { 925 | cursor: pointer; 926 | } 927 | 928 | .logForm textArea { 929 | font-size: 16px; 930 | padding: 12px 16px; 931 | font-family: ColfaxAI, Helvetica, sans-serif; 932 | } 933 | 934 | .mainLogs .promptTextArea { 935 | width: 95%; 936 | white-space: pre-wrap; 937 | font-size: 16px; 938 | line-height: 24px; 939 | font-family: ColfaxAI, Helvetica, sans-serif; 940 | padding: 12px 16px; 941 | } 942 | 943 | .logSVGInput{ 944 | background: white; 945 | padding: 20px; 946 | margin-top: 30px; 947 | border-radius: 5px; 948 | } 949 | 950 | .mainLogs .iterationContainer:nth-of-type(odd){ 951 | background: #454654; 952 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-keyframer/30cc5c1c835154139b0fd8c3c152217780627192/public/favicon.ico -------------------------------------------------------------------------------- /public/paint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-keyframer/30cc5c1c835154139b0fd8c3c152217780627192/public/paint.png --------------------------------------------------------------------------------