From 14e1f6abc49ff43005dcf9fefddf7b44fef30194 Mon Sep 17 00:00:00 2001 From: Runa-Dacino Date: Fri, 15 Mar 2024 17:53:55 +0100 Subject: [PATCH] add(colours): Ports matrix/colormate stuff from CHOMP Original PR: https://github.com/CHOMPStation2/CHOMPStation2/pull/6159 Original Author: https://github.com/BlackMajor --- code/__defines/color_priority.dm | 11 + code/__defines/lum.dm | 4 + code/_helpers/icons.dm | 4 +- code/_helpers/icons/flatten.dm | 268 +++++++ code/_helpers/icons_ch.dm | 665 ++++++++++++++++++ code/_helpers/type2type.dm | 129 ++++ code/datums/browser.dm | 85 +++ code/datums/interfaces/appearance.dm | 143 ++++ code/game/atoms.dm | 55 ++ code/game/machinery/painter_vr.dm | 325 +++++++-- code/matrices/color_matrix.dm | 240 +++++++ .../preference_setup/loadout/gear_tweaks.dm | 43 +- code/modules/mob/mob_helpers.dm | 6 +- icons/system/blank_32x32.dmi | Bin 0 -> 209 bytes tgui/packages/tgui/interfaces/ColorMate.jsx | 413 +++++++++++ tgui/packages/tgui/interfaces/ColorMate.tsx | 67 -- vorestation.dme | 6 + 17 files changed, 2321 insertions(+), 143 deletions(-) create mode 100644 code/__defines/color_priority.dm create mode 100644 code/__defines/lum.dm create mode 100644 code/_helpers/icons/flatten.dm create mode 100644 code/_helpers/icons_ch.dm create mode 100644 code/datums/interfaces/appearance.dm create mode 100644 code/matrices/color_matrix.dm create mode 100644 icons/system/blank_32x32.dmi create mode 100644 tgui/packages/tgui/interfaces/ColorMate.jsx delete mode 100644 tgui/packages/tgui/interfaces/ColorMate.tsx diff --git a/code/__defines/color_priority.dm b/code/__defines/color_priority.dm new file mode 100644 index 0000000000..e88784987f --- /dev/null +++ b/code/__defines/color_priority.dm @@ -0,0 +1,11 @@ +//different types of atom colorations +///only used by rare effects like greentext coloring mobs and when admins varedit color +#define ADMIN_COLOUR_PRIORITY 1 +///e.g. purple effect of the revenant on a mob, black effect when mob electrocuted +#define TEMPORARY_COLOUR_PRIORITY 2 +///color splashed onto an atom (e.g. paint on turf) +#define WASHABLE_COLOUR_PRIORITY 3 +///color inherent to the atom (e.g. blob color) +#define FIXED_COLOUR_PRIORITY 4 +///how many priority levels there are. +#define COLOUR_PRIORITY_AMOUNT 4 diff --git a/code/__defines/lum.dm b/code/__defines/lum.dm new file mode 100644 index 0000000000..942462b573 --- /dev/null +++ b/code/__defines/lum.dm @@ -0,0 +1,4 @@ +//Luma coefficients suggested for HDTVs. If you change these, make sure they add up to 1. +#define LUMA_R 0.213 +#define LUMA_G 0.715 +#define LUMA_B 0.072 diff --git a/code/_helpers/icons.dm b/code/_helpers/icons.dm index 9fd9e18500..414238fcaa 100644 --- a/code/_helpers/icons.dm +++ b/code/_helpers/icons.dm @@ -80,8 +80,8 @@ Blend(M, ICON_ADD) /proc/BlendRGB(rgb1, rgb2, amount) - var/list/RGB1 = rgb2num(rgb1) - var/list/RGB2 = rgb2num(rgb2) + var/list/RGB1 = ReadRGB(rgb1) + var/list/RGB2 = ReadRGB(rgb2) // add missing alpha if needed if(RGB1.len < RGB2.len) RGB1 += 255 diff --git a/code/_helpers/icons/flatten.dm b/code/_helpers/icons/flatten.dm new file mode 100644 index 0000000000..0435f7b6e1 --- /dev/null +++ b/code/_helpers/icons/flatten.dm @@ -0,0 +1,268 @@ +//? File contains functions to generate flat /icon's from things. +//? This is obviously expensive. Very, very expensive. +//? new get_flat_icon is faster, but, really, don't use these unless you need to. +//? Chances are unless you are: +//? - sending to html/browser (for non character preview purposes) +//? - taking photos +//? - doing complex icon operations that can't be done with filters/overlays +//? you probably don't need to use these. + +/** + * Generates an icon with all 4 directions of something. + * + * @params + * - A - appearancelike object. + * - no_anim - flatten out animations + */ +/proc/get_compound_icon(atom/A, no_anim) + var/mutable_appearance/N = new + N.appearance = A + N.dir = NORTH + var/icon/north = get_flat_icon(N, NORTH, no_anim = no_anim) + N.dir = SOUTH + var/icon/south = get_flat_icon(N, SOUTH, no_anim = no_anim) + N.dir = EAST + var/icon/east = get_flat_icon(N, EAST, no_anim = no_anim) + N.dir = WEST + var/icon/west = get_flat_icon(N, WEST, no_anim = no_anim) + qdel(N) + //Starts with a blank icon because of byond bugs. + var/icon/full = icon('icons/system/blank_32x32.dmi', "") + full.Insert(north, dir = NORTH) + full.Insert(south, dir = SOUTH) + full.Insert(east, dir = EAST) + full.Insert(west, dir = WEST) + qdel(north) + qdel(south) + qdel(east) + qdel(west) + return full + +/proc/get_flat_icon(appearance/appearancelike, dir, no_anim) + if(!dir && isloc(appearancelike)) + dir = appearancelike.dir + return _get_flat_icon(appearancelike, dir, no_anim, null, TRUE) + +/proc/_get_flat_icon(image/A, defdir, no_anim, deficon, start) + // start with blank image + var/static/icon/template = icon('icons/system/blank_32x32.dmi', "") + + #define BLANK icon(template) + + #define INDEX_X_LOW 1 + #define INDEX_X_HIGH 2 + #define INDEX_Y_LOW 3 + #define INDEX_Y_HIGH 4 + + #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] + + // invis? skip. + if(!A || A.alpha <= 0) + return BLANK + + // detect if state exists + var/icon/icon = A.icon || deficon + var/state = A.icon_state + var/none = !icon + if(!none) + var/list/states = icon_states(icon) + if(!(state in states)) + if(!("" in states)) + none = TRUE + else + state = "" + + // determine if there's directionals + // propagate forced direcitons down if and only if A has a direction + // todo: this results in a mismatch if someone is facing east but their overlays are facing south. + var/dir + if(start || !A.dir) + dir = defdir + else + dir = A.dir + var/ourdir = dir + if(!none && ourdir != SOUTH) + if(length(icon_states(icon(icon, state, NORTH)))) + else if(length(icon_states(icon(icon, state, EAST)))) + else if(length(icon_states(icon(icon, state, WEST)))) + else + ourdir = SOUTH + + // start generating + if(!A.overlays.len && !A.underlays.len) + // we don't even have ourselves! + if(none) + return BLANK + // no overlays/underlays, we're done, just mix in ourselves + var/icon/self_icon = icon(icon(icon, state, ourdir), "", 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) + return self_icon + + // safety/performance check + if((A.overlays.len + A.underlays.len) > 80) + // we use fucking insertion check + // > 80 = death. + CRASH("get_flat_icon tried to process more than 80 layers") + + // otherwise, we have to blend in all overlays/underlays. + var/icon/flat = BLANK + var/list/appearance/gathered = list() + var/appearance/copying + var/appearance/comparing + var/i + var/appearance/self + var/current_layer + + if(!none) + // add the atom itself + self = image(icon = icon, icon_state = state, layer = A.layer, dir = ourdir) + self.color = A.color + self.alpha = A.alpha + self.blend_mode = A.blend_mode + gathered[self] = A.layer + + // gather + for(copying as anything in A.overlays) + // todo: better handling + if(copying.plane != FLOAT_PLANE && copying.plane != A.plane) + // we don't care probably HUD or something lol + continue + current_layer = copying.layer + // if it's float layer, shove it right above atom. + if(current_layer < 0) + if(current_layer < -1000) + CRASH("who the hell is using -1000 or below on float layers?") + current_layer = A.layer + (1000 + current_layer) / 1000 + // else, add 1 so it doesn't potentially collide on float + else + ++current_layer + + // inject with insertion sort + for(i in 1 to gathered.len) + comparing = gathered[i] + if(current_layer < gathered[comparing]) + gathered.Insert(i, copying) + // associate + gathered[copying] = current_layer + + for(copying as anything in A.underlays) + // todo: better handling + if(copying.plane != FLOAT_PLANE && copying.plane != A.plane) + // we don't care probably HUD or something lol + continue + current_layer = copying.layer + // if it's float layer, shove it right below atom. + if(current_layer < 0) + if(current_layer < -1000) + CRASH("who the hell is using -1000 or below on float layers?") + current_layer = A.layer - (1000 + current_layer) / 1000 + // else, subtract 1 so it doesn't potentially collide on float + else + --current_layer + + // inject with insertion sort + for(i in 1 to gathered.len) + comparing = gathered[i] + if(current_layer < gathered[comparing]) + gathered.Insert(i, copying) + // associate + gathered[copying] = current_layer + + // adding icon we're mixing in + var/icon/adding + // current dimensions + var/list/flat_size = list(1, flat.Width(), 1, flat.Height()) + // adding dimensions + var/list/add_size[4] + // blend mode + var/blend_mode + + // blend in layers + for(copying as anything in gathered) + // if invis, skip + if(copying.alpha == 0) + continue + + // detect if it's literally ourselves + if(copying == self) + // blend in normally (no sense doing otherwise unless we're on map) + // we can't assume we're on map. + blend_mode = BLEND_OVERLAY + adding = icon(icon, state, ourdir) + else + // use full get_flat_icon + blend_mode = copying.blend_mode + adding = _get_flat_icon(copying, defdir, no_anim, icon) + + // if we got nothing, skip + if(!adding) + continue + + // detect adding size, taking into account copying overlay's pixel offsets + add_size[INDEX_X_LOW] = min(flatX1, copying.pixel_x + 1) + add_size[INDEX_X_HIGH] = max(flatX2, copying.pixel_x + adding.Width()) + add_size[INDEX_Y_LOW] = min(flatY1, copying.pixel_y + 1) + add_size[INDEX_Y_HIGH] = max(flatY2, copying.pixel_y + adding.Height()) + + // resize flat to fit if necessary + if(flat_size ~! add_size) + flat.Crop( + addX1 - flatX1 + 1, + addY1 - flatY1 + 1, + addX2 - flatX1 + 1, + addY2 - flatY1 + 1 + ) + flat_size = add_size.Copy() + + // blend the overlay/underlay in + flat.Blend(adding, blendMode2iconMode(blend_mode), copying.pixel_x + 2 - flatX1, copying.pixel_y + 2 - flatY1) + + // apply colors + if(A.color) + if(islist(A.color)) + flat.MapColors(arglist(A.color)) + else + flat.Blend(A.color, ICON_MULTIPLY) + + // apply alpha + if(A.alpha < 255) + flat.Blend(rgb(255, 255, 255, A.alpha), ICON_MULTIPLY) + + // finalize + if(no_anim) + // clean up frames + var/icon/cleaned = icon() + cleaned.Insert(flat, "", SOUTH, 1, 0) + return cleaned + else + // just return flat as SOUTH + return icon(flat, "", SOUTH) + + #undef flatX1 + #undef flatX2 + #undef flatY1 + #undef flatY2 + #undef addX1 + #undef addX2 + #undef addY1 + #undef addY2 + + #undef INDEX_X_LOW + #undef INDEX_X_HIGH + #undef INDEX_Y_LOW + #undef INDEX_Y_HIGH + + #undef BLANK diff --git a/code/_helpers/icons_ch.dm b/code/_helpers/icons_ch.dm new file mode 100644 index 0000000000..ff0f884e0f --- /dev/null +++ b/code/_helpers/icons_ch.dm @@ -0,0 +1,665 @@ +/* +IconProcs README + +A BYOND library for manipulating icons and colors + +by Lummox JR + +version 1.0 + +The IconProcs library was made to make a lot of common icon operations much easier. BYOND's icon manipulation +routines are very capable but some of the advanced capabilities like using alpha transparency can be unintuitive to beginners. + +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: + +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 + transparent is usually much easier than making it less so, however. This proc basically is a frontend + for MapColors() which can change opacity any way you like, in much the same way that SetIntensity() + can make an icon lighter or darker. If amount is 0.5, the opacity of the icon will be cut in half. + If amount is 2, opacity is doubled and anything more than half-opaque will become fully opaque. +icon/GrayScale() + Converts the icon to grayscale instead of a fully colored icon. Alpha values are left intact. +icon/ColorTone(tone) + Similar to GrayScale(), this proc converts the icon to a range of black -> tone -> white, where tone is an + RGB color (its alpha is ignored). This can be used to create a sepia tone or similar effect. + See also the global ColorTone() proc. +icon/MinColors(icon) + The icon is blended with a second icon where the minimum of each RGB pixel is the result. + Transparency may increase, as if the icons were blended with ICON_ADD. You may supply a color in place of an icon. +icon/MaxColors(icon) + The icon is blended with a second icon where the maximum of each RGB pixel is the result. + Opacity may increase, as if the icons were blended with ICON_OR. You may supply a color in place of an icon. +icon/Opaque(background = "#000000") + All alpha values are set to 255 throughout the icon. Transparent pixels become black, or whatever background color you specify. +icon/BecomeAlphaMask() + You can convert a simple grayscale icon into an alpha mask to use with other icons very easily with this proc. + The black parts become transparent, the white parts stay white, and anything in between becomes a translucent shade of white. +icon/AddAlphaMask(mask) + The alpha values of the mask icon will be blended with the current icon. Anywhere the mask is opaque, + the current icon is untouched. Anywhere the mask is transparent, the current icon becomes transparent. + Where the mask is translucent, the current icon becomes more transparent. +icon/UseAlphaMask(mask, mode) + Sometimes you may want to take the alpha values from one icon and use them on a different icon. + This proc will do that. Just supply the icon whose alpha mask you want to use, and src will change + so it has the same colors as before but uses the mask for opacity. + +COLOR MANAGEMENT AND HSV + +RGB isn't the only way to represent color. Sometimes it's more useful to work with a model called HSV, which stands for hue, saturation, and value. + + * The hue of a color describes where it is along the color wheel. It goes from red to yellow to green to + cyan to blue to magenta and back to red. + * The saturation of a color is how much color is in it. A color with low saturation will be more gray, + and with no saturation at all it is a shade of gray. + * 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. + + * 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 + +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". + +More than one HSV color can match the same RGB color. + +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 + 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. +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. +GrayScale(rgb) + Takes an RGB or RGBA color and converts it to grayscale. Returns an RGB or RGBA string. +ColorTone(rgb, tone) + Similar to GrayScale(), this proc converts an RGB or RGBA color to a range of black -> tone -> white instead of + using strict shades of gray. The tone value is an RGB color; any alpha value is ignored. +*/ + +/* +Get Flat Icon DEMO by DarkCampainger + +This is a test for the get flat icon proc, modified approprietly for icons and their states. +Probably not a good idea to run this unless you want to see how the proc works in detail. +mob + icon = 'old_or_unused.dmi' + icon_state = "green" + + Login() + // Testing image underlays + underlays += image(icon='old_or_unused.dmi',icon_state="red") + underlays += image(icon='old_or_unused.dmi',icon_state="red", pixel_x = 32) + underlays += image(icon='old_or_unused.dmi',icon_state="red", pixel_x = -32) + + // Testing image overlays + 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) + add_overlay('_flat_demoIcons2.dmi') + + // Testing icon_state overlays (defaults to mob's icon) + add_overlay("white") + + // Testing dynamic icon overlays + var/icon/I = icon('old_or_unused.dmi', icon_state="aqua") + I.Shift(NORTH,16,1) + add_overlay(I) + + // Testing dynamic image overlays + I=image(icon=I,pixel_x = -32, pixel_y = 32) + add_overlay(I) + + // Testing object types (and layers) + add_overlay(/obj/effect/overlayTest) + + loc = locate (10,10,1) + verb + Browse_Icon() + set name = "1. Browse Icon" + // Give it a name for the cache + var/iconName = "[ckey(src.name)]_flattened.dmi" + // Send the icon to src's local cache + src<

