Sharepoint : Google webchart not appearing from list? - sharepoint

I got sharepoint online working under O365, In our home page we need a chart showing the summary of our list data
I am trying to follow the below steps in my list
https://www.evoketechnologies.com/blog/visualizing-sharepoint-google-charts/
<html>
<head>
<script src="https://www.gstatic.com/charts/loader.js" type="text/javascript"></script>
<script src="https://code.jquery.com/jquery-3.0.0.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.SPServices/2014.02/jquery.SPServices.min.js" type="text/javascript"></script>
<script language="javascript">
var returnedItems = null;
function loadGoogleLibAndDraw(){
google.charts.load('current', {'packages':['bar','line']});
google.charts.setOnLoadCallback(visualizeData);
}
function visualizeData() {
var context = new SP.ClientContext();
var list = context.get_web().get_lists().getByTitle(document.getElementById('Project Issues').value);
var caml = new SP.CamlQuery();
caml.set_viewXml("<View></View>");
returnedItems = list.getItems(caml);
context.load(returnedItems);
context.executeQueryAsync(onSucceededCallback, onFailedCallback);
}
function onSucceededCallback(sender, args) {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Status');
data.addColumn('number', 'PercentComplete');
var enumerator = returnedItems.getEnumerator();
var markup = '';
while (enumerator.moveNext()) {
var row = [];
var listItem = enumerator.get_current();
row.push(listItem.get_item('Status'));
row.push(listItem.get_item('PercentComplete'));
data.addRow(row);
}
var options = {
chart: {
title: 'Sales Trend',
},
bars: 'vertical'
};
var barChart = new google.charts.Bar(document.getElementById('BarChart'));
barChart.draw(data, options);
var lineChart = new google.charts.Line(document.getElementById('LineChart'));
lineChart.draw(data, options);
}
function onFailedCallback(sender, args) {
var markup = '<p>The request failed: <br>';
markup += 'Message: ' + args.get_message() + '<br>';
displayDiv.innerHTML = markup;
}
</script>
</head>
<body onload="loadGoogleLibAndDraw()">
<form name="metricsform" id="metricsform">
<input id="customListName" name="customListName" value="Project Issues" type="hidden"/>
</form>
<div>
<div id="displayDiv"></div>
<div id="BarChart" style="width: 900px; height: 500px;"></div>
<div id="LineChart" style="width: 900px; height: 500px;"></div>
</div>
</body>
</html>
I embeded the above script to my sharepoint page , replacing listname,column names but its not showing any chart from my list but giving an error as below
Thank you

#aryan,
Please have a try below code, i am able to get the chart works.
<html>
<head>
<script src="https://www.gstatic.com/charts/loader.js" type="text/javascript"></script>
<script src="https://code.jquery.com/jquery-1.12.4.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.SPServices/2014.02/jquery.SPServices.min.js"
type="text/javascript"></script>
<script language="javascript" type="text/javascript">
var returnedItems = null;
$(document).ready(function () {
loadGoogleLibAndDraw();
});
function loadGoogleLibAndDraw() {
google.charts.load('current', { 'packages': ['bar', 'line'] });
google.charts.setOnLoadCallback(visualizeData);
}
function visualizeData() {
var context = new SP.ClientContext();
var list = context.get_web().get_lists().getByTitle('smartphone_sales');
var caml = new SP.CamlQuery();
caml.set_viewXml("<View></View>");
returnedItems = list.getItems(caml);
context.load(returnedItems);
context.executeQueryAsync(onSucceededCallback, onFailedCallback);
}
function onSucceededCallback(sender, args) {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Year');
data.addColumn('number', 'Red Mi3');
data.addColumn('number', 'Honor 5s');
data.addColumn('number', 'Le 1s');
var enumerator = returnedItems.getEnumerator();
var markup = '';
while (enumerator.moveNext()) {
var row = [];
var listItem = enumerator.get_current();
row.push(listItem.get_item('Title'));
row.push(listItem.get_item('RedMi3'));
row.push(listItem.get_item('Honor5s'));
row.push(listItem.get_item('Le1s'));
data.addRow(row);
}
var options = {
chart: {
title: 'Sales Trend',
},
bars: 'vertical'
};
var barChart = new google.charts.Bar(document.getElementById('BarChart'));
barChart.draw(data, options);
var lineChart = new google.charts.Line(document.getElementById('LineChart'));
lineChart.draw(data, options);
}
function onFailedCallback(sender, args) {
var markup = '<p>The request failed: <br>';
markup += 'Message: ' + args.get_message() + '<br>';
displayDiv.innerHTML = markup;
}
</script>
</head>
<body>
<div>
<div id="displayDiv"></div>
<div id="BarChart" style="width: 900px; height: 500px;"></div>
<div id="LineChart" style="width: 900px; height: 500px;"></div>
</div>
</body>
</html>
Test Result:
Please make sure the column's internal name in your code is correct. And you may check the source panel to see if the JS files have been loaded. Sometimes it will remains some caches, so please refresh the page or try it in a private window.
BR

