Skip to content

Latest commit

Β 

History

History
341 lines (232 loc) Β· 12.4 KB

File metadata and controls

341 lines (232 loc) Β· 12.4 KB

Welcome to the UI package for StableStudio!

If you're ready to dive in and make changes, be sure to read about technology, structure, conventions, and plugins first.

πŸ“œ History

StableStudio represents a new direction for DreamStudio with renewed focus on open-source and community-driven development, but the project has an interesting backstory...

  • Pre-Stable Diffusion Launch

    Summer 2022

    DreamStudio began as passion-project by @nin with the goal of creating an all-in-one creative studio for generative animations using Disco Diffusion. He and several collaborators wrote the original UI using Vue.js and a local Python back-end. With the imminent arrival of Stable Diffusion, focus shifted toward creating an easy-to-use interface for image generation so the release of the model would be as accessible as possible.

  • Post-Stable Diffusion Launch

    August 2022 – November 2022

    Following the release of Stable Diffusion, DreamStudio served as a vehicle for new models and features shipped by Stability AI. This period saw the introduction of in-painting, out-painting, and image-to-image.

  • React

    December 2022 – April 2023

    In early December, work began on a new infinite-canvas version of the existing editor. Given the need to manage complicated state and side effects declaratively, the decision was made to switch the editor to React. Once a working editor MVP was finished, the opportunity was taken to rewrite the entire app while also shipping a re-imagined UX.

  • StableStudio

    Present – Future

    The breakneck speed of the Stable Diffusion community has proven distributing generative AI is better achieved through open-source development. We're hopeful StableStudio can serve as a hub for community collaboration and a home for exciting new features.

SorcererWithReact

  • TypeScript – Type safety ftw!

  • Vite – Bundler and live-development tool. You can see the full config in vite.config.ts

  • React –  We're using React with modern hooks and functional components

  • Zustand –  Extremely fast and easy-to-use state management, it's powerful and tiny!

  • Tailwind – Our preferred method of styling components, the config is at tailwind.config.js

  • Emotion – For when we need to break free of Tailwind and write "native CSS"

CyberPyramid

🧱 Domain-Driven Design

The most important aspect of this codebase's structure is adherence to domain-driven design.

This means we organize code around the concepts rather than technical implementation details.

For example, if we identify a concept, let's say User, we would create a User "domain."

The User domain would own all user-related code, including visual representation, state, hooks, etc.

Most domains are composed of smaller domains; a hypothetical User domain might be composed of User.State, User.Avatar, User.Preferences, User.Details, etc.

This structure is fractal in nature, meaning User.Details might itself be composed of User.Details.SocialMedia, User.Details.ContactInformation, User.Details.UpdateRequest, etc.

*️⃣ Domain Syntax

Here's an example of how we might represent a User domain in code...

import { Avatar } from "./Avatar";
import { Preferences } from "./Preferences";
import { Details } from "./Details";

export type User = {
  id: ID;
  avatar?: User.Avatar;
  preferences?: User.Preferences;
  details?: User.Details;
};

export function User({ id }: User.Props) {
  const user = User.use(id);
  return (
    <>
      <User.Avatar avatar={avatar} />
      <User.Preferences preferences={preferences} />
      <User.Details details={details} />
    </>
  );
}

export declare namespace User {
  export { Avatar, Preferences, Details };
}

export namespace User {
  User.Avatar = Avatar;
  User.Preferences = Preferences;
  User.Details = Details;

  export type Props = {
    id?: ID;
  };

  export const use = (id: ID) => {
    // Make an API call for the user or something
  };
}

This syntax takes advantage of TypeScript's declaration merging feature to enable nice fluent-style APIs for domains...

import { User } from "./User";

// You can use `User` as a type

const bob: User = { id: "bob" };

function App(id: ID) {
  // You can use `User` as a namespace

  const user = User.use(id);
  const userPreferences = User.Preferences.use(id);

  // You can use `User` as a component

  return (
    <>
      <User id={user.id} />
      <User.Preferences id={user.id} />
    </>
  );
}

Here is an example of a real domain (Editor.Camera) you'll find in the app...

// Owner of all things camera-related in the editor
Editor.Camera;

// Centers the camera in the editor
Editor.Camera.Center;

// The hand tool is used for panning around
Editor.Camera.Hand;

// For resetting the camera to the default view
Editor.Camera.Reset;

// Specifies keyboard shortcuts for camera actions
Editor.Camera.Shortcuts;

// Controls the zoom level of the camera and mouse wheel controls
Editor.Camera.Zoom;

