From 8e791eb9940a77bb1e31fcc24fb5c8ce7967b3bb Mon Sep 17 00:00:00 2001 From: Ray Greenwell Date: Wed, 28 Feb 2007 22:50:05 +0000 Subject: [PATCH] Flash components we've made, display-related utils. git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@161 ed5b42cb-e716-0410-a449-f6a68f950b19 --- src/as/com/threerings/flash/ColorUtil.as | 49 ++ src/as/com/threerings/flash/DisplayUtil.as | 183 +++++++ src/as/com/threerings/flash/FPSDisplay.as | 67 +++ src/as/com/threerings/flash/ImageUtil.as | 88 +++ src/as/com/threerings/flash/MediaContainer.as | 517 ++++++++++++++++++ src/as/com/threerings/flash/MenuUtil.as | 50 ++ src/as/com/threerings/flash/VideoDisplayer.as | 292 ++++++++++ 7 files changed, 1246 insertions(+) create mode 100644 src/as/com/threerings/flash/ColorUtil.as create mode 100644 src/as/com/threerings/flash/DisplayUtil.as create mode 100644 src/as/com/threerings/flash/FPSDisplay.as create mode 100644 src/as/com/threerings/flash/ImageUtil.as create mode 100644 src/as/com/threerings/flash/MediaContainer.as create mode 100644 src/as/com/threerings/flash/MenuUtil.as create mode 100644 src/as/com/threerings/flash/VideoDisplayer.as diff --git a/src/as/com/threerings/flash/ColorUtil.as b/src/as/com/threerings/flash/ColorUtil.as new file mode 100644 index 00000000..4b83b8da --- /dev/null +++ b/src/as/com/threerings/flash/ColorUtil.as @@ -0,0 +1,49 @@ +// +// $Id$ +// +// Nenya library - tools for developing networked games +// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/nenya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.flash { + +/** + * Color utility methods. + * + * See also mx.utils.ColorUtil. + */ +public class ColorUtil +{ + /** + * Blend the two colors, either 50-50 or according to the ratio specified. + */ + public static function blend ( + first :uint, second :uint, firstPerc :Number = 0.5) :uint + { + var secondPerc :Number = 1 - firstPerc; + + var result :uint = 0; + for (var shift :int = 0; shift <= 16; shift += 8) { + var c1 :uint = (first >> shift) & 0xFF; + var c2 :uint = (second >> shift) & 0xFF; + result |= uint(Math.max(0, Math.min(255, + (c1 * firstPerc) + (c2 * secondPerc)))) << shift; + } + return result; + } +} +} diff --git a/src/as/com/threerings/flash/DisplayUtil.as b/src/as/com/threerings/flash/DisplayUtil.as new file mode 100644 index 00000000..eb221ed0 --- /dev/null +++ b/src/as/com/threerings/flash/DisplayUtil.as @@ -0,0 +1,183 @@ +// +// $Id$ +// +// Nenya library - tools for developing networked games +// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/nenya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.flash { + +import flash.display.DisplayObject; +import flash.display.DisplayObjectContainer; + +import flash.geom.Point; +import flash.geom.Rectangle; + +import mx.core.IRawChildrenContainer; + +public class DisplayUtil +{ + /** + * Call the specified function for the display object and all descendants. + * + * This is nearly exactly like mx.utils.DisplayUtil.walkDisplayObjects, + * except this method copes with security errors when examining a child. + */ + public static function applyToHierarchy ( + disp :DisplayObject, callbackFunction :Function) :void + { + callbackFunction(disp); + + if (disp is DisplayObjectContainer) { + // a little type-unsafety so that we don't have to write two blocks + var o :Object = (disp is IRawChildrenContainer) ? + IRawChildrenContainer(disp).rawChildren : disp; + var nn :int = int(o.numChildren); + for (var ii :int = 0; ii < nn; ii++) { + try { + disp = DisplayObject(o.getChildAt(ii)); + } catch (err :SecurityError) { + continue; + } + // and then we apply outside of the try/catch block so that + // we don't hide errors thrown by the callbackFunction. + applyToHierarchy(disp, callbackFunction); + } + } + } + + /** + * Returns the most reasonable position for the specified rectangle to + * be placed at so as to maximize its containment by the specified + * bounding rectangle while still placing it as near its original + * coordinates as possible. + * + * @param rect the rectangle to be positioned. + * @param bounds the containing rectangle. + */ + public static function fitRectInRect ( + rect :Rectangle , bounds :Rectangle) :Point + { + // Guarantee that the right and bottom edges will be contained + // and do our best for the top and left edges. + var br :Point = bounds.bottomRight; + return new Point( + Math.min(br.x - rect.width, Math.max(rect.x, bounds.x)), + Math.min(br.y - rect.height, Math.max(rect.y, bounds.y))); + } + + /** + * Position the specified rectangle within the bounds, avoiding + * any of the Rectangles in the avoid array, which may be destructively + * modified. + * + * @return true if the rectangle was successfully placed, given the + * constraints, or false if the positioning failed (the rectangle will + * be left at its original location. + */ + public static function positionRect ( + r :Rectangle, bounds :Rectangle, avoid :Array) :Boolean + { + var origPos :Point = r.topLeft; + var pointSorter :Function = createPointSorter(origPos); + var possibles :Array = new Array(); + // start things off with the passed-in point (adjusted to + // be inside the bounds, if needed) + possibles.push(fitRectInRect(r, bounds)); + + // keep track of area that doesn't generate new possibles + var dead :Array = new Array(); + + // Note: labeled breaks and continues are supposed to be legal, + // but they throw wacky runtime exceptions for me. So instead + // I'm throwing a boolean and using that to continue the while + /* CHECKPOSSIBLES: */ while (possibles.length > 0) { + try { + var p :Point = (possibles.shift() as Point); + r.x = p.x; + r.y = p.y; + + // make sure the rectangle is in the view + if (!bounds.containsRect(r)) { + continue; + } + + // and not over a dead area + for each (var deadRect :Rectangle in dead) { + if (deadRect.intersects(r)) { + //continue CHECKPOSSIBLES; + throw true; // continue outer loop + } + } + + // see if it hits any rects we're trying to avoid + for (var ii :int = 0; ii < avoid.length; ii++) { + var avoidRect :Rectangle = (avoid[ii] as Rectangle); + if (avoidRect.intersects(r)) { + // remove it from the avoid list + avoid.splice(ii, 1); + // but add it to the dead list + dead.push(avoidRect); + + // add 4 new possible points, each pushed in + // one direction + possibles.push( + new Point(avoidRect.x - r.width, r.y), + new Point(r.x, avoidRect.y - r.height), + new Point(avoidRect.x + avoidRect.width, r.y), + new Point(r.x, avoidRect.y + avoidRect.height)); + + // re-sort the list + possibles.sort(pointSorter); + //continue CHECKPOSSIBLES; + throw true; // continue outer loop + } + } + + // hey! if we got here, then it worked! + return true; + + } catch (continueWhile :Boolean) { + // simply catch the boolean and use it to continue inner loops + } + } + + // we never found a match, move the rectangle back + r.x = origPos.x; + r.y = origPos.y; + return false; + } + + /** + * Create a sort Function that can be used to compare Points in an + * Array according to their distance from the specified Point. + * + * Note: The function will always sort according to distance from the + * passed-in point, even if that point's coordinates change after + * the function is created. + */ + public static function createPointSorter (origin :Point) :Function + { + return function (p1 :Point, p2 :Point) :Number { + var dist1 :Number = Point.distance(origin, p1); + var dist2 :Number = Point.distance(origin, p2); + + return (dist1 > dist2) ? 1 : ((dist1 < dist2) ? -1 : 0); // signum + }; + } +} +} diff --git a/src/as/com/threerings/flash/FPSDisplay.as b/src/as/com/threerings/flash/FPSDisplay.as new file mode 100644 index 00000000..58b9c4a8 --- /dev/null +++ b/src/as/com/threerings/flash/FPSDisplay.as @@ -0,0 +1,67 @@ +// +// $Id$ +// +// Nenya library - tools for developing networked games +// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/nenya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.flash { + +import flash.events.Event; + +import flash.text.TextField; + +import flash.utils.getTimer; // function import + +public class FPSDisplay extends TextField +{ + public function FPSDisplay (framesToTrack :int = 150) + { + background = true; + text = String(Number.MIN_VALUE); + height = textHeight + 4; + + _framesToTrack = framesToTrack; + + addEventListener(Event.ENTER_FRAME, handleEnterFrame); + } + + protected function handleEnterFrame (event :Event) :void + { + var curStamp :Number = getTimer(); + _frameStamps.push(curStamp); + if (_frameStamps.length > _framesToTrack) { + _frameStamps.shift(); // forget the oldest timestamps + } + + var firstStamp :Number = Number(_frameStamps[0]); + var seconds :Number = (curStamp - firstStamp) / 1000; + // subtract one from the frames, since we're measuring the time + // elapsed over the frames (when there are two timestamps, that's + // the difference between 1 frame) + var frames :Number = _frameStamps.length - 1; + + this.text = String(frames / seconds); + } + + /** Timestamps of past ENTER_FRAME events. */ + protected var _frameStamps :Array = []; + + /** The number of running frames we track. */ + protected var _framesToTrack :int; +} +} diff --git a/src/as/com/threerings/flash/ImageUtil.as b/src/as/com/threerings/flash/ImageUtil.as new file mode 100644 index 00000000..a990d873 --- /dev/null +++ b/src/as/com/threerings/flash/ImageUtil.as @@ -0,0 +1,88 @@ +// +// $Id$ +// +// Nenya library - tools for developing networked games +// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/nenya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.flash { + +import flash.display.Bitmap; +import flash.display.BitmapData; +import flash.display.DisplayObject; +import flash.display.Shape; + +import flash.geom.Point; +import flash.geom.Rectangle; + +import flash.text.TextField; +import flash.text.TextLineMetrics; + +/** + * Image and Bitmap related utility functions. + */ +public class ImageUtil +{ + /** + * Create a DisplayObject that will display an error message + * of the specified dimensions. + */ + public static function createErrorImage (width :int, height :int) + :DisplayObject + { + var shape :Shape = new Shape(); + shape.graphics.beginBitmapFill(createErrorBitmap()); + shape.graphics.drawRect(0, 0, width, height); + shape.graphics.endFill(); + return shape; + + /* + // Alternate implementation that creates an actual Bitmap object + var src :BitmapData = createErrorBitmap(); + var data :BitmapData = new BitmapData(width, height, false); + data.draw(src); + var rect :Rectangle = new Rectangle(0, 0, src.width, src.height); + for (var xx :int = 0; xx < width; xx += src.width) { + for (var yy :int = 0; yy < height; yy += src.height) { + if (xx != 0 || yy != 0) { + data.copyPixels(data, rect, new Point(xx, yy)); + } + } + } + return new Bitmap(data); + */ + } + + /** + * Create a minimally-sized "error" BitmapData. + */ + public static function createErrorBitmap () :BitmapData + { + var txt :TextField = new TextField(); + txt.text = "Error"; + var metrics :TextLineMetrics = txt.getLineMetrics(0); + var data :BitmapData = new BitmapData( + metrics.width + ERROR_PADDING, metrics.height + ERROR_PADDING, + false, 0xFF4040); + data.draw(txt); + return data; + } + + /** The amount to pad error messages by. */ + private static const ERROR_PADDING :int = 5; +} +} diff --git a/src/as/com/threerings/flash/MediaContainer.as b/src/as/com/threerings/flash/MediaContainer.as new file mode 100644 index 00000000..139a0a55 --- /dev/null +++ b/src/as/com/threerings/flash/MediaContainer.as @@ -0,0 +1,517 @@ +// +// $Id$ +// +// Nenya library - tools for developing networked games +// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/nenya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.flash { + +//import flash.display.Bitmap; +import flash.display.DisplayObject; +import flash.display.DisplayObjectContainer; +import flash.display.Loader; +import flash.display.LoaderInfo; +import flash.display.Shape; +import flash.display.Sprite; + +import flash.errors.IOError; + +import flash.events.Event; +import flash.events.EventDispatcher; +import flash.events.IEventDispatcher; +import flash.events.IOErrorEvent; +import flash.events.MouseEvent; +import flash.events.NetStatusEvent; +import flash.events.ProgressEvent; +import flash.events.SecurityErrorEvent; +import flash.events.StatusEvent; +import flash.events.TextEvent; + +import flash.geom.Point; + +import flash.text.TextField; +import flash.text.TextFieldAutoSize; + +import flash.system.ApplicationDomain; +import flash.system.LoaderContext; +import flash.system.SecurityDomain; + +import flash.net.URLRequest; + +import com.threerings.util.StringUtil; +import com.threerings.util.ValueEvent; +import com.threerings.util.Util; + +/** + * A wrapper class for all media that will be placed on the screen. + * Subject to change. + */ +public class MediaContainer extends Sprite +{ + /** A ValueEvent we dispatch when our size is known. + * Value: [ width, height ]. */ + public static const SIZE_KNOWN :String = "mediaSizeKnown"; + + /** A log instance that can be shared by sprites. */ + protected static const log :Log = Log.getLog(MediaContainer); + + /** + * Constructor. + */ + public function MediaContainer (url :String = null) + { + if (url != null) { + setMedia(url); + } + + mouseEnabled = false; + mouseChildren = true; + } + + /** + * Get the media. If the media was loaded using a URL, this will + * likely be the Loader object holding the real media. + */ + public function getMedia () :DisplayObject + { + return _media; + } + + /** + * Configure the media to display. + */ + public function setMedia (url :String) :void + { + if (Util.equals(_url, url)) { + return; // no change + } + _url = url; + + // shutdown any previous media + if (_media != null) { + shutdown(false); + } + + // set up the new media + if (StringUtil.endsWith(url.toLowerCase(), ".flv")) { + setupVideo(url); + + } else { + setupSwfOrImage(url); + } + } + + /** + * Configure our media as an instance of the specified class. + */ + public function setMediaClass (clazz :Class) :void + { + setMediaObject(new clazz() as DisplayObject); + } + + /** + * Configure an already-instantiated DisplayObject as our media. + */ + public function setMediaObject (disp :DisplayObject) :void + { + _url = null; + if (_media != null) { + shutdown(false); + } + + addChild(disp); + _media = disp; + updateContentDimensions(disp.width, disp.height); + } + + /** + * Configure this sprite to show a video. + */ + protected function setupVideo (url :String) :void + { + var vid :VideoDisplayer = new VideoDisplayer(); + vid.addEventListener(VideoDisplayer.SIZE_KNOWN, handleVideoSizeKnown); + vid.addEventListener(VideoDisplayer.VIDEO_ERROR, handleVideoError); + _media = vid; + addChild(vid); + vid.setup(url); + } + + /** + * Configure this sprite to show an image or flash movie. + */ + protected function setupSwfOrImage (url :String) :void + { + startedLoading(); + + // create our loader and set up some event listeners + var loader :Loader = new Loader(); + _media = loader; + var info :LoaderInfo = loader.contentLoaderInfo; + info.addEventListener(Event.COMPLETE, loadingComplete); + info.addEventListener(IOErrorEvent.IO_ERROR, loadError); + info.addEventListener(ProgressEvent.PROGRESS, loadProgress); + + // create a mask to prevent the media from drawing out of bounds + if (getMaxContentWidth() < int.MAX_VALUE && + getMaxContentHeight() < int.MAX_VALUE) { + configureMask(getMaxContentWidth(), getMaxContentHeight()); + } + + // start it loading, add it as a child + loader.load(new URLRequest(url), getContext(url)); + addChild(loader); + + try { + updateContentDimensions(info.width, info.height); + } catch (err :Error) { + // an error is thrown trying to access these props before they're + // ready + } + } + + /** + * Display a 'broken image' to indicate there were troubles with + * loading the media. + */ + protected function setupBrokenImage (w :int = -1, h :int = -1) :void + { + if (w == -1) { + w = 100; + } + if (h == -1) { + h = 100; + } + setMediaObject(ImageUtil.createErrorImage(w, h)); + } + + /** + * Get the application domain being used by this media, or null if + * none or not applicable. + */ + public function getApplicationDomain () :ApplicationDomain + { + return (_media is Loader) + ? (_media as Loader).contentLoaderInfo.applicationDomain + : null; + } + + /** + * Unload the media we're displaying, clean up any resources. + * + * @param completely if true, we're going away and should stop + * everything. Otherwise, we're just loading up new media. + */ + public function shutdown (completely :Boolean = true) :void + { + try { + // remove the mask + if (_media != null && _media.mask != null) { + removeChild(_media.mask); + _media.mask = null; + } + + if (_media is Loader) { + var loader :Loader = (_media as Loader); + // remove any listeners + removeListeners(loader.contentLoaderInfo); + + // dispose of media + try { + loader.close(); + } catch (ioe :IOError) { + // ignore + } + loader.unload(); + + removeChild(loader); + + } else if (_media is VideoDisplayer) { + var vid :VideoDisplayer = (_media as VideoDisplayer); + vid.removeEventListener(VideoDisplayer.SIZE_KNOWN, handleVideoSizeKnown); + vid.removeEventListener(VideoDisplayer.VIDEO_ERROR, handleVideoError); + vid.shutdown(); + removeChild(vid); + + } else if (_media != null) { + removeChild(_media); + } + } catch (ioe :IOError) { + log.warning("Error shutting down media: " + ioe); + log.logStackTrace(ioe); + } + + // clean everything up + _w = 0; + _h = 0; + _media = null; + } + + /** + * Get the width of the content, bounded by the maximum. + */ + public function getContentWidth () :int + { + return Math.min(Math.abs(_w * getMediaScaleX()), getMaxContentWidth()); + } + + /** + * Get the height of the content, bounded by the maximum. + */ + public function getContentHeight () :int + { + return Math.min(Math.abs(_h * getMediaScaleY()), getMaxContentHeight()); + } + + /** + * Get the maximum allowable width for our content. + */ + public function getMaxContentWidth () :int + { + return int.MAX_VALUE; + } + + /** + * Get the maximum allowable height for our content. + */ + public function getMaxContentHeight () :int + { + return int.MAX_VALUE; + } + + /** + * Get the X scaling factor to use on the actual media. + */ + public function getMediaScaleX () :Number + { + return 1; + } + + /** + * Get the Y scaling factor to use on the actual media. + */ + public function getMediaScaleY () :Number + { + return 1; + } + + /** + * Called by MediaWrapper as notification that its size has changed. + */ + public function containerDimensionsUpdated (newWidth :Number, newHeight :Number) :void + { + // do nothing in base MediaContainer + } + + override public function toString () :String + { + return "MediaContainer[url=" + _url + "]"; + } + + /** + * Return the LoaderContext that should be used to load the media + * at the specified url. + */ + protected function getContext (url :String) :LoaderContext + { + if (isImage(url)) { + // load images into our domain so that we can view their pixels + return new LoaderContext(true, + new ApplicationDomain(ApplicationDomain.currentDomain), + SecurityDomain.currentDomain); + + } else { + // share nothing, trust nothing + return new LoaderContext(false, new ApplicationDomain(null), null); + } + } + + /** + * Does the specified url represent an image? + */ + protected function isImage (url :String) :Boolean + { + // look at the last 4 characters in the lowercased url + switch (url.toLowerCase().slice(-4)) { + case ".png": + case ".jpg": + case ".gif": + return true; + + default: + return false; + } + } + + /** + * Remove our listeners from the LoaderInfo object. + */ + protected function removeListeners (info :LoaderInfo) :void + { + info.removeEventListener(Event.COMPLETE, loadingComplete); + info.removeEventListener(IOErrorEvent.IO_ERROR, loadError); + info.removeEventListener(ProgressEvent.PROGRESS, loadProgress); + } + + /** + * A callback to receive IO_ERROR events. + */ + protected function loadError (event :IOErrorEvent) :void + { + stoppedLoading(); + setupBrokenImage(-1, -1); + } + + /** + * A callback to receive PROGRESS events. + */ + protected function loadProgress (event :ProgressEvent) :void + { + updateLoadingProgress(event.bytesLoaded, event.bytesTotal); + var info :LoaderInfo = (event.target as LoaderInfo); + try { + updateContentDimensions(info.width, info.height); + } catch (err :Error) { + // an error is thrown trying to access these props before they're + // ready + } + } + + /** + * Handles video size known. + */ + protected function handleVideoSizeKnown (event :ValueEvent) :void + { + var args :Array = (event.value as Array); + updateContentDimensions(int(args[0]), int(args[1])); + } + + /** + * Handles an error loading a video. + */ + protected function handleVideoError (event :ValueEvent) :void + { + log.warning("Error loading video [cause=" + event.value + "]."); + stoppedLoading(); + setupBrokenImage(-1, -1); + } + + /** + * Callback function to receive COMPLETE events for swfs or images. + */ + protected function loadingComplete (event :Event) :void + { + var info :LoaderInfo = (event.target as LoaderInfo); + removeListeners(info); + +// trace("Loading complete: " + info.url + +// ", childAllowsParent=" + info.childAllowsParent + +// ", parentAllowsChild=" + info.parentAllowsChild + +// ", sameDomain=" + info.sameDomain); + + updateContentDimensions(info.width, info.height); + updateLoadingProgress(1, 1); + stoppedLoading(); + } + + /** + * Configure the mask for this object. + */ + protected function configureMask (ww :int, hh :int) :void + { + var mask :Shape; + if (_media.mask != null) { + mask = (_media.mask as Shape); + + } else { + mask = new Shape(); + // the mask must be added to the display list (which is wacky) + addChild(mask); + _media.mask = mask; + } + + mask.graphics.clear(); + mask.graphics.beginFill(0xFFFFFF); + mask.graphics.drawRect(0, 0, ww, hh); + mask.graphics.endFill(); + } + + /** + * Called during loading as we figure out how big the content we're + * loading is. + */ + protected function updateContentDimensions (ww :int, hh :int) :void + { + // update our saved size, and possibly notify our container + if (_w != ww || _h != hh) { + _w = ww; + _h = hh; + contentDimensionsUpdated(); + // dispatch an event to any listeners + dispatchEvent(new ValueEvent(SIZE_KNOWN, [ ww, hh ])); + } + } + + /** + * Called when we know the true size of the content. + */ + protected function contentDimensionsUpdated () :void + { + // nada, by default + } + + /** + * Update the graphics to indicate how much is loaded. + */ + protected function updateLoadingProgress ( + soFar :Number, total :Number) :void + { + // nada, by default + } + + /** + * Called when we've started loading new media. Will not be called + * for new media that does not require loading. + */ + protected function startedLoading () :void + { + // nada + } + + /** + * Called when we've stopped loading, which may be as a result of + * completion, an error while loading, or early termination. + */ + protected function stoppedLoading () :void + { + // nada + } + + /** The unaltered URL of the content we're displaying. */ + protected var _url :String; + + /** The unscaled width of our content. */ + protected var _w :int; + + /** The unscaled height of our content. */ + protected var _h :int; + + /** Either a Loader or a VideoDisplay. */ + protected var _media :DisplayObject; +} +} diff --git a/src/as/com/threerings/flash/MenuUtil.as b/src/as/com/threerings/flash/MenuUtil.as new file mode 100644 index 00000000..cbe293f3 --- /dev/null +++ b/src/as/com/threerings/flash/MenuUtil.as @@ -0,0 +1,50 @@ +// +// $Id$ +// +// Nenya library - tools for developing networked games +// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/nenya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.flash { + +import flash.ui.ContextMenuItem; +import flash.events.ContextMenuEvent; + +import com.threerings.util.CommandEvent; + +/** + */ +public class MenuUtil +{ + /** + * Create a menu item that will submit a controller command when selected. + */ + public static function createControllerMenuItem ( + caption :String, cmd :String, arg :Object = null, + separatorBefore :Boolean = false, enabled :Boolean = true, + visible :Boolean = true) :ContextMenuItem + { + var item :ContextMenuItem = + new ContextMenuItem(caption, separatorBefore, enabled, visible); + item.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, + function (event :ContextMenuEvent) :void { + CommandEvent.dispatch(event.mouseTarget, cmd, arg); + }); + return item; + } +} +} diff --git a/src/as/com/threerings/flash/VideoDisplayer.as b/src/as/com/threerings/flash/VideoDisplayer.as new file mode 100644 index 00000000..acc86a6b --- /dev/null +++ b/src/as/com/threerings/flash/VideoDisplayer.as @@ -0,0 +1,292 @@ +// +// $Id$ +// +// Nenya library - tools for developing networked games +// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/nenya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.flash { + +import flash.display.Sprite; + +import flash.events.AsyncErrorEvent; +import flash.events.IOErrorEvent; +import flash.events.Event; +import flash.events.MouseEvent; +import flash.events.NetStatusEvent; +import flash.events.SecurityErrorEvent; +import flash.events.TimerEvent; + +import flash.media.Video; + +import flash.net.NetConnection; +import flash.net.NetStream; + +import flash.utils.Timer; + +import com.threerings.util.ValueEvent; + +public class VideoDisplayer extends Sprite +{ + /** A value event dispatched when the size of the video is known. + * Value: [ width, height ]. */ + public static const SIZE_KNOWN :String = "videoDisplayerSizeKnown"; + + /** A value event sent when there's an error loading the video. + * Value: original error event. */ + public static const VIDEO_ERROR :String = "videoDisplayerError"; + + /** + * Create a video displayer. + */ + public function VideoDisplayer () + { + _vid = new Video(); + addChild(_vid); + + addEventListener(MouseEvent.ROLL_OVER, handleRollOver); + addEventListener(MouseEvent.ROLL_OUT, handleRollOut); + + _videoChecker.addEventListener(TimerEvent.TIMER, handleVideoCheck); + + _pauser = new Sprite(); + _pauser.addEventListener(MouseEvent.CLICK, handleClick); + redrawPauser(); + } + + /** + * Start playing a video! + */ + public function setup (url :String) :void + { + _netCon = new NetConnection(); + _netCon.addEventListener(NetStatusEvent.NET_STATUS, handleNetStatus); + + // error handlers + _netCon.addEventListener(AsyncErrorEvent.ASYNC_ERROR, handleAsyncError); + _netCon.addEventListener(IOErrorEvent.IO_ERROR, handleIOError); + _netCon.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError); + + _netCon.connect(null); + _netStream = new NetStream(_netCon); + // pass in refs to some of our protected methods + _netStream.client = { + onMetaData: metaDataReceived + }; + _netStream.addEventListener(NetStatusEvent.NET_STATUS, handleStreamNetStatus); + + _vid.attachNetStream(_netStream); + _videoChecker.start(); + _netStream.play(url); + _paused = false; + } + + /** + * Stop playing our video. + */ + public function shutdown () :void + { + _videoChecker.reset(); + _vid.attachNetStream(null); + + if (_netStream != null) { + _netStream.close(); + _netStream.removeEventListener(NetStatusEvent.NET_STATUS, handleStreamNetStatus); + _netStream = null; + } + if (_netCon != null) { + _netCon.close(); + _netCon.removeEventListener(NetStatusEvent.NET_STATUS, handleNetStatus); + _netCon.removeEventListener(AsyncErrorEvent.ASYNC_ERROR, handleAsyncError); + _netCon.removeEventListener(IOErrorEvent.IO_ERROR, handleIOError); + _netCon.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError); + _netCon = null; + } + } + + /** + * Check to see if we now know the dimensions of the video. + */ + protected function handleVideoCheck (event :TimerEvent) :void + { + if (_vid.videoWidth == 0 || _vid.videoHeight == 0) { + return; // not known yet! + } + + // stop the checker timer + _videoChecker.stop(); + + // set up the width/height + _vid.width = _vid.videoWidth; + _vid.height = _vid.videoHeight; + redrawPauser(); + + // tell any interested parties + dispatchEvent(new ValueEvent(SIZE_KNOWN, [ _vid.videoWidth, _vid.videoHeight ])); + } + + protected function handleRollOver (event :MouseEvent) :void + { + addChild(_pauser); + } + + protected function handleRollOut (event :MouseEvent) :void + { + if (_pauser.parent) { + removeChild(_pauser); + } + } + + protected function handleClick (event :MouseEvent) :void + { + // the buck stops here! + event.stopImmediatePropagation(); + + if (_paused) { + _netStream.resume(); + _paused = false; + + } else { + _netStream.pause(); + _paused = true; + } + redrawPauser(); + } + + /** + * Draw the pauser sprite, update it's location. + * This will become something artistic, etc. + */ + protected function redrawPauser () :void + { + with (_pauser.graphics) { + clear(); + // draw a nice circle + beginFill(0x333333); + drawCircle(0, 0, 20); + endFill(); + lineStyle(2, 0); + drawCircle(0, 0, 20); + + if (_paused) { + lineStyle(0, 0, 0); + beginFill(0x00FF00); + moveTo(-4, -10); + lineTo(4, 0); + lineTo(-4, 10); + lineTo(-4, -10); + endFill(); + + } else { + lineStyle(2, 0x00FF00); + moveTo(-4, -10); + lineTo(-4, 10); + moveTo(4, -10); + lineTo(4, 10); + } + } + + // and update the location + _pauser.x = _vid.width/2; + _pauser.y = _vid.height/2; + } + + protected function handleNetStatus (event :NetStatusEvent) :void + { + var info :Object = event.info; + if ("error" == info.level) { + trace("NetStatus error: " + info.code); + redispatchError(event); + return; + } + // else info.level == "status" + switch (info.code) { + case "NetConnection.Connect.Success": + case "NetConnection.Connect.Closed": + // these status events we ignore + break; + + default: + trace("NetStatus status: " + info.code); + break; + } + } + + protected function handleStreamNetStatus (event :NetStatusEvent) :void + { + if (event.info.code == "NetStream.Play.Stop") { + _netStream.seek(0); + _netStream.pause(); + _paused = true; + redrawPauser(); + } + } + + protected function handleAsyncError (event :AsyncErrorEvent) :void + { + trace("AsyncError: " + event); + redispatchError(event); + } + + protected function handleIOError (event :IOErrorEvent) :void + { + trace("IOError: " + event); + redispatchError(event); + } + + protected function handleSecurityError (event :SecurityErrorEvent) :void + { + trace("SecurityError: " + event); + redispatchError(event); + } + + /** + * Redispatch some error we received to our listeners. + */ + protected function redispatchError (event :Event) :void + { + dispatchEvent(new ValueEvent(VIDEO_ERROR, event)); + } + + /** + * Called when metadata (if any) is found in the video stream. + */ + protected function metaDataReceived (obj :Object) :void + { + trace("Got video metadata:"); + for (var n :String in obj) { + trace(" " + n + ": " + obj[n]); + } + } + + protected var _vid :Video; + + protected var _paused :Boolean = false; + + protected var _netCon :NetConnection; + + protected var _netStream :NetStream; + + protected var _pauser :Sprite; + + /** Checks the video every 100ms to see if the dimensions are now know. + * Yes, this is how to do it. We could trigger on ENTER_FRAME, but then + * we may not know the dimensions unless we're added on the display list, + * and we want this to work in the general case. */ + protected var _videoChecker :Timer = new Timer(100); +} +}