Related

Lit-element children component not re rendering JS code based on updated props

Lit-Element updated .props not invoking full re-render of child component, i.e. the javascript code inside firstUpdated() of child constructs a leaflet map based on .props being passed in from parent component, when the parent component updates location and city, it doesn't create a re-render of the map with new location and city.
When user clicks on button to update location from Seattle to Toronto, the parents props are updated and passed to child, however, the map doesn't rebuild itself, how do I force the map to be "rebuilt" (re-rendered) based on the new .props being passed into the child ??
Git repo for my working sample code
THANKS! :) Been struggling with this for days on end - FYI new to Lit-Element.
Index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link type="text/css" href="./styles.css"
<script src="./webcomponents/webcomponents-loader.js"></script>
<script>
if (!window.customElements) { document.write('<!--'); }
</script>
<script src="./webcomponents/custom-elements-es5-adapter.js"></script>
<!-- ! DO NOT REMOVE THIS COMMENT, WE NEED ITS CLOSING MARKERS -->
</head>
<body>
<app-view></app-view>
</body>
</html>
Index.js:
import './views/app-view.js'
Parent component app-view.js
import { html, LitElement } from 'lit-element';
import './esri-map.js'
class AppView extends LitElement{
static get properties() {
return {
location: { type: Object },
city: { type: String }
}
}
constructor() {
super();
// set fallback location to seattle -- GET will pull in coordinates for Toronto
this.location = { lat: "47.608013", long: "-122.335167" }
this.city = "Seattle"
}
render() {
return html`
<style>
#app-container{
width: 100%,;
height: 100%;
display: flex;
}
#map-container{
flex-grow: 1;
height: 800px;
}
</style>
<button #click=${ (event) => this.updateLocation() }
>Set to Toronto
</button>
<div id="app-container">
<div id="map-container">
<esri-map
.location=${this.location}
.city=${this.city}
>
</esri-map>
</div>
</div>
`;
}
updateLocation(){
var oldLocation = this.location;
this.location = { lat: "43.651070", long: "-79.347015"} // Set to Toronto
this.city = "Toronto"; // Set to Toronto
console.log("New location is: " + this.city)
console.log("Coordinates: ");
console.log(this.location);
}
}
customElements.define('app-view', AppView);
Child Component
import { html, LitElement } from 'lit-element';
import * as L from 'leaflet';
import * as esri from 'esri-leaflet';
class EsriMap extends LitElement{
static get properties() {
return {
location: { type: Object }, // prop passed from parent
city: { type: String } // prop passed from parent
}
}
render() {
return html`
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.6.0/dist/leaflet.css"/>
<style>
#map{
height: 100%;
width: 100%;
}
</style>
<h2>Current City: ${this.city} </h2>
<h3>Coordinates: Lat: ${this.location.lat}</h3>
<div id="map"></div>
`
}
firstUpdated() {
const mapNode = this.shadowRoot.querySelector('#map');
// Render map with props from parent component
var map = L.map(mapNode, {
maxZoom: 18,
minZoom: 2,
}).setView([this.location.lat, this.location.long],8); // [Lat,Lng]
const esriLayer = esri.basemapLayer('Streets');
map.addLayer(esriLayer);
// Render circle with props from parent component
var circle = L.circle([this.location.lat, this.location.long], {
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5,
radius: 20000,
}).addTo(map);
}
}
customElements.define('esri-map', EsriMap);
When properties change, the render() function gets invoked.
firstUpdated() gets only invoked after the first update and not on every property change.
Try this:
updated(changedProps) {
if (changedProps.has('location')) {
this._setMap();
}
}
_setMap() {
const mapNode = this.shadowRoot.querySelector('#map');
if (mapNode == null || this.location == null) {
return;
}
// Render map with props from parent component
var map = L.map(mapNode, {
maxZoom: 18,
minZoom: 2,
}).setView([this.location.lat, this.location.long],8); // [Lat,Lng]
const esriLayer = esri.basemapLayer('Streets');
map.addLayer(esriLayer);
// Render circle with props from parent component
var circle = L.circle([this.location.lat, this.location.long], {
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5,
radius: 20000,
}).addTo(map);
}

Fabricjs i-text clone issue

