s2pd
v1.1.1
Published
Hi! ππ s2pd is a simple HTML5 canvas and web audio library for making art and 2D games in JavaScript.
Downloads
28
Readme
s2pd
Hi! ππ
s2pd is a stupidly simple HTML5 canvas and web audio library for making 2D games and art in JavaScript.
I originally created s2pd as a library for programming my own simple games and canvas animations, but it quickly got out of hand and took on a life of its own as a fully functioning (although bare-bones) game / html5 canvas library. It is my hope that s2pd is easy and intuitive enough to be used by beginners, students, and anyone else who is curious about delving into the world of digital art. Although there are a vast number of great JavaScript game and canvas libraries out there, I hope s2pd can find its humble place among them as a slightly dumber sibling. Bug reports and code contributions are very welcome!
Here are a few hastily thrown together examples of the kind of things you can build with s2pd. Enjoy!
Installation
The easiest way to get started is to play with s2pd on CodeSandbox. The next easiest way to start is to download boiler-plate.zip and start editing main.js right away. You will need to have a development server running to open index.html.
Otherwise...
With WebPack
npm install s2pd
import s from 's2pd';
Without WebPack
git clone https://github.com/HatAndBread/s2pd.git
In your JavaScript file.
import s from './s2pd/s2pd.js';
OR
There are also two minified versions of s2pd available.s2pd.js can be imported into your project as an es6 module. Alternatively, s2pd.glob.js can be included in the head section of your html file. It is recommended, however, that you use a non-minified version in development so that you can see code hints in your text editor.
<script src="s2pd.glob.js" defer></script>
<script src="my-game.js" defer></script>
Quick tutorial
Let's make a stupidly simple game! First, let's create a canvas.
import s from './s2pd/s2pd.js';
s.ezSetup(); // Basic setup for games. For more advanced options see API.
Now we have an empty canvas element. Let's give it a background using this image file:
The Background class will automatically create an infinitely repeating tile background taking up the entire width and height of your canvas (and infinitely beyond)!
const clouds = new s.Background('./clouds.png');
Now let's make the clouds appear to move. We can use the background's velX property (velocity along the x axis) to make the clouds drift to the left.
clouds.velX = -2; //clouds move -2 pixels along the x axis each call of the animation loop. We can use velY to make things move along the y axis.
If we try to run the program now we will only see a stationary image.π To complete the animation we need to create an animation loop! The animation loop will be called roughly 60 times per second.
s.loop(function () {
// Everything in this function will be called each tick of the loop.
});
All together we have...
import s from './s2pd/s2pd.js';
s.ezSetup();
const clouds = new s.Background('./clouds.png');
clouds.velX = -2;
s.loop(function () {});
Which gives us this
Now let's add a sprite. Making a sprite is simple in s2pd. All you need is an image file with your animation laid out in a single horizontal row with each frame evenly spaced. Let's use this image: Here we have 35 evenly spaced frames. Perfect! We can make a sprite like this:
const sprite = new s.Sprite(s.width / 2, s.height / 2, './hero.png', 35, 4);
// For a single frame sprite all you need is the first three arguments.
This will create an animated sprite in the center of the canvas. 35 is the number of frames in the image and 4 is animation speed. An animation speed of 1 will change frames every time the program goes through the loop. A speed of 2 every will change frames every two ticks, etc. Since our sprite file contains multiple animations we need to define where our animations begin and end. (There is no need to do this step if your sprite only has one animation). Let's animate our sprite blinking while facing to the right. The blink begins on frame 7 (starting from 0) and is four frames long, so...
sprite.addAnimation('blinking-right', 7, 4);
The default animation for sprites is to run through every frame of the entire image file. Since our sprite has multiple animations that would look weird, so let's set the current animation to 'blinking-right'.
sprite.changeAnimationTo('blinking-right');
And now we have an animated sprite!
Let's add one more animation and make our sprite turn to the left or right and walk when the left or right arrow keys on the keyboard are pressed.
sprite.addAnimation('blinking-left', 11, 4);
s.keyDown('right', () => {
sprite.changeAnimationTo('blinking-right');
sprite.xPos += 2; // will increase sprite's position on x axis by 2 pixels
});
s.keyDown('left', () => {
sprite.changeAnimationTo('blinking-left');
sprite.xPos -= 2;
});
Here is the result
Our sprite is floating in the sky. That's strange. Let's make if feel the force of gravity.
sprite.feelGravity(12);
/* A range from about 5 to 20 is good.
5 is moonish gravity. 14 is Earthish.
30 is Jupiterish. 14 is default.
*/
Oh no! Our sprite is falling! Let's put some ground below it. This time let's use the Tile class. The tile class is similar to the Background class, except it won't necessarily take up the entire background. Let's use this image:
const ground = new s.Tile('./ground.png', s.width / 2, s.height * 0.75, 2, 1);
This will create a tile centered horizontally, 3/4ths the height of the canvas vertically, repeating 2 times on the x axis and 1 time on the y axis. Now let's make the tile into a platform so our sprite won't fall through it.
ground.platform(true);
/*passing a truthy value as an argument will make the object into a block.
That means that objects with gravity will not be able to pass through it from any direction (from above, below, left, or right).
*/
Yay! Our sprite has a platform to stand on. Now let's give it the ability to jump.
s.keyUp('space', () => {
sprite.jump(200, true); // will make sprite jump 200 pixels.
}); // passing a truthy value as the second arguement in jump method will disable "double jumps", i.e. sprite won't be able to jump again until the jump is complete.
Here's what we have. Not bad! But a little boring. Let's make our game more game-like. We need some kind of obstacle...π€ Let's make a flying circle that will destroy our sprite when they collide.
const evilCircle = new s.Circle(s.getRandomColor(), -30, s.randomBetween(-10, s.height), s.randomBetween(20, 30));
/*
Make a randomly colored circle 30 pixels off to the left of the canvas
at a random height between -10 and canvas height
and with a random radius between 20 and 30.
*/
evilCircle.velX = 8; // Make the circle travel horitontally 8 pixels per frame.
s.onCollision(evilCircle, sprite, true, () => {
ground.notPlatform(); // Ground is no longer a platform so our sprite will fall. π’
evilCircle.destroy(); // Delete all references to evilCircle.
});
/*A truthy third argument for onCollision method will trigger the callback function
only once per collision. A falsy value will cause the the callback function
to be triggered every tick of the loop.
*/
In our loop callback let's add this code.
s.loop(function () {
if (evilCircle.xPos + evilCircle.radius > s.width) {
// if evil circle goes beyond width of canvas...
evilCircle.color = s.getRandomColor();
evilCircle.xPos = -30; // send evil circle back to left side of canvas.
evilCircle.yPos = s.randomBetween(0, s.height);
}
if (sprite.yPos > s.height) {
sprite.destroy(); // delete all references to our sprite when it has fallen off the canvas. π’π’π’
}
});
All together...
import s from './s2pd/s2pd.js';
s.ezSetup();
const clouds = new s.Background('./clouds.png');
clouds.velX = -2;
const sprite = new s.Sprite(s.width / 2, s.height / 2, './hero.png', 35, 4);
sprite.addAnimation('blinking-right', 7, 4);
sprite.changeAnimationTo('blinking-right');
sprite.addAnimation('blinking-left', 11, 4);
s.keyDown('right', () => {
sprite.changeAnimationTo('blinking-right');
sprite.xPos += 2;
});
s.keyDown('left', () => {
sprite.changeAnimationTo('blinking-left');
sprite.xPos -= 2;
});
sprite.feelGravity(12);
const ground = new s.Tile('./ground.png', s.width / 2, s.height * 0.75, 2, 1);
ground.platform(true);
s.keyUp('space', () => {
sprite.jump(200, true);
});
const evilCircle = new s.Circle(s.getRandomColor(), -30, s.randomBetween(-10, s.height), s.randomBetween(20, 30));
evilCircle.velX = 8;
s.onCollision(evilCircle, sprite, true, () => {
ground.notPlatform();
evilCircle.destroy();
});
s.loop(function () {
if (evilCircle.xPos + evilCircle.radius > s.width) {
evilCircle.color = s.getRandomColor();
evilCircle.xPos = -30;
evilCircle.yPos = s.randomBetween(0, s.height);
}
if (sprite.yPos > s.height) {
sprite.destroy();
}
});
Let's give our game a try. There we have it! A working game, albeit a rather stupid one. I think you can do better! What will you create?
API
π**ezSetup()**
Sets your project up quickly with default settings. Creates a canvas element with id 'canvas', sizes canvas to 900x600 on larger screens, sizes to window width and window height on mobile screens, automatically resizes on mobile orientation change, and prevents window movement on canvas touch and use of keyboard arrow keys.
- ezSetup is not recommended for integration with existing projects as it is likely to change the flow of your document in unexpected ways.
s.ezSetup();
π**createCanvas(id, width, height)**
Create a new html5 canvas element.
s.createCanvas('canvas', 900, 600);
// creates a 900x600 canvas
π**addCanvas(id, width, height)**
Add canvas context to an existing html5 canvas element.
const myCanvas = document.getElementById('myCanvas');
s.addCanvas(myCanvas, 900, 600);
// adds context to canvas and size 900x600
π**backgroundColor(color)**
Change background color of canvas.
s.backgroundColor('rgb(140,224,98)');
π**canvasOpacity(opacity)**
Change opacity of canvas.
s.canvasOpacity(0.5);
π**stillCanvas(how)**
Prevent window from unwanted movement on user interaction. User interaction (such as touching canvas or using keyboard arrow keys) can often cause window to move in unexpected and unwanted ways.
s.stillCanvas();
// prevents window from moving when user is touching canvas or when using arrow keys.
s.stillCanvas('touch');
// prevents window from moving when user is touching canvas only. Essential for drawing applications.
s.stillCanvas('keyboard');
// prevents window from moving when arrow keys are used only.
π**onResize(callback)**
What to do on window resize or orientation change.
π**loop(callback)**
Creates a game loop or animation loop. The loop function is an essential element of most applications. Without it only static images are possible. The computer will run through the loop roughly 60 times per second, executing the callback function each time through. The callback function should contain all tasks that you wanted carried out each go around of the loop.
s.loop(function(){
if (mySprite.xPos >= s.width){ // if sprite's x position is greater than width of canvas
mySprite.xPos = 0; // send it back to the far left side.
}
}
π**whileLoading(callback)**
Task to be carried out while assets (image/audio files) are being loaded. Callback will be called every tick of the loop until loading is completed.
const loadingInfo = new s.Text('red', 'center', 'center', `${s.percentLoaded}%`, 'sans-serif', 32);
s.whileLoading(() => {
// this function will be called every tick of the loop before all loading is completed.
// this function is optional!
loadingInfo.text = `${s.percentLoaded}%`; // continually set text to current percent loaded.
});
function game() {
// do some fun stuff.
// this function will be called every tick of the loop after all loading is completed.
}
s.loop(game);
π**onFirstTime(callback)**
Tasks to be carried out the first time through the loop after all assets have been loaded.
const loadingInfo = new s.Text('red', 'center', 'center', `${s.percentLoaded}%`, 'sans-serif', 32);
s.whileLoading(() => {
loadingInfo.text = `${s.percentLoaded}%`;
});
s.onFirstTime(() => {
loadingInfo.destroy(); // destroy loading screen after all assets loaded.
});
function game() {
// do some cool stuff.
}
s.loop(game);
π**stopLoop()**
Stops the loop.
s.stopLoop();
π**clear()**
Clears the canvas at the beginning of the loop. If clear is not called the image drawn to the canvas during the previous go through of the loop will remain.
s.clear();
π**dontClear()**
Undoes the clear() method. Prevents the image drawn to the canvas during the previous go through of the loop from being cleared.
let randomNumber = Math.floor(Math.random() * 10);
randomNumber === 0 ? s.clear() : s.dontClear();
// clears the canvas if random number is 0, otherwise prevents the canvas from being cleared.
Note: Sprite sheets must be laid out in a single horizontal row with each frame equally sized.
Example β
π**constructor(xPos, yPos, source, numberOfFrames, animationSpeed)**
const bunny = new s.Sprite(s.width/2, s.height/2, './bunny.png', 4,4);
//creates a bunny sprite in the center of the canvas. Sprite sheet has four frames. Frame will change every four times through loop. π°
Additional Parameters
Sprite Methods
π**addAnimation(name, startFrame, numberOfFrames)**
const bunny = new s.Sprite(s.width / 2, s.height / 2, './bunny.png', 4, 4);
bunny.addAnimation('jump', 3, 1);
// Creates a two frame animation called 'jump'. Begins on frame 3 and continues for 1 frame (until frame 4).
π**changeAnimationTo(name)**
Change the sprite's current animation.
bunny.changeAnimationTo('jump');
//changes current animation to 'jump'.
π**goToFrame(which)**
Change current animation frame.
π**onClick(callback, triggerOnce)**
What to do on mouse click.
bunny.onClick(() => {
bunny.changeAnimationTo('jump');
bunny.jump(200);
}, false);
// bunny will jump 200 pixels high each time iot is clicked.
π**onHold(callback)**
What to do when mouse button is held down over object or object is touched for a sustained period of time. Callback will execute each tick of the loop while object is held.
bunny.onHold(() => {
bunny.drag(); // drag and drop the sprite.
});
π**drag()**
Drag and drop the sprite. Must be triggered in onHold method.
bunny.onHold(() => {
bunny.drag();
});
π**onMouseOver(callback, triggerOnce)**
What to do when mouse is over object.
sprite.onMouseOver()=>{
sprite.updateSize(0.9);
})
π**feelGravity(gravity)**
Make your sprite feel the force of gravity. Will fall unless it lands on a platform.
bunny.feelGravity(10);
});
π**noGravity()**
Remove a sprite's ability to feel gravity.
rabbit.noGravity();
π**jump(howHigh, noDoubleJumps)**
Make object jump. Gravity must be enabled using the feelGravity method for the jump method to work.
rabbit.feelGravity(10);
rabbit.onClick(() => {
rabbit.jump(200);
}, true);
//Make rabbit jump 200 pixels when clicked. Will not be able to jump again until it has landed on a platform.
π**platform(blockify)**
Make sprite into a platform. Objects with gravity will not fall through platforms.
const bricks = new s.Sprite(200, 200, './bricks.png');
bricks.platform(true);
//bricks sprite is turned into a platform which objects with gravity will not be able to pass through from any direction.
π**notPlatform()**
Disable the sprite as a platform.
bricks.notPlatform();
π**trimHitBox(left, right, top, bottom)**
Reduces a sprite's hitbox. The hitbox is the area around the sprite used for detecting collisions and clicks. By default a sprite's hitbox is the size of a single animation frame. This can be reduced using trimHitBox for more precise collision detection.
π**updateSize(howMuch)**
Increase or decrease sprite's size.
π**changeCursor(type)**
Change mouse cursor style when mouse is over object.
rabbit.updateSize(0.5);
// make rabbit half its current size.
π**destroy()**
Remove all references to sprite.
π**Additional sprite parameters**
Note: Like sprite sheets , tile animations must be laid out in a single horizontal row with each frame equally sized. The Tile class automatically creates a repeated image for a specified number of times both vertically and horizontally. There are a lot of fun things you can do with the Tile classβ£οΈ
π**constructor(source, xPos, yPos, repeatX, repeatY, numberOfFrames, animationSpeed)**
const backgroundTile = new s.Tile('./stars.png');
// creates a tile background that covers the entire width and height of the canvas.
const ground = new s.Tile('./ground.png', 50,100, 5, 1);
// creates a tile at coordinates 50,100 that repeats 5 times along the x-axis.
const animatedTileBackground = new s.Tile('./cool-animation.png',false,false,false,false, 10, 4)
// creates an animated tile background that covers the entire width and height of the canvas.
πTile Methods
The tile class shares all methods and parameters with the Sprite class, with the exception of updateSize.
πAdditional tile parameters
In addition to the parameters the Tile class shares with the Sprite class, the Tile class has a few unique parameters.
const hearts = new s.Tile('./hearts.png', 100, 100);
hearts.innerVelX = 3;
// make hearts appear to move 3 pixels to the right within the boundaries of their frame every tick of the loop.
See an example of innerVelX and innerVelY in action π Click me!
Note: Like sprite sheets , background animations must be laid out in a single horizontal row with each frame equally sized. The Background class automatically creates an infinitely repeating tile background taking up the entire width and height of your canvas (and infinitely beyond)! Takes the hard work out of creating scrolling backgrounds and parralax effects.
π**constructor(source, numberOfFrames, animationSpeed)**
const sky = new s.Background('./sky.png', 10,4);
// creates an animated background with 10 frames and a speed of four.
Background Methods
π**addAnimation(name, startFrame, numberOfFrames)**
Add an animation to the background.
const sky = new s.Background(s.width / 2, s.height / 2, './bunny.png', 4, 4);
sky.addAnimation('rain', 3, 1);
// Creates a two frame animation called 'rain'. Begins on frame 3 and continues for 1 frame (until frame 4).
π**changeAnimationTo(name)**
Change the background's current animation.
sky.changeAnimationTo('sunny');
//changes current animation to 'sunny'.
π**destroy()**
Remove all references to background.
π**Additional background parameters**
βοΈ Circle
π**constructor(color, xPos, yPos, radius, thickness)**
const myCircle = new s.Circle('rgb(1,2,3)', s.width / 2, s.height / 2, 30, 3);
//creates an outline of a circle with a diameter of 60 pixels in the center of the canvas.
π₯ Ellipse
π**constructor(color, xPos, yPos, radiusX, radiusY, rotation, thickness)**
const myEllipse = new s.Ellipse('rgb(1,2,3)', s.width/2,s.height/2,30,60,1,3)
// creates an outline of an ellipse in the center of the canvas with a diameter of 60 along the x axis, 120 along the y axis, rotated 1 radian.
β¬οΈγRectangle
π**constructor(color, xPos, yPos, width, height, thickness)**
const myRectangle = new s.Rectangle('rgb(1,2,3)', s.width/2, w.height/2, 100, 100);
// creates a 100x100 square with the square's upper-left point in the center of the canvas.
π Line
πconstructor(color, startX, startY, endX, endY, thickness)
const myLine = new s.Line('rgb(1,2,3)', 100,100,100,300, 3);
// Creates a 3 pixel-wide vertical line stretching from coordinates (100,100) to (100, 300).
Shape Methods
Note: all members of the Shapes super class share the same methods.
π**onClick(callback, triggerOnce)**
What to do on mouse click.
myCircle.onClick(() => {
myCircle.color = s.getRandomColor();
}, false);
// Will change myCircle's color to a random color each time it is clicked.
π**onHold(callback)**
What to do when mouse button is held down over shape or shape is touched for a sustained period of time. Callback will execute each tick of the loop while object is held.
myRectangle.onHold(() => {
myRectangle.drag(); // drag and drop myRectangle.
});
π**drag()**
Drag and drop the shape. Must be triggered in onHold method.
myRectangle.onHold(() => {
myRectangle.drag(); // drag and drop myRectangle.
});
π**onMouseOver(callback, triggerOnce)**
What to do when mouse is over object.
circle.onMouseOver()=>{
circle.color = s.getRandomColor();
})
π**changeCursor(type)**
Change mouse cursor style when mouse is over object.
π**destroy()**
Remove all references to object.
π**Additional shape parameters**
π**constructor(color, xPos, yPos, text, font, size, thickness, innerColor)**
Prints text to the canvas. Text may be printed on multiple lines by inserting '\n' into the text parameter.
const someText = new s.Text('red', 'center', 'center', 'HELLO! π \n I β€οΈ you', 'sans-serif', 28, 3, 'green');
someText.center = true;
/*
prints...
HELLO!π
I β€οΈ you
... with text object centered on the canvas and text alignment within the text object also centered.
*/
Text Methods
π**onClick(callback, triggerOnce)**
What to do on mouse click.
myText.onClick(() => {
myText.center = false;
});
// Will uncenter text alignment (align to left) on mouse click.
π**onHold(callback)**
What to do when mouse button is held down over shape or shape is touched for a sustained period of time. Callback will execute each tick of the loop while object is held.
myText.onHold(()=>{
myText.text = Math.floor(Math.random()*10000));
});
// sets text to a new random number every tick of the loop while text object is held.
π**drag()**
Drag and drop the shape. Must be triggered in onHold method.
myText.onHold(() => {
myText.drag(); // drag and drop myText.
});
π**destroy()**
Remove all references to object.
π**Additional text parameters**
A note on using web audio
πconstructor(source, volume, loop, playbackRate)
const mySound = new s.Sound('./niceMusic.mp3', 0.3, true, 1);
const startButton = new s.Text('red', 'center', 'center', 'START', 'sans-serif', 32);
startButton.onClick(()=>{
s.loadAudio(); // loads all audio files associated with the Sound class.
mySound.play(); // mySound will begin playing when it is loaded.
}, true); //trigger once
s.whileLoading(()=>{
console.log(s.percentLoaded) // print percent of assets loaded to console while loading.
});
s.onFirstTime(()=>{
mySound.play(); // play audio after assets are loaded and loop begins.
});
s.loop(function(){
//do some cool stuff.
});
Sound Methods
πs.loadAudio() Global method to load ALL audio files. Percent of audio files (and image files) loaded can be retrieved through the global variable π s.percentLoaded
See above example for usage.
πplay()
Plays audio file.
mySound.play();
π**stop()**
Stops audio file.
mySound.stop();
π**pause()**
Pause audio file. Will resume where it left off when play is called again.
mySound.pause();
π**Additional sound parameters**
Mouse methods. Sprites, tiles, and shapes all have their own mouse methods. Refer to Sprite, Tile, and Shape sections of the API to see mouse methods for each individual class.
π**listenForMouse()**
Creates event listeners for mouse. No need to call this method if using ezSetup()
π**onClick(callback, triggerOnce)**
What to do on mouse click. Works for "touch clicks" too.
s.onClick(()=>{
console.log('π§');
})
//prints a penguin to the console every time user clicks mouse.
πmouse variables
π**listenForTouch()**
Creates event listeners for touch. No need to call this method if using ezSetup()
π**onTouch(callback)**
What to do when user is touching screen. For a touch click use s.onClick() method.
const mySprite = new s.Sprite(100, 100, 'cool-image.png');
s.onTouch(()=>{
mySprite.xPos = s.touchMoveX;
mySprite.yPos = s.touchMoveY;
})
//move sprite to touch coordinates everytime user moves their finger.
π**touch variables**
π**listenForKeyboard()**
Creates event listeners for keyboard. No need to call this method if using ezSetup()
π**keyDown(key, callback, triggerOnce)**
What to do when user is holding keyboard key down.
s.keyDown('right', ()=>{
mySprite.xPos += 2;
})
//make sprite move 2 pixels to the right every tick that right arrow key is held down.
πkeyUp(key, callback)
What to do when keyboard key is released.
s.keyUp('space', ()=>{
mySprite.jump(200);
})
//make sprite jump 200 pixels high when space key is lifted.
π**onCollision(obj1, obj2, triggerOnce, callback)**
Triggers a callback function when two game objects collide.
s.onCollision(mySprite, myOtherSprite, true, () => {
myOtherSprite.destroy();
});
//destroy myOtherSprite on collision with mySprite.
Random numbers
Random numbers are an essential part of game development and digital art. Here are some useful methods for obtaining them. β¬οΈ
π**choose(option1, option2)**
Randomly choose between option 1 and 2.
s.choose(thisSprite, thatSprite);
// returns either thisSprite or thatSprite randomly.
π**randomBetween(min, max)**
Returns a random integer between and including min and max.
s.randomBetween(-1, 1);
// will return either -1, 0, or 1
π**RandomNoRepeat(arr)**
RandomNoRepeat is a class that creates an object that will return random selections from a list without repeating any previous selections until all items in the list have been selected. After all items have been selected the object will start from the beginning again. get() method returns an item.
const fruits = ['π', 'π', 'π', 'π', 'π', 'π'];
const getFruits = new s.RandomNoRepeat(fruits);
for (let i = 0; i < 12; i++) {
console.log(getFruits.get());
}
// prints to the console...
// ["π"]
// ["π"]
// ["π"]
// ["π"]
// ["π"]
// ["π"]
// ["π"]
// ["π"]
// ["π"]
// ["π"]
// ["π"]
// ["π"]
π**getRandomColor()**
Will return a random color in rgb format.
console.log(s.getRandomColor());
// in the console π 'rgb(29, 201, 144)'
π**roundToDecimals(num, howManyDecimals)**
Returns inputed number rounded to decimals.
s.roundToDecimals(10 / 3, 3);
// returns 3.333
Example installation with React and WebPack
s2pd works with React. See the example below for usage. I haven't tried, but I think something similar is probably possible with Vue and other frameworks.
npm install s2pd
import React, { useRef, useEffect } from 'react';
import s from 's2pd';
export default function Canvas(props) {
const theRef = useRef(null);
useEffect(() => {
s.addCanvas(theRef.current, 900, 600);
const circle = new s.Circle('red', s.width / 2, s.height / 2, 30, 3);
s.listenForKeyboard();
s.listenForMouse();
s.listenForTouch();
s.stillCanvas();
s.backgroundColor('pink');
circle.onHold(() => {
circle.drag();
});
s.loop(() => {});
}, []);
return <canvas ref={theRef}></canvas>;
}