Luke Jones

chickenboard - commit 498829b

Initial Commit498829bb8991ea2077cbeaf7b470920b57da0f24LukeJ<152485046+LukePJ25@users.noreply.github.com>2024-12-09T19:03:27Z
diff --git a/index.html b/index.html new file mode 100644 index 0000000..ccbdf19 --- /dev/null +++ b/index.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<head> + <title>ChickenBoard</title> + <link rel="stylesheet" href="style.css"> + <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> +</head> +<body> + <div class="container" id="main_container"> + <h1>ChickenBoard</h1> + <div id="canvas_container"> + <canvas id="drawing_canvas" width="600" height="500"></canvas> + <canvas id="overlay_canvas" width="600" height="500"></canvas> + </div> + <div id="canvas_controls"> + <button class="control_button" onclick="clearCanvasRequest(gtx, '#fff')">Clear Canvas</button> + <button class="control_button" onclick="saveAsImage()">Save Image</button> + <button class="control_button" onclick="document.getElementById('fileInput').click()">Load Image</button> + <input type="file" id="fileInput" accept="image/*" style="display: none;" onchange="loadInImage()"> + <div class="vertical_separator"></div> + <div id="tools_container"> + <button aria-label="Brush" class="tool_button" id="button_brush" onclick="changeTool(0);"></button> + <button aria-label="Eraser" class="tool_button" id="button_eraser" onclick="changeTool(1);"></button> + <button aria-label="Pencil" class="tool_button" id="button_pencil" onclick="changeTool(2);"></button> + <button aria-label="Spray" class="tool_button" id="button_spray" onclick="changeTool(3);"></button> + <button aria-label="Line" class="tool_button" id="button_line" onclick="changeTool(4);"></button> + <button aria-label="Picker" class="tool_button" id="button_picker" onclick="changeTool(5);"></button> + </div> + <div class="vertical_separator"></div> + <div id="tools_settings_container"> + <div id="tool_size_selector_container"> + <label for="tool_size_selector">Size</label> + <input class="tool_setting" type="number" id="tool_size_selector" min="1" max="256" value="8"> + </div> + <div id="spray_settings_container"> + <button class="tool_button" id="spray_strength_1" onclick="sprayStrength(1);"></button> + <button class="tool_button" id="spray_strength_2" onclick="sprayStrength(2);"></button> + <button class="tool_button" id="spray_strength_3" onclick="sprayStrength(3);"></button> + </div> + </div> + <div class="vertical_separator"></div> + <div id="colour_container"> + <label for="colour_selector">Colour</label> + <input type="color" id="colour_selector" value="#000"> + </div> + </div> + <script src="js/drawing.js"></script> + </div> +</body> \ No newline at end of file diff --git a/js/drawing.js b/js/drawing.js new file mode 100644 index 0000000..25db3b9 --- /dev/null +++ b/js/drawing.js @@ -0,0 +1,465 @@ +/** + * The source for ChickenBoard + * Written by Luke Jones + * + * ██ ██ ██ ██ ██ ████████ ████████ ██ ██ ██ ██ ██ ██ ████████ ██ ██ ████████ ████████ + * ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ████ ██ ██ ████ ██ ██ ██ + * ██ ██ ██ ████ ████████ ██ ██ ██ ████ ██ ██ ██ ████ ████████ ██ ████ ████████ ██ + * ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + * ████████ ████████ ██ ██ ████████ ████████ ██ ██ ████████ ██ ██ ██ ████████ ██ ██ ██ ████████ ██ + * https://www.lukeonline.net/ChickenBoard + * V1.0 + */ + +/* +Math Functions + */ +const MathL = { + clamp: function (v, mn, mx) { + return Math.max(mn, Math.min(v, mx)); + }, + + // Check if value is in range + rangeCheck: function (v, mn, mx) { + return (v <= mx && v >= mn); + }, + + // Linear Interpolation + lerp: function (va, vb, ratio) { + return va + ratio * (vb - va); + } +} + +// Initialise Program +const canvas = document.getElementById("drawing_canvas"); +const overlay = document.getElementById("overlay_canvas"); +const gtx = canvas.getContext("2d"); +const otx = overlay.getContext("2d"); +const canv_width = canvas.width, canv_height = canvas.height; +const size_selector = document.getElementById("tool_size_selector"); +const size_selector_container = document.getElementById("tool_size_selector_container"); +const spray_strength_selector = document.getElementById("spray_settings_container"); +const colour_selector = document.getElementById("colour_selector"); +let isDrawingLine = false, lineA = [], lineB = []; +let canvasBounds; + +// Initialise usage Variables +const tools = { // Tool types + BRUSH: 'BRUSH', + ERASER: 'ERASER', + PENCIL: 'PENCIL', + SPRAY: 'SPRAY', + LINE: 'LINE', + PICKER: 'PICKER', + FILL: 'FILL' +} +let toolSize = 8; // Tool size, in pixels +let primaryColour = "#000"; // Current selected colour +let secondaryColour = "#fff"; // Secondary colour (unused) +let spray_strength = 1; // Spray strength, either 1, 2 or 3 + +let mouseHeld = false; // Is mouse held? +let mouseClicked = false; // Has mouse just been clicked? +let mousepos = [0,0]; // Current mouse position (x, y) +let prevMouseMos = [0,0]; // Mouse position in the last frame (x, y) + +/* + Initialise board appearance + */ +clearCanvas(gtx, "#fff"); +clearCanvas(otx, "rgba(0,0,0,0)"); + +/* + Checks to see the mouse and engine state before + beginning the action chain, if needed. + */ +function runMotion() { + // Make sure tool size is up to date + toolSize = MathL.clamp(size_selector.value,1,256); + setPrimaryColour(colour_selector.value); + updateOverlay(); + + if(mouseHeld) { + if(mouseInCanvas()){ + startAction(); + } + } +} + +/* + Executes the beginning of the "action" chain where what happens under the users + pointer is determined by current values, and is executed accordingly. + Called when mouse is pressed + */ +function startAction() { + if (currentTool === tools.LINE) { + if (!mouseClicked) return; + + if (!isDrawingLine) { + isDrawingLine = true; + lineA = [...mousepos]; + } else { + isDrawingLine = false; + lineB = [...mousepos]; + + gtx.beginPath(); + gtx.lineWidth = toolSize; + gtx.lineCap = "round"; + gtx.moveTo(lineA[0], lineA[1]); + gtx.lineTo(lineB[0], lineB[1]); + gtx.stroke(); + + lineA = []; + lineB = []; + } + mouseClicked = false; + return; + } + + if (currentTool === tools.ERASER) { + gtx.fillStyle = secondaryColour; + gtx.strokeStyle = secondaryColour; + } else { + gtx.fillStyle = primaryColour; + gtx.strokeStyle = primaryColour; + } + + function act() { + switch (currentTool) { + case tools.PENCIL: + gtx.fillRect(mousepos[0] - toolSize / 2, mousepos[1] - toolSize / 2, toolSize, toolSize); + break; + case tools.ERASER: + case tools.BRUSH: + gtx.beginPath(); + gtx.lineWidth = toolSize; + gtx.lineCap = "round"; + gtx.moveTo(prevMouseMos[0], prevMouseMos[1]); + gtx.lineTo(mousepos[0], mousepos[1]); + gtx.stroke(); + break; + case tools.SPRAY: + for (let s = 0; s <= Math.pow(spray_strength, 3); s++) { + let dist = Math.random() * (toolSize / 2); + let ang = Math.random() * 2 * Math.PI; + let dot = [ + mousepos[0] + dist * Math.cos(ang), + mousepos[1] + dist * Math.sin(ang), + ]; + gtx.fillRect(dot[0], dot[1], 1, 1); + } + break; + case tools.PICKER: + setColourPicker(rgbaToHex(getColorAt(mousepos[0], mousepos[1]))); + break; + default: + break; + } + } + act(); + mouseClicked = false; +} + +/* +Queries the user before clearing the canvas. +- context is the canvas context to clear +- col is the colour to clear it with + */ +function clearCanvasRequest(context, col) { + if(confirm("Clear the Canvas? This cannot be undone!")) { + clearCanvas(context, col); + } +} + +/* +Clearing the given canvas with the given colour. +- context is the canvas context to clear +- col is the colour to clear it with. If transparent (rgba(0,0,0,0)) it will just wipe it clear. + */ +function clearCanvas(context, col) { + context.fillStyle = col; + if(col === "rgba(0,0,0,0)") { + context.clearRect(0,0,canv_width,canv_height); + } else { + context.fillRect(0,0,canv_width,canv_height); + } +} + +/* +Updates the overlay canvas when called. Used for things like paint +previews, brush position, e.g. + */ +function updateOverlay() { + clearCanvas(otx, "rgba(0,0,0,0)") + otx.strokeStyle = "#666"; + otx.lineWidth = 2; + switch (currentTool) { + case "ERASER": + case "BRUSH": + case "SPRAY": + otx.beginPath(); + otx.arc(mousepos[0], mousepos[1], toolSize/2, 0, 2 * Math.PI); + otx.stroke(); + break; + case "PENCIL": + otx.strokeRect(mousepos[0]-(toolSize/2), mousepos[1]-(toolSize/2), toolSize, toolSize); + break; + case "LINE": + if(isDrawingLine){ + otx.lineWidth = toolSize; + otx.strokeStyle = "rgba(128,128,128,0.5)"; + otx.beginPath(); + otx.lineCap = "round"; + otx.moveTo(lineA[0],lineA[1]); + otx.lineTo(mousepos[0],mousepos[1]); + otx.stroke(); + otx.strokeStyle = "#666"; + otx.lineWidth = 2; + } + case "PICKER": + case "FILL": + otx.beginPath(); + otx.moveTo(mousepos[0]-12, mousepos[1]); + otx.lineTo(mousepos[0]+12, mousepos[1]); + otx.stroke(); + otx.beginPath(); + otx.moveTo(mousepos[0], mousepos[1]-12); + otx.lineTo(mousepos[0], mousepos[1]+12); + otx.stroke(); + break; + default: + break; + + } +} + +/* +Updates the sprayStrength to a set value +- str is the new spray strength, either 1, 2 or 3 - weak, light and heavy respectively + */ +function sprayStrength(str) { + spray_strength = MathL.clamp(str,1,3); +} + +/* + Returns array [R,G,B,A] of pixel at xy + - x position of the pixel to query + - y position of the pixel to query + */ +function getColorAt(x, y) { + const data = gtx.getImageData(x, y, 1, 1).data; + return { r: data[0], g: data[1], b: data[2], a: data[3] }; +} +/* +Returns an rgba value as a hex colour value. +- rgba is the colour to convert to hex. + */ +function rgbaToHex(rgba) { + const { r, g, b } = rgba; + const toHex = (value) => value.toString(16).padStart(2, '0'); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; +} + +/* +Checks to see if two colours match. + */ +function colorsMatch(c1, c2) { + return c1.r === c2.r && c1.g === c2.g && c1.b === c2.b && c1.a === c2.a; +} + +/* +Sets the colour picker selector to a given colour value +- colour is the colour to set the colour picker to. + */ +function setColourPicker(colour){ + colour_selector.value = colour; +} + +/* +Sets the primary colour to a given colour value +- colour is the colour to set the primary colour to. + */ +function setPrimaryColour (colour) { + primaryColour = colour; +} + +/* +- - UNUSED +Starts the fill algorithm +- x is the x position of the pixel to fill from +- y is the y position of the pixel to fill from + */ +function startFill(x, y) { + let startColor = getColorAt(x, y); + let visited = new Set(); + let queue = []; + queue.push([x, y]); + while (queue.length > 0) { + let [startX, startY] = queue.shift(); + if (visited.has(`${startX},${startY}`)) continue; + visited.add(`${startX},${startY}`); + if (!MathL.rangeCheck(startX, 0, canv_width) || !MathL.rangeCheck(startY, 0, canv_height)) continue; + let currentColor = getColorAt(startX, startY); + if (!colorsMatch(currentColor, startColor)) continue; + gtx.fillRect(startX, startY, 1, 1); + let left = startX; + while (left >= 0 && colorsMatch(getColorAt(left, startY), startColor)) { + visited.add(`${left},${startY}`); + gtx.fillRect(left, startY, 1, 1); + left--; + } + let right = startX + 1; + while (right < canv_width && colorsMatch(getColorAt(right, startY), startColor)) { + visited.add(`${right},${startY}`); + gtx.fillRect(right, startY, 1, 1); + right++; + } + if (startY - 1 >= 0) { + let newLeft = left + 1; + while (newLeft < right) { + if (colorsMatch(getColorAt(newLeft, startY - 1), startColor) && !visited.has(`${newLeft},${startY - 1}`)) { + queue.push([newLeft, startY - 1]); + } + newLeft++; + } + } + if (startY + 1 < canv_height) { + let newLeft = left + 1; + while (newLeft < right) { + if (colorsMatch(getColorAt(newLeft, startY + 1), startColor) && !visited.has(`${newLeft},${startY + 1}`)) { + queue.push([newLeft, startY + 1]); + } + newLeft++; + } + } + } +} + +/* +Sets the tool to the given tool value and updates the interface and +functionality accordingly +- tool is the tool to set the currentTool to. + */ +function changeTool(tool) { + size_selector_container.style.display = "none"; + spray_strength_selector.style.display = "none"; + $(".tool_button").css = ""; + let selected; + switch (tool) { + case 0: + currentTool = tools.BRUSH; + size_selector_container.style.display = "inline-block"; + selected = document.getElementById("button_brush"); + break; + case 1: + currentTool = tools.ERASER; + size_selector_container.style.display = "inline-block"; + selected = document.getElementById("button_eraser"); + break; + case 2: + currentTool = tools.PENCIL; + size_selector_container.style.display = "inline-block"; + selected = document.getElementById("button_pencil"); + break; + case 3: + currentTool = tools.SPRAY; + size_selector_container.style.display = "inline-block"; + spray_strength_selector.style.display = "inline-block"; + selected = document.getElementById("button_spray"); + break; + case 4: + currentTool = tools.LINE; + size_selector_container.style.display = "inline-block"; + selected = document.getElementById("button_line"); + break; + case 5: + currentTool = tools.PICKER; + selected = document.getElementById("button_picker"); + break; + /*case 6: + currentTool = tools.FILL; + break;*/ + default: + break; + } + selected.css = "border: '2px inset #bbccff'; borderRadius: '4px'; background: '#779'"; +} + +/* +Loads any bitmap image into the canvas + */ +function loadInImage() { + const fileInput = document.getElementById('fileInput'); + const file = fileInput.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = function (e) { + const img = new Image(); + img.src = e.target.result; + img.onload = function () { + gtx.drawImage(img, 0,0,canv_width,canv_height); + }; + + }; + + reader.readAsDataURL(file); + } +} + +/* +Provides a download dialog for the current canvas viewport + */ +function saveAsImage() { + if (canvas) { + const dataURL = canvas.toDataURL("image/png"); + const link = document.createElement("a"); + link.href = dataURL; + link.download = "canvas.png"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } else { + console.error("Canvas element not found!"); + } +} + +/* + Check to ensure the mouse is inside the canvas + */ +function mouseInCanvas() { + return ( + MathL.rangeCheck(mousepos[0], 0, canv_width) && + MathL.rangeCheck(mousepos[1], 0, canv_height) + ); +} + +/* + Update mouse variables on movement + */ +document.onmousemove = function (event) { + prevMouseMos = mousepos; + canvasBounds = canvas.getBoundingClientRect(); + mousepos = [Math.trunc(event.clientX - canvasBounds.left), Math.trunc(event.clientY - canvasBounds.top)]; + + runMotion(); + if (mouseHeld && mouseInCanvas()) { + startAction(); + } +}; +document.onmousedown = function (event) { + mouseHeld = true; + mouseClicked = true; + prevMouseMos = mousepos; + canvasBounds = canvas.getBoundingClientRect(); + mousepos = [Math.trunc(event.clientX - canvasBounds.left), Math.trunc(event.clientY - canvasBounds.top)]; + if (mouseInCanvas()) { + startAction(); + } +}; +document.onmouseup = function () { + mouseHeld = false; +}; + + +changeTool(0); + diff --git a/res/res.gif b/res/res.gif new file mode 100644 index 0000000000000000000000000000000000000000..9520f32ffb855939e316144b8aa37d3900120344 GIT binary patch literal 1841 zcmV-12hR9MNk%w1VE_R@0B`^RA^8LW000>PEC2ui0003%0E7Sl0RLW*f?PvXG#>zz z|1dBxA7G9^KtTWh{{TvVfPjFYpr8*SFaOv8z`(!&BtOiY0AF8UmzS6S0094!w*deD zKO__X)30D)U;qFB00000000000000000000000I5U?z@aX^uduo@}PRKr7EAyrOVz z=XCG?r`?Ta0~(LNVseLEDn(D1aM-LKr?Fnt%5?$)ffrE)28+9;mq0KWq;v$rIaR3J z>u<E)WHb2xc``2t2WNYSX=GtkEH7n?fqQCfZdp(OadQB7caex_eSdzQK`MlXpOT7? zq&g{J2(buej8&6uYgm~ub+QO}wOvA20HO)U$gPQ|0L-a~j*p9iioUeU#3^gI0=ja% z1-`)8woFDT#>mIym(0%3pehOq3k%nliLU{^){yww0JaUt1!I@NZ=4bqV231yFNS<n zs5{1h0mBsT3jKrE<i)&t_Zn5>7b|2rZIlTAzBrg-mqJbu&jEP2aZHMd2`XM}rtuxe zjw%{7s5cLlt1tUV@-qqK*1&EzgzTbIpq^E$!1UOl*;IuC2lF00&>3u5PmMoM0uV}6 zfl(a#lrHrLP@u{zQom8XdNpgj7hJu34LjCiSsfW;radYY0tBINFU6Isl<5qocSTNB z@v>7*nJKPrY4KvPf(Q{(koAF4qXY?&7mG~M_;G{S(I{7@t3`!fsLERE<=ok?l^3FU z*aTo2wQ9qxd8T+BTQ=hcjv<%Lt(!8f<-e;-(Nt%G`SWp%gH};*G_@4XQ=5xugV5`F z)DK&KQCoZPk=^aK6H45Orvz2!p+|uKdR-y6fP18Q1=eZuK`4ZM65t1$erfTCoo%=& zLIDM^1UMHtm;v|M809Hw9)rDwW|ereF*l)e9F7M-1w8h6-#kF(rx-Lredyn3(TrH4 ziL!hbA%RcP7-5U%#qr`oD?SN8azjdH<2*PHxMPnz{s^Q3LT34lhuPhxpFShTNn%m~ zT6R=;Ia1T2mFL0u-j)w4C)kY@s)+-cWo8Iu9A~C!osRvvi6T;RDoJN|cES}<N-7?= zNdZ+7kf5JhwkK$G@DVEIC5TpFrUHxxxnY-TMJgqnm0GG>rZ&t|V4jzK%IBz9E+^xv zrKAdGp<R(#$C+_NOF*Nu8Y)cxk!{{eX|8kb%G*&X_!=d!dKNHjpT!32AhPs*Ng=yZ zI9mX;(oQQYya3q>Y?9nA+2ps0SRnAgMUfjUl?hHjs+U|^o2<KGI;;bkRMl(mnHtjC z;95pX>M*~#`dbpfIt)y(TLzb#FuIe<XsX6K#)ogbJyu*V#`j9Rp1!n#Txp4!mTa;G zGpoF^!s&twD1_0;obsVHOO!yYIFF<A#ynrGBN*IvI}6DH4_)+>pdJmQ4KF{fgVQjl zK(W+RV{G*&Y=Wq^rd;<6_EAF<t@WS9&5Z+1Fdwz-*sMnV!n1N?S-83!XM4Aod565W zrhPx&Zs3mN<B%+fYkK(q<TUeW1LJ23PB#D-w0^;nT8S8E(3<)paOe@$@zw6U@2=kG zv1qPi!MN*aI$p%hqk1YSxc<7wvEyo~fhySUx9~dX-uw2y&jkA4`2avX>0Tc1CF9Gl z-uwl{x64Y7z<-Z@_?)9M3>PV2fI<6-n6GL2%u_+@_;-}|?Sv!*{9oYgH=lCoFM)}R z#Ns;Fp$4k&R9a%%U$8(vFU+r9q|o3Y?zbijW)OoT<RAo3Si-l6@KG>~p$un8!y0M@ z3ed2j4tGdH@HNE+IQ&}-R$z%C`jCf6Od<*z;i)A$F$tPt#1WN9#VQ^Lhd>mD6m*1_ zCtC4~U|f?I(-6d<F@kVBiP+)?!^lQ>Xlx@!MBWh1sHrFhVvbpCqaMN7MIvl5A6zul zFjT0<JsMJnq!YtGh&V?)rZERZ1R@irc*sg>AqdgKqL+Ge2}w$@lWVl3C@+b{tM##n zo0QlP6qyG`j`EfJLDD6=2uUYW;f^Y7WgKD2OHeQ|kC+UmWYG8qLE<2gB>ZD9mw83c z=|Gr8)TJYV$qrmH@_wOY|E3<bX01DF5|XYYrWz+{j%;c(j4KL;ENf85Vm4Bj;llzK z|7gy0e({u&#9vgTiON2*6Pv5dCnfD^$W!9epQIdTF}s4y`2>`p5GBns(<z8lE^;S< z^rRw(S<i^JF`B?!LO$o&%{bB$n;uOm88yk$bCmL(24!bEk2!^ms`RGE6s8&f69rki zvz0cT15R)1Q*O%Ck;toPIKes3p?cJa(4^xpU9r-TY80t89jH^$DM%lB4v(mWTrRgt z)~cqkqR?EW9K%}HWyXZ9F70GXnFz-$HgcqG-RdIa;Zh{IQ%8A)=vTFh$$_FYu!!vl fAFlP&C3ZBif<@~U4JJBQ$Xd3BaJ?)nLjV9fKI~dT literal 0 HcmV?d00001 diff --git a/style.css b/style.css new file mode 100644 index 0000000..0e41c21 --- /dev/null +++ b/style.css @@ -0,0 +1,131 @@ +html { + background-color: #203450; + font-size: 14px; + font-family: Arial, "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + color: #000000; +} + +.container { + background-color: #5e8ad0; + margin: auto; + padding: 20px; + border: 3px outset #6a99e8; + border-radius: 20px; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); +} + + +#image_input_container { + width: auto; + margin: auto; + z-index: 3; + display: none; + position: absolute; +} + +#main_container { + width: 600px; +} + +canvas { + position: absolute; + margin: auto; + border: 3px inset #6a99e8; + border-radius: 8px; +} +#canvas_container { + padding-bottom: 510px; +} + +#drawing_canvas { + z-index: 1; + image-rendering: pixelated; +} +#overlay_canvas { + z-index: 2; + background-color: rgba(0, 0, 0, 0); +} +#overlay_canvas:hover { + cursor: none; +} + +#canvas_controls { + display: inline-block; + width: 100%; +} + +button { + border: 2px outset #bce; + border-radius: 4px; + background-color: #bce; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.4); + font-family: inherit; +} +button:hover { + border: 2px outset #cdf; + background-color: #ddf; +} +button:active { + border: 2px inset #68c; + background-color: #68c; +} + +.control_button { + padding: 2px 4px; +} +.tool_button { + width: 32px; + height: 32px; + padding: 16px; + background-size: cover; + background-repeat: no-repeat; + image-rendering: pixelated; +} +.tool_button#button_pencil { + background: #bce url("res/res.gif") 0 0; +} +.tool_button#button_eraser { + background: #bce url("res/res.gif") -32px 0; +} +.tool_button#button_brush { + background: #bce url("res/res.gif") -64px 0; +} +.tool_button#button_spray { + background: #bce url("res/res.gif") -96px 0; +} +.tool_button#button_line { + background: #bce url("res/res.gif") -130px 0; +} +.tool_button#button_picker { + background: #bce url("res/res.gif") -162px 0; +} +.tool_button#button_fill { + background: #bce url("res/res.gif") -164px 0; +} +.tool_button#spray_strength_1 { + background: #bce url("res/res.gif") 0 -32px; +} +.tool_button#spray_strength_2 { + background: #bce url("res/res.gif") -32px -32px; +} +.tool_button#spray_strength_3 { + background: #bce url("res/res.gif") -64px -32px; +} +.tool_setting { + width: 64px; + font-family: inherit; +} + +h1 { + color: #fff; + text-shadow: #212121 2px 2px 4px; +} + +.vertical_separator { + margin: 4px 0; + height: 1px; + width: 100%; + border: 3px outset #6a99e8; + border-radius: 4px; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.4); +} \ No newline at end of file