Split Nenya into proper Maven submodules (same treatment Narya got).
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Nenya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://code.google.com/p/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.media.image {
|
||||
|
||||
import com.threerings.util.Log;
|
||||
import com.threerings.util.Map;
|
||||
import com.threerings.util.Maps;
|
||||
import com.threerings.util.RandomUtil;
|
||||
import com.threerings.util.StringUtil;
|
||||
|
||||
public class ClassRecord
|
||||
{
|
||||
private const log :Log = Log.getLog(ClassRecord);
|
||||
|
||||
/** An integer identifier for this class. */
|
||||
public var classId :int;
|
||||
|
||||
/** The name of the color class. */
|
||||
public var name :String;
|
||||
|
||||
/** The source color to use when recoloring colors in this class. */
|
||||
public var source :uint;
|
||||
|
||||
/** Data identifying the range of colors around the source color
|
||||
* that will be recolored when recoloring using this class. */
|
||||
public var range :Array;
|
||||
|
||||
/** The default starting legality value for this color class. See
|
||||
* {@link ColorRecord#starter}. */
|
||||
public var starter :Boolean;
|
||||
|
||||
/** The default colorId to use for recoloration in this class, or
|
||||
* 0 if there is no default defined. */
|
||||
public var defaultId :int;
|
||||
|
||||
/** A table of target colors included in this class. */
|
||||
public var colors :Map = Maps.newMapOf(int);
|
||||
|
||||
/** Used when parsing the color definitions. */
|
||||
public function addColor (record :ColorRecord) :void
|
||||
{
|
||||
// validate the color id
|
||||
if (record.colorId > 127) {
|
||||
log.warning("Refusing to add color record; colorId > 127",
|
||||
"class", this, "record", record);
|
||||
} else if (colors.containsKey(record.colorId)) {
|
||||
log.warning("Refusing to add duplicate colorId",
|
||||
"class", this, "record", record, "existing", colors.get(record.colorId));
|
||||
} else {
|
||||
record.cclass = this;
|
||||
colors.put(record.colorId, record);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a color identified in string form into the id that should be used to look up
|
||||
* its information. Throws an exception if no color could be found that associates with
|
||||
* that name.
|
||||
*/
|
||||
public function getColorId (name :String) :int
|
||||
{
|
||||
// Check if the string is itself a number
|
||||
var id :int = name as int;
|
||||
if (colors.containsKey(id)) {
|
||||
return id;
|
||||
}
|
||||
|
||||
// Look for name matches among all colors
|
||||
for each (var color :ColorRecord in colors.values()) {
|
||||
if (StringUtil.compareIgnoreCase(color.name, name) == 0) {
|
||||
return color.colorId;
|
||||
}
|
||||
}
|
||||
|
||||
// That input wasn't a color
|
||||
throw new Error("No color named '" + name + "'", 0);
|
||||
}
|
||||
|
||||
/** Returns a random starting id from the entries in this class. */
|
||||
public function randomStartingColor () :ColorRecord
|
||||
{
|
||||
// figure out our starter ids if we haven't already
|
||||
if (_starters == null) {
|
||||
_starters = [];
|
||||
for each (var color :ColorRecord in colors) {
|
||||
if (color.starter) {
|
||||
_starters.push(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if (_starters.length < 1) {
|
||||
log.warning("Requested random starting color from colorless component class",
|
||||
"class", this);
|
||||
return null;
|
||||
}
|
||||
|
||||
// return a random entry from the array
|
||||
return RandomUtil.pickRandom(_starters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default ColorRecord defined for this color class, or
|
||||
* null if none.
|
||||
*/
|
||||
public function getDefault () :ColorRecord
|
||||
{
|
||||
return colors.get(defaultId);
|
||||
}
|
||||
|
||||
public function toString () :String
|
||||
{
|
||||
return "[id=" + classId + ", name=" + name + ", source=#" +
|
||||
StringUtil.toHex(source & 0xFFFFFF, 6) +
|
||||
", range=" + StringUtil.toString(range) +
|
||||
", starter=" + starter + ", colors=" +
|
||||
StringUtil.toString(colors.values()) + "]";
|
||||
}
|
||||
|
||||
public static function fromXml (xml :XML) :ClassRecord
|
||||
{
|
||||
var rec :ClassRecord = new ClassRecord();
|
||||
rec.classId = xml.@classId;
|
||||
for each (var colorXml :XML in xml.color) {
|
||||
rec.colors.put(int(colorXml.@colorId), ColorRecord.fromXml(colorXml, rec));
|
||||
}
|
||||
|
||||
rec.name = xml.@name;
|
||||
var srcStr :String = xml.@source;
|
||||
rec.source = parseInt(srcStr.substr(1, srcStr.length - 1), 16);
|
||||
rec.range = toNumArray(xml.@range);
|
||||
rec.defaultId = xml.@defaultId;
|
||||
|
||||
return rec;
|
||||
}
|
||||
|
||||
protected static function toNumArray (str :String) :Array
|
||||
{
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return str.split(",").map(function(element :*, index :int, arr :Array) :Number {
|
||||
return Number(element);
|
||||
});
|
||||
}
|
||||
|
||||
protected var _starters :Array;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Nenya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://code.google.com/p/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.media.image {
|
||||
|
||||
import com.threerings.util.Log;
|
||||
import com.threerings.util.Map;
|
||||
import com.threerings.util.Maps;
|
||||
import com.threerings.util.StringUtil;
|
||||
|
||||
public class ColorPository
|
||||
{
|
||||
private const log :Log = Log.getLog(ColorPository);
|
||||
|
||||
public function getClasses () :Array
|
||||
{
|
||||
return _classes.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the records for the colors in the
|
||||
* specified class.
|
||||
*/
|
||||
public function getColors (className :String) :Array
|
||||
{
|
||||
// make sure the class exists
|
||||
var record :ClassRecord = getClassRecordByName(className);
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// create the array
|
||||
return record.colors.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the ids of the colors in the specified
|
||||
* class.
|
||||
*/
|
||||
public function getColorIds (className :String) :Array
|
||||
{
|
||||
// make sure the class exists
|
||||
var record :ClassRecord = getClassRecordByName(className);
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return record.colors.values().map(
|
||||
function(element :ColorRecord, index :int, arr :Array) :int {
|
||||
return element.colorId;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the specified color is legal for use at character creation time. false is
|
||||
* always returned for non-existent colors or classes.
|
||||
*/
|
||||
public function isLegalStartColor (classId :int, colorId :int) :Boolean
|
||||
{
|
||||
var color :ColorRecord = getColorRecord(classId, colorId);
|
||||
return (color == null) ? false : color.starter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random starting color from the specified color class.
|
||||
*/
|
||||
public function getRandomStartingColor (className :String) :ColorRecord
|
||||
{
|
||||
// make sure the class exists
|
||||
var record :ClassRecord = getClassRecordByName(className);
|
||||
return (record == null) ? null : record.randomStartingColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a colorization by id.
|
||||
*/
|
||||
public function getColorization (classId :int, colorId :int) :Colorization
|
||||
{
|
||||
var color :ColorRecord = getColorRecord(classId, colorId);
|
||||
return (color == null) ? null : color.getColorization();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a colorization by color print.
|
||||
*/
|
||||
public function getColorizationByPrint (colorPrint :int) :Colorization
|
||||
{
|
||||
return getColorization(colorPrint >> 8, colorPrint & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a colorization by class and color names.
|
||||
*/
|
||||
public function getColorizationByName (className :String, colorName :String) :Colorization
|
||||
{
|
||||
var crec :ClassRecord = getClassRecordByName(className);
|
||||
if (crec != null) {
|
||||
var colorId :int = crec.getColorId(colorName);
|
||||
|
||||
var color :ColorRecord = crec.colors.get(colorId);
|
||||
if (color != null) {
|
||||
return color.getColorization();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a colorization by class and color Id.
|
||||
*/
|
||||
public function getColorizationByNameAndId (className :String, colorId :int) :Colorization
|
||||
{
|
||||
var crec :ClassRecord = getClassRecordByName(className);
|
||||
if (crec != null) {
|
||||
var color :ColorRecord = crec.colors.get(colorId);
|
||||
if (color != null) {
|
||||
return color.getColorization();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads up a colorization class by name and logs a warning if it doesn't exist.
|
||||
*/
|
||||
public function getClassRecordByName (className :String) :ClassRecord
|
||||
{
|
||||
for each (var crec :ClassRecord in _classes.values()) {
|
||||
if (crec.name == className) {
|
||||
return crec;
|
||||
}
|
||||
}
|
||||
log.warning("No such color class", "class", className, new Error());
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the requested color class record.
|
||||
*/
|
||||
public function getClassRecord (classId :int) :ClassRecord
|
||||
{
|
||||
return _classes.get(classId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the requested color record.
|
||||
*/
|
||||
public function getColorRecord (classId :int, colorId :int) :ColorRecord
|
||||
{
|
||||
var record :ClassRecord = getClassRecord(classId);
|
||||
if (record == null) {
|
||||
// if they request color class zero, we assume they're just
|
||||
// decoding a blank colorprint, otherwise we complain
|
||||
if (classId != 0) {
|
||||
log.warning("Requested unknown color class",
|
||||
"classId", classId, "colorId", colorId, new Error());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return record.colors.get(colorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the requested color record by class & color names.
|
||||
*/
|
||||
public function getColorRecordByName (className :String, colorName :String) :ColorRecord
|
||||
{
|
||||
var record :ClassRecord = getClassRecordByName(className);
|
||||
if (record == null) {
|
||||
log.warning("Requested unknown color class",
|
||||
"className", className, "colorName", colorName, new Error());
|
||||
return null;
|
||||
}
|
||||
|
||||
var colorId :int = record.getColorId(colorName);
|
||||
|
||||
return record.colors.get(colorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads up a serialized color pository from the supplied resource
|
||||
* manager.
|
||||
*/
|
||||
public static function fromXml (xml :XML) :ColorPository
|
||||
{
|
||||
var pos :ColorPository = new ColorPository();
|
||||
for each (var classXml :XML in xml.elements("class")) {
|
||||
pos._classes.put(int(classXml.@classId), ClassRecord.fromXml(classXml));
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
/** Our mapping from class names to class records. */
|
||||
protected var _classes :Map = Maps.newMapOf(int);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Nenya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://code.google.com/p/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.media.image {
|
||||
|
||||
import com.threerings.util.Log;
|
||||
import com.threerings.util.StringUtil;
|
||||
|
||||
public class ColorRecord
|
||||
{
|
||||
private const log :Log = Log.getLog(ColorRecord);
|
||||
|
||||
/** The colorization class to which we belong. */
|
||||
public var cclass :ClassRecord;
|
||||
|
||||
/** A unique colorization identifier (used in fingerprints). */
|
||||
public var colorId :int;
|
||||
|
||||
/** The name of the target color. */
|
||||
public var name :String;
|
||||
|
||||
/** Data indicating the offset (in HSV color space) from the
|
||||
* source color to recolor to this color. */
|
||||
public var offsets :Array;
|
||||
|
||||
/** Tags this color as a legal starting color or not. This is a
|
||||
* shameful copout, placing application-specific functionality
|
||||
* into a general purpose library class. */
|
||||
public var starter :Boolean;
|
||||
|
||||
/**
|
||||
* Returns a value that is the composite of our class id and color
|
||||
* id which can be used to identify a colorization record. This
|
||||
* value will always be a positive integer that fits into 16 bits.
|
||||
*/
|
||||
public function getColorPrint () :int
|
||||
{
|
||||
return ((cclass.classId << 8) | colorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data in this record configured as a colorization
|
||||
* instance.
|
||||
*/
|
||||
public function getColorization () :Colorization
|
||||
{
|
||||
return new Colorization(getColorPrint(), cclass.source,
|
||||
cclass.range, offsets);
|
||||
}
|
||||
|
||||
public function toString () :String
|
||||
{
|
||||
return "[id=" + colorId + ", name=" + name +
|
||||
", offsets=" + StringUtil.toString(offsets) +
|
||||
", starter=" + starter + "]";
|
||||
}
|
||||
|
||||
|
||||
public static function fromXml (xml :XML, cclass :ClassRecord) :ColorRecord
|
||||
{
|
||||
var rec :ColorRecord = new ColorRecord();
|
||||
rec.colorId = xml.@colorId;
|
||||
rec.name = xml.@name;
|
||||
rec.offsets = toNumArray(xml.@offsets);
|
||||
rec.starter = xml.@starter;
|
||||
rec.cclass = cclass;
|
||||
|
||||
return rec;
|
||||
}
|
||||
|
||||
protected static function toNumArray (str :String) :Array
|
||||
{
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return str.split(",").map(function(element :String, index :int, arr :Array) :Number {
|
||||
return Number(StringUtil.trim(element));
|
||||
});
|
||||
}
|
||||
|
||||
/** Our data represented as a colorization. */
|
||||
protected var _zation :Colorization;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Nenya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://code.google.com/p/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.media.image {
|
||||
|
||||
import com.threerings.display.ColorUtil;
|
||||
import com.threerings.util.Hashable;
|
||||
import com.threerings.util.Short;
|
||||
import com.threerings.util.StringUtil;
|
||||
|
||||
public class Colorization
|
||||
implements Hashable
|
||||
{
|
||||
/** Every colorization must have a unique id that can be used to
|
||||
* compare a particular colorization record with another. */
|
||||
public var colorizationId :int;
|
||||
|
||||
/** The root color for the colorization. */
|
||||
public var rootColor :uint;
|
||||
|
||||
/** The range around the root color that will be colorized (in
|
||||
* delta-hue, delta-saturation, delta-value. Note that this range is inclusive. */
|
||||
public var range :Array;
|
||||
|
||||
/** The adjustments to make to hue, saturation and value. */
|
||||
public var offsets :Array;
|
||||
|
||||
/**
|
||||
* Constructs a colorization record with the specified identifier.
|
||||
*/
|
||||
public function Colorization (colorizationId :int, rootColor :uint,
|
||||
range :Array, offsets :Array)
|
||||
{
|
||||
this.colorizationId = colorizationId;
|
||||
this.rootColor = rootColor;
|
||||
this.range = range;
|
||||
this.offsets = offsets;
|
||||
|
||||
// compute our HSV and fixed HSV
|
||||
_hsv = ColorUtil.RGBtoHSB(ColorUtil.getRed(rootColor), ColorUtil.getGreen(rootColor),
|
||||
ColorUtil.getBlue(rootColor));
|
||||
_fhsv = toFixedHSV(_hsv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root color adjusted by the colorization.
|
||||
*/
|
||||
public function getColorizedRoot () :uint
|
||||
{
|
||||
return recolorColor(_hsv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the supplied color by the offests in this colorization,
|
||||
* taking the appropriate measures for hue (wrapping it around) and
|
||||
* saturation and value (clipping).
|
||||
*
|
||||
* @return the RGB value of the recolored color.
|
||||
*/
|
||||
public function recolorColor (hsv :Array) :uint
|
||||
{
|
||||
// for hue, we wrap around
|
||||
var hue :Number = hsv[0] + offsets[0];
|
||||
if (hue > 1.0) {
|
||||
hue -= 1.0;
|
||||
}
|
||||
|
||||
// otherwise we clip
|
||||
var sat :Number = Math.min(Math.max(hsv[1] + offsets[1], 0), 1);
|
||||
var val :Number = Math.min(Math.max(hsv[2] + offsets[2], 0), 1);
|
||||
|
||||
// convert back to RGB space
|
||||
return ColorUtil.HSBtoRGB(hue, sat, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this colorization matches the supplied color, false
|
||||
* otherwise.
|
||||
*
|
||||
* @param hsv the HSV values for the color in question.
|
||||
* @param fhsv the HSV values converted to fixed point via {@link
|
||||
* #toFixedHSV} for the color in question.
|
||||
*/
|
||||
public function matches (hsv :Array, fhsv :Array) :Boolean
|
||||
{
|
||||
// check to see that this color is sufficiently "close" to the
|
||||
// root color based on the supplied distance parameters
|
||||
if (distance(fhsv[0], _fhsv[0], Short.MAX_VALUE) >
|
||||
range[0] * Short.MAX_VALUE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// saturation and value don't wrap around like hue
|
||||
if (Math.abs(_hsv[1] - hsv[1]) > range[1] ||
|
||||
Math.abs(_hsv[2] - hsv[2]) > range[2]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function hashCode () :int
|
||||
{
|
||||
return colorizationId ^ rootColor;
|
||||
}
|
||||
|
||||
public function equals (other :Object) :Boolean
|
||||
{
|
||||
if (other is Colorization) {
|
||||
return (Colorization(other)).colorizationId == colorizationId;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function toString () :String
|
||||
{
|
||||
return String(colorizationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a long string representation of this colorization.
|
||||
*/
|
||||
public function toVerboseString () :String
|
||||
{
|
||||
return StringUtil.toString(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts floating point HSV values to a fixed point integer
|
||||
* representation.
|
||||
*
|
||||
* @param hsv the HSV values to be converted.
|
||||
* @param fhsv the destination array into which the fixed values will
|
||||
* be stored. If this is null, a new array will be created of the
|
||||
* appropriate length.
|
||||
*
|
||||
* @return the <code>fhsv</code> parameter if it was non-null or the
|
||||
* newly created target array.
|
||||
*/
|
||||
public static function toFixedHSV (hsv :Array) :Array
|
||||
{
|
||||
var fhsv :Array = new Array(hsv.length);
|
||||
|
||||
for (var ii :int = 0; ii < hsv.length; ii++) {
|
||||
fhsv[ii] = int(hsv[ii] * Short.MAX_VALUE);
|
||||
}
|
||||
return fhsv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the distance between the supplied to numbers modulo N.
|
||||
*/
|
||||
public static function distance (a :int, b :int, N :int) :int
|
||||
{
|
||||
return (a > b) ? Math.min(a - b, b + N - a) : Math.min(b - a, a + N - b);
|
||||
}
|
||||
|
||||
/** Fixed HSV values for our root color; used when calculating
|
||||
* recolorizations using this colorization. */
|
||||
protected var _fhsv :Array;
|
||||
|
||||
/** HSV values for our root color; used when calculating
|
||||
* recolorizations using this colorization. */
|
||||
protected var _hsv :Array;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Nenya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://code.google.com/p/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.media.image {
|
||||
|
||||
import flash.utils.ByteArray;
|
||||
|
||||
import com.threerings.display.ColorUtil;
|
||||
import com.threerings.util.Log;
|
||||
import com.threerings.util.StringUtil;
|
||||
|
||||
import com.threerings.util.F;
|
||||
public class PngRecolorUtil
|
||||
{
|
||||
private static const log :Log = Log.getLog(PngRecolorUtil);
|
||||
|
||||
protected static function isPng (bytes :ByteArray) :Boolean
|
||||
{
|
||||
if (bytes.length < PNG_HEADER.length) {
|
||||
return false;
|
||||
}
|
||||
for (var ii :int = 0; ii < PNG_HEADER.length; ii++) {
|
||||
if (PNG_HEADER[ii] != bytes[ii]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the required chunks in the bytes that represent a PNG image. All of the chunks that
|
||||
* are succesfully found are pushed into the returned array in the order in which they were
|
||||
* found in the image.
|
||||
*/
|
||||
protected static function findChunks (pngBytes :ByteArray, chunkTypes :Array) :Array
|
||||
{
|
||||
var chunks :Array = [];
|
||||
if (chunkTypes.length < 1) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
for (var ii :int = PNG_HEADER.length; ii < pngBytes.length;) {
|
||||
pngBytes.position = ii;
|
||||
var chunk :PngChunk =
|
||||
new PngChunk(ii + 8, pngBytes.readInt(), pngBytes.readUTFBytes(4));
|
||||
|
||||
var idx :int = chunkTypes.indexOf(chunk.type);
|
||||
if (idx >= 0) {
|
||||
chunks.push(chunk);
|
||||
if (chunkTypes.length == 1) {
|
||||
return chunks;
|
||||
} else {
|
||||
chunkTypes.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 4 bytes each for length, chunk type and CRC
|
||||
ii += 12 + chunk.length;
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
public static function recolorPNG (pngBytes :ByteArray,
|
||||
colors :Array/* of Colorization */) :ByteArray
|
||||
{
|
||||
if (colors == null || colors.length == 0) {
|
||||
return pngBytes;
|
||||
}
|
||||
|
||||
// first, lets make sure we really have a PNG
|
||||
if (!isPng(pngBytes)) {
|
||||
log.warning("recolorPNG received invalid pngBytes", new Error());
|
||||
return pngBytes;
|
||||
}
|
||||
|
||||
var chunks :Array = findChunks(pngBytes, [HEADER_CHUNK, PALETTE_CHUNK]);
|
||||
if (chunks.length != 2 || (chunks[0] as PngChunk).type != HEADER_CHUNK) {
|
||||
log.warning("recolorPNG received an unexected PNG format", "requiredChunksFound",
|
||||
chunks.length, new Error());
|
||||
return pngBytes;
|
||||
}
|
||||
var header :PngChunk = chunks[0] as PngChunk;
|
||||
var palette :PngChunk = chunks[1] as PngChunk;
|
||||
// Check to make sure the header declares this to be an indexed-color PNG
|
||||
var colorType :int = pngBytes[header.idx + COLOR_TYPE_IDX];
|
||||
if (colorType != INDEXED_COLOR_TYPE) {
|
||||
log.warning(
|
||||
"Color Type in PNG header is not indexed-color", "type", colorType, new Error());
|
||||
return pngBytes;
|
||||
}
|
||||
|
||||
initializeCRCTable();
|
||||
// CRC starts with chunk type
|
||||
var crc :uint = 0xFFFFFFFF;
|
||||
for (var ii :int = 0; ii < 4; ii++) {
|
||||
crc = crcCalc(crc, PALETTE_CHUNK.charCodeAt(ii));
|
||||
}
|
||||
pngBytes.position = palette.idx;
|
||||
for (ii = palette.idx; ii < palette.idx + palette.length; ii += 3) {
|
||||
var red :uint = pngBytes.readUnsignedByte();
|
||||
var green :uint = pngBytes.readUnsignedByte();
|
||||
var blue :uint = pngBytes.readUnsignedByte();
|
||||
|
||||
var hsv :Array = ColorUtil.RGBtoHSB(red, green, blue);
|
||||
var fhsv :Array = Colorization.toFixedHSV(hsv);
|
||||
for each (var color :Colorization in colors) {
|
||||
if (color != null && color.matches(hsv, fhsv)) {
|
||||
var newRgb :uint = color.recolorColor(hsv);
|
||||
pngBytes[ii] = red = ColorUtil.getRed(newRgb);
|
||||
pngBytes[ii + 1] = green = ColorUtil.getGreen(newRgb);
|
||||
pngBytes[ii + 2] = blue = ColorUtil.getBlue(newRgb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
crc = F.foldl([red, green, blue], crc, crcCalc);
|
||||
}
|
||||
crc = uint(crc ^ uint(0xFFFFFFFF));
|
||||
// write out the CRC for the modified color table
|
||||
pngBytes.position = palette.idx + palette.length;
|
||||
pngBytes.writeUnsignedInt(crc);
|
||||
|
||||
return pngBytes;
|
||||
}
|
||||
|
||||
protected static function initializeCRCTable () :void
|
||||
{
|
||||
if (_crcTable != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_crcTable = [];
|
||||
for (var n :uint = 0; n < 256; n++)
|
||||
{
|
||||
var c :uint = n;
|
||||
for (var k :uint = 0; k < 8; k++)
|
||||
{
|
||||
if (c & 1) {
|
||||
c = uint(uint(0xedb88320) ^ uint(c >>> 1));
|
||||
} else {
|
||||
c = uint(c >>> 1);
|
||||
}
|
||||
}
|
||||
_crcTable[n] = c;
|
||||
}
|
||||
}
|
||||
|
||||
protected static function crcCalc (crc :uint, byte :uint) :uint
|
||||
{
|
||||
return uint(_crcTable[(crc ^ byte) & uint(0xFF)] ^ uint(crc >>> 8));
|
||||
}
|
||||
|
||||
/** The amount to pad error messages by. */
|
||||
private static const ERROR_PADDING :int = 5;
|
||||
|
||||
/** Various Png-recolor related constants. */
|
||||
protected static const PNG_HEADER :Array =
|
||||
[ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ];
|
||||
protected static const PALETTE_CHUNK :String = "PLTE";
|
||||
protected static const HEADER_CHUNK :String = "IHDR";
|
||||
protected static const COLOR_TYPE_IDX :int = 9;
|
||||
protected static const INDEXED_COLOR_TYPE :int = 3;
|
||||
|
||||
protected static var _crcTable :Array;
|
||||
}
|
||||
}
|
||||
|
||||
import com.threerings.util.StringUtil;
|
||||
|
||||
/** Used for recoloring pngs. */
|
||||
class PngChunk
|
||||
{
|
||||
/** The index of the beginning of the data section of the chunk. */
|
||||
public var idx :int;
|
||||
|
||||
/** The length of the data section of the chunk. */
|
||||
public var length :int;
|
||||
|
||||
/** The type of this chunk. */
|
||||
public var type :String;
|
||||
|
||||
public function PngChunk (idx :int, length :int, type :String)
|
||||
{
|
||||
this.idx = idx;
|
||||
this.length = length;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public function toString () :String
|
||||
{
|
||||
return StringUtil.simpleToString(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user