I’m running into a peculiar issue where the JavaScript toDataURL() functionality isn’t grabbing the expected image data from the PlayCanvas canvas on iPad (and likely other tablets).
http://54.161.163.84/playcanvas-camera/
Above is a project using a basic 3D model with the camera code we’re using in our actual project (will be included at the end of the post).
In our actual application, we first call this code to gain access to PlayCanvas’s camera object:
var app = pc.Application.getApplication('application-canvas');
var context = app.context;
var camera = context.root.findByName('Camera');
Next, we run the following code to reorient the camera back to its default position and then move it to the position we want it to be moved to:
camera.script.camera.initialize();
camera.script.camera.orbit(-220, 20);
In our actual script, we have an Ajax function that takes a screenshot and saves it to a folder, but in this example we’ll have to either log it out to the console or open it in a new window (latter doesn’t seem to work properly in iOS for whatever reason):
(Also, note that I’m using [0] here because our main application uses jQuery. This application is also using jQuery for debugging/testing purposes.)
console.log($("#application-canvas")[0].toDataURL());
window.open($("#application-canvas")[0].toDataURL());
The code appears to work as expected on desktop, but I’m noticing on the iPad where it either works as expected, lags behind for a while until it works a few seconds later, or never works at all (but then I move it a little bit, and it works again — but sometimes it stops working again for no reason).
Any idea what could be causing this problem on the iPad? Is it a resources issue or some setting in PlayCanvas I need to enable/disable? Or a different way to grab the dataURL value that works better in iOS?
[Below is the camera script, in case that helps with debugging. Note that there’s references to hammer.js, but it’s just the basic minified library and isn’t anything customized or special.]
var Camera = pc.createScript('camera');
Camera.attributes.add('maxElevation', {
type: 'number',
title: 'Max Elevation',
default: 10
});
Camera.attributes.add('minZ', {
type: 'number',
title: 'Min Z',
default: 0.14
});
Camera.attributes.add('maxZ', {
type: 'number',
title: 'Max Z',
default: 0.20
});
// initialize code called once per entity
Camera.prototype.initialize = function() {
this.viewPos = new pc.Vec3();
this.targetViewPos = new pc.Vec3();
this.tempVec = new pc.Vec3();
this.distance = 3;
this.targetDistance = 3;
this.rotX = -180;
this.rotY = 0;
this.targetRotX = -40;
this.targetRotY = 30;
this.quatX = new pc.Quat();
this.quatY = new pc.Quat();
this.transformStarted = false;
// Disabling the context menu stops the browser disabling a menu when
// you right-click the page
this.app.mouse.disableContextMenu();
this.setBestCameraPositionForModel();
////////////////////
// Touch controls //
////////////////////
var options = {
prevent_default: true,
drag_max_touches: 2,
transform_min_scale: 0.08,
transform_min_rotation: 180,
transform_always_block: true,
hold: false,
release: false,
swipe: false,
tap: false
};
this.hammer = Hammer(this.app.graphicsDevice.canvas, options);
// Pinch zoom
var cachedTargetDistance;
this.hammer.on("transformstart", function (event) {
this.transformStarted = true;
cachedTargetDistance = this.targetDistance;
event.preventDefault();
this.hammer.options.drag = false;
}.bind(this));
this.hammer.on("transformend", function (event) {
this.transformStarted = false;
this.hammer.options.drag = true;
}.bind(this));
this.hammer.on("transform", function (event) {
if (this.transformStarted) {
var gesture = event.gesture;
var scale = gesture.scale;
this.targetDistance = cachedTargetDistance / scale;
}
}.bind(this));
// Orbit (1 finger) and pan (2 fingers)
var cachedX, cachedY;
this.hammer.on("dragstart", function (event) {
if (!this.transformStarted) {
var gesture = event.gesture;
var numTouches = (gesture.touches !== undefined) ? gesture.touches.length : 1;
this.panning = (numTouches === 2);
this.dragStarted = true;
cachedX = gesture.center.pageX;
cachedY = gesture.center.pageY;
}
}.bind(this));
this.hammer.on("dragend", function (event) {
if (this.dragStarted) {
this.dragStarted = false;
this.panning = false;
}
}.bind(this));
this.hammer.on("drag", function (event) {
var gesture = event.gesture;
var dx = gesture.center.pageX - cachedX;
var dy = gesture.center.pageY - cachedY;
if (this.panning) {
this.pan(dx * -0.025, dy * 0.025);
} else {
this.orbit(dx * 0.5, dy * 0.5);
}
cachedX = gesture.center.pageX;
cachedY = gesture.center.pageY;
}.bind(this));
this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
this.app.mouse.on(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
};
Camera.prototype.setBestCameraPositionForModel = function() {
var i, j;
// Position the camera somewhere sensible
var models = this.app.scene.getModels();
var isUnderCamera = function (mi) {
var parent = mi.node.getParent();
while (parent) {
if (parent.camera) {
return true;
}
parent = parent.getParent();
}
return false;
};
var meshInstances = [];
for (i = 0; i < models.length; i++) {
var mi = models[i].meshInstances;
for (j = 0; j < mi.length; j++) {
if (!isUnderCamera(mi[j])) {
meshInstances.push(mi[j]);
}
}
}
if (meshInstances.length > 0) {
var aabb = new pc.shape.Aabb();
aabb.copy(meshInstances[0].aabb);
for (i = 0; i < meshInstances.length; i++) {
aabb.add(meshInstances[i].aabb);
}
var focus = aabb.center;
var halfHeight = aabb.halfExtents.y;
var halfDepth = aabb.halfExtents.z;
var offset = 1.5 * halfHeight / Math.tan(0.5 * this.entity.camera.fov * Math.PI / 180.0);
this.reset(focus, offset + halfDepth);
} else {
this.reset(pc.Vec3.ZERO, 3);
}
};
Camera.prototype.reset = function(target, distance) {
this.viewPos.copy(target);
this.targetViewPos.copy(target);
this.distance = distance;
this.targetDistance = distance;
this.rotX = -180;
this.rotY = 0;
this.targetRotX = -40;
this.targetRotY = 30;
};
Camera.prototype.pan = function(movex, movey) {
// Pan around in the camera's local XY plane
this.tempVec.copy(this.entity.right).scale(movex);
this.targetViewPos.add(this.tempVec);
this.tempVec.copy(this.entity.up).scale(movey);
this.targetViewPos.add(this.tempVec);
};
Camera.prototype.dolly = function (movez) {
// Dolly along the Z axis of the camera's local transform
this.targetDistance += movez;
if (this.targetDistance < this.minZ) {
this.targetDistance = this.minZ;
}
if (this.targetDistance > this.maxZ) {
this.targetDistance = this.maxZ;
}
};
Camera.prototype.orbit = function (movex, movey) {
this.targetRotX += movex;
this.targetRotY += movey;
this.targetRotY = pc.math.clamp(this.targetRotY, -this.maxElevation, this.maxElevation);
};
Camera.prototype.onMouseWheel = function (event) {
event.event.preventDefault();
this.dolly(event.wheel * -0.25);
};
Camera.prototype.onMouseMove = function (event) {
if (event.buttons[pc.MOUSEBUTTON_LEFT]) {
this.orbit(event.dx * 0.2, event.dy * 0.2);
} else if (event.buttons[pc.MOUSEBUTTON_RIGHT]) {
var factor = this.distance / 700;
this.pan(event.dx * -factor, event.dy * factor);
}
};
// update code called every frame
Camera.prototype.update = function(dt) {
if (this.app.keyboard.wasPressed(pc.KEY_SPACE)) {
this.setBestCameraPositionForModel();
}
// Implement a delay in camera controls by lerping towards a target
this.viewPos.lerp(this.viewPos, this.targetViewPos, dt / 0.1);
this.distance = pc.math.lerp(this.distance, this.targetDistance, dt / 0.2);
this.rotX = pc.math.lerp(this.rotX, this.targetRotX, dt / 0.2);
this.rotY = pc.math.lerp(this.rotY, this.targetRotY, dt / 0.2);
// Calculate the camera's rotation
this.quatX.setFromAxisAngle(pc.Vec3.RIGHT, -this.rotY);
this.quatY.setFromAxisAngle(pc.Vec3.UP, -this.rotX);
this.quatY.mul(this.quatX);
// Set the camera's current position and orientation
this.entity.setPosition(this.viewPos);
this.entity.setRotation(this.quatY);
this.entity.translateLocal(0, 0, this.distance);
};