Pixi Solid
Features
Section titled “Features”Pixi Solid is a library that provides SolidJS components and hooks for working with PixiJS.
- 💙 Lightweight and flexible SolidJS library for creating PixiJS applications.
- 🎁 Provides a set of custom SolidJS components that create PixiJS objects instead of HTML elements.
- 📦 Supports all PixiJS objects, such as Filter, Container, Sprite, Graphics, Text, etc.
- 🧑💻 The convenience and speed of SolidJS stores and signals to manage state.
- ✨ All events emitted by PixiJS objects are supported.
- 😎 No limitations. Break out of SolidJS any time and interact directly with PixiJS.
- 💫 Useful helper utilities included.
- 🤩 Full Typescript support for type safety and auto completion.
Install
Section titled “Install”npm i pixi-solid pixi.js solid-jsPeer dependencies of
{ "pixi.js": "^8.14.3", "solid-js": "^1.9.10"}Why combine SolidJS with PixiJS?
Section titled “Why combine SolidJS with PixiJS?”-
Declarative PixiJS scene graph: Using SolidJS’s JSX templating means we get declarative control over the scene graph. No longer necessary to imperatively add and remove children.
-
Lifecycle hooks in our PixiJS components: SolidJS rendering PixiJS components means we can take advantage of the built in lifecycle methods in SolidJS
onMountandonCleanupas well as few extra custom hooks so we can automatically subscribe and unsubscribe from the ticker. -
Shared State and Reactivity: Pixi Solid leverages SolidJS’s reactivity system to automatically update PixiJS components when SolidJS signals or stores change. HTML elements and PixiJS graphics can stay in sync effortlessly.
-
Composability: Pixi Solid components can be easily composed together to create complex scenes and animations out of reusable components.
-
SolidJS is a thin wrapper: While Pixi Solid provides a nice abstraction over PixiJS it still provides access to all the properties and events of PixiJS objects directly.
-
SolidJS is really fast: SolidJS is on of the fatsest front-end frameworks out there so the overhead is very minimal.
-
SolidJS is fully featured: It has stores, signals, suspense, error boundaries, resource fetching and more. It’s a great feature set for simple or complex applications and you won’t have to reach for other libraries to manage templating or state.
Code example
Section titled “Code example”import type * as Pixi from "pixi.js";import { Assets, BlurFilter, Rectangle } from "pixi.js";import { CRTFilter } from "pixi-filters";import { Container, getPixiApp, onTick, PixiApplication, PixiCanvas, PixiStage, Sprite } from "pixi-solid";import { objectFit } from "pixi-solid/utils";import { createResource, onCleanup, Show } from "solid-js";import { Character } from "./character";import { Controls } from "./controls";import { createAppStore } from "./create-app-store";import { Ground } from "./ground";import { loadSceneAssets } from "./load-scene-assets";
export const DemoApp = () => { const appStore = createAppStore();
const [textureResource] = createResource(loadSceneAssets); const sceneBounds = new Rectangle(0, 0, 200, 133); const blurFilter = new BlurFilter({ strength: 8 }); const crtFilter = new CRTFilter({ curvature: 3, lineContrast: 0.1, vignetting: 0.1, noise: 0.04, noiseSize: 2, });
onCleanup(() => { blurFilter.destroy(); crtFilter.destroy(); });
return ( <div style={{ position: "relative" }}> <PixiApplication> <Controls isRunning={appStore.state.isRunning} direction={appStore.state.direction} onToggleDirectionClicked={appStore.toggleDirection} onToggleRunningClicked={appStore.toggleRunning} /> <PixiCanvas style={{ "aspect-ratio": `${sceneBounds.width}/${sceneBounds.height}`, overflow: "hidden", "border-radius": "10px", }} > <Show when={textureResource()}> <PixiStage filters={crtFilter}> <Container ref={(container) => { const app = getPixiApp(); onTick((ticker) => { objectFit(container, app.renderer, "cover"); crtFilter.seed = Math.random() * 10; crtFilter.time += ticker.deltaTime * 0.3; }); }} > <Sprite label="Sky" texture={Assets.get<Pixi.Texture>("sky")} filters={blurFilter} /> <Ground movementSpeed={appStore.state.isRunning ? 1.3 : 0} direction={appStore.state.direction} width={sceneBounds.width} height={sceneBounds.height * 0.3} position={{ x: 0, y: sceneBounds.height * 0.7 }} /> <Character direction={appStore.state.direction} isRunning={appStore.state.isRunning} position={{ x: sceneBounds.width * 0.5, y: sceneBounds.height * 0.5 }} /> </Container> </PixiStage> </Show> </PixiCanvas> </PixiApplication> </div> );};import { createStore } from "solid-js/store";
export type AppStore = { state: Readonly<AppState>; toggleRunning: () => void; toggleDirection: () => void;};
export type AppState = { isRunning: boolean; direction: Direction;};
export type Direction = "left" | "right";
export const createAppStore = (): AppStore => { const [state, setState] = createStore<AppState>({ isRunning: true, direction: "right", });
const toggleRunning = () => { setState("isRunning", (value) => !value); };
const toggleDirection = () => { setState("direction", (value) => (value === "left" ? "right" : "left")); };
return { state, toggleRunning, toggleDirection, };};import type * as Pixi from "pixi.js";import { Assets } from "pixi.js";import { onTick, TilingSprite } from "pixi-solid";import { splitProps } from "solid-js";
export type GroundProps = Omit<Pixi.TilingSpriteOptions, "children"> & { movementSpeed: number; direction: "left" | "right";};
export const Ground = (props: GroundProps) => { const [, pixiProps] = splitProps(props, ["movementSpeed", "direction"]); return ( <TilingSprite {...pixiProps} label="Ground" ref={(tileRef) => { onTick((ticker) => { tileRef.tilePosition.x += props.movementSpeed * ticker.deltaTime * (props.direction === "left" ? 1 : -1); }); }} texture={Assets.get<Pixi.Texture>("ground")} /> );};import type * as Pixi from "pixi.js";import { AnimatedSprite, Container } from "pixi-solid";import { createEffect, Match, Switch, splitProps } from "solid-js";import { getIdleAnimationTextures, getRunAnimationTextures } from "./get-animation-textures";
export type CharacterProps = Omit<Pixi.ContainerOptions, "children"> & { direction: "left" | "right"; isRunning: boolean;};
export const Character = (props: CharacterProps) => { const [, pixiProps] = splitProps(props, ["isRunning", "direction"]);
return ( <Container {...pixiProps} label={"Character"}> <Switch> <Match when={props.isRunning}> <AnimatedSprite autoPlay={props.isRunning} ref={(instance) => { createEffect(() => { if (props.isRunning) { instance.play(); } else { instance.stop(); } }); }} textures={getRunAnimationTextures()} scale={{ x: props.direction === "left" ? -1 : 1, y: 1 }} animationSpeed={0.25} anchor={{ x: 0.5, y: 0.5 }} /> </Match> <Match when={!props.isRunning}> <AnimatedSprite autoPlay={true} textures={getIdleAnimationTextures()} scale={{ x: props.direction === "left" ? -1 : 1, y: 1 }} animationSpeed={0.25} anchor={{ x: 0.5, y: 0.5 }} /> </Match> </Switch> </Container> );};import type * as Pixi from "pixi.js";import { Assets } from "pixi.js";
export const getRunAnimationTextures = () => [ Assets.get<Pixi.Texture>("run_01"), Assets.get<Pixi.Texture>("run_02"), Assets.get<Pixi.Texture>("run_03"), Assets.get<Pixi.Texture>("run_04"), Assets.get<Pixi.Texture>("run_05"), Assets.get<Pixi.Texture>("run_06"),];
export const getIdleAnimationTextures = () => [ Assets.get<Pixi.Texture>("idle_01"), Assets.get<Pixi.Texture>("idle_02"), Assets.get<Pixi.Texture>("idle_03"), Assets.get<Pixi.Texture>("idle_03"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_04"), Assets.get<Pixi.Texture>("idle_05"), Assets.get<Pixi.Texture>("idle_06"), Assets.get<Pixi.Texture>("idle_07"), Assets.get<Pixi.Texture>("idle_07"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"), Assets.get<Pixi.Texture>("idle_08"),];import { Assets, TextureStyle } from "pixi.js";import groundAssetUrl from "@/assets/ground-tile.png";import idleAssetUrl_01 from "@/assets/idle_01.png";import idleAssetUrl_02 from "@/assets/idle_02.png";import idleAssetUrl_03 from "@/assets/idle_03.png";import idleAssetUrl_04 from "@/assets/idle_04.png";import idleAssetUrl_05 from "@/assets/idle_05.png";import idleAssetUrl_06 from "@/assets/idle_06.png";import idleAssetUrl_07 from "@/assets/idle_07.png";import idleAssetUrl_08 from "@/assets/idle_08.png";import runAssetUrl_01 from "@/assets/run_01.png";import runAssetUrl_02 from "@/assets/run_02.png";import runAssetUrl_03 from "@/assets/run_03.png";import runAssetUrl_04 from "@/assets/run_04.png";import runAssetUrl_05 from "@/assets/run_05.png";import runAssetUrl_06 from "@/assets/run_06.png";import skyAssetUrl from "@/assets/sky.png";
export const loadSceneAssets = async () => { TextureStyle.defaultOptions.scaleMode = "nearest";
try { await Assets.load([ { alias: "sky", src: skyAssetUrl }, { alias: "ground", src: groundAssetUrl }, { alias: "run_01", src: runAssetUrl_01 }, { alias: "run_02", src: runAssetUrl_02 }, { alias: "run_03", src: runAssetUrl_03 }, { alias: "run_04", src: runAssetUrl_04 }, { alias: "run_05", src: runAssetUrl_05 }, { alias: "run_06", src: runAssetUrl_06 }, { alias: "idle_01", src: idleAssetUrl_01 }, { alias: "idle_02", src: idleAssetUrl_02 }, { alias: "idle_03", src: idleAssetUrl_03 }, { alias: "idle_04", src: idleAssetUrl_04 }, { alias: "idle_05", src: idleAssetUrl_05 }, { alias: "idle_06", src: idleAssetUrl_06 }, { alias: "idle_07", src: idleAssetUrl_07 }, { alias: "idle_08", src: idleAssetUrl_08 }, ]); return true; } catch { return false; }};import style from "./style.module.css";
export type ControlsProps = { isRunning: boolean; direction: "left" | "right"; onToggleRunningClicked: () => void; onToggleDirectionClicked: () => void;};
export const Controls = (props: ControlsProps) => { return ( <div class={style["controls-wrap"]}> <button type="button" class={style["controls-button"]} onClick={props.onToggleRunningClicked}> {props.isRunning ? "Click to stop" : "Click to run"} </button> <br /> <button type="button" class={style["controls-button"]} onClick={props.onToggleDirectionClicked}> {props.direction === "left" ? "Click to face right" : "Click to face left"} </button> </div> );};.controls-wrap { position: absolute; z-index: 1; top: 10px; left: 10px; display: flex; gap: 10px;}
.controls-button { margin: 0; cursor: pointer;}