Implement basic mask layer interface

This commit is contained in:
dynilath 2025-04-04 09:12:25 +08:00
parent 1b6730c321
commit 51a8cb4ce4
No known key found for this signature in database
7 changed files with 179 additions and 10 deletions
BondageClub

View file

@ -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.
*

View file

@ -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);

View file

@ -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,

View file

@ -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");

View file

@ -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 }
);
}
}

View file

@ -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 });
}
/**

View file

@ -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;
/**