shaku
Version:
A simple and effective JavaScript game development framework that knows its place!
526 lines (500 loc) • 20 kB
HTML
<html>
<head>
<meta charset="UTF-8">
<title>Shaku</title>
<meta name="description" content="Shaku - a simple and easy-to-use javascript library for videogame programming.">
<meta name="author" content="Ronen Ness">
<link href="css/style.css" rel="stylesheet" type="text/css" media="all">
</head>
<body>
<div style="overflow-y:scroll; border: 1px black solid; width:480px; position:fixed; height:100%; right:0px; top:0px; padding:1em;">
<p id="general-errors" style="color:red"></p>
<h2>Skeleton Structure</h2>
<textarea id="skeleton-textarea" rows="20" style="width: 100%; resize:vertical; cursor:text"></textarea>
<p id="skeleton-textarea-errors" style="color:red"></p>
<h2>Animation</h2>
<textarea id="animation-textarea" rows="20" style="width: 100%; resize:vertical; cursor:text"></textarea>
<p id="animation-textarea-errors" style="color:red"></p>
<h2>Skin</h2>
<button id="set-skin-bones" type="button" class="btn btn-primary">Skeleton</button>
<button id="set-skin-barb" type="button" class="btn btn-primary">Barberian</button>
<button id="set-skin-mix" type="button" class="btn btn-primary">Mix!</button>
<textarea id="skin-textarea" rows="20" style="width: 100%; resize:vertical; cursor:text"></textarea>
<p id="skin-textarea-errors" style="color:red"></p>
<br /><br /><br />
</div>
<div class="noselect">
<div style="padding:0.5em">
<h1 class="demo-title" style="margin-left: 0;">Shaku Gfx Demo: Skeleton Animation</h1>
</div>
<p>This demo demonstrate basic skeleton-based animation with Shaku.<br />
Check out <b>js/skeleton_animator.js</b> for a tiny skeleton animating utility.</p>
<!-- include shaku -->
<script src="js/demos.js"></script>
<script src="js/shaku.js"></script>
<script src="js/skeleton_animator.js"></script>
<!-- demo code -->
<script>
async function runDemo()
{
// init shaku
await Shaku.init();
// screen size
let screenX = 800;
let screenY = 600;
// load textures
let textures = {
'bones.png': await Shaku.assets.loadTexture('assets/bones.png'),
'barb.png': await Shaku.assets.loadTexture('assets/barb.png'),
};
// build the skeleton animation data
var skeleton =
{
name: 'root',
children: [
{
name: 'abdomen',
rotation: 90,
order: 0,
children: [
{
name: 'crotch',
children: [
{
name: 'leftHipJoint',
constLength: 2,
rotation: 90,
children: [
{
rotation: -90,
name: 'leftLeg',
children: [
{
name: 'leftFoot',
rotation: 20
}
]
}
]
},
{
name: 'rightHipJoint',
constLength: 2,
rotation: -90,
children: [
{
rotation: 60,
name: 'rightLeg',
order: -1,
children: [
{
order: -2,
name: 'rightFoot',
rotation: 30
}
]
}
]
}
]
}
]
},
{
name: 'chest',
rotation: -90,
order: 0,
children: [
{
name: 'head',
order: 1,
},
{
name: 'leftShoulder',
constLength: 5,
rotation: -100,
children: [
{
name: 'leftArm',
rotation: -60,
order: 12,
spriteLengthComponent: 'height',
children: [
{
name: 'leftHand',
rotation: -70,
order: 14,
children: [{
origin: 0.9,
order: 13,
name: 'sword'
}]
}
]
}
]
},
{
name: 'rightShoulder',
constLength: 5,
rotation: 100,
children: [
{
name: 'rightArm',
rotation: 60,
order: -1,
spriteLengthComponent: 'height',
children: [
{
name: 'rightHand',
rotation: -60,
order: -2,
children: [{
origin: 0.9,
order: -3,
name: 'sword'
}]
}
]
}
]
},
]
},
]
};
// animation to play
var animation = [
{
"duration": 1,
"transitionToNext": "linear",
"transformations": {
"root/chest": {
"rotation": 5
},
"root/chest/rightShoulder/rightArm": {
"rotation": -20
},
"root/chest/leftShoulder/leftArm": {
"rotation": 10
}
}
},
{
"duration": 1,
"transitionToNext": "quadLerp",
"transformations": {
"root/chest/head": {
"rotation": -5
},
"root/abdomen/crotch/leftHipJoint/leftLeg": {
"rotation": 10
},
"root/abdomen/crotch/leftHipJoint/leftLeg/leftFoot": {
"rotation": -10
},
"root/abdomen/crotch/rightHipJoint/rightLeg": {
"rotation": 10
},
"root/abdomen/crotch/rightHipJoint/rightLeg/rightFoot": {
"rotation": -10
}
}
}
];
// build the skeleton skin - the sprites we attach to bones.
const bonesSkin = {
head: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(0, 0, 8, 8),
boneLength: 8,
origin: new Shaku.utils.Vector2(0.5, 1),
rotation: 90
},
chest: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(8, 0, 10, 8),
boneLength: 8,
origin: new Shaku.utils.Vector2(0.45, 1),
rotation: 90
},
abdomen: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(1, 18, 6, 4),
boneLength: 3,
origin: new Shaku.utils.Vector2(0.5, 0),
rotation: -90
},
leftArm: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(0, 9, 3, 6),
boneLength: 6,
origin: new Shaku.utils.Vector2(0.5, 0),
rotation: -90
},
rightArm: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(3, 9, 3, 6),
boneLength: 6,
origin: new Shaku.utils.Vector2(0.5, 0),
rotation: -90
},
leftHand: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(6, 9, 4, 7),
boneLength: 7,
origin: new Shaku.utils.Vector2(0.5, 0),
rotation: -90
},
rightHand: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(12, 9, 4, 7),
boneLength: 7,
origin: new Shaku.utils.Vector2(0.5, 0),
rotation: -90
},
crotch: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(9, 17, 7, 5),
boneLength: 4,
origin: new Shaku.utils.Vector2(0.5, 0),
rotation: -90
},
sword: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(0, 43, 18, 5),
origin: new Shaku.utils.Vector2(0.1, 0.5),
boneLength: 18,
rotation: -90
},
leftLeg: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(1, 24, 3, 5),
boneLength: 5,
origin: new Shaku.utils.Vector2(0.5, 0),
rotation: -90
},
rightLeg: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(6, 24, 3, 5),
boneLength: 5,
origin: new Shaku.utils.Vector2(0.5, 0),
rotation: -90
},
leftFoot: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(1, 30, 6, 13),
boneLength: 13,
origin: new Shaku.utils.Vector2(0.4, 0),
rotation: -90
},
rightFoot: {
texture: 'bones.png',
sourceRect: new Shaku.utils.Rectangle(9, 30, 6, 13),
boneLength: 13,
origin: new Shaku.utils.Vector2(0.4, 0),
rotation: -90
},
};
// build the barberian skin - the sprites we attach to bones.
const barberianSkin = {
head: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(10, 33, 5, 6),
boneLength: 6,
origin: new Shaku.utils.Vector2(0.5, 1),
rotation: 90
},
chest: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(2, 0, 11, 9),
boneLength: 8,
origin: new Shaku.utils.Vector2(0.45, 0.9),
rotation: 90
},
abdomen: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(0, 17, 7, 5),
boneLength: 3,
origin: new Shaku.utils.Vector2(0.4, 0.1),
rotation: -90
},
leftArm: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(1, 9, 5, 7),
boneLength: 6,
origin: new Shaku.utils.Vector2(0.5, 0.1),
rotation: -90
},
rightArm: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(10, 9, 5, 7),
boneLength: 6,
origin: new Shaku.utils.Vector2(0.5, 0.1),
rotation: -90
},
leftHand: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(13, 24, 3, 8),
boneLength: 8,
origin: new Shaku.utils.Vector2(0.5, 0),
rotation: -90
},
rightHand: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(9, 24, 3, 8),
boneLength: 8,
origin: new Shaku.utils.Vector2(0.5, 0),
rotation: -90
},
crotch: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(8, 18, 8, 3),
boneLength: 2,
origin: new Shaku.utils.Vector2(0.47, 0),
rotation: -90
},
sword: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(0, 43, 18, 5),
origin: new Shaku.utils.Vector2(0.1, 0.5),
boneLength: 18,
rotation: -90
},
leftLeg: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(0, 23, 4, 8),
boneLength: 7,
origin: new Shaku.utils.Vector2(0.6, 0),
rotation: -90
},
rightLeg: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(5, 23, 4, 8),
boneLength: 7,
origin: new Shaku.utils.Vector2(0.6, 0),
rotation: -90
},
leftFoot: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(0, 31, 4, 11),
boneLength: 11,
origin: new Shaku.utils.Vector2(0.6, 0),
rotation: -90
},
rightFoot: {
texture: 'barb.png',
sourceRect: new Shaku.utils.Rectangle(5, 31, 4, 11),
boneLength: 11,
origin: new Shaku.utils.Vector2(0.4, 0),
rotation: -90
},
};
// set default skin to skeleton
var skin = Math.random() < 0.5 ? barberianSkin : bonesSkin;
// fill textareas
document.getElementById('skeleton-textarea').value = JSON.stringify(skeleton, null, 2);
document.getElementById('animation-textarea').value = JSON.stringify(animation, null, 2);
document.getElementById('skin-textarea').value = JSON.stringify(skin, null, 2);
// button to set skeleton skin
document.getElementById('set-skin-bones').onclick = () => {
document.getElementById('skin-textarea').value = JSON.stringify(bonesSkin, null, 2);
document.getElementById('skin-textarea').onchange();
}
document.getElementById('set-skin-barb').onclick = () => {
document.getElementById('skin-textarea').value = JSON.stringify(barberianSkin, null, 2);
document.getElementById('skin-textarea').onchange();
}
document.getElementById('set-skin-mix').onclick = () => {
const mixed = {};
for (let key in bonesSkin) {
mixed[key] = Math.random() < 0.5 ? barberianSkin[key] : bonesSkin[key];
}
document.getElementById('skin-textarea').value = JSON.stringify(mixed, null, 2);
document.getElementById('skin-textarea').onchange();
}
// create skeleton animator
var animator;
function updateAnimator()
{
const scale = 10;
animator = new SkeletonRenderer(skeleton, animation, skin, scale);
animator.position.set(400, 300);
}
updateAnimator();
// add callbacks to update skeleton from textareas
document.getElementById('skeleton-textarea').onkeyup = document.getElementById('skeleton-textarea').onchange = function() {
try {
skeleton = JSON.parse(this.value); updateAnimator();
this.style.color = 'black';
document.getElementById('skeleton-textarea-errors').innerHTML = "";
}
catch (e) { this.style.color = 'red'; document.getElementById('skeleton-textarea-errors').innerHTML = e; }
}
document.getElementById('animation-textarea').onkeyup = document.getElementById('animation-textarea').onchange = function() {
try {
animation = JSON.parse(this.value); updateAnimator();
this.style.color = 'black';
document.getElementById('animation-textarea-errors').innerHTML = "";
}
catch (e) { this.style.color = 'red'; document.getElementById('animation-textarea-errors').innerHTML = e; }
}
document.getElementById('skin-textarea').onkeyup = document.getElementById('skin-textarea').onchange = function() {
try {
skin = JSON.parse(this.value); updateAnimator();
this.style.color = 'black';
document.getElementById('skin-textarea-errors').innerHTML = "";
}
catch (e) { this.style.color = 'red'; document.getElementById('skin-textarea-errors').innerHTML = e; }
}
// add canvas to document
document.body.appendChild(Shaku.gfx.canvas);
Shaku.gfx.setResolution(screenX, screenY, true);
// create sprites batch
let spritesBatch = new Shaku.gfx.SpriteBatch();
// do a main loop step.
function step() {
Shaku.startFrame();
Shaku.gfx.clear(Shaku.utils.Color.gray);
// update skeleton animations
animator.update(Shaku.gameTime.delta);
// build sprites to draw for skeleton
const drawParts = [];
try {
animator.walk((currBone, parent, sprite, transformations) => {
const size = new Shaku.utils.Vector2(sprite.sourceRect.width, sprite.sourceRect.height).mulSelf(transformations.scale);
const rotation = (sprite.rotation + transformations.rotation) * (Math.PI / 180);
drawParts.push({
texture: sprite.texture,
position: transformations.position,
size: size,
source: Shaku.utils.Rectangle.fromDict(sprite.sourceRect),
rotation: rotation,
origin: Shaku.utils.Vector2.fromDict(sprite.origin),
order: currBone.order || 0
});
});
drawParts.sort((a, b) => (a.order - b.order));
document.getElementById('general-errors').innerHTML = "";
}
catch (e) {
document.getElementById('general-errors').innerHTML = e;
}
// draw the parts
spritesBatch.begin();
for (let part of drawParts) {
spritesBatch.drawQuad(textures[part.texture], part.position, part.size, part.source, undefined, part.rotation, part.origin);
}
spritesBatch.end();
Shaku.endFrame();
Shaku.requestAnimationFrame(step);
}
step();
}
// start demo
runDemo();
</script>
</div>
</body>
</html>