https://github.dsrkafuu.net/sakana-widget/
Add the Sakana! Widget to your own web page! Support custom images, auto resizing and more runtime params!
- Sizes:
~60 kB ALL-IN-ONE JS(includes 2 built-in characters, brotli compressed) - Register and use your own character
- Auto resizing support, 120px minimum
- Press and hold the stand and drag, after releasing the hand the stand will bounce in the opposite direction
- Use control bar to switch roles and use other functions
- Automatic mode, applying a force of random size at random intervals
- Prepared for CDN/NPM import, custom parameters, chained calls
Use Bun 1.4.2 to install dependencies and run the project:
bun install --frozen-lockfile
bun run dev
bun run lint
bun run typecheck
bun run test
bun run buildbuild runs the library, UMD bundle, and documentation builds in order.
mount() appends the widget to the target element and keeps its existing children and listeners. unmount() removes only the widget.
First you need to import the module, either directly using a CDN or by installing it as an NPM package:
<!-- https://cdn.jsdelivr.net/npm/sakana-widget@4.0.0/lib/index.umd.min.js -->
<!-- https://cdnjs.cloudflare.com/ajax/libs/sakana-widget/4.0.0/index.umd.min.js -->
<div id="sakana-widget"></div>
<script>
function initSakanaWidget() {
new SakanaWidget().mount('#sakana-widget');
}
</script>
<script
async
onload="initSakanaWidget()"
src="https://cdn.jsdelivr.net/npm/sakana-widget@4.0.0/lib/index.umd.min.js"
></script>// npm install --save sakana-widget
// manual css import is no longer needed since it is imported in the js entry
// import 'sakana-widget/lib/index.css';
import SakanaWidget from 'sakana-widget';
new SakanaWidget().mount('#sakana-widget');This package exports a class SakanaWidget by default, through which a widget can be initialized. The code above initializes a widget with default settings and mounts it to the #sakana-widget element.
The default entry includes both built-in characters. For an ESM bundle that includes only selected character images, import the core and register each character explicitly:
import SakanaWidget from 'sakana-widget/core';
import chisato from 'sakana-widget/characters/chisato';
SakanaWidget.registerCharacter('chisato', chisato);
new SakanaWidget().mount('#sakana-widget');Import sakana-widget/characters/takina and register it the same way if needed. The core contains no built-in character images; custom characters can be registered without importing either image. Registration must happen before creating a widget. The default entry and UMD bundle retain the two built-in characters for existing usage.
After registering a character with a tall or wide image, set imageFit: 'contain' to display the whole image inside the widget's square image area. The default cover fills the area and may crop a non-square image.
new SakanaWidget({ character: 'custom', imageFit: 'contain' }).mount('#sakana-widget');You can continue to create widget instances and mount to more DOM elements, where the data is completely independent between widgets except for roles, and non-static methods support chaining calls.
For example, you can modify some settings before mounting a widget and get a super-slow Chisato:
new SakanaWidget().setState({ i: 0.001, d: 1 }).mount('#sakana-widget');Or, get the built-in character object via the getCharacter static method, modify the parameters, and create a super-slow, non-damped (perpetual) Takina as a new character:
const takina = SakanaWidget.getCharacter('takina');
takina.initialState = {
...takina.initialState,
i: 0.001,
d: 1,
};
SakanaWidget.registerCharacter('takina-slow', takina);
new SakanaWidget({ character: 'takina-slow' }).mount('#sakana-widget');Of course, you can use your own image (url or base64) as a character, e.g. GitHub's icon:
const github = SakanaWidget.getCharacter('chisato');
github.image = `https://raw.githubusercontent.com/dsrkafuu/sakana-widget/main/docs/github.png`;
SakanaWidget.registerCharacter('github', github);
new SakanaWidget({ character: 'github' }).mount('#sakana-widget');Enable saveState: true to persist the widget's hidden state via localStorage. When enabled, clicking the close button hides the widget via display: none and saves 'hide'. On next page load, the widget automatically restores the hidden state.
A hidden widget can be revived by calling the show() instance method from a button elsewhere on the page.
Use addStateListener to react to visibility changes. Listeners fire once on mount and every time the widget is hidden or shown.
const widget = new SakanaWidget({ saveState: true })
.addStateListener((state) => {
console.log(`widget is now ${state}`);
})
.mount('#sakana-widget');
// Bring back the widget from a custom button
document.getElementById('sakana-revive').addEventListener('click', () => {
widget.show();
});To use multiple widgets on the same page, provide a unique stateKey for each instance to avoid conflicts:
new SakanaWidget({ saveState: true, stateKey: 'sakana-status-chisato' }).mount('#chisato');
new SakanaWidget({ saveState: true, stateKey: 'sakana-status-takina' }).mount('#takina');When saveState is false (default), clicking close permanently unmounts the widget.
See the API section below for detailed parameters and class type.
Set autoFit: true when initializing the component and it will automatically scale according to the size of its mount container, minimum 120px.
Note that to turn on auto-scaling you need to make sure the mounting container is a BFC, the easiest way to do this is to set position: relative or position: fixed. Components in auto-scaling mode additionally add a wrapper container between the app and the mount container, and set its width and height to 100% by which the actual size is detected, so the BFC is required.
Add a button to one widget's control bar before or after mounting it:
const icon = document.createElement('img');
icon.src = '/icons/notes.svg';
icon.alt = '';
const widget = new SakanaWidget()
.addControl({
id: 'notes',
label: 'Open notes',
icon,
onClick: () => console.log('Open notes'),
})
.mount('#sakana-widget');
widget.removeControl('notes');An icon DOM element is required. The widget clones it; label is used only as the button's accessible name (aria-label) and optional title, never as visible text. Custom controls are instance-specific and are hidden when controls: false. Listeners are detached on unmount and restored on remount.
export interface SakanaWidgetState {
/**
* inertia
*/
i: number;
/**
* stickiness
*/
s: number;
/**
* decay
*/
d: number;
/**
* angle
*/
r: number;
/**
* height
*/
y: number;
/**
* vertical speed
*/
t: number;
/**
* horizontal speed
*/
w: number;
}
export interface SakanaWidgetCharacter {
image: string;
initialState: SakanaWidgetState;
}
export interface SakanaWidgetControl {
id: string;
label: string;
icon: Element;
onClick: (widget: SakanaWidget, event: MouseEvent) => void;
}
export type SakanaWidgetVisibility = 'show' | 'hide';export interface SakanaWidgetOptions {
/**
* widget size, default to `200`
*/
size?: number;
/**
* auto fit size (120px minimum), default to `false`
*/
autoFit?: boolean;
/**
* default character, default to `chisato`
*/
character?: string;
/**
* how the character image fits its square area, default to `cover`
*/
imageFit?: 'cover' | 'contain';
/**
* controls bar, default to `true`
*/
controls?: boolean;
/**
* show spring rod, default to `true`
*/
rod?: boolean;
/**
* character draggable, default to `true`
*/
draggable?: boolean;
/**
* rod color and width, default to `#b4b4b4` & `10`
*/
stroke?: {
color?: string;
width?: number;
};
/**
* motion stop threshold, default to `0.1`
*/
threshold?: number;
/**
* rotate origin, default to `0`
*/
rotate?: number;
/**
* enable accessibility title feature, default to `false`
*/
title?: boolean;
/**
* enable persistent hide state via localStorage, default to `false`
*/
saveState?: boolean;
/**
* localStorage key for persist state, default to `sakana-widget-status`
*/
stateKey?: string;
}class SakanaWidget {
/**
* get data of a registered character
*/
static getCharacter(name: string): SakanaWidgetCharacter | null;
/**
* get all registered character
*/
static getCharacters();
/**
* registered a new character
*/
static registerCharacter(name: string, character: SakanaWidgetCharacter);
/**
* set current state of widget
*/
setState(state: Partial<SakanaWidgetState>);
/**
* set current character of widget
*/
setCharacter(name: string);
/**
* set to next character of widget
*/
nextCharacter();
/**
* add or remove a custom control for this widget
*/
addControl(control: SakanaWidgetControl): this;
removeControl(id: string): this;
/**
* switch the auto mode
*/
triggerAutoMode();
/**
* mount the widget
*/
mount(el: HTMLElement | string);
/**
* unmount the widget
*/
unmount();
/**
* hide the widget (stops animation, persists state if saveState is enabled)
*/
hide();
/**
* show the widget if previously hidden
*/
show();
/**
* add a listener for widget visibility changes (fires once on mount)
*/
addStateListener(listener: (state: SakanaWidgetVisibility) => void): this;
/**
* remove a previously added state listener
*/
removeStateListener(listener: (state: SakanaWidgetVisibility) => void): this;
}Released under MIT License, please note that the 2 default images should not be used for any commercial activities. This project used to be a secondary development based on https://github.com/itorr/sakana.