/**
* Copyright 2010 The ForPlay Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package forplay.html;
import com.google.gwt.core.client.Duration;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Cursor;
import com.google.gwt.user.client.Window;
import forplay.core.Storage;
import forplay.core.Analytics;
import forplay.core.Audio;
import forplay.core.ForPlay;
import forplay.core.Game;
import forplay.core.Graphics;
import forplay.core.Json;
import forplay.core.Keyboard;
import forplay.core.Log;
import forplay.core.Net;
import forplay.core.Platform;
import forplay.core.Pointer;
import forplay.core.Mouse;
import forplay.core.Touch;
import forplay.core.RegularExpression;
import forplay.html.HtmlUrlParameters.Renderer;
public class HtmlPlatform implements Platform {
/** Used by {@link #register(Mode)}. */
public static enum Mode {
WEBGL {
public boolean useGL() {
return true;
}
},
CANVAS {
public boolean useGL() {
return false;
}
},
AUTODETECT {
public boolean useGL() {
return shouldUseGL();
}
};
public abstract boolean useGL ();
}
static final int DEFAULT_WIDTH = 640;
static final int DEFAULT_HEIGHT = 480;
private static final int LOG_FREQ = 2500;
private static final float MAX_DELTA = 100;
/**
* Prepares the HTML platform for operation.
*/
public static HtmlPlatform register() {
return register(Mode.AUTODETECT);
}
/**
* Prepares the HTML platform for operation.
*
* @param mode indicates whether to force the use of WebGL, force the use of Canvas, or to
* autodetect whether the browser supports WebGL and use it if possible.
*/
public static HtmlPlatform register(Mode mode) {
HtmlPlatform platform = new HtmlPlatform(mode);
ForPlay.setPlatform(platform);
platform.init();
return platform;
}
static native void addEventListener(JavaScriptObject target, String name, EventHandler handler,
boolean capture) /*-{
target.addEventListener(name, function(e) {
handler.@forplay.html.EventHandler::handleEvent(Lcom/google/gwt/dom/client/NativeEvent;)(e);
}, capture);
}-*/;
static void captureEvent(String name, EventHandler handler) {
captureEvent(null, name, handler);
}
static void captureEvent(Element target, String name, EventHandler handler) {
addEventListener((target == null ? Document.get() : target), name, handler, true);
}
private HtmlAssetManager assetManager = GWT.create(HtmlAssetManager.class);
private HtmlAudio audio = new HtmlAudio();
private HtmlRegularExpression regularExpression = new HtmlRegularExpression();
private Game game;
private HtmlGraphics graphics;
private HtmlJson json = new HtmlJson();
private HtmlKeyboard keyboard = new HtmlKeyboard();
private HtmlLog log;
private HtmlNet net = new HtmlNet();
private HtmlPointer pointer;
private HtmlMouse mouse;
private HtmlTouch touch;
private HtmlStorage storage = new HtmlStorage();
private HtmlAnalytics analytics = new HtmlAnalytics();
private TimerCallback paintCallback;
private TimerCallback updateCallback;
// Non-instantiable.
private HtmlPlatform(Mode mode) {
// Setup logging first, so it can be used by other subsystems
log = GWT.create(HtmlLog.class);
if (!GWT.isProdMode()) {
log.info("You are running in GWT Development Mode. "
+ "For optimal performance you may want to use an alternative method. "
+ "See http://code.google.com/p/forplay/wiki/GameDebuggingOptions");
}
/*
* Wrap remaining calls in try-catch, since the UncaughtExceptionHandler installed by HtmlLog
* above won't take effect until we yield to the browser event loop. That means we have to catch
* our own exceptions here.
*/
try {
try {
graphics = mode.useGL() ? new HtmlGraphicsGL() : new HtmlGraphicsDom();
} catch (RuntimeException e) {
// HtmlGraphicsGL ctor throws a runtime exception if the context creation fails.
log().info("Failed to create GL context. Falling back.");
graphics = new HtmlGraphicsDom();
}
pointer = new HtmlPointer(graphics.getRootElement());
mouse = new HtmlMouse(graphics.getRootElement());
touch = new HtmlTouch(graphics.getRootElement());
} catch (Throwable e) {
log.error("init()", e);
Window.alert("failed to init(): " + e.getMessage());
}
}
public void init() {
analytics.init();
audio.init();
keyboard.init();
}
@Override
public HtmlAssetManager assetManager() {
return assetManager;
}
@Override
public Audio audio() {
return audio;
}
@Override
public Graphics graphics() {
return graphics;
}
@Override
public Json json() {
return json;
}
@Override
public Keyboard keyboard() {
return keyboard;
}
@Override
public Log log() {
return log;
}
@Override
public Net net() {
return net;
}
@Override
public Pointer pointer() {
return pointer;
}
@Override
public Mouse mouse() {
return mouse;
}
@Override
public Touch touch() {
return touch;
}
@Override
public Storage storage() {
return storage;
}
@Override
public Analytics analytics() {
return analytics;
}
@Override
public float random() {
return (float) Math.random();
}
@Override
public RegularExpression regularExpression() {
return regularExpression;
}
@Override
public void run(final Game game) {
final int updateRate = game.updateRate();
this.game = game;
game.init();
// Game loop.
paintCallback = new TimerCallback() {
private float accum = updateRate;
private double lastTime;
@Override
public void fire() {
requestAnimationFrame(paintCallback);
double now = time();
float delta = (float) (now - lastTime);
if (delta > MAX_DELTA) {
delta = MAX_DELTA;
}
lastTime = now;
if (updateRate == 0) {
game.update(delta);
accum = 0;
} else {
accum += delta;
while (accum > updateRate) {
game.update(updateRate);
accum -= updateRate;
}
}
game.paint(accum / updateRate);
graphics.updateLayers();
}
};
requestAnimationFrame(paintCallback);
}
@Override
public double time() {
return Duration.currentTimeMillis();
}
private native JavaScriptObject getWindow() /*-{
return $wnd;
}-*/;
private native void requestAnimationFrame(TimerCallback callback) /*-{
var fn = function() {
callback.@forplay.html.TimerCallback::fire()();
};
if ($wnd.requestAnimationFrame) {
$wnd.requestAnimationFrame(fn);
} else if ($wnd.mozRequestAnimationFrame) {
$wnd.mozRequestAnimationFrame(fn);
} else if ($wnd.webkitRequestAnimationFrame) {
$wnd.webkitRequestAnimationFrame(fn);
} else {
// 20ms => 50fps
$wnd.setTimeout(fn, 20);
}
}-*/;
private native int setInterval(TimerCallback callback, int ms) /*-{
return $wnd.setInterval(function() {
callback.@forplay.html.TimerCallback::fire()();
}, ms);
}-*/;
private native int setTimeout(TimerCallback callback, int ms) /*-{
return $wnd.setTimeout(function() {
callback.@forplay.html.TimerCallback::fire()();
}, ms);
}-*/;
/**
* Gets the URL's parameter of the specified name. Note that if multiple parameters have been
* specified with the same name, the last one will be returned.
*
* @param name the name of the URL's parameter
* @return the value of the URL's parameter
*/
public String getUrlParameter(String name) {
return Window.Location.getParameter(name);
}
/**
* @see forplay.core.Platform#openURL(java.lang.String)
*/
@Override
public void openURL(String url) {
Window.open(url, "_blank", "");
}
/**
* Sets the title of the browser's window or tab.
*
* @param title the window title
*/
public void setTitle(String title) {
Window.setTitle(title);
}
/**
* Sets the {@code cursor} CSS property.
*
* @param cursor the {@link Cursor} to use, or null to hide the cursor.
*/
public static void setCursor(Cursor cursor) {
Element rootElement = ((HtmlGraphics) ForPlay.graphics()).getRootElement();
if (cursor == null) {
rootElement.getStyle().setProperty("cursor", "none");
} else {
rootElement.getStyle().setCursor(cursor);
}
}
/**
* Disable the right-click context menu.
*/
public static void disableRightClickContextMenu() {
Element rootElement = ((HtmlGraphics) ForPlay.graphics()).getRootElement();
disableRightClickImpl(rootElement);
}
/**
* Return true if renderer query parameter equals {@link Renderer#GL} or is not set, and the
* browser supports WebGL
*
* @return true if renderer query parameter equals {@link Renderer#GL} or is not set, and the
* browser supports WebGL
*/
private static boolean shouldUseGL() {
boolean useGlFromFlag = Renderer.shouldUseGL();
return (useGlFromFlag && hasGLSupport());
}
/**
* Return true if the browser supports WebGL
*
* Note: This test can have false positives depending on the graphics hardware.
*
* @return true if the browser supports WebGL
*/
private static native boolean hasGLSupport() /*-{
return !!$wnd.WebGLRenderingContext &&
// WebGL is slow on Chrome OSX 10.5
(!/Chrome/.test(navigator.userAgent) || !/OS X 10_5/.test(navigator.userAgent));
}-*/;
private static native void disableRightClickImpl(JavaScriptObject target) /*-{
target.oncontextmenu = function() {
return false;
};
}-*/;
}