I am having problem in cloning fabricjs i-text. When i change the color of a selected cloned text then the original text color is also changing.
var canvas = new fabric.Canvas('mycanvas');
var itext = new fabric.IText( "New text", { left : 50, top : 50} );
itext.setSelectionStyles({fill: 'red'});
canvas.add(itext).renderAll();
$('#clone').on('click', function(){
var obj = canvas.getActiveObject();
if(obj == null) return;
var obj1 = obj.clone();
obj1.set({
letf : 150,
top : 150
});
canvas.add(obj1).renderAll();
});
$("#colorchange").on('click',function(){
var obj = canvas.getActiveObject();
if(obj == null) return;
if(obj.setSelectionStyles && obj.isEditing)
obj.setSelectionStyles({fill: 'red'});
else
obj.set({fill: 'red'});
canvas.renderAll();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js"></script>
<canvas id="c"></canvas>
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<canvas id="mycanvas" width="400" height="300" style="border:solid 1px #ccc;"></canvas><br>
<button id="clone">clone</button>
<button id="colorchange">change color</button>
I made a few changes and I am able to produce the result you need. Let me know if thats what you needed.
var canvas = new fabric.Canvas('mycanvas');
var itext = new fabric.IText( "New text", { left : 50, top : 50} );
itext.setSelectionStyles({fill: 'red'});
canvas.add(itext).renderAll();
$('#clone').on('click', function(){
var gobj = canvas.getActiveObject();
if(gobj == null) return;
var obj1 = gobj.clone();
var vobj = obj1;
vobj.set({
left: 100,
top: 100,
fill: 'blue'
});
canvas.add(vobj).renderAll();
});
$("#colorchange").on('click',function(){
var obj = canvas.getActiveObject();
if(obj == null) return;
if(obj.setSelectionStyles && obj.isEditing)
obj.setSelectionStyles({fill: 'red'});
else
obj.set({fill: 'red'});
canvas.renderAll();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js"></script>
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<canvas id="mycanvas" width="400" height="300" style="border:solid 1px #ccc;"></canvas><br>
<button id="clone">clone</button>
<button id="colorchange">change color</button>
I find the solution , guy !
The key to your problem is your clone function do not use callback.
you may use it like this :
obj.clone(function(newobj){
newobj.set({
letf : 150,
top : 150
});
canvas.add(newobj).renderAll();
});

web audio input doesnt exist in chrome's flags

I am working on a project about speech recognition, but my html file cannot find microphone.I mean that when I click the button "start recognition " it should pop a bar for allow or deny access to the microphone, but it doesn't.I figure out that I should enable web audio input from chrome://flags, but in chrome://flags there is no option for web audio input.
what should I do?
please help me.
<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
<head>
<title>Web Speech API</title>
<style type="text/css">
#controls {
text-align: center;
}
#start_button {
font-size: 16pt;
}
#transcript {
color: darkred;
font-size: 16pt;
border: 1px solid #ccc;
padding: 5px;
text-align: center;
}
#instructions {
color: darkblue;
font-size: 14pt;
text-align: center;
}
</style>
<script src="http://code.jquery.com/jquery-latest.min.js"></script>
</head>
<body>
<div id="transcript"> </div>
<br>
<div id="instructions"> </div>
<div id="controls">
<button id="start_button">Click to Start</button>
</div>
<script type="text/javascript">
var finalTranscript = '';
var recognizing = false;
$(document).ready(function() {
// check that your browser supports the API
if (!('webkitSpeechRecognition' in window)) {
alert("Sorry, your Browser does not support the Speech API");
} else {
// Create the recognition object and define the event handlers
var recognition = new webkitSpeechRecognition();
recognition.continuous = true; // keep processing input until stopped
recognition.interimResults = true; // show interim results
recognition.lang = 'en-GB'; // specify the language
recognition.onstart = function() {
recognizing = true;
$('#instructions').html('Speak slowly and clearly');
$('#start_button').html('Click to Stop');
};
recognition.onerror = function(event) {
console.log("There was a recognition error...");
};
recognition.onend = function() {
recognizing = false;
$('#instructions').html(' ');
};
recognition.onresult = function(event) {
var interimTranscript = '';
// Assemble the transcript from the array of results
for (var i = event.resultIndex; i < event.results.length; ++i) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript;
//me sunarthsh js na to kopseis kai na pareis tis lekseis kai na allazeis auta pou thes
} else {
//console.log("event.results[i][0].transcript: " + event.results[i][0].transcript);
//if (event.results[i][0].transcript == "equals") {
//... = "=";
//}
//else if (... == "by") {
//... = "*";
//}
//else if (... == "divide") {
//... = "/";
//}
//else if (... == "square") {
//... = "^2";
//}
interimTranscript += event.results[i][0].transcript;
}
}
console.log("interim: " + interimTranscript);
console.log("final: " + finalTranscript);
// update the page
if(finalTranscript.length > 0) {
$('#transcript').html(finalTranscript);
recognition.stop();
$('#start_button').html('Click to Start Again');
recognizing = false;
}
};
$("#start_button").click(function(e) {
e.preventDefault();
if (recognizing) {
recognition.stop();
$('#start_button').html('Click to Start Again');
recognizing = false;
} else {
finalTranscript = '';
// Request access to the User's microphone and Start recognizing voice input
recognition.start();
$('#instructions').html('Allow the browser to use your Microphone');
$('#start_button').html('waiting');
$('#transcript').html(' ');
}
});
}
});
</script>
</body>
</html>
You need that page to be served by a webserver, but it sounds like you're just trying to open the HTML file with your browser. Chrome will block access to the camera/microphone unless the page was served from a webserver. You can create a local server on your machine to serve the page using the command python -m SimpleHTTPServer in the folder with your html file. (assuming you're on a MacOS/linux system).
See this answer for more information
WebRTC - Browser doesn't ask for mic access permission for local html file