Quick note about the export declare namespace User syntax from the first example...

This is needed when a namespace exports a type.

It's an unfortunate bit of syntax which might not be needed in the future.

If you want to discuss this further, please check out this issue.

πŸ“ Directories and Files

Domains are usually correlated one-to-one with file/folder structure.

For example, the Editor.Camera domain mentioned above is composed of the following files...

./src/Editor/Camera/
β”œβ”€β”€ Center.tsx
β”œβ”€β”€ Hand.tsx
β”œβ”€β”€ Reset.tsx
β”œβ”€β”€ Shortcut.tsx
β”œβ”€β”€ Zoom.tsx
└── index.tsx

This means if you see a domain like Generation.Image.Download, you can expect to find it at ./src/Generation/Image/Download.

All root-level domains located at the top-level of the ./src folder and can be imported using the shorthand syntax...

import { ExampleDomain } from "~/ExampleDomain";

import { ExampleDomain } from "../../../../../ExampleDomain";
// ^ If you were deep in the folder tree, it would look like this without the `~` alias

πŸ“‹ Structuring Tips

  • Look for repeated references to a word or concept

    For example, if you're working in a User domain and find yourself using the word "avatar" a lot, you might want to create a User.Avatar domain

  • Compound words are structuring hints

    Domains like DeviceInformation might be better represented as Device.Information.

  • Plural domains are very important

    The singular Generation.Image.Input domain owns everything related to the concept of what's being fed to an image generation, but Generation.Image.Inputs owns the state of all inputs as a collective.

    Plural domains are usually exported at the same level as their singular counterparts, such as Editor.Dream and Editor.Dreams.

⭐️ Important Domains

  • App – Owner of app-level functionality such as the React root, sidebars, providers, etc.

  • Generation.Image – Likely the largest domain, owns all things related to image generation

  • Generation.Image.Session – Domain which was poorly named and grew too large as a result, manages the active "image generation session"

  • Editor – Home of all editor functionality

  • Plugin – Owner of plugin setup and access, contains the important Plugin.use hook

  • Theme – Contains the StableStudio's design system and common components such as Theme.Icon, Theme.Button, etc.

  • Shortcut – Contains the StableStudio's keyboard shortcut system and menu

  • GlobalState – Mostly a pass-through wrapper around Zustand

  • GlobalVariables – Extremely useful functions and types available across the StableStudio without needing any imports (css, classes, useEffect, etc.)

RobotsHoldingHands

πŸ’… Styles and CSS

Most styling in StableStudio is implemented with Tailwind CSS.

The Theme domain contains a bunch of useful pre-made components, make sure to check there first if you need something "off-the-shelf."

You can use the classes function anywhere without imports if you need conditional styling or to break apart class names...

function Example({
  isEmphasized,
  isDisabled,
  children,
}: PropsWithChildren<{
  isEmphasized?: boolean;
  isDisabled: boolean;
}>) {
  return (
    <div
      className={classes(
        "bg-gray-100",
        isEmphasized && "bg-red-500 text-4xl font-bold",
        isDisabled && "text-xs opacity-50"
      )}
    >
      {children}
    </div>
  );
}

You can always "break glass" and use raw CSS via the globally-available css function if you need to, but please try to avoid it...

function Example() {
  return (
    <div
      css={css`
        ::-webkit-scrollbar {
          display: none;
        }
      `}
    >
      Hello, World!
    </div>
  );
}

πŸ› State Management via Zustand

StableStudio uses Zustand for state management, which is honestly super cool.

It's fast and small, you can read through the whole codebase in a few minutes.

We originally used Recoil it couldn't handle "hot" paths nearly as well.

Whenever you need globally-available state, you can use the GlobalState domain, which wraps Zustand...

import { GlobalState } from "~/GlobalState";

function Count() {
  const count = Count.use();
  const setCount = Count.useSet();
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <span>{count}</span>
    </div>
  );
}

export namespace Count {
  export const use = State.use(({ count }) => count);
  export const useSet = () =>
    State.use(({ setCount }) => setCount, GlobalState.shallow);
}

type State = {
  count: number;
  setCount: (count: number) => void;
};

namespace State {
  const store = GlobalState.create<State>((set) => ({
    count: 0,
    setCount: (count) => set({ count }),
  }));

  export const use = store;
}

Notice how the Count domain exports Count.use and Count.useSet instead of exporting its state directly.

This allows us to change the state implementation without breaking any code that uses it.

Finally, you can see GlobalState.shallow used to limit rerenders.