") + + Output_Icon() + set name = "2. Output Icon" + to_chat(src, "Icon is: [icon2base64html(get_flat_icon(src))]") + + Label_Icon() + set name = "3. Label Icon" + // Give it a name for the cache + var/iconName = "[ckey(src.name)]_flattened.dmi" + // Copy the file to the rsc manually + var/icon/I = fcopy_rsc(get_flat_icon(src)) + // Send the icon to src's local cache + src< 57 && ch < 65) || (ch > 70 && ch < 97) || ch > 102) + break + ++digits + if(digits == 8) + break + + var/single = digits < 6 + if(digits != 3 && digits != 4 && digits != 6 && digits != 8) + return + if(digits == 4 || digits == 8) + usealpha = 1 + for(i=start, digits>0, ++i) + ch = text2ascii(rgb, i) + if(ch >= 48 && ch <= 57) + ch -= 48 + else if(ch >= 65 && ch <= 70) + ch -= 55 + else if(ch >= 97 && ch <= 102) + ch -= 87 + else + break + --digits + switch(which) + if(0) + r = (r << 4) | ch + if(single) + r |= r << 4 + ++which + else if(!(digits & 1)) + ++which + if(1) + g = (g << 4) | ch + if(single) + g |= g << 4 + ++which + else if(!(digits & 1)) + ++which + if(2) + b = (b << 4) | ch + if(single) + b |= b << 4 + ++which + else if(!(digits & 1)) + ++which + if(3) + alpha = (alpha << 4) | ch + if(single) + alpha |= alpha << 4 + + . = list(r, g, b) + if(usealpha) + . += alpha + +/proc/ReadHSV(hsv) + if(!hsv) + return + + // interpret the HSV or HSVA value + var/i=1,start=1 + if(text2ascii(hsv) == 35) + ++start // skip opening # + var/ch,which=0,hue=0,sat=0,val=0,alpha=0,usealpha + var/digits=0 + for(i=start, i<=length(hsv), ++i) + ch = text2ascii(hsv, i) + if(ch < 48 || (ch > 57 && ch < 65) || (ch > 70 && ch < 97) || ch > 102) + break + ++digits + if(digits == 9) + break + if(digits > 7) + usealpha = 1 + if(digits <= 4) + ++which + if(digits <= 2) + ++which + for(i=start, digits>0, ++i) + ch = text2ascii(hsv, i) + if(ch >= 48 && ch <= 57) + ch -= 48 + else if(ch >= 65 && ch <= 70) + ch -= 55 + else if(ch >= 97 && ch <= 102) + ch -= 87 + else + break + --digits + switch(which) + if(0) + hue = (hue << 4) | ch + if(digits == (usealpha ? 6 : 4)) + ++which + if(1) + sat = (sat << 4) | ch + if(digits == (usealpha ? 4 : 2)) + ++which + if(2) + val = (val << 4) | ch + if(digits == (usealpha ? 2 : 0)) + ++which + if(3) + alpha = (alpha << 4) | ch + + . = list(hue, sat, val) + if(usealpha) + . += alpha + +/proc/HSVtoRGB(hsv) + if(!hsv) + return "#000000" + var/list/HSV = ReadHSV(hsv) + if(!HSV) + return "#000000" + + var/hue = HSV[1] + var/sat = HSV[2] + var/val = HSV[3] + + // Compress hue into easier-to-manage range + hue -= hue >> 8 + if(hue >= 0x5fa) + hue -= 0x5fa + + var/hi,mid,lo,r,g,b + hi = val + lo = round((255 - sat) * val / 255, 1) + mid = lo + round(abs(round(hue, 510) - hue) * (hi - lo) / 255, 1) + if(hue >= 765) + if(hue >= 1275) {r=hi; g=lo; b=mid} + else if(hue >= 1020) {r=mid; g=lo; b=hi } + else {r=lo; g=mid; b=hi } + else + if(hue >= 510) {r=lo; g=hi; b=mid} + else if(hue >= 255) {r=mid; g=hi; b=lo } + else {r=hi; g=mid; b=lo } + + return (HSV.len > 3) ? rgb(r,g,b,HSV[4]) : rgb(r,g,b) + +/proc/RGBtoHSV(rgb) + if(!rgb) + return "#0000000" + var/list/RGB = ReadRGB(rgb) + if(!RGB) + return "#0000000" + + var/r = RGB[1] + var/g = RGB[2] + var/b = RGB[3] + var/hi = max(r,g,b) + var/lo = min(r,g,b) + + var/val = hi + var/sat = hi ? round((hi-lo) * 255 / hi, 1) : 0 + var/hue = 0 + + if(sat) + var/dir + var/mid + if(hi == r) + if(lo == b) {hue=0; dir=1; mid=g} + else {hue=1535; dir=-1; mid=b} + else if(hi == g) + if(lo == r) {hue=512; dir=1; mid=b} + else {hue=511; dir=-1; mid=r} + else if(hi == b) + if(lo == g) {hue=1024; dir=1; mid=r} + else {hue=1023; dir=-1; mid=g} + hue += dir * round((mid-lo) * 255 / (hi-lo), 1) + + return hsv(hue, sat, val, (RGB.len>3 ? RGB[4] : null)) + +/proc/hsv(hue, sat, val, alpha) + if(hue < 0 || hue >= 1536) + hue %= 1536 + if(hue < 0) + hue += 1536 + if((hue & 0xFF) == 0xFF) + ++hue + if(hue >= 1536) + hue = 0 + if(sat < 0) + sat = 0 + if(sat > 255) + sat = 255 + if(val < 0) + val = 0 + if(val > 255) + val = 255 + . = "#" + . += TO_HEX_DIGIT(hue >> 8) + . += TO_HEX_DIGIT(hue >> 4) + . += TO_HEX_DIGIT(hue) + . += TO_HEX_DIGIT(sat >> 4) + . += TO_HEX_DIGIT(sat) + . += TO_HEX_DIGIT(val >> 4) + . += TO_HEX_DIGIT(val) + if(!isnull(alpha)) + if(alpha < 0) + alpha = 0 + if(alpha > 255) + alpha = 255 + . += TO_HEX_DIGIT(alpha >> 4) + . += TO_HEX_DIGIT(alpha) + +/* + Smooth blend between HSV colors + + amount=0 is the first color + amount=1 is the second color + amount=0.5 is directly between the two colors + + 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) + +/proc/BlendRGBasHSV(rgb1, rgb2, amount) + return HSVtoRGB(RGBtoHSV(rgb1), RGBtoHSV(rgb2), amount) + +/proc/HueToAngle(hue) + // normalize hsv in case anything is screwy + 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) + var/hue = angle * (1530/360) + // Decompress hue + hue += round(hue / 255) + return hue + + +// positive angle rotates forward through red->green->blue +/proc/RotateHue(hsv, angle) + var/list/HSV = ReadHSV(hsv) + + // normalize hsv in case anything is screwy + if(HSV[1] >= 1536) + HSV[1] %= 1536 + if(HSV[1] < 0) + HSV[1] += 1536 + + // Compress hue into easier-to-manage range + HSV[1] -= HSV[1] >> 8 + + if(angle < 0 || angle >= 360) + angle -= 360 * round(angle / 360) + HSV[1] = round(HSV[1] + angle * (1530/360), 1) + + // normalize hue + if(HSV[1] < 0 || HSV[1] >= 1530) + HSV[1] %= 1530 + if(HSV[1] < 0) + HSV[1] += 1530 + // decompress hue + HSV[1] += round(HSV[1] / 255) + + return hsv(HSV[1], HSV[2], HSV[3], (HSV.len > 3 ? HSV[4] : null)) + +// Convert an rgb color to grayscale +/proc/GrayScale(rgb) + var/list/RGB = ReadRGB(rgb) + var/gray = RGB[1]*0.3 + RGB[2]*0.59 + RGB[3]*0.11 + return (RGB.len > 3) ? rgb(gray, gray, gray, RGB[4]) : rgb(gray, gray, gray) + +// Change grayscale color to black->tone->white range +/proc/ColorTone(rgb, tone) + var/list/RGB = ReadRGB(rgb) + var/list/TONE = ReadRGB(tone) + + var/gray = RGB[1]*0.3 + RGB[2]*0.59 + RGB[3]*0.11 + var/tone_gray = TONE[1]*0.3 + TONE[2]*0.59 + TONE[3]*0.11 + + if(gray <= tone_gray) + return BlendRGB("#000000", tone, gray/(tone_gray || 1)) + else + return BlendRGB(tone, "#ffffff", (gray-tone_gray)/((255-tone_gray) || 1)) + + +//Used in the OLD chem colour mixing algorithm +/proc/GetColors(hex) + hex = uppertext(hex) + // No alpha set? Default to full alpha. + if(length(hex) == 7) + hex += "FF" + var/hi1 = text2ascii(hex, 2) // R + var/lo1 = text2ascii(hex, 3) // R + var/hi2 = text2ascii(hex, 4) // G + var/lo2 = text2ascii(hex, 5) // G + var/hi3 = text2ascii(hex, 6) // B + var/lo3 = text2ascii(hex, 7) // B + var/hi4 = text2ascii(hex, 8) // A + var/lo4 = text2ascii(hex, 9) // A + return list(((hi1>= 65 ? hi1-55 : hi1-48)<<4) | (lo1 >= 65 ? lo1-55 : lo1-48), + ((hi2 >= 65 ? hi2-55 : hi2-48)<<4) | (lo2 >= 65 ? lo2-55 : lo2-48), + ((hi3 >= 65 ? hi3-55 : hi3-48)<<4) | (lo3 >= 65 ? lo3-55 : lo3-48), + ((hi4 >= 65 ? hi4-55 : hi4-48)<<4) | (lo4 >= 65 ? lo4-55 : lo4-48)) + +//Interface for using DrawBox() to draw 1 pixel on a coordinate. +//Returns the same icon specifed in the argument, but with the pixel drawn +/proc/DrawPixel(icon/I,colour,drawX,drawY) + if(!I) + return 0 + + var/Iwidth = I.Width() + var/Iheight = I.Height() + + if(drawX > Iwidth || drawX <= 0) + return 0 + if(drawY > Iheight || drawY <= 0) + return 0 + + I.DrawBox(colour,drawX, drawY) + return I + + +//Interface for easy drawing of one pixel on an atom. +/atom/proc/DrawPixelOn(colour, drawX, drawY) + var/icon/I = new(icon) + var/icon/J = DrawPixel(I, colour, drawX, drawY) + if(J) //Only set the icon if it succeeded, the icon without the pixel is 1000x better than a black square. + icon = J + return J + return 0 + +//Hook, override to run code on- wait this is images +//Images have dir without being an atom, so they get their own definition. +//Lame. +/image/proc/setDir(newdir) + dir = newdir + +/* Gives the result RGB of a RGB string after a matrix transformation. No alpha. + * Input: rr, rg, rb, gr, gg, gb, br, bg, bb, cr, cg, cb + * Output: RGB string + */ +/proc/RGBMatrixTransform(list/color, list/cm) + ASSERT(cm.len >= 9) + if(cm.len < 12) // fill in the rest + for(var/i in 1 to (12 - cm.len)) + cm += 0 + if(!islist(color)) + color = ReadRGB(color) + color[1] = color[1] * cm[1] + color[2] * cm[2] + color[3] * cm[3] + cm[10] * 255 + color[2] = color[1] * cm[4] + color[2] * cm[5] + color[3] * cm[6] + cm[11] * 255 + color[3] = color[1] * cm[7] + color[2] * cm[8] + color[3] * cm[9] + cm[12] * 255 + return rgb(color[1], color[2], color[3]) diff --git a/code/_helpers/type2type.dm b/code/_helpers/type2type.dm index 8bd45bbfc7..1479a07d35 100644 --- a/code/_helpers/type2type.dm +++ b/code/_helpers/type2type.dm @@ -183,6 +183,88 @@ var/hex_to_work_on = copytext(hex,5,7) return hex2num(hex_to_work_on) +/** + * Convert HSL to RGB + */ +/proc/hsl2rgb(hue, saturation, lightness) + var/red + var/green + var/blue + + if(saturation == 0) + red = lightness * 255 + green = red + blue = red + else + var/a;var/b; + if(lightness < 0.5) + b = lightness * (1 + saturation) + else + b = (lightness + saturation) - (saturation * lightness) + a = 2 * lightness - b + + red = round(255 * hue2rgb(a, b, hue + (1/3)), 1) + green = round(255 * hue2rgb(a, b, hue), 1) + blue = round(255 * hue2rgb(a, b, hue - (1/3)), 1) + + return list(red, green, blue) + +/** + * Convert RBG to HSL + */ +/proc/rgb2hsl(red, green, blue) + red /= 255 + green /= 255 + blue /= 255 + + var/max = max(red, green, blue) + var/min = min(red, green, blue) + var/range = max - min + + var/hue = 0 + var/saturation = 0 + var/lightness = 0 + + lightness = (max + min) / 2 + if(range != 0) + if(lightness < 0.5) + saturation = range / (max + min) + else + saturation = range / (2 - max - min) + + var/dred = ((max - red) / (6 * max)) + 0.5 + var/dgreen = ((max - green) / (6 * max)) + 0.5 + var/dblue = ((max - blue) / (6 * max)) + 0.5 + + if(max == red) + hue = dblue - dgreen + else if(max == green) + hue = dred - dblue + (1 / 3) + else + hue = dgreen - dred + (2 / 3) + if(hue < 0) + hue++ + else if(hue > 1) + hue-- + + return list(hue, saturation, lightness) + +/** + * Convert hue to RGB + */ +/proc/hue2rgb(a, b, hue) + if(hue < 0) + hue++ + else if(hue > 1) + hue-- + if(6*hue < 1) + return (a + (b - a) * 6 * hue) + if(2*hue < 1) + return b + if(3*hue < 2) + return (a + (b - a) * ((2 / 3) - hue) * 6) + return a + // heat2color functions. Adapted from: http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ /proc/heat2color(temp) return rgb(heat2color_r(temp), heat2color_g(temp), heat2color_b(temp)) @@ -211,6 +293,53 @@ else . = max(0, min(255, 138.5177312231 * log(temp - 10) - 305.0447927307)) +/** + * Assumes format #RRGGBB #rrggbb + */ +/proc/color_hex2num(A) + if(!A || length(A) != length_char(A)) + return 0 + var/R = hex2num(copytext(A, 2, 4)) + var/G = hex2num(copytext(A, 4, 6)) + var/B = hex2num(copytext(A, 6, 8)) + return R+G+B + +/** + *! Word of warning: + * Using a matrix like this as a color value will simplify it back to a string after being set. + */ +/proc/color_hex2color_matrix(string) + var/length = length(string) + if((length != 7 && length != 9) || length != length_char(string)) + return color_matrix_identity() + var/r = hex2num(copytext(string, 2, 4)) / 255 + var/g = hex2num(copytext(string, 4, 6)) / 255 + var/b = hex2num(copytext(string, 6, 8)) / 255 + var/a = 1 + if(length == 9) + a = hex2num(copytext(string, 8, 10)) / 255 + if(!isnum(r) || !isnum(g) || !isnum(b) || !isnum(a)) + return color_matrix_identity() + return list( + r,0,0,0,0, + g,0,0,0,0, + b,0,0,0,0, + a,0,0,0,0, + ) + +/** + * Will drop all values not on the diagonal. + */ +/proc/color_matrix2color_hex(list/the_matrix) + if(!istype(the_matrix) || the_matrix.len != 20) + return "#ffffffff" + return rgb( + the_matrix[1] * 255, // R + the_matrix[6] * 255, // G + the_matrix[11] * 255, // B + the_matrix[16] * 255, // A + ) + // Very ugly, BYOND doesn't support unix time and rounding errors make it really hard to convert it to BYOND time. // returns "YYYY-MM-DD" by default /proc/unix2date(timestamp, seperator = "-") diff --git a/code/datums/browser.dm b/code/datums/browser.dm index 62aa9aab86..e38b78ab4d 100644 --- a/code/datums/browser.dm +++ b/code/datums/browser.dm @@ -380,3 +380,88 @@ . |= GLOB.bitfields[bitfield][flag] else return + +/datum/browser/modal/color_matrix_picker + var/color_matrix + +/datum/browser/modal/color_matrix_picker/New(mob/user, message, title, button1 = "Ok", button2, button3, stealfocus = TRUE, timeout = 0, list/values) + if(!user) + return + if(!values) + values = list(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) + if(values.len < 12) + values.len = 12 + var/list/output = list() + output += "
" + output += "[message]" +#define MATRIX_FIELD(field, default) " " + output += "

