|
| 1 | +// Ref: https://github.com/malchata/parallelowow/blob/main/parallelowow.js |
| 2 | + |
| 3 | +if (typeof registerPaint !== "undefined") { |
| 4 | + registerPaint("parallelowow", class { |
| 5 | + static get inputProperties() { |
| 6 | + return [ |
| 7 | + "--parallelowow-tile-width", |
| 8 | + "--parallelowow-base-color", |
| 9 | + "--parallelowow-color-step", |
| 10 | + "--parallelowow-probability", |
| 11 | + "--parallelowow-stroke-weight", |
| 12 | + ]; |
| 13 | + } |
| 14 | + |
| 15 | + paint(ctx, geom, properties) { |
| 16 | + let tileWidth = Number(properties.get("--parallelowow-tile-width")) || 56; |
| 17 | + let baseColor = properties.get("--parallelowow-base-color").toString().trim() || "#c9f"; |
| 18 | + let colorStep = Number(properties.get("--parallelowow-color-step")) || -3; |
| 19 | + let probability = Number(properties.get("--parallelowow-probability")) || 0.33; |
| 20 | + let strokeWeight = Number(properties.get("--parallelowow-stroke-weight")) || 0.5; |
| 21 | + |
| 22 | + const radians = (Math.PI / 180) * 39.375; |
| 23 | + const tileHeight = tileWidth * 0.25; |
| 24 | + const yTiles = geom.height / tileHeight; |
| 25 | + const xTiles = geom.width / tileWidth; |
| 26 | + const outerRadius = geom.width > geom.height ? geom.width * 2 : geom.height * 2; |
| 27 | + |
| 28 | + let colors = [ |
| 29 | + baseColor, |
| 30 | + this.adjustBrightness(baseColor, -10), |
| 31 | + this.adjustBrightness(baseColor, -30) |
| 32 | + ]; |
| 33 | + |
| 34 | + if (strokeWeight > 0) { |
| 35 | + ctx.lineWidth = strokeWeight; |
| 36 | + ctx.strokeStyle = this.adjustBrightness(colors[0], 25); |
| 37 | + ctx.lineCap = "butt"; |
| 38 | + } |
| 39 | + |
| 40 | + for (let y = -1; y < yTiles; y++) { |
| 41 | + const yOffset = y * tileHeight; |
| 42 | + |
| 43 | + for (let x = -1; x < (xTiles + y); x++) { |
| 44 | + if (Math.random() > probability) { |
| 45 | + const xOffset = (x * tileWidth) - (y * tileHeight); |
| 46 | + |
| 47 | + // Helpers! |
| 48 | + const upperLeftX = xOffset; |
| 49 | + const upperLeftY = yOffset; |
| 50 | + const upperRightX = xOffset + tileWidth; |
| 51 | + const upperRightY = yOffset; |
| 52 | + const lowerRightX = xOffset + (tileWidth - tileHeight); |
| 53 | + const lowerRightY = yOffset + tileHeight; |
| 54 | + const lowerLeftX = xOffset - tileHeight; |
| 55 | + const lowerLeftY = lowerRightY; |
| 56 | + |
| 57 | + // 1. Draw shape on the right side of the parallelogram |
| 58 | + ctx.fillStyle = colors[1]; |
| 59 | + ctx.beginPath(); |
| 60 | + ctx.moveTo(upperRightX, upperRightY); |
| 61 | + ctx.lineTo((Math.cos(radians) * outerRadius), (Math.sin(radians) * outerRadius)); |
| 62 | + ctx.lineTo(lowerRightX, lowerRightY); |
| 63 | + ctx.lineTo(upperRightX, upperRightY); |
| 64 | + ctx.fill(); |
| 65 | + |
| 66 | + if (strokeWeight > 0) { |
| 67 | + ctx.stroke(); |
| 68 | + } |
| 69 | + |
| 70 | + // 2. Draw shape on the lower left side of the parallelogram |
| 71 | + ctx.fillStyle = colors[2]; |
| 72 | + ctx.beginPath(); |
| 73 | + ctx.moveTo(lowerRightX, lowerRightY); |
| 74 | + ctx.lineTo((Math.cos(radians) * outerRadius), (Math.sin(radians) * outerRadius)); |
| 75 | + ctx.lineTo(lowerLeftX, lowerLeftY); |
| 76 | + ctx.moveTo(lowerLeftX, lowerLeftY); |
| 77 | + ctx.fill(); |
| 78 | + |
| 79 | + if (strokeWeight > 0) { |
| 80 | + ctx.stroke(); |
| 81 | + } |
| 82 | + |
| 83 | + // 3. Draw parallelogram cap |
| 84 | + ctx.fillStyle = colors[0]; |
| 85 | + ctx.beginPath(); |
| 86 | + ctx.moveTo(upperLeftX, upperLeftY); |
| 87 | + ctx.lineTo(upperRightX, upperRightY); |
| 88 | + ctx.lineTo(lowerRightX, lowerRightY); |
| 89 | + ctx.lineTo(lowerLeftX, lowerLeftY); |
| 90 | + ctx.lineTo(upperLeftX, upperLeftY); |
| 91 | + ctx.fill(); |
| 92 | + |
| 93 | + if (strokeWeight > 0) { |
| 94 | + ctx.stroke(); |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + // 4. Slightly darken colors for next run. |
| 100 | + colors = colors.map(colorKey => this.adjustBrightness(colorKey, colorStep)); |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + isValidHexColor(hex) { |
| 105 | + return /^#?(?:[0-9a-f]{3}){1,2}$/i.test(hex); |
| 106 | + } |
| 107 | + |
| 108 | + hexToRgb(hex) { |
| 109 | + if (/^#/i.test(hex)) { |
| 110 | + hex = hex.replace("#", ""); |
| 111 | + } |
| 112 | + |
| 113 | + if (hex.length === 3) { |
| 114 | + const rHex = hex.substring(0, 1); |
| 115 | + const gHex = hex.substring(1, 2); |
| 116 | + const bHex = hex.substring(2, 3); |
| 117 | + |
| 118 | + hex = `${rHex}${rHex}${gHex}${gHex}${bHex}${bHex}`; |
| 119 | + } |
| 120 | + |
| 121 | + const rDec = parseInt(hex.substring(0, 2), 16); |
| 122 | + const gDec = parseInt(hex.substring(2, 4), 16); |
| 123 | + const bDec = parseInt(hex.substring(4, 6), 16); |
| 124 | + |
| 125 | + return `rgb(${rDec},${gDec},${bDec})`; |
| 126 | + } |
| 127 | + |
| 128 | + adjustBrightness(colorString, amt) { |
| 129 | + let rgbString = this.isValidHexColor(colorString) ? this.hexToRgb(colorString) : colorString; |
| 130 | + rgbString = rgbString.replace(/rgba?\(/g, "").replace(/\)/g, "").replace(/\s/g, ""); |
| 131 | + |
| 132 | + const rgbParts = rgbString.split(",").map((rgbPart, index) => { |
| 133 | + if (index > 2) { |
| 134 | + return; |
| 135 | + } |
| 136 | + |
| 137 | + rgbPart = parseInt(rgbPart) + amt; |
| 138 | + |
| 139 | + if (rgbPart < 0) { |
| 140 | + rgbPart = 0; |
| 141 | + } else if (rgbPart > 255) { |
| 142 | + rgbPart = 255; |
| 143 | + } |
| 144 | + |
| 145 | + return rgbPart; |
| 146 | + }); |
| 147 | + |
| 148 | + return rgbString.indexOf("rgba") !== -1 ? `rgba(${rgbParts.join(",")})` : `rgb(${rgbParts.join(",")})`; |
| 149 | + } |
| 150 | + }); |
| 151 | +} |
0 commit comments