I'm trying to get a multiple tool paperjs example going that is a bit of a vector based "paint program".
I'm trying to do this with a toolStack class from a Stack Overflow suggestion. The original post has two placeholder tools and I added another. The two native tools work fine and display, my third tool "multiLine" (which runs fine as a stand along file) seems to be working, does not throw errors in the browser, and from inspecting the results seems to have data in the structures. However the third tool doesn't display.
I realize the syntax is different in different section of the code. Both syntaxes seem to work fine. I'm new to Javascripting so don't know if this is an issue. Thanks in advance for any eyes or suggestions about the code.
<!DOCTYPE html>
<html>
<style>
html,
body,
canvas {
width: 100%;
height: 100%;
margin: 0;
}
</style>
<script>
window.onload = () => {
// Setup Paper
paper.setup(document.querySelector('canvas'))
// Toolstack
class ToolStack {
constructor(tools) {
this.tools = tools.map(tool => tool())
}
activateTool(name) {
const tool = this.tools.find(tool => tool.name === name)
tool.activate()
}
// add more methods here as you see fit ...
}
// Tool Path, draws paths on mouse-drag
const toolPath = () => {
const tool = new paper.Tool()
tool.name = 'toolPath'
let path
tool.onMouseDown = function(event) {
path = new paper.Path()
path.strokeColor = '#4242ff'
path.strokeWidth = 15;
path.add(event.point)
}
tool.onMouseDrag = function(event) {
path.add(event.point)
console.log(path);
}
return tool
}
// Tool Circle, draws a 30px circle on mousedown
const toolCircle = () => {
const tool = new paper.Tool()
tool.name = 'toolCircle'
let path
tool.onMouseDown = function(event) {
path = new paper.Path.Circle({
center: event.point,
radius: 30,
fillColor: '#9C27B0'
})
}
return tool
}
// This is the tool I added which does not display
const multiLine = () => {
const tool = new paper.Tool()
tool.name = 'multiLine'
var values = {
//lines: 5,
lines: 4,
//size: 40,
size: 10,
smooth: true
};
var paths;
// tool.onMouseDown = function(event) { // this worked for debugging the tool
// path = new paper.Path()
// path.strokeColor = '#ff4242'
// path.strokeWidth = 10
// path.add(event.point)
// }
//
// tool.onMouseDrag = function(event) {
// path.add(event.point)
// }
tool.onMouseDown = function(event) {
paths = [];
for (var i = 0; i < values.lines; i++) {
var path = new paper.Path();
path.strokeColor = '#FF2222';
path.strokeWidth = 25;
paths.push(path);
console.log(i);
}
}
tool.onMouseDrag = function(event) {
var offset = event.delta;
offset.angle = offset.angle + 90;
var lineSize = values.size / values.lines;
for (var i = 0; i < values.lines; i++) {
var path = paths[values.lines - 1 - i];
//offset.length = lineSize * i + lineSize / 2;
offset.length = (-i * lineSize) * (Math.max(event.delta.length / 15, 1));
path.add(event.middlePoint + offset);
// path.smooth();
console.log(paths[1]);
}
}
return tool
}
// Construct a Toolstack, passing your Tools
const toolStack = new ToolStack([toolPath, toolCircle, multiLine])
// Activate a certain Tool
toolStack.activateTool('toolPath')
// Attach click handlers for Tool activation on all
// DOM buttons with class '.tool-button'
document.querySelectorAll('.tool-button').forEach(toolBtn => {
toolBtn.addEventListener('click', e => {
toolStack.activateTool(e.target.getAttribute('data-tool-name'))
})
})
}
// function onKeyDown(event) {
// if (event.key == 'd'){
// project.activeLayer.removeChildren();
// }
// if (event.key == 'z'){
// project.activeLayer.removeChildren(project.activeLayer.lastChild.index);
// }
//
// }
</script>
<head>
<title>
StackExchange Multiple Tools
</title>
<meta name="generator" content="BBEdit 14.1" />
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.11.5/paper-core.js">
</script>
<button class="tool-button" data-tool-name="toolPath">
Draw Paths
</button>
<button class="tool-button" data-tool-name="toolCircle">
Stamp Circles
</button>
<button class="tool-button" data-tool-name="multiLine">
Multi Lines
</button>
<canvas resize>
</canvas>
</body>
</html>
Related
I've created a simple sketch using Instance Mode:
var start = 0
var trueSize
const ring = (sketch) => {
sketch.setup = () => {
trueSize = sketch.windowWidth > sketch.windowHeight ? sketch.windowHeight : sketch.windowWidth
sketch.createCanvas(sketch.windowWidth, sketch.windowHeight)
sketch.angleMode(sketch.DEGREES)
sketch.noiseDetail(2, 1)
}
sketch.draw = () => {
sketch.background(30)
sketch.noStroke()
sketch.translate(sketch.width / 2, sketch.height / 2)
var space = 0.1
for (var i = 0; i < 360; i += space) {
var xoff = sketch.map(sketch.cos(i), -1, 1, 0, 3)
var yoff = sketch.map(sketch.sin(i), -1, 1, 0, 3)
var n = sketch.noise(xoff + start, yoff + start)
var h = sketch.map(n, 0, 1, -90, 90)
var r = sketch.map(sketch.sin(i), -1, 1, 100, 200)
var g = sketch.map(h, -150, 150, 0, 150)
var b = sketch.map(n, 0, 1, 150, 255)
sketch.rotate(space)
sketch.fill(r, g, b)
sketch.rect(trueSize * 0.3, 0, h, 1)
}
start += 0.01
}
}
let myp5 = new p5(ring, 'ring')
//let myp5 = new p5(ring, document.getElementById('ring')); //This does not work either.
In my index.html, I have:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
<link rel="stylesheet" type="text/css" href="style.css" />
<meta charset="utf-8" />
<script src="sketch.js"></script>
</head>
<body>
<div id="ring" ></div> <!-- This is not respected. When commented out, the p5 image still appears. -->
</body>
</html>
The problem I have is that the sketch is not truly rendering within the div. When I comment out the div, or set a width/height to the div, the sketch is still drawn regardless of the div.
The fallback when the div isn't found is by design, although if the div exists, p5 does indeed use it. Let's look at the source:
// ... inside p5 constructor ... line 233 in src/core/main.js 5d4fd14
this._start = () => {
// Find node if id given
if (this._userNode) {
if (typeof this._userNode === 'string') {
this._userNode = document.getElementById(this._userNode);
}
}
const context = this._isGlobal ? window : this;
if (context.preload) {
// Setup loading screen
// Set loading screen into dom if not present
// Otherwise displays and removes user provided loading screen
let loadingScreen = document.getElementById(this._loadingScreenId);
if (!loadingScreen) {
loadingScreen = document.createElement('div');
loadingScreen.innerHTML = 'Loading...';
loadingScreen.style.position = 'absolute';
loadingScreen.id = this._loadingScreenId;
const node = this._userNode || document.body;
node.appendChild(loadingScreen);
}
// ...
If you pass a string, p5 uses this._userNode = document.getElementById(this._userNode); to try to find the element by that id. If you pass a node, this never runs, but either way, we eventually get to the code
const node = this._userNode || document.body;
node.appendChild(loadingScreen);
where if this._userNode wound up being falsey (i.e. you never passed anything or the id wasn't findable in the document), p5 falls back on document.body.
You can see this fallback throughout the codebase:
PS C:\Users\greg\Desktop\p5.js-main\src> grep -RC 1 userNode .
./core/main.js- this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1;
./core/main.js: this._userNode = node;
./core/main.js- this._curElement = null;
--
./core/main.js- // Find node if id given
./core/main.js: if (this._userNode) {
./core/main.js: if (typeof this._userNode === 'string') {
./core/main.js: this._userNode = document.getElementById(this._userNode);
./core/main.js- }
--
./core/main.js- loadingScreen.id = this._loadingScreenId;
./core/main.js: const node = this._userNode || document.body;
./core/main.js- node.appendChild(loadingScreen);
--
./core/p5.Graphics.js- this.canvas = document.createElement('canvas');
./core/p5.Graphics.js: const node = pInst._userNode || document.body;
./core/p5.Graphics.js- node.appendChild(this.canvas);
--
./core/rendering.js-
./core/rendering.js: if (this._userNode) {
./core/rendering.js- // user input node case
./core/rendering.js: this._userNode.appendChild(c);
./core/rendering.js- } else {
--
./dom/dom.js-function addElement(elt, pInst, media) {
./dom/dom.js: const node = pInst._userNode ? pInst._userNode : document.body;
./dom/dom.js- node.appendChild(elt);
--
./webgl/p5.RendererGL.js- pg.canvas = document.createElement('canvas');
./webgl/p5.RendererGL.js: const node = pg._pInst._userNode || document.body;
./webgl/p5.RendererGL.js- node.appendChild(pg.canvas);
--
./webgl/p5.RendererGL.js- c.id = defaultId;
./webgl/p5.RendererGL.js: if (this._pInst._userNode) {
./webgl/p5.RendererGL.js: this._pInst._userNode.appendChild(c);
./webgl/p5.RendererGL.js- } else {
If your goal is to not render the sketch except if a particular element exists, check that in your client code and don't call the p5 constructor.
#Hey-PeePs. The issue is happening in p5js:
I have: 1 function(), 1 draw(), 2 sketches(canvas)
Using: Instance Mode
All I want is a gallery! there is an image in thumbnail size, when click goes full-size, which is the reason I have 2 canvases. The following code is working. It will create a canvas with the thumbnail size of the image and then when I click on it, it will create a DIV which contains the full-screen image.
The issue is, every time I click on the thumbnail, it will create another DIV and it just goes on. How can I set a condition for this? So it will only draw once!
var PictureThumbnail = function(p) {
let ThumbImage;
let fullSize;
p.preload = function() {
ThumbImage = p.loadImage('https://www.paulwheeler.us/files/Coin120.png');
};
p.setup = function() {
var canvas = p.createCanvas(100, 100);
canvas.parent('#Thumbnail');
fullSize = createDiv('Full Size Image');
fullSize.addClass('fullSize');
fullSize.hide();
};
p.draw = function() {
p.image(ThumbImage, 0, 0, 700, 70);
if (mouseIsPressed) {
if ((p.mouseX > 0) && (p.mouseX < 100) && (p.mouseY > 0) && (p.mouseY < 100)) {
fullSize.show();
let sketch2 = function(p) {
p.setup = function() {
p.createCanvas(500, 500);
p.background(255);
}
};
new p5(sketch2, window.document.getElementById('fullSize'));
}
}
};
};
let sketch = new p5(PictureThumbnail);
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
</head>
<body>
<div id="Thumbnail"></div>
</body>
</html>
The last line of code is the issue.
new p5(sketch2, window.document.getElementById('fullSize'));
There are a number of issues:
1. Invalid references:
You have several references to global p5.js methods and properties where you should be referencing the instance methods (createDiv and mouseIsPressed).
2. Action in draw() that should be in mousePressed()
The way you're using mouseIsPressed is flawed. Draw is going to be running repeatedly and mouseIsPressed will be true over several calls so you're going to be repeatedly creating sketch2.
3. Attempt to get an element by id that does not exist
You create a div with the class 'fullSize' and no id and then you attempt to get an element with the id 'fullSize' but no such element exist.
Here's a working example:
var PictureThumbnail = function(p) {
let ThumbImage;
let fullSize;
let fullSizeOpen = false;
p.preload = function() {
ThumbImage = p.loadImage('https://www.paulwheeler.us/files/Coin120.png');
};
p.setup = function() {
var canvas = p.createCanvas(100, 100);
canvas.parent('#Thumbnail');
fullSize = p.createDiv('Full Size Image');
fullSize.addClass('fullSize');
fullSize.id('fullSize');
fullSize.hide();
};
p.mousePressed = function() {
if ((p.mouseX > 0) && (p.mouseX < 100) && (p.mouseY > 0) && (p.mouseY < 100)) {
if (!fullSizeOpen) {
fullSizeOpen = true;
fullSize.show();
let sketch2 = function(p) {
p.setup = function() {
p.createCanvas(500, 500);
p.background('red');
}
};
new p5(sketch2, window.document.getElementById('fullSize'));
}
}
}
p.draw = function() {
p.image(ThumbImage, 0, 0, 700, 70);
};
};
let sketch = new p5(PictureThumbnail);
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
</head>
<body>
<div id="Thumbnail"></div>
</body>
</html>
I am working on object detection using Tensorflow.js. I am trying to run custom object detection tensorflow.js model in a browser. I converted the tensorflow model - inference graph to tensorflow.js model.
Prediction for python works fine and the code below:
import io
import os
import scipy.misc
import numpy as np
import six
import time
import glob
from IPython.display import display
from six import BytesIO
import matplotlib
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw, ImageFont
import tensorflow as tf
from object_detection.utils import ops as utils_ops
from object_detection.utils import label_map_util
from object_detection.utils import visualization_utils as vis_util
%matplotlib inline
def load_image_into_numpy_array(path):
img_data = tf.io.gfile.GFile(path, 'rb').read()
image = Image.open(BytesIO(img_data))
(im_width, im_height) = image.size
return np.array(image.getdata()).reshape(
(im_height, im_width, 3)).astype(np.uint8)
category_index = label_map_util.create_category_index_from_labelmap('Models/Annotation/label_map.pbtxt', use_display_name=True)
tf.keras.backend.clear_session()
model = tf.saved_model.load(f'Models/saved_model/')
def run_inference_for_single_image(model, image):
image = np.asarray(image)
input_tensor = tf.convert_to_tensor(image)
input_tensor = input_tensor[tf.newaxis,...]
output_dict = model_fn(input_tensor)
num_detections = int(output_dict.pop('num_detections'))
output_dict = {key:value[0, :num_detections].numpy()
for key,value in output_dict.items()}
output_dict['num_detections'] = num_detections
output_dict['detection_classes'] = output_dict['detection_classes'].astype(np.int64)
if 'detection_masks' in output_dict:
detection_masks_reframed = utils_ops.reframe_box_masks_to_image_masks(
output_dict['detection_masks'], output_dict['detection_boxes'],
image.shape[0], image.shape[1])
detection_masks_reframed = tf.cast(detection_masks_reframed > 0.5,
tf.uint8)
output_dict['detection_masks_reframed'] = detection_masks_reframed.numpy()
return output_dict
for image_path in glob.glob('images/Group_A_406.jpg'):
image_np = load_image_into_numpy_array(image_path)
output_dict = run_inference_for_single_image(model, image_np)
scores = np.squeeze(output_dict['detection_scores'])
vis_util.visualize_boxes_and_labels_on_image_array(
image_np,
output_dict['detection_boxes'],
output_dict['detection_classes'],
output_dict['detection_scores'],
category_index,
instance_masks=output_dict.get('detection_masks_reframed', None),
use_normalized_coordinates=True,
max_boxes_to_draw=50,
min_score_thresh=.45,
line_thickness=8)
display(Image.fromarray(image_np))
sharing the code snippet of index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>The Recognizer</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="styles.css" rel="stylesheet">
</head>
<body>
<h1>Object Detection</h1>
<section id="demos">
<div id="liveView" >
<button id="webcamButton" class="invisible">Loading...</button>
<video id="webcam" class="background" playsinline crossorigin="anonymous"></video>
</div>
</section>
<!-- Import TensorFlow.js library -->
<!-- <script src="https://cdn.jsdelivr.net/npm/#tensorflow/tfjs/dist/tf.min.js"></script-->
<script src="https://cdn.jsdelivr.net/npm/#tensorflow/tfjs#3.3.0/dist/tf.min.js"></script>
<script src="script.js"></script>
</body>
</html>
code snippet of script.js
//Store hooks and video sizes:
const video = document.getElementById('webcam');
const liveView = document.getElementById('liveView');
const demosSection = document.getElementById('demos');
const enableWebcamButton = document.getElementById('webcamButton');
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
var vidWidth = 0;
var vidHeight = 0;
var xStart = 0;
var yStart = 0;
// Check if webcam access is supported.
function getUserMediaSupported() {
return !!(navigator.mediaDevices &&
navigator.mediaDevices.getUserMedia);
}
// If webcam supported, add event listener to activation button:
if (getUserMediaSupported()) {
enableWebcamButton.addEventListener('click', enableCam);
} else {
console.warn('getUserMedia() is not supported by your browser');
}
// Enable the live webcam view and start classification.
function enableCam(event) {
// Only continue if model has finished loading.
if (!model) {
return;
}
// Hide the button once clicked.
enableWebcamButton.classList.add('removed');
// getUsermedia parameters to force video but not audio.
const constraints = {
video: true
};
// Stream video from VAR (for safari also)
navigator.mediaDevices.getUserMedia({
video: {
facingMode: "environment"
},
}).then(stream => {
let $video = document.querySelector('video');
$video.srcObject = stream;
$video.onloadedmetadata = () => {
vidWidth = $video.videoHeight;
vidHeight = $video.videoWidth;
//The start position of the video (from top left corner of the viewport)
xStart = Math.floor((vw - vidWidth) / 2);
yStart = (Math.floor((vh - vidHeight) / 2)>=0) ? (Math.floor((vh - vidHeight) / 2)):0;
$video.play();
//Attach detection model to loaded data event:
$video.addEventListener('loadeddata', predictWebcamTF);
}
});
}
var model = undefined;
model_url = 'https://raw.githubusercontent.com/.../model/mobile_netv2/web_model2/model.json';
//Call load function
asyncLoadModel(model_url);
//Function Loads the GraphModel type model of
async function asyncLoadModel(model_url) {
model = await tf.loadGraphModel(model_url);
console.log('Model loaded');
//Enable start button:
enableWebcamButton.classList.remove('invisible');
enableWebcamButton.innerHTML = 'Start camera';
}
var children = [];
//Perform prediction based on webcam using Layer model model:
function predictWebcamTF() {
// Now let's start classifying a frame in the stream.
detectTFMOBILE(video).then(function () {
// Call this function again to keep predicting when the browser is ready.
window.requestAnimationFrame(predictWebcamTF);
});
}
const imageSize = 300;
//Match prob. threshold for object detection:
var classProbThreshold = 0.4;//40%
//Image detects object that matches the preset:
async function detectTFMOBILE(imgToPredict) {
//Get next video frame:
await tf.nextFrame();
//Create tensor from image:
const tfImg = tf.browser.fromPixels(imgToPredict);
//Create smaller image which fits the detection size
const smallImg = tf.image.resizeBilinear(tfImg, [vidHeight,vidWidth]);
const resized = tf.cast(smallImg, 'int32');
var tf4d_ = tf.tensor4d(Array.from(resized.dataSync()), [1,vidHeight, vidWidth, 3]);
const tf4d = tf.cast(tf4d_, 'int32');
//Perform the detection with your layer model:
let predictions = await model.executeAsync(tf4d);
//Draw box around the detected object:
renderPredictionBoxes(predictions[4].dataSync(), predictions[1].dataSync(), predictions[2].dataSync());
//Dispose of the tensors (so it won't consume memory)
tfImg.dispose();
smallImg.dispose();
resized.dispose();
tf4d.dispose();
}
//Function Renders boxes around the detections:
function renderPredictionBoxes (predictionBoxes, predictionClasses, predictionScores)
{
//Remove all detections:
for (let i = 0; i < children.length; i++) {
liveView.removeChild(children[i]);
}
children.splice(0);
//Loop through predictions and draw them to the live view if they have a high confidence score.
for (let i = 0; i < 99; i++) {
//If we are over 66% sure we are sure we classified it right, draw it!
const minY = (predictionBoxes[i * 4] * vidHeight+yStart).toFixed(0);
const minX = (predictionBoxes[i * 4 + 1] * vidWidth+xStart).toFixed(0);
const maxY = (predictionBoxes[i * 4 + 2] * vidHeight+yStart).toFixed(0);
const maxX = (predictionBoxes[i * 4 + 3] * vidWidth+xStart).toFixed(0);
const score = predictionScores[i * 3] * 100;
const width_ = (maxX-minX).toFixed(0);
const height_ = (maxY-minY).toFixed(0);
//If confidence is above 70%
if (score > 70 && score < 100){
const highlighter = document.createElement('div');
highlighter.setAttribute('class', 'highlighter');
highlighter.style = 'left: ' + minX + 'px; ' +
'top: ' + minY + 'px; ' +
'width: ' + width_ + 'px; ' +
'height: ' + height_ + 'px;';
highlighter.innerHTML = '<p>'+Math.round(score) + '% ' + 'Your Object Name'+'</p>';
liveView.appendChild(highlighter);
children.push(highlighter);
}
}
}
I'm struggling to rewrite the .js code for custom trained classes.
Also I'm unable to trace the tensor shape that needs to be mentioned in .js file. I have fine tuned using ssd mobilenetv2 320*320 on 4 custom classes.
Thanks in advance
I'm wanting to load the same an image for particles in pts.js.
When I try to use a local image from my assets folder I get the error "NS_ERROR_NOT_AVAILABLE:" in the console.
I read somewhere this may be due to the image trying to be used before it has even been loaded in...
I've also tried using an external link to some other image rather than a local one and that works. So not sure what is happening with my local files.
EDIT:
I just tried this on chrome rather than firefox and I'm getting a new and more detailed error message.
"Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state." in pts.min.js:6.
Still not sure exactly what is wrong.
Pts.quickStart('#hello', 'transparent')
var world
// Loading in image to be used
var myImg = new Image()
myImg.src = '/assets/img/myImage.png'
space.add({
start: (bound, space) => {
// Create world and 100 random points
world = new World(space.innerBound, 1, 0)
let pts = Create.distributeRandom(space.innerBound, 20)
// Create particles and hit them with a random impulse
for (let i = 0, len = pts.length; i < len; i++) {
let p = new Particle(pts[i]).size(i === 0 ? 10 : 20)
p.hit(0, 0)
world.add(p)
}
world.particle(0).lock = true
},
animate: (time, ftime) => {
world.drawParticles((p, i) => {
// Image variable for the particle to be drawn as
form.image(myImg)
})
world.update(ftime)
},
action: (type, px, py) => {
if (type == 'move') {
world.particle(0).position = new Pt(px, py)
}
},
resize: (bound, evt) => {
if (world) world.bound = space.innerBound
}
})
space.bindMouse().bindTouch()
space.play()
For drawing images on canvas, the image needs to be loaded first. You can keep track of it using myImg.addEventListener( 'load', ... ).
Once it's loaded, you can use it in form.image( myImg, ... ) in Pts' animate loop.
Here's a working example based on your code above:
Pts.quickStart( "#pt", "#123" );
//// Demo code starts (anonymous function wrapper is optional) ---
(function() {
var world;
var imgReady = false;
// Loading in image to be used
var myImg = new Image()
myImg.src = 'http://icons.iconarchive.com/icons/icojoy/maneki-neko/256/cat-6-icon.png';
myImg.addEventListener('load', function() {
imgReady = true;
}, false);
space.add( {
start: (bound, space) => {
// Create world and 100 random points
world = new World( space.innerBound, 1, 0 );
let pts = Create.distributeRandom( space.innerBound, 100 );
// Create particles and hit them with a random impulse
for (let i=0, len=pts.length; i<len; i++) {
let p = new Particle( pts[i] ).size( (i===0) ? 30 : 3+Math.random()*space.size.x/50 );
p.hit( Num.randomRange(-50,50), Num.randomRange(-25, 25) );
world.add( p );
}
world.particle( 0 ).lock = true; // lock it to move it by pointer later on
},
animate: (time, ftime) => {
world.drawParticles( (p, i) => {
if (imgReady) {
console.log( p )
form.image(myImg, [p.$subtract( p.radius ), p.$add( p.radius )] );
}
});
world.update( ftime );
},
action:( type, px, py) => {
if (type == "move") {
world.particle( 0 ).position = new Pt(px, py);
}
},
resize: (bound, evt) => {
if (world) world.bound = space.innerBound;
}
});
space.bindMouse().bindTouch();
space.play();
})();
I coded a little simulation where you can dig cubes, they fall an stack, you can consume some or make a selection explode.
I started this little project for fun and my aim is to handle as much cubes as possible (100k would be a good start).
The project is simple, 3 possibles actions:
Dig cubes (2k each click)
Consume cubes (50 each click)
Explode cubes (click on two points to form a rectangle)
Currently, on my computer, performance starts to drop when I got about 20k cubes. When you select a large portion of cubes to explode, performance are heavily slowed down too. I'm not sure the way I simplified physics is the best way to go.
Could you give me some hints on how to improve/optimize it ?
Here is the complete code : (The stacking doesn't work in the SO snippet so here is the codepen version)
(() => {
// Variables init
let defaultState = {
cubesPerDig : 2000,
cubesIncome : 0,
total : 0
};
let cubeSize = 2, dropSize = window.innerWidth, downSpeed = 5;
let state,
digButton, // Button to manually dig
gameState, // An object containing the state of the game
cubes, // Array containing all the spawned cubes
heightIndex, // fake physics
cubesPerX, // fake physics helper
playScene; // The gamescene
// App setup
let app = new PIXI.Application();
app.renderer.view.style.position = "absolute";
app.renderer.view.style.display = "block";
app.renderer.autoResize = true;
document.body.appendChild(app.view);
// Resize
function resize() {
app.renderer.resize(window.innerWidth, window.innerHeight);
}
window.onresize = resize;
resize();
// Hello ! we can talk in the chat.txt file
// Issue : When there are more than ~10k cubes, performance start to drop
// To test, click the "mine" button about 5-10 times
// Main state
function play(delta){
// Animate the cubes according to their states
let cube;
for(let c in cubes){
cube = cubes[c];
switch(cube.state) {
case STATE.LANDING:
// fake physics
if(!cube.landed){
if (cube.y < heightIndex[cube.x]) {
cube.y+= downSpeed;
}else if (cube.y >= heightIndex[cube.x]) {
cube.y = heightIndex[cube.x];
cube.landed = 1;
heightIndex[cube.x] -= cubeSize;
}
}
break;
case STATE.CONSUMING:
if(cube.y > -cubeSize){
cube.y -= cube.speed;
}else{
removeCube(c);
}
break;
case STATE.EXPLODING:
if(boundings(c)){
continue;
}
cube.x += cube.eDirX;
cube.y += cube.eDirY;
break;
}
}
updateUI();
}
// Game loop
function gameLoop(delta){
state(delta);
}
// Setup variables and gameState
function setup(){
state = play;
digButton = document.getElementById('dig');
digButton.addEventListener('click', mine);
playScene = new PIXI.Container();
gameState = defaultState;
/* User inputs */
// Mine
document.getElementById('consume').addEventListener('click', () => {consumeCubes(50)});
// Manual explode
let explodeOrigin = null
document.querySelector('canvas').addEventListener('click', e => {
if(!explodeOrigin){
explodeOrigin = {x: e.clientX, y: e.clientY};
}else{
explode(explodeOrigin, {x: e.clientX, y: e.clientY});
explodeOrigin = null;
}
});
window['explode'] = explode;
heightIndex = {};
cubesPerX = [];
// Todo fill with gameState.total cubes
cubes = [];
app.ticker.add(delta => gameLoop(delta));
app.stage.addChild(playScene);
}
/*
* UI
*/
function updateUI(){
document.getElementById('total').innerHTML = cubes.length;
}
/*
* Game logic
*/
// Add cube when user clicks
function mine(){
for(let i = 0; i < gameState.cubesPerDig; i++){
setTimeout(addCube, 5*i);
}
}
// Consume a number of cubes
function consumeCubes(nb){
let candidates = _.sampleSize(cubes.filter(c => !c.eDirX), Math.min(nb, cubes.length));
candidates = candidates.slice(0, nb);
candidates.map(c => {
dropCubes(c.x);
c.state = STATE.CONSUMING;
});
}
const STATE = {
LANDING: 0,
CONSUMING: 1,
EXPLODING: 2
}
// Add a cube
function addCube(){
let c = new cube(cubeSize);
let tres = dropSize / cubeSize / 2;
c.x = window.innerWidth / 2 + (_.random(-tres, tres) * cubeSize);
c.y = 0//-cubeSize;
c.speed = _.random(5,8);
cubes.push(c);
c.landed = !1;
c.state = STATE.LANDING;
if(!cubesPerX[c.x]) cubesPerX[c.x] = [];
if (!heightIndex[c.x]) heightIndex[c.x] = window.innerHeight - cubeSize;
cubesPerX[c.x].push(c);
playScene.addChild(c);
}
// Remove a cube
function removeCube(c){
let cube = cubes[c];
playScene.removeChild(cube);
cubes.splice(c,1);
}
// Delete the cube if offscreen
function boundings(c){
let cube = cubes[c];
if(cube.x < 0 || cube.x + cubeSize > window.innerWidth || cube.y < 0 || cube.y > window.innerHeight)
{
removeCube(c);
return true;
}
}
// explode some cubes
function explode(origin, dest){
if(dest.x < origin.x){
dest = [origin, origin = dest][0]; // swap
}
var candidates = cubes.filter(c => c.state != STATE.EXPLODING && c.x >= origin.x && c.x <= dest.x && c.y >= origin.y && c.y <= dest.y);
if(!candidates.length)
return;
for(let i = origin.x; i <= dest.x; i++){
dropCubes(i);
}
candidates.forEach(c => {
c.explodingSpeed = _.random(5,6);
c.eDirX = _.random(-1,1,1) * c.explodingSpeed * c.speed;
c.eDirY = _.random(-1,1,1) * c.explodingSpeed * c.speed;
c.state = STATE.EXPLODING;
});
}
// Drop cubes
function dropCubes(x){
heightIndex[x] = window.innerHeight - cubeSize;
if(cubesPerX[x] && cubesPerX[x].length)
cubesPerX[x].forEach(c => {
if(c.state == STATE.EXPLODING) return;
c.landed = false; c.state = STATE.LANDING;
});
}
/*
* Graphic display
*/
// Cube definition
function cube(size){
let graphic = new PIXI.Graphics();
graphic.beginFill(Math.random() * 0xFFFFFF);
graphic.drawRect(0, 0, size, size);
graphic.endFill();
return graphic;
}
// Init
setup();
})()
/* styles */
/* called by your view template */
* {
box-sizing: border-box;
}
body, html{
margin: 0;
padding: 0;
color:white;
}
#ui{
position: absolute;
z-index: 2;
top: 0;
width: 0;
left: 0;
bottom: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.1/pixi.min.js"></script>
<script>PIXI.utils.skipHello();</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
<!-- Click two points to make cubes explode -->
<div id="ui">
<button id="dig">
Dig 2k cubes
</button>
<button id="consume">
Consume 50 cubes
</button>
<p>
Total : <span id="total"></span>
</p>
</div>
Thanks
Using Sprites will always be faster than using Graphics (at least until v5 comes along!)
So I changed your cube creation function to
function cube(size) {
const sprite = new PIXI.Sprite(PIXI.Texture.WHITE);
sprite.tint = Math.random() * 0xFFFFFF;
sprite.width = sprite.height = size;
return sprite;
}
And that boosted the fps for myself