" + output += MATRIX_FIELD("rr", values[1]) + output += MATRIX_FIELD("gr", values[4]) + output += MATRIX_FIELD("br", values[7]) + output += "

" + output += MATRIX_FIELD("rg", values[2]) + output += MATRIX_FIELD("gg", values[5]) + output += MATRIX_FIELD("bg", values[8]) + output += "

" + output += MATRIX_FIELD("rb", values[3]) + output += MATRIX_FIELD("gb", values[6]) + output += MATRIX_FIELD("bb", values[9]) + output += "

" + output += MATRIX_FIELD("cr", values[10]) + output += MATRIX_FIELD("cg", values[11]) + output += MATRIX_FIELD("cb", values[12]) + output += "

" +#undef MATRIX_FIELD + + output += {"
+ "} + + if (button2) + output += {""} + + if (button3) + output += {""} + output += {"
"} + + ..(user, ckey("[user]-[message]-[title]-[world.time]-[rand(1,10000)]"), title, 800, 400, src, stealfocus, timeout) + set_content(output.Join("")) + +/datum/browser/modal/color_matrix_picker/Topic(href, list/href_list) + if(href_list["close"] || !user) + opentime = 0 + return + if(href_list["button"]) + var/button = text2num(href_list["button"]) + if(ISINRANGE(button, 1, 3)) + selectedbutton = button + var/list/cm = rgb_construct_color_matrix( + text2num(href_list["rr"]), + text2num(href_list["rg"]), + text2num(href_list["rb"]), + text2num(href_list["gr"]), + text2num(href_list["gg"]), + text2num(href_list["gb"]), + text2num(href_list["br"]), + text2num(href_list["bg"]), + text2num(href_list["bb"]), + text2num(href_list["cr"]), + text2num(href_list["cg"]), + text2num(href_list["cb"]) + ) + if(cm) + color_matrix = cm + opentime = 0 + close() + +/proc/color_matrix_picker(mob/user, message, title, button1 = "Ok", button2, button3, stealfocus, timeout = 10 MINUTES, list/values) + if(!istype(user)) + if(istype(user, /client)) + var/client/C = user + user = C.mob + else + return + var/datum/browser/modal/color_matrix_picker/B = new(user, message, title, button1, button2, button3, stealfocus, timeout, values) + B.open() + B.wait() + return list("button" = B.selectedbutton, "matrix" = B.color_matrix) diff --git a/code/datums/interfaces/appearance.dm b/code/datums/interfaces/appearance.dm new file mode 100644 index 0000000000..7bcb43d37d --- /dev/null +++ b/code/datums/interfaces/appearance.dm @@ -0,0 +1,143 @@ +/** + * hey, remember mutable appearance? + * + * only: + * - this isn't a real object rather than a struct + * - i'm making a cast for it so we can VV it + * - this is also used to cast procs that operate on appearance-like things. + * + * Sue me, I need to debug things somehow + * + * DO NOT USE THESE UNLESS YOU KNOW WHAT YOU ARE DOING. + */ +/appearance + var/alpha + var/appearance_flags + var/blend_mode + var/color + var/desc + var/dir + var/gender + var/icon + var/icon_state + var/invisibility + var/infra_luminosity + var/list/filters + var/layer + var/luminosity + var/maptext + var/maptext_width + var/maptext_height + var/maptext_x + var/maptext_y + var/mouse_over_pointer + var/mouse_drag_pointer + var/mouse_drop_pointer + var/mouse_drop_zone + var/mouse_opacity + var/name + var/opacity + var/list/overlays + var/override + var/pixel_x + var/pixel_y + var/pixel_w + var/pixel_z + var/plane + var/render_source + var/render_target + var/suffix + var/text + var/transform + var/list/underlays + // var/vis_flags + +//! vis_flags missing even though byond ref says it's there, fuck off why is this possible + +GLOBAL_REAL_VAR(_appearance_var_list) = list( + "alpha", + "appearance_flags", + "blend_mode", + "color", + "desc", + "dir", + "gender", + "icon", + "icon_state", + "invisibility", + "infra_luminosity", + "filters", + "layer", + "luminosity", + "maptext", + "maptext_width", + "maptext_height", + "maptext_x", + "maptext_y", + "mouse_over_pointer", + "mouse_drag_pointer", + "mouse_drop_pointer", + "mouse_drop_zone", + "mouse_opacity", + "name", + "opacity", + "overlays", + "override", + "pixel_x", + "pixel_y", + "pixel_w", + "pixel_z", + "plane", + "render_source", + "render_target", + "suffix", + "text", + "transform", + "underlays" + // "vis_flags" +) + +/proc/__appearance_v_debug(appearance/A, name) + switch(name) +#define DEBUG_APPEARANCE_VAR(n) if(#n) return debug_variable(name, A.n, 0, null) + DEBUG_APPEARANCE_VAR(alpha) + DEBUG_APPEARANCE_VAR(appearance_flags) + DEBUG_APPEARANCE_VAR(blend_mode) + DEBUG_APPEARANCE_VAR(color) + DEBUG_APPEARANCE_VAR(desc) + DEBUG_APPEARANCE_VAR(dir) + DEBUG_APPEARANCE_VAR(gender) + DEBUG_APPEARANCE_VAR(icon) + DEBUG_APPEARANCE_VAR(icon_state) + DEBUG_APPEARANCE_VAR(invisibility) + DEBUG_APPEARANCE_VAR(infra_luminosity) + DEBUG_APPEARANCE_VAR(filters) + DEBUG_APPEARANCE_VAR(layer) + DEBUG_APPEARANCE_VAR(luminosity) + DEBUG_APPEARANCE_VAR(maptext) + DEBUG_APPEARANCE_VAR(maptext_width) + DEBUG_APPEARANCE_VAR(maptext_height) + DEBUG_APPEARANCE_VAR(maptext_x) + DEBUG_APPEARANCE_VAR(maptext_y) + DEBUG_APPEARANCE_VAR(mouse_over_pointer) + DEBUG_APPEARANCE_VAR(mouse_drag_pointer) + DEBUG_APPEARANCE_VAR(mouse_drop_pointer) + DEBUG_APPEARANCE_VAR(mouse_drop_zone) + DEBUG_APPEARANCE_VAR(mouse_opacity) + DEBUG_APPEARANCE_VAR(name) + DEBUG_APPEARANCE_VAR(opacity) + DEBUG_APPEARANCE_VAR(overlays) + DEBUG_APPEARANCE_VAR(override) + DEBUG_APPEARANCE_VAR(pixel_x) + DEBUG_APPEARANCE_VAR(pixel_y) + DEBUG_APPEARANCE_VAR(pixel_w) + DEBUG_APPEARANCE_VAR(pixel_z) + DEBUG_APPEARANCE_VAR(plane) + DEBUG_APPEARANCE_VAR(render_source) + DEBUG_APPEARANCE_VAR(render_target) + DEBUG_APPEARANCE_VAR(suffix) + DEBUG_APPEARANCE_VAR(text) + DEBUG_APPEARANCE_VAR(transform) + DEBUG_APPEARANCE_VAR(underlays) + // DEBUG_APPEARANCE_VAR(vis_flags) +#undef DEBUG_APPEARANCE_VAR diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 9490a0af46..aab9326c51 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -49,6 +49,13 @@ var/chat_color_darkened /// The chat color var, without alpha. var/chat_color_hover + //! Colors + /** + * used to store the different colors on an atom + * + * its inherent color, the colored paint applied on it, special color effect etc... + */ + var/list/atom_colours /atom/New(loc, ...) // Don't call ..() unless /datum/New() ever exists @@ -731,3 +738,51 @@ // Airflow and ZAS zones now uses CanZASPass() instead of this proc. /atom/proc/CanPass(atom/movable/mover, turf/target) return !density + + +//! ## Atom Colour Priority System +/** + * A System that gives finer control over which atom colour to colour the atom with. + * The "highest priority" one is always displayed as opposed to the default of + * "whichever was set last is displayed" + */ + +/// Adds an instance of colour_type to the atom's atom_colours list +/atom/proc/add_atom_colour(coloration, colour_priority) + if(!atom_colours || !atom_colours.len) + atom_colours = list() + atom_colours.len = COLOUR_PRIORITY_AMOUNT //four priority levels currently. + if(!coloration) + return + if(colour_priority > atom_colours.len) + return + atom_colours[colour_priority] = coloration + update_atom_colour() + +/// Removes an instance of colour_type from the atom's atom_colours list +/atom/proc/remove_atom_colour(colour_priority, coloration) + if(!atom_colours) + atom_colours = list() + atom_colours.len = COLOUR_PRIORITY_AMOUNT //four priority levels currently. + if(colour_priority > atom_colours.len) + return + if(coloration && atom_colours[colour_priority] != coloration) + return //if we don't have the expected color (for a specific priority) to remove, do nothing + atom_colours[colour_priority] = null + update_atom_colour() + +/// Resets the atom's color to null, and then sets it to the highest priority colour available +/atom/proc/update_atom_colour() + if(!atom_colours) + atom_colours = list() + atom_colours.len = COLOUR_PRIORITY_AMOUNT //four priority levels currently. + color = null + for(var/C in atom_colours) + if(islist(C)) + var/list/L = C + if(L.len) + color = L + return + else if(C) + color = C + return diff --git a/code/game/machinery/painter_vr.dm b/code/game/machinery/painter_vr.dm index e10311915d..95b0e4a4ef 100644 --- a/code/game/machinery/painter_vr.dm +++ b/code/game/machinery/painter_vr.dm @@ -1,62 +1,85 @@ -// I'm honestly pretty sure that short of stuffing five million things into this -// there's absolutely no way it could ever have any performance impact -// Given that all it does is set the color var -// But just in case it's cursed in some arcane horrible way -// I'm going to leave this limit here -#define MAX_PROCESSING 10 // Arbitrary performance insurance +#define COLORMATE_TINT 1 +#define COLORMATE_HSV 2 +#define COLORMATE_MATRIX 3 /obj/machinery/gear_painter name = "Color Mate" - desc = "A machine to give your apparel a fresh new color! Recommended to use with white items for best results." + desc = "A machine to give your apparel a fresh new color!" icon = 'icons/obj/vending_vr.dmi' icon_state = "colormate" density = TRUE anchored = TRUE - var/list/processing = list() + var/atom/movable/inserted var/activecolor = "#FFFFFF" + var/list/color_matrix_last + var/active_mode = COLORMATE_HSV + + var/build_hue = 0 + var/build_sat = 1 + var/build_val = 1 + + /// Allow holder'd mobs + var/allow_mobs = TRUE + /// Minimum lightness for normal mode + var/minimum_normal_lightness = 50 + /// Minimum lightness for matrix mode, tested using 4 test colors of full red, green, blue, white. + var/minimum_matrix_lightness = 75 + /// Minimum matrix tests that must pass for something to be considered a valid color (see above) + var/minimum_matrix_tests = 2 + /// Temporary messages + var/temp + var/list/allowed_types = list( - /obj/item/clothing, - /obj/item/weapon/storage/backpack, - /obj/item/weapon/storage/belt, - /obj/item/device/radio/headset - ) + /obj/item/clothing, + /obj/item/weapon/storage/backpack, + /obj/item/weapon/storage/belt, + /obj/item/toy + ) + +/obj/machinery/gear_painter/Initialize(mapload) + . = ..() + color_matrix_last = list( + 1, 0, 0, + 0, 1, 0, + 0, 0, 1, + 0, 0, 0, + ) /obj/machinery/gear_painter/update_icon() if(panel_open) icon_state = "colormate_open" else if(inoperable()) icon_state = "colormate_off" - else if(processing.len) + else if(inserted) icon_state = "colormate_active" else icon_state = "colormate" /obj/machinery/gear_painter/Destroy() - for(var/atom/movable/O in processing) - O.forceMove(drop_location()) - processing.Cut() + if(inserted) //please i beg you do not drop nulls + inserted.forceMove(drop_location()) return ..() -/obj/machinery/gear_painter/attackby(obj/item/W, mob/user) - if(LAZYLEN(processing) >= MAX_PROCESSING) - to_chat(user, "The machine is full.") +/obj/machinery/gear_painter/attackby(obj/item/I, mob/living/user) + if(inserted) + to_chat(user, SPAN_WARNING("The machine is already loaded.")) return - if(default_deconstruction_screwdriver(user, W)) + if(default_deconstruction_screwdriver(user, I)) return - if(default_deconstruction_crowbar(user, W)) + if(default_deconstruction_crowbar(user, I)) return - if(default_unfasten_wrench(user, W, 40)) + if(default_unfasten_wrench(user, I, 40)) return - if(is_type_in_list(W, allowed_types) && !inoperable()) - user.visible_message("[user] inserts \the [W] into the Color Mate receptable.") - user.drop_from_inventory(W) - W.forceMove(src) - processing |= W + if(is_type_in_list(I, allowed_types) && !inoperable()) + user.visible_message("[user] inserts \the [I] into the Color Mate receptable.") + user.drop_from_inventory(I) + I.forceMove(src) + inserted = I SStgui.update_uis(src) + else - ..() - update_icon() + return ..() /obj/machinery/gear_painter/attack_hand(mob/user) if(..()) @@ -69,49 +92,209 @@ ui = new(user, src, "ColorMate", name) ui.open() -/obj/machinery/gear_painter/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) - var/list/data = ..() +/obj/machinery/gear_painter/proc/insert_mob(mob/victim, mob/user) + if(inserted) + return + if(user) + visible_message(SPAN_WARNING("[user] stuffs [victim] into [src]!")) + inserted = victim + inserted.forceMove(src) - var/list/items = list() - for(var/atom/movable/O in processing) - items.Add("[O]") - data["items"] = items +/obj/machinery/gear_painter/AllowDrop() + return FALSE - data["activecolor"] = activecolor - return data +// /obj/machinery/gear_painter/handle_atom_del(atom/movable/AM) +// if(AM == inserted) +// inserted = null +// return ..() -/obj/machinery/gear_painter/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) - if(..()) - return TRUE - - add_fingerprint(usr) - - switch(action) - if("select") - var/newcolor = input(usr, "Choose a color.", "", activecolor) as color|null - if(newcolor) - activecolor = newcolor - . = TRUE - - if("paint") - for(var/atom/movable/O in processing) - O.color = activecolor - CHECK_TICK - playsound(src, 'sound/effects/spray3.ogg', 50, 1) - . = TRUE - - if("clear") - for(var/atom/movable/O in processing) - O.color = initial(O.color) - CHECK_TICK - playsound(src, 'sound/effects/spray3.ogg', 50, 1) - . = TRUE - - if("eject") - for(var/atom/movable/O in processing) - O.forceMove(drop_location()) - CHECK_TICK - processing.Cut() - . = TRUE +/obj/machinery/gear_painter/AltClick(mob/user) + . = ..() + drop_item() +/obj/machinery/gear_painter/proc/drop_item() + if(!oview(1,src)) + return + if(!inserted) + return + to_chat(usr, SPAN_NOTICE("You remove [inserted] from [src]")) + inserted.forceMove(drop_location()) + var/mob/living/user = usr + if(istype(user)) + user.put_in_hands(inserted) + inserted = null update_icon() + SStgui.update_uis(src) + +/obj/machinery/gear_painter/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ColorMate", src.name) + ui.set_autoupdate(FALSE) //This might be a bit intensive, better to not update it every few ticks + ui.open() + +/obj/machinery/gear_painter/tgui_data(mob/user) + . = list() + .["activemode"] = active_mode + .["matrixcolors"] = list( + "rr" = color_matrix_last[1], + "rg" = color_matrix_last[2], + "rb" = color_matrix_last[3], + "gr" = color_matrix_last[4], + "gg" = color_matrix_last[5], + "gb" = color_matrix_last[6], + "br" = color_matrix_last[7], + "bg" = color_matrix_last[8], + "bb" = color_matrix_last[9], + "cr" = color_matrix_last[10], + "cg" = color_matrix_last[11], + "cb" = color_matrix_last[12], + ) + .["buildhue"] = build_hue + .["buildsat"] = build_sat + .["buildval"] = build_val + if(temp) + .["temp"] = temp + if(inserted) + .["item"] = list() + .["item"]["name"] = inserted.name + .["item"]["sprite"] = icon2base64(get_flat_icon(inserted,dir=SOUTH,no_anim=TRUE)) + .["item"]["preview"] = icon2base64(build_preview()) + else + .["item"] = null + +/obj/machinery/gear_painter/tgui_act(action, params) + . = ..() + if(.) + return + if(inserted) + switch(action) + if("switch_modes") + active_mode = text2num(params["mode"]) + return TRUE + if("choose_color") + var/chosen_color = input(usr, "Choose a color: ", "ColorMate colour picking", activecolor) as color|null + if(chosen_color) + activecolor = chosen_color + return TRUE + if("paint") + do_paint(usr) + temp = "Painted Successfully!" + return TRUE + if("drop") + temp = "" + drop_item() + return TRUE + if("clear") + inserted.remove_atom_colour(FIXED_COLOUR_PRIORITY) + playsound(src, 'sound/effects/spray3.ogg', 50, 1) + temp = "Cleared Successfully!" + return TRUE + if("set_matrix_color") + color_matrix_last[params["color"]] = params["value"] + return TRUE + if("set_hue") + build_hue = clamp(text2num(params["buildhue"]), 0, 360) + return TRUE + if("set_sat") + build_sat = clamp(text2num(params["buildsat"]), -10, 10) + return TRUE + if("set_val") + build_val = clamp(text2num(params["buildval"]), -10, 10) + return TRUE + + +/obj/machinery/gear_painter/proc/do_paint(mob/user) + var/color_to_use + switch(active_mode) + if(COLORMATE_TINT) + color_to_use = activecolor + if(COLORMATE_MATRIX) + color_to_use = rgb_construct_color_matrix( + text2num(color_matrix_last[1]), + text2num(color_matrix_last[2]), + text2num(color_matrix_last[3]), + text2num(color_matrix_last[4]), + text2num(color_matrix_last[5]), + text2num(color_matrix_last[6]), + text2num(color_matrix_last[7]), + text2num(color_matrix_last[8]), + text2num(color_matrix_last[9]), + text2num(color_matrix_last[10]), + text2num(color_matrix_last[11]), + text2num(color_matrix_last[12]), + ) + if(COLORMATE_HSV) + color_to_use = color_matrix_hsv(build_hue, build_sat, build_val) + color_matrix_last = color_to_use + if(!color_to_use || !check_valid_color(color_to_use, user)) + to_chat(user, SPAN_NOTICE("Invalid color.")) + return FALSE + inserted.add_atom_colour(color_to_use, FIXED_COLOUR_PRIORITY) + playsound(src, 'sound/effects/spray3.ogg', 50, 1) + return TRUE + + +/// Produces the preview image of the item, used in the UI, the way the color is not stacking is a sin. +/obj/machinery/gear_painter/proc/build_preview() + if(inserted) //sanity + var/list/cm + switch(active_mode) + if(COLORMATE_MATRIX) + cm = rgb_construct_color_matrix( + text2num(color_matrix_last[1]), + text2num(color_matrix_last[2]), + text2num(color_matrix_last[3]), + text2num(color_matrix_last[4]), + text2num(color_matrix_last[5]), + text2num(color_matrix_last[6]), + text2num(color_matrix_last[7]), + text2num(color_matrix_last[8]), + text2num(color_matrix_last[9]), + text2num(color_matrix_last[10]), + text2num(color_matrix_last[11]), + text2num(color_matrix_last[12]), + ) + if(!check_valid_color(cm, usr)) + return get_flat_icon(inserted, dir=SOUTH, no_anim=TRUE) + + if(COLORMATE_TINT) + if(!check_valid_color(activecolor, usr)) + return get_flat_icon(inserted, dir=SOUTH, no_anim=TRUE) + + if(COLORMATE_HSV) + cm = color_matrix_hsv(build_hue, build_sat, build_val) + color_matrix_last = cm + if(!check_valid_color(cm, usr)) + return get_flat_icon(inserted, dir=SOUTH, no_anim=TRUE) + + var/cur_color = inserted.color + inserted.color = null + inserted.color = (active_mode == COLORMATE_TINT ? activecolor : cm) + var/icon/preview = get_flat_icon(inserted, dir=SOUTH, no_anim=TRUE) + inserted.color = cur_color + temp = "" + + . = preview + +/obj/machinery/gear_painter/proc/check_valid_color(list/cm, mob/user) + if(!islist(cm)) // normal + var/list/HSV = ReadHSV(RGBtoHSV(cm)) + if(HSV[3] < minimum_normal_lightness) + temp = "[cm] is too dark (Minimum lightness: [minimum_normal_lightness])" + return FALSE + return TRUE + else // matrix + // We test using full red, green, blue, and white + // A predefined number of them must pass to be considered valid + var/passed = 0 +#define COLORTEST(thestring, thematrix) passed += (ReadHSV(RGBtoHSV(RGBMatrixTransform(thestring, thematrix)))[3] >= minimum_matrix_lightness) + COLORTEST("FF0000", cm) + COLORTEST("00FF00", cm) + COLORTEST("0000FF", cm) + COLORTEST("FFFFFF", cm) +#undef COLORTEST + if(passed < minimum_matrix_tests) + temp = "Matrix is too dark. (passed [passed] out of [minimum_matrix_tests] required tests. Minimum lightness: [minimum_matrix_lightness])." + return FALSE + return TRUE diff --git a/code/matrices/color_matrix.dm b/code/matrices/color_matrix.dm new file mode 100644 index 0000000000..1e2988ea9b --- /dev/null +++ b/code/matrices/color_matrix.dm @@ -0,0 +1,240 @@ +///////////////////// +// COLOUR MATRICES // +///////////////////// + +/* Documenting a couple of potentially useful color matrices here to inspire ideas. +// Greyscale - indentical to saturation @ 0 +list(LUMA_R,LUMA_R,LUMA_R,0, LUMA_G,LUMA_G,LUMA_G,0, LUMA_B,LUMA_B,LUMA_B,0, 0,0,0,1, 0,0,0,0) + +// Color inversion +list(-1,0,0,0, 0,-1,0,0, 0,0,-1,0, 0,0,0,1, 1,1,1,0) + +// Sepiatone +list(0.393,0.349,0.272,0, 0.769,0.686,0.534,0, 0.189,0.168,0.131,0, 0,0,0,1, 0,0,0,0) +*/ + +/// Does nothing. +/proc/color_matrix_identity() + return list(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0) + +/** + * Adds/subtracts overall lightness. + * 0 is identity, 1 makes everything white, -1 makes everything black. + */ +/proc/color_matrix_lightness(power) + return list(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1, power,power,power,0) + +/** + * Changes distance hues have from grey while maintaining the overall lightness. Greys are unaffected. + * 1 is identity, 0 is greyscale, >1 oversaturates colors. + */ +/proc/color_matrix_saturation(value) + var/inv = 1 - value + var/R = round(LUMA_R * inv, 0.001) + var/G = round(LUMA_G * inv, 0.001) + var/B = round(LUMA_B * inv, 0.001) + + return list(R + value,R,R,0, G,G + value,G,0, B,B,B + value,0, 0,0,0,1, 0,0,0,0) + +/** + * Exxagerates or removes colors. + */ +/proc/color_matrix_saturation_percent(percent) + if(percent == 0) + return color_matrix_identity() + percent = clamp(percent, -100, 100) + if(percent > 0) + percent *= 3 + var/x = 1 + percent / 100 + var/inv = 1 - x + var/R = LUMA_R * inv + var/G = LUMA_G * inv + var/B = LUMA_B * inv + + return list(R + x,R,R, G,G + x,G, B,B,B + x) + +/** + * Greyscale matrix. + */ +/proc/color_matrix_greyscale() + return list(LUMA_R, LUMA_R, LUMA_R, LUMA_G, LUMA_G, LUMA_G, LUMA_B, LUMA_B, LUMA_B) + +/** + * Changes distance colors have from rgb(127,127,127) grey. + * 1 is identity. 0 makes everything grey >1 blows out colors and greys. + */ +/proc/color_matrix_contrast(value) + var/add = (1 - value) / 2 + return list(value,0,0,0, 0,value,0,0, 0,0,value,0, 0,0,0,1, add,add,add,0) + +/** + * Exxagerates or removes brightness. + */ +/proc/color_matrix_contrast_percent(percent) + var/static/list/delta_index = list( + 0, 0.01, 0.02, 0.04, 0.05, 0.06, 0.07, 0.08, 0.1, 0.11, + 0.12, 0.14, 0.15, 0.16, 0.17, 0.18, 0.20, 0.21, 0.22, 0.24, + 0.25, 0.27, 0.28, 0.30, 0.32, 0.34, 0.36, 0.38, 0.40, 0.42, + 0.44, 0.46, 0.48, 0.5, 0.53, 0.56, 0.59, 0.62, 0.65, 0.68, + 0.71, 0.74, 0.77, 0.80, 0.83, 0.86, 0.89, 0.92, 0.95, 0.98, + 1.0, 1.06, 1.12, 1.18, 1.24, 1.30, 1.36, 1.42, 1.48, 1.54, + 1.60, 1.66, 1.72, 1.78, 1.84, 1.90, 1.96, 2.0, 2.12, 2.25, + 2.37, 2.50, 2.62, 2.75, 2.87, 3.0, 3.2, 3.4, 3.6, 3.8, + 4.0, 4.3, 4.7, 4.9, 5.0, 5.5, 6.0, 6.5, 6.8, 7.0, + 7.3, 7.5, 7.8, 8.0, 8.4, 8.7, 9.0, 9.4, 9.6, 9.8, + 10.0) + percent = clamp(percent, -100, 100) + if(percent == 0) + return color_matrix_identity() + + var/x = 0 + if (percent < 0) + x = 127 + percent / 100 * 127; + else + x = percent % 1 + if(x == 0) + x = delta_index[percent] + else + x = delta_index[percent] * (1-x) + delta_index[percent+1] * x//use linear interpolation for more granularity. + x = x * 127 + 127 + + var/mult = x / 127 + var/add = 0.5 * (127-x) / 255 + return list(mult,0,0, 0,mult,0, 0,0,mult, add,add,add) + +/** + * Moves all colors angle degrees around the color wheel while maintaining intensity of the color and not affecting greys. + * 0 is identity, 120 moves reds to greens, 240 moves reds to blues. + */ +// +// +/proc/color_matrix_rotate_hue(angle) + var/sin = sin(angle) + var/cos = cos(angle) + var/cos_inv_third = 0.333*(1-cos) + var/sqrt3_sin = sqrt(3)*sin + return list( + round(cos+cos_inv_third, 0.001), round(cos_inv_third+sqrt3_sin, 0.001), round(cos_inv_third-sqrt3_sin, 0.001), 0, + round(cos_inv_third-sqrt3_sin, 0.001), round(cos+cos_inv_third, 0.001), round(cos_inv_third+sqrt3_sin, 0.001), 0, + round(cos_inv_third+sqrt3_sin, 0.001), round(cos_inv_third-sqrt3_sin, 0.001), round(cos+cos_inv_third, 0.001), 0, + 0,0,0,1, + 0,0,0,0, + ) + +/** + * Moves all colors angle degrees around the color wheel while maintaining intensity of the color and not affecting whites. + * TODO: Need a version that only affects one color (ie shift red to blue but leave greens and blues alone) + */ +/proc/color_matrix_rotation(angle) + if(angle == 0) + return color_matrix_identity() + angle = clamp(angle, -180, 180) + var/cos = cos(angle) + var/sin = sin(angle) + + var/constA = 0.143 + var/constB = 0.140 + var/constC = -0.283 + return list( + LUMA_R + cos * (1-LUMA_R) + sin * -LUMA_R, LUMA_R + cos * -LUMA_R + sin * constA, LUMA_R + cos * -LUMA_R + sin * -(1-LUMA_R), + LUMA_G + cos * -LUMA_G + sin * -LUMA_G, LUMA_G + cos * (1-LUMA_G) + sin * constB, LUMA_G + cos * -LUMA_G + sin * LUMA_G, + LUMA_B + cos * -LUMA_B + sin * (1-LUMA_B), LUMA_B + cos * -LUMA_B + sin * constC, LUMA_B + cos * (1-LUMA_B) + sin * LUMA_B + ) + +/** + * These next three rotate values about one axis only. + * x is the red axis, y is the green axis, z is the blue axis. + */ +/proc/color_matrix_rotate_x(angle) + var/sinval = round(sin(angle), 0.001) + var/cosval = round(cos(angle), 0.001) + return list(1,0,0,0, 0,cosval,sinval,0, 0,-sinval,cosval,0, 0,0,0,1, 0,0,0,0) + +/proc/color_matrix_rotate_y(angle) + var/sinval = round(sin(angle), 0.001) + var/cosval = round(cos(angle), 0.001) + return list(cosval,0,-sinval,0, 0,1,0,0, sinval,0,cosval,0, 0,0,0,1, 0,0,0,0) + +/proc/color_matrix_rotate_z(angle) + var/sinval = round(sin(angle), 0.001) + var/cosval = round(cos(angle), 0.001) + return list(cosval,sinval,0,0, -sinval,cosval,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0) + +/** + * Builds a color matrix that transforms the hue, saturation, and value, all in one operation. + */ +/proc/color_matrix_hsv(hue, saturation, value) + hue = clamp(360 - hue, 0, 360) + + // This is very much a rough approximation of hueshifting. This carries some artifacting, such as negative values that simply shouldn't exist, but it does get the job done, and that's what matters. + var/cos_a = cos(hue) // These have to be inverted from 360, otherwise the hue's inverted + var/sin_a = sin(hue) + var/rot_x = cos_a + (1 - cos_a) / 3 + var/rot_y = (1 - cos_a) / 3 - 0.5774 * sin_a // 0.5774 is sqrt(1/3) + var/rot_z = (1 - cos_a) / 3 + 0.5774 * sin_a + + return list( + round((((1-saturation) * LUMA_R) + (rot_x * saturation)) * value, 0.01), round((((1-saturation) * LUMA_R) + (rot_y * saturation)) * value, 0.01), round((((1-saturation) * LUMA_R) + (rot_z * saturation)) * value, 0.01), + round((((1-saturation) * LUMA_G) + (rot_z * saturation)) * value, 0.01), round((((1-saturation) * LUMA_G) + (rot_x * saturation)) * value, 0.01), round((((1-saturation) * LUMA_G) + (rot_y * saturation)) * value, 0.01), + round((((1-saturation) * LUMA_B) + (rot_y * saturation)) * value, 0.01), round((((1-saturation) * LUMA_B) + (rot_z * saturation)) * value, 0.01), round((((1-saturation) * LUMA_B) + (rot_x * saturation)) * value, 0.01), + 0, 0, 0 + ) + +/** + * Returns a matrix addition of A with B. + */ +/proc/color_matrix_add(list/A, list/B) + if(!istype(A) || !istype(B)) + return color_matrix_identity() + if(A.len != 20 || B.len != 20) + return color_matrix_identity() + var/list/output = list() + output.len = 20 + for(var/value in 1 to 20) + output[value] = A[value] + B[value] + return output + +/** + * Returns a matrix multiplication of A with B. + */ +/proc/color_matrix_multiply(list/A, list/B) + if(!istype(A) || !istype(B)) + return color_matrix_identity() + if(A.len != 20 || B.len != 20) + return color_matrix_identity() + var/list/output = list() + output.len = 20 + var/x = 1 + var/y = 1 + var/offset = 0 + for(y in 1 to 5) + offset = (y-1)*4 + for(x in 1 to 4) + output[offset+x] = round(A[offset+1]*B[x] + A[offset+2]*B[x+4] + A[offset+3]*B[x+8] + A[offset+4]*B[x+12]+(y==5?B[x+16]:0), 0.001) + return output + +/** + * Assembles a color matrix, defaulting to identity. + */ +/proc/rgb_construct_color_matrix(rr = 1, rg, rb, gr, gg = 1, gb, br, bg, bb = 1, cr, cg, cb) + return list(rr, rg, rb, gr, gg, gb, br, bg, bb, cr, cg, cb) + +/** + * Assembles a color matrix, defaulting to identity. + */ +/proc/rgba_construct_color_matrix(rr = 1, rg, rb, ra, gr, gg = 1, gb, ga, br, bg, bb = 1, ba, ar, ag, ab, aa = 1, cr, cg, cb, ca) + return list(rr, rg, rb, ra, gr, gg, gb, ga, br, bg, bb, ba, ar, ag, ab, aa, cr, cg, cb, ca) + +/** + * Constructs a colored greyscale matrix. + * WARNING: Bad math up ahead. + */ +/proc/rgba_auto_greyscale_matrix(rgba_string) + // process rgb(a) + var/list/L1 = ReadRGB(rgba_string) + ASSERT(L1.len) + if(L1.len == 3) + return rgba_construct_color_matrix(0.39, 0.39, 0.39, 0, 0.5, 0.5, 0.5, 0, 0.11, 0.11, 0.11, 0, 0, 0, 0, 1, max(-0.5, (L1[1] - 255) / 255), max(-0.5, (L1[2] - 255) / 255), max(-0.5, (L1[3] - 255) / 255), 0) + else + // alpha + return rgba_construct_color_matrix(0.39, 0.39, 0.39, 0, 0.5, 0.5, 0.5, 0, 0.11, 0.11, 0.11, 0, 0, 0, 0, 0, max(-0.5, (L1[1] - 255) / 255), max(-0.5, (L1[2] - 255) / 255), max(-0.5, (L1[3] - 255) / 255), L1[4] / 255) diff --git a/code/modules/client/preference_setup/loadout/gear_tweaks.dm b/code/modules/client/preference_setup/loadout/gear_tweaks.dm index ed5306412b..f371fac6c1 100644 --- a/code/modules/client/preference_setup/loadout/gear_tweaks.dm +++ b/code/modules/client/preference_setup/loadout/gear_tweaks.dm @@ -40,7 +40,48 @@ /datum/gear_tweak/color/tweak_item(var/obj/item/I, var/metadata) if(valid_colors && !(metadata in valid_colors)) return - I.color = metadata + if(!metadata || (metadata == "#ffffff")) + return + if(istype(I)) + I.add_atom_colour(metadata, FIXED_COLOUR_PRIORITY) + else + I.color = metadata + +GLOBAL_DATUM_INIT(gear_tweak_free_matrix_recolor, /datum/gear_tweak/matrix_recolor, new) + +/datum/gear_tweak/matrix_recolor + +/datum/gear_tweak/matrix_recolor/get_contents(var/metadata) + if(islist(metadata) && length(metadata)) + return "Matrix Recolor: [english_list(metadata)]" + return "Matrix Recolor" + +/datum/gear_tweak/matrix_recolor/get_default() + return null + +/datum/gear_tweak/matrix_recolor/get_metadata(user, metadata) + var/list/returned = color_matrix_picker(user, "Pick a color matrix for this item", "Matrix Recolor", "Ok", "Erase", "Cancel", TRUE, 10 MINUTES, islist(metadata) && metadata) + var/list/L = returned["matrix"] + if(returned["button"] == 3) + return metadata + if((returned["button"] == 2) || !islist(L) || !ISINRANGE(L.len, 9, 20)) + return list() + var/identity = TRUE + var/static/list/ones = list(1, 5, 9) + for(var/i in 1 to L.len) + if(L[i] != ((i in ones)? 1 : 0)) + identity = FALSE + break + return identity? list() : L + +/datum/gear_tweak/matrix_recolor/tweak_item(obj/item/I, metadata) + . = ..() + if(!islist(metadata) || (length(metadata) < 12)) + return + if(istype(I)) + I.add_atom_colour(metadata, FIXED_COLOUR_PRIORITY) + else + I.color = metadata /* * Path adjustment diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index 797854a283..61d9c6020d 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -85,10 +85,12 @@ /proc/is_admin(var/mob/user) return check_rights(R_ADMIN|R_EVENT, 0, user) != 0 - +/** + * Moved into its own file as part of port from CHOMP. + * /proc/hsl2rgb(h, s, l) return //TODO: Implement - +*/ /* Miss Chance */ diff --git a/icons/system/blank_32x32.dmi b/icons/system/blank_32x32.dmi new file mode 100644 index 0000000000000000000000000000000000000000..4f5867e07e724a15ceb2638903a71ba2913faed2 GIT binary patch literal 209 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnL3?x0byx0z;m;-!5Tn`*LkmkKF1;}MA3GxeO zaCmkj4aiBU3W+FjNi9w;$}A|!%+F(BsF)KRR!~&>{Y!Ac$FEPcymhtCojD)8A=Kca z@q { + const { act, data } = useBackend(context); + const { activemode, temp } = data; + const item = data.item || []; + return ( + + +
+ {temp ? {temp} : null} + {Object.keys(item).length ? ( + <> + + +
+
Item:
+ +
+
+ +
+
Preview:
+ +
+
+
+ + + act('switch_modes', { + mode: 1, + }) + } + > + Tint coloring (Simple) + + + act('switch_modes', { + mode: 2, + }) + } + > + HSV coloring (Normal) + + + act('switch_modes', { + mode: 3, + }) + } + > + Matrix coloring (Advanced) + + +
Coloring: {item.name}
+ + +
+ + ) : ( +
No item inserted.
+ )} +
+
+
+ ); +}; + +export const ColorMateTint = (props, context) => { + const { act, data } = useBackend(context); + return ( + - - - - - - -
- {items.map((item, i) => ( - - #{i + 1}: {item} - - ))} -
- - )) || ( -
- No items inserted. -
- )} - - - ); -}; diff --git a/vorestation.dme b/vorestation.dme index 9a6c3310a0..e6fb7099fd 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -40,6 +40,7 @@ #include "code\__defines\chemistry.dm" #include "code\__defines\chemistry_vr.dm" #include "code\__defines\color.dm" +#include "code\__defines\color_priority.dm" #include "code\__defines\construction.dm" #include "code\__defines\cooldowns.dm" #include "code\__defines\crafting.dm" @@ -59,6 +60,7 @@ #include "code\__defines\lighting.dm" #include "code\__defines\lighting_vr.dm" #include "code\__defines\logging.dm" +#include "code\__defines\lum.dm" #include "code\__defines\machinery.dm" #include "code\__defines\map.dm" #include "code\__defines\materials.dm" @@ -137,6 +139,7 @@ #include "code\_helpers\global_lists.dm" #include "code\_helpers\global_lists_vr.dm" #include "code\_helpers\icons.dm" +#include "code\_helpers\icons_ch.dm" #include "code\_helpers\icons_vr.dm" #include "code\_helpers\lighting.dm" #include "code\_helpers\logging.dm" @@ -156,6 +159,7 @@ #include "code\_helpers\unsorted_vr.dm" #include "code\_helpers\view.dm" #include "code\_helpers\visual_filters.dm" +#include "code\_helpers\icons\flatten.dm" #include "code\_helpers\logging\ui.dm" #include "code\_helpers\sorts\__main.dm" #include "code\_helpers\sorts\comparators.dm" @@ -389,6 +393,7 @@ #include "code\datums\helper_datums\teleport.dm" #include "code\datums\helper_datums\teleport_vr.dm" #include "code\datums\helper_datums\topic_input.dm" +#include "code\datums\interfaces\appearance.dm" #include "code\datums\locations\locations.dm" #include "code\datums\locations\nyx.dm" #include "code\datums\locations\qerrvallis.dm" @@ -1693,6 +1698,7 @@ #include "code\game\turfs\unsimulated\walls.dm" #include "code\js\byjax.dm" #include "code\js\menus.dm" +#include "code\matrices\color_matrix.dm" #include "code\modules\admin\admin.dm" #include "code\modules\admin\admin_attack_log.dm" #include "code\modules\admin\admin_investigate.dm"