From 309c87119d587b8894dc4b88d2e134a3b14415a5 Mon Sep 17 00:00:00 2001 From: Preet Shihn Date: Fri, 7 Feb 2020 15:18:00 -0800 Subject: [PATCH] added color diff matcher --- package-lock.json | 54 ++++++++++++++++---------------- package.json | 9 +++--- src/color.ts | 80 +++++++++++++++++++++++++++++++++++++++++++++++ src/geometry.ts | 2 +- src/legra-core.ts | 27 +++++++++++++--- src/legra.ts | 10 ++++-- 6 files changed, 142 insertions(+), 40 deletions(-) create mode 100644 src/color.ts diff --git a/package-lock.json b/package-lock.json index adf3af0..8c60879 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "legra", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -25,15 +25,15 @@ } }, "@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.42.tgz", + "integrity": "sha512-K1DPVvnBCPxzD+G51/cxVIoc2X8uUVl1zpJeE6iKcgHMj4+tbat5Xu4TjV7v2QSDbIeAfLi2hIk+u2+s0MlpUQ==", "dev": true }, "@types/node": { - "version": "12.12.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.8.tgz", - "integrity": "sha512-XLla8N+iyfjvsa0KKV+BP/iGSoTmwxsu5Ci5sM33z9TjohF72DEz95iNvD6pPmemvbQgxAv/909G73gUn8QR7w==", + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.0.tgz", + "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==", "dev": true }, "acorn": { @@ -296,9 +296,9 @@ } }, "rollup": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.27.0.tgz", - "integrity": "sha512-yaMna4MJ8LLEHhHl1ilgHakylf0LKeQctDxhngZLQ+W57GnXa5vtH7XKaK8zlAhNEhlWiH5YFVFt+QCDPUmNkw==", + "version": "1.31.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.31.0.tgz", + "integrity": "sha512-9C6ovSyNeEwvuRuUUmsTpJcXac1AwSL1a3x+O5lpmQKZqi5mmrjauLeqIjvREC+yNRR8fPdzByojDng+af3nVw==", "dev": true, "requires": { "@types/estree": "*", @@ -307,16 +307,16 @@ } }, "rollup-plugin-terser": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.1.2.tgz", - "integrity": "sha512-sWKBCOS+vUkRtHtEiJPAf+WnBqk/C402fBD9AVHxSIXMqjsY7MnYWKYEUqGixtr0c8+1DjzUEPlNgOYQPVrS1g==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.2.0.tgz", + "integrity": "sha512-jQI+nYhtDBc9HFRBz8iGttQg7li9klmzR62RG2W2nN6hJ/FI2K2ItYQ7kJ7/zn+vs+BP1AEccmVRjRN989I+Nw==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "jest-worker": "^24.6.0", - "rollup-pluginutils": "^2.8.1", - "serialize-javascript": "^1.7.0", - "terser": "^4.1.0" + "@babel/code-frame": "^7.5.5", + "jest-worker": "^24.9.0", + "rollup-pluginutils": "^2.8.2", + "serialize-javascript": "^2.1.2", + "terser": "^4.6.2" } }, "rollup-pluginutils": { @@ -335,9 +335,9 @@ "dev": true }, "serialize-javascript": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", - "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", + "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", "dev": true }, "source-map": { @@ -372,9 +372,9 @@ } }, "terser": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.9.tgz", - "integrity": "sha512-NFGMpHjlzmyOtPL+fDw3G7+6Ueh/sz4mkaUYa4lJCxOPTNzd0Uj0aZJOmsDYoSQyfuVoWDMSWTPU3huyOm2zdA==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", + "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==", "dev": true, "requires": { "commander": "^2.20.0", @@ -419,9 +419,9 @@ } }, "typescript": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", - "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", + "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", "dev": true }, "wrappy": { diff --git a/package.json b/package.json index 29f6ff3..5e68562 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "legra", - "version": "0.2.0", + "version": "0.3.0", "description": "Create graphics using Lego like brick shapes.", "main": "lib/legra.umd.js", "module": "lib/legra.js", @@ -8,6 +8,7 @@ "types": "bin/legra.d.ts", "scripts": { "build": "rm -rf bin && tsc && rollup -c", + "lint": "tslint -p tsconfig.json", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { @@ -28,9 +29,9 @@ }, "homepage": "https://legrajs.com", "devDependencies": { - "rollup": "^1.27.0", - "rollup-plugin-terser": "^5.1.2", + "rollup": "^1.31.0", + "rollup-plugin-terser": "^5.2.0", "tslint": "^5.20.1", - "typescript": "^3.7.2" + "typescript": "^3.7.5" } } \ No newline at end of file diff --git a/src/color.ts b/src/color.ts new file mode 100644 index 0000000..f8ccbd4 --- /dev/null +++ b/src/color.ts @@ -0,0 +1,80 @@ +export interface Color { + r: number; + g: number; + b: number; +} + +export interface LabColor { + l: number; + a: number; + b: number; +} + +const labMap = new Map(); + +function toLab(c: Color): LabColor { + const rgb = `${c.r}, ${c.g}, ${c.b}`; + if (labMap.has(rgb)) { + return labMap.get(rgb)!; + } + let [r, g, b] = [c.r / 255, c.g / 255, c.b / 255]; + r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; + g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; + b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; + let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047; + let y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000; + let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883; + x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116; + y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + 16 / 116; + z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + 16 / 116; + const lab: LabColor = { + l: (116 * y) - 16, + a: 500 * (x - y), + b: 200 * (y - z) + }; + labMap.set(rgb, lab); + return lab; +} + +function colorDifference(a: Color, b: Color) { + const labA = toLab(a); + const labB = toLab(b); + return Math.sqrt( + Math.pow(labB.l - labA.l, 2) + + Math.pow(labB.a - labA.a, 2) + + Math.pow(labB.b - labA.b, 2) + ); +} + +// function colorDifference2(a: Color, b: Color) { +// const rbar = (a.r + b.r) / 2; +// return Math.sqrt( +// ((2 + (rbar / 256)) * Math.pow(b.r - a.r, 2)) + +// (4 * Math.pow(b.g - a.g, 2)) + +// ((2 + ((255 - rbar) / 256)) * Math.pow(b.b - a.b, 2)) +// ); +// } + +interface ColorDiffItem { + diff: number; + color: Color; +} + +export function closestColor(color: Color, palette: Color[]): Color { + if (palette.length) { + const diffItems = palette.map((p) => { + const diff = colorDifference(color, p); + return { + diff, + color: p + }; + }); + diffItems.sort((a, b) => { + return a.diff - b.diff; + }); + console.log(color, diffItems[0]); + return diffItems[0].color; + } else { + return color; + } +} \ No newline at end of file diff --git a/src/geometry.ts b/src/geometry.ts index a4ab535..b9b0c54 100644 --- a/src/geometry.ts +++ b/src/geometry.ts @@ -77,7 +77,7 @@ export function angle(o: Point, v1: Point, v2: Point): number { const dx1 = v1[0] - o[0]; const dy1 = v1[1] - o[1]; const dx2 = v2[0] - o[0]; - const dy2 = v2[1] - o[1] + const dy2 = v2[1] - o[1]; const cross = dx1 * dy2 - dy1 * dx2; const dot = dx1 * dx2 + dy1 * dy2; return Math.atan2(cross, dot); diff --git a/src/legra-core.ts b/src/legra-core.ts index d8d528e..a57f6b5 100644 --- a/src/legra-core.ts +++ b/src/legra-core.ts @@ -1,5 +1,6 @@ import { Point, Rectangle, EdgeEntry, ActiveEdgeEntry } from './geometry.js'; import { Bezier } from './bezier.js'; +import { Color, closestColor } from './color'; export interface ImageOrImageBitmap { width: number; @@ -9,12 +10,14 @@ export interface ImageOrImageBitmap { export interface BrickRenderOptions { color?: string; filled?: boolean; + palette?: Color[]; } export interface BrickRenderOptionsResolved extends BrickRenderOptions { brickSize: number; color: string; filled: boolean; + palette: Color[]; } const radiusCache = new Map(); @@ -475,15 +478,29 @@ export function drawImage(ctx: CanvasRenderingContext2D, style: BrickRenderOptio const refCtx = refCanvas.getContext('2d')!; refCtx.drawImage(image as any, src[0], src[1], srcSize[0], srcSize[1], 0, 0, dstSize[0], dstSize[1]); const imageData = refCtx.getImageData(0, 0, refW, refH); + const colorMap = new Map(); for (let j = 0; j < refH; j++) { for (let i = 0; i < refW; i++) { - const r = imageData.data[(j * refW * 4) + (i * 4)]; - const g = imageData.data[(j * refW * 4) + (i * 4) + 1]; - const b = imageData.data[(j * refW * 4) + (i * 4) + 2]; + let color: Color = { + r: imageData.data[(j * refW * 4) + (i * 4)], + g: imageData.data[(j * refW * 4) + (i * 4) + 1], + b: imageData.data[(j * refW * 4) + (i * 4) + 2] + }; + if (style.palette && style.palette.length) { + const ckey = `${color.r},${color.g},${color.b}`; + if (colorMap.has(ckey)) { + color = colorMap.get(ckey)!; + } else { + const matchingColor = closestColor(color, style.palette); + colorMap.set(ckey, matchingColor); + color = matchingColor; + } + } const pixelStyle: BrickRenderOptionsResolved = { brickSize, - color: `rgb(${r}, ${g}, ${b})`, - filled: false + color: `rgb(${color.r}, ${color.g}, ${color.b})`, + filled: false, + palette: [] }; drawBrick(dst[0] + i, dst[1] + j, ctx, pixelStyle); } diff --git a/src/legra.ts b/src/legra.ts index 004c3dc..1f9c40a 100644 --- a/src/legra.ts +++ b/src/legra.ts @@ -8,7 +8,8 @@ export default class Legra { private defaultOptions: BrickRenderOptionsResolved = { brickSize: 24, color: '#2196F3', - filled: false + filled: false, + palette: [] }; constructor(ctx: CanvasRenderingContext2D, brickSize = 24, options?: BrickRenderOptions) { @@ -21,6 +22,9 @@ export default class Legra { if (typeof options.filled === 'boolean') { this.defaultOptions.filled = options.filled; } + if (options.palette) { + this.defaultOptions.palette = options.palette; + } } } @@ -67,7 +71,7 @@ export default class Legra { quadraticCurve(x1, y1, cpx, cpy, x2, y2, this.ctx, this.opt(options)); } - drawImage(image: ImageOrImageBitmap, dst: Point, dstSize?: Point, src?: Point, srcSize?: Point) { - drawImage(this.ctx, this.opt(), image, dst, dstSize, src, srcSize); + drawImage(image: ImageOrImageBitmap, dst: Point, dstSize?: Point, src?: Point, srcSize?: Point, options?: BrickRenderOptions) { + drawImage(this.ctx, this.opt(options), image, dst, dstSize, src, srcSize); } } \ No newline at end of file