/
githubmirror
/
vgstation13
Обзор
Документация
Войти
/
githubmirror
/
vgstation13
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
Bleeding-Edge
code/modules/html_interface/paintTool/paintTool.js
559 строк
16 KB
DeityLink
The Paint & Linen Update [Splinter 4]: Painting Brush, Palette, Canvas, and Paintings changes (#35530)
11 дек 2023, 02:53
Не верифицирован
11 дек 2023, 02:53
0f9180b
Код
Авторство
О чём код?
/** * #### Painting tool #### * * A fairly simple painting tool, don't expect too many bells and whistles. * * === Requirements === * * Your html document must contain the following: * * - 'Width': An <input> with id="width". This will determine how many pixels wide our drawing will be * * - 'Height': An <input> with id="width". This will determine how many pixels tall our drawing will be * * - 'Bitmap': An <input> with id="bitmap". It's value must be a comma separated list of colors, with no * spaces (eg: "#0000ff,#ff0000,#ff0000,#0000ff"). It must contain 'Width'x'Height' colors, * as each color will be mapped to a pixel in our drawing. It's value will be updates as the * user draws. * * - 'Canvas': A <canvas> with id="canvas", and the events onmousedown="is_mouse_down = true;" and * onmousemove="draw_on_bitmap();". * This will be where the user will draw and see the result. It's width and height attributes * should be a multiple of 'Width' and 'Height', or you'll get some visual artifacts. * * - 'Tool Strength': An <input> with id="paint_opacity". It's value must be between 1 and 0 inclusive, and * represents the color's "alpha value", aka how much of an effect it has in changing a * pixel's color. * * - 'Min. Strength': An <input> with id="minPaintStrength". It's value must be between 1 and 0 inclusive, * and serves to limit 'Tool Strength' * * - 'Max. Strength': An <input> with id="maxPaintStrength". It's value must be between 1 and 0 inclusive, * and serves to limit 'Tool Strength' * * Inputs may be of type="hidden" as needed: eg. 'Bitmap' should usually be a hidden input, unless you'd * like for the user to import and export it's value freely. * * === Functions === * * --- init() --- * * Must be called as soon as all data is loaded and in place. Will load the html data (all those <input>s in the * requirements section) into the script. * * * --- setColor(color) --- * * There must be an element with id="current_color" for this function to be called. * Calling this function will change the color in use to the specified color, which must be a string in hex * format (eg: "#ffaa22"), and will update the 'current_color' element's background to said color. * If you'd rather forego this, you may use 'paint_color = "#ffaa22";' instead, at your own peril. * * * --- setOpacity() --- * * Updates the tool strenght to that of the 'Tool Strenght' input, sanitizing it's value in the process. Turns * NaNs to 0, rounds the value to the second decimal and clamps it to the min. and max. values. * Useful if you wish to let the user modify the strength directly. * * --- hexToRgba(hex), rgbaToHex(r, g, b) --- * * Helper functions to deal with rgb/hex conversions. * 'hexToRgba(hex)' takes eg. an "#aa88ff" string and returns an {r:170, g:136, b:255} object. * 'rgbaToHex(r, g, b)' takes three integers ranging from 0 to 255 and returns the corresponding string, eg. "#aa88ff" */ //Canvas and context for the player to draw in var canvas; var ctx; //Define our "bitmap" var width; var height; var bitmap; var nanomap; var nanopaint = 0; var polarized = false; //Keep track of how scaled up the canvas is vs the actual bitmap var scaleX = 20; var scaleY = 20; //Keep track of what the mouse is up to var previousX = -1; var previousY = -1; var is_mouse_down = false; //Color and tool data var paint_color = "#000000"; var paint_opacity = 0.5; var minPaintStrength = 0; var maxPaintStrength = 1; //Other options var grid_enabled = false; // Milliseconds to wait since we last moved the mouse in ordder to draw. // Ensures all browsers draw lines with the same opacity, instead of letting those that check more often drawing darker lines const PAINT_TOOL_MOVEMENT_THROTTLE = 10; /** * Initialize the script */ function initPaint(initData) { initData = JSON.parse(initData); canvas = document.getElementById("canvas"); ctx = canvas.getContext("2d"); width = initData.width; height = initData.height; canvas.width = width * scaleX; canvas.height = height * scaleY; bitmap = initData.bitmap; nanomap = initData.nanomap; nanopaint = initData.nanopaint; polarized = initData.polarized; minPaintStrength = initData.minPaintStrength; maxPaintStrength = initData.maxPaintStrength; setOpacity(maxPaintStrength); //No data? start with a blank canvas if (bitmap.length != width * height) { while (bitmap.length < width * height) { bitmap.push("#ffffff"); nanomap.push("#000000"); } } // Listener to catch any mouseup events to stop drawing window.addEventListener('mouseup', function(event){ end_path(); }) //Everything initialized, display the bitmap to the user for the first time. display_bitmap(); } /** * Sets the current color to the specified color, updating the "selected color" display */ function setPaintColor(color) { paint_color = color; } function setNanoPaint(nano) { nanopaint = nano; } function getPaintColor() { return paint_color; } function setOpacity(opacity) { paint_opacity = isNaN(opacity) ? 0 : opacity; paint_opacity = Math.round(paint_opacity*100)/100; paint_opacity = Math.min(Math.max(paint_opacity, minPaintStrength), maxPaintStrength); return paint_opacity; } function getOpacity() { return paint_opacity; } /** * Convert hex to RGBA objects. * Helper function, converts an hex string (eg: #AA88FF or #AA88FFFF) to an * {r:170, g:136, b:255, a:255} object. * If the alpha component is missing it'll be treated as FF */ function hexToRgba(hex) { //Pad with alpha component if missing hex = hex.slice(1); if (hex.length < 8) hex += "FF"; //Get rid of '#' hex = parseInt(hex, 16); //Bitwise magic return { r: (hex >> 24) & 255, g: (hex >> 16) & 255, b: (hex >> 8) & 255, a: hex & 255 }; } /** * Convert RGBA to hex. * Helper function, converts {r, g, b, a} objects (eg: {r:170, g:136, b:255, r:170}) to an * hex string (eg: #AA88FFAA). * If the alpha component is FF, it will be omitted on the hex */ function rgbaToHex(rgba_raw) { let rgba = { r: rgba_raw.r, g: rgba_raw.g, b: rgba_raw.b, a: rgba_raw.a, } for (k in rgba) { //Convert to hex value rgba[k] = Math.round(rgba[k]).toString(16); //Pad with 0 if needed rgba[k] = rgba[k].length > 1 ? rgba[k] : "0" + rgba[k]; } //Put it together return "#" + rgba.r + rgba.g + rgba.b + (rgba.a != "ff" ? rgba.a : "") } /* *-------------------------------------------------------------------------------------------------- * */ var blendFunction = colorRybBlend; /** * Draw a pixel into the bitmap. * Given 'rgba' as an hex string (eg: #AA88FF) and 'a' (alpha) as a 0-1 value, will mix said color * with whatever's on the specified pixel on the bitmap. */ function pixelDraw(x, y, rgba, alpha) { //Figure out the pixel index off the x and y let pixel = y * width + x; let painthex = rgba //Convert to numeric values rgba = hexToRgba(rgba); let orgba = hexToRgba(bitmap[pixel]); //In case we're using nano paint, let's keep track of the colour we're using let paint = rgba; let nanorba = hexToRgba(nanomap[pixel]); //Mix both color values if (nanopaint > 1)//nano paint is additive. radium blends normally. rgba = colorAdditiveBlend(rgba, orgba, alpha); else rgba = blendFunction(rgba, orgba, alpha); //Save result into bitmap bitmap[pixel] = rgbaToHex(rgba); //If we're painting with nano paint, we draw on the nanomap as well if (nanopaint > 0) { paint = colorAdditiveBlend(paint, nanorba, alpha);//Nano-Paint is additive if ((painthex == "#FFFFFF") || (painthex == "#ffffff")) nanomap[pixel] = "#FEFEFE";//workaround because Byond else nanomap[pixel] = rgbaToHex(paint); } else { //Otherwise, we erase the nanomap (by drawing on it in black) nanomap[pixel] = rgbaToHex(blendFunction(hexToRgba("#000000"), hexToRgba(nanomap[pixel]), alpha)); } } /* ------------------------------------------------------------------- Color blends Mostly based on: https://www.w3.org/TR/compositing-1/#valdef-blend-mode-hard-light */ function colorAlphaBlend(c1, c2, alpha) { var result = {}; for (k in c1) result[k] = Math.round(Math.sqrt(alpha * Math.pow(c1[k], 2) + (1-alpha) * Math.pow(c2[k], 2))); return result; } function colorMultiplyBlend(c1, c2, alpha) { var result = {}; for (k in c1) result[k] = Math.round(c1[k]*c2[k]/255); return colorAlphaBlend(result, c2, alpha); } //c1 is the new color, c2 is the canvas function colorAdditiveBlend(c1, c2, alpha) { var result = {}; for (k in c1) if (c2[k] >= c1[k]) result[k] = c2[k] else result[k] = Math.round(c2[k] + (c1[k] - c2[k]) * alpha); return result; } function colorScreenBlend(c1, c2, alpha) { var result = {}; for (k in c1) result[k] = Math.round(c1[k] + c2[k] - c1[k]*c2[k]/255); return colorAlphaBlend(result, c2, alpha); } function colorHardLightBlend(c1, c2, alpha) { var result = {r: c1.r * 2, g: c1.g * 2, b: c1.b * 2}; for (var c in c1) { if (c1[c] <= 127.5) { result[c] = Math.round(result[c]*c2[c]/255); } else { result[c] -= 255; result[c] = Math.round((result[c] + c2[c] - result[c]*c2[c]/255)); } } return colorAlphaBlend(result, c2, alpha); } function colorOverlayBlend(c1, c2, alpha) { return colorHardLightBlend(c2, c1, alpha); } function colorRybBlend(c1, c2, alpha) { var c1Ryb = rgbToRyb(c1); var c2Ryb = rgbToRyb(c2); var resultRyb = {r:0, y:0, b:0, a:Math.min(255.0, c1Ryb.a*alpha + c2Ryb.a)}; alpha *= c1Ryb.a / 255.0; resultRyb.r = Math.round(alpha * c1Ryb.r + (1-alpha) * c2Ryb.r); resultRyb.y = Math.round(alpha * c1Ryb.y + (1-alpha) * c2Ryb.y); resultRyb.b = Math.round(alpha * c1Ryb.b + (1-alpha) * c2Ryb.b); return rybToRgb(resultRyb); } /*--------------------------------------------- end color blends */ /** * RGB to RYB (red, yellow, blue) converter * Takes a RGB color object such as {r:40, g:15, b:90} and returns an RYB object such * as {r:215, y:165, b:240}. * Formula based on the following papers: * - http://nishitalab.org/user/UEI/publication/Sugita_IWAIT2015.pdf * - http://nishitalab.org/user/UEI/publication/Sugita_SIG2015.pdf */ function rgbToRyb(rgb) { // Soon-to-be result var ryb = {r:0, y:0, b:0, a:rgb.a}; // Make a copy of the input to work on var tmpRgb = {r: rgb.r, g: rgb.g, b: rgb.b}; // Remove white component var i = Math.min(rgb.r, rgb.g, rgb.b); tmpRgb.r -= i; tmpRgb.g -= i; tmpRgb.b -= i; // Convert colors ryb.r = tmpRgb.r - Math.min(tmpRgb.r, tmpRgb.g); ryb.y = (tmpRgb.g + Math.min(tmpRgb.r, tmpRgb.g))/2; ryb.b = (tmpRgb.b + tmpRgb.g - Math.min(tmpRgb.r, tmpRgb.g))/2; // Normalize var n = Math.max(ryb.r, ryb.y, ryb.b)/Math.max(tmpRgb.r, tmpRgb.g, tmpRgb.b); if (n > 0.000001) { // Should be zero, but floating point error could be an issue ryb.r /= n; ryb.y /= n; ryb.b /= n; } // Add black component, and round floating point errors i = Math.min(255 - rgb.r, 255 - rgb.g, 255 - rgb.b); ryb.r = Math.round(ryb.r + i); ryb.y = Math.round(ryb.y + i); ryb.b = Math.round(ryb.b + i); return ryb; } /** * RYB (red, yellow, blue) to RGB converter * Takes a RYB color object such as {r:215, y:165, b:240} and returns an RGB object such * as {r:40, g:15, b:90}. * Formula based on the following papers: * - http://nishitalab.org/user/UEI/publication/Sugita_IWAIT2015.pdf * - http://nishitalab.org/user/UEI/publication/Sugita_SIG2015.pdf */ function rybToRgb(ryb) { // Soon-to-be result var rgb = {r:0, g:0, b:0, a:ryb.a}; // Make a copy of the input to work on var tmpRyb = {r: ryb.r, y: ryb.y, b: ryb.b}; // Remove black component var i = Math.min(ryb.r, ryb.y, ryb.b); tmpRyb.r -= i; tmpRyb.y -= i; tmpRyb.b -= i; // Convert colors rgb.r = tmpRyb.r + tmpRyb.y - Math.min(tmpRyb.y, tmpRyb.b); rgb.g = tmpRyb.y + Math.min(tmpRyb.y, tmpRyb.b); rgb.b = 2*(tmpRyb.b - Math.min(tmpRyb.y, tmpRyb.b)); /* According to the RYB papers linked, the formula for green should be * "g = y + 2*min(y, b)" * But for whatever godforsaken reason that returns wrong values for colors where y < b * (eg: cyan). Got rid of the '2*' on a hunch and sure it WORKS without breaking anything * else, but WHY????? */ // Normalize var n = Math.max(rgb.r, rgb.g, rgb.b)/Math.max(tmpRyb.r, tmpRyb.y, tmpRyb.b); if (n > 0.000001) { // Should be zero, but floating point error could be an issue rgb.r /= n; rgb.g /= n; rgb.b /= n; } // Add white component, and round floating point errors i = Math.min(255 - ryb.r, 255 - ryb.y, 255 - ryb.b); rgb.r = Math.round(rgb.r + i); rgb.g = Math.round(rgb.g + i); rgb.b = Math.round(rgb.b + i); return rgb; } /** * Listen to mouse actions and draw. * Gets called whenever the mouse either moves within the canvas or is released from being * pressed down (eg. single clicks) */ var lastMove = 0; function draw_on_bitmap() { //If the mouse is pressed down and inside the canvas... if (is_mouse_down && event.offsetX > 0 && event.offsetX < canvas.width && event.offsetY > 0 && event.offsetY < canvas.height && (Date.now() - lastMove) > PAINT_TOOL_MOVEMENT_THROTTLE) { //Translate mouse position to bitmap position var x = Math.floor(width * event.offsetX/canvas.width); var y = Math.floor(height * event.offsetY/canvas.height); //If the mouse moves too fast, "skipping" pixels, fill the gap by drawing a line // between it and the last recorded position if (previousX > -1 && (Math.abs(previousX - x) > 1 || Math.abs(previousY - y) > 1 )) { lineDraw(previousX, previousY, x, y, paint_color, paint_opacity); } //Draw a pixel wherever we're at pixelDraw(x, y, paint_color, paint_opacity); //Record our current position as last recorded previousX = x; previousY = y; // Record the time of our last movement, for throttling lastMove = Date.now(); //Update the UI display_bitmap(); } } /** * Act on mouse no longer being pressed down. * Draws a single pixel if we just did a single click. Clears the last recorded position so * future clicks aren't treated as incredibly fast movements. */ function end_path () { if (previousX == -1) draw_on_bitmap(); is_mouse_down = false; previousX = -1; previousY = -1; } /** * Draws a line between two points, neither point included. * Sensitive both to mouse movement and the mouse button being released. */ function lineDraw(x1, y1, x2, y2, rgb, a) { //Difference in "steps" between both axes var sx = x2 - x1; var sy = y2 - y1; //Figure out how much to advance between steps, and how many steps to take (sx or sy, whichever is greater) var dx; var dy; var steps = 0; if (Math.abs(sx) > Math.abs(sy)) { steps = Math.abs(sx); dx = sx/Math.abs(sx);//Either 1 or -1 dy = sy/Math.abs(sx); } else if (sy != 0){ steps = Math.abs(sy); dx = sx/Math.abs(sy); dy = sy/Math.abs(sy);//Either 1 or -1 } //Move however many steps we decided on, starting from x1 and y1 and increasing both by dx and dy each step //Skip the first and last step though, draw_on_bitmap() already handles those for (var i = 1; i < steps; i++) { //Result might look like x:0.1 y:1, x:0.2 y:2.. Decimals are not possible, so round it pixelDraw(x1 + Math.round(i*dx), y1 + Math.round(i*dy), rgb, a); } } /** * Display the bitmap to the player * Draws the bitmap's contents on screen, scaled up for visibility */ function display_bitmap() { ctx.clearRect(0, 0, width*scaleX, height*scaleY); //Go through our pixel data and draw scaled up squares with the corresponding color for (var x = 0; x < width; x++) { for(var y = 0; y < height; y++) { //Convert to pixel index var pixel = (y * width + x); //Grab the pixel's color var color; if (polarized) color = hexToRgba(nanomap[pixel]); else color = hexToRgba(bitmap[pixel]); var alpha = color.a/255.0; ctx.globalAlpha = alpha; color.a = 255; ctx.fillStyle = rgbaToHex(color); //Draw a square, scaled up as needed ctx.fillRect(x*scaleX, y*scaleY, scaleX, scaleY); } } if (grid_enabled) { ctx.beginPath(); ctx.lineWidth = 1; ctx.setLineDash([6,6]); for (var x = 0; x < width; x++) { ctx.moveTo(x*scaleX+0.5, 0.5); ctx.lineTo(x*scaleX+0.5, height*scaleY+0.5); } for (var y = 0; y < height; y++) { ctx.moveTo(0.5, y*scaleY+0.5); ctx.lineTo(width*scaleX+0.5, y*scaleY+0.5); } ctx.lineDashOffset = 0; ctx.strokeStyle = "#333333"; ctx.stroke(); ctx.strokeStyle = "#cccccc"; ctx.lineDashOffset = 6; ctx.stroke(); } }