mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2025-04-25 17:59:34 +00:00
Implement basic mask layer interface
This commit is contained in:
parent
1b6730c321
commit
51a8cb4ce4
7 changed files with 179 additions and 10 deletions
BondageClub
Assets/Female3DCG
Screens/Character/Appearance
Scripts
|
@ -806,6 +806,10 @@ type AssetDefinition = (
|
|||
| AssetDefinition.Script
|
||||
);
|
||||
|
||||
interface AssetLayerMaskTexureDefinition {
|
||||
/** The groups that will be affected */
|
||||
Groups: AssetGroupName[];
|
||||
}
|
||||
|
||||
interface AssetLayerDefinition extends AssetCommonPropertiesGroupAssetLayer, AssetCommonPropertiesAssetLayer {
|
||||
/** The layer's name */
|
||||
|
@ -849,6 +853,15 @@ interface AssetLayerDefinition extends AssetCommonPropertiesGroupAssetLayer, Ass
|
|||
*/
|
||||
BlendingMode?: GlobalCompositeOperation;
|
||||
|
||||
|
||||
/**
|
||||
* Mark the layer as a mask layer, which will be used to apply alpha masks to other layers.
|
||||
* Only the alpha channel of the image, and positioning are concerned for a mask layer.
|
||||
* The color, priority and alpha are ignored for mask layers.
|
||||
* The blending mode of the mask layer must be set to "destination-in" or "destination-out".
|
||||
*/
|
||||
TextureMask?: AssetLayerMaskTexureDefinition;
|
||||
|
||||
/**
|
||||
* Specify that this is (one of) the asset's lock layer.
|
||||
*
|
||||
|
|
|
@ -442,7 +442,7 @@ function CharacterAppearanceSortLayers(C) {
|
|||
// Check if we need to draw a different variation (from type property)
|
||||
const typeRecord = item.Property && item.Property.TypeRecord;
|
||||
const layersToDraw = asset.Layer
|
||||
.filter(layer => CharacterAppearanceIsLayerVisible(C, layer, asset, typeRecord))
|
||||
.filter(layer => CharacterAppearanceIsLayerVisible(C, layer, asset, typeRecord) && !layer.TextureMask)
|
||||
.map(layer => {
|
||||
/** @type {Mutable<AssetLayer>} */
|
||||
const drawLayer = { ...layer };
|
||||
|
@ -484,6 +484,30 @@ function CharacterAppearanceSortLayers(C) {
|
|||
return AssetLayerSort(layers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a map of all mask layers in a character's appearance, grouped by the asset groups they affect.
|
||||
* Only includes mask layers from visible assets that aren't blocked and are allowed in the current chat room.
|
||||
*
|
||||
* @param {Character} C - The character whose masks should be built
|
||||
* @returns {AssetLayer[]} A map of group names to arrays of mask layers that affect them
|
||||
*/
|
||||
function CharacterAppearanceBuildMasks(C) {
|
||||
/** @type {AssetLayer[]} */
|
||||
const masks = C.DrawAppearance.reduce((acc, item) => {
|
||||
const asset = item.Asset;
|
||||
// The filter logic here is much the same as in `CharacterAppearanceSortLayers`, but we're only interested in mask layers
|
||||
if (asset.Visible && CharacterAppearanceVisible(C, asset.Name, asset.Group.Name) && InventoryChatRoomAllow(asset.Category)) {
|
||||
const typeRecord = item.Property && item.Property.TypeRecord;
|
||||
asset.Layer.filter(layer => CharacterAppearanceIsLayerVisible(C, layer, asset, typeRecord) && layer.TextureMask).reduce((acc_, layer) => {
|
||||
acc_.push(layer);
|
||||
return acc_;
|
||||
}, acc);
|
||||
}
|
||||
return acc;
|
||||
}, /** @type {AssetLayer[]} */([]));
|
||||
return masks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether an item or a whole item group is visible or not
|
||||
* @param {Character} C - The character whose assets are checked
|
||||
|
@ -614,8 +638,8 @@ function CharacterAppearanceBuildCanvas(C) {
|
|||
drawImageBlink: (src, x, y, opts) => DrawImageCanvas(src, C.CanvasBlink.getContext("2d"), x, y, opts),
|
||||
drawImageColorize: (src, x, y, opts) => DrawImageCanvas(src, C.Canvas.getContext("2d"), x, y, opts),
|
||||
drawImageColorizeBlink: (src, x, y, opts) => DrawImageCanvas(src, C.CanvasBlink.getContext("2d"), x, y, opts),
|
||||
drawCanvas: (Img, x, y, alphaMasks) => DrawCanvas(Img, C.Canvas.getContext("2d"), x, y, alphaMasks),
|
||||
drawCanvasBlink: (Img, x, y, alphaMasks) => DrawCanvas(Img, C.CanvasBlink.getContext("2d"), x, y, alphaMasks),
|
||||
drawCanvas: (Img, x, y, alphaMasks, maskLayers) => DrawCanvas(Img, C.Canvas.getContext("2d"), x, y, alphaMasks, maskLayers),
|
||||
drawCanvasBlink: (Img, x, y, alphaMasks, maskLayers) => DrawCanvas(Img, C.CanvasBlink.getContext("2d"), x, y, alphaMasks, maskLayers),
|
||||
});
|
||||
} else {
|
||||
GLDrawAppearanceBuild(C);
|
||||
|
|
|
@ -639,7 +639,7 @@ function AssetMapLayer(Layer, A, I) {
|
|||
/** @type {AssetLayer} */
|
||||
const L = {
|
||||
Name: Layer.Name || null,
|
||||
AllowColorize: typeof Layer.AllowColorize === "boolean" ? Layer.AllowColorize : A.AllowColorize,
|
||||
AllowColorize: !Layer.TextureMask && (typeof Layer.AllowColorize === "boolean" ? Layer.AllowColorize : A.AllowColorize),
|
||||
CopyLayerColor: typeof Layer.CopyLayerColor === "string" ? Layer.CopyLayerColor : null,
|
||||
ColorGroup: typeof Layer.ColorGroup === "string" ? Layer.ColorGroup : null,
|
||||
HideColoring: typeof Layer.HideColoring === "boolean" ? Layer.HideColoring : false,
|
||||
|
@ -660,6 +660,7 @@ function AssetMapLayer(Layer, A, I) {
|
|||
MinOpacity: minOpacity,
|
||||
MaxOpacity: maxOpacity,
|
||||
BlendingMode: Layer.BlendingMode || "source-over",
|
||||
TextureMask: Layer.TextureMask,
|
||||
LockLayer: typeof Layer.LockLayer === "boolean" ? Layer.LockLayer : false,
|
||||
MirrorExpression: Layer.MirrorExpression,
|
||||
ColorIndex: 0,
|
||||
|
|
|
@ -1371,6 +1371,9 @@ function CharacterLoadCanvas(C) {
|
|||
// Generates a layer array from the character's appearance array, sorted by drawing order
|
||||
C.AppearanceLayers = CharacterAppearanceSortLayers(C);
|
||||
|
||||
// Build the masks for the character
|
||||
C.AppearanceMasks = CharacterAppearanceBuildMasks(C);
|
||||
|
||||
// Run AfterLoadCanvas hooks
|
||||
C.RunHooks("AfterLoadCanvas");
|
||||
|
||||
|
|
|
@ -20,6 +20,112 @@ function CommonDrawCanvasPrepare(C) {
|
|||
C.MustDraw = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef { Map<AssetGroupName, TextureAlphaMask[]> } MaskLayersMap
|
||||
* @param {Character} C - The character to prepare
|
||||
* @returns { { maskLayers: MaskLayersMap, maskLayersBlink: MaskLayersMap} } - The grouped mask layers for the character's appearance
|
||||
*/
|
||||
function CommonDrawAppearancePrepareMaskLayers(C) {
|
||||
/** @type {{ maskLayers: MaskLayersMap, maskLayersBlink: MaskLayersMap}} */
|
||||
const maskLayers = (C.AppearanceMasks ?? []).reduce((acc, layer) => {
|
||||
// Same as url logic in CommonDrawAppearanceBuild, but masks should use its own properties
|
||||
// Also masks don't have colorization, so we don't need to check for it
|
||||
|
||||
const asset = layer.Asset;
|
||||
const group = asset.Group;
|
||||
let item = C.Appearance.find(i => i.Asset === asset);
|
||||
let groupName = asset.DynamicGroupName;
|
||||
|
||||
// If there's a pose style we must add (items take priority over groups, layers may override completely)
|
||||
let pose = CommonDrawResolveAssetPose(C, layer);
|
||||
|
||||
// If the layer belongs to a specific parent group, grab the group's current asset name to use it as a suffix
|
||||
let parentAssetName = "";
|
||||
const parentGroupName = layer.ParentGroup[pose] ?? layer.ParentGroup[PoseType.DEFAULT];
|
||||
if (parentGroupName) {
|
||||
const parentItem = C.Appearance.find(Item => Item.Asset.Group.Name === parentGroupName);
|
||||
if (parentItem) parentAssetName = parentItem.Asset.Name;
|
||||
}
|
||||
|
||||
// Check if we need to draw a different expression (for facial features)
|
||||
const currentExpression = CommonDrawResolveLayerExpression(C, item, layer);
|
||||
const expressionSegment = currentExpression ? currentExpression + "/" : "";
|
||||
const blinkExpressionSegment = (asset.OverrideBlinking ? !group.DrawingBlink : group.DrawingBlink) ? "Closed/" : expressionSegment;
|
||||
|
||||
// Find the X and Y position to draw on
|
||||
let { X, Y } = CommonDrawComputeDrawingCoordinates(C, asset, layer, groupName);
|
||||
|
||||
// Check if we need to draw a different variation (from type property)
|
||||
const typeRecord = (item.Property && item.Property.TypeRecord) || {};
|
||||
let layerSegment = "";
|
||||
let layerType = "";
|
||||
if (layer.CreateLayerTypes.length > 0) {
|
||||
layerType = layer.CreateLayerTypes.map(k => `${k}${typeRecord[k] || 0}`).join("");
|
||||
}
|
||||
if (layer.Name) layerSegment = layer.Name;
|
||||
|
||||
// If blending mode is not set correctly, skip the layer
|
||||
/** @type {"destination-in" | "destination-out"} */
|
||||
let blendingMode = "destination-in";
|
||||
if(layer.BlendingMode === "destination-in" || layer.BlendingMode === "destination-out") {
|
||||
blendingMode = layer.BlendingMode;
|
||||
}
|
||||
else if(!!layer.BlendingMode) return acc;
|
||||
|
||||
// Safeguard against a null pose
|
||||
if (typeof pose !== "string") pose = /** @type {AssetPoseName} */("");
|
||||
|
||||
// Check the current pose against the assets' supported pose mapping
|
||||
/** @type {string} */
|
||||
let poseSegment = layer.PoseMapping[pose];
|
||||
switch (poseSegment) {
|
||||
case PoseType.HIDE:
|
||||
case PoseType.DEFAULT:
|
||||
case undefined:
|
||||
poseSegment = "";
|
||||
break;
|
||||
default:
|
||||
poseSegment += "/";
|
||||
break;
|
||||
}
|
||||
|
||||
const baseURL = `Assets/${group.Family}/${groupName}/${poseSegment}${expressionSegment}`;
|
||||
const baseURLBlink = `Assets/${group.Family}/${groupName}/${poseSegment}${blinkExpressionSegment}`;
|
||||
|
||||
const urlParts = [asset.Name, parentAssetName, layerType, layerSegment].filter(c => c);
|
||||
const layerURL = urlParts.join("_") + ".png";
|
||||
|
||||
/** @type {TextureAlphaMask} */
|
||||
const maskLayer = {
|
||||
Url: baseURL + layerURL,
|
||||
X, Y,
|
||||
Mode: blendingMode,
|
||||
};
|
||||
|
||||
/** @type {TextureAlphaMask} */
|
||||
const maskLayerBlink = {
|
||||
Url: baseURLBlink + layerURL,
|
||||
X, Y,
|
||||
Mode: blendingMode,
|
||||
};
|
||||
|
||||
for(const maskedGroup of layer.TextureMask.Groups) {
|
||||
// if(!acc.has(groupName)) acc.set(groupName, [maskLayer]);
|
||||
// else acc.get(groupName).push(maskLayer);
|
||||
if(!acc.maskLayers.has(maskedGroup)) acc.maskLayers.set(maskedGroup, [maskLayer]);
|
||||
else acc.maskLayers.get(maskedGroup).push(maskLayer);
|
||||
if(!acc.maskLayersBlink.has(maskedGroup)) acc.maskLayersBlink.set(maskedGroup, [maskLayerBlink]);
|
||||
else acc.maskLayersBlink.get(maskedGroup).push(maskLayerBlink);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, /** @type {{ maskLayers: MaskLayersMap, maskLayersBlink: MaskLayersMap}} */( {
|
||||
maskLayers: new Map(),
|
||||
maskLayersBlink: new Map(),
|
||||
}));
|
||||
return maskLayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the given character's appearance using the provided drawing callbacks
|
||||
* @param {Character} C - The character whose appearance to draw
|
||||
|
@ -35,6 +141,9 @@ function CommonDrawAppearanceBuild(C, {
|
|||
drawImageColorize,
|
||||
drawImageColorizeBlink,
|
||||
}) {
|
||||
// Prepare masks for the character's appearance
|
||||
const {maskLayers: maskTexLayers, maskLayersBlink: maskTexLayersBlink} = CommonDrawAppearancePrepareMaskLayers(C);
|
||||
|
||||
// Loop through all layers in the character appearance
|
||||
for (const layer of C.AppearanceLayers) {
|
||||
const asset = layer.Asset;
|
||||
|
@ -229,27 +338,33 @@ function CommonDrawAppearanceBuild(C, {
|
|||
|
||||
const urlParts = [asset.Name, parentAssetName, layerType, colorSegment, layerSegment].filter(c => c);
|
||||
const layerURL = urlParts.join("_") + ".png";
|
||||
|
||||
|
||||
// prepare mask layers for the current group
|
||||
const maskLayer = maskTexLayers.get(groupName) ?? [];
|
||||
const maskLayerBlink = maskTexLayersBlink.get(groupName) ?? [];
|
||||
|
||||
if (shouldColorize) {
|
||||
drawImageColorize(
|
||||
baseURL + layerURL,
|
||||
X, Y,
|
||||
{ HexColor: layerColor, FullAlpha: asset.FullAlpha, AlphaMasks: masks, Alpha: opacity, Invert: inverted, Mirror: mirrored, BlendingMode: blendingMode }
|
||||
{ HexColor: layerColor, FullAlpha: asset.FullAlpha, AlphaMasks: masks, Alpha: opacity, Invert: inverted, Mirror: mirrored, BlendingMode: blendingMode, TextureAlphaMask: maskLayer }
|
||||
);
|
||||
drawImageColorizeBlink(
|
||||
baseURLBlink + layerURL,
|
||||
X, Y,
|
||||
{ HexColor: layerColor, FullAlpha: asset.FullAlpha, AlphaMasks: masks, Alpha: opacity, Invert: inverted, Mirror: mirrored, BlendingMode: blendingMode }
|
||||
{ HexColor: layerColor, FullAlpha: asset.FullAlpha, AlphaMasks: masks, Alpha: opacity, Invert: inverted, Mirror: mirrored, BlendingMode: blendingMode, TextureAlphaMask: maskLayerBlink }
|
||||
);
|
||||
} else {
|
||||
drawImage(
|
||||
baseURL + layerURL,
|
||||
X, Y,
|
||||
{ AlphaMasks: masks, Alpha: opacity, Invert: inverted, Mirror: mirrored, BlendingMode: blendingMode }
|
||||
{ AlphaMasks: masks, Alpha: opacity, Invert: inverted, Mirror: mirrored, BlendingMode: blendingMode, TextureAlphaMask: maskLayer }
|
||||
);
|
||||
drawImageBlink(
|
||||
baseURLBlink + layerURL,
|
||||
X, Y,
|
||||
{ AlphaMasks: masks, Alpha: opacity, Invert: inverted, Mirror: mirrored, BlendingMode: blendingMode }
|
||||
{ AlphaMasks: masks, Alpha: opacity, Invert: inverted, Mirror: mirrored, BlendingMode: blendingMode, TextureAlphaMask: maskLayerBlink }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -558,10 +558,11 @@ function DrawImageCanvas(Source, Canvas, X, Y, Options) {
|
|||
* @param {number} X - Position of the image on the X axis
|
||||
* @param {number} Y - Position of the image on the Y axis
|
||||
* @param {readonly RectTuple[]} AlphaMasks - A list of alpha masks to apply to the asset
|
||||
* @param {readonly MaskTexture[]} TexureMasks - A list of mask layers to apply to the asset
|
||||
* @returns {boolean} - whether the image was complete or not
|
||||
*/
|
||||
function DrawCanvas(Img, Canvas, X, Y, AlphaMasks) {
|
||||
return DrawImageEx(Img, Canvas, X, Y, { AlphaMasks });
|
||||
function DrawCanvas(Img, Canvas, X, Y, AlphaMasks, TexureMasks) {
|
||||
return DrawImageEx(Img, Canvas, X, Y, { AlphaMasks, TexureMasks: TexureMasks });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
12
BondageClub/Scripts/Typedef.d.ts
vendored
12
BondageClub/Scripts/Typedef.d.ts
vendored
|
@ -1070,6 +1070,7 @@ interface AssetLayer {
|
|||
readonly MinOpacity: number;
|
||||
readonly MaxOpacity: number;
|
||||
readonly BlendingMode: GlobalCompositeOperation;
|
||||
readonly TextureMask?: AssetLayerMaskTexureDefinition;
|
||||
readonly LockLayer: boolean;
|
||||
readonly MirrorExpression?: AssetGroupName;
|
||||
/** @deprecated */
|
||||
|
@ -1807,6 +1808,7 @@ interface Character {
|
|||
HasAttribute: (attribute: AssetAttribute) => boolean;
|
||||
DrawAppearance?: Item[];
|
||||
AppearanceLayers?: Mutable<AssetLayer>[];
|
||||
AppearanceMasks?: AssetLayer[];
|
||||
Hooks: Map<CharacterHook, Map<string, () => void>> | null;
|
||||
RegisterHook: (hookName: CharacterHook, hookInstance: string, callback: () => void) => boolean;
|
||||
UnregisterHook: (hookName: CharacterHook, hookInstance: string) => boolean;
|
||||
|
@ -3758,6 +3760,13 @@ interface AudioChatAction {
|
|||
|
||||
// #region Character drawing
|
||||
|
||||
interface TextureAlphaMask {
|
||||
X: number;
|
||||
Y: number;
|
||||
Url: string;
|
||||
Mode: "destination-in" | "destination-out";
|
||||
}
|
||||
|
||||
/** Options available to most draw calls */
|
||||
type DrawOptions = {
|
||||
/** Transparency between 0-1 */
|
||||
|
@ -3782,6 +3791,8 @@ type DrawOptions = {
|
|||
readonly AlphaMasks?: readonly RectTuple[];
|
||||
/** Blending mode for drawing the image */
|
||||
BlendingMode?: GlobalCompositeOperation;
|
||||
/** A list of mask layers to apply to the call */
|
||||
readonly TextureAlphaMask?: readonly TextureAlphaMask[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3804,6 +3815,7 @@ type DrawCanvasCallback = (
|
|||
x: number,
|
||||
y: number,
|
||||
alphaMasks?: RectTuple[],
|
||||
maskLayers?: TextureAlphaMask[],
|
||||
) => void;
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Reference in a new issue