From 8cb5952203475fae4f289b101797d7ae94fd5c2e Mon Sep 17 00:00:00 2001 From: Fluffy <65877598+FluffyGhoster@users.noreply.github.com> Date: Sat, 9 Mar 2024 21:00:21 +0100 Subject: [PATCH] sdfaas (#18609) --- aurorastation.dme | 1 + code/__DEFINES/gradient.dm | 4 + code/__HELPERS/icons.dm | 655 +++++++++--------- code/game/objects/items/weapons/cards_ids.dm | 4 +- .../fluffyghost-updateiconhelpersprocs.yml | 41 ++ icons/blanks/32x32.dmi | Bin 0 -> 216 bytes icons/blanks/96x96.dmi | Bin 0 -> 222 bytes 7 files changed, 386 insertions(+), 319 deletions(-) create mode 100644 code/__DEFINES/gradient.dm create mode 100644 html/changelogs/fluffyghost-updateiconhelpersprocs.yml create mode 100644 icons/blanks/32x32.dmi create mode 100644 icons/blanks/96x96.dmi diff --git a/aurorastation.dme b/aurorastation.dme index 1e9a4f6f566..12496752f15 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -63,6 +63,7 @@ #include "code\__DEFINES\ghostspawner.dm" #include "code\__DEFINES\global.dm" #include "code\__DEFINES\grab.dm" +#include "code\__DEFINES\gradient.dm" #include "code\__DEFINES\guns.dm" #include "code\__DEFINES\hallucinations.dm" #include "code\__DEFINES\hud.dm" diff --git a/code/__DEFINES/gradient.dm b/code/__DEFINES/gradient.dm new file mode 100644 index 00000000000..573a339880b --- /dev/null +++ b/code/__DEFINES/gradient.dm @@ -0,0 +1,4 @@ +// spacemandmm doesn't really implement gradient() right, so let's just handle that here yeah? +#define rgb_gradient(index, args...) UNLINT(gradient(args, index)) +#define hsl_gradient(index, args...) UNLINT(gradient(args, space = COLORSPACE_HSL, index)) +#define hsv_gradient(index, args...) UNLINT(gradient(args, space = COLORSPACE_HSV, index)) diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index a9d62802882..425d11be3ec 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -15,7 +15,7 @@ CHANGING ICONS Several new procs have been added to the /icon datum to simplify working with icons. To use them, remember you first need to setup an /icon var like so: -var/icon/my_icon = new('iconfile.dmi') +GLOBAL_DATUM_INIT(my_icon, /icon, new('iconfile.dmi')) icon/ChangeOpacity(amount = 1) A very common operation in DM is to try to make an icon more or less transparent. Making an icon more @@ -60,55 +60,41 @@ RGB isn't the only way to represent color. Sometimes it's more useful to work wi * The value of a color determines how bright it is. A high-value color is vivid, moderate value is dark, and no value at all is black. -Just as BYOND uses "#rrggbb" to represent RGB values, a similar format is used for HSV: "#hhhssvv". The hue is three -hex digits because it ranges from 0 to 0x5FF. +While rgb is typically stored in the #rrggbb" format (with optional "aa" on the end), HSV never needs to be displayed. +Most procs that work in HSV "space" will simply accept RGB inputs and convert them in place using rgb2num(color, space = COLORSPACE_HSV). - * 0 to 0xFF - red to yellow - * 0x100 to 0x1FF - yellow to green - * 0x200 to 0x2FF - green to cyan - * 0x300 to 0x3FF - cyan to blue - * 0x400 to 0x4FF - blue to magenta - * 0x500 to 0x5FF - magenta to red +That said, if you want to manually modify these values rgb2hsv() will hand you back a list in the format list(hue, saturation, value, alpha). +Converting back is simple, just a hsv2rgb(hsv) call -Knowing this, you can figure out that red is "#000ffff" in HSV format, which is hue 0 (red), saturation 255 (as colorful as possible), -value 255 (as bright as possible). Green is "#200ffff" and blue is "#400ffff". +Hue ranges from 0 to 360 (it's in degrees of a color wheel) +Saturation ranges from 0 to 100 +Value ranges from 0 to 100 -More than one HSV color can match the same RGB color. +Knowing this, you can figure out that red is list(0, 100, 100) in HSV format, which is hue 0 (red), saturation 100 (as colorful as possible), +value 255 (as bright as possible). Green is list(120, 100, 100) and blue is list(240, 100, 100). + +It is worth noting that while we do not have helpers for them currently, these same ideas apply to all of byond's color spaces +HSV (hue saturation value), HSL (hue satriation luminosity) and HCY (hue chroma luminosity) Here are some procs you can use for color management: -ReadRGB(rgb) - Takes an RGB string like "#ffaa55" and converts it to a list such as list(255,170,85). If an RGBA format is used - that includes alpha, the list will have a fourth item for the alpha value. -hsv(hue, sat, val, apha) - Counterpart to rgb(), this takes the values you input and converts them to a string in "#hhhssvv" or "#hhhssvvaa" - format. Alpha is not included in the result if null. -ReadHSV(rgb) - Takes an HSV string like "#100FF80" and converts it to a list such as list(256,255,128). If an HSVA format is used that - includes alpha, the list will have a fourth item for the alpha value. -RGBtoHSV(rgb) - Takes an RGB or RGBA string like "#ffaa55" and converts it into an HSV or HSVA color such as "#080aaff". -HSVtoRGB(hsv) - Takes an HSV or HSVA string like "#080aaff" and converts it into an RGB or RGBA color such as "#ff55aa". BlendRGB(rgb1, rgb2, amount) Blends between two RGB or RGBA colors using regular RGB blending. If amount is 0, the first color is the result; if 1, the second color is the result. 0.5 produces an average of the two. Values outside the 0 to 1 range are allowed as well. - The returned value is an RGB or RGBA color. -BlendHSV(hsv1, hsv2, amount) - Blends between two HSV or HSVA colors using HSV blending, which tends to produce nicer results than regular RGB + Returns an RGB or RGBA string +BlendHSV(rgb1, rgb2, amount) + Blends between two RGB or RGBA colors using HSV blending, which tends to produce nicer results than regular RGB blending because the brightness of the color is left intact. If amount is 0, the first color is the result; if 1, the second color is the result. 0.5 produces an average of the two. Values outside the 0 to 1 range are allowed as well. - The returned value is an HSV or HSVA color. -BlendRGBasHSV(rgb1, rgb2, amount) - Like BlendHSV(), but the colors used and the return value are RGB or RGBA colors. The blending is done in HSV form. + Returns an RGB or RGBA string HueToAngle(hue) Converts a hue to an angle range of 0 to 360. Angle 0 is red, 120 is green, and 240 is blue. AngleToHue(hue) Converts an angle to a hue in the valid range. -RotateHue(hsv, angle) - Takes an HSV or HSVA value and rotates the hue forward through red, green, and blue by an angle from 0 to 360. - (Rotating red by 60� produces yellow.) The result is another HSV or HSVA color with the same saturation and value - as the original, but a different hue. +RotateHue(rgb, angle) + Takes an RGB or RGBA value and rotates the hue forward through red, green, and blue by an angle from 0 to 360. + (Rotating red by 60° produces yellow.) + Returns an RGB or RGBA string GrayScale(rgb) Takes an RGB or RGBA color and converts it to grayscale. Returns an RGB or RGBA string. ColorTone(rgb, tone) @@ -132,27 +118,27 @@ mob underlays += image(icon='old_or_unused.dmi',icon_state="red", pixel_x = -32) // Testing image overlays - overlays += image(icon='old_or_unused.dmi',icon_state="green", pixel_x = 32, pixel_y = -32) - overlays += image(icon='old_or_unused.dmi',icon_state="green", pixel_x = 32, pixel_y = 32) - overlays += image(icon='old_or_unused.dmi',icon_state="green", pixel_x = -32, pixel_y = -32) + add_overlay(image(icon='old_or_unused.dmi',icon_state="green", pixel_x = 32, pixel_y = -32)) + add_overlay(image(icon='old_or_unused.dmi',icon_state="green", pixel_x = 32, pixel_y = 32)) + add_overlay(image(icon='old_or_unused.dmi',icon_state="green", pixel_x = -32, pixel_y = -32)) // Testing icon file overlays (defaults to mob's state) - overlays += '_flat_demoIcons2.dmi' + add_overlay('_flat_demoIcons2.dmi') // Testing icon_state overlays (defaults to mob's icon) - overlays += "white" + add_overlay("white") // Testing dynamic icon overlays var/icon/I = icon('old_or_unused.dmi', icon_state="aqua") I.Shift(NORTH,16,1) - overlays+=I + add_overlay(I) // Testing dynamic image overlays I=image(icon=I,pixel_x = -32, pixel_y = 32) - overlays+=I + add_overlay(I) // Testing object types (and layers) - overlays+=/obj/effect/overlayTest + add_overlay(/obj/effect/overlay_test) loc = locate (10,10,1) verb @@ -167,7 +153,7 @@ mob Output_Icon() set name = "2. Output Icon" - src<<"Icon is: [icon2html(getFlatIcon(src), usr)]" + to_chat(src, "Icon is: [icon2base64html(getFlatIcon(src))]") Label_Icon() set name = "3. Label Icon" @@ -178,11 +164,11 @@ mob // Send the icon to src's local cache src< transparent, gray -> translucent white, white -> solid white */ /icon/proc/BecomeAlphaMask() - SwapColor(null, "#000000ff") // don't let transparent become gray + SwapColor(null, "#000000ff") // don't let transparent become gray MapColors(0,0,0,0.3, 0,0,0,0.59, 0,0,0,0.11, 0,0,0,0, 1,1,1,0) /icon/proc/UseAlphaMask(mask) @@ -291,10 +273,10 @@ world AddAlphaMask(mask) /icon/proc/AddAlphaMask(mask) - var/icon/M = new(mask) - M.Blend("#ffffff", ICON_SUBTRACT) + var/icon/mask_icon = new(mask) + mask_icon.Blend("#ffffff", ICON_SUBTRACT) // apply mask - Blend(M, ICON_ADD) + Blend(mask_icon, ICON_ADD) /* HSV format is represented as "#hhhssvv" or "#hhhssvvaa" @@ -499,49 +481,7 @@ world amount<0 or amount>1 are allowed */ /proc/BlendHSV(hsv1, hsv2, amount) - var/list/HSV1 = ReadHSV(hsv1) - var/list/HSV2 = ReadHSV(hsv2) - - // add missing alpha if needed - if(HSV1.len < HSV2.len) HSV1 += 255 - else if(HSV2.len < HSV1.len) HSV2 += 255 - var/usealpha = HSV1.len > 3 - - // normalize hsv values in case anything is screwy - if(HSV1[1] > 1536) HSV1[1] %= 1536 - if(HSV2[1] > 1536) HSV2[1] %= 1536 - if(HSV1[1] < 0) HSV1[1] += 1536 - if(HSV2[1] < 0) HSV2[1] += 1536 - if(!HSV1[3]) {HSV1[1] = 0; HSV1[2] = 0} - if(!HSV2[3]) {HSV2[1] = 0; HSV2[2] = 0} - - // no value for one color means don't change saturation - if(!HSV1[3]) HSV1[2] = HSV2[2] - if(!HSV2[3]) HSV2[2] = HSV1[2] - // no saturation for one color means don't change hues - if(!HSV1[2]) HSV1[1] = HSV2[1] - if(!HSV2[2]) HSV2[1] = HSV1[1] - - // Compress hues into easier-to-manage range - HSV1[1] -= HSV1[1] >> 8 - HSV2[1] -= HSV2[1] >> 8 - - var/hue_diff = HSV2[1] - HSV1[1] - if(hue_diff > 765) hue_diff -= 1530 - else if(hue_diff <= -765) hue_diff += 1530 - - var/hue = round(HSV1[1] + hue_diff * amount, 1) - var/sat = round(HSV1[2] + (HSV2[2] - HSV1[2]) * amount, 1) - var/val = round(HSV1[3] + (HSV2[3] - HSV1[3]) * amount, 1) - var/alpha = usealpha ? round(HSV1[4] + (HSV2[4] - HSV1[4]) * amount, 1) : null - - // normalize hue - if(hue < 0 || hue >= 1530) hue %= 1530 - if(hue < 0) hue += 1530 - // decompress hue - hue += round(hue / 255) - - return hsv(hue, sat, val, alpha) + return hsv_gradient(amount, 0, hsv1, 1, hsv2, "loop") /* Smooth blend between RGB colors @@ -553,41 +493,22 @@ world amount<0 or amount>1 are allowed */ /proc/BlendRGB(rgb1, rgb2, amount) - var/cachekey = "[rgb1]_[rgb2]_[amount]" - . = SSicon_cache.rgb_blend_cache[cachekey] - if (.) - return - - var/list/RGB1 = ReadRGB(rgb1) - var/list/RGB2 = ReadRGB(rgb2) - - // add missing alpha if needed - if(RGB1.len < RGB2.len) RGB1 += 255 - else if(RGB2.len < RGB1.len) RGB2 += 255 - var/usealpha = RGB1.len > 3 - - var/r = round(RGB1[1] + (RGB2[1] - RGB1[1]) * amount, 1) - var/g = round(RGB1[2] + (RGB2[2] - RGB1[2]) * amount, 1) - var/b = round(RGB1[3] + (RGB2[3] - RGB1[3]) * amount, 1) - var/alpha = usealpha ? round(RGB1[4] + (RGB2[4] - RGB1[4]) * amount, 1) : null - - . = isnull(alpha) ? rgb(r, g, b) : rgb(r, g, b, alpha) - SSicon_cache.rgb_blend_cache[cachekey] = . - -/proc/BlendRGBasHSV(rgb1, rgb2, amount) - return HSVtoRGB(RGBtoHSV(rgb1), RGBtoHSV(rgb2), amount) + return rgb_gradient(amount, 0, rgb1, 1, rgb2, "loop") /proc/HueToAngle(hue) // normalize hsv in case anything is screwy - if(hue < 0 || hue >= 1536) hue %= 1536 - if(hue < 0) hue += 1536 + if(hue < 0 || hue >= 1536) + hue %= 1536 + if(hue < 0) + hue += 1536 // Compress hue into easier-to-manage range hue -= hue >> 8 return hue / (1530/360) /proc/AngleToHue(angle) // normalize hsv in case anything is screwy - if(angle < 0 || angle >= 360) angle -= 360 * round(angle / 360) + if(angle < 0 || angle >= 360) + angle -= 360 * round(angle / 360) var/hue = angle * (1530/360) // Decompress hue hue += round(hue / 255) @@ -640,235 +561,239 @@ world /// appearance system (overlays/underlays, etc.) is not available. /// /// Only the first argument is required. -/proc/getFlatIcon(image/A, defdir, deficon, defstate, defblend, start = TRUE, no_anim = FALSE, ignore_parent_dir = FALSE) - //Define... defines. - var/static/icon/flat_template = icon('icons/effects/effects.dmi', "nothing") +/proc/getFlatIcon(image/appearance, defdir, deficon, defstate, defblend, start = TRUE, no_anim = FALSE) + // Loop through the underlays, then overlays, sorting them into the layers list + #define PROCESS_OVERLAYS_OR_UNDERLAYS(flat, process, base_layer) \ + for (var/i in 1 to process.len) { \ + var/image/current = process[i]; \ + if (!current) { \ + continue; \ + } \ + if (current.plane != FLOAT_PLANE && current.plane != appearance.plane) { \ + continue; \ + } \ + var/current_layer = current.layer; \ + if (current_layer < 0) { \ + if (current_layer <= -1000) { \ + return flat; \ + } \ + current_layer = base_layer + appearance.layer + current_layer / 1000; \ + } \ + for (var/index_to_compare_to in 1 to layers.len) { \ + var/compare_to = layers[index_to_compare_to]; \ + if (current_layer < layers[compare_to]) { \ + layers.Insert(index_to_compare_to, current); \ + break; \ + } \ + } \ + layers[current] = current_layer; \ + } - #define BLANK icon(flat_template) - #define SET_SELF(SETVAR) do { \ - var/icon/SELF_ICON=icon(icon(curicon, curstate, base_icon_dir),"",SOUTH,no_anim?1:null); \ - if(A.alpha<255) { \ - SELF_ICON.Blend(rgb(255,255,255,A.alpha),ICON_MULTIPLY);\ - } \ - if(A.color) { \ - if(islist(A.color)){ \ - SELF_ICON.MapColors(arglist(A.color))} \ - else{ \ - SELF_ICON.Blend(A.color,ICON_MULTIPLY)} \ - } \ - ##SETVAR=SELF_ICON;\ - } while (0) - #define INDEX_X_LOW 1 - #define INDEX_X_HIGH 2 - #define INDEX_Y_LOW 3 - #define INDEX_Y_HIGH 4 + var/static/icon/flat_template = icon('icons/blanks/32x32.dmi', "nothing") - #define flatX1 flat_size[INDEX_X_LOW] - #define flatX2 flat_size[INDEX_X_HIGH] - #define flatY1 flat_size[INDEX_Y_LOW] - #define flatY2 flat_size[INDEX_Y_HIGH] - #define addX1 add_size[INDEX_X_LOW] - #define addX2 add_size[INDEX_X_HIGH] - #define addY1 add_size[INDEX_Y_LOW] - #define addY2 add_size[INDEX_Y_HIGH] + if(!appearance || appearance.alpha <= 0) + return icon(flat_template) - if(!A || A.alpha <= 0) - return BLANK - - var/noIcon = FALSE if(start) if(!defdir) - defdir = A.dir + defdir = appearance.dir if(!deficon) - deficon = A.icon + deficon = appearance.icon if(!defstate) - defstate = A.icon_state + defstate = appearance.icon_state if(!defblend) - defblend = A.blend_mode + defblend = appearance.blend_mode - var/curicon = A.icon || deficon - var/curstate = A.icon_state || defstate + var/curicon = appearance.icon || deficon + var/curstate = appearance.icon_state || defstate + var/curdir = (!appearance.dir || appearance.dir == SOUTH) ? defdir : appearance.dir - if(!((noIcon = (!curicon)))) + var/render_icon = curicon + + if (render_icon) var/curstates = icon_states(curicon) if(!(curstate in curstates)) - if("" in curstates) + if ("" in curstates) curstate = "" else - noIcon = TRUE // Do not render this object. + render_icon = FALSE - var/curdir - var/base_icon_dir //We'll use this to get the icon state to display if not null BUT NOT pass it to overlays as the dir we have - - //These should use the parent's direction (most likely) - if(!A.dir || A.dir == SOUTH || ignore_parent_dir) - curdir = defdir - else - curdir = A.dir + var/base_icon_dir //We'll use this to get the icon state to display if not null BUT NOT pass it to overlays as the dir we have //Try to remove/optimize this section ASAP, CPU hog. //Determines if there's directionals. - if(!noIcon && curdir != SOUTH) - var/exist = FALSE - var/static/list/checkdirs = list(NORTH, EAST, WEST) - for(var/i in checkdirs) //Not using GLOB for a reason. - if(length(icon_states(icon(curicon, curstate, i)))) - exist = TRUE - break - if(!exist) + if(render_icon && curdir != SOUTH) + if ( + !length(icon_states(icon(curicon, curstate, NORTH))) \ + && !length(icon_states(icon(curicon, curstate, EAST))) \ + && !length(icon_states(icon(curicon, curstate, WEST))) \ + ) base_icon_dir = SOUTH - // if(!base_icon_dir) base_icon_dir = curdir - ASSERT(!BLEND_DEFAULT) //I might just be stupid but lets make sure this define is 0. + var/curblend = appearance.blend_mode || defblend - var/curblend = A.blend_mode || defblend - - if(A.overlays.len || A.underlays.len) - var/icon/flat = BLANK + if(appearance.overlays.len || appearance.underlays.len) + var/icon/flat = icon(flat_template) // Layers will be a sorted list of icons/overlays, based on the order in which they are displayed var/list/layers = list() var/image/copy // Add the atom's icon itself, without pixel_x/y offsets. - if(!noIcon) - copy = image(icon=curicon, icon_state=curstate, layer=A.layer, dir=base_icon_dir) - copy.color = A.color - copy.alpha = A.alpha + if(render_icon) + copy = image(icon=curicon, icon_state=curstate, layer=appearance.layer, dir=base_icon_dir) + copy.color = appearance.color + copy.alpha = appearance.alpha copy.blend_mode = curblend - layers[copy] = A.layer + layers[copy] = appearance.layer - // Loop through the underlays, then overlays, sorting them into the layers list - for(var/process_set in 0 to 1) - var/list/process = process_set? A.overlays : A.underlays - for(var/i in 1 to process.len) - var/image/current = process[i] - if(!current) - continue - if(current.plane != FLOAT_PLANE && current.plane != A.plane) - continue - var/current_layer = current.layer - if(current_layer < 0) - if(current_layer <= -1000) - return flat - current_layer = process_set + A.layer + current_layer / 1000 - - for(var/p in 1 to layers.len) - var/image/cmp = layers[p] - if(current_layer < layers[cmp]) - layers.Insert(p, current) - break - layers[current] = current_layer - - //sortTim(layers, GLOBAL_PROC_REF(cmp_image_layer_asc)) + PROCESS_OVERLAYS_OR_UNDERLAYS(flat, appearance.underlays, 0) + PROCESS_OVERLAYS_OR_UNDERLAYS(flat, appearance.overlays, 1) var/icon/add // Icon of overlay being added - // Current dimensions of flattened icon - var/list/flat_size = list(1, flat.Width(), 1, flat.Height()) - // Dimensions of overlay being added - var/list/add_size[4] - var/blend_color = A.color ? TRUE : FALSE + var/flatX1 = 1 + var/flatX2 = flat.Width() + var/flatY1 = 1 + var/flatY2 = flat.Height() - for(var/V in layers) - var/image/I = V - if(I.alpha == 0) + var/addX1 = 0 + var/addX2 = 0 + var/addY1 = 0 + var/addY2 = 0 + + for(var/image/layer_image as anything in layers) + if(layer_image.alpha == 0) continue - if(blend_color && I.appearance_flags == RESET_COLOR) - blend_color = FALSE - - if(I == copy) // 'I' is an /image based on the object being flattened. + if(layer_image == copy) // 'layer_image' is an /image based on the object being flattened. curblend = BLEND_OVERLAY - add = icon(I.icon, I.icon_state, base_icon_dir) + add = icon(layer_image.icon, layer_image.icon_state, base_icon_dir) else // 'I' is an appearance object. - add = getFlatIcon(image(I), curdir, curicon, curstate, curblend, FALSE, no_anim) + add = getFlatIcon(image(layer_image), curdir, curicon, curstate, curblend, FALSE, no_anim) if(!add) continue - // Find the new dimensions of the flat icon to fit the added overlay - add_size = list( - min(flatX1, I.pixel_x+1), - max(flatX2, I.pixel_x+add.Width()), - min(flatY1, I.pixel_y+1), - max(flatY2, I.pixel_y+add.Height()) - ) - if(flat_size ~! add_size) + // Find the new dimensions of the flat icon to fit the added overlay + addX1 = min(flatX1, layer_image.pixel_x + 1) + addX2 = max(flatX2, layer_image.pixel_x + add.Width()) + addY1 = min(flatY1, layer_image.pixel_y + 1) + addY2 = max(flatY2, layer_image.pixel_y + add.Height()) + + if ( + addX1 != flatX1 \ + && addX2 != flatX2 \ + && addY1 != flatY1 \ + && addY2 != flatY2 \ + ) // Resize the flattened icon so the new icon fits flat.Crop( - addX1 - flatX1 + 1, - addY1 - flatY1 + 1, - addX2 - flatX1 + 1, - addY2 - flatY1 + 1 + addX1 - flatX1 + 1, + addY1 - flatY1 + 1, + addX2 - flatX1 + 1, + addY2 - flatY1 + 1 ) - flat_size = add_size.Copy() + + flatX1 = addX1 + flatX2 = addY1 + flatY1 = addX2 + flatY2 = addY2 // Blend the overlay into the flattened icon - flat.Blend(add, blendMode2iconMode(curblend), I.pixel_x + 2 - flatX1, I.pixel_y + 2 - flatY1) + flat.Blend(add, blendMode2iconMode(curblend), layer_image.pixel_x + 2 - flatX1, layer_image.pixel_y + 2 - flatY1) - if(A.color && blend_color) - if(islist(A.color)) - flat.MapColors(arglist(A.color)) + if(appearance.color) + if(islist(appearance.color)) + flat.MapColors(arglist(appearance.color)) else - flat.Blend(A.color, ICON_MULTIPLY) + flat.Blend(appearance.color, ICON_MULTIPLY) - if(A.alpha < 255) - flat.Blend(rgb(255, 255, 255, A.alpha), ICON_MULTIPLY) + if(appearance.alpha < 255) + flat.Blend(rgb(255, 255, 255, appearance.alpha), ICON_MULTIPLY) if(no_anim) //Clean up repeated frames var/icon/cleaned = new /icon() cleaned.Insert(flat, "", SOUTH, 1, 0) - . = cleaned + return cleaned else - . = icon(flat, "", SOUTH) - else //There's no overlays. - if(!noIcon) - SET_SELF(.) + return icon(flat, "", SOUTH) + else if (render_icon) // There's no overlays. + var/icon/final_icon = icon(icon(curicon, curstate, base_icon_dir), "", SOUTH, no_anim ? TRUE : null) - //Clear defines - #undef flatX1 - #undef flatX2 - #undef flatY1 - #undef flatY2 - #undef addX1 - #undef addX2 - #undef addY1 - #undef addY2 + if (appearance.alpha < 255) + final_icon.Blend(rgb(255,255,255, appearance.alpha), ICON_MULTIPLY) - #undef INDEX_X_LOW - #undef INDEX_X_HIGH - #undef INDEX_Y_LOW - #undef INDEX_Y_HIGH + if (appearance.color) + if (islist(appearance.color)) + final_icon.MapColors(arglist(appearance.color)) + else + final_icon.Blend(appearance.color, ICON_MULTIPLY) - #undef BLANK - #undef SET_SELF + return final_icon -/proc/getIconMask(atom/A)//By yours truly. Creates a dynamic mask for a mob/whatever. /N - var/icon/alpha_mask = new(A.icon,A.icon_state)//So we want the default icon and icon state of A. - for(var/I in A.overlays)//For every image in overlays. var/image/I will not work, don't try it. - if(I:layer>A.layer) continue//If layer is greater than what we need, skip it. - var/icon/image_overlay = new(I:icon,I:icon_state)//Blend only works with icon objects. + #undef PROCESS_OVERLAYS_OR_UNDERLAYS + +/proc/getIconMask(atom/atom_to_mask)//By yours truly. Creates a dynamic mask for a mob/whatever. /N + var/icon/alpha_mask = new(atom_to_mask.icon, atom_to_mask.icon_state)//So we want the default icon and icon state of atom_to_mask. + for(var/iterated_image in atom_to_mask.overlays)//For every image in overlays. var/image/image will not work, don't try it. + var/image/image = iterated_image + if(image.layer > atom_to_mask.layer) + continue//If layer is greater than what we need, skip it. + var/icon/image_overlay = new(image.icon, image.icon_state)//Blend only works with icon objects. //Also, icons cannot directly set icon_state. Slower than changing variables but whatever. - alpha_mask.Blend(image_overlay,ICON_OR)//OR so they are lumped together in a nice overlay. + alpha_mask.Blend(image_overlay, ICON_OR)//OR so they are lumped together in a nice overlay. return alpha_mask//And now return the mask. +/** + * Helper proc to generate a cutout alpha mask out of an icon. + * + * Why is it a helper if it's so simple? + * + * Because BYOND's documentation is hot garbage and I don't trust anyone to actually + * figure this out on their own without sinking countless hours into it. Yes, it's that + * simple, now enjoy. + * + * But why not use filters? + * + * Filters do not allow for masks that are not the exact same on every dir. An example of a + * need for that can be found in [/proc/generate_left_leg_mask()]. + * + * Arguments: + * * icon_to_mask - The icon file you want to generate an alpha mask out of. + * * icon_state_to_mask - The specific icon_state you want to generate an alpha mask out of. + * + * Returns an `/icon` that is the alpha mask of the provided icon and icon_state. + */ +/proc/generate_icon_alpha_mask(icon_to_mask, icon_state_to_mask) + var/icon/mask_icon = icon(icon_to_mask, icon_state_to_mask) + // I hate the MapColors documentation, so I'll explain what happens here. + // Basically, what we do here is that we invert the mask by using none of the original + // colors, and then the fourth group of number arguments is actually the alpha values of + // each of the original colors, which we multiply by 255 and subtract a value of 255 to the + // result for the matching pixels, while starting with a base color of white everywhere. + mask_icon.MapColors(0,0,0,0, 0,0,0,0, 0,0,0,0, 255,255,255,-255, 1,1,1,1) + return mask_icon + /mob/proc/AddCamoOverlay(atom/A)//A is the atom which we are using as the overlay. var/icon/opacity_icon = new(A.icon, A.icon_state)//Don't really care for overlays/underlays. //Now we need to culculate overlays+underlays and add them together to form an image for a mask. - //var/icon/alpha_mask = getFlatIcon(src)//Accurate but SLOW. Not designed for running each tick. Could have other uses I guess. - var/icon/alpha_mask = getIconMask(src)//Which is why I created that proc. Also a little slow since it's blending a bunch of icons together but good enough. + var/icon/alpha_mask = getIconMask(src)//getFlatIcon(src) is accurate but SLOW. Not designed for running each tick. This is also a little slow since it's blending a bunch of icons together but good enough. opacity_icon.AddAlphaMask(alpha_mask)//Likely the main source of lag for this proc. Probably not designed to run each tick. opacity_icon.ChangeOpacity(0.4)//Front end for MapColors so it's fast. 0.5 means half opacity and looks the best in my opinion. - for(var/i=0,i<5,i++)//And now we add it as overlays. It's faster than creating an icon and then merging it. - var/image/I = image("icon" = opacity_icon, "icon_state" = A.icon_state, "layer" = layer+0.8)//So it's above other stuff but below weapons and the like. + for(var/i in 1 to 5)//And now we add it as overlays. It's faster than creating an icon and then merging it. + var/image/camo_image = image("icon" = opacity_icon, "icon_state" = A.icon_state, "layer" = layer+0.8)//So it's above other stuff but below weapons and the like. switch(i)//Now to determine offset so the result is somewhat blurred. - if(1) I.pixel_x-- - if(2) I.pixel_x++ - if(3) I.pixel_y-- - if(4) I.pixel_y++ - overlays += I//And finally add the overlay. + if(2) + camo_image.pixel_x-- + if(3) + camo_image.pixel_x++ + if(4) + camo_image.pixel_y-- + if(5) + camo_image.pixel_y++ + add_overlay(camo_image)//And finally add the overlay. /proc/build_disappear_icon(atom/A) var/icon/disappear_icon = new(getFlatIcon(A)) @@ -999,6 +924,23 @@ lighting determines lighting capturing (optional), suppress_errors suppreses err return rgb(red,green,0) +/// Gets a dummy savefile for usage in icon generation. +/// Savefiles generated from this proc will be empty. +/proc/get_dummy_savefile(from_failure = FALSE) + var/static/next_id = 0 + if(next_id++ > 9) + next_id = 0 + var/savefile_path = "tmp/dummy-save-[next_id].sav" + try + if(fexists(savefile_path)) + fdel(savefile_path) + return new /savefile(savefile_path) + catch(var/exception/error) + // if we failed to create a dummy once, try again; maybe someone slept somewhere they shouldnt have + if(from_failure) // this *is* the retry, something fucked up + CRASH("get_dummy_savefile failed to create a dummy savefile: '[error]'") + return get_dummy_savefile(from_failure = TRUE) + /** * Converts an icon to base64. Operates by putting the icon in the iconCache savefile, * exporting it as text, and then parsing the base64 from that. @@ -1007,14 +949,11 @@ lighting determines lighting capturing (optional), suppress_errors suppreses err /proc/icon2base64(icon/icon) if (!isicon(icon)) return FALSE - var/savefile/dummySave = new("tmp/dummySave.sav") - to_file(dummySave["dummy"], icon) + var/savefile/dummySave = get_dummy_savefile() + WRITE_FILE(dummySave["dummy"], icon) var/iconData = dummySave.ExportText("dummy") var/list/partial = splittext(iconData, "{") - . = replacetext(copytext_char(partial[2], 3, -5), "\n", "") //if cleanup fails we want to still return the correct base64 - dummySave.Unlock() - dummySave = null - fdel("tmp/dummySave.sav") //if you get the idea to try and make this more optimized, make sure to still call unlock on the savefile after every write to unlock it. + return replacetext(copytext_char(partial[2], 3, -5), "\n", "") //if cleanup fails we want to still return the correct base64 /// generates a filename for a given asset. /// like generate_asset_name(), except returns the rsc reference and the rsc file hash as well as the asset name (sans extension) @@ -1226,16 +1165,13 @@ lighting determines lighting capturing (optional), suppress_errors suppreses err return icon2html(I, target, sourceonly = sourceonly) /** - * Centers an image. - * + * Center's an image. * Requires: - * * The Image - * * The x dimension of the icon file used in the image - * * The y dimension of the icon file used in the image - * - * Examples: - * * center_image(image_to_center, 32,32) - * * center_image(image_to_center, 96,96) + * The Image + * The x dimension of the icon file used in the image + * The y dimension of the icon file used in the image + * eg: center_image(image_to_center, 32,32) + * eg2: center_image(image_to_center, 96,96) **/ /proc/center_image(image/image_to_center, x_dimension = 0, y_dimension = 0) if(!image_to_center) @@ -1247,7 +1183,7 @@ lighting determines lighting capturing (optional), suppress_errors suppreses err if((x_dimension == world.icon_size) && (y_dimension == world.icon_size)) return image_to_center - //Offset the image so that its bottom left corner is shifted this many pixels + //Offset the image so that it's bottom left corner is shifted this many pixels //This makes it infinitely easier to draw larger inhands/images larger than world.iconsize //but still use them in game var/x_offset = -((x_dimension / world.icon_size) - 1) * (world.icon_size * 0.5) @@ -1263,3 +1199,88 @@ lighting determines lighting capturing (optional), suppress_errors suppreses err image_to_center.pixel_y = y_offset return image_to_center + +///Flickers an overlay on an atom +/atom/proc/flick_overlay_static(overlay_image, duration) + set waitfor = FALSE + if(!overlay_image) + return + add_overlay(overlay_image) + sleep(duration) + cut_overlay(overlay_image) + +/// Perform a shake on an atom, resets its position afterwards +/atom/proc/Shake(pixelshiftx = 2, pixelshifty = 2, duration = 2.5 SECONDS, shake_interval = 0.02 SECONDS) + var/initialpixelx = pixel_x + var/initialpixely = pixel_y + animate(src, pixel_x = initialpixelx + rand(-pixelshiftx,pixelshiftx), pixel_y = initialpixelx + rand(-pixelshifty,pixelshifty), time = shake_interval, flags = ANIMATION_PARALLEL) + for (var/i in 3 to ((duration / shake_interval))) // Start at 3 because we already applied one, and need another to reset + animate(pixel_x = initialpixelx + rand(-pixelshiftx,pixelshiftx), pixel_y = initialpixely + rand(-pixelshifty,pixelshifty), time = shake_interval) + animate(pixel_x = initialpixelx, pixel_y = initialpixely, time = shake_interval) + +///Checks if the given iconstate exists in the given file, caching the result. Setting scream to TRUE will print a stack trace ONCE. +/proc/icon_exists(file, state, scream) + var/static/list/icon_states_cache = list() + if(icon_states_cache[file]?[state]) + return TRUE + + if(icon_states_cache[file]?[state] == FALSE) + return FALSE + + var/list/states = icon_states(file) + + if(!icon_states_cache[file]) + icon_states_cache[file] = list() + + if(state in states) + icon_states_cache[file][state] = TRUE + return TRUE + else + icon_states_cache[file][state] = FALSE + if(scream) + stack_trace("Icon Lookup for state: [state] in file [file] failed.") + return FALSE + +/** + * Returns the size of the sprite in tiles. + * Takes the icon size and divides it by the world icon size (default 32). + * This gives the size of the sprite in tiles. + * + * @return size of the sprite in tiles + */ +/proc/get_size_in_tiles(obj/target) + var/icon/size_check = icon(target.icon, target.icon_state) + var/size = size_check.Width() / world.icon_size + + return size + +/** + * Updates the bounds of a rotated object + * This ensures that the bounds are always correct, + * even if the object is rotated after init. + */ +/obj/proc/set_bounds() + var/size = get_size_in_tiles(src) + + if(dir in list(NORTH, SOUTH)) + bound_width = size * world.icon_size + bound_height = world.icon_size + else + bound_width = world.icon_size + bound_height = size * world.icon_size + +/// Cache of the width and height of icon files, to avoid repeating the same expensive operation +GLOBAL_LIST_EMPTY(icon_dimensions) + +/// Returns a list containing the width and height of an icon file +/proc/get_icon_dimensions(icon_path) + // Icons can be a real file(), a rsc backed file(), a dynamic rsc (dyn.rsc) reference (known as a cache reference in byond docs), or an /icon which is pointing to one of those. + // Runtime generated dynamic icons are an unbounded concept cache identity wise, the same icon can exist millions of ways and holding them in a list as a key can lead to unbounded memory usage if called often by consumers. + // Check distinctly that this is something that has this unspecified concept, and thus that we should not cache. + if (!isfile(icon_path) || !length("[icon_path]")) + var/icon/my_icon = icon(icon_path) + return list("width" = my_icon.Width(), "height" = my_icon.Height()) + if (isnull(GLOB.icon_dimensions[icon_path])) + var/icon/my_icon = icon(icon_path) + GLOB.icon_dimensions[icon_path] = list("width" = my_icon.Width(), "height" = my_icon.Height()) + return GLOB.icon_dimensions[icon_path] diff --git a/code/game/objects/items/weapons/cards_ids.dm b/code/game/objects/items/weapons/cards_ids.dm index 62d948af694..876fe151e9d 100644 --- a/code/game/objects/items/weapons/cards_ids.dm +++ b/code/game/objects/items/weapons/cards_ids.dm @@ -157,9 +157,9 @@ var/const/NO_EMAG_ACT = -50 chat_user.username = chat_user.generateUsernameIdCard(src) /obj/item/card/id/proc/set_id_photo(var/mob/M) - front = getFlatIcon(M, SOUTH, ignore_parent_dir = TRUE) + front = getFlatIcon(M, SOUTH) front.Scale(128, 128) - side = getFlatIcon(M, WEST, ignore_parent_dir = TRUE) + side = getFlatIcon(M, WEST) side.Scale(128, 128) /mob/proc/set_id_info(var/obj/item/card/id/id_card) diff --git a/html/changelogs/fluffyghost-updateiconhelpersprocs.yml b/html/changelogs/fluffyghost-updateiconhelpersprocs.yml new file mode 100644 index 00000000000..4db73d478fe --- /dev/null +++ b/html/changelogs/fluffyghost-updateiconhelpersprocs.yml @@ -0,0 +1,41 @@ +################################ +# Example Changelog File +# +# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb. +# +# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.) +# When it is, any changes listed below will disappear. +# +# Valid Prefixes: +# bugfix +# wip (For works in progress) +# tweak +# soundadd +# sounddel +# rscadd (general adding of nice things) +# rscdel (general deleting of nice things) +# imageadd +# imagedel +# maptweak +# spellcheck (typo fixes) +# experiment +# balance +# admin +# backend +# security +# refactor +################################# + +# Your name. +author: FluffyGhost + +# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again. +delete-after: True + +# Any changes you've made. See valid prefix list above. +# INDENT WITH TWO SPACES. NOT TABS. SPACES. +# SCREW THIS UP AND IT WON'T WORK. +# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries. +# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog. +changes: + - backend: "Updated and added various icon helper procs, from TG; some operations should now also be faster as they are offloaded to the engine." diff --git a/icons/blanks/32x32.dmi b/icons/blanks/32x32.dmi new file mode 100644 index 0000000000000000000000000000000000000000..6c4f2b33e0fee9d3f99ff3febc0760cfd078aed2 GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnL3?x0byx0z;m;-!5Tn`*LkmkKF1;}MA3GxeO zaCmkj4amu^3W+FjNi9w;$}A|!%+F(BsF)KRR!~&>{Y!Ac$FEPcymhtCojD)8A=Kca z@q&JF>8>?&JF>8>?r978O6lM^IZ7bl4HG(F^GV0pm6sL^WE Q0hDF%boFyt=akR{025G63;+NC literal 0 HcmV?d00001