A declarative osu! storyboard library with zero dependencies and zero configurations.
npm i @osbjs/tiny-osbjs@latest
The recommended way to create a storyboard project is using create-tiny-sb
:
npm create tiny-sb
It also comes with some prebuilt components!
It's strongly recommended to use TypeScript
and a text editor/IDE with good TypeScript
support like VSCode
for better developing experience.
npm install -D typescript
Install tsx
as a global package so you can use it anywhere (you will only need to do this once), or as a devDependency.
tsx
is a CLI command (alternative to node) that allows you to run TypeScript/JavaScript right in the terminal. It also watches for file changes to speed up your development.
npm i -g tsx
or
npm i -D tsx
Then add this script to your package.json:
//...
"scripts": {
"start": "tsx watch index.js"
// or if you are using TypeScript
"start": "tsx watch index.ts"
}
index.js
(index.ts
) is the entry point to your storyboard. This way when you run npm start
it will automatically rebuild your storyboard when you change your code.
Note: The example below will be written in TypeScript/ES Module syntax.
Before you do anything, you have to create a storyboard context and tell the library to use it. This context is shared accross the whole project.
import { createContext, useContext } from '@osbjs/tiny-osbjs'
const context = createContext()
useContext(context)
Then you can start creating storyboard objects.
import { createSprite, Layer, Origin } from 'tiny-osbjs'
createSprite('test.png', Layer.Background, Origin.Centre, [320, 240], () => {})
If you want to create a storyboard for each difficulty, you must specify the context at the entry point of each storyboard.
// difficulty1.ts
import { createContext, useContext } from '@osbjs/tiny-osbjs'
export default function difficulty1Storyboard() {
const context = createContext()
useContext(context)
// createSprite...
}
// difficulty2.ts
import { createContext, useContext } from '@osbjs/tiny-osbjs'
export default function Difficulty2() {
const context = createContext()
useContext(context)
// createSprite...
}
// index.ts
import Difficulty1 from './difficulty1'
import Difficulty2 from './difficulty2'
difficulty1Storyboard()
difficulty2Storyboard()
Most of the commands will have their syntax looking like this (except for a few special commands):
commandName([startTime, endTime], startValue, endValue, easing) // command that changes overtime
commandName([startTime, endTime], value) // command that is only effective in a specific time range
commandName(time, value) // command that only need to be set once
The killer-feature of tiny-osbjs
is that you can specify the commands in a declarative way and the library will know which objects they are refering to.
import { createSprite, Layer, Origin, fade, loop } from '@osbjs/tiny-osbjs'
createSprite('test.png', Layer.Background, Origin.Centre, [320, 240], () => {
fade([0, 1000], 0, 1, Easing.Out) // refers to sprite
loop(3000, 5, () => {
fade([0, 1000], 0, 1, Easing.Out) // refers to loop
})
})
You can pass osu timestamp to the start time/end time of the command and the library will try to parse it.
import { createSprite, Layer, Origin, fade, loop } from '@osbjs/tiny-osbjs'
createSprite('test.png', Layer.Background, Origin.Centre, [320, 240], () => {
fade([0, "00:00:015"], 0, 1) // this works
})
Even though it isn't enforced, you should split your storyboard into multiple components.
// components/Background.ts
import { createSprite, fade, Layer, Origin } from '@osbjs/tiny-osbjs'
export default function Background(startTime: number, endTime: number) {
createSprite('bg.jpg', Layer.Background, Origin.Centre, [320, 240], () => {
fade([startTime, endTime], 1)
})
}
// index.ts
import Background from './components/Background'
Background(0, 30000)
Finally, you can generate the osb string of the storyboard. You can use that string to write to your osb file.
import { generateStoryboardOsb } from '@osbjs/tiny-osbjs'
import fs from 'fs'
fs.writeFileSync('Artist - Song (Creator).osb', generateStoryboardOsb(), 'utf8')
Your final storyboard will look like this:
// components/Background.ts
import { createSprite, fade, Layer, Origin } from '@osbjs/tiny-osbjs'
export default function Background(startTime: number, endTime: number) {
createSprite('bg.jpg', Layer.Background, Origin.Centre, [320, 240], () => {
fade([startTime, endTime], 1)
})
}
// index.ts
import { createContext, generateStoryboardOsb, useContext } from '@osbjs/tiny-osbjs'
import fs from 'fs'
import Background from './components/Background'
const context = createContext()
useContext(context)
Background(0, 30000)
fs.writeFileSync('Artist - Song (Creator).osb', generateStoryboardOsb(), 'utf8')
If you ran into any issues or need help, contact Nanachi#1381
on discord.
// [r, g, b] respectively
type Color = [number, number, number]
// [x, y] respectively
type Vector2 = [number, number]
// ex: 01:29:345
type Timestamp = `${number}:${number}:${number}`
type Time = Timestamp | number
// represent start time/end time
type TimeRange = [Time, Time]
// osu storyboard layer
enum Layer {
Background = 'Background',
Foreground = 'Foreground',
Fail = 'Fail',
Pass = 'Pass',
Overlay = 'Overlay',
}
// origin of the sprite/animation
enum Origin {
TopLeft = 'TopLeft',
TopCentre = 'TopCentre',
TopRight = 'TopRight',
CentreRight = 'CentreRight',
Centre = 'Centre',
CentreLeft = 'CentreLeft',
BottomLeft = 'BottomLeft',
BottomCentre = 'BottomCentre',
BottomRight = 'BottomRight',
}
// see https://easings.net/en
enum Easing {
Linear,
Out,
In,
InQuad,
OutQuad,
InOutQuad,
InCubic,
OutCubic,
InOutCubic,
InQuart,
OutQuart,
InOutQuart,
InQuint,
OutQuint,
InOutQuint,
InSine,
OutSine,
InOutSine,
InExpo,
OutExpo,
InOutExpo,
InCirc,
OutCirc,
InOutCirc,
InElastic,
OutElastic,
OutElasticHalf,
OutElasticQuarter,
InOutElastic,
InBack,
OutBack,
InOutBack,
InBounce,
OutBounce,
InOutBounce,
}
function createContext(): Context
Create a new context.
function useContext(context: Context)
Specify the context of the storyboard.
function createBackground(path: string)
Create a new Background image. You should only use this if you are generating a storyboard for a specific osu difficulty.
function createVideo(path: string, offset: number)
Create a new Video. You should only use this if you are generating a storyboard for a specific osu difficulty.
function createSample(
startTime: number,
layer: SampleLayer,
path: AudioPath,
volume: number
)
type AudioPath = `${string}.mp3` | `${string}.ogg` | `${string}.wav`
enum SampleLayer {
Background,
Fail,
Pass,
Foreground,
}
Create a new Sample.
function createSprite(
path: string,
layer: Layer,
origin: Origin,
initialPosition: Vector2,
invokeFunction: () => void
)
Create a new Sprite. All commands must be called inside the invoke function.
function createAnimation(
path: string,
layer: Layer,
origin: Origin,
initialPosition: Vector2,
frameCount: number,
frameDelay: number,
loopType: LoopType,
invokeFunction: () => void
)
enum LoopType {
Forever = 'LoopForever',
Once = 'LoopOnce',
}
Create a new Animation. All commands must be called inside the invoke function.
function generateStoryboardOsb(): string
Generate string that can be used to create .osb file.
function replaceOsuEvents(parsedOsuDifficulty: string): string
Returns .osu file after replacing [Events] section with events generated from storyboard.
function color(time: Time | TimeRange, startColor: Color, endColor: Color = startColor, easing?: Easing)
The virtual light source colour on the object. The colours of the pixels on the object are determined subtractively.
function fade(time: Time | TimeRange, startOpacity: number, endOpacity: number = startOpacity, easing?: Easing)
Change the opacity of the object.
function move(time: Time | TimeRange, startPosition: Vector2, endPosition: Vector2 = startPosition, easing?: Easing)
Change the location of the object in the play area.
function moveX(time: Time | TimeRange, startX: number, endX: number = startX, easing?: Easing)
Change the x coordinate of the object.
function moveY(time: Time | TimeRange, startY: number, endY: number = startY, easing?: Easing)
Change the y coordinate of the object.
function rotate(time: Time | TimeRange, startAngle: number, endAngle: number = startAngle, easing?: Easing)
Change the amount an object is rotated from its original image, in radians, clockwise.
function scale(time: Time | TimeRange, startScaleFactor: number, endScaleFactor: number = startScaleFactor, easing?: Easing)
Change the size of the object relative to its original size.
function scaleVec(time: Time | TimeRange, startScaleVector: Vector2, endScaleVector: Vector2 = startScaleVector, easing?: Easing)
Change the size of the object relative to its original size, but X and Y scale separately.
function flipHorizontal(time: TimeRange)
Flip the image horizontally.
function flipVertical(time: TimeRange)
Flip the image vertically.
function additiveBlending(time: TimeRange)
Use additive-colour blending instead of alpha-blending
function loop(startTime: Time, count: number, invokeFunction: () => void)
Create a loop group.
Loops can be defined to repeat a set of events constantly for a set number of iterations. Note that events inside a loop should be timed with a zero-base. This means that you should start from 0ms for the inner event's timing and work up from there. The loop event's start time will be added to this value at game runtime.
function trigger(time: TimeRange, triggerType: TriggerType, invokeFunction: () => void)
type TriggerType = `HitSound${SampleSet}${SampleSet}${Addition}${number | ''}`
enum SampleSet {
None = '',
All = 'All',
Normal = 'Normal',
Soft = 'Soft',
Drum = 'Drum',
}
enum Addition {
None = '',
Whistle = 'Whistle',
Finish = 'Finish',
Clap = 'Clap',
}
Create a trigger group.
Trigger loops can be used to trigger animations based on play-time events. Although called loops, trigger loops only execute once when triggered.Trigger loops are zero-based similar to normal loops. If two overlap, the first will be halted and replaced by a new loop from the beginning. If they overlap any existing storyboarded events, they will not trigger until those transformations are no in effect.
function makeTriggerType(sampleSet: SampleSet, additionsSampleSet: SampleSet, addition: Addition, customSampleSet?: number): TriggerType
Helper to create TriggerType
function degToRad(deg: number): number
Convert degrees to radians.
function radToDeg(rad: number): number
Convert radians to degrees.
function randInt(min: number, max: number, seed?: number | string)
Random integer in the interval [min, max].
function randFloat(min: number, max: number, seed?: number | string)
Random float in the interval [min, max].
Note that the same seed will always return the same value. Leaving it empty will result in a true random value.
function addVec(v1: Vector2, v2: Vector2): Vector2
Adds 2 vectors and returns a new vector.
function subVec(v1: Vector2, v2: Vector2): Vector2
Subtracts 2nd vector from 1st vector and returns a new vector.
function mulVec(v1: Vector2, v2: Vector2): Vector2
Multiplies 2 vectors and returns a new vector.
function mulVecScalar(v: Vector2, s: number): Vector2
Multiplies both x and y with a specified scalar and returns a new vector.
function addVecScalar(v: Vector2, s: number): Vector2
Adds a scalar to both x and y and returns a new vector.
function dotVec(v1: Vector2, v2: Vector2): number
Returns the dot product of 2 vectors.
function crossVec(v1: Vector2, v2: Vector2): number
Returns the cross product of 2 vectors.
function lengthSqrVec(v: Vector2): number
Returns the length squared of the vector.
function lengthVec(v: Vector2): number
Returns the length of the vector.
function areEqualVecs(v1: Vector2, v2: Vector2): boolean
Check if 2 vectors are equals.
function normalizeVec(v: Vector2): Vector2
Return a vector with the same direction but its length equals 1.
function cloneVec(v: Vector2): Vector2
Return a new Vector with the same x and y with the specified vector.
function interpolateVec(v1: Vector2, v2: Vector2, alpha: number): Vector2
Performs a linear interpolation between two vectors based on the given weighting, alpha = 0 will be v1 and alpha = 1 will be v2.
function reportBuildTime(sb: (end: () => void) => void)
Print to console how long it takes to generate the storyboard. Call end()
once you have done everything.
function interpolate(input: number, inputRange: [number, number], outputRange: [number, number], easing: Easing = Easing.Linear): number
Map a value from an input range to an output range. This will clamp the result if the input is outside of the input range.
function hexToRgb(hex: string): Color
Convert hex color code to RGB color.
You can use the DefaultPallete
constant to access the predefined colors if you are not sure which colors to use.
All colors are picked up from here.
const DefaultPallete: { [key: string]: Color }
function HideBg(path: string)
Hide the background image. This is a component so you need to call this after you have specified the storyboard context with useContext
.
const WIDTH = 854
const HEIGHT = 480
const LEGACY_WIDTH = 640
const MIN_X = -107
const MAX_X = 747
const MIN_Y = 0
const MAX_Y = 480
A few constants you can use to get the storyboard dimensions quickly.
- @osbjs/spectrum-tiny-osbjs - Get spectrum data used to create spectrum effect.
- @osbjs/hitobjects-tiny-osbjs - Get hit objects position used to create highlight effect.
- @osbjs/txtgen-tiny-osbjs - Generate text images, commonly used for creating lyrics.