changing google map marker colors

How do I change the colours of the Icons easily? I just want to be able to change it to a different colour using googles standard markers? Is there a way where I can just declare the color where I create the marker with var = marker..... ?
<script type="text/javascript">
var map = null;
function initialize() {
var myOptions = {
zoom: 5,
center: new google.maps.LatLng(54.2361100,-4.5480600),
mapTypeControl: true,
mapTypeControlOptions: {style: google.maps.MapTypeControlStyle.DROPDOWN_MENU},
navigationControl: true,
mapTypeId: google.maps.MapTypeId.ROADMAP
}
map = new google.maps.Map(document.getElementById("map_canvas"),
myOptions);
google.maps.event.addListener(map, 'click', function() {
infowindow.close();
});
var point = new google.maps.LatLng(50.7184100,-3.5339000);
var marker = createMarker(point,'<div style="width:200px"><h2>text</h2><\/div>')
var point = new google.maps.LatLng(50.9097000,-1.4043500);
var marker = createMarker(point,'<div style="width:200px"><h2>text</h2><\/div>')
}
var infowindow = new google.maps.InfoWindow(
{
size: new google.maps.Size(150,50)
});
function createMarker(latlng, html) {
var contentString = html;
var marker = new google.maps.Marker({
position: latlng,
map: map,
zIndex: Math.round(latlng.lat()*-100000)<<5
});
google.maps.event.addListener(marker, 'click', function() {
infowindow.setContent(contentString);
infowindow.open(map,marker);
});
}
</script>
</head>
<body style="margin:0px; padding:0px;" onload="initialize()">
<div id="map_canvas" style="width: 535px; height: 500px"></div>
Adjusting Sean's answer from here How can I change the color of a Google Maps marker?.
Try changing your create marker function to
function createMarker(latlng, html, iconName) {
var contentString = html;
var marker = new google.maps.Marker({
position: latlng,
map: map,
icon: iconName,
zIndex: Math.round(latlng.lat()*-100000)<<5
});
And then call it with
var marker = createMarker(point,'<div style="width:200px"><h2>text</h2><\/div>', 'brown_markerA.png')
Making sure you place all the icons in the same place as your map page.
You can then adjust how you build up the string for different colors or letters.

SpriteKit's SKPhysicsBody with polygon helper tool

I wonder if there is a tool that could be used for easy generation of complex physics bodies in SpriteKit. I would like to have a volume based physical bodies with polygon-type shapes. SpriteKit allows to create such bodies with that method:
+ (SKPhysicsBody *)bodyWithPolygonFromPath:(CGPathRef)path
Unfortunately it's time consuming task to generate such paths manually, and it could be problematic when testing. There is a SpriteHelper application that allows you to define body shape within easy-to-use visual editor, but this app can't export paths that could be used here. It was made for cocos2d and it does a lot of things like texture packing etc. that I don't need and I can't use with SpriteKit. Does anyone know a solution that will allow to define CGPath's easily or maybe even auto-generate them from png images with alpha channel? Although auto-generation feature from my experience would need optimization, because the body shapes should be as simple as possible when textures could have more complicated shapes.
I am looking for the exact same thing, as it turn out I have done a small web app for this purpose.
SKPhysicsBody Path Generator
as action in example:
Update 2015-02-13: script
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SpriteKit Tools - SKPhysicsBody Path Generator</title>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<style>
/* disable responsive */
.container {
max-width: none;
width: 970px;
}
#sprite {
background-color: #eee;
position: absolute;
}
#path {
cursor: crosshair;
opacity: 0.5;
}
</style>
</head>
<body>
<div class="container">
<h1>SKPhysicsBody Path Generator</h1>
<p class="lead">Want to use [SKPhysicsBody bodyWithPolygonFromPath:path] easier way like me? Here with a small helper for easier path drawing, hope it help others too.</p>
<div class="row">
<div class="col-md-6">
<h5>Basic Instruction</h5>
<ol>
<li><small>Drag and drop the sprite image into drop zone.</small></li>
<li><small>Start drawing path by clicking on coordinates.</small></li>
</ol>
</div>
<div class="col-md-6">
<h5>Some Rules / Known Issue</h5>
<ul>
<li><small>Path need to be as a convex polygonal path with counterclockwise winding and no self intersections. The points are specified relative to the owning node’s origin. (documentation link)</small></li>
<li><small>Please use Chrome for best compatibility as I have not tested on other browsers.</small></li>
</ul>
</div>
</div>
<hr>
<div class="btn-group">
<button class="btn btn-primary" type="button" onclick="resetShape()">Reset Shape</button>
<button class="btn btn-primary" type="button" onclick="location.reload()">Reset All</button>
</div>
<input type="checkbox" onclick="toggleRetinaMode()" id="retinaCheckbox" checked> Retina? (please check before declaring path)
<br><br>
<canvas id="sprite" width="940" height="100"></canvas>
<canvas id="path" width="0" height="100"></canvas>
<p class="text-muted"><small>X:<span id="tooltipX">0</span> Y:<span id="tooltipY">0</span></small></p>
<br>
<h5>Output</h5>
<pre>
SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:#"<span id="codeImgName">img</span>"];
CGFloat offsetX = sprite.frame.size.width * sprite.anchorPoint.x;
CGFloat offsetY = sprite.frame.size.height * sprite.anchorPoint.y;
CGMutablePathRef path = CGPathCreateMutable();
<span id="codeCGPath"></span>
CGPathCloseSubpath(path);
sprite.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];
</pre>
</div>
<script>
// reference from http://davidwalsh.name/resize-image-canvas
var spriteCanvas = document.getElementById('sprite');
var spriteContext = spriteCanvas.getContext('2d');
spriteContext.fillText('Drop Sprite Image Here', 400, 50);
var pathCanvas = document.getElementById('path');
var pathContext = pathCanvas.getContext('2d');
function render(src){
var image = new Image();
image.onload = function(){
spriteContext.clearRect(0, 0, spriteCanvas.width, spriteCanvas.height);
spriteCanvas.width = image.width;
spriteCanvas.height = image.height;
spriteContext.drawImage(image, 0, 0, image.width, image.height);
pathContext.clearRect(0, 0, pathCanvas.width, pathCanvas.height);
pathCanvas.width = image.width;
pathCanvas.height = image.height;
};
image.src = src;
}
function loadImage(src){
if(!src.type.match(/image.*/)){
console.log('Dropped file is not image format');
return;
}
var reader = new FileReader();
reader.onload = function(e){
render(e.target.result);
};
reader.readAsDataURL(src);
var fileName = src.name;
var codeImgName = document.getElementById('codeImgName');
codeImgName.innerHTML = fileName;
}
spriteCanvas.addEventListener('dragover', function(e){
e.preventDefault();
}, true);
spriteCanvas.addEventListener('drop', function(e){
e.preventDefault();
loadImage(e.dataTransfer.files[0]);
}, true);
var retinaMode = true;
function toggleRetinaMode(){
var status = document.getElementById('retinaCheckbox');
retinaMode = status.checked ? true : false;
}
var actualX = 0;
var actualY = 0;
var displayX = document.getElementById('tooltipX');
var displayY = document.getElementById('tooltipY');
pathCanvas.onmousemove = function(e){
actualX = e.pageX - this.offsetLeft;
actualY = e.pageY - this.offsetTop;
displayX.innerHTML = retinaMode ? Math.floor(actualX / 2) : actualX;
displayY.innerHTML = retinaMode ? Math.floor((spriteCanvas.height - actualY - 1) / 2) : spriteCanvas.height - actualY - 1;
}
var pathArray = new Array();
pathCanvas.onclick = function(e){
var coor = {
actualX: actualX,
actualY: actualY,
displayX: displayX.innerHTML,
displayY: displayY.innerHTML,
};
pathArray.push(coor);
refreshShape(pathArray);
}
var codeCGPath = document.getElementById('codeCGPath');
function refreshShape(pathArray){
pathContext.clearRect(0, 0, pathCanvas.width, pathCanvas.height);
pathContext.beginPath();
for(var i in pathArray){
if(i == 0) {
pathContext.moveTo(pathArray[i].actualX, pathArray[i].actualY);
codeCGPath.innerHTML = 'CGPathMoveToPoint(path, NULL, '+pathArray[i].displayX+' - offsetX, '+pathArray[i].displayY+' - offsetY);<br>';
continue;
}
pathContext.lineTo(pathArray[i].actualX, pathArray[i].actualY);
codeCGPath.innerHTML += 'CGPathAddLineToPoint(path, NULL, '+pathArray[i].displayX+' - offsetX, '+pathArray[i].displayY+' - offsetY);<br>';
}
pathContext.closePath();
pathContext.lineWidth = 1;
pathContext.strokeStyle = 'blue';
pathContext.stroke();
pathContext.fillStyle = 'blue';
pathContext.fill();
}
function resetShape(){
pathArray = new Array();
codeCGPath.innerHTML = null;
pathContext.clearRect(0, 0, pathCanvas.width, pathCanvas.height);
}
</script>
</body>
</html>
I created an editor and loader class to create complex SKPhysicsBodies and import them into your code. It allows you to trace around your sprite, add multiple bodies and export all within a pretty nice interface. Check out the SKImport here and the editor.
I know this is a bit late, but I've just created a cool tool for this purpose which automatically creates a path around the sprite image (so you don't have to manually click on the points yourself), and then you can adjust various settings to better suit your requirements. The tool also outputs both Objective C and Swift program code for adding the path to a sprite physics body. Hope it's helpful to some people. Thanks:
http://www.radicalphase.com/pathgen/
Here is the original script (from DazChong) adapted for Swift
SKPhysicsBody Path Generator Swift Version
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SpriteKit Tools - SKPhysicsBody Path Generator (Swift version </title>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<style>
/* disable responsive */
.container {
max-width: none;
width: 970px;
}
#sprite {
background-color: #eee;
position: absolute;
}
#path {
cursor: crosshair;
opacity: 0.5;
}
</style>
</head>
<body>
<div class="container">
<h1>SKPhysicsBody Path Generator</h1>
<p class="lead">Want to use SKPhysicsBody(polygonFromPath: path) easier way like me? Here with a small helper for easier path drawing, hope it help others too.</p>
<div class="row">
<div class="col-md-6">
<h5>Basic Instruction</h5>
<ol>
<li><small>Drag and drop the sprite image into drop zone.</small></li>
<li><small>Start drawing path by clicking on coordinates.</small></li>
</ol>
</div>
<div class="col-md-6">
<h5>Some Rules / Known Issue</h5>
<ul>
<li><small>Path need to be as a convex polygonal path with counterclockwise winding and no self intersections. The points are specified relative to the owning node’s origin. (documentation link)</small></li>
<li><small>Please use Chrome for best compatibility as I have not tested on other browsers.</small></li>
</ul>
</div>
</div>
<hr>
<div class="btn-group">
<button class="btn btn-primary" type="button" onclick="resetShape()">Reset Shape</button>
<button class="btn btn-primary" type="button" onclick="location.reload()">Reset All</button>
</div>
<input type="checkbox" onclick="toggleRetinaMode()" id="retinaCheckbox" checked> Retina? (please check before declaring path)
<br><br>
<canvas id="sprite" width="940" height="100"></canvas>
<canvas id="path" width="0" height="100"></canvas>
<p class="text-muted"><small>X:<span id="tooltipX">0</span> Y:<span id="tooltipY">0</span></small></p>
<br>
<h5>Output</h5>
<pre>
let sprite = SKSpriteNode(imageNamed: "codeImgName")
let offsetX = sprite.size.width * sprite.anchorPoint.x
let offsetY = sprite.size.height * sprite.anchorPoint.y
let path = CGPathCreateMutable()
<span id="codeCGPath"></span>
CGPathCloseSubpath(path)
sprite.physicsBody = SKPhysicsBody(polygonFromPath: path)
</pre>
</div>
<script>
// reference from http://davidwalsh.name/resize-image-canvas
var spriteCanvas = document.getElementById('sprite');
var spriteContext = spriteCanvas.getContext('2d');
spriteContext.fillText('Drop Sprite Image Here', 400, 50);
var pathCanvas = document.getElementById('path');
var pathContext = pathCanvas.getContext('2d');
function render(src){
var image = new Image();
image.onload = function(){
spriteContext.clearRect(0, 0, spriteCanvas.width, spriteCanvas.height);
spriteCanvas.width = image.width;
spriteCanvas.height = image.height;
spriteContext.drawImage(image, 0, 0, image.width, image.height);
pathContext.clearRect(0, 0, pathCanvas.width, pathCanvas.height);
pathCanvas.width = image.width;
pathCanvas.height = image.height;
};
image.src = src;
}
function loadImage(src){
if(!src.type.match(/image.*/)){
console.log('Dropped file is not image format');
return;
}
var reader = new FileReader();
reader.onload = function(e){
render(e.target.result);
};
reader.readAsDataURL(src);
var fileName = src.name;
var codeImgName = document.getElementById('codeImgName');
codeImgName.innerHTML = fileName;
}
spriteCanvas.addEventListener('dragover', function(e){
e.preventDefault();
}, true);
spriteCanvas.addEventListener('drop', function(e){
e.preventDefault();
loadImage(e.dataTransfer.files[0]);
}, true);
var retinaMode = true;
function toggleRetinaMode(){
var status = document.getElementById('retinaCheckbox');
retinaMode = status.checked ? true : false;
}
var actualX = 0;
var actualY = 0;
var displayX = document.getElementById('tooltipX');
var displayY = document.getElementById('tooltipY');
pathCanvas.onmousemove = function(e){
actualX = e.pageX - this.offsetLeft;
actualY = e.pageY - this.offsetTop;
displayX.innerHTML = retinaMode ? Math.floor(actualX / 2) : actualX;
displayY.innerHTML = retinaMode ? Math.floor((spriteCanvas.height - actualY - 1) / 2) : spriteCanvas.height - actualY - 1;
}
var pathArray = new Array();
pathCanvas.onclick = function(e){
var coor = {
actualX: actualX,
actualY: actualY,
displayX: displayX.innerHTML,
displayY: displayY.innerHTML,
};
pathArray.push(coor);
refreshShape(pathArray);
}
var codeCGPath = document.getElementById('codeCGPath');
function refreshShape(pathArray){
pathContext.clearRect(0, 0, pathCanvas.width, pathCanvas.height);
pathContext.beginPath();
for(var i in pathArray){
if(i == 0) {
pathContext.moveTo(pathArray[i].actualX, pathArray[i].actualY);
codeCGPath.innerHTML = 'CGPathMoveToPoint(path, nil, '+pathArray[i].displayX+' - offsetX, '+pathArray[i].displayY+' - offsetY)<br>';
continue;
}
pathContext.lineTo(pathArray[i].actualX, pathArray[i].actualY);
codeCGPath.innerHTML += 'CGPathAddLineToPoint(path, nil, '+pathArray[i].displayX+' - offsetX, '+pathArray[i].displayY+' - offsetY)<br>';
}
pathContext.closePath();
pathContext.lineWidth = 1;
pathContext.strokeStyle = 'blue';
pathContext.stroke();
pathContext.fillStyle = 'blue';
pathContext.fill();
}
function resetShape(){
pathArray = new Array();
codeCGPath.innerHTML = null;
pathContext.clearRect(0, 0, pathCanvas.width, pathCanvas.height);
}
</script>
</body>
</html>
The Skphysicsbody Path generator tool seems to be missing.
I wrote an app that does the same thing though on mac: https://itunes.apple.com/us/app/physicsbodymaker/id951249779?ls=1&mt=12
This is an adaptation of Xelt's answer in Swift 3.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SpriteKit Tools - SKPhysicsBody Path Generator (Swift version </title>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<style>
/* disable responsive */
.container {
max-width: none;
width: 970px;
}
#sprite {
background-color: #eee;
position: absolute;
}
#path {
cursor: crosshair;
opacity: 0.5;
}
</style>
</head>
<body>
<div class="container">
<h1>SKPhysicsBody Path Generator</h1>
<p class="lead">Want to use SKPhysicsBody(polygonFromPath: path) easier way like me? Here with a small helper for easier path drawing, hope it help others too.</p>
<div class="row">
<div class="col-md-6">
<h5>Basic Instruction</h5>
<ol>
<li><small>Drag and drop the sprite image into drop zone.</small></li>
<li><small>Start drawing path by clicking on coordinates.</small></li>
</ol>
</div>
<div class="col-md-6">
<h5>Some Rules / Known Issue</h5>
<ul>
<li><small>Path need to be as a convex polygonal path with counterclockwise winding and no self intersections. The points are specified relative to the owning node’s origin. (documentation link)</small></li>
<li><small>Please use Chrome for best compatibility as I have not tested on other browsers.</small></li>
</ul>
</div>
</div>
<hr>
<div class="btn-group">
<button class="btn btn-primary" type="button" onclick="resetShape()">Reset Shape</button>
<button class="btn btn-primary" type="button" onclick="location.reload()">Reset All</button>
</div>
<input type="checkbox" onclick="toggleRetinaMode()" id="retinaCheckbox" checked> Retina? (please check before declaring path)
<br><br>
<canvas id="sprite" width="940" height="100"></canvas>
<canvas id="path" width="0" height="100"></canvas>
<p class="text-muted"><small>X:<span id="tooltipX">0</span> Y:<span id="tooltipY">0</span></small></p>
<br>
<h5>Output</h5>
<pre>
let sprite = SKSpriteNode(imageNamed: "codeImgName")
let offsetX = sprite.size.width * sprite.anchorPoint.x
let offsetY = sprite.size.height * sprite.anchorPoint.y
let path = CGMutablePath()
<span id="codeCGPath"></span>
path.closeSubpath()
sprite.physicsBody = SKPhysicsBody(polygonFromPath: path)
</pre>
</div>
<script>
// reference from http://davidwalsh.name/resize-image-canvas
var spriteCanvas = document.getElementById('sprite');
var spriteContext = spriteCanvas.getContext('2d');
spriteContext.fillText('Drop Sprite Image Here', 400, 50);
var pathCanvas = document.getElementById('path');
var pathContext = pathCanvas.getContext('2d');
function render(src){
var image = new Image();
image.onload = function(){
spriteContext.clearRect(0, 0, spriteCanvas.width, spriteCanvas.height);
spriteCanvas.width = image.width;
spriteCanvas.height = image.height;
spriteContext.drawImage(image, 0, 0, image.width, image.height);
pathContext.clearRect(0, 0, pathCanvas.width, pathCanvas.height);
pathCanvas.width = image.width;
pathCanvas.height = image.height;
};
image.src = src;
}
function loadImage(src){
if(!src.type.match(/image.*/)){
console.log('Dropped file is not image format');
return;
}
var reader = new FileReader();
reader.onload = function(e){
render(e.target.result);
};
reader.readAsDataURL(src);
var fileName = src.name;
var codeImgName = document.getElementById('codeImgName');
codeImgName.innerHTML = fileName;
}
spriteCanvas.addEventListener('dragover', function(e){
e.preventDefault();
}, true);
spriteCanvas.addEventListener('drop', function(e){
e.preventDefault();
loadImage(e.dataTransfer.files[0]);
}, true);
var retinaMode = true;
function toggleRetinaMode(){
var status = document.getElementById('retinaCheckbox');
retinaMode = status.checked ? true : false;
}
var actualX = 0;
var actualY = 0;
var displayX = document.getElementById('tooltipX');
var displayY = document.getElementById('tooltipY');
pathCanvas.onmousemove = function(e){
actualX = e.pageX - this.offsetLeft;
actualY = e.pageY - this.offsetTop;
displayX.innerHTML = retinaMode ? Math.floor(actualX / 2) : actualX;
displayY.innerHTML = retinaMode ? Math.floor((spriteCanvas.height - actualY - 1) / 2) : spriteCanvas.height - actualY - 1;
}
var pathArray = new Array();
pathCanvas.onclick = function(e){
var coor = {
actualX: actualX,
actualY: actualY,
displayX: displayX.innerHTML,
displayY: displayY.innerHTML,
};
pathArray.push(coor);
refreshShape(pathArray);
}
var codeCGPath = document.getElementById('codeCGPath');
function refreshShape(pathArray){
pathContext.clearRect(0, 0, pathCanvas.width, pathCanvas.height);
pathContext.beginPath();
for(var i in pathArray){
if(i == 0) {
pathContext.moveTo(pathArray[i].actualX, pathArray[i].actualY);
codeCGPath.innerHTML = 'path.move(to: CGPoint(x: '+pathArray[i].displayX+' - offsetX, y: '+pathArray[i].displayY+' - offsetY))<br>';
continue;
}
pathContext.lineTo(pathArray[i].actualX, pathArray[i].actualY);
codeCGPath.innerHTML += 'path.addLine(to: CGPoint(x: '+pathArray[i].displayX+' - offsetX, y: '+pathArray[i].displayY+' - offsetY))<br>';
}
pathContext.closePath();
pathContext.lineWidth = 1;
pathContext.strokeStyle = 'blue';
pathContext.stroke();
pathContext.fillStyle = 'blue';
pathContext.fill();
}
function resetShape(){
pathArray = new Array();
codeCGPath.innerHTML = null;
pathContext.clearRect(0, 0, pathCanvas.width, pathCanvas.height);
}
</script>
</body>
</html>
Awesome little web app, by DazChong. And canot wait for the update of PhysicsEditor!!
This one is also in development
You can download it in alpha stage here
You can just do this now to generate physics body from your sprite's PNG:
SKSpriteNode *yourPhysicsSprite = [SKSpriteNode spriteNodeWithImageNamed:#"yourPNG"];
yourPhysicsSprite.physicsBody = [SKPhysicsBody bodyWithTexture:yourPhysicsSprite.texture alphaThreshold:0.0f size:yourPhysicsSprite.texture.size];
Less precise and perhaps more costly than doing by hand, but works fine.

Resources